进度暂存:启动抽屉加固定参数「责任人」(名册下拉、默认当前登录人);接口文档 §5 加 ownerId 固定字段;需求文档记 2026-08-27 两条决策
CI / ci (push) Failing after 10s

This commit is contained in:
shanqin
2026-08-27 15:04:18 +08:00
parent 3171d8a864
commit 27d2fe6bb4
3 changed files with 48 additions and 9 deletions
@@ -7,17 +7,18 @@
*/
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
CONTROL_SEMANTIC_LABEL,
TASK_RUNTIME_STATE_LABEL,
TERMINAL_STATE_LABEL,
WORKFLOW_STATE_LABEL,
controlTaskRoute,
meRoute,
reassignTaskRoute,
resumeTaskRoute,
} from '@contracts';
import type { ControlSemantic } from '@contracts';
import type { ControlSemantic, Principal } from '@contracts';
import { Ico } from '@web-shared/Ico.tsx';
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';
@@ -27,6 +28,7 @@ import type { Nav } from '@web-shared/gov/gov-route.ts';
import { FlowFigure } from '@web-shared/gov/flow/FlowFigure.tsx';
import { atFull, atMinute } from '@web-shared/at.ts';
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';
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 实测);
* `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));
/* 责任人(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 关抽屉 —— 抽屉盖住整屏,不给键盘出路是把人关在里面 */
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
@@ -459,7 +469,7 @@ function LaunchDrawer({ name, spec, onCancel, onLaunch }: { name: string; spec:
}, [onCancel]);
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(
<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>
<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>
<h4>启动参数</h4>
{spec.map((p) => (
@@ -539,7 +568,11 @@ function LaunchDrawer({ name, spec, onCancel, onLaunch }: { name: string; spec:
<Btn className="g-btn" onClick={onCancel}>
取消
</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} /> 启动活动
</Btn>
</footer>
@@ -619,16 +652,16 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
setPage(0);
};
/** 启动提交(2026-08-27):启动接口未接 —— 先在清单头落一条「待执行」演示全链路;
输入摘要取第一个填了值的参数,让人认得出这是刚启动的那条 */
输入摘要取第一个填了值的参数,让人认得出这是刚启动的那条;负责人 = 抽屉里选的责任人 */
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() !== '');
setRows((cur) => [
{
id: `INS-${2418 + launchN}`,
input: `${(first ?? '手动启动').trim().slice(0, 24)} · ${launchSpec.length} 项参数`,
state: '待执行' as InstanceState,
owner: a.owner,
owner: owner.name === '' ? a.owner : owner.name,
startedAt: fmtMinute(Date.now()),
output: '—',
},