mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
832e2e8bdd | ||
|
|
be703a0c5f | ||
|
|
c2c86928c5 | ||
|
|
bda21ccc61 | ||
|
|
72c56680b9 | ||
|
|
a6808499f0 | ||
|
|
fdb954b3d1 | ||
|
|
a069001f01 | ||
|
|
d3a87d11ad |
@@ -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"
|
||||
>
|
||||
|
||||
@@ -52,7 +52,7 @@ const sizeClasses: Record<InputSize, string> = {
|
||||
}
|
||||
|
||||
const containerClasses = computed(() => [
|
||||
'relative inline-flex w-full min-w-0 items-center overflow-visible rounded-cp border-0 text-cp-text shadow-cp-input transition-[background-color,box-shadow,color] duration-[160ms] [--base-input-autofill-bg:var(--cp-input-bg)] hover:[--base-input-autofill-bg:var(--cp-input-hover-bg)] focus-within:[--base-input-autofill-bg:var(--cp-input-active-bg)] motion-reduce:transition-none',
|
||||
'relative inline-flex w-full min-w-0 items-center overflow-visible rounded-cp border-0 text-cp-text shadow-cp-input transition-[background-color,box-shadow,color] duration-[160ms] motion-reduce:transition-none',
|
||||
sizeClasses[props.size],
|
||||
props.disabled
|
||||
? 'cursor-not-allowed bg-cp-bg-container-disabled text-cp-text-disabled shadow-none'
|
||||
@@ -73,6 +73,11 @@ const iconClasses = computed(() => [
|
||||
function updateModel(event: Event) {
|
||||
model.value = (event.target as HTMLInputElement).value
|
||||
}
|
||||
|
||||
function syncAutofill(event: AnimationEvent) {
|
||||
if (event.animationName === 'base-input-autofill-sync')
|
||||
updateModel(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -93,6 +98,8 @@ function updateModel(event: Event) {
|
||||
:aria-describedby="describedBy"
|
||||
:aria-invalid="invalid || undefined"
|
||||
:aria-required="field?.required.value || undefined"
|
||||
@animationstart="syncAutofill"
|
||||
@change="updateModel"
|
||||
@input="updateModel"
|
||||
>
|
||||
<span v-if="$slots.suffix" :class="iconClasses">
|
||||
@@ -107,9 +114,23 @@ function updateModel(event: Event) {
|
||||
.base-input__field:-webkit-autofill:hover,
|
||||
.base-input__field:-webkit-autofill:focus,
|
||||
.base-input__field:autofill {
|
||||
animation-name: base-input-autofill-sync;
|
||||
animation-duration: 1ms;
|
||||
background-clip: text !important;
|
||||
caret-color: var(--cp-color-text);
|
||||
-webkit-background-clip: text !important;
|
||||
-webkit-text-fill-color: var(--cp-color-text) !important;
|
||||
-webkit-box-shadow: 0 0 0 1000px var(--base-input-autofill-bg) inset !important;
|
||||
box-shadow: 0 0 0 1000px var(--base-input-autofill-bg) inset !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@keyframes base-input-autofill-sync {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -31,7 +31,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const edgeGap = 12
|
||||
const defaultButtonSize = 44
|
||||
const defaultButtonSize = 48
|
||||
const dragClickThreshold = 6
|
||||
const storageKey = 'codex-proxy:mobile-sidebar-toggle-position'
|
||||
|
||||
@@ -220,7 +220,7 @@ onScopeDispose(() => {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex size-11.5 items-center justify-center rounded-cp border-0 bg-cp-bg-container text-cp-text shadow-[0_18px_34px_-18px_var(--cp-color-shadow)] outline-none transition-[background-color,box-shadow,color,opacity,transform] duration-150 hover:bg-cp-bg-text-hover hover:text-cp-primary-text active:scale-95 focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-cp-bg-layout"
|
||||
class="inline-flex size-12 touch-none select-none items-center justify-center rounded-cp border-0 bg-cp-fill-secondary text-cp-text shadow-cp-tertiary outline-none transition-[background-color,box-shadow,color,opacity,transform] duration-150 [-webkit-tap-highlight-color:transparent] hover:bg-cp-bg-text-active active:scale-95 active:bg-cp-control-item-bg-active focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-cp-bg-layout"
|
||||
:class="
|
||||
isDragging
|
||||
? 'scale-95 cursor-grabbing shadow-[0_14px_28px_-20px_var(--cp-color-shadow)]'
|
||||
|
||||
@@ -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[]>(() => {
|
||||
|
||||
@@ -40,15 +40,17 @@ const groupOptions = computed(() => [
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex w-full flex-col gap-3 md:flex-row md:flex-wrap md:items-center"
|
||||
class="flex w-full flex-col gap-3 xl:flex-row xl:flex-wrap xl:items-center"
|
||||
role="group"
|
||||
aria-label="账号筛选与操作"
|
||||
>
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2 md:flex-none md:gap-3">
|
||||
<div
|
||||
class="grid w-full min-w-0 grid-cols-2 items-center gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_7.75rem] xl:flex xl:w-auto xl:flex-none xl:flex-wrap xl:gap-3"
|
||||
>
|
||||
<BaseInput
|
||||
v-model="search"
|
||||
placeholder="搜索账号"
|
||||
class="min-w-0 flex-1 md:w-80 md:flex-none"
|
||||
class="col-span-2 min-w-0 sm:col-span-3 xl:w-80 xl:flex-none"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="size-4.5 text-cp-text-tertiary" />
|
||||
@@ -59,7 +61,7 @@ const groupOptions = computed(() => [
|
||||
v-model="status"
|
||||
:options="accountStatusFilterOptions"
|
||||
aria-label="按账号状态筛选"
|
||||
class="w-34 shrink-0 md:w-40"
|
||||
class="w-full min-w-0 xl:w-40 xl:shrink-0"
|
||||
/>
|
||||
|
||||
<BaseSelect
|
||||
@@ -67,22 +69,22 @@ const groupOptions = computed(() => [
|
||||
:options="groupOptions"
|
||||
:disabled="groupsLoading"
|
||||
aria-label="按账号分组筛选"
|
||||
class="w-34 shrink-0 md:w-40"
|
||||
class="w-full min-w-0 xl:w-40 xl:shrink-0"
|
||||
/>
|
||||
|
||||
<ProviderFilterSegmented
|
||||
v-model="provider"
|
||||
class="w-31 shrink-0"
|
||||
class="col-span-2 w-full sm:col-span-1 xl:w-31 xl:shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid w-full grid-cols-2 gap-2 md:flex md:w-auto md:flex-wrap md:shrink-0 md:self-end md:items-center md:justify-end md:ml-auto"
|
||||
class="grid w-full grid-cols-2 gap-2 xl:flex xl:w-auto xl:flex-wrap xl:shrink-0 xl:self-end xl:items-center xl:justify-end xl:ml-auto"
|
||||
>
|
||||
<BaseButton
|
||||
v-if="selectedCount > 0"
|
||||
variant="secondary"
|
||||
class="w-full whitespace-nowrap md:w-auto"
|
||||
class="w-full whitespace-nowrap xl:w-auto"
|
||||
@click="emit('editSelected')"
|
||||
>
|
||||
<Pencil class="size-4 text-cp-link" />
|
||||
@@ -91,7 +93,7 @@ const groupOptions = computed(() => [
|
||||
<BaseButton
|
||||
v-if="selectedCount > 0"
|
||||
variant="destructive"
|
||||
class="w-full whitespace-nowrap md:w-auto"
|
||||
class="w-full whitespace-nowrap xl:w-auto"
|
||||
:disabled="batchDeleting"
|
||||
@click="emit('deleteSelected')"
|
||||
>
|
||||
@@ -101,7 +103,7 @@ const groupOptions = computed(() => [
|
||||
<BaseButton
|
||||
v-if="selectedCount > 0"
|
||||
variant="secondary"
|
||||
class="w-full whitespace-nowrap md:w-auto"
|
||||
class="w-full whitespace-nowrap xl:w-auto"
|
||||
:loading="exportingAccounts"
|
||||
@click="emit('exportSelected')"
|
||||
>
|
||||
@@ -112,7 +114,7 @@ const groupOptions = computed(() => [
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
variant="primary"
|
||||
class="whitespace-nowrap md:w-auto"
|
||||
class="whitespace-nowrap xl:w-auto"
|
||||
:class="selectedCount > 0 ? 'col-span-2 w-full' : 'col-span-2 justify-self-end'"
|
||||
@click="emit('create')"
|
||||
>
|
||||
|
||||
@@ -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))">
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -545,9 +545,9 @@ function trendSummaryTone(label: string) {
|
||||
return 'danger'
|
||||
if (label.includes('总耗时'))
|
||||
return 'warning'
|
||||
if (label.includes('输出') || label.includes('成功'))
|
||||
if (label.includes('吞吐') || label.includes('输出') || label.includes('成功'))
|
||||
return 'success'
|
||||
if (label.includes('缓存'))
|
||||
if (label.includes('首字') || label.includes('缓存'))
|
||||
return 'normal'
|
||||
return 'info'
|
||||
}
|
||||
@@ -556,9 +556,9 @@ function trendSummaryColorVar(kind: DashboardTrendKind, label: string) {
|
||||
if (kind === 'latency') {
|
||||
if (label.includes('总耗时'))
|
||||
return '--cp-color-orange-solid'
|
||||
if (label.includes('输出'))
|
||||
if (label.includes('吞吐'))
|
||||
return '--cp-color-green-solid'
|
||||
return '--cp-color-blue-solid'
|
||||
return '--cp-color-cyan-solid'
|
||||
}
|
||||
if (kind === 'errors') {
|
||||
if (label.includes('错误'))
|
||||
@@ -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 [
|
||||
@@ -286,7 +286,7 @@ export function useRequestTrendChart(options: {
|
||||
lineSeries(
|
||||
'首字 P95',
|
||||
activeSeriesValues('firstTokenP95Ms'),
|
||||
trendColor('首字', '--cp-color-blue-solid', '#5983F4'),
|
||||
trendColor('首字', '--cp-color-cyan-solid', '#13C2C2'),
|
||||
{ area: true, smooth: 0.24, width: 2.5, z: 4 },
|
||||
),
|
||||
lineSeries(
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowRight, CircleAlert, Eye, EyeOff, KeyRound, Mail, Moon, Sun } from '@lucide/vue'
|
||||
import { CircleAlert, Eye, EyeOff, KeyRound, Mail, Moon, Sun } from '@lucide/vue'
|
||||
import { computed, shallowRef } from 'vue'
|
||||
|
||||
import AppBrandMark from '@/components/AppBrandMark.vue'
|
||||
@@ -45,7 +45,7 @@ function togglePasswordVisible(): void {
|
||||
<BaseCard
|
||||
as="form"
|
||||
padding="none"
|
||||
class="login-form relative grid min-h-129.25 w-[min(440px,100%)] gap-2.5 rounded-lg px-7.5 pt-6.5 pb-6 max-[560px]:min-h-auto max-[560px]:gap-2.5 max-[560px]:p-5.5"
|
||||
class="login-form relative grid min-h-120 w-[min(440px,100%)] gap-2.5 rounded-lg px-7.5 pt-6.5 pb-6 max-[560px]:min-h-auto max-[560px]:gap-4 max-[560px]:p-5.5"
|
||||
@submit.prevent="emit('submit')"
|
||||
>
|
||||
<div class="login-form-line" />
|
||||
@@ -81,14 +81,14 @@ function togglePasswordVisible(): void {
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="grid min-w-0 gap-1" aria-labelledby="login-title">
|
||||
<section class="grid min-w-0 gap-1 max-[560px]:gap-2" aria-labelledby="login-title">
|
||||
<h1
|
||||
id="login-title"
|
||||
class="m-0 text-[34px] leading-[1.02] font-semibold text-(--cp-login-title-color) max-[560px]:text-[30px]"
|
||||
>
|
||||
控制台登录
|
||||
</h1>
|
||||
<p class="m-0 -ml-2 text-sm leading-[1.45] font-normal text-(--cp-login-description-color)">
|
||||
<p class="m-0 -ml-2 text-sm leading-[1.45] font-normal text-(--cp-login-description-color) max-[560px]:py-2">
|
||||
「 欢迎回来,登录以开始您的数据之旅 」
|
||||
</p>
|
||||
</section>
|
||||
@@ -153,7 +153,6 @@ function togglePasswordVisible(): void {
|
||||
:disabled="props.submitDisabled"
|
||||
>
|
||||
<span>{{ submitLabel }}</span>
|
||||
<ArrowRight v-if="!props.loading" :size="18" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,6 +250,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 +276,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 +286,7 @@ function togglePasswordVisible(): void {
|
||||
}
|
||||
|
||||
.login-theme-toggle.is-dark .login-theme-knob {
|
||||
transform: translateX(34px);
|
||||
transform: translateX(33px);
|
||||
}
|
||||
|
||||
.login-error {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -18,7 +18,7 @@ function valueClass(item: { mono?: boolean, wrap?: boolean }) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dl class="mt-3 grid grid-cols-1 gap-x-4 gap-y-3 sm:grid-cols-2">
|
||||
<dl class="mt-3 grid min-w-0 grid-cols-1 gap-x-4 gap-y-3 sm:grid-cols-2">
|
||||
<div v-for="item in items" :key="item.label" class="min-w-0">
|
||||
<dt :class="fieldLabelClass">
|
||||
{{ item.label }}
|
||||
|
||||
@@ -47,7 +47,7 @@ const tokenDetails = computed(() => props.record ? usageTokenDetails(props.recor
|
||||
const billing = computed(() => props.record ? usageBilling(props.record) : null)
|
||||
const latencyDetails = computed(() => props.record ? usageLatencyDetails(props.record) : null)
|
||||
|
||||
const panelClass = 'rounded-cp-card bg-cp-fill-quaternary px-4 py-3.5'
|
||||
const panelClass = 'min-w-0 rounded-cp-card bg-cp-fill-quaternary px-4 py-3.5'
|
||||
const panelTitleClass = 'm-0 text-cp-sm leading-none font-heavy text-cp-text-secondary'
|
||||
|
||||
const accountDisplay = computed(() => props.record ? usageAccountText(props.record) : '—')
|
||||
@@ -284,7 +284,7 @@ const tokenDonutOption = computed<EChartsOption>(() => {
|
||||
tone="info"
|
||||
size="xl"
|
||||
>
|
||||
<div v-if="record" class="grid gap-3">
|
||||
<div v-if="record" class="grid min-w-0 gap-3">
|
||||
<section :class="panelClass">
|
||||
<div class="min-w-0">
|
||||
<span :class="fieldLabelClass">账号</span>
|
||||
@@ -324,8 +324,8 @@ const tokenDonutOption = computed<EChartsOption>(() => {
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-3 lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
|
||||
<div class="flex min-h-0 flex-col gap-3">
|
||||
<section class="grid min-w-0 gap-3 lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
|
||||
<div class="flex min-h-0 min-w-0 flex-col gap-3">
|
||||
<section :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
{{ modelRouteGroup.title }}
|
||||
@@ -338,7 +338,7 @@ const tokenDonutOption = computed<EChartsOption>(() => {
|
||||
Token
|
||||
</h3>
|
||||
<div
|
||||
class="mt-3 grid min-h-38 flex-1 grid-cols-1 content-center items-center gap-3 sm:grid-cols-[150px_minmax(0,1fr)]"
|
||||
class="mt-3 grid min-h-38 min-w-0 flex-1 grid-cols-1 content-center items-center gap-3 sm:grid-cols-[150px_minmax(0,1fr)]"
|
||||
>
|
||||
<div class="relative mx-auto w-38 sm:mx-0">
|
||||
<BaseChart :option="tokenDonutOption" :height="152" />
|
||||
@@ -356,7 +356,7 @@ const tokenDonutOption = computed<EChartsOption>(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
<dl class="grid min-w-0 grid-cols-2 gap-x-4 gap-y-3">
|
||||
<div v-for="item in tokenChartItems" :key="item.label" class="min-w-0">
|
||||
<dt class="flex min-w-0 items-center gap-1.5" :class="fieldLabelClass">
|
||||
<i
|
||||
@@ -377,7 +377,7 @@ const tokenDonutOption = computed<EChartsOption>(() => {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="flex min-h-0 flex-col gap-3">
|
||||
<div class="flex min-h-0 min-w-0 flex-col gap-3">
|
||||
<section class="flex-1" :class="[panelClass]">
|
||||
<h3 :class="panelTitleClass">
|
||||
{{ clientUpstreamGroup.title }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -24,6 +24,11 @@ export function usageLineSeries(
|
||||
): LineSeriesOption {
|
||||
const area = options.area ?? (options.stack ? 'strong' : false)
|
||||
const alpha = area ? areaAlpha[area] : null
|
||||
const sampleCount = data.reduce<number>(
|
||||
(count, value) => count + (value == null ? 0 : 1),
|
||||
0,
|
||||
)
|
||||
const showSymbols = sampleCount > 0 && sampleCount <= 12
|
||||
|
||||
return {
|
||||
name,
|
||||
@@ -32,9 +37,10 @@ export function usageLineSeries(
|
||||
connectNulls: false,
|
||||
stack: options.stack,
|
||||
smooth: true,
|
||||
showSymbol: data.length <= 12,
|
||||
showSymbol: showSymbols,
|
||||
showAllSymbol: showSymbols,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
symbolSize: sampleCount === 1 ? 7 : 5,
|
||||
lineStyle: { color, width: 2.2 },
|
||||
itemStyle: { color },
|
||||
areaStyle: alpha
|
||||
|
||||
@@ -1 +1 @@
|
||||
version: "3.3.5"
|
||||
version: "3.3.9"
|
||||
|
||||
Reference in New Issue
Block a user