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>
|
<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 handleThemeChange = () => {
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
// 确保响应式状态同步
|
||||||
mediaQuery.addListener(() => {
|
updateReactiveState()
|
||||||
const savedTheme = localStorage.getItem('theme')
|
// 强制重新渲染
|
||||||
// 只有在用户没有手动设置主题时才跟随系统
|
setTimeout(() => {
|
||||||
if (!savedTheme) {
|
updateReactiveState()
|
||||||
checkThemePreference()
|
}, 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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user