mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Fix score popover duplication
This commit is contained in:
@@ -678,14 +678,14 @@
|
|||||||
</span>
|
</span>
|
||||||
<Popover
|
<Popover
|
||||||
v-if="key.pool_score"
|
v-if="key.pool_score"
|
||||||
:open="scorePopoverOpenKeyId === key.key_id"
|
:open="scoreDesktopPopoverOpenKeyId === key.key_id"
|
||||||
@update:open="(open: boolean) => handleScorePopoverToggle(key.key_id, open)"
|
@update:open="(open: boolean) => handleScoreDesktopPopoverToggle(key.key_id, open)"
|
||||||
>
|
>
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-5 w-5 rounded-full text-muted-foreground hover:text-foreground"
|
class="h-5 w-5 rounded-full border border-transparent text-muted-foreground/80 hover:border-border/60 hover:bg-muted/60 hover:text-foreground"
|
||||||
title="查看评分计算结果"
|
title="查看评分计算结果"
|
||||||
aria-label="查看评分计算结果"
|
aria-label="查看评分计算结果"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -694,36 +694,39 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
v-if="scorePopoverOpenKeyId === key.key_id"
|
v-if="scoreDesktopPopoverOpenKeyId === key.key_id"
|
||||||
class="w-96 max-w-[calc(100vw-2rem)] p-3"
|
class="w-[22rem] max-w-[calc(100vw-1rem)] overflow-hidden rounded-xl border-border/60 bg-card/95 p-0 text-card-foreground shadow-xl shadow-black/5 backdrop-blur supports-[backdrop-filter]:bg-card/90"
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="center"
|
align="end"
|
||||||
|
:side-offset="8"
|
||||||
>
|
>
|
||||||
<div class="space-y-2 text-left">
|
<div class="text-left">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3 border-b border-border/60 bg-muted/30 px-3 py-2.5">
|
||||||
<span class="text-xs font-medium">评分计算结果</span>
|
<span class="text-xs font-semibold text-foreground">评分计算结果</span>
|
||||||
<span class="font-mono text-xs tabular-nums">
|
<span class="font-mono text-xs tabular-nums text-foreground/90">
|
||||||
{{ formatPoolScore(key.pool_score?.score) }}
|
{{ formatPoolScore(key.pool_score?.score) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-1.5">
|
<div class="space-y-2 px-3 py-2.5">
|
||||||
<Badge
|
<div class="flex flex-wrap items-center gap-1.5">
|
||||||
variant="outline"
|
<Badge
|
||||||
class="text-[10px]"
|
variant="outline"
|
||||||
>
|
class="h-5 rounded-md border-border/60 bg-background/60 px-2 text-[10px] font-normal"
|
||||||
{{ getPoolScoreHardStateLabel(key.pool_score?.hard_state) }}
|
>
|
||||||
</Badge>
|
{{ getPoolScoreHardStateLabel(key.pool_score?.hard_state) }}
|
||||||
<Badge
|
</Badge>
|
||||||
variant="secondary"
|
<Badge
|
||||||
class="text-[10px]"
|
variant="secondary"
|
||||||
>
|
class="h-5 rounded-md px-2 text-[10px] font-normal"
|
||||||
{{ getPoolScoreProbeStatusLabel(key.pool_score?.probe_status) }}
|
>
|
||||||
</Badge>
|
{{ getPoolScoreProbeStatusLabel(key.pool_score?.probe_status) }}
|
||||||
<span class="text-[10px] text-muted-foreground">
|
</Badge>
|
||||||
更新 {{ formatUnixSeconds(key.pool_score?.updated_at) }}
|
<span class="text-[10px] text-muted-foreground">
|
||||||
</span>
|
更新 {{ formatUnixSeconds(key.pool_score?.updated_at) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<pre class="max-h-56 overflow-auto rounded-md border border-border/50 bg-muted/30 px-3 py-2 font-mono text-[11px] leading-5 text-muted-foreground whitespace-pre-wrap break-words">{{ formatPoolScoreReason(key.pool_score?.score_reason) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<pre class="max-h-64 overflow-auto rounded-lg bg-muted/40 p-3 text-[11px] leading-5 text-foreground whitespace-pre-wrap break-words">{{ formatPoolScoreReason(key.pool_score?.score_reason) }}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -1000,14 +1003,14 @@
|
|||||||
</span>
|
</span>
|
||||||
<Popover
|
<Popover
|
||||||
v-if="key.pool_score"
|
v-if="key.pool_score"
|
||||||
:open="scorePopoverOpenKeyId === key.key_id"
|
:open="scoreMobilePopoverOpenKeyId === key.key_id"
|
||||||
@update:open="(open: boolean) => handleScorePopoverToggle(key.key_id, open)"
|
@update:open="(open: boolean) => handleScoreMobilePopoverToggle(key.key_id, open)"
|
||||||
>
|
>
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-5 w-5 rounded-full text-muted-foreground hover:text-foreground"
|
class="h-5 w-5 rounded-full border border-transparent text-muted-foreground/80 hover:border-border/60 hover:bg-muted/60 hover:text-foreground"
|
||||||
title="查看评分计算结果"
|
title="查看评分计算结果"
|
||||||
aria-label="查看评分计算结果"
|
aria-label="查看评分计算结果"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -1016,36 +1019,39 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
v-if="scorePopoverOpenKeyId === key.key_id"
|
v-if="scoreMobilePopoverOpenKeyId === key.key_id"
|
||||||
class="w-96 max-w-[calc(100vw-2rem)] p-3"
|
class="w-[22rem] max-w-[calc(100vw-1rem)] overflow-hidden rounded-xl border-border/60 bg-card/95 p-0 text-card-foreground shadow-xl shadow-black/5 backdrop-blur supports-[backdrop-filter]:bg-card/90"
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="center"
|
align="end"
|
||||||
|
:side-offset="8"
|
||||||
>
|
>
|
||||||
<div class="space-y-2 text-left">
|
<div class="text-left">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3 border-b border-border/60 bg-muted/30 px-3 py-2.5">
|
||||||
<span class="text-xs font-medium">评分计算结果</span>
|
<span class="text-xs font-semibold text-foreground">评分计算结果</span>
|
||||||
<span class="font-mono text-xs tabular-nums">
|
<span class="font-mono text-xs tabular-nums text-foreground/90">
|
||||||
{{ formatPoolScore(key.pool_score?.score) }}
|
{{ formatPoolScore(key.pool_score?.score) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-1.5">
|
<div class="space-y-2 px-3 py-2.5">
|
||||||
<Badge
|
<div class="flex flex-wrap items-center gap-1.5">
|
||||||
variant="outline"
|
<Badge
|
||||||
class="text-[10px]"
|
variant="outline"
|
||||||
>
|
class="h-5 rounded-md border-border/60 bg-background/60 px-2 text-[10px] font-normal"
|
||||||
{{ getPoolScoreHardStateLabel(key.pool_score?.hard_state) }}
|
>
|
||||||
</Badge>
|
{{ getPoolScoreHardStateLabel(key.pool_score?.hard_state) }}
|
||||||
<Badge
|
</Badge>
|
||||||
variant="secondary"
|
<Badge
|
||||||
class="text-[10px]"
|
variant="secondary"
|
||||||
>
|
class="h-5 rounded-md px-2 text-[10px] font-normal"
|
||||||
{{ getPoolScoreProbeStatusLabel(key.pool_score?.probe_status) }}
|
>
|
||||||
</Badge>
|
{{ getPoolScoreProbeStatusLabel(key.pool_score?.probe_status) }}
|
||||||
<span class="text-[10px] text-muted-foreground">
|
</Badge>
|
||||||
更新 {{ formatUnixSeconds(key.pool_score?.updated_at) }}
|
<span class="text-[10px] text-muted-foreground">
|
||||||
</span>
|
更新 {{ formatUnixSeconds(key.pool_score?.updated_at) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<pre class="max-h-56 overflow-auto rounded-md border border-border/50 bg-muted/30 px-3 py-2 font-mono text-[11px] leading-5 text-muted-foreground whitespace-pre-wrap break-words">{{ formatPoolScoreReason(key.pool_score?.score_reason) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<pre class="max-h-64 overflow-auto rounded-lg bg-muted/40 p-3 text-[11px] leading-5 text-foreground whitespace-pre-wrap break-words">{{ formatPoolScoreReason(key.pool_score?.score_reason) }}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -1849,7 +1855,8 @@ async function selectProvider(
|
|||||||
closeProviderProxyPopovers()
|
closeProviderProxyPopovers()
|
||||||
proxyDesktopPopoverOpenKeyId.value = null
|
proxyDesktopPopoverOpenKeyId.value = null
|
||||||
proxyMobilePopoverOpenKeyId.value = null
|
proxyMobilePopoverOpenKeyId.value = null
|
||||||
scorePopoverOpenKeyId.value = null
|
scoreDesktopPopoverOpenKeyId.value = null
|
||||||
|
scoreMobilePopoverOpenKeyId.value = null
|
||||||
suppressFiltersWatch = true
|
suppressFiltersWatch = true
|
||||||
if (!options.preservePagination) {
|
if (!options.preservePagination) {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
@@ -1913,7 +1920,8 @@ const resettingCycleKeyId = ref<string | null>(null)
|
|||||||
const savingProxyKeyId = ref<string | null>(null)
|
const savingProxyKeyId = ref<string | null>(null)
|
||||||
const proxyDesktopPopoverOpenKeyId = ref<string | null>(null)
|
const proxyDesktopPopoverOpenKeyId = ref<string | null>(null)
|
||||||
const proxyMobilePopoverOpenKeyId = ref<string | null>(null)
|
const proxyMobilePopoverOpenKeyId = ref<string | null>(null)
|
||||||
const scorePopoverOpenKeyId = ref<string | null>(null)
|
const scoreDesktopPopoverOpenKeyId = ref<string | null>(null)
|
||||||
|
const scoreMobilePopoverOpenKeyId = ref<string | null>(null)
|
||||||
const deletingKeyId = ref<string | null>(null)
|
const deletingKeyId = ref<string | null>(null)
|
||||||
const togglingKeyId = ref<string | null>(null)
|
const togglingKeyId = ref<string | null>(null)
|
||||||
const editingPriorityKeyId = ref<string | null>(null)
|
const editingPriorityKeyId = ref<string | null>(null)
|
||||||
@@ -2597,8 +2605,18 @@ function getKeyProxyNodeName(key: PoolKeyDetail): string | null {
|
|||||||
return node ? node.name : `${key.proxy.node_id.slice(0, 8)}...`
|
return node ? node.name : `${key.proxy.node_id.slice(0, 8)}...`
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScorePopoverToggle(keyId: string, open: boolean) {
|
function handleScoreDesktopPopoverToggle(keyId: string, open: boolean) {
|
||||||
scorePopoverOpenKeyId.value = open ? keyId : null
|
scoreDesktopPopoverOpenKeyId.value = open ? keyId : null
|
||||||
|
if (open) {
|
||||||
|
scoreMobilePopoverOpenKeyId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScoreMobilePopoverToggle(keyId: string, open: boolean) {
|
||||||
|
scoreMobilePopoverOpenKeyId.value = open ? keyId : null
|
||||||
|
if (open) {
|
||||||
|
scoreDesktopPopoverOpenKeyId.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleProxyDesktopPopoverToggle(keyId: string, open: boolean) {
|
function handleProxyDesktopPopoverToggle(keyId: string, open: boolean) {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ vi.mock('lucide-vue-next', async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
vi.mock('@/components/ui', async () => {
|
vi.mock('@/components/ui', async () => {
|
||||||
const { defineComponent, h } = await import('vue')
|
const { computed, defineComponent, h, inject, provide } = await import('vue')
|
||||||
const passthrough = (name: string, tag = 'div') => defineComponent({
|
const passthrough = (name: string, tag = 'div') => defineComponent({
|
||||||
name,
|
name,
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
@@ -205,6 +205,53 @@ vi.mock('@/components/ui', async () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const popoverContextKey = Symbol('PopoverStubContext')
|
||||||
|
|
||||||
|
const Popover = defineComponent({
|
||||||
|
name: 'PopoverStub',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
open: Boolean,
|
||||||
|
},
|
||||||
|
emits: ['update:open'],
|
||||||
|
setup(props, { slots, emit }) {
|
||||||
|
const context = {
|
||||||
|
open: computed(() => props.open),
|
||||||
|
toggle: () => emit('update:open', !props.open),
|
||||||
|
}
|
||||||
|
provide(popoverContextKey, context)
|
||||||
|
return () => slots.default?.()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const PopoverTrigger = defineComponent({
|
||||||
|
name: 'PopoverTriggerStub',
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup(_, { attrs, slots }) {
|
||||||
|
const context = inject<{ open: { value: boolean }, toggle: () => void } | null>(popoverContextKey, null)
|
||||||
|
return () => {
|
||||||
|
return h('span', {
|
||||||
|
...attrs,
|
||||||
|
onClickCapture: () => {
|
||||||
|
context?.toggle()
|
||||||
|
},
|
||||||
|
}, slots.default?.())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const PopoverContent = defineComponent({
|
||||||
|
name: 'PopoverContentStub',
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup(_, { attrs, slots }) {
|
||||||
|
const context = inject<{ open: { value: boolean } } | null>(popoverContextKey, null)
|
||||||
|
return () => {
|
||||||
|
if (!context?.open.value) return null
|
||||||
|
return h('div', { ...attrs, 'data-state': 'open' }, slots.default?.())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Card: passthrough('CardStub'),
|
Card: passthrough('CardStub'),
|
||||||
Badge: passthrough('BadgeStub', 'span'),
|
Badge: passthrough('BadgeStub', 'span'),
|
||||||
@@ -225,9 +272,9 @@ vi.mock('@/components/ui', async () => {
|
|||||||
TableCell: passthrough('TableCellStub', 'td'),
|
TableCell: passthrough('TableCellStub', 'td'),
|
||||||
Switch,
|
Switch,
|
||||||
Pagination,
|
Pagination,
|
||||||
Popover: passthrough('PopoverStub'),
|
Popover,
|
||||||
PopoverTrigger: passthrough('PopoverTriggerStub'),
|
PopoverTrigger,
|
||||||
PopoverContent: passthrough('PopoverContentStub'),
|
PopoverContent,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -559,6 +606,52 @@ describe('PoolManagement Codex cycle stats mode', () => {
|
|||||||
expect(root.querySelectorAll('button[title="查看评分计算结果"]').length).toBeGreaterThan(0)
|
expect(root.querySelectorAll('button[title="查看评分计算结果"]').length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('opens only one score popover across desktop and mobile layouts', async () => {
|
||||||
|
const scoredKey = createPoolKey('codex', {
|
||||||
|
pool_score: {
|
||||||
|
id: 'pms-account-score',
|
||||||
|
capability: 'account',
|
||||||
|
scope_kind: 'account',
|
||||||
|
scope_id: null,
|
||||||
|
score: 0.662,
|
||||||
|
hard_state: 'available',
|
||||||
|
score_version: 1,
|
||||||
|
score_reason: {
|
||||||
|
rules: {
|
||||||
|
probe_failure_penalty: 0.05,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
last_ranked_at: 1_700_000_000,
|
||||||
|
last_scheduled_at: null,
|
||||||
|
last_success_at: null,
|
||||||
|
last_failure_at: null,
|
||||||
|
failure_count: 0,
|
||||||
|
last_probe_attempt_at: null,
|
||||||
|
last_probe_success_at: null,
|
||||||
|
last_probe_failure_at: null,
|
||||||
|
probe_failure_count: 0,
|
||||||
|
probe_status: 'ok',
|
||||||
|
updated_at: 1_700_000_050,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
endpointMocks.getPoolOverview.mockResolvedValue({ items: [createOverview('codex')] })
|
||||||
|
endpointMocks.listPoolKeys.mockResolvedValue(createKeyPage(scoredKey))
|
||||||
|
endpointMocks.getProvider.mockResolvedValue(createProvider('codex'))
|
||||||
|
|
||||||
|
const root = mountPoolManagement()
|
||||||
|
await settle()
|
||||||
|
|
||||||
|
const helpButtons = root.querySelectorAll<HTMLButtonElement>('button[title="查看评分计算结果"]')
|
||||||
|
expect(helpButtons.length).toBe(2)
|
||||||
|
|
||||||
|
helpButtons[0]?.click()
|
||||||
|
await settle()
|
||||||
|
|
||||||
|
expect(root.querySelectorAll('pre').length).toBe(1)
|
||||||
|
expect(root.textContent).toContain('评分计算结果')
|
||||||
|
expect(root.textContent).toContain('0.662')
|
||||||
|
})
|
||||||
|
|
||||||
it('refreshes quota only for keys on the current page', async () => {
|
it('refreshes quota only for keys on the current page', async () => {
|
||||||
const pageKeys = [
|
const pageKeys = [
|
||||||
createPoolKey('codex', { key_id: 'codex-page-key-1', quota_updated_at: null }),
|
createPoolKey('codex', { key_id: 'codex-page-key-1', quota_updated_at: null }),
|
||||||
|
|||||||
Reference in New Issue
Block a user