style(frontend): 统一核心模块代码风格

- Composables: 优化 OAuth 相关 hooks 代码格式
- Stores: 规范状态管理模块格式
- Types: 统一类型定义格式
- Utils: 优化工具函数格式
- App.vue & style.css: 调整全局样式和主组件格式
This commit is contained in:
ianshaw
2025-12-25 08:41:43 -08:00
parent 5763f5ced3
commit 01f990a5c9
10 changed files with 775 additions and 755 deletions

View File

@@ -26,17 +26,25 @@ function updateFavicon(logoUrl: string) {
} }
// Watch for site settings changes and update favicon/title // Watch for site settings changes and update favicon/title
watch(() => appStore.siteLogo, (newLogo) => { watch(
if (newLogo) { () => appStore.siteLogo,
updateFavicon(newLogo) (newLogo) => {
} if (newLogo) {
}, { immediate: true }) updateFavicon(newLogo)
}
},
{ immediate: true }
)
watch(() => appStore.siteName, (newName) => { watch(
if (newName) { () => appStore.siteName,
document.title = `${newName} - AI API Gateway` (newName) => {
} if (newName) {
}, { immediate: true }) document.title = `${newName} - AI API Gateway`
}
},
{ immediate: true }
)
onMounted(async () => { onMounted(async () => {
// Check if setup is needed // Check if setup is needed

View File

@@ -53,9 +53,10 @@ export function useAccountOAuth() {
try { try {
const proxyConfig = proxyId ? { proxy_id: proxyId } : {} const proxyConfig = proxyId ? { proxy_id: proxyId } : {}
const endpoint = addMethod === 'oauth' const endpoint =
? '/admin/accounts/generate-auth-url' addMethod === 'oauth'
: '/admin/accounts/generate-setup-token-url' ? '/admin/accounts/generate-auth-url'
: '/admin/accounts/generate-setup-token-url'
const response = await adminAPI.accounts.generateAuthUrl(endpoint, proxyConfig) const response = await adminAPI.accounts.generateAuthUrl(endpoint, proxyConfig)
authUrl.value = response.auth_url authUrl.value = response.auth_url
@@ -85,9 +86,10 @@ export function useAccountOAuth() {
try { try {
const proxyConfig = proxyId ? { proxy_id: proxyId } : {} const proxyConfig = proxyId ? { proxy_id: proxyId } : {}
const endpoint = addMethod === 'oauth' const endpoint =
? '/admin/accounts/exchange-code' addMethod === 'oauth'
: '/admin/accounts/exchange-setup-token-code' ? '/admin/accounts/exchange-code'
: '/admin/accounts/exchange-setup-token-code'
const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, { const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, {
session_id: sessionId.value, session_id: sessionId.value,
@@ -121,9 +123,10 @@ export function useAccountOAuth() {
try { try {
const proxyConfig = proxyId ? { proxy_id: proxyId } : {} const proxyConfig = proxyId ? { proxy_id: proxyId } : {}
const endpoint = addMethod === 'oauth' const endpoint =
? '/admin/accounts/cookie-auth' addMethod === 'oauth'
: '/admin/accounts/setup-token-cookie-auth' ? '/admin/accounts/cookie-auth'
: '/admin/accounts/setup-token-cookie-auth'
const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, { const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, {
session_id: '', session_id: '',
@@ -142,7 +145,10 @@ export function useAccountOAuth() {
// Parse multiple session keys // Parse multiple session keys
const parseSessionKeys = (input: string): string[] => { const parseSessionKeys = (input: string): string[] => {
return input.split('\n').map(k => k.trim()).filter(k => k) return input
.split('\n')
.map((k) => k.trim())
.filter((k) => k)
} }
// Build extra info from token response // Build extra info from token response

View File

@@ -55,7 +55,10 @@ export function useOpenAIOAuth() {
payload.redirect_uri = redirectUri payload.redirect_uri = redirectUri
} }
const response = await adminAPI.accounts.generateAuthUrl('/admin/openai/generate-auth-url', payload) const response = await adminAPI.accounts.generateAuthUrl(
'/admin/openai/generate-auth-url',
payload
)
authUrl.value = response.auth_url authUrl.value = response.auth_url
sessionId.value = response.session_id sessionId.value = response.session_id
return true return true

View File

@@ -9,13 +9,16 @@ This directory contains all Pinia stores for the Sub2API frontend application.
Manages user authentication state, login/logout, and token persistence. Manages user authentication state, login/logout, and token persistence.
**State:** **State:**
- `user: User | null` - Current authenticated user - `user: User | null` - Current authenticated user
- `token: string | null` - JWT authentication token - `token: string | null` - JWT authentication token
**Computed:** **Computed:**
- `isAuthenticated: boolean` - Whether user is currently authenticated - `isAuthenticated: boolean` - Whether user is currently authenticated
**Actions:** **Actions:**
- `login(credentials)` - Authenticate user with username/password - `login(credentials)` - Authenticate user with username/password
- `register(userData)` - Register new user account - `register(userData)` - Register new user account
- `logout()` - Clear authentication and logout - `logout()` - Clear authentication and logout
@@ -27,14 +30,17 @@ Manages user authentication state, login/logout, and token persistence.
Manages global UI state including sidebar, loading indicators, and toast notifications. Manages global UI state including sidebar, loading indicators, and toast notifications.
**State:** **State:**
- `sidebarCollapsed: boolean` - Sidebar collapsed state - `sidebarCollapsed: boolean` - Sidebar collapsed state
- `loading: boolean` - Global loading state - `loading: boolean` - Global loading state
- `toasts: Toast[]` - Active toast notifications - `toasts: Toast[]` - Active toast notifications
**Computed:** **Computed:**
- `hasActiveToasts: boolean` - Whether any toasts are active - `hasActiveToasts: boolean` - Whether any toasts are active
**Actions:** **Actions:**
- `toggleSidebar()` - Toggle sidebar state - `toggleSidebar()` - Toggle sidebar state
- `setSidebarCollapsed(collapsed)` - Set sidebar state explicitly - `setSidebarCollapsed(collapsed)` - Set sidebar state explicitly
- `setLoading(isLoading)` - Set loading state - `setLoading(isLoading)` - Set loading state
@@ -54,106 +60,104 @@ Manages global UI state including sidebar, loading indicators, and toast notific
### Auth Store ### Auth Store
```typescript ```typescript
import { useAuthStore } from '@/stores'; import { useAuthStore } from '@/stores'
// In component setup // In component setup
const authStore = useAuthStore(); const authStore = useAuthStore()
// Initialize on app startup // Initialize on app startup
authStore.checkAuth(); authStore.checkAuth()
// Login // Login
try { try {
await authStore.login({ username: 'user', password: 'pass' }); await authStore.login({ username: 'user', password: 'pass' })
console.log('Logged in:', authStore.user); console.log('Logged in:', authStore.user)
} catch (error) { } catch (error) {
console.error('Login failed:', error); console.error('Login failed:', error)
} }
// Check authentication // Check authentication
if (authStore.isAuthenticated) { if (authStore.isAuthenticated) {
console.log('User is logged in:', authStore.user?.username); console.log('User is logged in:', authStore.user?.username)
} }
// Logout // Logout
authStore.logout(); authStore.logout()
``` ```
### App Store ### App Store
```typescript ```typescript
import { useAppStore } from '@/stores'; import { useAppStore } from '@/stores'
// In component setup // In component setup
const appStore = useAppStore(); const appStore = useAppStore()
// Sidebar control // Sidebar control
appStore.toggleSidebar(); appStore.toggleSidebar()
appStore.setSidebarCollapsed(true); appStore.setSidebarCollapsed(true)
// Loading state // Loading state
appStore.setLoading(true); appStore.setLoading(true)
// ... do work // ... do work
appStore.setLoading(false); appStore.setLoading(false)
// Or use helper // Or use helper
await appStore.withLoading(async () => { await appStore.withLoading(async () => {
const data = await fetchData(); const data = await fetchData()
return data; return data
}); })
// Toast notifications // Toast notifications
appStore.showSuccess('Operation completed!'); appStore.showSuccess('Operation completed!')
appStore.showError('Something went wrong!', 5000); appStore.showError('Something went wrong!', 5000)
appStore.showInfo('FYI: This is informational'); appStore.showInfo('FYI: This is informational')
appStore.showWarning('Be careful!'); appStore.showWarning('Be careful!')
// Custom toast // Custom toast
const toastId = appStore.showToast('info', 'Custom message', undefined); // No auto-dismiss const toastId = appStore.showToast('info', 'Custom message', undefined) // No auto-dismiss
// Later... // Later...
appStore.hideToast(toastId); appStore.hideToast(toastId)
``` ```
### Combined Usage in Vue Component ### Combined Usage in Vue Component
```vue ```vue
<script setup lang="ts"> <script setup lang="ts">
import { useAuthStore, useAppStore } from '@/stores'; import { useAuthStore, useAppStore } from '@/stores'
import { onMounted } from 'vue'; import { onMounted } from 'vue'
const authStore = useAuthStore(); const authStore = useAuthStore()
const appStore = useAppStore(); const appStore = useAppStore()
onMounted(() => { onMounted(() => {
// Check for existing session // Check for existing session
authStore.checkAuth(); authStore.checkAuth()
}); })
async function handleLogin(username: string, password: string) { async function handleLogin(username: string, password: string) {
try { try {
await appStore.withLoading(async () => { await appStore.withLoading(async () => {
await authStore.login({ username, password }); await authStore.login({ username, password })
}); })
appStore.showSuccess('Welcome back!'); appStore.showSuccess('Welcome back!')
} catch (error) { } catch (error) {
appStore.showError('Login failed. Please check your credentials.'); appStore.showError('Login failed. Please check your credentials.')
} }
} }
async function handleLogout() { async function handleLogout() {
authStore.logout(); authStore.logout()
appStore.showInfo('You have been logged out.'); appStore.showInfo('You have been logged out.')
} }
</script> </script>
<template> <template>
<div> <div>
<button @click="appStore.toggleSidebar"> <button @click="appStore.toggleSidebar">Toggle Sidebar</button>
Toggle Sidebar
</button>
<div v-if="appStore.loading">Loading...</div> <div v-if="appStore.loading">Loading...</div>
<div v-if="authStore.isAuthenticated"> <div v-if="authStore.isAuthenticated">
Welcome, {{ authStore.user?.username }}! Welcome, {{ authStore.user?.username }}!
<button @click="handleLogout">Logout</button> <button @click="handleLogout">Logout</button>
@@ -170,7 +174,6 @@ async function handleLogout() {
- **Auth Store**: Token and user data are automatically persisted to `localStorage` - **Auth Store**: Token and user data are automatically persisted to `localStorage`
- Keys: `auth_token`, `auth_user` - Keys: `auth_token`, `auth_user`
- Restored on `checkAuth()` call - Restored on `checkAuth()` call
- **App Store**: No persistence (UI state resets on page reload) - **App Store**: No persistence (UI state resets on page reload)
## TypeScript Support ## TypeScript Support
@@ -178,7 +181,7 @@ async function handleLogout() {
All stores are fully typed with TypeScript. Import types from `@/types`: All stores are fully typed with TypeScript. Import types from `@/types`:
```typescript ```typescript
import type { User, Toast, ToastType } from '@/types'; import type { User, Toast, ToastType } from '@/types'
``` ```
## Testing ## Testing
@@ -187,8 +190,8 @@ Stores can be reset to initial state:
```typescript ```typescript
// Auth store // Auth store
authStore.logout(); // Clears all auth state authStore.logout() // Clears all auth state
// App store // App store
appStore.reset(); // Resets to defaults appStore.reset() // Resets to defaults
``` ```

View File

@@ -3,47 +3,51 @@
* Manages global UI state including sidebar, loading indicators, and toast notifications * Manages global UI state including sidebar, loading indicators, and toast notifications
*/ */
import { defineStore } from 'pinia'; import { defineStore } from 'pinia'
import { ref, computed } from 'vue'; import { ref, computed } from 'vue'
import type { Toast, ToastType, PublicSettings } from '@/types'; import type { Toast, ToastType, PublicSettings } from '@/types'
import { checkUpdates as checkUpdatesAPI, type VersionInfo, type ReleaseInfo } from '@/api/admin/system'; import {
import { getPublicSettings as fetchPublicSettingsAPI } from '@/api/auth'; checkUpdates as checkUpdatesAPI,
type VersionInfo,
type ReleaseInfo
} from '@/api/admin/system'
import { getPublicSettings as fetchPublicSettingsAPI } from '@/api/auth'
export const useAppStore = defineStore('app', () => { export const useAppStore = defineStore('app', () => {
// ==================== State ==================== // ==================== State ====================
const sidebarCollapsed = ref<boolean>(false); const sidebarCollapsed = ref<boolean>(false)
const mobileOpen = ref<boolean>(false); const mobileOpen = ref<boolean>(false)
const loading = ref<boolean>(false); const loading = ref<boolean>(false)
const toasts = ref<Toast[]>([]); const toasts = ref<Toast[]>([])
// Public settings cache state // Public settings cache state
const publicSettingsLoaded = ref<boolean>(false); const publicSettingsLoaded = ref<boolean>(false)
const publicSettingsLoading = ref<boolean>(false); const publicSettingsLoading = ref<boolean>(false)
const siteName = ref<string>('Sub2API'); const siteName = ref<string>('Sub2API')
const siteLogo = ref<string>(''); const siteLogo = ref<string>('')
const siteVersion = ref<string>(''); const siteVersion = ref<string>('')
const contactInfo = ref<string>(''); const contactInfo = ref<string>('')
const apiBaseUrl = ref<string>(''); const apiBaseUrl = ref<string>('')
const docUrl = ref<string>(''); const docUrl = ref<string>('')
// Version cache state // Version cache state
const versionLoaded = ref<boolean>(false); const versionLoaded = ref<boolean>(false)
const versionLoading = ref<boolean>(false); const versionLoading = ref<boolean>(false)
const currentVersion = ref<string>(''); const currentVersion = ref<string>('')
const latestVersion = ref<string>(''); const latestVersion = ref<string>('')
const hasUpdate = ref<boolean>(false); const hasUpdate = ref<boolean>(false)
const buildType = ref<string>('source'); const buildType = ref<string>('source')
const releaseInfo = ref<ReleaseInfo | null>(null); const releaseInfo = ref<ReleaseInfo | null>(null)
// Auto-incrementing ID for toasts // Auto-incrementing ID for toasts
let toastIdCounter = 0; let toastIdCounter = 0
// ==================== Computed ==================== // ==================== Computed ====================
const hasActiveToasts = computed(() => toasts.value.length > 0); const hasActiveToasts = computed(() => toasts.value.length > 0)
const loadingCount = ref<number>(0); const loadingCount = ref<number>(0)
// ==================== Actions ==================== // ==================== Actions ====================
@@ -51,7 +55,7 @@ export const useAppStore = defineStore('app', () => {
* Toggle sidebar collapsed state * Toggle sidebar collapsed state
*/ */
function toggleSidebar(): void { function toggleSidebar(): void {
sidebarCollapsed.value = !sidebarCollapsed.value; sidebarCollapsed.value = !sidebarCollapsed.value
} }
/** /**
@@ -59,14 +63,14 @@ export const useAppStore = defineStore('app', () => {
* @param collapsed - Whether sidebar should be collapsed * @param collapsed - Whether sidebar should be collapsed
*/ */
function setSidebarCollapsed(collapsed: boolean): void { function setSidebarCollapsed(collapsed: boolean): void {
sidebarCollapsed.value = collapsed; sidebarCollapsed.value = collapsed
} }
/** /**
* Toggle mobile sidebar open state * Toggle mobile sidebar open state
*/ */
function toggleMobileSidebar(): void { function toggleMobileSidebar(): void {
mobileOpen.value = !mobileOpen.value; mobileOpen.value = !mobileOpen.value
} }
/** /**
@@ -74,7 +78,7 @@ export const useAppStore = defineStore('app', () => {
* @param open - Whether mobile sidebar should be open * @param open - Whether mobile sidebar should be open
*/ */
function setMobileOpen(open: boolean): void { function setMobileOpen(open: boolean): void {
mobileOpen.value = open; mobileOpen.value = open
} }
/** /**
@@ -83,11 +87,11 @@ export const useAppStore = defineStore('app', () => {
*/ */
function setLoading(isLoading: boolean): void { function setLoading(isLoading: boolean): void {
if (isLoading) { if (isLoading) {
loadingCount.value++; loadingCount.value++
} else { } else {
loadingCount.value = Math.max(0, loadingCount.value - 1); loadingCount.value = Math.max(0, loadingCount.value - 1)
} }
loading.value = loadingCount.value > 0; loading.value = loadingCount.value > 0
} }
/** /**
@@ -97,30 +101,26 @@ export const useAppStore = defineStore('app', () => {
* @param duration - Auto-dismiss duration in ms (undefined = no auto-dismiss) * @param duration - Auto-dismiss duration in ms (undefined = no auto-dismiss)
* @returns Toast ID for manual dismissal * @returns Toast ID for manual dismissal
*/ */
function showToast( function showToast(type: ToastType, message: string, duration?: number): string {
type: ToastType, const id = `toast-${++toastIdCounter}`
message: string,
duration?: number
): string {
const id = `toast-${++toastIdCounter}`;
const toast: Toast = { const toast: Toast = {
id, id,
type, type,
message, message,
duration, duration,
startTime: duration !== undefined ? Date.now() : undefined, startTime: duration !== undefined ? Date.now() : undefined
}; }
toasts.value.push(toast); toasts.value.push(toast)
// Auto-dismiss if duration is specified // Auto-dismiss if duration is specified
if (duration !== undefined) { if (duration !== undefined) {
setTimeout(() => { setTimeout(() => {
hideToast(id); hideToast(id)
}, duration); }, duration)
} }
return id; return id
} }
/** /**
@@ -129,7 +129,7 @@ export const useAppStore = defineStore('app', () => {
* @param duration - Auto-dismiss duration in ms (default: 3000) * @param duration - Auto-dismiss duration in ms (default: 3000)
*/ */
function showSuccess(message: string, duration: number = 3000): string { function showSuccess(message: string, duration: number = 3000): string {
return showToast('success', message, duration); return showToast('success', message, duration)
} }
/** /**
@@ -138,7 +138,7 @@ export const useAppStore = defineStore('app', () => {
* @param duration - Auto-dismiss duration in ms (default: 5000) * @param duration - Auto-dismiss duration in ms (default: 5000)
*/ */
function showError(message: string, duration: number = 5000): string { function showError(message: string, duration: number = 5000): string {
return showToast('error', message, duration); return showToast('error', message, duration)
} }
/** /**
@@ -147,7 +147,7 @@ export const useAppStore = defineStore('app', () => {
* @param duration - Auto-dismiss duration in ms (default: 3000) * @param duration - Auto-dismiss duration in ms (default: 3000)
*/ */
function showInfo(message: string, duration: number = 3000): string { function showInfo(message: string, duration: number = 3000): string {
return showToast('info', message, duration); return showToast('info', message, duration)
} }
/** /**
@@ -156,7 +156,7 @@ export const useAppStore = defineStore('app', () => {
* @param duration - Auto-dismiss duration in ms (default: 4000) * @param duration - Auto-dismiss duration in ms (default: 4000)
*/ */
function showWarning(message: string, duration: number = 4000): string { function showWarning(message: string, duration: number = 4000): string {
return showToast('warning', message, duration); return showToast('warning', message, duration)
} }
/** /**
@@ -164,9 +164,9 @@ export const useAppStore = defineStore('app', () => {
* @param id - Toast ID to hide * @param id - Toast ID to hide
*/ */
function hideToast(id: string): void { function hideToast(id: string): void {
const index = toasts.value.findIndex((t) => t.id === id); const index = toasts.value.findIndex((t) => t.id === id)
if (index !== -1) { if (index !== -1) {
toasts.value.splice(index, 1); toasts.value.splice(index, 1)
} }
} }
@@ -174,7 +174,7 @@ export const useAppStore = defineStore('app', () => {
* Clear all toasts * Clear all toasts
*/ */
function clearAllToasts(): void { function clearAllToasts(): void {
toasts.value = []; toasts.value = []
} }
/** /**
@@ -184,11 +184,11 @@ export const useAppStore = defineStore('app', () => {
* @returns Promise resolving to operation result * @returns Promise resolving to operation result
*/ */
async function withLoading<T>(operation: () => Promise<T>): Promise<T> { async function withLoading<T>(operation: () => Promise<T>): Promise<T> {
setLoading(true); setLoading(true)
try { try {
return await operation(); return await operation()
} finally { } finally {
setLoading(false); setLoading(false)
} }
} }
@@ -203,18 +203,15 @@ export const useAppStore = defineStore('app', () => {
operation: () => Promise<T>, operation: () => Promise<T>,
errorMessage?: string errorMessage?: string
): Promise<T | null> { ): Promise<T | null> {
setLoading(true); setLoading(true)
try { try {
return await operation(); return await operation()
} catch (error) { } catch (error) {
const message = const message = errorMessage || (error as { message?: string }).message || 'An error occurred'
errorMessage || showError(message)
(error as { message?: string }).message || return null
'An error occurred';
showError(message);
return null;
} finally { } finally {
setLoading(false); setLoading(false)
} }
} }
@@ -223,10 +220,10 @@ export const useAppStore = defineStore('app', () => {
* Useful for cleanup or testing * Useful for cleanup or testing
*/ */
function reset(): void { function reset(): void {
sidebarCollapsed.value = false; sidebarCollapsed.value = false
loading.value = false; loading.value = false
loadingCount.value = 0; loadingCount.value = 0
toasts.value = []; toasts.value = []
} }
// ==================== Version Management ==================== // ==================== Version Management ====================
@@ -244,30 +241,30 @@ export const useAppStore = defineStore('app', () => {
has_update: hasUpdate.value, has_update: hasUpdate.value,
build_type: buildType.value, build_type: buildType.value,
release_info: releaseInfo.value || undefined, release_info: releaseInfo.value || undefined,
cached: true, cached: true
}; }
} }
// Prevent duplicate requests // Prevent duplicate requests
if (versionLoading.value) { if (versionLoading.value) {
return null; return null
} }
versionLoading.value = true; versionLoading.value = true
try { try {
const data = await checkUpdatesAPI(force); const data = await checkUpdatesAPI(force)
currentVersion.value = data.current_version; currentVersion.value = data.current_version
latestVersion.value = data.latest_version; latestVersion.value = data.latest_version
hasUpdate.value = data.has_update; hasUpdate.value = data.has_update
buildType.value = data.build_type || 'source'; buildType.value = data.build_type || 'source'
releaseInfo.value = data.release_info || null; releaseInfo.value = data.release_info || null
versionLoaded.value = true; versionLoaded.value = true
return data; return data
} catch (error) { } catch (error) {
console.error('Failed to fetch version:', error); console.error('Failed to fetch version:', error)
return null; return null
} finally { } finally {
versionLoading.value = false; versionLoading.value = false
} }
} }
@@ -275,8 +272,8 @@ export const useAppStore = defineStore('app', () => {
* Clear version cache (e.g., after update) * Clear version cache (e.g., after update)
*/ */
function clearVersionCache(): void { function clearVersionCache(): void {
versionLoaded.value = false; versionLoaded.value = false
hasUpdate.value = false; hasUpdate.value = false
} }
// ==================== Public Settings Management ==================== // ==================== Public Settings Management ====================
@@ -299,31 +296,31 @@ export const useAppStore = defineStore('app', () => {
api_base_url: apiBaseUrl.value, api_base_url: apiBaseUrl.value,
contact_info: contactInfo.value, contact_info: contactInfo.value,
doc_url: docUrl.value, doc_url: docUrl.value,
version: siteVersion.value, version: siteVersion.value
}; }
} }
// Prevent duplicate requests // Prevent duplicate requests
if (publicSettingsLoading.value) { if (publicSettingsLoading.value) {
return null; return null
} }
publicSettingsLoading.value = true; publicSettingsLoading.value = true
try { try {
const data = await fetchPublicSettingsAPI(); const data = await fetchPublicSettingsAPI()
siteName.value = data.site_name || 'Sub2API'; siteName.value = data.site_name || 'Sub2API'
siteLogo.value = data.site_logo || ''; siteLogo.value = data.site_logo || ''
siteVersion.value = data.version || ''; siteVersion.value = data.version || ''
contactInfo.value = data.contact_info || ''; contactInfo.value = data.contact_info || ''
apiBaseUrl.value = data.api_base_url || ''; apiBaseUrl.value = data.api_base_url || ''
docUrl.value = data.doc_url || ''; docUrl.value = data.doc_url || ''
publicSettingsLoaded.value = true; publicSettingsLoaded.value = true
return data; return data
} catch (error) { } catch (error) {
console.error('Failed to fetch public settings:', error); console.error('Failed to fetch public settings:', error)
return null; return null
} finally { } finally {
publicSettingsLoading.value = false; publicSettingsLoading.value = false
} }
} }
@@ -331,7 +328,7 @@ export const useAppStore = defineStore('app', () => {
* Clear public settings cache * Clear public settings cache
*/ */
function clearPublicSettingsCache(): void { function clearPublicSettingsCache(): void {
publicSettingsLoaded.value = false; publicSettingsLoaded.value = false
} }
// ==================== Return Store API ==================== // ==================== Return Store API ====================
@@ -387,6 +384,6 @@ export const useAppStore = defineStore('app', () => {
// Public settings actions // Public settings actions
fetchPublicSettings, fetchPublicSettings,
clearPublicSettingsCache, clearPublicSettingsCache
}; }
}); })

View File

@@ -3,31 +3,31 @@
* Manages user authentication state, login/logout, and token persistence * Manages user authentication state, login/logout, and token persistence
*/ */
import { defineStore } from 'pinia'; import { defineStore } from 'pinia'
import { ref, computed } from 'vue'; import { ref, computed } from 'vue'
import { authAPI } from '@/api'; import { authAPI } from '@/api'
import type { User, LoginRequest, RegisterRequest } from '@/types'; import type { User, LoginRequest, RegisterRequest } from '@/types'
const AUTH_TOKEN_KEY = 'auth_token'; const AUTH_TOKEN_KEY = 'auth_token'
const AUTH_USER_KEY = 'auth_user'; const AUTH_USER_KEY = 'auth_user'
const AUTO_REFRESH_INTERVAL = 60 * 1000; // 60 seconds const AUTO_REFRESH_INTERVAL = 60 * 1000 // 60 seconds
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
// ==================== State ==================== // ==================== State ====================
const user = ref<User | null>(null); const user = ref<User | null>(null)
const token = ref<string | null>(null); const token = ref<string | null>(null)
let refreshIntervalId: ReturnType<typeof setInterval> | null = null; let refreshIntervalId: ReturnType<typeof setInterval> | null = null
// ==================== Computed ==================== // ==================== Computed ====================
const isAuthenticated = computed(() => { const isAuthenticated = computed(() => {
return !!token.value && !!user.value; return !!token.value && !!user.value
}); })
const isAdmin = computed(() => { const isAdmin = computed(() => {
return user.value?.role === 'admin'; return user.value?.role === 'admin'
}); })
// ==================== Actions ==================== // ==================== Actions ====================
@@ -37,24 +37,24 @@ export const useAuthStore = defineStore('auth', () => {
* Also starts auto-refresh and immediately fetches latest user data * Also starts auto-refresh and immediately fetches latest user data
*/ */
function checkAuth(): void { function checkAuth(): void {
const savedToken = localStorage.getItem(AUTH_TOKEN_KEY); const savedToken = localStorage.getItem(AUTH_TOKEN_KEY)
const savedUser = localStorage.getItem(AUTH_USER_KEY); const savedUser = localStorage.getItem(AUTH_USER_KEY)
if (savedToken && savedUser) { if (savedToken && savedUser) {
try { try {
token.value = savedToken; token.value = savedToken
user.value = JSON.parse(savedUser); user.value = JSON.parse(savedUser)
// Immediately refresh user data from backend (async, don't block) // Immediately refresh user data from backend (async, don't block)
refreshUser().catch((error) => { refreshUser().catch((error) => {
console.error('Failed to refresh user on init:', error); console.error('Failed to refresh user on init:', error)
}); })
// Start auto-refresh interval // Start auto-refresh interval
startAutoRefresh(); startAutoRefresh()
} catch (error) { } catch (error) {
console.error('Failed to parse saved user data:', error); console.error('Failed to parse saved user data:', error)
clearAuth(); clearAuth()
} }
} }
} }
@@ -65,15 +65,15 @@ export const useAuthStore = defineStore('auth', () => {
*/ */
function startAutoRefresh(): void { function startAutoRefresh(): void {
// Clear existing interval if any // Clear existing interval if any
stopAutoRefresh(); stopAutoRefresh()
refreshIntervalId = setInterval(() => { refreshIntervalId = setInterval(() => {
if (token.value) { if (token.value) {
refreshUser().catch((error) => { refreshUser().catch((error) => {
console.error('Auto-refresh user failed:', error); console.error('Auto-refresh user failed:', error)
}); })
} }
}, AUTO_REFRESH_INTERVAL); }, AUTO_REFRESH_INTERVAL)
} }
/** /**
@@ -81,8 +81,8 @@ export const useAuthStore = defineStore('auth', () => {
*/ */
function stopAutoRefresh(): void { function stopAutoRefresh(): void {
if (refreshIntervalId) { if (refreshIntervalId) {
clearInterval(refreshIntervalId); clearInterval(refreshIntervalId)
refreshIntervalId = null; refreshIntervalId = null
} }
} }
@@ -94,24 +94,24 @@ export const useAuthStore = defineStore('auth', () => {
*/ */
async function login(credentials: LoginRequest): Promise<User> { async function login(credentials: LoginRequest): Promise<User> {
try { try {
const response = await authAPI.login(credentials); const response = await authAPI.login(credentials)
// Store token and user // Store token and user
token.value = response.access_token; token.value = response.access_token
user.value = response.user; user.value = response.user
// Persist to localStorage // Persist to localStorage
localStorage.setItem(AUTH_TOKEN_KEY, response.access_token); localStorage.setItem(AUTH_TOKEN_KEY, response.access_token)
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(response.user)); localStorage.setItem(AUTH_USER_KEY, JSON.stringify(response.user))
// Start auto-refresh interval // Start auto-refresh interval
startAutoRefresh(); startAutoRefresh()
return response.user; return response.user
} catch (error) { } catch (error) {
// Clear any partial state on error // Clear any partial state on error
clearAuth(); clearAuth()
throw error; throw error
} }
} }
@@ -123,24 +123,24 @@ export const useAuthStore = defineStore('auth', () => {
*/ */
async function register(userData: RegisterRequest): Promise<User> { async function register(userData: RegisterRequest): Promise<User> {
try { try {
const response = await authAPI.register(userData); const response = await authAPI.register(userData)
// Store token and user // Store token and user
token.value = response.access_token; token.value = response.access_token
user.value = response.user; user.value = response.user
// Persist to localStorage // Persist to localStorage
localStorage.setItem(AUTH_TOKEN_KEY, response.access_token); localStorage.setItem(AUTH_TOKEN_KEY, response.access_token)
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(response.user)); localStorage.setItem(AUTH_USER_KEY, JSON.stringify(response.user))
// Start auto-refresh interval // Start auto-refresh interval
startAutoRefresh(); startAutoRefresh()
return response.user; return response.user
} catch (error) { } catch (error) {
// Clear any partial state on error // Clear any partial state on error
clearAuth(); clearAuth()
throw error; throw error
} }
} }
@@ -150,10 +150,10 @@ export const useAuthStore = defineStore('auth', () => {
*/ */
function logout(): void { function logout(): void {
// Call API logout (client-side cleanup) // Call API logout (client-side cleanup)
authAPI.logout(); authAPI.logout()
// Clear state // Clear state
clearAuth(); clearAuth()
} }
/** /**
@@ -164,23 +164,23 @@ export const useAuthStore = defineStore('auth', () => {
*/ */
async function refreshUser(): Promise<User> { async function refreshUser(): Promise<User> {
if (!token.value) { if (!token.value) {
throw new Error('Not authenticated'); throw new Error('Not authenticated')
} }
try { try {
const updatedUser = await authAPI.getCurrentUser(); const updatedUser = await authAPI.getCurrentUser()
user.value = updatedUser; user.value = updatedUser
// Update localStorage
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(updatedUser));
return updatedUser; // Update localStorage
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(updatedUser))
return updatedUser
} catch (error) { } catch (error) {
// If refresh fails with 401, clear auth state // If refresh fails with 401, clear auth state
if ((error as { status?: number }).status === 401) { if ((error as { status?: number }).status === 401) {
clearAuth(); clearAuth()
} }
throw error; throw error
} }
} }
@@ -190,12 +190,12 @@ export const useAuthStore = defineStore('auth', () => {
*/ */
function clearAuth(): void { function clearAuth(): void {
// Stop auto-refresh // Stop auto-refresh
stopAutoRefresh(); stopAutoRefresh()
token.value = null; token.value = null
user.value = null; user.value = null
localStorage.removeItem(AUTH_TOKEN_KEY); localStorage.removeItem(AUTH_TOKEN_KEY)
localStorage.removeItem(AUTH_USER_KEY); localStorage.removeItem(AUTH_USER_KEY)
} }
// ==================== Return Store API ==================== // ==================== Return Store API ====================
@@ -214,6 +214,6 @@ export const useAuthStore = defineStore('auth', () => {
register, register,
logout, logout,
checkAuth, checkAuth,
refreshUser, refreshUser
}; }
}); })

View File

@@ -3,9 +3,9 @@
* Central export point for all application stores * Central export point for all application stores
*/ */
export { useAuthStore } from './auth'; export { useAuthStore } from './auth'
export { useAppStore } from './app'; export { useAppStore } from './app'
// Re-export types for convenience // Re-export types for convenience
export type { User, LoginRequest, RegisterRequest, AuthResponse } from '@/types'; export type { User, LoginRequest, RegisterRequest, AuthResponse } from '@/types'
export type { Toast, ToastType, AppState } from '@/types'; export type { Toast, ToastType, AppState } from '@/types'

View File

@@ -10,7 +10,7 @@
} }
html { html {
@apply antialiased scroll-smooth; @apply scroll-smooth antialiased;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
} }
@@ -21,7 +21,7 @@
/* 自定义滚动条 */ /* 自定义滚动条 */
::-webkit-scrollbar { ::-webkit-scrollbar {
@apply w-2 h-2; @apply h-2 w-2;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@@ -29,7 +29,7 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
@apply bg-gray-300 dark:bg-dark-600 rounded-full; @apply rounded-full bg-gray-300 dark:bg-dark-600;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
@@ -46,10 +46,10 @@
/* ============ 按钮样式 ============ */ /* ============ 按钮样式 ============ */
.btn { .btn {
@apply inline-flex items-center justify-center gap-2; @apply inline-flex items-center justify-center gap-2;
@apply px-4 py-2.5 rounded-xl font-medium text-sm; @apply rounded-xl px-4 py-2.5 text-sm font-medium;
@apply transition-all duration-200 ease-out; @apply transition-all duration-200 ease-out;
@apply focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500/50; @apply focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2;
@apply disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none; @apply disabled:transform-none disabled:cursor-not-allowed disabled:opacity-50;
@apply active:scale-[0.98]; @apply active:scale-[0.98];
} }
@@ -80,53 +80,53 @@
} }
.btn-sm { .btn-sm {
@apply px-3 py-1.5 text-xs rounded-lg; @apply rounded-lg px-3 py-1.5 text-xs;
} }
.btn-lg { .btn-lg {
@apply px-6 py-3 text-base rounded-2xl; @apply rounded-2xl px-6 py-3 text-base;
} }
.btn-icon { .btn-icon {
@apply p-2.5 rounded-xl; @apply rounded-xl p-2.5;
} }
/* ============ 输入框样式 ============ */ /* ============ 输入框样式 ============ */
.input { .input {
@apply w-full px-4 py-2.5 rounded-xl text-sm; @apply w-full rounded-xl px-4 py-2.5 text-sm;
@apply bg-white dark:bg-dark-800; @apply bg-white dark:bg-dark-800;
@apply border border-gray-200 dark:border-dark-600; @apply border border-gray-200 dark:border-dark-600;
@apply text-gray-900 dark:text-gray-100; @apply text-gray-900 dark:text-gray-100;
@apply placeholder:text-gray-400 dark:placeholder:text-dark-400; @apply placeholder:text-gray-400 dark:placeholder:text-dark-400;
@apply transition-all duration-200; @apply transition-all duration-200;
@apply focus:outline-none focus:ring-2 focus:ring-primary-500/30 focus:border-primary-500; @apply focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/30;
@apply disabled:bg-gray-100 dark:disabled:bg-dark-900 disabled:cursor-not-allowed; @apply disabled:cursor-not-allowed disabled:bg-gray-100 dark:disabled:bg-dark-900;
} }
.input-error { .input-error {
@apply border-red-500 focus:ring-red-500/30 focus:border-red-500; @apply border-red-500 focus:border-red-500 focus:ring-red-500/30;
} }
.input-label { .input-label {
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5; @apply mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300;
} }
.input-hint { .input-hint {
@apply text-xs text-gray-500 dark:text-dark-400 mt-1; @apply mt-1 text-xs text-gray-500 dark:text-dark-400;
} }
.input-error-text { .input-error-text {
@apply text-xs text-red-500 mt-1; @apply mt-1 text-xs text-red-500;
} }
/* Hide number input spinner buttons for cleaner UI */ /* Hide number input spinner buttons for cleaner UI */
input[type="number"]::-webkit-inner-spin-button, input[type='number']::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
margin: 0; margin: 0;
} }
input[type="number"] { input[type='number'] {
-moz-appearance: textfield; -moz-appearance: textfield;
} }
@@ -140,7 +140,7 @@
} }
.card-hover { .card-hover {
@apply hover:shadow-card-hover hover:-translate-y-0.5; @apply hover:-translate-y-0.5 hover:shadow-card-hover;
@apply hover:border-gray-200 dark:hover:border-dark-600; @apply hover:border-gray-200 dark:hover:border-dark-600;
} }
@@ -158,7 +158,7 @@
} }
.stat-icon { .stat-icon {
@apply w-12 h-12 rounded-xl; @apply h-12 w-12 rounded-xl;
@apply flex items-center justify-center; @apply flex items-center justify-center;
@apply text-xl; @apply text-xl;
} }
@@ -188,7 +188,7 @@
} }
.stat-trend { .stat-trend {
@apply text-xs font-medium flex items-center gap-1 mt-1; @apply mt-1 flex items-center gap-1 text-xs font-medium;
} }
.stat-trend-up { .stat-trend-up {
@@ -233,7 +233,7 @@
/* ============ 徽章样式 ============ */ /* ============ 徽章样式 ============ */
.badge { .badge {
@apply inline-flex items-center gap-1; @apply inline-flex items-center gap-1;
@apply px-2.5 py-0.5 rounded-full text-xs font-medium; @apply rounded-full px-2.5 py-0.5 text-xs font-medium;
} }
.badge-primary { .badge-primary {
@@ -264,7 +264,7 @@
@apply border border-gray-200 dark:border-dark-700; @apply border border-gray-200 dark:border-dark-700;
@apply shadow-lg; @apply shadow-lg;
@apply py-1; @apply py-1;
@apply animate-scale-in origin-top-right; @apply origin-top-right animate-scale-in;
} }
.dropdown-item { .dropdown-item {
@@ -290,7 +290,7 @@
} }
.modal-header { .modal-header {
@apply px-6 py-4 border-b border-gray-100 dark:border-dark-700; @apply border-b border-gray-100 px-6 py-4 dark:border-dark-700;
@apply flex items-center justify-between; @apply flex items-center justify-between;
} }
@@ -303,13 +303,13 @@
} }
.modal-footer { .modal-footer {
@apply px-6 py-4 border-t border-gray-100 dark:border-dark-700; @apply border-t border-gray-100 px-6 py-4 dark:border-dark-700;
@apply flex items-center justify-end gap-3; @apply flex items-center justify-end gap-3;
} }
/* ============ Toast 通知 ============ */ /* ============ Toast 通知 ============ */
.toast { .toast {
@apply fixed top-4 right-4 z-[100]; @apply fixed right-4 top-4 z-[100];
@apply min-w-[320px] max-w-md; @apply min-w-[320px] max-w-md;
@apply bg-white dark:bg-dark-800; @apply bg-white dark:bg-dark-800;
@apply rounded-xl shadow-lg; @apply rounded-xl shadow-lg;
@@ -350,11 +350,11 @@
} }
.sidebar-nav { .sidebar-nav {
@apply flex-1 overflow-y-auto py-4 px-3; @apply flex-1 overflow-y-auto px-3 py-4;
} }
.sidebar-link { .sidebar-link {
@apply flex items-center gap-3 px-3 py-2.5 rounded-xl; @apply flex items-center gap-3 rounded-xl px-3 py-2.5;
@apply text-sm font-medium; @apply text-sm font-medium;
@apply text-gray-600 dark:text-dark-300; @apply text-gray-600 dark:text-dark-300;
@apply transition-all duration-200; @apply transition-all duration-200;
@@ -373,7 +373,7 @@
} }
.sidebar-section-title { .sidebar-section-title {
@apply px-3 mb-2; @apply mb-2 px-3;
@apply text-xs font-semibold uppercase tracking-wider; @apply text-xs font-semibold uppercase tracking-wider;
@apply text-gray-400 dark:text-dark-500; @apply text-gray-400 dark:text-dark-500;
} }
@@ -388,51 +388,51 @@
} }
.page-description { .page-description {
@apply text-sm text-gray-500 dark:text-dark-400 mt-1; @apply mt-1 text-sm text-gray-500 dark:text-dark-400;
} }
/* ============ 空状态 ============ */ /* ============ 空状态 ============ */
.empty-state { .empty-state {
@apply flex flex-col items-center justify-center py-12 px-4; @apply flex flex-col items-center justify-center px-4 py-12;
@apply text-center; @apply text-center;
} }
.empty-state-icon { .empty-state-icon {
@apply w-16 h-16 mb-4; @apply mb-4 h-16 w-16;
@apply text-gray-300 dark:text-dark-600; @apply text-gray-300 dark:text-dark-600;
} }
.empty-state-title { .empty-state-title {
@apply text-lg font-medium text-gray-900 dark:text-white mb-1; @apply mb-1 text-lg font-medium text-gray-900 dark:text-white;
} }
.empty-state-description { .empty-state-description {
@apply text-sm text-gray-500 dark:text-dark-400 max-w-sm; @apply max-w-sm text-sm text-gray-500 dark:text-dark-400;
} }
/* ============ 加载状态 ============ */ /* ============ 加载状态 ============ */
.spinner { .spinner {
@apply w-5 h-5 border-2 border-current border-t-transparent rounded-full; @apply h-5 w-5 rounded-full border-2 border-current border-t-transparent;
@apply animate-spin; @apply animate-spin;
} }
.skeleton { .skeleton {
@apply bg-gray-200 dark:bg-dark-700 rounded animate-pulse; @apply animate-pulse rounded bg-gray-200 dark:bg-dark-700;
} }
/* ============ 分隔线 ============ */ /* ============ 分隔线 ============ */
.divider { .divider {
@apply h-px bg-gray-200 dark:bg-dark-700 my-4; @apply my-4 h-px bg-gray-200 dark:bg-dark-700;
} }
/* ============ 标签页 ============ */ /* ============ 标签页 ============ */
.tabs { .tabs {
@apply flex gap-1 p-1; @apply flex gap-1 p-1;
@apply bg-gray-100 dark:bg-dark-800 rounded-xl; @apply rounded-xl bg-gray-100 dark:bg-dark-800;
} }
.tab { .tab {
@apply px-4 py-2 rounded-lg text-sm font-medium; @apply rounded-lg px-4 py-2 text-sm font-medium;
@apply text-gray-600 dark:text-dark-400; @apply text-gray-600 dark:text-dark-400;
@apply transition-all duration-200; @apply transition-all duration-200;
@apply hover:text-gray-900 dark:hover:text-white; @apply hover:text-gray-900 dark:hover:text-white;
@@ -446,7 +446,7 @@
/* ============ 进度条 ============ */ /* ============ 进度条 ============ */
.progress { .progress {
@apply h-2 bg-gray-200 dark:bg-dark-700 rounded-full overflow-hidden; @apply h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-dark-700;
} }
.progress-bar { .progress-bar {
@@ -456,7 +456,7 @@
/* ============ 开关 ============ */ /* ============ 开关 ============ */
.switch { .switch {
@apply relative w-11 h-6 rounded-full cursor-pointer; @apply relative h-6 w-11 cursor-pointer rounded-full;
@apply bg-gray-300 dark:bg-dark-600; @apply bg-gray-300 dark:bg-dark-600;
@apply transition-colors duration-200; @apply transition-colors duration-200;
} }
@@ -466,7 +466,7 @@
} }
.switch-thumb { .switch-thumb {
@apply absolute top-0.5 left-0.5 w-5 h-5 rounded-full; @apply absolute left-0.5 top-0.5 h-5 w-5 rounded-full;
@apply bg-white shadow-sm; @apply bg-white shadow-sm;
@apply transition-transform duration-200; @apply transition-transform duration-200;
} }
@@ -479,14 +479,14 @@
.code { .code {
@apply font-mono text-sm; @apply font-mono text-sm;
@apply bg-gray-100 dark:bg-dark-800; @apply bg-gray-100 dark:bg-dark-800;
@apply px-1.5 py-0.5 rounded; @apply rounded px-1.5 py-0.5;
@apply text-primary-600 dark:text-primary-400; @apply text-primary-600 dark:text-primary-400;
} }
.code-block { .code-block {
@apply font-mono text-sm; @apply font-mono text-sm;
@apply bg-gray-900 text-gray-100; @apply bg-gray-900 text-gray-100;
@apply p-4 rounded-xl overflow-x-auto; @apply overflow-x-auto rounded-xl p-4;
} }
} }
@@ -498,7 +498,7 @@
/* 玻璃效果 */ /* 玻璃效果 */
.glass { .glass {
@apply bg-white/80 dark:bg-dark-800/80 backdrop-blur-xl; @apply bg-white/80 backdrop-blur-xl dark:bg-dark-800/80;
} }
/* 隐藏滚动条 */ /* 隐藏滚动条 */

File diff suppressed because it is too large Load Diff

View File

@@ -90,7 +90,10 @@ export function formatBytes(bytes: number, decimals: number = 2): string {
* @param format 格式字符串,支持 YYYY, MM, DD, HH, mm, ss * @param format 格式字符串,支持 YYYY, MM, DD, HH, mm, ss
* @returns 格式化后的日期字符串 * @returns 格式化后的日期字符串
*/ */
export function formatDate(date: string | Date | null | undefined, format: string = 'YYYY-MM-DD HH:mm:ss'): string { export function formatDate(
date: string | Date | null | undefined,
format: string = 'YYYY-MM-DD HH:mm:ss'
): string {
if (!date) return '' if (!date) return ''
const d = new Date(date) const d = new Date(date)