diff --git a/electron-vue-template/src/main/main.ts b/electron-vue-template/src/main/main.ts index 40d45fd..ebade99 100644 --- a/electron-vue-template/src/main/main.ts +++ b/electron-vue-template/src/main/main.ts @@ -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 { return new Promise((resolve, reject) => { diff --git a/electron-vue-template/src/main/preload.ts b/electron-vue-template/src/main/preload.ts index ae932cd..3194283 100644 --- a/electron-vue-template/src/main/preload.ts +++ b/electron-vue-template/src/main/preload.ts @@ -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)) diff --git a/electron-vue-template/src/renderer/App.vue b/electron-vue-template/src/renderer/App.vue index a74310e..04e3705 100644 --- a/electron-vue-template/src/renderer/App.vue +++ b/electron-vue-template/src/renderer/App.vue @@ -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() diff --git a/electron-vue-template/src/renderer/api/amazon.ts b/electron-vue-template/src/renderer/api/amazon.ts index d4ec719..846562f 100644 --- a/electron-vue-template/src/renderer/api/amazon.ts +++ b/electron-vue-template/src/renderer/api/amazon.ts @@ -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'); }, }; diff --git a/electron-vue-template/src/renderer/api/auth.ts b/electron-vue-template/src/renderer/api/auth.ts index 61f291b..e404c1a 100644 --- a/electron-vue-template/src/renderer/api/auth.ts +++ b/electron-vue-template/src/renderer/api/auth.ts @@ -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 }) } } \ No newline at end of file diff --git a/electron-vue-template/src/renderer/api/device.ts b/electron-vue-template/src/renderer/api/device.ts index f113e34..104bd0b 100644 --- a/electron-vue-template/src/renderer/api/device.ts +++ b/electron-vue-template/src/renderer/api/device.ts @@ -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) } } \ No newline at end of file diff --git a/electron-vue-template/src/renderer/api/feedback.ts b/electron-vue-template/src/renderer/api/feedback.ts new file mode 100644 index 0000000..2544c35 --- /dev/null +++ b/electron-vue-template/src/renderer/api/feedback.ts @@ -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) + } +} diff --git a/electron-vue-template/src/renderer/api/http.ts b/electron-vue-template/src/renderer/api/http.ts index 4f553c0..492d790 100644 --- a/electron-vue-template/src/renderer/api/http.ts +++ b/electron-vue-template/src/renderer/api/http.ts @@ -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 { 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(path: string, options: RequestInit): Promise { const res = await fetch(`${resolveBase(path)}${path}`, { credentials: 'omit', @@ -34,22 +33,27 @@ async function request(path: string, options: RequestInit): Promise { ...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(`${path}${buildQuery(params)}`, { method: 'GET' }); }, post(path: string, body?: unknown) { - return request(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }); + return request(path, { + method: 'POST', + body: body ? JSON.stringify(body) : undefined + }); }, + delete(path: string) { return request(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(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; + 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; }); - }, + } }; diff --git a/electron-vue-template/src/renderer/api/update.ts b/electron-vue-template/src/renderer/api/update.ts index 9dafd8c..e796d73 100644 --- a/electron-vue-template/src/renderer/api/update.ts +++ b/electron-vue-template/src/renderer/api/update.ts @@ -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) { diff --git a/electron-vue-template/src/renderer/components/common/SettingsDialog.vue b/electron-vue-template/src/renderer/components/common/SettingsDialog.vue index 78f90e9..4b6a312 100644 --- a/electron-vue-template/src/renderer/components/common/SettingsDialog.vue +++ b/electron-vue-template/src/renderer/components/common/SettingsDialog.vue @@ -1,6 +1,6 @@ @@ -99,62 +216,191 @@ onMounted(() => { -
- -
+
+ +
- {{ platform.icon }} - {{ platform.name }} + :class="['sidebar-item', { active: activeTab === platform.key }]" + @click="scrollToSection(platform.key)"> + {{ platform.icon }} + {{ platform.name }} +
+
+ 💬 + 反馈
- -
-
- 📁 - {{ platforms.find(p => p.key === activeTab)?.name }} 导出设置 -
- -
-
默认导出路径
-
设置 {{ platforms.find(p => p.key === activeTab)?.name }} Excel文件的默认保存位置
-
- - - + +
+ +
+
+ Amazon 导出设置 +
+ +
+
默认导出路径
+
设置 Amazon Excel文件的默认保存位置
+
+ + + +
+
+ +
+ + 重置此平台 +
-
- - 重置此平台 - + +
+
+ Rakuten 导出设置 +
+ +
+
默认导出路径
+
设置 Rakuten Excel文件的默认保存位置
+
+ + + +
+
+ +
+ + 重置此平台 + +
+
+ + +
+
+ Zebra 导出设置 +
+ +
+
默认导出路径
+
设置 Zebra Excel文件的默认保存位置
+
+ + + +
+
+ +
+ + 重置此平台 + +
+
+ + +
+
+ 用户反馈 +
+ +
-
- - - - - - - + + + + + + + - + @@ -241,6 +241,111 @@
+ + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + + {{ currentFeedback.id }} + {{ currentFeedback.username }} + {{ currentFeedback.deviceId }} + + + {{ getFeedbackStatusText(currentFeedback.status) }} + + + + {{ currentFeedback.logDate || '未附带日志' }} + + {{ currentFeedback.createTime }} + +
{{ currentFeedback.feedbackContent }}
+
+ +
{{ currentFeedback.remark }}
+
+
+ +
+ + + +
+
+ + 刷新 + + + 下载日志 + + 日志日期: {{ currentFeedbackLog.logDate || '-' }} +
+
+
{{ feedbackLogContent }}
+
+
+
@@ -250,6 +355,7 @@ import LineChart from '@/components/Charts/LineChart' import PieChart from '@/components/Charts/PieChart' import { listInfo, listError, listData } from '@/api/monitor/client' import { getVersionInfo } from '@/api/monitor/version' +import { listFeedback, getFeedback, updateFeedbackStatus, delFeedback, getFeedbackLogContent, downloadFeedbackLog } from '@/api/monitor/feedback' import request from '@/utils/request' export default { @@ -321,7 +427,27 @@ export default { logLoading: false, currentLogClient: {}, logContent: '', - lastLogUpdate: '' + lastLogUpdate: '', + // 用户反馈 + feedbackDialogVisible: false, + feedbackLoading: false, + feedbackList: [], + feedbackQueryParams: { + pageNum: 1, + pageSize: 10 + }, + feedbackTotal: 0, + feedbackDetailDialogVisible: false, + currentFeedback: {}, + feedbackStats: { + pendingCount: 0, + todayCount: 0 + }, + // 反馈日志查看 + feedbackLogDialogVisible: false, + feedbackLogLoading: false, + feedbackLogContent: '', + currentFeedbackLog: {} } }, created() { @@ -329,6 +455,7 @@ export default { this.getOnlineClientTrend() this.getDataTypeDistribution() this.getClientList() + this.getFeedbackStatistics() }, methods: { // 获取统计数据 @@ -558,6 +685,186 @@ export default { console.error('获取版本信息失败:', error) this.$message.error('获取下载链接失败: ' + (error.message || '网络错误')) }) + }, + + // 获取反馈统计信息 + getFeedbackStatistics() { + request.get('/monitor/feedback/statistics').then(res => { + if (res.code === 200) { + this.feedbackStats = { + pendingCount: res.data.pendingCount || 0, + todayCount: res.data.todayCount || 0 + } + } + }).catch(error => { + console.error('获取反馈统计失败:', error) + }) + }, + + // 显示反馈列表 + showFeedbackList() { + this.feedbackDialogVisible = true + this.getFeedbackList() + }, + + // 获取反馈列表 + getFeedbackList() { + this.feedbackLoading = true + listFeedback(this.feedbackQueryParams).then(res => { + this.feedbackLoading = false + if (res.code === 200) { + this.feedbackList = res.rows || [] + this.feedbackTotal = res.total || 0 + } + }).catch(() => { + this.feedbackLoading = false + }) + }, + + // 查看反馈详情 + viewFeedbackDetail(row) { + this.currentFeedback = {...row} + this.feedbackDetailDialogVisible = true + }, + + // 查看反馈日志 + viewFeedbackLog(row) { + if (!row.logFilePath) { + this.$message.warning('该反馈未附带日志文件') + return + } + this.currentFeedbackLog = {...row} + this.feedbackLogDialogVisible = true + this.loadFeedbackLog(row.id) + }, + + // 加载反馈日志内容 + loadFeedbackLog(id) { + this.feedbackLogLoading = true + this.feedbackLogContent = '' + getFeedbackLogContent(id).then(res => { + this.feedbackLogLoading = false + if (res.code === 200) { + this.feedbackLogContent = res.data.content || '日志内容为空' + } else { + this.$message.error(res.msg || '获取日志失败') + this.feedbackLogContent = '获取日志失败' + } + }).catch(error => { + this.feedbackLogLoading = false + this.$message.error('获取日志失败: ' + (error.message || '未知错误')) + this.feedbackLogContent = '获取日志失败: ' + (error.message || '未知错误') + }) + }, + + // 下载反馈日志 + downloadFeedbackLogFile(row) { + if (!row.logFilePath) { + this.$message.warning('该反馈未附带日志文件') + return + } + // 使用request下载文件(带token) + const url = '/monitor/feedback/log/download/' + row.id + const loading = this.$loading({ + lock: true, + text: '正在下载日志文件...', + spinner: 'el-icon-loading', + background: 'rgba(0, 0, 0, 0.7)' + }) + + request({ + url: url, + method: 'get', + responseType: 'blob' + }).then(response => { + const blob = new Blob([response]) + const link = document.createElement('a') + link.href = window.URL.createObjectURL(blob) + link.download = `feedback_${row.id}_log.txt` + link.click() + window.URL.revokeObjectURL(link.href) + loading.close() + this.$message.success('下载成功') + }).catch(error => { + loading.close() + console.error('下载失败:', error) + this.$message.error('下载失败: ' + (error.message || '请稍后重试')) + }) + }, + + // 更新反馈状态 + handleFeedbackStatus(row, status) { + this.$prompt('请输入处理备注(可选)', '更新状态', { + confirmButtonText: '确定', + cancelButtonText: '取消', + inputType: 'textarea' + }).then(({ value }) => { + updateFeedbackStatus(row.id, { + status: status, + remark: value || '' + }).then(res => { + if (res.code === 200) { + this.$message.success('状态更新成功') + this.getFeedbackList() + this.getFeedbackStatistics() + } else { + this.$message.error(res.msg || '更新失败') + } + }).catch(error => { + this.$message.error('更新失败: ' + (error.message || '未知错误')) + }) + }).catch(() => {}) + }, + + // 删除反馈 + deleteFeedback(row) { + this.$confirm('确定要删除该反馈吗?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + delFeedback(row.id).then(res => { + if (res.code === 200) { + this.$message.success('删除成功') + this.getFeedbackList() + this.getFeedbackStatistics() + } else { + this.$message.error(res.msg || '删除失败') + } + }).catch(error => { + this.$message.error('删除失败: ' + (error.message || '未知错误')) + }) + }).catch(() => {}) + }, + + // 反馈分页 + handleFeedbackSizeChange(val) { + this.feedbackQueryParams.pageSize = val + this.getFeedbackList() + }, + handleFeedbackCurrentChange(val) { + this.feedbackQueryParams.pageNum = val + this.getFeedbackList() + }, + + // 获取状态标签类型 + getFeedbackStatusType(status) { + const typeMap = { + 'pending': 'warning', + 'processing': 'primary', + 'completed': 'success' + } + return typeMap[status] || 'info' + }, + + // 获取状态文本 + getFeedbackStatusText(status) { + const textMap = { + 'pending': '待处理', + 'processing': '处理中', + 'completed': '已完成' + } + return textMap[status] || status } } }