diff --git a/apps/web/src/features/gov-process/internal/panel.tsx b/apps/web/src/features/gov-process/internal/panel.tsx
index 1bb5c97..1e9c871 100644
--- a/apps/web/src/features/gov-process/internal/panel.tsx
+++ b/apps/web/src/features/gov-process/internal/panel.tsx
@@ -41,7 +41,7 @@ import { packageToGraph } from '@web-shared/gov/flow/pkg-to-graph.ts';
import { mergeGraphIntoPackage } from '@web-shared/gov/flow/graph-to-pkg.ts';
import type { FlowGraph, FlowNode } from '@web-shared/gov/flow/flow-layout.ts';
import type { SpPosition } from '@web-shared/gov/flow/support.tsx';
-import { TriggersHome } from '@features/gov-trigger';
+import { TriggerRulesHome } from '@features/gov-trigger';
export type HomeProps = { tab: string; nav: Nav };
@@ -60,7 +60,7 @@ function edgeCheckOf(pkg: ProcessPackage, report: ClosureReport | undefined) {
export function WorkflowsHome({ tab, nav }: HomeProps) {
// 调起规则 = 触发器的**定义面**:谁调起这条流,与流程包一处维护(启停在运营中心)
- if (tab === 'rules') return ;
+ if (tab === 'rules') return ;
return ;
}
@@ -825,12 +825,26 @@ function Orchestrator({
title={t.name}
sub={t.spec.kind === 'event' ? t.spec.rule : t.spec.kind === 'registration' ? TRIGGER_KIND_LABEL.registration : TRIGGER_KIND_LABEL.periodic}
cols={[{ k: '状态', v: t.enabled ? '已启用' : '已停用' }]}
+ right={
+
+ }
onClick={() => nav.jump('workflows', 'rules', { k: 'trigger', id: t.id })}
/>
))}
) : (
- 暂无触发器绑定 —— 该版本只能由已绑定的旧版本或登记起单。
+
+ 还没有触发器指向这条流 —— 它定义得再完整,也没人调得起来。去
+ 调起规则页签配一条(周期 / 事件 / 登记三选一)。
+
)}
>
diff --git a/apps/web/src/features/gov-task/internal/panel.tsx b/apps/web/src/features/gov-task/internal/panel.tsx
index 1e59ed1..c2e7a4d 100644
--- a/apps/web/src/features/gov-task/internal/panel.tsx
+++ b/apps/web/src/features/gov-task/internal/panel.tsx
@@ -23,6 +23,7 @@ import { Ava, Card, Chip, Clear, Entry, Facts, Kpi, NList, NRow, Note, PageHead,
import type { ActSpec } from '@web-shared/gov/primitives.tsx';
import { APR_ST, PARA, RUNTIME_ST, TERMINAL_ST, WF_ST } from '@web-shared/gov/tone.ts';
import type { Nav } from '@web-shared/gov/gov-route.ts';
+import { TriggerOpsTab } from '@features/gov-trigger';
import { FlowFigure } from '@web-shared/gov/flow/FlowFigure.tsx';
import {
useGovApprovals,
@@ -35,6 +36,12 @@ import {
export type HomeProps = { tab: string; nav: Nav };
export function TasksHome({ tab, nav }: HomeProps) {
+ // 起单开关 = 触发器的**处置面**:今天开不开是当班主管的事,规则怎么写在设计中心改
+ if (tab === 'trg') return ;
+ return ;
+}
+
+function RuntimeHome({ tab, nav }: HomeProps) {
const q = useGovTasks();
const approvals = useGovApprovals();
const tasks = q.data?.tasks ?? [];
diff --git a/apps/web/src/features/gov-trigger/index.ts b/apps/web/src/features/gov-trigger/index.ts
index 6dde48c..5023284 100644
--- a/apps/web/src/features/gov-trigger/index.ts
+++ b/apps/web/src/features/gov-trigger/index.ts
@@ -1,5 +1,5 @@
/**
* feature `gov-trigger` 唯一出口(编码规范 §9.1)。
- * 触发器配置(周期 / 事件 / 登记三类):与流程包平级,启停不改任何版本
+ * 触发器的两副面孔:规则定义在设计中心(流程包 - 调起规则),启停在运营中心(任务 - 起单开关)。
*/
-export { TriggersHome, TriggerPage } from './internal/panel.tsx';
+export { TriggerOpsTab, TriggerPage, TriggerRulesHome } from './internal/panel.tsx';
diff --git a/apps/web/src/features/gov-trigger/internal/panel.tsx b/apps/web/src/features/gov-trigger/internal/panel.tsx
index 415449a..2f68f6b 100644
--- a/apps/web/src/features/gov-trigger/internal/panel.tsx
+++ b/apps/web/src/features/gov-trigger/internal/panel.tsx
@@ -1,62 +1,49 @@
/**
- * feature `gov-trigger` —— 触发器配置(周期 / 事件 / 登记三类):与流程包平级,启停不改任何版本
+ * feature `gov-trigger` —— 触发器的两副面孔,各归其台:
+ *
+ * `TriggerRulesHome`(设计中心 · 流程包 - 调起规则):**定义面** —— 谁按什么规则调起这条流,
+ * 起单载荷怎么装配。与流程包一处维护:「谁调起它」本就是流程定义的一部分。
+ * `TriggerOpsTab`(运营中心 · 任务 - 起单开关):**处置面** —— 今天开不开。
+ *
+ * 同一个对象两种动作各归其位:定义是设计者的事,启停是当班主管的事。
+ * 触发器与流程包**平级** —— 不入包结构、不随包版本,增删启停改定义都不动任何版本。
*
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
- * 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
*/
+import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
+import type { TriggerConfig } from '@contracts';
import {
TRIGGER_KIND_LABEL,
+ processPackageIdSchema,
toggleTriggerRoute,
triggerKindOf,
+ upsertTriggerRoute,
} from '@contracts';
import { call } from '@web-shared/typed-fetch.ts';
-import { Card, Chip, NList, NRow, Note, PageHead, QueryGate, Rows, St } from '@web-shared/gov/primitives.tsx';
-import type { Nav } from '@web-shared/gov/gov-route.ts';
import {
- useGovTriggers,
-} from '@web-shared/gov/gov-api.ts';
+ Acts,
+ Card,
+ Chip,
+ Clear,
+ Field,
+ ListEdit,
+ NList,
+ NRow,
+ Note,
+ PageHead,
+ Prop,
+ QueryGate,
+ Rows,
+ Seg,
+ St,
+} from '@web-shared/gov/primitives.tsx';
+import type { Nav } from '@web-shared/gov/gov-route.ts';
+import { useGovObjects, useGovPackages, useGovTriggers } from '@web-shared/gov/gov-api.ts';
export type HomeProps = { tab: string; nav: Nav };
-export function TriggersHome({ nav }: HomeProps) {
- const q = useGovTriggers();
- const triggers = q.data?.triggers ?? [];
- return (
-
- <>
-
- 触发器与流程包平级——不入包结构、不随包版本,增删启停不改任何版本。周期 / 事件 / 登记三类。
-
-
- {triggers.map((t) => {
- const kind = triggerKindOf(t.spec);
- return (
- {TRIGGER_KIND_LABEL[kind]} }]}
- right={
-
- {t.enabled ? '已启用' : '已停用'}
-
- }
- onClick={() => nav.push({ k: 'trigger', id: t.id })}
- />
- );
- })}
-
- >
-
- );
-}
-
-/* ---------------- ⑤ 活动库管理 ---------------- */
-
/** 治理写动作统一入口:成功即失效相关查询,画面由服务端重投影 */
function useGovAction(mutate: (input: TInput) => Promise, keys: readonly string[]) {
const queryClient = useQueryClient();
@@ -68,7 +55,376 @@ function useGovAction(mutate: (input: TInput) => Promise, keys:
});
}
-/* ---------------- 工作流 ---------------- */
+/** 规则原文:一行说清「什么时候起」 */
+function ruleText(t: TriggerConfig): string {
+ switch (t.spec.kind) {
+ case 'periodic-calendar':
+ return `按日历 ${t.spec.calendar}`;
+ case 'periodic-data':
+ return `按 ${t.spec.domain}.${t.spec.object} 的 ${t.spec.dateField} ${t.spec.offset}${t.spec.filter === undefined ? '' : ` 且 ${t.spec.filter}`}`;
+ case 'event':
+ return `事件(${t.spec.eventType})命中:${t.spec.rule}`;
+ case 'registration':
+ return '人工登记即起单';
+ }
+}
+
+/* ==================== 设计侧:调起规则(定义面) ==================== */
+
+const SPEC_KINDS = [
+ { k: 'registration', label: '登记' },
+ { k: 'event', label: '事件' },
+ { k: 'periodic-calendar', label: '周期 · 日历' },
+ { k: 'periodic-data', label: '周期 · 数据日历' },
+] as const;
+
+export function TriggerRulesHome({ nav }: { nav: Nav }) {
+ const q = useGovTriggers();
+ const [editing, setEditing] = useState(undefined);
+ const triggers = q.data?.triggers ?? [];
+
+ return (
+
+ <>
+
+ 调起规则决定什么时候起一单:周期 / 事件 / 登记三类。触发器与流程包平级——不入包结构、
+ 不随包版本,改规则不动任何版本。启停不在这里,那是运营中心的事。
+
+ {triggers.length === 0 ? (
+ 还没有调起规则 —— 配一条,这条流就能被自动或人工调起来。
+ ) : (
+
+ {triggers.map((t) =>
+ editing === t.id ? (
+ setEditing(undefined)}
+ onCancel={() => setEditing(undefined)}
+ />
+ ) : (
+ {TRIGGER_KIND_LABEL[triggerKindOf(t.spec)]} },
+ { k: '调起', v: t.processPackageId },
+ { k: '装配', v: `${t.mapping.length} 条` },
+ ]}
+ right={
+
+ }
+ onClick={() => nav.push({ k: 'trigger', id: t.id })}
+ />
+ ),
+ )}
+
+ )}
+ >
+
+ );
+}
+
+/**
+ * 行内编辑表单 —— 改名 / 改目标包 / 改规则 / 改起单装配。
+ *
+ * 起单装配这里只编辑 `initiation` 一种来源(起单载荷 → 调用接口字段):
+ * 触发器只知道调用接口、不知流程内部,别的来源(上游输出口、工作流变量)是流程包边映射的事。
+ * 写成 `目标字段 = 载荷路径` 一行一条 —— 与边映射页的读法一致。
+ */
+function TriggerForm({
+ trigger,
+ onDone,
+ onCancel,
+}: {
+ trigger: TriggerConfig;
+ onDone: () => void;
+ onCancel: () => void;
+}) {
+ const packages = useGovPackages();
+ const objects = useGovObjects();
+ const [draft, setDraft] = useState(trigger);
+
+ const save = useGovAction(
+ (next: TriggerConfig) => call(upsertTriggerRoute, { body: { trigger: next } }),
+ ['triggers'],
+ );
+
+ const packageIds = [...new Set((packages.data?.packages ?? []).map((p) => p.id))];
+ const domains = [...new Set((objects.data?.objects ?? []).map((o) => o.domain))];
+ const objectsOfDomain = (domain: string) =>
+ (objects.data?.objects ?? []).filter((o) => o.domain === domain).map((o) => o.object);
+
+ /**
+ * 起单装配的行式写法:`目标字段 = 载荷路径`。
+ *
+ * 编辑期这些行是**本地状态**,不从 `draft.mapping` 派生 ——
+ * 派生的话,刚点「添加」的空行会被解析器当场滤掉,人永远打不出第一个字(实测)。
+ * 解析只在存盘那一刻做。非 initiation 来源不在此面编辑,原样带着走。
+ */
+ const [lines, setLines] = useState(() =>
+ trigger.mapping.flatMap((m) => (m.source.from === 'initiation' ? [`${m.target} = ${m.source.path}`] : [])),
+ );
+ const foreignMappings = trigger.mapping.filter((m) => m.source.from !== 'initiation');
+
+ /** 行 → 映射:写不成 `目标 = 路径` 的行直接丢掉,不塞半条进契约 */
+ const parseLines = (): TriggerConfig['mapping'] => [
+ ...lines.flatMap((line) => {
+ const at = line.indexOf('=');
+ if (at <= 0) return [];
+ const target = line.slice(0, at).trim();
+ const path = line.slice(at + 1).trim();
+ if (target === '' || path === '') return [];
+ return [{ target, source: { from: 'initiation' as const, path } }];
+ }),
+ ...foreignMappings,
+ ];
+
+ const setSpecKind = (k: string) => {
+ if (k === draft.spec.kind) return;
+ const spec: TriggerConfig['spec'] =
+ k === 'event'
+ ? { kind: 'event', rule: 'true', eventType: 'data' }
+ : k === 'periodic-calendar'
+ ? { kind: 'periodic-calendar', calendar: '0 9 * * 1' }
+ : k === 'periodic-data'
+ ? {
+ kind: 'periodic-data',
+ domain: domains[0] ?? 'shared',
+ object: objectsOfDomain(domains[0] ?? 'shared')[0] ?? 'record',
+ dateField: 'dueDate',
+ offset: '-P3D',
+ }
+ : { kind: 'registration' };
+ setDraft({ ...draft, spec });
+ };
+
+ return (
+
+
+
+ setDraft({ ...draft, name: e.target.value })} />
+
+
+
+
+
+
+
+
+ ({ k: s.k, label: s.label }))} onPick={setSpecKind} />
+
+ {draft.spec.kind === 'registration' && (
+ 登记型无参:人在工作台按调用接口的 schema 填表提交,即起一单。
+ )}
+ {draft.spec.kind === 'event' && (
+ <>
+
+ {
+ if (draft.spec.kind !== 'event') return;
+ if (k !== 'data' && k !== 'time' && k !== 'human' && k !== 'control') return;
+ setDraft({ ...draft, spec: { ...draft.spec, eventType: k } });
+ }}
+ />
+
+
+
+ draft.spec.kind === 'event' && setDraft({ ...draft, spec: { ...draft.spec, rule: e.target.value } })
+ }
+ />
+
+ >
+ )}
+ {draft.spec.kind === 'periodic-calendar' && (
+
+
+ draft.spec.kind === 'periodic-calendar' &&
+ setDraft({ ...draft, spec: { ...draft.spec, calendar: e.target.value } })
+ }
+ />
+
+ )}
+ {draft.spec.kind === 'periodic-data' && (
+ <>
+
+
+
+
+
+ draft.spec.kind === 'periodic-data' &&
+ setDraft({ ...draft, spec: { ...draft.spec, dateField: e.target.value } })
+ }
+ />
+
+
+
+ draft.spec.kind === 'periodic-data' &&
+ setDraft({ ...draft, spec: { ...draft.spec, offset: e.target.value } })
+ }
+ />
+
+
+ {
+ if (draft.spec.kind !== 'periodic-data') return;
+ const filter = e.target.value.trim();
+ const { filter: _drop, ...rest } = draft.spec;
+ setDraft({ ...draft, spec: filter === '' ? rest : { ...rest, filter } });
+ }}
+ />
+
+ >
+ )}
+
+
+
+
+
+
+ {foreignMappings.length > 0 && (
+
+ 另有 {foreignMappings.length} 条非起单来源的映射(上游输出口 / 工作流变量),
+ 那是流程包边映射的事 —— 在流程包详情的边映射页维护,这里原样保留不动。
+
+ )}
+
+
+ 触发器与流程包平级 —— 改规则不动任何流程包版本,已起的单照常跑完。,
+ impact: {
+ who: '此后命中新规则才起单;在途工作流不受影响',
+ undo: '可再改;要临时不起单,去运营中心停用',
+ when: '确认后即时生效',
+ },
+ okLabel: '确认保存',
+ onOk: () => save.mutate({ ...draft, mapping: parseLines() }, { onSuccess: onDone }),
+ },
+ },
+ ]}
+ />
+
+ );
+}
+
+/* ==================== 运营侧:起单开关(处置面) ==================== */
+
+export function TriggerOpsTab({ nav }: { nav: Nav }) {
+ const q = useGovTriggers();
+ const triggers = q.data?.triggers ?? [];
+ const toggle = useGovAction(
+ (input: { id: string; enabled: boolean }) =>
+ call(toggleTriggerRoute, { params: { id: input.id }, body: { enabled: input.enabled } }),
+ ['triggers'],
+ );
+
+ return (
+
+ <>
+
+ 开关管的是今天起不起单,不是规则本身。停用后此后不再起新单,已起的照常跑完;
+ 规则怎么写在设计中心 · 流程包 - 调起规则里改。
+
+ {triggers.length === 0 ? (
+ 还没有调起规则 —— 设计中心配好之后,开关会出现在这里。
+ ) : (
+
+ {triggers.map((t) => (
+ {TRIGGER_KIND_LABEL[triggerKindOf(t.spec)]} },
+ { k: '调起', v: t.processPackageId },
+ ]}
+ right={
+
+ {t.enabled ? '已启用' : '已停用'}
+
+ }
+ onClick={() => nav.push({ k: 'trigger', id: t.id })}
+ />
+ ))}
+
+ )}
+ 逐条启停在触发器详情页做 —— 开关是治理动作,要走影响确认带,不做列表上的顺手一点。
+ {toggle.isError && 切换失败,请重试。}
+ >
+
+ );
+}
+
+/* ==================== 触发器详情(两面共用) ==================== */
export function TriggerPage({ id }: { id: string }) {
const q = useGovTriggers();
@@ -91,7 +447,7 @@ export function TriggerPage({ id }: { id: string }) {
调起 {t.processPackageId}
>
}
- desc="触发器与流程包平级——不入包结构、不随包版本,增删启停不改任何版本。"
+ desc="触发器与流程包平级——不入包结构、不随包版本,增删启停不改任何版本。规则在设计中心改,开关在运营中心拨。"
acts={[
{
label: t.enabled ? '停用' : '启用',
@@ -115,6 +471,7 @@ export function TriggerPage({ id }: { id: string }) {
);
}
-
-/* ---------------- 活动 / 工具 ---------------- */
-