Archived
进度暂存:L2 启动活动按钮接功能 —— 参数抽屉(LaunchDrawer)按规格渲染,占位参数表每活动一份,提交落「待执行」演示行;需求文档记 2026-08-27 决策
This commit is contained in:
@@ -735,6 +735,21 @@
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.g-actvd .g-popask, .g-actid .g-popask { animation: none; }
|
||||
}
|
||||
|
||||
/* ══ 启动活动 · 参数抽屉(2026-08-27 用户定)══
|
||||
骨架复用全局 g-drawer;portal 挂在 body 上,作用域类 .g-launch 得自己带着走。
|
||||
这里只补抽屉内表单特有的三件:必填星号、数组参数(list)编辑行、底注 */
|
||||
.g-launch .g-fld > span .req { font-style: normal; margin-left: 3px; color: var(--danger-accent); }
|
||||
/* 数组参数:一行一条 + 行尾删除,底下虚线「再加一项」—— 可传多个的参数(如候选 PN)的趁手件 */
|
||||
.g-lplist { display: grid; gap: 7px; }
|
||||
.g-lplist-row { display: flex; align-items: center; gap: 7px; }
|
||||
.g-lplist-row .g-input { flex: 1; min-width: 0; }
|
||||
.g-lplist-row .x { flex: none; width: 32px; height: 32px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 8px; background: #fff; color: var(--ink-4); cursor: pointer; transition: color var(--t-fast), border-color var(--t-fast); }
|
||||
.g-lplist-row .x:hover:not(:disabled) { color: var(--danger-accent); border-color: var(--danger-accent); }
|
||||
.g-lplist-row .x:disabled { opacity: .38; cursor: not-allowed; }
|
||||
.g-lplist-add { justify-self: start; display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px; border: 1px dashed var(--cline); border-radius: 8px; background: transparent; color: var(--ink-3); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; transition: color var(--t-fast), border-color var(--t-fast); }
|
||||
.g-lplist-add:hover { color: var(--brand); border-color: var(--brand); }
|
||||
.g-launch-hint { margin: 13px 0 0; font-size: 11.5px; line-height: 1.6; color: var(--ink-4); }
|
||||
.g-fcard .mbtn { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--ink-4); cursor: pointer; }
|
||||
.g-fcard .mbtn:hover, .g-fcard .mbtn.on { border-color: var(--cline); background: #f3f6fa; color: var(--ink); }
|
||||
.g-fcard .mbtn.bad:hover { border-color: #e6c4c4; background: #fdf2f2; color: var(--danger); }
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
CONTROL_SEMANTIC_LABEL,
|
||||
@@ -313,13 +314,193 @@ function PopAsk({ spec, onCancel }: { spec: PopAskSpec; onCancel: () => void })
|
||||
);
|
||||
}
|
||||
|
||||
/* ── 启动活动 · 参数抽屉(2026-08-27 用户定)────────────────────────────
|
||||
点「启动活动」弹抽屉填启动参数:**参数表由服务端按活动下发**(参数名/中文名/控件种类/必填/描述),
|
||||
每个活动要的参数不一样 —— 前端只按规格渲染,不写死字段;
|
||||
接口未落地前用 LAUNCH_PARAMS 占位,提交先在清单头落一条「待执行」看全链路效果 */
|
||||
|
||||
/** 启动参数的一项规格 —— 将来就是接口返回的参数表行 */
|
||||
type LaunchParam = {
|
||||
/** 提交给接口的字段名 */
|
||||
key: string;
|
||||
/** 表单上的中文名 */
|
||||
label: string;
|
||||
/** 控件种类:text 单行 / textarea 多行 / select 单选 / list 字符串数组(一次可传多个) */
|
||||
type: 'text' | 'textarea' | 'select' | 'list';
|
||||
required?: boolean;
|
||||
/** 参数描述(服务端给),挂在控件下当辅助说明 */
|
||||
desc?: string;
|
||||
placeholder?: string;
|
||||
/** select 的候选项 */
|
||||
options?: readonly string[];
|
||||
};
|
||||
|
||||
/** 占位参数表 —— 一活动一份,接口定稿后按活动取(2026-08-27 用户定:参数由服务器给,每个活动不一样) */
|
||||
const LAUNCH_PARAMS: Readonly<Record<string, readonly LaunchParam[]>> = {
|
||||
/* 极简四参(2026-08-26《一键启动入参说明》口径:客户/需求/原PN/候选PN数组,其余启动后进工单补) */
|
||||
'quote-intake': [
|
||||
{ key: 'customer_id', label: '客户', type: 'select', required: true, desc: '决定通知对象与默认负责人线。', options: ['Itelma', '伏尔加', 'GAZ', 'KAMAZ', 'UAZ', 'Lada'] },
|
||||
{ key: 'request_description', label: '客户需求', type: 'textarea', required: true, desc: '保留原始需求语义,据此新建需求主记录(V1)。', placeholder: '如:车身控制器询价 · 3 项物料,期望 9 月交期' },
|
||||
{ key: 'original_internal_pn', label: '原内部料号', type: 'select', required: true, desc: '差异评估基准,引用物料台账。', options: ['PN-880101', 'PN-880205', 'PN-880310'] },
|
||||
{ key: 'candidate_internal_pn', label: '候选内部料号', type: 'list', required: true, desc: '被评估物料,一次可传多个、逐个评估 —— 至少 1 个。', placeholder: '如 PN-880412' },
|
||||
],
|
||||
'quote-pricing': [
|
||||
{ key: 'quote_id', label: '询价单号', type: 'text', required: true, desc: '针对哪一张询价单做测算。', placeholder: '如 INS-2415' },
|
||||
{ key: 'strategy', label: '计价策略', type: 'select', required: true, desc: '测算走哪套成本模型。', options: ['成本加成', '市场竞争', '历史跟随'] },
|
||||
{ key: 'target_margin', label: '目标毛利率', type: 'text', desc: '选填,留空按客户线默认。', placeholder: '如 18%' },
|
||||
],
|
||||
'crm-coverage': [
|
||||
{ key: 'customer_id', label: '客户', type: 'select', required: true, desc: '为哪家客户排触达计划。', options: ['Itelma', '伏尔加', 'GAZ', 'KAMAZ', 'UAZ', 'Lada'] },
|
||||
{ key: 'cadence', label: '触达节奏', type: 'select', required: true, desc: '关键人触达的排期密度。', options: ['每周', '双周', '每月'] },
|
||||
{ key: 'note', label: '补充说明', type: 'textarea', desc: '选填。' },
|
||||
],
|
||||
'competitor-watch': [
|
||||
{ key: 'rival_pn', label: '竞品型号', type: 'text', required: true, desc: '监测哪个竞品料号的价格动向。', placeholder: '如 Bosch-0261S04' },
|
||||
{ key: 'window', label: '监测窗口', type: 'select', required: true, desc: '往回拉多长时间的价格数据。', options: ['7 天', '30 天', '90 天'] },
|
||||
],
|
||||
'market-weekly': [
|
||||
{ key: 'region', label: '覆盖区域', type: 'text', required: true, desc: '周报覆盖的市场区域。', placeholder: '如 东欧 · 商用车' },
|
||||
{ key: 'window', label: '数据窗口', type: 'select', required: true, desc: '汇总哪个时间跨度的信号。', options: ['自然周', '自然月'] },
|
||||
],
|
||||
'supplier-verify': [
|
||||
{ key: 'supplier', label: '供应商名称', type: 'text', required: true, desc: '核验哪家供应商的资质。', placeholder: '如 深圳某某电子有限公司' },
|
||||
{ key: 'note', label: '资质说明', type: 'textarea', desc: '选填 —— 已有的资质线索先记下来,核验时对照。' },
|
||||
],
|
||||
};
|
||||
|
||||
/** 没在表里占位的活动给一份最小通用参数 —— 接口落地后不会有这条路 */
|
||||
const LAUNCH_PARAMS_FALLBACK: readonly LaunchParam[] = [
|
||||
{ key: 'subject', label: '启动事项', type: 'text', required: true, desc: '这次启动要处理什么,一句话。' },
|
||||
{ key: 'note', label: '补充说明', type: 'textarea', desc: '选填。' },
|
||||
];
|
||||
|
||||
type LaunchValues = Record<string, string | string[]>;
|
||||
|
||||
/** 参数值初始形:list 先摆一行空行(至少一行),其余空串 */
|
||||
function initLaunchValues(spec: readonly LaunchParam[]): LaunchValues {
|
||||
const v: LaunchValues = {};
|
||||
for (const p of spec) v[p.key] = p.type === 'list' ? [''] : '';
|
||||
return v;
|
||||
}
|
||||
|
||||
/** 这一项算不算填了 —— list 至少一行非空,其余非空串 */
|
||||
function paramFilled(p: LaunchParam, v: string | string[]): boolean {
|
||||
return p.type === 'list' ? (v as string[]).some((s) => s.trim() !== '') : (v as string).trim() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动抽屉 —— 复用全局 `g-drawer` 骨架(与系统区模型抽屉同一副);
|
||||
* 挂 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 }) {
|
||||
const [values, setValues] = useState<LaunchValues>(() => initLaunchValues(spec));
|
||||
/* Esc 关抽屉 —— 抽屉盖住整屏,不给键盘出路是把人关在里面 */
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onCancel();
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [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] ?? ''));
|
||||
|
||||
return createPortal(
|
||||
<div className="gov-root g-drawer g-launch" role="dialog" aria-modal="true" aria-label={`启动活动 · ${name}`}>
|
||||
<button className="g-drawer-veil" aria-label="关闭" onClick={onCancel} />
|
||||
<div className="g-drawer-p">
|
||||
<header>
|
||||
<div>
|
||||
<b>启动活动 · {name}</b>
|
||||
<span>参数表由服务端按活动下发 —— 每个活动要的不一样;带 * 必填</span>
|
||||
</div>
|
||||
<button className="x" aria-label="关闭" onClick={onCancel}>
|
||||
<Ico name="x" s={15} />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="g-drawer-b">
|
||||
<section>
|
||||
<h4>启动参数</h4>
|
||||
{spec.map((p) => (
|
||||
<label key={p.key} className="g-fld">
|
||||
<span>
|
||||
{p.label}
|
||||
{p.required === true && <i className="req">*</i>}
|
||||
</span>
|
||||
{p.type === 'text' && (
|
||||
<input className="g-input" value={values[p.key] as string} placeholder={p.placeholder} onChange={(e) => setVal(p.key, e.target.value)} />
|
||||
)}
|
||||
{p.type === 'textarea' && (
|
||||
<textarea className="g-ta" value={values[p.key] as string} placeholder={p.placeholder} onChange={(e) => setVal(p.key, e.target.value)} />
|
||||
)}
|
||||
{p.type === 'select' && (
|
||||
<select className="g-select" value={values[p.key] as string} onChange={(e) => setVal(p.key, e.target.value)}>
|
||||
<option value="">请选择…</option>
|
||||
{(p.options ?? []).map((o) => (
|
||||
<option key={o} value={o}>
|
||||
{o}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{p.type === 'list' && (
|
||||
<span className="g-lplist">
|
||||
{(values[p.key] as string[]).map((v, i, arr) => (
|
||||
<span key={i} className="g-lplist-row">
|
||||
<input
|
||||
className="g-input mono"
|
||||
value={v}
|
||||
placeholder={p.placeholder}
|
||||
aria-label={`${p.label} 第 ${i + 1} 项`}
|
||||
onChange={(e) => setVal(p.key, arr.map((x, j) => (j === i ? e.target.value : x)))}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="x"
|
||||
aria-label={`删除第 ${i + 1} 项`}
|
||||
disabled={arr.length <= 1}
|
||||
onClick={() => setVal(p.key, arr.filter((_, j) => j !== i))}
|
||||
>
|
||||
<Ico name="x" s={13} />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<button type="button" className="g-lplist-add" onClick={() => setVal(p.key, [...(values[p.key] as string[]), ''])}>
|
||||
<Ico name="plus" s={12} /> 再加一项
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{p.desc !== undefined && <small>{p.desc}</small>}
|
||||
</label>
|
||||
))}
|
||||
<p className="g-launch-hint">本期为占位参数表(接口未接);提交走启动接口 —— 现在先在历史活动清单头落一条「待执行」看效果。</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<Btn className="g-btn" onClick={onCancel}>
|
||||
取消
|
||||
</Btn>
|
||||
<Btn className="g-btn primary" disabled={invalid} onClick={() => onLaunch(values)}>
|
||||
<Ico name="play" s={12} /> 启动活动
|
||||
</Btn>
|
||||
</footer>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* L2 · 活动详情 —— 两个问题一屏答:「这个活动是什么」(定义,只读)
|
||||
* 与「它跑得怎么样」(小统计 + 实例清单,点行进 L3)。
|
||||
*
|
||||
* 头部动作三个(2026-08-25 用户定):
|
||||
* 头部动作两个(2026-08-27 更新):
|
||||
* · 激活/禁用 —— 本期做交互,二次确认;**有实例时无法禁用**(规则来自接口,本期前端占位演示);
|
||||
* · 启动活动(即创建实例)/ 版本迭代 —— 只留入口,功能后做(disabled)。
|
||||
* · 启动活动(即创建实例)—— 弹参数抽屉(LaunchDrawer),参数表按活动下发;已禁用不能启动。
|
||||
*
|
||||
* 沙箱区:加载服务器返回的活动视图 HTML —— iframe + sandbox 隔离;
|
||||
* v1 静态占位页,src 与内容高度通讯(postMessage)待接口一起定。
|
||||
@@ -341,6 +522,9 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
const [fTo, setFTo] = useState('');
|
||||
/* 轻量二次确认(2026-08-26 用户定):一句话 + 确认/取消的气泡,不走 AskBand 三必答 */
|
||||
const [ask, setAsk] = useState<PopAskSpec | null>(null);
|
||||
/* 启动抽屉(2026-08-27 用户定):launchN 给占位新实例编流水号 */
|
||||
const [launch, setLaunch] = useState(false);
|
||||
const [launchN, setLaunchN] = useState(0);
|
||||
if (a === undefined) return <Note tone="warn">活动不存在 —— 它可能已被调整,返回活动清单看看。</Note>;
|
||||
const st = status ?? a.status;
|
||||
const instances = rows ?? demoInstances(a.owner);
|
||||
@@ -379,6 +563,27 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
setFTo('');
|
||||
setPage(0);
|
||||
};
|
||||
/** 启动提交(2026-08-27):启动接口未接 —— 先在清单头落一条「待执行」演示全链路;
|
||||
输入摘要取第一个填了值的参数,让人认得出这是刚启动的那条 */
|
||||
const launchSpec = LAUNCH_PARAMS[a.id] ?? LAUNCH_PARAMS_FALLBACK;
|
||||
const onLaunch = (values: LaunchValues): 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,
|
||||
startedAt: fmtMinute(Date.now()),
|
||||
output: '—',
|
||||
},
|
||||
...(cur ?? demoInstances(a.owner)),
|
||||
]);
|
||||
setLaunchN((n) => n + 1);
|
||||
setLaunch(false);
|
||||
/* 正筛着别的条件时新行可能直接被滤没 —— 启动完清掉检索,让人一眼看到它 */
|
||||
clearFilter();
|
||||
};
|
||||
/* 分页(2026-08-25 用户定:单页最多 8 条)—— 检索后按筛出的集合翻页 */
|
||||
const pageCount = Math.max(1, Math.ceil(filtered.length / INSTANCE_PAGE_SIZE));
|
||||
const cur = Math.min(page, pageCount - 1);
|
||||
@@ -438,13 +643,15 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ label: '启动活动', ico: 'plus', disabled: true },
|
||||
/* 启动活动(2026-08-27 接):弹参数抽屉填参启动;已禁用不能启动 —— 先激活,再启动 */
|
||||
{ label: '启动活动', ico: 'play', primary: st === 'active', disabled: st !== 'active', onClick: () => setLaunch(true) },
|
||||
/* 版本迭代入口先隐藏(2026-08-25 用户定):功能设计回头讨论,要做时把这行加回 acts
|
||||
{ label: '版本迭代', ico: 'git-branch', disabled: true },
|
||||
*/
|
||||
]}
|
||||
/>
|
||||
{ask !== null && <PopAsk spec={ask} onCancel={() => setAsk(null)} />}
|
||||
{launch && <LaunchDrawer name={a.name} spec={launchSpec} onCancel={() => setLaunch(false)} onLaunch={onLaunch} />}
|
||||
</div>
|
||||
{blocked && (
|
||||
<Note tone="warn">
|
||||
|
||||
@@ -92,6 +92,7 @@ L1(模块首页 · 活动清单)需要一个**标签页**切换,因为活
|
||||
- 2026-08-26 · **L3 实例详情已落地(静态占位版,用户确认规划后实施)**:路由 `acti`(id = `活动id:实例号`),L2 行内「查看详情」接入。布局 = 主从 2:1(DESIGN-SPEC §9):左侧「动作过程记录」竖时间线是主角(五步节点流:输入接收 → 要素结构化 → 定价测算 → **负责人验收** → 输出归档 —— 验收永远留给人,对应人机分工),当前步呼吸高亮、未到达空心灰显、连接线成不间断竖轨;右栏输入(键值)/ 输出(文件行,下载随接口开放)。头部 = 状态丸 + 负责人/启动/结束·耗时/专属 LLM;驳回/失败出原因说明块;终止后的过程面与「自己跑挂」区分:当前步记「已终止」而非失败原因。数据仍是静态假数据。
|
||||
- 2026-08-26 · **实例不可删除、不可编辑(用户定,推翻 L3 原规划)**:L3 的编辑/删除入口撤掉,唯一动作是进行中的「终止活动」(二次确认);六功能里实例的「编辑/删除」两项就此划掉,待确认问题里的两条(编辑指什么 / 删除是否放开)同时关闭。
|
||||
- 2026-08-26 · **二次确认的规格(用户定)**:活动模块的确认走**轻气泡**(锚在触发钮下方的小泡泡,只问一句 + 确认/取消,点罩即取消)—— 不摆影响三必答;三必答(AskBand)是治理台重度动作的规格,别处的确认带暂不动。
|
||||
- 2026-08-27 · **「启动活动」按钮接功能(用户定)**:L2 头部「启动活动」不再占位 —— 点击弹**参数抽屉**(复用全局 `g-drawer` 右抽屉骨架),填启动参数后启动。**参数表由服务端按活动下发**(参数名 / 中文名 / 控件种类 / 必填 / 描述),每个活动要的参数不一样,前端只按规格渲染不写死字段;控件种类已支持单行 / 多行 / 单选 / 字符串数组(可传多个,如候选 PN)。接口未落地前用 `LAUNCH_PARAMS` 占位表(询价接收与结构化 = 入参说明的极简四参),提交先在历史活动清单头落一条「待执行」演示全链路,接口对接后换启动 mutation。**已禁用的活动不能启动**(钮置灰,先激活再启动)。
|
||||
|
||||
## 三、功能表(整理稿)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user