feat: improve app ui and project metadata
This commit is contained in:
@@ -1,6 +1,20 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { Blocks, Database, Pencil, ShieldCheck } from "lucide-react";
|
||||
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { PageHeader } from "@/components/ui/PageHeader";
|
||||
import { SectionHeader } from "@/components/ui/SectionHeader";
|
||||
import { StatusNote } from "@/components/ui/StatusNote";
|
||||
import type { ProjectResponse, SkillView } from "@/lib/api/types";
|
||||
import { groupByScope, scopeLabel, tierLabel } from "@/lib/skills/skills";
|
||||
import {
|
||||
groupByScope,
|
||||
scopeLabel,
|
||||
summarizeSkills,
|
||||
tierLabel,
|
||||
} from "@/lib/skills/skills";
|
||||
|
||||
interface SkillsPageProps {
|
||||
project: ProjectResponse;
|
||||
@@ -10,6 +24,7 @@ interface SkillsPageProps {
|
||||
// 技能库(UX §7):只读注册表视图(name/scope/tier/reads/writes)。Server Component(纯读)。
|
||||
export function SkillsPage({ project, skills }: SkillsPageProps) {
|
||||
const groups = groupByScope(skills);
|
||||
const summary = summarizeSkills(skills);
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
@@ -18,60 +33,161 @@ export function SkillsPage({ project, skills }: SkillsPageProps) {
|
||||
projectId={project.id}
|
||||
activeNav="skills"
|
||||
>
|
||||
<div className="mx-auto flex max-w-4xl flex-col gap-6 p-6">
|
||||
<h1 className="font-serif text-lg text-ink">技能库</h1>
|
||||
<p className="text-xs text-ink-soft">
|
||||
声明式 Skill 注册表:每个技能声明能力档位与可读/写的表(沙箱白名单,越权产出丢弃并审计)。
|
||||
</p>
|
||||
{groups.length === 0 ? (
|
||||
<p className="text-sm text-ink-soft">(暂无已注册技能)</p>
|
||||
) : (
|
||||
groups.map((group) => (
|
||||
<section key={group.scope}>
|
||||
<h2 className="mb-2 font-serif text-sm text-ink">
|
||||
{scopeLabel(group.scope)}
|
||||
<span className="ml-2 text-xs text-ink-soft">
|
||||
{group.skills.length}
|
||||
</span>
|
||||
</h2>
|
||||
<ul className="flex flex-col gap-2">
|
||||
{group.skills.map((skill) => (
|
||||
<li
|
||||
key={skill.name}
|
||||
className="rounded border border-line bg-panel p-3 text-sm"
|
||||
>
|
||||
<div className="mb-1 flex flex-wrap items-center gap-2">
|
||||
<span className="font-mono text-ink">{skill.name}</span>
|
||||
<span className="rounded bg-bg px-2 py-0.5 text-xs text-ink-soft">
|
||||
{tierLabel(skill.tier)}
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-6 p-6">
|
||||
<PageHeader
|
||||
title="技能库"
|
||||
description="声明式 Skill 注册表:每个技能声明能力档位与可读/写的表,越权产出会被丢弃并审计。"
|
||||
/>
|
||||
<div className="grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
|
||||
<aside className="flex flex-col gap-4 lg:sticky lg:top-[8rem] lg:self-start">
|
||||
<Card as="section" className="p-4">
|
||||
<SectionHeader
|
||||
title="注册表概览"
|
||||
description="按来源、档位和表权限汇总当前可用技能。"
|
||||
/>
|
||||
<dl className="mt-4 grid grid-cols-3 gap-2 text-center">
|
||||
<div className="rounded border border-line bg-paper p-3">
|
||||
<dt className="text-xs text-ink-soft">总数</dt>
|
||||
<dd className="mt-1 font-serif text-2xl text-ink">
|
||||
{summary.total}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="rounded border border-line bg-paper p-3">
|
||||
<dt className="text-xs text-ink-soft">可写</dt>
|
||||
<dd className="mt-1 font-serif text-2xl text-ink">
|
||||
{summary.writableCount}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="rounded border border-line bg-paper p-3">
|
||||
<dt className="text-xs text-ink-soft">只读</dt>
|
||||
<dd className="mt-1 font-serif text-2xl text-ink">
|
||||
{summary.readonlyCount}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{summary.scopes.length > 0 ? (
|
||||
summary.scopes.map((item) => (
|
||||
<Badge key={item.key} variant="info">
|
||||
{scopeLabel(item.key)} {item.count}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-ink-soft">暂无来源</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card as="section" className="p-4">
|
||||
<SectionHeader title="能力档位" />
|
||||
{summary.tiers.length > 0 ? (
|
||||
<ul className="mt-3 flex flex-col gap-2 text-sm">
|
||||
{summary.tiers.map((item) => (
|
||||
<li
|
||||
key={item.key}
|
||||
className="flex items-center justify-between border-b border-line/70 py-2 last:border-b-0"
|
||||
>
|
||||
<span className="text-ink">{tierLabel(item.key)}</span>
|
||||
<span className="font-mono text-xs text-ink-soft">
|
||||
{item.count}
|
||||
</span>
|
||||
{skill.genre ? (
|
||||
<span className="rounded bg-bg px-2 py-0.5 text-xs text-ink-soft">
|
||||
{skill.genre}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4 text-xs text-ink-soft">
|
||||
<span>
|
||||
读:
|
||||
{skill.reads && skill.reads.length > 0
|
||||
? skill.reads.join("、")
|
||||
: "—"}
|
||||
</span>
|
||||
<span>
|
||||
写:
|
||||
{skill.writes && skill.writes.length > 0
|
||||
? skill.writes.join("、")
|
||||
: "—"}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="mt-3 text-sm text-ink-soft">暂无档位</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<StatusNote title="写入边界" variant="info">
|
||||
<p className="text-sm leading-6">
|
||||
可写表:{formatTables(summary.writableTables)}
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-5 text-ink-soft">
|
||||
技能产出仍会经过表权限白名单;审稿与验收相关写入继续由人工确认流程收口。
|
||||
</p>
|
||||
</StatusNote>
|
||||
</aside>
|
||||
|
||||
<div className="flex min-w-0 flex-col gap-5">
|
||||
{groups.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={Blocks}
|
||||
title="暂无已注册技能"
|
||||
description="技能注册后会在这里按作用域展示能力档位、可读表与可写表。"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{groups.map((group) => (
|
||||
<section key={group.scope}>
|
||||
<SectionHeader
|
||||
title={scopeLabel(group.scope)}
|
||||
description={`${group.skills.length} 个技能`}
|
||||
/>
|
||||
<ul className="mt-3 grid gap-3">
|
||||
{group.skills.map((skill) => (
|
||||
<SkillListItem key={skill.name} skill={skill} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillListItem({ skill }: { skill: SkillView }) {
|
||||
return (
|
||||
<li className="rounded border border-line bg-panel p-4 text-sm shadow-paper">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="break-all font-mono text-ink">{skill.name}</span>
|
||||
<Badge variant="info">{tierLabel(skill.tier)}</Badge>
|
||||
{skill.genre ? <Badge>{skill.genre}</Badge> : null}
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 text-xs text-ink-soft md:grid-cols-2">
|
||||
<TableLine
|
||||
icon={Database}
|
||||
label="读"
|
||||
tables={skill.reads ?? []}
|
||||
/>
|
||||
<TableLine icon={Pencil} label="写" tables={skill.writes ?? []} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2 text-xs text-ink-soft">
|
||||
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
<span>
|
||||
{skill.writes && skill.writes.length > 0 ? "受控写入" : "只读"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function TableLine({
|
||||
icon: Icon,
|
||||
label,
|
||||
tables,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
tables: readonly string[];
|
||||
}) {
|
||||
return (
|
||||
<span className="flex min-w-0 items-start gap-1.5">
|
||||
<Icon className="mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
||||
<span className="shrink-0">{label}:</span>
|
||||
<span className="min-w-0 break-words">{formatTables(tables)}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function formatTables(tables: readonly string[]): string {
|
||||
return tables.length > 0 ? tables.join("、") : "—";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user