1
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import {app, BrowserWindow, ipcMain, Menu, screen} from 'electron';
|
||||
import {app, BrowserWindow, ipcMain, Menu, screen, dialog} from 'electron';
|
||||
import { existsSync, createWriteStream, promises as fs, statSync } from 'fs';
|
||||
import {join, dirname} from 'path';
|
||||
import {spawn, ChildProcessWithoutNullStreams} from 'child_process';
|
||||
@@ -178,7 +178,7 @@ WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " &
|
||||
});
|
||||
|
||||
setTimeout(() => app.quit(), 1000);
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
console.error('[UPDATE] 更新失败:', error);
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,7 @@ ipcMain.handle('download-update', async (event, downloadUrl: string) => {
|
||||
isDownloading = false;
|
||||
|
||||
return { success: true, filePath: updateFilePath };
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
isDownloading = false;
|
||||
downloadedFilePath = null;
|
||||
|
||||
@@ -304,7 +304,7 @@ WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " &
|
||||
}, 500);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
return { success: false, error: error instanceof Error ? error.message : '重启失败' };
|
||||
}
|
||||
});
|
||||
@@ -321,6 +321,37 @@ ipcMain.handle('get-update-status', () => {
|
||||
return { downloadedFilePath, isDownloading, downloadProgress, isPackaged: app.isPackaged, isDev };
|
||||
});
|
||||
|
||||
// 添加文件保存对话框处理器
|
||||
ipcMain.handle('show-save-dialog', async (event, options) => {
|
||||
if (!mainWindow) {
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await dialog.showSaveDialog(mainWindow, options);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('文件保存对话框错误:', error);
|
||||
return { canceled: true, error: error instanceof Error ? error.message : '对话框打开失败' };
|
||||
}
|
||||
});
|
||||
|
||||
// 添加文件夹选择对话框处理器
|
||||
ipcMain.handle('show-open-dialog', async (event, options) => {
|
||||
if (!mainWindow) {
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await dialog.showOpenDialog(mainWindow, options);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('文件夹选择对话框错误:', error);
|
||||
return { canceled: true, error: error instanceof Error ? error.message : '对话框打开失败' };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
async function downloadFile(url: string, filePath: string, onProgress: (progress: any) => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
|
||||
@@ -9,6 +9,11 @@ const electronAPI = {
|
||||
cancelDownload: () => ipcRenderer.invoke('cancel-download'),
|
||||
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
|
||||
|
||||
// 添加文件保存对话框 API
|
||||
showSaveDialog: (options: any) => ipcRenderer.invoke('show-save-dialog', options),
|
||||
// 添加文件夹选择对话框 API
|
||||
showOpenDialog: (options: any) => ipcRenderer.invoke('show-open-dialog', options),
|
||||
|
||||
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
||||
},
|
||||
|
||||
@@ -426,9 +426,13 @@ onUnmounted(() => {
|
||||
SSEManager.disconnect()
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
|
||||
<div>
|
||||
<div id="app-root" class="root">
|
||||
<div class="loading-container" id="loading">
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
Reference in New Issue
Block a user