1
This commit is contained in:
@@ -134,56 +134,43 @@ function handleMenuSelect(key: string) {
|
||||
async function handleLoginSuccess(data: { token: string; permissions?: string }) {
|
||||
isAuthenticated.value = true
|
||||
showAuthDialog.value = false
|
||||
showRegDialog.value = false // 确保注册对话框也关闭
|
||||
showRegDialog.value = false
|
||||
|
||||
try {
|
||||
// 保存token到本地数据库
|
||||
await authApi.saveToken(data.token)
|
||||
|
||||
const username = getUsernameFromToken(data.token)
|
||||
currentUsername.value = username
|
||||
userPermissions.value = data?.permissions || ''
|
||||
await deviceApi.register({username})
|
||||
|
||||
// 建立SSE连接
|
||||
SSEManager.connect()
|
||||
} catch (e: any) {
|
||||
// 设备注册失败时回滚登录状态
|
||||
isAuthenticated.value = false
|
||||
showAuthDialog.value = true
|
||||
await authApi.deleteTokenCache()
|
||||
ElMessage({
|
||||
message: e?.message || '设备注册失败,请重试',
|
||||
type: 'error'
|
||||
})
|
||||
ElMessage.error(e?.message || '设备注册失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
// 主动设置设备离线
|
||||
try {
|
||||
const deviceId = await getClientIdFromToken()
|
||||
if (deviceId) {
|
||||
await deviceApi.offline({ deviceId })
|
||||
}
|
||||
if (deviceId) await deviceApi.offline({ deviceId })
|
||||
} catch (error) {
|
||||
console.warn('离线通知失败:', error)
|
||||
}
|
||||
|
||||
const token = await authApi.getToken()
|
||||
if (token) {
|
||||
await authApi.logout(token)
|
||||
}
|
||||
try {
|
||||
const tokenRes: any = await authApi.getToken()
|
||||
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
|
||||
if (token) await authApi.logout(token)
|
||||
} catch {}
|
||||
|
||||
await authApi.deleteTokenCache()
|
||||
// 清理前端状态
|
||||
isAuthenticated.value = false
|
||||
currentUsername.value = ''
|
||||
userPermissions.value = ''
|
||||
showAuthDialog.value = true
|
||||
showDeviceDialog.value = false
|
||||
|
||||
// 关闭SSE连接
|
||||
SSEManager.disconnect()
|
||||
}
|
||||
|
||||
@@ -199,12 +186,8 @@ async function handleUserClick() {
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
await logout()
|
||||
ElMessage({
|
||||
message: '已退出登录',
|
||||
type: 'success'
|
||||
})
|
||||
} catch {
|
||||
}
|
||||
ElMessage.success('已退出登录')
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function showRegisterDialog() {
|
||||
@@ -218,26 +201,23 @@ function backToLogin() {
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
const authRequiredMenus = ['rakuten', 'amazon', 'zebra', 'shopee']
|
||||
|
||||
try {
|
||||
await authApi.sessionBootstrap().catch(() => undefined)
|
||||
const token = await authApi.getToken()
|
||||
const tokenRes: any = await authApi.getToken()
|
||||
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
|
||||
|
||||
if (token) {
|
||||
const response = await authApi.verifyToken(token)
|
||||
if (response?.success) {
|
||||
isAuthenticated.value = true
|
||||
currentUsername.value = getUsernameFromToken(token) || ''
|
||||
SSEManager.connect()
|
||||
return
|
||||
}
|
||||
await authApi.deleteTokenCache()
|
||||
await authApi.verifyToken(token)
|
||||
isAuthenticated.value = true
|
||||
currentUsername.value = getUsernameFromToken(token) || ''
|
||||
SSEManager.connect()
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// 忽略
|
||||
await authApi.deleteTokenCache()
|
||||
}
|
||||
|
||||
if (authRequiredMenus.includes(activeMenu.value)) {
|
||||
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
|
||||
showAuthDialog.value = true
|
||||
}
|
||||
}
|
||||
@@ -246,11 +226,11 @@ async function getClientIdFromToken(token?: string) {
|
||||
try {
|
||||
let t = token
|
||||
if (!t) {
|
||||
t = await authApi.getToken()
|
||||
const tokenRes: any = await authApi.getToken()
|
||||
t = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
|
||||
}
|
||||
if (!t) return ''
|
||||
|
||||
const payload = JSON.parse(atob(t.split('.')[1] || ''))
|
||||
const payload = JSON.parse(atob(t.split('.')[1]))
|
||||
return payload.clientId || ''
|
||||
} catch {
|
||||
return ''
|
||||
@@ -259,7 +239,7 @@ async function getClientIdFromToken(token?: string) {
|
||||
|
||||
function getUsernameFromToken(token: string) {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1] || ''))
|
||||
const payload = JSON.parse(atob(token.split('.')[1]))
|
||||
return payload.username || ''
|
||||
} catch {
|
||||
return ''
|
||||
@@ -273,21 +253,24 @@ const SSEManager = {
|
||||
if (this.connection) return
|
||||
|
||||
try {
|
||||
const token = await authApi.getToken()
|
||||
if (!token) return
|
||||
const tokenRes: any = await authApi.getToken()
|
||||
const token = typeof tokenRes === 'string' ? tokenRes : tokenRes?.data
|
||||
if (!token) {
|
||||
console.warn('SSE连接失败: 没有有效的 token')
|
||||
return
|
||||
}
|
||||
|
||||
const clientId = await getClientIdFromToken(token)
|
||||
if (!clientId) return
|
||||
if (!clientId) {
|
||||
console.warn('SSE连接失败: 无法从 token 获取 clientId')
|
||||
return
|
||||
}
|
||||
|
||||
let sseUrl = 'http://192.168.1.89:8080/monitor/account/events'
|
||||
try {
|
||||
const resp = await fetch('/api/config/server')
|
||||
if (resp.ok) {
|
||||
const config = await resp.json()
|
||||
sseUrl = config.sseUrl || sseUrl
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
if (resp.ok) sseUrl = (await resp.json()).sseUrl || sseUrl
|
||||
} catch {}
|
||||
|
||||
const src = new EventSource(`${sseUrl}?clientId=${clientId}&token=${token}`)
|
||||
this.connection = src
|
||||
@@ -296,15 +279,13 @@ const SSEManager = {
|
||||
src.onerror = () => this.handleError()
|
||||
} catch (e: any) {
|
||||
console.warn('SSE连接失败:', e?.message || e)
|
||||
this.disconnect()
|
||||
}
|
||||
},
|
||||
|
||||
handleMessage(e: MessageEvent) {
|
||||
try {
|
||||
// 处理ping心跳
|
||||
if (e.type === 'ping') {
|
||||
return // ping消息自动保持连接,无需处理
|
||||
}
|
||||
if (e.type === 'ping') return
|
||||
|
||||
console.log('SSE消息:', e.data)
|
||||
const payload = JSON.parse(e.data)
|
||||
@@ -314,17 +295,11 @@ const SSEManager = {
|
||||
break
|
||||
case 'DEVICE_REMOVED':
|
||||
logout()
|
||||
ElMessage({
|
||||
message: '您的设备已被移除,请重新登录',
|
||||
type: 'warning'
|
||||
})
|
||||
ElMessage.warning('您的设备已被移除,请重新登录')
|
||||
break
|
||||
case 'FORCE_LOGOUT':
|
||||
logout()
|
||||
ElMessage({
|
||||
message: '会话已失效,请重新登录',
|
||||
type: 'warning'
|
||||
})
|
||||
ElMessage.warning('会话已失效,请重新登录')
|
||||
break
|
||||
case 'PERMISSIONS_UPDATED':
|
||||
checkAuth()
|
||||
@@ -336,18 +311,16 @@ const SSEManager = {
|
||||
},
|
||||
|
||||
handleError() {
|
||||
this.disconnect()
|
||||
setTimeout(() => this.connect(), 3000)
|
||||
if (!this.connection) return
|
||||
try { this.connection.close() } catch {}
|
||||
this.connection = null
|
||||
console.warn('SSE连接失败,已断开')
|
||||
},
|
||||
|
||||
disconnect() {
|
||||
if (this.connection) {
|
||||
try {
|
||||
this.connection.close()
|
||||
} catch {
|
||||
}
|
||||
this.connection = null
|
||||
}
|
||||
if (!this.connection) return
|
||||
try { this.connection.close() } catch {}
|
||||
this.connection = null
|
||||
},
|
||||
}
|
||||
|
||||
@@ -366,26 +339,22 @@ function openSettings() {
|
||||
|
||||
async function fetchDeviceData() {
|
||||
if (!currentUsername.value) {
|
||||
ElMessage({
|
||||
message: '未获取到用户名,请重新登录',
|
||||
type: 'warning'
|
||||
})
|
||||
ElMessage.warning('未获取到用户名,请重新登录')
|
||||
return
|
||||
}
|
||||
try {
|
||||
deviceLoading.value = true
|
||||
const [quota, list] = await Promise.all([
|
||||
const [quotaRes, listRes] = await Promise.all([
|
||||
deviceApi.getQuota(currentUsername.value),
|
||||
deviceApi.list(currentUsername.value),
|
||||
])
|
||||
deviceQuota.value = quota || {limit: 0, used: 0}
|
||||
]) as any[]
|
||||
|
||||
deviceQuota.value = quotaRes?.data || quotaRes || {limit: 0, used: 0}
|
||||
const clientId = await getClientIdFromToken()
|
||||
devices.value = (list || []).map(d => ({...d, isCurrent: d.deviceId === clientId})) as any
|
||||
const list = listRes?.data || listRes || []
|
||||
devices.value = list.map(d => ({...d, isCurrent: d.deviceId === clientId}))
|
||||
} catch (e: any) {
|
||||
ElMessage({
|
||||
message: e?.message || '获取设备列表失败',
|
||||
type: 'error'
|
||||
})
|
||||
ElMessage.error(e?.message || '获取设备列表失败')
|
||||
} finally {
|
||||
deviceLoading.value = false
|
||||
}
|
||||
@@ -403,21 +372,12 @@ async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
|
||||
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
|
||||
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
|
||||
|
||||
// 如果是本机设备被移除,执行logout
|
||||
const clientId = await getClientIdFromToken()
|
||||
if (row.deviceId === clientId) {
|
||||
await logout()
|
||||
}
|
||||
if (row.deviceId === clientId) await logout()
|
||||
|
||||
ElMessage({
|
||||
message: '已移除设备',
|
||||
type: 'success'
|
||||
})
|
||||
ElMessage.success('已移除设备')
|
||||
} catch (e: any) {
|
||||
ElMessage({
|
||||
message: '移除设备失败: ' + ((e as any)?.message || '未知错误'),
|
||||
type: 'error'
|
||||
})
|
||||
if (e !== 'cancel') ElMessage.error('移除设备失败: ' + (e?.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,113 +1,39 @@
|
||||
import { http } from './http';
|
||||
|
||||
// 统一响应处理函数 - 适配ERP客户端格式
|
||||
function unwrap<T>(res: any): T {
|
||||
if (res && typeof res.success === 'boolean') {
|
||||
if (!res.success) {
|
||||
const message: string = res.message || res.msg || '请求失败';
|
||||
throw new Error(message);
|
||||
}
|
||||
return res as T;
|
||||
}
|
||||
// 兼容标准格式
|
||||
if (res && typeof res.code === 'number') {
|
||||
if (res.code !== 0) {
|
||||
const message: string = res.msg || '请求失败';
|
||||
throw new Error(message);
|
||||
}
|
||||
return (res.data as T) ?? ({} as T);
|
||||
}
|
||||
return res as T;
|
||||
}
|
||||
|
||||
// 认证相关类型定义
|
||||
interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface RegisterRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface LoginResponse {
|
||||
success: boolean;
|
||||
token: string;
|
||||
permissions: string[];
|
||||
username: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface RegisterResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
import { http } from './http'
|
||||
|
||||
export const authApi = {
|
||||
// 用户登录
|
||||
login(params: LoginRequest) {
|
||||
return http
|
||||
.post('/api/login', params)
|
||||
.then(res => unwrap<LoginResponse>(res));
|
||||
login(params: { username: string; password: string }) {
|
||||
return http.post('/api/login', params)
|
||||
},
|
||||
|
||||
// 用户注册
|
||||
register(params: RegisterRequest) {
|
||||
return http
|
||||
.post('/api/register', params)
|
||||
.then(res => unwrap<RegisterResponse>(res));
|
||||
register(params: { username: string; password: string }) {
|
||||
return http.post('/api/register', params)
|
||||
},
|
||||
|
||||
// 检查用户名可用性
|
||||
checkUsername(username: string) {
|
||||
return http
|
||||
.get('/api/check-username', { username })
|
||||
.then(res => {
|
||||
if (res && res.code === 200) {
|
||||
return { available: res.data };
|
||||
}
|
||||
throw new Error(res?.msg || '检查用户名失败');
|
||||
});
|
||||
return http.get('/api/check-username', { username })
|
||||
},
|
||||
|
||||
// 验证token有效性
|
||||
verifyToken(token: string) {
|
||||
return http
|
||||
.post('/api/verify', { token })
|
||||
.then(res => unwrap<{ success: boolean }>(res));
|
||||
return http.post('/api/verify', { token })
|
||||
},
|
||||
|
||||
// 用户登出
|
||||
logout(token: string) {
|
||||
return http.postVoid('/api/logout', { token });
|
||||
return http.postVoid('/api/logout', { token })
|
||||
},
|
||||
|
||||
// 删除token缓存
|
||||
deleteTokenCache() {
|
||||
return http.postVoid('/api/cache/delete?key=token');
|
||||
return http.postVoid('/api/cache/delete?key=token')
|
||||
},
|
||||
// 保存token到本地数据库
|
||||
|
||||
saveToken(token: string) {
|
||||
return http.postVoid('/api/cache/save', { key: 'token', value: token });
|
||||
return http.postVoid('/api/cache/save', { key: 'token', value: token })
|
||||
},
|
||||
|
||||
// 从本地数据库获取token
|
||||
getToken(): Promise<string | undefined> {
|
||||
return http.get<any>('/api/cache/get?key=token').then((res: any) => {
|
||||
if (typeof res === 'string') return res;
|
||||
if (res && typeof res === 'object') {
|
||||
if (typeof res.code === 'number') {
|
||||
return res.code === 0 ? (res.data as string | undefined) : undefined;
|
||||
}
|
||||
if (typeof (res as any).data === 'string') return (res as any).data as string;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
getToken() {
|
||||
return http.get('/api/cache/get?key=token')
|
||||
},
|
||||
|
||||
// 会话引导:检查并恢复会话(返回体各异,这里保持 any)
|
||||
sessionBootstrap() {
|
||||
return http.get<any>('/api/session/bootstrap');
|
||||
},
|
||||
};
|
||||
return http.get('/api/session/bootstrap')
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,27 @@
|
||||
import { http } from './http'
|
||||
|
||||
// 与老版保持相同的接口路径与参数
|
||||
const base = '/api/device'
|
||||
|
||||
export interface DeviceQuota {
|
||||
limit: number
|
||||
used: number
|
||||
}
|
||||
|
||||
export interface DeviceItem {
|
||||
deviceId: string
|
||||
name?: string
|
||||
status?: 'online' | 'offline'
|
||||
lastActiveAt?: string
|
||||
}
|
||||
|
||||
// 统一处理AjaxResult格式
|
||||
function handleAjaxResult(res: any) {
|
||||
if (res?.code !== 200) {
|
||||
throw new Error(res?.msg || '操作失败')
|
||||
}
|
||||
return res.data
|
||||
}
|
||||
|
||||
export const deviceApi = {
|
||||
getQuota(username: string): Promise<DeviceQuota> {
|
||||
return http.get(`${base}/quota`, { username }).then(handleAjaxResult)
|
||||
getQuota(username: string) {
|
||||
return http.get('/api/device/quota', { username })
|
||||
},
|
||||
|
||||
list(username: string): Promise<DeviceItem[]> {
|
||||
return http.get(`${base}/list`, { username }).then(handleAjaxResult)
|
||||
list(username: string) {
|
||||
return http.get('/api/device/list', { username })
|
||||
},
|
||||
|
||||
register(payload: { username: string }) {
|
||||
return http.post(`${base}/register`, payload).then(handleAjaxResult)
|
||||
return http.post('/api/device/register', payload)
|
||||
},
|
||||
|
||||
remove(payload: { deviceId: string }) {
|
||||
return http.post(`${base}/remove`, payload).then(handleAjaxResult)
|
||||
return http.post('/api/device/remove', payload)
|
||||
},
|
||||
|
||||
heartbeat(payload: { username: string; deviceId: string; version?: string }) {
|
||||
return http.post(`${base}/heartbeat`, payload).then(handleAjaxResult)
|
||||
return http.post('/api/device/heartbeat', payload)
|
||||
},
|
||||
|
||||
offline(payload: { deviceId: string }) {
|
||||
return http.post(`${base}/offline`, payload).then(handleAjaxResult)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
return http.post('/api/device/offline', payload)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,21 @@
|
||||
import { http } from './http';
|
||||
|
||||
function unwrap<T>(res: any): T {
|
||||
if (res && typeof res.code === 'number') {
|
||||
if (res.code !== 0) {
|
||||
const message: string = res.msg || '请求失败';
|
||||
throw new Error(message);
|
||||
}
|
||||
return (res.data as T) ?? ({} as T);
|
||||
}
|
||||
return res as T;
|
||||
}
|
||||
import { http } from './http'
|
||||
|
||||
export const rakutenApi = {
|
||||
// 上传 Excel 或按店铺名查询
|
||||
getProducts(params: { file?: File; shopName?: string; batchId?: string }) {
|
||||
const formData = new FormData();
|
||||
if (params.file) formData.append('file', params.file);
|
||||
if (params.batchId) formData.append('batchId', params.batchId);
|
||||
if (params.shopName) formData.append('shopName', params.shopName);
|
||||
return http
|
||||
.upload('/api/rakuten/products', formData)
|
||||
.then(res => unwrap<{ products: any[]; total?: number; sessionId?: string }>(res));
|
||||
const formData = new FormData()
|
||||
if (params.file) formData.append('file', params.file)
|
||||
if (params.batchId) formData.append('batchId', params.batchId)
|
||||
if (params.shopName) formData.append('shopName', params.shopName)
|
||||
return http.upload('/api/rakuten/products', formData)
|
||||
},
|
||||
|
||||
search1688(imageUrl: string, sessionId?: string) {
|
||||
const payload: Record<string, unknown> = { imageUrl };
|
||||
if (sessionId) payload.sessionId = sessionId;
|
||||
return http.post('/api/rakuten/search1688', payload).then(res => unwrap<any>(res));
|
||||
const payload: Record<string, unknown> = { imageUrl }
|
||||
if (sessionId) payload.sessionId = sessionId
|
||||
return http.post('/api/rakuten/search1688', payload)
|
||||
},
|
||||
|
||||
getLatestProducts() {
|
||||
return http.get('/api/rakuten/products/latest').then(res => unwrap<{ products: any[] }>(res));
|
||||
},
|
||||
};
|
||||
return http.get('/api/rakuten/products/latest')
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { http } from './http';
|
||||
|
||||
export const shopeeApi = {
|
||||
getAdHosting(params: Record<string, unknown> = {}) {
|
||||
return http.get('/api/shopee/ad-hosting', params);
|
||||
},
|
||||
getReviews(params: Record<string, unknown> = {}) {
|
||||
return http.get('/api/shopee/reviews', params);
|
||||
},
|
||||
exportData(exportParams: Record<string, unknown> = {}) {
|
||||
return http.post('/api/shopee/export', exportParams);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
11
electron-vue-template/src/renderer/api/update.ts
Normal file
11
electron-vue-template/src/renderer/api/update.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { http } from './http'
|
||||
|
||||
export const updateApi = {
|
||||
getVersion() {
|
||||
return http.get('/api/update/version')
|
||||
},
|
||||
|
||||
checkUpdate(currentVersion: string) {
|
||||
return http.get(`/system/version/check?currentVersion=${currentVersion}`)
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,39 @@
|
||||
// 斑马订单模型(根据页面所需字段精简定义)
|
||||
export interface ZebraOrder {
|
||||
orderedAt?: string;
|
||||
productImage?: string;
|
||||
productTitle?: string;
|
||||
shopOrderNumber?: string;
|
||||
timeSinceOrder?: string;
|
||||
priceJpy?: number;
|
||||
productQuantity?: number;
|
||||
shippingFeeJpy?: number;
|
||||
serviceFee?: string;
|
||||
productNumber?: string;
|
||||
poNumber?: string;
|
||||
shippingFeeCny?: number;
|
||||
internationalShippingFee?: number;
|
||||
poLogisticsCompany?: string;
|
||||
poTrackingNumber?: string;
|
||||
internationalTrackingNumber?: string;
|
||||
trackInfo?: string;
|
||||
}
|
||||
import { http } from './http'
|
||||
|
||||
export interface ZebraOrdersResp {
|
||||
orders: ZebraOrder[];
|
||||
total?: number;
|
||||
totalPages?: number;
|
||||
}
|
||||
|
||||
import { http } from './http';
|
||||
|
||||
export interface BanmaAccount {
|
||||
id?: number;
|
||||
name?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
tokenExpireAt?: string | number;
|
||||
isDefault?: number;
|
||||
status?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
// 斑马 API:与原 zebra-api.js 对齐的接口封装
|
||||
export const zebraApi = {
|
||||
// 账号管理(ruoyi-admin)
|
||||
getAccounts() {
|
||||
return http.get<{ code?: number; msg?: string; data: BanmaAccount[] }>('/tool/banma/accounts');
|
||||
return http.get('/tool/banma/accounts')
|
||||
},
|
||||
saveAccount(body: BanmaAccount) {
|
||||
return http.post<{ id: number }>('/tool/banma/accounts', body);
|
||||
|
||||
saveAccount(body: any) {
|
||||
return http.post('/tool/banma/accounts', body)
|
||||
},
|
||||
|
||||
removeAccount(id: number) {
|
||||
// 用 postVoid 也可,但这里前端未用到,保留以备将来
|
||||
return http.delete<void>(`/tool/banma/accounts/${id}`);
|
||||
return http.delete(`/tool/banma/accounts/${id}`)
|
||||
},
|
||||
|
||||
// 业务采集
|
||||
getShops(params?: { accountId?: number }) {
|
||||
return http.get<{ data?: { list?: Array<{ id: string; shopName: string }> } }>(
|
||||
'/api/banma/shops', params as unknown as Record<string, unknown>
|
||||
);
|
||||
},
|
||||
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>);
|
||||
return http.get('/api/banma/shops', params as Record<string, unknown>)
|
||||
},
|
||||
|
||||
getOrders(params: any) {
|
||||
return http.get('/api/banma/orders', params as Record<string, unknown>)
|
||||
},
|
||||
|
||||
// 其他功能(客户端微服务)
|
||||
getOrdersByBatch(batchId: string) {
|
||||
return http.get<ZebraOrdersResp>(`/api/banma/orders/batch/${batchId}`);
|
||||
return http.get(`/api/banma/orders/batch/${batchId}`)
|
||||
},
|
||||
|
||||
getLatestOrders() {
|
||||
return http.get<ZebraOrdersResp>('/api/banma/orders/latest');
|
||||
return http.get('/api/banma/orders/latest')
|
||||
},
|
||||
|
||||
getOrderStats() {
|
||||
return http.get('/api/banma/orders/stats');
|
||||
return http.get('/api/banma/orders/stats')
|
||||
},
|
||||
|
||||
searchOrders(searchParams: Record<string, unknown>) {
|
||||
return http.get('/api/banma/orders/search', searchParams);
|
||||
},
|
||||
};
|
||||
return http.get('/api/banma/orders/search', searchParams)
|
||||
}
|
||||
}
|
||||
@@ -295,6 +295,17 @@ onMounted(async () => {
|
||||
<div class="body-layout">
|
||||
<!-- 左侧步骤栏 -->
|
||||
<aside class="steps-sidebar">
|
||||
<!-- 顶部标签栏 -->
|
||||
<div class="top-tabs">
|
||||
<div class="tab-item active">
|
||||
<span class="tab-icon">📦</span>
|
||||
<span class="tab-text">ASIN查询</span>
|
||||
</div>
|
||||
<div class="tab-item" @click="openGenmaiSpirit">
|
||||
<span class="tab-icon">🔍</span>
|
||||
<span class="tab-text">跟卖精灵</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="steps-title">操作流程:</div>
|
||||
<div class="steps-flow">
|
||||
<!-- 1 -->
|
||||
@@ -356,7 +367,6 @@ onMounted(async () => {
|
||||
</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>
|
||||
</div>
|
||||
@@ -453,14 +463,25 @@ onMounted(async () => {
|
||||
<style scoped>
|
||||
.amazon-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
|
||||
.main-container { background: #fff; border-radius: 4px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: flex; flex-direction: column; }
|
||||
.body-layout { display: flex; gap: 12px; height: 100%; }
|
||||
|
||||
/* 顶部标签栏 */
|
||||
.top-tabs { display: flex; margin-bottom: 8px; }
|
||||
.tab-item { flex: 1; display: flex; align-items: center; justify-content: center; gap: 3px; padding: 4px 6px; cursor: pointer; transition: all 0.2s ease; background: #f5f7fa; color: #606266; font-size: 11px; font-weight: 500; border: 1px solid #ebeef5; }
|
||||
.tab-item:first-child { border-radius: 3px 0 0 3px; }
|
||||
.tab-item:last-child { border-radius: 0 3px 3px 0; border-left: none; }
|
||||
.tab-item:hover { background: #e8f4ff; color: #409EFF; }
|
||||
.tab-item.active { background: #1677FF; color: #fff; border-color: #1677FF; cursor: default; }
|
||||
.tab-icon { font-size: 12px; }
|
||||
.tab-text { line-height: 1; }
|
||||
|
||||
.body-layout { display: flex; gap: 12px; flex: 1; overflow: hidden; }
|
||||
.steps-sidebar { width: 220px; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 10px; height: 100%; flex-shrink: 0; }
|
||||
.steps-title { font-size: 14px; font-weight: 600; color: #303133; margin-bottom: 8px; text-align: left; }
|
||||
.steps-title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
||||
.steps-flow { position: relative; }
|
||||
.steps-flow:before { content: ''; position: absolute; left: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
|
||||
.flow-item { position: relative; display: grid; grid-template-columns: 24px 1fr; gap: 10px; padding: 8px 0; }
|
||||
.steps-flow:before { content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6); }
|
||||
.flow-item { position: relative; display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 8px 0; }
|
||||
.flow-item + .flow-item { border-top: 1px dashed #ebeef5; }
|
||||
.flow-item .step-index { position: static; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
||||
.flow-item .step-index { position: static; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
||||
.flow-item:after { display: none; }
|
||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
|
||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
@@ -521,14 +542,14 @@ onMounted(async () => {
|
||||
.table-wrapper::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
|
||||
.table-wrapper:hover::-webkit-scrollbar-thumb { background: #a8abb2; }
|
||||
.table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
|
||||
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: left; }
|
||||
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
|
||||
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: center; }
|
||||
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; text-align: center; }
|
||||
.table tbody tr:hover { background: #f9f9f9; }
|
||||
.table th:nth-child(1), .table td:nth-child(1) { width: 33.33%; }
|
||||
.table th:nth-child(2), .table td:nth-child(2) { width: 33.33%; }
|
||||
.table th:nth-child(3), .table td:nth-child(3) { width: 33.33%; }
|
||||
.asin-out { color: #f56c6c; font-weight: 600; }
|
||||
.seller-info { display: flex; align-items: center; gap: 4px; }
|
||||
.seller-info { display: flex; align-items: center; gap: 4px; justify-content: center; }
|
||||
.seller { color: #303133; font-weight: 500; }
|
||||
.shipper { color: #909399; font-size: 12px; }
|
||||
.price { color: #e6a23c; font-weight: 600; }
|
||||
|
||||
@@ -31,11 +31,10 @@ async function handleAuth() {
|
||||
|
||||
authLoading.value = true
|
||||
try {
|
||||
// 1. 先检查设备限制
|
||||
await deviceApi.register({ username: authForm.value.username })
|
||||
|
||||
// 2. 设备检查通过,进行登录
|
||||
const data = await authApi.login(authForm.value)
|
||||
const loginRes: any = await authApi.login(authForm.value)
|
||||
const data = loginRes?.data || loginRes
|
||||
|
||||
emit('loginSuccess', {
|
||||
token: data.token,
|
||||
user: {
|
||||
|
||||
@@ -41,8 +41,9 @@ async function checkUsernameAvailability() {
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await authApi.checkUsername(registerForm.value.username)
|
||||
usernameCheckResult.value = data.available
|
||||
const res: any = await authApi.checkUsername(registerForm.value.username)
|
||||
const data = res?.data || res
|
||||
usernameCheckResult.value = data?.available || false
|
||||
} catch {
|
||||
usernameCheckResult.value = null
|
||||
}
|
||||
@@ -53,18 +54,17 @@ async function handleRegister() {
|
||||
|
||||
registerLoading.value = true
|
||||
try {
|
||||
// 1. 注册
|
||||
await authApi.register({
|
||||
username: registerForm.value.username,
|
||||
password: registerForm.value.password
|
||||
})
|
||||
|
||||
// 2. 注册成功后直接登录
|
||||
const loginData = await authApi.login({
|
||||
|
||||
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: {
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
getSettings,
|
||||
saveSettings,
|
||||
savePlatformSettings,
|
||||
type Platform,
|
||||
type PlatformExportSettings
|
||||
} from '../../utils/settings'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 平台配置
|
||||
const platforms = [
|
||||
{ key: 'amazon' as Platform, name: 'Amazon', icon: '🛒', color: '#FF9900' },
|
||||
{ key: 'rakuten' as Platform, name: 'Rakuten', icon: '🛍️', color: '#BF0000' },
|
||||
{ key: 'zebra' as Platform, name: 'Zebra', icon: '🦓', color: '#34495E' }
|
||||
]
|
||||
|
||||
// 设置项
|
||||
const platformSettings = ref<Record<Platform, PlatformExportSettings>>({
|
||||
amazon: { exportPath: '' },
|
||||
rakuten: { exportPath: '' },
|
||||
zebra: { exportPath: '' }
|
||||
})
|
||||
|
||||
const activeTab = ref<Platform>('amazon')
|
||||
|
||||
const show = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
// 选择导出路径
|
||||
async function selectExportPath(platform: Platform) {
|
||||
const result = await (window as any).electronAPI.showOpenDialog({
|
||||
title: `选择${platforms.find(p => p.key === platform)?.name}默认导出路径`,
|
||||
properties: ['openDirectory'],
|
||||
defaultPath: platformSettings.value[platform].exportPath
|
||||
})
|
||||
|
||||
if (!result.canceled && result.filePaths?.length > 0) {
|
||||
platformSettings.value[platform].exportPath = result.filePaths[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
function saveAllSettings() {
|
||||
Object.keys(platformSettings.value).forEach(platformKey => {
|
||||
const platform = platformKey as Platform
|
||||
const platformConfig = platformSettings.value[platform]
|
||||
savePlatformSettings(platform, platformConfig)
|
||||
})
|
||||
|
||||
ElMessage({ message: '设置已保存', type: 'success' })
|
||||
show.value = false
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
function loadAllSettings() {
|
||||
const settings = getSettings()
|
||||
platformSettings.value = {
|
||||
amazon: { ...settings.platforms.amazon },
|
||||
rakuten: { ...settings.platforms.rakuten },
|
||||
zebra: { ...settings.platforms.zebra }
|
||||
}
|
||||
}
|
||||
|
||||
// 重置单个平台设置
|
||||
function resetPlatformSettings(platform: Platform) {
|
||||
platformSettings.value[platform] = {
|
||||
exportPath: ''
|
||||
}
|
||||
}
|
||||
|
||||
// 重置所有设置
|
||||
function resetAllSettings() {
|
||||
platforms.forEach(platform => {
|
||||
resetPlatformSettings(platform.key)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAllSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="show"
|
||||
title="应用设置"
|
||||
width="480px"
|
||||
:close-on-click-modal="false"
|
||||
class="settings-dialog">
|
||||
|
||||
<div class="settings-content">
|
||||
<!-- 平台选择标签 -->
|
||||
<div class="platform-tabs">
|
||||
<div
|
||||
v-for="platform in platforms"
|
||||
:key="platform.key"
|
||||
:class="['platform-tab', { active: activeTab === platform.key }]"
|
||||
@click="activeTab = platform.key"
|
||||
:style="{ '--platform-color': platform.color }">
|
||||
<span class="platform-icon">{{ platform.icon }}</span>
|
||||
<span class="platform-name">{{ platform.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前平台设置 -->
|
||||
<div class="setting-section">
|
||||
<div class="section-title">
|
||||
<span class="title-icon">📁</span>
|
||||
<span>{{ platforms.find(p => p.key === activeTab)?.name }} 导出设置</span>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">默认导出路径</div>
|
||||
<div class="setting-desc">设置 {{ platforms.find(p => p.key === activeTab)?.name }} Excel文件的默认保存位置</div>
|
||||
<div class="path-input-group">
|
||||
<el-input
|
||||
v-model="platformSettings[activeTab].exportPath"
|
||||
placeholder="留空时自动弹出保存对话框"
|
||||
readonly
|
||||
class="path-input">
|
||||
<template #suffix>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="selectExportPath(activeTab)"
|
||||
class="select-btn">
|
||||
浏览
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="resetPlatformSettings(activeTab)">
|
||||
重置此平台
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="resetAllSettings">重置全部</el-button>
|
||||
<el-button @click="show = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveAllSettings">保存设置</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-dialog :deep(.el-dialog__body) {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.platform-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
padding: 4px;
|
||||
background: #F8F9FA;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.platform-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: transparent;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.platform-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: var(--platform-color);
|
||||
}
|
||||
|
||||
.platform-tab.active {
|
||||
background: #fff;
|
||||
color: var(--platform-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.platform-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.platform-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.setting-desc {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.path-input-group {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.path-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.path-input :deep(.el-input__wrapper) {
|
||||
padding-right: 80px;
|
||||
}
|
||||
|
||||
.select-btn {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 24px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
background: #F8F9FA;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.info-content p {
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.info-content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setting-actions {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #EBEEF5;
|
||||
}
|
||||
|
||||
.settings-dialog :deep(.el-dialog__header) {
|
||||
text-align: center;
|
||||
padding-right: 40px; /* 为右侧关闭按钮留出空间 */
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SettingsDialog'
|
||||
}
|
||||
</script>
|
||||
@@ -2,15 +2,18 @@
|
||||
<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="stage === 'downloading' ? `正在更新 ${appName}` : '软件更新'">
|
||||
<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">
|
||||
<img src="/icon/icon.png" class="app-icon app-icon-large" alt="App Icon" />
|
||||
<img src="/icon/icon.png" class="app-icon app-icon-large" alt="App Icon"/>
|
||||
</div>
|
||||
<div class="right-pane">
|
||||
<p class="announce">新版本的"{{ appName }}"已经发布</p>
|
||||
<p class="desc">{{ appName }} {{ info.latestVersion }} 可供安装,您现在的版本是 {{ version }},要现在安装吗?</p>
|
||||
<p class="desc">{{ appName }} {{ info.latestVersion }} 可供安装,您现在的版本是 {{
|
||||
version
|
||||
}},要现在安装吗?</p>
|
||||
|
||||
<div class="update-details form">
|
||||
<h4>更新信息</h4>
|
||||
@@ -20,7 +23,7 @@
|
||||
class="notes-box"
|
||||
:rows="6"
|
||||
readonly
|
||||
resize="none" />
|
||||
resize="none"/>
|
||||
</div>
|
||||
|
||||
<div class="update-actions row">
|
||||
@@ -41,7 +44,7 @@
|
||||
<div v-else-if="stage === 'downloading'" class="update-content">
|
||||
<div class="download-main">
|
||||
<div class="download-icon">
|
||||
<img src="/icon/icon.png" class="app-icon" alt="App Icon" />
|
||||
<img src="/icon/icon.png" class="app-icon" alt="App Icon"/>
|
||||
</div>
|
||||
<div class="download-content">
|
||||
<div class="download-info">
|
||||
@@ -52,7 +55,7 @@
|
||||
:percentage="prog.percentage"
|
||||
:show-text="false"
|
||||
:stroke-width="6"
|
||||
color="#409EFF" />
|
||||
color="#409EFF"/>
|
||||
<div class="progress-details">
|
||||
<span style="font-weight: 500">{{ prog.current }} / {{ prog.total }}</span>
|
||||
<el-button size="small" @click="cancelDownload">取消</el-button>
|
||||
@@ -64,7 +67,7 @@
|
||||
|
||||
<div v-else-if="stage === 'completed'" class="update-content">
|
||||
<div class="update-header text-center">
|
||||
<img src="/icon/icon.png" class="app-icon" alt="App Icon" />
|
||||
<img src="/icon/icon.png" class="app-icon" alt="App Icon"/>
|
||||
<h3>更新完成</h3>
|
||||
<p>更新文件已下载,将在重启后自动应用</p>
|
||||
</div>
|
||||
@@ -77,7 +80,7 @@
|
||||
:percentage="100"
|
||||
:show-text="false"
|
||||
:stroke-width="6"
|
||||
color="#67C23A" />
|
||||
color="#67C23A"/>
|
||||
</div>
|
||||
|
||||
<div class="update-buttons">
|
||||
@@ -90,9 +93,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { updateApi } from '../../api/update'
|
||||
import {ref, computed, onMounted, onUnmounted} from 'vue'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {updateApi} from '../../api/update'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
@@ -105,8 +108,8 @@ const show = computed({
|
||||
type Stage = 'check' | 'downloading' | 'completed'
|
||||
const stage = ref<Stage>('check')
|
||||
const appName = ref('我了个电商')
|
||||
const version = ref('2.0.0')
|
||||
const prog = ref({ percentage: 0, current: '0 MB', total: '0 MB', speed: '' })
|
||||
const version = ref('')
|
||||
const prog = ref({percentage: 0, current: '0 MB', total: '0 MB', speed: ''})
|
||||
const info = ref({
|
||||
latestVersion: '2.4.8',
|
||||
downloadUrl: '',
|
||||
@@ -117,49 +120,48 @@ const info = ref({
|
||||
|
||||
async function autoCheck() {
|
||||
try {
|
||||
ElMessage({ message: '正在检查更新...', type: 'info' })
|
||||
version.value = await (window as any).electronAPI.getJarVersion()
|
||||
const checkRes: any = await updateApi.checkUpdate(version.value)
|
||||
const result = checkRes?.data || checkRes
|
||||
|
||||
try {
|
||||
version.value = await updateApi.getVersion()
|
||||
} catch (error) {
|
||||
console.error('获取版本失败:', error)
|
||||
version.value = '2.0.0'
|
||||
if (!result.needUpdate) {
|
||||
ElMessage.info('当前已是最新版本')
|
||||
return
|
||||
}
|
||||
|
||||
info.value = {
|
||||
currentVersion: version.value,
|
||||
latestVersion: '2.4.9',
|
||||
downloadUrl: 'https://qiniu.pxdj.tashowz.com/2025/09/becac13811214c909d11162d2ff2c863.asar',
|
||||
currentVersion: result.currentVersion,
|
||||
latestVersion: result.latestVersion,
|
||||
downloadUrl: result.downloadUrl || '',
|
||||
updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 轻量级更新(仅替换app.asar)',
|
||||
hasUpdate: true
|
||||
}
|
||||
|
||||
show.value = true
|
||||
stage.value = 'check'
|
||||
ElMessage({ message: '发现新版本', type: 'success' })
|
||||
ElMessage.success('发现新版本')
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error)
|
||||
ElMessage({ message: '检查更新失败', type: 'error' })
|
||||
ElMessage.error('检查更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function start() {
|
||||
if (!info.value.downloadUrl) {
|
||||
ElMessage({ message: '下载链接不可用', type: 'error' });
|
||||
return;
|
||||
ElMessage.error('下载链接不可用')
|
||||
return
|
||||
}
|
||||
|
||||
stage.value = 'downloading';
|
||||
prog.value = { percentage: 0, current: '0 MB', total: '0 MB', speed: '' };
|
||||
stage.value = 'downloading'
|
||||
prog.value = {percentage: 0, current: '0 MB', total: '0 MB', speed: ''}
|
||||
|
||||
(window as any).electronAPI.onDownloadProgress((progress: any) => {
|
||||
;(window as any).electronAPI.onDownloadProgress((progress: any) => {
|
||||
prog.value = {
|
||||
percentage: progress.percentage || 0,
|
||||
current: progress.current || '0 MB',
|
||||
total: progress.total || '0 MB',
|
||||
speed: progress.speed || ''
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await (window as any).electronAPI.downloadUpdate(info.value.downloadUrl)
|
||||
@@ -167,14 +169,14 @@ async function start() {
|
||||
if (response.success) {
|
||||
stage.value = 'completed'
|
||||
prog.value.percentage = 100
|
||||
ElMessage({ message: '下载完成', type: 'success' })
|
||||
ElMessage.success('下载完成')
|
||||
} else {
|
||||
ElMessage({ message: '下载失败: ' + (response.error || '未知错误'), type: 'error' })
|
||||
ElMessage.error('下载失败: ' + (response.error || '未知错误'))
|
||||
stage.value = 'check'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage({ message: '下载失败', type: 'error' })
|
||||
ElMessage.error('下载失败')
|
||||
stage.value = 'check'
|
||||
}
|
||||
}
|
||||
@@ -195,37 +197,26 @@ async function cancelDownload() {
|
||||
async function installUpdate() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'安装过程中程序将自动重启,请确保已保存所有工作。确定要立即安装更新吗?',
|
||||
'确认安装',
|
||||
{
|
||||
confirmButtonText: '立即安装',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
'安装过程中程序将自动重启,请确保已保存所有工作。确定要立即安装更新吗?',
|
||||
'确认安装',
|
||||
{
|
||||
confirmButtonText: '立即安装',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
const response = await (window as any).electronAPI.installUpdate()
|
||||
|
||||
if (response.success) {
|
||||
ElMessage({ message: '应用即将重启', type: 'success' })
|
||||
ElMessage.success('应用即将重启')
|
||||
setTimeout(() => show.value = false, 1000)
|
||||
} else {
|
||||
ElMessage({ message: '重启失败: ' + (response.error || '未知错误'), type: 'error' })
|
||||
stage.value = 'check'
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('安装失败:', error)
|
||||
ElMessage({ message: '安装失败', type: 'error' })
|
||||
}
|
||||
if (error !== 'cancel') ElMessage.error('安装失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
version.value = await updateApi.getVersion()
|
||||
} catch (error) {
|
||||
console.error('获取版本失败:', error)
|
||||
}
|
||||
version.value = await (window as any).electronAPI.getJarVersion()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -238,7 +229,7 @@ onUnmounted(() => {
|
||||
position: fixed;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
background: rgba(255,255,255,0.9);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
@@ -273,11 +264,38 @@ onUnmounted(() => {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.left-pane { display: flex; flex-direction: column; align-items: flex-start; }
|
||||
.app-icon-large { width: 70px; height: 70px; border-radius: 12px; margin: 4px 0 0 0; }
|
||||
.right-pane { min-width: 0; }
|
||||
.right-pane .announce { font-size: 16px; font-weight: 600; color: #1f2937; margin: 4px 0 6px; word-break: break-word; }
|
||||
.right-pane .desc { font-size: 13px; color: #6b7280; line-height: 1.6; margin: 0; word-break: break-word; }
|
||||
.left-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.app-icon-large {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 12px;
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
.right-pane {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.right-pane .announce {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 4px 0 6px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.right-pane .desc {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.update-header {
|
||||
display: flex;
|
||||
@@ -324,8 +342,13 @@ onUnmounted(() => {
|
||||
margin: 12px 0 8px 0;
|
||||
}
|
||||
|
||||
.update-details.form { max-height: none; }
|
||||
.notes-box :deep(textarea.el-textarea__inner) { white-space: pre-wrap; }
|
||||
.update-details.form {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.notes-box :deep(textarea.el-textarea__inner) {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.update-details h4 {
|
||||
font-size: 14px;
|
||||
@@ -347,10 +370,24 @@ onUnmounted(() => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.update-actions.row .update-buttons { justify-content: space-between; }
|
||||
:deep(.update-actions.row .update-buttons .el-button) { flex: none; min-width: 100px; }
|
||||
.left-actions { display: flex; gap: 12px; }
|
||||
.right-actions { display: flex; gap: 8px; }
|
||||
.update-actions.row .update-buttons {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:deep(.update-actions.row .update-buttons .el-button) {
|
||||
flex: none;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.left-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:deep(.update-buttons .el-button) {
|
||||
flex: 1;
|
||||
|
||||
@@ -49,9 +49,7 @@ const selectedFileName = ref('')
|
||||
const pendingFile = ref<File | null>(null)
|
||||
const region = ref('JP')
|
||||
const regionOptions = [
|
||||
{ label: '日本 (Japan)', value: 'JP', flag: '🇯🇵' },
|
||||
{ label: '美国 (USA)', value: 'US', flag: '🇺🇸' },
|
||||
{ label: '中国 (China)', value: 'CN', flag: '🇨🇳' },
|
||||
{ label: '日本 (Japan)', value: 'JP', flag: '🇯🇵' }
|
||||
]
|
||||
// 获取数据筛选:查询日期
|
||||
const dateRange = ref<string[] | null>(null)
|
||||
@@ -432,7 +430,7 @@ onMounted(loadLatest)
|
||||
<div class="title">网站地区</div>
|
||||
</div>
|
||||
<div class="desc">请选择目标网站地区,如:日本区。</div>
|
||||
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%">
|
||||
<el-select v-model="region" placeholder="选择地区" size="small" style="width: 100%" disabled>
|
||||
<el-option v-for="opt in regionOptions" :key="opt.value" :label="opt.label" :value="opt.value">
|
||||
<span style="margin-right:6px">{{ opt.flag }}</span>{{ opt.label }}
|
||||
</el-option>
|
||||
@@ -602,14 +600,14 @@ onMounted(loadLatest)
|
||||
|
||||
.body-layout { display: flex; gap: 12px; height: 100%; }
|
||||
.steps-sidebar { width: 220px; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 10px; height: 100%; flex-shrink: 0; }
|
||||
.steps-title { font-size: 14px; font-weight: 600; color: #303133; margin-bottom: 8px; text-align: left; }
|
||||
.steps-title { font-size: 14px; font-weight: 600; color: #303133; text-align: left; }
|
||||
|
||||
/* 卡片式步骤,与示例一致 */
|
||||
.steps-flow { position: relative; }
|
||||
.steps-flow:before { content: ''; position: absolute; left: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
|
||||
.flow-item { position: relative; display: grid; grid-template-columns: 24px 1fr; gap: 10px; padding: 8px 0; }
|
||||
.steps-flow:before { content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6); }
|
||||
.flow-item { position: relative; display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 8px 0; }
|
||||
.flow-item + .flow-item { border-top: 1px dashed #ebeef5; }
|
||||
.flow-item .step-index { position: static; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
||||
.flow-item .step-index { position: static; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
||||
.flow-item:after { display: none; }
|
||||
.step-card { border: none; border-radius: 0; padding: 0; background: transparent; }
|
||||
.step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
|
||||
@@ -538,12 +538,12 @@ export default {
|
||||
.aside.collapsed { width: 56px; overflow: hidden; }
|
||||
.aside-header { display: flex; justify-content: flex-start; align-items: center; font-weight: 600; color: #606266; margin-bottom: 8px; }
|
||||
.aside-steps { position: relative; }
|
||||
.step { display: grid; grid-template-columns: 24px 1fr; gap: 10px; position: relative; padding: 8px 0; }
|
||||
.step { display: grid; grid-template-columns: 22px 1fr; gap: 10px; position: relative; padding: 8px 0; }
|
||||
.step + .step { border-top: 1px dashed #ebeef5; }
|
||||
.step-index { width: 24px; height: 24px; background: #1677FF; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
||||
.step-index { width: 22px; height: 22px; background: #1677FF; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
||||
.step-body { min-width: 0; text-align: left; }
|
||||
.step-title { font-size: 13px; color: #606266; margin-bottom: 6px; font-weight: 600; text-align: left; }
|
||||
.aside-steps:before { content: ''; position: absolute; left: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
|
||||
.aside-steps:before { content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6); }
|
||||
.account-list {height: auto; }
|
||||
.step-actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
.step-accounts { position: relative; }
|
||||
|
||||
Reference in New Issue
Block a user