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

@@ -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
}
}