feat(electron-vue-template):重构认证与设备管理模块

- 统一token存取逻辑,封装getToken/setToken/removeToken方法
-优化设备ID获取逻辑,调整API路径
- 完善设备管理接口类型定义,增强类型安全
- 调整SSE连接逻辑,使用统一配置管理- 重构HTTP客户端,集中管理后端服务配置
- 更新认证相关API接口,完善请求/响应类型
- 优化设备列表展示逻辑,移除冗余字段
- 调整图片代理路径,统一API前缀
- 完善用户反馈列表展示功能,增强交互体验
- 移除冗余的错误处理逻辑,简化代码结构
This commit is contained in:
2025-10-16 10:37:00 +08:00
parent 6f04658265
commit 132299c4b7
37 changed files with 2193 additions and 682 deletions

View File

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