This commit is contained in:
2025-09-25 16:05:29 +08:00
parent bb997857fd
commit 5e876b0f1d
17 changed files with 1001 additions and 632 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -17,9 +17,9 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.1",
"chalk": "^4.1.2",
"chalk": "^4.1.2",
"chokidar": "^3.5.3",
"electron": "^32.1.2",
"electron": "^38.1.2",
"electron-builder": "^25.1.6",
"typescript": "^5.2.2",
"vite": "^4.5.0"

View File

@@ -43,6 +43,10 @@ export const deviceApi = {
heartbeat(payload: { username: string; deviceId: string; version?: string }) {
return http.postVoid(`${base}/heartbeat`, payload)
},
offline(payload: { deviceId: string }) {
return http.postVoid(`${base}/offline`, payload)
},
}

View File

@@ -2,10 +2,13 @@
export type HttpMethod = 'GET' | 'POST';
const BASE_CLIENT = 'http://localhost:8081'; // erp_client_sb
const BASE_RUOYI = 'http://localhost:8080'; // ruoyi-admin
const BASE_RUOYI = 'http://localhost:8080';
function resolveBase(path: string): string {
if (path.startsWith('/tool/banma')) return BASE_RUOYI;
// 走 ruoyi-admin 的路径:鉴权与版本、平台工具路由
if (path.startsWith('/system/')) return BASE_RUOYI; // 版本控制器 VersionController
if (path.startsWith('/tool/banma')) return BASE_RUOYI; // 既有规则保留
// 其他默认走客户端服务
return BASE_CLIENT;
}

View File

@@ -31,6 +31,7 @@ export interface BanmaAccount {
id?: number;
name?: string;
username?: string;
password?: string;
token?: string;
tokenExpireAt?: string | number;
isDefault?: number;
@@ -53,12 +54,12 @@ export const zebraApi = {
},
// 业务采集(仍走客户端微服务 8081
getShops() {
getShops(params?: { accountId?: number }) {
return http.get<{ data?: { list?: Array<{ id: string; shopName: string }> } }>(
'/api/banma/shops'
'/api/banma/shops', params as unknown as Record<string, unknown>
);
},
getOrders(params: { startDate?: string; endDate?: string; page?: number; pageSize?: number; shopIds?: string }) {
getOrders(params: { accountId?: number; startDate?: string; endDate?: string; page?: number; pageSize?: number; shopIds?: string }) {
return http.get<ZebraOrdersResp>('/api/banma/orders', params as unknown as Record<string, unknown>);
},

View File

@@ -63,21 +63,24 @@ function showRegister() {
<template>
<el-dialog
title="用户登录"
title=""
v-model="visible"
:close-on-click-modal="false"
width="400px"
center>
<div style="text-align: center; padding: 20px 0;">
<div style="margin-bottom: 30px; color: #666;">
<el-icon size="48" color="#409EFF"><User /></el-icon>
<p style="margin-top: 15px; font-size: 16px;">请登录以使用系统功能</p>
width="360px"
center
class="auth-dialog">
<div style="padding: 20px 0;">
<div style="text-align: center; margin-bottom: 16px; color: #666;">
<img src="/icon/image.png" alt="logo" class="auth-logo" />
</div>
<div class="auth-title-wrap">
<h3 class="auth-title">账号登录</h3>
<p class="auth-subtitle">登录账户以获取完整服务体验</p>
</div>
<el-input
v-model="authForm.username"
placeholder="请输入用户名"
prefix-icon="User"
size="large"
style="margin-bottom: 15px;"
:disabled="authLoading"
@@ -101,16 +104,9 @@ function showRegister() {
:loading="authLoading"
:disabled="!authForm.username || !authForm.password || authLoading"
@click="handleAuth"
style="width: 120px; margin-right: 10px;">
style="width: 100%;">
登录
</el-button>
<el-button
size="large"
:disabled="authLoading"
@click="cancelAuth"
style="width: 120px;">
取消
</el-button>
</div>
<div style="margin-top: 20px; text-align: center;">
@@ -120,4 +116,39 @@ function showRegister() {
</div>
</div>
</el-dialog>
</template>
</template>
<style scoped>
.auth-logo {
width: 160px;
height: auto;
}
.auth-dialog {
--el-color-primary: #1677FF;
}
.auth-dialog :deep(.el-button--primary) {
background-color: #1677FF;
border-color: #1677FF;
}
.auth-title-wrap {
margin-bottom: 12px;
}
.auth-title {
margin: 0;
font-size: 18px;
font-weight: 700;
color: #1f1f1f;
text-align: left;
}
.auth-subtitle {
margin: 6px 0 0;
font-size: 12px;
color: #8c8c8c;
text-align: left;
}
</style>

View File

@@ -85,21 +85,24 @@ function backToLogin() {
<template>
<el-dialog
title="账号注册"
title=""
v-model="visible"
:close-on-click-modal="false"
width="450px"
center>
<div style="text-align: center; padding: 20px 0;">
<div style="margin-bottom: 20px; color: #666;">
<el-icon size="48" color="#67C23A"><User /></el-icon>
<p style="margin-top: 15px; font-size: 16px;">创建新账号</p>
width="380px"
center
class="auth-dialog">
<div style="padding: 20px 0;">
<div style="text-align: center; margin-bottom: 16px; color: #666;">
<img src="/icon/image.png" alt="logo" class="auth-logo" />
</div>
<div class="auth-title-wrap">
<h3 class="auth-title">创建账号</h3>
<p class="auth-subtitle">创建账号以获取完整服务体验</p>
</div>
<el-input
v-model="registerForm.username"
placeholder="请输入用户名"
prefix-icon="User"
size="large"
style="margin-bottom: 15px;"
:disabled="registerLoading"
@@ -135,21 +138,14 @@ function backToLogin() {
<div>
<el-button
type="success"
type="primary"
size="large"
:loading="registerLoading"
:disabled="!canRegister || registerLoading"
@click="handleRegister"
style="width: 120px; margin-right: 10px;">
style="width: 100%;">
注册
</el-button>
<el-button
size="large"
:disabled="registerLoading"
@click="cancelRegister"
style="width: 120px;">
取消
</el-button>
</div>
<div style="margin-top: 20px; text-align: center;">
@@ -159,4 +155,38 @@ function backToLogin() {
</div>
</div>
</el-dialog>
</template>
</template>
<style scoped>
.auth-logo {
width: 160px;
height: auto;
}
.auth-dialog {
--el-color-primary: #1677FF;
}
.auth-dialog :deep(.el-button--primary) {
background-color: #1677FF;
border-color: #1677FF;
}
.auth-title-wrap {
margin-bottom: 12px;
}
.auth-title {
margin: 0;
font-size: 18px;
font-weight: 700;
color: #1f1f1f;
text-align: left;
}
.auth-subtitle {
margin: 6px 0 0;
font-size: 12px;
color: #8c8c8c;
text-align: left;
}
</style>

View File

@@ -53,6 +53,10 @@ const regionOptions = [
// 获取数据筛选:查询日期
const dateRange = ref<string[] | null>(null)
// 示例弹窗与示例数据
const rakutenExampleVisible = ref(false)
const rakutenExampleRows = ref([{ shopName: 'GBAmarket' }, { shopName: 'jotimei' }])
function handleSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
@@ -66,6 +70,21 @@ function openRakutenUpload() {
uploadInputRef.value?.click()
}
function viewRakutenExample() { rakutenExampleVisible.value = true }
function downloadRakutenTemplate() {
const html = '<table><tr><th>店铺名</th></tr><tr><td>GBAmarket</td></tr><tr><td>jotimei</td></tr></table>'
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'rakuten店铺模板.xls'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
function parseSkuPrices(input: any) {
try {
let skuSource: any = input
@@ -315,18 +334,18 @@ onMounted(loadLatest)
</div>
<div class="desc">请导入店铺信息仅限 Excel 文件表格第一列必须为乐天店铺名</div>
<div class="links">
<a class="link" @click.prevent>点击查看示例</a>
<a class="link" @click.prevent="viewRakutenExample">点击查看示例</a>
<span class="sep">|</span>
<a class="link" @click.prevent>点击下载模板</a>
<a class="link" @click.prevent="downloadRakutenTemplate">点击下载模板</a>
</div>
<div class="dropzone" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop" @click="openRakutenUpload" :class="{ disabled: loading }">
<div class="dz-el-icon">📤</div>
<div class="dz-text">点击或将文件拖拽到这里上传</div>
<div class="dz-sub">支持扩展名.xls .xlsx .numbers</div>
<div class="dz-sub">支持扩展名.xls .xlsx</div>
<div class="dz-sub">文件单列1/1</div>
</div>
<input ref="uploadInputRef" style="display:none" type="file" accept=".xlsx,.xls" @change="handleExcelUpload" :disabled="loading"/>
<input ref="uploadInputRef" style="display:none" type="file" accept=".xls,.xlsx" @change="handleExcelUpload" :disabled="loading"/>
<div v-if="selectedFileName" class="file-chip">
<span class="dot"></span>
<span class="name">{{ selectedFileName }}</span>
@@ -357,7 +376,10 @@ onMounted(loadLatest)
<div class="title">获取数据</div>
</div>
<div class="desc">导入表格后点击下方按钮开始获取店铺商品数据</div>
<el-button size="small" class="w100 btn-blue" :loading="loading" @click="handleStartSearch" :disabled="loading || (!pendingFile && allProducts.length === 0)">获取数据</el-button>
<div class="action-buttons column">
<el-button size="small" class="w100 btn-blue" :loading="loading" @click="handleStartSearch" :disabled="loading || !pendingFile">获取数据</el-button>
<el-button size="small" class="w100" :disabled="!loading" @click="stopTask">停止获取</el-button>
</div>
</div>
</div>
@@ -380,6 +402,14 @@ onMounted(loadLatest)
<!-- 右侧主区域 -->
<section class="content-panel">
<el-dialog v-model="rakutenExampleVisible" title="示例 - 乐天店铺Excel格式" width="420px">
<el-table :data="rakutenExampleRows" size="small" border>
<el-table-column prop="shopName" label="店铺名" />
</el-table>
<template #footer>
<el-button type="primary" class="btn-blue" @click="rakutenExampleVisible = false">我知道了</el-button>
</template>
</el-dialog>
<!-- 数据显示区域 -->
<div class="table-container">
<div class="table-section">

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
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'
@@ -49,7 +50,7 @@ function formatCny(v?: number) {
async function loadShops() {
try {
const resp = await zebraApi.getShops()
const resp = await zebraApi.getShops({ accountId: Number(accountId.value) || undefined })
const list = (resp as any)?.data?.data?.list ?? (resp as any)?.list ?? []
shopList.value = list
} catch (e) {
@@ -99,6 +100,7 @@ async function fetchPageData(startDate: string, endDate: string) {
try {
const data = await zebraApi.getOrders({
accountId: Number(accountId.value) || undefined,
startDate,
endDate,
page: fetchCurrentPage.value,
@@ -146,9 +148,9 @@ async function exportToExcel() {
exportLoading.value = true
try {
const result = await zebraApi.exportAndSaveOrders({ orders: allOrderData.value })
alert(`Excel文件已保存到: ${result.filePath}`)
} catch (e) {
alert('导出Excel失败')
ElMessage({ message: `Excel文件已保存到: ${result.filePath}` as any, type: 'success' })
} catch (e: any) {
ElMessage({ message: e?.message || '导出Excel失败', type: 'error' })
} finally {
exportLoading.value = false
}
@@ -190,28 +192,32 @@ function openManageAccount() {
}
async function submitAccount() {
if (!formUsername.value) { alert('请输入账号'); return }
if (!formUsername.value) { ElMessage({ message: '请输入账号', type: 'warning' }); return }
const payload: BanmaAccount = {
id: accountForm.value.id,
name: accountForm.value.name || formUsername.value,
username: formUsername.value,
password: formPassword.value || '',
isDefault: accountForm.value.isDefault || 0,
status: accountForm.value.status || 1,
}
const { id } = await zebraApi.saveAccount(payload)
if (rememberPwd.value && formPassword.value) {
localStorage.setItem(`banma:pwd:${formUsername.value}`, formPassword.value)
} else {
localStorage.removeItem(`banma:pwd:${formUsername.value}`)
try {
const res = await zebraApi.saveAccount(payload)
const id = (res as any)?.data?.id || (res as any)?.id
if (!id) throw new Error((res as any)?.msg || '保存失败')
accountDialogVisible.value = false
await loadAccounts()
if (id) accountId.value = id
} catch (e: any) {
ElMessage({ message: e?.message || '账号或密码错误无法获取Token', type: 'error' })
}
accountDialogVisible.value = false
await loadAccounts()
if (id) accountId.value = id
}
async function removeCurrentAccount() {
if (!isEditMode.value || !accountForm.value.id) return
if (!confirm('确认删除该账号?')) return
try {
await ElMessageBox.confirm('确认删除该账号?', '提示', { type: 'warning' })
} catch { return }
await zebraApi.removeAccount(accountForm.value.id)
accountDialogVisible.value = false
await loadAccounts()
@@ -304,6 +310,16 @@ async function removeCurrentAccount() {
<div class="content">
<!-- 数据表格无数据时也显示表头 -->
<div class="table-section">
<div v-if="showProgress" class="progress-section">
<div class="progress-box">
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
</div>
<div class="progress-text">{{ progressPercentage }}%</div>
</div>
</div>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
@@ -365,18 +381,14 @@ async function removeCurrentAccount() {
<div>加载中...</div>
</div>
<div v-else class="empty-container">
<div class="empty-icon">📄</div>
<div class="empty-icon" style="font-size:48px;">📄</div>
<div class="empty-text">暂无数据请获取订单</div>
</div>
</div>
</div>
<!-- 底部区域进度条 + 分页器 -->
<!-- 底部区域分页器 -->
<div class="pagination-fixed">
<div v-if="showProgress" class="progress-bottom">
<div class="progress-bar"><div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div></div>
<div class="progress-text">{{ progressPercentage }}%</div>
</div>
<el-pagination
background
:current-page="currentPage"
@@ -458,9 +470,10 @@ export default {
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
.tip { color: #909399; font-size: 12px; margin-bottom: 8px; text-align: left; }
.avatar { width: 18px; height: 18px; border-radius: 50%; margin-right: 6px; vertical-align: -2px; }
.avatar { width: 22px; height: 22px; border-radius: 50%; margin-right: 6px; vertical-align: -2px; }
.acct-text { vertical-align: middle; }
.acct-row { display: grid; grid-template-columns: 8px 18px 1fr auto; align-items: center; gap: 6px; width: 100%; }
.acct-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px; }
.status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
.status-dot.on { background: #22c55e; }
.status-dot.off { background: #f87171; }
@@ -489,8 +502,8 @@ export default {
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
.table tbody tr:hover { background: #f9f9f9; }
.truncate { max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.image-container { display: flex; justify-content: center; align-items: center; width: 24px; height: 20px; margin: 0 auto; background: #f8f9fa; border-radius: 2px; }
.thumb { width: 16px; height: 16px; object-fit: contain; border-radius: 2px; }
.image-container { display: flex; justify-content: center; align-items: center; width: 28px; height: 24px; margin: 0 auto; background: #f8f9fa; border-radius: 2px; }
.thumb { width: 22px; height: 22px; object-fit: contain; border-radius: 2px; }
.price-tag { color: #e6a23c; font-weight: bold; }
.fee-tag { color: #909399; font-weight: 500; }
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
@@ -499,10 +512,12 @@ export default {
.pagination-fixed { position: sticky; bottom: 0; z-index: 2; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; }
.tag { display: inline-block; padding: 0 6px; margin-left: 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; }
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.progress-bottom { display: flex; align-items: center; gap: 8px; margin-right: auto; }
.progress-bottom .progress-bar { width: 100%; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden; }
.progress-bottom .progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
.progress-bottom .progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
.progress-section { margin: 0px 12px 0px 12px; }
.progress-box { padding: 4px 0; }
.progress-container { display: flex; align-items: center; gap: 8px; }
.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; }
</style>

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vite + Vue template</title>
<title>erpClient</title>
<link rel="icon" href="/icon/icon.png">
<meta name="theme-color" content="#ffffff">
</head>