mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor: 优化调度候选排序与用量写入链路并改进 Fernet 缓存与前端批量列表
This commit is contained in:
@@ -61,7 +61,7 @@
|
||||
|
||||
<div class="flex flex-col gap-2 text-xs lg:flex-row lg:items-center lg:justify-between">
|
||||
<div class="text-muted-foreground">
|
||||
共 {{ filteredTotal }} 个匹配账号,当前页 {{ pageKeys.length }} 个,已选 {{ selectedCount }} 个
|
||||
共 {{ filteredTotal }} 个匹配账号,当前页 {{ pageKeyRows.length }} 个,已选 {{ selectedCount }} 个
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<div class="mr-1 flex items-center gap-2">
|
||||
@@ -77,7 +77,7 @@
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2 text-[11px]"
|
||||
:disabled="pageKeys.length === 0 || loading || executing || selectAllFiltered"
|
||||
:disabled="pageKeyRows.length === 0 || loading || executing || selectAllFiltered"
|
||||
@click="toggleSelectCurrentPage"
|
||||
>
|
||||
{{ isCurrentPageFullySelected ? '取消本页全选' : '本页全选' }}
|
||||
@@ -106,54 +106,54 @@
|
||||
正在加载账号列表...
|
||||
</div>
|
||||
<div
|
||||
v-else-if="pageKeys.length === 0"
|
||||
v-else-if="pageKeyRows.length === 0"
|
||||
class="py-10 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
无匹配账号
|
||||
</div>
|
||||
<label
|
||||
v-for="key in pageKeys"
|
||||
:key="key.key_id"
|
||||
v-for="row in pageKeyRows"
|
||||
:key="row.key.key_id"
|
||||
class="flex items-center gap-2.5 px-3 py-2 border-b last:border-b-0 cursor-pointer hover:bg-muted/30"
|
||||
>
|
||||
<Checkbox
|
||||
:checked="selectAllFiltered || selectedIdSet.has(key.key_id)"
|
||||
:checked="selectAllFiltered || selectedIdSet.has(row.key.key_id)"
|
||||
:disabled="executing || selectAllFiltered"
|
||||
@update:checked="(checked) => toggleOne(key.key_id, checked === true)"
|
||||
@update:checked="(checked) => toggleOne(row.key.key_id, checked === true)"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="text-xs font-medium truncate">{{ key.key_name || '未命名' }}</span>
|
||||
<span class="text-xs font-medium truncate">{{ row.key.key_name || '未命名' }}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-[10px] px-1 py-0 h-4 shrink-0"
|
||||
>{{ normalizeAuthTypeLabel(key) }}</Badge>
|
||||
>{{ row.authTypeLabel }}</Badge>
|
||||
<Badge
|
||||
v-if="getStatusBadgeLabel(key)"
|
||||
v-if="row.statusBadgeLabel"
|
||||
variant="destructive"
|
||||
class="text-[10px] px-1 py-0 h-4 shrink-0"
|
||||
:title="getStatusBadgeTitle(key)"
|
||||
>{{ getStatusBadgeLabel(key) }}</Badge>
|
||||
:title="row.statusBadgeTitle"
|
||||
>{{ row.statusBadgeLabel }}</Badge>
|
||||
<Badge
|
||||
v-if="key.oauth_plan_type"
|
||||
v-if="row.key.oauth_plan_type"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1 py-0 h-4 shrink-0"
|
||||
>{{ key.oauth_plan_type }}</Badge>
|
||||
>{{ row.key.oauth_plan_type }}</Badge>
|
||||
<Badge
|
||||
v-if="getOAuthOrgBadge(key)"
|
||||
v-if="row.oauthOrgBadge"
|
||||
variant="secondary"
|
||||
class="text-[10px] px-1 py-0 h-4 shrink-0"
|
||||
:title="getOAuthOrgBadge(key)?.title"
|
||||
>{{ getOAuthOrgBadge(key)?.label }}</Badge>
|
||||
:title="row.oauthOrgBadge.title"
|
||||
>{{ row.oauthOrgBadge.label }}</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 mt-0.5 text-[11px] text-muted-foreground flex-wrap">
|
||||
<span :class="key.is_active ? '' : 'text-destructive'">{{ key.is_active ? '启用' : '禁用' }}</span>
|
||||
<span v-if="getQuotaText(key)">{{ shortenQuota(getQuotaText(key) || '') }}</span>
|
||||
<span v-if="key.proxy?.node_id">独立代理</span>
|
||||
<span :class="row.key.is_active ? '' : 'text-destructive'">{{ row.key.is_active ? '启用' : '禁用' }}</span>
|
||||
<span v-if="row.quotaText">{{ row.quotaTextShort }}</span>
|
||||
<span v-if="row.key.proxy?.node_id">独立代理</span>
|
||||
<span
|
||||
v-if="key.last_used_at"
|
||||
v-if="row.lastUsedRelative"
|
||||
class="ml-auto shrink-0"
|
||||
>{{ formatRelativeTime(key.last_used_at) }}</span>
|
||||
>{{ row.lastUsedRelative }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
@@ -331,6 +331,17 @@ type BatchActionOption = {
|
||||
destructive?: boolean
|
||||
}
|
||||
|
||||
type PageKeyRow = {
|
||||
key: PoolKeyDetail
|
||||
authTypeLabel: string
|
||||
statusBadgeLabel: string | null
|
||||
statusBadgeTitle: string
|
||||
oauthOrgBadge: ReturnType<typeof getOAuthOrgBadge>
|
||||
quotaText: string | null
|
||||
quotaTextShort: string
|
||||
lastUsedRelative: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
providerId: string
|
||||
@@ -407,17 +418,32 @@ const totalPages = computed(() => Math.max(1, Math.ceil(filteredTotal.value / PA
|
||||
const isAllFilteredSelected = computed(() => selectAllFiltered.value && filteredTotal.value > 0)
|
||||
const isPartiallyFilteredSelected = computed(() => !selectAllFiltered.value && selectedKeyIds.value.length > 0)
|
||||
const hasActiveFilters = computed(() => searchText.value.trim().length > 0 || activeQuickSelectors.value.length > 0)
|
||||
const pageKeyRows = computed<PageKeyRow[]>(() => pageKeys.value.map((key) => {
|
||||
const statusBadgeLabel = getStatusBadgeLabel(key)
|
||||
const quotaText = getQuotaText(key)
|
||||
|
||||
return {
|
||||
key,
|
||||
authTypeLabel: normalizeAuthTypeLabel(key),
|
||||
statusBadgeLabel,
|
||||
statusBadgeTitle: statusBadgeLabel ? getStatusBadgeTitle(key) : '',
|
||||
oauthOrgBadge: getOAuthOrgBadge(key),
|
||||
quotaText,
|
||||
quotaTextShort: quotaText ? shortenQuota(quotaText) : '',
|
||||
lastUsedRelative: key.last_used_at ? formatRelativeTime(key.last_used_at) : '',
|
||||
}
|
||||
}))
|
||||
const selectedOnCurrentPageCount = computed(() => {
|
||||
if (selectAllFiltered.value) return pageKeys.value.length
|
||||
if (selectAllFiltered.value) return pageKeyRows.value.length
|
||||
let count = 0
|
||||
for (const key of pageKeys.value) {
|
||||
if (selectedIdSet.value.has(key.key_id)) count += 1
|
||||
for (const row of pageKeyRows.value) {
|
||||
if (selectedIdSet.value.has(row.key.key_id)) count += 1
|
||||
}
|
||||
return count
|
||||
})
|
||||
const isCurrentPageFullySelected = computed(() => {
|
||||
if (selectAllFiltered.value || pageKeys.value.length === 0) return false
|
||||
return selectedOnCurrentPageCount.value === pageKeys.value.length
|
||||
if (selectAllFiltered.value || pageKeyRows.value.length === 0) return false
|
||||
return selectedOnCurrentPageCount.value === pageKeyRows.value.length
|
||||
})
|
||||
const canClearSelection = computed(() => selectAllFiltered.value || selectedKeyIds.value.length > 0)
|
||||
const activeQuickSelectorSet = computed(() => new Set(activeQuickSelectors.value))
|
||||
|
||||
@@ -40,4 +40,43 @@ describe('getOAuthOrgBadge', () => {
|
||||
|
||||
expect(badge).toBeNull()
|
||||
})
|
||||
|
||||
it('reuses cached badge objects when identity fields are unchanged', () => {
|
||||
const identity = {
|
||||
oauth_account_id: 'acct-demo-001',
|
||||
oauth_account_name: 'Workspace Alpha',
|
||||
oauth_account_user_id: 'user-1__acct-demo-001',
|
||||
oauth_organizations: [
|
||||
{ id: 'org-personal-1234', title: 'Personal', is_default: true },
|
||||
],
|
||||
}
|
||||
|
||||
const first = getOAuthOrgBadge(identity)
|
||||
const second = getOAuthOrgBadge(identity)
|
||||
|
||||
expect(first).toBe(second)
|
||||
})
|
||||
|
||||
it('invalidates the cached badge when the selected organization changes in place', () => {
|
||||
const organizations = [
|
||||
{ id: 'org-personal-1234', title: 'Personal', is_default: true },
|
||||
{ id: 'org-team-5678', title: 'Team', is_default: false },
|
||||
]
|
||||
const identity = {
|
||||
oauth_account_id: 'acct-demo-001',
|
||||
oauth_organizations: organizations,
|
||||
}
|
||||
|
||||
const first = getOAuthOrgBadge(identity)
|
||||
organizations[0].is_default = false
|
||||
organizations[1].is_default = true
|
||||
const second = getOAuthOrgBadge(identity)
|
||||
|
||||
expect(first).not.toBe(second)
|
||||
expect(second).toEqual({
|
||||
id: 'org-team-5678',
|
||||
label: 'org:team-5678',
|
||||
title: 'account_id: acct-demo-001 | org_id: org-team-5678 | org_title: Team',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,23 +7,65 @@ type OAuthIdentityDisplayValue = {
|
||||
oauth_organizations?: OAuthOrganizationInfo[] | null
|
||||
} | null | undefined
|
||||
|
||||
function formatOAuthIdentityShort(
|
||||
value: string | null | undefined,
|
||||
head = 8,
|
||||
tail = 6,
|
||||
): string {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) return ''
|
||||
if (normalized.length <= head + tail + 3) return normalized
|
||||
return `${normalized.slice(0, head)}...${normalized.slice(-tail)}`
|
||||
type OAuthOrgBadge = {
|
||||
id: string
|
||||
label: string
|
||||
title: string
|
||||
}
|
||||
|
||||
type SelectedOAuthOrganization = {
|
||||
id: string
|
||||
title: string
|
||||
rawId: unknown
|
||||
rawTitle: unknown
|
||||
rawIsDefault: boolean | null | undefined
|
||||
index: number
|
||||
}
|
||||
|
||||
type OAuthOrgBadgeCacheEntry = {
|
||||
rawAccountId: unknown
|
||||
rawAccountName: unknown
|
||||
rawAccountUserId: unknown
|
||||
rawOrganizations: OAuthOrganizationInfo[] | null
|
||||
organizationCount: number
|
||||
selectedOrganizationIndex: number
|
||||
selectedOrganizationRawId: unknown
|
||||
selectedOrganizationRawTitle: unknown
|
||||
selectedOrganizationRawIsDefault: boolean | null | undefined
|
||||
result: OAuthOrgBadge | null
|
||||
}
|
||||
|
||||
const oauthOrgBadgeCache = new WeakMap<object, OAuthOrgBadgeCacheEntry>()
|
||||
|
||||
function readStr(raw: unknown): string {
|
||||
return typeof raw === 'string' ? raw.trim() : ''
|
||||
}
|
||||
|
||||
function formatOAuthIdentityShort(value: string, head = 8, tail = 6): string {
|
||||
if (!value) return ''
|
||||
if (value.length <= head + tail + 3) return value
|
||||
return `${value.slice(0, head)}...${value.slice(-tail)}`
|
||||
}
|
||||
|
||||
function stripOAuthOrganizationPrefix(orgId: string): string {
|
||||
return orgId.replace(/^org[-_:]+/i, '').trim()
|
||||
if (orgId.length < 4) return orgId
|
||||
|
||||
const first = orgId.charCodeAt(0) | 32
|
||||
const second = orgId.charCodeAt(1) | 32
|
||||
const third = orgId.charCodeAt(2) | 32
|
||||
|
||||
if (first !== 111 || second !== 114 || third !== 103) {
|
||||
return orgId
|
||||
}
|
||||
|
||||
let index = 3
|
||||
while (index < orgId.length) {
|
||||
const code = orgId.charCodeAt(index)
|
||||
if (code !== 45 && code !== 95 && code !== 58) break
|
||||
index += 1
|
||||
}
|
||||
|
||||
return index === 3 ? orgId : orgId.slice(index)
|
||||
}
|
||||
|
||||
function formatOAuthOrganizationBadge(orgId: string): string {
|
||||
@@ -45,60 +87,154 @@ function formatOAuthAccountUserBadge(accountUserId: string): string {
|
||||
|
||||
function getPrimaryOAuthOrganization(
|
||||
value: OAuthIdentityDisplayValue,
|
||||
): { id: string; title: string } | null {
|
||||
const organizations: OAuthOrganizationInfo[] = Array.isArray(value?.oauth_organizations)
|
||||
): SelectedOAuthOrganization | null {
|
||||
const organizations = Array.isArray(value?.oauth_organizations)
|
||||
? value.oauth_organizations
|
||||
: []
|
||||
let firstWithId: OAuthOrganizationInfo | null = null
|
||||
: null
|
||||
if (!organizations?.length) return null
|
||||
|
||||
let firstWithId: SelectedOAuthOrganization | null = null
|
||||
|
||||
for (let index = 0; index < organizations.length; index += 1) {
|
||||
const org = organizations[index]
|
||||
if (typeof org?.id !== 'string' || !org.id.trim()) continue
|
||||
if (!firstWithId) firstWithId = org
|
||||
const rawId = org?.id
|
||||
if (typeof rawId !== 'string') continue
|
||||
|
||||
const id = rawId.trim()
|
||||
if (!id) continue
|
||||
|
||||
const selectedOrganization: SelectedOAuthOrganization = {
|
||||
id,
|
||||
title: readStr(org?.title),
|
||||
rawId,
|
||||
rawTitle: org?.title,
|
||||
rawIsDefault: org?.is_default,
|
||||
index,
|
||||
}
|
||||
|
||||
if (!firstWithId) firstWithId = selectedOrganization
|
||||
if (org.is_default) {
|
||||
firstWithId = org
|
||||
firstWithId = selectedOrganization
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstWithId?.id) return null
|
||||
return firstWithId
|
||||
}
|
||||
|
||||
return {
|
||||
id: firstWithId.id.trim(),
|
||||
title: typeof firstWithId.title === 'string' ? firstWithId.title.trim() : '',
|
||||
function appendTitlePart(title: string, label: string, value: string): string {
|
||||
if (!value) return title
|
||||
if (!title) return `${label}: ${value}`
|
||||
return `${title} | ${label}: ${value}`
|
||||
}
|
||||
|
||||
function buildOAuthIdentityTitle(
|
||||
accountName: string,
|
||||
accountId: string,
|
||||
accountUserId: string,
|
||||
organization: SelectedOAuthOrganization | null,
|
||||
): string {
|
||||
let title = ''
|
||||
|
||||
title = appendTitlePart(title, 'name', accountName)
|
||||
title = appendTitlePart(title, 'account_id', accountId)
|
||||
title = appendTitlePart(title, 'account_user_id', accountUserId)
|
||||
title = appendTitlePart(title, 'org_id', organization?.id || '')
|
||||
title = appendTitlePart(title, 'org_title', organization?.title || '')
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
function getCachedOAuthOrgBadge(
|
||||
value: OAuthIdentityDisplayValue,
|
||||
organization: SelectedOAuthOrganization | null,
|
||||
): OAuthOrgBadge | null | undefined {
|
||||
if (!value || typeof value !== 'object') return undefined
|
||||
|
||||
const cached = oauthOrgBadgeCache.get(value)
|
||||
if (!cached) return undefined
|
||||
|
||||
const organizations = Array.isArray(value.oauth_organizations)
|
||||
? value.oauth_organizations
|
||||
: null
|
||||
|
||||
if (
|
||||
cached.rawAccountId === value.oauth_account_id
|
||||
&& cached.rawAccountName === value.oauth_account_name
|
||||
&& cached.rawAccountUserId === value.oauth_account_user_id
|
||||
&& cached.rawOrganizations === organizations
|
||||
&& cached.organizationCount === (organizations?.length || 0)
|
||||
&& cached.selectedOrganizationIndex === (organization?.index ?? -1)
|
||||
&& cached.selectedOrganizationRawId === organization?.rawId
|
||||
&& cached.selectedOrganizationRawTitle === organization?.rawTitle
|
||||
&& cached.selectedOrganizationRawIsDefault === organization?.rawIsDefault
|
||||
) {
|
||||
return cached.result
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function setCachedOAuthOrgBadge(
|
||||
value: OAuthIdentityDisplayValue,
|
||||
organization: SelectedOAuthOrganization | null,
|
||||
result: OAuthOrgBadge | null,
|
||||
): void {
|
||||
if (!value || typeof value !== 'object') return
|
||||
|
||||
const organizations = Array.isArray(value.oauth_organizations)
|
||||
? value.oauth_organizations
|
||||
: null
|
||||
|
||||
oauthOrgBadgeCache.set(value, {
|
||||
rawAccountId: value.oauth_account_id,
|
||||
rawAccountName: value.oauth_account_name,
|
||||
rawAccountUserId: value.oauth_account_user_id,
|
||||
rawOrganizations: organizations,
|
||||
organizationCount: organizations?.length || 0,
|
||||
selectedOrganizationIndex: organization?.index ?? -1,
|
||||
selectedOrganizationRawId: organization?.rawId,
|
||||
selectedOrganizationRawTitle: organization?.rawTitle,
|
||||
selectedOrganizationRawIsDefault: organization?.rawIsDefault,
|
||||
result,
|
||||
})
|
||||
}
|
||||
|
||||
export function getOAuthOrgBadge(
|
||||
value: OAuthIdentityDisplayValue,
|
||||
): { id: string; label: string; title: string } | null {
|
||||
): OAuthOrgBadge | null {
|
||||
const org = getPrimaryOAuthOrganization(value)
|
||||
const cached = getCachedOAuthOrgBadge(value, org)
|
||||
if (cached !== undefined) return cached
|
||||
|
||||
const accountId = readStr(value?.oauth_account_id)
|
||||
const accountName = readStr(value?.oauth_account_name)
|
||||
const accountUserId = readStr(value?.oauth_account_user_id)
|
||||
|
||||
const badgeId = org?.id || accountId || accountUserId || ''
|
||||
const label = org?.id
|
||||
if (!badgeId) {
|
||||
setCachedOAuthOrgBadge(value, org, null)
|
||||
return null
|
||||
}
|
||||
|
||||
const label = org
|
||||
? formatOAuthOrganizationBadge(org.id)
|
||||
: accountId
|
||||
? formatOAuthAccountBadge(accountId)
|
||||
: accountUserId
|
||||
? formatOAuthAccountUserBadge(accountUserId)
|
||||
: ''
|
||||
if (!badgeId || !label) return null
|
||||
if (!label) {
|
||||
setCachedOAuthOrgBadge(value, org, null)
|
||||
return null
|
||||
}
|
||||
|
||||
const titleParts = [
|
||||
accountName ? `name: ${accountName}` : '',
|
||||
accountId ? `account_id: ${accountId}` : '',
|
||||
accountUserId ? `account_user_id: ${accountUserId}` : '',
|
||||
org?.id ? `org_id: ${org.id}` : '',
|
||||
org?.title ? `org_title: ${org.title}` : '',
|
||||
].filter(Boolean)
|
||||
|
||||
return {
|
||||
const result: OAuthOrgBadge = {
|
||||
id: badgeId,
|
||||
label,
|
||||
title: titleParts.join(' | '),
|
||||
title: buildOAuthIdentityTitle(accountName, accountId, accountUserId, org),
|
||||
}
|
||||
|
||||
setCachedOAuthOrgBadge(value, org, result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -406,7 +406,7 @@
|
||||
v-for="key in keyPage.keys"
|
||||
:key="key.key_id"
|
||||
class="border-b border-border/40 last:border-b-0 hover:bg-muted/30 transition-colors"
|
||||
:class="getRowClass(key)"
|
||||
:class="keyUiStateMap[key.key_id]?.rowClass || ''"
|
||||
>
|
||||
<TableCell
|
||||
class="py-3"
|
||||
@@ -469,7 +469,7 @@
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:disabled="refreshingOAuthKeyId === key.key_id"
|
||||
:title="getOAuthRefreshButtonTitle(key)"
|
||||
:title="keyUiStateMap[key.key_id]?.oauthRefreshButtonTitle || ''"
|
||||
@click.stop="handleRefreshOAuth(key)"
|
||||
>
|
||||
<RefreshCw
|
||||
@@ -478,33 +478,33 @@
|
||||
/>
|
||||
</Button>
|
||||
<span
|
||||
v-if="getVisibleOAuthState(key)"
|
||||
v-if="keyUiStateMap[key.key_id]?.visibleOAuthState"
|
||||
class="text-[10px]"
|
||||
:class="{
|
||||
'text-destructive': getVisibleOAuthState(key)?.isInvalid || getVisibleOAuthState(key)?.isExpired,
|
||||
'text-warning': getVisibleOAuthState(key)?.isExpiringSoon && !getVisibleOAuthState(key)?.isExpired && !getVisibleOAuthState(key)?.isInvalid,
|
||||
'text-muted-foreground': !getVisibleOAuthState(key)?.isExpired && !getVisibleOAuthState(key)?.isExpiringSoon && !getVisibleOAuthState(key)?.isInvalid
|
||||
'text-destructive': keyUiStateMap[key.key_id]?.visibleOAuthState?.isInvalid || keyUiStateMap[key.key_id]?.visibleOAuthState?.isExpired,
|
||||
'text-warning': keyUiStateMap[key.key_id]?.visibleOAuthState?.isExpiringSoon && !keyUiStateMap[key.key_id]?.visibleOAuthState?.isExpired && !keyUiStateMap[key.key_id]?.visibleOAuthState?.isInvalid,
|
||||
'text-muted-foreground': !keyUiStateMap[key.key_id]?.visibleOAuthState?.isExpired && !keyUiStateMap[key.key_id]?.visibleOAuthState?.isExpiringSoon && !keyUiStateMap[key.key_id]?.visibleOAuthState?.isInvalid
|
||||
}"
|
||||
:title="getOAuthStatusTitle(key)"
|
||||
:title="keyUiStateMap[key.key_id]?.oauthStatusTitle || ''"
|
||||
>
|
||||
{{ getVisibleOAuthState(key)?.text }}
|
||||
{{ keyUiStateMap[key.key_id]?.visibleOAuthState?.text }}
|
||||
</span>
|
||||
</template>
|
||||
<Badge
|
||||
v-if="key.oauth_plan_type"
|
||||
variant="outline"
|
||||
class="text-[9px] px-1 py-0 h-4 shrink-0"
|
||||
:class="getOAuthPlanTypeClass(key.oauth_plan_type)"
|
||||
:class="keyUiStateMap[key.key_id]?.planClass || ''"
|
||||
>
|
||||
{{ formatOAuthPlanType(key.oauth_plan_type) }}
|
||||
{{ keyUiStateMap[key.key_id]?.planLabel }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="getOAuthOrgBadge(key)"
|
||||
v-if="keyUiStateMap[key.key_id]?.oauthOrgBadge"
|
||||
variant="secondary"
|
||||
class="text-[9px] px-1 py-0 h-4 shrink-0"
|
||||
:title="getOAuthOrgBadge(key)?.title"
|
||||
:title="keyUiStateMap[key.key_id]?.oauthOrgBadge?.title"
|
||||
>
|
||||
{{ getOAuthOrgBadge(key)?.label }}
|
||||
{{ keyUiStateMap[key.key_id]?.oauthOrgBadge?.label }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -546,10 +546,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-else-if="getQuotaFallbackText(key)"
|
||||
:class="getQuotaTextClass(getQuotaFallbackText(key) || '')"
|
||||
v-else-if="keyUiStateMap[key.key_id]?.quotaFallbackText"
|
||||
:class="keyUiStateMap[key.key_id]?.quotaTextClass || ''"
|
||||
>
|
||||
{{ getQuotaFallbackText(key) }}
|
||||
{{ keyUiStateMap[key.key_id]?.quotaFallbackText }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
@@ -580,16 +580,16 @@
|
||||
</TableCell>
|
||||
<TableCell class="py-3 text-center">
|
||||
<span class="text-[10px] text-muted-foreground whitespace-nowrap">
|
||||
{{ key.last_used_at ? formatRelativeTime(key.last_used_at) : '-' }}
|
||||
{{ keyUiStateMap[key.key_id]?.lastUsedRelative || '-' }}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-3 text-center">
|
||||
<Badge
|
||||
:variant="getSchedulingBadgeVariant(key)"
|
||||
:variant="keyUiStateMap[key.key_id]?.schedulingBadgeVariant || 'default'"
|
||||
class="text-[10px]"
|
||||
:title="getSchedulingTitle(key)"
|
||||
:title="keyUiStateMap[key.key_id]?.schedulingTitle || ''"
|
||||
>
|
||||
{{ getSchedulingBadgeLabel(key) }}
|
||||
{{ keyUiStateMap[key.key_id]?.schedulingBadgeLabel }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="py-3 px-2 align-middle">
|
||||
@@ -705,7 +705,7 @@
|
||||
v-for="key in keyPage.keys"
|
||||
:key="key.key_id"
|
||||
class="p-4 sm:p-5 hover:bg-muted/30 transition-colors"
|
||||
:class="getRowClass(key)"
|
||||
:class="keyUiStateMap[key.key_id]?.rowClass || ''"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-medium truncate">
|
||||
@@ -714,11 +714,11 @@
|
||||
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<Badge
|
||||
:variant="getSchedulingBadgeVariant(key)"
|
||||
:variant="keyUiStateMap[key.key_id]?.schedulingBadgeVariant || 'default'"
|
||||
class="text-[10px] shrink-0"
|
||||
:title="getSchedulingTitle(key)"
|
||||
:title="keyUiStateMap[key.key_id]?.schedulingTitle || ''"
|
||||
>
|
||||
{{ getSchedulingBadgeLabel(key) }}
|
||||
{{ keyUiStateMap[key.key_id]?.schedulingBadgeLabel }}
|
||||
</Badge>
|
||||
<span
|
||||
v-if="key.cooldown_ttl_seconds"
|
||||
@@ -727,7 +727,7 @@
|
||||
冷却 {{ formatTTL(key.cooldown_ttl_seconds) }}
|
||||
</span>
|
||||
<template
|
||||
v-for="item in getMobileTagItems(key)"
|
||||
v-for="item in keyUiStateMap[key.key_id]?.mobileTagItems || []"
|
||||
:key="`${key.key_id}-${item.key}`"
|
||||
>
|
||||
<button
|
||||
@@ -744,7 +744,7 @@
|
||||
v-else-if="item.key === 'plan'"
|
||||
variant="outline"
|
||||
class="text-[9px] px-1 py-0 h-4 shrink-0"
|
||||
:class="key.oauth_plan_type ? getOAuthPlanTypeClass(key.oauth_plan_type) : ''"
|
||||
:class="keyUiStateMap[key.key_id]?.planClass || ''"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Badge>
|
||||
@@ -752,7 +752,7 @@
|
||||
v-else-if="item.key === 'org'"
|
||||
variant="secondary"
|
||||
class="text-[9px] px-1 py-0 h-4 shrink-0"
|
||||
:title="getOAuthOrgBadge(key)?.title"
|
||||
:title="keyUiStateMap[key.key_id]?.oauthOrgBadge?.title"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Badge>
|
||||
@@ -775,7 +775,7 @@
|
||||
<span class="mx-1.5 text-muted-foreground/40">|</span>
|
||||
<span class="font-medium text-foreground/90">费用:{{ formatStatUsd(key.total_cost_usd) }}</span>
|
||||
<span class="mx-1.5 text-muted-foreground/40">|</span>
|
||||
<span class="font-medium text-foreground/90">最后使用:{{ key.last_used_at ? formatRelativeTime(key.last_used_at) : '-' }}</span>
|
||||
<span class="font-medium text-foreground/90">最后使用:{{ keyUiStateMap[key.key_id]?.lastUsedRelative || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -819,10 +819,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="getQuotaFallbackText(key)"
|
||||
:class="getQuotaTextClass(getQuotaFallbackText(key) || '')"
|
||||
v-else-if="keyUiStateMap[key.key_id]?.quotaFallbackText"
|
||||
:class="keyUiStateMap[key.key_id]?.quotaTextClass || ''"
|
||||
>
|
||||
{{ getQuotaFallbackText(key) }}
|
||||
{{ keyUiStateMap[key.key_id]?.quotaFallbackText }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
@@ -834,7 +834,7 @@
|
||||
|
||||
<div class="flex items-center gap-0.5">
|
||||
<div
|
||||
v-for="actionId in getMobileActionIds(key)"
|
||||
v-for="actionId in keyUiStateMap[key.key_id]?.mobileActionIds || []"
|
||||
:key="`${key.key_id}-${actionId}`"
|
||||
class="min-w-0 flex-1 flex justify-center"
|
||||
>
|
||||
@@ -864,7 +864,7 @@
|
||||
size="icon"
|
||||
class="h-7 w-7 shrink-0"
|
||||
:disabled="refreshingOAuthKeyId === key.key_id"
|
||||
:title="getOAuthRefreshButtonTitle(key)"
|
||||
:title="keyUiStateMap[key.key_id]?.oauthRefreshButtonTitle || ''"
|
||||
@click.stop="handleRefreshOAuth(key)"
|
||||
>
|
||||
<RefreshCw
|
||||
@@ -1626,6 +1626,24 @@ interface QuotaProgressItem {
|
||||
updatedAtSeconds?: number | null
|
||||
}
|
||||
|
||||
type PoolKeyUiState = {
|
||||
rowClass: string
|
||||
schedulingBadgeLabel: string
|
||||
schedulingBadgeVariant: PoolStatusVariant
|
||||
schedulingTitle: string
|
||||
oauthOrgBadge: ReturnType<typeof getOAuthOrgBadge>
|
||||
visibleOAuthState: ReturnType<typeof getOAuthStatusDisplay>
|
||||
oauthStatusTitle: string
|
||||
oauthRefreshButtonTitle: string
|
||||
planLabel: string
|
||||
planClass: string
|
||||
quotaFallbackText: string | null
|
||||
quotaTextClass: string
|
||||
lastUsedRelative: string
|
||||
mobileTagItems: PoolMobileTagItem[]
|
||||
mobileActionIds: PoolMobileActionId[]
|
||||
}
|
||||
|
||||
const quotaProgressMap = computed<Record<string, QuotaProgressItem[]>>(() => {
|
||||
const map: Record<string, QuotaProgressItem[]> = {}
|
||||
for (const key of keyPage.value.keys) {
|
||||
@@ -1634,6 +1652,42 @@ const quotaProgressMap = computed<Record<string, QuotaProgressItem[]>>(() => {
|
||||
return map
|
||||
})
|
||||
|
||||
const keyUiStateMap = computed<Record<string, PoolKeyUiState>>(() => {
|
||||
const map: Record<string, PoolKeyUiState> = {}
|
||||
|
||||
for (const key of keyPage.value.keys) {
|
||||
const visibleOAuthState = getVisibleOAuthState(key)
|
||||
const oauthOrgBadge = getOAuthOrgBadge(key)
|
||||
const quotaFallbackText = getQuotaFallbackText(key)
|
||||
const canRefreshToken = canRefreshOAuthCredential(key)
|
||||
|
||||
map[key.key_id] = {
|
||||
rowClass: getRowClass(key),
|
||||
schedulingBadgeLabel: getSchedulingBadgeLabel(key),
|
||||
schedulingBadgeVariant: getSchedulingBadgeVariant(key),
|
||||
schedulingTitle: getSchedulingTitle(key),
|
||||
oauthOrgBadge,
|
||||
visibleOAuthState,
|
||||
oauthStatusTitle: visibleOAuthState ? getOAuthStatusTitle(key) : '',
|
||||
oauthRefreshButtonTitle: canRefreshToken ? getOAuthRefreshButtonTitle(key) : '',
|
||||
planLabel: key.oauth_plan_type ? formatOAuthPlanType(key.oauth_plan_type) : '',
|
||||
planClass: key.oauth_plan_type ? getOAuthPlanTypeClass(key.oauth_plan_type) : '',
|
||||
quotaFallbackText,
|
||||
quotaTextClass: quotaFallbackText ? getQuotaTextClass(quotaFallbackText) : '',
|
||||
lastUsedRelative: key.last_used_at ? formatRelativeTime(key.last_used_at) : '-',
|
||||
mobileTagItems: getMobileTagItems(key),
|
||||
mobileActionIds: splitPoolMobileActions({
|
||||
canDownloadOrCopy: true,
|
||||
canRefreshToken,
|
||||
canClearCooldown: Boolean(key.cooldown_reason),
|
||||
hasProxy: true,
|
||||
}).primary,
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
})
|
||||
|
||||
const quotaRefreshSupported = computed(() => {
|
||||
return selectedProviderType.value === 'codex'
|
||||
|| selectedProviderType.value === 'kiro'
|
||||
@@ -2506,15 +2560,6 @@ function getMobileTagItems(key: PoolKeyDetail): PoolMobileTagItem[] {
|
||||
})
|
||||
}
|
||||
|
||||
function getMobileActionIds(key: PoolKeyDetail): PoolMobileActionId[] {
|
||||
return splitPoolMobileActions({
|
||||
canDownloadOrCopy: true,
|
||||
canRefreshToken: canRefreshOAuthCredential(key),
|
||||
canClearCooldown: Boolean(key.cooldown_reason),
|
||||
hasProxy: true,
|
||||
}).primary
|
||||
}
|
||||
|
||||
function getMobileTagClass(item: PoolMobileTagItem): string {
|
||||
if (item.tone === 'danger') {
|
||||
return 'border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-300'
|
||||
|
||||
Reference in New Issue
Block a user