This commit is contained in:
2025-10-10 10:06:56 +08:00
parent 4fbe51d625
commit 6f22c9bffd
37 changed files with 2176 additions and 1183 deletions

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Deluze
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -16,7 +16,7 @@
},
"publish": {
"provider": "generic",
"url": "http://192.168.1.89:8080/static/updates/"
"url": "http://192.168.1.89:8085/static/updates/"
},
"nsis": {
"oneClick": false,

File diff suppressed because it is too large Load Diff

View File

@@ -311,9 +311,9 @@ app.whenReady().then(() => {
splashWindow.loadFile(splashPath);
}
setTimeout(() => {
openAppIfNotOpened();
}, 2000);
// setTimeout(() => {
// openAppIfNotOpened();
// }, 2000);
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {

View File

@@ -5,6 +5,7 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import {authApi} from './api/auth'
import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
import {getOrCreateDeviceId} from './utils/deviceId'
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
@@ -46,6 +47,19 @@ const devices = ref<DeviceItem[]>([])
const deviceQuota = ref<DeviceQuota>({limit: 0, used: 0})
const userPermissions = ref<string>('')
// VIP状态
const vipExpireTime = ref<Date | null>(null)
const vipStatus = computed(() => {
if (!vipExpireTime.value) return { isVip: false, daysLeft: 0, status: 'expired' }
const now = new Date()
const expire = new Date(vipExpireTime.value)
const daysLeft = Math.ceil((expire.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
if (daysLeft <= 0) return { isVip: false, daysLeft: 0, status: 'expired' }
if (daysLeft <= 7) return { isVip: true, daysLeft, status: 'warning' }
if (daysLeft <= 30) return { isVip: true, daysLeft, status: 'normal' }
return { isVip: true, daysLeft, status: 'active' }
})
// 更新对话框状态
const showUpdateDialog = ref(false)
@@ -131,7 +145,7 @@ function handleMenuSelect(key: string) {
addToHistory(key)
}
async function handleLoginSuccess(data: { token: string; permissions?: string }) {
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string }) {
isAuthenticated.value = true
showAuthDialog.value = false
showRegDialog.value = false
@@ -141,7 +155,15 @@ async function handleLoginSuccess(data: { token: string; permissions?: string })
const username = getUsernameFromToken(data.token)
currentUsername.value = username
userPermissions.value = data?.permissions || ''
await deviceApi.register({username})
vipExpireTime.value = data?.expireTime ? new Date(data.expireTime) : null
// 获取设备ID并注册设备
const deviceId = await getOrCreateDeviceId()
await deviceApi.register({
username,
deviceId,
os: navigator.platform
})
SSEManager.connect()
} catch (e: any) {
isAuthenticated.value = false
@@ -169,6 +191,7 @@ async function logout() {
isAuthenticated.value = false
currentUsername.value = ''
userPermissions.value = ''
vipExpireTime.value = null
showAuthDialog.value = true
showDeviceDialog.value = false
SSEManager.disconnect()
@@ -207,9 +230,15 @@ async function checkAuth() {
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
if (token) {
await authApi.verifyToken(token)
const verifyRes: any = await authApi.verifyToken(token)
isAuthenticated.value = true
currentUsername.value = getUsernameFromToken(token) || ''
userPermissions.value = verifyRes?.data?.permissions || verifyRes?.permissions || ''
if (verifyRes?.data?.expireTime || verifyRes?.expireTime) {
vipExpireTime.value = new Date(verifyRes?.data?.expireTime || verifyRes?.expireTime)
}
SSEManager.connect()
return
}
@@ -266,7 +295,7 @@ const SSEManager = {
return
}
let sseUrl = 'http://192.168.1.89:8080/monitor/account/events'
let sseUrl = 'http://192.168.1.89:8085/monitor/account/events'
try {
const resp = await fetch('/api/config/server')
if (resp.ok) sseUrl = (await resp.json()).sseUrl || sseUrl
@@ -304,6 +333,14 @@ const SSEManager = {
case 'PERMISSIONS_UPDATED':
checkAuth()
break
case 'ACCOUNT_EXPIRED':
vipExpireTime.value = null
ElMessage.warning('您的VIP已过期部分功能将受限')
break
case 'VIP_RENEWED':
checkAuth()
ElMessage.success('VIP已续费成功')
break
}
} catch (err) {
console.error('SSE消息处理失败:', err, '原始数据:', e.data)
@@ -423,6 +460,25 @@ onUnmounted(() => {
</li>
</ul>
<!-- VIP状态卡片 -->
<div v-if="isAuthenticated" class="vip-status-card" :class="'vip-' + vipStatus.status">
<div class="vip-info">
<div class="vip-status-text">
<template v-if="vipStatus.isVip">
<span v-if="vipStatus.status === 'warning'">即将到期</span>
<span v-else>订阅中</span>
</template>
<template v-else>
<span>已过期</span>
</template>
</div>
<div class="vip-expire-date" v-if="vipExpireTime">
有效期至{{ vipExpireTime ? new Date(vipExpireTime).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }) : '-' }}
</div>
</div>
</div>
</div>
<div class="main-content">
@@ -445,7 +501,7 @@ onUnmounted(() => {
</div>
</div>
<keep-alive v-if="activeDashboard">
<component :is="activeDashboard" :key="activeMenu"/>
<component :is="activeDashboard" :key="activeMenu" :is-vip="vipStatus.isVip"/>
</keep-alive>
<div v-if="showPlaceholder" class="placeholder">
<div class="placeholder-card">
@@ -580,6 +636,8 @@ onUnmounted(() => {
border-right: 1px solid #e8eaec;
padding: 16px 12px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.platform-icons {
@@ -789,4 +847,104 @@ onUnmounted(() => {
cursor: pointer;
user-select: none;
}
/* VIP状态卡片样式 */
.vip-status-card {
margin-top: auto;
width: 100%;
min-height: 64px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
box-sizing: border-box;
background: linear-gradient(135deg, #FFFAF0 0%, #FFE4B5 50%, #FFD700 100%);
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
transition: all 0.3s ease;
position: relative;
}
/* 正常状态和警告状态 - 统一温暖金色渐变 */
.vip-status-card.vip-active,
.vip-status-card.vip-normal,
.vip-status-card.vip-warning {
background: linear-gradient(135deg, #FFFAF0 0%, #FFE4B5 50%, #FFD700 100%);
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
}
/* 过期状态 - 灰色,垂直布局 */
.vip-status-card.vip-expired {
background: linear-gradient(135deg, #FAFAFA 0%, #E8E8E8 100%);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
flex-direction: column;
justify-content: space-between;
align-items: stretch;
padding: 10px;
gap: 8px;
}
.vip-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
text-align: left;
}
.vip-status-text {
font-size: 13px;
font-weight: 600;
color: #8B6914;
text-align: left;
letter-spacing: 0.3px;
}
.vip-expire-date {
font-size: 10px;
color: #A67C00;
line-height: 1.3;
text-align: left;
opacity: 0.9;
}
/* 右侧徽章按钮 */
.vip-badge {
padding: 5px 10px;
border-radius: 5px;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
color: white;
box-shadow: 0 2px 6px rgba(74, 144, 226, 0.3);
}
/* 过期状态续费按钮 - 置底 */
.vip-renew-btn {
padding: 7px 0;
text-align: center;
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
color: white;
border-radius: 5px;
font-size: 11px;
font-weight: 600;
width: 100%;
flex-shrink: 0;
box-shadow: 0 2px 6px rgba(74, 144, 226, 0.3);
}
.vip-status-card.vip-expired .vip-info {
align-items: flex-start;
}
.vip-status-card.vip-expired .vip-status-text {
color: #909399;
}
.vip-status-card.vip-expired .vip-expire-date {
color: #B0B0B0;
}
</style>

View File

@@ -2,25 +2,31 @@ import { http } from './http'
export const authApi = {
login(params: { username: string; password: string }) {
return http.post('/api/login', params)
// 直接调用 RuoYi 后端的登录接口
return http.post('/monitor/account/login', params)
},
register(params: { username: string; password: string }) {
return http.post('/api/register', params)
// 直接调用 RuoYi 后端的注册接口
return http.post('/monitor/account/register', params)
},
checkUsername(username: string) {
return http.get('/api/check-username', { username })
// 直接调用 RuoYi 后端的用户名检查接口
return http.get('/monitor/account/check-username', { username })
},
verifyToken(token: string) {
return http.post('/api/verify', { token })
// 直接调用 RuoYi 后端的验证接口
return http.post('/monitor/account/verify', { token })
},
logout(token: string) {
// 保留客户端的 logout用于清理本地状态
return http.postVoid('/api/logout', { token })
},
// 以下缓存相关接口仍使用客户端服务(用于本地 SQLite 存储)
deleteTokenCache() {
return http.postVoid('/api/cache/delete?key=token')
},

View File

@@ -2,26 +2,32 @@ import { http } from './http'
export const deviceApi = {
getQuota(username: string) {
return http.get('/api/device/quota', { username })
// 直接调用 RuoYi 后端的设备配额接口
return http.get('/monitor/device/quota', { username })
},
list(username: string) {
return http.get('/api/device/list', { username })
// 直接调用 RuoYi 后端的设备列表接口
return http.get('/monitor/device/list', { username })
},
register(payload: { username: string }) {
return http.post('/api/device/register', payload)
// 直接调用 RuoYi 后端的设备注册接口
return http.post('/monitor/device/register', payload)
},
remove(payload: { deviceId: string }) {
return http.post('/api/device/remove', payload)
// 直接调用 RuoYi 后端的设备移除接口
return http.post('/monitor/device/remove', payload)
},
heartbeat(payload: { username: string; deviceId: string; version?: string }) {
return http.post('/api/device/heartbeat', payload)
// 直接调用 RuoYi 后端的心跳接口
return http.post('/monitor/device/heartbeat', payload)
},
offline(payload: { deviceId: string }) {
return http.post('/api/device/offline', payload)
// 直接调用 RuoYi 后端的离线接口
return http.post('/monitor/device/offline', payload)
}
}

View File

@@ -2,10 +2,12 @@
export type HttpMethod = 'GET' | 'POST';
const BASE_CLIENT = 'http://localhost:8081'; // erp_client_sb
const BASE_RUOYI = 'http://192.168.1.89:8080';
const BASE_RUOYI = 'http://192.168.1.89:8085';
function resolveBase(path: string): string {
// 走 ruoyi-admin 的路径:鉴权版本、平台工具路由
// 走 ruoyi-admin 的路径:鉴权、设备管理、版本、平台工具路由
if (path.startsWith('/monitor/account')) return BASE_RUOYI; // 账号认证相关
if (path.startsWith('/monitor/device')) return BASE_RUOYI; // 设备管理
if (path.startsWith('/system/')) return BASE_RUOYI; // 版本控制器 VersionController
if (path.startsWith('/tool/banma')) return BASE_RUOYI; // 既有规则保留
// 其他默认走客户端服务
@@ -41,7 +43,12 @@ async function request<T>(path: string, options: RequestInit): Promise<T> {
}
const contentType = res.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return (await res.json()) as T;
const json: any = await res.json();
// 检查业务状态码
if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
throw new Error(json.msg || json.message || '请求失败');
}
return json as T;
}
return (await res.text()) as unknown as T;
}

View File

@@ -1,9 +1,14 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { amazonApi } from '../../api/amazon'
import { handlePlatformFileExport } from '../../utils/settings'
// 接收VIP状态
const props = defineProps<{
isVip: boolean
}>()
// 响应式状态
const loading = ref(false) // 主加载状态
const tableLoading = ref(false) // 表格加载状态
@@ -85,6 +90,22 @@ async function onDrop(e: DragEvent) {
// 批量获取产品信息 - 核心数据处理逻辑
async function batchGetProductInfo(asinList: string[]) {
// VIP检查
if (!props.isVip) {
try {
await ElMessageBox.confirm(
'VIP已过期数据采集功能受限。请联系管理员续费后继续使用。',
'VIP功能限制',
{
confirmButtonText: '我知道了',
showCancelButton: false,
type: 'warning'
}
)
} catch {}
return
}
try {
currentAsin.value = '正在处理...'
progressPercentage.value = 0
@@ -165,8 +186,6 @@ async function startQueuedFetch() {
// 导出Excel数据
const exportLoading = ref(false)
const exportProgress = ref(0)
const showExportProgress = ref(false)
async function exportToExcel() {
if (!localProductData.value.length) {
@@ -175,12 +194,6 @@ async function exportToExcel() {
}
exportLoading.value = true
showExportProgress.value = true
exportProgress.value = 0
const progressInterval = setInterval(() => {
if (exportProgress.value < 90) exportProgress.value += Math.random() * 20
}, 100)
// 生成Excel HTML格式
let html = `<table>
@@ -198,17 +211,12 @@ async function exportToExcel() {
const blob = new Blob([html], { type: 'application/vnd.ms-excel' })
const fileName = `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xls`
await handlePlatformFileExport('amazon', blob, fileName)
const success = await handlePlatformFileExport('amazon', blob, fileName)
clearInterval(progressInterval)
exportProgress.value = 100
showMessage('Excel文件导出成功', 'success')
setTimeout(() => {
showExportProgress.value = false
exportLoading.value = false
exportProgress.value = 0
}, 2000)
if (success) {
showMessage('Excel文件导出成功', 'success')
}
exportLoading.value = false
}
// 获取卖家/配送方信息 - 数据处理辅助函数
@@ -360,13 +368,6 @@ onMounted(async () => {
<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 || 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>
</div>
</div>
</div>
@@ -529,10 +530,6 @@ 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; }

View File

@@ -4,6 +4,7 @@ import { ElMessage } from 'element-plus'
import { User } from '@element-plus/icons-vue'
import { authApi } from '../../api/auth'
import { deviceApi } from '../../api/device'
import { getOrCreateDeviceId } from '../../utils/deviceId'
interface Props {
modelValue: boolean
@@ -11,7 +12,7 @@ interface Props {
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'loginSuccess', data: { token: string; user: any }): void
(e: 'loginSuccess', data: { token: string; permissions?: string; expireTime?: string }): void
(e: 'showRegister'): void
}
@@ -31,16 +32,26 @@ async function handleAuth() {
authLoading.value = true
try {
await deviceApi.register({ username: authForm.value.username })
const loginRes: any = await authApi.login(authForm.value)
const data = loginRes?.data || loginRes
// 获取或生成设备ID
const deviceId = await getOrCreateDeviceId()
// 注册设备
await deviceApi.register({
username: authForm.value.username,
deviceId: deviceId,
os: navigator.platform
})
// 登录
const loginRes: any = await authApi.login({
...authForm.value,
clientId: deviceId
})
emit('loginSuccess', {
token: data.token,
user: {
username: data.username,
permissions: data.permissions
}
token: loginRes.data.accessToken || loginRes.data.token,
permissions: loginRes.data.permissions,
expireTime: loginRes.data.expireTime
})
ElMessage.success('登录成功')
resetForm()

View File

@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { User } from '@element-plus/icons-vue'
import { authApi } from '../../api/auth'
import { getOrCreateDeviceId } from '../../utils/deviceId'
interface Props {
modelValue: boolean
@@ -10,7 +11,7 @@ interface Props {
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'loginSuccess', data: { token: string; user: any }): void
(e: 'loginSuccess', data: { token: string; permissions?: string; expireTime?: string }): void
(e: 'backToLogin'): void
}
@@ -42,8 +43,8 @@ async function checkUsernameAvailability() {
try {
const res: any = await authApi.checkUsername(registerForm.value.username)
const data = res?.data || res
usernameCheckResult.value = data?.available || false
// 后端返回 {code: 200, data: true/false}data 直接是布尔值
usernameCheckResult.value = res.data
} catch {
usernameCheckResult.value = null
}
@@ -54,23 +55,34 @@ async function handleRegister() {
registerLoading.value = true
try {
await authApi.register({
// 获取设备ID
const deviceId = await getOrCreateDeviceId()
// 注册账号传递设备ID用于判断是否赠送VIP
const registerRes: any = await authApi.register({
username: registerForm.value.username,
password: registerForm.value.password
password: registerForm.value.password,
deviceId: deviceId
})
const loginRes: any = await authApi.login({
username: registerForm.value.username,
password: registerForm.value.password
})
const loginData = loginRes?.data || loginRes
emit('loginSuccess', {
token: loginData.token,
user: {
username: loginData.username,
permissions: loginData.permissions
// 显示注册成功和VIP信息
if (registerRes.data.expireTime) {
const expireDate = new Date(registerRes.data.expireTime)
const now = new Date()
const daysLeft = Math.ceil((expireDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
if (daysLeft > 0) {
ElMessage.success(`注册成功!您获得了 ${daysLeft} 天VIP体验`)
} else {
ElMessage.warning('注册成功!该设备已使用过新人福利,请联系管理员续费')
}
}
// 使用注册返回的token直接登录
emit('loginSuccess', {
token: registerRes.data.accessToken || registerRes.data.token,
permissions: registerRes.data.permissions,
expireTime: registerRes.data.expireTime
})
resetForm()
} catch (err) {

View File

@@ -117,14 +117,16 @@ const info = ref({
const SKIP_VERSION_KEY = 'skipped_version'
const REMIND_LATER_KEY = 'remind_later_time'
async function autoCheck() {
async function autoCheck(silent = false) {
try {
version.value = await (window as any).electronAPI.getJarVersion()
const checkRes: any = await updateApi.checkUpdate(version.value)
const result = checkRes?.data || checkRes
if (!result.needUpdate) {
ElMessage.info('当前已是最新版本')
if (!silent) {
ElMessage.info('当前已是最新版本')
}
return
}
@@ -149,10 +151,14 @@ async function autoCheck() {
}
show.value = true
stage.value = 'check'
ElMessage.success('发现新版本')
if (!silent) {
ElMessage.success('发现新版本')
}
} catch (error) {
console.error('检查更新失败:', error)
ElMessage.error('检查更新失败')
if (!silent) {
ElMessage.error('检查更新失败')
}
}
}
@@ -239,6 +245,7 @@ async function installUpdate() {
onMounted(async () => {
version.value = await (window as any).electronAPI.getJarVersion()
await autoCheck(true)
})
onUnmounted(() => {

View File

@@ -1,10 +1,15 @@
<script setup lang="ts">
import {ref, computed, onMounted} from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import {rakutenApi} from '../../api/rakuten'
import { batchConvertImages } from '../../utils/imageProxy'
import { handlePlatformFileExport } from '../../utils/settings'
// 接收VIP状态
const props = defineProps<{
isVip: boolean
}>()
// UI 与加载状态
const loading = ref(false)
const tableLoading = ref(false)
@@ -114,17 +119,22 @@ function needsSearch(product: any) {
}
async function loadLatest() {
const resp = await rakutenApi.getLatestProducts()
allProducts.value = (resp.products || []).map(p => ({...p, skuPrices: parseSkuPrices(p)}))
const resp: any = await rakutenApi.getLatestProducts()
const products = resp.data.products || []
allProducts.value = products.map((p: any) => ({...p, skuPrices: parseSkuPrices(p)}))
}
async function searchProductInternal(product: any) {
if (!product || !product.imgUrl) return
if (!needsSearch(product)) return
const res = await rakutenApi.search1688(product.imgUrl, currentBatchId.value)
const data = res
const skuJson = (data as any)?.skuPriceJson ?? (data as any)?.skuPrice
if (!props.isVip) {
ElMessage.warning('VIP已过期1688识图功能受限')
return
}
const res: any = await rakutenApi.search1688(product.imgUrl, currentBatchId.value)
const data = res.data
const skuJson = data.skuPriceJson || data.skuPrice
Object.assign(product, {
mapRecognitionLink: data.mapRecognitionLink,
freight: data.freight,
@@ -186,8 +196,24 @@ async function onDrop(e: DragEvent) {
}
// 点击获取数据
// 点击"获取数据
async function handleStartSearch() {
// VIP检查
if (!props.isVip) {
try {
await ElMessageBox.confirm(
'VIP已过期数据采集功能受限。请联系管理员续费后继续使用。',
'VIP功能限制',
{
confirmButtonText: '我知道了',
showCancelButton: false,
type: 'warning'
}
)
} catch {}
return
}
if (pendingFile.value) {
try {
loading.value = true
@@ -199,8 +225,8 @@ async function handleStartSearch() {
progressPercentage.value = 0
totalProducts.value = 0
processedProducts.value = 0
const resp = await rakutenApi.getProducts({file: pendingFile.value, batchId: currentBatchId.value})
const products = (resp.products || []).map(p => ({...p, skuPrices: parseSkuPrices(p)}))
const resp: any = await rakutenApi.getProducts({file: pendingFile.value, batchId: currentBatchId.value})
const products = (resp.data.products || []).map((p: any) => ({...p, skuPrices: parseSkuPrices(p)}))
if (products.length === 0) {
showMessage('未采集到数据,请检查代理或店铺是否存在', 'warning')
@@ -373,9 +399,11 @@ async function exportToExcel() {
})
const fileName = `乐天商品数据_${new Date().toISOString().slice(0, 10)}.xlsx`
await handlePlatformFileExport('rakuten', blob, fileName)
const success = await handlePlatformFileExport('rakuten', blob, fileName)
showMessage('Excel文件导出成功', 'success')
if (success) {
showMessage('Excel文件导出成功', 'success')
}
} catch (error) {
showMessage('导出失败', 'error')
} finally {

View File

@@ -6,6 +6,11 @@ import AccountManager from '../common/AccountManager.vue'
import { batchConvertImages } from '../../utils/imageProxy'
import { handlePlatformFileExport } from '../../utils/settings'
// 接收VIP状态
const props = defineProps<{
isVip: boolean
}>()
type Shop = { id: string; shopName: string }
const accounts = ref<BanmaAccount[]>([])
@@ -86,6 +91,22 @@ function handleCurrentChange(page: number) {
async function fetchData() {
if (isFetching.value) return
// VIP检查
if (!props.isVip) {
try {
await ElMessageBox.confirm(
'VIP已过期数据采集功能受限。请联系管理员续费后继续使用。',
'VIP功能限制',
{
confirmButtonText: '我知道了',
showCancelButton: false,
type: 'warning'
}
)
} catch {}
return
}
loading.value = true
isFetching.value = true
showProgress.value = true
@@ -237,9 +258,11 @@ async function exportToExcel() {
})
const fileName = `斑马订单数据_${new Date().toISOString().slice(0, 10)}.xlsx`
await handlePlatformFileExport('zebra', blob, fileName)
const success = await handlePlatformFileExport('zebra', blob, fileName)
showMessage('Excel文件导出成功', 'success')
if (success) {
showMessage('Excel文件导出成功', 'success')
}
} catch (error) {
showMessage('导出失败', 'error')
} finally {

View File

@@ -0,0 +1,70 @@
/**
* 设备ID管理工具
* 从客户端服务获取硬件UUID通过 wmic 命令)
*/
const BASE_CLIENT = 'http://localhost:8081'
/**
* 从客户端服务获取硬件设备ID
* 客户端会使用 wmic 命令获取硬件UUID仅Windows
*/
async function fetchDeviceIdFromClient(): Promise<string> {
const response = await fetch(`${BASE_CLIENT}/api/device-id`, {
method: 'GET',
credentials: 'omit',
cache: 'no-store'
})
if (!response.ok) {
throw new Error('获取设备ID失败')
}
const result = await response.json()
const deviceId = result?.data
if (!deviceId) {
throw new Error('设备ID为空')
}
return deviceId
}
/**
* 获取或创建设备ID
* 1. 优先从本地缓存读取
* 2. 如果没有缓存从客户端服务获取使用硬件UUID
* 3. 保存到本地缓存
*/
export async function getOrCreateDeviceId(): Promise<string> {
try {
// 尝试从本地缓存获取
const response = await fetch(`${BASE_CLIENT}/api/cache/get?key=deviceId`)
if (response.ok) {
const result = await response.json()
const cachedDeviceId = result?.data
if (cachedDeviceId) {
return cachedDeviceId
}
}
} catch (error) {
console.warn('从缓存读取设备ID失败:', error)
}
// 从客户端服务获取新的设备ID硬件UUID
const newDeviceId = await fetchDeviceIdFromClient()
// 保存到本地缓存
try {
await fetch(`${BASE_CLIENT}/api/cache/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'deviceId', value: newDeviceId })
})
} catch (error) {
console.warn('保存设备ID到缓存失败:', error)
}
return newDeviceId
}

View File

@@ -90,7 +90,7 @@ export async function handlePlatformFileExport(
platform: Platform,
blob: Blob,
defaultFileName: string
): Promise<void> {
): Promise<boolean> {
const config = getPlatformExportConfig(platform)
if (!config.exportPath) {
@@ -105,10 +105,13 @@ export async function handlePlatformFileExport(
if (!result.canceled && result.filePath) {
await writeFileToPath(blob, result.filePath)
return true
}
return false
} else {
const filePath = `${config.exportPath}/${defaultFileName}`
await writeFileToPath(blob, filePath)
return true
}
}