Loading...
Welcome, {{ authStore.user?.username }}!
@@ -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
```
diff --git a/frontend/src/stores/app.ts b/frontend/src/stores/app.ts
index 09140cbe..cfc9d677 100644
--- a/frontend/src/stores/app.ts
+++ b/frontend/src/stores/app.ts
@@ -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
(false);
- const mobileOpen = ref(false);
- const loading = ref(false);
- const toasts = ref([]);
+ const sidebarCollapsed = ref(false)
+ const mobileOpen = ref(false)
+ const loading = ref(false)
+ const toasts = ref([])
// Public settings cache state
- const publicSettingsLoaded = ref(false);
- const publicSettingsLoading = ref(false);
- const siteName = ref('Sub2API');
- const siteLogo = ref('');
- const siteVersion = ref('');
- const contactInfo = ref('');
- const apiBaseUrl = ref('');
- const docUrl = ref('');
+ const publicSettingsLoaded = ref(false)
+ const publicSettingsLoading = ref(false)
+ const siteName = ref('Sub2API')
+ const siteLogo = ref('')
+ const siteVersion = ref('')
+ const contactInfo = ref('')
+ const apiBaseUrl = ref('')
+ const docUrl = ref('')
// Version cache state
- const versionLoaded = ref(false);
- const versionLoading = ref(false);
- const currentVersion = ref('');
- const latestVersion = ref('');
- const hasUpdate = ref(false);
- const buildType = ref('source');
- const releaseInfo = ref(null);
+ const versionLoaded = ref(false)
+ const versionLoading = ref(false)
+ const currentVersion = ref('')
+ const latestVersion = ref('')
+ const hasUpdate = ref(false)
+ const buildType = ref('source')
+ const releaseInfo = ref(null)
// Auto-incrementing ID for toasts
- let toastIdCounter = 0;
+ let toastIdCounter = 0
// ==================== Computed ====================
-
- const hasActiveToasts = computed(() => toasts.value.length > 0);
-
- const loadingCount = ref(0);
+
+ const hasActiveToasts = computed(() => toasts.value.length > 0)
+
+ const loadingCount = ref(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(operation: () => Promise): Promise {
- 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,
errorMessage?: string
): Promise {
- 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
+ }
+})
diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts
index 9220186e..8012984a 100644
--- a/frontend/src/stores/auth.ts
+++ b/frontend/src/stores/auth.ts
@@ -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(null);
- const token = ref(null);
- let refreshIntervalId: ReturnType | null = null;
+ const user = ref(null)
+ const token = ref(null)
+ let refreshIntervalId: ReturnType | 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 {
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 {
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 {
if (!token.value) {
- throw new Error('Not authenticated');
+ throw new Error('Not authenticated')
}
try {
- const updatedUser = await authAPI.getCurrentUser();
- user.value = updatedUser;
-
- // Update localStorage
- localStorage.setItem(AUTH_USER_KEY, JSON.stringify(updatedUser));
+ const updatedUser = await authAPI.getCurrentUser()
+ user.value = updatedUser
- return updatedUser;
+ // Update localStorage
+ localStorage.setItem(AUTH_USER_KEY, JSON.stringify(updatedUser))
+
+ return updatedUser
} catch (error) {
// If refresh fails with 401, clear auth state
if ((error as { status?: number }).status === 401) {
- clearAuth();
+ clearAuth()
}
- throw error;
+ throw error
}
}
@@ -190,12 +190,12 @@ export const useAuthStore = defineStore('auth', () => {
*/
function clearAuth(): void {
// Stop auto-refresh
- stopAutoRefresh();
+ stopAutoRefresh()
- token.value = null;
- user.value = null;
- localStorage.removeItem(AUTH_TOKEN_KEY);
- localStorage.removeItem(AUTH_USER_KEY);
+ token.value = null
+ user.value = null
+ localStorage.removeItem(AUTH_TOKEN_KEY)
+ localStorage.removeItem(AUTH_USER_KEY)
}
// ==================== Return Store API ====================
@@ -214,6 +214,6 @@ export const useAuthStore = defineStore('auth', () => {
register,
logout,
checkAuth,
- refreshUser,
- };
-});
+ refreshUser
+ }
+})
diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts
index b891b6f6..d518940c 100644
--- a/frontend/src/stores/index.ts
+++ b/frontend/src/stores/index.ts
@@ -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'
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 582edb9e..2c52407b 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -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;
}
/* 隐藏滚动条 */
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 96d04d73..4d004eb0 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -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; // 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 // 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;
- last_update: string;
+ subscription_id: number
+ total_nodes: number
+ available_nodes: number
+ avg_latency: number | null
+ by_type: Record
+ 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 {
- 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 {
- 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;
- extra?: CodexUsageSnapshot & Record; // 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
+ extra?: CodexUsageSnapshot & Record // 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;
- extra?: Record;
- proxy_id?: number | null;
- concurrency?: number;
- priority?: number;
- group_ids?: number[];
+ name: string
+ platform: AccountPlatform
+ type: AccountType
+ credentials: Record
+ extra?: Record
+ proxy_id?: number | null
+ concurrency?: number
+ priority?: number
+ group_ids?: number[]
}
export interface UpdateAccountRequest {
- name?: string;
- type?: AccountType;
- credentials?: Record;
- extra?: Record;
- proxy_id?: number | null;
- concurrency?: number;
- priority?: number;
- status?: 'active' | 'inactive';
- group_ids?: number[];
+ name?: string
+ type?: AccountType
+ credentials?: Record
+ extra?: Record
+ 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;
+ 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
}
// ==================== 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[]
}
diff --git a/frontend/src/utils/format.ts b/frontend/src/utils/format.ts
index 557e6e3a..7bdfda47 100644
--- a/frontend/src/utils/format.ts
+++ b/frontend/src/utils/format.ts
@@ -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)