Merge branch 'main' into feature/antigravity_auth
This commit is contained in:
@@ -8,7 +8,7 @@ import type {
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
AuthResponse,
|
||||
User,
|
||||
CurrentUserResponse,
|
||||
SendVerifyCodeRequest,
|
||||
SendVerifyCodeResponse,
|
||||
PublicSettings
|
||||
@@ -70,9 +70,8 @@ export async function register(userData: RegisterRequest): Promise<AuthResponse>
|
||||
* Get current authenticated user
|
||||
* @returns User profile data
|
||||
*/
|
||||
export async function getCurrentUser(): Promise<User> {
|
||||
const { data } = await apiClient.get<User>('/auth/me')
|
||||
return data
|
||||
export async function getCurrentUser() {
|
||||
return apiClient.get<CurrentUserResponse>('/auth/me')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -585,7 +585,7 @@
|
||||
: 'https://api.anthropic.com'
|
||||
"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.baseUrlHint') }}</p>
|
||||
<p class="input-hint">{{ baseUrlHint }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.apiKeyRequired') }}</label>
|
||||
@@ -602,13 +602,7 @@
|
||||
: 'sk-ant-...'
|
||||
"
|
||||
/>
|
||||
<p class="input-hint">
|
||||
{{
|
||||
form.platform === 'gemini'
|
||||
? t('admin.accounts.gemini.apiKeyHint')
|
||||
: t('admin.accounts.apiKeyHint')
|
||||
}}
|
||||
</p>
|
||||
<p class="input-hint">{{ apiKeyHint }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Model Restriction Section (不适用于 Gemini) -->
|
||||
@@ -1055,8 +1049,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group Selection -->
|
||||
<!-- Group Selection - 仅标准模式显示 -->
|
||||
<GroupSelector
|
||||
v-if="!authStore.isSimpleMode"
|
||||
v-model="form.group_ids"
|
||||
:groups="groups"
|
||||
:platform="form.platform"
|
||||
@@ -1172,6 +1167,7 @@
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import {
|
||||
useAccountOAuth,
|
||||
@@ -1199,6 +1195,7 @@ interface OAuthFlowExposed {
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const oauthStepTitle = computed(() => {
|
||||
if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')
|
||||
@@ -1207,6 +1204,19 @@ const oauthStepTitle = computed(() => {
|
||||
return t('admin.accounts.oauth.title')
|
||||
})
|
||||
|
||||
// Platform-specific hints for API Key type
|
||||
const baseUrlHint = computed(() => {
|
||||
if (form.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
|
||||
if (form.platform === 'gemini') return t('admin.accounts.gemini.baseUrlHint')
|
||||
return t('admin.accounts.baseUrlHint')
|
||||
})
|
||||
|
||||
const apiKeyHint = computed(() => {
|
||||
if (form.platform === 'openai') return t('admin.accounts.openai.apiKeyHint')
|
||||
if (form.platform === 'gemini') return t('admin.accounts.gemini.apiKeyHint')
|
||||
return t('admin.accounts.apiKeyHint')
|
||||
})
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
proxies: Proxy[]
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
: 'https://api.anthropic.com'
|
||||
"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.baseUrlHint') }}</p>
|
||||
<p class="input-hint">{{ baseUrlHint }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.apiKey') }}</label>
|
||||
@@ -497,8 +497,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group Selection -->
|
||||
<!-- Group Selection - 仅标准模式显示 -->
|
||||
<GroupSelector
|
||||
v-if="!authStore.isSimpleMode"
|
||||
v-model="form.group_ids"
|
||||
:groups="groups"
|
||||
:platform="account?.platform"
|
||||
@@ -549,6 +550,7 @@
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Account, Proxy, Group } from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
@@ -571,6 +573,15 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Platform-specific hint for Base URL
|
||||
const baseUrlHint = computed(() => {
|
||||
if (!props.account) return t('admin.accounts.baseUrlHint')
|
||||
if (props.account.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
|
||||
if (props.account.platform === 'gemini') return t('admin.accounts.gemini.baseUrlHint')
|
||||
return t('admin.accounts.baseUrlHint')
|
||||
})
|
||||
|
||||
// Model mapping type
|
||||
interface ModelMapping {
|
||||
|
||||
@@ -297,7 +297,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.select-dropdown {
|
||||
@apply absolute z-[100] mt-2 w-full;
|
||||
@apply absolute left-0 z-[100] mt-2 min-w-full w-max max-w-[300px];
|
||||
@apply bg-white dark:bg-dark-800;
|
||||
@apply rounded-xl;
|
||||
@apply border border-gray-200 dark:border-dark-700;
|
||||
@@ -339,7 +339,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.select-option-label {
|
||||
@apply truncate;
|
||||
@apply flex-1 min-w-0 truncate text-left;
|
||||
}
|
||||
|
||||
.select-empty {
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Personal Section for Admin -->
|
||||
<div class="sidebar-section">
|
||||
<!-- Personal Section for Admin (hidden in simple mode) -->
|
||||
<div v-if="!authStore.isSimpleMode" class="sidebar-section">
|
||||
<div v-if="!sidebarCollapsed" class="sidebar-section-title">
|
||||
{{ t('nav.myAccount') }}
|
||||
</div>
|
||||
@@ -402,36 +402,54 @@ const ChevronDoubleRightIcon = {
|
||||
}
|
||||
|
||||
// User navigation items (for regular users)
|
||||
const userNavItems = computed(() => [
|
||||
{ path: '/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
|
||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon },
|
||||
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon },
|
||||
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon },
|
||||
{ path: '/profile', label: t('nav.profile'), icon: UserIcon }
|
||||
])
|
||||
const userNavItems = computed(() => {
|
||||
const items = [
|
||||
{ path: '/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
|
||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
||||
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
||||
{ path: '/profile', label: t('nav.profile'), icon: UserIcon }
|
||||
]
|
||||
return authStore.isSimpleMode ? items.filter(item => !item.hideInSimpleMode) : items
|
||||
})
|
||||
|
||||
// Personal navigation items (for admin's "My Account" section, without Dashboard)
|
||||
const personalNavItems = computed(() => [
|
||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon },
|
||||
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon },
|
||||
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon },
|
||||
{ path: '/profile', label: t('nav.profile'), icon: UserIcon }
|
||||
])
|
||||
const personalNavItems = computed(() => {
|
||||
const items = [
|
||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
||||
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
||||
{ path: '/profile', label: t('nav.profile'), icon: UserIcon }
|
||||
]
|
||||
return authStore.isSimpleMode ? items.filter(item => !item.hideInSimpleMode) : items
|
||||
})
|
||||
|
||||
// Admin navigation items
|
||||
const adminNavItems = computed(() => [
|
||||
{ path: '/admin/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
|
||||
{ path: '/admin/users', label: t('nav.users'), icon: UsersIcon },
|
||||
{ path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon },
|
||||
{ path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon },
|
||||
{ path: '/admin/accounts', label: t('nav.accounts'), icon: GlobeIcon },
|
||||
{ path: '/admin/proxies', label: t('nav.proxies'), icon: ServerIcon },
|
||||
{ path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon },
|
||||
{ path: '/admin/usage', label: t('nav.usage'), icon: ChartIcon },
|
||||
{ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon }
|
||||
])
|
||||
const adminNavItems = computed(() => {
|
||||
const baseItems = [
|
||||
{ path: '/admin/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
|
||||
{ path: '/admin/users', label: t('nav.users'), icon: UsersIcon, hideInSimpleMode: true },
|
||||
{ path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon, hideInSimpleMode: true },
|
||||
{ path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||
{ path: '/admin/accounts', label: t('nav.accounts'), icon: GlobeIcon },
|
||||
{ path: '/admin/proxies', label: t('nav.proxies'), icon: ServerIcon },
|
||||
{ path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true },
|
||||
{ path: '/admin/usage', label: t('nav.usage'), icon: ChartIcon },
|
||||
]
|
||||
|
||||
// 简单模式下,在系统设置前插入 API密钥
|
||||
if (authStore.isSimpleMode) {
|
||||
const filtered = baseItems.filter(item => !item.hideInSimpleMode)
|
||||
filtered.push({ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon })
|
||||
filtered.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||
return filtered
|
||||
}
|
||||
|
||||
baseItems.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||
return baseItems
|
||||
})
|
||||
|
||||
function toggleSidebar() {
|
||||
appStore.toggleSidebar()
|
||||
|
||||
@@ -676,14 +676,21 @@ export default {
|
||||
description: 'Description',
|
||||
platform: 'Platform',
|
||||
rateMultiplier: 'Rate Multiplier',
|
||||
status: 'Status'
|
||||
status: 'Status',
|
||||
exclusive: 'Exclusive Group'
|
||||
},
|
||||
enterGroupName: 'Enter group name',
|
||||
optionalDescription: 'Optional description',
|
||||
platformHint: 'Select the platform this group is associated with',
|
||||
platformNotEditable: 'Platform cannot be changed after creation',
|
||||
rateMultiplierHint: 'Cost multiplier for this group (e.g., 1.5 = 150% of base cost)',
|
||||
exclusiveHint: 'Exclusive (requires explicit user access)',
|
||||
exclusiveHint: 'Exclusive group, manually assign to specific users',
|
||||
exclusiveTooltip: {
|
||||
title: 'What is an exclusive group?',
|
||||
description: 'When enabled, users cannot see this group when creating API Keys. Only after an admin manually assigns a user to this group can they use it.',
|
||||
example: 'Use case:',
|
||||
exampleContent: 'Public group rate is 0.8. Create an exclusive group with 0.7 rate, manually assign VIP users to give them better pricing.'
|
||||
},
|
||||
noGroupsYet: 'No groups yet',
|
||||
createFirstGroup: 'Create your first group to organize API keys.',
|
||||
creating: 'Creating...',
|
||||
@@ -910,6 +917,11 @@ export default {
|
||||
apiKeyRequired: 'API Key *',
|
||||
apiKeyPlaceholder: 'sk-ant-api03-...',
|
||||
apiKeyHint: 'Your Claude Console API Key',
|
||||
// OpenAI specific hints
|
||||
openai: {
|
||||
baseUrlHint: 'Leave default for official OpenAI API',
|
||||
apiKeyHint: 'Your OpenAI API Key'
|
||||
},
|
||||
modelRestriction: 'Model Restriction (Optional)',
|
||||
modelWhitelist: 'Model Whitelist',
|
||||
modelMapping: 'Model Mapping',
|
||||
@@ -1096,6 +1108,7 @@ export default {
|
||||
modelPassthrough: 'Gemini Model Passthrough',
|
||||
modelPassthroughDesc:
|
||||
'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.',
|
||||
baseUrlHint: 'Leave default for official Gemini API',
|
||||
apiKeyHint: 'Your Gemini API Key (starts with AIza)'
|
||||
},
|
||||
// Re-Auth Modal
|
||||
@@ -1215,9 +1228,9 @@ export default {
|
||||
batchAdd: 'Quick Add',
|
||||
batchInput: 'Proxy List',
|
||||
batchInputPlaceholder:
|
||||
"Enter one proxy per line in the following formats:\nsocks5://user:pass@192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass@proxy.example.com:443",
|
||||
"Enter one proxy per line in the following formats:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443",
|
||||
batchInputHint:
|
||||
"Supports http, https, socks5 protocols. Format: protocol://[user:pass@]host:port",
|
||||
"Supports http, https, socks5 protocols. Format: protocol://[user:pass{'@'}]host:port",
|
||||
parsedCount: '{count} valid',
|
||||
invalidCount: '{count} invalid',
|
||||
duplicateCount: '{count} duplicate',
|
||||
|
||||
@@ -733,14 +733,15 @@ export default {
|
||||
platform: '平台',
|
||||
rateMultiplier: '费率倍数',
|
||||
status: '状态',
|
||||
exclusive: '专属分组',
|
||||
nameLabel: '分组名称',
|
||||
namePlaceholder: '请输入分组名称',
|
||||
descriptionLabel: '描述',
|
||||
descriptionPlaceholder: '请输入描述(可选)',
|
||||
rateMultiplierLabel: '费率倍数',
|
||||
rateMultiplierHint: '1.0 = 标准费率,0.5 = 半价,2.0 = 双倍',
|
||||
exclusiveLabel: '独占模式',
|
||||
exclusiveHint: '启用后,此分组的用户将独占使用分配的账号',
|
||||
exclusiveLabel: '专属分组',
|
||||
exclusiveHint: '专属分组,可以手动指定给用户',
|
||||
platformLabel: '平台限制',
|
||||
platformPlaceholder: '选择平台(留空则不限制)',
|
||||
accountsLabel: '指定账号',
|
||||
@@ -753,8 +754,14 @@ export default {
|
||||
yes: '是',
|
||||
no: '否'
|
||||
},
|
||||
exclusive: '独占',
|
||||
exclusiveHint: '启用后,此分组的用户将独占使用分配的账号',
|
||||
exclusive: '专属',
|
||||
exclusiveHint: '专属分组,可以手动指定给特定用户',
|
||||
exclusiveTooltip: {
|
||||
title: '什么是专属分组?',
|
||||
description: '开启后,用户在创建 API Key 时将无法看到此分组。只有管理员手动将用户分配到此分组后,用户才能使用。',
|
||||
example: '使用场景:',
|
||||
exampleContent: '公开分组费率 0.8,您可以创建一个费率 0.7 的专属分组,手动分配给 VIP 用户,让他们享受更优惠的价格。'
|
||||
},
|
||||
rateMultiplierHint: '1.0 = 标准费率,0.5 = 半价,2.0 = 双倍',
|
||||
platforms: {
|
||||
all: '全部平台',
|
||||
@@ -773,8 +780,8 @@ export default {
|
||||
allPlatforms: '全部平台',
|
||||
allStatus: '全部状态',
|
||||
allGroups: '全部分组',
|
||||
exclusiveFilter: '独占',
|
||||
nonExclusive: '非独占',
|
||||
exclusiveFilter: '专属',
|
||||
nonExclusive: '公开',
|
||||
public: '公开',
|
||||
rateAndAccounts: '{rate}x 费率 · {count} 个账号',
|
||||
accountsCount: '{count} 个账号',
|
||||
@@ -1058,6 +1065,11 @@ export default {
|
||||
apiKeyRequired: 'API Key *',
|
||||
apiKeyPlaceholder: 'sk-ant-api03-...',
|
||||
apiKeyHint: '您的 Claude Console API Key',
|
||||
// OpenAI specific hints
|
||||
openai: {
|
||||
baseUrlHint: '留空使用官方 OpenAI API',
|
||||
apiKeyHint: '您的 OpenAI API Key'
|
||||
},
|
||||
modelRestriction: '模型限制(可选)',
|
||||
modelWhitelist: '模型白名单',
|
||||
modelMapping: '模型映射',
|
||||
@@ -1226,7 +1238,8 @@ export default {
|
||||
gemini: {
|
||||
modelPassthrough: 'Gemini 直接转发模型',
|
||||
modelPassthroughDesc: '所有模型请求将直接转发至 Gemini API,不进行模型限制或映射。',
|
||||
apiKeyHint: 'Your Gemini API Key(以 AIza 开头)'
|
||||
baseUrlHint: '留空使用官方 Gemini API',
|
||||
apiKeyHint: '您的 Gemini API Key(以 AIza 开头)'
|
||||
},
|
||||
// Re-Auth Modal
|
||||
reAuthorizeAccount: '重新授权账号',
|
||||
@@ -1364,8 +1377,8 @@ export default {
|
||||
batchAdd: '快捷添加',
|
||||
batchInput: '代理列表',
|
||||
batchInputPlaceholder:
|
||||
"每行输入一个代理,支持以下格式:\nsocks5://user:pass@192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass@proxy.example.com:443",
|
||||
batchInputHint: "支持 http、https、socks5 协议,格式:协议://[用户名:密码@]主机:端口",
|
||||
"每行输入一个代理,支持以下格式:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443",
|
||||
batchInputHint: "支持 http、https、socks5 协议,格式:协议://[用户名:密码{'@'}]主机:端口",
|
||||
parsedCount: '有效 {count} 个',
|
||||
invalidCount: '无效 {count} 个',
|
||||
duplicateCount: '重复 {count} 个',
|
||||
|
||||
@@ -341,6 +341,23 @@ router.beforeEach((to, _from, next) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 简易模式下限制访问某些页面
|
||||
if (authStore.isSimpleMode) {
|
||||
const restrictedPaths = [
|
||||
'/admin/groups',
|
||||
'/admin/subscriptions',
|
||||
'/admin/redeem',
|
||||
'/subscriptions',
|
||||
'/redeem'
|
||||
]
|
||||
|
||||
if (restrictedPaths.some((path) => to.path.startsWith(path))) {
|
||||
// 简易模式下访问受限页面,重定向到仪表板
|
||||
next(authStore.isAdmin ? '/admin/dashboard' : '/dashboard')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// All checks passed, allow navigation
|
||||
next()
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, readonly } from 'vue'
|
||||
import { authAPI } from '@/api'
|
||||
import type { User, LoginRequest, RegisterRequest } from '@/types'
|
||||
|
||||
@@ -17,6 +17,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
const user = ref<User | null>(null)
|
||||
const token = ref<string | null>(null)
|
||||
const runMode = ref<'standard' | 'simple'>('standard')
|
||||
let refreshIntervalId: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// ==================== Computed ====================
|
||||
@@ -29,6 +30,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return user.value?.role === 'admin'
|
||||
})
|
||||
|
||||
const isSimpleMode = computed(() => runMode.value === 'simple')
|
||||
|
||||
// ==================== Actions ====================
|
||||
|
||||
/**
|
||||
@@ -98,16 +101,22 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
// Store token and user
|
||||
token.value = response.access_token
|
||||
user.value = response.user
|
||||
|
||||
// Extract run_mode if present
|
||||
if (response.user.run_mode) {
|
||||
runMode.value = response.user.run_mode
|
||||
}
|
||||
const { run_mode, ...userData } = response.user
|
||||
user.value = userData
|
||||
|
||||
// Persist to localStorage
|
||||
localStorage.setItem(AUTH_TOKEN_KEY, response.access_token)
|
||||
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(response.user))
|
||||
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userData))
|
||||
|
||||
// Start auto-refresh interval
|
||||
startAutoRefresh()
|
||||
|
||||
return response.user
|
||||
return userData
|
||||
} catch (error) {
|
||||
// Clear any partial state on error
|
||||
clearAuth()
|
||||
@@ -127,16 +136,22 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
// Store token and user
|
||||
token.value = response.access_token
|
||||
user.value = response.user
|
||||
|
||||
// Extract run_mode if present
|
||||
if (response.user.run_mode) {
|
||||
runMode.value = response.user.run_mode
|
||||
}
|
||||
const { run_mode, ...userDataWithoutRunMode } = response.user
|
||||
user.value = userDataWithoutRunMode
|
||||
|
||||
// Persist to localStorage
|
||||
localStorage.setItem(AUTH_TOKEN_KEY, response.access_token)
|
||||
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(response.user))
|
||||
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userDataWithoutRunMode))
|
||||
|
||||
// Start auto-refresh interval
|
||||
startAutoRefresh()
|
||||
|
||||
return response.user
|
||||
return userDataWithoutRunMode
|
||||
} catch (error) {
|
||||
// Clear any partial state on error
|
||||
clearAuth()
|
||||
@@ -168,13 +183,17 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedUser = await authAPI.getCurrentUser()
|
||||
user.value = updatedUser
|
||||
const response = await authAPI.getCurrentUser()
|
||||
if (response.data.run_mode) {
|
||||
runMode.value = response.data.run_mode
|
||||
}
|
||||
const { run_mode, ...userData } = response.data
|
||||
user.value = userData
|
||||
|
||||
// Update localStorage
|
||||
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(updatedUser))
|
||||
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(userData))
|
||||
|
||||
return updatedUser
|
||||
return userData
|
||||
} catch (error) {
|
||||
// If refresh fails with 401, clear auth state
|
||||
if ((error as { status?: number }).status === 401) {
|
||||
@@ -204,10 +223,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
// State
|
||||
user,
|
||||
token,
|
||||
runMode: readonly(runMode),
|
||||
|
||||
// Computed
|
||||
isAuthenticated,
|
||||
isAdmin,
|
||||
isSimpleMode,
|
||||
|
||||
// Actions
|
||||
login,
|
||||
|
||||
@@ -60,7 +60,11 @@ export interface PublicSettings {
|
||||
export interface AuthResponse {
|
||||
access_token: string
|
||||
token_type: string
|
||||
user: User
|
||||
user: User & { run_mode?: 'standard' | 'simple' }
|
||||
}
|
||||
|
||||
export interface CurrentUserResponse extends User {
|
||||
run_mode?: 'standard' | 'simple'
|
||||
}
|
||||
|
||||
// ==================== Subscription Types ====================
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, type ComponentPublicInstance } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Account, Proxy, Group } from '@/types'
|
||||
import type { Column } from '@/components/common/types'
|
||||
@@ -522,22 +523,34 @@ import { formatRelativeTime } from '@/utils/format'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Table columns
|
||||
const columns = computed<Column[]>(() => [
|
||||
{ key: 'select', label: '', sortable: false },
|
||||
{ key: 'name', label: t('admin.accounts.columns.name'), sortable: true },
|
||||
{ key: 'platform_type', label: t('admin.accounts.columns.platformType'), sortable: false },
|
||||
{ key: 'concurrency', label: t('admin.accounts.columns.concurrencyStatus'), sortable: false },
|
||||
{ key: 'status', label: t('admin.accounts.columns.status'), sortable: true },
|
||||
{ key: 'schedulable', label: t('admin.accounts.columns.schedulable'), sortable: true },
|
||||
{ key: 'today_stats', label: t('admin.accounts.columns.todayStats'), sortable: false },
|
||||
{ key: 'groups', label: t('admin.accounts.columns.groups'), sortable: false },
|
||||
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
|
||||
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
|
||||
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
|
||||
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
|
||||
])
|
||||
const columns = computed<Column[]>(() => {
|
||||
const cols: Column[] = [
|
||||
{ key: 'select', label: '', sortable: false },
|
||||
{ key: 'name', label: t('admin.accounts.columns.name'), sortable: true },
|
||||
{ key: 'platform_type', label: t('admin.accounts.columns.platformType'), sortable: false },
|
||||
{ key: 'concurrency', label: t('admin.accounts.columns.concurrencyStatus'), sortable: false },
|
||||
{ key: 'status', label: t('admin.accounts.columns.status'), sortable: true },
|
||||
{ key: 'schedulable', label: t('admin.accounts.columns.schedulable'), sortable: true },
|
||||
{ key: 'today_stats', label: t('admin.accounts.columns.todayStats'), sortable: false }
|
||||
]
|
||||
|
||||
// 简易模式下不显示分组列
|
||||
if (!authStore.isSimpleMode) {
|
||||
cols.push({ key: 'groups', label: t('admin.accounts.columns.groups'), sortable: false })
|
||||
}
|
||||
|
||||
cols.push(
|
||||
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
|
||||
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
|
||||
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
|
||||
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
|
||||
)
|
||||
|
||||
return cols
|
||||
})
|
||||
|
||||
// Filter options
|
||||
const platformOptions = computed(() => [
|
||||
|
||||
@@ -407,10 +407,20 @@ const trendData = ref<TrendDataPoint[]>([])
|
||||
const modelStats = ref<ModelStat[]>([])
|
||||
const userTrend = ref<UserUsageTrendPoint[]>([])
|
||||
|
||||
// Helper function to format date in local timezone
|
||||
const formatLocalDate = (date: Date): string => {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// Initialize date range immediately
|
||||
const now = new Date()
|
||||
const weekAgo = new Date(now)
|
||||
weekAgo.setDate(weekAgo.getDate() - 6)
|
||||
|
||||
// Date range
|
||||
const granularity = ref<'day' | 'hour'>('day')
|
||||
const startDate = ref('')
|
||||
const endDate = ref('')
|
||||
const startDate = ref(formatLocalDate(weekAgo))
|
||||
const endDate = ref(formatLocalDate(now))
|
||||
|
||||
// Granularity options for Select component
|
||||
const granularityOptions = computed(() => [
|
||||
@@ -597,18 +607,6 @@ const onDateRangeChange = (range: {
|
||||
loadChartData()
|
||||
}
|
||||
|
||||
// Initialize default date range
|
||||
const initializeDateRange = () => {
|
||||
const now = new Date()
|
||||
const today = now.toISOString().split('T')[0]
|
||||
const weekAgo = new Date(now)
|
||||
weekAgo.setDate(weekAgo.getDate() - 6)
|
||||
|
||||
startDate.value = weekAgo.toISOString().split('T')[0]
|
||||
endDate.value = today
|
||||
granularity.value = 'day'
|
||||
}
|
||||
|
||||
// Load data
|
||||
const loadDashboardStats = async () => {
|
||||
loading.value = true
|
||||
@@ -649,7 +647,6 @@ const loadChartData = async () => {
|
||||
|
||||
onMounted(() => {
|
||||
loadDashboardStats()
|
||||
initializeDateRange()
|
||||
loadChartData()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -282,34 +282,66 @@
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.groups.rateMultiplierHint') }}</p>
|
||||
</div>
|
||||
<div v-if="createForm.subscription_type !== 'subscription'" class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="createForm.is_exclusive = !createForm.is_exclusive"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
||||
createForm.is_exclusive ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
<div v-if="createForm.subscription_type !== 'subscription'">
|
||||
<div class="mb-1.5 flex items-center gap-1">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.groups.form.exclusive') }}
|
||||
</label>
|
||||
<!-- Help Tooltip -->
|
||||
<div class="group relative inline-flex">
|
||||
<svg
|
||||
class="h-3.5 w-3.5 cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<!-- Tooltip Popover -->
|
||||
<div class="pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<div class="rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800">
|
||||
<p class="mb-2 text-xs font-medium">{{ t('admin.groups.exclusiveTooltip.title') }}</p>
|
||||
<p class="mb-2 text-xs leading-relaxed text-gray-300">
|
||||
{{ t('admin.groups.exclusiveTooltip.description') }}
|
||||
</p>
|
||||
<div class="rounded bg-gray-800 p-2 dark:bg-gray-700">
|
||||
<p class="text-xs leading-relaxed text-gray-300">
|
||||
<span class="text-primary-400">💡 {{ t('admin.groups.exclusiveTooltip.example') }}</span>
|
||||
{{ t('admin.groups.exclusiveTooltip.exampleContent') }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Arrow -->
|
||||
<div class="absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="createForm.is_exclusive = !createForm.is_exclusive"
|
||||
:class="[
|
||||
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
|
||||
createForm.is_exclusive ? 'translate-x-6' : 'translate-x-1'
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
||||
createForm.is_exclusive ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
<label class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.groups.exclusiveHint') }}
|
||||
</label>
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
|
||||
createForm.is_exclusive ? 'translate-x-6' : 'translate-x-1'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ createForm.is_exclusive ? t('admin.groups.exclusive') : t('admin.groups.public') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subscription Configuration -->
|
||||
<div class="mt-4 border-t pt-4">
|
||||
<h4 class="mb-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.groups.subscription.title') }}
|
||||
</h4>
|
||||
|
||||
<div class="mb-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.groups.subscription.type') }}</label>
|
||||
<Select v-model="createForm.subscription_type" :options="subscriptionTypeOptions" />
|
||||
<p class="input-hint">{{ t('admin.groups.subscription.typeHint') }}</p>
|
||||
@@ -432,25 +464,61 @@
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="editForm.subscription_type !== 'subscription'" class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="editForm.is_exclusive = !editForm.is_exclusive"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
||||
editForm.is_exclusive ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
<div v-if="editForm.subscription_type !== 'subscription'">
|
||||
<div class="mb-1.5 flex items-center gap-1">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.groups.form.exclusive') }}
|
||||
</label>
|
||||
<!-- Help Tooltip -->
|
||||
<div class="group relative inline-flex">
|
||||
<svg
|
||||
class="h-3.5 w-3.5 cursor-help text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<!-- Tooltip Popover -->
|
||||
<div class="pointer-events-none absolute bottom-full left-0 z-50 mb-2 w-72 opacity-0 transition-all duration-200 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<div class="rounded-lg bg-gray-900 p-3 text-white shadow-lg dark:bg-gray-800">
|
||||
<p class="mb-2 text-xs font-medium">{{ t('admin.groups.exclusiveTooltip.title') }}</p>
|
||||
<p class="mb-2 text-xs leading-relaxed text-gray-300">
|
||||
{{ t('admin.groups.exclusiveTooltip.description') }}
|
||||
</p>
|
||||
<div class="rounded bg-gray-800 p-2 dark:bg-gray-700">
|
||||
<p class="text-xs leading-relaxed text-gray-300">
|
||||
<span class="text-primary-400">💡 {{ t('admin.groups.exclusiveTooltip.example') }}</span>
|
||||
{{ t('admin.groups.exclusiveTooltip.exampleContent') }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Arrow -->
|
||||
<div class="absolute -bottom-1.5 left-3 h-3 w-3 rotate-45 bg-gray-900 dark:bg-gray-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="editForm.is_exclusive = !editForm.is_exclusive"
|
||||
:class="[
|
||||
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
|
||||
editForm.is_exclusive ? 'translate-x-6' : 'translate-x-1'
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
||||
editForm.is_exclusive ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
<label class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.groups.exclusiveHint') }}
|
||||
</label>
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
|
||||
editForm.is_exclusive ? 'translate-x-6' : 'translate-x-1'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ editForm.is_exclusive ? t('admin.groups.exclusive') : t('admin.groups.public') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.groups.form.status') }}</label>
|
||||
@@ -459,11 +527,7 @@
|
||||
|
||||
<!-- Subscription Configuration -->
|
||||
<div class="mt-4 border-t pt-4">
|
||||
<h4 class="mb-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.groups.subscription.title') }}
|
||||
</h4>
|
||||
|
||||
<div class="mb-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.groups.subscription.type') }}</label>
|
||||
<Select
|
||||
v-model="editForm.subscription_type"
|
||||
|
||||
@@ -736,9 +736,19 @@ const groupOptions = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
// Helper function to format date in local timezone
|
||||
const formatLocalDate = (date: Date): string => {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// Initialize date range immediately
|
||||
const now = new Date()
|
||||
const weekAgo = new Date(now)
|
||||
weekAgo.setDate(weekAgo.getDate() - 6)
|
||||
|
||||
// Date range state
|
||||
const startDate = ref('')
|
||||
const endDate = ref('')
|
||||
const startDate = ref(formatLocalDate(weekAgo))
|
||||
const endDate = ref(formatLocalDate(now))
|
||||
|
||||
const filters = ref<AdminUsageQueryParams>({
|
||||
user_id: undefined,
|
||||
@@ -752,18 +762,9 @@ const filters = ref<AdminUsageQueryParams>({
|
||||
end_date: undefined
|
||||
})
|
||||
|
||||
// Initialize default date range (last 7 days)
|
||||
const initializeDateRange = () => {
|
||||
const now = new Date()
|
||||
const today = now.toISOString().split('T')[0]
|
||||
const weekAgo = new Date(now)
|
||||
weekAgo.setDate(weekAgo.getDate() - 6)
|
||||
|
||||
startDate.value = weekAgo.toISOString().split('T')[0]
|
||||
endDate.value = today
|
||||
filters.value.start_date = startDate.value
|
||||
filters.value.end_date = endDate.value
|
||||
}
|
||||
// Initialize filters with date range
|
||||
filters.value.start_date = startDate.value
|
||||
filters.value.end_date = endDate.value
|
||||
|
||||
// User search with debounce
|
||||
const debounceSearchUsers = () => {
|
||||
@@ -988,9 +989,12 @@ const loadModelOptions = async () => {
|
||||
const endDate = new Date()
|
||||
const startDateRange = new Date(endDate)
|
||||
startDateRange.setDate(startDateRange.getDate() - 29)
|
||||
// Use local timezone instead of UTC
|
||||
const endDateStr = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')}`
|
||||
const startDateStr = `${startDateRange.getFullYear()}-${String(startDateRange.getMonth() + 1).padStart(2, '0')}-${String(startDateRange.getDate()).padStart(2, '0')}`
|
||||
const response = await adminAPI.dashboard.getModelStats({
|
||||
start_date: startDateRange.toISOString().split('T')[0],
|
||||
end_date: endDate.toISOString().split('T')[0]
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr
|
||||
})
|
||||
const uniqueModels = new Set<string>()
|
||||
response.models?.forEach((stat) => {
|
||||
@@ -1022,7 +1026,13 @@ const resetFilters = () => {
|
||||
}
|
||||
granularity.value = 'day'
|
||||
// Reset date range to default (last 7 days)
|
||||
initializeDateRange()
|
||||
const now = new Date()
|
||||
const weekAgo = new Date(now)
|
||||
weekAgo.setDate(weekAgo.getDate() - 6)
|
||||
startDate.value = formatLocalDate(weekAgo)
|
||||
endDate.value = formatLocalDate(now)
|
||||
filters.value.start_date = startDate.value
|
||||
filters.value.end_date = endDate.value
|
||||
pagination.value.page = 1
|
||||
loadApiKeys()
|
||||
loadUsageLogs()
|
||||
@@ -1114,7 +1124,6 @@ const hideTooltip = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeDateRange()
|
||||
loadFilterOptions()
|
||||
loadApiKeys()
|
||||
loadUsageLogs()
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<!-- Row 1: Core Stats -->
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<!-- Balance -->
|
||||
<div class="card p-4">
|
||||
<div v-if="!authStore.isSimpleMode" class="card p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
|
||||
<svg
|
||||
@@ -727,10 +727,20 @@ const trendChartRef = ref<ChartComponentRef | null>(null)
|
||||
// Recent usage
|
||||
const recentUsage = ref<UsageLog[]>([])
|
||||
|
||||
// Helper function to format date in local timezone
|
||||
const formatLocalDate = (date: Date): string => {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// Initialize date range immediately (not in onMounted)
|
||||
const now = new Date()
|
||||
const weekAgo = new Date(now)
|
||||
weekAgo.setDate(weekAgo.getDate() - 6)
|
||||
|
||||
// Date range
|
||||
const granularity = ref<'day' | 'hour'>('day')
|
||||
const startDate = ref('')
|
||||
const endDate = ref('')
|
||||
const startDate = ref(formatLocalDate(weekAgo))
|
||||
const endDate = ref(formatLocalDate(now))
|
||||
|
||||
// Granularity options for Select component
|
||||
const granularityOptions = computed(() => [
|
||||
@@ -963,18 +973,6 @@ const onDateRangeChange = (range: {
|
||||
loadChartData()
|
||||
}
|
||||
|
||||
// Initialize default date range
|
||||
const initializeDateRange = () => {
|
||||
const now = new Date()
|
||||
const today = now.toISOString().split('T')[0]
|
||||
const weekAgo = new Date(now)
|
||||
weekAgo.setDate(weekAgo.getDate() - 6)
|
||||
|
||||
startDate.value = weekAgo.toISOString().split('T')[0]
|
||||
endDate.value = today
|
||||
granularity.value = 'day'
|
||||
}
|
||||
|
||||
// Load data
|
||||
const loadDashboardStats = async () => {
|
||||
loading.value = true
|
||||
@@ -1015,8 +1013,11 @@ const loadChartData = async () => {
|
||||
const loadRecentUsage = async () => {
|
||||
loadingUsage.value = true
|
||||
try {
|
||||
const endDate = new Date().toISOString().split('T')[0]
|
||||
const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
|
||||
// Use local timezone instead of UTC
|
||||
const now = new Date()
|
||||
const endDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||||
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
||||
const startDate = `${weekAgo.getFullYear()}-${String(weekAgo.getMonth() + 1).padStart(2, '0')}-${String(weekAgo.getDate()).padStart(2, '0')}`
|
||||
const usageResponse = await usageAPI.getByDateRange(startDate, endDate)
|
||||
recentUsage.value = usageResponse.items.slice(0, 5)
|
||||
} catch (error) {
|
||||
@@ -1035,9 +1036,6 @@ onMounted(async () => {
|
||||
console.error('Failed to refresh subscription status:', error)
|
||||
})
|
||||
|
||||
// Initialize date range (synchronous)
|
||||
initializeDateRange()
|
||||
|
||||
// Load chart data and recent usage in parallel (non-critical)
|
||||
Promise.all([loadChartData(), loadRecentUsage()]).catch((error) => {
|
||||
console.error('Error loading secondary data:', error)
|
||||
|
||||
@@ -488,9 +488,19 @@ const apiKeyOptions = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
// Helper function to format date in local timezone
|
||||
const formatLocalDate = (date: Date): string => {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// Initialize date range immediately
|
||||
const now = new Date()
|
||||
const weekAgo = new Date(now)
|
||||
weekAgo.setDate(weekAgo.getDate() - 6)
|
||||
|
||||
// Date range state
|
||||
const startDate = ref('')
|
||||
const endDate = ref('')
|
||||
const startDate = ref(formatLocalDate(weekAgo))
|
||||
const endDate = ref(formatLocalDate(now))
|
||||
|
||||
const filters = ref<UsageQueryParams>({
|
||||
api_key_id: undefined,
|
||||
@@ -498,18 +508,9 @@ const filters = ref<UsageQueryParams>({
|
||||
end_date: undefined
|
||||
})
|
||||
|
||||
// Initialize default date range (last 7 days)
|
||||
const initializeDateRange = () => {
|
||||
const now = new Date()
|
||||
const today = now.toISOString().split('T')[0]
|
||||
const weekAgo = new Date(now)
|
||||
weekAgo.setDate(weekAgo.getDate() - 6)
|
||||
|
||||
startDate.value = weekAgo.toISOString().split('T')[0]
|
||||
endDate.value = today
|
||||
filters.value.start_date = startDate.value
|
||||
filters.value.end_date = endDate.value
|
||||
}
|
||||
// Initialize filters with date range
|
||||
filters.value.start_date = startDate.value
|
||||
filters.value.end_date = endDate.value
|
||||
|
||||
// Handle date range change from DateRangePicker
|
||||
const onDateRangeChange = (range: {
|
||||
@@ -629,7 +630,13 @@ const resetFilters = () => {
|
||||
end_date: undefined
|
||||
}
|
||||
// Reset date range to default (last 7 days)
|
||||
initializeDateRange()
|
||||
const now = new Date()
|
||||
const weekAgo = new Date(now)
|
||||
weekAgo.setDate(weekAgo.getDate() - 6)
|
||||
startDate.value = formatLocalDate(weekAgo)
|
||||
endDate.value = formatLocalDate(now)
|
||||
filters.value.start_date = startDate.value
|
||||
filters.value.end_date = endDate.value
|
||||
pagination.page = 1
|
||||
loadUsageLogs()
|
||||
loadUsageStats()
|
||||
@@ -772,7 +779,6 @@ const hideTooltip = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeDateRange()
|
||||
loadApiKeys()
|
||||
loadUsageLogs()
|
||||
loadUsageStats()
|
||||
|
||||
Reference in New Issue
Block a user