Compare commits

..
6 Commits
Author SHA1 Message Date
ZyyandCodex bda21ccc61 chore: bump version to 3.3.8
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 03:38:03 +08:00
ZyyandCodex 72c56680b9 fix(frontend): polish interface presentation
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 03:37:35 +08:00
ZyyandCodex a6808499f0 chore: bump version to 3.3.7
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 02:24:41 +08:00
ZyyandCodex fdb954b3d1 fix(frontend): refine semantic theme states
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 02:23:29 +08:00
ZyyandCodex a069001f01 chore: bump version to 3.3.6
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 01:55:01 +08:00
ZyyandCodex d3a87d11ad fix(frontend): restore theme visual consistency
Co-authored-by: Codex <noreply@openai.com>
2026-08-30 01:53:00 +08:00
23 changed files with 84 additions and 60 deletions
@@ -33,7 +33,7 @@ const authenticationLabel = computed(() => {
<template>
<span class="inline-flex items-center whitespace-nowrap" :class="groupGapClass">
<span
class="inline-flex shrink-0 items-center justify-center bg-cp-fill-tertiary text-cp-text"
class="inline-flex shrink-0 items-center justify-center bg-cp-fill-secondary/80 text-cp-text"
:class="iconContainerClass"
:title="providerLabel"
>
@@ -44,7 +44,7 @@ const authenticationLabel = computed(() => {
<span
v-if="showAuthenticationKind"
class="inline-flex shrink-0 items-center justify-center bg-cp-fill-tertiary text-cp-text"
class="inline-flex shrink-0 items-center justify-center bg-cp-fill-secondary/80 text-cp-text"
:class="iconContainerClass"
:title="authenticationLabel"
>
@@ -238,7 +238,7 @@ onBeforeUnmount(() => {
<p
v-if="description"
:id="descriptionId"
class="mt-2 mb-0 text-cp leading-[1.45] font-semibold text-cp-text-secondary"
class="mt-1 mb-0 text-cp leading-[1.45] font-semibold text-cp-text-secondary"
>
{{ description }}
</p>
@@ -184,7 +184,7 @@ function sortButtonLabel(column: ResolvedTableColumn<Row>) {
</script>
<template>
<div class="flex h-full min-h-0 w-full max-w-full flex-col overflow-hidden">
<div class="isolate flex h-full min-h-0 w-full max-w-full flex-col overflow-hidden">
<div v-loading="loading" class="relative flex min-h-0 max-w-full flex-1 overflow-hidden">
<BaseScrollbar
v-if="hasRows"
@@ -342,14 +342,14 @@ onBeforeUnmount(() => {
class="inline-flex h-4.5 min-w-0 cursor-pointer items-center gap-1 rounded-cp-sm border-0 px-1.5 font-mono text-[10px] leading-none font-bold transition-colors outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-cp-bg-container"
:class="[
hasUpdate
? 'bg-cp-info-bg text-cp-info-text hover:bg-cp-info-bg-hover'
? 'bg-cp-success-bg text-cp-success-text hover:bg-cp-success-bg-hover'
: 'bg-cp-fill-quaternary text-cp-text-quaternary hover:bg-cp-fill-tertiary hover:text-cp-text-secondary',
]"
:title="updateButtonLabel"
@click="openSystemUpdate"
>
<span>{{ versionLabel }}</span>
<ArrowUpCircle v-if="hasUpdate" class="size-3 shrink-0 text-cp-info" />
<ArrowUpCircle v-if="hasUpdate" class="size-3 shrink-0 text-cp-success" />
</button>
</span>
</span>
@@ -408,7 +408,7 @@ onBeforeUnmount(() => {
<div class="flex items-center" :class="isCollapsed ? 'grid gap-1' : 'gap-1'">
<BaseIconButton
v-if="isCollapsed && hasUpdate"
variant="primary"
variant="success"
size="md"
:label="updateButtonLabel"
@click="openSystemUpdate"
@@ -197,6 +197,10 @@ watch(
size="lg"
:dismissible="!updating && !restarting"
>
<template #icon>
<ArrowUpCircle class="size-4.5 text-cp-success" />
</template>
<div class="grid gap-3.5">
<section class="grid gap-4 rounded-cp-card bg-cp-fill-quaternary px-4 py-4">
<div class="flex flex-wrap items-center justify-between gap-3">
@@ -26,7 +26,7 @@ const toneClasses: Record<MetricTone, string> = {
secondary: 'text-cp-text-secondary',
success: 'text-cp-success-text',
warning: 'text-cp-warning',
active: 'text-cp-primary-text',
active: 'text-cp-success',
}
const metrics = computed<MetricItem[]>(() => {
@@ -70,7 +70,7 @@ const avatarToneClass = computed(() => {
</span>
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-2">
<span class="min-w-0 flex-1 truncate text-cp-lg font-heavy text-cp-text">
<span class="min-w-0 flex-1 truncate text-cp font-heavy text-cp-text">
{{ displayTitle }}
</span>
<span
@@ -83,7 +83,7 @@ const avatarToneClass = computed(() => {
</div>
<div
v-if="metaPosition === 'secondary' && (showPlan || $slots.meta)"
class="mt-1 inline-flex min-w-0 items-center gap-1.5"
class="mt-0.5 inline-flex min-w-0 items-center gap-1.5"
>
<slot name="meta" />
<AccountPlanBadge v-if="showPlan" :plan-type="account.planType" size="sm" />
@@ -26,6 +26,11 @@ const levelClasses: Record<ProfileActivityLevel, string> = {
}
const grid = computed(() => buildProfileActivityGrid(props.dailyUsage ?? [], mode.value))
const rangeLabel = computed(() => `${grid.value.rangeStart} 至 ${grid.value.rangeEnd}`)
const monthLabels = computed(() =>
grid.value.weeks.flatMap((week, weekIndex) =>
week.monthLabel ? [{ label: week.monthLabel, weekIndex }] : [],
),
)
</script>
<template>
@@ -51,11 +56,15 @@ const rangeLabel = computed(() => `${grid.value.rangeStart} 至 ${grid.value.ran
<div class="min-w-190">
<div class="mb-1.5 grid gap-1" style="grid-template-columns: repeat(52, minmax(0, 1fr))">
<span
v-for="week in grid.weeks"
:key="week.key"
v-for="(month, monthIndex) in monthLabels"
:key="`${month.weekIndex}-${month.label}`"
class="h-4 whitespace-nowrap text-[10px] leading-4 font-semibold text-cp-text-quaternary"
:class="monthIndex === monthLabels.length - 1 ? 'text-right' : undefined"
:style="{
gridColumn: `${month.weekIndex + (monthIndex === monthLabels.length - 1 ? 0 : 1)} / span 2`,
}"
>
{{ week.monthLabel }}
{{ month.label }}
</span>
</div>
<div class="grid gap-1" style="grid-template-columns: repeat(52, minmax(0, 1fr))">
+1 -1
View File
@@ -147,7 +147,7 @@ export function modelSuccessRateTextClass(successRate: number | null) {
if (successRate >= 99.5)
return 'text-cp-success-text'
if (successRate >= 98)
return 'text-cp-info-text'
return 'text-cp-cyan-text'
if (successRate >= 95)
return 'text-cp-warning-text'
return 'text-cp-error-text'
@@ -221,7 +221,7 @@ const statusBars = computed(() => {
</p>
<div
class="mt-5 flex w-full flex-col gap-2 overflow-hidden xl:mt-6.75 xl:h-82.5 xl:gap-1.5"
class="mt-5 flex w-full flex-col gap-2 overflow-hidden xl:mt-6.75 xl:h-82.5 xl:gap-2.5"
>
<BaseEmpty
v-if="accounts.length === 0"
@@ -233,7 +233,7 @@ const statusBars = computed(() => {
<article
v-for="account in accounts"
:key="account.id"
class="grid w-full shrink-0 grid-cols-1 gap-3 rounded-xl bg-cp-fill-alter/70 px-3.5 py-3.5 transition-colors xl:h-19.5 xl:grid-cols-[minmax(0,1fr)_minmax(70px,0.36fr)_minmax(74px,0.38fr)_minmax(82px,0.46fr)] xl:items-center xl:gap-4 xl:py-0"
class="grid w-full shrink-0 grid-cols-1 gap-3 rounded-xl bg-cp-fill-alter/70 px-3.5 py-3.5 transition-colors xl:h-18.75 xl:grid-cols-[minmax(0,1fr)_minmax(70px,0.36fr)_minmax(74px,0.38fr)_minmax(82px,0.46fr)] xl:items-center xl:gap-4 xl:py-0"
>
<AccountIdentityCell
:account="account"
@@ -246,6 +246,7 @@ const statusBars = computed(() => {
<ProviderIconGroup
:provider="account.provider"
:authentication-kind="account.authenticationKind"
size="sm"
/>
</template>
</AccountIdentityCell>
@@ -113,7 +113,7 @@ const metricItems = outcomeMeta
<div
v-for="item in metricItems"
:key="item.label"
class="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-cp bg-cp-fill-quaternary px-2.5 py-2.5"
class="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-cp bg-cp-fill-tertiary px-2.5 py-2.5"
>
<span class="size-1.5 rounded-full" :class="item.dotClass" />
<span class="truncate text-[10px] leading-none font-emphasis text-cp-text-secondary">
@@ -72,7 +72,7 @@ const {
'bg-cp-success': item.tone === 'success',
'bg-cp-warning': item.tone === 'warning',
'bg-cp-error': item.tone === 'danger',
'bg-cp-orange-solid': item.tone === 'normal',
'bg-cp-text-tertiary': item.tone === 'normal',
},
]"
/>
@@ -108,7 +108,7 @@ const releaseStatus = computed(() => {
return {
label: '待检查',
title: '尚未检查官方发布渠道',
tone: 'bg-cp-info-bg text-cp-info-text',
tone: 'bg-cp-cyan-bg text-cp-cyan-text-on-bg',
icon: RefreshCw,
}
})
@@ -270,12 +270,12 @@ function metricCards(
{
label: '可用',
value: credentials.available,
tone: credentials.availableValue > 0 ? 'success' : 'normal',
tone: 'success',
},
{
label: '不可用',
value: credentials.unavailable,
tone: credentials.unavailableValue > 0 ? 'danger' : 'normal',
tone: 'danger',
},
],
},
@@ -433,7 +433,7 @@ function usageTrendSummary(points: ReturnType<typeof aggregateUsageTrend>) {
label: '缓存',
value: formatCompactNumber(cachedTokens),
tone: 'normal',
colorVar: '--cp-color-orange-solid',
colorVar: '--cp-color-text-tertiary',
},
]
}
@@ -570,6 +570,6 @@ function trendSummaryColorVar(kind: DashboardTrendKind, label: string) {
if (label.includes('输出'))
return '--cp-color-green-solid'
if (label.includes('缓存'))
return '--cp-color-orange-solid'
return '--cp-color-text-tertiary'
return '--cp-color-blue-solid'
}
@@ -193,12 +193,12 @@ export function useRequestTrendChart(options: {
tooltipItem(
'缓存',
point?.cachedTokens,
trendColor('缓存', '--cp-color-orange-solid', '#F59E0B'),
trendColor('缓存', '--cp-color-text-tertiary', '#94A3B8'),
),
tooltipItem(
'缓存命中',
trendPointText(point, 'cacheHitRate'),
trendColor('缓存', '--cp-color-orange-solid', '#F59E0B'),
trendColor('缓存', '--cp-color-text-tertiary', '#94A3B8'),
),
tooltipItem('请求', point?.requests, palette.value.textSecondary),
]
@@ -243,7 +243,7 @@ export function useRequestTrendChart(options: {
function getSeries() {
if (activeKind.value === 'usage') {
const cacheColor = trendColor('缓存', '--cp-color-orange-solid', '#F59E0B')
const cacheColor = trendColor('缓存', '--cp-color-text-tertiary', '#94A3B8')
const inputColor = trendColor('输入', '--cp-color-blue-solid', '#5983F4')
const outputColor = trendColor('输出', '--cp-color-green-solid', '#12B981')
return [
+3 -3
View File
@@ -80,8 +80,8 @@ export const healthStatusMeta: Record<HealthStatus, HealthStatusMeta> = {
},
low_sample: {
label: '低样本',
cellClass: 'bg-cp-info',
badgeClass: 'bg-cp-info-bg text-cp-info-text',
cellClass: 'bg-cp-cyan-solid',
badgeClass: 'bg-cp-cyan-bg text-cp-cyan-text-on-bg',
},
stable: {
label: '稳定',
@@ -99,7 +99,7 @@ export function healthReliabilityValueClass(successRequests: number, failedReque
if (reliability >= 99.5)
return 'text-cp-success-text'
if (reliability >= 98)
return 'text-cp-info-text'
return 'text-cp-cyan-text'
if (reliability >= 95)
return 'text-cp-warning-text'
return 'text-cp-error-text'
@@ -251,6 +251,12 @@ function togglePasswordVisible(): void {
color 0.16s ease;
}
@media (hover: hover) {
.login-theme-toggle:hover {
background: var(--cp-login-toggle-bg-hover);
}
}
.login-theme-toggle:focus-visible {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--cp-login-input-icon-color) 46%, transparent);
}
@@ -271,7 +277,7 @@ function togglePasswordVisible(): void {
.login-theme-knob {
position: absolute;
top: 5px;
left: 5px;
left: 5.5px;
width: 22px;
height: 22px;
border-radius: 50%;
@@ -281,7 +287,7 @@ function togglePasswordVisible(): void {
}
.login-theme-toggle.is-dark .login-theme-knob {
transform: translateX(34px);
transform: translateX(33px);
}
.login-error {
+7 -5
View File
@@ -123,7 +123,8 @@ async function handleSubmit(): Promise<void> {
--cp-login-error-text-color: var(--cp-color-error-text);
--cp-login-button-shadow-color: color-mix(in srgb, var(--cp-color-primary) 30%, transparent);
--cp-login-footer-color: color-mix(in srgb, var(--cp-color-text-secondary) 82%, var(--cp-color-text-tertiary));
--cp-login-toggle-bg: color-mix(in srgb, var(--cp-color-fill-tertiary) 74%, transparent);
--cp-login-toggle-bg: color-mix(in srgb, var(--cp-color-fill-tertiary) 90%, var(--cp-color-bg-container));
--cp-login-toggle-bg-hover: var(--cp-color-fill-tertiary);
--cp-login-toggle-sun-color: var(--cp-login-logo-color);
--cp-login-toggle-moon-color: var(--cp-color-text-secondary);
--cp-login-toggle-knob: var(--cp-color-bg-container);
@@ -187,10 +188,11 @@ async function handleSubmit(): Promise<void> {
--cp-login-placeholder-color: var(--cp-color-text-quaternary);
--cp-login-button-shadow-color: color-mix(in srgb, var(--cp-color-primary) 30%, transparent);
--cp-login-footer-color: color-mix(in srgb, var(--cp-color-text-secondary) 80%, var(--cp-color-text-quaternary));
--cp-login-toggle-bg: color-mix(in srgb, var(--cp-color-bg-container) 72%, transparent);
--cp-login-toggle-bg: var(--cp-color-fill-tertiary);
--cp-login-toggle-bg-hover: color-mix(in srgb, var(--cp-color-fill-tertiary) 94%, var(--cp-color-white));
--cp-login-toggle-sun-color: var(--cp-color-text-tertiary);
--cp-login-toggle-moon-color: var(--cp-color-text-secondary);
--cp-login-toggle-knob: color-mix(in srgb, var(--cp-color-bg-elevated) 82%, var(--cp-color-primary));
--cp-login-toggle-shadow-color: color-mix(in srgb, var(--cp-color-primary) 12%, transparent);
--cp-login-toggle-moon-color: var(--cp-color-white);
--cp-login-toggle-knob: var(--cp-color-primary-hover);
--cp-login-toggle-shadow-color: color-mix(in srgb, var(--cp-color-primary-text) 30%, transparent);
}
</style>
@@ -57,7 +57,7 @@ const emit = defineEmits<{
<div class="flex min-h-16 items-center justify-between gap-4 rounded-cp bg-cp-fill-quaternary px-4 py-3">
<div class="flex min-w-0 items-center gap-3">
<BaseMotionIcon
class="inline-flex size-9 shrink-0 items-center justify-center rounded-cp bg-cp-bg-container text-cp-primary-text shadow-cp-tertiary"
class="inline-flex size-9 shrink-0 items-center justify-center rounded-cp bg-cp-bg-container text-cp-cyan-solid shadow-cp-tertiary"
>
<KeyRound class="size-4" />
</BaseMotionIcon>
@@ -56,12 +56,6 @@ const emit = defineEmits<{
暂无模型映射
</div>
<div v-else class="grid gap-3">
<div class="grid grid-cols-[1fr_auto_1fr_auto] items-center gap-3 px-1 text-cp-sm font-bold text-cp-text-quaternary">
<span>请求模型</span>
<span />
<span>上游模型</span>
<span />
</div>
<div
v-for="(row, index) in mappings"
:key="index"
@@ -69,14 +63,14 @@ const emit = defineEmits<{
>
<BaseInput
:model-value="row.requestedModel"
placeholder="gpt-5.4"
placeholder="请求模型"
aria-label="请求模型"
@update:model-value="emit('updateMapping', index, 'requestedModel', $event)"
/>
<span class="text-cp-text-quaternary">→</span>
<BaseInput
:model-value="row.upstreamModel"
placeholder="gpt-5.5"
placeholder="上游模型"
aria-label="上游模型名称"
@update:model-value="emit('updateMapping', index, 'upstreamModel', $event)"
/>
@@ -16,10 +16,10 @@ const billingItems = computed(() => {
return []
return [
{ label: '服务档位', value: value.serviceTierDisplay },
{ label: '倍率', value: value.multiplierDisplay },
{ label: '总费用', value: value.totalAmountDisplay },
{ label: '标准费用', value: value.standardAmountDisplay },
{ label: '服务档位', value: value.serviceTierDisplay, tone: 'info' },
{ label: '倍率', value: value.multiplierDisplay, tone: 'info' },
{ label: '总费用', value: value.totalAmountDisplay, tone: 'success' },
{ label: '标准费用', value: value.standardAmountDisplay, tone: 'default' },
]
})
@@ -29,15 +29,23 @@ const amountItems = computed(() => {
return []
return [
{ label: '输入费用', value: value.inputAmountDisplay },
{ label: '输出费用', value: value.outputAmountDisplay },
{ label: '输入单价', value: value.inputPriceDisplay },
{ label: '输出单价', value: value.outputPriceDisplay },
{ label: '缓存读取费用', value: value.cacheReadAmountDisplay },
{ label: '缓存写入费用', value: value.cacheWriteAmountDisplay },
{ label: '缓存写入单价', value: value.cacheWritePriceDisplay },
{ label: '输入费用', value: value.inputAmountDisplay, accent: false },
{ label: '输出费用', value: value.outputAmountDisplay, accent: false },
{ label: '输入单价', value: value.inputPriceDisplay, accent: true },
{ label: '输出单价', value: value.outputPriceDisplay, accent: true },
{ label: '缓存读取费用', value: value.cacheReadAmountDisplay, accent: false },
{ label: '缓存写入费用', value: value.cacheWriteAmountDisplay, accent: false },
{ label: '缓存写入单价', value: value.cacheWritePriceDisplay, accent: true },
]
})
function itemValueClass(tone?: string, accent?: boolean) {
if (tone === 'success')
return 'text-cp-success-text'
if (tone === 'info' || accent)
return 'text-cp-info-text'
return 'text-cp-text'
}
</script>
<template>
@@ -50,7 +58,7 @@ const amountItems = computed(() => {
<div class="grid gap-1.5 text-cp-text-secondary">
<div v-for="item in amountItems" :key="item.label" class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-4">
<span class="whitespace-nowrap">{{ item.label }}</span>
<span class="justify-self-end whitespace-nowrap font-mono font-heavy text-cp-text">
<span class="justify-self-end whitespace-nowrap font-mono font-heavy" :class="itemValueClass(undefined, item.accent)">
{{ item.value }}
</span>
</div>
@@ -58,7 +66,7 @@ const amountItems = computed(() => {
<div class="mt-1 grid gap-1.5 rounded-cp bg-cp-fill-tertiary p-2 text-cp-text-secondary">
<div v-for="item in billingItems" :key="item.label" class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-4">
<span class="whitespace-nowrap">{{ item.label }}</span>
<span class="justify-self-end whitespace-nowrap font-mono font-heavy text-cp-text">
<span class="justify-self-end whitespace-nowrap font-mono font-heavy" :class="itemValueClass(item.tone)">
{{ item.value }}
</span>
</div>
@@ -45,7 +45,7 @@ const items = computed(() => [
icon: Timer,
value: averageLatencyDisplay(props.summary.averageLatencyMs),
detail: '成功请求平均值',
tone: 'bg-cp-primary-bg text-cp-primary-text',
tone: 'bg-cp-cyan-bg text-cp-cyan-text-on-bg',
},
])
</script>
+1 -1
View File
@@ -1 +1 @@
version: "3.3.5"
version: "3.3.8"