Archived
进度暂存:禁用规则重定(任何时刻可禁用,只拦新启动)+ 启动参数加 combo 控件(选择+新增,库里没有不拦启动、启动后派数据工单);新增《API文档-活动模块》(L1/L2/L3 九条路由,供后端编写)
This commit is contained in:
@@ -750,6 +750,11 @@
|
||||
.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); }
|
||||
/* combo 选择+新增(2026-08-27 用户定):新增态 = 输入框 + 右侧「返回选择」链 */
|
||||
.g-launch .g-combo { display: flex; align-items: center; gap: 7px; }
|
||||
.g-launch .g-combo .g-input { flex: 1; min-width: 0; }
|
||||
.g-launch .g-combo-back { flex: none; padding: 4px 6px; border: 0; border-radius: 6px; background: transparent; color: var(--brand); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; }
|
||||
.g-launch .g-combo-back:hover { text-decoration: underline; }
|
||||
.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); }
|
||||
|
||||
@@ -317,6 +317,8 @@ function PopAsk({ spec, onCancel }: { spec: PopAskSpec; onCancel: () => void })
|
||||
/* ── 启动活动 · 参数抽屉(2026-08-27 用户定)────────────────────────────
|
||||
点「启动活动」弹抽屉填启动参数:**参数表由服务端按活动下发**(参数名/中文名/控件种类/必填/描述),
|
||||
每个活动要的参数不一样 —— 前端只按规格渲染,不写死字段;
|
||||
控件口径(2026-08-27 用户定):大部分是自由输入;客户/料号这类「台账里可能有」的用 combo
|
||||
(选择+新增:先选,候选没有就新增自填)—— **填了库里没有的值不拦启动**,存在性检查在启动后做,缺口派数据工单;
|
||||
接口未落地前用 LAUNCH_PARAMS 占位,提交先在清单头落一条「待执行」看全链路效果 */
|
||||
|
||||
/** 启动参数的一项规格 —— 将来就是接口返回的参数表行 */
|
||||
@@ -325,24 +327,25 @@ type LaunchParam = {
|
||||
key: string;
|
||||
/** 表单上的中文名 */
|
||||
label: string;
|
||||
/** 控件种类:text 单行 / textarea 多行 / select 单选 / list 字符串数组(一次可传多个) */
|
||||
type: 'text' | 'textarea' | 'select' | 'list';
|
||||
/** 控件种类:text 单行 / textarea 多行 / select 单选(只能选) / combo 选择+新增(候选没有就自己填) / list 字符串数组(一次可传多个) */
|
||||
type: 'text' | 'textarea' | 'select' | 'combo' | 'list';
|
||||
required?: boolean;
|
||||
/** 参数描述(服务端给),挂在控件下当辅助说明 */
|
||||
desc?: string;
|
||||
placeholder?: string;
|
||||
/** select 的候选项 */
|
||||
/** select / combo 的候选项 */
|
||||
options?: readonly string[];
|
||||
};
|
||||
|
||||
/** 占位参数表 —— 一活动一份,接口定稿后按活动取(2026-08-27 用户定:参数由服务器给,每个活动不一样) */
|
||||
const LAUNCH_PARAMS: Readonly<Record<string, readonly LaunchParam[]>> = {
|
||||
/* 极简四参(2026-08-26《一键启动入参说明》口径:客户/需求/原PN/候选PN数组,其余启动后进工单补) */
|
||||
/* 极简四参(2026-08-26《一键启动入参说明》口径:客户/需求/原PN/候选PN数组,其余启动后进工单补);
|
||||
客户与料号用 combo(2026-08-27 用户定):台账里有就先选,没有就新增自填 —— 不拦启动,缺口派数据工单 */
|
||||
'quote-intake': [
|
||||
{ key: 'customer_id', label: '客户', type: 'select', required: true, desc: '决定通知对象与默认负责人线。', options: ['Itelma', '伏尔加', 'GAZ', 'KAMAZ', 'UAZ', 'Lada'] },
|
||||
{ key: 'customer_id', label: '客户', type: 'combo', 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' },
|
||||
{ key: 'original_internal_pn', label: '原内部料号', type: 'combo', required: true, desc: '差异评估基准;库里没有该 PN 也可启动,启动后派数据工单补参数。', options: ['PN-880101', 'PN-880205', 'PN-880310'], placeholder: '如 PN-880501' },
|
||||
{ key: 'candidate_internal_pn', label: '候选内部料号', type: 'list', required: true, desc: '被评估物料,一次可传多个、逐个评估;库里没有的 PN 照样填,至少 1 个。', placeholder: '如 PN-880412' },
|
||||
],
|
||||
'quote-pricing': [
|
||||
{ key: 'quote_id', label: '询价单号', type: 'text', required: true, desc: '针对哪一张询价单做测算。', placeholder: '如 INS-2415' },
|
||||
@@ -350,7 +353,7 @@ const LAUNCH_PARAMS: Readonly<Record<string, readonly LaunchParam[]>> = {
|
||||
{ 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: 'customer_id', label: '客户', type: 'combo', required: true, desc: '为哪家客户排触达计划;新客户直接填,启动后补档案。', options: ['Itelma', '伏尔加', 'GAZ', 'KAMAZ', 'UAZ', 'Lada'] },
|
||||
{ key: 'cadence', label: '触达节奏', type: 'select', required: true, desc: '关键人触达的排期密度。', options: ['每周', '双周', '每月'] },
|
||||
{ key: 'note', label: '补充说明', type: 'textarea', desc: '选填。' },
|
||||
],
|
||||
@@ -388,6 +391,57 @@ function paramFilled(p: LaunchParam, v: string | string[]): boolean {
|
||||
return p.type === 'list' ? (v as string[]).some((s) => s.trim() !== '') : (v as string).trim() !== '';
|
||||
}
|
||||
|
||||
/** combo 里「新增」这一项的哨兵值 —— 不会与真实候选撞名 */
|
||||
const COMBO_CREATE = '__create__';
|
||||
|
||||
/**
|
||||
* 选择+新增(2026-08-27 用户定):先摆选择框,候选末项是「新增 …」;
|
||||
* 选了它就换成输入框自己填(库里没有的值也收 —— 不拦启动,缺口由服务端派数据工单),
|
||||
* 旁边留「返回选择」换回去。
|
||||
*/
|
||||
function ComboInput({ p, value, onChange }: { p: LaunchParam; value: string; onChange: (v: string) => void }) {
|
||||
const [custom, setCustom] = useState(false);
|
||||
if (custom) {
|
||||
return (
|
||||
<span className="g-combo">
|
||||
<input className="g-input" value={value} placeholder={p.placeholder ?? `输入新的${p.label}`} onChange={(e) => onChange(e.target.value)} />
|
||||
<button
|
||||
type="button"
|
||||
className="g-combo-back"
|
||||
onClick={() => {
|
||||
setCustom(false);
|
||||
onChange('');
|
||||
}}
|
||||
>
|
||||
返回选择
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<select
|
||||
className="g-select"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === COMBO_CREATE) {
|
||||
setCustom(true);
|
||||
onChange('');
|
||||
} else {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">请选择…</option>
|
||||
{(p.options ?? []).map((o) => (
|
||||
<option key={o} value={o}>
|
||||
{o}
|
||||
</option>
|
||||
))}
|
||||
<option value={COMBO_CREATE}>+ 新增{p.label}…</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动抽屉 —— 复用全局 `g-drawer` 骨架(与系统区模型抽屉同一副);
|
||||
* 挂 body portal:详情页容器带入场动画 transform,fixed 抽屉不 portal 会被钉进卡片盒子里(2026-07-30 实测);
|
||||
@@ -446,6 +500,7 @@ function LaunchDrawer({ name, spec, onCancel, onLaunch }: { name: string; spec:
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{p.type === 'combo' && <ComboInput p={p} value={values[p.key] as string} onChange={(v) => setVal(p.key, v)} />}
|
||||
{p.type === 'list' && (
|
||||
<span className="g-lplist">
|
||||
{(values[p.key] as string[]).map((v, i, arr) => (
|
||||
@@ -499,7 +554,8 @@ function LaunchDrawer({ name, spec, onCancel, onLaunch }: { name: string; spec:
|
||||
* 与「它跑得怎么样」(小统计 + 实例清单,点行进 L3)。
|
||||
*
|
||||
* 头部动作两个(2026-08-27 更新):
|
||||
* · 激活/禁用 —— 本期做交互,二次确认;**有实例时无法禁用**(规则来自接口,本期前端占位演示);
|
||||
* · 激活/禁用 —— 本期做交互,二次确认;**任何时刻都可禁用**(2026-08-27 用户重定:
|
||||
* 禁用只拦新启动,不影响已完成与进行中的实例,废除「有实例不能禁用」);
|
||||
* · 启动活动(即创建实例)—— 弹参数抽屉(LaunchDrawer),参数表按活动下发;已禁用不能启动。
|
||||
*
|
||||
* 沙箱区:加载服务器返回的活动视图 HTML —— iframe + sandbox 隔离;
|
||||
@@ -508,7 +564,6 @@ function LaunchDrawer({ name, spec, onCancel, onLaunch }: { name: string; spec:
|
||||
export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
const a = DEMO_ACTIVITIES.find((x) => x.id === id);
|
||||
const [status, setStatus] = useState<'active' | 'disabled' | undefined>(undefined);
|
||||
const [blocked, setBlocked] = useState(false);
|
||||
const [frameLoaded, setFrameLoaded] = useState(false);
|
||||
/* 实例清单入组件状态:操作列的「终止活动」要就地改行(本期前端演示,接口对接后走 mutation) */
|
||||
const [rows, setRows] = useState<readonly DemoInstance[] | undefined>(undefined);
|
||||
@@ -618,15 +673,8 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
title: '确认禁用这个活动?',
|
||||
okLabel: '确认禁用',
|
||||
danger: true,
|
||||
onOk: () => {
|
||||
/* 规则(2026-08-25 用户定):有实例时接口会提示无法禁用 —— 本期前端先演出这个拦截 */
|
||||
if (a.instances > 0) {
|
||||
setBlocked(true);
|
||||
} else {
|
||||
setStatus('disabled');
|
||||
setBlocked(false);
|
||||
}
|
||||
},
|
||||
/* 任何时刻都可禁用(2026-08-27 用户重定):只拦新启动,存量实例不受影响 */
|
||||
onOk: () => setStatus('disabled'),
|
||||
}),
|
||||
}
|
||||
: {
|
||||
@@ -637,10 +685,7 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
setAsk({
|
||||
title: '确认激活这个活动?',
|
||||
okLabel: '确认激活',
|
||||
onOk: () => {
|
||||
setStatus('active');
|
||||
setBlocked(false);
|
||||
},
|
||||
onOk: () => setStatus('active'),
|
||||
}),
|
||||
},
|
||||
/* 启动活动(2026-08-27 接):弹参数抽屉填参启动;已禁用不能启动 —— 先激活,再启动 */
|
||||
@@ -653,11 +698,6 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
{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">
|
||||
无法禁用:该活动还有 {a.instances} 个实例 —— 这条提示将来由接口返回(有实例时接口拒绝禁用),本期为前端占位演示。
|
||||
</Note>
|
||||
)}
|
||||
|
||||
<Sec ico="repeat" title="历史活动" extra={`共 ${instances.length} 个 · 查看详情进 L3`}>
|
||||
{/* 统计带 + 检索条同一行(2026-08-26 用户定:检索放右侧)——
|
||||
|
||||
Reference in New Issue
Block a user