frontend: add dark mode support

This commit is contained in:
2026-01-03 12:48:25 +08:00
parent 827c9198ae
commit f46c2a039b
11 changed files with 710 additions and 166 deletions
+13 -2
View File
@@ -5,14 +5,25 @@
</template> </template>
<script setup> <script setup>
import { onMounted } from 'vue' import { onMounted, computed } from 'vue'
import { ConfigProvider as AConfigProvider } from 'ant-design-vue' import { ConfigProvider as AConfigProvider } from 'ant-design-vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN' import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import antdTheme from './antd-theme' import getAntdTheme from './antd-theme'
import { useTheme, initTheme, watchSystemTheme } from '@/composables/useTheme'
const authStore = useAuthStore() const authStore = useAuthStore()
// 初始化主题(全局)
initTheme()
watchSystemTheme()
// 使用主题
const { isDark } = useTheme()
// 动态生成 Ant Design 主题
const antdTheme = computed(() => getAntdTheme(isDark.value))
// 应用启动时验证 Token // 应用启动时验证 Token
onMounted(async () => { onMounted(async () => {
if (authStore.isAuthenticated) { if (authStore.isAuthenticated) {
+116 -8
View File
@@ -1,14 +1,18 @@
import { theme } from 'ant-design-vue'
/** /**
* Ant Design Vue 主题配置 * Ant Design Vue 主题配置
* 匹配现有 Material Design 3 色彩系统 * 匹配现有 Material Design 3 色彩系统
* @param {boolean} isDark - 是否为暗黑模式
*/ */
export default { export default function getAntdTheme(isDark = false) {
return {
token: { token: {
// 主色调 - 绿色(与 MD3 primary 保持一致) // 主色调 - 绿色(与 MD3 primary 保持一致)
colorPrimary: '#4caf50', colorPrimary: isDark ? '#81c784' : '#4caf50',
// 成功色 // 成功色
colorSuccess: '#4caf50', colorSuccess: isDark ? '#81c784' : '#4caf50',
// 警告色 // 警告色
colorWarning: '#ff9800', colorWarning: '#ff9800',
@@ -17,7 +21,27 @@ export default {
colorError: '#f56c6c', colorError: '#f56c6c',
// 信息色 - 蓝色(与 MD3 secondary 保持一致) // 信息色 - 蓝色(与 MD3 secondary 保持一致)
colorInfo: '#2196f3', colorInfo: isDark ? '#64b5f6' : '#2196f3',
// 背景色
colorBgBase: isDark ? '#121212' : '#ffffff',
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff',
colorBgElevated: isDark ? '#2c2c2e' : '#ffffff',
colorBgLayout: isDark ? '#121212' : '#fafafa',
colorBgSpotlight: isDark ? '#2c2c2e' : '#ffffff',
// 文字色
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
colorTextSecondary: isDark ? '#a0a0a3' : '#64748b',
colorTextTertiary: isDark ? '#808083' : '#94a3b8',
colorTextQuaternary: isDark ? '#606063' : '#cbd5e1',
// 边框色
colorBorder: isDark ? '#3a3a3c' : '#e5e7eb',
colorBorderSecondary: isDark ? '#2c2c2e' : '#f3f4f6',
// 分割线颜色
colorSplit: isDark ? '#3a3a3c' : '#e5e7eb',
// 边框圆角 - 与 Material Design 3 一致 // 边框圆角 - 与 Material Design 3 一致
borderRadius: 12, borderRadius: 12,
@@ -26,21 +50,31 @@ export default {
fontFamily: "'Inter', 'Segoe UI', 'Roboto', system-ui, -apple-system, sans-serif", fontFamily: "'Inter', 'Segoe UI', 'Roboto', system-ui, -apple-system, sans-serif",
// 链接色 // 链接色
colorLink: '#2196f3', colorLink: isDark ? '#64b5f6' : '#2196f3',
colorLinkHover: isDark ? '#90caf9' : '#1976d2',
colorLinkActive: isDark ? '#42a5f5' : '#1565c0',
// 字体大小 // 字体大小
fontSize: 14, fontSize: 14,
// 行高 // 行高
lineHeight: 1.5715, lineHeight: 1.5715,
// 控制组件高度
controlHeight: 40,
}, },
components: { components: {
// Card 组件定制 // Card 组件定制
Card: { Card: {
borderRadiusLG: 16, borderRadiusLG: 16,
boxShadowTertiary: '0 1px 3px 1px rgba(0, 0, 0, 0.08)', boxShadowTertiary: isDark
? '0 1px 3px 1px rgba(0, 0, 0, 0.5)'
: '0 1px 3px 1px rgba(0, 0, 0, 0.08)',
paddingLG: 24, paddingLG: 24,
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff',
colorBorderSecondary: isDark ? '#3a3a3c' : '#f0f0f0',
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f',
}, },
// Button 组件定制 // Button 组件定制
@@ -48,30 +82,104 @@ export default {
borderRadius: 24, // 圆角按钮,类似 MD3 borderRadius: 24, // 圆角按钮,类似 MD3
controlHeight: 40, controlHeight: 40,
fontSize: 14, fontSize: 14,
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
colorBgContainer: isDark ? '#2c2c2e' : '#ffffff',
}, },
// Input 组件定制 // Input 组件定制
Input: { Input: {
borderRadius: 12, borderRadius: 12,
controlHeight: 40, controlHeight: 40,
colorBgContainer: isDark ? '#2c2c2e' : '#ffffff',
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
colorTextPlaceholder: isDark ? '#808083' : '#94a3b8',
colorBorder: isDark ? '#3a3a3c' : '#e5e7eb',
},
// Select 组件定制
Select: {
borderRadius: 12,
controlHeight: 40,
colorBgContainer: isDark ? '#2c2c2e' : '#ffffff',
colorBgElevated: isDark ? '#2c2c2e' : '#ffffff',
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
colorTextPlaceholder: isDark ? '#808083' : '#94a3b8',
colorBorder: isDark ? '#3a3a3c' : '#e5e7eb',
}, },
// Modal 组件定制 // Modal 组件定制
Modal: { Modal: {
borderRadiusLG: 16, borderRadiusLG: 16,
colorBgElevated: isDark ? '#1c1c1e' : '#ffffff',
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f',
}, },
// Table 组件定制 // Table 组件定制
Table: { Table: {
borderRadius: 12, borderRadius: 12,
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff',
colorFillAlter: isDark ? '#2c2c2e' : '#f5f7fa',
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f',
colorBorderSecondary: isDark ? '#3a3a3c' : '#f0f0f0',
}, },
// Tabs 组件定制 // Tabs 组件定制
Tabs: { Tabs: {
borderRadius: 12, borderRadius: 12,
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff',
},
// Menu 组件定制
Menu: {
colorItemBg: isDark ? '#1c1c1e' : '#ffffff',
colorItemBgHover: isDark ? '#2c2c2e' : '#f5f7fa',
colorItemBgSelected: isDark ? '#2c2c2e' : '#e8f5e9',
colorItemText: isDark ? '#e5e5e7' : '#1c1b1f',
colorItemTextSelected: isDark ? '#81c784' : '#4caf50',
},
// Dropdown 组件定制
Dropdown: {
colorBgElevated: isDark ? '#2c2c2e' : '#ffffff',
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
},
// Descriptions 组件定制
Descriptions: {
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
colorTextSecondary: isDark ? '#a0a0a3' : '#64748b',
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff',
colorFillAlter: isDark ? '#2c2c2e' : '#f5f7fa',
},
// Alert 组件定制
Alert: {
borderRadiusLG: 12,
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
},
// Drawer 组件定制
Drawer: {
colorBgElevated: isDark ? '#1c1c1e' : '#ffffff',
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
},
// Form 组件定制
Form: {
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f',
},
// Empty 组件定制
Empty: {
colorTextDescription: isDark ? '#a0a0a3' : '#94a3b8',
}, },
}, },
// 算法配置 // 算法配置 - 使用 Ant Design 内置的暗黑算法
algorithm: [], algorithm: isDark ? [theme.darkAlgorithm] : [],
}
} }
+4
View File
@@ -29,6 +29,10 @@ onMounted(() => {
background: linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%); background: linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%);
} }
.dark .layout-container {
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
}
.main-content { .main-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
+41 -22
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="sticky top-0 z-50 glass-effect border-b border-gray-200/50 shadow-md3-2"> <div class="sticky top-0 z-50 glass-effect border-b border-gray-200/50 dark:border-gray-700/50 shadow-md3-2">
<nav class="max-w-7xl mx-auto px-6 py-4"> <nav class="max-w-7xl mx-auto px-6 py-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<!-- Logo and Brand --> <!-- Logo and Brand -->
@@ -23,8 +23,8 @@
:class="[ :class="[
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer', 'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
isActive isActive
? 'bg-primary-100 text-primary-700' ? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
: 'text-gray-700 hover:bg-gray-100' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
]" ]"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
@@ -44,8 +44,8 @@
:class="[ :class="[
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer', 'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
isActive isActive
? 'bg-primary-100 text-primary-700' ? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
: 'text-gray-700 hover:bg-gray-100' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
]" ]"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
@@ -65,8 +65,8 @@
:class="[ :class="[
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer', 'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
isActive isActive
? 'bg-primary-100 text-primary-700' ? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
: 'text-gray-700 hover:bg-gray-100' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
]" ]"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
@@ -81,7 +81,7 @@
<a <a
:class="[ :class="[
'px-4 py-2 rounded-full font-medium transition-all flex items-center space-x-2 cursor-pointer', 'px-4 py-2 rounded-full font-medium transition-all flex items-center space-x-2 cursor-pointer',
isAdminPath ? 'bg-secondary-100 text-secondary-700' : 'text-gray-700 hover:bg-gray-100' isAdminPath ? 'bg-secondary-100 dark:bg-secondary-900/30 text-secondary-700 dark:text-secondary-400' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
]" ]"
> >
<SettingOutlined /> <SettingOutlined />
@@ -117,43 +117,57 @@
</div> </div>
<!-- User Menu & Mobile Hamburger --> <!-- User Menu & Mobile Hamburger -->
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-2 md:space-x-4">
<!-- Token Status Indicator (Desktop) --> <!-- Token Status Indicator (Desktop & Mobile) -->
<a-tooltip v-if="!isMobile && showTokenStatus" :title="tokenStatusTooltip"> <a-tooltip v-if="showTokenStatus" :title="tokenStatusTooltip">
<div <div
class="px-3 py-1.5 rounded-full cursor-pointer transition-all hover:bg-gray-100 flex items-center space-x-2" class="px-2 md:px-3 py-1.5 rounded-full cursor-pointer transition-all hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center space-x-1 md:space-x-2"
@click="handleTokenStatusClick" @click="handleTokenStatusClick"
> >
<a-badge :status="tokenBadgeStatus" /> <a-badge :status="tokenBadgeStatus" />
<ClockCircleOutlined :class="tokenIconClass" /> <ClockCircleOutlined :class="[tokenIconClass, 'text-sm md:text-base']" />
<span class="text-sm">{{ tokenBadgeText }}</span> <span class="text-xs md:text-sm hidden sm:inline">{{ tokenBadgeText }}</span>
<!-- 过期时显示刷新按钮 --> <!-- 过期时显示刷新按钮响应式设计 -->
<a-button <a-button
v-if="remainingMinutes !== null && remainingMinutes < 0" v-if="remainingMinutes !== null && remainingMinutes < 0"
type="primary" type="primary"
size="small" size="small"
class="!text-xs !px-2 md:!px-3"
@click.stop="handleRefreshToken" @click.stop="handleRefreshToken"
> >
刷新 <span class="hidden sm:inline">刷新</span>
<ReloadOutlined class="sm:hidden" />
</a-button> </a-button>
</div> </div>
</a-tooltip> </a-tooltip>
<!-- Theme Toggle Button -->
<a-tooltip :title="isDark ? '切换到亮色模式' : '切换到暗黑模式'">
<a-button
type="text"
class="!p-2 !flex !items-center !justify-center hover:!bg-gray-100 dark:hover:!bg-gray-700 transition-all"
@click="toggleTheme"
>
<BulbFilled v-if="isDark" class="text-lg text-yellow-400" />
<BulbOutlined v-else class="text-lg text-gray-700 dark:text-gray-300" />
</a-button>
</a-tooltip>
<!-- Desktop User Menu --> <!-- Desktop User Menu -->
<a-dropdown v-if="!isMobile" :trigger="['hover']"> <a-dropdown v-if="!isMobile" :trigger="['hover']">
<a class="flex items-center space-x-3 px-4 py-2 rounded-full hover:bg-gray-100 transition-all cursor-pointer"> <a class="flex items-center space-x-3 px-4 py-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all cursor-pointer">
<a-avatar :style="{ backgroundColor: '#f56a00' }"> <a-avatar :style="{ backgroundColor: '#f56a00' }">
{{ userInitial }} {{ userInitial }}
</a-avatar> </a-avatar>
<span class="hidden md:block font-medium text-gray-700">{{ authStore.user?.alias || '用户' }}</span> <span class="hidden md:block font-medium text-gray-700 dark:text-gray-200">{{ authStore.user?.alias || '用户' }}</span>
<DownOutlined class="text-xs text-gray-500" /> <DownOutlined class="text-xs text-gray-500 dark:text-gray-400" />
</a> </a>
<template #overlay> <template #overlay>
<a-menu> <a-menu>
<a-menu-item key="info" disabled> <a-menu-item key="info" disabled>
<div class="px-2 py-1"> <div class="px-2 py-1">
<p class="text-sm font-medium text-gray-900">{{ authStore.user?.alias }}</p> <p class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ authStore.user?.alias }}</p>
<p class="text-xs text-gray-500 mt-1">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p> <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p>
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-divider /> <a-menu-divider />
@@ -176,7 +190,7 @@
@click="drawerVisible = true" @click="drawerVisible = true"
class="!p-2" class="!p-2"
> >
<MenuOutlined class="text-xl" /> <MenuOutlined class="text-xl text-gray-700 dark:text-gray-300" />
</a-button> </a-button>
</div> </div>
</div> </div>
@@ -277,6 +291,7 @@ import { useAuthStore } from '@/stores/auth'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useTokenMonitor } from '@/composables/useTokenMonitor' import { useTokenMonitor } from '@/composables/useTokenMonitor'
import { useBreakpoint } from '@/composables/useBreakpoint' import { useBreakpoint } from '@/composables/useBreakpoint'
import { useTheme } from '@/composables/useTheme'
import { Modal, message } from 'ant-design-vue' import { Modal, message } from 'ant-design-vue'
import QRCodeModal from './QRCodeModal.vue' import QRCodeModal from './QRCodeModal.vue'
import { import {
@@ -293,6 +308,9 @@ import {
DownOutlined, DownOutlined,
CheckCircleOutlined, CheckCircleOutlined,
ClockCircleOutlined, ClockCircleOutlined,
BulbOutlined,
BulbFilled,
ReloadOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
const router = useRouter() const router = useRouter()
@@ -301,6 +319,7 @@ const authStore = useAuthStore()
const userStore = useUserStore() const userStore = useUserStore()
const { isMobile } = useBreakpoint() const { isMobile } = useBreakpoint()
const { getRemainingMinutes, tokenStatus } = useTokenMonitor() const { getRemainingMinutes, tokenStatus } = useTokenMonitor()
const { isDark, toggleTheme } = useTheme()
const drawerVisible = ref(false) const drawerVisible = ref(false)
const qrcodeModalVisible = ref(false) const qrcodeModalVisible = ref(false)
+106
View File
@@ -0,0 +1,106 @@
import { ref, computed } from 'vue'
const THEME_STORAGE_KEY = 'checkin-app-theme'
// 全局主题状态(单例模式)
const theme = ref('light')
/**
* 应用主题到 DOM
*/
const applyTheme = (newTheme) => {
const html = document.documentElement
if (newTheme === 'dark') {
html.classList.add('dark')
} else {
html.classList.remove('dark')
}
}
/**
* 初始化主题
* 优先级: localStorage > 系统偏好 > 默认亮色
*/
export const initTheme = () => {
// 1. 尝试从 localStorage 读取
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY)
if (savedTheme === 'light' || savedTheme === 'dark') {
theme.value = savedTheme
applyTheme(savedTheme)
return
}
// 2. 检测系统偏好
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme.value = 'dark'
applyTheme('dark')
return
}
// 3. 默认亮色
theme.value = 'light'
applyTheme('light')
}
/**
* 监听系统主题变化
*/
export const watchSystemTheme = () => {
if (!window.matchMedia) return
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = (e) => {
// 仅在用户未手动设置主题时才跟随系统
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY)
if (!savedTheme) {
const systemTheme = e.matches ? 'dark' : 'light'
theme.value = systemTheme
applyTheme(systemTheme)
}
}
mediaQuery.addEventListener('change', handleChange)
// 返回清理函数
return () => mediaQuery.removeEventListener('change', handleChange)
}
/**
* 主题管理 Composable
* 支持亮色/暗色模式切换,并持久化到 localStorage
*/
export function useTheme() {
/**
* 切换主题
*/
const toggleTheme = () => {
const newTheme = theme.value === 'light' ? 'dark' : 'light'
theme.value = newTheme
applyTheme(newTheme)
localStorage.setItem(THEME_STORAGE_KEY, newTheme)
}
/**
* 设置指定主题
*/
const setTheme = (newTheme) => {
if (newTheme !== 'light' && newTheme !== 'dark') {
console.warn(`Invalid theme: ${newTheme}. Using 'light' instead.`)
newTheme = 'light'
}
theme.value = newTheme
applyTheme(newTheme)
localStorage.setItem(THEME_STORAGE_KEY, newTheme)
}
return {
theme,
toggleTheme,
setTheme,
isDark: computed(() => theme.value === 'dark'),
isLight: computed(() => theme.value === 'light')
}
}
+1 -10
View File
@@ -4,9 +4,6 @@ import { createPinia } from 'pinia'
// Ant Design Vue // Ant Design Vue
import Antd from 'ant-design-vue' import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css' import 'ant-design-vue/dist/reset.css'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { ConfigProvider } from 'ant-design-vue'
import antdTheme from './antd-theme'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
@@ -18,13 +15,7 @@ const pinia = createPinia()
app.use(pinia) app.use(pinia)
app.use(router) app.use(router)
// Ant Design Vue with custom theme // Ant Design Vue
app.use(Antd) app.use(Antd)
// Configure Ant Design globally
app.config.globalProperties.$antdConfig = {
theme: antdTheme,
locale: zhCN,
}
app.mount('#app') app.mount('#app')
+363 -16
View File
@@ -14,7 +14,7 @@
font-weight: 400; font-weight: 400;
color-scheme: light; color-scheme: light;
/* Material Design 3 color tokens */ /* Material Design 3 color tokens - Light Mode */
--md-sys-color-primary: #4caf50; --md-sys-color-primary: #4caf50;
--md-sys-color-on-primary: #ffffff; --md-sys-color-on-primary: #ffffff;
--md-sys-color-secondary: #2196f3; --md-sys-color-secondary: #2196f3;
@@ -25,6 +25,21 @@
--md-sys-color-on-background: #1c1b1f; --md-sys-color-on-background: #1c1b1f;
} }
/* Dark Mode Colors */
.dark {
color-scheme: dark;
/* Material Design 3 color tokens - Dark Mode */
--md-sys-color-primary: #81c784;
--md-sys-color-on-primary: #1b5e20;
--md-sys-color-secondary: #64b5f6;
--md-sys-color-on-secondary: #0d47a1;
--md-sys-color-surface: #1c1c1e;
--md-sys-color-on-surface: #e5e5e7;
--md-sys-color-background: #121212;
--md-sys-color-on-background: #e5e5e7;
}
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -41,6 +56,11 @@
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
transition: background-color 0.3s ease, color 0.3s ease;
}
.dark body {
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
} }
#app { #app {
@@ -53,7 +73,7 @@
@layer components { @layer components {
/* Material Design 3 Card */ /* Material Design 3 Card */
.md3-card { .md3-card {
@apply bg-white rounded-md3 shadow-md3-2 overflow-hidden transition-all duration-300; @apply bg-white dark:bg-gray-800 rounded-md3 shadow-md3-2 overflow-hidden transition-all duration-300;
} }
.md3-card:hover { .md3-card:hover {
@@ -61,7 +81,7 @@
} }
.md3-card-elevated { .md3-card-elevated {
@apply bg-white rounded-md3-lg shadow-md3-3; @apply bg-white dark:bg-gray-800 rounded-md3-lg shadow-md3-3;
} }
/* Material Design 3 Button */ /* Material Design 3 Button */
@@ -71,25 +91,25 @@
} }
.md3-button-filled { .md3-button-filled {
@apply md3-button bg-primary-600 text-white hover:bg-primary-700 hover:shadow-md3-2; @apply md3-button bg-primary-600 dark:bg-primary-500 text-white hover:bg-primary-700 dark:hover:bg-primary-600 hover:shadow-md3-2;
} }
.md3-button-outlined { .md3-button-outlined {
@apply md3-button border-2 border-primary-600 text-primary-600 hover:bg-primary-50; @apply md3-button border-2 border-primary-600 dark:border-primary-400 text-primary-600 dark:text-primary-400 hover:bg-primary-50 dark:hover:bg-gray-700;
} }
.md3-button-text { .md3-button-text {
@apply md3-button text-primary-600 hover:bg-primary-50; @apply md3-button text-primary-600 dark:text-primary-400 hover:bg-primary-50 dark:hover:bg-gray-700;
} }
/* Fluent Design elements */ /* Fluent Design elements */
.fluent-card { .fluent-card {
@apply bg-white/80 backdrop-blur-xl rounded-lg border border-gray-200/50 shadow-lg; @apply bg-white/80 dark:bg-gray-800/80 backdrop-blur-xl rounded-lg border border-gray-200/50 dark:border-gray-700/50 shadow-lg;
@apply transition-all duration-300 hover:shadow-xl hover:border-gray-300/50; @apply transition-all duration-300 hover:shadow-xl hover:border-gray-300/50 dark:hover:border-gray-600/50;
} }
.fluent-acrylic { .fluent-acrylic {
@apply bg-white/70 backdrop-blur-2xl backdrop-saturate-150; @apply bg-white/70 dark:bg-gray-800/70 backdrop-blur-2xl backdrop-saturate-150;
} }
/* Status badges */ /* Status badges */
@@ -98,24 +118,24 @@
} }
.status-success { .status-success {
@apply status-badge bg-green-100 text-green-800; @apply status-badge bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300;
} }
.status-warning { .status-warning {
@apply status-badge bg-yellow-100 text-yellow-800; @apply status-badge bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300;
} }
.status-error { .status-error {
@apply status-badge bg-red-100 text-red-800; @apply status-badge bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300;
} }
.status-info { .status-info {
@apply status-badge bg-blue-100 text-blue-800; @apply status-badge bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300;
} }
/* Loading skeleton */ /* Loading skeleton */
.skeleton { .skeleton {
@apply animate-pulse bg-gray-200 rounded; @apply animate-pulse bg-gray-200 dark:bg-gray-700 rounded;
} }
} }
@@ -126,11 +146,11 @@
} }
.glass-effect { .glass-effect {
@apply bg-white/60 backdrop-blur-md backdrop-saturate-150; @apply bg-white/60 dark:bg-gray-800/60 backdrop-blur-md backdrop-saturate-150;
} }
.text-gradient { .text-gradient {
@apply bg-gradient-to-r from-primary-600 to-secondary-600 bg-clip-text text-transparent; @apply bg-gradient-to-r from-primary-600 to-secondary-600 dark:from-primary-400 dark:to-secondary-400 bg-clip-text text-transparent;
} }
} }
@@ -234,6 +254,12 @@
font-weight: 600; font-weight: 600;
} }
.dark .ant-table-thead > tr > th {
background: #2c2c2e;
color: #e5e5e7;
border-color: #3a3a3c;
}
/* Ant Design Tabs */ /* Ant Design Tabs */
.ant-tabs { .ant-tabs {
color: var(--md-sys-color-on-surface); color: var(--md-sys-color-on-surface);
@@ -264,15 +290,34 @@
background: #f5f7fa; background: #f5f7fa;
} }
.dark .ant-descriptions-bordered .ant-descriptions-item-label {
background: #2c2c2e;
color: #e5e5e7;
}
.dark .ant-descriptions-item-label,
.dark .ant-descriptions-item-content {
color: #e5e5e7;
border-color: #3a3a3c;
}
/* Ant Design Statistic */ /* Ant Design Statistic */
.ant-statistic-title { .ant-statistic-title {
color: #64748b; color: #64748b;
} }
.dark .ant-statistic-title {
color: #a0a0a3;
}
.ant-statistic-content { .ant-statistic-content {
color: var(--md-sys-color-on-surface); color: var(--md-sys-color-on-surface);
} }
.dark .ant-statistic-content {
color: #e5e5e7;
}
/* Ant Design Drawer */ /* Ant Design Drawer */
.ant-drawer-content { .ant-drawer-content {
border-radius: 16px 0 0 16px; border-radius: 16px 0 0 16px;
@@ -287,6 +332,308 @@
border-radius: 12px; border-radius: 12px;
} }
/* Dark mode support for common elements */
.dark .ant-card {
background: #1c1c1e;
border-color: #3a3a3c;
color: #e5e5e7;
}
.dark .ant-card-head {
color: #e5e5e7;
border-color: #3a3a3c;
}
.dark .ant-card-body {
color: #e5e5e7;
}
.dark .ant-select-selector {
background: #2c2c2e !important;
border-color: #3a3a3c !important;
color: #e5e5e7 !important;
}
.dark .ant-select-selection-item {
color: #e5e5e7;
}
.dark .ant-select-arrow {
color: #a0a0a3;
}
.dark .ant-input {
background: #2c2c2e;
border-color: #3a3a3c;
color: #e5e5e7;
}
.dark .ant-input::placeholder {
color: #808083;
}
.dark .ant-modal-content {
background: #1c1c1e;
}
.dark .ant-modal-header {
background: #1c1c1e;
border-color: #3a3a3c;
}
.dark .ant-modal-title {
color: #e5e5e7;
}
.dark .ant-modal-body {
color: #e5e5e7;
}
.dark .ant-table {
background: #1c1c1e;
color: #e5e5e7;
}
.dark .ant-table-tbody > tr > td {
border-color: #3a3a3c;
color: #e5e5e7;
}
.dark .ant-table-tbody > tr:hover > td {
background: #2c2c2e;
}
.dark .ant-pagination-item {
background: #2c2c2e;
border-color: #3a3a3c;
}
.dark .ant-pagination-item a {
color: #e5e5e7;
}
.dark .ant-pagination-item:hover {
border-color: #81c784;
}
.dark .ant-pagination-item:hover a {
color: #81c784;
}
.dark .ant-empty-description {
color: #a0a0a3;
}
.dark .ant-form-item-label > label {
color: #e5e5e7;
}
.dark .ant-checkbox-wrapper {
color: #e5e5e7;
}
.dark .ant-radio-wrapper {
color: #e5e5e7;
}
.dark .ant-divider {
border-color: #3a3a3c;
}
/* Dropdown 暗黑模式 */
.dark .ant-dropdown-menu {
background: #2c2c2e;
}
.dark .ant-dropdown-menu-item {
color: #e5e5e7;
}
.dark .ant-dropdown-menu-item:hover {
background: #3a3a3c;
}
/* 通用文本颜色 - 暗黑模式 */
.dark p,
.dark span,
.dark div {
color: inherit;
}
.dark .hint,
.dark .card-header {
color: #e5e5e7;
}
/* Select dropdown 暗黑模式 */
.dark .ant-select-dropdown {
background: #2c2c2e;
}
.dark .ant-select-item {
color: #e5e5e7;
}
.dark .ant-select-item:hover {
background: #3a3a3c;
}
.dark .ant-select-item-option-selected {
background: #2c2c2e;
color: #81c784;
}
/* Drawer 暗黑模式 */
.dark .ant-drawer-content {
background: #1c1c1e;
}
.dark .ant-drawer-header {
background: #1c1c1e;
border-color: #3a3a3c;
}
.dark .ant-drawer-title {
color: #e5e5e7;
}
.dark .ant-drawer-body {
background: #1c1c1e;
color: #e5e5e7;
}
/* 通用文本和标签颜色 */
.dark .ant-tag {
border-color: #3a3a3c;
}
.dark .ant-btn-text {
color: #e5e5e7;
}
.dark .ant-btn-text:hover {
background: #2c2c2e;
}
.dark .ant-skeleton-content .ant-skeleton-title {
background: linear-gradient(90deg, #2c2c2e 25%, #3a3a3c 37%, #2c2c2e 63%);
}
.dark .ant-skeleton-content .ant-skeleton-paragraph > li {
background: linear-gradient(90deg, #2c2c2e 25%, #3a3a3c 37%, #2c2c2e 63%);
}
/* Message 和 Notification */
.dark .ant-message-notice-content {
background: #2c2c2e;
color: #e5e5e7;
}
.dark .ant-notification-notice {
background: #2c2c2e;
color: #e5e5e7;
}
/* Spin loading */
.dark .ant-spin-dot-item {
background: #81c784;
}
/* Tooltip */
.ant-tooltip-inner {
background: rgba(255, 255, 255, 0.95) !important;
color: #1c1b1f !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important;
padding: 8px 12px !important;
display: flex !important;
align-items: center !important;
min-height: 32px !important;
}
.dark .ant-tooltip-inner {
background: rgba(50, 50, 50, 0.95) !important;
color: #e5e5e7 !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5) !important;
}
.ant-tooltip-arrow-content {
background: rgba(255, 255, 255, 0.95) !important;
}
.dark .ant-tooltip-arrow-content {
background: rgba(50, 50, 50, 0.95) !important;
}
/* 通用标题和文本颜色 */
.dark h1,
.dark h2,
.dark h3,
.dark h4,
.dark h5,
.dark h6 {
color: #e5e5e7;
}
/* 通用容器和组件类 */
.dark .card-header,
.dark .hint,
.dark .label {
color: #e5e5e7;
}
.dark .hint {
color: #a0a0a3;
}
/* 亮色模式下的通用样式 */
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: bold;
color: #1c1b1f;
}
.hint {
margin-bottom: 20px;
color: #909399;
font-size: 14px;
}
.label {
font-weight: bold;
margin-bottom: 10px;
color: #606266;
}
/* Material Design 3 卡片 - 暗黑模式统一样式 */
.dark .md3-card,
.dark .md3-card-elevated {
background: #1c1c1e;
border-color: #3a3a3c;
color: #e5e5e7;
}
/* Fluent Design 卡片 - 暗黑模式 */
.dark .fluent-card {
background: rgba(28, 28, 30, 0.8);
border-color: rgba(58, 58, 60, 0.5);
}
/* 通用按钮对齐 */
.ant-btn {
display: inline-flex;
align-items: center;
justify-content: center;
}
.ant-btn .anticon {
display: inline-flex;
align-items: center;
vertical-align: middle;
line-height: 1;
}
/* Ant Design Pagination */ /* Ant Design Pagination */
.ant-pagination-item-active { .ant-pagination-item-active {
border-color: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary);
-36
View File
@@ -335,13 +335,6 @@ onMounted(async () => {
margin: 0 auto; margin: 0 auto;
} }
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: bold;
}
.loading-container { .loading-container {
padding: 20px; padding: 20px;
} }
@@ -357,37 +350,8 @@ onMounted(async () => {
padding: 20px; padding: 20px;
} }
.hint {
margin-bottom: 20px;
color: #909399;
font-size: 14px;
}
.last-check-in { .last-check-in {
width: 100%; width: 100%;
margin-top: 20px; margin-top: 20px;
} }
.label {
font-weight: bold;
margin-bottom: 10px;
color: #606266;
}
.status-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
/* 修复按钮图标对齐 */
:deep(.ant-btn) {
display: inline-flex;
align-items: center;
justify-content: center;
}
:deep(.ant-btn .anticon) {
display: inline-flex;
align-items: center;
vertical-align: middle;
}
</style> </style>
+6 -13
View File
@@ -2,11 +2,11 @@
<Layout> <Layout>
<div class="settings-view"> <div class="settings-view">
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold text-gray-800 mb-6">个人设置</h1> <h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 mb-6">个人设置</h1>
<!-- 基本信息卡片 --> <!-- 基本信息卡片 -->
<div class="md3-card p-6 mb-6"> <div class="md3-card p-6 mb-6">
<h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center"> <h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
<UserOutlined class="mr-2" /> <UserOutlined class="mr-2" />
基本信息 基本信息
</h2> </h2>
@@ -32,7 +32,7 @@
<!-- 修改邮箱 --> <!-- 修改邮箱 -->
<div class="md3-card p-6 mb-6"> <div class="md3-card p-6 mb-6">
<h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center"> <h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
<EditOutlined class="mr-2" /> <EditOutlined class="mr-2" />
修改个人信息 修改个人信息
</h2> </h2>
@@ -77,7 +77,7 @@
<!-- 设置/修改密码 --> <!-- 设置/修改密码 -->
<div class="md3-card p-6"> <div class="md3-card p-6">
<h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center"> <h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
<KeyOutlined class="mr-2" /> <KeyOutlined class="mr-2" />
{{ hasPassword ? '修改密码' : '设置密码' }} {{ hasPassword ? '修改密码' : '设置密码' }}
</h2> </h2>
@@ -296,14 +296,7 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
.md3-card { .settings-view {
background: white; min-height: 100%;
border-radius: 12px;
box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.md3-card:hover {
box-shadow: 0 4px 8px 3px rgba(0, 0, 0, 0.10);
} }
</style> </style>
+13 -13
View File
@@ -7,7 +7,7 @@
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<div> <div>
<h1 class="text-4xl font-bold text-gradient mb-2">任务管理</h1> <h1 class="text-4xl font-bold text-gradient mb-2">任务管理</h1>
<p class="text-gray-600">管理您的自动打卡任务</p> <p class="text-gray-600 dark:text-gray-400">管理您的自动打卡任务</p>
</div> </div>
<a-button <a-button
type="primary" type="primary"
@@ -28,11 +28,11 @@
<div class="fluent-card p-6 animate-slide-up"> <div class="fluent-card p-6 animate-slide-up">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm text-gray-600 mb-1">总任务数</p> <p class="text-sm text-gray-600 dark:text-gray-400 mb-1">总任务数</p>
<p class="text-3xl font-bold text-primary-600">{{ taskStore.taskStats.total }}</p> <p class="text-3xl font-bold text-primary-600 dark:text-primary-400">{{ taskStore.taskStats.total }}</p>
</div> </div>
<div class="w-12 h-12 bg-primary-100 rounded-md3 flex items-center justify-center"> <div class="w-12 h-12 bg-primary-100 dark:bg-primary-900/30 rounded-md3 flex items-center justify-center">
<FileTextOutlined class="text-2xl text-primary-600" /> <FileTextOutlined class="text-2xl text-primary-600 dark:text-primary-400" />
</div> </div>
</div> </div>
</div> </div>
@@ -42,11 +42,11 @@
<div class="fluent-card p-6 animate-slide-up" style="animation-delay: 0.1s"> <div class="fluent-card p-6 animate-slide-up" style="animation-delay: 0.1s">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm text-gray-600 mb-1">启用中</p> <p class="text-sm text-gray-600 dark:text-gray-400 mb-1">启用中</p>
<p class="text-3xl font-bold text-green-600">{{ taskStore.taskStats.active }}</p> <p class="text-3xl font-bold text-green-600 dark:text-green-400">{{ taskStore.taskStats.active }}</p>
</div> </div>
<div class="w-12 h-12 bg-green-100 rounded-md3 flex items-center justify-center"> <div class="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-md3 flex items-center justify-center">
<CheckCircleOutlined class="text-2xl text-green-600" /> <CheckCircleOutlined class="text-2xl text-green-600 dark:text-green-400" />
</div> </div>
</div> </div>
</div> </div>
@@ -56,11 +56,11 @@
<div class="fluent-card p-6 animate-slide-up" style="animation-delay: 0.2s"> <div class="fluent-card p-6 animate-slide-up" style="animation-delay: 0.2s">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm text-gray-600 mb-1">已禁用</p> <p class="text-sm text-gray-600 dark:text-gray-400 mb-1">已禁用</p>
<p class="text-3xl font-bold text-gray-600">{{ taskStore.taskStats.inactive }}</p> <p class="text-3xl font-bold text-gray-600 dark:text-gray-300">{{ taskStore.taskStats.inactive }}</p>
</div> </div>
<div class="w-12 h-12 bg-gray-100 rounded-md3 flex items-center justify-center"> <div class="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-md3 flex items-center justify-center">
<StopOutlined class="text-2xl text-gray-600" /> <StopOutlined class="text-2xl text-gray-600 dark:text-gray-300" />
</div> </div>
</div> </div>
</div> </div>
+1
View File
@@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
darkMode: 'class', // 启用 class 模式的暗黑模式
content: [ content: [
"./index.html", "./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}", "./src/**/*.{vue,js,ts,jsx,tsx}",