fix(frontend): 保留 Mermaid 大图文字并完善图表与卡片交互
This commit is contained in:
@@ -267,9 +267,19 @@ onMounted(load)
|
||||
|
||||
<style scoped>
|
||||
.editor-fields { display: grid; gap: var(--space-lg); border: 0; padding: 0; margin: 0; min-width: 0; }
|
||||
.mcp-page { overflow: auto; }.notice-banner,.error-banner { margin-bottom: var(--space-lg); }.server-list { display: grid; gap: var(--space-lg); }.server-card { display: grid; gap: var(--space-md); }
|
||||
.mcp-page { overflow: auto; }
|
||||
.mcp-page > :is(.feature-header, .notice-banner, .error-banner, .server-list, .empty) { box-sizing: border-box; width: 100%; max-width: 1180px; margin-inline: auto; }
|
||||
.server-list { grid-template-columns: minmax(0, 1fr); }
|
||||
.server-card, .server-main, .server-title, .server-title > div, .secret-input { min-width: 0; }
|
||||
.server-title { flex: 1; }
|
||||
.server-title h2, .metadata span, .secrets label > span { overflow-wrap: anywhere; }
|
||||
.server-title code { display: block; white-space: pre-wrap; }
|
||||
.server-main > .badge { flex-shrink: 0; }
|
||||
.secret-input input { min-width: 0; width: 100%; border: 1px solid var(--color-border-default); border-radius: var(--radius-sm); padding: 8px 10px; background: var(--color-background-secondary); color: var(--color-text-primary); font: inherit; }
|
||||
.secret-input button { flex-shrink: 0; }
|
||||
.notice-banner,.error-banner { margin-bottom: var(--space-lg); }.server-list { display: grid; gap: var(--space-lg); }.server-card { display: grid; gap: var(--space-md); }
|
||||
.server-main,.server-title,.metadata,.card-actions,.inline-actions,.template-row,.modal-card header,.modal-card footer { display: flex; align-items: center; gap: var(--space-sm); }.server-main { justify-content: space-between; }.server-title { align-items: flex-start; }.server-title h2 { margin-bottom: 4px; }.server-title code { color: var(--color-text-secondary); overflow-wrap: anywhere; }.metadata { flex-wrap: wrap; color: var(--color-text-tertiary); font-size: var(--font-size-sm); }.metadata span + span::before { content: '·'; margin-right: var(--space-sm); }.compact { margin: 0; }
|
||||
.card-actions { justify-content: flex-end; border-top: 1px solid var(--color-border-subtle); padding-top: var(--space-md); }.empty { text-align: center; place-items: center; display: grid; gap: var(--space-md); padding: 64px; }.secrets { border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); padding: var(--space-md); display: grid; gap: var(--space-sm); }.secrets label { display: grid; grid-template-columns: minmax(220px,.7fr) 1fr; align-items: center; gap: var(--space-md); }.secrets small,.modal-card small { color: var(--color-text-tertiary); }.secret-input { display: flex; gap: var(--space-sm); }.secret-input input { flex: 1; }
|
||||
.card-actions { flex-wrap: wrap; justify-content: flex-end; border-top: 1px solid var(--color-border-subtle); padding-top: var(--space-md); }.empty { text-align: center; place-items: center; display: grid; gap: var(--space-md); padding: 64px; }.secrets { border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); padding: var(--space-md); display: grid; gap: var(--space-sm); }.secrets label { display: grid; grid-template-columns: minmax(0,.7fr) minmax(0,1fr); align-items: center; gap: var(--space-md); }.secrets small,.modal-card small { color: var(--color-text-tertiary); }.secret-input { display: flex; gap: var(--space-sm); }.secret-input input { flex: 1; }
|
||||
.modal-backdrop { position: fixed; inset: 0; z-index: 1000; background: rgb(0 0 0 / .48); display: grid; place-items: center; padding: var(--space-xl); }.modal-card { width: min(800px,100%); max-height: calc(100vh - 48px); overflow: auto; background: var(--color-background-primary); border: 1px solid var(--color-border-default); border-radius: var(--radius-xl); box-shadow: var(--shadow-xl); padding: var(--space-xl); display: grid; gap: var(--space-lg); animation: modal-in var(--motion-normal) ease-out; }.modal-card header,.modal-card footer { justify-content: space-between; }.modal-card footer { justify-content: flex-end; }.modal-card label { display: grid; gap: var(--space-xs); font-weight: 600; }.modal-card input,.modal-card textarea { width: 100%; border: 1px solid var(--color-border-default); border-radius: var(--radius-md); padding: 10px 12px; color: var(--color-text-primary); background: var(--color-background-secondary); font: inherit; }.modal-card textarea { resize: vertical; font-family: var(--font-family-mono); font-size: var(--font-size-sm); }.json-editor { line-height: 1.55; }.close { border: 0; background: transparent; color: var(--color-text-secondary); font-size: 28px; cursor: pointer; }
|
||||
.template-row { flex-wrap: wrap; }.template-row > span { margin-right: auto; font-weight: 600; }.template,.mode-tabs button { border: 1px solid var(--color-border-default); background: var(--color-background-secondary); color: var(--color-text-secondary); padding: 7px 10px; border-radius: var(--radius-md); cursor: pointer; }.template.active,.mode-tabs button.active { color: var(--color-accent-primary); border-color: var(--color-accent-primary); background: var(--color-accent-soft); }.mode-tabs { display: inline-flex; justify-self: start; gap: 2px; padding: 3px; border-radius: var(--radius-md); background: var(--color-background-secondary); }.two-columns { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-md); }
|
||||
@keyframes modal-in { from { opacity: 0; transform: translateY(8px) scale(.99); } } @media (max-width:720px) { .two-columns,.secrets label { grid-template-columns:1fr; }.card-actions { flex-wrap:wrap; } }
|
||||
|
||||
@@ -8,6 +8,10 @@ it('distinguishes unreported tokens from zero and switches to request counts', a
|
||||
api: { requests: 2, totals: { input_tokens: 0 }, coverage: { input_tokens: 1 } },
|
||||
}] } })
|
||||
expect(wrapper.findAll('.usage-bar.missing')).toHaveLength(1)
|
||||
expect(wrapper.get('.usage-pie').attributes('aria-label')).toContain('本地模型: 0')
|
||||
await wrapper.get('.chart-column').trigger('mouseenter')
|
||||
expect(wrapper.get('.chart-column').classes()).toContain('highlighted')
|
||||
expect(wrapper.get('.chart-readout').text()).toContain('未提供')
|
||||
expect(wrapper.get('.usage-bar.local').attributes('aria-label')).toContain('未提供')
|
||||
expect(wrapper.get('.usage-bar.api').attributes('aria-label')).toContain('覆盖 1/2')
|
||||
await wrapper.get('select').setValue('requests')
|
||||
|
||||
@@ -11,6 +11,11 @@ export interface UsageBucket {
|
||||
}
|
||||
const props = defineProps<{ buckets: UsageBucket[] }>()
|
||||
const metric = ref('input_tokens')
|
||||
const hovered = ref<string | null>(null)
|
||||
const activeBucket = computed(() => props.buckets.find(bucket => bucket.date === hovered.value))
|
||||
const sums = computed(() => sources.map(source => ({ source, value: props.buckets.reduce((sum, bucket) => sum + (value(bucket, source) ?? 0), 0), coverage: props.buckets.reduce((sum, bucket) => sum + (metric.value === 'requests' ? bucket[source].requests : bucket[source].coverage[metric.value] ?? 0), 0), requests: props.buckets.reduce((sum, bucket) => sum + bucket[source].requests, 0) })))
|
||||
const total = computed(() => sums.value.reduce((sum, item) => sum + item.value, 0))
|
||||
const pie = computed(() => total.value ? `conic-gradient(var(--color-info) 0 ${sums.value[0]!.value / total.value * 100}%, var(--color-accent-primary) 0 100%)` : 'var(--color-border-subtle)')
|
||||
const sources = ['local', 'api'] as const
|
||||
const labels = computed(() => ({ local: t('本地模型', 'Local models'), api: t('模型提供商', 'Providers') }))
|
||||
const metricLabel = computed(() => metric.value === 'requests' ? t('请求次数', 'Requests') : metric.value === 'input_tokens' ? t('输入 Token', 'Input tokens') : metric.value === 'output_tokens' ? t('输出 Token', 'Output tokens') : t('总 Token', 'Total tokens'))
|
||||
@@ -38,33 +43,48 @@ const maximum = computed(() => Math.max(1, ...props.buckets.flatMap(bucket => so
|
||||
</select>
|
||||
<div class="chart-legend"><span class="local"><AppIcon :icon="Cpu" :size="16" />{{ labels.local }} · {{ t('实色', 'Solid') }}</span><span class="api"><AppIcon :icon="Connection" :size="16" />{{ labels.api }} · {{ t('斜纹', 'Striped') }}</span></div>
|
||||
</div>
|
||||
<div class="chart-layout"><div class="bar-pane">
|
||||
<p class="subtle chart-scale">{{ metricLabel }} · 0 — {{ maximum.toLocaleString() }}</p>
|
||||
<div class="chart-scroll" tabindex="0" :aria-label="t('按日期横向滚动查看柱状图', 'Scroll the chart by date')">
|
||||
<div class="chart-columns" :style="{ minWidth: `${buckets.length * 48}px` }">
|
||||
<div v-for="bucket in buckets" :key="bucket.date" class="chart-column">
|
||||
<div class="chart-columns" :style="{ minWidth: `${buckets.length * 14}px` }">
|
||||
<div v-for="bucket in buckets" :key="bucket.date" class="chart-column" :class="{ highlighted: hovered === bucket.date }" tabindex="0" @mouseenter="hovered = bucket.date" @mouseleave="hovered = null" @focus="hovered = bucket.date" @blur="hovered = null">
|
||||
<div class="chart-bars">
|
||||
<div v-for="source in sources" :key="source" class="usage-bar" :class="[source, { missing: value(bucket, source) === null }]"
|
||||
:style="{ height: value(bucket, source) === null ? '8px' : `${(value(bucket, source) ?? 0) / maximum * 160}px` }"
|
||||
role="img" :aria-label="description(bucket, source)" :title="description(bucket, source)" />
|
||||
</div>
|
||||
<small :title="`${bucket.date} – ${bucket.end_date}`">{{ bucket.date.slice(5) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-readout" aria-live="polite"><template v-if="activeBucket"><strong>{{ activeBucket.date }}</strong><span v-for="source in sources" :key="source" :class="source">{{ description(activeBucket, source) }}</span></template><span v-else>{{ t('悬停或聚焦日期查看数据', 'Hover or focus a date for details') }}</span></div>
|
||||
</div><aside class="pie-pane"><h4>{{ t('来源占比', 'Usage by source') }}</h4>
|
||||
<div class="usage-pie" role="img" :aria-label="sums.map(item => `${labels[item.source]}: ${item.value}`).join(' · ')" :style="{ background: pie }"><div><strong>{{ total.toLocaleString() }}</strong><small>{{ metricLabel }}</small></div></div>
|
||||
<div v-for="item in sums" :key="item.source" class="pie-value" :class="item.source"><AppIcon :icon="item.source === 'local' ? Cpu : Connection" :size="16" /><span>{{ labels[item.source] }}</span><strong>{{ item.coverage ? item.value.toLocaleString() : item.requests ? t('未提供', 'Unavailable') : '0' }} · {{ total ? (item.value / total * 100).toFixed(1) + '%' : '—' }}</strong><small>{{ t('覆盖', 'Coverage') }} {{ item.coverage }}/{{ item.requests }}</small></div>
|
||||
<p class="subtle">{{ t('占比仅基于已报告值;缺失指标不计入分母。', 'Shares use reported values only; missing counters are excluded.') }}</p>
|
||||
</aside></div>
|
||||
<p class="subtle">{{ t('按本机时区分组;柱高仅汇总已报告值,悬停可查看覆盖请求数。虚线表示有请求但未提供该指标,不作为零消耗。', 'Grouped by your local UTC offset. Bars sum reported values; hover for coverage. Dashed markers mean requests with unavailable counters, not zero usage.') }}</p>
|
||||
<details><summary>{{ t('查看图表数据', 'View chart data') }}</summary><div class="chart-scroll"><table><thead><tr><th>{{ t('日期', 'Date') }}</th><th>{{ labels.local }}</th><th>{{ labels.api }}</th></tr></thead><tbody><tr v-for="bucket in buckets" :key="bucket.date"><th>{{ bucket.date }}<template v-if="bucket.date !== bucket.end_date"> – {{ bucket.end_date }}</template></th><td v-for="source in sources" :key="source">{{ description(bucket, source) }}</td></tr></tbody></table></div></details>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chart-layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(220px, 1fr); gap: 24px; margin-top: 16px; }
|
||||
.bar-pane { min-width: 0; }.pie-pane { border-left: 1px dashed var(--color-border-default); padding-left: 24px; min-width: 0; }
|
||||
.usage-pie { width: 170px; aspect-ratio: 1; margin: 20px auto; border-radius: 50%; display: grid; place-items: center; }
|
||||
.usage-pie > div { width: 112px; aspect-ratio: 1; border-radius: 50%; background: var(--color-surface-primary); display: flex; flex-direction: column; align-items: center; justify-content: center; }.usage-pie strong { font-size: 20px; }.usage-pie small { font-size: 11px; }
|
||||
.pie-value { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 12px; margin: 10px 0; }.pie-value small { width: 100%; color: var(--color-text-secondary); }.pie-pane > p { font-size: 12px; }
|
||||
.chart-column { transition: background-color 150ms; border-radius: 4px; }.chart-column.highlighted { background: var(--color-accent-soft); outline: 1px solid var(--color-border-focus); outline-offset: -1px; }.highlighted .usage-bar { filter: brightness(1.15); }
|
||||
.chart-readout { min-height: 76px; font-size: 12px; display: flex; flex-direction: column; gap: 4px; margin-top: 12px; padding: 10px; border: 1px dashed var(--color-border-subtle); border-radius: var(--radius-sm); background: var(--color-surface-primary); overflow-wrap: anywhere; }
|
||||
@media (max-width: 760px) { .chart-layout { grid-template-columns: 1fr; }.pie-pane { border-left: 0; border-top: 1px dashed var(--color-border-default); padding: 20px 0 0; } }
|
||||
|
||||
.usage-chart { min-width: 0; padding: 16px; border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); background: var(--color-background-primary); }
|
||||
.chart-toolbar, .chart-legend, .chart-legend span { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
.chart-toolbar { justify-content: space-between; }.chart-toolbar .select { width: auto; }.chart-legend { font-size: 12px; }
|
||||
.local { color: var(--color-info); }.api { color: var(--color-accent-primary); }
|
||||
.chart-scale { margin: 12px 0 0; font-size: 12px; }.chart-scroll { overflow-x: auto; padding-bottom: 8px; }.chart-columns { display: flex; gap: 8px; }
|
||||
.chart-column { flex: 1; min-width: 40px; text-align: center; }.chart-column small { font-size: 11px; color: var(--color-text-secondary); white-space: nowrap; }
|
||||
.chart-bars { height: 176px; display: flex; align-items: flex-end; justify-content: center; gap: 4px; border-bottom: 1px solid var(--color-border-default); background: repeating-linear-gradient(to top, transparent 0 39px, var(--color-border-subtle) 39px 40px); }
|
||||
.usage-bar { width: 14px; max-width: 35%; background: currentColor; border-radius: 3px 3px 0 0; }.usage-bar.api { background: repeating-linear-gradient(45deg, currentColor 0 4px, color-mix(in srgb, currentColor 45%, var(--color-surface-primary)) 4px 7px); }
|
||||
.chart-scale { margin: 12px 0 0; font-size: 12px; }.chart-scroll { overflow-x: auto; padding-bottom: 2px; }.chart-columns { display: flex; gap: 2px; }
|
||||
.chart-column { flex: 1; min-width: 12px; text-align: center; }
|
||||
.chart-bars { height: 176px; display: flex; align-items: flex-end; justify-content: center; gap: 2px; border-bottom: 1px solid var(--color-border-default); background: repeating-linear-gradient(to top, transparent 0 39px, var(--color-border-subtle) 39px 40px); }
|
||||
.usage-bar { width: 35%; max-width: 28px; background: currentColor; border-radius: 3px 3px 0 0; }.usage-bar.api { background: repeating-linear-gradient(45deg, currentColor 0 4px, color-mix(in srgb, currentColor 45%, var(--color-surface-primary)) 4px 7px); }
|
||||
.usage-bar.missing { background: transparent; border: 1px dashed currentColor; box-sizing: border-box; }
|
||||
.usage-chart > p:last-of-type { font-size: 12px; margin: 12px 0; }.usage-chart summary { cursor: pointer; color: var(--color-text-link); font-size: 12px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; }th, td { padding: 8px; text-align: left; border-bottom: 1px solid var(--color-border-subtle); }
|
||||
|
||||
Reference in New Issue
Block a user