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
+330 -80
View File
@@ -1,137 +1,387 @@
/**
* A4 成本定价的案例模型 —— **算价归判定表,超限那一下归人**(2026-07-31 照 A1 的状态机模式重做)。
* A4 成本定价的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。
*
* 与 A2/A3 的分工:那两个是「一次专家判断」,派一张执行单收回结论就够了;
* 定价不同 —— 它**有确定性规则可依**(P-01 利润模型、P-02 底价门禁两支 DMN),
* 算价这件事该算不该问。所以它照 A1 的样子分步建模,而不是甩一张单给人。
* 业务出处:`docs/MVP示例工作流.html` §1.2(S1 取价基准 → S2 逐条定价 → S3 底价自检),
* 按 A3 同一条形状建模(2026-08-03 重写,@3.0.0):
*
* 三项:
* T_price —— 逐条算价并落草案(拟报单价 / 毛利 / 偏离度 / 门禁处置);
* T_approve —— **只在门禁判超限时入场**:触达客户利益的一下,人工激活,派决策卡;
* T_release —— 放行(人工激活)。
* 上游(A3 的目标报价清单 / A2 的询价受理清单 / A1 直达的受理记录)进来 → 归一成
* **报价草案**底表,逐条按 P-01 利润模型算出底价线与拟报单价、过 P-02 底价门禁 →
* **一张执行单**交 BD 核价(拟报单价可校可改,无基准的那几行由他定价)→
* **只有压在拦截档上的那几条才叫人拍板**(决策卡)→ 放行,整份报价草案交定价审批。
*
* 「超限才叫人」是这个活动的要害:每条都叫人,人就成了橡皮图章;一条都不叫,
* 底价门禁就是摆设。所以那一项挂前提(`s_over`),由判定表的结论决定它入不入场。
* 上一版(@2.0.0)三处不合现行口径,这一版整个置换:
* ① 入参必填 `rfqRef + customerCode + records` —— 而 A3 发的是 `{rfqRef, rows}`,
* 门口拦的恰是主路(现在宽进到底,归一在活动内做);
* ② 台账一个 `required` 都没有 —— 执行卡的闸没牙,人不填也能交卷;
* ③ 入场哨兵 `s_priced` 读活动自报的 `draft_total`(「机检只读基座数」第 5 类违反)——
* 模型不记这个账,门就永不开,而它一步也没做错。
*/
export const CMMN_XML = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL">
<case id="pricing">
<!--
案卷项的 id **就是任务视图里的键名**:
draft_total 是草案条数(算完没算完),over_total 是超限条数(要不要叫人)。
两个都由 T_price 写进台账,摊平到顶层后驱动前提。
案卷项的 id **就是任务视图里的键名**。上游载荷(rows / records)与本活动台账列
都摊平到任务视图顶层,照它们命名;quote_pricing_* 那几个是**基座读数**
(行数 / 缺必填 / 按值现数),由基座现算,模型碰不着。
-->
<caseFileModel>
<caseFileItem id="pn" name="型号(开案即有)"/>
<caseFileItem id="draft_total" name="草案条数"/>
<caseFileItem id="over_total" name="超限条数"/>
<caseFileItem id="rows" name="上游清单行(寻源/选型转来,流内开案即有)"/>
<caseFileItem id="records" name="上游受理记录(受理直达时有)"/>
<caseFileItem id="line_total" name="草案条目数(自报)"/>
<caseFileItem id="priced_total" name="已算出拟报价的条数"/>
<caseFileItem id="confirmed_total" name="BD 已核价条数(人环走完的凭据)"/>
<caseFileItem id="route_decided_total" name="已定盘"/>
<caseFileItem id="quote_pricing_draft_rows" name="草案行数(基座现数)"/>
<caseFileItem id="quote_pricing_draft_missing" name="缺拟报单价的行数(基座现数)"/>
<caseFileItem id="quote_pricing_draft_disposition_block" name="压在拦截档的条数(基座现数)"/>
</caseFileModel>
<casePlanModel id="plan" name="成本定价" autoComplete="true">
<!--
① 算完了 —— 交 BD 核价那一步的入场前提。
行数两边对上(自报 = 基座现数),且算价那一轮走完(priced_total 是算完之后
显式写下的数;「大于等于 0」对「写过的 0」为真、对「没写过」为假)。
-->
<sentry id="s_priced">
<onPart id="op_pr" caseFileItemRef="draft_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_pr"><condition>draft_total &gt; 0</condition></ifPart>
<onPart id="op_pr" caseFileItemRef="priced_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_pr"><condition>line_total &gt; 0 and quote_pricing_draft_rows = line_total and priced_total &gt;= 0</condition></ifPart>
</sentry>
<!-- 超限才叫人:门禁判出有超限条目,这一项才入场 -->
<!--
② 人定完价了 —— 底价自检那一步的入场前提。
confirmed_total 只在人交回执行单之后才产生,它就是「经过人了」的凭据。
-->
<sentry id="s_confirmed">
<onPart id="op_cf" caseFileItemRef="confirmed_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_cf"><condition>confirmed_total &gt; 0</condition></ifPart>
</sentry>
<!--
③ **超限才叫人** —— 这个活动的要害。
「压在拦截档上的条数」由基座按 disposition 的取值域现数(不是活动自报),
且要等 BD 核过价(confirmed_total 只在人交回执行单之后才产生)——
他自己回调掉的那几条不必再惊动第二个人。
-->
<sentry id="s_over">
<onPart id="op_ov" caseFileItemRef="over_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_ov"><condition>over_total &gt; 0</condition></ifPart>
<onPart id="op_ov" caseFileItemRef="quote_pricing_draft_disposition_block"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_ov"><condition>quote_pricing_draft_disposition_block &gt; 0 and confirmed_total &gt; 0</condition></ifPart>
</sentry>
<!--
── 放行门的入场条件:**认「办完了没有」,且读基座数**────────────────
· 每行都有拟报单价(quote_pricing_draft_missing 由基座按 required 现数,
与执行卡的提交闸、验收 L1 同一个数同一套算法);
· 门禁处置逐条落定且守恒(三档相加 = 行数,两边都是基座按值现数);
· **没有还压在拦截档上的条目**(要么回调、要么经人拍板改记附理由放行);
· 定盘过了(route_decided_total 是定盘那一步显式写下的记账数)。
-->
<sentry id="s_ready">
<onPart id="op_rd" caseFileItemRef="route_decided_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_rd"><condition>route_decided_total &gt; 0 and quote_pricing_draft_rows &gt; 0 and quote_pricing_draft_missing = 0 and quote_pricing_draft_disposition_block = 0 and quote_pricing_draft_disposition_pass + quote_pricing_draft_disposition_note + quote_pricing_draft_disposition_block = quote_pricing_draft_rows</condition></ifPart>
</sentry>
<!-- 算价:判定表说了算,不是临场判 -->
<humanTask id="T_price" name="逐条算价:取基准、算毛利与偏离度、过底价门禁">
<itemControl><repetitionRule id="r_pr"/></itemControl>
<!-- ① 归一草案 + 取基准 + 逐条算价过门禁(能算的不问人) -->
<humanTask id="S1_price" name="归一报价草案:取成本基准、按利润模型算底价线与建议价">
<itemControl><repetitionRule id="r_s1"/></itemControl>
</humanTask>
<!-- 超限审批:触达客户利益的一下,必须人工激活 -->
<humanTask id="T_approve" name="超限条目决策:低于底价的那几条要人拍板">
<!--
② 交 BD 核价 —— **一张执行单**(拟报价可校可改,无基准的行由他定价)。
manualActivation + 不给 doneWhen(发卡触达真人,引擎不替人按)。
-->
<humanTask id="S2_review" name="交BD定价:工作单上逐条定价,收回后过底价门禁">
<entryCriterion id="ec_s2" sentryRef="s_priced"/>
<itemControl><manualActivationRule id="m_s2"/></itemControl>
</humanTask>
<!--
③ 底价自检 —— SOP 里的 S3,一件独立的事:按人定的价算毛利、偏离度、门禁处置。
它不触达世界,所以给 doneWhen(三档处置的条数加起来等于行数就算判完了),
引擎自己收 —— 从前这一段藏在「交人核价」的收尾里,模型写完 confirmed_total
就把那一项收了,三列处置一格没写,放行门当场判「处置没落定」,而拒回只会说
「数不对/行不对」—— 两扇打不开的门(实测 8 次拒回熔断)。
-->
<humanTask id="S3_gate" name="底价自检:按定下的价算毛利与偏离度,逐条过底价门禁">
<entryCriterion id="ec_s3" sentryRef="s_confirmed"/>
<itemControl><repetitionRule id="r_s3"/></itemControl>
</humanTask>
<!-- ④ 超限拍板:触达客户利益的那一下,必须人工激活;只在拦截档还有条目时入场 -->
<humanTask id="T_approve" name="超限拍板:压在拦截档的条目要人定怎么办">
<entryCriterion id="ec_ap" sentryRef="s_over"/>
<itemControl><manualActivationRule id="m_ap"/><repetitionRule id="r_ap"/></itemControl>
</humanTask>
<!-- 放行:触达世界,必须人工激活 -->
<!-- 草案放行:终局唯放行 —— 触达世界,必须人工激活 -->
<humanTask id="T_release" name="报价草案验收放行">
<entryCriterion id="ec_rel" sentryRef="s_priced"/>
<entryCriterion id="ec_rel" sentryRef="s_ready"/>
<itemControl><manualActivationRule id="m_rl"/></itemControl>
</humanTask>
</casePlanModel>
</case>
</definitions>`;
export const NARRATIVE = `§0 开案:拿到一批待定价条目即开案。成品是**一份全条目带偏离度的报价草案** ——
人签的不是一个数字,是**这个数字的来路**:基准从哪来、毛利怎么算、偏离多少、门禁怎么判。
/**
* 案例说明书 —— **只写整单一以贯之的事**(逐步做法归剧本,两处不重复写同一件事)。
*/
export const NARRATIVE = `§0 开案:**一批料要定出各自的拟报单价**。来路三种,形状都收(宽进):
· **寻源转来**:任务数据里有 \`rows\` 数组 —— 目标报价清单的落地行,每行带底价(base_price)
与价源;这是最全的一种来路;
· **受理/选型直达**:有 \`records\` 或 \`rows\` 而行上没有底价 —— 只有料号与年用量;
· **单飞**:只有一段需求文字,列了几颗料要报价。
**能算的不问人**:P-01(利润模型)与 P-02(底价门禁)两支判定表说了算,你不临场拍价。
你的活是把判据取齐、把结论落进台账;**只有超限的那几条才叫人**。
**成品只有一样**:一份**报价草案** —— 整份行都在、**每一行都有拟报单价**,
而且每个价都说得出来路:基准从哪来、毛利档是哪一档、偏离底价线多少、门禁怎么判。
人签的不是一个数字,是**这个数字的来路**。它是定价审批的直接输入。
【T_price 逐条算价】基准价取自历史价情报(A3 的比价结论 / CRM 历史价)。新料号无历史时
基准记 0,由底价门禁按「无基准」处置 —— **不凭空编一个偏离率出来**。
逐条写进 \`quote_pricing.draft\`:拟报单价 / 目标毛利 / 相对基准偏离 / 门禁处置。
同一拍接着把 draft_total(条数)与 over_total(门禁判「超限」的条数)写上 ——
这两个数一个是完成判据,一个决定要不要叫人。写完 finish。
**能算的不问人,该人签的不代签**:P-01(利润模型)给毛利档、P-02(底价门禁)给处置,
两支判定表说了算,你不临场拍价;而**拟报单价那一列是 BD 定的,你一格都不写** ——
你给的是**建议价**,他确认或改成自己的数。你的活是把判据取齐、把结论落进台账。
【T_approve 超限决策】**只在 over_total > 0 时走这一步**。它触达客户利益,
所以标了人工激活:**先 start,再发决策卡**(decide,itemId 填 T_approve)。
选项按门禁的处置给(照原价报 / 按底价报 / 退回重算),consequence 写清各自的后果;
basis 摆上是哪几条、偏离多少、底价是多少(裸卡基座拒发)。
人点完,轨迹上会出现「decision 卡回传 —— 人选了:xxx」—— **看到它就照那条路把 draft 改掉**,
别再发第二张卡。改完 finish。
**四步走**:归一取基准算建议价 → 交 BD 定价 → 底价自检(按他定的价算毛利、偏离度、
门禁处置)→ 定盘放行。
【提交放行】草案有行了 —— **先 start T_release**(人工激活),然后 submit,BD 验收。
提交前把两条裁量判据裁掉(judge,criterion 照判据名原样写);终局唯放行,你不能自己宣布完成。
**这一单要经过人一到两次**:BD 在定价工作单上逐条定价(那张单绑着整份草案,
拟报单价缺一行按不下提交 —— 基座的闸看着,他填的格子直接落账);
**只有还压在拦截档上的条目才另外叫人拍板**(一张决策卡)。每条都叫人,人就成了橡皮图章;
一条都不叫,底价门禁就是摆设。最后 BD 放行签字。
【一路上要说话】每一拍带一句 say,说**业务的话**:这批料算下来什么价、哪几条压线、
要不要找人拍板。一两句、口语。别报「写草稿 N 行」这种系统词。`;
【一路上要说话】负责人正看着这一单。**每一拍都带一句 say** —— 说业务的话:
这批料算下来什么价、哪几条压线、要不要找人拍板;不说系统词。
到了要找人的那一拍,先说清这一轮办完了什么、为什么现在需要他。
【怎么把一项干完 · 一拍连做】write 和 tool 都不会自动收步 —— 做完用 finish 宣告。
动作排进同一拍的 actions 串一口气给;只有调工具(出参下一拍才看得见)与
发卡 / 提交(挂起)必须停拍。**这一单常路 7-9 拍就该收口**。
§Z 结案:BD 在验收单上签字,系统把草稿合并成正式草案并发出 draft 口的载荷 ——
整份报价草案交给定价审批。`;
/**
* 步骤剧本 —— 与 A1 同一套:`doneWhen` 为真,引擎自己收步、**不问模型**。
* 步骤剧本 —— 一个计划项一条;`doneWhen` 是「成果已存在」的判据,不是「该不该做」。
*
* 两处不给 `doneWhen`:`T_approve`(人工激活,要人拍板)与 `T_release`(触达世界)。
* 给了就等于给引擎一把替人签字的钥匙。
* 三处不给 `doneWhen`:`S2_review`(发卡触达真人)、`T_approve`(要人拍板)、
* `T_release`(签下去触达世界)—— 给了等于给引擎一把替人签字的钥匙。
*/
export const PRICING_STEPS = [
{
item: 'T_price',
intent: '逐条算价并落草案:拟报单价 / 毛利 / 偏离度 / 门禁处置,并结算条数与超限数',
item: 'S1_price',
intent: '整份归一成报价草案:取成本基准 → 按利润模型算出底价线与建议价,逐条落表',
writes: ['quote_pricing.draft', 'quote_pricing.verdict'],
reads: [],
/* 上游载荷数组不声明就会被整表段的截断刀砍掉(砍掉的恰是要归一的行) */
reads: ['rows', 'records'],
view: 'pricing-worksheet',
tools: ['crm.history-price-read'],
/*
* 归一 + 算价两轮都走完才算办完:行数两边对上(一边是基座现数),
* 且 priced_total 写过(全是无基准的单子这个数就是 0;S-FEEL 未绑定的名字
* 参与比较为假,所以「没写过」进不来)。
*/
doneWhen: 'line_total > 0 and quote_pricing_draft_rows = line_total and priced_total >= 0',
guide: `第一件事:把这一单的**报价草案**落出来,并把能算的价都算出来。
【料从哪儿来】
· 寻源转来(\`rows\` 里的行带 base_price / price_basis):照抄 —— 底价就是成本基准,
一个数字都别改;带 quote_line_total 这类总数的是结论行,不是料,别落进草案。
· 受理/选型直达(\`records\` 或没有底价的 \`rows\`):料号、品类、制造商、年用量、
客户目标价、类别(type_tag)照抄,**底价一栏留空**。
· 单飞:照正文归一,列了几颗落几行;没说类别的按 B。
**按 pn 归并,一颗料一行**;pn 一个字符不许改(它是主键,改一个字就是新开一行)。
【第一拍:只做两件事 —— 落草案行 + 串尾发一次查价】
① 写 \`quote_pricing.draft\`,一颗料一行(rows 收数组):
rfq_ref(入参有就原样抄;没有从主题认;认不出才自己造一个,**造出来的必须以 \`PRC-\` 开头**
(\`PRC-<主题或第一颗料的型号>\`)。两条硬规矩:
**① 整单只有一个单号,不是一颗料一个** —— 它是这一单的案号,草案所有行、verdict 那一行
用的都得是同一个串(实测栽过:两颗料造出两个号,一单裂成两单);
**② 只从本单来件取材** —— 台账里躺着的 RFQ-xxxx 是别人的单,借它的号等于把两单的行
混进同一个案卷(实测栽过一轮))、pn、customer_pn、category、
brand、eau、target_price(客户目标价)、lead_time、remark(客户约束/急料,一个字别丢)、
type_tag(照上游现标;单飞按 B)、base_price(上游给了就抄,没有留空)。
suggested_price / margin_band / floor_price **这一拍全部留空** —— 基准还没取齐。
**quoted_price / margin / deviation / disposition 这四列从头到尾都不是你写的**(见下)。
② 串尾调 \`crm.history-price-read\`:pns 收数组,把**底价还空着的那几颗料**一次传完
(上游已经给了底价的不必查)。
**这一拍不写 \`quote_pricing.verdict\`,一列都不写** —— 尤其 priced_total:
它是「算价走完了」的凭据,提前写等于把闸提前打开,引擎会在下一拍开头当场收掉这一步,
**算价从此不会发生**(A3 实测栽过:嘴上说预填了,台账里没有)。
【**这一列你一格都不写**:quoted_price】
拟报单价是 **BD 在工作单上定的那个数** —— 人签的就是它。你给的是**建议价**
(suggested_price),他确认或改成自己的数;margin / deviation / disposition 三列
要等他定完价才算得出来,这一步也别写。
这不是客气话:从前只有一列价、剧本写着「算不出的留空」,实测跑单时两颗库里查不到的料
照样被编出了价(一颗抄客户目标价、一颗凭空写了个数),而交代里还说着「价空着等你定」。
**一条要你分辨「这行算得出/算不出」的规矩会走岔;「这一列不写」没有分支可走岔。**
【第二拍:算建议价 + 记账 + 收步,一拍连做】
① 补基准:**只补上一拍 base_price 空着的那几行** —— 工具出参里 hit=true 的写
base_price = lastPrice、price_basis 写 **HIST**;hit=false 的**别编价**,base_price 留空,
它就是「无基准」那一档。
**上游已经给了 base_price 的行:工具回什么都不采用,那一格一个数字都别改** ——
上游给的是本单问回来的货源底价,工具给的是库内历史价;这一单的成本是前者
(实测栽过:两行上游底价被历史价盖掉,毛利从 20% 变成 115%,数字全对而结论是假的)。
② 逐条算**建议价**,规矩写死,照做即可(**这些是算术,不是判断**):
· 毛利档 margin_band 按利润模型 P-01 取(客户等级 × 类别):
战略客户 + A 类(大批量长单)→ 8%~10%;普通客户 + A 类 → 12%~15%;
B 类 → 15%~20%;C 类 → 20%~50%;拿不准按 12%~15%。写成「12%~15%」这样的文本。
· **底价线** floor_price = base_price ×(1 + 档下限)。保留四位小数。
· **建议价** suggested_price:
- 客户没给目标价(target_price 空)→ 取 base_price ×(1 + 档上限);
- 客户目标价 ≥ 底价线 → 就取客户目标价(接得住,毛利还在档内);
- 客户目标价 < 底价线 → 仍取客户目标价(贴着客户报),这一条到门禁那儿会亮灯;
- **base_price 空(无基准)→ margin_band / floor_price / suggested_price 三列一起留空**。
不要拿一个不相干的价去凑一个数出来 —— 那比没有建议更危险,它看起来像有依据。
· **「没有」要给空串 \`""\`,不许写 0** —— 0 是一个数,底价线写 0 看起来像「成本为零」,
偏离度写 0 看起来像「正好压在底价线上」。空串基座会当成「这一列不写」。
③ 这时才写 \`quote_pricing.verdict\`(一次写全):rfq_ref、customer_code / customer_name
(顶层有就抄)、customer_grade(来件认得出就写,认不出写「普通客户」)、
**line_total** = 草案行数、**basis_total** = 有成本基准的条数、
**priced_total** = 这一拍算出建议价的条数(0 也要写)。
④ finish 这一项。`,
},
{
item: 'S2_review',
intent: '一张执行单交 BD:工作单上逐条定价(拟报单价归他);收回后过底价门禁并定盘',
writes: ['quote_pricing.draft', 'quote_pricing.verdict'],
reads: ['quote_pricing.draft'],
view: 'pricing-worksheet',
tools: [],
// 草案有行了才算算完。不写 `!= null` —— 未绑定的名字那样求值为 true(A1 踩过)
doneWhen: 'draft_total > 0',
guide: `基准价取自历史价情报(A3 的比价结论 / CRM 历史价)。新料号无历史时基准记 0,
由底价门禁按「无基准」处置 —— **不凭空编一个偏离率**。
逐条写进 \`quote_pricing.draft\`:拟报单价 / 目标毛利 / 相对基准偏离 / 门禁处置。
**同一拍接着**把 draft_total 与 over_total(门禁判超限的条数)写上 ——
前者是完成判据,后者决定要不要叫人。
**同一拍再写一行 \`quote_pricing.verdict\`**(验收机检读的就是它):
· draft_total / unpriced_total = 草案条数 / 还没定出价的条数(后者要 = 0);
· over_total = 门禁判超限的条数;
· unresolved_over_total = 超限里**还没处置**的条数(要 = 0 —— 回调了或注明了理由都算已处置);
· summary 一句人话结论。写不对 submit 会被门挡回来。三笔写完再 finish。
**每一行都要带 \`rfq_ref\`** —— 它是这一单的单标识(起单入参里就有,原样抄进去)。
台账主键第一列就是它:不写,这一行会和**别的单**同料号的那一行撞主键,把人家覆盖掉
(实测栽过:A4 的报价草案主键只有 pn,跑四轮只剩最后一轮的 3 行)。`,
/* 不给 doneWhen —— 发卡触达真人(manualActivation),引擎不替人按 */
guide: `建议价算完了,**该 BD 出场了** —— 这是本活动唯一的一张执行单,
**拟报单价是他定的,不是你定的**。工作区摆的是**定价工作单**:逐颗料一张定价档案卡
(成本基准、毛利档、客户目标价、底价线、系统建议价),利润模型与底价门禁两把尺挂在上面,
填回区长在同一屏 —— 他在「拟报单价」那一列逐行填(照建议价确认,或改成自己的数)。
这一步标了人工激活:发卡就算启动,不必单发 start;引擎也不会替你收 —— 人交回后要你自己 finish。
【照这个骨架发,别少字段】(卡面是纯文本,steps/standard 里**别写星号记号**):
{"kind":"human","itemId":"S2_review","workOrder":"execution","card":{
"title":"报价定价:<单号> 共 <N> 条待你定价",
"ask":"系统已按成本基准与利润模型算出每一颗的建议价,请在工作单的「拟报单价」一列逐行定价:认可建议价就照抄,不认可就改成你的数;库内没有成本基准的那几行没有建议价,由你定。",
"steps":["拟报单价每一行都要有 —— 缺一行这张单按不下提交;它是这一单最后签下去的那个数,系统不替你填",
"建议价是从成本基准按利润模型的毛利档推出来的:照抄就是认可这个毛利,改高改低都可以,改了在备注里写一句缘由",
"低于底价线的价会被底价门禁标出来:两成以内附理由即可放行,超过两成的要另外找人拍板 —— 你在这里往上调一点,就不必再惊动一个人",
"库内查不到成本的料没有建议价:按你的经验定价,并在备注写明依据;实在定不了的按行首 × 把这一条撤出本轮"],
"standard":"每一行都有拟报单价,而且每个价都说得出来路(基准 / 毛利档 / 让价理由)—— 缺一行这张单按不下提交(系统的闸看着)。",
"basis":[{"k":"<PN>","v":"<类别 · 成本基准 · 毛利档 · 建议价>"}]}}
· \`basis\` 逐条列本单的料:PN + 基准与建议价。几颗写几条,别铺全表。
· **卡上不摆表、不摆格子**(执行卡零输入):整份草案就在工作单那份件上,
拟报单价与备注两列都能就地改、行能撤 —— **别往卡里塞 \`blocks\` 或 \`fields\`**。
· **这张卡带提交闸**:拟报单价(必填列)还空着,他按不下提交 —— 缺几格、缺在哪行,
闸上与件上的红带都写着(与放行门 L1 同一个数同一套算法)。
发完卡这一拍就结束了(发卡挂起)。
【人交回之后 —— 他填的价基座已经替你写回台账了】这一步你只做两件事,然后 finish:
① 写 verdict:**confirmed_total** = 他定过价的条数(照草案行数记);
他在件上撤掉了行的话,**line_total 跟着改成撤后的实数**。
② finish 这一项。
**别在这一拍算门禁** —— 底价自检是下一步的活(它有自己的一段做法),这里算了也白算。
草案行你一列都不用重写:他填的值基座已经落进台账了,重写会被「写了等于没写」拒回。
【这张卡一单只发一次】人交回来之后不许再发同一张卡 —— 必填有提交闸兜着,
卡回来时拟报单价一定是齐的(或那几行已被他撤掉)。`,
},
{
item: 'S3_gate',
intent: '底价自检:按人定下的价逐条算毛利、偏离度与门禁处置,判完即定盘',
writes: ['quote_pricing.draft', 'quote_pricing.verdict'],
reads: ['quote_pricing.draft'],
view: 'pricing-worksheet',
tools: [],
/*
* **收步判据与验收判据同源**:三档处置的条数加起来等于行数(两边都是基座现数)——
* 判完了才算办完。`_rows > 0` 那一半不能省:开案时行数是 0,而 0+0+0 = 0
* 会让这一步开局即收(未绑定名字的坑之外,还有「全零相等」这一坑)。
*/
doneWhen:
'quote_pricing_draft_rows > 0 and quote_pricing_draft_disposition_pass + quote_pricing_draft_disposition_note + quote_pricing_draft_disposition_block = quote_pricing_draft_rows',
guide: `人把价定完了,**现在才轮到算门禁** —— 这一步是纯算术,一拍连做:
① **逐行按他定的 quoted_price 算三列**(**quoted_price 你一格都不写,只写这三列**):
· margin =(quoted_price − base_price)/ base_price,四位小数;
· **偏离度** deviation =(quoted_price − floor_price)/ floor_price,四位小数;
· **门禁处置** disposition 按底价门禁 P-02 判,只许写这三个大写码之一,一个字母不许差:
- deviation ≥ 0 → **PASS**(在底价线之上,直接放行)
- −0.20 ≤ deviation < 0 → **NOTE**(低于底价线两成以内,附理由放行)
- deviation < −0.20 → **BLOCK**(低于两成以上,压在拦截档,要人拍板)
· **base_price 空(无基准)的行**:margin / deviation 给空串 \`""\`(**不许写 0** ——
0 看起来像「正好压在底价线上」,而这一行压根没有底价线)、disposition 记 **NOTE**,
price_note 里保留他写的定价依据(他没写就补一句「库内无成本基准,由 BD 定价」)。
**一行都不能漏**:三档处置的条数加起来必须等于 quote_pricing_draft_rows(基座数的行数)。
现在三档都是 0,就是一行都还没判 —— 这一步没办完,放行门开不了。
② 看这一拍写完之后的**拦截档条数**(disposition = BLOCK 的行):
· 一条都没有 → 同一拍把 verdict 的 **route_decided_total 记 1**(定盘)、
summary 写一句人话(几条直接放行、几条附理由),然后 finish 这一项;
· 还有 → **别写 route_decided_total**,只 finish 这一项 —— 下一拍会有一步专门叫人拍板。`,
},
{
item: 'T_approve',
intent: '超限条目要人拍板 —— 发决策卡,照人选的那条路改草案',
writes: ['quote_pricing.draft'],
reads: [],
intent: '压在拦截档的条目要人拍板 —— 发一张决策卡,照他选的那条路把草案改掉',
writes: ['quote_pricing.draft', 'quote_pricing.verdict'],
reads: ['quote_pricing.draft'],
view: 'pricing-roster',
tools: [],
// 人工激活 + 要人拍板 —— 不给 doneWhen,引擎不许替人收
guide: `**只在 over_total > 0 时走这一步**。它触达客户利益,标了人工激活:
**先 start T_approve**(与发卡排同一拍即可),再发决策卡(decide,itemId 填 T_approve)。
选项按门禁处置给(照原价报 / 按底价报 / 退回重算),consequence 写清后果;
basis 摆上哪几条、偏离多少、底价多少(**裸卡基座拒发**)。
人点完轨迹上会出现「decision 卡回传 —— 人选了:xxx」,**照那条路改 draft,别再发第二张卡**。`,
/* 人工激活 + 要人拍板 —— 不给 doneWhen,引擎不许替人收 */
guide: `**这一单只发一张决策卡。动手之前先分清你在哪一半:**
· 轨迹里**已经有**「decision 卡回传」/「人选了:xxx」—— 那就跳到下面【人点完之后】那一段,
照他选的那条路改草案、写 route_decided_total、finish。**一个 decide 都不许再发**
(实测栽过:人点完了,而这一步又从头念了一遍「发一张决策卡」,同一张卡连发四次,
调人预算烧光,单子 exhausted —— 他点过的那一下就白点了);
· 轨迹里**还没有** —— 才轮到下面【发一张决策卡】那一段。
这一步只在还有条目压在拦截档(基座现数 quote_pricing_draft_disposition_block > 0)时入场;
它触达客户利益,标了人工激活:发卡就算启动,不必单发 start。
工作区这时摆的是**报价草案清单**,整份草案与门禁分布都在上面,他看得见全局再拍板。
【发一张决策卡,照这个骨架】(选项少于两个基座拒发):
{"kind":"decide","itemId":"T_approve","ask":"<N> 条低于底价线两成以上,按哪条路办?",
"options":[
{"value":"floor","label":"回调到底价线报","consequence":"守住毛利下限,客户可能不接受这个价","recommended":true},
{"value":"as-is","label":"照现价破线报,附理由","consequence":"接住客户目标价拿单,毛利跌破下限,理由随草案交审批"},
{"value":"drop","label":"这几条本轮不报","consequence":"撤出本单草案,客户拿到的是一份缺项的报价"}],
"basis":[{"k":"<PN>","v":"成本基准 <X> · 底价线 <Y> · 拟报 <Z> · 偏离 <W>"}]}
· basis 逐条摆压在拦截档的那几颗料:基准、底价线、拟报价、偏离度 —— 裸卡基座拒发。
· 发完这一拍就结束了(发卡挂起)。
【人点完之后】轨迹上会出现「decision 卡回传 —— 人选了:xxx」。**照那条路把草案改掉,
别再发第二张卡**,一拍连做:
· 选 floor:**这是全活动唯一一次你可以写 quoted_price** —— 那个数不是你定的,
是他在卡上点下的(就是该行的底价线),你只是替他落账:
那几行 quoted_price 改成本行的 floor_price,margin 记该行毛利档的下限、deviation 记 0、
disposition 改 **PASS**,price_note 写「按底价线回调,由负责人拍板」;
· 选 as-is:价不动,disposition 改 **NOTE**,price_note 写明破线的理由与拍板人;
· 选 drop:用 \`drop\` 把那几行撤出本单草稿(主键照 \`quote_pricing_draft_keys\` 名单**原样粘**),
并把 verdict 的 line_total 改成撤后的实数。
然后写 verdict:**route_decided_total 记 1**、summary 补一句拍板结果,finish 这一项。
拦截档清零之后,放行门才开得了。`,
},
{
item: 'T_release',
intent: '草案已齐,提交放行交 BD 验收',
intent: '草案已定盘,提交放行交 BD 验收',
writes: [],
reads: [],
view: 'pricing-roster',
tools: [],
guide: `草案有行了 —— **先 start T_release**(人工激活),提交前用 judge 裁掉裁量判据
(criterion 照判据名原样写),然后 submit。终局唯放行:你不能自己宣布完成。`,
/* 放行触达世界,必须人按一下 —— 引擎不自动收它(剧本里也不给 doneWhen) */
guide: `每行有价、门禁处置守恒、拦截档清零、定盘已记 —— 可以走放行了
(T_release 人工激活:动它就算启动,不必单发 start)。先把裁量判据裁掉,然后 submit,同一拍连做:
· judge criterion="报价的来路说得清:基准价来源、毛利档与让价理由逐条可溯,BD 对报价负责" ——
看草案里每一行的基准、毛利档与备注:说得出来路就 satisfied=true;
有行说不出的(比如无基准由人定的价没写缘由),satisfied=**false**,rationale 逐行写清,
交签字的人定。**裁成不满足照样能提交** —— 理由连着卡一起交给他。
criterion 照判据名**整句原样抄**,写别的名字对不上。
终局唯放行:你不能自己宣布完成。`,
},
];
/**
* 阶段 —— 进度条的刻度,只给人看。除放行外每个计划项恰属一个阶段;
* 放行那一格由基座统一加,人看到的是 x/4。
*/
export const PRICING_PHASES = [
{ name: '取基准算建议价', items: ['S1_price'] },
{ name: '交BD定价', items: ['S2_review'] },
{ name: '底价自检', items: ['S3_gate'] },
{ name: '超限拍板', items: ['T_approve'] },
];
+12 -74
View File
@@ -1,79 +1,17 @@
/**
* 统一报价 A4 · 成本定价 —— 活动包代码入口(ADR-004)。
* 统一报价 A4 · 成本定价 —— 活动包代码入口(ADR-004:默认导出 defineActivity 的返回值)。
*
* flow 活动:下一步由步序声明给定。本文件只做声明装不下的两件事 ——
* 视图流控,与「结论不可静默覆写」的呈现:P-02 拦下的条目必须显式呈现给人。
* **这个活动没有 `run`,不是缺件。**
*
* 案例活动的推进由 Loop 驱动,行为全部写在清单里:案例模型 + 说明书 + 步骤剧本 +
* 两把判定表 + 两条技能 + 台账声明 + 两张 HTML 报告模版。
* 上一版这里有一段 `run()`(读草案、拼 kv/table 块、按拦下条数返回 await/submit)——
* 案例活动根本不会调它;摆一个空转的 run 去假装有实现,更坏:
* 它写的那套呈现与 `viewTemplates` 里的模版各说各话,而没人看得出以哪份为准。
*
* 这个文件之所以还存在,是因为清单的 `entry` 指着它 —— 它是**被静态审扫描的产物文本**
* (查 import 与全局引用,证明活动包零环境能力)。
*/
import { defineActivity } from '@sdk';
import type { ActivityBase, ActivityOutcome } from '@sdk';
const DRAFT = 'quote_pricing.draft';
export default defineActivity({
name: 'quote-pricing',
async run(base: ActivityBase): Promise<ActivityOutcome> {
const items = await base.ledger.read(DRAFT);
const priced = items.filter((r) => typeof r['quoted_price'] === 'number');
// 底价门禁 P-02 拦下的条目 —— DMN 结论是**约束性输入**,不得静默覆写
const blocked = items.filter((r) => String(r['disposition'] ?? '').includes('拦截'));
await base.interaction.pushView([
{
t: 'kv',
title: '定价概览',
items: [
{ k: '条目', v: `${items.length} 条` },
{ k: '已定价', v: `${priced.length} 条` },
{ k: '底价门禁拦下', v: `${blocked.length} 条` },
],
},
{
t: 'table',
title: '报价草案',
cols: ['PN', '拟报单价', '毛利率', '偏离度', '门禁处置'],
rows: items.map((r) => [
String(r['pn'] ?? ''),
fmt(r['quoted_price']),
pct(r['margin']),
pct(r['deviation']),
String(r['disposition'] ?? '待判'),
]),
linkDonut: {
col: 4,
title: '门禁分布',
map: [
{ match: '直接放行', label: '直接放行', tone: 'ok' },
{ match: 'PM', label: 'PM 附理由', tone: 'warn' },
{ match: '拦截', label: '拦截', tone: 'danger' },
],
},
},
]);
if (priced.length < items.length) {
await base.interaction.say(`还有 ${items.length - priced.length} 条未定价,继续按利润模型 P-01 求值。`, 'conclusion');
return { kind: 'await', reason: '等待定价步完成' };
}
if (blocked.length > 0) {
// 超出授权边界 → 走「人 · 决策」,不自行破线
await base.interaction.say(
`${blocked.length} 条低于底价 20% 以上,已被底价门禁 P-02 拦下 —— 需要人裁决回调还是注明理由。`,
'criterion',
);
return { kind: 'await', reason: '等待超限条目的人裁决' };
}
await base.interaction.say(`${items.length} 条已全部定价且过底价门禁,可提交审批。`, 'criterion');
return { kind: 'submit', deliverable: 'quote-draft', rationale: '全条目带偏离度,底价门禁全过' };
},
});
function fmt(value: unknown): string {
return typeof value === 'number' ? value.toFixed(2) : '—';
}
function pct(value: unknown): string {
return typeof value === 'number' ? `${(value * 100).toFixed(1)}%` : '—';
}
export default defineActivity({ name: 'quote-pricing' });
+313 -85
View File
@@ -1,27 +1,45 @@
/**
* 活动包清单 —— 统一报价 A4 · 成本定价。
* 活动包清单 —— 统一报价 A4 · 成本定价(案例活动)。
*
* 业务素材:docs/MVP示例工作流.html §1.2(S1 取价基准 → S2 逐条定价 → S3 底价自检)。
* README《已裁定的落地决策》③:**A4 = flow** —— 取价、查表定价、自检的判断
* **全部装进 DMN 决策表确定性求值**;一旦出现「由 AI 决定怎么办」,按 AM 2.1 上升。
* 业务素材:`docs/MVP示例工作流.html` §1.2(S1 取价基准 → S2 逐条定价 → S3 底价自检):
* 「BD 逐条定价:价格 = 底价 × 利润模型。提交审批前先自查底价门禁 ——
* 超限的自己先调,不把问题留给 PM。」
*
* 形状与 A3 同一条(2026-08-03 重写 @3.0.0):归一算价 → 一张执行单交人 → 超限才叫人
* 拍板 → 放行。**能算的不问人**(P-01 毛利档、P-02 底价门禁两把尺写死),
* **该人负责的不自算**(拟报单价是必填,库内没有基准的那几行系统一个价都不编)。
*
* 与 @2.0.0 的差别(三处都是踩过的账):
* ① `input.required` 从 `['rfqRef','customerCode','records']` 清空 —— 宽进严出,
* 上游(A3)发的是 `{rfqRef, rows}`,门口拦的恰是主路;
* ② 台账补 `required` —— 从前一个必填都没有,执行卡的闸没牙,人不填也能交卷;
* ③ 判据与哨兵全部换成**基座读数**(`_rows` / `_missing` / `_<列>_<值>`)——
* 从前 L1 读 `unpriced_total`、哨兵读 `draft_total`,两个都是活动自报的数。
*/
import type { ActivityManifest, DmnModel } from '@contracts';
import { PRICING_SKILLS } from './skills.ts';
import { CMMN_XML, NARRATIVE, PRICING_STEPS } from './case-model.ts';
import { activityIdSchema } from '@contracts';
import { PRICING_BASIS, PRICING_PRICED, PRICING_RELEASE } from './views/report.ts';
import { PRICING_SKILLS } from './skills.ts';
import { CMMN_XML, NARRATIVE, PRICING_PHASES, PRICING_STEPS } from './case-model.ts';
import { PRICING_ROSTER, PRICING_WORKSHEET } from './views/report.ts';
const DOMAIN = 'quote_pricing';
const D = 'quote_pricing';
const ACTIVITY_ID = activityIdSchema.parse('quote-pricing');
/**
* 利润模型 P-01 —— 品牌 × 类型 × 客户等级 × 数量档 → 目标毛利。
* 「经验」段落不入决策表(那是技能 / 活动记忆种子:载方法不载权力)。
* 两把尺 —— **规则本身不是数据**,随包冻结。
*
* · 利润模型 P-01:客户等级 × 类别 → 目标毛利区间(FIRST,行序即业务优先级);
* · 底价门禁 P-02:相对底价线的偏离度 → 处置码。
*
* 出参就是台账列的取值(`disposition` 的三个大写码),模型算完照抄即可 ——
* 「出参键名与台账列同名」是为了它拿回去直接 write(规范 §5.6)。
* 两把尺的档位在三处出现:这张表、剧本 guide、工作单上的规则块 ——
* 包内自测把三处钉在一起(改一处不改另两处,测就红)。
*/
export const P01_DMN: DmnModel = {
drd: [
{ decision: 'p01-margin', requires: [], inputs: ['customerGrade', 'materialType', 'volumeTier'] },
{ decision: 'p02-floor', requires: ['p01-margin'], inputs: ['quotedPrice', 'floorPrice'] },
{ decision: 'p01-margin', requires: [], inputs: ['customer_grade', 'type_tag'] },
{ decision: 'p02-floor', requires: ['p01-margin'], inputs: ['deviation'] },
],
tables: [
{
@@ -29,30 +47,29 @@ export const P01_DMN: DmnModel = {
label: '利润模型 P-01',
hitPolicy: 'FIRST',
inputs: [
{ name: 'customerGrade', expression: 'customerGrade' },
{ name: 'materialType', expression: 'materialType' },
{ name: 'volumeTier', expression: 'volumeTier' },
{ name: 'customer_grade', expression: 'customer_grade' },
{ name: 'type_tag', expression: 'type_tag' },
],
outputs: [{ name: 'marginLow' }, { name: 'marginHigh' }],
outputs: [{ name: 'margin_low' }, { name: 'margin_high' }],
rules: [
{ id: 'p01-1', when: ['"战略客户"', '"A"', '"大批量"'], then: [0.08, 0.1], annotation: '战略客户 A 类标准件大批量 8%–10%' },
{ id: 'p01-2', when: ['"普通客户"', '"A"', '-'], then: [0.12, 0.15], annotation: '普通客户 A 类标准件 12%–15%' },
{ id: 'p01-3', when: ['-', '"B"', '-'], then: [0.15, 0.2], annotation: 'B 类波动件 15%–20%' },
{ id: 'p01-4', when: ['-', '"C"', '-'], then: [0.2, 0.5], annotation: 'C 类替代/EOL ≥20% 或一事一议' },
{ id: 'p01-default', when: ['-', '-', '-'], then: [0.12, 0.15], annotation: '兜底按普通客户标准件' },
{ id: 'p01-1', when: ['"战略客户"', '"A"'], then: [0.08, 0.1], annotation: '战略客户 A 类标准件(大批量长单)8%~10%' },
{ id: 'p01-2', when: ['"普通客户"', '"A"'], then: [0.12, 0.15], annotation: '普通客户 A 类标准件 12%~15%' },
{ id: 'p01-3', when: ['-', '"B"'], then: [0.15, 0.2], annotation: 'B 类波动件 15%~20%' },
{ id: 'p01-4', when: ['-', '"C"'], then: [0.2, 0.5], annotation: 'C 类替代/停产件 20%~50%(一事一议)' },
{ id: 'p01-default', when: ['-', '-'], then: [0.12, 0.15], annotation: '拿不准按普通客户标准件档' },
],
},
{
/** 底价门禁 P-02 —— 偏离底价 ±20% 即止 */
/** 底价门禁 P-02 —— 偏离底价线 ±20% 即止;出参就是台账 `disposition` 的取值 */
id: 'p02-floor',
label: '底价门禁 P-02',
hitPolicy: 'FIRST',
inputs: [{ name: 'deviation', expression: 'deviation' }],
outputs: [{ name: 'disposition' }, { name: 'passable' }],
rules: [
{ id: 'p02-1', when: ['>= 0'], then: ['直接放行', true], annotation: '≥底价 → 直接放行' },
{ id: 'p02-2', when: ['[-0.20..0)'], then: ['PM 附理由放行', true], annotation: '低于底价 ≤20% → PM 附理由' },
{ id: 'p02-3', when: ['< -0.20'], then: ['拦截 · BOSS 拍板', false], annotation: '低于底价 >20% → 拦截' },
{ id: 'p02-1', when: ['>= 0'], then: ['PASS', true], annotation: '在底价线之上 → 直接放行' },
{ id: 'p02-2', when: ['[-0.20..0)'], then: ['NOTE', true], annotation: '低于底价线两成以内 → 附理由放行' },
{ id: 'p02-3', when: ['< -0.20'], then: ['BLOCK', false], annotation: '低于两成以上 → 拦截,人拍板' },
],
},
],
@@ -62,95 +79,162 @@ export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'A4 · 成本定价',
packageVersion: '2.0.0',
packageVersion: '3.5.0',
category: '统一报价',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-quote-pricing',
},
/* ── 契约七件 ────────────────────────────────────────────────── */
contract: {
activityId: ACTIVITY_ID,
version: '0.1.0',
version: '1.0.0',
input: {
/*
* **宽进到底:一个必填都不设**(与 A1/A2/A3 同口径 —— 宽进严出,
* 输入契约在活动内部归一 + 补全)。
*
* 来路三种形状:寻源转来(rows,行上带 base_price)、受理/选型直达(records/rows,
* 没有底价)、单飞(一段文字)。上一版必填 `rfqRef + customerCode + records`,
* 而 A3 的 `quotation` 口发的是 `{rfqRef, rows}` —— 门口拦的恰是主路。
*/
data: {
type: 'object',
required: ['rfqRef', 'customerCode', 'records'],
required: [],
properties: {
rfqRef: { type: 'string', title: '询价单号(单标识,贯穿全流程)' },
customerCode: { type: 'string' },
records: { type: 'array' },
// B/C 类由 A3/A2 交回的底价与替代方案;A 类直通时为空
floorPrices: { type: 'array' },
customerCode: { type: 'string', title: '客户码' },
rows: { type: 'array', 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: '读出来的文本' },
},
},
},
},
},
messageAllowed: true,
messageHint: '客户等级、急单、竞对价、这一单的让价授权写这里',
},
output: {
/*
* 只有一个出口:报价草案交定价审批(流程包 E8 是无条件边)。没有选择组 ——
* 没有去向要选,定盘就是把账写齐。
*
* 载荷键名照下游(A6 议价)的入参写:`rfqRef / customerCode / items` ——
* 边读什么、下游要什么,上游就得发什么。标量都有台账列承载(自测钉着)。
*/
mandatory: [
{
name: 'draft',
payload: {
type: 'object',
required: ['rfqRef', 'items'],
properties: { rfqRef: { type: 'string' }, items: { type: 'array' } },
properties: {
rfqRef: { type: 'string', title: '询价单号(单标识 —— 下游台账的主键)' },
customerCode: { type: 'string', title: '客户码' },
items: { type: 'array', title: '报价草案(每条带拟报单价、偏离度与门禁处置)' },
},
},
description: '报价草案(逐条带偏离度)',
description: '报价草案(全条目带拟报单价与门禁处置)',
},
],
selective: [],
},
/*
* 必交付物**必须给 `ledger` 地址**(2026-07-31 修)。
* 不给的话预检退回 `scope[key]` 去找 —— key 是 `benchmark-table` 这样的短横线别名,
* 任务视图里永远没有这个键,于是「必交付物缺一不得提交放行」永远拒,
* 活办完了也放不了行,连拒五拍熔断(实测 A2 死于此)。
* 必交付物只有一件:**报价草案**。定价结论(verdict)是它的结论栏,
* `ledgers` 说清这份成果由哪几张表承载(一份成果,不是两份)。
*/
requiredDeliverables: [
{ key: 'quote-draft', label: '报价草案(全条目带偏离度)', ledger: `${DOMAIN}.draft` },
{
key: 'quote-draft',
label: '报价草案(每条带拟报单价与门禁处置)',
ledger: `${D}.draft`,
/* 验收页上让 BD 就地改的两列 —— 恰是他的职权面(定价与让价理由) */
editableAtRelease: ['quoted_price', 'price_note'],
ledgers: [`${D}.draft`, `${D}.verdict`],
},
],
/*
* 机检谓词读的是 `verdict` 那张**汇总表**的列(照 A1 的口径)。
* 上一版的名字(unpriced;floor-gate 更是连谓词都没有)**台账里根本没有** —— S-FEEL 拿未绑定的名字
* 比大小求值为假,判据永远达不了标,submit 门永远拒,连拒五拍熔断。
* 判据的名字必须落在台账上,否则「机检」是一句空话(2026-07-31 销账)。
* 验收判据 —— **机检(L1/L2)只读基座数得出来的数**:
* `_rows` 基座数行数、`_missing` 基座按 required 现数(与执行卡提交闸同一个数
* 同一套算法)、`_disposition_*` 基座按值现数。
* 上一版两条机检读活动自报的 `unpriced_total` / `unresolved_over_total` ——
* 那不是门,是「改哪个数能过」的提示(违反史已记四次,这是第五类)。
*/
acceptanceCriteria: [
{ key: 'all-priced', label: '每条都有拟报单价与毛利率', level: 'L1', predicate: 'unpriced_total = 0' },
// 底价门禁沉为 L2 机检 —— 「约束力收缩」的终点
{ key: 'floor-gate', label: '偏离底价 ≤±20%,超限者已回调或注明理由', level: 'L2', modelRef: 'p02-floor', predicate: 'unresolved_over_total = 0' },
{ key: 'pm-release', label: 'BD 放行', level: 'L5' },
{
key: 'roster-complete',
label: '草案齐:上游条目逐条承继,说了几条草案里就躺着几行',
level: 'L1',
predicate: `line_total > 0 and ${D}_draft_rows = line_total`,
},
{
key: 'all-priced',
label: '每条都有拟报单价:库内无基准价的那几行也由人定过价,缺一行放不了',
level: 'L1',
predicate: `${D}_draft_rows > 0 and ${D}_draft_missing = 0`,
},
{
key: 'floor-gate',
label: '底价门禁已过:处置逐条落定,没有还压在拦截档上的条目 —— 超限的要么回调、要么经人拍板附理由',
level: 'L2',
modelRef: 'p02-floor',
predicate: `${D}_draft_disposition_block = 0 and ${D}_draft_disposition_pass + ${D}_draft_disposition_note + ${D}_draft_disposition_block = ${D}_draft_rows`,
},
/*
* 报价来路可核是**裁量**(L3):基准是不是那个基准、让价理由站不站得住,
* 机器数不出来。这句 label 与剧本里 judge 的 criterion **必须逐字一致**。
*/
{
key: 'price-defensible',
label: '报价的来路说得清:基准价来源、毛利档与让价理由逐条可溯,BD 对报价负责',
level: 'L3',
},
/* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */
{ key: 'bd-release', label: 'BD 放行:签下去这份报价草案即定盘并交定价审批', level: 'L5' },
],
revisableAtRelease: [],
prohibitions: [
{ key: 'no-silent-override', statement: '不得静默绕开 P-02 的结论;要破线只能走「人 · 决策」' },
{ key: 'no-stale-floor', statement: '底价超一周未更新的,先刷底价再判' },
{ key: 'no-silent-override', statement: '不得静默绕开底价门禁的结论;要破线只能走「人 · 决策」并留下理由' },
{ key: 'no-made-up-basis', statement: '没有成本基准的料不许编一个偏离率出来:基准空着就空着,价由人定并注明依据' },
{ key: 'no-stale-floor', statement: '底价超一周未更新的,先刷底价再判 —— 拿过期的成本算出来的毛利是假的' },
],
authorization: {
discretion: [{ key: 'margin-band', statement: '在 P-01 给出的毛利区间内自决;出区间必须调人决策' }],
discretion: [{ key: 'margin-band', statement: '在 P-01 给出的毛利区间内自决;出区间要么回调,要么调人拍板' }],
preauthorizedActions: [],
humanCallBudget: 3,
/* 一张执行单 + 可能一张决策卡 + 放行 = 三次;留一倍余量 */
humanCallBudget: 5,
},
resources: {
/*
* `quote-pricing.basis` 已从声明里去掉(2026-07-31):工具箱里从没注册过它,
* 装包时静态审一直报「运行时转人代办」—— 而重做后的案例模型压根不调它
* (算价归判定表 P-01/P-02,取基准读历史价)。声明一件不存在也不用的工具,
* 只会让每一单跑到 A4 就弹一张莫名其妙的转人卡。
*/
tools: [{ name: 'crm.history-price-read' }],
skills: PRICING_SKILLS.map((s) => s.id),
models: ['deepseek-v4-flash'],
roles: ['BD'],
budget: { steps: 60, tokens: 150_000, activeMinutes: 120, humanCalls: 3 },
/* SLA <2h(MVP示例 §1.2);预算按 A3 实测(6-8 拍 / 40-50k)留一倍余量 */
budget: { steps: 60, tokens: 400_000, activeMinutes: 120, humanCalls: 5 },
},
},
/* ── 声明 ────────────────────────────────────────────────────── */
declarations: {
models: ['deepseek-v4-flash'],
tools: [{ name: 'crm.history-price-read' }],
ledger: [
{
domain: DOMAIN,
domain: D,
object: 'draft',
kind: 'record',
caseKey: 'rfq_ref',
@@ -158,82 +242,226 @@ export const manifest: ActivityManifest = {
fields: [
{ name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
{ name: 'pn', type: 'text', primaryKey: true, masked: false, label: 'PN' },
{ name: 'quoted_price', type: 'number', primaryKey: false, masked: false, label: '拟报单价' },
{ 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: 'lead_time', type: 'text', primaryKey: false, masked: false, label: '客户要求交期' },
/* 客户备注承继自上游 —— 认证要求、预授权、急料标注,让价时的第一手依据 */
{ name: 'remark', type: 'text', primaryKey: false, masked: false, label: '客户备注(约束/急料)' },
{ name: 'type_tag', type: 'text', primaryKey: false, masked: false, label: '类别(承继)', tally: ['A', 'B', 'C'] },
/*
* **客户目标价标 `expect`** —— 客户该给而没给的那一问不能省(件上出橘色
* 「建议填」胶囊)。它不拦单:`_gaps` 只喂 L4,而本活动没有 L4 判据。
* 注意边界:我方自己写的注(price_note)**不标 expect** —— 标了 `_gaps`
* 恒大于 0,那条判据永远绿不了(A1 的账)。
*/
{ name: 'target_price', type: 'number', primaryKey: false, masked: false, expect: true, label: '客户目标价' },
/* 成本基准:上游底价或库内历史价。**取不到就空着** —— 空着不是异常,是「无基准」那一档 */
{ name: 'base_price', type: 'number', primaryKey: false, masked: false, label: '成本基准' },
{ name: 'price_basis', type: 'text', primaryKey: false, masked: false, label: '基准来源', tally: ['HIST', 'SOURCED'] },
{ name: 'margin_band', type: 'text', primaryKey: false, masked: false, label: '毛利档(P-01)' },
{ name: 'floor_price', type: 'number', primaryKey: false, masked: false, label: '底价线' },
/*
* **系统建议价是 AI 的活,拟报单价是人的活** —— 两列分开是**结构治**,不是措辞治。
*
* 实测账(2026-08-03,单飞那条案例):从前只有 `quoted_price` 一列,剧本写着
* 「无基准的行留空,由 BD 定」—— 模型照样给两颗库里查不到的料编了价
* (一颗抄了客户目标价,另一颗凭空写了 0.085),交代里还说着「价空着等你定」。
* 一条要模型分辨「这行算得出/算不出」的规矩,它十次里会走岔;
* 而「这一列你一格都不写」没有分支可走岔。
*
* 于是:AI 只写 `suggested_price`(算得出就给,算不出就空着),
* **`quoted_price` 永远是人在件上定的那个数** —— 它标 `required`,
* 件上的缺价红带、执行卡的提交闸、放行门 L1 三处同源(都数基座的 `_missing`)。
* 不用 `requiredWhen`:每一行都要有价,没有「这一行还没轮到」的情形。
*/
{ name: 'suggested_price', type: 'number', primaryKey: false, masked: false, label: '系统建议价' },
{ name: 'quoted_price', type: 'number', primaryKey: false, masked: false, required: true, label: '拟报单价(BD 定)' },
{ name: 'margin', type: 'number', primaryKey: false, masked: false, label: '毛利率' },
{ name: 'last_price', type: 'number', primaryKey: false, masked: false, label: '基准价' },
{ name: 'deviation', type: 'number', primaryKey: false, masked: false, label: '偏离度' },
{ name: 'disposition', type: 'text', primaryKey: false, masked: false, label: '门禁处置' },
{ name: 'deviation', type: 'number', primaryKey: false, masked: false, label: '偏离底价线' },
/*
* **门禁处置:取值域给出来,条数由基座现数** —— PASS / NOTE / BLOCK。
* 守恒判据、分布条、门禁徽读的全是基座读数;值是机器码,
* 人读名(直接放行 / 附理由 / 拦截待拍板)由模版样式表翻译。
*/
{ name: 'disposition', type: 'text', primaryKey: false, masked: false, label: '门禁处置', tally: ['PASS', 'NOTE', 'BLOCK'] },
{ name: 'price_note', type: 'text', primaryKey: false, masked: false, label: '备注 / 让价理由' },
],
},
{
/*
* 定价结论 —— 一单一行的**汇总表**,验收机检谓词的入参就在这儿。
* 定价结论 —— 一单一行的**汇总表**,哨兵与报告读它。
* 汇总列与逐行列**故意不同名**:任务视图把草稿行摊平到顶层是「后写的赢」,
* 拿逐行列求值只会验到最后写的那一行,那不叫机检、叫抽样(A1 的教训)。
*/
domain: DOMAIN,
domain: D,
object: 'verdict',
kind: 'record',
caseKey: 'rfq_ref',
label: '定价结论',
fields: [
{ name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
{ name: 'pn', type: 'text', primaryKey: false, masked: false, label: '型号' },
{ name: 'draft_total', type: 'integer', primaryKey: false, masked: false, label: '草案条数' },
{ name: 'unpriced_total', type: 'integer', primaryKey: false, masked: false, label: '未定价条数' },
{ name: 'over_total', type: 'integer', primaryKey: false, masked: false, label: '超限条数' },
{ name: 'unresolved_over_total', type: 'integer', 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: '客户名称' },
/* 客户等级是 P-01 的入参之一(战略客户 / 普通客户),认不出按普通客户 */
{ name: 'customer_grade', type: 'text', primaryKey: false, masked: false, label: '客户等级' },
{ name: 'line_total', type: 'integer', primaryKey: false, masked: false, label: '草案条目数' },
{ name: 'basis_total', type: 'integer', primaryKey: false, masked: false, label: '有成本基准的条数' },
/* 算价走完的凭据(0 也要写)—— 哨兵按它放交人核价那一步入场 */
{ name: 'priced_total', type: 'integer', primaryKey: false, masked: false, label: '已算出建议价条数' },
/* 「BD 核过价了」的凭据 —— 只在人环走完才产生 */
{ name: 'confirmed_total', type: 'integer', primaryKey: false, masked: false, label: 'BD 已核价条数' },
{ name: 'route_decided_total', type: 'integer', primaryKey: false, masked: false, label: '已定盘' },
{ name: 'summary', type: 'text', primaryKey: false, masked: false, label: '定价结论' },
],
},
],
listeners: [
{
/** 时限过半:SLA <2h,给 BD 与负责人提个醒 —— 静态监听,时限必配 */
origin: 'static',
eventType: 'human',
keyFilter: { kind: 'decision' },
semantic: 'complete-step',
deadline: 'PT4H',
escalation: ['nudge', 'escalate'],
eventType: 'time',
keyFilter: { kind: 'half-deadline' },
semantic: 'observe',
deadline: 'PT1H',
escalation: ['nudge'],
},
],
// SLA <2h(MVP示例 §1.2 留痕 · SLA)
harness: {
memoryWindow: 20,
memoryWindow: 22,
// SLA <2h(MVP示例 §1.2 留痕 · SLA)
stallAfter: 'PT2H',
nudge: [{ after: 'PT1H', action: 'nudge' }],
retry: { times: 2, backoff: 'PT30S' },
},
},
/* ── 资产初始版 ──────────────────────────────────────────────── */
assets: {
/* 资产初始版:案例模型(2026-07-29 起活动只有这一种业务模型) */
businessModel: {
cmmnXml: CMMN_XML,
narrative: NARRATIVE,
dmn: P01_DMN,
/* 步骤剧本 —— 与 A1 同一套:doneWhen 真了引擎自己收步,不问模型 */
steps: PRICING_STEPS,
phases: PRICING_PHASES,
},
skills: PRICING_SKILLS,
/*
* 两个局面各一张(与 A2/A3 同款口径):
* · `pricing-worksheet` 定价工作单 —— 算价与 BD 核价期间摆它(逐颗定价档案、
* 两把尺、填回工作表);
* · `pricing-roster` 报价草案清单 —— 拍板、放行、归档摆它。
*/
viewTemplates: [
{ name: 'pricing-basis', html: PRICING_BASIS },
{ name: 'pricing-priced', html: PRICING_PRICED },
{ name: 'pricing-release', html: PRICING_RELEASE },
{ name: 'pricing-worksheet', html: PRICING_WORKSHEET },
{ name: 'pricing-roster', html: PRICING_ROSTER },
],
cardSchemas: {},
distillHooks: [
{
on: 'terminal',
prompt: '归纳本单定价的经验:哪些组合的毛利档被人回调过、底价门禁拦下的形态、破线的理由。',
prompt: '归纳本单定价:哪些组合的毛利档被人回调过、底价门禁拦下的形态、破线的理由与拍板结果。',
},
],
},
/* ── 样例集 —— 三条形状真不同(常备案例即验收案例)───────────── */
samples: [
{
name: '战略客户 A 类大批量',
input: { customerCode: 'C001', records: [{ pn: 'LM358DR', type: 'A' }] },
/*
* ① 寻源转来 · 常路:A3 `quotation` 口的真实形状 —— rows 里清单行(带底价与价源)
* 混着结论行。客户目标价都在底价线之上,全 PASS,不必叫人拍板。
*/
name: '寻源转来 · 两条料底价齐 · 全在底价线之上(常路)',
input: {
rfqRef: 'RFQ MD14.1',
customerCode: 'CU-0142',
customer_name: 'Itelma',
rows: [
{
rfq_ref: 'RFQ MD14.1',
pn: 'BBPY00201209601Y00',
customer_pn: '4-14-01502',
category: '滤波器',
brand: 'YAGEO',
eau: 30000,
target_price: 0.085,
lead_time: '8周',
type_tag: 'B',
base_price: 0.058,
price_basis: 'SOURCED',
supplier: '深圳华强渠道',
},
{
rfq_ref: 'RFQ MD14.1',
pn: 'CJTB0101R6N',
customer_pn: '4-00-04102',
category: '集成电路',
brand: 'Jiangsu Changjing',
eau: 60000,
target_price: 0.42,
lead_time: '6周',
type_tag: 'A',
base_price: 0.35,
price_basis: 'HIST',
},
{ rfq_ref: 'RFQ MD14.1', quote_line_total: 2, route_decided_total: 1, customer_name: 'Itelma', summary: '两条底价齐,交定价' },
],
},
expect: { ports: ['draft'] },
},
{
/*
* ② 有超限条目:客户把目标价压到成本之下(0.06 对成本 0.082)——
* 偏离底价线超过两成,门禁判 BLOCK,必须叫人拍板(决策卡)。
*/
name: '客户压价 · 一条低于底价线两成以上 → 决策卡叫人拍板',
input: {
rfqRef: 'RFQ-2026-0777',
customerCode: 'CU-0311',
customer_name: '青岛海洋电子',
rows: [
{
rfq_ref: 'RFQ-2026-0777',
pn: 'PESD2ETH-XR',
category: '二极管',
brand: 'Nexperia',
eau: 30000,
target_price: 0.06,
lead_time: '8周',
type_tag: 'B',
base_price: 0.082,
price_basis: 'SOURCED',
remark: '客户明说这个价是竞对给的,要跟',
},
{
rfq_ref: 'RFQ-2026-0777',
pn: 'NE555P',
category: '定时器',
brand: 'TI',
eau: 5000,
target_price: 1.05,
lead_time: '4周',
type_tag: 'A',
base_price: 0.85,
price_basis: 'HIST',
},
{ rfq_ref: 'RFQ-2026-0777', quote_line_total: 2, route_decided_total: 1, customer_name: '青岛海洋电子' },
],
},
expect: { ports: ['draft'] },
},
{
/*
* ③ 单飞宽进:没有单号没有清单,正文列料 —— 归一造案号;库内查不到历史价,
* 系统一个价都不编,两行拟报单价空着交 BD 定 → **提交闸真拦**空手交卷。
*/
name: '单飞需求 · 正文列两颗料 · 库内无基准 → 价由人定(闸真拦)',
input: {
subject: '两颗料报个价',
body: '客户要两颗料的报价:XCJ-7788T 电源模块 8K,目标价 12.5;LMR-3302QQ 稳压器 15K,没给目标价。库里应该没做过这两颗,你先出个草案。',
},
expect: { ports: ['draft'] },
},
],
+23 -13
View File
@@ -1,5 +1,8 @@
/**
* A4 技能 —— 定价的方法经验(AM 08)。载方法,不载权力。
* A4 技能 —— 定价的方法经验。载方法,不载权力。
*
* 与剧本的分工:剧本写「这一步做什么、写哪几列」,技能写「为什么这么定、
* 拿不准时按哪条走」。剧本改了口径,技能里同一句要跟着改(规范 §5.9)。
*/
import { skillIdSchema } from '@contracts';
import type { Skill } from '@contracts';
@@ -8,34 +11,41 @@ export const PRICING_SKILLS: Skill[] = [
{
id: skillIdSchema.parse('pricing.pick-basis'),
label: '选对价格基准',
applicability: '定价开始,需要确定「跟谁比」时。',
applicability: '定价开始,需要确定「这个价跟谁比」时。',
method: [
'基准的优先级:① 本客户近期成交价 → ② 同型号他客户成交价 → ③ 供应商底价表 → ④ 无基准。',
'基准的优先级:① 上游寻源问回来的货源底价 → ② 本客户近期成交价 → ③ 同型号他客户成交价 → ④ 无基准。',
'越靠前越贴近这一单的真实议价空间;越靠后越要留余量。',
'',
'**无基准不是异常**,是一种基准:新料号、久未报价的老料号都会落到这里。',
'此时偏离率记 0(没有可偏离的对象),由底价门禁按「无基准」放行 —— ',
'此时成本基准、底价线、偏离度**一律留空**,拟报单价交给人定 ——',
'不要拿一个不相干的价去凑一个偏离率出来,那比没有基准更危险:它看起来像有依据。',
'',
'基准超一周未更新的先刷再判:拿过期成本算出来的毛利,数字是真的,结论是假的。',
].join('\n'),
crystallized: [{ tool: 'crm.history-price-read', input: { pn: 'pn' } }],
knowledgeRefs: ['利润模型 P-01'],
version: '1.0.0',
version: '2.0.0',
},
{
id: skillIdSchema.parse('pricing.read-gate'),
label: '读懂底价门禁的处置',
applicability: '底价门禁(P-02)给出处置结论,需要决定下一步时。',
applicability: '底价门禁(P-02)给出处置码,需要决定下一步时。',
method: [
'门禁不是「拦下」而是**分流**:',
'· 直接放行 —— 偏离在容许带内,照常提审。',
'· 超限待裁 —— 偏离超 ±20%,交人决断:回调价格,还是注明理由硬报。',
' 两条路都合法,但都要留下**理由**:这条留痕将来会成为客户等级与利润模型的迭代素材。',
'门禁不是「拦下」而是**分流**,三档各有各的下一步:',
'· PASS(在底价线之上)—— 照常提审,不必解释。',
'· NOTE(低于底价线两成以内)—— 附理由放行:让价的缘由写进备注,',
' 理由随草案一起交审批。不写理由的让价,到审批那一关同样过不去。',
'· BLOCK(低于两成以上)—— 系统不替任何人放行,交人拍板:回调到底价线、',
' 破线报并留理由、或这一条本轮不报。三条路都合法,都要留痕。',
'',
'不要为了让门禁「过」而回头改毛利参数 —— 那是把判据改成结论,',
'决策表就失去了它唯一的价值:同输入必同结论。',
'两条别做的事:',
'· 不要为了让门禁「过」回头改毛利档或改基准 —— 那是把判据改成结论,',
' 决策表就失去了它唯一的价值:同输入必同结论;',
'· 不要每一条都叫人。BD 自己能回调掉的,回调掉就是处置 ——',
' 每条都叫人,人就成了橡皮图章;一条都不叫,门禁就是摆设。',
].join('\n'),
crystallized: [],
knowledgeRefs: ['底价门禁 P-02'],
version: '1.0.0',
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>`;
+565 -178
View File
@@ -1,192 +1,579 @@
/**
* A4 成本定价的 HTML 报告模版 —— **活动自己持有的资产**(§10.3),随包版本冻结。
* A4 成本定价的 HTML 报告模版 —— **活动自己持有的资产**,随包版本冻结。
*
* 定价这件事最需要「把判据摊开」:**人签的不是一个数字,是这个数字的来路**。
* 所以三张屏不按「概览 / 明细 / 结论」排,按**一个价是怎么长出来的**排:
* ① 取价基准 —— 跟谁比。基准取错,后面每一条偏离率都是假的,而假偏离率长得跟真的一样;
* ② 定价与底价门禁 —— 凭什么是这个价。两张决策表的规则原样摆出来,人能逐条倒着核;
* ③ 报价草案待放行 —— 签字前最后一次能改的机会,门后的都算数了。
* 两个局面各一张(与 A2/A3 同款口径):
* · `pricing-worksheet` **定价工作单** —— 算价与 BD 核价期间摆它:逐颗料一张定价档案卡
* (题面在左、定价推演在右)、利润模型 P-01 的毛利档、底价门禁 P-02 的三档尺、
* 填回工作表就长在同一屏;
* · `pricing-roster` **报价草案清单** —— 超限拍板、放行与归档摆它:整份草案只读,
* 门禁分布与落地动作在上面。
*
* 一个局面一张,局面变了让 Agent 用 `view` 换,不在一张里堆条件分支。
* 容器是 `sandbox=""` iframe:无脚本、无外链、样式自带;数据由基座推送时填
* (`data-slot` 取值、`data-rows` 逐行展开)。
* 「1:1」的落点是**每份件与它的办结快照**(在办可编辑 ↔ 回执收下那一刻的 finalHtml),
* 不是全程只许一张:作业的件与收口的件是两件工作两份件(规范姊妹篇 §1)。
*
* 两条贯穿全包的呈现纪律:
* · **状态章一律用「在办」灯,不用「已办」印** —— 模版是随包冻结的静态字符串,
* 它盖不了一个还没发生的章。三张屏都是放行前的局面,盖「已通过」就是撒谎;
* · **`verdict` 的橙档专指「还没发生,等你按」** —— 绿只留给已达标的机检结论。
* 放行后果是预告不是既成事实,一律走橙档。
* 版面语言沿用 A1 从 PRD 目标件机械抽取的那套(`prd-style.ts` 原样拷入)——
* A4 没有自己的 PRD 目标件,与 A2/A3 同一处置:沿用家族版面自造。
*
* 取值三纪律:顶层只取汇总列与单值列;逐条走 `mine:quote_pricing.draft`;
* 列名逐字照 `declarations.ledger` 抄。**模版不算不判** —— 缺价红带、门禁徽、分布条
* 全部由基座算好的数经 `data-class-of` 投影,判断留在样式表。
*/
import { noteOnly, section, stagesOnly, statsBand, tableOnly, verdict, viewShell } from '@sdk';
import { PRD_CSS, PRD_SPRITE } from './prd-style.ts';
/**
* 静态阶梯 —— **规则本身不是数据**:P-01 的毛利档、P-02 的三档处置随包冻结,
* 它们不该走 `data-rows` 等基座来填(那会让「规则」看起来像「这一单的结果」)。
* 沿用外壳已有的 `.steps` 版式,与引擎推的阶段列表同一副面孔 —— 页面上只该有一套样式。
*/
function ladder(rows: ReadonlyArray<readonly [string, string, string]>): string {
return `<ul class="steps">${rows
.map(
([no, term, gloss]) =>
`<li><span class="n">${no}</span><span><span class="t">${term}</span> <span class="d">${gloss}</span></span></li>`,
)
.join('')}</ul>`;
/* ==================== 绑定期补的样式(照 A1/A3 的 BIND_CSS,地址换成本活动)==================== */
const BIND_CSS = `
/* 空占位三级(2026-08-02 全局口径):必填红 / 建议填橘 / 选填灰 —— 值真的空才轮到 */
.proto-root .jc-tbl-in td.req > .jc-vcell:empty::after,
.proto-root .jc-tbl-in td.sug > .jc-vcell:empty::after,
.proto-root .jc-tbl-in td.opt > .jc-vcell:empty::after {
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-tbl-in td.req > .jc-vcell:empty::after { content: "必填"; color: var(--danger); background: var(--danger-soft); border-color: #f3b6b3; }
.proto-root .jc-tbl-in td.sug > .jc-vcell:empty::after { content: "建议填"; color: var(--warn-accent); background: var(--warn-soft); border-color: #f0cf9a; }
.proto-root .jc-tbl-in td.opt > .jc-vcell:empty::after { content: "选填"; color: var(--ink-4); background: var(--soft); border-color: var(--line); }
/* 归档件里空格子就是「没有」,不再摆「你还能填」的胶囊 */
.proto-root .jc-tbl-in td.dimempty > .jc-vcell:empty::after { content: "—"; color: var(--ink-4); border: 0; background: none; padding: 0; }
/* 缺拟报价的整行浅红 + 行首红条 —— 原型的 tr.miss,这里由 :has() 认 */
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty) td { background: #fffbfb; }
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty):hover td { background: #fff5f5; }
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty) td.jc-rn { color: var(--danger); font-weight: 800; position: relative; }
@media (min-width: 1001px) {
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty) td.jc-rn::after {
content: ""; position: absolute; left: 0; top: 4px; bottom: 4px; width: 3px; border-radius: 0 3px 3px 0; background: var(--danger);
}
}
/** 局面一:S1 算价刚开场 —— 人此刻要核的是「这一单跟谁比、落在哪一档」 */
export const PRICING_BASIS = viewShell({
title: '取价基准',
kicker: 'A4 · 成本定价 · S1 算价',
tone: 'brand',
icon: 'coins',
stamp: { kind: 'live', text: '算价中' },
sign: 'A4 成本定价 · 取价基准(本单草稿)',
/* 序号由 CSS 计数器排 */
.proto-root .jc-tbl-in tbody { counter-reset: rn; }
.proto-root .jc-tbl-in td.jc-rn::before { counter-increment: rn; content: counter(rn); }
@media (max-width: 1000px) {
.proto-root .jc-tbl-in td.jc-rn::before { content: "第 " counter(rn) " 条"; }
.proto-root .jc-tbl-in td.jc-rn::after { content: none; }
}
/* 就地改:可改格子渲了只读与输入两份,默认锁着,表上加 on 才换孪生件 */
.proto-root .jc-cell { display: none; }
.proto-root .jc-tbl-in.on td > .jc-vcell,
.proto-root .jc-tbl-in.on td > .jc-tbadge { display: none; }
.proto-root .jc-tbl-in.on td > .jc-cell { display: block; }
/* 类徽与门禁徽不进编辑态(承继与引擎产物),on 时也照旧摆着 */
.proto-root .jc-tbl-in.on td > .jc-obadge, .proto-root .jc-tbl-in.on td > .jc-gbadge { display: inline-flex; }
/* 数值列与表头同侧左对齐(一行一个料,彼此不相加) */
.proto-root .jc-vcell.num, .proto-root .jc-cell.num { text-align: left; }
/* 编辑态下必填空格子:红框 + 整行浅红(由 :placeholder-shown 认) */
.proto-root .jc-tbl-in.on td.req > .jc-cell:placeholder-shown { border-color: #f3b6b3; background: #fffafa; }
.proto-root .jc-tbl-in.on tbody tr:has(td.req > .jc-cell:placeholder-shown) td { background: #fffbfb; }
/* 铺满工作区 */
.jc-stage { max-width: none; padding: 0 0 4px; }
.proto-root .jc-report { border: 0; border-radius: 0; box-shadow: none; }
/* 那道闸 —— 默认「已锁定 + 编辑」,开了变成「取消 / 保存」;只在可改屏现身 */
.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 那一档 display:none) */
.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; }
/* 增删行 */
.proto-root .jc-rowdel { display: none; }
.proto-root .jc-tbl-in.on td.jc-rn::before { content: none; }
.proto-root .jc-tbl-in.on .jc-rowdel {
display: block; width: 20px; height: 20px; margin: 0 auto; cursor: pointer;
border: 1px solid var(--line); border-radius: 6px; padding: 0;
font: inherit; font-size: 13px; line-height: 1; color: var(--ink-3); background: #fff;
transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast);
}
.proto-root .jc-tbl-in.on .jc-rowdel:hover { border-color: var(--danger); color: var(--danger); background: #fff6f6; }
.proto-root .jc-tbl-in.on tr.jc-row-gone td { background: #fff6f6; opacity: .55; }
.proto-root .jc-tbl-in.on tr.jc-row-gone .jc-cell { text-decoration: line-through; color: var(--ink-3); }
.proto-root .jc-tbl-in.on tr.jc-row-gone .jc-rowdel { border-color: var(--danger); color: var(--danger); background: #fff; }
.proto-root .jc-tbl-in.on tr.jc-row-new td { background: #f2fbf7; }
.proto-root .jc-addrow { display: none; }
html.editable .proto-root .jc-tbl-wrap:has(table.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 .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-segmeter > i.v-pass { background: var(--ok); }
.proto-root .jc-segmeter > i.v-note { background: var(--warn-accent); }
.proto-root .jc-segmeter > i.v-block { background: var(--danger); }
/* 门禁徽:直接放行绿 / 附理由橙 / 拦截红 —— 值(PASS/NOTE/BLOCK)是机器码,人读名交给样式表 */
.proto-root .jc-gbadge {
display: inline-flex; align-items: center; justify-content: center;
min-width: 56px; height: 20px; padding: 0 7px; border-radius: 6px;
font-size: 10px; font-weight: 800; letter-spacing: .02em;
color: var(--ink-4); border: 1.5px solid var(--ink-4); background: #fff;
}
.proto-root .jc-gbadge::after { content: "待判"; }
.proto-root .jc-gbadge.v-pass { color: var(--ok); border-color: var(--ok); }
.proto-root .jc-gbadge.v-pass::after { content: "直接放行"; }
.proto-root .jc-gbadge.v-note { color: var(--warn-accent); border-color: var(--warn-accent); }
.proto-root .jc-gbadge.v-note::after { content: "附理由"; }
.proto-root .jc-gbadge.v-block { color: var(--danger); border-color: var(--danger); }
.proto-root .jc-gbadge.v-block::after { content: "拦截待拍板"; }
/* 分布条:三段按条数分宽 —— 条数投影成 v-<N>,梯子取宽;零条那段自己消失 */
.proto-root .jc-distbar > i { flex: 24; min-width: 34px; display: inline-flex; align-items: center; justify-content: center; }
.proto-root .jc-distbar > i.v-0, .proto-root .jc-distbar > i:not([class*=" v-"]) { display: none; }
.proto-root .jc-distbar > i.v-1 { flex: 1 } .proto-root .jc-distbar > i.v-2 { flex: 2 }
.proto-root .jc-distbar > i.v-3 { flex: 3 } .proto-root .jc-distbar > i.v-4 { flex: 4 }
.proto-root .jc-distbar > i.v-5 { flex: 5 } .proto-root .jc-distbar > i.v-6 { flex: 6 }
.proto-root .jc-distbar > i.v-7 { flex: 7 } .proto-root .jc-distbar > i.v-8 { flex: 8 }
.proto-root .jc-distbar > i.v-9 { flex: 9 } .proto-root .jc-distbar > i.v-10 { flex: 10 }
.proto-root .jc-distbar > i.v-11 { flex: 11 } .proto-root .jc-distbar > i.v-12 { flex: 12 }
.proto-root .jc-distbar > i.v-13 { flex: 13 } .proto-root .jc-distbar > i.v-14 { flex: 14 }
.proto-root .jc-distbar > i.v-15 { flex: 15 } .proto-root .jc-distbar > i.v-16 { flex: 16 }
.proto-root .jc-distbar > i.v-17 { flex: 17 } .proto-root .jc-distbar > i.v-18 { flex: 18 }
.proto-root .jc-distbar > i.v-19 { flex: 19 } .proto-root .jc-distbar > i.v-20 { flex: 20 }
.proto-root .jc-distbar > i.p { background: var(--ok); }
.proto-root .jc-distbar > i.n { background: var(--warn-accent); }
.proto-root .jc-distbar > i.k { background: var(--danger); }
.proto-root .jc-distbar > i.p::before { content: "直接放行 · "; }
.proto-root .jc-distbar > i.n::before { content: "附理由 · "; }
.proto-root .jc-distbar > i.k::before { content: "拦截 · "; }
.proto-root .jc-logo svg, .proto-root .jc-face svg, .proto-root .jc-rp-ava svg { width: 17px; height: 17px; }
/* ── 定价档案卡(工作单独有,照 A3 询价档案卡的骨架)──────────────────────
一颗料一张卡,data-rows 逐行复制:左题面、右推演。
形态按门禁处置分档(data-class-of):拦截档整卡描红,附理由档描橙 —— 判断在样式表。 */
.proto-root .jc-selcards { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; }
.proto-root .jc-selcard { border: 1px solid var(--line); border-radius: 11px; background: #fff; overflow: hidden; }
.proto-root .jc-selcard.v-note { border-color: #f0cf9a; box-shadow: 0 1px 0 rgba(191,132,32,.06); }
.proto-root .jc-selcard.v-block { border-color: #f3b6b3; box-shadow: 0 1px 0 rgba(207,64,58,.08); }
.proto-root .jc-selcard-h {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
padding: 9px 14px; background: var(--soft); border-bottom: 1px solid var(--line-soft);
}
.proto-root .jc-selcard.v-block .jc-selcard-h { background: var(--danger-soft); border-bottom-color: #f3b6b3; }
.proto-root .jc-selcard.v-note .jc-selcard-h { background: var(--warn-soft); border-bottom-color: #f0cf9a; }
.proto-root .jc-selcard-h code { font-size: 13px; font-weight: 800; color: var(--ink); }
.proto-root .jc-selcard-h .why { flex: 1 1 auto; min-width: 0; font-size: 11px; color: var(--ink-3); }
.proto-root .jc-selcard-b { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
@media (max-width: 1000px) { .proto-root .jc-selcard-b { grid-template-columns: 1fr; } }
.proto-root .jc-selcol { padding: 11px 14px; }
.proto-root .jc-selcol + .jc-selcol { border-left: 1px solid var(--line-soft); }
@media (max-width: 1000px) { .proto-root .jc-selcol + .jc-selcol { border-left: 0; border-top: 1px solid var(--line-soft); } }
.proto-root .jc-selcol h5 { margin: 0 0 7px; font-size: 10.5px; font-weight: 800; letter-spacing: .08em; color: var(--ink-3); }
.proto-root .jc-kvrow { display: flex; gap: 8px; padding: 3px 0; font-size: 12px; line-height: 1.55; }
.proto-root .jc-kvrow i { flex: 0 0 84px; font-style: normal; color: var(--ink-4); font-size: 11px; padding-top: 1px; }
.proto-root .jc-kvrow span { flex: 1 1 auto; min-width: 0; color: var(--ink); overflow-wrap: anywhere; }
.proto-root .jc-kvrow span:empty::after { content: "—"; color: var(--ink-4); }
.proto-root .jc-selcard .sc-req:empty::after {
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;
content: "必填"; color: var(--danger); background: var(--danger-soft); border-color: #f3b6b3;
}
.proto-root .jc-selcard .sc-sug:empty::after {
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;
content: "建议填"; color: var(--warn-accent); background: var(--warn-soft); border-color: #f0cf9a;
}
.proto-root .jc-selcard .sc-opt:empty::after {
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;
content: "选填"; color: var(--ink-4); background: var(--soft); border-color: var(--line);
}
/* 两把尺(静态规则表 —— 尺挂在墙上,不是这一单的数据) */
.proto-root .jc-graderule { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; margin-bottom: 12px; }
.proto-root .jc-graderule-r { display: flex; gap: 12px; align-items: flex-start; padding: 9px 13px; font-size: 12px; line-height: 1.6; }
.proto-root .jc-graderule-r + .jc-graderule-r { border-top: 1px solid var(--line-soft); }
.proto-root .jc-graderule-r p { margin: 0; color: var(--ink-2); }
.proto-root .jc-graderule-r p b { color: var(--ink); }
.proto-root .jc-band {
flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; margin-top: 1px;
min-width: 74px; height: 20px; padding: 0 8px; border-radius: 6px;
font-size: 10.5px; font-weight: 800; letter-spacing: .04em; border: 1.5px solid; background: #fff;
}
.proto-root .jc-band.ga { color: var(--ok); border-color: var(--ok); }
.proto-root .jc-band.gb { color: var(--brand); border-color: var(--brand); }
.proto-root .jc-band.gc { color: var(--warn-accent); border-color: var(--warn-accent); }
.proto-root .jc-band.gp { color: var(--ok); border-color: var(--ok); }
.proto-root .jc-band.gn { color: var(--warn-accent); border-color: var(--warn-accent); }
.proto-root .jc-band.gk { color: var(--danger); border-color: var(--danger); }
/* 一个价怎么长出来的:成本 → 底价线 → 拟报价,内联线稿 SVG 走位图 */
.proto-root .jc-priceline { display: flex; align-items: center; gap: 0; margin: 2px 0 14px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 11px; background: var(--soft); }
.proto-root .jc-plnode { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 3px; align-items: flex-start; }
.proto-root .jc-plnode b { font-size: 10.5px; font-weight: 800; letter-spacing: .06em; color: var(--ink-3); }
.proto-root .jc-plnode span { font-size: 12px; color: var(--ink-2); line-height: 1.5; }
.proto-root .jc-plarrow { flex: 0 0 auto; padding: 0 10px; color: var(--brand-soft); }
@media (max-width: 1000px) { .proto-root .jc-priceline { flex-direction: column; align-items: stretch; gap: 8px; } .proto-root .jc-plarrow { display: none; } }
`;
/** 一枚图标 —— 与原型同一写法 */
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>`;
/** 三层壳照原型:少一层样式表全失效 */
const doc = (input: { badge: string; kicker: string; title: string; titleRef: string; state: string; body: string; foot: string }): string =>
`<style>${PRD_CSS}${BIND_CSS}</style>
${PRD_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>
${input.state}
</header>
<div class="jc-rp-bd">${input.body}</div>
<footer class="jc-rp-foot">${input.foot}</footer>
</article>
</div></div></div>`;
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 sign = (note: string, tag = ''): string =>
`<div class="jc-rp-sign">
<span class="jc-rp-ava">${ico('user', 15)}</span>
<div><b data-slot="case:owner"></b><span>${note}</span></div>
</div>${tag === '' ? '' : `<span class="jc-done-tag">${ico('git-branch', 13)}${tag}</span>`}`;
/* ==================== 组成件 ==================== */
/** 要素四卡:客户 / 待定价 / 一格一料的门禁刻度 / 定价的尺 */
const FOUR = `<div class="jc-four">
<div class="jc-fcard">
<span class="jc-fcard-k"><i>${ico('building')}</i>客户公司</span>
<div class="jc-fcard-b">
<span class="jc-logo">${ico('building', 17)}</span>
<div class="jc-fcard-t"><b data-slot="customer_name" data-blank></b><span data-slot="customer_code" data-blank></span></div>
</div>
<span class="jc-fcard-note">承自上游;单飞起单时此栏可空</span>
</div>
<div class="jc-fcard t-teal">
<span class="jc-fcard-k"><i>${ico('boxes')}</i>待定价</span>
<div class="jc-count"><b data-slot="quote_pricing_draft_missing"></b><span>/ <b data-slot="quote_pricing_draft_rows"></b> 条草案</span></div>
<span class="jc-fcard-note">拟报单价还空着的行 —— 这一列由你落笔,系统只给建议价;补一格少一格,基座现数</span>
</div>
<div class="jc-fcard">
<span class="jc-fcard-k"><i>${ico('shield-check')}</i>底价门禁逐条</span>
<div class="jc-segmeter" data-rows="mine:quote_pricing.draft">
<i data-class-of="disposition"></i>
</div>
<span class="jc-fcard-note">一格一条料 · 绿=在底价线之上,橙=低于两成内附理由,红=压在拦截档待拍板</span>
</div>
<div class="jc-fcard t-ok">
<span class="jc-fcard-k"><i>${ico('history')}</i>取价的尺</span>
<span class="jc-tagbig">${ico('check', 13)}<span>成本 × 利润模型 → 底价线</span></span>
<span class="jc-fcard-note">基准取上游底价或库内历史价;毛利档由 P-01 定;偏离底价线两成即止(P-02)</span>
</div>
</div>`;
/** 一个价是怎么长出来的 —— 走位图(形状写死,不算不判) */
const PRICE_LINE = `<div class="jc-priceline">
<div class="jc-plnode"><b>成本基准</b><span>上游底价 / 库内历史价 —— 取不到就空着,不编</span></div>
<div class="jc-plarrow">${ico('arrow-right', 18)}</div>
<div class="jc-plnode"><b>毛利档 P-01</b><span>客户等级 × 类别 × 量档 → 一段区间</span></div>
<div class="jc-plarrow">${ico('arrow-right', 18)}</div>
<div class="jc-plnode"><b>底价线</b><span>成本 ×(1 + 档下限)—— 比它低就要有人负责</span></div>
<div class="jc-plarrow">${ico('arrow-right', 18)}</div>
<div class="jc-plnode"><b>建议价 → 你定价</b><span>系统给建议,拟报单价那一格由你落笔</span></div>
<div class="jc-plarrow">${ico('arrow-right', 18)}</div>
<div class="jc-plnode"><b>门禁 P-02</b><span>偏离底价线:≥0 放行 / 两成内附理由 / 超两成拍板</span></div>
</div>`;
/** 缺价红带 —— 与提交闸、放行门 L1 同一个数同一套算法(基座现数);一格不缺自动收 */
const GAP_BAR = `<div class="jc-gapbar" data-class-of="quote_pricing_draft_missing">${ico('alert', 14)}
<span><b>还差 <span data-slot="quote_pricing_draft_missing"></span> 行拟报单价没定</b>
—— <span data-slot="quote_pricing_draft_missing_keys"></span>。
按右上角「编辑」开表,红框那几格就是要填的:认可系统建议价就照抄那个数,不认可就改成你的;
库内查不到成本的那几行没有建议价,由你按经验定并在备注写明依据。补齐了执行单才提交得了。</span>
</div>`;
const editGate = (address: string, what: string): string => `<div class="jc-editgate" data-edit-for="${address}">
<span class="jc-editgate-t">${ico('lock', 12)}${what}<b>已锁定</b> —— 看的时候不误碰,要改按右边</span>
<button type="button" class="jc-egbtn open" data-edit-open="${address}">${ico('pen', 12)}编辑</button>
<button type="button" class="jc-egbtn ghost" data-edit-cancel="${address}">取消</button>
<button type="button" class="jc-egbtn save" data-edit-save="${address}">${ico('check', 12)}保存</button>
</div>
<div class="jc-editbar">${ico('pen', 14)}
<span>可填的是<b>拟报单价</b>与<b>备注</b>两列:<b>拟报单价这一列系统不替你填</b> ——
认可建议价就照抄那个数,改高改低都可以,改了在备注里写一句缘由;
毛利、偏离度与门禁结论随后按你定的价重算。本轮不报的料按行首 × 撤掉。
改动只落任务草稿层,<b>保存不等于放行</b>。</span>
</div>`;
const addRowBar = (address: string): string => `<div class="jc-addrow">
<button type="button" class="jc-egbtn ghost" data-edit-add="${address}">${ico('plus', 12)}加一行</button>
<em>照末行的样子添一行空的;主键(型号 PN)填了才存得下,不填这一行不落库。</em>
</div>`;
/**
* 草案主表 —— 一行一料:承继列 + 定价五列 + 类徽与门禁徽。
* `data-col` 只挂人可改的两列(quoted_price / price_note)+ 主键 pn ——
* 编辑面精确等于 BD 的职权面;**主键格必挂 `data-col`**(它不是编辑面,是行的钥匙:
* 不挂,服务端按「全表一致列」补键,两行会对到同一行上,后者盖前者 —— A2 走查逮过)。
* 拟报单价挂 `req`:空着出「必填」胶囊、整行标底,与提交闸、放行门同源(都数 `_missing`)。
*/
const DRAFT_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
<thead><tr>
<th style="width:36px;min-width:36px"></th>
<th style="width:44px;min-width:44px">类</th>
<th style="min-width:170px">PN</th>
<th style="min-width:84px">品类</th>
<th style="min-width:70px">年用量</th>
<th style="min-width:78px">成本基准</th>
<th style="min-width:86px">毛利档</th>
<th style="min-width:78px">底价线</th>
<th style="min-width:78px">客户目标价</th>
<th style="min-width:82px">系统建议价</th>
<th class="req" style="min-width:96px">拟报单价(你定)</th>
<th style="min-width:70px">毛利率</th>
<th style="min-width:70px">偏离度</th>
<th style="width:78px;min-width:78px">门禁</th>
<th style="min-width:150px;width:100%">备注 / 让价理由</th>
</tr></thead>
<tbody data-rows="mine:quote_pricing.draft" data-edit="quote_pricing.draft"><tr>
<td class="jc-rn"><button type="button" class="jc-rowdel" data-edit-del="quote_pricing.draft" title="撤掉这一行">×</button></td>
<td class="jc-ty" data-h="类"><span class="jc-tbadge" data-class-of="type_tag" data-slot="type_tag"></span></td>
<td data-h="PN"><span class="jc-vcell mono" data-slot="pn" data-col="pn" data-blank></span></td>
<td class="dimempty" data-h="品类"><span class="jc-vcell" data-slot="category" data-blank></span></td>
<td class="dimempty" data-h="年用量"><span class="jc-vcell mono" data-slot="eau" data-blank></span></td>
<td class="dimempty" data-h="成本基准"><span class="jc-vcell num" data-slot="base_price" data-blank></span></td>
<td class="dimempty" data-h="毛利档"><span class="jc-vcell" data-slot="margin_band" data-blank></span></td>
<td class="dimempty" data-h="底价线"><span class="jc-vcell num" data-slot="floor_price" data-blank></span></td>
<td class="sug" data-h="客户目标价"><span class="jc-vcell num" data-slot="target_price" data-blank></span></td>
<td class="dimempty" data-h="系统建议价"><span class="jc-vcell num" data-slot="suggested_price" data-blank></span></td>
<td class="req" data-h="拟报单价(你定)"><span class="jc-vcell num" data-slot="quoted_price" data-col="quoted_price" data-blank></span></td>
<td class="dimempty" data-h="毛利率"><span class="jc-vcell num" data-slot="margin" data-blank></span></td>
<td class="dimempty" data-h="偏离度"><span class="jc-vcell num" data-slot="deviation" data-blank></span></td>
<td data-h="门禁"><span class="jc-gbadge" data-class-of="disposition"></span></td>
<td class="opt" data-h="备注 / 让价理由"><span class="jc-vcell" data-slot="price_note" data-col="price_note" data-blank></span></td>
</tr></tbody>
</table></div>`;
/** 门禁分布:三段按条数分宽 —— 守恒等式(三档相加 = 行数)的可视形 */
const DIST = `<div class="jc-dist">
<div class="jc-distbar">
<i class="p" data-class-of="quote_pricing_draft_disposition_pass" data-slot="quote_pricing_draft_disposition_pass"></i>
<i class="n" data-class-of="quote_pricing_draft_disposition_note" data-slot="quote_pricing_draft_disposition_note"></i>
<i class="k" data-class-of="quote_pricing_draft_disposition_block" data-slot="quote_pricing_draft_disposition_block"></i>
</div>
<div class="jc-distlg">
<span><em class="a"></em>直接放行 · 在底价线之上 <b data-slot="quote_pricing_draft_disposition_pass"></b></span>
<span><em class="b"></em>附理由放行 · 低于两成内 <b data-slot="quote_pricing_draft_disposition_note"></b></span>
<span><em class="c"></em>拦截待拍板 · 低于两成以上 <b data-slot="quote_pricing_draft_disposition_block"></b></span>
</div>
</div>`;
/* ==================== 局面一 · 定价工作单(算价与 BD 核价期间)==================== */
/**
* 逐颗定价档案 —— 一颗料一张卡(data-rows 逐行复制):左题面、右推演。
*
* **容器用 `<section>` 不用 `<div>`**:渲染器的行展开按第一个同名闭合标签截行内容,
* 行模版里嵌着 `<div>` 时 `<div data-rows>` 会把行截成套娃(A2 逮过,记在陷阱表)。
*/
const PRICE_CARDS = `<section class="jc-selcards" data-rows="mine:quote_pricing.draft">
<article class="jc-selcard" data-class-of="disposition">
<div class="jc-selcard-h">
<span class="jc-tbadge" data-class-of="type_tag" data-slot="type_tag"></span>
<code data-slot="pn"></code>
<span class="why" data-slot="remark" data-blank></span>
<span class="jc-gbadge" data-class-of="disposition"></span>
</div>
<div class="jc-selcard-b">
<div class="jc-selcol">
<h5>题面(客户要什么)</h5>
<div class="jc-kvrow"><i>客户料号</i><span data-slot="customer_pn" data-blank></span></div>
<div class="jc-kvrow"><i>品类</i><span data-slot="category" data-blank></span></div>
<div class="jc-kvrow"><i>制造商</i><span data-slot="brand" data-blank></span></div>
<div class="jc-kvrow"><i>年用量</i><span data-slot="eau" data-blank></span></div>
<div class="jc-kvrow"><i>客户目标价</i><span class="sc-sug num" data-slot="target_price" data-blank></span></div>
<div class="jc-kvrow"><i>交期要求</i><span data-slot="lead_time" data-blank></span></div>
<div class="jc-kvrow"><i>客户约束</i><span data-slot="remark" data-blank></span></div>
</div>
<div class="jc-selcol">
<h5>定价推演(填回下表,这里回显)</h5>
<div class="jc-kvrow"><i>成本基准</i><span class="num" data-slot="base_price" data-blank></span></div>
<div class="jc-kvrow"><i>毛利档</i><span data-slot="margin_band" data-blank></span></div>
<div class="jc-kvrow"><i>底价线</i><span class="num" data-slot="floor_price" data-blank></span></div>
<div class="jc-kvrow"><i>系统建议价</i><span class="num" data-slot="suggested_price" data-blank></span></div>
<div class="jc-kvrow"><i>拟报单价</i><span class="sc-req num" data-slot="quoted_price" data-blank></span></div>
<div class="jc-kvrow"><i>毛利率</i><span class="num" data-slot="margin" data-blank></span></div>
<div class="jc-kvrow"><i>偏离底价线</i><span class="num" data-slot="deviation" data-blank></span></div>
<div class="jc-kvrow"><i>让价理由</i><span class="sc-opt" data-slot="price_note" data-blank></span></div>
</div>
</div>
</article>
</section>`;
/** 利润模型 P-01 的毛利档 —— 静态规则,尺挂在墙上(素材:MVP 示例工作流 §1.2) */
const MARGIN_RULES = `<div class="jc-graderule">
<div class="jc-graderule-r">
<span class="jc-band ga">8%~10%</span>
<p><b>战略客户 · A 类标准件 · 大批量。</b>量换价,毛利让到下限也要守住 —— 这一档的价不是拿来谈的,
是拿来锁长单的。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gb">12%~15%</span>
<p><b>普通客户 · A 类标准件。</b>常规档,也是拿不准时的兜底档 —— 行情透明的料按这一档报不吃亏。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gc">15%~20%</span>
<p><b>B 类波动件。</b>货源与行情都会动,毛利要留出被动让价的余地。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gc">20%~50%</span>
<p><b>C 类替代 / 停产件。</b>一事一议:找料本身就是成本,替代还带验证风险 ——
报低了后面每一次补货都在亏。</p>
</div>
</div>`;
/** 底价门禁 P-02 的三档尺 —— 静态规则(素材:MVP 示例工作流 §1.2 底价自检) */
const GATE_RULES = `<div class="jc-graderule">
<div class="jc-graderule-r">
<span class="jc-band gp">≥ 底价线</span>
<p><b>直接放行。</b>报价在底价线之上,毛利还在档内 —— 不必解释,提审即可。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gn">低于两成内</span>
<p><b>附理由放行。</b>为拿单让到底价线以下两成以内:在备注里写清让价的缘由
(客户长单、带单品、竞对压价)—— 理由随草案一起交审批,不写等于没有依据。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gk">低于两成以上</span>
<p><b>拦截,要人拍板。</b>这一档系统不替任何人放行:要么回调到底价线,要么由负责人
拍板破线并留下理由,要么这一条本轮不报。<b>私自绕开门禁不算处置</b>。</p>
</div>
</div>`;
export const PRICING_WORKSHEET = doc({
badge: 'sheet',
kicker: '统一报价 · A4 成本定价 · 定价作业',
title: '定价工作单',
titleRef: 'rfq_ref',
state:
'<span class="jc-rp-wait warn">待定价 <b data-slot="quote_pricing_draft_missing"></b> / <b data-slot="quote_pricing_draft_rows"></b> 条</span>',
body:
/*
* 指标带摆的是**利润模型 P-01 的三个入参 + 这一单的工作量**,不是所有字段。
* 三个档位键取的是清单里 P-01 声明的决策入参名(customerGrade / materialType /
* volumeTier)—— 决策表照这三个名字取值,模版就照这三个名字摆,不另起炉灶。
* `records` 是起单入参的数组,基座渲染数组时给的是条数,正好是「这一单要定几条价」。
*/
statsBand([
['待定价条目', 'records'],
['客户等级', 'customerGrade'],
['料号类型', 'materialType'],
['数量档', 'volumeTier'],
]) +
section('基准从哪来 —— 四级优先级',
ladder([
['1', '本客户近期成交价', '最贴近这一单的真实议价空间'],
['2', '同型号他客户成交价', '价位可比,但议价空间要留余量'],
['3', '供应商底价表', '成本侧的锚,不含客户侧信息'],
['4', '无基准', '新料号、久未报价的老料号都会落到这里'],
]) +
noteOnly(
'<b>无基准不是异常,它本身就是一种基准。</b>此时偏离率记 0 —— 没有可偏离的对象,' +
'由底价门禁按「无基准」处置。不要拿一个不相干的价去凑一个偏离率出来:' +
'那比没有基准更危险,因为它<b>看起来像有依据</b>。',
),
{ text: '越靠前越贴近实情' },
) +
section('本单待定价条目',
tableOnly('mine:quote_pricing.draft', [
['型号 PN', 'pn', 'mono'],
['基准价', 'last_price'],
['拟报单价', 'quoted_price'],
['毛利率', 'margin'],
]),
{ text: '只列本单写下的行' },
) +
section('案例当下进程', stagesOnly('case:stages'), { text: 'Enabled ≠ Active' }),
part('定价任务', '客户 <b data-slot="customer_name" data-blank></b> · 共 <b data-slot="line_total"></b> 条 · 有基准价 <b data-slot="basis_total"></b> 条') +
`<div class="jc-derive">${ico('info', 14)}
<span>这一屏是一份报价草案的作业台:系统按「成本基准 × 利润模型」给出底价线与
<b>建议价</b>,<b>拟报单价那一列由你落笔</b> —— 人签的就是这个数,系统不代签。
库内查不到成本的料连建议价都没有(不编),那几行按你的经验定并在备注写明依据。
你定完价,毛利、偏离度与底价门禁的结论跟着重算。</span>
</div>` +
FOUR +
part('一个价是怎么长出来的', '成本 → 毛利档 → 底价线 → 拟报价 → 门禁') +
PRICE_LINE +
part('逐颗定价档案', '题面在左 · 推演在右') +
PRICE_CARDS +
part('利润模型 P-01 的毛利档', '客户等级 × 类别 × 量档 —— 尺挂在墙上') +
MARGIN_RULES +
part('底价门禁 P-02', '偏离底价线两成即止') +
GATE_RULES +
part('填回工作表', '定价落在这 —— 补齐了执行单才提交得了') +
GAP_BAR +
editGate('quote_pricing.draft', '报价草案填回表') +
DRAFT_TABLE +
addRowBar('quote_pricing.draft') +
`<div class="jc-drop">${ico('info', 13)}
<span>带红标的<b>拟报单价</b>是必填,而且<b>只由你填</b> —— 缺一行执行单按不下提交,
与放行门是同一个数同一套算法。系统给的是建议价:照抄即认可,改了以你的为准。
查不到成本的料没有建议价(系统不编):按你的经验定一个价并在备注写明依据,
或者按行首 × 把这一条撤出本轮。压在拦截档(红)的条目在你这儿往上调一点,
就不必再惊动第二个人。</span>
</div>`,
foot: sign('BD 核价 · AI 按利润模型与底价门禁算价,你定最后那个数', '统一报价 · A4 成本定价 · 在办'),
});
/** 局面二:S2 自检 —— 人此刻要核的是「凭什么是这个价、门禁怎么判的」 */
export const PRICING_PRICED = viewShell({
title: '定价与底价门禁',
kicker: 'A4 · 成本定价 · S2 自检',
tone: 'brand',
icon: 'gauge',
stamp: { kind: 'live', text: '自检中' },
sign: 'A4 成本定价 · 定价自检(判定表 P-01 / P-02)',
body:
/* 标签一律照台账声明的列名(基准价 / 拟报单价 / 毛利率 / 偏离度 / 门禁处置):
同一个数在指标带上叫一个名、在下面的表里叫另一个名,签字的人就得先花一秒确认它俩是不是一回事 */
statsBand([
['基准价', 'last_price'],
['拟报单价', 'quoted_price'],
['毛利率', 'margin'],
['偏离度', 'deviation'],
]) +
section('逐条定价结论',
tableOnly('mine:quote_pricing.draft', [
['型号 PN', 'pn', 'mono'],
['基准价', 'last_price'],
['拟报单价', 'quoted_price'],
['毛利率', 'margin'],
['偏离度', 'deviation'],
['门禁处置', 'disposition'],
]),
{ text: '判据在左 · 结论在右' },
) +
section('目标毛利怎么定的 —— 利润模型 P-01',
ladder([
['1', '战略客户 · A 类标准件 · 大批量', '目标毛利 8%–10%'],
['2', '普通客户 · A 类标准件', '12%–15%'],
['3', 'B 类波动件', '15%–20%'],
['4', 'C 类替代 / EOL', '≥20%,或一事一议'],
['5', '以上都不命中', '兜底按普通客户标准件 12%–15%'],
]) +
noteOnly(
'自上而下取<b>第一条命中</b>的规则,同输入必得同结论、零方差零 token,命中的规则行随出参留痕。' +
'不要为了让门禁「过」而回头改毛利参数 —— 那是把判据改成结论,决策表也就失去了它唯一的价值。',
),
{ text: '首条命中' },
) +
section('偏离怎么判的 —— 底价门禁 P-02',
ladder([
['1', '拟报价 ≥ 底价', '直接放行'],
['2', '低于底价 ≤ 20%', 'PM 附理由放行'],
['3', '低于底价 超 20%', '拦截 · BOSS 拍板'],
]) +
noteOnly(
'门禁不是「拦下」而是<b>分流</b>:超限的一律调人 —— 要么回调价格,要么写明为什么这样报,' +
'两条路都合法,但都要留下理由。这条留痕将来会成为客户等级与利润模型的迭代素材。',
),
{ text: '±20% 即止' },
) +
section('本单门禁结论',
/*
* 结论章里嵌 `data-slot`:处置与偏离都是**这一单算出来的事实**,
* 写死一句「已通过」就是替判定表下结论。橙档在此的意思是「还没定,等人处置」。
*/
verdict('底价门禁 P-02 处置:<b data-slot="disposition"></b>', 'wait', '偏离度 <b data-slot="deviation"></b>') +
noteOnly(
'禁区两条随本单同行:<b>不得静默绕开 P-02 的结论</b> —— 要破线只能走「人 · 决策」;' +
'<b>底价超一周未更新的,先刷底价再判</b> —— 拿旧底价判新行情等于没判。',
),
),
});
/* ==================== 局面二 · 报价草案清单(拍板 / 放行 / 归档)==================== */
/** 局面三:待放行 —— 验收单上人要签的那份成果报告 */
export const PRICING_RELEASE = viewShell({
title: '报价草案 · 待放行',
kicker: 'A4 · 成本定价 · 成果待签',
tone: 'case',
icon: 'shield-check',
stamp: { kind: 'live', text: '待签字放行' },
sign: 'A4 成本定价 · 报价草案(待放行)',
export const PRICING_ROSTER = doc({
badge: 'file-check',
kicker: '统一报价 · A4 成本定价 · 交付成果',
title: '报价草案',
titleRef: 'rfq_ref',
state:
'<span class="jc-rp-wait">草案 <b data-slot="quote_pricing_draft_rows"></b> 条 · 待拍板 <b data-slot="quote_pricing_draft_disposition_block"></b></span>',
body:
statsBand([
['拟报单价', 'quoted_price'],
['毛利率', 'margin'],
['偏离度', 'deviation'],
['门禁处置', 'disposition'],
]) +
section('本单交付:报价草案',
tableOnly('mine:quote_pricing.draft', [
['型号 PN', 'pn', 'mono'],
['基准价', 'last_price'],
['拟报单价', 'quoted_price'],
['毛利率', 'margin'],
['偏离度', 'deviation'],
['门禁处置', 'disposition'],
]) +
noteOnly('只列<b>本单</b>写下的行 —— 别单放行过的记录不混进来。逐条列出,便于放行前逐条核对。'),
{ text: '本单草稿' },
) +
section('放行前要过的三道判据',
ladder([
['1', '每条都有拟报单价与毛利率', 'L1 机检 · unpriced = 0'],
['2', '偏离底价 ≤ ±20%,超限者已回调或注明理由', 'L2 机检 · 底价门禁 P-02'],
['3', 'BD 放行', 'L5 人签 · 签了才算数'],
]),
{ text: '两机检 · 一人签' },
) +
section('放行之后会发生什么',
verdict('放行即落地:草稿合并入正式版,草案提交 A5 定价审批', 'wait', '审批点卡在 PM 工位') +
noteOnly(
'审批驳回按<b>声明</b>走向,不由临场决定:<b>回 A4 重开新实例</b> —— 新任务 ID、留痕关联,' +
'不复活旧任务。所以这一屏上还改得动的每一格,过了这道门就要花一整个新实例才改得回来。',
),
),
part('定价要素', '成本定价 · 能算的归判定表,超限的归人') +
FOUR +
part('底价门禁分布', '直接放行 + 附理由 + 拦截 = 总条数') +
DIST +
`<div class="jc-derive">${ico('info', 14)}
<span>三段条数由基座逐行现数,不是谁报上来的:红段一条不剩,这份草案才走得到放行门。
压在拦截档的条目只有三条出路 —— 回调到底价线、由负责人拍板破线并留理由、
或者本轮不报。</span>
</div>` +
part('报价草案', '<b data-slot="line_total"></b> 条 · 每条都带来路') +
GAP_BAR +
editGate('quote_pricing.draft', '报价草案') +
DRAFT_TABLE +
`<div class="jc-drop">${ico('info', 13)}
<span>每一行摆的是这个价的<b>来路</b>:成本基准从哪来、毛利档是哪一档、底价线在哪、
偏离多少、门禁怎么判。签字前请重点看两处:<b>门禁列</b>里非绿的条目,
与<b>备注列</b>里的让价理由 —— 没有理由的让价,到了审批那一关同样过不去。</span>
</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_pricing_draft_rows"></b> 条
(每条带拟报单价与门禁处置)交出 —— 放行之前,世界上什么都没有发生。</span></div>
</div>
<div class="jc-act"><i>${ico('check', 11)}</i>
<div><b>转入定价审批</b>&#12288;<span>本活动只有一个出口 —— 报价草案是审批环节的直接输入,
已进对方的任务清单。</span></div>
</div>
</div>`,
foot: sign('BD 定价 · AI 按判定表算价并留痕 —— 要改哪一格,在这张单的草案表上就地改', '统一报价 · A4 成本定价 → 交审批'),
});