Archived
进度暂存:启动抽屉加固定参数「责任人」(名册下拉、默认当前登录人);接口文档 §5 加 ownerId 固定字段;需求文档记 2026-08-27 两条决策
CI / ci (push) Failing after 10s
CI / ci (push) Failing after 10s
This commit is contained in:
@@ -7,17 +7,18 @@
|
|||||||
*/
|
*/
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
CONTROL_SEMANTIC_LABEL,
|
CONTROL_SEMANTIC_LABEL,
|
||||||
TASK_RUNTIME_STATE_LABEL,
|
TASK_RUNTIME_STATE_LABEL,
|
||||||
TERMINAL_STATE_LABEL,
|
TERMINAL_STATE_LABEL,
|
||||||
WORKFLOW_STATE_LABEL,
|
WORKFLOW_STATE_LABEL,
|
||||||
controlTaskRoute,
|
controlTaskRoute,
|
||||||
|
meRoute,
|
||||||
reassignTaskRoute,
|
reassignTaskRoute,
|
||||||
resumeTaskRoute,
|
resumeTaskRoute,
|
||||||
} from '@contracts';
|
} from '@contracts';
|
||||||
import type { ControlSemantic } from '@contracts';
|
import type { ControlSemantic, Principal } from '@contracts';
|
||||||
import { Ico } from '@web-shared/Ico.tsx';
|
import { Ico } from '@web-shared/Ico.tsx';
|
||||||
import { call } from '@web-shared/typed-fetch.ts';
|
import { call } from '@web-shared/typed-fetch.ts';
|
||||||
import { Ava, Card, Chip, Clear, Entry, Facts, NList, NRow, Note, PageHead, Sec } from '@web-shared/gov/primitives.tsx';
|
import { Ava, Card, Chip, Clear, Entry, Facts, NList, NRow, Note, PageHead, Sec } from '@web-shared/gov/primitives.tsx';
|
||||||
@@ -27,6 +28,7 @@ import type { Nav } from '@web-shared/gov/gov-route.ts';
|
|||||||
import { FlowFigure } from '@web-shared/gov/flow/FlowFigure.tsx';
|
import { FlowFigure } from '@web-shared/gov/flow/FlowFigure.tsx';
|
||||||
import { atFull, atMinute } from '@web-shared/at.ts';
|
import { atFull, atMinute } from '@web-shared/at.ts';
|
||||||
import { Btn } from '@web-shared/Btn.tsx';
|
import { Btn } from '@web-shared/Btn.tsx';
|
||||||
|
import { queryKeys } from '@web-shared/query-keys.ts';
|
||||||
import { useGovStaff, useGovTaskLog, useGovTasks, useGovWorkflow } from '@web-shared/gov/gov-api.ts';
|
import { useGovStaff, useGovTaskLog, useGovTasks, useGovWorkflow } from '@web-shared/gov/gov-api.ts';
|
||||||
|
|
||||||
export type HomeProps = { tab: string; nav: Nav };
|
export type HomeProps = { tab: string; nav: Nav };
|
||||||
@@ -447,8 +449,16 @@ function ComboInput({ p, value, onChange }: { p: LaunchParam; value: string; onC
|
|||||||
* 挂 body portal:详情页容器带入场动画 transform,fixed 抽屉不 portal 会被钉进卡片盒子里(2026-07-30 实测);
|
* 挂 body portal:详情页容器带入场动画 transform,fixed 抽屉不 portal 会被钉进卡片盒子里(2026-07-30 实测);
|
||||||
* `gov-root` 类必须跟着挂:设计令牌定义在它上面,出了作用域面板背景会变透明。
|
* `gov-root` 类必须跟着挂:设计令牌定义在它上面,出了作用域面板背景会变透明。
|
||||||
*/
|
*/
|
||||||
function LaunchDrawer({ name, spec, onCancel, onLaunch }: { name: string; spec: readonly LaunchParam[]; onCancel: () => void; onLaunch: (values: LaunchValues) => void }) {
|
function LaunchDrawer({ name, spec, onCancel, onLaunch }: { name: string; spec: readonly LaunchParam[]; onCancel: () => void; onLaunch: (values: LaunchValues, owner: { id: string; name: string }) => void }) {
|
||||||
const [values, setValues] = useState<LaunchValues>(() => initLaunchValues(spec));
|
const [values, setValues] = useState<LaunchValues>(() => initLaunchValues(spec));
|
||||||
|
/* 责任人(2026-08-27 用户定):**固定参数,每个活动都要**,不进按活动下发的参数表 ——
|
||||||
|
候选 = 员工名册(GET /api/gov/staff,真接口),默认 = 当前登录人;只列在职的 */
|
||||||
|
const staffQ = useGovStaff();
|
||||||
|
const meQ = useQuery({ queryKey: queryKeys.me, queryFn: () => call<Principal>(meRoute) });
|
||||||
|
const staff = (staffQ.data?.staff ?? []).filter((s) => s.state === 'active');
|
||||||
|
const meId = meQ.data?.staff.id;
|
||||||
|
const [pickedOwner, setPickedOwner] = useState<string | undefined>(undefined);
|
||||||
|
const ownerId = pickedOwner ?? meId ?? '';
|
||||||
/* Esc 关抽屉 —— 抽屉盖住整屏,不给键盘出路是把人关在里面 */
|
/* Esc 关抽屉 —— 抽屉盖住整屏,不给键盘出路是把人关在里面 */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
@@ -459,7 +469,7 @@ function LaunchDrawer({ name, spec, onCancel, onLaunch }: { name: string; spec:
|
|||||||
}, [onCancel]);
|
}, [onCancel]);
|
||||||
|
|
||||||
const setVal = (key: string, v: string | string[]) => setValues((cur) => ({ ...cur, [key]: v }));
|
const setVal = (key: string, v: string | string[]) => setValues((cur) => ({ ...cur, [key]: v }));
|
||||||
const invalid = spec.some((p) => p.required === true && !paramFilled(p, values[p.key] ?? ''));
|
const invalid = ownerId === '' || spec.some((p) => p.required === true && !paramFilled(p, values[p.key] ?? ''));
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className="gov-root g-drawer g-launch" role="dialog" aria-modal="true" aria-label={`启动活动 · ${name}`}>
|
<div className="gov-root g-drawer g-launch" role="dialog" aria-modal="true" aria-label={`启动活动 · ${name}`}>
|
||||||
@@ -476,6 +486,25 @@ function LaunchDrawer({ name, spec, onCancel, onLaunch }: { name: string; spec:
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="g-drawer-b">
|
<div className="g-drawer-b">
|
||||||
|
{/* 固定参数(2026-08-27 用户定):责任人 —— 每个活动启动都要,与按活动下发的业务参数分开摆 */}
|
||||||
|
<section>
|
||||||
|
<h4>固定参数</h4>
|
||||||
|
<label className="g-fld">
|
||||||
|
<span>
|
||||||
|
责任人<i className="req">*</i>
|
||||||
|
</span>
|
||||||
|
<select className="g-select" value={ownerId} onChange={(e) => setPickedOwner(e.target.value)}>
|
||||||
|
<option value="">请选择…</option>
|
||||||
|
{staff.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.name}
|
||||||
|
{s.id === meId ? '(我自己)' : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<small>这次启动的实例归他 —— 负责人验收、数据工单都落到责任人身上;默认是你自己。</small>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h4>启动参数</h4>
|
<h4>启动参数</h4>
|
||||||
{spec.map((p) => (
|
{spec.map((p) => (
|
||||||
@@ -539,7 +568,11 @@ function LaunchDrawer({ name, spec, onCancel, onLaunch }: { name: string; spec:
|
|||||||
<Btn className="g-btn" onClick={onCancel}>
|
<Btn className="g-btn" onClick={onCancel}>
|
||||||
取消
|
取消
|
||||||
</Btn>
|
</Btn>
|
||||||
<Btn className="g-btn primary" disabled={invalid} onClick={() => onLaunch(values)}>
|
<Btn
|
||||||
|
className="g-btn primary"
|
||||||
|
disabled={invalid}
|
||||||
|
onClick={() => onLaunch(values, { id: ownerId, name: staff.find((s) => s.id === ownerId)?.name ?? '' })}
|
||||||
|
>
|
||||||
<Ico name="play" s={12} /> 启动活动
|
<Ico name="play" s={12} /> 启动活动
|
||||||
</Btn>
|
</Btn>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -619,16 +652,16 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
|||||||
setPage(0);
|
setPage(0);
|
||||||
};
|
};
|
||||||
/** 启动提交(2026-08-27):启动接口未接 —— 先在清单头落一条「待执行」演示全链路;
|
/** 启动提交(2026-08-27):启动接口未接 —— 先在清单头落一条「待执行」演示全链路;
|
||||||
输入摘要取第一个填了值的参数,让人认得出这是刚启动的那条 */
|
输入摘要取第一个填了值的参数,让人认得出这是刚启动的那条;负责人 = 抽屉里选的责任人 */
|
||||||
const launchSpec = LAUNCH_PARAMS[a.id] ?? LAUNCH_PARAMS_FALLBACK;
|
const launchSpec = LAUNCH_PARAMS[a.id] ?? LAUNCH_PARAMS_FALLBACK;
|
||||||
const onLaunch = (values: LaunchValues): void => {
|
const onLaunch = (values: LaunchValues, owner: { id: string; name: string }): void => {
|
||||||
const first = launchSpec.map((p) => values[p.key]).flat().find((v): v is string => typeof v === 'string' && v.trim() !== '');
|
const first = launchSpec.map((p) => values[p.key]).flat().find((v): v is string => typeof v === 'string' && v.trim() !== '');
|
||||||
setRows((cur) => [
|
setRows((cur) => [
|
||||||
{
|
{
|
||||||
id: `INS-${2418 + launchN}`,
|
id: `INS-${2418 + launchN}`,
|
||||||
input: `${(first ?? '手动启动').trim().slice(0, 24)} · ${launchSpec.length} 项参数`,
|
input: `${(first ?? '手动启动').trim().slice(0, 24)} · ${launchSpec.length} 项参数`,
|
||||||
state: '待执行' as InstanceState,
|
state: '待执行' as InstanceState,
|
||||||
owner: a.owner,
|
owner: owner.name === '' ? a.owner : owner.name,
|
||||||
startedAt: fmtMinute(Date.now()),
|
startedAt: fmtMinute(Date.now()),
|
||||||
output: '—',
|
output: '—',
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-1
@@ -203,6 +203,7 @@ iframe 带不了 Authorization 头,走票据(`GET /api/auth/ticket`,ADR-012 §6)
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"idempotencyKey": "01J9…",
|
"idempotencyKey": "01J9…",
|
||||||
|
"ownerId": "8f3c1a2b-6c1e-4b3a-9f0e-2d5c7a1b9e44",
|
||||||
"params": {
|
"params": {
|
||||||
"customer_id": "cust-itelma",
|
"customer_id": "cust-itelma",
|
||||||
"request_description": "车身控制器询价 · 3 项物料,期望 9 月交期",
|
"request_description": "车身控制器询价 · 3 项物料,期望 9 月交期",
|
||||||
@@ -212,6 +213,9 @@ iframe 带不了 Authorization 头,走票据(`GET /api/auth/ticket`,ADR-012 §6)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**`ownerId`(必填,2026-08-27 用户定)是启动契约的固定字段,不进 §4 参数表** —— 每个活动启动都要指定这次实例的**责任人**:负责人验收、存在性检查派出的数据工单都落到责任人身上。候选名单 = 既有 `GET /api/gov/staff`(前端只列 `state='active'` 的);前端默认填当前登录人,允许改。服务端要验:ownerId 是名册里在职的人(400),不验「是不是启动人自己」(代别人启动是合法场景)。
|
||||||
|
启动成功后,实例视图(§3/§6)里的 `owner` 就是它。
|
||||||
|
|
||||||
→ 200 一条实例详情(同 §6)。**同一 `idempotencyKey` 重放返回第一次创建的实例,不起第二个**(前端每开一次抽屉生成一枚;重复点提交、网络重试都靠它挡住)。
|
→ 200 一条实例详情(同 §6)。**同一 `idempotencyKey` 重放返回第一次创建的实例,不起第二个**(前端每开一次抽屉生成一枚;重复点提交、网络重试都靠它挡住)。
|
||||||
`params` 的键值形状由 §4 参数表决定:`list` 回字符串数组,其余回字符串;`select` 回 `options` 里某个 `value`;**`combo` 回候选的 `value` 或新增的原样字符串,两种都合法**。
|
`params` 的键值形状由 §4 参数表决定:`list` 回字符串数组,其余回字符串;`select` 回 `options` 里某个 `value`;**`combo` 回候选的 `value` 或新增的原样字符串,两种都合法**。
|
||||||
412:活动已禁用(先激活再启动)。400:缺必填 / 类型不符 / select 的值不在 options 里(**combo 不适用这条** —— 候选之外的值是合法新增)。
|
412:活动已禁用(先激活再启动)。400:缺必填 / 类型不符 / select 的值不在 options 里(**combo 不适用这条** —— 候选之外的值是合法新增)。
|
||||||
@@ -309,7 +313,7 @@ type LaunchParamType = 'text' | 'textarea' | 'select' | 'combo' | 'list';
|
|||||||
type LaunchParam = { key: string; label: string; type: LaunchParamType; required?: boolean; desc?: string;
|
type LaunchParam = { key: string; label: string; type: LaunchParamType; required?: boolean; desc?: string;
|
||||||
placeholder?: string; options?: { value: string; label: string }[] /* select / combo 必填 */; default?: string | string[] };
|
placeholder?: string; options?: { value: string; label: string }[] /* select / combo 必填 */; default?: string | string[] };
|
||||||
type LaunchParams = { activityId: string; version: string; params: LaunchParam[] };
|
type LaunchParams = { activityId: string; version: string; params: LaunchParam[] };
|
||||||
type LaunchInput = { idempotencyKey: string; params: Record<string, string | string[]> };
|
type LaunchInput = { idempotencyKey: string; ownerId: string /* 责任人,固定字段,名册 staffId */; params: Record<string, string | string[]> };
|
||||||
|
|
||||||
type InstanceFile = { index: number; name: string; contentType: string; size: number };
|
type InstanceFile = { index: number; name: string; contentType: string; size: number };
|
||||||
type InstanceReason = { kind: 'rejected' | 'failed' | 'terminated'; text: string };
|
type InstanceReason = { kind: 'rejected' | 'failed' | 'terminated'; text: string };
|
||||||
@@ -331,6 +335,7 @@ type InstanceDetail = InstanceSummary & { activityId: string; llm: string; ended
|
|||||||
| 统计带由前端从全量数组数 | §3 `stats`(服务端给,分页下才数得准) |
|
| 统计带由前端从全量数组数 | §3 `stats`(服务端给,分页下才数得准) |
|
||||||
| `LAUNCH_PARAMS` 占位参数表 | §4;`options` 从纯字符串升级为 `{value,label}` 两段(渲染器小改);控件种类已含 `combo`(选择+新增,前端已实现) |
|
| `LAUNCH_PARAMS` 占位参数表 | §4;`options` 从纯字符串升级为 `{value,label}` 两段(渲染器小改);控件种类已含 `combo`(选择+新增,前端已实现) |
|
||||||
| `onLaunch` 在清单头落假行 | §5;抽屉每开一次生成 `idempotencyKey`,提交走 mutation,成功失效 `['gov']` |
|
| `onLaunch` 在清单头落假行 | §5;抽屉每开一次生成 `idempotencyKey`,提交走 mutation,成功失效 `['gov']` |
|
||||||
|
| —(新增)责任人固定字段 | §5 `ownerId`;抽屉「固定参数」段已实现(名册下拉、默认当前登录人),接接口后直接随 body 走 |
|
||||||
| 禁用/激活/终止就地改 state | §8;mutation 成功后由服务端重投影(广播 → 失效 → 重取) |
|
| 禁用/激活/终止就地改 state | §8;mutation 成功后由服务端重投影(广播 → 失效 → 重取) |
|
||||||
| `demoSteps` / `terminatedSteps` 五步占位 | §6 `steps`;终止归态以 `reason.kind='terminated'` 为准,前端不再落「失败」占位 |
|
| `demoSteps` / `terminatedSteps` 五步占位 | §6 `steps`;终止归态以 `reason.kind='terminated'` 为准,前端不再落「失败」占位 |
|
||||||
| 沙箱 `src="/sandbox-demo/activity-view.html"` 固定 480 高 | §2 `view.url` + 票据;`view` 为 null 藏起沙箱区;高度 postMessage 随第一版视图定 |
|
| 沙箱 `src="/sandbox-demo/activity-view.html"` 固定 480 高 | §2 `view.url` + 票据;`view` 为 null 藏起沙箱区;高度 postMessage 随第一版视图定 |
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ L1(模块首页 · 活动清单)需要一个**标签页**切换,因为活
|
|||||||
- 2026-08-27 · **「启动活动」按钮接功能(用户定)**:L2 头部「启动活动」不再占位 —— 点击弹**参数抽屉**(复用全局 `g-drawer` 右抽屉骨架),填启动参数后启动。**参数表由服务端按活动下发**(参数名 / 中文名 / 控件种类 / 必填 / 描述),每个活动要的参数不一样,前端只按规格渲染不写死字段;控件种类已支持单行 / 多行 / 单选 / 字符串数组(可传多个,如候选 PN)。接口未落地前用 `LAUNCH_PARAMS` 占位表(询价接收与结构化 = 入参说明的极简四参),提交先在历史活动清单头落一条「待执行」演示全链路,接口对接后换启动 mutation。**已禁用的活动不能启动**(钮置灰,先激活再启动)。
|
- 2026-08-27 · **「启动活动」按钮接功能(用户定)**:L2 头部「启动活动」不再占位 —— 点击弹**参数抽屉**(复用全局 `g-drawer` 右抽屉骨架),填启动参数后启动。**参数表由服务端按活动下发**(参数名 / 中文名 / 控件种类 / 必填 / 描述),每个活动要的参数不一样,前端只按规格渲染不写死字段;控件种类已支持单行 / 多行 / 单选 / 字符串数组(可传多个,如候选 PN)。接口未落地前用 `LAUNCH_PARAMS` 占位表(询价接收与结构化 = 入参说明的极简四参),提交先在历史活动清单头落一条「待执行」演示全链路,接口对接后换启动 mutation。**已禁用的活动不能启动**(钮置灰,先激活再启动)。
|
||||||
- 2026-08-27 · **禁用规则重定(用户定,废除 08-25 的「有实例不能禁用」)**:**任何时刻都可以禁用活动** —— 禁用后只拦「新启动」(启动接口返回 412);已完成的实例是历史留痕、进行中的实例继续跑完,都不受影响;激活即恢复。前端占位的拦截逻辑(有实例弹「无法禁用」提示条)已随此删除;接口口径见 `docs/API文档-活动模块.md` §0/§8。
|
- 2026-08-27 · **禁用规则重定(用户定,废除 08-25 的「有实例不能禁用」)**:**任何时刻都可以禁用活动** —— 禁用后只拦「新启动」(启动接口返回 412);已完成的实例是历史留痕、进行中的实例继续跑完,都不受影响;激活即恢复。前端占位的拦截逻辑(有实例弹「无法禁用」提示条)已随此删除;接口口径见 `docs/API文档-活动模块.md` §0/§8。
|
||||||
- 2026-08-27 · **启动参数的控件口径(用户定)**:启动表单**大部分是自由输入框**;客户 / 料号这类「台账里可能有」的参数用 **combo(选择+新增)** —— 先摆选择框,候选末项「新增」选了换输入框自己填。**填了库里没有的值不拦启动**:启动后服务端做存在性检查,查不到就派**数据工单**到工作台补档案(客户进客户台账、PN 补参数,确认即入库,活动从断点续跑)。前端已加 `combo` 控件类型;接口口径见 `docs/API文档-活动模块.md` §4「combo 语义」/§5。
|
- 2026-08-27 · **启动参数的控件口径(用户定)**:启动表单**大部分是自由输入框**;客户 / 料号这类「台账里可能有」的参数用 **combo(选择+新增)** —— 先摆选择框,候选末项「新增」选了换输入框自己填。**填了库里没有的值不拦启动**:启动后服务端做存在性检查,查不到就派**数据工单**到工作台补档案(客户进客户台账、PN 补参数,确认即入库,活动从断点续跑)。前端已加 `combo` 控件类型;接口口径见 `docs/API文档-活动模块.md` §4「combo 语义」/§5。
|
||||||
|
- 2026-08-27 · **启动固定参数「责任人」(用户定)**:每次启动必须指定这次实例的责任人(负责人验收、数据工单都落到他身上)—— 它是**启动契约的固定字段 `ownerId`**,不进按活动下发的参数表。抽屉里已加「固定参数」段:人员下拉来自员工名册真接口(`GET /api/gov/staff`,只列在职),默认当前登录人(标注「我自己」);接口口径见 `docs/API文档-活动模块.md` §5。
|
||||||
|
|
||||||
## 三、功能表(整理稿)
|
## 三、功能表(整理稿)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user