A2-A7 全部补齐到验收线;顺手把几处「门拦住却不给路」的坑填了

活动:A2 选型 / A3 寻源 / A4 定价 / A6 议价 / A7 归档五件各补成真活动包 ——
契约宽进(input.required 空,归一在活动内)、台账必填声明(含行级 requiredWhen)、
入场哨兵与 L1/L2 判据一律读基座数、两份件(在办可编辑 + 办结静态快照)、
三案例常备、包内自测。e2e 各三案例 released、行动被拒 0、发口正确、提交闸每单真拦。

基座(共享面,改一处影响所有活动,故每处都带回归):
· 重发判断走在站上场之前 —— 人答完那步常已收掉,readyToWork 先抛「不能再往它上面挂活」,
  于是「重发已答过的卡 = 无事发生」这条唯一活路在最需要时够不着,A1 基线当场熔断;
· 决策卡补幂等兜底(与执行工单同一把尺):头两次无事发生,第三次起才算被拒;
· 输出没成形先就地重问一次,原话进工作日志、不再摆上卡面;
· 工具调用读数 tool_<名>_calls —— 「机检只读基座数」这条纪律第一次覆盖到工具;
· 执行卡 steps/standard 补进出参 schema(从前能到卡面全靠厂商 JSON 模式宽松);
· 件的就地更新改逐节点 morph,保存先把新值镜像进只读那份 —— 不闪、不等回声。

A1(基线,全程守住):措辞三档对齐(必填/建议/选填)、禁止拿历史价当客户目标价、
卡骨架从步骤剧本搬进说明书(压成一拍连做时剧本没摊开,会发出没有指导书的裸卡)。

契约两处**新增可选字段**(非破坏):FieldDeclaration.requiredWhen、工单 finalHtml;
api-report 同批更新。docs/adr 与 docs/archive 的 13 份文件在工作树里不见了,
本次予以恢复而非提交删除。

门禁:typecheck / lint / depcruise / test(1366)全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 12:28:31 +08:00
co-authored by Claude Opus 5
parent 85466e815f
commit ade0fa5a94
90 changed files with 17969 additions and 2316 deletions
+291 -90
View File
@@ -1,138 +1,339 @@
/**
* A2 替代选型的案例模型 —— **一张执行单办完**(2026-07-31 用户定,大幅简化)。
* A2 替代选型的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。
*
* 上一版把 SOP 上的每一小步都建成了计划项(查规格、判品牌、查沉淀、撒网初筛、
* 逐条核验、确认主推、收规格书、参数对比、风险评估…… 九个 humanTask)。
* 那是把「FAE 脑子里怎么想」当成了「系统要管几步」——
* 而这一整段在业务上是**一件事**:拿着原型号去选一个替代料,选完给出对标结论。
* 系统该做的不是替他把这件事切成九块,是**把该填的表摆好、把结果收进台账**。
* 业务出处:`docs/MVP示例工作流.html` §1.5,但按 **MVP 简化口径**建模
* (2026-08-02 用户定:A2 是复杂活动,MVP 期间不展开大规模业务建模):
*
* 现在只有两项:
* T_select —— 派一张执行单给 FAE(卡面两张可编辑表),他填完提交;
* T_release —— 放行(触达世界,人工激活)。
* A1 带着询价受理记录转进来 → 挑出 C 类 → **一张执行单**让 FAE 在系统外完成选型、
* 把替代料填回来 → 判替代料在库里行情稳不稳:稳转 A、不稳转 B →
* 询价受理记录就此转换成**询价受理清单**,传给下游(有 B 类去寻源,全 A 直通定价)。
*
* 与 A1 的分工:A1 是**受理归一**,活多、判据多、有确定性规则可依(两张判定表),
* 值得逐步建模;A2 是**一次专家判断**,建模建得再细也换不来业务上的好处,
* 只会让 Agent 全程跟 harness 较劲(A1 上一版 12 项的教训就在这儿)。
* SOP 里初选核险 / 圈选替代 / 对比评估那三段的**实质工作在 FAE 脑子里与系统外**
* (撒网、读规格书、参数对比 —— 本系统没有那些数据源);系统的本分是归一、组卡、
* 判稳、留痕。所以图上只有四项:归一 → 交人填回 → 判稳定标 → 放行。
*
* ── 建模纪律(照 A1 的账)──────────────────────────────────────────────
* 图上有几步就是几项,不多建一件;触达世界的项 manualActivation 且不给 doneWhen;
* 哨兵必须有 ifPart 且读基座数、认「办完了没有」;案卷项 id 就是任务视图键名。
*
* 上一版(@2.0.0)只有「派单 + 放行」两项,归一与判稳整个不存在 ——
* C 类怎么来的、去向旗从哪儿出,全靠模型临场发挥;E5 那条边(needSourcing)
* 因为台账里没有承载列,**从来没有成立过**。这一版把这两步补成真的。
*/
export const CMMN_XML = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL">
<case id="alternative">
<!--
案卷项的 id **就是任务视图里的键名**。这里只认两样:
起单入参(altRequests —— A1 发过来的那批替代评估请求)与「对标表写了几行」(benchmark_total)——
后者既是放行前提,也是这一单唯一的机检判据。
案卷项的 id **就是任务视图里的键名** —— 前提的 onPart 认的是「这个键有值了」。
流内起单时上游载荷(records 等)与本活动台账列都会摊平到任务视图顶层,照它们命名。
开案即有的只有上游载荷(或单飞的自由入参)。rfq_ref 是归一那一步认出来
(或造出来)的:本活动所有台账对象都以它为主键。
-->
<caseFileModel>
<caseFileItem id="altRequests" name="替代评估请求(开案即有,一条料一行)"/>
<caseFileItem id="benchmark_total" name="对标表条数"/>
<caseFileItem id="records" name="上游受理记录(流内开案即有)"/>
<caseFileItem id="alt_line_total" name="清单条目数(自报)"/>
<caseFileItem id="quote_alternative_list_rows" name="清单行数(基座现数)"/>
<caseFileItem id="c_total" name="待选型条数"/>
<caseFileItem id="filled_total" name="已填回替代料条数"/>
<caseFileItem id="quote_alternative_list_type_tag_c" name="仍为 C 类的行数(基座现数)"/>
<caseFileItem id="route_decided_total" name="已定去向"/>
<caseFileItem id="need_sourcing" name="去向旗"/>
</caseFileModel>
<casePlanModel id="plan" name="替代选型" autoComplete="true">
<sentry id="s_benchmarked">
<onPart id="op_bm" caseFileItemRef="benchmark_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_bm"><condition>benchmark_total &gt; 0</condition></ifPart>
<!--
① 归一完了 —— 交 FAE 那一步的入场前提。
两边对上才算:alt_line_total 是活动自报的条数,quote_alternative_list_rows
是基座数出来的本单行数 —— 少拆一行、多写一行都进不了下一步
(同一个错,在第 2 拍发现比在第 10 拍便宜得多,A1 的账)。
-->
<sentry id="s_normalized">
<onPart id="op_nm" caseFileItemRef="alt_line_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_nm"><condition>alt_line_total &gt; 0 and quote_alternative_list_rows = alt_line_total</condition></ifPart>
</sentry>
<!--
② FAE 的答复处理过了 —— 判稳那一步的入场前提。
filled_total 是「人交回执行单之后」由 AI 清点写下的数(填回替代料的条数)——
它只在人环走完才存在,所以它就是「经过人了」的凭据(照 A1 confirmed_total 的路子)。
全部替代未果时它是 0:「大于等于 0」对「写过的 0」为真、对「没写过」为假
(S-FEEL 未绑定的名字参与比较为假)。
-->
<sentry id="s_filled">
<onPart id="op_fl" caseFileItemRef="filled_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_fl"><condition>filled_total &gt;= 0</condition></ifPart>
</sentry>
<!--
── 放行门的入场条件:**认「办完了没有」,且读基座数**(A1 s_routed 的账)────
· 清单上不再有 C 类(quote_alternative_list_type_tag_c 由基座按值现数)——
FAE 漏填一行替代料,那一行转不了 A/B,这道门就不开;
· 定标守恒:A + B = 行数(两边都是基座出的数);
· 去向定过了(route_decided_total 是判稳那一步显式写下的记账数 ——
旗是布尔,分不清「没写」与「写了 false」)。
-->
<sentry id="s_routed">
<onPart id="op_rt" caseFileItemRef="route_decided_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_rt"><condition>route_decided_total &gt; 0 and quote_alternative_list_rows &gt; 0 and quote_alternative_list_type_tag_c = 0 and quote_alternative_list_type_tag_a + quote_alternative_list_type_tag_b = quote_alternative_list_rows</condition></ifPart>
</sentry>
<!-- 唯一的活:派一张执行单给 FAE,他填完提交 -->
<humanTask id="T_select" name="替代选型:派执行单给 FAE,收回初筛与对标结论">
<itemControl><repetitionRule id="r_sel"/></itemControl>
<!--
① 归一询价受理清单 —— 上游受理记录(或单飞来件)整份落成本活动的清单,
C 类行就是本单要办的对象。**整份都落**:A/B 行原样承继 ——
这份清单最终要整份交给下游,不是只有 C 类那几行。
-->
<humanTask id="S1_normalize" name="归一询价受理清单:整份落表,C 类标待选型">
<itemControl><repetitionRule id="r_s1"/></itemControl>
</humanTask>
<!-- 放行:触达世界,必须人工激活 -->
<humanTask id="T_release" name="替代选型结论验收放行">
<entryCriterion id="ec_rel" sentryRef="s_benchmarked"/>
<!--
② 替代选型交 FAE —— **一张执行单**(MVP 简化口径:选型的实质工作在系统外)。
manualActivation + 不给 doneWhen:发卡触达真人,引擎不替你按;
「人看过、选完、填回来了」也不是台账里任何一个数能自己变出来的。
-->
<humanTask id="S2_fill" name="替代选型交FAE:执行单上把 C 类行的替代料填回来">
<entryCriterion id="ec_s2" sentryRef="s_normalized"/>
<itemControl><manualActivationRule id="m_s2"/></itemControl>
</humanTask>
<!--
③ 判稳定标 —— 替代料查历史行情,稳转 A、不稳转 B(规则写死,零裁量);
清单就此从「受理记录」转成「受理清单」,去向旗随之立起。
-->
<humanTask id="S3_reclass" name="判稳定标:替代料查行情,稳转A不稳转B,立去向旗">
<entryCriterion id="ec_s3" sentryRef="s_filled"/>
</humanTask>
<!-- 清单放行:终局唯放行 —— 触达世界,必须人工激活 -->
<humanTask id="T_release" name="询价受理清单验收放行">
<entryCriterion id="ec_rel" sentryRef="s_routed"/>
<itemControl><manualActivationRule id="m_rl"/></itemControl>
</humanTask>
</casePlanModel>
</case>
</definitions>`;
export const NARRATIVE = `§0 开案:A1 判出一条料要替代(原型号 EOL / 缺货 / 客户点名),这一单就到你手上。
成品只有一样:**一份能让 BD 拿去跟客户讲的替代对标结论** ——
哪几个候选、推荐哪一个、参数差在哪、有什么风险、要不要打样验证。
/**
* 案例说明书 —— **只写整单一以贯之的事**(逐步做法归剧本,两处不重复写同一件事)。
*/
export const NARRATIVE = `§0 开案:**一份带 C 类料的询价受理记录要过替代选型**。来路有两种,形状都收(宽进):
· **流内**:上游受理活动放行后带着它的落地事实进来 —— 顶层有单号 / 客户名 / 三类条数等
摊平字段,\`records\` 数组里是上游全部落地行**混在一起**(带年用量与制造商的是受理记录行、
带分级依据与历史天数的是分类表行、带各种总数的是结论行 —— 按 PN 归并自辨,别当成三张表);
· **单飞**:没有上游,只有一段需求(型号或芯片类型 + 参数 + 场景)—— 照来件归一,
没给的照实留空。
**这一单只有一步**:把选型这件事派给 FAE(执行工单),他填完你收进台账,然后提交放行。
别把它拆成「先查规格、再判品牌、再撒网、再核验……」—— 那是他脑子里的过程,
不是系统要管的步。你替他把表摆好就够了。
**这个活动是复杂活动的 MVP 简化形**:替代选型的实质工作(查规格、撒网、参数对比)
由 FAE 在**系统外**完成,系统只负责三件事 —— 把清单归一好、把执行单组好交给他、
把他填回的替代料**判稳定标**(在库行情稳转 A,不稳转 B)。
【T_select 派执行单】发 human 行动(workOrder="execution"),卡面 blocks 放**两张可编辑表**:
· 初筛清单:列 = 原型号 / 候选型号 / 品牌 / 生命周期;
· 替代对标表:列 = 候选型号 / 替代等级 / 参数差异项 / 差异风险 / 验证建议 / FAE签字。
两张都 editable:true;没有的值给空字符串 —— 空格子界面会自己标红「待填写」。
ask 一句话说清要他做什么;basis 摆上原型号、芯片类型、客户的特殊要求(裸卡基座拒发)。
**成品只有一样**:一份**询价受理清单** —— 整份行都在、C 类已全部转成 A 或 B、
去向旗立了(还有 B 类就去寻源补价,全 A 直通定价)。判稳的尺与三分类 P-03 的 A/B
两行同一把:**历史价 ≤ 90 天且行情稳 → A,否则 → B**。
FAE 的定案不只是一个型号:**替代等级(P2P 直换 / FUNC 功能兼容 / OTHER 须验证)、
参数差异、风险与验证建议一并落表** —— 那是他的专业产出,逐颗可溯。
他提交后,回传值出现在你的轨迹里:表格那一份的键是 **table:0 / table:1**(块序号),
值是整张表的 JSON —— **第一行是列名**,其余每行一条,列序与你派出去时一致。
照它 write 进 \`quote_alternative.shortlist\` 与 \`quote_alternative.benchmark\`,
并把对标表条数记进 benchmark_total。**写完就 finish T_select,别再发第二张单** —— 他已经答过了。
**人环两次,不多不少**:FAE 交回**一张选型执行单**(工作单里一颗待选型芯片一页,
右上角翻页逐颗定案,必填缺着按不下提交)、放行时 FAE 签字。
【提交放行】对标表有行了 —— **先 start T_release**(人工激活:放行触达世界,必须按一下),
然后 submit,BD 验收。终局唯放行:你不能自己宣布完成。
【一路上要说话】负责人正看着这一单。**每一拍都带一句 say** —— 说业务的话:
哪几颗料要替代、替代料查出来什么行情、这单接下来往哪走;
不说「归一化」「写草稿 N 行」这类系统词。到了要找人的那一拍,先说清这一轮办完了什么、
为什么现在需要他。别每拍一个腔调复读。
【一路上要说话】负责人正看着这一单。每一拍带一句 say,说**业务的话**:
哪颗料要替代、你打算怎么办、FAE 回了什么结论、这一单往哪走。
一两句、口语,像同事扭头说一声。别报「写草稿 2 行」这种系统词 ——
他明天要拿这些话去跟客户讲。`;
【怎么把一项干完 · 一拍连做】一个计划项常要两三个动作才交得出成果。write 和 tool
都**不会**自动把那一项收掉 —— 都做完了,用 finish 宣告完成。动作排进同一拍的 actions
串里一口气给,别拆拍。只有两处必须停拍:调工具要看出参(tool 放串尾),
发卡 / 提交会挂起(只能是串尾)。**常路 6-9 拍就该收口** —— 选型只有一张卡,颗数多不多不加拍。
§Z 结案:FAE 在验收单上签字,系统把草稿合并成正式清单并发出 roster 口的载荷 ——
清单整份交给下游,下游按自己的口径归一需要的数据。`;
/**
* 步骤剧本 —— 与 A1 同一套机制:`doneWhen` 为真,引擎自己收步、**不问模型**。
*
* 只有一条真活。放行那一项不给 `doneWhen` —— 它触达世界,必须人按一下(铁律③)。
* 步骤剧本 —— 一个计划项一条;`doneWhen` 是「成果已存在」的判据,不是「该不该做」。
*/
export const ALTERNATIVE_STEPS = [
{
item: 'T_select',
intent: '派一张执行单给 FAE,收回初筛清单与替代对标结论,并结算汇总数',
writes: ['quote_alternative.shortlist', 'quote_alternative.benchmark', 'quote_alternative.verdict'],
reads: [],
item: 'S1_normalize',
intent: '把上游受理记录(或单飞来件)整份归一成选型底表,C 类行标待选型',
writes: ['quote_alternative.list', 'quote_alternative.verdict'],
/*
* `records` 是上游整包落地行 —— 不声明的话它会被整表段的截断刀砍掉
* (那一段按字符数截,上游一单的落地行远超刀口,砍掉的恰是 C 类行)。
* 声明了的键**置前且不参与截断**。单飞起单没有这个键,声明也无副作用。
*/
reads: ['records'],
/* 归一与 FAE 选型期间摆**选型工作单**;判稳转正后才换收口那张清单 */
view: 'alt-worksheet',
tools: [],
/*
* 对标表有行了才算办完。**不能写 `benchmark_total != null`** ——
* S-FEEL 里未绑定的名字 `!= null` 求值为 true,那一步开局即被收掉(A1 踩过,测里拦下的)。
* 归一完才算办完:两边对上 —— alt_line_total 自报、rows 基座现数。
* **不能写 `records != null`**:S-FEEL 未绑定名字 `!= null` 为 true,开局即收(A1 踩过)。
*/
doneWhen: 'benchmark_total > 0',
guide: `**照这个骨架发,一个字段都别少**(prose 描述不管用,直接抄):
{"kind":"human","itemId":"T_select","workOrder":"execution","card":{
"title":"…","ask":"这颗料原型号停产,请挑替代并给对标结论",
"basis":[{"k":"…","v":"…"}],
"blocks":[
{"t":"table","title":"初筛清单","cols":["原型号","候选型号","品牌","生命周期"],"editable":true,
"rows":[["LM358DR","LM358BIDR","",""],["LM358DR","TLV9062IDR","",""]]},
{"t":"table","title":"替代对标表","cols":["候选型号","替代等级","参数差异项","差异风险","验证建议","FAE签字"],"editable":true,
"rows":[["","","","","",""]]}
]}}
**rows 必须有行**:一条料一行,不知道的值给空字符串(界面自己标红「待填写」)。
只给 cols 不给 rows,人看到的是一排表头加一片空白,一个格子都填不了(实测栽过)。
**blocks 少了整张卡就废了** —— 基座会用当下局面兜底成一句「需要你判断下一步」,
人拿到一张没有表的空卡,这一步等于没派(实测栽过)。
doneWhen: 'alt_line_total > 0 and quote_alternative_list_rows = alt_line_total',
guide: `第一件事:把这一单的**选型底表**落出来 —— 整份承继,不是只有 C 类。
工作区这一步摆的是**替代选型工作单**(逐颗料的选型档案与填回区),你落的行就是它的血肉。
**候选是你提的,不是让 FAE 从零想** —— 初筛清单每个原型号先摆 **2~3 行候选**
(原型号 + 候选型号两列你填,你认得这些料;品牌与生命周期留空让他核)。
判据「2~3 颗分级候选经确认」机检的就是候选条数,只给一行**过不了门**。
替代对标表照候选逐行起,basis 摆上原型号、芯片类型、客户特殊要求(**裸卡基座拒发**)。
【料从哪儿来】
· 流内(任务数据里有 \`records\` 数组):那是上游全部落地行**混在一起**的一份 ——
同一颗料会出现两三次:带 eau/brand/target_price 的是**受理记录行**(以它为准),
带 basis/history_days/market_stable 的是分类表行(它的 type_tag 与受理记录一致),
带 line_total/type_a_total 这类总数的是结论行(不是料,别落进清单)。
**按 pn 归并**:一颗料归一成清单里的一行,列取受理记录行的值;
type_tag 照抄成本清单的 **orig_type(原标)与 type_tag(现标)两列同一个值**。
· 单飞(没有 \`records\`):照入参与正文归一 —— 有型号落型号;只给芯片类型与参数、
没有具体 PN 的,\`pn\` 写「类型-场景」需求代号(如「运放-车规-01」),
这类需求本来就是来选型的,orig_type 与 type_tag 都写 C。
他提交后回传值进你的轨迹:表格的键是 **table:0 / table:1**,值是整张表的 JSON,
**第一行是列名**。照它 write 进 \`shortlist\` 与 \`benchmark\`,
**同一拍再写一行 \`quote_alternative.verdict\`**(验收机检读的就是它):
· candidate_total = 初筛清单条数(要 ≥2);
· benchmark_total = 对标表条数;
· undocumented_total = 对标表里「参数差异项」或「差异风险」还空着的条数(要 = 0);
· recommend_pn 主推候选、summary 一句人话结论。
这三个数写不对,submit 会被门挡回来 —— **它们不是台账装饰,是判据本身**。
**写完就 finish,别再发第二张单** —— 他已经答过了。
**每一行都要带 \`rfq_ref\`** —— 它是这一单的单标识(起单入参里就有,原样抄进去)。
台账主键第一列就是它:不写,这一行会和**别的单**同料号的那一行撞主键,把人家覆盖掉
(实测栽过:A4 的报价草案主键只有 pn,跑四轮只剩最后一轮的 3 行)。`,
【单号 rfq_ref】入参有 \`rfqRef\` 就**原样照抄**;没有就从主题/正文里认(RFQ-xxxx);
认不出才造一个稳定案号「ALT-<型号或类型>」—— 它是本活动所有台账行的主键,
造完这一单从头用到尾,**不许中途换写法**(换一个写法就是新开一行)。
造号**只从本单来件取材,不许从台账里已有的别单单号里挑** —— 正式表里躺着的
单号是别人的单,借它的号等于把两单的行混进同一个案卷。
【逐列怎么落】写 \`quote_alternative.list\`,一颗料一行,一拍写完(rows 收数组):
pn(**一个字符不许改**)、customer_pn、category、brand、eau、target_price、lead_time、
**remark(客户备注:预授权替代、认证要求、禁用约束 —— 选型的第一手依据,一个字别丢)**
照上游值抄,没有的留空字符串(**绝不写 0 顶空**);orig_type / type_tag 照抄原标
(**只写 A、B、C 三个大写字母之一**,写别的基座数不出来);
C 类行把「为什么是 C」写进 basis(从分类表行的 basis 抄,或客户点名替代的原话);
alt_pn / alt_brand / alt_grade / alt_diff / alt_risk / alt_verify / alt_note
**全部留空** —— 那是 FAE 的定案,你不许编。
【同一拍记账】写 \`quote_alternative.verdict\`(一单一行):
rfq_ref、customer_code / customer_name(顶层摊平字段里有就抄,没有留空)、
**alt_line_total** = 清单行数、**c_total** = C 类条数。
写完 finish 这一项 —— 引擎按「自报条数 = 基座行数」收步,少拆多写都过不去。`,
},
{
item: 'S2_fill',
intent: '一张选型执行单交 FAE:工作单一颗一页,定案(型号/品牌/等级)逐页填回',
writes: ['quote_alternative.verdict'],
reads: ['quote_alternative.list'],
view: 'alt-worksheet',
tools: [],
/*
* 不给 doneWhen —— 发卡触达真人(manualActivation),引擎不替人按;
* 「FAE 选完填回来了」也判不出来:它是人环,不是台账里能自己变出来的数。
*/
guide: `底表归一好了,**该 FAE 出场了**。规矩(2026-08-02 用户定):
**一张执行单,工作单里一颗待选型芯片一页** —— 他在右上角翻页逐颗定案,
定案三格(型号/品牌/等级)是每页的必填,缺着按不下提交(系统的闸看着)。
【怎么发】照这个骨架发一张卡(它标了人工激活:发卡就算启动,不必单发 start;
引擎也不会替你收 —— 人交回后要你自己 finish)。卡面是纯文本,
steps/standard 里**别写星号记号**:
{"kind":"human","itemId":"S2_fill","workOrder":"execution","card":{
"title":"替代选型:<单号> 待选型 <N> 颗",
"ask":"这几颗料要替代:请照工作单逐页的选型档案在系统外完成选型,把定案填进各页右栏的「替代定案」(型号/品牌/等级是必填,缺着按不下提交),填完保存,回这张卡提交;确实找不到替代的,在那一页的选型备注写明缘由后交回。",
"steps":["工作单一颗一页,右上角翻页;每页「替代定案」栏里给出替代料型号与品牌,替代等级点三档之一:P2P(同封装同引脚可直换)/ FUNC(功能兼容,需外围电路或 layout 调整)/ OTHER(部分兼容,须打样验证)",
"P2P 必须逐项核对过五样:封装尺寸、引脚定义、电气参数、温度等级、认证 —— 以原厂官网规格书为准,同封装不等于同引脚;核对过哪几项写进参数差异项",
"FUNC 或 OTHER 的,参数差异项与差异风险写清差在哪、要动什么;验证建议写明要不要打样、验什么",
"客户备注里已预授权的替代优先采信;点名禁用的品牌不得作候选;确实找不到替代:在那一页的选型备注写明缘由(那也是一个答案);填完保存,回这张卡提交"],
"standard":"每一颗:型号、品牌、等级都有且来源可溯(三格必填,闸看着);或备注写明替代未果的缘由。交回后判稳定标由系统接着办。",
"basis":[{"k":"<待选型 PN 之一>","v":"<为什么是 C + 客户约束(预授权/认证/禁用)>"}]}}
· \`basis\` 逐颗列待选型的料(几颗写几条);标题带单号与颗数。
· **卡上不摆格子**(填写全在工作区):定案填在工作单各页的「替代定案」栏里,
不往卡里塞 \`fields\` 或 \`blocks\`。
发完卡挂起,这一拍到此为止。**这张卡一单只发一次** —— 必填有提交闸兜着,
卡回来时每颗要么三格齐、要么备注写明未果。
【人交回之后】**他的定案已经在台账里了** —— 他在工作单上按「保存」那一刻,
基座就把那几格落进了草稿层(该由人亲手落下的,不经你的手)。
**不许再把那几格 write 一遍** —— 值相同的写入被「写了等于没写」拒(实测)。
这一拍只做两件事,然后 finish:
① 清点 alt_pn 有值的颗数写 **filled_total** 进 verdict(全部未果就是 0,照实写);
② finish 这一项,**这一拍到此为止** —— 判稳、转标、提交都不是这一拍的活:
转标还没做,submit 排进这一串必被「判据没过」拒回(实测白挨一次)。
**这一拍也不许调 crm.history-price-read** —— 查行情是判稳那一步(下一步)开头的活;
实测提前查过一次:查完没处落,连着四拍把同一行原样重写、四次被
「写了等于没写」拒回,白烧四拍才想起该 finish。`,
},
{
item: 'S3_reclass',
intent: '替代料批量查历史行情:稳转 A、不稳转 B;清单转正,立去向旗',
writes: ['quote_alternative.list', 'quote_alternative.verdict'],
reads: ['quote_alternative.list'],
view: 'alt-roster',
tools: ['crm.history-price-read'],
/*
* **收步判据与验收判据同源**:C 类清零(基座按值现数)才算这一步办完 ——
* 收步比验收松,松出来的那一截就是返工(A1 的账)。
* route_decided_total 是本步显式写下的记账数(布尔旗分不清「没写」与「写了 false」)。
*/
doneWhen: 'quote_alternative_list_type_tag_c = 0 and route_decided_total > 0',
guide: `FAE 填回来了,现在把清单**转正**:每颗替代料判稳、定标,清单从「受理记录」
变成「询价受理清单」。
【第一拍:查行情】把所有原标 C 行的 **\`alt_pn\` 那一列的值**(替代料的料号)收进
一个数组,调 \`crm.history-price-read\` 一次传完(参数 pns 收数组)。
**判稳的行情只有这一条来路 —— 这一拍亲自查 alt_pn。**两条近路都不许走:
· **不许传原 PN**(pn 列是被替代的旧料,查它查到的是旧料的行情);
· **不许拿上游带进来的数顶替**:入参里若躺着 history_days / market_stable /
lifecycle,那是 A1 当时查**原 PN** 得的账 —— 跟这颗**替代料**稳不稳没有关系。
实测抄过一次近路:上游写着 9999,模型没调工具就把替代料判成 B,
而那颗替代料库里明明有 43 天的稳定历史,整单去向就此走岔。
调完这一拍就结束(工具出参要下一拍才看得见)。
【第二拍:定标 + 记账 + 收步,一拍连做】
① **规则写死,不留裁量**(与三分类 P-03 的 A/B 两行同一把尺):
historyDays ≤ 90 且 marketStable = true → **type_tag 转 A**;否则 → **转 B**。
工具回的 lifecycle 若是 EOL/缺货/要求替代 —— 那说明替代料选错了,**不转标**,
当成替代未果处置(见③)。
② 逐行写回 \`quote_alternative.list\`(按 rfq_ref + pn 对行,主键一个字符不许改):
type_tag(A 或 B,**照规则写,不许臆断**)、history_days(工具出参照抄)、
basis(一句依据:「替代料 <alt_pn> 历史价 <N> 天·行情稳 → 转 A」这样)。
**先对号再用**:工具出参每行带着查询的 pn —— 那个 pn 必须等于这一行的 alt_pn,
对不上就是第一拍传错了列,回去重查,别把别人的行情安到这颗替代料头上。
orig_type **不动** —— 原标是留痕,转的是现标。
③ **替代料留空(FAE 判未果)的 C 行**:
· 选型备注写了缘由 → 用 \`drop\` 把那一行撤出本单草稿(主键照 \`_keys\` 名单**原样粘**),
把撤了哪几颗、为什么写进 verdict.skipped_note,并把 alt_line_total 改成撤后的行数
(清单条数跟着改对,别让「条数 5 而表里 4 行」把归一那步的判据顶翻);
· 备注也空着 → 发一张**决策卡(decide)**问负责人:「这颗料替代未果,剔出本轮
还是退回整单」两个选项摆出来,人点哪个照哪个办。**只问这一次**
(补全卡已退役 —— 二选一是处置,本就该用决策卡)。
④ 写 verdict:converted_a_total / converted_b_total(转成 A/B 各几条,照②的实数)、
**need_sourcing**(清单里还有 B 类 → true,全 A → false;照基座读数
quote_alternative_list_type_tag_b 的正负写,别凭记忆)、**route_decided_total 记 1**、
summary 一句人话(几颗替代、转成什么、往哪走)。
⑤ finish 这一项,**这一拍到此为止**。引擎按「C 类清零 + 去向定过」收步 ——
有一颗 C 没处置,这一步收不掉,出路只有三条:转标(填了替代料)、
剔除(未果且有说明)、问人(未果且没说明)。
**别把 submit 排进这一串**:放行那一步要先把 L3 裁量裁过才轮得到 submit,
跳过它会被当场拒回(实测白挨过一次)。
**去向不用你选,也不用人选**:契约给两个出口各写了判据(还有没有 B 类),
放行那一刻基座按序求值自己定;need_sourcing 那枚旗是给流程包的边读的,
两处读的都是同一个事实 —— 你只负责把标转对。`,
},
{
item: 'T_release',
intent: '对标结论已齐,提交放行交 BD 验收',
view: 'alt-roster',
intent: '清单已转正,提交放行交 FAE 验收',
writes: [],
reads: [],
tools: [],
// 放行触达世界 —— 引擎不自动收它(所以这里不给 doneWhen)
guide: `对标表有行了 —— **先 start T_release**(它标了人工激活),然后 submit,BD 验收。
/* 放行触达世界,必须人按一下 —— 引擎不自动收它(剧本里也不给 doneWhen) */
guide: `清单整份转正、去向已定 —— 可以走放行了(T_release 人工激活:
动它就算启动,不必单发 start)。先把裁量判据裁掉,然后 submit,同一拍连做:
· judge criterion="替代料来源可溯且选型说明已留:以原厂官网规格书或客户预授权为准,每颗替代写明凭什么" ——
看清单里每颗替代料的 basis 与选型备注:都写了依据就 satisfied=true;
有替代料说不出来源的,satisfied=**false**,rationale 逐颗写清哪几颗没依据,
交签字的人定。**裁成不满足照样能提交** —— 理由连着卡一起交给他,那正是这条判据的用处。
criterion 照判据名**整句原样抄**,写别的名字对不上。
**去向不用你裁**:两个出口各写了判据,放行那一刻基座按序求值、首个命中即出。
终局唯放行:你不能自己宣布完成。`,
},
];
/**
* 阶段 —— 进度条的刻度,只给人看。除放行外每个计划项恰属一个阶段;
* 放行那一格由基座统一加(CLOSING_PHASE),人看到的是 x/4。
*/
export const ALTERNATIVE_PHASES = [
{ name: '归一清单', items: ['S1_normalize'] },
{ name: '选型填回', items: ['S2_fill'] },
{ name: '判稳定标', items: ['S3_reclass'] },
];
+12 -58
View File
@@ -1,62 +1,16 @@
/**
* 统一报价 A2 · 替代选型 —— 活动包代码入口(ADR-004)。
* loop 活动:调度权归 AI;本文件只做视图流控与「差异没核清不放行」的把关呈现。
* 统一报价 A2 · 替代选型 —— 活动包代码入口(ADR-004:默认导出 defineActivity 的返回值)。
*
* **这个活动没有 `run`,不是缺件。**
*
* 案例活动的推进由 Loop 驱动,行为全部写在清单里:案例模型(CMMN 子集)+ 案例说明书 +
* 步骤剧本 + 两条技能 + 台账声明 + 一张 HTML 报告模版。「清单机器可读,
* 系统不执行代码即知包为何物」。上一版这里有 62 行 `run()` —— loop 活动根本不会调它,
* 还在读一个契约里早已退场的 `originalPn`:摆一个空转的 run 去假装有实现,更坏。
*
* 这个文件之所以还存在,是因为清单的 `entry` 指着它 —— 它是**被静态审扫描的产物文本**
* (查 import 与全局引用,证明活动包零环境能力)。
*/
import { defineActivity } from '@sdk';
import type { ActivityBase, ActivityOutcome } from '@sdk';
const SHORTLIST = 'quote_alternative.shortlist';
const BENCHMARK = 'quote_alternative.benchmark';
export default defineActivity({
name: 'quote-alternative',
async run(base: ActivityBase): Promise<ActivityOutcome> {
const shortlist = await base.ledger.read(SHORTLIST);
const benchmark = await base.ledger.read(BENCHMARK);
// 差异没核清的不放行 —— 禁区的机检形态
const unclear = benchmark.filter((r) => String(r['risk'] ?? '').trim() === '');
await base.interaction.pushView([
{
t: 'kv',
title: '选型进展',
items: [
{ k: '原型号', v: String(base.contract.data['originalPn'] ?? '') },
{ k: '初筛候选', v: `${shortlist.length} 颗` },
{ k: '已对标', v: `${benchmark.length} 颗` },
{ k: '差异未核清', v: `${unclear.length} 颗` },
],
},
{
t: 'cards',
title: '候选替代',
items: benchmark.map((r) => ({
tag: String(r['grade'] ?? '待定级'),
title: String(r['candidate_pn'] ?? ''),
swap: { from: String(r['original_pn'] ?? ''), to: String(r['candidate_pn'] ?? '') },
big: String(r['grade'] ?? '—'),
bigLabel: '替代等级',
note: String(r['risk'] ?? '差异风险未写'),
tone: String(r['risk'] ?? '').trim() === '' ? 'danger' : 'ok',
})),
},
]);
if (benchmark.length < 2) {
await base.interaction.say(
shortlist.length === 0 ? '尚未撒网,先拉初筛清单。' : `已初筛 ${shortlist.length} 颗,继续逐条核验分级。`,
'conclusion',
);
return { kind: 'await', reason: '候选不足 2 颗' };
}
if (unclear.length > 0) {
await base.interaction.say(`${unclear.length} 颗候选的差异风险未写清 —— 差异没核清的不放行。`, 'criterion');
return { kind: 'await', reason: '差异风险未核清' };
}
await base.interaction.say('候选已分级、差异与风险已写清,可签字交回发起方。', 'criterion');
return { kind: 'submit', deliverable: 'benchmark-table', rationale: '2 颗以上分级候选,差异与风险齐备' };
},
});
export default defineActivity({ name: 'quote-alternative' });
+402 -124
View File
@@ -1,127 +1,222 @@
/**
* 活动包清单 —— 统一报价 A2 · 替代选型(**案例活动 · loop**)。
* 活动包清单 —— 统一报价 A2 · 替代选型(案例活动 · MVP 简化形)。
*
* 业务素材:docs/MVP示例工作流.html §1.6。
* `▶需拍板`(签字交出)→ manualActivation + 放行;`✋客户改需求` → userEventListener;
* `⏱时限过半` → 时间监听(**时限必配**)。
* 业务素材:`docs/MVP示例工作流.html` §1.5,按 2026-08-02 用户改盘的口径归一:
* **A2 是复杂活动(需要大规模业务建模),MVP 期间不展开** —— 只做一张精美的
* HTML 验收报告 + 一张执行单:AI 挑出 C 类、组好清单交 FAE 在系统外选型填回,
* 替代料查库内行情判稳(稳转 A / 不稳转 B),询价受理记录就此转换成
* **询价受理清单**传给下游。提交就是输出。
*
* ── 这里**去掉了 `GRADE_DMN`(替代等级判定表)**(2026-08-02)────────────────
* 它声明着三个入参(pinCompatible / packageSame / paramDelta),而台账上没有任何列
* 承载它们、剧本里也没有一步调它 —— **一张没有求值器的判定表不是资产,是错觉**:
* 看清单的人以为等级是算出来的,实际全程没人算过。MVP 口径下替代等级的判断
* 整个在 FAE 脑子里(系统外),系统不假装接得住;判稳转标那一步的规则
* (≤90 天且稳 → A,否则 → B)写死在剧本里、依据逐行落表,可回核。
*/
import type { ActivityManifest, DmnModel } from '@contracts';
import { ALTERNATIVE_SKILLS } from './skills.ts';
import { ALTERNATIVE_CASE } from './views/report.ts';
import { activityIdSchema } from '@contracts';
import { ALTERNATIVE_STEPS, CMMN_XML, NARRATIVE } from './case-model.ts';
import { ALTERNATIVE_SKILLS } from './skills.ts';
import { ALT_ROSTER, ALT_WORKSHEET } from './views/report.ts';
import { ALTERNATIVE_PHASES, ALTERNATIVE_STEPS, CMMN_XML, NARRATIVE } from './case-model.ts';
const DOMAIN = 'quote_alternative';
const D = 'quote_alternative';
const ACTIVITY_ID = activityIdSchema.parse('quote-alternative');
/** 替代等级 —— 把「装得下的判断」从 LLM 手里拿走 */
export const GRADE_DMN: DmnModel = {
drd: [{ decision: 'alt-grade', requires: [], inputs: ['pinCompatible', 'packageSame', 'paramDelta'] }],
tables: [
{
id: 'alt-grade',
label: '替代等级判定',
hitPolicy: 'FIRST',
inputs: [
{ name: 'pinCompatible', expression: 'pinCompatible' },
{ name: 'packageSame', expression: 'packageSame' },
{ name: 'paramDelta', expression: 'paramDelta' },
],
outputs: [{ name: 'grade' }, { name: 'effort' }],
rules: [
{ id: 'g-p2p', when: ['true', 'true', '<= 0.05'], then: ['P2P', '核确免改动'] },
{ id: 'g-func', when: ['-', '-', '<= 0.20'], then: ['功能兼容', '评外围电路与 layout'] },
{ id: 'g-other', when: ['-', '-', '-'], then: ['其他', '须验证'] },
],
},
],
};
/** MVP 口径:A2 没有判定表(见文件头注释)—— 判稳规则写死在剧本,依据逐行落表 */
export const ALT_DMN: DmnModel = { drd: [], tables: [] };
export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'A2 · 替代选型',
packageVersion: '2.0.0',
packageVersion: '4.16.0',
category: '统一报价',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-quote-alternative',
},
/* ── 契约七件 ────────────────────────────────────────────────── */
contract: {
activityId: ACTIVITY_ID,
version: '0.1.0',
version: '1.0.0',
input: {
/*
* 入参对齐**上游真发的东西**(2026-07-31,MVP 第一轮跑出来的)。
* **宽进到底:一个必填都不设**(2026-08-02 用户当场定:「活动是宽进严出,
* 输入接受任何类型」「活动的输入契约是在活动内部归一+补全的」)。
*
* A1 的 `to-alternative` 口发的是 `altRequests`(一批替代评估请求,一条料一行),
* 不是单个 `originalPn` —— 而这里从前把 `originalPn` 写成必填,
* 于是流程走到 A2 就停在补全卡上:「缺 originalPn」。
* 上游发一批,下游要一个,这是**契约对不上**,不是数据缺失。
* 来路本就有两种形状:流内(上游放行载荷 —— rfqRef + records + 一堆摊平字段)
* 与单飞(一段需求文字,可能连具体型号都没有)。门口拦谁都不对 ——
* 归一是第一个计划项的活,缺什么在活动内问。
*
* 一单可能要替代好几颗料,`altRequests` 才是那件事的真形状。
* 上一版必填 `rfqRef + altRequests`:而 `alt_request` 表 2026-08-02 已在 A1 退休,
* 那个键只是载荷数组通配规则下的空壳(内容与 records 完全相同)——
* 必填一个不存在的东西,单飞起单永远停在补全卡上。
*/
data: {
type: 'object',
required: ['rfqRef', 'altRequests'],
required: [],
properties: {
rfqRef: { type: 'string', title: '询价单号(单标识,贯穿全流程)' },
altRequests: { type: 'array', title: '替代评估请求(原 PN + 替代要求)' },
originalPn: { type: 'string', title: '原型号(单条时的便捷入口)' },
chipType: { type: 'string' },
scenario: { type: 'string' },
rfqRef: { type: 'string', title: '询价单号' },
records: { type: 'array', title: '上游受理记录(已打标)' },
subject: { type: 'string', title: '需求主题' },
body: { type: 'string', title: '需求正文' },
attachments: {
type: 'array',
format: 'file',
title: '附件',
items: {
type: 'object',
properties: {
name: { type: 'string', title: '文件名' },
contentType: { type: 'string', title: '类型' },
size: { type: 'integer', title: '字节数' },
ref: { type: 'string', title: '存档号' },
text: { type: 'string', title: '读出来的文本' },
note: { type: 'string', title: '读取说明' },
},
},
},
},
},
messageAllowed: true,
messageHint: '客户点名的替代授权、禁用品牌、场景约束等写这里',
},
output: {
/*
* 出口载荷**从落地事实推导**:标量字段必须有台账列承载。
* `needSourcing` ← verdict.need_sourcing —— 流程包 E5(仍需寻源 → A3)读的
* 就是它;上一版声明了这个键而台账没有承载列,**E5 从来没有成立过**:
* 选完型仍要寻价的单一律被默认边送去定价(2026-08-02 探查实证)。
*/
mandatory: [
{
name: 'benchmark',
payload: { type: 'object', required: ['rfqRef', 'rows'], properties: {
/*
* **下游那条边读的就是它**(流程包 E5:A2→A3 `needSourcing = true`)。
* 从前这个口只发对标表,而边上写着 needSourcing —— 那条边永远不成立,
* 选完型再要寻价的单子出了 A2 就没路可走(与 A1 当年的 G1 全流死路同一种错)。
*/
needSourcing: { type: 'boolean', title: '选定的替代料仍需寻价' }, rfqRef: { type: 'string' }, rows: { type: 'array' } } },
description: '替代对标表(已签字)',
name: 'roster',
payload: {
type: 'object',
required: ['rfqRef', 'rows', 'needSourcing'],
properties: {
rfqRef: { type: 'string', title: '询价单号(单标识 —— 下游台账的主键)' },
rows: { type: 'array', title: '询价受理清单(C 类已转 A/B)' },
needSourcing: { type: 'boolean', title: '仍需寻源(清单里还有 B 类)' },
},
},
description: '询价受理清单(C 类已转 A/B)+ 寻源旗',
},
],
/*
* **两条出边 = 一个 XOR 选择组**(对齐流程包 E5/E6)。
* 每一口都写了 `when`(读基座按值现数的 tally 读数),这一单往哪走是**算出来的**:
* 放行时基座按序求值首个命中即出,卡上只如实告诉人走哪一口、凭哪一条。
*/
selective: [
{
group: 'alt-route',
ports: [
{
name: 'to-sourcing',
payload: {
type: 'object',
required: ['rfqRef', 'rows'],
properties: {
rfqRef: { type: 'string', title: '询价单号' },
rows: { type: 'array', title: '询价受理清单(含待补价的 B 类)' },
},
},
description: '清单里仍有 B 类 —— 去寻源补货源价格',
when: 'quote_alternative_list_type_tag_b > 0',
},
{
name: 'to-pricing',
payload: {
type: 'object',
required: ['rfqRef', 'rows'],
properties: {
rfqRef: { type: 'string', title: '询价单号' },
rows: { type: 'array', title: '询价受理清单(全 A 类)' },
},
},
description: '全 A 类:行情完备 —— 直通成本定价',
when: 'quote_alternative_list_type_tag_b = 0',
},
],
},
],
selective: [],
},
/*
* 必交付物**必须给 `ledger` 地址**(2026-07-31 修)。
* 不给的话预检退回 `scope[key]` 去找 —— key 是 `benchmark-table` 这样的短横线别名,
* 任务视图里永远没有这个键,于是「必交付物缺一不得提交放行」永远拒,
* 活办完了也放不了行,连拒五拍熔断(实测 A2 死于此)。
* 必交付物只有一件:**询价受理清单**。选型结论(verdict)是它的结论栏,
* 不是第二份成果 —— `ledgers` 说清这份成果由哪几张表承载(A1 的账:
* 各占一条,人在成果面上会看到两样东西)。
*/
requiredDeliverables: [
{ key: 'benchmark-table', label: '替代对标表(FAE 签字)', ledger: `${DOMAIN}.benchmark` },
{
key: 'alt-roster',
label: '询价受理清单(C 类已转 A/B)',
ledger: `${D}.list`,
/*
* 验收页上让 FAE 就地改的那几列 —— 恰是**他的职权面**:
* 替代料三列是他填的,现标 `type_tag` 是判稳规则出的 ——
* 但算得对不对该由人说了算,他改一颗料的标就是在推翻一次判定(A1 的账:
* 不在名单里的列压根不进写回载荷,人改了等于没改)。
* 去向不在其列:它是现标的函数,改标去向就跟着变。
*/
editableAtRelease: ['alt_pn', 'alt_brand', 'alt_grade', 'alt_diff', 'alt_risk', 'alt_verify', 'alt_note', 'type_tag'],
ledgers: [`${D}.list`, `${D}.verdict`],
},
],
/*
* 机检谓词读的是 `verdict` 那张**汇总表**的列(照 A1 的口径)。
*
* 上一版写的是 `candidatesConfirmed >= 2` 与 `undocumentedDiff = 0` ——
* 这两个名字**台账里根本没有**(既不是案卷项,也不是任何一张表的列)。
* S-FEEL 拿未绑定的名字比大小求值为假,于是这两条判据**永远达不了标**:
* Agent 把活办得再干净也过不了 submit 门,连拒五拍熔断 —— 实测 A2 就死在这儿。
* 判据的名字必须落在台账上,否则「机检」是一句空话(2026-07-31 销账)。
* 验收判据 —— **机检(L1/L2)只读基座数得出来的数**(A1 被违反三次的那条纪律):
* `quote_alternative_list_rows` 基座数行数、`_type_tag_a/b/c` 基座按取值域现数。
* 上一版两条 L2 读的是活动自报的 verdict 列(candidate_total / undocumented_total)——
* 那不是门,是「改哪个数能过」的提示。
*/
acceptanceCriteria: [
{ key: 'candidates', label: '2~3 颗分级候选经确认', level: 'L2', predicate: 'candidate_total >= 2' },
{ key: 'diff-clear', label: '差异项与风险逐项写清', level: 'L2', predicate: 'undocumented_total = 0' },
{ key: 'source-traceable', label: '每项数据来源可溯(以原厂官网规格书为准)', level: 'L3' },
{ key: 'fae-sign', label: 'FAE 签字放行', level: 'L5' },
{
key: 'roster-complete',
label: '清单齐:上游受理记录逐条承继,说了几条清单里就躺着几行',
level: 'L1',
predicate: 'alt_line_total > 0 and quote_alternative_list_rows = alt_line_total',
},
{
key: 'no-c-left',
label: '选型办完:每颗待选型的 C 类料都已填回替代并转成 A 或 B,或经确认剔除',
level: 'L1',
predicate: 'quote_alternative_list_rows > 0 and quote_alternative_list_type_tag_c = 0',
},
{
key: 'tag-conserved',
label: '定标守恒:清单每一行都有现标,A 类加 B 类恰等于总行数',
level: 'L2',
predicate:
'quote_alternative_list_type_tag_a + quote_alternative_list_type_tag_b = quote_alternative_list_rows',
},
/*
* 来源可溯是**裁量**(L3):替代凭什么、说明留没留,机器数不出来 ——
* 别假装机器判得了。裁成不满足照样能提交,理由连着卡交给签字的人。
* 这句 label 与剧本里 judge 的 criterion **必须逐字一致**(judge 按整句对名)。
*/
{
key: 'alt-traceable',
label: '替代料来源可溯且选型说明已留:以原厂官网规格书或客户预授权为准,每颗替代写明凭什么',
level: 'L3',
},
/* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */
{ key: 'fae-release', label: 'FAE 放行:签下去这份清单即转正并流向下游', level: 'L5' },
],
/* 去向不给人改(现标的函数);人在验收页改的是成果本身(editableAtRelease) */
revisableAtRelease: [],
prohibitions: [
{ key: 'no-unclear-diff', statement: '差异没核清的不放行' },
{ key: 'no-banned', statement: '客户技术画像禁用清单里的型号不得作候选' },
{ key: 'no-fake-alt', statement: '没有把握的替代不填:宁可在选型备注写明替代未果,也不编一个候选顶上' },
{ key: 'no-banned', statement: '客户点名禁用的品牌与型号不得作替代' },
{ key: 'no-tag-fudge', statement: '不许为过判据改标:现标只能来自判稳规则,或人在件上的亲手修改' },
],
authorization: {
discretion: [{ key: 'widen-search', statement: '候选不足 2~3 颗时可放宽参数或换角度重新撒网' }],
discretion: [
{ key: 'widen-search', statement: 'FAE 可放宽参数或换角度重新选型;是否接受非原厂品牌、参数降级必须问客户,不得替客户决定' },
],
/* 空着不是省事:这个字段全仓没有一个消费者(债 ⑨),写了也不会被执行 */
preauthorizedActions: [],
/* 一颗芯片一张选型单(按顺序派)—— 打扰上限按料数给:MVP 案例 ≤3 颗 + 放行 + 余量 */
humanCallBudget: 6,
},
resources: {
@@ -129,76 +224,121 @@ export const manifest: ActivityManifest = {
skills: ALTERNATIVE_SKILLS.map((s) => s.id),
models: ['deepseek-v4-flash'],
roles: ['FAE'],
budget: { steps: 100, tokens: 260_000, activeMinutes: 360, humanCalls: 6 },
/*
* FAE 在系统外选型,回来才继续 —— activeMinutes 给足一个工作日的等待;
* token 按实测估:每颗料一张卡各占两三拍,常路(1-2 颗)8-12 拍,留一倍余量。
*/
budget: { steps: 60, tokens: 300_000, activeMinutes: 480, humanCalls: 6 },
},
},
/* ── 声明 ────────────────────────────────────────────────────── */
declarations: {
models: ['deepseek-v4-flash'],
tools: [{ name: 'crm.history-price-read' }],
ledger: [
/*
* ── 这里**去掉了 `shortlist` / `benchmark` / `request` 三张表**(2026-08-02)──
*
* MVP 口径下初筛清单与对标表都不在系统内产出 —— 选型的实质工作在 FAE
* 系统外,系统收的是**结论**(替代料是哪颗、凭什么),落在清单行的三列上。
* 撤掉声明不 drop 物理表(物化只增不破坏),历史行还在库里。
*/
{
domain: DOMAIN,
object: 'shortlist',
domain: D,
object: 'list',
kind: 'record',
caseKey: 'rfq_ref',
label: '初筛清单',
label: '询价受理清单',
fields: [
{ name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
{ name: 'original_pn', type: 'text', primaryKey: true, masked: false, label: '原型号' },
{ name: 'candidate_pn', type: 'text', primaryKey: true, masked: false, label: '候选型号' },
{ name: 'brand', type: 'text', primaryKey: false, masked: false, label: '品牌' },
{ name: 'lifecycle', type: 'text', primaryKey: false, masked: false, label: '生命周期' },
],
},
{
domain: DOMAIN,
object: 'benchmark',
kind: 'record',
caseKey: 'rfq_ref',
label: '替代对标表',
fields: [
{ name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
{ name: 'original_pn', type: 'text', primaryKey: true, masked: false },
{ name: 'candidate_pn', type: 'text', primaryKey: true, masked: false },
{ name: 'grade', type: 'text', primaryKey: false, masked: false, label: '替代等级' },
{ name: 'param_diff', type: 'text', primaryKey: false, masked: false, label: '参数差异项' },
{ name: 'risk', type: 'text', primaryKey: false, masked: false, label: '差异风险' },
{ name: 'verify_advice', type: 'text', primaryKey: false, masked: false, label: '验证建议' },
{ name: 'signed_by', type: 'text', primaryKey: false, masked: false, label: 'FAE 签字' },
{ name: 'pn', type: 'text', primaryKey: true, masked: false, label: 'PN' },
{ name: 'customer_pn', type: 'text', primaryKey: false, masked: false, label: '客户料号' },
{ name: 'category', type: 'text', primaryKey: false, masked: false, label: '品类' },
{ name: 'brand', type: 'text', primaryKey: false, masked: false, label: '制造商' },
{ name: 'eau', type: 'integer', primaryKey: false, masked: false, label: '年用量 EAU' },
{ name: 'target_price', type: 'number', primaryKey: false, masked: false, label: '目标价' },
{ name: 'lead_time', type: 'text', primaryKey: false, masked: false, label: '交期' },
/* 客户备注承继自上游 —— 预授权替代、认证要求、禁用约束都在这一格,是选型的第一手依据 */
{ name: 'remark', type: 'text', primaryKey: false, masked: false, label: '客户备注(约束/预授权)' },
/*
* **原标与现标两列同名不同物**:orig_type 是上游受理时的三分类(留痕,本活动不改);
* type_tag 是本活动的产物(C 类判稳后转 A/B)。两列都声明取值域,
* 条数由基座按值现数 —— 判据、分布条、出口谓词读的全是基座读数。
*/
{ name: 'orig_type', type: 'text', primaryKey: false, masked: false, label: '原标(上游三分类)', tally: ['A', 'B', 'C'] },
/*
* **定案三格是待选型行的必填**(2026-08-02 用户:「工作区的 HTML 必填项
* 必须填写了才能提交啊,不填写能算任务完成了吗?」)。
* `requiredWhen` 把必填钉在 orig_type=C 的行(一张执行单、件里一颗一页,
* 每页的定案三格都得齐);A/B 行原样承继,不拦。
* 「替代未果」不走「留空交回」—— 那在闸上就是空手交卷:
* 未果 = 在选型备注写明缘由后交回,判稳那一步按剔除处置(剧本 §S3)。
*/
{ name: 'alt_pn', type: 'text', primaryKey: false, masked: false, label: '替代料 PN', requiredWhen: { column: 'orig_type', value: 'C' } },
{ name: 'alt_brand', type: 'text', primaryKey: false, masked: false, label: '替代料品牌', requiredWhen: { column: 'orig_type', value: 'C' } },
/*
* **替代等级与对标三列**(2026-08-02 用户指出「执行单完全是验收单了」后补齐):
* 选型的定案不只是一个型号 —— FAE 的专业产出是**等级 + 差异 + 风险 + 验证建议**
* (照 MVP 素材对标表的口径)。等级用三个机器码(取值域声明,基座按值现数):
* P2P = 同封装同引脚可直换(核对五项:封装尺寸/引脚定义/电气/温度等级/认证);
* FUNC = 功能兼容(需外围电路或 layout 调整);
* OTHER = 其他(部分兼容,须打样验证)。
* 人读名由模版样式表翻译(徽上色 + 图例),FAE 在卡与件上照码填。
*/
{ name: 'alt_grade', type: 'text', primaryKey: false, masked: false, label: '替代等级(P2P/FUNC/OTHER)', tally: ['P2P', 'FUNC', 'OTHER'], requiredWhen: { column: 'orig_type', value: 'C' } },
{ name: 'alt_diff', type: 'text', primaryKey: false, masked: false, label: '参数差异项', expect: true },
{ name: 'alt_risk', type: 'text', primaryKey: false, masked: false, label: '差异风险与改动评估', expect: true },
{ name: 'alt_verify', type: 'text', primaryKey: false, masked: false, label: '验证建议', expect: true },
/*
* 选型备注是**我方写的注**,不标 expect —— 标了 `_gaps` 恒大于 0,
* 那条门就废了(A1 的 remark 同款教训)。
*/
{ name: 'alt_note', type: 'text', primaryKey: false, masked: false, label: '选型备注(来源/预授权)' },
{ name: 'type_tag', type: 'text', primaryKey: false, masked: false, label: '现标', tally: ['A', 'B', 'C'] },
{ name: 'basis', type: 'text', primaryKey: false, masked: false, label: '转标依据' },
{ name: 'history_days', type: 'integer', primaryKey: false, masked: false, label: '替代料距上次报价天数' },
/*
* **选型排程**(2026-08-02 用户定:「多个芯片按顺序派选型单」)——
* 一颗芯片一张选型单,按序派:WAIT 排队 / NOW 当前在办 / DONE 已定案。
* 只有待选型的料写这一列(A/B 行留空,读数自然为 0);工作单照它组画面
* (队列丸、当前颗的档案、填回表只显当前行),判断全在样式表。
* 它是排程不是编辑面 —— 不进 data-col,人改不到。
*/
],
},
{
/*
* 选型结论 —— 一单一行的**汇总表**,验收机检谓词的入参就在这儿。
* 汇总列与逐行列**故意不同名**:任务视图把草稿行摊平到顶层是「后写的赢」,
* 拿逐行列求值只会验到最后写的那一行,那不叫机检、叫抽样(A1 的教训)。
* 选型结论 —— **一单一行**:汇总数(报告要素卡与哨兵读)、
* 去向旗(流程包 E5 唯一读的东西)、一句人话结论。
*/
domain: DOMAIN,
domain: D,
object: 'verdict',
kind: 'record',
caseKey: 'rfq_ref',
label: '选型结论',
fields: [
{ name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
{ name: 'original_pn', type: 'text', primaryKey: false, masked: false, label: '原型号' },
{ name: 'candidate_total', type: 'integer', primaryKey: false, masked: false, label: '候选条数' },
{ name: 'benchmark_total', type: 'integer', primaryKey: false, masked: false, label: '对标条数' },
{
name: 'undocumented_total',
type: 'integer',
primaryKey: false,
masked: false,
label: '差异项或风险缺失条数',
},
{ name: 'recommend_pn', type: 'text', primaryKey: false, masked: false, label: '主推候选' },
{ name: 'customer_code', type: 'text', primaryKey: false, masked: false, label: '客户码' },
{ name: 'customer_name', type: 'text', primaryKey: false, masked: false, label: '客户名称' },
{ name: 'alt_line_total', type: 'integer', primaryKey: false, masked: false, label: '清单条目数' },
{ name: 'c_total', type: 'integer', primaryKey: false, masked: false, label: '待选型条数' },
/* 「FAE 的答复处理过了」的凭据 —— 只在人环走完才产生(哨兵按它放行下一步) */
{ name: 'filled_total', type: 'integer', primaryKey: false, masked: false, label: '已填回替代料条数' },
{ name: 'converted_a_total', type: 'integer', primaryKey: false, masked: false, label: '判稳转 A 条数' },
{ name: 'converted_b_total', type: 'integer', primaryKey: false, masked: false, label: '转 B(需寻价)条数' },
{ name: 'skipped_note', type: 'text', primaryKey: false, masked: false, label: '剔除了哪几颗·为什么' },
/* 流程包 E5(仍需寻源 → A3)读的就是它 —— 没有这一列,E5 永远判假(实证过) */
{ name: 'need_sourcing', type: 'boolean', primaryKey: false, masked: false, label: '仍需寻源' },
/* 去向定过了没有 —— 旗是布尔,分不清「没写」与「写了 false」,另记一个计数 */
{ name: 'route_decided_total', type: 'integer', primaryKey: false, masked: false, label: '已定去向' },
{ name: 'summary', type: 'text', primaryKey: false, masked: false, label: '选型结论' },
],
},
],
listeners: [
{
// ⏱ 时限过半:向 BD 与 PM 同步进度与风险
/** 时限过半:给 FAE 与负责人提个醒 —— 静态监听,时限必配 */
origin: 'static',
eventType: 'time',
keyFilter: { kind: 'half-deadline' },
@@ -207,7 +347,7 @@ export const manifest: ActivityManifest = {
escalation: ['nudge'],
},
{
// ✋ 客户改需求:应用条件/参数变更 → 回初选核险与圈选复核
/** 客户改需求(应用条件/参数变更):记一笔观察,回炉与否由 Agent 裁量 */
origin: 'static',
eventType: 'human',
keyFilter: { kind: 'requirement-change' },
@@ -217,35 +357,173 @@ export const manifest: ActivityManifest = {
},
],
harness: {
memoryWindow: 30,
memoryWindow: 24,
// FAE 在系统外干活,等他不算停滞 —— 停滞检测放宽到一天
stallAfter: 'P1D',
nudge: [{ after: 'PT12H', action: 'nudge' }],
retry: { times: 2, backoff: 'PT5M' },
},
},
/* ── 资产初始版 ──────────────────────────────────────────────── */
assets: {
businessModel: {
cmmnXml: CMMN_XML,
narrative: NARRATIVE,
dmn: GRADE_DMN,
/* 步骤剧本 —— 与 A1 同一套:doneWhen 真了引擎自己收步,不问模型 */
dmn: ALT_DMN,
steps: ALTERNATIVE_STEPS,
phases: ALTERNATIVE_PHASES,
},
skills: ALTERNATIVE_SKILLS,
viewTemplates: [{ name: 'case-overview', html: ALTERNATIVE_CASE }],
/*
* 两个局面各一张(2026-08-02 用户指出「执行单完全是验收单了」):
* · `alt-worksheet` 替代选型工作单 —— 归一与 FAE 选型期间摆它:逐颗料的选型档案、
* 替代等级判据、pin-to-pin 核对要点、三层筛指引 + 填回工作表。**这是干活的件**;
* 同一局面在办与归档同一张;局面变了换张(A1 三张同款口径)。
*/
viewTemplates: [
/*
* **每份件两态 1:1**(2026-08-02/03 用户两次校准):在办可编辑 ↔ 办结快照
* 静态归档(finalHtml),这是「1:1」的落点;而**选型作业与收口验收各有各的件**。
*/
{ name: 'alt-worksheet', html: ALT_WORKSHEET },
/*
* 收口件(2026-08-03 用户:「验收工单怎么又变成选型了?」)—— 选型作业与
* 收口验收是两件工作,各有各的件:One Pager 归选型执行单,**整合清单归验收**
* (「清单是所有选型完成后收口才整合的东西」)。「1:1」落在每份件与它的
* 办结快照之间(finalHtml),不是全程只许一张。
*/
{ name: 'alt-roster', html: ALT_ROSTER },
],
cardSchemas: {},
distillHooks: [
{ on: 'milestone', milestone: 'M_candidates', prompt: '归纳本轮撒网的有效角度与无效角度。' },
{ on: 'terminal', prompt: '归纳替代经验:哪些参数差异实际造成了问题、哪些品牌的替代关系可复用。' },
{
on: 'terminal',
prompt: '归纳本单替代选型的经验:哪类料找得到替代、FAE 的选型依据形态、判稳转标的分布。',
},
],
},
/* ── 样例集 —— 三条形状真不同(常备案例即验收案例)───────────── */
samples: [
{
name: 'EOL 料替代',
input: { originalPn: 'TL072CDR', chipType: '运放', scenario: '车规' },
expect: { ports: ['benchmark'] },
/*
* ① 流内常路:上游放行载荷的**真实形状** —— records 里同一颗料出现两次
* (受理记录行 + 分类表行)、还混着结论行;顶层带摊平字段。
* 替代料(客户预授权的 PBY)库内无历史 → 判稳转 B → to-sourcing(E5 复活的那条路)。
*/
name: '流内常路 · Itelma 两颗 C 类(一颗一张选型单按序派 · 替代料无历史 → 转 B 去寻源)',
input: {
rfqRef: 'RFQ MD14.1',
customer_code: 'CU-0142',
customer_name: 'Itelma',
type_c_total: 2,
records: [
{
rfq_ref: 'RFQ MD14.1',
pn: 'BBPY00201209601Y00',
customer_pn: '4-14-01502',
category: '滤波器',
brand: 'YAGEO',
eau: 30000,
remark: '客户预授权替代:PBY201209T-601Y-N(BOM Analog 列)',
type_tag: 'C',
},
{
rfq_ref: 'RFQ MD14.1',
pn: 'BBPY00201209601Y00',
history_days: 47,
market_stable: true,
lifecycle: '要求替代',
type_tag: 'C',
basis: 'EOL/缺货/要求替代 → C',
},
{
rfq_ref: 'RFQ MD14.1',
pn: 'DAG2400L321504XZB01-R0',
customer_pn: '9-00-00251',
category: '天线',
brand: '浙江嘉康',
eau: 30000,
remark: '',
type_tag: 'C',
},
{
rfq_ref: 'RFQ MD14.1',
pn: 'DAG2400L321504XZB01-R0',
history_days: 9999,
market_stable: false,
lifecycle: 'EOL',
type_tag: 'C',
basis: 'EOL/缺货/要求替代 → C',
},
{
rfq_ref: 'RFQ MD14.1',
pn: 'CJTB0101R6N',
customer_pn: '4-00-04102',
category: '集成电路',
brand: 'Jiangsu Changjing',
eau: 60000,
type_tag: 'A',
},
{
rfq_ref: 'RFQ MD14.1',
line_total: 3,
type_a_total: 1,
type_b_total: 0,
type_c_total: 2,
need_alternative: true,
need_sourcing: false,
customer_name: 'Itelma',
},
],
},
expect: { ports: ['roster'] },
},
{
/*
* ② 客户点名替代、替代料**库内行情稳** → 判稳转 A → 全 A 直通定价(to-pricing)。
* FAE 若填回 CJTB0101R6N(库内 43 天·行情稳),就走这条路。
*/
name: '点名替代 · 替代料库内行情稳(转 A 直通定价)',
input: {
rfqRef: 'RFQ-2026-0777',
customer_name: '青岛海洋电子',
records: [
{
rfq_ref: 'RFQ-2026-0777',
pn: 'TL072CDR',
category: '运放',
brand: 'TI',
eau: 3000,
target_price: 22,
remark: '客户点名替代:国产运放优先',
type_tag: 'C',
},
{
rfq_ref: 'RFQ-2026-0777',
pn: 'TL072CDR',
history_days: 9999,
market_stable: false,
lifecycle: '要求替代',
type_tag: 'C',
basis: '客户要求替代 → C',
},
],
},
expect: { ports: ['roster'] },
},
{
/*
* ③ 单飞宽进:没有单号、没有上游清单,只有一段需求(类型 + 参数 + 场景)——
* 归一造案号、需求代号,全标 C,照常走完(输入契约在活动内归一+补全)。
*/
name: '单飞需求 · 只有类型与参数(无单号无清单,宽进归一)',
input: {
subject: '车规运放选型需求',
body: '需要一颗车规级双通道运放:供电 3-36V,AEC-Q100,替代 TL072 系列,年用量约 5K。请选型给替代建议;定案后要给采购去问价。',
},
expect: { ports: ['roster'] },
},
],
+64 -25
View File
@@ -1,45 +1,84 @@
/**
* A2 技能 —— 替代选型的方法经验(AM 08)。
* A2 技能 —— 替代选型 MVP 简化形的方法经验(AM 08)。
*
* **载方法,不载权力**:这里提到的工具都在本活动白名单内;技能不扩权、不改约。
* 渐进披露:常驻上下文只有 `id label:applicability`,正文 Agent 说要才加载。
*
* 剧本改了口径,技能里同一句也要跟着改(A1 的账)—— 2026-08-02 改盘后:
* 选型的实质工作在 FAE 系统外,系统管归一 / 组卡 / 判稳;
* 旧的「圈替代候选(三层筛)」讲的是 FAE 脑子里的方法,系统不替他办,删。
*/
import { skillIdSchema } from '@contracts';
import type { Skill } from '@contracts';
export const ALTERNATIVE_SKILLS: Skill[] = [
{
id: skillIdSchema.parse('alternative.screen-candidates'),
label: '圈替代候选',
applicability: '原型号停产或缺货,要找替代时。',
/*
* 这条讲的是**组卡与核验时的选型口径**(FAE 的实质工作在系统外,但卡面指导书、
* basis 的取舍、收回后的核验都要按同一把尺)—— 2026-08-02 用户指出执行件
* 不是选型工作台后,连同工作单一起补回。
*/
id: skillIdSchema.parse('alternative.grade-discipline'),
label: '替代等级与三层筛',
applicability: '组选型执行单、核验 FAE 填回的定案、或放行前裁量来源可溯时。',
method: [
'三层筛,一层比一层贵,所以要按顺序:',
'① **参数硬指标**:封装、电压范围、精度、温度等级 —— 不满足直接淘汰,不必往下看。',
'② **认证与合规**:车规 / 医疗 / 工业等级不可降级替代,客户没明说也不行。',
'③ **供应与价格**:到这一步才比价,前两层没过的便宜也没用。',
'三层筛,一层比一层贵,按顺序(组卡的 steps 与核验都照它):',
'① 参数硬指标:封装、引脚、电气、温度等级 —— 不满足直接淘汰,不必往下看;',
'② 认证与合规:车规 AEC-Q100/Q200、医疗、工业等级不可降级替代,客户没明说也不行;',
'③ 供应与价格:生命周期在产、货态、品牌接受度 —— 到这一步才比价。',
'',
'「Pin-to-pin 兼容」要看数据手册核实,不要信分销商的一句话 —— ',
'同封装不等于同引脚定义,这个错误要到打样才暴露。',
'等级三码(照 MVP 素材对标表的口径,写别的码基座数不出来):',
'· P2P = 同封装同引脚可直换 —— 必须核对过五项:封装尺寸/引脚定义/电气/温度等级/认证;',
' 同封装不等于同引脚定义,要对数据手册,不要信分销商一句话(这个错要到打样才暴露);',
'· FUNC = 功能兼容,需外围电路或 layout 调整 —— 差异项与改动评估必须写清;',
'· OTHER = 部分兼容须打样验证 —— 关键差异逐项列出、给验证方案。',
'',
'客户备注里已预授权的替代优先采信;点名禁用的品牌不得作候选;',
'是否接受非原厂品牌、参数降级必须问客户,不得替客户决定。',
].join('\n'),
crystallized: [],
knowledgeRefs: ['替代选型判据表'],
knowledgeRefs: ['docs/MVP示例工作流.html §1.5(替代等级/初选核险/圈选替代)'],
version: '1.0.0',
},
{
id: skillIdSchema.parse('alternative.ask-customer'),
label: '什么时候必须问客户',
applicability: '候选已圈定,但存在客户口径不明的项时。',
id: skillIdSchema.parse('alternative.normalize-roster'),
label: '归一询价受理清单',
applicability: '开案第一步:把上游受理记录(或单飞需求)整份落成本活动的清单时。',
method: [
'这几件事**必须问**,不能替客户决定:',
'· 是否接受非原厂品牌;',
'· 是否接受参数降级(哪怕只降一档温度等级);',
'· 是否需要重新打样验证、由谁承担费用。',
'上游载荷里 `records` 是**全部落地行混在一起**的一份 —— 同一颗料出现两三次:',
'带 eau/brand/target_price 的是受理记录行(以它为准),带 basis/history_days 的是',
'分类表行,带 line_total 这类总数的是结论行(不是料,别落进清单)。',
'**按 pn 归并**,一颗料一行,一拍写完(rows 收数组)。',
'',
'问的方式是**跨任务征答**:卡以系统消息送到 BD 的消息要素,就地作答。',
'它不占对方的任务清单——那是一次性的解答请求,不是把责任转过去。',
'',
'问之前先把候选与判据整理好一并发过去:让对方**在数据上作判断**,',
'而不是回一句「你看着办」。',
'三条纪律:',
'① **PN 一个字符不许改**;上游没给的列留空字符串,绝不写 0 顶空。',
'② orig_type 与 type_tag 两列**同一个值起步**(照上游的标):原标是留痕,现标才是本活动的产物。',
'③ 单号 rfq_ref 原样承继;单飞认不出才造「ALT-<型号或类型>」,造完从头用到尾。',
].join('\n'),
crystallized: [],
knowledgeRefs: [],
version: '1.0.0',
knowledgeRefs: ['docs/MVP示例工作流.html §1.5'],
version: '2.0.0',
},
{
id: skillIdSchema.parse('alternative.reclass-stable'),
label: '替代料判稳转标',
applicability: 'FAE 填回替代料之后,要把 C 类行转成 A/B 时。',
method: [
'判稳的尺与三分类 P-03 的 A/B 两行**同一把**:',
'历史价 ≤ 90 天且行情稳 → 转 A(行情完备,可直接取历史锚点);否则 → 转 B(要去寻源补价)。',
'',
'判据从 `crm.history-price-read` 来:**参数 pns 收数组,所有替代料一次传完**。',
'没历史回 historyDays=9999、hit=false —— **那本身就是判据**(新料无历史 → 不稳 → B),',
'不是缺数据,不必替它补。工具回的 lifecycle 若是 EOL/缺货/要求替代,',
'说明替代料选错了 —— 不转标,当替代未果处置。',
'',
'转标写回清单行时:type_tag 只写 A 或 B(照规则,不许臆断);',
'依据一句话落 basis 列(「替代料 X 历史价 N 天·行情稳 → 转 A」),逐条可回核;',
'orig_type 不动。**不许为过判据改标** —— 判据读的是基座按值现数的条数,你改不动。',
].join('\n'),
// 结晶片段:查替代料行情这一步每单都一样 —— 确定性子序列,不必每次让模型重想
crystallized: [{ tool: 'crm.history-price-read', input: { pns: 'pns' } }],
knowledgeRefs: ['三分类规则 P-03(A/B 两行)'],
version: '2.0.0',
},
];
@@ -0,0 +1,409 @@
/**
* A1 两份执行页的样式表 —— **从 PRD 目标件原样搬过来**,不是我照着复述的。
*
* 出处:`~/coding/JCAI_PRD/docs/活动执行页/A1-询价受理需求.html` 的整段 `<style>`,
* 外加 `A1-询价受理记录.html` 里归档件独有的那几件(分类分布条 / 类徽 / 判定表 /
* 结论横幅 / 落地动作 / 完成标)。**一个数值都没改**。
*
* 为什么是搬而不是抄:前三轮我用自己那套外壳「照着做」,块搬齐了、版面是两套 ——
* 章节头成了彩色方块编号、多出摘要与元信息带、字号用 clamp。人一眼就看出不是那份件。
* 目标件是**像素级的答案**,搬过来才不会在复述里走样。
*
* 选择器保留 `.proto-root .app-shell` 前缀,所以模版正文也要照原型那样包两层壳 ——
* 少一层壳,整张样式表一条都不生效(踩过)。
*/
export const PRD_CSS = `
/* ============================================================================
JCAI 员工面工作台 · A1 受理归一的过程成果「询价受理需求」——报告本体
立场:**AI 已从 RFQ 邮件抽取归一**,人在这里审核并就地修订。
两态:读态(AI 稿 + 红/灰占位提示缺项) ⇄ 编辑态(全字段可改,保存 / 取消)
核心是「有效 PN 清单」;清单之上四件受理要素:客户公司 / 联系人 / 客户属性 / 有效 PN。
令牌与既有组件取自 workbench/src/app/(proto)/app.css,数值未改。
⚠️ 修正一处原型 bug:app.css:2062-2064 的 '.jc-rp-sign span'(权重 0,1,1)
盖过了 '.jc-rp-ava'(0,1,0),把头像的 display:grid 覆盖成 block、字色改灰,
导致落款头像里的字掉到圆外。本文件把该规则收窄为 '.jc-rp-sign > div > span'。
新增件:四要素卡 .jc-four/.jc-fcard · 占位胶囊 .jc-ph · 分段控件 .jc-seg ·
来源条 .jc-src · 录入表 .jc-tbl-in
========================================================================== */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { background: #e6eaf1; -webkit-font-smoothing: antialiased; }
.proto-root .app-shell {
--brand: #3370ff; --brand-deep: #245bdb;
--brand-soft: #e1eaff; --brand-wash: #f0f4ff;
--ai-grad: linear-gradient(135deg, #3370ff, #7f3bf5);
--ink: #1f2329; --ink-2: #41464e; --ink-3: #646a73; --ink-4: #8f959e;
--line: #dee0e3; --line-soft: #e8eaed; --soft: #f8f9fb;
--jcai-bg: #e6eaf1; --cardline: #e1e6ec; --card-shadow: 0 1px 2px rgba(31, 35, 41, .05);
--ok: #278717; --ok-soft: #e8f8e4;
--warn: #de7802; --warn-accent: #ff8800; --warn-soft: #fff4e5;
--danger: #d83931; --danger-soft: #feeae9;
--mono: ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono", Menlo, Consolas, monospace;
--shadow-brand: 0 6px 16px rgba(51, 112, 255, .22);
--t-fast: .16s cubic-bezier(.3, 0, .2, 1);
--t-med: .26s cubic-bezier(.3, 0, .2, 1);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--jcai-bg); min-height: 100vh; color: var(--ink);
}
.proto-root :where(button, input, select, a, [tabindex]):focus-visible {
outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 8px;
}
.proto-root ::selection { background: var(--brand-soft); }
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes jcBlink { 0%,100% { opacity: 1 } 50% { opacity: .35 } }
@keyframes jcStamp { from { opacity: 0; transform: rotate(-6deg) scale(1.7); } to { opacity: .9; transform: rotate(-6deg) scale(1); } }
/* 放开宽度:1160px 是「9 列全展开、一个字都不裁」的最小容器宽度(见下方列 min-width 之和) */
.jc-stage { max-width: 1160px; margin: 0 auto; padding: 22px 16px 52px; }
.proto-root .jc-mono { font-family: var(--mono); font-size: 11px; color: var(--ink-3); }
.proto-root .jc-pulse-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--brand); display: inline-block; animation: jcBlink 1.1s ease-in-out infinite; }
/* ── 报告外壳 ───────────────────────────────────────────────────────── */
.proto-root .jc-report { background: #fff; border: 1px solid var(--cardline); border-radius: 14px; box-shadow: var(--card-shadow); overflow: hidden; animation: rise var(--t-med) both; }
.proto-root .jc-rp-head { position: relative; overflow: hidden; display: flex; align-items: flex-start; gap: 12px; padding: 16px 20px 13px; border-bottom: 2px solid var(--ink); }
/* 题头下缘粗细双线 = 正式件版式 */
.proto-root .jc-rp-head::after { content: ""; position: absolute; left: 0; right: 0; bottom: -4px; height: 1px; background: var(--line); }
/* 题头右上的品牌光晕:与工作台标题区同一签名件 */
.proto-root .jc-rp-head::before { content: ""; position: absolute; right: -70px; top: -96px; width: 240px; height: 240px; border-radius: 999px; background: radial-gradient(circle, rgba(51,112,255,.10), transparent 70%); pointer-events: none; }
.proto-root .jc-rp-head > * { position: relative; z-index: 1; }
.proto-root .jc-rp-badge { flex: none; width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center; color: #fff; background: var(--ai-grad); box-shadow: var(--shadow-brand); }
.proto-root .jc-rp-tit { flex: 1; min-width: 0; }
.proto-root .jc-rp-tit em { display: block; font-style: normal; font-size: 10px; font-weight: 800; letter-spacing: .08em; color: var(--ink-4); font-family: var(--mono); }
.proto-root .jc-rp-tit h3 { margin: 4px 0 0; font-size: 18px; font-weight: 800; color: var(--ink); letter-spacing: -.01em; line-height: 1.3; }
.proto-root .jc-rp-wait { display: inline-flex; align-items: center; gap: 6px; flex: none; margin-top: 4px; font-size: 11px; font-weight: 700; border-radius: 999px; padding: 4px 12px; color: var(--brand-deep); background: var(--brand-wash); border: 1px solid var(--brand-soft); }
.proto-root .jc-rp-wait.warn { color: var(--warn); background: var(--warn-soft); border-color: #ffd9a8; }
.proto-root .jc-rp-wait.edit { color: #fff; background: var(--brand); border-color: var(--brand); }
.proto-root .jc-rp-stamp { flex: none; margin-top: 2px; transform: rotate(-6deg); border: 2px solid currentColor; border-radius: 9px; padding: 4px 8px 3px 12px; font-size: 13px; font-weight: 900; letter-spacing: .28em; color: var(--ok); opacity: .9; animation: jcStamp .32s cubic-bezier(.2, 1.4, .4, 1) both; }
.proto-root .jc-rp-bd { padding: 18px 20px 12px; }
.proto-root .jc-rp-part { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 9px; margin: 20px 0 11px; }
.proto-root .jc-rp-part:first-child { margin-top: 0; }
.proto-root .jc-rp-part > b { flex: none; font-size: 11px; font-weight: 800; letter-spacing: .12em; color: var(--ink-3); }
.proto-root .jc-rp-part > .ln { flex: 1; height: 1px; background: var(--line-soft); }
.proto-root .jc-rp-part > em { flex: none; font-style: normal; font-size: 10px; font-weight: 700; color: var(--ink-4); font-family: var(--mono); background: var(--soft); border-radius: 999px; padding: 2px 9px; white-space: nowrap; }
.proto-root .jc-rp-part > em.warn { color: var(--warn); background: var(--warn-soft); }
/* 落款 —— ⚠️ 选择器收窄到 > div > *,别再命中头像那个 span */
.proto-root .jc-rp-foot { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 13px 20px 15px; border-top: 2px solid var(--line); background: #fbfcfd; }
.proto-root .jc-rp-sign { display: flex; align-items: center; gap: 10px; flex: 1 1 200px; min-width: 0; }
.proto-root .jc-rp-ava { flex: 0 0 auto; width: 30px; height: 30px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; line-height: 1; color: #fff; background: var(--brand); }
.proto-root .jc-rp-sign > div { min-width: 0; }
.proto-root .jc-rp-sign > div > b { display: block; font-size: 12px; font-weight: 700; color: var(--ink-2); }
.proto-root .jc-rp-sign > div > span { display: block; margin-top: 1px; font-size: 10.5px; color: var(--ink-4); }
.proto-root .jc-rp-acts { display: flex; align-items: center; gap: 8px; margin-left: auto; flex: none; }
.proto-root .jc-hintline { font-size: 11px; font-weight: 700; color: var(--warn); line-height: 1.5; }
.proto-root .jc-btn { display: inline-flex; align-items: center; gap: 5px; border: 0; border-radius: 7px; padding: 8px 14px; font-size: 12px; font-weight: 600; font-family: inherit; background: var(--brand); color: #fff; cursor: pointer; transition: all var(--t-fast); }
.proto-root .jc-btn:hover:not(:disabled) { background: var(--brand-deep); }
.proto-root .jc-btn:disabled { opacity: .45; cursor: not-allowed; }
.proto-root .jc-btn-ghost { background: #fff; color: var(--ink-2); border: 1px solid var(--line); }
.proto-root .jc-btn-ghost:hover:not(:disabled) { background: var(--brand-wash); color: var(--brand); border-color: var(--brand-soft); }
.proto-root .jc-btn-primary { background: var(--ai-grad); }
.proto-root .jc-btn-primary:hover:not(:disabled) { filter: brightness(1.07); box-shadow: var(--shadow-brand); transform: translateY(-1px); }
/* ── 来源信件:客户 RFQ 邮件原文(复用原型 jc-letter 件,青色 = 收件) ── */
.proto-root .jc-letter { position: relative; border: 1px solid var(--cardline); border-left: 3px solid #0aa294; border-radius: 12px; padding: 14px 16px 15px; background: #fff; box-shadow: var(--card-shadow); }
.proto-root .jc-letter-h { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 11px; }
.proto-root .jc-letter-dir { display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px; font-weight: 800; padding: 3px 9px; border-radius: 999px; background: #e4f8f5; color: #0a7f74; }
.proto-root .jc-letter-h .jc-chip { font-size: 10.5px; padding: 3px 9px; border-radius: 999px; background: var(--brand-wash); color: var(--brand-deep); border: 1px solid var(--brand-soft); }
.proto-root .jc-letter-h > code { margin-left: auto; font-family: var(--mono); font-size: 10.5px; color: var(--ink-4); }
.proto-root .jc-letter-meta { display: flex; flex-direction: column; gap: 4px; margin-bottom: 11px; padding-bottom: 11px; border-bottom: 1px solid var(--line-soft); font-size: 11.5px; color: var(--ink-2); }
.proto-root .jc-letter-meta p { margin: 0; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.proto-root .jc-letter-meta i { flex: none; font-style: normal; width: 44px; font-size: 10px; font-weight: 800; letter-spacing: .06em; color: var(--ink-4); }
.proto-root .jc-letter-meta code { font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); overflow-wrap: anywhere; }
.proto-root .jc-letter-sub { margin: 0 0 9px; font-size: 14px; font-weight: 800; color: var(--ink); line-height: 1.45; overflow-wrap: anywhere; }
/* 正文:原文照录,pre-wrap 保原始换行;不截断、不折叠 */
.proto-root .jc-letter-body { margin: 0; font-size: 12.5px; line-height: 1.72; color: var(--ink-2); white-space: pre-wrap; overflow-wrap: anywhere; }
.proto-root .jc-letter-body b { color: var(--ink); font-weight: 700; }
.proto-root .jc-atts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; padding-top: 11px; border-top: 1px dashed var(--line-soft); }
.proto-root .jc-att { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px 5px 8px; border: 1px solid var(--brand-soft); border-radius: 8px; background: var(--brand-wash); font-size: 11.5px; color: var(--brand-deep); }
.proto-root .jc-att svg { flex: none; color: var(--brand); }
.proto-root .jc-att code { font-family: var(--mono); font-size: 10.5px; }
.proto-root .jc-att span { color: var(--ink-4); }
/* 抽取指向:从原文到下方要素的一句衔接 */
.proto-root .jc-derive { display: flex; align-items: flex-start; gap: 8px; margin-top: 11px; padding: 9px 12px; border-radius: 9px; background: var(--brand-wash); font-size: 11.5px; line-height: 1.7; color: var(--brand-deep); }
.proto-root .jc-derive svg { flex: none; margin-top: 2px; }
/* ── 四要素卡 ───────────────────────────────────────────────────────────
色调字典:每张卡带一套 --tone / --tone-soft / --tone-line,组件只读变量。
卡 3、卡 4 的色调随数据变(老客户绿 / 新客户橙;齐了蓝 / 有缺橙),
所以这条要素带本身就是一行可读的状态,不只是四个格子。 */
.proto-root .jc-four { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 11px; }
.proto-root .jc-fcard {
position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; gap: 10px; min-width: 0;
padding: 14px 14px 14px; border: 1px solid var(--cardline); border-radius: 13px; background: #fff;
box-shadow: 0 1px 2px rgba(31,35,41,.05), 0 8px 20px -14px rgba(31,35,41,.35);
transition: transform var(--t-fast), box-shadow var(--t-fast), border-color var(--t-fast);
--tone: var(--brand); --tone-soft: rgba(51,112,255,.13); --tone-line: var(--brand-soft);
}
/* 顶缘色棱 + 右上角光晕:卡的身份色,一眼分得清四件是四件 */
.proto-root .jc-fcard::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px; background: var(--tone); }
.proto-root .jc-fcard::after { content: ""; position: absolute; right: -46px; top: -52px; width: 132px; height: 132px; border-radius: 999px; background: radial-gradient(circle, var(--tone-soft), transparent 70%); pointer-events: none; }
.proto-root .jc-fcard > * { position: relative; z-index: 1; }
.proto-root .jc-fcard:hover { transform: translateY(-2px); border-color: var(--tone-line); box-shadow: 0 2px 4px rgba(31,35,41,.05), 0 14px 26px -16px rgba(31,35,41,.45); }
.proto-root .jc-fcard.t-teal { --tone: #0aa294; --tone-soft: rgba(10,162,148,.15); --tone-line: #9fe0d9; }
.proto-root .jc-fcard.t-ok { --tone: var(--ok); --tone-soft: rgba(39,135,23,.14); --tone-line: #bfedb4; }
.proto-root .jc-fcard.t-warn { --tone: var(--warn-accent); --tone-soft: rgba(255,136,0,.12); --tone-line: #ffd9a8; }
.proto-root .jc-fcard-k { display: flex; align-items: center; gap: 6px; font-size: 9.5px; font-weight: 800; letter-spacing: .12em; color: var(--tone); }
.proto-root .jc-fcard-k > i { flex: none; width: 19px; height: 19px; border-radius: 6px; display: inline-grid; place-items: center; background: var(--tone-soft); color: var(--tone); }
.proto-root .jc-fcard-b { display: flex; align-items: center; gap: 10px; min-width: 0; }
.proto-root .jc-fcard-t { min-width: 0; flex: 1; }
/* 一律换行,不省略号——宁可多占一行,也不丢字 */
.proto-root .jc-fcard-t > b { display: block; font-size: 14px; font-weight: 800; color: var(--ink); line-height: 1.35; overflow-wrap: anywhere; }
.proto-root .jc-fcard-t > span { display: block; margin-top: 2px; font-size: 10.5px; color: var(--ink-4); line-height: 1.55; overflow-wrap: anywhere; }
/* 公司标:圆角方 + AI 渐变 + 外圈光环(方=组织) */
.proto-root .jc-logo { flex: none; width: 38px; height: 38px; border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 800; line-height: 1; letter-spacing: .04em; color: #fff; background: var(--ai-grad); box-shadow: var(--shadow-brand), 0 0 0 3px rgba(51,112,255,.10); }
/* 人像:圆 + 青色系(圆=人,与方形的组织标一眼分开) */
.proto-root .jc-face { flex: none; width: 38px; height: 38px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 800; line-height: 1; color: #0a7f74; background: #e4f8f5; border: 1px solid #9fe0d9; box-shadow: 0 0 0 3px rgba(10,162,148,.10); }
/* 邮箱做成可点的胶囊,复制按钮嵌在里面——比一行裸文字有抓手 */
/* 邮箱:软底胶囊、无描边——有描边会读成输入框,而读态这里不是可填的 */
.proto-root .jc-mailrow { display: flex; align-items: center; gap: 4px; margin-top: 5px; min-width: 0; max-width: 100%; padding: 3px 4px 3px 9px; border: 0; border-radius: 8px; background: var(--tone-soft); transition: background var(--t-fast); }
.proto-root .jc-mailrow:hover { background: rgba(10,162,148,.2); }
/* 编辑态里它包着输入框,软底会变成「框中框」,去掉 */
.proto-root .jc-report.editing .jc-mailrow { background: transparent; padding: 0; margin-top: 6px; }
.proto-root .jc-mailrow > code { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; line-height: 1.5; font-family: var(--mono); font-size: 10.5px; color: var(--ink-2); }
.proto-root .jc-copy { flex: none; width: 22px; height: 22px; display: inline-grid; place-items: center; border: 0; border-radius: 6px; background: #fff; color: var(--ink-4); cursor: pointer; box-shadow: 0 1px 2px rgba(31,35,41,.08); transition: all var(--t-fast); }
.proto-root .jc-copy:hover { color: #0aa294; }
.proto-root .jc-copy.ok { color: var(--ok); background: var(--ok-soft); box-shadow: none; }
/* 客户属性徽章 */
.proto-root .jc-tagbig { display: inline-flex; align-items: center; gap: 6px; align-self: flex-start; padding: 7px 14px; border-radius: 999px; font-size: 13.5px; font-weight: 800; line-height: 1; color: var(--tone); background: var(--tone-soft); border: 1px solid var(--tone-line); }
.proto-root .jc-fcard-note { font-size: 10.5px; color: var(--ink-4); line-height: 1.55; }
.proto-root .jc-fcard-note b { color: var(--ink-2); font-weight: 700; }
.proto-root .jc-fcard-note b.bad { color: var(--tone); font-weight: 800; }
/* 成交迷你柱:把「台账 6 笔成交」从一句话变成看得见的节奏 */
.proto-root .jc-spark { display: flex; align-items: flex-end; gap: 4px; height: 26px; }
.proto-root .jc-spark > i { flex: 1 1 0; min-width: 4px; max-width: 15px; border-radius: 3px 3px 1px 1px; background: var(--tone); opacity: .3; transition: opacity var(--t-fast), height var(--t-med); }
.proto-root .jc-spark > i:last-child { opacity: 1; }
.proto-root .jc-spark:hover > i { opacity: .55; }
.proto-root .jc-spark:hover > i:last-child { opacity: 1; }
.proto-root .jc-spark.empty { align-items: stretch; }
.proto-root .jc-spark.empty > i { height: 100%; opacity: 1; background: repeating-linear-gradient(45deg, var(--tone-soft) 0 5px, transparent 5px 10px); border: 1px dashed var(--tone-line); border-radius: 5px; }
/* 有效 PN 计数 */
.proto-root .jc-count { display: flex; align-items: baseline; gap: 6px; }
.proto-root .jc-count > b { font-family: var(--mono); font-size: 32px; font-weight: 800; line-height: .95; color: var(--ink); letter-spacing: -.04em; }
.proto-root .jc-count > span { font-size: 11px; color: var(--ink-4); }
/* 分段刻度条:一格 = 一条 PN,缺必填的那格直接标红——比一根实心进度条信息量大 */
.proto-root .jc-segmeter { display: flex; gap: 3px; }
.proto-root .jc-segmeter > i { flex: 1; height: 7px; border-radius: 2px; background: var(--ok); transition: background var(--t-fast); }
.proto-root .jc-segmeter > i.bad { background: var(--danger); }
.proto-root .jc-lockhint { display: inline-flex; align-items: center; gap: 4px; font-size: 10px; color: var(--ink-4); }
/* ── 占位胶囊:必填=红虚线,选填=灰虚线;**都带文字**,不靠颜色单打 ── */
.proto-root .jc-ph { display: inline-flex; align-items: center; font-style: normal; font-size: 10px; font-weight: 700; line-height: 1; padding: 3px 7px; border-radius: 5px; border: 1px dashed; }
.proto-root .jc-ph.req { color: var(--danger); background: var(--danger-soft); border-color: #f3b6b3; }
.proto-root .jc-ph.opt { color: var(--ink-4); background: var(--soft); border-color: var(--line); }
/* ── 分段控件(新客户 / 老客户)── */
.proto-root .jc-seg { display: inline-flex; padding: 2px; gap: 2px; border: 1px solid var(--cardline); border-radius: 9px; background: var(--soft); align-self: flex-start; }
.proto-root .jc-seg button { border: 0; border-radius: 7px; padding: 6px 13px; font: inherit; font-size: 11.5px; font-weight: 700; color: var(--ink-3); background: transparent; cursor: pointer; transition: all var(--t-fast); }
.proto-root .jc-seg button:hover:not(.on) { color: var(--brand); }
.proto-root .jc-seg button.on { background: #fff; color: var(--ink); box-shadow: 0 1px 3px rgba(31,35,41,.12); }
/* ── 表:读态与编辑态共用骨架 ─────────────────────────────────────── */
.proto-root .jc-tbl-wrap { overflow-x: auto; border: 1px solid var(--line-soft); border-radius: 10px; }
.proto-root .jc-tbl-in { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 12px; }
.proto-root .jc-tbl-in th { background: var(--soft); text-align: left; padding: 8px 9px; font-size: 10.5px; font-weight: 800; letter-spacing: .03em; color: var(--ink-3); white-space: nowrap; border-bottom: 1px solid var(--line-soft); }
.proto-root .jc-tbl-in th.req::after { content: "*"; margin-left: 1px; color: var(--danger); font-size: 10px; }
.proto-root .jc-tbl-in td { padding: 4px 5px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
.proto-root .jc-tbl-in tr:last-child td { border-bottom: 0; }
.proto-root .jc-tbl-in tbody tr:hover td { background: #f7f9fd; }
/* 缺必填的整行浅红打底 + 行首红标——不用找红胶囊,一眼扫到是哪几行 */
.proto-root .jc-tbl-in tr.miss td { background: #fffbfb; }
.proto-root .jc-tbl-in tbody tr.miss:hover td { background: #fff5f5; }
.proto-root .jc-tbl-in td.jc-rn { position: relative; width: 38px; padding: 4px 6px 4px 13px; white-space: nowrap; font-family: var(--mono); font-size: 10.5px; color: var(--ink-4); }
.proto-root .jc-tbl-in tr.miss td.jc-rn { color: var(--danger); font-weight: 800; }
/* 行首红标只在表态用 ::after —— 卡片态的 ::after 被「第 N 条」占着,不能抢 */
@media (min-width: 961px) {
.proto-root .jc-tbl-in tr.miss td.jc-rn::after { content: ""; position: absolute; left: 0; top: 4px; bottom: 4px; width: 3px; border-radius: 0 3px 3px 0; background: var(--danger); }
}
.proto-root .jc-rndot { display: inline-block; width: 5px; height: 5px; margin-left: 4px; border-radius: 999px; background: var(--danger); vertical-align: 2px; }
/* 读态单元格 */
.proto-root .jc-vcell { display: block; padding: 6px 6px; font-size: 12px; color: var(--ink); line-height: 1.5; overflow-wrap: anywhere; }
.proto-root .jc-vcell.mono { font-family: var(--mono); font-size: 11px; color: var(--ink-2); }
.proto-root .jc-vcell.num { font-family: var(--mono); font-size: 11.5px; text-align: right; }
/* 编辑态单元格:安静的电子表格 */
.proto-root .jc-cell { width: 100%; min-width: 64px; border: 1px solid var(--line); border-radius: 6px; padding: 6px 7px; font: inherit; font-size: 12px; color: var(--ink); background: #fff; transition: border-color var(--t-fast), box-shadow var(--t-fast); }
.proto-root .jc-cell.mono { font-family: var(--mono); font-size: 11px; }
.proto-root .jc-cell.num { text-align: right; font-family: var(--mono); font-size: 11.5px; }
.proto-root .jc-cell::placeholder { color: var(--ink-4); }
.proto-root .jc-cell:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-wash); outline: none; }
.proto-root .jc-cell.req-empty { border-color: #f3b6b3; background: #fffafa; }
.proto-root .jc-cell.req-empty::placeholder { color: var(--danger); }
/* 备注这类长文本用文本域:能换行、随内容长高,不裁字 */
.proto-root textarea.jc-cell { display: block; resize: vertical; overflow: hidden; min-height: 31px; line-height: 1.55; }
/* 编辑态的整表提示带 */
.proto-root .jc-editbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; padding: 9px 12px; border-radius: 9px; background: var(--brand-wash); border: 1px solid var(--brand-soft); font-size: 11.5px; color: var(--brand-deep); }
.proto-root .jc-editbar svg { flex: none; }
.proto-root .jc-editbar .sp { flex: 1 1 auto; }
/* 剔除行说明 */
.proto-root .jc-drop { display: flex; align-items: flex-start; gap: 8px; margin-top: 10px; padding: 9px 12px; border-radius: 9px; border: 1px dashed var(--line); background: var(--soft); font-size: 11.5px; color: var(--ink-3); line-height: 1.6; }
.proto-root .jc-drop svg { flex: none; margin-top: 2px; color: var(--ink-4); }
.proto-root .jc-drop code { font-family: var(--mono); font-size: 10.5px; color: var(--ink-2); }
.proto-root .jc-callout { display: flex; gap: 8px; align-items: flex-start; border-radius: 9px; padding: 11px 13px; font-size: 12.5px; line-height: 1.75; }
.proto-root .jc-callout p { margin: 0; }
.proto-root .jc-callout svg { flex: none; margin-top: 2px; }
.proto-root .jc-callout.t-mute { background: var(--soft); color: var(--ink-2) }
@media (prefers-reduced-motion: reduce) {
.proto-root .jc-pulse-dot { animation: none; }
.proto-root .jc-report, .proto-root .jc-rp-stamp { animation: none; }
}
/* ══════════════════════════════════════════════════════════════════════════
响应式:宁可另起一行,也不损失完整度
≥961px 六列全展开的表(列 min-width 之和 878px,容器有余量,绝不裁字、绝不横滚)
≤960px 表 → 卡片:一条 PN 一张卡、一行一字段(标签 / 值),无横滚、无省略
≤560px 字段行再拆成上下两行(标签在上、值在下)
══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 1040px) { .proto-root .jc-four { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { .proto-root .jc-four { grid-template-columns: 1fr; } }
@media (max-width: 960px) {
.proto-root .jc-tbl-wrap { overflow: visible; border: 0; border-radius: 0; background: transparent; }
.proto-root .jc-tbl-in,
.proto-root .jc-tbl-in tbody,
.proto-root .jc-tbl-in tr,
.proto-root .jc-tbl-in td { display: block; width: 100%; }
.proto-root .jc-tbl-in thead { display: none; }
/* 一条 PN = 一张卡 */
.proto-root .jc-tbl-in tr { margin-bottom: 10px; padding: 4px 13px 9px; border: 1px solid var(--line-soft); border-radius: 11px; background: #fff; box-shadow: var(--card-shadow); }
.proto-root .jc-tbl-in tr:last-child { margin-bottom: 0; }
.proto-root .jc-tbl-in tbody tr:hover td { background: transparent; }
.proto-root .jc-tbl-in tr.miss { border-color: #f3b6b3; background: #fffbfb; }
/* 卡头:第 N 条 */
.proto-root .jc-tbl-in td.jc-rn { padding: 8px 0 8px; margin-bottom: 4px; border-bottom: 1px solid var(--line-soft); font-family: inherit; font-size: 12px; font-weight: 800; color: var(--ink-2); }
.proto-root .jc-tbl-in td.jc-rn::before { content: "第 "; }
.proto-root .jc-tbl-in td.jc-rn::after { content: " 条"; }
.proto-root .jc-tbl-in tr.miss td.jc-rn { color: var(--danger); }
/* 字段行:左标签 + 右值,值超长自动折行 */
.proto-root .jc-tbl-in td:not(.jc-rn) { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 10px; align-items: center; padding: 6px 0; border-bottom: 1px dashed var(--line-soft); }
.proto-root .jc-tbl-in tr td:last-child { border-bottom: 0; padding-bottom: 2px; }
.proto-root .jc-tbl-in td:not(.jc-rn)::before {
content: attr(data-h); font-size: 10.5px; font-weight: 700; letter-spacing: .02em; color: var(--ink-4); line-height: 1.5;
}
.proto-root .jc-tbl-in td[data-req="1"]:not(.jc-rn)::before { content: attr(data-h) " *"; }
.proto-root .jc-tbl-in .jc-vcell { padding: 0; overflow-wrap: anywhere; }
.proto-root .jc-tbl-in .jc-vcell.num { text-align: left; }
.proto-root .jc-tbl-in .jc-cell.num { text-align: left; }
.proto-root .jc-tbl-in .jc-cell { min-width: 0; }
}
/* 窄屏:落款三件各占一行,动作按钮撑满一行 */
@media (max-width: 560px) {
.proto-root .jc-rp-acts { margin-left: 0; width: 100%; flex-wrap: wrap; }
.proto-root .jc-rp-acts .jc-btn { flex: 1 1 auto; justify-content: center; }
.proto-root .jc-hintline { flex: 1 1 100%; }
.proto-root .jc-tbl-in td:not(.jc-rn) { grid-template-columns: 1fr; gap: 3px; align-items: start; }
}
/* ══ 以下取自 A1-询价受理记录.html(归档件独有的件)══ */
/* ── 料号分类:分布条 + 图例 ───────────────────────────────────────── */
.proto-root .jc-dist { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding: 13px 15px; border: 1px solid var(--line-soft); border-radius: 12px; background: linear-gradient(180deg, #fbfcfe, #fff); }
.proto-root .jc-distbar { flex: 1 1 320px; min-width: 260px; display: flex; gap: 3px; }
.proto-root .jc-distbar > i { height: 30px; border-radius: 4px; display: grid; place-items: center; font-size: 10.5px; font-weight: 800; color: #fff; font-family: var(--mono); }
.proto-root .jc-distbar > i.a { background: var(--ok); }
.proto-root .jc-distbar > i.b { background: var(--brand); }
.proto-root .jc-distbar > i.c { background: var(--warn-accent); }
.proto-root .jc-distlg { display: flex; flex-wrap: wrap; gap: 8px 18px; }
.proto-root .jc-distlg > span { display: inline-flex; align-items: center; gap: 7px; font-size: 11.5px; color: var(--ink-3); }
.proto-root .jc-distlg em { width: 10px; height: 10px; border-radius: 3px; }
.proto-root .jc-distlg em.a { background: var(--ok); } .proto-root .jc-distlg em.b { background: var(--brand); } .proto-root .jc-distlg em.c { background: var(--warn-accent); }
.proto-root .jc-distlg b { font-family: var(--mono); font-weight: 800; color: var(--ink); }
/* 类别徽 A / B / C */
.proto-root .jc-tbadge { display: inline-grid; place-items: center; width: 20px; height: 20px; border-radius: 6px; font-family: var(--mono); font-size: 11px; font-weight: 800; line-height: 1; color: #fff; }
.proto-root .jc-tbadge.a { background: var(--ok); } .proto-root .jc-tbadge.b { background: var(--brand); } .proto-root .jc-tbadge.c { background: var(--warn-accent); }
/* ── 结论与去向 ─────────────────────────────────────────────────────────
G2 是**分支网关**:三条互斥条件按序求值,只命中一条、只落一个活动。
所以这里不是三张并列的去向卡,而是一张**判定表 + 一条结论**。 */
/* 判定表:一行一条规则,命中行上色留痕(DMN 口径) */
.proto-root .jc-dmn { border: 1px solid var(--line-soft); border-radius: 12px; overflow: hidden; }
.proto-root .jc-dmn-h, .proto-root .jc-dmn-r { display: grid; grid-template-columns: 40px minmax(0, 1fr) 170px 200px; align-items: center; gap: 12px; padding: 9px 14px; }
.proto-root .jc-dmn-h { background: var(--soft); border-bottom: 1px solid var(--line-soft); font-size: 10px; font-weight: 800; letter-spacing: .08em; color: var(--ink-4); }
.proto-root .jc-dmn-r { font-size: 12px; color: var(--ink-4); background: #fff; }
.proto-root .jc-dmn-r + .jc-dmn-r { border-top: 1px solid var(--line-soft); }
.proto-root .jc-dmn-i { font-family: var(--mono); font-size: 11px; font-weight: 800; }
.proto-root .jc-dmn-c { line-height: 1.6; overflow-wrap: anywhere; }
.proto-root .jc-dmn-t { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; }
.proto-root .jc-dmn-t code { font-family: var(--mono); font-size: 10.5px; font-weight: 800; color: var(--ink-4); background: var(--soft); border: 1px solid var(--line); border-radius: 5px; padding: 2px 6px; }
.proto-root .jc-dmn-v { font-size: 11px; line-height: 1.55; }
/* 命中行:上色 + 左缘棱 + 加粗 */
.proto-root .jc-dmn-r.hit { position: relative; background: var(--tone-soft); color: var(--ink-2); }
.proto-root .jc-dmn-r.hit::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--tone); }
.proto-root .jc-dmn-r.hit .jc-dmn-i { color: var(--tone); }
.proto-root .jc-dmn-r.hit .jc-dmn-c { color: var(--ink); }
.proto-root .jc-dmn-r.hit .jc-dmn-c b { color: var(--tone); font-weight: 800; }
.proto-root .jc-dmn-r.hit .jc-dmn-t { color: var(--ink); }
.proto-root .jc-dmn-r.hit .jc-dmn-t code { color: #fff; background: var(--tone); border-color: var(--tone); }
.proto-root .jc-dmn-v.y { display: inline-flex; align-items: center; gap: 5px; font-weight: 800; color: var(--tone); }
.proto-root .jc-dmn-v.n { color: var(--ink-4); }
/* 色调随命中类别 */
.proto-root .jc-dmn.k-a { --tone: var(--ok); --tone-soft: rgba(39,135,23,.10); }
.proto-root .jc-dmn.k-b { --tone: var(--brand); --tone-soft: rgba(51,112,255,.09); }
.proto-root .jc-dmn.k-c { --tone: var(--warn-accent); --tone-soft: rgba(255,136,0,.11); }
/* 结论横幅:命中条件 → 唯一去向 */
.proto-root .jc-verdict {
position: relative; overflow: hidden; display: flex; align-items: stretch; flex-wrap: wrap; gap: 16px;
margin-top: 11px; padding: 16px 18px; border: 1px solid var(--tone-line); border-radius: 13px;
background: linear-gradient(105deg, var(--tone-soft), #fff 62%);
box-shadow: 0 1px 2px rgba(31,35,41,.05), 0 10px 24px -16px rgba(31,35,41,.4);
--tone: var(--warn-accent); --tone-soft: rgba(255,136,0,.14); --tone-line: #ffd9a8;
}
.proto-root .jc-verdict.k-a { --tone: var(--ok); --tone-soft: rgba(39,135,23,.13); --tone-line: #bfedb4; }
.proto-root .jc-verdict.k-b { --tone: var(--brand); --tone-soft: rgba(51,112,255,.13); --tone-line: var(--brand-soft); }
.proto-root .jc-verdict::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px; background: var(--tone); }
.proto-root .jc-verdict > * { position: relative; z-index: 1; }
.proto-root .jc-vd-l { flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column; gap: 7px; }
.proto-root .jc-vd-k { font-size: 9.5px; font-weight: 800; letter-spacing: .12em; color: var(--tone); }
.proto-root .jc-vd-cond { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 15px; font-weight: 800; color: var(--ink); }
.proto-root .jc-vd-why { margin: 0; font-size: 11.5px; line-height: 1.7; color: var(--ink-3); overflow-wrap: anywhere; }
.proto-root .jc-vd-why code { font-family: var(--mono); font-size: 10.5px; color: var(--ink); }
.proto-root .jc-vd-arrow { flex: none; align-self: center; color: var(--tone); }
.proto-root .jc-vd-r { flex: 1 1 260px; min-width: 0; display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-radius: 11px; background: #fff; border: 1px solid var(--tone-line); }
.proto-root .jc-vd-node { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 800; color: var(--ink); }
.proto-root .jc-vd-node code { font-family: var(--mono); font-size: 12px; font-weight: 800; color: #fff; background: var(--tone); border-radius: 6px; padding: 3px 8px; }
.proto-root .jc-vd-who { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--ink-2); }
.proto-root .jc-vd-who > i { width: 20px; height: 20px; border-radius: 999px; display: inline-grid; place-items: center; font-style: normal; font-size: 10px; font-weight: 700; color: #fff; background: var(--tone); }
.proto-root .jc-vd-next { margin: 0; padding-top: 8px; border-top: 1px dashed var(--line-soft); font-size: 10.5px; line-height: 1.65; color: var(--ink-4); }
.proto-root .jc-vd-next b { color: var(--ink-2); font-weight: 700; }
/* 命中分支携带的条目 */
.proto-root .jc-pnlist { display: flex; flex-wrap: wrap; gap: 5px; margin: 0; padding: 0; list-style: none; }
.proto-root .jc-pnlist > li { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 999px; background: #fff; border: 1px solid var(--tone-line); font-size: 11px; color: var(--ink-2); overflow-wrap: anywhere; }
.proto-root .jc-pnlist > li > i { flex: none; width: 5px; height: 5px; border-radius: 999px; background: var(--tone); }
.proto-root .jc-pnlist code { font-family: var(--mono); font-size: 10.5px; color: var(--ink); }
/* ── 落地动作 ───────────────────────────────────────────────────────── */
.proto-root .jc-acts { display: flex; flex-direction: column; gap: 0; border: 1px solid var(--line-soft); border-radius: 11px; overflow: hidden; }
.proto-root .jc-act { display: flex; align-items: flex-start; gap: 9px; padding: 10px 13px; font-size: 12px; color: var(--ink-2); line-height: 1.6; background: #fff; }
.proto-root .jc-act + .jc-act { border-top: 1px solid var(--line-soft); }
.proto-root .jc-act > i { flex: none; width: 18px; height: 18px; margin-top: 1px; border-radius: 999px; display: inline-grid; place-items: center; color: #fff; background: var(--ok); }
.proto-root .jc-act b { font-weight: 700; color: var(--ink); }
.proto-root .jc-act span { color: var(--ink-4); }
.proto-root .jc-act code { font-family: var(--mono); font-size: 10.5px; color: var(--brand-deep); background: var(--brand-wash); border-radius: 4px; padding: 1px 5px; }
.proto-root .jc-done-tag { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; flex: none; padding: 6px 13px; border-radius: 999px; font-size: 11.5px; font-weight: 800; color: var(--ok); background: #fff; border: 1px solid #bfedb4; }
.proto-root .jc-vcell.dim { color: var(--ink-4); }
.proto-root .jc-tbl-in td.jc-ty { width: 44px; text-align: center; }
.proto-root .jc-tbl-in td.jc-ty { width: auto; text-align: left; }
`;
/**
* 图标精灵 —— 与样式表同一出处,两份目标件的 `<symbol>` 取并集。
* 沙箱里取不到图标字体,所以图标一律内联;`<use href="#i-xxx">` 引的就是这一坨。
*/
export const PRD_SPRITE = `<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs><symbol id="i-alert" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></symbol><symbol id="i-arrow-right" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></symbol><symbol id="i-boxes" viewBox="0 0 24 24"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></symbol><symbol id="i-building" viewBox="0 0 24 24"><path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"/><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/><path d="M10 6h4"/><path d="M10 10h4"/><path d="M10 14h4"/><path d="M10 18h4"/></symbol><symbol id="i-check" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></symbol><symbol id="i-copy" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></symbol><symbol id="i-file-check" viewBox="0 0 24 24"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="m9 15 2 2 4-4"/></symbol><symbol id="i-file-text" viewBox="0 0 24 24"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></symbol><symbol id="i-git-branch" viewBox="0 0 24 24"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-history" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></symbol><symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></symbol><symbol id="i-lock" viewBox="0 0 24 24"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol><symbol id="i-mail-open" viewBox="0 0 24 24"><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/></symbol><symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z"/></symbol><symbol id="i-plus" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></symbol><symbol id="i-sheet" viewBox="0 0 24 24"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M8 13h2"/><path d="M14 13h2"/><path d="M8 17h2"/><path d="M14 17h2"/></symbol><symbol id="i-shield-check" viewBox="0 0 24 24"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></symbol><symbol id="i-user" viewBox="0 0 24 24"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/></symbol><symbol id="i-x" viewBox="0 0 24 24"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></symbol></defs></svg>`;
+626 -94
View File
@@ -1,102 +1,634 @@
/**
* A2 替代选型的 HTML 汇报模版 —— **活动自己持有的资产**(§9.3),随包版本冻结。
* A2 替代选型的两张报告 —— 2026-08-03 第四轮重设计(隔离设计代理从零起版面;
* standalone 目标件先行,全绿后按《HTML报告生成规范》§7 机械平移成本文件)。
*
* ── 这一屏给谁看、他此刻要判什么 ──────────────────────────────────
* 看的人是 FAE 与发起的 BD/PM。他此刻只判一件事:**这单还用不用我插手**。
* 于是版面按「先成果、后局面」排 —— 成果是他要签的东西,局面是他决定插不插手的依据:
* 一 初筛清单(撒了多少网)· 二 替代对标表(交付件本体)
* → 三 案例阶段 · 四 当下可为 · 五 目标缺口(AI 的办案局面)
* 指标带扫一眼就该有结论(候选够不够、差异核没核清),往下五节是「凭什么这么说」。
* · `ALT_WORKSHEET` 选型执行工单(S1/S2 在办;**一颗待选型料 = 一页**,tr 挂 data-page):
* 档案头(大字原厂 PN + 为什么要替代)→ 蓝图带(原厂芯片 → 五道核对 → 替代插座:
* 空位虚线红槽、定了实线绿槽 —— 概念图形是第一语言)→ 题面/定案双栏
* (左边客户给的,右边 FAE 的产出;等级三枚 radio 直接选,口径贴在钮上)→
* 方法脚注(五项核对 · 三层筛 · 未果通道)。
* 页容器是**真表格块化**(table.alt-pages > tbody > tr):宿主的加行/删行写死找 tr,
* 「补一颗漏掉的待选型料」与「这颗不该选型(剔除)」才真能用。
* · `ALT_ROSTER` 选型验收报告(S3/放行/归档;**通篇只读**):收口总览四格账 + 选型账一行
* → 询价受理清单大表(原料 → 原标 → 替代定案 → 现标 → 依据)→ 落边图
* (清单 → 还有 B 类? → A3 渠道寻源 / A4 成本定价,命中边实线亮 + 流光,
* 由 case:routeKey 投影,一条没命中两边全灰虚 —— 不冒充判过)→ 结论带 → 落地动作。
* 就地改走验收卡的 editableAtRelease 表,这里一个 data-col / data-edit 都没有。
*
* ── 画面上的数从哪来(一个都不是编的)────────────────────────────
* · `mine:quote_alternative.*` 是**本单自己写下的草稿行** —— 别单放行过的记录不混进来;
* 列名逐字取自 `declarations.ledger`,编一个不存在的列名 = 屏上永远空白。
* · `case:*` / `enabledCount` / `gapCount` / `activeStage` 由语义引擎每拍算出,
* 是确定事实、零 LLM 即可成屏 —— 模型只决定「下一步做什么」,不负责画面。
* · 指标带上「初筛候选 / 已对标」两格指的是**数组本身**:填数时数组取长度
* (view-render 的 `displayValue` 约定),所以不必为「几颗」另立一个计数字段。
* 设计目标件(standalone,浏览器直接可开,与本文件 1:1):
* ~/.claude/jobs/0aebbba5/tmp/target-alt-worksheet.html / target-alt-roster.html
*
* ── 两条不撒谎的纪律 ──────────────────────────────────────────
* ① 右上角挂的是「在办」灯不是印章:这一屏是活的局面,它没盖过章,就别画成盖了章的样子;
* 紫(tone `case`)是**案例活动的标记色不是状态色** —— 红绿橙只留给「达标没达标」。
* ② 表空着就是「本单还没写下这一行」,不是渲染坏了 —— 每张表的说明里都写明空着意味着什么,
* 否则人会把空白读成故障。
*
* 容器是 `sandbox=""` iframe:无脚本、无外链,样式与图标由 `@sdk` 自带。
* ── 工程纪律(都踩过,一条不破)──────────────────────────────────────
* ① 主键格必挂 `data-col`(行的钥匙;在 .decide 的 keyrow 里,开闸才现身,
* 也是「补一页」落键的地方 —— 不填不落库);
* ② data-rows 行容器的标签名不得在行模版内部再现(tbody 的行内无 tbody);
* ③ 收值按「input 的祖父」归行 —— 一页里所有输入格(含 radio)的祖父都是同一个 .decide;
* ④ radio(name=alt_grade_pick)不写 checked:当前档由 data-class-of 投影描虚线,
* 「没点」=「不改」;渲染期基座按行号缀名分组。已知边角:人「补一页」克隆末页时
* 克隆页 radio 与末页同组,新页选档会把末页的勾在 DOM 上顶掉 —— 但未选中列
* 不进写回载荷,台账无恙;
* ⑤ 只有待选型(orig_type=C)的行成页:`.alt-page:not(.v-c){display:none}`,判断在样式表;
* ⑥ 模版不算不判;列名逐字照 manifest 声明抄;逐条数据走 `mine:quote_alternative.list`;
* ⑦ 空占位三级全局口径:**必填红 / 建议填橘 / 选填灰**(required / expect / 其余),
* 蓝图插座的空位也归「必填」。
*/
import { noteOnly, section, stagesOnly, statsBand, tableOnly, viewShell } from '@sdk';
import { PRD_CSS, PRD_SPRITE } from './prd-style.ts';
export const ALTERNATIVE_CASE = viewShell({
title: '替代选型 · 案例全景',
kicker: 'A2 · 替代选型 · 案例活动',
tone: 'case',
icon: 'search',
stamp: { kind: 'live', text: '案例在办 · 非定稿' },
sign: 'A2 替代选型 · 本页每拍按当下局面重画',
/** 一枚图标 —— `use` 引精灵里的 symbol */
const ico = (name: string, size = 11): string =>
`<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-${name}"/></svg>`;
/** 本件补充精灵:五项核对与等级示意的 6 枚 symbol(lucide 线稿风,随模版走,不动 prd-style) */
const EXTRA_SPRITE = `<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="i-ruler" viewBox="0 0 24 24"><path d="M3 17 17 3l4 4L7 21z"/><path d="m8.5 12.5 1.8 1.8M11.5 9.5l1.8 1.8M14.5 6.5l1.8 1.8"/></symbol>
<symbol id="i-pins" viewBox="0 0 24 24"><path d="M4 7h5M4 12h5M4 17h5M15 7h5M15 12h5M15 17h5"/><rect x="9" y="4" width="6" height="16" rx="1.5"/></symbol>
<symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13 2 4 14h6l-1 8 9-12h-6z"/></symbol>
<symbol id="i-thermo" viewBox="0 0 24 24"><path d="M12 3a2 2 0 0 0-2 2v8.5a4 4 0 1 0 4 0V5a2 2 0 0 0-2-2z"/><path d="M12 9v6"/></symbol>
<symbol id="i-flask" viewBox="0 0 24 24"><path d="M10 2v6L4.5 18a2 2 0 0 0 1.8 3h11.4a2 2 0 0 0 1.8-3L14 8V2"/><path d="M8.5 2h7"/><path d="M7.5 15h9"/></symbol>
<symbol id="i-gear" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.2 5.2l2.1 2.1M16.7 16.7l2.1 2.1M18.8 5.2l-2.1 2.1M7.3 16.7l-2.1 2.1"/></symbol>
</svg>`;
/* ==================== 编辑机构 + 家族补丁(两张共用)====================
闸/红带/加行/撤行/孪生件开合 —— 拷自 A1 报告的 BIND_CSS 并按本件容器适配:
A1 的开合钉在 .jc-tbl-in 上,本件工单的容器是 table.alt-pages(.on 由宿主落在
table 上,data-edit 在 tbody);类徽三色与铺满工作区的两行也一并收在这里。 */
const HARNESS_CSS = `
/* 铺满工作区:件就是这一屏的正文,不是嵌在里面的另一个页面 */
.jc-stage { max-width: none; padding: 0 0 4px; }
.proto-root .jc-report { border: 0; border-radius: 0; box-shadow: none; }
.proto-root .jc-logo svg, .proto-root .jc-face svg, .proto-root .jc-rp-ava svg { width: 17px; height: 17px; }
/* 孪生输入件默认收着 —— 件首先是给人看的 */
.proto-root .jc-cell { display: none; }
/* 类徽三色(A/B/C) */
.proto-root .jc-tbadge { background: var(--ink-4); }
.proto-root .jc-tbadge.v-a { background: var(--ok); }
.proto-root .jc-tbadge.v-b { background: var(--brand); }
.proto-root .jc-tbadge.v-c { background: var(--warn-accent); }
/* 那道闸 —— 默认「已锁定 + 编辑」,开了变成「取消 / 保存」;只在可改屏现身 */
.proto-root .jc-editgate { display: none; }
html.editable .proto-root .jc-editgate {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
margin-bottom: 10px; padding: 8px 12px; border-radius: 9px;
background: var(--soft); border: 1px solid var(--line-soft);
font-size: 11.5px; color: var(--ink-3);
}
.proto-root .jc-editgate-t { display: inline-flex; align-items: center; gap: 6px; flex: 1 1 auto; min-width: 0; }
.proto-root .jc-editgate-t b { color: var(--ink-2); font-weight: 700; }
.proto-root .jc-egbtn {
flex: none; display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
border: 1px solid var(--line); border-radius: 7px; padding: 5px 11px;
font: inherit; font-size: 11.5px; font-weight: 700; color: var(--ink-2); background: #fff;
transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast);
}
.proto-root .jc-egbtn:hover { border-color: var(--brand); color: var(--brand); }
.proto-root .jc-egbtn.save { color: #fff; background: var(--brand); border-color: var(--brand); }
.proto-root .jc-egbtn.save:hover { background: var(--brand-deep); border-color: var(--brand-deep); }
.proto-root .jc-editgate .jc-egbtn.ghost, .proto-root .jc-editgate .jc-egbtn.save { display: none; }
.proto-root .jc-editgate.on { background: var(--brand-wash); border-color: var(--brand-soft); color: var(--brand-deep); }
.proto-root .jc-editgate.on .jc-egbtn.open { display: none; }
.proto-root .jc-editgate.on .jc-egbtn.ghost, .proto-root .jc-editgate.on .jc-egbtn.save { display: inline-flex; }
.proto-root .jc-editgate.on .jc-editgate-t { visibility: hidden; }
/* 缺项红带 —— 只在可改屏出,一格不缺自动收(v-0 那档隐藏,判断在样式表) */
.proto-root .jc-gapbar { display: none; }
html.editable .proto-root .jc-gapbar {
display: flex; align-items: flex-start; gap: 8px;
margin: 0 0 10px; padding: 9px 12px; border-radius: 9px;
background: #fff5f5; border: 1px solid #f3b6b3; color: var(--danger);
font-size: 12px; line-height: 1.65;
}
.proto-root .jc-gapbar svg { flex: 0 0 auto; margin-top: 2px; }
.proto-root .jc-gapbar b { font-weight: 800; }
.proto-root .jc-gapbar span { color: #9a3b36; }
html.editable .proto-root .jc-gapbar.v-0 { display: none; }
/* 「怎么改」的提示带只在开了闸之后说 —— 锁着的时候它是句废话 */
.proto-root .jc-editbar { display: none; }
.proto-root .jc-editgate.on + .jc-editbar {
display: flex; align-items: flex-start; gap: 8px;
margin: 0 0 10px; padding: 9px 12px; border-radius: 9px;
background: var(--brand-wash); border: 1px solid var(--brand-soft);
font-size: 12px; line-height: 1.65; color: var(--ink-2);
}
.proto-root .jc-editbar svg { flex: 0 0 auto; margin-top: 2px; color: var(--brand); }
/* 表尾「补一颗」—— 只在这一屏可改且开了闸时现身(容器是 table.alt-pages) */
.proto-root .jc-addrow { display: none; }
html.editable .proto-root table.alt-pages.on + .jc-addrow {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
margin: -2px 0 12px; padding: 7px 12px; border-radius: 9px;
background: var(--soft); border: 1px dashed var(--line);
}
.proto-root .jc-addrow em { font-style: normal; font-size: 11px; color: var(--ink-3); }
`;
/**
* 一份件的外壳 —— 照家族三层壳:.proto-root > .app-shell > .jc-stage > article.jc-report;
* 题头(徽 + kicker + 题 + 状态灯)与落款(署名 + 流位签)与 A1 同一套版面语言。
*/
const doc = (input: {
badge: string;
kicker: string;
title: string;
titleRef: string;
state: string;
css: string;
body: string;
foot: string;
tag: string;
}): string => `<style>${PRD_CSS}${HARNESS_CSS}${input.css}</style>
${PRD_SPRITE}${EXTRA_SPRITE}
<div class="proto-root"><div class="app-shell"><div class="jc-stage">
<article class="jc-report">
<header class="jc-rp-head">
<div class="jc-rp-badge">${ico(input.badge, 17)}</div>
<div class="jc-rp-tit">
<em>${input.kicker}</em>
<h3>${input.title} <span data-slot="${input.titleRef}"></span></h3>
</div>
<span class="jc-rp-wait">${input.state}</span>
</header>
<div class="jc-rp-bd">${input.body}</div>
<footer class="jc-rp-foot">
<div class="jc-rp-sign">
<span class="jc-rp-ava">${ico('user', 15)}</span>
<div><b data-slot="case:owner"></b><span>${input.foot}</span></div>
</div>
<span class="jc-done-tag">${ico('git-branch', 13)}${input.tag}</span>
</footer>
</article>
</div></div></div>`;
/** 章节头 —— 家族的 jc-rp-part:小标签 + 细线 + 右端小签 */
const part = (label: string, aside = '', tone = ''): string =>
`<div class="jc-rp-part"><b>${label}</b><i class="ln"></i>${aside === '' ? '' : `<em class="${tone}">${aside}</em>`}</div>`;
/* ==================== 选型执行工单(一颗一页)==================== */
const SHEET_CSS = `
/* 编辑区容器:左缘品牌竖线把「这一段可就地修订」从说明区里立出来 */
.proto-root .jc-zone.z-edit { border-left: 3px solid var(--brand); padding-left: 14px; margin: 2px 0 14px; }
/* ── 页容器:真表格块化(tbody 挂行来源与行地址,tr 即一页,宿主翻页认 [data-page])── */
.proto-root table.alt-pages { display: block; width: 100%; border-collapse: separate; border-spacing: 0; }
.proto-root .alt-pages tbody { display: block; }
.proto-root .alt-pages tr.alt-page { display: block; margin: 0 0 16px; }
.proto-root .alt-pages td.alt-cell { display: block; padding: 0; }
/* 只有待选型(原标 C)的行成页;A/B 行原样承继,不进这份工单(判断在样式表) */
.proto-root .alt-pages tr.alt-page:not(.v-c) { display: none; }
/* ── 档案页外框 ── */
.proto-root .alt-cell { border: 1px solid var(--line); border-radius: 12px; overflow: hidden; background: #fff; }
/* ── 档案头:这颗料的出身 ── */
.proto-root .archrow { display: flex; align-items: flex-start; gap: 16px; padding: 13px 16px 11px; border-bottom: 1px solid var(--line-soft); }
.proto-root .archrow .chip-id { flex: 1 1 auto; min-width: 0; }
.proto-root .archrow .chip-id em { display: block; font-style: normal; font-size: 10px; font-weight: 800; letter-spacing: .14em; color: var(--ink-4); margin-bottom: 3px; }
.proto-root .archrow .chip-id .pn { display: block; font-size: 23px; font-weight: 800; letter-spacing: -.3px; color: var(--ink); word-break: break-all; line-height: 1.2; }
.proto-root .archrow .why { flex: 0 1 300px; min-width: 210px; border-left: 1px solid var(--line-soft); padding-left: 14px; }
.proto-root .archrow .why i { display: inline-flex; align-items: center; gap: 5px; font-style: normal; font-size: 10px; font-weight: 800; letter-spacing: .1em; color: var(--ink-4); }
.proto-root .archrow .why p { margin: 4px 0 0; font-size: 11.5px; line-height: 1.55; color: var(--ink-2); }
.proto-root .archrow .why p span:empty::after { content: "上游未留约束 —— 以原厂官网规格书为准"; color: var(--ink-4); }
/* 剔除这一页:只在开了闸时现身,宿主认 data-edit-del(找 tr 挂删记号) */
.proto-root .alt-pages .jc-rowdel { display: none; }
.proto-root .alt-pages.on .jc-rowdel { display: inline-flex; align-items: center; gap: 4px; width: auto; padding: 3px 9px; font-size: 11px; }
.proto-root .alt-pages.on tr.jc-row-gone .alt-cell { opacity: .5; background: #fff6f6; }
.proto-root .alt-pages.on tr.jc-row-gone .chip-id .pn { text-decoration: line-through; color: var(--ink-3); }
.proto-root .alt-pages.on tr.jc-row-new .alt-cell { background: #f2fbf7; }
/* ── 蓝图带:替代路径要穿过五道核对(概念图形是第一语言)── */
.proto-root .blueprint { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--line-soft);
background-image:
repeating-linear-gradient(0deg, rgba(51,112,255,.05) 0 1px, transparent 1px 22px),
repeating-linear-gradient(90deg, rgba(51,112,255,.05) 0 1px, transparent 1px 22px),
linear-gradient(180deg, var(--brand-wash), #fff 88%);
}
.proto-root .bp-box { flex: 0 0 auto; text-align: center; }
.proto-root .bp-box em { display: block; font-style: normal; font-size: 9.5px; font-weight: 800; letter-spacing: .1em; color: var(--ink-4); margin-bottom: 3px; }
.proto-root .bp-box svg { display: block; margin: 0 auto; }
.proto-root .bp-to .socket-pn { display: block; margin-top: 4px; font-size: 11px; font-weight: 800; color: var(--ink-2); max-width: 150px; word-break: break-all; line-height: 1.3; }
.proto-root .bp-mid { flex: 1 1 auto; min-width: 120px; text-align: center; }
.proto-root .bp-mid svg { display: block; width: 100%; height: 46px; }
.proto-root .bp-mid .cap { font-size: 10px; color: var(--ink-4); }
.proto-root .bp-mid .lane { stroke: var(--brand-soft); stroke-width: 2; stroke-dasharray: 6 5; }
.proto-root .bp-mid .gate { fill: #fff; stroke: var(--brand); stroke-width: 1.6; }
.proto-root .bp-mid .tipp { fill: var(--brand); }
/* 插座:定案空着是虚线红槽;填了替代 PN 就换成实线绿槽(判断在样式表,:has + :empty) */
.proto-root .bp-to .sk-body { stroke: var(--ok); fill: rgba(39,135,23,.06); }
.proto-root .bp-to .sk-slot { stroke: var(--ok); }
.proto-root .bp-to:has(.socket-pn:empty) .sk-body { stroke: var(--danger); fill: #fff; stroke-dasharray: 7 5; }
.proto-root .bp-to:has(.socket-pn:empty) .sk-slot { stroke: #f3b6b3; }
.proto-root .bp-to .socket-pn:empty::after { content: "必填"; display: inline-block; font-size: 10px; font-weight: 800; color: var(--danger); background: var(--danger-soft); border: 1px dashed #f3b6b3; border-radius: 5px; padding: 3px 8px; }
.proto-root .bp-from .chipfig .cf-body { fill: var(--ink-2); stroke: var(--ink); }
.proto-root .bp-from .chipfig .cf-pin { stroke: var(--ink-3); stroke-width: 2; }
.proto-root .bp-from .chipfig .cf-dot { fill: #fff; opacity: .8; }
/* ── 题面 / 定案双栏 ── */
.proto-root .duo { display: grid; grid-template-columns: 1fr 1.2fr; gap: 0; }
.proto-root .duo .given { padding: 12px 16px 14px; border-right: 1px solid var(--line-soft); }
.proto-root .duo .decide { padding: 12px 16px 14px; background: linear-gradient(180deg, rgba(51,112,255,.035), transparent 60px); }
.proto-root .duo h5 { display: flex; align-items: center; gap: 6px; margin: 0 0 8px; font-size: 11px; font-weight: 800; letter-spacing: .06em; color: var(--ink-3); }
.proto-root .duo .decide h5 { color: var(--brand-deep); }
.proto-root .fld { display: flex; align-items: baseline; gap: 10px; padding: 5px 0; border-bottom: 1px dashed var(--line-soft); }
.proto-root .fld:last-child { border-bottom: 0; }
.proto-root .fld > i { flex: 0 0 96px; font-style: normal; font-size: 11px; color: var(--ink-4); }
.proto-root .fld > .jc-vcell, .proto-root .fld > .gradechip { flex: 1 1 auto; min-width: 0; font-size: 12px; color: var(--ink); line-height: 1.5; word-break: break-all; }
/* 行键那一格:平时收起,开了闸才现出来(它不是编辑面,是行的钥匙;新加的页在这里落键) */
.proto-root .fld.keyrow { display: none; }
.proto-root .alt-pages.on .fld.keyrow { display: flex; background: var(--soft); border-radius: 6px; padding: 5px 8px; }
.proto-root .fld.keyrow > i { color: var(--ink-3); font-weight: 700; }
/* 占位三级(全局口径):必填红 · 建议填橘 · 选填灰;题面空格是「客户没给」,只出一杠 */
.proto-root .ph-req:empty::after { content: "必填"; display: inline-block; font-size: 10px; font-weight: 700; line-height: 1; padding: 3px 7px; border-radius: 5px; border: 1px dashed #f3b6b3; color: var(--danger); background: var(--danger-soft); }
.proto-root .ph-sug:empty::after { content: "建议填"; display: inline-block; font-size: 10px; font-weight: 700; line-height: 1; padding: 3px 7px; border-radius: 5px; border: 1px dashed #f0c184; color: var(--warn-accent); background: rgba(255,136,0,.08); }
.proto-root .ph-opt:empty::after { content: "选填"; display: inline-block; font-size: 10px; font-weight: 700; line-height: 1; padding: 3px 7px; border-radius: 5px; border: 1px dashed var(--line); color: var(--ink-4); background: var(--soft); }
.proto-root .dimv:empty::after { content: "—"; color: var(--ink-4); }
.proto-root .decide .fld:has(.ph-req:empty) { background: #fffbfb; }
.proto-root .decide .fld:has(.ph-req:empty) > i { color: var(--danger); font-weight: 800; }
/* 定案格的就地改:锁着看只读那份,开了闸换输入孪生件(渲染期基座生成 input.jc-cell) */
.proto-root .alt-pages.on .decide .fld > .jc-vcell,
.proto-root .alt-pages.on .decide .fld > .gradechip { display: none; }
.proto-root .alt-pages.on .decide .fld > input.jc-cell { display: block; flex: 1 1 auto; font: inherit; font-size: 12px; color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; background: #fff; }
.proto-root .alt-pages.on .decide .fld > input.jc-cell:placeholder-shown { border-color: #f3b6b3; background: #fffafa; }
/* 等级三选:口径贴在选择钮上;当前档由 data-class-of 投影描虚线;radio 只在开了闸时可点 */
.proto-root .gopts { margin: 6px 0 4px; display: grid; gap: 6px; }
.proto-root .gopt { position: relative; display: flex; align-items: center; gap: 9px; border: 1px solid var(--line-soft); border-radius: 8px; padding: 7px 10px; background: #fff; }
.proto-root .gopt > input.jc-cell { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.proto-root .alt-pages.on .gopt > input.jc-cell { display: block; }
.proto-root .gopt b { flex: 0 0 52px; font-size: 12px; font-weight: 800; color: var(--ink-2); }
.proto-root .gopt em { flex: 1 1 auto; font-style: normal; font-size: 10.5px; color: var(--ink-3); line-height: 1.45; }
.proto-root .gopt svg { flex: 0 0 auto; color: var(--ink-4); }
/* 当前档:虚线描边(值是台账里的,不写 checked —— 「没点」就是「不改」) */
.proto-root .gopts.v-p2p .gopt.gp, .proto-root .gopts.v-func .gopt.gf, .proto-root .gopts.v-other .gopt.go { border: 1px dashed var(--brand); background: var(--brand-wash); }
.proto-root .gopts.v-p2p .gopt.gp b, .proto-root .gopts.v-func .gopt.gf b, .proto-root .gopts.v-other .gopt.go b { color: var(--brand-deep); }
.proto-root .gopts.v-p2p .gopt.gp svg, .proto-root .gopts.v-func .gopt.gf svg, .proto-root .gopts.v-other .gopt.go svg { color: var(--brand); }
/* 开了闸:radio 现出真钮的手感(选中那档实线亮) */
.proto-root .alt-pages.on .gopt:has(input:checked) { border: 1.6px solid var(--brand); background: var(--brand-wash); }
/* 等级徽:三档三色(P2P 绿 · FUNC 蓝 · OTHER 橙) */
/* gradechip 必须同时是 jc-vcell(2026-08-03 用户:「替代等级后面那个必填没对齐」)——
* 上两行的必填胶囊住在 jc-vcell 的 6px 内距里,裸 gradechip 没这层盒,胶囊贴左又偏高。 */
.proto-root .gradechip.v-p2p { color: var(--ok); font-weight: 800; }
.proto-root .gradechip.v-func { color: var(--brand); font-weight: 800; }
.proto-root .gradechip.v-other { color: var(--warn-accent); font-weight: 800; }
/* ── 方法区(固定说明,不进编辑容器)── */
.proto-root .m-funnel { display: flex; gap: 8px; margin: 8px 0 10px; flex-wrap: wrap; }
.proto-root .m-funnel .fs { flex: 1 1 180px; border: 1px solid var(--line-soft); border-left: 3px solid var(--brand-soft); border-radius: 8px; padding: 8px 10px; font-size: 11px; color: var(--ink-3); line-height: 1.5; background: var(--soft); }
.proto-root .m-funnel .fs b { display: block; color: var(--ink-2); font-size: 11.5px; margin-bottom: 2px; }
.proto-root .m-five { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0 10px; }
.proto-root .m-five .c5 { flex: 1 1 130px; display: flex; align-items: center; gap: 8px; border: 1px solid var(--line-soft); border-radius: 8px; padding: 7px 9px; background: #fff; }
.proto-root .m-five .c5 svg { flex: 0 0 auto; color: var(--brand); }
.proto-root .m-five .c5 b { display: block; font-size: 11px; color: var(--ink-2); }
.proto-root .m-five .c5 span { display: block; font-size: 9.5px; color: var(--ink-4); }
/* 窄屏:双栏叠置、蓝图竖排(媒体块排在基础规则之后,同权重后定义胜) */
@media (max-width: 900px) {
.proto-root .duo { grid-template-columns: 1fr; }
.proto-root .duo .given { border-right: 0; border-bottom: 1px solid var(--line-soft); }
.proto-root .archrow { flex-direction: column; gap: 8px; }
.proto-root .archrow .why { border-left: 0; padding-left: 0; border-top: 1px dashed var(--line-soft); padding-top: 8px; }
.proto-root .blueprint { flex-direction: column; }
.proto-root .bp-mid svg { transform: rotate(90deg); width: 46px; height: 46px; margin: 0 auto; }
}
/* 「必填」空槽轻呼吸 —— 连 @keyframes 一起包在动效许可里,减弱设置下全静 */
@media (prefers-reduced-motion: no-preference) {
.proto-root .bp-to .socket-pn:empty::after { animation: jc-breath 2.2s ease-in-out infinite; }
@keyframes jc-breath { 0%,100% { opacity: 1; } 50% { opacity: .55; } }
}
`;
/** 原厂芯片小像(方体 + 双侧引脚 + 一脚圆点) */
const CHIP_FIG = `<svg width="96" height="64" viewBox="0 0 96 64" fill="none" stroke-linecap="round" class="chipfig">
<line class="cf-pin" x1="8" y1="18" x2="22" y2="18"/><line class="cf-pin" x1="8" y1="28" x2="22" y2="28"/><line class="cf-pin" x1="8" y1="38" x2="22" y2="38"/><line class="cf-pin" x1="8" y1="48" x2="22" y2="48"/>
<line class="cf-pin" x1="74" y1="18" x2="88" y2="18"/><line class="cf-pin" x1="74" y1="28" x2="88" y2="28"/><line class="cf-pin" x1="74" y1="38" x2="88" y2="38"/><line class="cf-pin" x1="74" y1="48" x2="88" y2="48"/>
<rect class="cf-body" x="22" y="10" width="52" height="44" rx="6" stroke-width="1.6"/>
<circle class="cf-dot" cx="31" cy="19" r="2.5"/>
</svg>`;
/** 蓝图带:原厂芯片 → 五道核对 → 替代插座(空=虚线红槽,定了=实线绿槽) */
const BLUEPRINT = `<div class="blueprint">
<div class="bp-box bp-from"><em>原厂芯片</em>${CHIP_FIG}</div>
<div class="bp-mid">
<svg viewBox="0 0 240 46" fill="none">
<path class="lane" d="M6 23h206"/>
<circle class="gate" cx="38" cy="23" r="5"/><circle class="gate" cx="76" cy="23" r="5"/><circle class="gate" cx="114" cy="23" r="5"/><circle class="gate" cx="152" cy="23" r="5"/><circle class="gate" cx="190" cy="23" r="5"/>
<path class="tipp" d="M216 15l16 8-16 8z"/>
</svg>
<div class="cap">替代路径要穿过五道核对(封装 · 引脚 · 电气 · 温度 · 认证)</div>
</div>
<div class="bp-box bp-to"><em>替代定案</em>
<svg width="96" height="64" viewBox="0 0 96 64" fill="none" stroke-linecap="round">
<rect class="sk-body" x="10" y="8" width="76" height="48" rx="8" stroke-width="1.8"/>
<path class="sk-slot" d="M28 22h40M28 32h40M28 42h24" stroke-width="2"/>
</svg>
<span class="socket-pn jc-mono" data-slot="alt_pn" data-blank></span>
</div>
</div>`;
/** 等级三选(radio 不写 checked;当前档由 gopts 上的投影描虚线) */
const GRADE_OPTS = `<div class="gopts" data-class-of="alt_grade">
<div class="gopt gp"><input type="radio" name="alt_grade_pick" value="P2P" class="jc-cell" data-col="alt_grade"><b>P2P</b><em>同封装同引脚 · 可直换 —— 五道核对逐项过了再选,同封装不等于同引脚</em>${ico('pins', 15)}</div>
<div class="gopt gf"><input type="radio" name="alt_grade_pick" value="FUNC" class="jc-cell" data-col="alt_grade"><b>FUNC</b><em>功能兼容 —— 需外围电路或 layout 调整;差多少、怎么改,写进下面两格</em>${ico('gear', 15)}</div>
<div class="gopt go"><input type="radio" name="alt_grade_pick" value="OTHER" class="jc-cell" data-col="alt_grade"><b>OTHER</b><em>部分兼容 —— 须打样验证;怎么验写进验证建议</em>${ico('flask', 15)}</div>
</div>`;
/** 一页 = 一颗待选型料的选型档案(行模版;只有 orig_type=C 的行成页) */
const SHEET_PAGE = `<table class="alt-pages"><tbody data-rows="mine:quote_alternative.list" data-edit="quote_alternative.list">
<tr class="alt-page" data-page data-class-of="orig_type"><td class="alt-cell">
<div class="archrow">
<div class="chip-id"><em>原厂 · 被替换</em><span class="pn jc-mono" data-slot="pn"></span></div>
<div class="why"><i>${ico('history')}为什么要替代&nbsp;<span class="jc-tbadge" data-class-of="orig_type" data-slot="orig_type"></span></i>
<p><span data-slot="remark" data-blank></span></p>
</div>
<button type="button" class="jc-egbtn jc-rowdel" data-edit-del="quote_alternative.list" title="这颗不在本轮选型(剔除)">×&nbsp;剔除</button>
</div>
${BLUEPRINT}
<div class="duo">
<div class="given">
<h5>${ico('file-text', 12)}题面 · 客户给的</h5>
<div class="fld"><i>客户料号</i><span class="jc-vcell jc-mono dimv" data-slot="customer_pn" data-blank></span></div>
<div class="fld"><i>品类</i><span class="jc-vcell dimv" data-slot="category" data-blank></span></div>
<div class="fld"><i>制造商</i><span class="jc-vcell dimv" data-slot="brand" data-blank></span></div>
<div class="fld"><i>年用量</i><span class="jc-vcell jc-mono dimv" data-slot="eau" data-blank></span></div>
<div class="fld"><i>目标价</i><span class="jc-vcell jc-mono dimv" data-slot="target_price" data-blank></span></div>
<div class="fld"><i>交期要求</i><span class="jc-vcell dimv" data-slot="lead_time" data-blank></span></div>
</div>
<div class="decide">
<h5>${ico('pen', 12)}替代定案 · 你的产出</h5>
<div class="fld keyrow"><i>原厂 PN(行键)</i><span class="jc-vcell jc-mono" data-slot="pn" data-col="pn"></span></div>
<div class="fld"><i>替代料 PN</i><span class="jc-vcell jc-mono ph-req" data-slot="alt_pn" data-col="alt_pn" data-blank></span></div>
<div class="fld"><i>替代料品牌</i><span class="jc-vcell ph-req" data-slot="alt_brand" data-col="alt_brand" data-blank></span></div>
<div class="fld"><i>替代等级</i><span class="jc-vcell gradechip ph-req" data-class-of="alt_grade" data-slot="alt_grade" data-blank></span></div>
${GRADE_OPTS}
<div class="fld"><i>参数差异项</i><span class="jc-vcell ph-sug" data-slot="alt_diff" data-col="alt_diff" data-blank></span></div>
<div class="fld"><i>风险与改动</i><span class="jc-vcell ph-sug" data-slot="alt_risk" data-col="alt_risk" data-blank></span></div>
<div class="fld"><i>验证建议</i><span class="jc-vcell ph-sug" data-slot="alt_verify" data-col="alt_verify" data-blank></span></div>
<div class="fld"><i>选型备注</i><span class="jc-vcell ph-opt" data-slot="alt_note" data-col="alt_note" data-blank></span></div>
</div>
</div>
</td></tr>
</tbody></table>`;
export const ALT_WORKSHEET = doc({
badge: 'boxes',
kicker: '统一报价 · A2 替代选型 · 选型作业',
title: '替代选型工单',
titleRef: 'rfq_ref',
state: '在办 · 逐颗定案',
css: SHEET_CSS,
body:
statsBand([
['初筛候选', 'mine:quote_alternative.shortlist'],
['已对标', 'mine:quote_alternative.benchmark'],
['当下可为', 'enabledCount'],
['目标缺口', 'gapCount'],
]) +
section('初筛清单(本单撒网所得)',
tableOnly('mine:quote_alternative.shortlist', [
['原型号', 'original_pn', 'mono'],
['候选型号', 'candidate_pn', 'mono'],
['品牌', 'brand'],
['生命周期', 'lifecycle'],
]) +
noteOnly(
'先撒网后核验,常要两三轮:检索与聚合平台拉来的候选先落这一张,<b>逐条人工核验之前都不算数</b>。' +
'生命周期一栏是初筛就要看的第一道险 —— EOL/停产的当场淘汰;' +
'不足 2~3 颗就放宽参数或换角度重新撒网。这张表空着,是本单还没撒网,不是没找着。',
),
{ text: '常要两三轮' },
) +
section('替代对标表(本单交付件)',
tableOnly('mine:quote_alternative.benchmark', [
['候选型号', 'candidate_pn', 'mono'],
['替代等级', 'grade'],
['参数差异项', 'param_diff'],
['差异风险', 'risk'],
['验证建议', 'verify_advice'],
['FAE 签字', 'signed_by'],
]) +
noteOnly(
'这是要签字交回发起方的东西:<b>差异没核清的不放行</b> —— 「差异风险」空着的行,' +
'就是此刻拦着放行的那几行。替代等级由判定表出(P2P / 功能兼容 / 其他),不是模型自己说了算;' +
'等级定评估深度:P2P 核确免改动,其余逐项评软件、外围电路与 layout。' +
'每项数据一律以<b>原厂官网规格书</b>为准。',
),
{ text: '差异没核清不放行' },
) +
section('案例阶段(CMMN 生命周期)',
stagesOnly('case:stages') +
noteOnly(
'阶段的生命周期态由语义引擎推进到 <b>Enabled</b> 为止;凡触达世界或产出关键交付的计划项' +
'必须人工启动 —— <b>Enabled ≠ Active</b>。选型是<b>一事一议</b>:撒几轮网、哪颗值得深评、' +
'要不要换角度重来,事前排不出谱,所以调度权交给 AI,系统只保证边框:预算、停滞熔断、放行归人。',
),
{ text: '推进到 Enabled 为止' },
) +
section('当下可为(AI 的合法选项)',
stagesOnly('case:enabled') +
noteOnly(
'AI 只能在这个集合里选下一步:<b>可以越谱,不可越权</b>。标「待启动」的是触达世界或' +
'产出关键交付的项 —— 要按一下再动。这一栏空着不是它偷懒,是当下确实无可为:' +
'要么在等人回话,要么在等外面的回音。',
),
{ text: '本拍的合法选项' },
) +
section('目标缺口(离交付还差什么)',
stagesOnly('case:gaps') +
noteOnly(
'缺口只有人知道 → 调人。选型常要向 BD 补客户口径,那是<b>跨任务征答</b>:卡以系统消息送达' +
'对方、<b>就地作答</b>,不占对方的任务清单,也不开放本任务的对话流 —— 一次性的解答请求,' +
'不是责任转移。缺口在世界侧 → 立监听(<b>时限必配</b>)。' +
'当下可为 ∩ 目标所需 = 空,就是<b>依赖断点</b>。这一栏空着,是必做的事都办完了。',
),
{ text: '空 = 必做的都办完了' },
),
`<div class="jc-derive">${ico('info', 14)}
<span>这份工单只办一件事:给每颗<b>待选型(原标 C)</b>的料定一颗替代 —— 客户
<b data-slot="customer_name"></b> 本单待选型 <b data-slot="c_total"></b> 颗,
一颗一页(右上角翻页)。每页上半是<b>题面</b>(客户给的,改不了),下半右栏是<b>你的定案</b>;
定案保存后,查库判稳与转标(≤90 天且行情稳转 A,否则转 B 去寻源)由系统接办。
保存只落任务草稿层;交卷在对话区的执行卡上。</span>
</div>` +
part('替代定案 · 一颗一页', '可就地修订') +
`<div class="jc-zone z-edit">
<div class="jc-gapbar" data-class-of="quote_alternative_list_missing">${ico('alert', 14)}
<span><b>定案还差 <span data-slot="quote_alternative_list_missing"></span> 格必填</b>
—— <span data-slot="quote_alternative_list_missing_keys"></span>。
翻到那一颗,按「编辑」开闸就地填;补齐了执行卡上才交得了卷。</span>
</div>
<div class="jc-editgate" data-edit-for="quote_alternative.list">
<span class="jc-editgate-t">${ico('lock', 12)}替代定案<b>已锁定</b> —— 看的时候不误碰,要改按右边</span>
<button type="button" class="jc-egbtn open" data-edit-open="quote_alternative.list">${ico('pen', 12)}编辑</button>
<button type="button" class="jc-egbtn ghost" data-edit-cancel="quote_alternative.list">取消</button>
<button type="button" class="jc-egbtn save" data-edit-save="quote_alternative.list">${ico('check', 12)}保存</button>
</div>
<div class="jc-editbar">${ico('pen', 14)}
<span>定案三格是<b>必填</b>(替代料 PN / 品牌 / 等级),等级直接点三档之一;
这颗料整个不该选型,按档案头右上角的 × 剔除(划着摆在那儿,可后悔);
漏了一颗,用底下的「补一颗待选型料」。改动只落任务草稿层,<b>保存不等于放行</b>。</span>
</div>
${SHEET_PAGE}
<div class="jc-addrow">
<button type="button" class="jc-egbtn ghost" data-edit-add="quote_alternative.list">${ico('plus', 12)}补一颗待选型料</button>
<em>照末页添一页空档案;行键(原厂 PN)填了才存得下,不填这一页不落库。</em>
</div>
</div>` +
part('选型方法 · 凭什么这么选', '替代等级口径') +
`<div class="m-five">
<div class="c5">${ico('ruler', 16)}<div><b>封装尺寸</b><span>本体 / 脚距 / 高度</span></div></div>
<div class="c5">${ico('pins', 16)}<div><b>引脚定义</b><span>逐脚对数据手册</span></div></div>
<div class="c5">${ico('bolt', 16)}<div><b>电气参数</b><span>供电 / 精度 / 速度</span></div></div>
<div class="c5">${ico('thermo', 16)}<div><b>温度等级</b><span>工作温区不降级</span></div></div>
<div class="c5">${ico('shield-check', 16)}<div><b>认证</b><span>车规 / 医疗不降级</span></div></div>
</div>
<div class="m-funnel">
<div class="fs"><b>① 参数硬指标</b>封装 / 引脚 / 电气 / 温度 —— 任一不满足直接淘汰,不进下一层。</div>
<div class="fs"><b>② 认证与合规</b>车规 AEC-Q100/Q200、医疗、工业等级不可降级;客户点名禁用的品牌与型号不选。</div>
<div class="fs"><b>③ 供应与价格</b>在产 / 货态 / 品牌接受度;客户预授权的候选优先。</div>
</div>
<div class="jc-drop">${ico('info', 13)}
<span>一律以<b>原厂官网规格书</b>为准,不以商城参数表为准。没有把握的替代不填:
宁可在选型备注写明「替代未果」的缘由后保存交回,由判稳一步按剔除处置 ——
编一个候选顶上,是把风险埋进下游的报价单。是否接受非原厂品牌、参数降级,
必须问客户,不得替客户决定。</span>
</div>`,
foot: 'FAE 逐颗定案(右上角翻页)—— 判稳与转标由系统查库接办',
tag: '统一报价 · A2 替代选型 · 选型作业',
});
/* ==================== 选型验收报告(收口清单 + 落边)==================== */
const ROSTER_CSS = `
/* ── 收口总览:四格账 ── */
.proto-root .statrow { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin: 2px 0 10px; }
.proto-root .stat { border: 1px solid var(--line); border-radius: 12px; padding: 10px 12px 9px; background: #fff; position: relative; overflow: hidden; }
.proto-root .stat::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--line); }
.proto-root .stat.sa::before { background: var(--ok); }
.proto-root .stat.sb::before { background: var(--brand); }
.proto-root .stat.sc::before { background: var(--warn-accent); }
.proto-root .stat b { display: block; font-size: 24px; font-weight: 800; letter-spacing: -.4px; color: var(--ink); line-height: 1.15; }
.proto-root .stat span { display: block; font-size: 10.5px; color: var(--ink-3); margin-top: 2px; }
/* 「待选型未清」那格:0 才是收口该有的样子 —— 0 绿、非 0 红(判断在样式表) */
.proto-root .stat.sc b.v-0 { color: var(--ok); }
.proto-root .stat.sc b.v-1, .proto-root .stat.sc b.v-2, .proto-root .stat.sc b.v-3, .proto-root .stat.sc b.v-4, .proto-root .stat.sc b.v-5 { color: var(--danger); }
/* 选型账一行:C 类怎么清零的 */
.proto-root .workline { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; margin: 0 0 12px; padding: 8px 12px; border: 1px dashed var(--line); border-radius: 9px; background: var(--soft); font-size: 11.5px; color: var(--ink-3); }
.proto-root .workline b { color: var(--ink); font-weight: 800; }
.proto-root .workline .sep { color: var(--ink-4); }
/* 剔除说明空着就整句收起(判断在样式表;有剔除才有这句话) */
.proto-root .workline span:has(> b:empty) { display: none; }
.proto-root .workline:has(span > b:empty) .sep:last-of-type { display: none; }
/* ── 清单表:序号计数器 + 归档空格「—」+ 底行小字(承继的第二行不出杠)── */
.proto-root .jc-tbl-in tbody { counter-reset: rn; }
.proto-root .jc-tbl-in td.jc-rn::before { counter-increment: rn; content: counter(rn); }
.proto-root .jc-tbl-in td.dimempty > .jc-vcell:empty::after { content: "—"; color: var(--ink-4); }
.proto-root .jc-tbl-in .sub2 { display: block; font-size: 10px; color: var(--ink-4); margin-top: 1px; }
.proto-root .gchip { font-weight: 800; font-size: 11.5px; }
.proto-root .gchip.v-p2p { color: var(--ok); }
.proto-root .gchip.v-func { color: var(--brand); }
.proto-root .gchip.v-other { color: var(--warn-accent); }
.proto-root .gchip:empty::after { content: "—"; color: var(--ink-4); font-weight: 400; }
/* ── 落边图:清单 → 还有 B 类? → A3 / A4(命中边由壳上 v-r1/v-r2 上色,件不算不判)── */
.proto-root .route { border: 1px solid var(--line); border-radius: 12px; padding: 12px 12px 8px; background: linear-gradient(180deg, #fff, var(--soft)); margin: 0 0 10px; }
.proto-root .route svg { display: block; width: 100%; height: auto; }
.proto-root .route .rnode { fill: #fff; stroke: var(--line); stroke-width: 1.4; }
.proto-root .route .rdia { fill: #fff; stroke: var(--ink-3); stroke-width: 1.4; }
.proto-root .route .redge { stroke: var(--line); stroke-width: 2; stroke-dasharray: 6 5; fill: none; }
/* 已走过的共段(清单 → 判定):中性实线,不随命中变色 */
.proto-root .route .rspine { stroke: var(--line); stroke-width: 2; fill: none; }
.proto-root .route .rpulse { stroke: transparent; stroke-width: 2.5; fill: none; }
.proto-root .route .rtxt { font-size: 12px; font-weight: 800; fill: var(--ink-2); }
.proto-root .route .rsub { font-size: 9.5px; font-weight: 400; fill: var(--ink-4); }
.proto-root .route .rcond { font-size: 10px; font-weight: 700; fill: var(--ink-4); }
.proto-root .route .rtip { fill: var(--line); }
/* 命中 r1:仍有 B 类 → A3 渠道寻源(蓝);未命中那条留灰虚线,不冒充判过 */
.proto-root .route.v-r1 .e1 { stroke: var(--brand); stroke-dasharray: none; }
.proto-root .route.v-r1 .n1 { stroke: var(--brand); stroke-width: 2; }
.proto-root .route.v-r1 .t1 { fill: var(--brand-deep); }
.proto-root .route.v-r1 .c1 { fill: var(--brand-deep); }
.proto-root .route.v-r1 .k1 { fill: var(--brand); }
/* 命中 r2:全 A → A4 成本定价(绿) */
.proto-root .route.v-r2 .e2 { stroke: var(--ok); stroke-dasharray: none; }
.proto-root .route.v-r2 .n2 { stroke: var(--ok); stroke-width: 2; }
.proto-root .route.v-r2 .t2 { fill: var(--ok); }
.proto-root .route.v-r2 .c2 { fill: var(--ok); }
.proto-root .route.v-r2 .k2 { fill: var(--ok); }
.proto-root .route .legend { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; margin: 6px 0 2px; font-size: 10.5px; color: var(--ink-3); }
.proto-root .route .legend b { font-weight: 800; color: var(--ink-2); }
/* 结论带:一句人话结论 + 去向凭据 */
.proto-root .concl { display: flex; gap: 12px; align-items: flex-start; border: 1px solid var(--line); border-left: 4px solid var(--brand); border-radius: 10px; padding: 10px 14px; background: #fff; margin: 0 0 12px; }
.proto-root .concl svg { flex: 0 0 auto; margin-top: 2px; color: var(--brand); }
.proto-root .concl .cl-main { flex: 1 1 auto; min-width: 0; font-size: 12px; line-height: 1.6; color: var(--ink-2); }
.proto-root .concl .cl-main b { color: var(--ink); }
.proto-root .concl .cl-route { flex: 0 0 auto; text-align: right; }
.proto-root .concl .cl-route em { display: block; font-style: normal; font-size: 9.5px; font-weight: 800; letter-spacing: .1em; color: var(--ink-4); }
.proto-root .concl .cl-route b { display: block; font-size: 14px; font-weight: 800; color: var(--brand-deep); margin: 2px 0; }
.proto-root .concl .cl-route code { font-size: 9.5px; color: var(--ink-4); background: var(--soft); border: 1px solid var(--line-soft); border-radius: 5px; padding: 2px 6px; }
/* 窄屏(媒体块排在基础规则之后) */
@media (max-width: 860px) {
.proto-root .statrow { grid-template-columns: repeat(2, 1fr); }
.proto-root .concl { flex-direction: column; }
.proto-root .concl .cl-route { text-align: left; }
}
/* 命中边的流光 —— 连 @keyframes 一起包在动效许可里,减弱设置下全静 */
@media (prefers-reduced-motion: no-preference) {
.proto-root .route.v-r1 .p1, .proto-root .route.v-r2 .p2 { stroke-dasharray: 10 130; animation: jc-flow 2.6s linear infinite; }
.proto-root .route.v-r1 .p1 { stroke: rgba(51,112,255,.65); }
.proto-root .route.v-r2 .p2 { stroke: rgba(39,135,23,.6); }
@keyframes jc-flow { to { stroke-dashoffset: -140; } }
}
`;
/** 清单表(只读;原料 → 原标 → 定案 → 现标 → 依据,一行一颗可回核) */
const ROSTER_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="min-width:190px">原厂料</th>
<th style="width:44px;min-width:44px">原标</th>
<th style="min-width:190px">替代定案</th>
<th style="width:64px;min-width:64px">等级</th>
<th style="width:44px;min-width:44px">现标</th>
<th style="min-width:220px;width:100%">转标依据</th>
</tr></thead>
<tbody data-rows="mine:quote_alternative.list"><tr>
<td class="jc-rn"></td>
<td class="dimempty" data-h="原厂料"><span class="jc-vcell jc-mono" data-slot="pn" data-blank></span><span class="sub2" data-slot="brand" data-blank></span></td>
<td class="jc-ty" data-h="原标"><span class="jc-tbadge" data-class-of="orig_type" data-slot="orig_type"></span></td>
<td class="dimempty" data-h="替代定案"><span class="jc-vcell jc-mono" data-slot="alt_pn" data-blank></span><span class="sub2" data-slot="alt_brand" data-blank></span></td>
<td data-h="等级"><span class="gchip" data-class-of="alt_grade" data-slot="alt_grade" data-blank></span></td>
<td class="jc-ty" data-h="现标"><span class="jc-tbadge" data-class-of="type_tag" data-slot="type_tag"></span></td>
<td class="dimempty" data-h="转标依据"><span class="jc-vcell" data-slot="basis" data-blank></span></td>
</tr></tbody>
</table></div>`;
/** 落边图:两条整段 path(命中哪条哪条亮);共段是中性 spine */
const ROUTE_FIG = `<div class="route" data-class-of="case:routeKey">
<svg viewBox="0 0 660 210" fill="none">
<path class="rspine" d="M148 100 H195"/>
<path class="redge e1" d="M305 100 C355 100 380 50 460 50"/>
<path class="rpulse p1" d="M305 100 C355 100 380 50 460 50"/>
<path class="redge e2" d="M250 138 C250 172 340 168 460 168"/>
<path class="rpulse p2" d="M250 138 C250 172 340 168 460 168"/>
<path class="rtip k1" d="M448 44 l14 6 -14 6 z"/>
<path class="rtip k2" d="M448 162 l14 6 -14 6 z"/>
<rect class="rnode" x="8" y="70" width="140" height="60" rx="10"/>
<text class="rtxt" x="78" y="95" text-anchor="middle">询价受理清单</text>
<text class="rsub" x="78" y="113" text-anchor="middle">共 <tspan data-slot="quote_alternative_list_rows"></tspan> 条 · C 已清零</text>
<path class="rdia" d="M250 62 L305 100 250 138 195 100 Z"/>
<text class="rcond" x="250" y="104" text-anchor="middle">还有 B 类?</text>
<text class="rcond c1" x="330" y="38">是 · B 类 <tspan data-slot="quote_alternative_list_type_tag_b"></tspan> 条</text>
<text class="rcond c2" x="330" y="196">否 · 全 A 类</text>
<rect class="rnode n1" x="460" y="18" width="170" height="64" rx="10"/>
<text class="rtxt t1" x="545" y="45" text-anchor="middle">A3 · 渠道寻源</text>
<text class="rsub" x="545" y="63" text-anchor="middle">补上 B 类的货源价格</text>
<rect class="rnode n2" x="460" y="136" width="170" height="64" rx="10"/>
<text class="rtxt t2" x="545" y="163" text-anchor="middle">A4 · 成本定价</text>
<text class="rsub" x="545" y="181" text-anchor="middle">行情完备 · 直通核价</text>
</svg>
<div class="legend"><span>命中边实线亮 · 未命中留灰虚线</span><span>判定在你签字那一刻由系统现算 —— 要改去向,在验收卡的成果表上改现标</span></div>
</div>`;
export const ALT_ROSTER = doc({
badge: 'file-check',
kicker: '统一报价 · A2 替代选型 · 交付成果',
title: '询价受理清单',
titleRef: 'rfq_ref',
state: '收口 · 待放行',
css: ROSTER_CSS,
body:
`<div class="jc-derive">${ico('info', 14)}
<span>这份清单是 A2 的<b>收口成果</b>:上游受理记录逐条承继,待选型(原标 C)的料
已由 FAE 逐颗定案,系统按库内行情判稳转标(≤90 天且行情稳 → A,否则 → B 去寻源)——
询价受理记录就此转成<b>询价受理清单</b>,客户 <b data-slot="customer_name"></b>。
本页只读:要改哪一格,在验收卡的成果表上就地改,这里跟着重渲。</span>
</div>` +
part('收口总览', 'C 类清零 · 判稳定标', 'ok') +
`<div class="statrow">
<div class="stat"><b data-slot="quote_alternative_list_rows"></b><span>清单条目 · 逐条承继上游</span></div>
<div class="stat sa"><b data-slot="quote_alternative_list_type_tag_a"></b><span>A 类 · 行情完备</span></div>
<div class="stat sb"><b data-slot="quote_alternative_list_type_tag_b"></b><span>B 类 · 待补货源价</span></div>
<div class="stat sc"><b data-class-of="quote_alternative_list_type_tag_c" data-slot="quote_alternative_list_type_tag_c"></b><span>C 类 · 待选型未清(应为 0)</span></div>
</div>
<div class="workline">
<span>待选型 <b data-slot="c_total"></b> 颗</span><span class="sep">→</span>
<span>已填回替代 <b data-slot="filled_total"></b> 颗</span><span class="sep">→</span>
<span>判稳转 A <b data-slot="converted_a_total"></b> 颗</span><span class="sep">·</span>
<span>转 B 待寻源 <b data-slot="converted_b_total"></b> 颗</span><span class="sep">·</span>
<span>剔除说明:<b data-slot="skipped_note" data-blank></b></span>
</div>` +
part('询价受理清单', '<b data-slot="quote_alternative_list_rows"></b> 条 · 交下游的就是这一份') +
ROSTER_TABLE +
`<div class="jc-drop">${ico('info', 13)}
<span><b>原标</b>是上游受理时的三分类(留痕,本活动不改);<b>现标</b>是本活动的产物:
C 类经替代定案与判稳转成 A 或 B。现标定去向 —— 改一颗料的现标,下面的落边判定跟着变。
等级口径:<b>P2P</b> 同封装同引脚可直换 · <b>FUNC</b> 功能兼容需调整 · <b>OTHER</b> 部分兼容须打样验证。</span>
</div>` +
part('落边 · 这一签流向哪里', '出口按序求值 · 首个命中即出') +
ROUTE_FIG +
`<div class="concl">
${ico('git-branch', 15)}
<div class="cl-main"><b>选型结论</b> —— <span data-slot="summary"></span></div>
<div class="cl-route"><em>转入活动</em><b data-slot="case:routeTo"></b><code data-slot="case:routeWhy"></code></div>
</div>` +
part('放行后即执行的落地动作', '门前 → 门后') +
`<div class="jc-acts">
<div class="jc-act"><i>${ico('check', 11)}</i>
<div><b>草稿层合并进正式版</b>&#12288;<span>本任务写下的询价受理清单与选型结论一并落账;
你在验收卡上就地改的那几格随之落定。</span></div>
</div>
<div class="jc-act"><i>${ico('check', 11)}</i>
<div><b>命中的那一口此刻才发射</b>&#12288;<span>清单
<b data-slot="quote_alternative_list_rows"></b> 条随载荷交给上面亮线指向的下游;
另一口本次不起单 —— 放行之前,世界上什么都没有发生。</span></div>
</div>
</div>`,
foot: 'AI 拟稿 · FAE 审定放行 —— 签下去这份清单即转正并流向下游',
tag: '统一报价 · A2 替代选型 → 交下游',
});