Archived
运营中心撤掉「数据」区;流程包首页重排:卡有层次、⋯ 管编辑与删除、新建变幽灵卡
**运营 · 数据(活动记忆四层)整个撤掉** —— 记忆的放行口还没成形,摆一个空区不如
不摆;真要做时随「数据资产」一并重开。区表、面归属、shell 映射、KnowledgeHome /
MemPage、活动详情里那枚入口一并清干净;旧深链 `#/gov/memory/layers` 落运营首页
(测试跟着改口径,不留死链)。运营中心自此六区。
**流程包首页**:
· 「编排要素:四种」这枚计数删了 —— 网关早废,它是句没人用的口号;
· 卡片重做。从前是「头 + 芯片行 + 一大片空 + 脚注」,158 高全靠留白撑;
现在两段:头(图标 · 名 · 标识 · 生效版 · ⋯)+ **数据板**(活动/审批/边闭合/版本
四格等宽、竖线分隔)。段段有内容、段间有线,高矮由内容说了算 —— 实测 111 高;
· 卡上 **⋯ 菜单**:编辑(就地改名,落在卡面那一版上)/ 删除(整条流,菜单原地翻成
确认,不另弹窗);
· **新建按钮变幽灵卡**:虚线卡住在货架末位,与真卡同宽同高(111/111)。
删整条流是新能力:`DELETE /api/gov/process/:id` + `process.deleteFlow`。
生效版这道闸不设(明示动作,不是手滑),在途单那道照设(PinnedVersionError → 409);
**「还有调起规则指着它」由路由层反查** —— process 是 L7,不该知道 trigger 的存在。
实测(丁子乔账号):改名往返、删除被 2 条调起规则如实挡回、幽灵卡建 `tmp-drop`
再删掉整条流,库里回到只剩 unified-quote v4,试跑数据已清。五闸绿。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -15,7 +15,6 @@ import {
|
||||
Card,
|
||||
Chip,
|
||||
Clear,
|
||||
Entry,
|
||||
Field,
|
||||
Note,
|
||||
PageHead,
|
||||
@@ -39,10 +38,7 @@ type InstallReply = {
|
||||
};
|
||||
import { PARA } from '@web-shared/gov/tone.ts';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import {
|
||||
useGovActivities,
|
||||
useGovMemory,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
import { useGovActivities } from '@web-shared/gov/gov-api.ts';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
@@ -108,9 +104,8 @@ export function LibraryHome({ nav }: HomeProps) {
|
||||
|
||||
/* ---------------- ⑥ 工具管理 ---------------- */
|
||||
|
||||
export function ActPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
export function ActPage({ id }: { id: string }) {
|
||||
const q = useGovActivities();
|
||||
const memory = useGovMemory(id);
|
||||
const a = (q.data?.activities ?? []).find((x) => x.activityId === id);
|
||||
if (a === undefined) return <Note>活动不存在。</Note>;
|
||||
return (
|
||||
@@ -208,12 +203,6 @@ export function ActPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
{a.assetVersions.length === 0 && <Clear>尚无资产版本。</Clear>}
|
||||
</div>
|
||||
</Card>
|
||||
<Entry
|
||||
ico="layers"
|
||||
title="活动记忆"
|
||||
desc={`四层条目 · 当前 ${(memory.data?.entries ?? []).length} 条`}
|
||||
onClick={() => nav.jump('knowledge', 'layers')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
* feature `gov-memory` 唯一出口(编码规范 §9.1)。
|
||||
* 记忆管理:度量仪表、活动记忆四层
|
||||
*/
|
||||
export { CockpitHome, KnowledgeHome, MemPage } from './internal/panel.tsx';
|
||||
export { CockpitHome } from './internal/panel.tsx';
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
/**
|
||||
* feature `gov-memory` —— 运营中心的两处:**经营驾驶舱**(度量八指标)与**知识库**(活动记忆四层)。
|
||||
* feature `gov-memory` —— **经营驾驶舱**(运营首页:度量八指标 + 异常面)。
|
||||
*
|
||||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
*
|
||||
* 原本这两件同挂在「记忆管理」一区,是两种心智硬挤在一起:
|
||||
* 看指标问的是「今天跑得怎样」,管沉淀问的是「学到了什么、放不放行」。三面拆分后各自成页。
|
||||
* 「活动记忆」那一页 2026-07-29 撤销(用户定:运营中心删掉数据模块)——
|
||||
* 记忆的放行口还没成形,摆一个空区不如不摆;真要做时随「数据资产」一并重开。
|
||||
* 文件名仍叫 gov-memory:度量本就是留痕的投影,改名要动一串 import,等有第二处再说。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { TERMINAL_STATE_LABEL } from '@contracts';
|
||||
import type { TerminalState } from '@contracts';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import { Chip, Clear, Kpi, NList, NRow, Note, PageHead, QueryGate, Todo } from '@web-shared/gov/primitives.tsx';
|
||||
import { Clear, Kpi, Note, QueryGate, Todo } from '@web-shared/gov/primitives.tsx';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import {
|
||||
useGovActivities,
|
||||
useGovMemory,
|
||||
useGovMetrics,
|
||||
useGovTasks,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
import { useGovMetrics, useGovTasks } from '@web-shared/gov/gov-api.ts';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
@@ -165,76 +160,3 @@ export function CockpitHome({ nav }: HomeProps) {
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 知识库 ---------------- */
|
||||
|
||||
/** 活动记忆四层 + 候选增量的放行口 —— 记忆的终点是成为模型,那道门在这里 */
|
||||
export function KnowledgeHome({ nav }: HomeProps) {
|
||||
const activities = useGovActivities();
|
||||
const [picked, setPicked] = useState<string | undefined>(undefined);
|
||||
const memory = useGovMemory(picked);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="g-picker">
|
||||
{(activities.data?.activities ?? []).map((a) => (
|
||||
<button
|
||||
key={a.activityId}
|
||||
className={'g-pick' + (picked === a.activityId ? ' on' : '')}
|
||||
onClick={() => setPicked(a.activityId)}
|
||||
>
|
||||
{a.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<NList>
|
||||
{(memory.data?.entries ?? []).map((e) => (
|
||||
<NRow
|
||||
key={e.id}
|
||||
tone={e.incrementState === undefined ? 'brand' : 'warn'}
|
||||
ico="layers"
|
||||
title={e.statement}
|
||||
sub={`${e.layer} · ${e.createdAt.slice(0, 16).replace('T', ' ')}`}
|
||||
cols={[{ k: '证据', v: e.evidenceCount }]}
|
||||
right={
|
||||
e.incrementState !== undefined ? (
|
||||
<Chip tone="warn">候选 · {e.incrementState}</Chip>
|
||||
) : (
|
||||
<Chip tone="ok">已成条目</Chip>
|
||||
)
|
||||
}
|
||||
onClick={() => nav.push({ k: 'mem', id: e.id })}
|
||||
/>
|
||||
))}
|
||||
{picked === undefined && <Clear>选一个活动,看它这些年学到了什么。</Clear>}
|
||||
{picked !== undefined && (memory.data?.entries ?? []).length === 0 && (
|
||||
<Clear>这个活动还没有记忆条目 —— 任务落终态封卷、蒸馏钩子跑过之后,沉淀会出现在这里。</Clear>
|
||||
)}
|
||||
</NList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 记忆条目详情 ---------------- */
|
||||
|
||||
export function MemPage({ id }: { id: string }) {
|
||||
const activities = useGovActivities();
|
||||
const first = (activities.data?.activities ?? [])[0]?.activityId;
|
||||
const memory = useGovMemory(first);
|
||||
const e = (memory.data?.entries ?? []).find((x) => x.id === id);
|
||||
if (e === undefined) return <Note>记忆条目需从对应活动进入。</Note>;
|
||||
return (
|
||||
<PageHead
|
||||
ico="layers"
|
||||
kicker={`活动记忆 · ${e.layer}`}
|
||||
title={e.statement}
|
||||
chips={
|
||||
<>
|
||||
<Chip ico="link">{e.evidenceCount} 条证据</Chip>
|
||||
{e.incrementState !== undefined && <Chip tone="warn">候选增量 · {e.incrementState}</Chip>}
|
||||
</>
|
||||
}
|
||||
desc="写入唯蒸馏,条目必挂证据链;记忆不载权力——强制力只经候选增量 + 回放 + 人放行进入业务模型。"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { KeyboardEvent } from 'react';
|
||||
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||
import type { ClosureReport, ProcessPackage, ProcessNode } from '@contracts';
|
||||
import {
|
||||
deleteProcessFlowRoute,
|
||||
deleteProcessVersionRoute,
|
||||
processPackageSchema,
|
||||
editProcessVersionRoute,
|
||||
@@ -110,8 +111,6 @@ function OverviewHome() {
|
||||
unit="%"
|
||||
sub="不闭合不得激活"
|
||||
/>
|
||||
{/* 网关已废除:分支 = 条件出边,不再是一种要素 */}
|
||||
<Kpi ico="boxes" label="编排要素" value="四种" sub="活动 · 审批 · 触发器 · 流程包" />
|
||||
</div>
|
||||
</QueryGate>
|
||||
);
|
||||
@@ -151,6 +150,11 @@ function FlowShelf({ nav }: { nav: Nav }) {
|
||||
const [nfName, setNfName] = useState('');
|
||||
const [nfId, setNfId] = useState('');
|
||||
const [nfError, setNfError] = useState<string | undefined>(undefined);
|
||||
/** 卡上的 ⋯ 菜单:开在哪张卡上;`drop` = 那张卡的菜单已翻到删除确认 */
|
||||
const [menu, setMenu] = useState<{ id: string; drop: boolean } | null>(null);
|
||||
/** 就地改名:改的是卡面显示的**那一版**(名字是版本的属性) */
|
||||
const [renaming, setRenaming] = useState<string | null>(null);
|
||||
const [cardErr, setCardErr] = useState<string | undefined>(undefined);
|
||||
// 标识进 URL 和触发器规则里引用,所以收紧:小写字母开头,只许小写/数字/连字符
|
||||
const idOk = /^[a-z][a-z0-9-]{1,40}$/.test(nfId);
|
||||
const idTaken = flows.some((f) => f.id === nfId);
|
||||
@@ -180,13 +184,41 @@ function FlowShelf({ nav }: { nav: Nav }) {
|
||||
onError: (e: unknown) => setNfError((e as { message?: string }).message ?? '建流程失败'),
|
||||
});
|
||||
|
||||
/** 卡上改名 —— 落在卡面那一版上(整条流的名字就是它生效版的名字) */
|
||||
const rename = useMutation({
|
||||
mutationFn: (input: { id: string; version: number; name: string }) =>
|
||||
call<{ packages: ProcessPackage[] }>(editProcessVersionRoute, {
|
||||
params: { id: input.id, version: String(input.version) },
|
||||
body: { name: input.name },
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
|
||||
setRenaming(null);
|
||||
},
|
||||
onError: (e: unknown) => setCardErr((e as { message?: string }).message ?? '改名失败'),
|
||||
});
|
||||
|
||||
/** 删整条流 —— 在途单钉着、还有调起规则指着,服务端都会挡回来(409),判词照抄 */
|
||||
const dropFlow = useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
call<{ packages: ProcessPackage[] }>(deleteProcessFlowRoute, { params: { id } }),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
|
||||
setMenu(null);
|
||||
},
|
||||
onError: (e: unknown) => {
|
||||
setCardErr((e as { message?: string }).message ?? '删不掉');
|
||||
setMenu(null);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
{creating ? (
|
||||
{creating && (
|
||||
<Card ico="workflow" title="新建流程" extra="建出来就是 v1 并即刻生效 —— 一张「开始 → 结束」的最小图,进详情再画">
|
||||
<div className="g-newflow">
|
||||
<Field k="流程名称" hint="人读的名字,如「统一报价」">
|
||||
<input className="g-input" value={nfName} placeholder="如:统一报价" onChange={(e) => setNfName(e.target.value)} />
|
||||
<input className="g-input" autoFocus value={nfName} placeholder="如:统一报价" onChange={(e) => setNfName(e.target.value)} />
|
||||
</Field>
|
||||
<Field k="标识" hint="进 URL 与触发器规则里引用 —— 小写字母开头,只许小写 / 数字 / 连字符">
|
||||
<input
|
||||
@@ -214,9 +246,8 @@ function FlowShelf({ nav }: { nav: Nav }) {
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Acts items={[{ label: '新建流程', ico: 'plus', primary: true, onClick: () => setCreating(true) }]} />
|
||||
)}
|
||||
{cardErr !== undefined && <Note tone="warn">{cardErr}</Note>}
|
||||
|
||||
<div className="g-shelf">
|
||||
{flows.map((f) => {
|
||||
@@ -227,50 +258,110 @@ function FlowShelf({ nav }: { nav: Nav }) {
|
||||
activity: shown.nodes.filter((n) => n.kind === 'activity').length,
|
||||
approval: shown.nodes.filter((n) => n.kind === 'approval').length,
|
||||
};
|
||||
const open = menu?.id === f.id;
|
||||
return (
|
||||
<button
|
||||
key={f.id}
|
||||
className={'g-tile t-' + (f.active !== undefined ? 'ok' : 'mute')}
|
||||
onClick={() => nav.push({ k: 'pkg', id: f.id })}
|
||||
>
|
||||
<span className="g-tile-h">
|
||||
<span className="g-tile-i">
|
||||
<Ico name="workflow" s={16} />
|
||||
/*
|
||||
* 卡是个 div,整面盖一枚透明的「进这条流」按钮 —— 卡里还要住 ⋯ 菜单
|
||||
* 与改名输入框,按钮套按钮是非法 DOM。字面部分不吃点击,直接穿到底下那枚。
|
||||
*/
|
||||
<div key={f.id} className={'g-fcard' + (f.active === undefined ? ' off' : '')}>
|
||||
<button className="hit" aria-label={`打开 ${shown.name}`} onClick={() => nav.push({ k: 'pkg', id: f.id })} />
|
||||
<div className="hd">
|
||||
<span className="ico">
|
||||
<Ico name="workflow" s={15} />
|
||||
</span>
|
||||
<span className="g-tile-t">
|
||||
<b>{shown.name}</b>
|
||||
<div className="ttl">
|
||||
{renaming === f.id ? (
|
||||
<input
|
||||
className="g-input"
|
||||
autoFocus
|
||||
maxLength={40}
|
||||
defaultValue={shown.name}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onBlur={() => setRenaming(null)}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
const val = (e.target as HTMLInputElement).value.trim();
|
||||
if (e.key === 'Enter' && val !== '') rename.mutate({ id: f.id, version: shown.version, name: val });
|
||||
if (e.key === 'Escape') setRenaming(null);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<b>{shown.name}</b>
|
||||
)}
|
||||
<em>{f.id}</em>
|
||||
</div>
|
||||
<span className={'stx' + (f.active === undefined ? '' : ' on')}>
|
||||
{f.active === undefined ? '已下线' : `生效 v${f.active.version}`}
|
||||
</span>
|
||||
{f.active !== undefined ? (
|
||||
<St tone="ok" ico="circle-check">
|
||||
生效 v{f.active.version}
|
||||
</St>
|
||||
) : (
|
||||
<St tone="warn" ico="pause">
|
||||
已下线
|
||||
</St>
|
||||
)}
|
||||
</span>
|
||||
<span className="g-tile-m">
|
||||
<Chip tone="auto" ico="git-commit">
|
||||
活动 {c.activity}
|
||||
</Chip>
|
||||
<Chip tone="warn" ico="shield-check">
|
||||
审批 {c.approval}
|
||||
</Chip>
|
||||
<Chip tone="mute" ico="git-branch">
|
||||
边 {shown.edges.length}
|
||||
</Chip>
|
||||
</span>
|
||||
<span className="g-tile-f">
|
||||
<St tone={ec.closed === ec.total ? 'ok' : 'danger'} ico={ec.closed === ec.total ? 'circle-check' : 'alert-circle'}>
|
||||
边 {ec.closed}/{ec.total}
|
||||
</St>
|
||||
<span className="r">{f.versions.length} 个版本</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={'mbtn' + (open ? ' on' : '')}
|
||||
aria-label="更多"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCardErr(undefined);
|
||||
setMenu(open ? null : { id: f.id, drop: false });
|
||||
}}
|
||||
>
|
||||
<Ico name="more-horizontal" s={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/* 数据板:四格等宽、竖线分隔 —— 一行说完这条流有多大、闭合没有 */}
|
||||
<div className="plate">
|
||||
<span>
|
||||
<b>{c.activity}</b>
|
||||
<i>活动</i>
|
||||
</span>
|
||||
<span>
|
||||
<b>{c.approval}</b>
|
||||
<i>审批</i>
|
||||
</span>
|
||||
<span className={ec.closed === ec.total ? '' : 'bad'}>
|
||||
<b>
|
||||
{ec.closed}/{ec.total}
|
||||
</b>
|
||||
<i>边闭合</i>
|
||||
</span>
|
||||
<span>
|
||||
<b>{f.versions.length}</b>
|
||||
<i>版本</i>
|
||||
</span>
|
||||
</div>
|
||||
{open && (
|
||||
<div className="menu">
|
||||
{menu.drop ? (
|
||||
<>
|
||||
<span className="q">删掉整条流?{f.versions.length} 个版本一起没</span>
|
||||
<button onClick={() => setMenu({ id: f.id, drop: false })}>算了</button>
|
||||
<button className="bad" disabled={dropFlow.isPending} onClick={() => dropFlow.mutate(f.id)}>
|
||||
删除
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setRenaming(f.id);
|
||||
setMenu(null);
|
||||
}}
|
||||
>
|
||||
<Ico name="pen-line" s={12} /> 编辑
|
||||
</button>
|
||||
<button className="bad" onClick={() => setMenu({ id: f.id, drop: true })}>
|
||||
<Ico name="trash-2" s={12} /> 删除
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* 幽灵卡:新建就住货架里 —— 与真卡同一尺寸,一眼看出「这儿还能再添一条」 */}
|
||||
<button className="g-fcard ghost" onClick={() => setCreating(true)}>
|
||||
<Ico name="plus" s={18} />
|
||||
新建流程
|
||||
</button>
|
||||
</div>
|
||||
</QueryGate>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user