feat(electron-vue-template):重构认证与设备管理模块
- 统一token存取逻辑,封装getToken/setToken/removeToken方法 -优化设备ID获取逻辑,调整API路径 - 完善设备管理接口类型定义,增强类型安全 - 调整SSE连接逻辑,使用统一配置管理- 重构HTTP客户端,集中管理后端服务配置 - 更新认证相关API接口,完善请求/响应类型 - 优化设备列表展示逻辑,移除冗余字段 - 调整图片代理路径,统一API前缀 - 完善用户反馈列表展示功能,增强交互体验 - 移除冗余的错误处理逻辑,简化代码结构
This commit is contained in:
@@ -495,6 +495,63 @@ ipcMain.handle('write-file', async (event, filePath: string, data: Uint8Array) =
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// 获取日志日期列表
|
||||
ipcMain.handle('get-log-dates', async () => {
|
||||
try {
|
||||
const logDir = 'C:/ProgramData/erp-logs';
|
||||
if (!existsSync(logDir)) {
|
||||
return { dates: [] };
|
||||
}
|
||||
|
||||
const files = await fs.readdir(logDir);
|
||||
const dates: string[] = [];
|
||||
|
||||
// 获取今天的日期(YYYY-MM-DD格式)
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
files.forEach(file => {
|
||||
if (file === 'spring-boot.log') {
|
||||
// 当天的日志文件,使用今天的日期
|
||||
dates.push(today);
|
||||
} else if (file.startsWith('spring-boot-') && file.endsWith('.log')) {
|
||||
// 历史日志文件,提取日期
|
||||
const date = file.replace('spring-boot-', '').replace('.log', '');
|
||||
dates.push(date);
|
||||
}
|
||||
});
|
||||
|
||||
// 排序,最新的在前面
|
||||
dates.sort().reverse();
|
||||
|
||||
return { dates };
|
||||
} catch (error) {
|
||||
console.error('获取日志日期列表失败:', error);
|
||||
return { dates: [] };
|
||||
}
|
||||
});
|
||||
|
||||
// 读取指定日期的日志文件
|
||||
ipcMain.handle('read-log-file', async (event, logDate: string) => {
|
||||
try {
|
||||
const logDir = 'C:/ProgramData/erp-logs';
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// 如果是今天的日期,读取 spring-boot.log,否则读取带日期的文件
|
||||
const fileName = logDate === today ? 'spring-boot.log' : `spring-boot-${logDate}.log`;
|
||||
const logFilePath = `${logDir}/${fileName}`;
|
||||
|
||||
if (!existsSync(logFilePath)) {
|
||||
return { success: false, error: '日志文件不存在' };
|
||||
}
|
||||
|
||||
const content = await fs.readFile(logFilePath, 'utf-8');
|
||||
return { success: true, content };
|
||||
} catch (error) {
|
||||
console.error('读取日志文件失败:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '读取失败' };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
async function getFileSize(url: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -17,6 +17,9 @@ const electronAPI = {
|
||||
showOpenDialog: (options: any) => ipcRenderer.invoke('show-open-dialog', options),
|
||||
// 添加文件写入 API
|
||||
writeFile: (filePath: string, data: Uint8Array) => ipcRenderer.invoke('write-file', filePath, data),
|
||||
// 添加日志相关 API
|
||||
getLogDates: () => ipcRenderer.invoke('get-log-dates'),
|
||||
readLogFile: (logDate: string) => ipcRenderer.invoke('read-log-file', logDate),
|
||||
|
||||
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||
ipcRenderer.on('download-progress', (event, progress) => callback(progress))
|
||||
|
||||
@@ -3,9 +3,11 @@ import {onMounted, ref, computed, defineAsyncComponent, type Component, onUnmoun
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import 'element-plus/dist/index.css'
|
||||
import {authApi, TOKEN_KEY} from './api/auth'
|
||||
import {authApi} from './api/auth'
|
||||
import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
|
||||
import {getOrCreateDeviceId} from './utils/deviceId'
|
||||
import {getToken, setToken, removeToken, getUsernameFromToken, getClientIdFromToken} from './utils/token'
|
||||
import {CONFIG} from './api/http'
|
||||
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
|
||||
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
|
||||
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
|
||||
@@ -147,7 +149,7 @@ function handleMenuSelect(key: string) {
|
||||
|
||||
async function handleLoginSuccess(data: { token: string; permissions?: string; expireTime?: string }) {
|
||||
try {
|
||||
localStorage.setItem(TOKEN_KEY, data.token)
|
||||
setToken(data.token)
|
||||
isAuthenticated.value = true
|
||||
showAuthDialog.value = false
|
||||
showRegDialog.value = false
|
||||
@@ -166,13 +168,13 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e
|
||||
} catch (e: any) {
|
||||
isAuthenticated.value = false
|
||||
showAuthDialog.value = true
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
removeToken()
|
||||
ElMessage.error(e?.message || '设备注册失败')
|
||||
}
|
||||
}
|
||||
|
||||
function clearLocalAuth() {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
removeToken()
|
||||
isAuthenticated.value = false
|
||||
currentUsername.value = ''
|
||||
userPermissions.value = ''
|
||||
@@ -221,7 +223,7 @@ function backToLogin() {
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const token = localStorage.getItem(TOKEN_KEY)
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
|
||||
showAuthDialog.value = true
|
||||
@@ -229,40 +231,24 @@ async function checkAuth() {
|
||||
return
|
||||
}
|
||||
|
||||
const res: any = await authApi.verifyToken(token)
|
||||
const res = await authApi.verifyToken(token)
|
||||
isAuthenticated.value = true
|
||||
currentUsername.value = getUsernameFromToken(token) || ''
|
||||
userPermissions.value = res?.data?.permissions || res?.permissions || ''
|
||||
currentUsername.value = getUsernameFromToken(token)
|
||||
userPermissions.value = res.data.permissions || ''
|
||||
|
||||
const expireTime = res?.data?.expireTime || res?.expireTime
|
||||
if (expireTime) vipExpireTime.value = new Date(expireTime)
|
||||
if (res.data.expireTime) {
|
||||
vipExpireTime.value = new Date(res.data.expireTime)
|
||||
}
|
||||
|
||||
SSEManager.connect()
|
||||
} catch {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
removeToken()
|
||||
if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) {
|
||||
showAuthDialog.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getClientIdFromToken(token?: string) {
|
||||
try {
|
||||
const t = token || localStorage.getItem(TOKEN_KEY)
|
||||
if (!t) return ''
|
||||
return JSON.parse(atob(t.split('.')[1])).clientId || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getUsernameFromToken(token: string) {
|
||||
try {
|
||||
return JSON.parse(atob(token.split('.')[1])).username || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const SSEManager = {
|
||||
connection: null as EventSource | null,
|
||||
@@ -270,19 +256,13 @@ const SSEManager = {
|
||||
if (this.connection) return
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem(TOKEN_KEY)
|
||||
if (!token) return console.warn('SSE连接失败: 没有token')
|
||||
const token = getToken()
|
||||
if (!token) return
|
||||
|
||||
const clientId = getClientIdFromToken(token)
|
||||
if (!clientId) return console.warn('SSE连接失败: 无法获取clientId')
|
||||
const clientId = getClientIdFromToken()
|
||||
if (!clientId) return
|
||||
|
||||
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
|
||||
} catch {}
|
||||
|
||||
const src = new EventSource(`${sseUrl}?clientId=${clientId}&token=${token}`)
|
||||
const src = new EventSource(`${CONFIG.SSE_URL}?clientId=${clientId}&token=${token}`)
|
||||
this.connection = src
|
||||
src.onopen = () => console.log('SSE连接已建立')
|
||||
src.onmessage = (e) => this.handleMessage(e)
|
||||
@@ -335,10 +315,10 @@ const SSEManager = {
|
||||
|
||||
disconnect() {
|
||||
if (this.connection) {
|
||||
try { this.connection.close() } catch {}
|
||||
this.connection.close()
|
||||
this.connection = null
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function openDeviceManager() {
|
||||
@@ -363,13 +343,12 @@ async function fetchDeviceData() {
|
||||
deviceLoading.value = true
|
||||
const [quotaRes, listRes] = await Promise.all([
|
||||
deviceApi.getQuota(currentUsername.value),
|
||||
deviceApi.list(currentUsername.value),
|
||||
]) as any[]
|
||||
deviceApi.list(currentUsername.value)
|
||||
])
|
||||
|
||||
deviceQuota.value = quotaRes?.data || quotaRes || {limit: 0, used: 0}
|
||||
const clientId = await getClientIdFromToken()
|
||||
const list = listRes?.data || listRes || []
|
||||
devices.value = list.map(d => ({...d, isCurrent: d.deviceId === clientId}))
|
||||
deviceQuota.value = quotaRes.data
|
||||
const clientId = getClientIdFromToken()
|
||||
devices.value = listRes.data.map(d => ({...d, isCurrent: d.deviceId === clientId}))
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '获取设备列表失败')
|
||||
} finally {
|
||||
@@ -377,7 +356,7 @@ async function fetchDeviceData() {
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
|
||||
async function confirmRemoveDevice(row: DeviceItem) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要移除该设备吗?', '你确定要移除设备吗?', {
|
||||
confirmButtonText: '确定移除',
|
||||
@@ -387,7 +366,7 @@ async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
|
||||
|
||||
await deviceApi.remove({deviceId: row.deviceId})
|
||||
devices.value = devices.value.filter(d => d.deviceId !== row.deviceId)
|
||||
deviceQuota.value.used = Math.max(0, (deviceQuota.value.used || 0) - 1)
|
||||
deviceQuota.value.used = Math.max(0, deviceQuota.value.used - 1)
|
||||
|
||||
if (row.deviceId === getClientIdFromToken()) {
|
||||
clearLocalAuth()
|
||||
|
||||
@@ -30,6 +30,6 @@ export const amazonApi = {
|
||||
return http.get('/api/amazon/products/search', searchParams);
|
||||
},
|
||||
openGenmaiSpirit() {
|
||||
return http.post('/api/genmai/open');
|
||||
return http.post('/api/system/genmai/open');
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import { http } from './http'
|
||||
|
||||
export const TOKEN_KEY = 'auth_token'
|
||||
export interface LoginParams {
|
||||
username: string
|
||||
password: string
|
||||
clientId?: string
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
token: string
|
||||
permissions?: string
|
||||
accountName?: string
|
||||
expireTime?: string
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
login(params: { username: string; password: string }) {
|
||||
return http.post('/monitor/account/login', params)
|
||||
login(params: LoginParams) {
|
||||
return http.post<{ data: AuthResponse }>('/monitor/account/login', params)
|
||||
},
|
||||
|
||||
register(params: { username: string; password: string }) {
|
||||
return http.post('/monitor/account/register', params)
|
||||
register(params: { username: string; password: string; deviceId?: string }) {
|
||||
return http.post<{ data: AuthResponse }>('/monitor/account/register', params)
|
||||
},
|
||||
|
||||
checkUsername(username: string) {
|
||||
return http.get('/monitor/account/check-username', { username })
|
||||
return http.get<{ data: boolean }>('/monitor/account/check-username', { username })
|
||||
},
|
||||
|
||||
verifyToken(token: string) {
|
||||
return http.post('/monitor/account/verify', { token })
|
||||
return http.post<{ data: AuthResponse }>('/monitor/account/verify', { token })
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,37 @@
|
||||
import { http } from './http'
|
||||
|
||||
export interface DeviceItem {
|
||||
deviceId: string
|
||||
name?: string
|
||||
os?: string
|
||||
status: 'online' | 'offline'
|
||||
lastActiveAt?: string
|
||||
isCurrent?: boolean
|
||||
}
|
||||
|
||||
export interface DeviceQuota {
|
||||
limit: number
|
||||
used: number
|
||||
}
|
||||
|
||||
export const deviceApi = {
|
||||
getQuota(username: string) {
|
||||
// 直接调用 RuoYi 后端的设备配额接口
|
||||
return http.get('/monitor/device/quota', { username })
|
||||
return http.get<{ data: DeviceQuota }>('/monitor/device/quota', { username })
|
||||
},
|
||||
|
||||
list(username: string) {
|
||||
// 直接调用 RuoYi 后端的设备列表接口
|
||||
return http.get('/monitor/device/list', { username })
|
||||
return http.get<{ data: DeviceItem[] }>('/monitor/device/list', { username })
|
||||
},
|
||||
|
||||
register(payload: { username: string }) {
|
||||
// 直接调用 RuoYi 后端的设备注册接口
|
||||
register(payload: { username: string; deviceId: string; os?: string }) {
|
||||
return http.post('/monitor/device/register', payload)
|
||||
},
|
||||
|
||||
remove(payload: { deviceId: string }) {
|
||||
// 直接调用 RuoYi 后端的设备移除接口
|
||||
return http.post('/monitor/device/remove', payload)
|
||||
},
|
||||
|
||||
offline(payload: { deviceId: string }) {
|
||||
// 直接调用 RuoYi 后端的离线接口
|
||||
return http.post('/monitor/device/offline', payload)
|
||||
}
|
||||
}
|
||||
21
electron-vue-template/src/renderer/api/feedback.ts
Normal file
21
electron-vue-template/src/renderer/api/feedback.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { http } from './http'
|
||||
|
||||
export interface FeedbackParams {
|
||||
username: string
|
||||
deviceId: string
|
||||
feedbackContent: string
|
||||
logDate?: string
|
||||
logFile?: File
|
||||
}
|
||||
|
||||
export const feedbackApi = {
|
||||
submit(data: FeedbackParams) {
|
||||
const formData = new FormData()
|
||||
formData.append('username', data.username)
|
||||
formData.append('deviceId', data.deviceId)
|
||||
formData.append('feedbackContent', data.feedbackContent)
|
||||
if (data.logDate) formData.append('logDate', data.logDate)
|
||||
if (data.logFile) formData.append('logFile', data.logFile)
|
||||
return http.upload('/monitor/feedback/submit', formData)
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,31 @@
|
||||
// 极简 HTTP 工具:封装 GET/POST,按路径选择后端服务
|
||||
export type HttpMethod = 'GET' | 'POST';
|
||||
// HTTP 工具:统一管理后端服务配置和请求
|
||||
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
|
||||
|
||||
const BASE_CLIENT = 'http://localhost:8081'; // erp_client_sb
|
||||
const BASE_RUOYI = 'http://192.168.1.89:8085';
|
||||
// 集中管理所有后端服务配置
|
||||
export const CONFIG = {
|
||||
CLIENT_BASE: 'http://localhost:8081',
|
||||
RUOYI_BASE: 'http://192.168.1.89:8085',
|
||||
SSE_URL: 'http://192.168.1.89:8085/monitor/account/events'
|
||||
} as const;
|
||||
|
||||
function resolveBase(path: string): string {
|
||||
// 走 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; // 既有规则保留
|
||||
// 其他默认走客户端服务
|
||||
return BASE_CLIENT;
|
||||
// RuoYi 后端路径:鉴权、设备、反馈、版本、工具
|
||||
if (path.startsWith('/monitor/') || path.startsWith('/system/') || path.startsWith('/tool/banma')) {
|
||||
return CONFIG.RUOYI_BASE;
|
||||
}
|
||||
// 其他走客户端服务
|
||||
return CONFIG.CLIENT_BASE;
|
||||
}
|
||||
|
||||
// 将对象转为查询字符串
|
||||
function buildQuery(params?: Record<string, unknown>): string {
|
||||
if (!params) return '';
|
||||
const usp = new URLSearchParams();
|
||||
const query = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null) return;
|
||||
usp.append(key, String(value));
|
||||
if (value != null) query.append(key, String(value));
|
||||
});
|
||||
const queryString = usp.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
return query.toString() ? `?${query}` : '';
|
||||
}
|
||||
|
||||
// 统一请求入口:自动加上 BASE_URL、JSON 头与错误处理
|
||||
async function request<T>(path: string, options: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${resolveBase(path)}${path}`, {
|
||||
credentials: 'omit',
|
||||
@@ -34,22 +33,27 @@ async function request<T>(path: string, options: RequestInit): Promise<T> {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {}),
|
||||
},
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(text || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
const json: any = await res.json();
|
||||
// 检查业务状态码
|
||||
// 业务状态码判断:支持两种格式
|
||||
// - erp_client_sb (本地服务): code=0 表示成功
|
||||
// - RuoYi 后端: code=200 表示成功
|
||||
if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
|
||||
throw new Error(json.msg || json.message || '请求失败');
|
||||
throw new Error(json.msg || '请求失败');
|
||||
}
|
||||
return json as T;
|
||||
}
|
||||
|
||||
return (await res.text()) as unknown as T;
|
||||
}
|
||||
|
||||
@@ -58,40 +62,38 @@ export const http = {
|
||||
return request<T>(`${path}${buildQuery(params)}`, { method: 'GET' });
|
||||
},
|
||||
post<T>(path: string, body?: unknown) {
|
||||
return request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined });
|
||||
return request<T>(path, {
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
});
|
||||
},
|
||||
|
||||
delete<T>(path: string) {
|
||||
return request<T>(path, { method: 'DELETE' });
|
||||
},
|
||||
// 用于无需读取响应体的 POST(如删除/心跳等),从根源避免读取中断
|
||||
postVoid(path: string, body?: unknown) {
|
||||
return fetch(`${resolveBase(path)}${path}`, {
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
credentials: 'omit',
|
||||
cache: 'no-store',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}).then(res => {
|
||||
if (!res.ok) return res.text().then(t => Promise.reject(new Error(t || `HTTP ${res.status}`)));
|
||||
return undefined as unknown as void;
|
||||
});
|
||||
},
|
||||
// 文件上传:透传 FormData,不设置 Content-Type 让浏览器自动处理
|
||||
|
||||
upload<T>(path: string, form: FormData) {
|
||||
const res = fetch(`${resolveBase(path)}${path}`, {
|
||||
return fetch(`${resolveBase(path)}${path}`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
credentials: 'omit',
|
||||
cache: 'no-store',
|
||||
});
|
||||
return res.then(async response => {
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '');
|
||||
throw new Error(text || `HTTP ${response.status}`);
|
||||
cache: 'no-store'
|
||||
}).then(async res => {
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(text || `HTTP ${res.status}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
const json: any = await res.json();
|
||||
if (json.code !== undefined && json.code !== 0 && json.code !== 200) {
|
||||
throw new Error(json.msg || '请求失败');
|
||||
}
|
||||
return json as T;
|
||||
}
|
||||
return (await res.text()) as unknown as T;
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { http } from './http'
|
||||
|
||||
export const updateApi = {
|
||||
getVersion() {
|
||||
return http.get('/api/update/version')
|
||||
return http.get('/api/system/version')
|
||||
},
|
||||
|
||||
checkUpdate(currentVersion: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getSettings,
|
||||
saveSettings,
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
type Platform,
|
||||
type PlatformExportSettings
|
||||
} from '../../utils/settings'
|
||||
import { feedbackApi } from '../../api/feedback'
|
||||
import { getToken, getUsernameFromToken } from '../../utils/token'
|
||||
import { getOrCreateDeviceId } from '../../utils/deviceId'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -34,7 +37,15 @@ const platformSettings = ref<Record<Platform, PlatformExportSettings>>({
|
||||
zebra: { exportPath: '' }
|
||||
})
|
||||
|
||||
const activeTab = ref<Platform>('amazon')
|
||||
const activeTab = ref<string>('amazon')
|
||||
const settingsMainRef = ref<HTMLElement | null>(null)
|
||||
const isScrolling = ref(false)
|
||||
|
||||
// 反馈表单
|
||||
const feedbackContent = ref('')
|
||||
const selectedLogDate = ref('')
|
||||
const feedbackSubmitting = ref(false)
|
||||
const logDates = ref<string[]>([])
|
||||
|
||||
const show = computed({
|
||||
get: () => props.modelValue,
|
||||
@@ -90,8 +101,114 @@ function resetAllSettings() {
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动到指定区域
|
||||
function scrollToSection(sectionKey: string) {
|
||||
if (isScrolling.value) return
|
||||
|
||||
const element = document.getElementById(`section-${sectionKey}`)
|
||||
if (element && settingsMainRef.value) {
|
||||
isScrolling.value = true
|
||||
activeTab.value = sectionKey
|
||||
|
||||
settingsMainRef.value.scrollTo({
|
||||
top: element.offsetTop - 20,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
isScrolling.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听滚动更新高亮
|
||||
function handleScroll() {
|
||||
if (isScrolling.value) return
|
||||
|
||||
const sections = ['amazon', 'rakuten', 'zebra', 'feedback']
|
||||
const scrollTop = settingsMainRef.value?.scrollTop || 0
|
||||
|
||||
for (const key of sections) {
|
||||
const element = document.getElementById(`section-${key}`)
|
||||
if (element) {
|
||||
const offsetTop = element.offsetTop - 50
|
||||
const offsetBottom = offsetTop + element.offsetHeight
|
||||
|
||||
if (scrollTop >= offsetTop && scrollTop < offsetBottom) {
|
||||
activeTab.value = key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取可用的日志日期列表
|
||||
async function loadLogDates() {
|
||||
try {
|
||||
const result = await (window as any).electronAPI.getLogDates()
|
||||
if (result && result.dates) {
|
||||
logDates.value = result.dates
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('获取日志日期列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交反馈
|
||||
async function submitFeedback() {
|
||||
if (!feedbackContent.value.trim()) {
|
||||
ElMessage.warning('请输入反馈内容')
|
||||
return
|
||||
}
|
||||
|
||||
const username = getUsernameFromToken()
|
||||
if (!username) {
|
||||
ElMessage.warning('请先登录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
feedbackSubmitting.value = true
|
||||
|
||||
const deviceId = await getOrCreateDeviceId()
|
||||
let logFile: File | undefined = undefined
|
||||
|
||||
// 如果选择了日志日期,读取日志文件
|
||||
if (selectedLogDate.value) {
|
||||
try {
|
||||
const result = await (window as any).electronAPI.readLogFile(selectedLogDate.value)
|
||||
if (result && result.content) {
|
||||
// 将日志内容转换为File对象
|
||||
const blob = new Blob([result.content], { type: 'text/plain' })
|
||||
logFile = new File([blob], `spring-boot-${selectedLogDate.value}.log`, { type: 'text/plain' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('读取日志文件失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
await feedbackApi.submit({
|
||||
username,
|
||||
deviceId,
|
||||
feedbackContent: feedbackContent.value,
|
||||
logDate: selectedLogDate.value || undefined,
|
||||
logFile
|
||||
})
|
||||
|
||||
ElMessage.success('反馈提交成功,感谢您的反馈!')
|
||||
feedbackContent.value = ''
|
||||
selectedLogDate.value = ''
|
||||
show.value = false
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '提交失败,请稍后重试')
|
||||
} finally {
|
||||
feedbackSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAllSettings()
|
||||
loadLogDates()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -99,62 +216,191 @@ onMounted(() => {
|
||||
<el-dialog
|
||||
v-model="show"
|
||||
title="应用设置"
|
||||
width="480px"
|
||||
width="720px"
|
||||
:close-on-click-modal="false"
|
||||
class="settings-dialog">
|
||||
|
||||
<div class="settings-content">
|
||||
<!-- 平台选择标签 -->
|
||||
<div class="platform-tabs">
|
||||
<div class="settings-layout">
|
||||
<!-- 左侧导航 -->
|
||||
<div class="settings-sidebar">
|
||||
<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>
|
||||
:class="['sidebar-item', { active: activeTab === platform.key }]"
|
||||
@click="scrollToSection(platform.key)">
|
||||
<span class="sidebar-icon">{{ platform.icon }}</span>
|
||||
<span class="sidebar-text">{{ platform.name }}</span>
|
||||
</div>
|
||||
<div
|
||||
:class="['sidebar-item', { active: activeTab === 'feedback' }]"
|
||||
@click="scrollToSection('feedback')">
|
||||
<span class="sidebar-icon">💬</span>
|
||||
<span class="sidebar-text">反馈</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 class="settings-main" ref="settingsMainRef" @scroll="handleScroll">
|
||||
<!-- Amazon 设置 -->
|
||||
<div id="section-amazon" class="setting-section">
|
||||
<div class="section-title">
|
||||
<span>Amazon 导出设置</span>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">默认导出路径</div>
|
||||
<div class="setting-desc">设置 Amazon Excel文件的默认保存位置</div>
|
||||
<div class="path-input-group">
|
||||
<el-input
|
||||
v-model="platformSettings.amazon.exportPath"
|
||||
placeholder="留空时自动弹出保存对话框"
|
||||
readonly
|
||||
class="path-input">
|
||||
<template #suffix>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="selectExportPath('amazon')"
|
||||
class="select-btn">
|
||||
浏览
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="resetPlatformSettings('amazon')">
|
||||
重置此平台
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="resetPlatformSettings(activeTab)">
|
||||
重置此平台
|
||||
</el-button>
|
||||
<!-- Rakuten 设置 -->
|
||||
<div id="section-rakuten" class="setting-section">
|
||||
<div class="section-title">
|
||||
<span>Rakuten 导出设置</span>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">默认导出路径</div>
|
||||
<div class="setting-desc">设置 Rakuten Excel文件的默认保存位置</div>
|
||||
<div class="path-input-group">
|
||||
<el-input
|
||||
v-model="platformSettings.rakuten.exportPath"
|
||||
placeholder="留空时自动弹出保存对话框"
|
||||
readonly
|
||||
class="path-input">
|
||||
<template #suffix>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="selectExportPath('rakuten')"
|
||||
class="select-btn">
|
||||
浏览
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="resetPlatformSettings('rakuten')">
|
||||
重置此平台
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zebra 设置 -->
|
||||
<div id="section-zebra" class="setting-section">
|
||||
<div class="section-title">
|
||||
<span>Zebra 导出设置</span>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">默认导出路径</div>
|
||||
<div class="setting-desc">设置 Zebra Excel文件的默认保存位置</div>
|
||||
<div class="path-input-group">
|
||||
<el-input
|
||||
v-model="platformSettings.zebra.exportPath"
|
||||
placeholder="留空时自动弹出保存对话框"
|
||||
readonly
|
||||
class="path-input">
|
||||
<template #suffix>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="selectExportPath('zebra')"
|
||||
class="select-btn">
|
||||
浏览
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="resetPlatformSettings('zebra')">
|
||||
重置此平台
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 反馈页面 -->
|
||||
<div id="section-feedback" class="setting-section">
|
||||
<div class="section-title">
|
||||
<span>用户反馈</span>
|
||||
</div>
|
||||
|
||||
<div class="feedback-form">
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">反馈内容 <span style="color: #F56C6C;">*</span></div>
|
||||
<div class="setting-desc">请描述您遇到的问题或提出您的建议</div>
|
||||
<el-input
|
||||
v-model="feedbackContent"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入您的反馈内容..."
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
style="margin-top: 8px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-label">附带日志(可选)</div>
|
||||
<div class="setting-desc">选择要附带的日志日期,有助于我们更快定位问题</div>
|
||||
<el-select
|
||||
v-model="selectedLogDate"
|
||||
placeholder="请选择日志日期(可选)"
|
||||
clearable
|
||||
style="width: 100%; margin-top: 8px;">
|
||||
<el-option
|
||||
v-for="date in logDates"
|
||||
:key="date"
|
||||
:label="date"
|
||||
:value="date"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="feedback-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="feedbackSubmitting"
|
||||
@click="submitFeedback">
|
||||
{{ feedbackSubmitting ? '提交中...' : '提交反馈' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -169,80 +415,102 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.settings-dialog :deep(.el-dialog__body) {
|
||||
padding: 0 20px 20px 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
max-height: 500px;
|
||||
.settings-layout {
|
||||
display: flex;
|
||||
min-height: 450px;
|
||||
max-height: 550px;
|
||||
}
|
||||
|
||||
/* 左侧导航 */
|
||||
.settings-sidebar {
|
||||
width: 160px;
|
||||
flex-shrink: 0;
|
||||
background: #FFFFFF;
|
||||
border-right: 1px solid #E5E6EB;
|
||||
padding: 16px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.platform-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
padding: 4px;
|
||||
background: #F8F9FA;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.platform-tab {
|
||||
flex: 1;
|
||||
.sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
margin: 0 8px 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: transparent;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.platform-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: var(--platform-color);
|
||||
.sidebar-item:hover {
|
||||
background: #F3F6FF;
|
||||
color: #165DFF;
|
||||
}
|
||||
|
||||
.platform-tab.active {
|
||||
background: #fff;
|
||||
color: var(--platform-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
.sidebar-item.active {
|
||||
background: #F3F6FF;
|
||||
color: #165DFF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.platform-icon {
|
||||
font-size: 16px;
|
||||
.sidebar-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 60%;
|
||||
background: #165DFF;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.platform-name {
|
||||
font-size: 12px;
|
||||
.sidebar-icon {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 右侧内容 */
|
||||
.settings-main {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
background: #F9FAFB;
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
margin-bottom: 24px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid #E5E6EB;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 18px;
|
||||
color: #1F2937;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.setting-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
@@ -258,18 +526,20 @@ onMounted(() => {
|
||||
.setting-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
color: #1F2937;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.setting-desc {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
color: #86909C;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.path-input-group {
|
||||
margin-top: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.path-input {
|
||||
@@ -277,7 +547,19 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.path-input :deep(.el-input__wrapper) {
|
||||
padding-right: 80px;
|
||||
padding-right: 90px;
|
||||
border-color: #E5E6EB;
|
||||
box-shadow: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.path-input :deep(.el-input__wrapper:hover) {
|
||||
border-color: #165DFF;
|
||||
}
|
||||
|
||||
.path-input :deep(.el-input__wrapper.is-focus) {
|
||||
border-color: #165DFF;
|
||||
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1);
|
||||
}
|
||||
|
||||
.select-btn {
|
||||
@@ -285,9 +567,16 @@ onMounted(() => {
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 24px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
height: 28px;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
background: #165DFF;
|
||||
border-color: #165DFF;
|
||||
}
|
||||
|
||||
.select-btn:hover {
|
||||
background: #4080FF;
|
||||
border-color: #4080FF;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
@@ -308,20 +597,119 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.setting-actions {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #EBEEF5;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #E5E6EB;
|
||||
}
|
||||
|
||||
.setting-actions :deep(.el-button) {
|
||||
border-color: #E5E6EB;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.setting-actions :deep(.el-button:hover) {
|
||||
border-color: #165DFF;
|
||||
color: #165DFF;
|
||||
}
|
||||
|
||||
.settings-dialog :deep(.el-dialog__header) {
|
||||
text-align: center;
|
||||
padding-right: 40px; /* 为右侧关闭按钮留出空间 */
|
||||
padding-right: 40px;
|
||||
border-bottom: 1px solid #E5E6EB;
|
||||
}
|
||||
|
||||
.settings-dialog :deep(.el-dialog__title) {
|
||||
font-weight: 600;
|
||||
color: #1F2937;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dialog-footer :deep(.el-button) {
|
||||
padding: 8px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dialog-footer :deep(.el-button--primary) {
|
||||
background: #165DFF;
|
||||
border-color: #165DFF;
|
||||
}
|
||||
|
||||
.dialog-footer :deep(.el-button--primary:hover) {
|
||||
background: #4080FF;
|
||||
border-color: #4080FF;
|
||||
}
|
||||
|
||||
.dialog-footer :deep(.el-button--default) {
|
||||
border-color: #E5E6EB;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.dialog-footer :deep(.el-button--default:hover) {
|
||||
border-color: #165DFF;
|
||||
color: #165DFF;
|
||||
background: #F3F6FF;
|
||||
}
|
||||
|
||||
.feedback-form {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.feedback-form :deep(.el-textarea__inner) {
|
||||
border-color: #E5E6EB;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.feedback-form :deep(.el-textarea__inner:hover) {
|
||||
border-color: #165DFF;
|
||||
}
|
||||
|
||||
.feedback-form :deep(.el-textarea__inner:focus) {
|
||||
border-color: #165DFF;
|
||||
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1);
|
||||
}
|
||||
|
||||
.feedback-form :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feedback-form :deep(.el-select .el-input__wrapper) {
|
||||
border-color: #E5E6EB;
|
||||
}
|
||||
|
||||
.feedback-form :deep(.el-select .el-input__wrapper:hover) {
|
||||
border-color: #165DFF;
|
||||
}
|
||||
|
||||
.feedback-form :deep(.el-select .el-input__wrapper.is-focus) {
|
||||
border-color: #165DFF;
|
||||
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1);
|
||||
}
|
||||
|
||||
.feedback-actions {
|
||||
margin-top: 24px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #E5E6EB;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.feedback-actions :deep(.el-button--primary) {
|
||||
background: #165DFF;
|
||||
border-color: #165DFF;
|
||||
padding: 9px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.feedback-actions :deep(.el-button--primary:hover) {
|
||||
background: #4080FF;
|
||||
border-color: #4080FF;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,29 +1,22 @@
|
||||
const BASE_CLIENT = 'http://localhost:8081'
|
||||
import { CONFIG } from '../api/http'
|
||||
|
||||
const DEVICE_ID_KEY = 'device_id'
|
||||
|
||||
// 从客户端服务获取硬件UUID(通过 wmic 命令)
|
||||
async function fetchDeviceIdFromClient(): Promise<string> {
|
||||
const response = await fetch(`${BASE_CLIENT}/api/device-id`, {
|
||||
const response = await fetch(`${CONFIG.CLIENT_BASE}/api/system/device-id`, {
|
||||
method: 'GET',
|
||||
credentials: 'omit',
|
||||
cache: 'no-store'
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取设备ID失败')
|
||||
}
|
||||
if (!response.ok) throw new Error('获取设备ID失败')
|
||||
|
||||
const result = await response.json()
|
||||
const deviceId = result?.data
|
||||
if (!result?.data) throw new Error('设备ID为空')
|
||||
|
||||
if (!deviceId) {
|
||||
throw new Error('设备ID为空')
|
||||
}
|
||||
|
||||
return deviceId
|
||||
return result.data
|
||||
}
|
||||
|
||||
// 获取或创建设备ID(优先读缓存,没有则从客户端服务获取硬件UUID)
|
||||
export async function getOrCreateDeviceId(): Promise<string> {
|
||||
const cached = localStorage.getItem(DEVICE_ID_KEY)
|
||||
if (cached) return cached
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
export async function convertImageToBase64ViaProxy(imageUrl: string, maxSize: number = 80): Promise<string | null> {
|
||||
if (!imageUrl) return null
|
||||
try {
|
||||
const proxyUrl = `http://127.0.0.1:8081/api/proxy/image-url?url=${encodeURIComponent(imageUrl)}`
|
||||
const proxyUrl = `http://127.0.0.1:8081/api/system/proxy/image?url=${encodeURIComponent(imageUrl)}`
|
||||
const response = await fetch(proxyUrl)
|
||||
if (!response.ok) return null
|
||||
|
||||
|
||||
37
electron-vue-template/src/renderer/utils/token.ts
Normal file
37
electron-vue-template/src/renderer/utils/token.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Token 工具函数
|
||||
export const TOKEN_KEY = 'auth_token';
|
||||
|
||||
export function getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function removeToken(): void {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function getUsernameFromToken(token?: string): string {
|
||||
try {
|
||||
const t = token || getToken();
|
||||
if (!t) return '';
|
||||
const payload = JSON.parse(atob(t.split('.')[1]));
|
||||
return payload.username || payload.sub || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function getClientIdFromToken(token?: string): string {
|
||||
try {
|
||||
const t = token || getToken();
|
||||
if (!t) return '';
|
||||
const payload = JSON.parse(atob(t.split('.')[1]));
|
||||
return payload.clientId || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user