feat(theme): 优化主题切换功能并添加全局暗黑主题支持- 重构主题切换逻辑,使用 data-theme 属性控制暗黑主题

- 添加全局暗黑主题样式,优化多个组件的暗黑模式显示
- 在 App.vue 中实现主题切换的响应式处理
- 在 Dashboard 和 TokenImport 页面中添加主题切换按钮
- 优化 TokenManager 组件的样式
This commit is contained in:
steve
2025-09-04 15:34:57 +08:00
parent 4b3ad56573
commit 1fba85b86b
4 changed files with 154 additions and 76 deletions

View File

@@ -1,5 +1,5 @@
<template> <template>
<n-config-provider :theme="theme"> <n-config-provider :theme="naiveTheme">
<n-message-provider> <n-message-provider>
<n-loading-bar-provider> <n-loading-bar-provider>
<n-notification-provider> <n-notification-provider>
@@ -15,41 +15,40 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { computed, onMounted, onUnmounted } from 'vue'
import { darkTheme } from 'naive-ui' import { darkTheme } from 'naive-ui'
import { useTheme } from '@/composables/useTheme'
// 主题控制 const { isDark, initTheme, setupSystemThemeListener, updateReactiveState } = useTheme()
const theme = ref(null)
// 检查用户偏好的主题 // Naive UI 主题
const checkThemePreference = () => { const naiveTheme = computed(() => {
const savedTheme = localStorage.getItem('theme') return isDark.value ? darkTheme : null
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
theme.value = darkTheme
document.documentElement.classList.add('dark')
} else {
theme.value = null
document.documentElement.classList.remove('dark')
}
}
// 监听系统主题变化
const setupThemeListener = () => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addListener(() => {
const savedTheme = localStorage.getItem('theme')
// 只有在用户没有手动设置主题时才跟随系统
if (!savedTheme) {
checkThemePreference()
}
}) })
// 监听主题变化事件
const handleThemeChange = () => {
// 确保响应式状态同步
updateReactiveState()
// 强制重新渲染
setTimeout(() => {
updateReactiveState()
}, 50)
} }
onMounted(() => { onMounted(() => {
checkThemePreference() initTheme()
setupThemeListener() setupSystemThemeListener()
// 监听自定义主题变化事件
window.addEventListener('theme-change', handleThemeChange)
// 初始化时更新状态
updateReactiveState()
})
onUnmounted(() => {
window.removeEventListener('theme-change', handleThemeChange)
}) })
</script> </script>
@@ -74,37 +73,135 @@ onMounted(() => {
--border-color: #4a5568 !important; --border-color: #4a5568 !important;
} }
/* 强制深色主题样式 - 更具体的选择器 */ /* 深色主题样式优化 - 针对Naive UI组件 */
html.dark, html.dark,
html.dark body, html[data-theme="dark"] {
html.dark #app, color-scheme: dark;
html.dark * { }
/* 全局深色主题文字颜色 */
html.dark *,
html[data-theme="dark"] * {
color: #ffffff;
}
/* Naive UI 表单组件 */
html.dark .n-form-item-label,
html.dark .n-form-item-label__text,
html[data-theme="dark"] .n-form-item-label,
html[data-theme="dark"] .n-form-item-label__text {
color: #ffffff !important; color: #ffffff !important;
} }
html.dark .n-form-item-label, /* Naive UI 输入组件 */
html.dark .n-form-item-label__text,
html.dark .n-input, html.dark .n-input,
html.dark .n-input__input, html.dark .n-input__input,
html.dark .n-input__textarea, html.dark .n-input__textarea,
html[data-theme="dark"] .n-input,
html[data-theme="dark"] .n-input__input,
html[data-theme="dark"] .n-input__textarea {
color: #ffffff !important;
background-color: rgba(255, 255, 255, 0.1) !important;
}
/* Naive UI 弹框组件 */
html.dark .n-modal,
html.dark .n-drawer,
html.dark .n-popover,
html.dark .n-dropdown,
html.dark .n-tooltip,
html.dark .n-dialog,
html[data-theme="dark"] .n-modal,
html[data-theme="dark"] .n-drawer,
html[data-theme="dark"] .n-popover,
html[data-theme="dark"] .n-dropdown,
html[data-theme="dark"] .n-tooltip,
html[data-theme="dark"] .n-dialog {
color: #ffffff !important;
}
/* Naive UI 弹框内容 */
html.dark .n-modal .n-card,
html.dark .n-drawer-content,
html.dark .n-popover-content,
html.dark .n-dropdown-option,
html.dark .n-dialog__content,
html[data-theme="dark"] .n-modal .n-card,
html[data-theme="dark"] .n-drawer-content,
html[data-theme="dark"] .n-popover-content,
html[data-theme="dark"] .n-dropdown-option,
html[data-theme="dark"] .n-dialog__content {
color: #ffffff !important;
}
/* Naive UI 下拉选项 */
html.dark .n-dropdown-option__label,
html.dark .n-select-option,
html.dark .n-menu-item-content,
html[data-theme="dark"] .n-dropdown-option__label,
html[data-theme="dark"] .n-select-option,
html[data-theme="dark"] .n-menu-item-content {
color: #ffffff !important;
}
/* 其他组件 */
html.dark .n-collapse-item__header, html.dark .n-collapse-item__header,
html.dark .n-radio-button, html.dark .n-radio-button,
html.dark .n-card, html.dark .n-card,
html.dark .n-card__content, html.dark .n-card__content,
html.dark h1, html.dark .n-button,
html.dark h2, html.dark .n-tag,
html.dark h3, html[data-theme="dark"] .n-collapse-item__header,
html.dark p, html[data-theme="dark"] .n-radio-button,
html.dark span, html[data-theme="dark"] .n-card,
html.dark div { html[data-theme="dark"] .n-card__content,
html[data-theme="dark"] .n-button,
html[data-theme="dark"] .n-tag {
color: #ffffff !important; color: #ffffff !important;
} }
/* 标题和文本 */
html.dark h1,
html.dark h2,
html.dark h3,
html.dark h4,
html.dark h5,
html.dark h6,
html.dark p,
html.dark span,
html.dark div,
html.dark label,
html[data-theme="dark"] h1,
html[data-theme="dark"] h2,
html[data-theme="dark"] h3,
html[data-theme="dark"] h4,
html[data-theme="dark"] h5,
html[data-theme="dark"] h6,
html[data-theme="dark"] p,
html[data-theme="dark"] span,
html[data-theme="dark"] div,
html[data-theme="dark"] label {
color: #ffffff !important;
}
/* 占位符文本 */
html.dark .n-input__placeholder, html.dark .n-input__placeholder,
html.dark ::placeholder { html.dark ::placeholder,
html[data-theme="dark"] .n-input__placeholder,
html[data-theme="dark"] ::placeholder {
color: rgba(255, 255, 255, 0.6) !important; color: rgba(255, 255, 255, 0.6) !important;
} }
/* 确保Portal渲染的组件也应用深色主题 */
body.dark .n-modal-container,
body.dark .n-drawer-container,
body.dark .n-popover-container,
body[data-theme="dark"] .n-modal-container,
body[data-theme="dark"] .n-drawer-container,
body[data-theme="dark"] .n-popover-container {
color: #ffffff !important;
}
#app { #app {
min-height: 100vh; min-height: 100vh;
background: var(--app-background); background: var(--app-background);

View File

@@ -92,10 +92,13 @@
<n-dropdown <n-dropdown
:options="getTokenMenuOptions(tokenData)" :options="getTokenMenuOptions(tokenData)"
@select="handleTokenAction($event, roleId, tokenData)"
trigger="click" trigger="click"
@select="handleTokenAction($event, roleId, tokenData)"
>
<n-button
size="tiny"
type="tertiary"
> >
<n-button size="tiny" type="tertiary">
<template #icon> <template #icon>
<n-icon><EllipsisHorizontal /></n-icon> <n-icon><EllipsisHorizontal /></n-icon>
</template> </template>

View File

@@ -64,6 +64,9 @@
</div> </div>
<div class="nav-user"> <div class="nav-user">
<!-- 主题切换按钮 -->
<ThemeToggle />
<n-dropdown <n-dropdown
:options="userMenuOptions" :options="userMenuOptions"
@select="handleUserAction" @select="handleUserAction"
@@ -222,6 +225,7 @@ import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
import { useTokenStore } from '@/stores/tokenStore' import { useTokenStore } from '@/stores/tokenStore'
import ThemeToggle from '@/components/ThemeToggle.vue'
import { import {
Home, Home,
PersonCircle, PersonCircle,
@@ -543,6 +547,9 @@ onMounted(async () => {
.nav-user { .nav-user {
margin-left: auto; margin-left: auto;
display: flex;
align-items: center;
gap: var(--spacing-md);
} }
.user-info { .user-info {

View File

@@ -11,21 +11,7 @@
class="brand-logo" class="brand-logo"
> >
<!-- 主题切换按钮 --> <!-- 主题切换按钮 -->
<n-button <ThemeToggle />
circle
size="small"
class="theme-toggle"
@click="toggleTheme"
>
<template #icon>
<n-icon v-if="isDarkTheme">
<Sunny />
</n-icon>
<n-icon v-else>
<Moon />
</n-icon>
</template>
</n-button>
</div> </div>
<h1>游戏Token管理</h1> <h1>游戏Token管理</h1>
</div> </div>
@@ -451,10 +437,11 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, computed, h } from 'vue' import { ref, reactive, onMounted, h } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useMessage, useDialog, NIcon } from 'naive-ui' import { useMessage, useDialog, NIcon } from 'naive-ui'
import { useTokenStore } from '@/stores/tokenStore' import { useTokenStore } from '@/stores/tokenStore'
import ThemeToggle from '@/components/ThemeToggle.vue'
import { import {
Add, Add,
CloudUpload, CloudUpload,
@@ -462,8 +449,6 @@ import {
EllipsisHorizontal, EllipsisHorizontal,
Key, Key,
Refresh, Refresh,
Sunny,
Moon,
Home, Home,
Create, Create,
Copy, Copy,
@@ -488,20 +473,6 @@ const editingToken = ref(null)
const importMethod = ref('manual') const importMethod = ref('manual')
const refreshingTokens = ref(new Set()) const refreshingTokens = ref(new Set())
// 主题控制(使用 data-theme 与全局变量匹配)
const isDarkTheme = computed(() => document.documentElement.getAttribute('data-theme') === 'dark')
const toggleTheme = () => {
const current = document.documentElement.getAttribute('data-theme') === 'dark'
if (current) {
document.documentElement.removeAttribute('data-theme')
localStorage.setItem('theme', 'light')
} else {
document.documentElement.setAttribute('data-theme', 'dark')
localStorage.setItem('theme', 'dark')
}
}
// 导入表单 // 导入表单
const importForm = reactive({ const importForm = reactive({
name: '', name: '',