- 修改AmazonController以支持按用户和区域筛选最新会话数据 - 更新AmazonProductRepository中的findLatestProducts方法,增加region参数实现数据隔离 -优化AmazonScrapingServiceImpl的数据处理流程,增强缓存清理机制 - 调整GenmaiServiceImpl的token验证逻辑并改进Chrome启动配置- 升级系统版本至2.5.5并完善相关依赖管理 - 改进前端设置对话框中关于缓存清理描述的信息准确性 -重构SystemController接口,移除不必要的用户名参数传递- 强化GenmaiAccountController和服务层的安全校验逻辑
1189 lines
30 KiB
Vue
1189 lines
30 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
getSettings,
|
||
saveSettings,
|
||
savePlatformSettings,
|
||
type Platform,
|
||
type PlatformExportSettings
|
||
} from '../../utils/settings'
|
||
import { feedbackApi } from '../../api/feedback'
|
||
import { getToken, getUsernameFromToken } from '../../utils/token'
|
||
import { getOrCreateDeviceId } from '../../utils/deviceId'
|
||
import { updateApi } from '../../api/update'
|
||
|
||
interface Props {
|
||
modelValue: boolean
|
||
}
|
||
|
||
interface Emits {
|
||
(e: 'update:modelValue', value: boolean): void
|
||
(e: 'autoUpdateChanged', value: boolean): void
|
||
(e: 'openUpdateDialog'): 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<string>('export')
|
||
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 closeAction = ref<'quit' | 'minimize' | 'tray'>('quit')
|
||
|
||
// 缓存相关
|
||
const clearingCache = ref(false)
|
||
|
||
// 启动配置
|
||
const autoLaunch = ref(false)
|
||
const launchMinimized = ref(false)
|
||
|
||
// 更新相关
|
||
const currentVersion = ref('')
|
||
const latestVersion = ref('')
|
||
const updateNotes = ref('')
|
||
const checkingUpdate = ref(false)
|
||
const hasUpdate = ref(false)
|
||
const autoUpdate = ref(false)
|
||
|
||
const show = computed({
|
||
get: () => props.modelValue,
|
||
set: (value) => emit('update:modelValue', value)
|
||
})
|
||
|
||
// 监听对话框打开,重新加载当前用户的设置
|
||
watch(() => props.modelValue, (newVal) => {
|
||
if (newVal) {
|
||
loadAllSettings()
|
||
loadCurrentVersion()
|
||
}
|
||
})
|
||
|
||
// 选择导出路径
|
||
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]
|
||
}
|
||
}
|
||
|
||
// 保存设置
|
||
async function saveAllSettings() {
|
||
const username = getUsernameFromToken()
|
||
const oldSettings = getSettings(username)
|
||
const autoUpdateChanged = oldSettings.autoUpdate !== autoUpdate.value
|
||
|
||
// 1. 保存到 localStorage(按账号隔离)
|
||
saveSettings({
|
||
platforms: platformSettings.value,
|
||
autoUpdate: autoUpdate.value,
|
||
closeAction: closeAction.value,
|
||
autoLaunch: autoLaunch.value,
|
||
launchMinimized: launchMinimized.value
|
||
}, username)
|
||
|
||
// 2. 同步到 Electron 主进程(控制应用行为)
|
||
await (window as any).electronAPI.setCloseAction(closeAction.value)
|
||
await (window as any).electronAPI.setLaunchConfig({
|
||
autoLaunch: autoLaunch.value,
|
||
launchMinimized: launchMinimized.value
|
||
})
|
||
|
||
ElMessage({ message: '设置已保存', type: 'success' })
|
||
show.value = false
|
||
|
||
if (autoUpdateChanged) {
|
||
emit('autoUpdateChanged', autoUpdate.value)
|
||
}
|
||
}
|
||
|
||
// 加载设置
|
||
function loadAllSettings() {
|
||
const username = getUsernameFromToken()
|
||
const settings = getSettings(username)
|
||
platformSettings.value = {
|
||
amazon: { ...settings.platforms.amazon },
|
||
rakuten: { ...settings.platforms.rakuten },
|
||
zebra: { ...settings.platforms.zebra }
|
||
}
|
||
autoUpdate.value = settings.autoUpdate ?? false
|
||
closeAction.value = settings.closeAction ?? 'quit'
|
||
autoLaunch.value = settings.autoLaunch ?? false
|
||
launchMinimized.value = settings.launchMinimized ?? false
|
||
}
|
||
|
||
// 重置单个平台设置
|
||
function resetPlatformSettings(platform: Platform) {
|
||
platformSettings.value[platform] = {
|
||
exportPath: ''
|
||
}
|
||
}
|
||
|
||
// 重置所有设置
|
||
async function resetAllSettings() {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'确定要重置所有设置吗?此操作不可恢复。',
|
||
'确认重置',
|
||
{
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}
|
||
)
|
||
|
||
platforms.forEach(platform => {
|
||
resetPlatformSettings(platform.key)
|
||
})
|
||
|
||
autoUpdate.value = false
|
||
closeAction.value = 'quit'
|
||
autoLaunch.value = false
|
||
launchMinimized.value = false
|
||
|
||
ElMessage.success('所有设置已重置')
|
||
} catch {
|
||
// 用户取消操作
|
||
}
|
||
}
|
||
|
||
// 清理缓存
|
||
async function handleClearCache() {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'确定要清理客户端缓存吗?将清除本地设备的所有缓存数据、更新文件及相关状态(不影响登录状态)',
|
||
'确认清理',
|
||
{
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}
|
||
)
|
||
|
||
clearingCache.value = true
|
||
|
||
// 1. 清理后端数据库缓存
|
||
const result = await (window as any).electronAPI.clearCache()
|
||
if (!result || result.code !== 0) {
|
||
ElMessage.error('缓存清理失败')
|
||
return
|
||
}
|
||
|
||
// 2. 清除更新相关的 localStorage 状态
|
||
localStorage.removeItem('skipped_version')
|
||
localStorage.removeItem('remind_later_time')
|
||
|
||
// 3. 清除已下载的更新文件
|
||
await (window as any).electronAPI.clearUpdateFiles()
|
||
|
||
ElMessage.success('缓存清理成功')
|
||
} catch (error: any) {
|
||
if (error !== 'cancel') {
|
||
ElMessage.error('缓存清理失败')
|
||
}
|
||
} finally {
|
||
clearingCache.value = false
|
||
}
|
||
}
|
||
|
||
// 滚动到指定区域
|
||
function scrollToSection(sectionKey: string) {
|
||
if (isScrolling.value) return
|
||
|
||
const element = document.getElementById(`section-${sectionKey}`)
|
||
if (element && settingsMainRef.value) {
|
||
isScrolling.value = true
|
||
activeTab.value = sectionKey
|
||
|
||
const container = settingsMainRef.value
|
||
// 计算元素相对于容器的位置
|
||
const containerRect = container.getBoundingClientRect()
|
||
const elementRect = element.getBoundingClientRect()
|
||
const relativeTop = elementRect.top - containerRect.top
|
||
const targetTop = container.scrollTop + relativeTop - 14 // 14是容器的padding
|
||
const startTop = container.scrollTop
|
||
const distance = targetTop - startTop
|
||
const duration = 800 // 增加持续时间,使滚动更慢更平滑
|
||
let start: number | null = null
|
||
|
||
function animation(currentTime: number) {
|
||
if (start === null) start = currentTime
|
||
const timeElapsed = currentTime - start
|
||
const progress = Math.min(timeElapsed / duration, 1)
|
||
|
||
// 使用 easeInOutCubic 缓动函数
|
||
const easing = progress < 0.5
|
||
? 4 * progress * progress * progress
|
||
: 1 - Math.pow(-2 * progress + 2, 3) / 2
|
||
|
||
container.scrollTop = startTop + distance * easing
|
||
|
||
if (progress < 1) {
|
||
requestAnimationFrame(animation)
|
||
} else {
|
||
isScrolling.value = false
|
||
}
|
||
}
|
||
|
||
requestAnimationFrame(animation)
|
||
}
|
||
}
|
||
|
||
|
||
// 获取可用的日志日期列表
|
||
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 checkForUpdates() {
|
||
try {
|
||
checkingUpdate.value = true
|
||
currentVersion.value = await (window as any).electronAPI.getJarVersion()
|
||
const checkRes: any = await updateApi.checkUpdate(currentVersion.value)
|
||
const result = checkRes?.data || checkRes
|
||
|
||
if (!result.needUpdate) {
|
||
hasUpdate.value = false
|
||
updateNotes.value = ''
|
||
ElMessage.success('当前已是最新版本')
|
||
} else {
|
||
hasUpdate.value = true
|
||
latestVersion.value = result.latestVersion
|
||
updateNotes.value = result.updateNotes || ''
|
||
ElMessage.success('发现新版本:' + result.latestVersion)
|
||
}
|
||
} catch (error: any) {
|
||
ElMessage.error('检查更新失败:' + (error?.message || '网络错误'))
|
||
} finally {
|
||
checkingUpdate.value = false
|
||
}
|
||
}
|
||
|
||
// 处理立即升级按钮点击
|
||
function handleUpgradeClick() {
|
||
show.value = false
|
||
emit('openUpdateDialog')
|
||
}
|
||
|
||
// 加载当前版本
|
||
async function loadCurrentVersion() {
|
||
try {
|
||
currentVersion.value = await (window as any).electronAPI.getJarVersion()
|
||
} 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()
|
||
loadCurrentVersion()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<Teleport to="body">
|
||
<Transition name="dialog-fade">
|
||
<div v-if="show" class="dialog-mask" @click="show = false">
|
||
<div class="dialog-container" @click.stop>
|
||
<div class="settings-layout">
|
||
<!-- 左侧导航 -->
|
||
<div class="settings-sidebar">
|
||
<div class="sidebar-header">应用设置</div>
|
||
<div
|
||
:class="['sidebar-item', { active: activeTab === 'export' }]"
|
||
@click="scrollToSection('export')">
|
||
<span class="sidebar-icon">📤</span>
|
||
<span class="sidebar-text">导出</span>
|
||
</div>
|
||
<div
|
||
:class="['sidebar-item', { active: activeTab === 'update' }]"
|
||
@click="scrollToSection('update')">
|
||
<span class="sidebar-icon">🔄</span>
|
||
<span class="sidebar-text">更新</span>
|
||
</div>
|
||
<div
|
||
:class="['sidebar-item', { active: activeTab === 'cache' }]"
|
||
@click="scrollToSection('cache')">
|
||
<span class="sidebar-icon">💾</span>
|
||
<span class="sidebar-text">缓存</span>
|
||
</div>
|
||
<div
|
||
:class="['sidebar-item', { active: activeTab === 'startup' }]"
|
||
@click="scrollToSection('startup')">
|
||
<span class="sidebar-icon">🚀</span>
|
||
<span class="sidebar-text">启动</span>
|
||
</div>
|
||
<div
|
||
:class="['sidebar-item', { active: activeTab === 'feedback' }]"
|
||
@click="scrollToSection('feedback')">
|
||
<span class="sidebar-icon">💬</span>
|
||
<span class="sidebar-text">反馈</span>
|
||
</div>
|
||
<div
|
||
:class="['sidebar-item', { active: activeTab === 'general' }]"
|
||
@click="scrollToSection('general')">
|
||
<span class="sidebar-icon">⚙️</span>
|
||
<span class="sidebar-text">通用</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧内容区域 -->
|
||
<div class="settings-content">
|
||
<!-- 滚动内容 -->
|
||
<div class="settings-main" ref="settingsMainRef">
|
||
<!-- 导出设置 -->
|
||
<div id="section-export" class="setting-section export-section" @mouseenter="activeTab = 'export'">
|
||
<div class="section-title">导出设置</div>
|
||
<div class="section-subtitle-text">配置各平台默认导出路径</div>
|
||
|
||
<!-- 遍历显示所有平台 -->
|
||
<div
|
||
v-for="platform in platforms"
|
||
:key="platform.key"
|
||
class="platform-item">
|
||
<div class="platform-header">
|
||
<span class="platform-icon">{{ platform.icon }}</span>
|
||
<span class="platform-name">{{ platform.name }}</span>
|
||
</div>
|
||
<div class="input-button-row">
|
||
<el-input
|
||
v-model="platformSettings[platform.key].exportPath"
|
||
placeholder="留空时自动弹出保存对话框"
|
||
readonly
|
||
class="path-input">
|
||
</el-input>
|
||
<el-button
|
||
size="small"
|
||
type="primary"
|
||
@click="selectExportPath(platform.key)">
|
||
浏览
|
||
</el-button>
|
||
<el-button
|
||
size="small"
|
||
style="margin-left: 3px"
|
||
@click="resetPlatformSettings(platform.key)">
|
||
重置
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 更新设置 -->
|
||
<div id="section-update" class="setting-section" @mouseenter="activeTab = 'update'">
|
||
<div class="section-header">
|
||
<div class="section-title">软件更新</div>
|
||
</div>
|
||
|
||
<div class="setting-item">
|
||
<div class="update-info-row">
|
||
<div class="version-display">
|
||
<span class="version-label">{{ hasUpdate ? '发现新版本:' : '当前版本:' }}</span>
|
||
<span class="version-value">{{ hasUpdate ? latestVersion : (currentVersion || '-') }}</span>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
:loading="checkingUpdate"
|
||
@click="hasUpdate ? handleUpgradeClick() : checkForUpdates()">
|
||
{{ checkingUpdate ? '检查中...' : (hasUpdate ? '立即升级' : '检查更新') }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="setting-item" style="margin-top: 10px;">
|
||
<div class="checkbox-row">
|
||
<el-checkbox v-model="autoUpdate" size="default">
|
||
自动安装更新
|
||
</el-checkbox>
|
||
</div>
|
||
<div class="setting-desc" style="margin-top: 4px;">
|
||
如果自动安装,将仅会安装到新版本后,但需手动点击"重启升级"后才可升级
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="hasUpdate" class="setting-item" style="margin-top: 10px;">
|
||
<div class="update-notes-section">
|
||
<h4 class="notes-title">更新详情</h4>
|
||
<div class="notes-content">{{ updateNotes }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 缓存设置 -->
|
||
<div id="section-cache" class="setting-section" @mouseenter="activeTab = 'cache'">
|
||
<div class="section-title">缓存设置</div>
|
||
|
||
<div class="setting-item">
|
||
<div class="setting-row">
|
||
<span class="cache-desc">清理本地设备的所有缓存数据,不影响登录状态</span>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
:loading="clearingCache"
|
||
:disabled="clearingCache"
|
||
@click="handleClearCache">
|
||
{{ clearingCache ? '清理中' : '清理缓存' }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 启动设置 -->
|
||
<div id="section-startup" class="setting-section" @mouseenter="activeTab = 'startup'">
|
||
<div class="section-title">启动</div>
|
||
|
||
<div class="setting-item">
|
||
<div class="checkbox-row">
|
||
<el-checkbox v-model="autoLaunch" size="default">
|
||
开机时自动启动
|
||
</el-checkbox>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="setting-item" style="margin-top: 10px;">
|
||
<div class="checkbox-row">
|
||
<el-checkbox v-model="launchMinimized" size="default">
|
||
启动后最小化到程序坞
|
||
</el-checkbox>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 反馈页面 -->
|
||
<div id="section-feedback" class="setting-section" @mouseenter="activeTab = 'feedback'">
|
||
<div class="section-title">反馈</div>
|
||
|
||
<div class="feedback-form">
|
||
<div class="setting-item">
|
||
<el-input
|
||
v-model="feedbackContent"
|
||
type="textarea"
|
||
:rows="5"
|
||
placeholder="描述问题或建议..."
|
||
maxlength="1000"
|
||
show-word-limit
|
||
/>
|
||
</div>
|
||
|
||
<div class="setting-item">
|
||
<div class="input-button-row">
|
||
<el-select
|
||
v-model="selectedLogDate"
|
||
placeholder="附带日志(可选)"
|
||
clearable
|
||
class="path-input">
|
||
<el-option
|
||
v-for="date in logDates"
|
||
:key="date"
|
||
:label="date"
|
||
:value="date"
|
||
/>
|
||
</el-select>
|
||
<el-button
|
||
type="primary"
|
||
:loading="feedbackSubmitting"
|
||
@click="submitFeedback">
|
||
{{ feedbackSubmitting ? '提交中' : '提交' }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 通用设置 -->
|
||
<div id="section-general" class="setting-section" @mouseenter="activeTab = 'general'">
|
||
<div class="section-header">
|
||
<div class="section-title">关闭</div>
|
||
<div class="section-subtitle">当关闭应用程序时</div>
|
||
</div>
|
||
|
||
<div class="setting-item">
|
||
<el-radio-group v-model="closeAction" class="compact-radio-group">
|
||
<el-radio value="tray" size="default">在后台运行此程序</el-radio>
|
||
<el-radio value="quit" size="default">完全退出此程序</el-radio>
|
||
</el-radio-group>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部按钮 -->
|
||
<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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* 对话框遮罩 */
|
||
.dialog-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2000;
|
||
}
|
||
|
||
/* 对话框容器 */
|
||
.dialog-container {
|
||
width: 540px;
|
||
background: #FFFFFF;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: 80vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 对话框动画 */
|
||
.dialog-fade-enter-active,
|
||
.dialog-fade-leave-active {
|
||
transition: opacity 0.2s ease;
|
||
}
|
||
|
||
.dialog-fade-enter-active .dialog-container,
|
||
.dialog-fade-leave-active .dialog-container {
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.dialog-fade-enter-from,
|
||
.dialog-fade-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
.dialog-fade-enter-from .dialog-container,
|
||
.dialog-fade-leave-to .dialog-container {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.settings-layout {
|
||
display: flex;
|
||
height: 100%;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 左侧导航 */
|
||
.settings-sidebar {
|
||
width: 130px;
|
||
flex-shrink: 0;
|
||
background: #FFFFFF;
|
||
border-right: 1px solid #E5E6EB;
|
||
padding: 0;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 12px 12px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1F2937;
|
||
border-bottom: 1px solid #E5E6EB;
|
||
text-align: left;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.settings-sidebar::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.settings-sidebar::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.settings-sidebar::-webkit-scrollbar-thumb {
|
||
background: #C9CDD4;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.settings-sidebar::-webkit-scrollbar-thumb:hover {
|
||
background: #86909C;
|
||
}
|
||
|
||
.sidebar-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
margin: 0;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
color: #6B7280;
|
||
font-size: 13px;
|
||
user-select: none;
|
||
position: relative;
|
||
}
|
||
|
||
.sidebar-item:first-of-type {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.sidebar-item:hover {
|
||
background: #F3F6FF;
|
||
color: #165DFF;
|
||
}
|
||
|
||
.sidebar-item.active {
|
||
background: #F3F6FF;
|
||
color: #165DFF;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.sidebar-item.active::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 3px;
|
||
height: 100%;
|
||
background: #165DFF;
|
||
}
|
||
|
||
.sidebar-icon {
|
||
font-size: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sidebar-text {
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* 右侧内容区域 */
|
||
.settings-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 滚动内容区 */
|
||
.settings-main {
|
||
flex: 1;
|
||
padding: 12px;
|
||
overflow-y: auto;
|
||
background: #F9FAFB;
|
||
}
|
||
|
||
.settings-main::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.settings-main::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.settings-main::-webkit-scrollbar-thumb {
|
||
background: #C9CDD4;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.settings-main::-webkit-scrollbar-thumb:hover {
|
||
background: #86909C;
|
||
}
|
||
|
||
.setting-section {
|
||
background: #FFFFFF;
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
margin-bottom: 10px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||
border: 1px solid #E5E6EB;
|
||
}
|
||
|
||
.export-section {
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1F2937;
|
||
text-align: left;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.section-header .section-title {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.section-subtitle {
|
||
font-size: 11px;
|
||
color: #86909C;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.section-subtitle-text {
|
||
font-size: 12px;
|
||
color: #86909C;
|
||
margin-bottom: 8px;
|
||
text-align: left;
|
||
}
|
||
|
||
.cache-desc {
|
||
font-size: 13px;
|
||
color: #86909C;
|
||
}
|
||
|
||
.setting-item {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.setting-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.setting-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.setting-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.setting-label {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #1F2937;
|
||
margin-bottom: 6px;
|
||
display: block;
|
||
text-align: left;
|
||
}
|
||
|
||
.setting-desc {
|
||
font-size: 12px;
|
||
color: #86909C;
|
||
line-height: 1.5;
|
||
margin-bottom: 8px;
|
||
text-align: left;
|
||
}
|
||
|
||
.input-button-row {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.path-input {
|
||
flex: 1;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.path-input :deep(.el-input__wrapper) {
|
||
border-color: #E5E6EB;
|
||
box-shadow: none;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.path-input :deep(.el-input__inner) {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.input-button-row :deep(.el-button) {
|
||
padding: 6px 16px;
|
||
font-size: 13px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
|
||
|
||
.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;
|
||
}
|
||
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
padding: 5px 14px;
|
||
border-top: 1px solid #E5E6EB;
|
||
background: #FFFFFF;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.dialog-footer :deep(.el-button) {
|
||
padding: 5px 14px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.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 .setting-item + .setting-item {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.feedback-form :deep(.el-textarea__inner) {
|
||
border-color: #E5E6EB;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
.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__inner) {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.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-form .input-button-row :deep(.el-button) {
|
||
height: 32px;
|
||
padding: 0 16px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.feedback-form .input-button-row :deep(.el-button--primary) {
|
||
background: #165DFF;
|
||
border-color: #165DFF;
|
||
}
|
||
|
||
.feedback-form .input-button-row :deep(.el-button--primary:hover) {
|
||
background: #4080FF;
|
||
border-color: #4080FF;
|
||
}
|
||
|
||
/* 单选框组 */
|
||
.compact-radio-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.compact-radio-group :deep(.el-radio) {
|
||
margin-right: 0;
|
||
height: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.compact-radio-group :deep(.el-radio__label) {
|
||
font-size: 13px;
|
||
color: #1F2937;
|
||
padding-left: 8px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.compact-radio-group :deep(.el-radio__inner) {
|
||
width: 16px;
|
||
height: 16px;
|
||
border-width: 1px;
|
||
}
|
||
|
||
.compact-radio-group :deep(.el-radio__input) {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.compact-radio-group :deep(.el-radio__input.is-checked .el-radio__inner) {
|
||
background: #165DFF;
|
||
border-color: #165DFF;
|
||
border-width: 1px;
|
||
}
|
||
|
||
.compact-radio-group :deep(.el-radio__inner::after) {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
.compact-radio-group :deep(.el-radio__input.is-checked + .el-radio__label) {
|
||
color: #165DFF;
|
||
}
|
||
|
||
/* 更新相关样式 */
|
||
.update-info-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.version-display {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.version-label {
|
||
font-size: 13px;
|
||
color: #6B7280;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.version-value {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #165DFF;
|
||
flex: 1;
|
||
}
|
||
|
||
.version-display :deep(.el-button) {
|
||
padding: 6px 16px;
|
||
font-size: 13px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.checkbox-row {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.checkbox-row :deep(.el-checkbox) {
|
||
height: auto;
|
||
}
|
||
|
||
.checkbox-row :deep(.el-checkbox__label) {
|
||
font-size: 13px;
|
||
color: #1F2937;
|
||
padding-left: 8px;
|
||
}
|
||
|
||
.checkbox-row :deep(.el-checkbox__inner) {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
.checkbox-row :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||
background: #165DFF;
|
||
border-color: #165DFF;
|
||
}
|
||
|
||
.update-notes-section {
|
||
background: #F8F9FA;
|
||
border-radius: 6px;
|
||
padding: 10px;
|
||
}
|
||
|
||
.notes-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
margin: 0 0 6px 0;
|
||
}
|
||
|
||
.notes-content {
|
||
font-size: 12px;
|
||
color: #6B7280;
|
||
line-height: 1.6;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
/* 平台项样式 */
|
||
.platform-item {
|
||
margin-bottom: 8px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #E5E6EB;
|
||
}
|
||
|
||
.platform-item:last-child {
|
||
margin-bottom: 0;
|
||
padding-bottom: 0;
|
||
border-bottom: none;
|
||
}
|
||
|
||
.platform-item:first-child {
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.platform-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.platform-icon {
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.platform-name {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #1F2937;
|
||
}
|
||
</style>
|
||
|
||
<script lang="ts">
|
||
export default {
|
||
name: 'SettingsDialog'
|
||
}
|
||
</script>
|