"vscode:/vscode.git/clone" did not exist on "421b4c0affb471c56c20955b065cfd64308abe82"
Commit 01f990a5 authored by ianshaw's avatar ianshaw
Browse files

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

- Composables: 优化 OAuth 相关 hooks 代码格式
- Stores: 规范状态管理模块格式
- Types: 统一类型定义格式
- Utils: 优化工具函数格式
- App.vue & style.css: 调整全局样式和主组件格式
parent 5763f5ce
...@@ -26,17 +26,25 @@ function updateFavicon(logoUrl: string) { ...@@ -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
......
...@@ -53,9 +53,10 @@ export function useAccountOAuth() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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
......
...@@ -55,7 +55,10 @@ export function useOpenAIOAuth() { ...@@ -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
......
...@@ -9,13 +9,16 @@ This directory contains all Pinia stores for the Sub2API frontend application. ...@@ -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. ...@@ -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 ...@@ -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() { ...@@ -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() { ...@@ -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: ...@@ -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
``` ```
...@@ -3,47 +3,51 @@ ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -387,6 +384,6 @@ export const useAppStore = defineStore('app', () => {
// Public settings actions // Public settings actions
fetchPublicSettings, fetchPublicSettings,
clearPublicSettingsCache, clearPublicSettingsCache
}; }
}); })
...@@ -3,31 +3,31 @@ ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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 // Update localStorage
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(updatedUser)); localStorage.setItem(AUTH_USER_KEY, JSON.stringify(updatedUser))
return 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', () => { ...@@ -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', () => { ...@@ -214,6 +214,6 @@ export const useAuthStore = defineStore('auth', () => {
register, register,
logout, logout,
checkAuth, checkAuth,
refreshUser, refreshUser
}; }
}); })
...@@ -3,9 +3,9 @@ ...@@ -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'
...@@ -10,7 +10,7 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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;
} }
/* 隐藏滚动条 */ /* 隐藏滚动条 */
......
...@@ -5,726 +5,726 @@ ...@@ -5,726 +5,726 @@
// ==================== User & Auth Types ==================== // ==================== User & Auth Types ====================
export interface User { export interface User {
id: number; id: number
username: string; username: string
wechat: string; wechat: string
notes: string; notes: string
email: string; email: string
role: 'admin' | 'user'; // User role for authorization role: 'admin' | 'user' // User role for authorization
balance: number; // User balance for API usage balance: number // User balance for API usage
concurrency: number; // Allowed concurrent requests concurrency: number // Allowed concurrent requests
status: 'active' | 'disabled'; // Account status status: 'active' | 'disabled' // Account status
allowed_groups: number[] | null; // Allowed group IDs (null = all non-exclusive groups) allowed_groups: number[] | null // Allowed group IDs (null = all non-exclusive groups)
subscriptions?: UserSubscription[]; // User's active subscriptions subscriptions?: UserSubscription[] // User's active subscriptions
created_at: string; created_at: string
updated_at: string; updated_at: string
} }
export interface LoginRequest { export interface LoginRequest {
email: string; email: string
password: string; password: string
turnstile_token?: string; turnstile_token?: string
} }
export interface RegisterRequest { export interface RegisterRequest {
email: string; email: string
password: string; password: string
verify_code?: string; verify_code?: string
turnstile_token?: string; turnstile_token?: string
} }
export interface SendVerifyCodeRequest { export interface SendVerifyCodeRequest {
email: string; email: string
turnstile_token?: string; turnstile_token?: string
} }
export interface SendVerifyCodeResponse { export interface SendVerifyCodeResponse {
message: string; message: string
countdown: number; countdown: number
} }
export interface PublicSettings { export interface PublicSettings {
registration_enabled: boolean; registration_enabled: boolean
email_verify_enabled: boolean; email_verify_enabled: boolean
turnstile_enabled: boolean; turnstile_enabled: boolean
turnstile_site_key: string; turnstile_site_key: string
site_name: string; site_name: string
site_logo: string; site_logo: string
site_subtitle: string; site_subtitle: string
api_base_url: string; api_base_url: string
contact_info: string; contact_info: string
doc_url: string; doc_url: string
version: string; version: string
} }
export interface AuthResponse { export interface AuthResponse {
access_token: string; access_token: string
token_type: string; token_type: string
user: User; user: User
} }
// ==================== Subscription Types ==================== // ==================== Subscription Types ====================
export interface Subscription { export interface Subscription {
id: number; id: number
user_id: number; user_id: number
name: string; name: string
url: string; url: string
type: 'clash' | 'v2ray' | 'surge' | 'quantumult' | 'shadowrocket'; type: 'clash' | 'v2ray' | 'surge' | 'quantumult' | 'shadowrocket'
update_interval: number; // in hours update_interval: number // in hours
last_updated: string | null; last_updated: string | null
node_count: number; node_count: number
is_active: boolean; is_active: boolean
created_at: string; created_at: string
updated_at: string; updated_at: string
} }
export interface CreateSubscriptionRequest { export interface CreateSubscriptionRequest {
name: string; name: string
url: string; url: string
type: Subscription['type']; type: Subscription['type']
update_interval?: number; update_interval?: number
} }
export interface UpdateSubscriptionRequest { export interface UpdateSubscriptionRequest {
name?: string; name?: string
url?: string; url?: string
type?: Subscription['type']; type?: Subscription['type']
update_interval?: number; update_interval?: number
is_active?: boolean; is_active?: boolean
} }
// ==================== Proxy Node Types ==================== // ==================== Proxy Node Types ====================
export interface ProxyNode { export interface ProxyNode {
id: number; id: number
subscription_id: number; subscription_id: number
name: string; name: string
type: 'ss' | 'ssr' | 'vmess' | 'vless' | 'trojan' | 'hysteria' | 'hysteria2'; type: 'ss' | 'ssr' | 'vmess' | 'vless' | 'trojan' | 'hysteria' | 'hysteria2'
server: string; server: string
port: number; port: number
config: Record<string, unknown>; // JSON configuration specific to proxy type config: Record<string, unknown> // JSON configuration specific to proxy type
latency: number | null; // in milliseconds latency: number | null // in milliseconds
last_checked: string | null; last_checked: string | null
is_available: boolean; is_available: boolean
created_at: string; created_at: string
updated_at: string; updated_at: string
} }
// ==================== Conversion Types ==================== // ==================== Conversion Types ====================
export interface ConversionRequest { export interface ConversionRequest {
subscription_ids: number[]; subscription_ids: number[]
target_type: 'clash' | 'v2ray' | 'surge' | 'quantumult' | 'shadowrocket'; target_type: 'clash' | 'v2ray' | 'surge' | 'quantumult' | 'shadowrocket'
filter?: { filter?: {
name_pattern?: string; name_pattern?: string
types?: ProxyNode['type'][]; types?: ProxyNode['type'][]
min_latency?: number; min_latency?: number
max_latency?: number; max_latency?: number
available_only?: boolean; available_only?: boolean
}; }
sort?: { sort?: {
by: 'name' | 'latency' | 'type'; by: 'name' | 'latency' | 'type'
order: 'asc' | 'desc'; order: 'asc' | 'desc'
}; }
} }
export interface ConversionResult { export interface ConversionResult {
url: string; // URL to download the converted subscription url: string // URL to download the converted subscription
expires_at: string; expires_at: string
node_count: number; node_count: number
} }
// ==================== Statistics Types ==================== // ==================== Statistics Types ====================
export interface SubscriptionStats { export interface SubscriptionStats {
subscription_id: number; subscription_id: number
total_nodes: number; total_nodes: number
available_nodes: number; available_nodes: number
avg_latency: number | null; avg_latency: number | null
by_type: Record<ProxyNode['type'], number>; by_type: Record<ProxyNode['type'], number>
last_update: string; last_update: string
} }
export interface UserStats { export interface UserStats {
total_subscriptions: number; total_subscriptions: number
total_nodes: number; total_nodes: number
active_subscriptions: number; active_subscriptions: number
total_conversions: number; total_conversions: number
last_conversion: string | null; last_conversion: string | null
} }
// ==================== API Response Types ==================== // ==================== API Response Types ====================
export interface ApiResponse<T = unknown> { export interface ApiResponse<T = unknown> {
code: number; code: number
message: string; message: string
data: T; data: T
} }
export interface ApiError { export interface ApiError {
detail: string; detail: string
code?: string; code?: string
field?: string; field?: string
} }
export interface PaginatedResponse<T> { export interface PaginatedResponse<T> {
items: T[]; items: T[]
total: number; total: number
page: number; page: number
page_size: number; page_size: number
pages: number; pages: number
} }
// ==================== UI State Types ==================== // ==================== UI State Types ====================
export type ToastType = 'success' | 'error' | 'info' | 'warning'; export type ToastType = 'success' | 'error' | 'info' | 'warning'
export interface Toast { export interface Toast {
id: string; id: string
type: ToastType; type: ToastType
message: string; message: string
title?: string; title?: string
duration?: number; // in milliseconds, undefined means no auto-dismiss duration?: number // in milliseconds, undefined means no auto-dismiss
startTime?: number; // timestamp when toast was created, for progress bar startTime?: number // timestamp when toast was created, for progress bar
} }
export interface AppState { export interface AppState {
sidebarCollapsed: boolean; sidebarCollapsed: boolean
loading: boolean; loading: boolean
toasts: Toast[]; toasts: Toast[]
} }
// ==================== Validation Types ==================== // ==================== Validation Types ====================
export interface ValidationError { export interface ValidationError {
field: string; field: string
message: string; message: string
} }
// ==================== Table/List Types ==================== // ==================== Table/List Types ====================
export interface SortConfig { export interface SortConfig {
key: string; key: string
order: 'asc' | 'desc'; order: 'asc' | 'desc'
} }
export interface FilterConfig { export interface FilterConfig {
[key: string]: string | number | boolean | null | undefined; [key: string]: string | number | boolean | null | undefined
} }
export interface PaginationConfig { export interface PaginationConfig {
page: number; page: number
page_size: number; page_size: number
} }
// ==================== API Key & Group Types ==================== // ==================== API Key & Group Types ====================
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini'; export type GroupPlatform = 'anthropic' | 'openai' | 'gemini'
export type SubscriptionType = 'standard' | 'subscription'; export type SubscriptionType = 'standard' | 'subscription'
export interface Group { export interface Group {
id: number; id: number
name: string; name: string
description: string | null; description: string | null
platform: GroupPlatform; platform: GroupPlatform
rate_multiplier: number; rate_multiplier: number
is_exclusive: boolean; is_exclusive: boolean
status: 'active' | 'inactive'; status: 'active' | 'inactive'
subscription_type: SubscriptionType; subscription_type: SubscriptionType
daily_limit_usd: number | null; daily_limit_usd: number | null
weekly_limit_usd: number | null; weekly_limit_usd: number | null
monthly_limit_usd: number | null; monthly_limit_usd: number | null
account_count?: number; account_count?: number
created_at: string; created_at: string
updated_at: string; updated_at: string
} }
export interface ApiKey { export interface ApiKey {
id: number; id: number
user_id: number; user_id: number
key: string; key: string
name: string; name: string
group_id: number | null; group_id: number | null
status: 'active' | 'inactive'; status: 'active' | 'inactive'
created_at: string; created_at: string
updated_at: string; updated_at: string
group?: Group; group?: Group
} }
export interface CreateApiKeyRequest { export interface CreateApiKeyRequest {
name: string; name: string
group_id?: number | null; group_id?: number | null
custom_key?: string; // 可选的自定义API Key custom_key?: string // 可选的自定义API Key
} }
export interface UpdateApiKeyRequest { export interface UpdateApiKeyRequest {
name?: string; name?: string
group_id?: number | null; group_id?: number | null
status?: 'active' | 'inactive'; status?: 'active' | 'inactive'
} }
export interface CreateGroupRequest { export interface CreateGroupRequest {
name: string; name: string
description?: string | null; description?: string | null
platform?: GroupPlatform; platform?: GroupPlatform
rate_multiplier?: number; rate_multiplier?: number
is_exclusive?: boolean; is_exclusive?: boolean
} }
export interface UpdateGroupRequest { export interface UpdateGroupRequest {
name?: string; name?: string
description?: string | null; description?: string | null
platform?: GroupPlatform; platform?: GroupPlatform
rate_multiplier?: number; rate_multiplier?: number
is_exclusive?: boolean; is_exclusive?: boolean
status?: 'active' | 'inactive'; status?: 'active' | 'inactive'
} }
// ==================== Account & Proxy Types ==================== // ==================== Account & Proxy Types ====================
export type AccountPlatform = 'anthropic' | 'openai'; export type AccountPlatform = 'anthropic' | 'openai' | 'gemini'
export type AccountType = 'oauth' | 'setup-token' | 'apikey'; export type AccountType = 'oauth' | 'setup-token' | 'apikey'
export type OAuthAddMethod = 'oauth' | 'setup-token'; export type OAuthAddMethod = 'oauth' | 'setup-token'
export type ProxyProtocol = 'http' | 'https' | 'socks5'; export type ProxyProtocol = 'http' | 'https' | 'socks5'
// Claude Model type (returned by /v1/models and account models API) // Claude Model type (returned by /v1/models and account models API)
export interface ClaudeModel { export interface ClaudeModel {
id: string; id: string
type: string; type: string
display_name: string; display_name: string
created_at: string; created_at: string
} }
export interface Proxy { export interface Proxy {
id: number; id: number
name: string; name: string
protocol: ProxyProtocol; protocol: ProxyProtocol
host: string; host: string
port: number; port: number
username: string | null; username: string | null
password?: string | null; password?: string | null
status: 'active' | 'inactive'; status: 'active' | 'inactive'
account_count?: number; // Number of accounts using this proxy account_count?: number // Number of accounts using this proxy
created_at: string; created_at: string
updated_at: string; updated_at: string
} }
export interface Account { export interface Account {
id: number; id: number
name: string; name: string
platform: AccountPlatform; platform: AccountPlatform
type: AccountType; type: AccountType
credentials?: Record<string, unknown>; credentials?: Record<string, unknown>
extra?: CodexUsageSnapshot & Record<string, unknown>; // Extra fields including Codex usage extra?: CodexUsageSnapshot & Record<string, unknown> // Extra fields including Codex usage
proxy_id: number | null; proxy_id: number | null
concurrency: number; concurrency: number
current_concurrency?: number; // Real-time concurrency count from Redis current_concurrency?: number // Real-time concurrency count from Redis
priority: number; priority: number
status: 'active' | 'inactive' | 'error'; status: 'active' | 'inactive' | 'error'
error_message: string | null; error_message: string | null
last_used_at: string | null; last_used_at: string | null
created_at: string; created_at: string
updated_at: string; updated_at: string
proxy?: Proxy; proxy?: Proxy
group_ids?: number[]; // Groups this account belongs to group_ids?: number[] // Groups this account belongs to
groups?: Group[]; // Preloaded group objects groups?: Group[] // Preloaded group objects
// Rate limit & scheduling fields // Rate limit & scheduling fields
schedulable: boolean; schedulable: boolean
rate_limited_at: string | null; rate_limited_at: string | null
rate_limit_reset_at: string | null; rate_limit_reset_at: string | null
overload_until: string | null; overload_until: string | null
// Session window fields (5-hour window) // Session window fields (5-hour window)
session_window_start: string | null; session_window_start: string | null
session_window_end: string | null; session_window_end: string | null
session_window_status: 'allowed' | 'allowed_warning' | 'rejected' | null; session_window_status: 'allowed' | 'allowed_warning' | 'rejected' | null
} }
// Account Usage types // Account Usage types
export interface WindowStats { export interface WindowStats {
requests: number; requests: number
tokens: number; tokens: number
cost: number; cost: number
} }
export interface UsageProgress { export interface UsageProgress {
utilization: number; // Percentage (0-100+, 100 = 100%) utilization: number // Percentage (0-100+, 100 = 100%)
resets_at: string | null; resets_at: string | null
remaining_seconds: number; remaining_seconds: number
window_stats?: WindowStats | null; // 窗口期统计(从窗口开始到当前的使用量) window_stats?: WindowStats | null // 窗口期统计(从窗口开始到当前的使用量)
} }
export interface AccountUsageInfo { export interface AccountUsageInfo {
updated_at: string | null; updated_at: string | null
five_hour: UsageProgress | null; five_hour: UsageProgress | null
seven_day: UsageProgress | null; seven_day: UsageProgress | null
seven_day_sonnet: UsageProgress | null; seven_day_sonnet: UsageProgress | null
} }
// OpenAI Codex usage snapshot (from response headers) // OpenAI Codex usage snapshot (from response headers)
export interface CodexUsageSnapshot { export interface CodexUsageSnapshot {
// Legacy fields (kept for backwards compatibility) // Legacy fields (kept for backwards compatibility)
// NOTE: The naming is ambiguous - actual window type is determined by window_minutes value // NOTE: The naming is ambiguous - actual window type is determined by window_minutes value
codex_primary_used_percent?: number; // Usage percentage (check window_minutes for actual window type) codex_primary_used_percent?: number // Usage percentage (check window_minutes for actual window type)
codex_primary_reset_after_seconds?: number; // Seconds until reset codex_primary_reset_after_seconds?: number // Seconds until reset
codex_primary_window_minutes?: number; // Window in minutes codex_primary_window_minutes?: number // Window in minutes
codex_secondary_used_percent?: number; // Usage percentage (check window_minutes for actual window type) codex_secondary_used_percent?: number // Usage percentage (check window_minutes for actual window type)
codex_secondary_reset_after_seconds?: number; // Seconds until reset codex_secondary_reset_after_seconds?: number // Seconds until reset
codex_secondary_window_minutes?: number; // Window in minutes codex_secondary_window_minutes?: number // Window in minutes
codex_primary_over_secondary_percent?: number; // Overflow ratio codex_primary_over_secondary_percent?: number // Overflow ratio
// Canonical fields (normalized by backend, use these preferentially) // Canonical fields (normalized by backend, use these preferentially)
codex_5h_used_percent?: number; // 5-hour window usage percentage codex_5h_used_percent?: number // 5-hour window usage percentage
codex_5h_reset_after_seconds?: number; // Seconds until 5h window reset codex_5h_reset_after_seconds?: number // Seconds until 5h window reset
codex_5h_window_minutes?: number; // 5h window in minutes (should be ~300) codex_5h_window_minutes?: number // 5h window in minutes (should be ~300)
codex_7d_used_percent?: number; // 7-day window usage percentage codex_7d_used_percent?: number // 7-day window usage percentage
codex_7d_reset_after_seconds?: number; // Seconds until 7d window reset codex_7d_reset_after_seconds?: number // Seconds until 7d window reset
codex_7d_window_minutes?: number; // 7d window in minutes (should be ~10080) codex_7d_window_minutes?: number // 7d window in minutes (should be ~10080)
codex_usage_updated_at?: string; // Last update timestamp codex_usage_updated_at?: string // Last update timestamp
} }
export interface CreateAccountRequest { export interface CreateAccountRequest {
name: string; name: string
platform: AccountPlatform; platform: AccountPlatform
type: AccountType; type: AccountType
credentials: Record<string, unknown>; credentials: Record<string, unknown>
extra?: Record<string, string>; extra?: Record<string, string>
proxy_id?: number | null; proxy_id?: number | null
concurrency?: number; concurrency?: number
priority?: number; priority?: number
group_ids?: number[]; group_ids?: number[]
} }
export interface UpdateAccountRequest { export interface UpdateAccountRequest {
name?: string; name?: string
type?: AccountType; type?: AccountType
credentials?: Record<string, unknown>; credentials?: Record<string, unknown>
extra?: Record<string, string>; extra?: Record<string, string>
proxy_id?: number | null; proxy_id?: number | null
concurrency?: number; concurrency?: number
priority?: number; priority?: number
status?: 'active' | 'inactive'; status?: 'active' | 'inactive'
group_ids?: number[]; group_ids?: number[]
} }
export interface CreateProxyRequest { export interface CreateProxyRequest {
name: string; name: string
protocol: ProxyProtocol; protocol: ProxyProtocol
host: string; host: string
port: number; port: number
username?: string | null; username?: string | null
password?: string | null; password?: string | null
} }
export interface UpdateProxyRequest { export interface UpdateProxyRequest {
name?: string; name?: string
protocol?: ProxyProtocol; protocol?: ProxyProtocol
host?: string; host?: string
port?: number; port?: number
username?: string | null; username?: string | null
password?: string | null; password?: string | null
status?: 'active' | 'inactive'; status?: 'active' | 'inactive'
} }
// ==================== Usage & Redeem Types ==================== // ==================== Usage & Redeem Types ====================
export type RedeemCodeType = 'balance' | 'concurrency' | 'subscription'; export type RedeemCodeType = 'balance' | 'concurrency' | 'subscription'
// 消费类型: 0=钱包余额, 1=订阅套餐 // 消费类型: 0=钱包余额, 1=订阅套餐
export type BillingType = 0 | 1; export type BillingType = 0 | 1
export interface UsageLog { export interface UsageLog {
id: number; id: number
user_id: number; user_id: number
api_key_id: number; api_key_id: number
account_id: number | null; account_id: number | null
model: string; model: string
input_tokens: number; input_tokens: number
output_tokens: number; output_tokens: number
cache_creation_tokens: number; cache_creation_tokens: number
cache_read_tokens: number; cache_read_tokens: number
total_cost: number; total_cost: number
actual_cost: number; actual_cost: number
rate_multiplier: number; rate_multiplier: number
billing_type: BillingType; billing_type: BillingType
stream: boolean; stream: boolean
duration_ms: number; duration_ms: number
first_token_ms: number | null; first_token_ms: number | null
created_at: string; created_at: string
user?: User; user?: User
api_key?: ApiKey; api_key?: ApiKey
account?: Account; account?: Account
} }
export interface RedeemCode { export interface RedeemCode {
id: number; id: number
code: string; code: string
type: RedeemCodeType; type: RedeemCodeType
value: number; value: number
status: 'active' | 'used' | 'expired' | 'unused'; status: 'active' | 'used' | 'expired' | 'unused'
used_by: number | null; used_by: number | null
used_at: string | null; used_at: string | null
created_at: string; created_at: string
updated_at?: string; updated_at?: string
group_id?: number | null; // 订阅类型专用 group_id?: number | null // 订阅类型专用
validity_days?: number; // 订阅类型专用 validity_days?: number // 订阅类型专用
user?: User; user?: User
group?: Group; // 关联的分组 group?: Group // 关联的分组
} }
export interface GenerateRedeemCodesRequest { export interface GenerateRedeemCodesRequest {
count: number; count: number
type: RedeemCodeType; type: RedeemCodeType
value: number; value: number
group_id?: number | null; // 订阅类型专用 group_id?: number | null // 订阅类型专用
validity_days?: number; // 订阅类型专用 validity_days?: number // 订阅类型专用
} }
export interface RedeemCodeRequest { export interface RedeemCodeRequest {
code: string; code: string
} }
// ==================== Dashboard & Statistics ==================== // ==================== Dashboard & Statistics ====================
export interface DashboardStats { export interface DashboardStats {
// 用户统计 // 用户统计
total_users: number; total_users: number
today_new_users: number; // 今日新增用户数 today_new_users: number // 今日新增用户数
active_users: number; // 今日有请求的用户数 active_users: number // 今日有请求的用户数
// API Key 统计 // API Key 统计
total_api_keys: number; total_api_keys: number
active_api_keys: number; // 状态为 active 的 API Key 数 active_api_keys: number // 状态为 active 的 API Key 数
// 账户统计 // 账户统计
total_accounts: number; total_accounts: number
normal_accounts: number; // 正常账户数 normal_accounts: number // 正常账户数
error_accounts: number; // 异常账户数 error_accounts: number // 异常账户数
ratelimit_accounts: number; // 限流账户数 ratelimit_accounts: number // 限流账户数
overload_accounts: number; // 过载账户数 overload_accounts: number // 过载账户数
// 累计 Token 使用统计 // 累计 Token 使用统计
total_requests: number; total_requests: number
total_input_tokens: number; total_input_tokens: number
total_output_tokens: number; total_output_tokens: number
total_cache_creation_tokens: number; total_cache_creation_tokens: number
total_cache_read_tokens: number; total_cache_read_tokens: number
total_tokens: number; total_tokens: number
total_cost: number; // 累计标准计费 total_cost: number // 累计标准计费
total_actual_cost: number; // 累计实际扣除 total_actual_cost: number // 累计实际扣除
// 今日 Token 使用统计 // 今日 Token 使用统计
today_requests: number; today_requests: number
today_input_tokens: number; today_input_tokens: number
today_output_tokens: number; today_output_tokens: number
today_cache_creation_tokens: number; today_cache_creation_tokens: number
today_cache_read_tokens: number; today_cache_read_tokens: number
today_tokens: number; today_tokens: number
today_cost: number; // 今日标准计费 today_cost: number // 今日标准计费
today_actual_cost: number; // 今日实际扣除 today_actual_cost: number // 今日实际扣除
// 系统运行统计 // 系统运行统计
average_duration_ms: number; // 平均响应时间 average_duration_ms: number // 平均响应时间
uptime: number; // 系统运行时间(秒) uptime: number // 系统运行时间(秒)
// 性能指标 // 性能指标
rpm: number; // 近5分钟平均每分钟请求数 rpm: number // 近5分钟平均每分钟请求数
tpm: number; // 近5分钟平均每分钟Token数 tpm: number // 近5分钟平均每分钟Token数
} }
export interface UsageStatsResponse { export interface UsageStatsResponse {
period?: string; period?: string
total_requests: number; total_requests: number
total_input_tokens: number; total_input_tokens: number
total_output_tokens: number; total_output_tokens: number
total_cache_tokens: number; total_cache_tokens: number
total_tokens: number; total_tokens: number
total_cost: number; // 标准计费 total_cost: number // 标准计费
total_actual_cost: number; // 实际扣除 total_actual_cost: number // 实际扣除
average_duration_ms: number; average_duration_ms: number
models?: Record<string, number>; models?: Record<string, number>
} }
// ==================== Trend & Chart Types ==================== // ==================== Trend & Chart Types ====================
export interface TrendDataPoint { export interface TrendDataPoint {
date: string; date: string
requests: number; requests: number
input_tokens: number; input_tokens: number
output_tokens: number; output_tokens: number
cache_tokens: number; cache_tokens: number
total_tokens: number; total_tokens: number
cost: number; // 标准计费 cost: number // 标准计费
actual_cost: number; // 实际扣除 actual_cost: number // 实际扣除
} }
export interface ModelStat { export interface ModelStat {
model: string; model: string
requests: number; requests: number
input_tokens: number; input_tokens: number
output_tokens: number; output_tokens: number
total_tokens: number; total_tokens: number
cost: number; // 标准计费 cost: number // 标准计费
actual_cost: number; // 实际扣除 actual_cost: number // 实际扣除
} }
export interface UserUsageTrendPoint { export interface UserUsageTrendPoint {
date: string; date: string
user_id: number; user_id: number
email: string; email: string
requests: number; requests: number
tokens: number; tokens: number
cost: number; // 标准计费 cost: number // 标准计费
actual_cost: number; // 实际扣除 actual_cost: number // 实际扣除
} }
export interface ApiKeyUsageTrendPoint { export interface ApiKeyUsageTrendPoint {
date: string; date: string
api_key_id: number; api_key_id: number
key_name: string; key_name: string
requests: number; requests: number
tokens: number; tokens: number
} }
// ==================== Admin User Management ==================== // ==================== Admin User Management ====================
export interface UpdateUserRequest { export interface UpdateUserRequest {
email?: string; email?: string
password?: string; password?: string
username?: string; username?: string
wechat?: string; wechat?: string
notes?: string; notes?: string
role?: 'admin' | 'user'; role?: 'admin' | 'user'
balance?: number; balance?: number
concurrency?: number; concurrency?: number
status?: 'active' | 'disabled'; status?: 'active' | 'disabled'
allowed_groups?: number[] | null; allowed_groups?: number[] | null
} }
export interface ChangePasswordRequest { export interface ChangePasswordRequest {
old_password: string; old_password: string
new_password: string; new_password: string
} }
// ==================== User Subscription Types ==================== // ==================== User Subscription Types ====================
export interface UserSubscription { export interface UserSubscription {
id: number; id: number
user_id: number; user_id: number
group_id: number; group_id: number
status: 'active' | 'expired' | 'revoked'; status: 'active' | 'expired' | 'revoked'
daily_usage_usd: number; daily_usage_usd: number
weekly_usage_usd: number; weekly_usage_usd: number
monthly_usage_usd: number; monthly_usage_usd: number
daily_window_start: string | null; daily_window_start: string | null
weekly_window_start: string | null; weekly_window_start: string | null
monthly_window_start: string | null; monthly_window_start: string | null
created_at: string; created_at: string
updated_at: string; updated_at: string
expires_at: string | null; expires_at: string | null
user?: User; user?: User
group?: Group; group?: Group
} }
export interface SubscriptionProgress { export interface SubscriptionProgress {
subscription_id: number; subscription_id: number
daily: { daily: {
used: number; used: number
limit: number | null; limit: number | null
percentage: number; percentage: number
reset_in_seconds: number | null; reset_in_seconds: number | null
} | null; } | null
weekly: { weekly: {
used: number; used: number
limit: number | null; limit: number | null
percentage: number; percentage: number
reset_in_seconds: number | null; reset_in_seconds: number | null
} | null; } | null
monthly: { monthly: {
used: number; used: number
limit: number | null; limit: number | null
percentage: number; percentage: number
reset_in_seconds: number | null; reset_in_seconds: number | null
} | null; } | null
expires_at: string | null; expires_at: string | null
days_remaining: number | null; days_remaining: number | null
} }
export interface AssignSubscriptionRequest { export interface AssignSubscriptionRequest {
user_id: number; user_id: number
group_id: number; group_id: number
validity_days?: number; validity_days?: number
} }
export interface BulkAssignSubscriptionRequest { export interface BulkAssignSubscriptionRequest {
user_ids: number[]; user_ids: number[]
group_id: number; group_id: number
validity_days?: number; validity_days?: number
} }
export interface ExtendSubscriptionRequest { export interface ExtendSubscriptionRequest {
days: number; days: number
} }
// ==================== Query Parameters ==================== // ==================== Query Parameters ====================
export interface UsageQueryParams { export interface UsageQueryParams {
page?: number; page?: number
page_size?: number; page_size?: number
api_key_id?: number; api_key_id?: number
user_id?: number; user_id?: number
start_date?: string; start_date?: string
end_date?: string; end_date?: string
} }
// ==================== Account Usage Statistics ==================== // ==================== Account Usage Statistics ====================
export interface AccountUsageHistory { export interface AccountUsageHistory {
date: string; date: string
label: string; label: string
requests: number; requests: number
tokens: number; tokens: number
cost: number; cost: number
actual_cost: number; actual_cost: number
} }
export interface AccountUsageSummary { export interface AccountUsageSummary {
days: number; days: number
actual_days_used: number; actual_days_used: number
total_cost: number; total_cost: number
total_standard_cost: number; total_standard_cost: number
total_requests: number; total_requests: number
total_tokens: number; total_tokens: number
avg_daily_cost: number; avg_daily_cost: number
avg_daily_requests: number; avg_daily_requests: number
avg_daily_tokens: number; avg_daily_tokens: number
avg_duration_ms: number; avg_duration_ms: number
today: { today: {
date: string; date: string
cost: number; cost: number
requests: number; requests: number
tokens: number; tokens: number
} | null; } | null
highest_cost_day: { highest_cost_day: {
date: string; date: string
label: string; label: string
cost: number; cost: number
requests: number; requests: number
} | null; } | null
highest_request_day: { highest_request_day: {
date: string; date: string
label: string; label: string
requests: number; requests: number
cost: number; cost: number
} | null; } | null
} }
export interface AccountUsageStatsResponse { export interface AccountUsageStatsResponse {
history: AccountUsageHistory[]; history: AccountUsageHistory[]
summary: AccountUsageSummary; summary: AccountUsageSummary
models: ModelStat[]; models: ModelStat[]
} }
...@@ -90,7 +90,10 @@ export function formatBytes(bytes: number, decimals: number = 2): string { ...@@ -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)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment