From 7c164f6aeab6681e40b2a502e6a44ee0f753c17d Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 29 Apr 2026 07:45:20 +0800 Subject: [PATCH] feat(web): auth/ui/notifications pinia stores + theme composable --- web/src/composables/useTheme.ts | 10 +++++++++ web/src/stores/auth.ts | 34 ++++++++++++++++++++++++++++++ web/src/stores/notifications.ts | 27 ++++++++++++++++++++++++ web/src/stores/ui.ts | 37 +++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 web/src/composables/useTheme.ts create mode 100644 web/src/stores/auth.ts create mode 100644 web/src/stores/notifications.ts create mode 100644 web/src/stores/ui.ts diff --git a/web/src/composables/useTheme.ts b/web/src/composables/useTheme.ts new file mode 100644 index 0000000..d0b7154 --- /dev/null +++ b/web/src/composables/useTheme.ts @@ -0,0 +1,10 @@ +import { useUIStore } from '@/stores/ui' + +export function useTheme() { + const ui = useUIStore() + return { + mode: ui.themeMode, + isDark: ui.effectiveDark, + setMode: ui.setThemeMode, + } +} diff --git a/web/src/stores/auth.ts b/web/src/stores/auth.ts new file mode 100644 index 0000000..2319e6e --- /dev/null +++ b/web/src/stores/auth.ts @@ -0,0 +1,34 @@ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +export interface User { + id: string + email: string + display_name: string + is_admin: boolean +} + +export const useAuthStore = defineStore('auth', () => { + const token = ref(localStorage.getItem('token')) + const user = ref( + JSON.parse(localStorage.getItem('user') || 'null') as User | null, + ) + + const isAuthenticated = computed(() => !!token.value) + + function setSession(t: string, u: User) { + token.value = t + user.value = u + localStorage.setItem('token', t) + localStorage.setItem('user', JSON.stringify(u)) + } + + function clearSession() { + token.value = null + user.value = null + localStorage.removeItem('token') + localStorage.removeItem('user') + } + + return { token, user, isAuthenticated, setSession, clearSession } +}) diff --git a/web/src/stores/notifications.ts b/web/src/stores/notifications.ts new file mode 100644 index 0000000..45dd276 --- /dev/null +++ b/web/src/stores/notifications.ts @@ -0,0 +1,27 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export interface Notification { + id: string + topic: string + severity: 'info' | 'warning' | 'error' + title: string + body: string + link?: string + read_at?: string + created_at: string +} + +export const useNotificationsStore = defineStore('notifications', () => { + const items = ref([]) + const unreadCount = ref(0) + + function setItems(list: Notification[]) { + items.value = list + } + function setUnreadCount(n: number) { + unreadCount.value = n + } + + return { items, unreadCount, setItems, setUnreadCount } +}) diff --git a/web/src/stores/ui.ts b/web/src/stores/ui.ts new file mode 100644 index 0000000..1a823c8 --- /dev/null +++ b/web/src/stores/ui.ts @@ -0,0 +1,37 @@ +import { defineStore } from 'pinia' +import { computed, ref, watch } from 'vue' + +type ThemeMode = 'light' | 'dark' | 'system' + +export const useUIStore = defineStore('ui', () => { + const themeMode = ref( + (localStorage.getItem('themeMode') as ThemeMode) || 'system', + ) + const systemDark = ref(window.matchMedia('(prefers-color-scheme: dark)').matches) + + window + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', (e) => (systemDark.value = e.matches)) + + const effectiveDark = computed(() => + themeMode.value === 'system' ? systemDark.value : themeMode.value === 'dark', + ) + + watch( + effectiveDark, + (v) => document.documentElement.classList.toggle('dark', v), + { immediate: true }, + ) + + function setThemeMode(mode: ThemeMode) { + themeMode.value = mode + localStorage.setItem('themeMode', mode) + } + + const sidebarCollapsed = ref(localStorage.getItem('sidebarCollapsed') === 'true') + watch(sidebarCollapsed, (v) => + localStorage.setItem('sidebarCollapsed', String(v)), + ) + + return { themeMode, effectiveDark, setThemeMode, sidebarCollapsed } +})