This commit is contained in:
2025-09-27 14:05:45 +08:00
parent 89f600fa11
commit fe3e24b945
23 changed files with 474 additions and 261 deletions

View File

@@ -426,9 +426,13 @@ onUnmounted(() => {
SSEManager.disconnect()
})
</script>
<template>
<div>
<div id="app-root" class="root">
<div class="loading-container" id="loading">

View File

@@ -23,9 +23,6 @@ export const amazonApi = {
deleteProduct(productId: string) {
return http.post('/api/amazon/products/delete', { id: productId });
},
exportToExcel(products: unknown[], options: Record<string, unknown> = {}) {
return http.post('/api/amazon/export', { products, ...options });
},
getProductStats() {
return http.get('/api/amazon/stats');
},

View File

@@ -30,9 +30,4 @@ export const rakutenApi = {
getLatestProducts() {
return http.get('/api/rakuten/products/latest').then(res => unwrap<{ products: any[] }>(res));
},
exportAndSave(exportData: unknown) {
return http
.post('/api/rakuten/export-and-save', exportData)
.then(res => unwrap<{ filePath: string; fileName?: string; recordCount?: number; hasImages?: boolean }>(res));
},
};

View File

@@ -59,7 +59,7 @@ export const zebraApi = {
'/api/banma/shops', params as unknown as Record<string, unknown>
);
},
getOrders(params: { accountId?: number; startDate?: string; endDate?: string; page?: number; pageSize?: number; shopIds?: string }) {
getOrders(params: { accountId?: number; startDate?: string; endDate?: string; page?: number; pageSize?: number; shopIds?: string; batchId: string }) {
return http.get<ZebraOrdersResp>('/api/banma/orders', params as unknown as Record<string, unknown>);
},
@@ -70,9 +70,6 @@ export const zebraApi = {
getLatestOrders() {
return http.get<ZebraOrdersResp>('/api/banma/orders/latest');
},
exportAndSaveOrders(exportData: unknown) {
return http.post<{ filePath: string }>('/api/banma/export-and-save', exportData);
},
getOrderStats() {
return http.get('/api/banma/orders/stats');
},

View File

@@ -15,7 +15,6 @@ const genmaiLoading = ref(false) // Genmai Spirit加载状态
// 分页配置
const currentPage = ref(1)
const pageSize = ref(15)
const totalPages = computed(() => Math.max(1, Math.ceil((localProductData.value.length || 0) / pageSize.value)))
const amazonUpload = ref<HTMLInputElement | null>(null)
const dragActive = ref(false)
@@ -26,14 +25,6 @@ const paginatedData = computed(() => {
return localProductData.value.slice(start, end)
})
// 左侧步骤栏进度
const activeStep = computed(() => {
// 0 导入/输入 -> 1 采集 -> 2 查看校验 -> 3 导出
if (loading.value && progressPercentage.value < 100) return 1
if (!localProductData.value.length) return 0
if (localProductData.value.length && progressPercentage.value < 100) return 1
return 2
})
// 左侧:网站地区 & 待采集队列
const region = ref('JP')
@@ -53,8 +44,7 @@ function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'i
async function processExcelFile(file: File) {
try {
loading.value = true
tableLoading.value = true
localProductData.value = []
// 不再立即清空表格数据,保留之前的数据
progressPercentage.value = 0
progressVisible.value = false
@@ -70,7 +60,6 @@ async function processExcelFile(file: File) {
showMessage(error.message || '处理文件失败', 'error')
} finally {
loading.value = false
tableLoading.value = false
}
}
@@ -99,7 +88,7 @@ async function batchGetProductInfo(asinList: string[]) {
try {
currentAsin.value = '正在处理...'
progressPercentage.value = 0
localProductData.value = []
localProductData.value = [] // 开始采集时才清空表格数据
const batchId = `BATCH_${Date.now()}`
const batchSize = 2 // 每批处理2个ASIN
@@ -147,12 +136,7 @@ async function batchGetProductInfo(asinList: string[]) {
progressPercentage.value = 100
currentAsin.value = '处理完成'
// 结果提示
if (failedCount > 0) {
showMessage(`采集完成!共 ${asinList.length} 个ASIN成功 ${asinList.length - failedCount} 个,失败 ${failedCount}`, 'warning')
} else {
showMessage(`采集完成!成功获取 ${asinList.length} 个产品信息`, 'success')
}
} catch (error: any) {
showMessage(error.message || '批量获取产品信息失败', 'error')
@@ -180,33 +164,53 @@ async function startQueuedFetch() {
}
// 导出Excel数据
const exportLoading = ref(false)
const exportProgress = ref(0)
const showExportProgress = ref(false)
async function exportToExcel() {
if (!localProductData.value.length) {
showMessage('没有数据可供导出', 'warning')
return
}
try {
loading.value = true
showMessage('正在生成Excel文件请稍候...', 'info')
exportLoading.value = true
showExportProgress.value = true
exportProgress.value = 0
// 数据格式化 - 只保留核心字段
const exportData = localProductData.value.map(product => ({
asin: product.asin || '',
seller_shipper: getSellerShipperText(product),
price: product.price || '无货'
}))
const progressInterval = setInterval(() => {
if (exportProgress.value < 90) exportProgress.value += Math.random() * 20
}, 100)
await amazonApi.exportToExcel(exportData, {
filename: `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xlsx`
})
// 生成Excel HTML格式
let html = `<table>
<tr><th>ASIN</th><th>卖家/配送方</th><th>当前售价</th></tr>`
showMessage('Excel文件导出成功', 'success')
} catch (error: any) {
showMessage(error.message || '导出Excel失败', 'error')
} finally {
loading.value = false
}
localProductData.value.forEach(product => {
html += `<tr>
<td>${product.asin || ''}</td>
<td>${getSellerShipperText(product)}</td>
<td>${product.price || '无货'}</td>
</tr>`
})
html += '</table>'
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xls`
link.click()
URL.revokeObjectURL(link.href)
clearInterval(progressInterval)
exportProgress.value = 100
showMessage('Excel文件导出成功', 'success')
setTimeout(() => {
showExportProgress.value = false
exportLoading.value = false
exportProgress.value = 0
}, 2000)
}
// 获取卖家/配送方信息 - 数据处理辅助函数
@@ -275,6 +279,7 @@ function downloadAmazonTemplate() {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
// 组件挂载时获取最新数据
onMounted(async () => {
try {
@@ -345,7 +350,14 @@ onMounted(async () => {
<div class="step-card">
<div class="step-header"><div class="title">导出数据</div></div>
<div class="action-buttons column">
<el-button size="small" class="w100 btn-blue" :disabled="!localProductData.length || loading" @click="exportToExcel">导出Excel</el-button>
<el-button size="small" class="w100 btn-blue" :disabled="!localProductData.length || loading || exportLoading" :loading="exportLoading" @click="exportToExcel">{{ exportLoading ? '导出中...' : '导出Excel' }}</el-button>
<!-- 导出进度条 -->
<div v-if="showExportProgress" class="export-progress">
<div class="export-progress-bar">
<div class="export-progress-fill" :style="{ width: exportProgress + '%' }"></div>
</div>
<div class="export-progress-text">{{ Math.round(exportProgress) }}%</div>
</div>
<el-button size="small" class="w100 btn-blue" :loading="genmaiLoading" @click="openGenmaiSpirit">{{ genmaiLoading ? '启动中...' : '跟卖精灵' }}</el-button>
</div>
</div>
@@ -498,6 +510,10 @@ onMounted(async () => {
.progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
.progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
.export-progress { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px; }
.export-progress-bar { flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden; }
.export-progress-fill { height: 100%; background: #1677FF; border-radius: 2px; transition: width 0.3s ease; }
.export-progress-text { font-size: 11px; color: #1677FF; font-weight: 500; min-width: 32px; text-align: right; }
.table-container { display: flex; flex-direction: column; flex: 1; min-height: 400px; overflow: hidden; }
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column; }
.table-wrapper { flex: 1; overflow: auto; }
@@ -528,6 +544,7 @@ onMounted(async () => {
.empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.6; }
.empty-text { font-size: 14px; color: #909399; }
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; }
</style>
<script lang="ts">

View File

@@ -2,7 +2,7 @@
<div>
<div class="version-info" @click="autoCheck">v{{ version || '-' }}</div>
<el-dialog v-model="show" width="522px" :close-on-click-modal="false" align-center class="update-dialog" title="软件更新">
<el-dialog v-model="show" width="522px" :close-on-click-modal="false" align-center class="update-dialog" :title="stage === 'downloading' ? `正在更新 ${appName}` : '软件更新'">
<div v-if="stage === 'check'" class="update-content">
<div class="update-layout">
<div class="left-pane">
@@ -39,30 +39,27 @@
</div>
<div v-else-if="stage === 'downloading'" class="update-content">
<div class="update-header text-center">
<img src="/icon/icon.png" class="app-icon" alt="App Icon" />
<h3>正在更新 {{ appName }}</h3>
<p>正在下载更新文件...</p>
</div>
<div class="download-progress">
<div class="progress-info">
<span>{{ prog.current }} / {{ prog.total }}</span>
<div class="download-main">
<div class="download-icon">
<img src="/icon/icon.png" class="app-icon" alt="App Icon" />
</div>
<el-progress
:percentage="prog.percentage"
:show-text="false"
:stroke-width="6"
color="#409EFF" />
<div class="progress-details">
<span>{{ prog.percentage }}%</span>
<span v-if="prog.speed">{{ prog.speed }}</span>
<div class="download-content">
<div class="download-info">
<p>正在下载更新</p>
</div>
<div class="download-progress">
<el-progress
:percentage="prog.percentage"
:show-text="false"
:stroke-width="6"
color="#409EFF" />
<div class="progress-details">
<span style="font-weight: 500">{{ prog.current }} / {{ prog.total }}</span>
<el-button size="small" @click="cancelDownload">取消</el-button>
</div>
</div>
</div>
</div>
<div class="update-buttons">
<el-button @click="cancelDownload">取消下载</el-button>
</div>
</div>
<div v-else-if="stage === 'completed'" class="update-content">
@@ -107,7 +104,7 @@ const show = computed({
type Stage = 'check' | 'downloading' | 'completed'
const stage = ref<Stage>('check')
const appName = ref('ERP客户端')
const appName = ref('我了个电商')
const version = ref('2.0.0')
const prog = ref({ percentage: 0, current: '0 MB', total: '0 MB', speed: '' as string | undefined })
const info = ref({
@@ -149,14 +146,12 @@ async function start() {
ElMessage({ message: '下载链接不可用', type: 'error' })
return
}
if (!window.electronAPI) {
ElMessage({ message: '更新功能不可用', type: 'error' })
return
}
stage.value = 'downloading'
prog.value = { percentage: 0, current: '0 MB', total: '0 MB', speed: '' }
window.electronAPI.onDownloadProgress((progress) => {
prog.value = {
percentage: progress.percentage || 0,
@@ -210,12 +205,6 @@ async function installUpdate() {
type: 'warning'
}
)
if (!window.electronAPI) {
ElMessage({ message: '更新功能不可用', type: 'error' })
return
}
const response = await window.electronAPI.installUpdate()
if (response.success) {
@@ -374,8 +363,53 @@ onUnmounted(() => {
border-radius: 8px;
}
.download-header {
text-align: center;
margin-bottom: 20px;
}
.download-header h3 {
font-size: 14px;
font-weight: 500;
margin: 0;
color: #1f2937;
}
.download-main {
display: grid;
grid-template-columns: 80px 1fr;
align-items: start;
}
.download-icon {
display: flex;
justify-content: center;
}
.download-icon .app-icon {
width: 64px;
height: 64px;
border-radius: 12px;
}
.download-content {
min-width: 0;
}
.download-info {
margin-bottom: 12px;
}
.download-info p {
font-size: 14px;
font-weight: 600;
color: #6b7280;
margin: 0;
}
.download-progress {
margin: 24px 0;
margin: 0;
}
.progress-info {
@@ -388,11 +422,15 @@ onUnmounted(() => {
}
.progress-details {
margin-top: 8px;
font-size: 12px;
color: #909399;
margin-top: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.progress-details span {
font-size: 12px;
color: #909399;
}
:deep(.el-progress-bar__outer) {

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import {ref, computed, onMounted} from 'vue'
import { ElMessage } from 'element-plus'
import {rakutenApi} from '../../api/rakuten'
import { batchConvertImages } from '../../utils/imageProxy'
// UI 与加载状态
const loading = ref(false)
@@ -139,7 +141,7 @@ async function searchProductInternal(product: any) {
function beforeUpload(file: File) {
const ok = /\.xlsx?$/.test(file.name)
if (!ok) alert('仅支持 .xlsx/.xls 文件')
if (!ok) ElMessage({ message: '仅支持 .xlsx/.xls 文件', type: 'warning' })
return ok
}
@@ -288,36 +290,104 @@ function nextTickSafe() {
return Promise.resolve()
}
function showMessage(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') {
ElMessage({ message, type })
}
async function exportToExcel() {
if (!allProducts.value.length) {
showMessage('没有数据可供导出', 'warning')
return
}
exportLoading.value = true
try {
if (allProducts.value.length === 0) return alert('没有数据可导出')
exportLoading.value = true
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
const fileName = `乐天商品数据_${timestamp}.xlsx`
const payload = {
products: allProducts.value,
title: '乐天商品数据导出',
fileName,
timestamp: new Date().toLocaleString('zh-CN'),
// 传给后端的可选提示参数
useMultiThread: true,
chunkSize: 300,
skipImages: allProducts.value.length > 200,
const ExcelJS = (await import('exceljs')).default
const workbook = new ExcelJS.Workbook()
const worksheet = workbook.addWorksheet('乐天商品数据')
worksheet.columns = [
{ header: '店铺名', key: 'shopName', width: 15 },
{ header: '商品图片', key: 'image', width: 15 },
{ header: '商品链接', key: 'productUrl', width: 30 },
{ header: '商品标题', key: 'title', width: 30 },
{ header: '价格', key: 'price', width: 10 },
{ header: '排名', key: 'ranking', width: 8 },
{ header: '1688识图链接', key: 'mapLink', width: 30 },
{ header: '1688运费', key: 'freight', width: 10 },
{ header: '1688中位价', key: 'median', width: 10 },
{ header: '1688最低价', key: 'minPrice', width: 10 },
{ header: '1688中间价', key: 'midPrice', width: 10 },
{ header: '1688最高价', key: 'maxPrice', width: 10 }
]
const imageUrls = allProducts.value.map(product => product.imgUrl || '')
const images = await batchConvertImages(imageUrls, 80)
for (let i = 0; i < allProducts.value.length; i++) {
const product = allProducts.value[i]
const base64Image = images[i]
const row = worksheet.addRow({
shopName: product.originalShopName || '',
image: base64Image ? '图片' : '无图片',
productUrl: product.productUrl || '',
title: product.productTitle || '',
price: product.price || '',
ranking: product.ranking || '',
mapLink: product.mapRecognitionLink || '',
freight: product.freight || '',
median: product.median || '',
minPrice: product.skuPrices?.[0] || '',
midPrice: product.skuPrices?.[Math.floor((product.skuPrices?.length || 0) / 2)] || '',
maxPrice: product.skuPrices?.[product.skuPrices?.length - 1] || ''
})
if (base64Image) {
const base64Data = base64Image.split(',')[1]
if (base64Data) {
const imageId = workbook.addImage({
base64: base64Data,
extension: 'jpeg',
})
worksheet.addImage(imageId, {
tl: { col: 1, row: row.number - 1 },
ext: { width: 60, height: 60 }
})
row.height = 50
}
}
}
const resp = await rakutenApi.exportAndSave(payload)
alert(`Excel文件已保存到: ${resp.filePath}`)
} catch (e: any) {
alert(e?.message || '导出失败')
const buffer = await workbook.xlsx.writeBuffer()
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `乐天商品数据_${new Date().toISOString().slice(0, 10)}.xlsx`
link.click()
URL.revokeObjectURL(link.href)
showMessage('Excel文件导出成功', 'success')
} catch (error) {
showMessage('导出失败', 'error')
} finally {
exportLoading.value = false
}
}
onMounted(loadLatest)
</script>
<template>
<div class="rakuten-root">
<div class="main-container">
<div class="body-layout">
<!-- 左侧步骤栏 -->
@@ -390,9 +460,10 @@ onMounted(loadLatest)
<div class="step-header">
<div class="title">导出数据</div>
</div>
<div class="mini-hint">点击下方按钮导出 Excel</div>
<el-button size="small" class="w100 btn-blue" :disabled="!allProducts.length || loading" @click="exportToExcel">导出数据</el-button>
<div class="desc">点击下方按钮导出所有商品数据到 Excel 文件</div>
<el-button size="small" class="w100 btn-blue" :disabled="!allProducts.length || loading || exportLoading" :loading="exportLoading" @click="exportToExcel">{{ exportLoading ? '导出中...' : '导出数据' }}</el-button>
<!-- 导出进度条 -->
</div>
</div>
@@ -590,6 +661,10 @@ onMounted(loadLatest)
color: #606266;
padding-left: 2px;
}
.export-progress { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px; }
.export-progress-bar { flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden; }
.export-progress-fill { height: 100%; background: #1677FF; border-radius: 2px; transition: width 0.3s ease; }
.export-progress-text { font-size: 11px; color: #1677FF; font-weight: 500; min-width: 32px; text-align: right; }
.table-container {
display: flex;

View File

@@ -3,6 +3,7 @@ import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { zebraApi, type ZebraOrder, type BanmaAccount } from '../../api/zebra'
import AccountManager from '../common/AccountManager.vue'
import { batchConvertImages } from '../../utils/imageProxy'
type Shop = { id: string; shopName: string }
@@ -21,6 +22,7 @@ const showProgress = ref(false)
const allOrderData = ref<ZebraOrder[]>([])
const currentPage = ref(1)
const pageSize = ref(15)
const currentBatchId = ref('')
// 批量获取状态
const fetchCurrentPage = ref(1)
@@ -90,6 +92,7 @@ async function fetchData() {
allOrderData.value = []
fetchCurrentPage.value = 1
fetchTotalItems.value = 0
currentBatchId.value = `ZEBRA_${Date.now()}`
const [startDate = '', endDate = ''] = dateRange.value || []
await fetchPageData(startDate, endDate)
@@ -105,7 +108,8 @@ async function fetchPageData(startDate: string, endDate: string) {
endDate,
page: fetchCurrentPage.value,
pageSize: 50,
shopIds: selectedShops.value.join(',')
shopIds: selectedShops.value.join(','),
batchId: currentBatchId.value
})
const orders = data.orders || []
@@ -143,14 +147,103 @@ function stopFetch() {
// 进度条保留显示,不自动隐藏
}
function showMessage(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') {
ElMessage({ message, type })
}
async function exportToExcel() {
if (!allOrderData.value.length) return
if (!allOrderData.value.length) {
showMessage('没有数据可供导出', 'warning')
return
}
exportLoading.value = true
try {
const result = await zebraApi.exportAndSaveOrders({ orders: allOrderData.value })
ElMessage({ message: `Excel文件已保存到: ${result.filePath}` as any, type: 'success' })
} catch (e: any) {
ElMessage({ message: e?.message || '导出Excel失败', type: 'error' })
const ExcelJS = (await import('exceljs')).default
const workbook = new ExcelJS.Workbook()
const worksheet = workbook.addWorksheet('斑马订单数据')
worksheet.columns = [
{ header: '下单时间', key: 'orderedAt', width: 15 },
{ header: '商品图片', key: 'image', width: 15 },
{ header: '商品名称', key: 'productTitle', width: 25 },
{ header: '乐天订单号', key: 'shopOrderNumber', width: 20 },
{ header: '下单距今', key: 'timeSinceOrder', width: 12 },
{ header: '订单金额/日元', key: 'priceJpy', width: 15 },
{ header: '数量', key: 'productQuantity', width: 8 },
{ header: '税费/日元', key: 'shippingFeeJpy', width: 12 },
{ header: '回款抽点rmb', key: 'serviceFee', width: 15 },
{ header: '商品番号', key: 'productNumber', width: 15 },
{ header: '1688订单号', key: 'poNumber', width: 15 },
{ header: '采购金额/rmb', key: 'shippingFeeCny', width: 15 },
{ header: '国际运费/rmb', key: 'internationalShippingFee', width: 15 },
{ header: '国内物流', key: 'poLogisticsCompany', width: 12 },
{ header: '国内单号', key: 'poTrackingNumber', width: 15 },
{ header: '日本单号', key: 'internationalTrackingNumber', width: 15 },
{ header: '地址状态', key: 'trackInfo', width: 12 }
]
const imageUrls = allOrderData.value.map(order => order.productImage || '')
const imageBase64s = await batchConvertImages(imageUrls, 80)
for (let i = 0; i < allOrderData.value.length; i++) {
const order = allOrderData.value[i]
const base64Image = imageBase64s[i]
const row = worksheet.addRow({
orderedAt: order.orderedAt || '',
image: base64Image ? '图片' : '无图片',
productTitle: order.productTitle || '',
shopOrderNumber: order.shopOrderNumber || '',
timeSinceOrder: order.timeSinceOrder || '',
priceJpy: formatJpy(order.priceJpy),
productQuantity: order.productQuantity || 0,
shippingFeeJpy: formatJpy(order.shippingFeeJpy),
serviceFee: order.serviceFee || '',
productNumber: order.productNumber || '',
poNumber: order.poNumber || '',
shippingFeeCny: formatCny(order.shippingFeeCny),
internationalShippingFee: order.internationalShippingFee || '',
poLogisticsCompany: order.poLogisticsCompany || '',
poTrackingNumber: order.poTrackingNumber || '',
internationalTrackingNumber: order.internationalTrackingNumber || '',
trackInfo: order.trackInfo || ''
})
if (base64Image) {
const base64Data = base64Image.split(',')[1]
if (base64Data) {
const imageId = workbook.addImage({
base64: base64Data,
extension: 'jpeg',
})
worksheet.addImage(imageId, {
tl: { col: 1, row: row.number - 1 },
ext: { width: 60, height: 60 }
})
row.height = 50
}
}
}
const buffer = await workbook.xlsx.writeBuffer()
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `斑马订单数据_${new Date().toISOString().slice(0, 10)}.xlsx`
link.click()
URL.revokeObjectURL(link.href)
showMessage('Excel文件导出成功', 'success')
} catch (error) {
showMessage('导出失败', 'error')
} finally {
exportLoading.value = false
}
@@ -160,7 +253,8 @@ onMounted(async () => {
await loadAccounts()
try {
const latest = await zebraApi.getLatestOrders()
allOrderData.value = latest?.orders || []
const data = (latest as any)?.data || latest
allOrderData.value = data?.orders || []
} catch {}
})
@@ -298,9 +392,10 @@ async function removeCurrentAccount() {
<div class="step-index">4</div>
<div class="step-body">
<div class="step-title">导出数据</div>
<div class="tip">点击下方按钮导出数据 Excel</div>
<div class="tip">点击下方按钮导出所有订单数据 Excel 文件</div>
<div class="btn-col">
<el-button size="small" type="success" :disabled="exportLoading || !allOrderData.length" @click="exportToExcel" class="w100">导出数据</el-button>
<el-button size="small" type="success" :disabled="exportLoading || !allOrderData.length" :loading="exportLoading" @click="exportToExcel" class="w100">{{ exportLoading ? '导出中...' : '导出数据' }}</el-button>
<!-- 导出进度条 -->
</div>
</div>
</section>
@@ -518,6 +613,10 @@ export default {
.progress-bar { flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden; }
.progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
.progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
.export-progress { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px; }
.export-progress-bar { flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden; }
.export-progress-fill { height: 100%; background: #67c23a; border-radius: 2px; transition: width 0.3s ease; }
.export-progress-text { font-size: 11px; color: #67c23a; font-weight: 500; min-width: 32px; text-align: right; }
</style>

View File

@@ -1,8 +1,12 @@
import { createApp } from 'vue'
import 'element-plus/dist/index.css'
import './style.css';
import 'element-plus/dist/index.css'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.use(ElementPlus, {
locale: zhCn,
})
app.mount('#app')

View File

@@ -7,6 +7,8 @@ export default interface ElectronApi {
getDownloadProgress: () => Promise<{ percentage: number; current: string; total: string; speed: string; isDownloading: boolean }>
installUpdate: () => Promise<{ success: boolean; error?: string }>
cancelDownload: () => Promise<{ success: boolean }>
showSaveDialog: (options: { title?: string; defaultPath?: string; filters?: { name: string; extensions: string[] }[] }) => Promise<{ canceled: boolean; filePath?: string; error?: string }>
showOpenDialog: (options: { title?: string; properties?: string[]; filters?: { name: string; extensions: string[] }[] }) => Promise<{ canceled: boolean; filePaths?: string[]; error?: string }>
onDownloadProgress: (callback: (progress: any) => void) => void
removeDownloadProgressListener: () => void
getUpdateStatus: () => Promise<{ downloadedFilePath: string | null; isDownloading: boolean; downloadProgress: any; isPackaged: boolean }>