forked from EduCraft/curriculum-project-hub
feat(hub): drop redundant /admin/org/:slug path + release v0.0.35
Silo hostname already carries tenancy. Admin SPA routes become /admin/..., legacy /admin/org/:slug/* bookmarks redirect, login lands on /admin.
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { api, type UsageReport, type UsageBreakdownRow } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import {
|
||||
fmtCost,
|
||||
fmtDateOnly,
|
||||
fmtNum,
|
||||
fmtQuantity,
|
||||
fmtTokens,
|
||||
usageKindLabel,
|
||||
} from '$lib/format';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import StatCard from '$lib/components/StatCard.svelte';
|
||||
import LoadingState from '$lib/components/LoadingState.svelte';
|
||||
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
|
||||
let usage = $state<UsageReport | null>(null);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let from = $state('');
|
||||
let to = $state('');
|
||||
|
||||
function toIsoStart(dateLocal: string): string | undefined {
|
||||
if (!dateLocal) return undefined;
|
||||
const d = new Date(`${dateLocal}T00:00:00`);
|
||||
return Number.isNaN(d.getTime()) ? undefined : d.toISOString();
|
||||
}
|
||||
|
||||
function toIsoEnd(dateLocal: string): string | undefined {
|
||||
if (!dateLocal) return undefined;
|
||||
const d = new Date(`${dateLocal}T23:59:59.999`);
|
||||
return Number.isNaN(d.getTime()) ? undefined : d.toISOString();
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!slug) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
usage = await api.usage(slug, {
|
||||
...(toIsoStart(from) !== undefined ? { from: toIsoStart(from) } : {}),
|
||||
...(toIsoEnd(to) !== undefined ? { to: toIsoEnd(to) } : {}),
|
||||
});
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearRange() {
|
||||
from = '';
|
||||
to = '';
|
||||
void load();
|
||||
}
|
||||
|
||||
function sourceLabel(row: UsageBreakdownRow): string {
|
||||
if (row.capabilityId) return row.capabilityId;
|
||||
if (row.model) return row.model;
|
||||
return '—';
|
||||
}
|
||||
|
||||
function meterCell(row: UsageBreakdownRow): string {
|
||||
if (row.unit) return fmtQuantity(row.quantity, row.unit);
|
||||
if (row.inputTokens > 0 || row.outputTokens > 0) return fmtTokens(row.inputTokens, row.outputTokens);
|
||||
return '—';
|
||||
}
|
||||
|
||||
function meterHint(row: UsageBreakdownRow): string {
|
||||
if (row.unit) return '非 token 计量';
|
||||
if (row.inputTokens > 0 || row.outputTokens > 0) return 'in / out tokens';
|
||||
return '无计量';
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (slug) void load();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if loading && !usage}
|
||||
<LoadingState />
|
||||
{:else if error && !usage}
|
||||
<ErrorBanner message={error} onretry={load} />
|
||||
{:else if usage}
|
||||
<PageHeader
|
||||
title="用量报告"
|
||||
description="按 UsageFact 分账:模型完成与外部能力(PDF→MD、ASR 等)分开汇总。缺失成本计为未知,不为 0。"
|
||||
/>
|
||||
|
||||
<div class="saas-card-pad mb-6">
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<div>
|
||||
<label class="saas-label" for="usage-from">从</label>
|
||||
<input id="usage-from" class="saas-input" type="date" bind:value={from} />
|
||||
</div>
|
||||
<div>
|
||||
<label class="saas-label" for="usage-to">到</label>
|
||||
<input id="usage-to" class="saas-input" type="date" bind:value={to} />
|
||||
</div>
|
||||
<button class="saas-btn-primary py-1.5! text-sm" type="button" onclick={load} disabled={loading}>
|
||||
{loading ? '加载中…' : '应用筛选'}
|
||||
</button>
|
||||
<button class="saas-btn-secondary py-1.5! text-sm" type="button" onclick={clearRange} disabled={loading}>
|
||||
清除
|
||||
</button>
|
||||
{#if usage.from || usage.to}
|
||||
<p class="saas-muted grow text-right text-xs">
|
||||
窗口:
|
||||
{usage.from ? fmtDateOnly(usage.from) : '—'}
|
||||
→
|
||||
{usage.to ? fmtDateOnly(usage.to) : '—'}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
<p class="mt-3 text-sm text-error-700">{error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mb-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<StatCard label="运行总数" value={fmtNum(usage.totals.runCount)} />
|
||||
<StatCard
|
||||
label="有成本 / 无成本"
|
||||
value={`${fmtNum(usage.totals.runsWithCost)} / ${fmtNum(usage.totals.runsWithoutCost)}`}
|
||||
hint="无成本 = 成本未知,不是 $0"
|
||||
/>
|
||||
<StatCard label="成本 (USD)" value={fmtCost(usage.totals.costUsd)} hint="仅汇总已知 costUsd" />
|
||||
<StatCard label="输入 tokens" value={fmtNum(usage.totals.inputTokens)} hint="主要来自模型完成" />
|
||||
<StatCard label="输出 tokens" value={fmtNum(usage.totals.outputTokens)} hint="主要来自模型完成" />
|
||||
<StatCard
|
||||
label="分账条目"
|
||||
value={fmtNum(usage.breakdown.reduce((n, b) => n + b.factCount, 0))}
|
||||
hint={`${fmtNum(usage.breakdown.length)} 个分项`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="saas-card overflow-hidden mb-6">
|
||||
<div class="border-b border-surface-200 px-5 py-3">
|
||||
<h3 class="text-sm font-semibold text-surface-800">按来源分账</h3>
|
||||
<p class="saas-muted mt-0.5 text-xs">
|
||||
kind × provider × model/capability。外部能力显示页数/秒等计量,不与 tokens 混排。
|
||||
</p>
|
||||
</div>
|
||||
{#if usage.breakdown.length === 0}
|
||||
<EmptyState title="暂无用量事实" description="跑过智能体后,模型与外部能力消费会出现在此。" />
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>类型</th>
|
||||
<th>供应方</th>
|
||||
<th>模型 / 能力</th>
|
||||
<th>次数</th>
|
||||
<th>计量</th>
|
||||
<th>有成本 / 未知</th>
|
||||
<th>成本</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each usage.breakdown as row}
|
||||
<tr>
|
||||
<td>
|
||||
<span
|
||||
class={row.kind === 'external_capability'
|
||||
? 'saas-badge-primary'
|
||||
: row.kind === 'model_completion'
|
||||
? 'saas-badge-success'
|
||||
: 'saas-badge-primary'}
|
||||
>
|
||||
{usageKindLabel(row.kind)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="font-mono text-xs">{row.provider}</td>
|
||||
<td class="font-mono text-xs">{sourceLabel(row)}</td>
|
||||
<td class="tabular-nums">{fmtNum(row.factCount)}</td>
|
||||
<td class="tabular-nums">
|
||||
<div>{meterCell(row)}</div>
|
||||
<div class="text-[11px] text-surface-600">{meterHint(row)}</div>
|
||||
</td>
|
||||
<td class="tabular-nums text-surface-700">
|
||||
{fmtNum(row.factsWithCost)} / {fmtNum(row.factsWithoutCost)}
|
||||
</td>
|
||||
<td class="tabular-nums font-medium">{fmtCost(row.costUsd)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="saas-card overflow-hidden">
|
||||
<div class="border-b border-surface-200 px-5 py-3">
|
||||
<h3 class="text-sm font-semibold text-surface-800">按项目</h3>
|
||||
<p class="saas-muted mt-0.5 text-xs">项目仍是权限边界;行内成本已含该项目全部 fact 类型。</p>
|
||||
</div>
|
||||
{#if usage.projects.length === 0}
|
||||
<EmptyState title="暂无项目" description="创建项目并触发智能体后会出现用量。" />
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>项目</th>
|
||||
<th>运行</th>
|
||||
<th>有成本 / 未知</th>
|
||||
<th>in / out tokens</th>
|
||||
<th>成本</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each usage.projects as p}
|
||||
<tr>
|
||||
<td class="font-medium">{p.projectName}</td>
|
||||
<td class="tabular-nums">{fmtNum(p.runCount)}</td>
|
||||
<td class="tabular-nums text-surface-700">
|
||||
{fmtNum(p.runsWithCost)} / {fmtNum(p.runsWithoutCost)}
|
||||
</td>
|
||||
<td class="tabular-nums text-surface-600">{fmtTokens(p.inputTokens, p.outputTokens)}</td>
|
||||
<td class="tabular-nums">{fmtCost(p.costUsd)}</td>
|
||||
<td class="text-right">
|
||||
<a class="text-sm text-primary-700 hover:underline" href={`/admin/projects/${p.projectId}`}>
|
||||
查看项目
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user