feat(theme): 优化主题切换功能并添加全局暗黑主题支持- 重构主题切换逻辑,使用 data-theme 属性控制暗黑主题
- 添加全局暗黑主题样式,优化多个组件的暗黑模式显示 - 在 App.vue 中实现主题切换的响应式处理 - 在 Dashboard 和 TokenImport 页面中添加主题切换按钮 - 优化 TokenManager 组件的样式
This commit is contained in:
181
src/App.vue
181
src/App.vue
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<n-config-provider :theme="theme">
|
||||
<n-config-provider :theme="naiveTheme">
|
||||
<n-message-provider>
|
||||
<n-loading-bar-provider>
|
||||
<n-notification-provider>
|
||||
@@ -15,41 +15,40 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { darkTheme } from 'naive-ui'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
// 主题控制
|
||||
const theme = ref(null)
|
||||
const { isDark, initTheme, setupSystemThemeListener, updateReactiveState } = useTheme()
|
||||
|
||||
// 检查用户偏好的主题
|
||||
const checkThemePreference = () => {
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
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()
|
||||
}
|
||||
// Naive UI 主题
|
||||
const naiveTheme = computed(() => {
|
||||
return isDark.value ? darkTheme : null
|
||||
})
|
||||
|
||||
// 监听主题变化事件
|
||||
const handleThemeChange = () => {
|
||||
// 确保响应式状态同步
|
||||
updateReactiveState()
|
||||
// 强制重新渲染
|
||||
setTimeout(() => {
|
||||
updateReactiveState()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkThemePreference()
|
||||
setupThemeListener()
|
||||
initTheme()
|
||||
setupSystemThemeListener()
|
||||
|
||||
// 监听自定义主题变化事件
|
||||
window.addEventListener('theme-change', handleThemeChange)
|
||||
|
||||
// 初始化时更新状态
|
||||
updateReactiveState()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('theme-change', handleThemeChange)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -74,37 +73,135 @@ onMounted(() => {
|
||||
--border-color: #4a5568 !important;
|
||||
}
|
||||
|
||||
/* 强制深色主题样式 - 更具体的选择器 */
|
||||
/* 深色主题样式优化 - 针对Naive UI组件 */
|
||||
html.dark,
|
||||
html.dark body,
|
||||
html.dark #app,
|
||||
html.dark * {
|
||||
html[data-theme="dark"] {
|
||||
color-scheme: 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;
|
||||
}
|
||||
|
||||
html.dark .n-form-item-label,
|
||||
html.dark .n-form-item-label__text,
|
||||
/* Naive UI 输入组件 */
|
||||
html.dark .n-input,
|
||||
html.dark .n-input__input,
|
||||
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-radio-button,
|
||||
html.dark .n-card,
|
||||
html.dark .n-card__content,
|
||||
html.dark h1,
|
||||
html.dark h2,
|
||||
html.dark h3,
|
||||
html.dark p,
|
||||
html.dark span,
|
||||
html.dark div {
|
||||
html.dark .n-button,
|
||||
html.dark .n-tag,
|
||||
html[data-theme="dark"] .n-collapse-item__header,
|
||||
html[data-theme="dark"] .n-radio-button,
|
||||
html[data-theme="dark"] .n-card,
|
||||
html[data-theme="dark"] .n-card__content,
|
||||
html[data-theme="dark"] .n-button,
|
||||
html[data-theme="dark"] .n-tag {
|
||||
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 ::placeholder {
|
||||
html.dark ::placeholder,
|
||||
html[data-theme="dark"] .n-input__placeholder,
|
||||
html[data-theme="dark"] ::placeholder {
|
||||
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 {
|
||||
min-height: 100vh;
|
||||
background: var(--app-background);
|
||||
|
||||
@@ -92,10 +92,13 @@
|
||||
|
||||
<n-dropdown
|
||||
:options="getTokenMenuOptions(tokenData)"
|
||||
@select="handleTokenAction($event, roleId, tokenData)"
|
||||
trigger="click"
|
||||
@select="handleTokenAction($event, roleId, tokenData)"
|
||||
>
|
||||
<n-button
|
||||
size="tiny"
|
||||
type="tertiary"
|
||||
>
|
||||
<n-button size="tiny" type="tertiary">
|
||||
<template #icon>
|
||||
<n-icon><EllipsisHorizontal /></n-icon>
|
||||
</template>
|
||||
|
||||
@@ -64,6 +64,9 @@
|
||||
</div>
|
||||
|
||||
<div class="nav-user">
|
||||
<!-- 主题切换按钮 -->
|
||||
<ThemeToggle />
|
||||
|
||||
<n-dropdown
|
||||
:options="userMenuOptions"
|
||||
@select="handleUserAction"
|
||||
@@ -222,6 +225,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||
import {
|
||||
Home,
|
||||
PersonCircle,
|
||||
@@ -543,6 +547,9 @@ onMounted(async () => {
|
||||
|
||||
.nav-user {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
|
||||
@@ -11,21 +11,7 @@
|
||||
class="brand-logo"
|
||||
>
|
||||
<!-- 主题切换按钮 -->
|
||||
<n-button
|
||||
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>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<h1>游戏Token管理</h1>
|
||||
</div>
|
||||
@@ -451,10 +437,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, h } from 'vue'
|
||||
import { ref, reactive, onMounted, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage, useDialog, NIcon } from 'naive-ui'
|
||||
import { useTokenStore } from '@/stores/tokenStore'
|
||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||
import {
|
||||
Add,
|
||||
CloudUpload,
|
||||
@@ -462,8 +449,6 @@ import {
|
||||
EllipsisHorizontal,
|
||||
Key,
|
||||
Refresh,
|
||||
Sunny,
|
||||
Moon,
|
||||
Home,
|
||||
Create,
|
||||
Copy,
|
||||
@@ -488,20 +473,6 @@ const editingToken = ref(null)
|
||||
const importMethod = ref('manual')
|
||||
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({
|
||||
name: '',
|
||||
|
||||
Reference in New Issue
Block a user