This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/apps/web/src/features/gov-data/internal/panel.tsx
T
ChaiHaoandClaude Fable 5 34e207c3e8 三面拆分⑫:UI 打磨 —— 补 10 枚缺失图标,蓝图改因果链,台账收成一个模块
图标注册表缺 arrow-left-right/columns/table/tag 等 10 名,用到即渲染成空 SVG —— 补齐;
蓝图四库改真箭头相连的因果链节点卡(卡即入口,删重复的入库段);
驾驶舱终态分布改五色堆叠条;运营数据收成左轨一枚「台账」,域下沉为 2-3 字短名页签
(受理→寻源→定价→议价→替代→共享,按流程序),进模块自动落首域。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-27 22:02:49 +08:00

204 lines
7.7 KiB
TypeScript

/**
* feature `gov-data` —— 数据的两副面孔,各归其台:
*
* `DataHome`(设计中心 · 数据域):**定义面** —— 域 × 对象的结构:有哪些列、主键是什么、谁声明了它。
* `LedgersHome`(运营中心 · 台账 · 某域页签):**运营面** —— 这张表现在有多少行、里面是什么。
*
* 分开的理由:改 schema 和查行数据是两种心智。
* 设计者问「这个对象定义全不全」,运营者问「今天这张表有几条」——
* 摞在一屏,设计页就会变成一个查数据的地方,行数把结构挤到看不见。
*
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
*/
import { Ico } from '@web-shared/Ico.tsx';
import { Card, Chip, Clear, NList, NRow, Note, PageHead, QueryGate, Sec } from '@web-shared/gov/primitives.tsx';
import type { Nav } from '@web-shared/gov/gov-route.ts';
import { useGovLedger, useGovObjects } from '@web-shared/gov/gov-api.ts';
import { domainShort, sortDomains } from '@web-shared/gov/domains.ts';
export type HomeProps = { tab: string; nav: Nav };
/* ==================== 设计侧:数据域(定义面) ==================== */
/**
* 只呈现**结构**:域分组 → 对象 → 列与主键。
* 行数与行浏览入口不在这里 —— 那在运营中心的台账。
*/
export function DataHome({ nav }: HomeProps) {
const q = useGovObjects();
const objects = q.data?.objects ?? [];
const domains = sortDomains([...new Set(objects.map((o) => o.domain))]);
return (
<QueryGate query={q}>
<>
<Note>
数据<b>不设箱、靠域</b>:域是承载的划分,对象由活动声明物化——最大化合并、只增不破坏。
这里改结构;<b>行数据在运营中心 · 台账看</b>。
</Note>
{domains.map((d) => (
<div key={d} className="g-sec">
<div className="g-band">
<Ico name="database" s={13} />
{domainShort(d)}
<span className="g-mono" style={{ fontWeight: 500 }}>
{d}
</span>
<span className="x">{objects.filter((o) => o.domain === d).length} 个对象 · 发布即入运营台账</span>
</div>
<NList>
{objects
.filter((o) => o.domain === d)
.map((o) => (
<ObjectSchemaRow key={`${o.domain}.${o.object}`} domain={o.domain} object={o.object} nav={nav} />
))}
</NList>
</div>
))}
{objects.length === 0 && (
<Clear>还没有物化的对象 —— 活动第一次写入时,系统按它的声明把表建出来,然后出现在这里。</Clear>
)}
</>
</QueryGate>
);
}
/** 一个对象的结构一行:列数 / 主键 —— 点开看完整模式,仍不含行数据 */
function ObjectSchemaRow({ domain, object, nav }: { domain: string; object: string; nav: Nav }) {
const page = useGovLedger(domain, object);
const columns = page.data?.columns ?? [];
const pk = page.data?.primaryKey ?? [];
return (
<NRow
tone="brand"
ico="table"
title={object}
sub={`${domain}.${object}`}
cols={[
{ k: '列', v: columns.length === 0 ? '—' : String(columns.length) },
{ k: '主键', v: pk.length === 0 ? '—' : pk.join(', ') },
]}
onClick={() => nav.push({ k: 'obj', id: `${domain}.${object}` })}
/>
);
}
/* ==================== 运营侧:台账(某一个域页签) ==================== */
/**
* 台账的一个域 —— 壳层把选中的域页签(键)传进来;没带域时壳层已落到首域。
* 这里看的是运营事实:这张表现在多少行,点进去逐行看。
*/
export function LedgersHome({ tab, nav }: HomeProps) {
const q = useGovObjects();
const domain = tab;
const inDomain = (q.data?.objects ?? []).filter((o) => o.domain === domain);
return (
<QueryGate query={q}>
<>
<Note>
<b>{domainShort(domain)}</b>域(<span className="g-mono">{domain}</span>)的行数据、主键与列结构。
结构要改去设计中心 · 数据域;这里做的是运营动作 —— 查行、看版本、追引用。
</Note>
{inDomain.length === 0 ? (
<Clear>这个域还没有对象 —— 对象随活动首次写入而物化,建好就出现在这里。</Clear>
) : (
<NList>
{inDomain.map((o) => (
<LedgerObjectRow key={`${o.domain}.${o.object}`} domain={o.domain} object={o.object} nav={nav} />
))}
</NList>
)}
</>
</QueryGate>
);
}
/** 一个数据对象一行:行数是运营侧最想先看到的那个数 */
function LedgerObjectRow({ domain, object, nav }: { domain: string; object: string; nav: Nav }) {
const page = useGovLedger(domain, object);
const total = page.data?.total;
return (
<NRow
tone={total !== undefined && total > 0 ? 'brand' : 'mute'}
ico="table"
title={object}
sub={`${domain}.${object}`}
cols={[
{ k: '行数', v: total === undefined ? '…' : String(total) },
{ k: '列', v: String((page.data?.columns ?? []).length) },
]}
right={<Chip tone={total !== undefined && total > 0 ? 'ok' : 'mute'}>{total ?? 0} 行</Chip>}
onClick={() => nav.push({ k: 'obj', id: `${domain}.${object}` })}
/>
);
}
/* ==================== 对象详情(两面共用) ==================== */
export function ObjPage({ id }: { id: string }) {
const [domain, object] = id.split('.');
const objects = useGovObjects();
const page = useGovLedger(domain, object);
const known = (objects.data?.objects ?? []).some((o) => o.domain === domain && o.object === object);
if (!known) return <Note>对象未物化。</Note>;
const columns = page.data?.columns ?? [];
return (
<>
<PageHead
ico="table"
kicker={`数据对象 · ${domain}`}
title={object ?? ''}
chips={
<>
<Chip ico="columns">{columns.length} 列</Chip>
<Chip ico="hash">{page.data?.total ?? 0} 行</Chip>
<Chip ico="key">主键 {(page.data?.primaryKey ?? []).join(', ') || '—'}</Chip>
</>
}
desc="声明物化:最大化合并、只增不破坏——不删列、不改窄、不换主键。此处只读正式版,跨任务只见过门的事实。"
/>
<Card ico="columns" title="模式">
<div className="g-rows">
{columns.map((c) => (
<div key={c.name}>
<em>{c.name}</em>
<b className="mono">{c.type}</b>
</div>
))}
</div>
</Card>
<Sec ico="table" title="记录浏览" extra={`前 ${(page.data?.rows ?? []).length} 行`}>
{(page.data?.rows ?? []).length === 0 ? (
<Clear>这张表还没有记录 —— 任务放行后,过门的事实才落进来。</Clear>
) : (
<div className="g-dbwrap">
<table className="g-dbtable">
<thead>
<tr>
{columns.map((c) => (
<th key={c.name}>{c.name}</th>
))}
</tr>
</thead>
<tbody>
{(page.data?.rows ?? []).map((row, i) => (
<tr key={i}>
{columns.map((c) => (
<td key={c.name} className="g-mono">
{String(row[c.name] ?? '')}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</Sec>
</>
);
}