Files
map-client-vue/web/src/components/DisplaySettings.vue
2025-10-14 21:52:11 +08:00

635 lines
16 KiB
Vue

<template>
<div class="display-settings-page">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-info">
<h1>显示设置</h1>
<p>自定义应用外观和用户体验</p>
</div>
</div>
<!-- 设置内容 -->
<div class="settings-content">
<!-- 主题 -->
<n-card title="主题" size="large">
<div class="setting-section">
<div class="setting-item">
<div class="setting-label">
<span class="label-text">主题模式</span>
<span class="label-desc">选择应用的主题风格</span>
</div>
<div class="setting-control">
<n-radio-group v-model:value="displaySettings.theme" name="theme">
<n-radio-button value="light">
<div class="theme-option">
<n-icon :component="SunIcon" />
<span>浅色</span>
</div>
</n-radio-button>
<n-radio-button value="dark">
<div class="theme-option">
<n-icon :component="MoonIcon" />
<span>深色</span>
</div>
</n-radio-button>
<n-radio-button value="system">
<div class="theme-option">
<n-icon :component="DeviceDesktopIcon" />
<span>系统</span>
</div>
</n-radio-button>
</n-radio-group>
</div>
</div>
<!-- 主题颜色 -->
<div class="setting-item">
<div class="setting-label">
<span class="label-text">主题颜色</span>
</div>
<div class="setting-control">
<div class="color-picker-grid">
<div
v-for="color in themeColors"
:key="color"
class="color-option"
:class="{ active: displaySettings.primaryColor === color }"
:style="{ backgroundColor: color }"
@click="displaySettings.primaryColor = color"
>
<n-icon v-if="displaySettings.primaryColor === color" :component="CheckIcon" color="white" />
</div>
<!-- 自定义颜色输入 -->
<n-input
v-model:value="displaySettings.primaryColor"
placeholder="#00B96B"
size="small"
style="width: 80px; margin-left: 8px;"
/>
</div>
</div>
</div>
<!-- 透明窗口 -->
<div class="setting-item">
<div class="setting-label">
<span class="label-text">透明窗口</span>
</div>
<div class="setting-control">
<n-switch v-model:value="displaySettings.transparentWindow" />
</div>
</div>
</div>
</n-card>
<!-- 导航栏设置 -->
<n-card title="导航栏设置" size="large">
<div class="setting-section">
<div class="setting-item">
<div class="setting-label">
<span class="label-text">导航位置</span>
</div>
<div class="setting-control">
<n-radio-group v-model:value="displaySettings.navPosition" name="navPosition">
<n-radio value="left">左侧</n-radio>
<n-radio value="top">顶部</n-radio>
</n-radio-group>
</div>
</div>
</div>
</n-card>
<!-- 缩放设置 -->
<n-card title="缩放设置" size="large">
<div class="setting-section">
<div class="setting-item">
<div class="setting-label">
<span class="label-text">缩放</span>
</div>
<div class="setting-control">
<div class="zoom-controls">
<n-button size="small" @click="decreaseZoom">-</n-button>
<span class="zoom-value">{{ displaySettings.zoomLevel }}%</span>
<n-button size="small" @click="increaseZoom">+</n-button>
<n-button size="small" @click="resetZoom">
<n-icon :component="RefreshIcon" />
</n-button>
</div>
</div>
</div>
</div>
</n-card>
<!-- 字体设置 -->
<n-card title="字体设置" size="large">
<div class="setting-section">
<div class="setting-item">
<div class="setting-label">
<span class="label-text">全局字体</span>
</div>
<div class="setting-control">
<n-select
v-model:value="displaySettings.globalFont"
:options="globalFontOptions"
style="width: 200px"
/>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<span class="label-text">代码字体</span>
</div>
<div class="setting-control">
<n-select
v-model:value="displaySettings.codeFont"
:options="codeFontOptions"
style="width: 200px"
/>
</div>
</div>
</div>
</n-card>
<!-- 话题设置 -->
<n-card title="话题设置" size="large">
<div class="setting-section">
<div class="setting-item">
<div class="setting-label">
<span class="label-text">话题位置</span>
</div>
<div class="setting-control">
<n-radio-group v-model:value="displaySettings.topicPosition" name="topicPosition">
<n-radio value="left">左侧</n-radio>
<n-radio value="right">右侧</n-radio>
</n-radio-group>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<span class="label-text">自动切换到话题</span>
</div>
<div class="setting-control">
<n-switch v-model:value="displaySettings.autoSwitchTopic" />
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<span class="label-text">显示话题时间</span>
</div>
<div class="setting-control">
<n-switch v-model:value="displaySettings.showTopicTime" />
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<span class="label-text">固定话题置顶</span>
</div>
<div class="setting-control">
<n-switch v-model:value="displaySettings.pinnedTopicsTop" />
</div>
</div>
</div>
</n-card>
<!-- 助手设置 -->
<n-card title="助手设置" size="large">
<div class="setting-section">
<div class="setting-item">
<div class="setting-label">
<span class="label-text">模型图标类型</span>
</div>
<div class="setting-control">
<n-radio-group v-model:value="displaySettings.modelIconType" name="modelIconType">
<n-radio value="modelIcon">模型图标</n-radio>
<n-radio value="emoji">Emoji 表情</n-radio>
<n-radio value="none">不显示</n-radio>
</n-radio-group>
</div>
</div>
</div>
</n-card>
<!-- 自定义 CSS -->
<n-card title="自定义 CSS" size="large">
<div class="setting-section">
<div class="css-editor">
<div class="css-header">
<span>/* 这里写自定义 CSS */</span>
<a href="#" target="_blank" class="css-help"> cherryCss.com 获取</a>
</div>
<n-input
v-model:value="displaySettings.customCSS"
type="textarea"
:rows="10"
placeholder="/* 在此添加您的自定义CSS样式 */"
/>
</div>
</div>
</n-card>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, watch, onMounted } from 'vue'
import {
NCard,
NRadioGroup,
NRadioButton,
NRadio,
NSelect,
NSwitch,
NButton,
NIcon,
NInput,
useMessage
} from 'naive-ui'
import {
Sun as SunIcon,
Moon as MoonIcon,
DeviceDesktop as DeviceDesktopIcon,
Check as CheckIcon,
Refresh as RefreshIcon
} from '@vicons/tabler'
const message = useMessage()
// 显示设置数据
const displaySettings = reactive({
// 主题设置
theme: 'light' as 'light' | 'dark' | 'system',
primaryColor: '#00B96B',
transparentWindow: true,
// 导航栏设置
navPosition: 'left' as 'left' | 'top',
// 缩放设置
zoomLevel: 100,
// 字体设置
globalFont: 'default',
codeFont: 'default',
// 话题设置
topicPosition: 'left' as 'left' | 'right',
autoSwitchTopic: true,
showTopicTime: false,
pinnedTopicsTop: false,
// 助手设置
modelIconType: 'modelIcon' as 'modelIcon' | 'emoji' | 'none',
// 自定义 CSS
customCSS: ''
})
// 主题颜色选项
const themeColors = [
'#18a058', // 翠绿
'#d03050', // 红色
'#2080f0', // 蓝色
'#7c3aed', // 紫色
'#d946ef', // 品红
'#0ea5e9', // 天蓝
'#f59e0b', // 橙色
'#8b5cf6', // 紫罗兰
'#06b6d4', // 青色
]
// 全局字体选项
const globalFontOptions = [
{ label: '默认', value: 'default' },
{ label: 'Arial', value: 'Arial' },
{ label: 'Helvetica', value: 'Helvetica' },
{ label: 'Microsoft YaHei', value: 'Microsoft YaHei' },
{ label: 'PingFang SC', value: 'PingFang SC' },
{ label: 'Source Han Sans', value: 'Source Han Sans' }
]
// 代码字体选项
const codeFontOptions = [
{ label: '默认', value: 'default' },
{ label: 'Monaco', value: 'Monaco' },
{ label: 'Menlo', value: 'Menlo' },
{ label: 'Consolas', value: 'Consolas' },
{ label: 'Source Code Pro', value: 'Source Code Pro' },
{ label: 'JetBrains Mono', value: 'JetBrains Mono' },
{ label: 'Fira Code', value: 'Fira Code' }
]
// 缩放控制方法
const decreaseZoom = () => {
if (displaySettings.zoomLevel > 50) {
displaySettings.zoomLevel -= 10
applySettings()
}
}
const increaseZoom = () => {
if (displaySettings.zoomLevel < 200) {
displaySettings.zoomLevel += 10
applySettings()
}
}
const resetZoom = () => {
displaySettings.zoomLevel = 100
applySettings()
}
// 保存设置
const saveSettings = () => {
try {
localStorage.setItem('cherry-display-settings', JSON.stringify(displaySettings))
message.success('显示设置已保存')
} catch (error) {
message.error('保存设置失败')
}
}
// 加载设置
const loadSettings = () => {
try {
const saved = localStorage.getItem('cherry-display-settings')
if (saved) {
const settings = JSON.parse(saved)
Object.assign(displaySettings, settings)
}
} catch (error) {
console.error('加载显示设置失败:', error)
}
}
// 应用设置
const applySettings = () => {
const root = document.documentElement
// 应用主题
let actualTheme = displaySettings.theme
if (displaySettings.theme === 'system') {
actualTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
root.setAttribute('data-theme', actualTheme)
// 应用主色调 - 修复颜色应用逻辑
const primaryColor = displaySettings.primaryColor
root.style.setProperty('--primary-color', primaryColor)
// Naive UI 主题变量
root.style.setProperty('--n-color-primary', primaryColor)
root.style.setProperty('--n-color-primary-hover', primaryColor + 'CC') // 80% 透明度
root.style.setProperty('--n-color-primary-pressed', primaryColor + '99') // 60% 透明度
root.style.setProperty('--n-color-primary-suppl', primaryColor)
// 计算颜色变体
const hexToRgb = (hex: string) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null
}
const rgb = hexToRgb(primaryColor)
if (rgb) {
root.style.setProperty('--n-color-primary-hover', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.8)`)
root.style.setProperty('--n-color-primary-pressed', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.9)`)
root.style.setProperty('--n-border-color-primary', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.3)`)
}
// 应用缩放
if (typeof document !== 'undefined') {
document.body.style.zoom = `${displaySettings.zoomLevel}%`
}
// 应用字体
if (displaySettings.globalFont !== 'default') {
root.style.setProperty('--font-family', displaySettings.globalFont)
} else {
root.style.removeProperty('--font-family')
}
if (displaySettings.codeFont !== 'default') {
root.style.setProperty('--code-font-family', displaySettings.codeFont)
} else {
root.style.removeProperty('--code-font-family')
}
// 应用自定义CSS
let customStyleElement = document.getElementById('custom-styles')
if (!customStyleElement) {
customStyleElement = document.createElement('style')
customStyleElement.id = 'custom-styles'
document.head.appendChild(customStyleElement)
}
customStyleElement.textContent = displaySettings.customCSS
// 应用其他样式类
root.classList.toggle('transparent-window', displaySettings.transparentWindow)
root.classList.toggle('nav-top', displaySettings.navPosition === 'top')
root.classList.toggle('topic-right', displaySettings.topicPosition === 'right')
// 保存设置
saveSettings()
}
// 监听设置变化并自动应用
watch(displaySettings, () => {
applySettings()
// 触发主题更新事件
window.dispatchEvent(new CustomEvent('theme-color-changed', {
detail: displaySettings.primaryColor
}))
}, { deep: true })
// 生命周期
onMounted(() => {
loadSettings()
applySettings()
})
</script>
<style scoped>
.display-settings-page {
padding: 32px;
background: #f8fafc;
height: 100%;
overflow-y: auto;
}
.page-header {
margin-bottom: 32px;
}
.page-header .header-info h1 {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 700;
color: #1e293b;
}
.page-header .header-info p {
margin: 0;
color: #64748b;
font-size: 16px;
}
.settings-content {
display: flex;
flex-direction: column;
gap: 24px;
max-width: 900px;
}
.setting-section {
display: flex;
flex-direction: column;
gap: 0;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid #e2e8f0;
}
.setting-item:last-child {
border-bottom: none;
}
.setting-label {
flex: 1;
display: flex;
flex-direction: column;
}
.label-text {
font-weight: 500;
margin-bottom: 4px;
color: #374151;
}
.label-desc {
font-size: 14px;
color: #6b7280;
}
.setting-control {
flex-shrink: 0;
}
.theme-option {
display: flex;
align-items: center;
gap: 6px;
}
.color-picker-grid {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.color-option {
width: 32px;
height: 32px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid transparent;
transition: all 0.2s ease;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.active {
border-color: #ffffff;
box-shadow: 0 0 0 2px var(--primary-color);
}
.zoom-controls {
display: flex;
align-items: center;
gap: 12px;
}
.zoom-value {
min-width: 50px;
text-align: center;
font-weight: 500;
color: #374151;
}
.css-editor {
width: 100%;
}
.css-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
}
.css-header span {
color: #6b7280;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
}
.css-help {
color: #3b82f6;
text-decoration: none;
}
.css-help:hover {
text-decoration: underline;
}
/* 深色模式 */
[data-theme="dark"] .display-settings-page {
background: #0f172a;
}
[data-theme="dark"] .page-header .header-info h1 {
color: #f8fafc;
}
[data-theme="dark"] .page-header .header-info p {
color: #94a3b8;
}
[data-theme="dark"] .setting-item {
border-bottom-color: #334155;
}
[data-theme="dark"] .label-text {
color: #f1f5f9;
}
[data-theme="dark"] .label-desc {
color: #94a3b8;
}
[data-theme="dark"] .zoom-value {
color: #f1f5f9;
}
[data-theme="dark"] .css-header span {
color: #94a3b8;
}
</style>