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) {
}
// Watch for site settings changes and update favicon/title
watch(() => appStore.siteLogo, (newLogo) => {
watch(
() => appStore.siteLogo,
(newLogo) => {
if (newLogo) {
updateFavicon(newLogo)
}
}, { immediate: true })
},
{ immediate: true }
)
watch(() => appStore.siteName, (newName) => {
watch(
() => appStore.siteName,
(newName) => {
if (newName) {
document.title = `${newName} - AI API Gateway`
}
}, { immediate: true })
},
{ immediate: true }
)
onMounted(async () => {
// Check if setup is needed
......
......@@ -53,7 +53,8 @@ export function useAccountOAuth() {
try {
const proxyConfig = proxyId ? { proxy_id: proxyId } : {}
const endpoint = addMethod === 'oauth'
const endpoint =
addMethod === 'oauth'
? '/admin/accounts/generate-auth-url'
: '/admin/accounts/generate-setup-token-url'
......@@ -85,7 +86,8 @@ export function useAccountOAuth() {
try {
const proxyConfig = proxyId ? { proxy_id: proxyId } : {}
const endpoint = addMethod === 'oauth'
const endpoint =
addMethod === 'oauth'
? '/admin/accounts/exchange-code'
: '/admin/accounts/exchange-setup-token-code'
......@@ -121,7 +123,8 @@ export function useAccountOAuth() {
try {
const proxyConfig = proxyId ? { proxy_id: proxyId } : {}
const endpoint = addMethod === 'oauth'
const endpoint =
addMethod === 'oauth'
? '/admin/accounts/cookie-auth'
: '/admin/accounts/setup-token-cookie-auth'
......@@ -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
......
......@@ -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
......
......@@ -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,103 +60,101 @@ 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>
......@@ -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
```
......@@ -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 hasActiveToasts = computed(() => toasts.value.length > 0)
const loadingCount = ref<number>(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
}
})
......@@ -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;
const updatedUser = await authAPI.getCurrentUser()
user.value = updatedUser
// Update localStorage
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(updatedUser));
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(updatedUser))
return 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
}
})
......@@ -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'
......@@ -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;
}
/* 隐藏滚动条 */
......
......@@ -5,726 +5,726 @@
// ==================== User & Auth Types ====================
export interface User {
id: number;
username: string;
wechat: string;
notes: string;
email: string;
role: 'admin' | 'user'; // User role for authorization
balance: number; // User balance for API usage
concurrency: number; // Allowed concurrent requests
status: 'active' | 'disabled'; // Account status
allowed_groups: number[] | null; // Allowed group IDs (null = all non-exclusive groups)
subscriptions?: UserSubscription[]; // User's active subscriptions
created_at: string;
updated_at: string;
id: number
username: string
wechat: string
notes: string
email: string
role: 'admin' | 'user' // User role for authorization
balance: number // User balance for API usage
concurrency: number // Allowed concurrent requests
status: 'active' | 'disabled' // Account status
allowed_groups: number[] | null // Allowed group IDs (null = all non-exclusive groups)
subscriptions?: UserSubscription[] // User's active subscriptions
created_at: string
updated_at: string
}
export interface LoginRequest {
email: string;
password: string;
turnstile_token?: string;
email: string
password: string
turnstile_token?: string
}
export interface RegisterRequest {
email: string;
password: string;
verify_code?: string;
turnstile_token?: string;
email: string
password: string
verify_code?: string
turnstile_token?: string
}
export interface SendVerifyCodeRequest {
email: string;
turnstile_token?: string;
email: string
turnstile_token?: string
}
export interface SendVerifyCodeResponse {
message: string;
countdown: number;
message: string
countdown: number
}
export interface PublicSettings {
registration_enabled: boolean;
email_verify_enabled: boolean;
turnstile_enabled: boolean;
turnstile_site_key: string;
site_name: string;
site_logo: string;
site_subtitle: string;
api_base_url: string;
contact_info: string;
doc_url: string;
version: string;
registration_enabled: boolean
email_verify_enabled: boolean
turnstile_enabled: boolean
turnstile_site_key: string
site_name: string
site_logo: string
site_subtitle: string
api_base_url: string
contact_info: string
doc_url: string
version: string
}
export interface AuthResponse {
access_token: string;
token_type: string;
user: User;
access_token: string
token_type: string
user: User
}
// ==================== Subscription Types ====================
export interface Subscription {
id: number;
user_id: number;
name: string;
url: string;
type: 'clash' | 'v2ray' | 'surge' | 'quantumult' | 'shadowrocket';
update_interval: number; // in hours
last_updated: string | null;
node_count: number;
is_active: boolean;
created_at: string;
updated_at: string;
id: number
user_id: number
name: string
url: string
type: 'clash' | 'v2ray' | 'surge' | 'quantumult' | 'shadowrocket'
update_interval: number // in hours
last_updated: string | null
node_count: number
is_active: boolean
created_at: string
updated_at: string
}
export interface CreateSubscriptionRequest {
name: string;
url: string;
type: Subscription['type'];
update_interval?: number;
name: string
url: string
type: Subscription['type']
update_interval?: number
}
export interface UpdateSubscriptionRequest {
name?: string;
url?: string;
type?: Subscription['type'];
update_interval?: number;
is_active?: boolean;
name?: string
url?: string
type?: Subscription['type']
update_interval?: number
is_active?: boolean
}
// ==================== Proxy Node Types ====================
export interface ProxyNode {
id: number;
subscription_id: number;
name: string;
type: 'ss' | 'ssr' | 'vmess' | 'vless' | 'trojan' | 'hysteria' | 'hysteria2';
server: string;
port: number;
config: Record<string, unknown>; // JSON configuration specific to proxy type
latency: number | null; // in milliseconds
last_checked: string | null;
is_available: boolean;
created_at: string;
updated_at: string;
id: number
subscription_id: number
name: string
type: 'ss' | 'ssr' | 'vmess' | 'vless' | 'trojan' | 'hysteria' | 'hysteria2'
server: string
port: number
config: Record<string, unknown> // JSON configuration specific to proxy type
latency: number | null // in milliseconds
last_checked: string | null
is_available: boolean
created_at: string
updated_at: string
}
// ==================== Conversion Types ====================
export interface ConversionRequest {
subscription_ids: number[];
target_type: 'clash' | 'v2ray' | 'surge' | 'quantumult' | 'shadowrocket';
subscription_ids: number[]
target_type: 'clash' | 'v2ray' | 'surge' | 'quantumult' | 'shadowrocket'
filter?: {
name_pattern?: string;
types?: ProxyNode['type'][];
min_latency?: number;
max_latency?: number;
available_only?: boolean;
};
name_pattern?: string
types?: ProxyNode['type'][]
min_latency?: number
max_latency?: number
available_only?: boolean
}
sort?: {
by: 'name' | 'latency' | 'type';
order: 'asc' | 'desc';
};
by: 'name' | 'latency' | 'type'
order: 'asc' | 'desc'
}
}
export interface ConversionResult {
url: string; // URL to download the converted subscription
expires_at: string;
node_count: number;
url: string // URL to download the converted subscription
expires_at: string
node_count: number
}
// ==================== Statistics Types ====================
export interface SubscriptionStats {
subscription_id: number;
total_nodes: number;
available_nodes: number;
avg_latency: number | null;
by_type: Record<ProxyNode['type'], number>;
last_update: string;
subscription_id: number
total_nodes: number
available_nodes: number
avg_latency: number | null
by_type: Record<ProxyNode['type'], number>
last_update: string
}
export interface UserStats {
total_subscriptions: number;
total_nodes: number;
active_subscriptions: number;
total_conversions: number;
last_conversion: string | null;
total_subscriptions: number
total_nodes: number
active_subscriptions: number
total_conversions: number
last_conversion: string | null
}
// ==================== API Response Types ====================
export interface ApiResponse<T = unknown> {
code: number;
message: string;
data: T;
code: number
message: string
data: T
}
export interface ApiError {
detail: string;
code?: string;
field?: string;
detail: string
code?: string
field?: string
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
page_size: number;
pages: number;
items: T[]
total: number
page: number
page_size: number
pages: number
}
// ==================== UI State Types ====================
export type ToastType = 'success' | 'error' | 'info' | 'warning';
export type ToastType = 'success' | 'error' | 'info' | 'warning'
export interface Toast {
id: string;
type: ToastType;
message: string;
title?: string;
duration?: number; // in milliseconds, undefined means no auto-dismiss
startTime?: number; // timestamp when toast was created, for progress bar
id: string
type: ToastType
message: string
title?: string
duration?: number // in milliseconds, undefined means no auto-dismiss
startTime?: number // timestamp when toast was created, for progress bar
}
export interface AppState {
sidebarCollapsed: boolean;
loading: boolean;
toasts: Toast[];
sidebarCollapsed: boolean
loading: boolean
toasts: Toast[]
}
// ==================== Validation Types ====================
export interface ValidationError {
field: string;
message: string;
field: string
message: string
}
// ==================== Table/List Types ====================
export interface SortConfig {
key: string;
order: 'asc' | 'desc';
key: string
order: 'asc' | 'desc'
}
export interface FilterConfig {
[key: string]: string | number | boolean | null | undefined;
[key: string]: string | number | boolean | null | undefined
}
export interface PaginationConfig {
page: number;
page_size: number;
page: number
page_size: number
}
// ==================== 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 {
id: number;
name: string;
description: string | null;
platform: GroupPlatform;
rate_multiplier: number;
is_exclusive: boolean;
status: 'active' | 'inactive';
subscription_type: SubscriptionType;
daily_limit_usd: number | null;
weekly_limit_usd: number | null;
monthly_limit_usd: number | null;
account_count?: number;
created_at: string;
updated_at: string;
id: number
name: string
description: string | null
platform: GroupPlatform
rate_multiplier: number
is_exclusive: boolean
status: 'active' | 'inactive'
subscription_type: SubscriptionType
daily_limit_usd: number | null
weekly_limit_usd: number | null
monthly_limit_usd: number | null
account_count?: number
created_at: string
updated_at: string
}
export interface ApiKey {
id: number;
user_id: number;
key: string;
name: string;
group_id: number | null;
status: 'active' | 'inactive';
created_at: string;
updated_at: string;
group?: Group;
id: number
user_id: number
key: string
name: string
group_id: number | null
status: 'active' | 'inactive'
created_at: string
updated_at: string
group?: Group
}
export interface CreateApiKeyRequest {
name: string;
group_id?: number | null;
custom_key?: string; // 可选的自定义API Key
name: string
group_id?: number | null
custom_key?: string // 可选的自定义API Key
}
export interface UpdateApiKeyRequest {
name?: string;
group_id?: number | null;
status?: 'active' | 'inactive';
name?: string
group_id?: number | null
status?: 'active' | 'inactive'
}
export interface CreateGroupRequest {
name: string;
description?: string | null;
platform?: GroupPlatform;
rate_multiplier?: number;
is_exclusive?: boolean;
name: string
description?: string | null
platform?: GroupPlatform
rate_multiplier?: number
is_exclusive?: boolean
}
export interface UpdateGroupRequest {
name?: string;
description?: string | null;
platform?: GroupPlatform;
rate_multiplier?: number;
is_exclusive?: boolean;
status?: 'active' | 'inactive';
name?: string
description?: string | null
platform?: GroupPlatform
rate_multiplier?: number
is_exclusive?: boolean
status?: 'active' | 'inactive'
}
// ==================== Account & Proxy Types ====================
export type AccountPlatform = 'anthropic' | 'openai';
export type AccountType = 'oauth' | 'setup-token' | 'apikey';
export type OAuthAddMethod = 'oauth' | 'setup-token';
export type ProxyProtocol = 'http' | 'https' | 'socks5';
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini'
export type AccountType = 'oauth' | 'setup-token' | 'apikey'
export type OAuthAddMethod = 'oauth' | 'setup-token'
export type ProxyProtocol = 'http' | 'https' | 'socks5'
// Claude Model type (returned by /v1/models and account models API)
export interface ClaudeModel {
id: string;
type: string;
display_name: string;
created_at: string;
id: string
type: string
display_name: string
created_at: string
}
export interface Proxy {
id: number;
name: string;
protocol: ProxyProtocol;
host: string;
port: number;
username: string | null;
password?: string | null;
status: 'active' | 'inactive';
account_count?: number; // Number of accounts using this proxy
created_at: string;
updated_at: string;
id: number
name: string
protocol: ProxyProtocol
host: string
port: number
username: string | null
password?: string | null
status: 'active' | 'inactive'
account_count?: number // Number of accounts using this proxy
created_at: string
updated_at: string
}
export interface Account {
id: number;
name: string;
platform: AccountPlatform;
type: AccountType;
credentials?: Record<string, unknown>;
extra?: CodexUsageSnapshot & Record<string, unknown>; // Extra fields including Codex usage
proxy_id: number | null;
concurrency: number;
current_concurrency?: number; // Real-time concurrency count from Redis
priority: number;
status: 'active' | 'inactive' | 'error';
error_message: string | null;
last_used_at: string | null;
created_at: string;
updated_at: string;
proxy?: Proxy;
group_ids?: number[]; // Groups this account belongs to
groups?: Group[]; // Preloaded group objects
id: number
name: string
platform: AccountPlatform
type: AccountType
credentials?: Record<string, unknown>
extra?: CodexUsageSnapshot & Record<string, unknown> // Extra fields including Codex usage
proxy_id: number | null
concurrency: number
current_concurrency?: number // Real-time concurrency count from Redis
priority: number
status: 'active' | 'inactive' | 'error'
error_message: string | null
last_used_at: string | null
created_at: string
updated_at: string
proxy?: Proxy
group_ids?: number[] // Groups this account belongs to
groups?: Group[] // Preloaded group objects
// Rate limit & scheduling fields
schedulable: boolean;
rate_limited_at: string | null;
rate_limit_reset_at: string | null;
overload_until: string | null;
schedulable: boolean
rate_limited_at: string | null
rate_limit_reset_at: string | null
overload_until: string | null
// Session window fields (5-hour window)
session_window_start: string | null;
session_window_end: string | null;
session_window_status: 'allowed' | 'allowed_warning' | 'rejected' | null;
session_window_start: string | null
session_window_end: string | null
session_window_status: 'allowed' | 'allowed_warning' | 'rejected' | null
}
// Account Usage types
export interface WindowStats {
requests: number;
tokens: number;
cost: number;
requests: number
tokens: number
cost: number
}
export interface UsageProgress {
utilization: number; // Percentage (0-100+, 100 = 100%)
resets_at: string | null;
remaining_seconds: number;
window_stats?: WindowStats | null; // 窗口期统计(从窗口开始到当前的使用量)
utilization: number // Percentage (0-100+, 100 = 100%)
resets_at: string | null
remaining_seconds: number
window_stats?: WindowStats | null // 窗口期统计(从窗口开始到当前的使用量)
}
export interface AccountUsageInfo {
updated_at: string | null;
five_hour: UsageProgress | null;
seven_day: UsageProgress | null;
seven_day_sonnet: UsageProgress | null;
updated_at: string | null
five_hour: UsageProgress | null
seven_day: UsageProgress | null
seven_day_sonnet: UsageProgress | null
}
// OpenAI Codex usage snapshot (from response headers)
export interface CodexUsageSnapshot {
// Legacy fields (kept for backwards compatibility)
// 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_reset_after_seconds?: number; // Seconds until reset
codex_primary_window_minutes?: number; // Window in minutes
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_window_minutes?: number; // Window in minutes
codex_primary_over_secondary_percent?: number; // Overflow ratio
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_window_minutes?: number // Window in minutes
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_window_minutes?: number // Window in minutes
codex_primary_over_secondary_percent?: number // Overflow ratio
// Canonical fields (normalized by backend, use these preferentially)
codex_5h_used_percent?: number; // 5-hour window usage percentage
codex_5h_reset_after_seconds?: number; // Seconds until 5h window reset
codex_5h_window_minutes?: number; // 5h window in minutes (should be ~300)
codex_7d_used_percent?: number; // 7-day window usage percentage
codex_7d_reset_after_seconds?: number; // Seconds until 7d window reset
codex_7d_window_minutes?: number; // 7d window in minutes (should be ~10080)
codex_5h_used_percent?: number // 5-hour window usage percentage
codex_5h_reset_after_seconds?: number // Seconds until 5h window reset
codex_5h_window_minutes?: number // 5h window in minutes (should be ~300)
codex_7d_used_percent?: number // 7-day window usage percentage
codex_7d_reset_after_seconds?: number // Seconds until 7d window reset
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 {
name: string;
platform: AccountPlatform;
type: AccountType;
credentials: Record<string, unknown>;
extra?: Record<string, string>;
proxy_id?: number | null;
concurrency?: number;
priority?: number;
group_ids?: number[];
name: string
platform: AccountPlatform
type: AccountType
credentials: Record<string, unknown>
extra?: Record<string, string>
proxy_id?: number | null
concurrency?: number
priority?: number
group_ids?: number[]
}
export interface UpdateAccountRequest {
name?: string;
type?: AccountType;
credentials?: Record<string, unknown>;
extra?: Record<string, string>;
proxy_id?: number | null;
concurrency?: number;
priority?: number;
status?: 'active' | 'inactive';
group_ids?: number[];
name?: string
type?: AccountType
credentials?: Record<string, unknown>
extra?: Record<string, string>
proxy_id?: number | null
concurrency?: number
priority?: number
status?: 'active' | 'inactive'
group_ids?: number[]
}
export interface CreateProxyRequest {
name: string;
protocol: ProxyProtocol;
host: string;
port: number;
username?: string | null;
password?: string | null;
name: string
protocol: ProxyProtocol
host: string
port: number
username?: string | null
password?: string | null
}
export interface UpdateProxyRequest {
name?: string;
protocol?: ProxyProtocol;
host?: string;
port?: number;
username?: string | null;
password?: string | null;
status?: 'active' | 'inactive';
name?: string
protocol?: ProxyProtocol
host?: string
port?: number
username?: string | null
password?: string | null
status?: 'active' | 'inactive'
}
// ==================== Usage & Redeem Types ====================
export type RedeemCodeType = 'balance' | 'concurrency' | 'subscription';
export type RedeemCodeType = 'balance' | 'concurrency' | 'subscription'
// 消费类型: 0=钱包余额, 1=订阅套餐
export type BillingType = 0 | 1;
export type BillingType = 0 | 1
export interface UsageLog {
id: number;
user_id: number;
api_key_id: number;
account_id: number | null;
model: string;
input_tokens: number;
output_tokens: number;
cache_creation_tokens: number;
cache_read_tokens: number;
total_cost: number;
actual_cost: number;
rate_multiplier: number;
billing_type: BillingType;
stream: boolean;
duration_ms: number;
first_token_ms: number | null;
created_at: string;
user?: User;
api_key?: ApiKey;
account?: Account;
id: number
user_id: number
api_key_id: number
account_id: number | null
model: string
input_tokens: number
output_tokens: number
cache_creation_tokens: number
cache_read_tokens: number
total_cost: number
actual_cost: number
rate_multiplier: number
billing_type: BillingType
stream: boolean
duration_ms: number
first_token_ms: number | null
created_at: string
user?: User
api_key?: ApiKey
account?: Account
}
export interface RedeemCode {
id: number;
code: string;
type: RedeemCodeType;
value: number;
status: 'active' | 'used' | 'expired' | 'unused';
used_by: number | null;
used_at: string | null;
created_at: string;
updated_at?: string;
group_id?: number | null; // 订阅类型专用
validity_days?: number; // 订阅类型专用
user?: User;
group?: Group; // 关联的分组
id: number
code: string
type: RedeemCodeType
value: number
status: 'active' | 'used' | 'expired' | 'unused'
used_by: number | null
used_at: string | null
created_at: string
updated_at?: string
group_id?: number | null // 订阅类型专用
validity_days?: number // 订阅类型专用
user?: User
group?: Group // 关联的分组
}
export interface GenerateRedeemCodesRequest {
count: number;
type: RedeemCodeType;
value: number;
group_id?: number | null; // 订阅类型专用
validity_days?: number; // 订阅类型专用
count: number
type: RedeemCodeType
value: number
group_id?: number | null // 订阅类型专用
validity_days?: number // 订阅类型专用
}
export interface RedeemCodeRequest {
code: string;
code: string
}
// ==================== Dashboard & Statistics ====================
export interface DashboardStats {
// 用户统计
total_users: number;
today_new_users: number; // 今日新增用户数
active_users: number; // 今日有请求的用户数
total_users: number
today_new_users: number // 今日新增用户数
active_users: number // 今日有请求的用户数
// API Key 统计
total_api_keys: number;
active_api_keys: number; // 状态为 active 的 API Key 数
total_api_keys: number
active_api_keys: number // 状态为 active 的 API Key 数
// 账户统计
total_accounts: number;
normal_accounts: number; // 正常账户数
error_accounts: number; // 异常账户数
ratelimit_accounts: number; // 限流账户数
overload_accounts: number; // 过载账户数
total_accounts: number
normal_accounts: number // 正常账户数
error_accounts: number // 异常账户数
ratelimit_accounts: number // 限流账户数
overload_accounts: number // 过载账户数
// 累计 Token 使用统计
total_requests: number;
total_input_tokens: number;
total_output_tokens: number;
total_cache_creation_tokens: number;
total_cache_read_tokens: number;
total_tokens: number;
total_cost: number; // 累计标准计费
total_actual_cost: number; // 累计实际扣除
total_requests: number
total_input_tokens: number
total_output_tokens: number
total_cache_creation_tokens: number
total_cache_read_tokens: number
total_tokens: number
total_cost: number // 累计标准计费
total_actual_cost: number // 累计实际扣除
// 今日 Token 使用统计
today_requests: number;
today_input_tokens: number;
today_output_tokens: number;
today_cache_creation_tokens: number;
today_cache_read_tokens: number;
today_tokens: number;
today_cost: number; // 今日标准计费
today_actual_cost: number; // 今日实际扣除
today_requests: number
today_input_tokens: number
today_output_tokens: number
today_cache_creation_tokens: number
today_cache_read_tokens: number
today_tokens: number
today_cost: number // 今日标准计费
today_actual_cost: number // 今日实际扣除
// 系统运行统计
average_duration_ms: number; // 平均响应时间
uptime: number; // 系统运行时间(秒)
average_duration_ms: number // 平均响应时间
uptime: number // 系统运行时间(秒)
// 性能指标
rpm: number; // 近5分钟平均每分钟请求数
tpm: number; // 近5分钟平均每分钟Token数
rpm: number // 近5分钟平均每分钟请求数
tpm: number // 近5分钟平均每分钟Token数
}
export interface UsageStatsResponse {
period?: string;
total_requests: number;
total_input_tokens: number;
total_output_tokens: number;
total_cache_tokens: number;
total_tokens: number;
total_cost: number; // 标准计费
total_actual_cost: number; // 实际扣除
average_duration_ms: number;
models?: Record<string, number>;
period?: string
total_requests: number
total_input_tokens: number
total_output_tokens: number
total_cache_tokens: number
total_tokens: number
total_cost: number // 标准计费
total_actual_cost: number // 实际扣除
average_duration_ms: number
models?: Record<string, number>
}
// ==================== Trend & Chart Types ====================
export interface TrendDataPoint {
date: string;
requests: number;
input_tokens: number;
output_tokens: number;
cache_tokens: number;
total_tokens: number;
cost: number; // 标准计费
actual_cost: number; // 实际扣除
date: string
requests: number
input_tokens: number
output_tokens: number
cache_tokens: number
total_tokens: number
cost: number // 标准计费
actual_cost: number // 实际扣除
}
export interface ModelStat {
model: string;
requests: number;
input_tokens: number;
output_tokens: number;
total_tokens: number;
cost: number; // 标准计费
actual_cost: number; // 实际扣除
model: string
requests: number
input_tokens: number
output_tokens: number
total_tokens: number
cost: number // 标准计费
actual_cost: number // 实际扣除
}
export interface UserUsageTrendPoint {
date: string;
user_id: number;
email: string;
requests: number;
tokens: number;
cost: number; // 标准计费
actual_cost: number; // 实际扣除
date: string
user_id: number
email: string
requests: number
tokens: number
cost: number // 标准计费
actual_cost: number // 实际扣除
}
export interface ApiKeyUsageTrendPoint {
date: string;
api_key_id: number;
key_name: string;
requests: number;
tokens: number;
date: string
api_key_id: number
key_name: string
requests: number
tokens: number
}
// ==================== Admin User Management ====================
export interface UpdateUserRequest {
email?: string;
password?: string;
username?: string;
wechat?: string;
notes?: string;
role?: 'admin' | 'user';
balance?: number;
concurrency?: number;
status?: 'active' | 'disabled';
allowed_groups?: number[] | null;
email?: string
password?: string
username?: string
wechat?: string
notes?: string
role?: 'admin' | 'user'
balance?: number
concurrency?: number
status?: 'active' | 'disabled'
allowed_groups?: number[] | null
}
export interface ChangePasswordRequest {
old_password: string;
new_password: string;
old_password: string
new_password: string
}
// ==================== User Subscription Types ====================
export interface UserSubscription {
id: number;
user_id: number;
group_id: number;
status: 'active' | 'expired' | 'revoked';
daily_usage_usd: number;
weekly_usage_usd: number;
monthly_usage_usd: number;
daily_window_start: string | null;
weekly_window_start: string | null;
monthly_window_start: string | null;
created_at: string;
updated_at: string;
expires_at: string | null;
user?: User;
group?: Group;
id: number
user_id: number
group_id: number
status: 'active' | 'expired' | 'revoked'
daily_usage_usd: number
weekly_usage_usd: number
monthly_usage_usd: number
daily_window_start: string | null
weekly_window_start: string | null
monthly_window_start: string | null
created_at: string
updated_at: string
expires_at: string | null
user?: User
group?: Group
}
export interface SubscriptionProgress {
subscription_id: number;
subscription_id: number
daily: {
used: number;
limit: number | null;
percentage: number;
reset_in_seconds: number | null;
} | null;
used: number
limit: number | null
percentage: number
reset_in_seconds: number | null
} | null
weekly: {
used: number;
limit: number | null;
percentage: number;
reset_in_seconds: number | null;
} | null;
used: number
limit: number | null
percentage: number
reset_in_seconds: number | null
} | null
monthly: {
used: number;
limit: number | null;
percentage: number;
reset_in_seconds: number | null;
} | null;
expires_at: string | null;
days_remaining: number | null;
used: number
limit: number | null
percentage: number
reset_in_seconds: number | null
} | null
expires_at: string | null
days_remaining: number | null
}
export interface AssignSubscriptionRequest {
user_id: number;
group_id: number;
validity_days?: number;
user_id: number
group_id: number
validity_days?: number
}
export interface BulkAssignSubscriptionRequest {
user_ids: number[];
group_id: number;
validity_days?: number;
user_ids: number[]
group_id: number
validity_days?: number
}
export interface ExtendSubscriptionRequest {
days: number;
days: number
}
// ==================== Query Parameters ====================
export interface UsageQueryParams {
page?: number;
page_size?: number;
api_key_id?: number;
user_id?: number;
start_date?: string;
end_date?: string;
page?: number
page_size?: number
api_key_id?: number
user_id?: number
start_date?: string
end_date?: string
}
// ==================== Account Usage Statistics ====================
export interface AccountUsageHistory {
date: string;
label: string;
requests: number;
tokens: number;
cost: number;
actual_cost: number;
date: string
label: string
requests: number
tokens: number
cost: number
actual_cost: number
}
export interface AccountUsageSummary {
days: number;
actual_days_used: number;
total_cost: number;
total_standard_cost: number;
total_requests: number;
total_tokens: number;
avg_daily_cost: number;
avg_daily_requests: number;
avg_daily_tokens: number;
avg_duration_ms: number;
days: number
actual_days_used: number
total_cost: number
total_standard_cost: number
total_requests: number
total_tokens: number
avg_daily_cost: number
avg_daily_requests: number
avg_daily_tokens: number
avg_duration_ms: number
today: {
date: string;
cost: number;
requests: number;
tokens: number;
} | null;
date: string
cost: number
requests: number
tokens: number
} | null
highest_cost_day: {
date: string;
label: string;
cost: number;
requests: number;
} | null;
date: string
label: string
cost: number
requests: number
} | null
highest_request_day: {
date: string;
label: string;
requests: number;
cost: number;
} | null;
date: string
label: string
requests: number
cost: number
} | null
}
export interface AccountUsageStatsResponse {
history: AccountUsageHistory[];
summary: AccountUsageSummary;
models: ModelStat[];
history: AccountUsageHistory[]
summary: AccountUsageSummary
models: ModelStat[]
}
......@@ -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)
......
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