style(frontend): refine badges and cycle stats

This commit is contained in:
elky
2026-07-18 00:05:22 +08:00
parent 88a057b8d9
commit e558f55cd9
8 changed files with 53 additions and 19 deletions
@@ -0,0 +1,31 @@
import { afterEach, describe, expect, it } from 'vitest'
import { createApp, h, type App } from 'vue'
import Badge from '../badge.vue'
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
afterEach(() => {
for (const { app, root } of mountedApps.splice(0)) {
app.unmount()
root.remove()
}
})
describe('Badge', () => {
it('renders the transparent outline variant without the card background', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp({
render: () => h(Badge, { variant: 'outline-transparent' }, () => 'Fast'),
})
app.mount(root)
mountedApps.push({ app, root })
const badge = root.firstElementChild
expect(badge?.classList.contains('border-border')).toBe(true)
expect(badge?.classList.contains('bg-transparent')).toBe(true)
expect(badge?.classList.contains('bg-card/50')).toBe(false)
})
})
+2 -1
View File
@@ -20,6 +20,7 @@ const badgeVariants = cva(
destructive: destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground border-border bg-card/50', outline: 'text-foreground border-border bg-card/50',
'outline-transparent': 'text-foreground border-border bg-transparent',
success: success:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
warning: warning:
@@ -35,7 +36,7 @@ const badgeVariants = cva(
) )
interface Props { interface Props {
variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'dark' variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'outline-transparent' | 'success' | 'warning' | 'dark'
class?: string class?: string
} }
@@ -7,15 +7,13 @@
<div <div
v-for="row in cycleMetricRows" v-for="row in cycleMetricRows"
:key="`${row.key}-${variant}-cycle-row`" :key="`${row.key}-${variant}-cycle-row`"
class="flex items-center justify-between gap-2" class="truncate"
:title="`${row.label} ${row.valueText}`"
> >
<span class="shrink-0 text-muted-foreground">{{ row.label }}</span>
<span <span
class="min-w-0 truncate text-right font-medium tabular-nums text-foreground/90"
:data-testid="variant === 'desktop' ? `pool-stats-cycle-${row.key}` : undefined" :data-testid="variant === 'desktop' ? `pool-stats-cycle-${row.key}` : undefined"
:title="row.valueText"
> >
{{ row.valueText }} {{ row.label }} {{ row.valueText }}
</span> </span>
</div> </div>
</div> </div>
@@ -116,7 +114,7 @@ const cycleMetricRows = computed(() => {
}) })
const cycleContainerClass = computed(() => [ const cycleContainerClass = computed(() => [
'mx-auto w-[132px] space-y-1 text-[9px] leading-3', 'mx-auto w-[132px] space-y-0.5 text-[10px] leading-4 text-foreground/90 tabular-nums',
props.variant === 'mobile' ? 'py-0.5' : '', props.variant === 'mobile' ? 'py-0.5' : '',
].filter(Boolean).join(' ')) ].filter(Boolean).join(' '))
@@ -30,7 +30,7 @@ describe('pool key display panels', () => {
expect(root.querySelector('[data-testid="pool-stats-cycle-text"]')).toBeTruthy() expect(root.querySelector('[data-testid="pool-stats-cycle-text"]')).toBeTruthy()
expect(root.querySelector('[data-testid="pool-stats-cycle-text"]')?.className).toContain('w-[132px]') expect(root.querySelector('[data-testid="pool-stats-cycle-text"]')?.className).toContain('w-[132px]')
expect(root.querySelector('[data-testid="pool-stats-cycle-request_count"]')?.textContent?.trim()).toBe('12/88') expect(root.querySelector('[data-testid="pool-stats-cycle-request_count"]')?.textContent?.trim()).toBe('请求 12/88')
expect(root.querySelector('[data-testid="pool-stats-cycle-small-overlay"]')).toBeNull() expect(root.querySelector('[data-testid="pool-stats-cycle-small-overlay"]')).toBeNull()
expect(root.querySelector('[data-testid="pool-stats-cycle-large-base"]')).toBeNull() expect(root.querySelector('[data-testid="pool-stats-cycle-large-base"]')).toBeNull()
expect(root.textContent).not.toContain('5H') expect(root.textContent).not.toContain('5H')
@@ -98,7 +98,7 @@ describe('pool key display panels', () => {
app.use(createI18n()) app.use(createI18n())
app.mount(root) app.mount(root)
expect(root.querySelector('[data-testid="pool-stats-cycle-request_count"]')?.textContent?.trim()).toBe('31') expect(root.querySelector('[data-testid="pool-stats-cycle-request_count"]')?.textContent?.trim()).toBe('请求 31')
expect(root.querySelector('[data-testid="pool-stats-cycle-single-marker"]')).toBeNull() expect(root.querySelector('[data-testid="pool-stats-cycle-single-marker"]')).toBeNull()
expect(root.querySelector('[data-testid="pool-stats-cycle-bar-request_count"]')).toBeNull() expect(root.querySelector('[data-testid="pool-stats-cycle-bar-request_count"]')).toBeNull()
expect(root.textContent).not.toContain('月') expect(root.textContent).not.toContain('月')
@@ -30,7 +30,7 @@
:key="badge.key" :key="badge.key"
:data-usage-model-badge="badge.key" :data-usage-model-badge="badge.key"
:data-request-detail-model-badge="context === 'detail' ? badge.key : undefined" :data-request-detail-model-badge="context === 'detail' ? badge.key : undefined"
variant="outline" :variant="badge.variant"
class="h-4 shrink-0 whitespace-nowrap rounded-full px-1.5 text-[10px] leading-4" class="h-4 shrink-0 whitespace-nowrap rounded-full px-1.5 text-[10px] leading-4"
:class="badge.className" :class="badge.className"
:title="badge.title" :title="badge.title"
@@ -52,7 +52,7 @@
:key="badge.key" :key="badge.key"
:data-usage-model-badge="badge.key" :data-usage-model-badge="badge.key"
:data-request-detail-model-badge="context === 'detail' ? badge.key : undefined" :data-request-detail-model-badge="context === 'detail' ? badge.key : undefined"
variant="outline" :variant="badge.variant"
class="h-4 shrink-0 whitespace-nowrap rounded-full px-1.5 text-[10px] leading-4" class="h-4 shrink-0 whitespace-nowrap rounded-full px-1.5 text-[10px] leading-4"
:class="badge.className" :class="badge.className"
:title="badge.title" :title="badge.title"
@@ -76,6 +76,7 @@ type ModelBadgeKey = 'reasoning' | 'fast' | 'cyber'
interface ModelBadgePresentation { interface ModelBadgePresentation {
key: ModelBadgeKey key: ModelBadgeKey
label: string label: string
variant: 'outline' | 'outline-transparent'
className: string className: string
title: string title: string
ariaLabel: string ariaLabel: string
@@ -130,6 +131,7 @@ const modelBadges = computed<ModelBadgePresentation[]>(() => {
badges.push({ badges.push({
key: 'reasoning', key: 'reasoning',
label: reasoningLabel.value, label: reasoningLabel.value,
variant: 'outline',
className: 'border-primary/30 bg-primary/5 text-primary', className: 'border-primary/30 bg-primary/5 text-primary',
title: `Reasoning: ${reasoningLabel.value}`, title: `Reasoning: ${reasoningLabel.value}`,
ariaLabel: `Reasoning: ${reasoningLabel.value}`, ariaLabel: `Reasoning: ${reasoningLabel.value}`,
@@ -140,7 +142,8 @@ const modelBadges = computed<ModelBadgePresentation[]>(() => {
badges.push({ badges.push({
key: 'fast', key: 'fast',
label: 'Fast', label: 'Fast',
className: 'border-amber-400/50 !bg-transparent text-amber-700 dark:border-amber-300/40 dark:text-amber-300', variant: 'outline-transparent',
className: 'text-amber-700 dark:text-amber-300',
title: '上游请求档位:Fast\n计费档位:Fast', title: '上游请求档位:Fast\n计费档位:Fast',
ariaLabel: '上游请求档位:Fast,计费档位:Fast', ariaLabel: '上游请求档位:Fast,计费档位:Fast',
}) })
@@ -150,6 +153,7 @@ const modelBadges = computed<ModelBadgePresentation[]>(() => {
badges.push({ badges.push({
key: 'cyber', key: 'cyber',
label: 'Cyber', label: 'Cyber',
variant: 'outline',
className: 'border-primary/30 bg-primary/5 text-rose-600 dark:text-rose-300', className: 'border-primary/30 bg-primary/5 text-rose-600 dark:text-rose-300',
title: '上游 Cyber Policy 拒绝', title: '上游 Cyber Policy 拒绝',
ariaLabel: '上游 Cyber Policy 拒绝', ariaLabel: '上游 Cyber Policy 拒绝',
@@ -1579,7 +1579,7 @@ function buildServiceTierBadgePresentation(
const title = titleLines.join('\n') const title = titleLines.join('\n')
return { return {
label: 'Fast', label: 'Fast',
className: 'border-amber-400/50 !bg-transparent text-amber-700 dark:border-amber-300/40 dark:text-amber-300', className: '!bg-transparent text-amber-700 dark:text-amber-300',
title, title,
ariaLabel: titleLines.join(''), ariaLabel: titleLines.join(''),
} }
@@ -339,9 +339,9 @@ describe('UsageRecordsTable', () => {
expect(reasoningBadge?.classList.contains('border-primary/30')).toBe(true) expect(reasoningBadge?.classList.contains('border-primary/30')).toBe(true)
expect(reasoningBadge?.classList.contains('bg-primary/5')).toBe(true) expect(reasoningBadge?.classList.contains('bg-primary/5')).toBe(true)
expect(reasoningBadge?.classList.contains('text-primary')).toBe(true) expect(reasoningBadge?.classList.contains('text-primary')).toBe(true)
expect(fastBadge?.classList.contains('border-amber-400/50')).toBe(true) expect(fastBadge?.getAttribute('variant')).toBe('outline-transparent')
expect(fastBadge?.classList.contains('!bg-transparent')).toBe(true) expect(fastBadge?.classList.contains('border-amber-400/50')).toBe(false)
expect(fastBadge?.classList.contains('bg-transparent')).toBe(false) expect(fastBadge?.classList.contains('!bg-transparent')).toBe(false)
expect(fastBadge?.classList.contains('bg-amber-400/10')).toBe(false) expect(fastBadge?.classList.contains('bg-amber-400/10')).toBe(false)
expect(fastBadge?.classList.contains('text-amber-700')).toBe(true) expect(fastBadge?.classList.contains('text-amber-700')).toBe(true)
expect(cyberBadge?.classList.contains('border-primary/30')).toBe(true) expect(cyberBadge?.classList.contains('border-primary/30')).toBe(true)
@@ -577,8 +577,8 @@ describe('PoolManagement Codex cycle stats mode', () => {
expect(root.querySelector('[data-testid="pool-stats-mode-control"]')).toBeNull() expect(root.querySelector('[data-testid="pool-stats-mode-control"]')).toBeNull()
expect(root.querySelector('[data-testid="pool-stats-cycle-text"]')).not.toBeNull() expect(root.querySelector('[data-testid="pool-stats-cycle-text"]')).not.toBeNull()
expect(root.querySelector('[data-testid="pool-stats-cycle-request_count"]')?.textContent?.trim()).toBe('7/12') expect(root.querySelector('[data-testid="pool-stats-cycle-request_count"]')?.textContent?.trim()).toBe('请求 7/12')
expect(root.querySelector('[data-testid="pool-stats-cycle-total_tokens"]')?.textContent?.trim()).toBe('2.5K/5K') expect(root.querySelector('[data-testid="pool-stats-cycle-total_tokens"]')?.textContent?.trim()).toBe('Token 2.5K/5K')
expect(root.querySelector('[data-testid="pool-stats-cycle-small-overlay"]')).toBeNull() expect(root.querySelector('[data-testid="pool-stats-cycle-small-overlay"]')).toBeNull()
expect(root.querySelector('[data-testid="pool-stats-cycle-large-base"]')).toBeNull() expect(root.querySelector('[data-testid="pool-stats-cycle-large-base"]')).toBeNull()
expect(endpointMocks.listPoolKeys).toHaveBeenLastCalledWith( expect(endpointMocks.listPoolKeys).toHaveBeenLastCalledWith(
@@ -707,7 +707,7 @@ describe('PoolManagement Codex cycle stats mode', () => {
expect(periodLabels).toContain('月') expect(periodLabels).toContain('月')
expect(periodLabels).not.toContain('5H') expect(periodLabels).not.toContain('5H')
expect(periodLabels).not.toContain('周') expect(periodLabels).not.toContain('周')
expect(root.querySelector('[data-testid="pool-stats-cycle-request_count"]')?.textContent?.trim()).toBe('23') expect(root.querySelector('[data-testid="pool-stats-cycle-request_count"]')?.textContent?.trim()).toBe('请求 23')
expect(root.querySelector('[data-testid="pool-stats-cycle-small-overlay"]')).toBeNull() expect(root.querySelector('[data-testid="pool-stats-cycle-small-overlay"]')).toBeNull()
expect(root.querySelector('[data-testid="pool-stats-cycle-bar-request_count"]')).toBeNull() expect(root.querySelector('[data-testid="pool-stats-cycle-bar-request_count"]')).toBeNull()
expect(root.querySelector('[data-testid="pool-stats-cycle-single-marker"]')).toBeNull() expect(root.querySelector('[data-testid="pool-stats-cycle-single-marker"]')).toBeNull()