feat(web): W3-2 library, subscriptions and rules, preferences, decisions inbox, identity and metadata
- library list (kind/status/search, cursor paging) and detail: paths and roots, external identity mapping with lock, field-level metadata sources and locks, slot cards with rejection reasons, content units (provisional flag), downloads with verification; remap/lock/refresh/merge actions - subscriptions: list/create/update, slot rules editor (hard/soft/upgrade/completion), policy revision draft -> preview -> publish (grant warning) -> rollback - preferences: tag CRUD, disable/enable, suppression records and lift, history timeline - inbox: grouped by entity and kind, evidence/suggestion/impact/preview, resolve options, SSE refresh - identity: pool assign/dismiss/retry, external identity revisions and lock, title alias management - all responses validated against contracts zod schemas Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e5e42b1f0d
commit
ef73abe3df
25 files changed
+6670
-19
No files matched your search
@@ -0,0 +1,107 @@
|
||||
import type { AcquisitionSlot, CandidateEvaluation } from "@umt/contracts";
|
||||
import { Link2 } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { formatDateTime } from "~/lib/utils.ts";
|
||||
import { CANDIDATE_DECISION, SLOT_STATUS } from "./labels.ts";
|
||||
import { Chip, ChipList, Tone } from "./parts.tsx";
|
||||
import { summarizeCompletion, summarizeHard, summarizeSoft, summarizeUpgrade } from "./slot-rules.tsx";
|
||||
|
||||
/** 一个 Slot 的卡片(卡内子块,不叠玻璃):状态、规则摘要、当前资产、候选拒绝理由 */
|
||||
export function SlotCard({
|
||||
slot,
|
||||
currentAsset,
|
||||
unitLabel,
|
||||
candidates = [],
|
||||
actions,
|
||||
}: {
|
||||
slot: AcquisitionSlot;
|
||||
/** 当前资产的一行说明(交付特征) */
|
||||
currentAsset?: ReactNode;
|
||||
/** 绑定到某个内容单元时的说明 */
|
||||
unitLabel?: string;
|
||||
candidates?: CandidateEvaluation[];
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
const st = SLOT_STATUS[slot.status];
|
||||
const hard = summarizeHard(slot.hardConditions, slot.fallback);
|
||||
const soft = summarizeSoft(slot.softPreferences);
|
||||
const upgrade = summarizeUpgrade(slot.upgradeThreshold);
|
||||
return (
|
||||
<article className="min-w-0 rounded-xl border border-border/80 bg-foreground/[0.02] p-4">
|
||||
<header className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="min-w-0 truncate font-mono text-body font-semibold text-foreground">{slot.name}</h3>
|
||||
<Tone v={st} />
|
||||
{unitLabel ? <Chip>{unitLabel}</Chip> : null}
|
||||
{slot.requireDistinctAsset ? (
|
||||
<Chip>
|
||||
<Link2 className="h-3 w-3" />
|
||||
需不同物理资产{slot.distinctGroup ? ` · ${slot.distinctGroup}` : ""}
|
||||
</Chip>
|
||||
) : null}
|
||||
<span className="ml-auto text-meta text-muted-foreground tabular-nums">r{slot.revision}</span>
|
||||
{actions}
|
||||
</header>
|
||||
{slot.statusReason ? <p className="mt-2 text-caption text-muted-foreground">{slot.statusReason}</p> : null}
|
||||
<dl className="mt-3 grid gap-3 text-sm sm:grid-cols-2">
|
||||
<div className="min-w-0">
|
||||
<dt className="mb-1 text-meta text-muted-foreground">硬条件</dt>
|
||||
<dd>
|
||||
<ChipList items={hard} empty="不限" />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<dt className="mb-1 text-meta text-muted-foreground">软偏好</dt>
|
||||
<dd>
|
||||
<ChipList items={soft} empty="无" />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<dt className="mb-1 text-meta text-muted-foreground">升级</dt>
|
||||
<dd>
|
||||
<ChipList items={upgrade} empty="未设置" />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<dt className="mb-1 text-meta text-muted-foreground">完成条件</dt>
|
||||
<dd className="text-caption text-foreground">{summarizeCompletion(slot.completion)}</dd>
|
||||
</div>
|
||||
<div className="min-w-0 sm:col-span-2">
|
||||
<dt className="mb-1 text-meta text-muted-foreground">当前资产</dt>
|
||||
<dd className="text-caption break-words text-foreground">{currentAsset ?? "无"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{candidates.length ? (
|
||||
<div className="mt-3 border-t border-border/60 pt-3">
|
||||
<p className="mb-2 text-meta text-muted-foreground">最近候选 · 为什么选 / 不选</p>
|
||||
<ul className="space-y-2">
|
||||
{candidates.map((c) => (
|
||||
<li key={`${c.sourceReleaseId}-${c.evaluatedAt}`} className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Tone v={CANDIDATE_DECISION[c.decision]} />
|
||||
<span className="min-w-0 font-mono text-meta break-all text-foreground">{c.title}</span>
|
||||
</div>
|
||||
{c.rejected.length ? (
|
||||
<ul className="mt-1 list-disc space-y-0.5 pl-5 text-meta text-muted-foreground">
|
||||
{c.rejected.map((r) => (
|
||||
<li key={r.code} className="break-words">
|
||||
{r.message}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{c.reasons.length ? (
|
||||
<p className="mt-1 text-meta break-words text-muted-foreground">{c.reasons.join(";")}</p>
|
||||
) : null}
|
||||
<p className="text-micro text-muted-foreground tabular-nums">{formatDateTime(c.evaluatedAt)}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
<p className="mt-3 text-micro text-muted-foreground tabular-nums">
|
||||
最近评估 {formatDateTime(slot.lastEvaluatedAt)}
|
||||
{slot.nextEvaluateAt ? ` · 下次 ${formatDateTime(slot.nextEvaluateAt)}` : ""}
|
||||
</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user