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(() => appStore.siteLogo, (newLogo) => {
if (newLogo) {
updateFavicon(newLogo)
}
}, { immediate: true })
watch(
() => appStore.siteLogo,
(newLogo) => {
if (newLogo) {
updateFavicon(newLogo)
}
},
{ immediate: true }
)
watch(() => appStore.siteName, (newName) => {
if (newName) {
document.title = `${newName} - AI API Gateway`
}
}, { immediate: true })
watch(
() => appStore.siteName,
(newName) => {
if (newName) {
document.title = `${newName} - AI API Gateway`
}
},
{ immediate: true }
)
onMounted(async () => {
// Check if setup is needed

View File

@@ -53,9 +53,10 @@ export function useAccountOAuth() {
try {
const proxyConfig = proxyId ? { proxy_id: proxyId } : {}
const endpoint = addMethod === 'oauth'
? '/admin/accounts/generate-auth-url'
: '/admin/accounts/generate-setup-token-url'
const endpoint =
addMethod === 'oauth'
? '/admin/accounts/generate-auth-url'
: '/admin/accounts/generate-setup-token-url'
const response = await adminAPI.accounts.generateAuthUrl(endpoint, proxyConfig)
authUrl.value = response.auth_url
@@ -85,9 +86,10 @@ export function useAccountOAuth() {
try {
const proxyConfig = proxyId ? { proxy_id: proxyId } : {}
const endpoint = addMethod === 'oauth'
? '/admin/accounts/exchange-code'
: '/admin/accounts/exchange-setup-token-code'
const endpoint =
addMethod === 'oauth'
? '/admin/accounts/exchange-code'
: '/admin/accounts/exchange-setup-token-code'
const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, {
session_id: sessionId.value,
@@ -121,9 +123,10 @@ export function useAccountOAuth() {
try {
const proxyConfig = proxyId ? { proxy_id: proxyId } : {}
const endpoint = addMethod === 'oauth'
? '/admin/accounts/cookie-auth'
: '/admin/accounts/setup-token-cookie-auth'
const endpoint =
addMethod === 'oauth'
? '/admin/accounts/cookie-auth'
: '/admin/accounts/setup-token-cookie-auth'
const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, {
session_id: '',
@@ -142,7 +145,10 @@ export function useAccountOAuth() {
// Parse multiple session keys
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

View File

@@ -55,7 +55,10 @@ export function useOpenAIOAuth() {
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
sessionId.value = response.session_id
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.
**State:**
- `user: User | null` - Current authenticated user
- `token: string | null` - JWT authentication token
**Computed:**
- `isAuthenticated: boolean` - Whether user is currently authenticated
**Actions:**
- `login(credentials)` - Authenticate user with username/password
- `register(userData)` - Register new user account
- `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.
**State:**
- `sidebarCollapsed: boolean` - Sidebar collapsed state
- `loading: boolean` - Global loading state
- `toasts: Toast[]` - Active toast notifications
**Computed:**
- `hasActiveToasts: boolean` - Whether any toasts are active
**Actions:**
- `toggleSidebar()` - Toggle sidebar state
- `setSidebarCollapsed(collapsed)` - Set sidebar state explicitly
- `setLoading(isLoading)` - Set loading state
@@ -54,106 +60,104 @@ Manages global UI state including sidebar, loading indicators, and toast notific
### Auth Store
```typescript
import { useAuthStore } from '@/stores';
import { useAuthStore } from '@/stores'
// In component setup
const authStore = useAuthStore();
const authStore = useAuthStore()
// Initialize on app startup
authStore.checkAuth();
authStore.checkAuth()
// Login
try {
await authStore.login({ username: 'user', password: 'pass' });
console.log('Logged in:', authStore.user);
await authStore.login({ username: 'user', password: 'pass' })
console.log('Logged in:', authStore.user)
} catch (error) {
console.error('Login failed:', error);
console.error('Login failed:', error)
}
// Check authentication
if (authStore.isAuthenticated) {
console.log('User is logged in:', authStore.user?.username);
console.log('User is logged in:', authStore.user?.username)
}
// Logout
authStore.logout();
authStore.logout()
```
### App Store
```typescript
import { useAppStore } from '@/stores';
import { useAppStore } from '@/stores'
// In component setup
const appStore = useAppStore();
const appStore = useAppStore()
// Sidebar control
appStore.toggleSidebar();
appStore.setSidebarCollapsed(true);
appStore.toggleSidebar()
appStore.setSidebarCollapsed(true)
// Loading state
appStore.setLoading(true);
appStore.setLoading(true)
// ... do work
appStore.setLoading(false);
appStore.setLoading(false)
// Or use helper
await appStore.withLoading(async () => {
const data = await fetchData();
return data;
});
const data = await fetchData()
return data
})
// Toast notifications
appStore.showSuccess('Operation completed!');
appStore.showError('Something went wrong!', 5000);
appStore.showInfo('FYI: This is informational');
appStore.showWarning('Be careful!');
appStore.showSuccess('Operation completed!')
appStore.showError('Something went wrong!', 5000)
appStore.showInfo('FYI: This is informational')
appStore.showWarning('Be careful!')
// 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...
appStore.hideToast(toastId);
appStore.hideToast(toastId)
```
### Combined Usage in Vue Component
```vue
<script setup lang="ts">
import { useAuthStore, useAppStore } from '@/stores';
import { onMounted } from 'vue';
import { useAuthStore, useAppStore } from '@/stores'
import { onMounted } from 'vue'
const authStore = useAuthStore();
const appStore = useAppStore();
const authStore = useAuthStore()
const appStore = useAppStore()
onMounted(() => {
// Check for existing session
authStore.checkAuth();
});
authStore.checkAuth()
})
async function handleLogin(username: string, password: string) {
try {
await appStore.withLoading(async () => {
await authStore.login({ username, password });
});
appStore.showSuccess('Welcome back!');
await authStore.login({ username, password })
})
appStore.showSuccess('Welcome back!')
} catch (error) {
appStore.showError('Login failed. Please check your credentials.');
appStore.showError('Login failed. Please check your credentials.')
}
}
async function handleLogout() {
authStore.logout();
appStore.showInfo('You have been logged out.');
authStore.logout()
appStore.showInfo('You have been logged out.')
}
</script>
<template>
<div>
<button @click="appStore.toggleSidebar">
Toggle Sidebar
</button>
<button @click="appStore.toggleSidebar">Toggle Sidebar</button>
<div v-if="appStore.loading">Loading...</div>
<div v-if="authStore.isAuthenticated">
Welcome, {{ authStore.user?.username }}!
<button @click="handleLogout">Logout</button>
@@ -170,7 +174,6 @@ async function handleLogout() {
- **Auth Store**: Token and user data are automatically persisted to `localStorage`
- Keys: `auth_token`, `auth_user`
- Restored on `checkAuth()` call
- **App Store**: No persistence (UI state resets on page reload)
## TypeScript Support
@@ -178,7 +181,7 @@ async function handleLogout() {
All stores are fully typed with TypeScript. Import types from `@/types`:
```typescript
import type { User, Toast, ToastType } from '@/types';
import type { User, Toast, ToastType } from '@/types'
```
## Testing
@@ -187,8 +190,8 @@ Stores can be reset to initial state:
```typescript
// Auth store
authStore.logout(); // Clears all auth state
authStore.logout() // Clears all auth state
// 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
*/
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { Toast, ToastType, PublicSettings } from '@/types';
import { checkUpdates as checkUpdatesAPI, type VersionInfo, type ReleaseInfo } from '@/api/admin/system';
import { getPublicSettings as fetchPublicSettingsAPI } from '@/api/auth';
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Toast, ToastType, PublicSettings } from '@/types'
import {
checkUpdates as checkUpdatesAPI,
type VersionInfo,
type ReleaseInfo
} from '@/api/admin/system'
import { getPublicSettings as fetchPublicSettingsAPI } from '@/api/auth'
export const useAppStore = defineStore('app', () => {
// ==================== State ====================
const sidebarCollapsed = ref<boolean>(false);
const mobileOpen = ref<boolean>(false);
const loading = ref<boolean>(false);
const toasts = ref<Toast[]>([]);
const sidebarCollapsed = ref<boolean>(false)
const mobileOpen = ref<boolean>(false)
const loading = ref<boolean>(false)
const toasts = ref<Toast[]>([])
// Public settings cache state
const publicSettingsLoaded = ref<boolean>(false);
const publicSettingsLoading = ref<boolean>(false);
const siteName = ref<string>('Sub2API');
const siteLogo = ref<string>('');
const siteVersion = ref<string>('');
const contactInfo = ref<string>('');
const apiBaseUrl = ref<string>('');
const docUrl = ref<string>('');
const publicSettingsLoaded = ref<boolean>(false)
const publicSettingsLoading = ref<boolean>(false)
const siteName = ref<string>('Sub2API')
const siteLogo = ref<string>('')
const siteVersion = ref<string>('')
const contactInfo = ref<string>('')
const apiBaseUrl = ref<string>('')
const docUrl = ref<string>('')
// Version cache state
const versionLoaded = ref<boolean>(false);
const versionLoading = ref<boolean>(false);
const currentVersion = ref<string>('');
const latestVersion = ref<string>('');
const hasUpdate = ref<boolean>(false);
const buildType = ref<string>('source');
const releaseInfo = ref<ReleaseInfo | null>(null);
const versionLoaded = ref<boolean>(false)
const versionLoading = ref<boolean>(false)
const currentVersion = ref<string>('')
const latestVersion = ref<string>('')
const hasUpdate = ref<boolean>(false)
const buildType = ref<string>('source')
const releaseInfo = ref<ReleaseInfo | null>(null)
// Auto-incrementing ID for toasts
let toastIdCounter = 0;
let toastIdCounter = 0
// ==================== Computed ====================
const hasActiveToasts = computed(() => toasts.value.length > 0);
const loadingCount = ref<number>(0);
const hasActiveToasts = computed(() => toasts.value.length > 0)
const loadingCount = ref<number>(0)
// ==================== Actions ====================
@@ -51,7 +55,7 @@ export const useAppStore = defineStore('app', () => {
* Toggle sidebar collapsed state
*/
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
*/
function setSidebarCollapsed(collapsed: boolean): void {
sidebarCollapsed.value = collapsed;
sidebarCollapsed.value = collapsed
}
/**
* Toggle mobile sidebar open state
*/
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
*/
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 {
if (isLoading) {
loadingCount.value++;
loadingCount.value++
} 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)
* @returns Toast ID for manual dismissal
*/
function showToast(
type: ToastType,
message: string,
duration?: number
): string {
const id = `toast-${++toastIdCounter}`;
function showToast(type: ToastType, message: string, duration?: number): string {
const id = `toast-${++toastIdCounter}`
const toast: Toast = {
id,
type,
message,
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
if (duration !== undefined) {
setTimeout(() => {
hideToast(id);
}, duration);
hideToast(id)
}, duration)
}
return id;
return id
}
/**
@@ -129,7 +129,7 @@ export const useAppStore = defineStore('app', () => {
* @param duration - Auto-dismiss duration in ms (default: 3000)
*/
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)
*/
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)
*/
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)
*/
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
*/
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) {
toasts.value.splice(index, 1);
toasts.value.splice(index, 1)
}
}
@@ -174,7 +174,7 @@ export const useAppStore = defineStore('app', () => {
* Clear all toasts
*/
function clearAllToasts(): void {
toasts.value = [];
toasts.value = []
}
/**
@@ -184,11 +184,11 @@ export const useAppStore = defineStore('app', () => {
* @returns Promise resolving to operation result
*/
async function withLoading<T>(operation: () => Promise<T>): Promise<T> {
setLoading(true);
setLoading(true)
try {
return await operation();
return await operation()
} finally {
setLoading(false);
setLoading(false)
}
}
@@ -203,18 +203,15 @@ export const useAppStore = defineStore('app', () => {
operation: () => Promise<T>,
errorMessage?: string
): Promise<T | null> {
setLoading(true);
setLoading(true)
try {
return await operation();
return await operation()
} catch (error) {
const message =
errorMessage ||
(error as { message?: string }).message ||
'An error occurred';
showError(message);
return null;
const message = errorMessage || (error as { message?: string }).message || 'An error occurred'
showError(message)
return null
} finally {
setLoading(false);
setLoading(false)
}
}
@@ -223,10 +220,10 @@ export const useAppStore = defineStore('app', () => {
* Useful for cleanup or testing
*/
function reset(): void {
sidebarCollapsed.value = false;
loading.value = false;
loadingCount.value = 0;
toasts.value = [];
sidebarCollapsed.value = false
loading.value = false
loadingCount.value = 0
toasts.value = []
}
// ==================== Version Management ====================
@@ -244,30 +241,30 @@ export const useAppStore = defineStore('app', () => {
has_update: hasUpdate.value,
build_type: buildType.value,
release_info: releaseInfo.value || undefined,
cached: true,
};
cached: true
}
}
// Prevent duplicate requests
if (versionLoading.value) {
return null;
return null
}
versionLoading.value = true;
versionLoading.value = true
try {
const data = await checkUpdatesAPI(force);
currentVersion.value = data.current_version;
latestVersion.value = data.latest_version;
hasUpdate.value = data.has_update;
buildType.value = data.build_type || 'source';
releaseInfo.value = data.release_info || null;
versionLoaded.value = true;
return data;
const data = await checkUpdatesAPI(force)
currentVersion.value = data.current_version
latestVersion.value = data.latest_version
hasUpdate.value = data.has_update
buildType.value = data.build_type || 'source'
releaseInfo.value = data.release_info || null
versionLoaded.value = true
return data
} catch (error) {
console.error('Failed to fetch version:', error);
return null;
console.error('Failed to fetch version:', error)
return null
} finally {
versionLoading.value = false;
versionLoading.value = false
}
}
@@ -275,8 +272,8 @@ export const useAppStore = defineStore('app', () => {
* Clear version cache (e.g., after update)
*/
function clearVersionCache(): void {
versionLoaded.value = false;
hasUpdate.value = false;
versionLoaded.value = false
hasUpdate.value = false
}
// ==================== Public Settings Management ====================
@@ -299,31 +296,31 @@ export const useAppStore = defineStore('app', () => {
api_base_url: apiBaseUrl.value,
contact_info: contactInfo.value,
doc_url: docUrl.value,
version: siteVersion.value,
};
version: siteVersion.value
}
}
// Prevent duplicate requests
if (publicSettingsLoading.value) {
return null;
return null
}
publicSettingsLoading.value = true;
publicSettingsLoading.value = true
try {
const data = await fetchPublicSettingsAPI();
siteName.value = data.site_name || 'Sub2API';
siteLogo.value = data.site_logo || '';
siteVersion.value = data.version || '';
contactInfo.value = data.contact_info || '';
apiBaseUrl.value = data.api_base_url || '';
docUrl.value = data.doc_url || '';
publicSettingsLoaded.value = true;
return data;
const data = await fetchPublicSettingsAPI()
siteName.value = data.site_name || 'Sub2API'
siteLogo.value = data.site_logo || ''
siteVersion.value = data.version || ''
contactInfo.value = data.contact_info || ''
apiBaseUrl.value = data.api_base_url || ''
docUrl.value = data.doc_url || ''
publicSettingsLoaded.value = true
return data
} catch (error) {
console.error('Failed to fetch public settings:', error);
return null;
console.error('Failed to fetch public settings:', error)
return null
} finally {
publicSettingsLoading.value = false;
publicSettingsLoading.value = false
}
}
@@ -331,7 +328,7 @@ export const useAppStore = defineStore('app', () => {
* Clear public settings cache
*/
function clearPublicSettingsCache(): void {
publicSettingsLoaded.value = false;
publicSettingsLoaded.value = false
}
// ==================== Return Store API ====================
@@ -387,6 +384,6 @@ export const useAppStore = defineStore('app', () => {
// Public settings actions
fetchPublicSettings,
clearPublicSettingsCache,
};
});
clearPublicSettingsCache
}
})

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
}
html {
@apply antialiased scroll-smooth;
@apply scroll-smooth antialiased;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
@@ -21,7 +21,7 @@
/* 自定义滚动条 */
::-webkit-scrollbar {
@apply w-2 h-2;
@apply h-2 w-2;
}
::-webkit-scrollbar-track {
@@ -29,7 +29,7 @@
}
::-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 {
@@ -46,10 +46,10 @@
/* ============ 按钮样式 ============ */
.btn {
@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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500/50;
@apply disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none;
@apply focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2;
@apply disabled:transform-none disabled:cursor-not-allowed disabled:opacity-50;
@apply active:scale-[0.98];
}
@@ -80,53 +80,53 @@
}
.btn-sm {
@apply px-3 py-1.5 text-xs rounded-lg;
@apply rounded-lg px-3 py-1.5 text-xs;
}
.btn-lg {
@apply px-6 py-3 text-base rounded-2xl;
@apply rounded-2xl px-6 py-3 text-base;
}
.btn-icon {
@apply p-2.5 rounded-xl;
@apply rounded-xl p-2.5;
}
/* ============ 输入框样式 ============ */
.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 border border-gray-200 dark:border-dark-600;
@apply text-gray-900 dark:text-gray-100;
@apply placeholder:text-gray-400 dark:placeholder:text-dark-400;
@apply transition-all duration-200;
@apply focus:outline-none focus:ring-2 focus:ring-primary-500/30 focus:border-primary-500;
@apply disabled:bg-gray-100 dark:disabled:bg-dark-900 disabled:cursor-not-allowed;
@apply focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/30;
@apply disabled:cursor-not-allowed disabled:bg-gray-100 dark:disabled:bg-dark-900;
}
.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 {
@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 {
@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 {
@apply text-xs text-red-500 mt-1;
@apply mt-1 text-xs text-red-500;
}
/* Hide number input spinner buttons for cleaner UI */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
input[type='number'] {
-moz-appearance: textfield;
}
@@ -140,7 +140,7 @@
}
.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;
}
@@ -158,7 +158,7 @@
}
.stat-icon {
@apply w-12 h-12 rounded-xl;
@apply h-12 w-12 rounded-xl;
@apply flex items-center justify-center;
@apply text-xl;
}
@@ -188,7 +188,7 @@
}
.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 {
@@ -233,7 +233,7 @@
/* ============ 徽章样式 ============ */
.badge {
@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 {
@@ -264,7 +264,7 @@
@apply border border-gray-200 dark:border-dark-700;
@apply shadow-lg;
@apply py-1;
@apply animate-scale-in origin-top-right;
@apply origin-top-right animate-scale-in;
}
.dropdown-item {
@@ -290,7 +290,7 @@
}
.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;
}
@@ -303,13 +303,13 @@
}
.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;
}
/* ============ 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 bg-white dark:bg-dark-800;
@apply rounded-xl shadow-lg;
@@ -350,11 +350,11 @@
}
.sidebar-nav {
@apply flex-1 overflow-y-auto py-4 px-3;
@apply flex-1 overflow-y-auto px-3 py-4;
}
.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-gray-600 dark:text-dark-300;
@apply transition-all duration-200;
@@ -373,7 +373,7 @@
}
.sidebar-section-title {
@apply px-3 mb-2;
@apply mb-2 px-3;
@apply text-xs font-semibold uppercase tracking-wider;
@apply text-gray-400 dark:text-dark-500;
}
@@ -388,51 +388,51 @@
}
.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 {
@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;
}
.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;
}
.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 {
@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 {
@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;
}
.skeleton {
@apply bg-gray-200 dark:bg-dark-700 rounded animate-pulse;
@apply animate-pulse rounded bg-gray-200 dark:bg-dark-700;
}
/* ============ 分隔线 ============ */
.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 {
@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 {
@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 transition-all duration-200;
@apply hover:text-gray-900 dark:hover:text-white;
@@ -446,7 +446,7 @@
/* ============ 进度条 ============ */
.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 {
@@ -456,7 +456,7 @@
/* ============ 开关 ============ */
.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 transition-colors duration-200;
}
@@ -466,7 +466,7 @@
}
.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 transition-transform duration-200;
}
@@ -479,14 +479,14 @@
.code {
@apply font-mono text-sm;
@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;
}
.code-block {
@apply font-mono text-sm;
@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 {
@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
* @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 ''
const d = new Date(date)