diff --git a/apps/new-frontend/package.json b/apps/new-frontend/package.json
index 056a30d..1577922 100644
--- a/apps/new-frontend/package.json
+++ b/apps/new-frontend/package.json
@@ -16,9 +16,11 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.2.4",
+ "@vueuse/core": "^14.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^1.0.0",
+ "reka-ui": "^2.9.6",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.4",
"vue": "^3.5.33"
diff --git a/apps/new-frontend/pnpm-lock.yaml b/apps/new-frontend/pnpm-lock.yaml
index 48a120a..0d470d8 100644
--- a/apps/new-frontend/pnpm-lock.yaml
+++ b/apps/new-frontend/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
'@tailwindcss/vite':
specifier: ^4.2.4
version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1))
+ '@vueuse/core':
+ specifier: ^14.3.0
+ version: 14.3.0(vue@3.5.33(typescript@6.0.3))
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -20,6 +23,9 @@ importers:
lucide-vue-next:
specifier: ^1.0.0
version: 1.0.0(vue@3.5.33(typescript@6.0.3))
+ reka-ui:
+ specifier: ^2.9.6
+ version: 2.9.6(vue@3.5.33(typescript@6.0.3))
tailwind-merge:
specifier: ^3.5.0
version: 3.5.0
@@ -139,6 +145,18 @@ packages:
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@floating-ui/core@1.7.5':
+ resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
+
+ '@floating-ui/dom@1.7.6':
+ resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
+
+ '@floating-ui/utils@0.2.11':
+ resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+
+ '@floating-ui/vue@1.1.11':
+ resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==}
+
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
@@ -159,6 +177,12 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
+ '@internationalized/date@3.12.1':
+ resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==}
+
+ '@internationalized/number@3.6.6':
+ resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==}
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -289,6 +313,9 @@ packages:
'@rolldown/pluginutils@1.0.0-rc.17':
resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==}
+ '@swc/helpers@0.5.21':
+ resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==}
+
'@tailwindcss/node@4.2.4':
resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==}
@@ -383,6 +410,14 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
+ '@tanstack/virtual-core@3.14.0':
+ resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==}
+
+ '@tanstack/vue-virtual@3.13.24':
+ resolution: {integrity: sha512-A0k2qF0zFSUStXSZkGXABouXr2Tw2Ztl/cVIYG9qy84uR8W7UNjAcX3DvzBS3YnDcwvLxab8v7dbmYBZ39itDA==}
+ peerDependencies:
+ vue: ^2.7.0 || ^3.0.0
+
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -398,6 +433,9 @@ packages:
'@types/node@25.6.0':
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
+ '@types/web-bluetooth@0.0.21':
+ resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
+
'@typescript-eslint/parser@8.59.1':
resolution: {integrity: sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -500,6 +538,19 @@ packages:
vue:
optional: true
+ '@vueuse/core@14.3.0':
+ resolution: {integrity: sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==}
+ peerDependencies:
+ vue: ^3.5.0
+
+ '@vueuse/metadata@14.3.0':
+ resolution: {integrity: sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==}
+
+ '@vueuse/shared@14.3.0':
+ resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==}
+ peerDependencies:
+ vue: ^3.5.0
+
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -523,6 +574,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ aria-hidden@1.2.6:
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+ engines: {node: '>=10'}
+
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -589,6 +644,9 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ defu@6.1.7:
+ resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
+
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -912,6 +970,9 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+ ohash@2.0.11:
+ resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -971,6 +1032,11 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
+ reka-ui@2.9.6:
+ resolution: {integrity: sha512-K6bL457owpvWONc7hsjFxo3HDC9s6IzhRqShW0w9JSKelPGfRbkHD558UQTn/NH1cvrXVHygKyC7fExFmRketg==}
+ peerDependencies:
+ vue: '>= 3.4.0'
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -1099,6 +1165,17 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
+ vue-demi@0.14.10:
+ resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
+ engines: {node: '>=12'}
+ hasBin: true
+ peerDependencies:
+ '@vue/composition-api': ^1.0.0-rc.1
+ vue: ^3.0.0-0 || ^2.6.0
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+
vue-eslint-parser@10.4.0:
resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1213,6 +1290,26 @@ snapshots:
'@eslint/core': 0.17.0
levn: 0.4.1
+ '@floating-ui/core@1.7.5':
+ dependencies:
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/dom@1.7.6':
+ dependencies:
+ '@floating-ui/core': 1.7.5
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/utils@0.2.11': {}
+
+ '@floating-ui/vue@1.1.11(vue@3.5.33(typescript@6.0.3))':
+ dependencies:
+ '@floating-ui/dom': 1.7.6
+ '@floating-ui/utils': 0.2.11
+ vue-demi: 0.14.10(vue@3.5.33(typescript@6.0.3))
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+ - vue
+
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/types': 0.15.0
@@ -1229,6 +1326,14 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
+ '@internationalized/date@3.12.1':
+ dependencies:
+ '@swc/helpers': 0.5.21
+
+ '@internationalized/number@3.6.6':
+ dependencies:
+ '@swc/helpers': 0.5.21
+
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -1312,6 +1417,10 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.17': {}
+ '@swc/helpers@0.5.21':
+ dependencies:
+ tslib: 2.8.1
+
'@tailwindcss/node@4.2.4':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -1380,6 +1489,13 @@ snapshots:
tailwindcss: 4.2.4
vite: 8.0.10(@types/node@25.6.0)(jiti@2.6.1)
+ '@tanstack/virtual-core@3.14.0': {}
+
+ '@tanstack/vue-virtual@3.13.24(vue@3.5.33(typescript@6.0.3))':
+ dependencies:
+ '@tanstack/virtual-core': 3.14.0
+ vue: 3.5.33(typescript@6.0.3)
+
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
@@ -1395,6 +1511,8 @@ snapshots:
dependencies:
undici-types: 7.19.2
+ '@types/web-bluetooth@0.0.21': {}
+
'@typescript-eslint/parser@8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.59.1
@@ -1543,6 +1661,19 @@ snapshots:
typescript: 6.0.3
vue: 3.5.33(typescript@6.0.3)
+ '@vueuse/core@14.3.0(vue@3.5.33(typescript@6.0.3))':
+ dependencies:
+ '@types/web-bluetooth': 0.0.21
+ '@vueuse/metadata': 14.3.0
+ '@vueuse/shared': 14.3.0(vue@3.5.33(typescript@6.0.3))
+ vue: 3.5.33(typescript@6.0.3)
+
+ '@vueuse/metadata@14.3.0': {}
+
+ '@vueuse/shared@14.3.0(vue@3.5.33(typescript@6.0.3))':
+ dependencies:
+ vue: 3.5.33(typescript@6.0.3)
+
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
@@ -1564,6 +1695,10 @@ snapshots:
argparse@2.0.1: {}
+ aria-hidden@1.2.6:
+ dependencies:
+ tslib: 2.8.1
+
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
@@ -1616,6 +1751,8 @@ snapshots:
deep-is@0.1.4: {}
+ defu@6.1.7: {}
+
detect-libc@2.1.2: {}
enhanced-resolve@5.21.0:
@@ -1900,6 +2037,8 @@ snapshots:
dependencies:
boolbase: 1.0.0
+ ohash@2.0.11: {}
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -1952,6 +2091,22 @@ snapshots:
punycode@2.3.1: {}
+ reka-ui@2.9.6(vue@3.5.33(typescript@6.0.3)):
+ dependencies:
+ '@floating-ui/dom': 1.7.6
+ '@floating-ui/vue': 1.1.11(vue@3.5.33(typescript@6.0.3))
+ '@internationalized/date': 3.12.1
+ '@internationalized/number': 3.6.6
+ '@tanstack/vue-virtual': 3.13.24(vue@3.5.33(typescript@6.0.3))
+ '@vueuse/core': 14.3.0(vue@3.5.33(typescript@6.0.3))
+ '@vueuse/shared': 14.3.0(vue@3.5.33(typescript@6.0.3))
+ aria-hidden: 1.2.6
+ defu: 6.1.7
+ ohash: 2.0.11
+ vue: 3.5.33(typescript@6.0.3)
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+
resolve-from@4.0.0: {}
rolldown@1.0.0-rc.17:
@@ -2010,8 +2165,7 @@ snapshots:
dependencies:
typescript: 6.0.3
- tslib@2.8.1:
- optional: true
+ tslib@2.8.1: {}
tw-animate-css@1.4.0: {}
@@ -2043,6 +2197,10 @@ snapshots:
vscode-uri@3.1.0: {}
+ vue-demi@0.14.10(vue@3.5.33(typescript@6.0.3)):
+ dependencies:
+ vue: 3.5.33(typescript@6.0.3)
+
vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1)):
dependencies:
debug: 4.4.3
diff --git a/apps/new-frontend/src/app/theme.ts b/apps/new-frontend/src/app/theme.ts
index 4bd48a4..99d9ad4 100644
--- a/apps/new-frontend/src/app/theme.ts
+++ b/apps/new-frontend/src/app/theme.ts
@@ -108,7 +108,7 @@ export function useTheme() {
modeLabel: computed(() => {
if (state.mode === 'light') return '亮色'
if (state.mode === 'dark') return '暗色'
- return state.resolved === 'dark' ? '跟随系统:暗色' : '跟随系统:亮色'
+ return '设备'
}),
setThemeMode,
cycleThemeMode,
diff --git a/apps/new-frontend/src/components/AppLayout.vue b/apps/new-frontend/src/components/AppLayout.vue
index 58c1358..d8e3607 100644
--- a/apps/new-frontend/src/components/AppLayout.vue
+++ b/apps/new-frontend/src/components/AppLayout.vue
@@ -13,6 +13,7 @@ import {
ScrollText,
Settings,
Shield,
+ Sun,
UserRound,
Users,
X,
@@ -20,7 +21,9 @@ import {
import { computed, ref } from 'vue'
import { useAuth } from '@/app/auth'
import { useRouter } from '@/app/router'
-import { useTheme } from '@/app/theme'
+import { type ThemeMode, useTheme } from '@/app/theme'
+import { Button } from '@/components/ui/button'
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
const { state: authState, isAdmin, logout } = useAuth()
const router = useRouter()
@@ -47,6 +50,18 @@ const isAdminRoute = computed(() => router.state.path.startsWith('/admin'))
const approvalLabel = computed(() => (authState.user?.is_approved ? '已审批' : '待审批'))
const roleLabel = computed(() => (isAdmin.value ? '管理员' : '普通用户'))
const themeLabel = computed(() => theme.modeLabel.value)
+const themeModes = [
+ { mode: 'light', label: '亮色', icon: Sun },
+ { mode: 'dark', label: '暗色', icon: MoonStar },
+ { mode: 'system', label: '设备', icon: Monitor },
+] as const
+
+function themeModeButtonClass(mode: ThemeMode) {
+ if (theme.state.mode === mode) {
+ return 'bg-zinc-900 text-white shadow-sm hover:bg-zinc-900 hover:text-white dark:bg-zinc-100 dark:text-zinc-950 dark:hover:bg-zinc-100 dark:hover:text-zinc-950'
+ }
+ return 'text-zinc-500 hover:bg-zinc-100 hover:text-zinc-950 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-50'
+}
function go(path: string) {
mobileOpen.value = false
@@ -64,16 +79,18 @@ function signOut() {