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 -54
View File
@@ -1,95 +1,371 @@
/**
* A7 情报归档的案例模型 —— **一步落三张表**(2026-07-31 照 A1 的状态机模式重做)。
* A7 情报归档的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。
*
* 归档这件事没有分支,也没有判断:一单报价有了结果,把它沉淀成下一单用得上的情报 ——
* 历史价、客户画像、落地率统计。上一版把这三样各建成一个计划项,
* 可它们**互不依赖、同一拍就能写完**,分开只换来两次多余的 finish。
* 业务出处:`docs/MVP示例工作流.html` §1.6(S1 结果登记 → S2 画像更新 → S3 落地率统计),
* 按 A4/A6 同一条形状重建(2026-08-03 重写 @3.0.0):
*
* 两项:
* T_archive —— 一步落三张表(历史价 / 客户画像 / 落地率);
* T_release —— 封卷放行(人工激活)。
* 上游(A6 三口之一的载荷 `{rfqRef, customerCode, quoteNo, outcome, rows}`)进来 →
* 归一成**情报卡**底表(一颗料一条:报价 / 成交价 / 结果 / 时间)→ **一张执行单**
* 交 BD:逐条落**输赢归因**与竞对价(归因那一列 AI 一格都不写)→ 按败价幅度 A-01
* 给复用建议 → **入库口径要人拍板**(决策卡:全量 / 脱敏 / 只留内部)→ 放行归档闭环。
*
* 归档的价值全在**下一单**:这一单的拒绝原因,是下一单定价时的判据。
* 所以「不得空归档」是它唯一的硬判据 —— 三张表都写了才算办完。
* 上一版(@2.1.0)六处不合现行口径,这一版整个置换:
* ① 入参必填 `rfqRef + customerCode + quoteNo` —— 而宽进严出是全线口径,归一在活动内做;
* ② 两条判据读活动自报的 `reason_logged_total` / `profile_updated_total` ——
* 「机检只读基座数得出来的数」违反史第七类:那个数模型自己写,写 1 就过;
* ③ 台账一个 `required` 都没有 —— 执行卡的闸没牙,人不填也能交卷;
* ④ **没有人的判断**:归档全由 AI 自己写完自己放行,而输赢归因恰恰是只有跟单的人
* 才说得出的事(客户到底为什么不下单,系统里没有一个字段答得了);
* ⑤ 三张共享域表(`market.price_history` / `customer.profile` / `shared.landing_stats`)
* 同批退役,墓志铭在清单里;
* ⑥ 那段 `run()` 删掉 —— 案例活动不会调它。
*
* 【为什么归档也要叫人】它是全流程最后一站,**这一单之后再没有人会回头看它**。
* 归因写错没有下一道工序纠正,而它会一路流进下一单的判据里 ——
* 「价格高」这种由 AI 从「客户没回」推出来的结论,三个月后没人记得它是编的。
* 所以两处人:归因逐条落在执行单上,入库口径一张决策卡拍板。
*/
export const CMMN_XML = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL">
<case id="archive">
<!--
案卷项的 id **就是任务视图里的键名**。上游载荷(rows)与本活动台账列都摊平到
任务视图顶层,照它们命名;quote_archive_* 那几个是**基座读数**
(行数 / 缺必填 / 按值现数),由基座现算,模型碰不着。
-->
<caseFileModel>
<caseFileItem id="quoteNo" name="报价单号(开案即有)"/>
<caseFileItem id="archived_total" name="已归档条数"/>
<caseFileItem id="rows" name="上游条目(报价函行,流内开案即有)"/>
<caseFileItem id="intel_total" name="情报卡条数(自报)"/>
<caseFileItem id="collected_total" name="已归一落表的条数(归一走完的凭据)"/>
<caseFileItem id="attributed_total" name="已落归因的条数(人环走完的凭据)"/>
<caseFileItem id="scope_decided_total" name="入库口径已拍板"/>
<caseFileItem id="quote_archive_intel_rows" name="情报卡行数(基座现数)"/>
<caseFileItem id="quote_archive_intel_missing" name="缺归因的行数(基座现数)"/>
<caseFileItem id="quote_archive_intel_share_scope_pending" name="入库口径还待定的条数(基座现数)"/>
</caseFileModel>
<casePlanModel id="plan" name="情报归档" autoComplete="true">
<sentry id="s_archived">
<onPart id="op_ar" caseFileItemRef="archived_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_ar"><condition>archived_total &gt; 0</condition></ifPart>
<!--
① 归一完了 —— 交 BD 归因那一步的入场前提。
行数两边对上(一边是基座现数),且归一那一轮走完(collected_total 是归一之后
显式写下的数;「大于等于 0」对「写过的 0」为真、对「没写过」为假)。
-->
<sentry id="s_collected">
<onPart id="op_cl" caseFileItemRef="collected_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_cl"><condition>intel_total &gt; 0 and quote_archive_intel_rows = intel_total and collected_total &gt;= 0</condition></ifPart>
</sentry>
<!--
② **归因齐了才轮到问入库口径** —— 这个活动的要害(与 A6「还价才叫人」同一形状)。
它读的是**人自己填的那一格**(缺归因的行数由基座按 required 现数,
attributed_total 只在人交回执行单之后才产生),不是模型判出来的结论列。
A6 两版都栽在同一个病根上 —— **「该不该叫人」不能由被叫的那一方说了算**。
入库口径这一格也照同一条办:AI 归一时一律写 PENDING(过渡态),
FULL / MASKED / INTERNAL 三个码**只有拍板那一步写得出来**;
待定条数归零,这一步就不会再找人,放行门也才开得了。
-->
<sentry id="s_attributed">
<onPart id="op_at" caseFileItemRef="attributed_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_at"><condition>attributed_total &gt; 0 and quote_archive_intel_rows &gt; 0 and quote_archive_intel_missing = 0 and quote_archive_intel_share_scope_pending &gt; 0</condition></ifPart>
</sentry>
<!--
── 放行门的入场条件:**认「办完了没有」,且读基座数**────────────────
· 每条都有归因(quote_archive_intel_missing 由基座按 required 现数,
与执行卡的提交闸、放行门 L1 同一个数同一套算法);
· **没有还压在待定口径上的条目**(拍板那一下把 PENDING 换成三个结论码之一);
· 口径拍过了(scope_decided_total 是拍板那一步显式写下的记账数);
· 口径四档相加 = 行数(守恒,两边都是基座按值现数)。
-->
<sentry id="s_ready">
<onPart id="op_rd" caseFileItemRef="scope_decided_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_rd"><condition>scope_decided_total &gt; 0 and quote_archive_intel_rows &gt; 0 and quote_archive_intel_missing = 0 and quote_archive_intel_share_scope_pending = 0 and quote_archive_intel_share_scope_full + quote_archive_intel_share_scope_masked + quote_archive_intel_share_scope_internal + quote_archive_intel_share_scope_pending = quote_archive_intel_rows</condition></ifPart>
</sentry>
<!-- 一步落三张表:它们互不依赖,同一拍写完 -->
<humanTask id="T_archive" name="情报归档:落历史价、客户画像、落地率统计">
<itemControl><repetitionRule id="r_ar"/></itemControl>
<!-- ① 归一情报卡:一颗料一条,承继报价与结果,口径一律先落待定 -->
<humanTask id="S1_collect" name="归一情报卡:逐条承继报价与结果,标上报价日期">
<itemControl><repetitionRule id="r_s1"/></itemControl>
</humanTask>
<humanTask id="T_release" name="封卷放行">
<entryCriterion id="ec_rel" sentryRef="s_archived"/>
<!--
② 逐条归因 —— **一张执行单**(输赢的缘由只有跟单的人说得出)。
manualActivation + 不给 doneWhen(发卡触达真人,引擎不替人按)。
-->
<humanTask id="S2_attribute" name="逐条落输赢归因与竞对价:工作单上一条一格">
<entryCriterion id="ec_s2" sentryRef="s_collected"/>
<itemControl><manualActivationRule id="m_s2"/></itemControl>
</humanTask>
<!--
③ 入库口径拍板 —— 情报进不进共享库、要不要脱敏,这一下触达公司对外的边界:
竞对报价与客户身份写进共享库之后,下一单谁都读得到。必须人工激活。
-->
<humanTask id="T_scope" name="入库口径拍板:这一单的情报按哪一档进库">
<entryCriterion id="ec_sc" sentryRef="s_attributed"/>
<itemControl><manualActivationRule id="m_sc"/><repetitionRule id="r_sc"/></itemControl>
</humanTask>
<!-- 归档闭环放行:终局唯放行 —— 触达世界,必须人工激活 -->
<humanTask id="T_release" name="归档闭环验收放行">
<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\` 数组 —— A6 的报价函行(每条带报价、客户表态与定案),
外加 \`outcome\`(won / lost)说明这一单是成了还是丢了;这是最全的一种来路;
· **单飞**:只有一段需求文字,说了哪一单、给谁报的、什么价、结果如何。
**一步就能办完** —— 三张表互不依赖,同一拍写完:
· \`market.price_history\`:这一单报了什么价、什么时候报的、结果如何、拒绝原因是什么;
· \`customer.profile\`:客户画像该更新什么(价格敏感度、决策周期、常用条款);
· \`shared.landing_stats\`:这个月的落地率、A 类命中、三日回复率。
**成品只有一样**:一份**情报卡清单** —— 一颗料一条,每条都有**输赢归因**,
每条都定了**入库口径**。人签的不是「归了个档」,是**这一单留下了什么、下一单谁用得上**。
**不得空归档**:三张表都得有东西。「这一单没什么好记的」不是理由 ——
结果本身就是情报,拒绝的原因尤其是。写完把 archived_total 记上,再 finish。
**这一单的价值全在下一单**:今天记下的败因,是下次定价时的判据;今天记下的成交价,
是下次的基准。所以归档不是收尾的仪式,是情报生产。
**「客户没回」不是原因** —— 问不出来就如实写「说不清」,**不许编一个听着合理的理由**:
编出来的原因会一路流进下一单的判据里,而那时已经没人记得它是编的。
【封卷放行】**先 start T_release**(人工激活),提交前用 judge 裁掉裁量判据
(criterion 照判据名原样写),然后 submit。终局唯放行:你不能自己宣布完成。
**能算的不问人,该人说的不代说**:败价幅度 A-01(价差 → 复用建议)、入库口径建议 A-02
(归因 → 建议档位),两支判定表说了算,你不临场拍;而**输赢归因、竞对价、客户原话
三列是人落的,你一格都不写** —— 客户到底为什么不下单,系统里没有一个字段答得了。
【一路上要说话】每一拍带一句 say,说**业务的话**:这一单最后怎么样、为什么、
下次遇到同类该注意什么。一两句、口语。别报「写草稿 N 行」这种系统词。`;
**三步走**:归一情报卡 → 交 BD 逐条归因 → 入库口径拍板 → 放行闭环。
**这一单要经过人两次**:BD 在情报归档工作单上逐条落归因(那张单绑着整份情报卡,
归因缺一条按不下提交 —— 基座的闸看着,他填的格子直接落账);
**入库口径另叫他拍一次板**(一张决策卡)。竞对报价与客户身份写进共享情报库之后,
下一单谁都读得到 —— 这条边界不该由系统自己越过。最后 BD 放行签字。
**这是全流程最后一站**:放行之后没有下一个节点,只有一口 \`archived\`(归档闭环)。
去向不必你选,也没得选。
【这一单只有一个岔路口,它长这样 —— 无论哪一拍都照这个发,别自己另编一张】
入库口径要人拍板,那张决策卡**三处写死**:
· \`"itemId":"T_scope"\` —— 英文计划项号。**别写中文名「入库口径拍板」**:中文名不是 id,
写了当场被拒(【目标缺口】那一段摆的就是中文名,别照它抄);
· \`"ask":"<N> 条情报待定入库口径,按哪一档进库?"\` —— 只把 N 换成条数,**别的字一个都不改**。
换了问法,系统就认不出这是同一张卡:人已经点过的那张会被你再发一遍;
· 三个 \`value\`:\`full\`(全量入库)、\`masked\`(脱敏入库)、\`internal\`(只留内部)。
label 与 consequence 可以按本单的数改写,**value 不行**。
**FULL / MASKED / INTERNAL 三个码只有拍板那一步写得出来** —— 归一那一步遇到情报一律落
PENDING,替他写了口径,那张卡就永远不会发,这条边界成了摆设。
拍板之前**别急着发卡也别绕路** —— 归一那一步先把情报卡写完、finish;
下一拍引擎会把拍板那一步的做法摊开给你,那时再发。
【一路上要说话】负责人正看着这一单。**每一拍都带一句 say** —— 说业务的话:
这一单最后怎么样、归了几条情报、下次遇到同类该注意什么;不说系统词。
到了要找人的那一拍,先说清这一轮办完了什么、为什么现在需要他。
【怎么把一项干完 · 一拍连做】write 和 tool 都不会自动收步 —— 做完用 finish 宣告。
动作排进同一拍的 actions 串一口气给;只有发卡 / 提交(挂起)必须停拍。
**这一单常路 6-8 拍就该收口**。
§Z 结案:BD 在验收单上签字,系统把草稿合并成正式情报卡并发出 \`archived\` 那一口 ——
这条工作流就此走到终点。归档做得好不好,不在这一单看得出来,
在下一次报价能不能一眼看到「上次为什么丢」看得出来。`;
/**
* 步骤剧本 —— 与 A1 同一套。放行那一项不给 `doneWhen`(触达世界)。
* 步骤剧本 —— 一个计划项一条;`doneWhen` 是「成果已存在」的判据,不是「该不该做」。
*
* 三处不给 `doneWhen`:`S2_attribute`(发卡触达真人)、`T_scope`(要人拍板)、
* `T_release`(签下去触达世界)—— 给了等于给引擎一把替人签字的钥匙。
*/
export const ARCHIVE_STEPS = [
{
item: 'T_archive',
intent: '一步落三张表:历史价、客户画像、落地率统计',
writes: ['market.price_history', 'customer.profile', 'shared.landing_stats', 'quote_archive.verdict'],
reads: [],
item: 'S1_collect',
intent: '整份归一成情报卡:逐条承继报价与结果、标上报价日期、口径一律先落待定',
writes: ['quote_archive.intel', 'quote_archive.verdict'],
/* 上游载荷数组不声明就会被整表段的截断刀砍掉(砍掉的恰是要归一的行) */
reads: ['rows'],
view: 'archive-worksheet',
tools: [],
// 三张都写了才算归档过。不写 `!= null` —— 未绑定的名字那样求值为 true(A1 踩过)
doneWhen: 'archived_total > 0',
guide: `三张表互不依赖,**同一拍写完**:
· \`market.price_history\`:报了什么价、什么时候、结果如何、拒绝原因;
· \`customer.profile\`:客户画像该更新什么(价格敏感度、决策周期、常用条款);
· \`shared.landing_stats\`:本月落地率、A 类命中、三日回复率。
**不得空归档** —— 「没什么好记的」不是理由,结果本身就是情报,拒绝原因尤其是。
**同一拍再写一行 \`quote_archive.verdict\`**(验收机检读的就是它):
· archived_total = 已归档条数;
· reason_logged_total = 已回写拒绝原因/竞对价位的条数(要 > 0);
· profile_updated_total = 已更新客户画像标签的条数(要 > 0);
· summary 一句人话结论。写不对 submit 会被门挡回来。写完再 finish。
**每一行都要带 \`rfq_ref\`** —— 它是这一单的单标识(起单入参里就有,原样抄进去)。
台账主键第一列就是它:不写,这一行会和**别的单**同料号的那一行撞主键,把人家覆盖掉
(实测栽过:A4 的报价草案主键只有 pn,跑四轮只剩最后一轮的 3 行)。`,
/*
* 归一走完才算办完:行数两边对上(一边是基座现数),
* 且 collected_total 写过(S-FEEL 未绑定的名字参与比较为假,所以「没写过」进不来)。
*/
doneWhen: 'intel_total > 0 and quote_archive_intel_rows = intel_total and collected_total >= 0',
guide: `第一件事:把这一单的**情报卡**落出来 —— 一拍连做,别拆成两拍。
【条目从哪儿来】
· 流内转来(\`rows\` 里的行带 pn / quoted_price / reply_type / deal_status):照抄 ——
报过的价、谈成的价都是既成事实,**一个数字都别改**;带 line_total 这类总数的是结论行,
不是料,别落进情报卡。
· 单飞:照正文归一,列了几颗落几行;正文里说的报价与结果就是要沉淀的那一份。
**按 pn 归并,一颗料一行**;pn 一个字符不许改(它是主键,改一个字就是新开一行)。
【写 \`quote_archive.intel\`,一颗料一行】(rows 收数组)
rfq_ref(入参有就原样抄;没有从主题认;认不出才自己造一个,**造出来的必须以 \`ARC-\` 开头**
(\`ARC-<主题或第一颗料的型号>\`)。两条硬规矩:
**① 整单只有一个单号,不是一颗料一个** —— 它是这一单的案号,情报卡所有行、verdict 那一行
用的都得是同一个串(实测栽过:两颗料造出两个号,一单裂成两单);
**② 只从本单来件取材** —— 台账里躺着的 RFQ-xxxx 是别人的单,借它的号等于把两单的行
混进同一个案卷(实测栽过一轮))、pn、customer_code、customer_name、category、brand、eau、
**quoted_price**(我方报过的价,原样抄)、**final_price**(成交了才有;没成交留空)。
另两列**照这个口径落**:
· **deal_result** —— 这一条的结果,只许写 \`WON\`(成交)或 \`LOST\`(不成交)两个大写码之一。
上游给了 outcome 就照它:won → WON,lost / revise → LOST;
逐行还带着 deal_status 的,以行上的为准(WON → WON,其余一律 LOST)。
认不出就按 **LOST** 落 —— 归档里不许把说不清的算成成交。
· **quoted_at** —— 报价日期,写成 \`YYYY-MM-DD\`。来件里有报价日就抄它,没有就写今天。
这一格是下一单查「距上次报价多久」的那把尺,别留空。
【**这三列你一格都不写**:cause_tag / competitor_price / evidence】
输赢归因、竞对报的那个价、客户原话 —— 这三样只有跟单的人知道,**系统不替人归因**。
它们是 BD 在工作单上落的;price_gap_band 那一列要等竞对价回来才算得了,这一步也别写。
这不是客气话:一条要你分辨「这行能不能替人填」的规矩会走岔;
**「这几列不写」没有分支可走岔。**
【**share_scope 这一列一律写 \`PENDING\`**】入库口径是人在决策卡上拍的,
\`FULL\` / \`MASKED\` / \`INTERNAL\` 三个码这一步一个都不许写 —— 替他写了,那张卡就永远不会发。
【同一拍写 \`quote_archive.verdict\`(一行)】
rfq_ref、**quote_no**(报价单号:来件有就抄,没有写 \`Q-\` 开头 + 本单案号的尾段,整单一个)、
customer_code / customer_name(顶层有就抄)、**outcome**(整单结果:有一条 WON 就写 \`won\`,
一条都没有就写 \`lost\`)、**intel_total** = 情报卡行数、**collected_total** = 这一拍归一落表的
条数(0 也要写)、summary 一句人话(几条料、给谁报的、最后成没成)。
写完 finish 这一项。`,
},
{
item: 'S2_attribute',
intent: '一张执行单交 BD:逐条落输赢归因、竞对价与客户原话',
writes: ['quote_archive.intel', 'quote_archive.verdict'],
reads: ['quote_archive.intel'],
view: 'archive-worksheet',
tools: [],
/* 不给 doneWhen —— 发卡触达真人(manualActivation),引擎不替人按 */
guide: `情报卡落好了,**该 BD 出场了** —— 这是本活动唯一的一张执行单。
输赢的缘由只有跟单的人说得出:客户到底为什么不下单、竞对报了多少、他原话怎么说的。
工作区摆的是**情报归档工作单**:逐颗料一张情报档案卡(报价、结果、客户约束),
归因六档与败价幅度的尺挂在上面,填回区长在同一屏 —— 他在「输赢归因」那一列逐行落。
这一步标了人工激活:发卡就算启动,不必单发 start;引擎也不会替你收 ——
人交回后要你自己 finish。
【照这个骨架发,别少字段】(卡面是纯文本,steps/standard 里别写星号记号)
{"kind":"human","itemId":"S2_attribute","workOrder":"execution","card":{
"title":"情报归因:<单号> 共 <N> 条待归因",
"ask":"这一单的情报卡已按报价与结果归好,报价、成交价与日期都在工作单上。请逐条落输赢归因:输在价格 / 交期 / 认证 / 规格 / 关系,还是说不清;输在价格的把竞对报的那个价也填上,并把客户原话记进依据一栏。",
"steps":["归因只有六档,选最主要的那一个:PRICE 价格 · LEADTIME 交期 · CERT 认证资质 · SPEC 规格不匹配 · RELATION 关系或既有供应商 · OTHER 说不清",
"「客户没回」不是原因 —— 问不出来就落 OTHER 并在依据里写明「未获反馈」,不要编一个听着合理的理由:它会一路流进下一单的判据里",
"归因落 PRICE 的行必须填竞对价:没有那个数,「输在价格」下一单读不出任何东西 —— 缺这一格这张单按不下提交",
"依据一栏尽量记客户原话:「你们比 XX 贵两毛」比「价格偏高」值钱得多 —— 下一单定价时看的就是这一句"],
"standard":"整份情报卡每一条都有归因,归因落 PRICE 的那几条填了竞对报的价 —— 缺一条这张单按不下提交(系统的闸看着)。",
"basis":[{"k":"<PN>","v":"<报价 · 成交价 · 结果 · 报价日期>"}]}}
· \`basis\` 逐条列本单的料:PN + 报价与结果。几颗写几条,别铺全表。
· **卡上不摆表、不摆格子**(执行卡零输入):整份情报卡就在工作单那份件上,
归因、竞对价、竞对名、依据四列都能就地改、行能撤 —— **别往卡里塞 \`blocks\` 或 \`fields\`**。
· **这张卡带提交闸**:归因(必填列)还空着,他按不下提交 —— 缺几格、缺在哪行,
闸上与件上的红带都写着(与放行门 L1 同一个数同一套算法)。
发完卡这一拍就结束了(发卡挂起)。
【人交回之后 —— 他落的归因基座已经替你写回台账了】这一步你只做三件事,然后 finish:
① **算 price_gap_band**(只给归因是 PRICE 且填了竞对价的那几行):按败价幅度 A-01,
价差 gap =(quoted_price − competitor_price)/ quoted_price,只许写这三个大写码之一:
· gap ≤ 0.03 → **SLIGHT**(擦肩而过:下一单同价位再试,输的多半不在价格);
· 0.03 < gap ≤ 0.10 → **MODERATE**(有差距但够得着:下一单让到这一档或换替代料降本);
· gap > 0.10 → **STRUCTURAL**(结构性价差:下一单别再报同一价位,先走替代选型)。
不是 PRICE 的行、或没有竞对价的行,这一格给空串 \`""\`,**不许写 0 也不许编一个码**。
② 写 verdict:**attributed_total** = 已落归因的条数(照情报卡行数记);
他在件上撤掉了行的话,**intel_total 跟着改成撤后的实数**。
③ finish 这一项。
**别在这一拍写入库口径** —— 那是下一步人拍板的事(它有自己的一段做法),这里写了也白写。
情报卡其余列你一列都不用重写:他落的值基座已经落进台账了,重写会被「写了等于没写」拒回。
【这张卡一单只发一次】人交回来之后不许再发同一张卡 —— 必填有提交闸兜着,
卡回来时归因一定是齐的(或那几行已被他撤掉)。`,
},
{
item: 'T_scope',
intent: '入库口径要人定 —— 发一张决策卡,照他选的那一档把待定口径改掉',
writes: ['quote_archive.intel', 'quote_archive.verdict'],
reads: ['quote_archive.intel'],
view: 'archive-roster',
tools: [],
/* 人工激活 + 要人拍板 —— 不给 doneWhen,引擎不许替人收 */
guide: `**这一单只发一张决策卡。动手之前先在轨迹里找一行 \`[card-reply]\` 带「人选了:」:**
· **找到了** —— 跳到下面【人点完之后】那一段,照他选的那一档把那几行的 share_scope
从 PENDING 改掉、写 scope_decided_total、finish。**一个 decide 都不许再发**
(实测栽过:人点完了,而这一步又从头念了一遍「发一张决策卡」,同一张卡连发六次,
前两次无事发生、后四次被拒 —— 他点过的那一下就白点了);
· **没找到** —— 才轮到下面【发一张决策卡】那一段。
【这一步办完了没有,看一个数】待定口径条数 quote_archive_intel_share_scope_pending:
**它归零就是这一步办完了**。PENDING 是过渡态(情报归好了、还没定进哪一档库),
拍板那一下必须把它换成 FULL / MASKED / INTERNAL 之一 —— 三档都是结论。
这个数不归零,放行门开不了;它归零,这一步也就不会再找你。
这一步只在**归因齐了**(基座现数 quote_archive_intel_missing = 0 且人交回过执行单)时入场 ——
读的是人自己填的那一格,不是你判的结论:该不该叫人,不由被叫的那一方说了算。
它触达公司对外的边界(竞对报价与客户身份进了共享库,下一单谁都读得到),标了人工激活:
发卡就算启动,不必单发 start。
工作区这时摆的是**情报归档清单**,整份情报卡与归因分布都在上面,他看得见全局再拍板。
【发一张决策卡,照这个骨架】(选项少于两个基座拒发)
**三处一个字都不许改**(与说明书里那一段是同一份):
· \`itemId\` 只写英文计划项号 \`T_scope\` —— **别写中文名「入库口径拍板」**,
中文名不是 id,写了当场被拒(实测栽过两拍:「计划项不存在:异议拍板」);
【目标缺口】那一段摆的是中文名,它不是 id,别照它抄。
· \`ask\` 照抄「<N> 条情报待定入库口径,按哪一档进库?」,**只把 N 换成条数** ——
换了问法系统就认不出这是同一张卡,人已经点过的那张会被你再发一遍(实测发出去两张)。
· 三个 \`value\` 照抄 \`full\` / \`masked\` / \`internal\` —— 它们是三档的代号,
换了名字下一段的对应关系就对不上,你自己也不知道人点的是哪一档。
label 与 consequence 可以按本单的数改写,value 不行。
{"kind":"decide","itemId":"T_scope","ask":"<N> 条情报待定入库口径,按哪一档进库?",
"options":[
{"value":"full","label":"全量入库(客户与竞对都留名)","consequence":"下一单查这颗料时,客户是谁、竞对是谁、报了多少一目了然 —— 情报最值钱,但客户身份与第三方报价也就此对内公开"},
{"value":"masked","label":"脱敏入库(只留价位与败因)","consequence":"竞对与客户的名字抹掉,价位、归因、日期照留 —— 下一单照样判得了价,追不到是谁说的"},
{"value":"internal","label":"只留内部(不进共享情报库)","consequence":"这一单的情报只在本案卷里躺着,下一单查不到 —— 涉密或客户明说不外传时才选这一档"}],
"basis":[{"k":"<PN>","v":"归因 <码> · 报价 <X> · 竞对价 <Y> · 败价幅度 <码>"}]}
· basis 逐条摆本单的料:归因、报价、竞对价、败价幅度 —— 裸卡基座拒发。
· **recommended 按入库口径建议 A-02 标**,只标一项:归因里有 PRICE 的(带着第三方报价)
标 \`masked\`;一条 PRICE 都没有的标 \`full\`。
· 发完这一拍就结束了(发卡挂起)。
【人点完之后】轨迹上那一行长这样:\`[card-reply] …人选了:xxx\`。**照那一档把口径改掉,
别再发第二张卡**,一拍连做 —— **整份情报卡逐行都要动 share_scope**,一条都不许留在 PENDING:
· 选 full:每行 share_scope 改 **FULL**;verdict 的 share_policy 写 \`full\`;
· 选 masked:每行 share_scope 改 **MASKED**;verdict 的 share_policy 写 \`masked\`;
· 选 internal:每行 share_scope 改 **INTERNAL**;verdict 的 share_policy 写 \`internal\`。
然后把 verdict 写全:**scope_decided_total 记 1**、**buyer_profile** 一句客户行为画像
(照这一单看得出来的写:价格敏感 / 决策快慢 / 认证门槛 / 常用条款,是 MVP §1.6 S2 那一步的产物)、
summary 补一句拍板结果,finish 这一项。
改完之后待定口径条数就是 0 了,这一步再也不会找你。`,
},
{
item: 'T_release',
intent: '情报已沉淀,封卷放行',
intent: '情报已沉淀、口径已定,提交放行交 BD 验收',
writes: [],
reads: [],
view: 'archive-roster',
tools: [],
guide: `**先 start T_release**(人工激活),提交前用 judge 裁掉裁量判据,然后 submit。
/* 放行触达世界,必须人按一下 —— 引擎不自动收它(剧本里也不给 doneWhen) */
guide: `每条都有归因、口径全定过、待定清零 —— 可以走放行了
(T_release 人工激活:动它就算启动,不必单发 start)。先把裁量判据裁掉,然后 submit,同一拍连做:
· judge criterion="情报站得住:每条归因说得出来路、竞对价与客户原话可溯,BD 对这份情报负责" ——
看情报卡里每一行的归因与依据:说得出来路就 satisfied=true;
有行说不出的(比如归因落了 PRICE 却没有客户原话),satisfied=**false**,rationale 逐行写清,
交签字的人定。**裁成不满足照样能提交** —— 理由连着卡一起交给他。
criterion 照判据名**整句原样抄**,写别的名字对不上。
**去向没得选**:这是全流程最后一站,只有一口 \`archived\`(归档闭环),放行那一刻自动发射。
你要做的只是把情报落准,别在提交里手选出口。
终局唯放行:你不能自己宣布完成。`,
},
];
/**
* 阶段 —— 进度条的刻度,只给人看。除放行外每个计划项恰属一个阶段;
* 放行那一格由基座统一加,人看到的是 x/4。
*/
export const ARCHIVE_PHASES = [
{ name: '归一情报卡', items: ['S1_collect'] },
{ name: '逐条归因', items: ['S2_attribute'] },
{ name: '入库口径拍板', items: ['T_scope'] },
];
+14 -32
View File
@@ -1,36 +1,18 @@
/**
* 统一报价 A7 · 情报归档 —— 活动包代码入口(ADR-004)。
* 「不成交也留资产」:把归档结果摆给人看,确认无空档再提交放行。
* 统一报价 A7 · 情报归档 —— 活动包代码入口(ADR-004:默认导出 defineActivity 的返回值)。
*
* **这个活动没有 `run`,不是缺件。**
*
* 案例活动的推进由 Loop 驱动,行为全部写在清单里:案例模型 + 说明书 + 步骤剧本 +
* 两把判定表 + 技能 + 台账声明 + 两张 HTML 报告模版。
* 上一版这里有一段 `run()`(读 `shared.history_price`、推一个 kv 块、按行数返回
* await/submit)—— 案例活动根本不会调它;更坏的是它读的那张表**从来没被声明过**
* (声明的是 `market.price_history`),而它推的那套呈现与 `viewTemplates` 里的模版
* 各说各话,没人看得出以哪份为准。
*
* 这个文件之所以还存在,是因为清单的 `entry` 指着它 —— 它是**被静态审扫描的产物文本**
* (查 import 与全局引用,证明活动包零环境能力)。
*/
import { defineActivity } from '@sdk';
import type { ActivityBase, ActivityOutcome } from '@sdk';
export default defineActivity({
name: 'quote-archive',
async run(base: ActivityBase): Promise<ActivityOutcome> {
const history = await base.ledger.read('shared.history_price');
const stats = await base.ledger.read('shared.landing_stats');
await base.interaction.pushView([
{
t: 'kv',
title: '归档闭环',
items: [
{ k: '情报回写', v: `${history.length} 条` },
{ k: '落地率', v: stats.length > 0 ? String(stats[0]?.['landing_rate'] ?? '—') : '—' },
{ k: '客户', v: String(base.contract.data['customerCode'] ?? '') },
],
},
]);
// 不得空归档 —— 每一单不成交的报价也是情报资产
if (history.length === 0) {
await base.interaction.say('情报尚未回写,继续登记结果。', 'conclusion');
return { kind: 'await', reason: '等待结果登记步完成' };
}
await base.interaction.say('拒绝原因与竞对价位已回写,画像与落地率已落账。', 'criterion');
return { kind: 'submit', deliverable: 'history-archive', rationale: '情报、画像、落地率三样齐备' };
},
});
export default defineActivity({ name: 'quote-archive' });
+475 -85
View File
@@ -1,139 +1,392 @@
/**
* 活动包清单 —— 统一报价 A7 · 情报归档。
* 活动包清单 —— 统一报价 A7 · 情报归档(案例活动)。
*
* 业务素材:docs/MVP示例工作流.html §1.7(S1 结果登记 → S2 画像更新 → S3 落地率统计)。
* 「不成交也留资产:情报、客户画像、落地率」—— 写的是**共享域**,跨活动的沉淀事实。
* 业务素材:`docs/MVP示例工作流.html` §1.6(S1 结果登记 → S2 画像更新 → S3 落地率统计):
* 「不成交也留资产:情报、客户画像、落地率」
* 「拒绝或无回复的单子 7 天内闭环归档。每一单不成交的报价也是情报资产 ——
* 沉淀下来,下一单更快更准」。
*
* 形状与 A4/A6 同一条(2026-08-03 重写 @3.0.0):归一情报卡 → 一张执行单交人
* (逐条落输赢归因与竞对价)→ 按败价幅度给复用建议 → **入库口径要人拍板**
* (决策卡)→ 放行归档闭环。
* **能算的不问人**(A-01 败价幅度、A-02 入库口径建议两把尺写死),
* **该人说的不代说**(归因、竞对价、客户原话那三列 AI 一格都不写)。
*
* 与 @2.1.0 的差别(六处都是踩过的账,逐条见 case-model 头注):
* ① `input.required` 从 `['rfqRef','customerCode','quoteNo']` 清空 —— 宽进严出;
* ② 两条 L1/L2 判据读 `reason_logged_total` / `profile_updated_total` ——
* **活动自报的数**,写个 1 就过(「机检只读基座数」违反史第七类);
* ③ 台账补 `required` / `requiredWhen` —— 从前一个必填都没有,执行卡的闸没牙;
* ④ 补两处人的判断(归因 + 入库口径)—— 从前 AI 自己写完自己放行;
* ⑤ 三张共享域表退役(墓志铭见 `declarations.ledger` 头注);
* ⑥ `index.ts` 里那段 `run()` 删掉 —— 案例活动不会调它。
*/
import type { ActivityManifest } from '@contracts';
import { ARCHIVE_SKILLS } from './skills.ts';
import { CMMN_XML, NARRATIVE, ARCHIVE_STEPS } from './case-model.ts';
import type { ActivityManifest, DmnModel } from '@contracts';
import { activityIdSchema } from '@contracts';
import { ARCHIVE_LOGGED, ARCHIVE_RELEASE } from './views/report.ts';
import { ARCHIVE_SKILLS } from './skills.ts';
import { ARCHIVE_PHASES, ARCHIVE_STEPS, CMMN_XML, NARRATIVE } from './case-model.ts';
import { ARCHIVE_ROSTER, ARCHIVE_WORKSHEET } from './views/report.ts';
const D = 'quote_archive';
const ACTIVITY_ID = activityIdSchema.parse('quote-archive');
/** 本活动自己的数据域 —— 三张沉淀表落 `shared`,只有归档结论落这儿 */
const DOMAIN = 'quote_archive';
/**
* 两把尺 —— **规则本身不是数据**,随包冻结。
*
* · 败价幅度 A-01:我方报价与竞对价的价差 → 这条情报下一单该怎么用
* (出参就是台账 `price_gap_band` 的取值);
* · 入库口径建议 A-02:输赢归因 → 决策卡上标 recommended 的那一档
* (出参是三个 `value` 之一,不落台账 —— 建议不是结论,结论由人拍)。
*
* 出参键名与台账列同名,模型算完照抄即可(规范 §5.6)。
* A-01 的档位在三处出现:这张表、剧本 guide、工作单上的规则块 ——
* 包内自测把三处钉在一起(改一处不改另两处,测就红)。
*/
export const A01_DMN: DmnModel = {
drd: [
{ decision: 'a01-band', requires: [], inputs: ['gap'] },
{ decision: 'a02-scope', requires: [], inputs: ['cause_tag'] },
],
tables: [
{
/** 败价幅度 A-01 —— FIRST 命中即定,所以第二行等价于「三到十个点」 */
id: 'a01-band',
label: '败价幅度 A-01',
hitPolicy: 'FIRST',
inputs: [{ name: 'gap', expression: 'gap' }],
outputs: [{ name: 'price_gap_band' }],
rules: [
{ id: 'a01-1', when: ['<= 0.03'], then: ['SLIGHT'], annotation: '三个点以内 —— 擦肩而过,下一单同价位再试,输的多半不在价格' },
{ id: 'a01-2', when: ['<= 0.10'], then: ['MODERATE'], annotation: '三到十个点 —— 够得着,下一单让到这一档或换替代料降本' },
{ id: 'a01-3', when: ['> 0.10'], then: ['STRUCTURAL'], annotation: '超过一成 —— 结构性价差,下一单别再报同一价位,先走替代选型' },
],
},
{
/**
* 入库口径建议 A-02 —— 只出**建议**,决策卡上那一枚 recommended 就是它。
* 带着第三方报价的情报默认建议脱敏:竞对报价写进共享库,下一单谁都读得到。
*/
id: 'a02-scope',
label: '入库口径建议 A-02',
hitPolicy: 'FIRST',
inputs: [{ name: 'cause_tag', expression: 'cause_tag' }],
outputs: [{ name: 'scope_hint' }],
rules: [
{ id: 'a02-1', when: ['"PRICE"'], then: ['masked'], annotation: '归因带竞对报价 —— 建议脱敏入库:价位与败因照留,谁说的抹掉' },
{ id: 'a02-default', when: ['-'], then: ['full'], annotation: '不涉第三方报价的败因 —— 建议全量入库,下一单查得越全越好' },
],
},
],
};
export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'A7 · 情报归档',
packageVersion: '2.1.0',
packageVersion: '3.0.0',
category: '统一报价',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-quote-archive',
},
/* ── 契约七件 ────────────────────────────────────────────────── */
contract: {
activityId: ACTIVITY_ID,
version: '0.1.0',
version: '1.0.0',
input: {
/*
* **宽进到底:一个必填都不设**(与 A1/A2/A3/A4/A6 同口径 —— 宽进严出,
* 输入契约在活动内部归一 + 补全)。
*
* 来路两种形状:流内转来(A6 三口的载荷 `{rfqRef, customerCode, quoteNo,
* outcome, rows}` —— `lost` 走 E13 直接进来,`won` 那一口的单子结案时也归这一份档)、
* 单飞(一段文字)。上一版必填 `rfqRef + customerCode + quoteNo`,
* 门口拦的恰是单飞那条路,而归档最常见的来路就是有人补一单历史。
*/
data: {
type: 'object',
required: ['rfqRef', 'customerCode', 'quoteNo'],
required: [],
properties: {
/** 归档写 (pn, quoted_at) 这一行,两个主键都由调用方给定 */
rfqRef: { type: 'string', title: '询价单号(单标识,贯穿全流程)' },
pn: { type: 'string' },
quotedAt: { type: 'string' },
customerCode: { type: 'string' },
quoteNo: { type: 'string' },
reason: { type: 'string' },
competitorPrice: { type: 'number' },
customerCode: { type: 'string', title: '客户码' },
quoteNo: { type: 'string', title: '报价单号' },
outcome: { type: 'string', title: '上游议价结果(won / lost / revise)' },
reason: { type: 'string', title: '不成交缘由(上游给的一句话,归因时的第一手材料)' },
rows: { type: 'array', title: '上游条目(报价函行,带报价、客户表态与定案)' },
items: { 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: {
/*
* **一口,且是必交付口**:A7 是流程包的最后一站(E14 `A7 → end_arch` 是无条件边),
* 没有去向要选 —— 归档闭环就是把情报落准、口径定死。
* 载荷全部从**已落地的事实**推导(verdict 那一行),不由模型另述一遍。
*/
mandatory: [
{
name: 'archived',
payload: { type: 'object', properties: { landingRate: { type: 'number' } } },
description: '归档闭环:情报回写 + 落地率落账',
payload: {
type: 'object',
required: ['rfqRef', 'intelTotal'],
properties: {
rfqRef: { type: 'string', title: '询价单号(单标识)' },
customerCode: { type: 'string', title: '客户码' },
quoteNo: { type: 'string', title: '报价单号' },
outcome: { type: 'string', title: '这一单最后成没成(won / lost)' },
intelTotal: { type: 'integer', title: '归档情报条数' },
sharePolicy: { type: 'string', title: '入库口径(full / masked / internal —— 人拍的)' },
rows: { type: 'array', title: '情报卡(逐条归因与败价幅度)' },
},
},
description: '归档闭环:情报卡逐条落定、入库口径已拍板 —— 这条工作流走到终点',
},
],
selective: [],
},
/*
* 必交付物**必须给 `ledger` 地址**(2026-07-31 修)。
* 不给的话预检退回 `scope[key]` 去找 —— key 是 `benchmark-table` 这样的短横线别名,
* 任务视图里永远没有这个键,于是「必交付物缺一不得提交放行」永远拒,
* 活办完了也放不了行,连拒五拍熔断(实测 A2 死于此)。
* 必交付物只有一件:**情报卡清单**。归档结论(verdict)是它的结论栏,
* `ledgers` 说清这份成果由哪几张表承载(一份成果,不是两份)。
*/
requiredDeliverables: [
{ key: 'history-archive', label: '历史价格情报(归档)', ledger: 'market.price_history' },
{ key: 'landing-stats', label: '报价落地率统计', ledger: 'shared.landing_stats' },
{
key: 'intel-archive',
label: '情报卡清单(逐条输赢归因 + 入库口径)',
ledger: `${D}.intel`,
/* 验收页上让 BD 就地改的两列 —— 恰是他的职权面(归因与依据) */
editableAtRelease: ['cause_tag', 'evidence'],
ledgers: [`${D}.intel`, `${D}.verdict`],
},
],
/*
* 机检谓词读的是 `verdict` 那张**汇总表**的列(照 A1 的口径)。
* 上一版的名字(reasonLogged / profileUpdated)**台账里根本没有** —— S-FEEL 拿未绑定的名字
* 比大小求值为假,判据永远达不了标,submit 门永远拒,连拒五拍熔断。
* 判据的名字必须落在台账上,否则「机检」是一句空话(2026-07-31 销账)。
* 验收判据 —— **机检(L1/L2)只读基座数得出来的数**:
* `_rows` 基座数行数、`_missing` 基座按 required / requiredWhen 现数
* (与执行卡提交闸同一个数同一套算法)、`_<列>_<值>` 基座按值现数。
* 上一版两条机检读活动自报的 `reason_logged_total` / `profile_updated_total` ——
* 那不是门,是「改哪个数能过」的提示(写个 1 就过去了)。
*/
acceptanceCriteria: [
{ key: 'reason-logged', label: '拒绝原因 / 竞对价位已回写', level: 'L1', predicate: 'reason_logged_total > 0' },
{ key: 'profile-updated', label: '客户画像标签已更新', level: 'L2', predicate: 'profile_updated_total > 0' },
{ key: 'bd-release', label: 'BD 放行', level: 'L5' },
{
key: 'intel-complete',
label: '情报卡齐:上游条目逐条承继,说了几条情报卡里就躺着几行',
level: 'L1',
predicate: `intel_total > 0 and ${D}_intel_rows = intel_total`,
},
{
key: 'attributed-all',
label: '每条都有输赢归因:归因落「输在价格」的那几条还落下了竞对报的那个价,缺一行放不了',
level: 'L1',
predicate: `${D}_intel_rows > 0 and ${D}_intel_missing = 0`,
},
{
key: 'result-typed',
label: '结果逐条落成两种之一:成交 / 不成交 —— 没有含糊其辞的格子',
level: 'L2',
predicate: `${D}_intel_deal_result_won + ${D}_intel_deal_result_lost = ${D}_intel_rows`,
},
{
key: 'scope-settled',
label: '入库口径逐条落定:全量 / 脱敏 / 只留内部 / 待定,四档相加等于情报条数',
level: 'L2',
modelRef: 'a02-scope',
predicate: `${D}_intel_rows > 0 and ${D}_intel_share_scope_full + ${D}_intel_share_scope_masked + ${D}_intel_share_scope_internal + ${D}_intel_share_scope_pending = ${D}_intel_rows`,
},
{
/* 与 A6 的「待拍板清零」同一形状:待定是过渡态,人拍过板它才归零 */
key: 'pending-cleared',
label: '入库口径都拍过板了:没有还压在「待定」上的情报条目',
level: 'L2',
predicate: `${D}_intel_share_scope_pending = 0`,
},
{
/*
* 有败价幅度的行**必是归因落了「输在价格」的行** —— 两边都是基座按值现数。
* 不是价格败的却算出一个价差档,是模型给自己找了一条不必问竞对价的近路。
*/
key: 'band-from-price',
label: '算了败价幅度的,都是归因落「输在价格」的那几条 —— 没有凭空算出来的价差档',
level: 'L2',
modelRef: 'a01-band',
predicate: `${D}_intel_price_gap_band_slight + ${D}_intel_price_gap_band_moderate + ${D}_intel_price_gap_band_structural <= ${D}_intel_cause_tag_price`,
},
/*
* 情报站不站得住是**裁量**(L3):归因说不说得出来路、客户原话记没记全,
* 机器数不出来。这句 label 与剧本里 judge 的 criterion **必须逐字一致**。
*/
{
key: 'intel-defensible',
label: '情报站得住:每条归因说得出来路、竞对价与客户原话可溯,BD 对这份情报负责',
level: 'L3',
},
/* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */
{ key: 'bd-release', label: 'BD 放行:签下去这一单的情报即入库,下一单查得到', level: 'L5' },
],
revisableAtRelease: [],
prohibitions: [{ key: 'no-blank-archive', statement: '不得空归档 —— 每一单不成交的报价也是情报资产' }],
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 2 },
prohibitions: [
{
key: 'no-blank-archive',
statement: '不得空归档 —— 每一单不成交的报价也是情报资产,「没什么好记的」不是理由',
},
{
key: 'no-invented-reason',
statement: '不许编败因:问不出来就落 OTHER 并写明未获反馈 —— 编出来的原因会流进下一单的判据',
},
{
key: 'no-self-scope',
statement: '入库口径不许系统自己定:竞对报价与客户身份进共享库是对外边界,只由人拍板',
},
],
authorization: {
discretion: [
{ key: 'band-advice', statement: '败价幅度 A-01 给出的复用建议由活动自行判定;进不进共享库一律交人' },
],
preauthorizedActions: [],
/* 一张执行单 + 一张决策卡 + 放行 = 三次;留一倍余量 */
humanCallBudget: 5,
},
resources: {
/*
* **不调工具**。归档这一段没有要问外面的事 —— 上游把结果交过来,
* 人把缘由补上,系统把它落成下一单查得到的形状。
*/
tools: [],
skills: ARCHIVE_SKILLS.map((s) => s.id),
models: ['deepseek-v4-flash'],
roles: ['BD'],
budget: { steps: 30, tokens: 60_000, activeMinutes: 60, humanCalls: 2 },
/* SLA:MVP §1.6「7 天闭环」;预算按 A6 实测(6-9 拍)留一倍余量 */
budget: { steps: 60, tokens: 400_000, activeMinutes: 120, humanCalls: 5 },
},
},
/* ── 声明 ────────────────────────────────────────────────────── */
declarations: {
models: ['deepseek-v4-flash'],
tools: [],
/*
* 【三张共享域表的墓志铭】上一版声明了 `market.price_history` /
* `customer.profile` / `shared.landing_stats`,这一版三张一并退役:
*
* · `shared.landing_stats`(月度落地率 / TypeA 命中 / D+3 回复率)——
* **单案例算不出一个月的统计**。上一版让 AI 写一个 0.27 进去,那是界面撒谎:
* 它既不是本单的事实,也没有第二个数据源可核。月度统计的正经落点是驾驶舱
* **按表现算**,这一单该做的只是把自己那一条如实落准。
* · `customer.profile`(客户画像)—— **A1 已经持有这张主数据表**,
* A7 再声明一遍就是同一批业务对象的两套地址(六个占位槽退役时清过一次的账)。
* 本单看得出来的客户行为写进 `verdict.buyer_profile` 一句话,
* 要更新主数据由人在客户档上改,不由归档活动代笔。
* · `market.price_history`(历史价格情报)—— 与下面这张 `intel` **是同一份事实**。
* 两处各存一份,改一处就分叉而没人看得出以哪份为准(A1 开请求单那次的账)。
* 情报卡本身就是「下次查得到的形状」:按 pn 查得到价、按 customer_code 查得到客户、
* 按 quoted_at 查得到「距上次报价多久」(P-03 的那把尺读的就是它)。
*/
ledger: [
{
// 共享域:跨活动的沉淀事实(AM 11.3 暗耦合禁令的两条合法路径之一)
domain: 'market',
object: 'price_history',
kind: 'master',
label: '历史价格情报',
domain: D,
object: 'intel',
kind: 'record',
caseKey: 'rfq_ref',
label: '情报卡',
fields: [
{ name: 'pn', type: 'text', primaryKey: true, masked: false },
{ name: 'quoted_at', type: 'date', primaryKey: true, masked: false },
{ name: 'lost_reason', type: 'text', primaryKey: false, masked: false, label: '拒绝原因' },
{ name: 'competitor_price', type: 'number', primaryKey: false, masked: false, label: '竞对价位' },
],
},
{
domain: 'customer',
object: 'profile',
kind: 'master',
label: '客户资料库',
fields: [
{ name: 'customer_code', type: 'text', primaryKey: true, masked: false, label: '客户码' },
{ name: 'grade', type: 'text', primaryKey: false, masked: false, label: '客户等级' },
{ name: 'profile_tags', type: 'text', primaryKey: false, masked: false, label: '画像标签' },
],
},
{
domain: 'shared',
object: 'landing_stats',
kind: 'derived',
label: '报价落地率统计',
fields: [
{ name: 'period', type: 'text', primaryKey: true, masked: false, label: '月份' },
{ name: 'landing_rate', type: 'number', primaryKey: false, masked: false, label: '落地率' },
{ name: 'type_a_hit', type: 'number', primaryKey: false, masked: false, label: 'TypeA命中' },
{ name: 'reply_rate_d3', type: 'number', primaryKey: false, masked: false, label: 'D+3回复率' },
{ name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
{ name: 'pn', type: 'text', primaryKey: true, masked: false, label: 'PN' },
{ name: 'customer_code', type: 'text', primaryKey: false, masked: false, label: '客户码' },
{ name: 'customer_name', 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: 'quoted_at', type: 'date', primaryKey: false, masked: false, label: '报价日期' },
{ name: 'quoted_price', type: 'number', primaryKey: false, masked: false, label: '我方报价 CNY' },
{ name: 'final_price', type: 'number', primaryKey: false, masked: false, label: '成交价(成交才有)' },
{
name: 'deal_result',
type: 'text',
primaryKey: false,
masked: false,
label: '结果',
tally: ['WON', 'LOST'],
},
/*
* ── 人那一侧的三列:**AI 一格都不写** ──────────────────────────
*
* 与 A6 的「客户表态三列」同一处结构治:一条要模型分辨「这行能不能替人填」
* 的规矩会走岔;「这几列不写」没有分支可走岔。
*
* `cause_tag` 标 `required` —— 件上的缺项红带、执行卡的提交闸、放行门 L1
* 三处同源(都数基座的 `_missing`)。
* `competitor_price` 用 `requiredWhen`:**只有归因落「输在价格」的那一行才算必填**
* (表级 required 会让交期/认证败的行永远缺一格,把闸拦成死锁)。
*/
{
name: 'cause_tag',
type: 'text',
primaryKey: false,
masked: false,
required: true,
label: '输赢归因',
tally: ['PRICE', 'LEADTIME', 'CERT', 'SPEC', 'RELATION', 'OTHER'],
},
{
name: 'competitor_price',
type: 'number',
primaryKey: false,
masked: false,
requiredWhen: { column: 'cause_tag', value: 'PRICE' },
label: '竞对价',
},
{ name: 'competitor_name', type: 'text', primaryKey: false, masked: false, label: '竞对是谁' },
{ name: 'evidence', type: 'text', primaryKey: false, masked: false, label: '依据 / 客户原话' },
/* ── 算出来的那一格:尺说了算,模型照抄 ─────────────────────────── */
{
name: 'price_gap_band',
type: 'text',
primaryKey: false,
masked: false,
label: '败价幅度(A-01)',
tally: ['SLIGHT', 'MODERATE', 'STRUCTURAL'],
},
/*
* 入库口径,**PENDING 是过渡态**(情报归好了、还没定进哪一档库):
* 拍板那一下把它换成 FULL / MASKED / INTERNAL 之一,于是「归因齐了叫人」
* 那道哨兵当场为假。A6 的账:让「拍完不动数」的路存在,同一张卡会连发六次;
* 让模型自己写得出结论码,那张卡就永远不会发。两头都堵死才算数。
*/
{
name: 'share_scope',
type: 'text',
primaryKey: false,
masked: false,
label: '入库口径',
tally: ['PENDING', 'FULL', 'MASKED', 'INTERNAL'],
},
],
},
{
/*
* 归档结论 —— 一单一行的**汇总表**,验收机检谓词的入参就在这儿。
* 归档结论 —— 一单一行的**汇总表**,哨兵与报告读它。
* 汇总列与逐行列**故意不同名**:任务视图把草稿行摊平到顶层是「后写的赢」,
* 拿逐行列求值只会验到最后写的那一行,那不叫机检、叫抽样(A1 的教训)。
*/
domain: DOMAIN,
domain: D,
object: 'verdict',
kind: 'record',
caseKey: 'rfq_ref',
@@ -141,42 +394,179 @@ export const manifest: ActivityManifest = {
fields: [
{ name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
{ name: 'quote_no', type: 'text', primaryKey: false, masked: false, label: '报价单号' },
{ name: 'archived_total', type: 'integer', primaryKey: false, masked: false, label: '已归档条数' },
{ name: 'reason_logged_total', type: 'integer', primaryKey: false, masked: false, label: '已回写原因条数' },
{ name: 'profile_updated_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: '客户名称' },
{ name: 'outcome', type: 'text', primaryKey: false, masked: false, label: '这一单最后成没成(won/lost)' },
{ name: 'intel_total', type: 'integer', primaryKey: false, masked: false, label: '情报卡条数' },
/* 归一走完的凭据(0 也要写)—— 哨兵按它放交人归因那一步入场 */
{ name: 'collected_total', type: 'integer', primaryKey: false, masked: false, label: '已归一条数' },
/* 「归因落齐了」的凭据 —— 只在人环走完才产生 */
{ name: 'attributed_total', type: 'integer', primaryKey: false, masked: false, label: '已落归因条数' },
{ name: 'scope_decided_total', type: 'integer', primaryKey: false, masked: false, label: '入库口径已拍板' },
{ name: 'share_policy', type: 'text', primaryKey: false, masked: false, label: '入库口径(人拍的)' },
/* MVP §1.6 S2「画像更新」的落点 —— 一句话,不代笔改主数据 */
{ name: 'buyer_profile', type: 'text', primaryKey: false, masked: false, label: '客户行为画像' },
{ name: 'summary', type: 'text', primaryKey: false, masked: false, label: '归档结论' },
],
},
],
listeners: [],
// 7 天闭环(MVP示例 §1.7)
harness: { memoryWindow: 15, stallAfter: 'P1D', nudge: [], retry: { times: 2, backoff: 'PT30S' } },
/*
* 两条静态监听 —— **时限必配**(AM 4.4)。MVP §1.6 写死「7 天闭环」:
* 归档没有外部世界的回音要等,这两条管的是「别把它挂到天荒地老」——
* D+3 提醒把归因补上,D+7 到期升级。
*/
listeners: [
{
origin: 'static',
eventType: 'time',
keyFilter: { kind: 'archive-d3' },
semantic: 'observe',
deadline: 'P3D',
escalation: ['nudge'],
},
{
origin: 'static',
eventType: 'time',
keyFilter: { kind: 'archive-deadline' },
semantic: 'observe',
deadline: 'P7D',
escalation: ['nudge', 'escalate'],
},
],
harness: {
memoryWindow: 22,
// 7 天闭环是对**这一单办完**的约束;活本身只需两个人环
stallAfter: 'PT2H',
nudge: [{ after: 'PT1H', action: 'nudge' }],
retry: { times: 2, backoff: 'PT30S' },
},
},
/* ── 资产初始版 ──────────────────────────────────────────────── */
assets: {
/* 资产初始版:案例模型(2026-07-29 起活动只有这一种业务模型) */
businessModel: {
cmmnXml: CMMN_XML,
narrative: NARRATIVE,
dmn: { drd: [], tables: [] },
/* 步骤剧本 —— 与 A1 同一套:doneWhen 真了引擎自己收步,不问模型 */
dmn: A01_DMN,
steps: ARCHIVE_STEPS,
phases: ARCHIVE_PHASES,
},
skills: ARCHIVE_SKILLS,
/*
* 两个局面各一张(与 A2/A3/A4/A6 同款口径):
* · `archive-worksheet` 情报归档工作单 —— 归一与逐条归因期间摆它(逐颗情报档案、
* 归因六档与败价幅度两把尺、填回工作表);
* · `archive-roster` 情报归档清单 —— 拍板、放行、归档摆它。
*/
viewTemplates: [
{ name: 'archive-logged', html: ARCHIVE_LOGGED },
{ name: 'archive-release', html: ARCHIVE_RELEASE },
{ name: 'archive-worksheet', html: ARCHIVE_WORKSHEET },
{ name: 'archive-roster', html: ARCHIVE_ROSTER },
],
cardSchemas: {},
distillHooks: [
{ on: 'terminal', prompt: '归纳丢单的共性:价格、交期、竞对、客户类型 —— 供下一单更快更准。' },
{
on: 'terminal',
prompt: '归纳丢单的共性:价格、交期、竞对、客户类型 —— 供下一单更快更准。',
},
],
},
/* ── 样例集 —— 三条形状真不同(常备案例即验收案例)───────────── */
samples: [
{
name: '客户拒绝',
input: { customerCode: 'C001', quoteNo: 'Q202601', reason: '竞对报价更低' },
/*
* ① 流内失单:A6 `lost` 口的真实形状 —— rows 里报价函行(带客户表态与定案)
* 混着结论行。客户拿竞对价压,归因落 PRICE,竞对价一填败价幅度就算得出来。
*/
name: '流内转来 · 客户拒绝 · 竞对价压两成(失单归档常路)',
input: {
rfqRef: 'RFQ-2026-0777',
customerCode: 'CU-0311',
quoteNo: 'Q-0777',
outcome: 'lost',
reason: '客户明确回绝:竞对价更低',
customer_name: '青岛海洋电子',
rows: [
{
rfq_ref: 'RFQ-2026-0777',
pn: 'PESD2ETH-XR',
category: '二极管',
brand: 'Nexperia',
eau: 30000,
quoted_price: 0.098,
reply_type: 'REJECT',
deal_status: 'LOST',
reply_note: '客户回:这个价做不了,别家给到 0.078',
},
{
rfq_ref: 'RFQ-2026-0777',
pn: 'NE555P',
category: '定时器',
brand: 'TI',
eau: 5000,
quoted_price: 1.05,
reply_type: 'REJECT',
deal_status: 'LOST',
reply_note: '客户回:这颗一起给别家了',
},
{ rfq_ref: 'RFQ-2026-0777', line_total: 2, customer_name: '青岛海洋电子', summary: '两条都没谈成,交情报归档' },
],
},
expect: { ports: ['archived'] },
},
{
/*
* ② 成交归档:A6 `won` 那一口的单子结案后也归这一份档 —— 成交价与胜因同样是情报
* (下一单的基准价就在这儿)。归因不是 PRICE,竞对价那一格不该被闸拦。
*/
name: '成交归档 · 两条谈成 · 胜在交期与既有供应关系',
input: {
rfqRef: 'RFQ MD14.1',
customerCode: 'CU-0142',
quoteNo: 'Q-MD14.1',
outcome: 'won',
customer_name: 'Itelma',
rows: [
{
rfq_ref: 'RFQ MD14.1',
pn: 'BBPY00201209601Y00',
customer_pn: '4-14-01502',
category: '滤波器',
brand: 'YAGEO',
eau: 30000,
quoted_price: 0.085,
final_price: 0.085,
reply_type: 'ACCEPT',
deal_status: 'WON',
reply_note: '客户回:按这个价走,PO 下周发',
},
{
rfq_ref: 'RFQ MD14.1',
pn: 'CJTB0101R6N',
category: '集成电路',
brand: 'Jiangsu Changjing',
eau: 60000,
quoted_price: 0.42,
final_price: 0.42,
reply_type: 'ACCEPT',
deal_status: 'WON',
reply_note: '客户回:可以,一起下',
},
{ rfq_ref: 'RFQ MD14.1', line_total: 2, customer_name: 'Itelma' },
],
},
expect: { ports: ['archived'] },
},
{
/*
* ③ 单飞宽进:没有单号没有清单,正文说了哪一单、报了什么价、客户怎么回的 ——
* 归一造案号(ARC- 开头),两颗料一条都没成。
*/
name: '单飞需求 · 正文列两颗料 · 客户始终未表态(补一单历史)',
input: {
subject: 'MD14.2 这一单补个归档',
body: '上个月发出去的报价函一直没回音:XCJ-7788T 电源模块 8K,报价 14.80;LMR-3302QQ 稳压器 15K,报价 2.35。客户是 深圳市锐驰电子,联系人王工。报价日 2026-07-05,D+3 与 D+7 都催过,客户始终没表态,这一单按丢单归档。',
},
expect: { ports: ['archived'] },
},
],
+28 -6
View File
@@ -1,5 +1,5 @@
/**
* A7 技能 —— 归档的方法经验(AM 08)。
* A7 技能 —— 归档的方法经验(AM 08)。**载方法不载权力**:这里一条工具都不引。
*/
import { skillIdSchema } from '@contracts';
import type { Skill } from '@contracts';
@@ -7,16 +7,38 @@ import type { Skill } from '@contracts';
export const ARCHIVE_SKILLS: Skill[] = [
{
id: skillIdSchema.parse('archive.log-lost-reason'),
label: '把拒绝原因写成下一单的判据',
applicability: '客户拒绝或超时,要登记结果时。',
label: '把输赢的缘由写成下一单的判据',
applicability: '逐条落归因、准备发那张执行单时。',
method: [
'「价格高」不是原因,是结论。要写的是**可比对的事实**:',
'· 竞对报价多少、差几个百分点;',
'· 输在价格、交期、还是资质/认证;',
'· 客户是否明说了可接受区间。',
'· 竞对报价多少、差几个百分点(差 2 个点与差 30 个点是两件完全不同的事);',
'· 输在价格、交期、认证资质、规格,还是既有供应关系;',
'· 客户是否明说了可接受区间 —— 下一单的起点就藏在这一句里。',
'',
'判据要能被下一单**机器读取**:P-03 用「距上次报价 / 行情稳定」,P-01 用客户等级。',
'写成一段散文,它就只能被人读;写成字段,它才进得了决策表。',
'',
'**「客户没回」不是原因**:问不出来就落 OTHER 并在依据里写明「未获反馈」。',
'编一个听着合理的理由,三个月后没人记得它是编的 —— 而它已经在替下一单定价了。',
].join('\n'),
crystallized: [],
knowledgeRefs: [],
version: '1.0.0',
},
{
id: skillIdSchema.parse('archive.share-scope'),
label: '情报入库口径:哪些该留名,哪些该抹掉',
applicability: '发入库口径决策卡、给出建议档位时。',
method: [
'进了共享情报库的东西,**下一单谁都读得到** —— 这条边界过去了就收不回来。',
'三档的分界:',
'· **全量入库**:不涉第三方报价的败因(交期、认证、规格)—— 查得越全越好;',
'· **脱敏入库**:带着竞对报价的情报 —— 价位、归因、日期照留,谁说的抹掉。',
' 竞对的报价是他给客户的,不是给我们的;客户转述给我们,不等于我们可以对内广播;',
'· **只留内部**:客户明说不外传、或涉及在谈的项目 —— 这一单的情报只在本案卷里躺着。',
'',
'**建议归尺,结论归人**:A-02 只在决策卡上标一枚 recommended,',
'按不按它是签字的人的事 —— 这不是效率问题,是谁承担后果的问题。',
].join('\n'),
crystallized: [],
knowledgeRefs: [],
@@ -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>`;
+755 -140
View File
@@ -1,154 +1,769 @@
/**
* A7 情报归档的 HTML 报告模版 —— **活动自己持有的资产**(§10.3),随包版本冻结。
* A7 情报归档的 HTML 报告模版 —— **活动自己持有的资产**,随包版本冻结。
*
* 归档最容易被当成走过场:单子已经丢了,再填一遍原因像是给失败写检讨。
* 所以这两屏从头到尾只讲一件事 —— **这一单留下了什么,下一单谁会用它**。
* 三张共享域表各自对着下一单的一个判据:历史价格情报喂三分类 P-03 的「距上次报价」、
* 客户资料库喂利润模型 P-01 的客户等级因子、落地率统计喂驾驶舱。人在这一屏上要判断的
* 不是「填没填」,是**填的这几个字下一单的机器读不读得懂** —— 写成散文只能被人读,
* 写成字段才进得了决策表。
* 两个局面各一张(与 A2/A3/A4/A6 同款口径):
* · `archive-worksheet` **情报归档工作单** —— 归一与逐条归因期间摆它:
* 逐颗料一张情报档案卡(既成事实在左、归因在右)、归因六档与败价幅度 A-01 两把尺、
* 填回工作表就长在同一屏;
* · `archive-roster` **情报归档清单** —— 入库口径拍板、放行与归档摆它:整份情报卡,
* 口径分布、下一单谁用得上的落边图与落地动作在上面。
*
* 两个局面:
* ① 结果登记 —— 拒绝原因与竞对价位在不在、算不算数;
* ② 闭环待放行 —— 三样沉淀齐不齐;签完这一笔整条流走到终点。
* 「1:1」的落点是**每份件与它的办结快照**(在办可编辑 ↔ 回执收下那一刻的 finalHtml),
* 不是全程只许一张:作业的件与收口的件是两件工作两份件(规范姊妹篇 §1)。
*
* 这里**没有「案例进程」一节**:归档只有 S1 一个阶段,摆出来是一行凑数的进度条,
* 而版面上每多一节,真正要看的三张表就少一分注意力。
* 版面语言沿用 A1 从 PRD 目标件机械抽取的那套(`prd-style.ts` 原样拷入)——
* A7 没有自己的 PRD 目标件,与 A2/A3/A4/A6 同一处置:沿用家族版面自造。
*
* 容器是 `sandbox=""` iframe:无脚本、无外链、样式自带;数据由基座推送时填
* (`data-slot` 取值、`data-rows` 逐行展开)。状态章一律用「在办」灯 ——
* 模版是随包冻结的静态字符串,盖不了一个还没发生的章;`verdict` 的橙档专指
* 「还没发生,等你按」,绿只留给已达标的机检结论。
* 取值三纪律:顶层只取汇总列与单值列;逐条走 `mine:quote_archive.intel`;
* 列名逐字照 `declarations.ledger` 抄。**模版不算不判** —— 缺归因红带、归因徽、口径徽、
* 分布条全部由基座算好的数经 `data-class-of` 投影,判断留在样式表。
*/
import { noteOnly, section, statsBand, tableOnly, verdict, viewShell } from '@sdk';
import { PRD_CSS, PRD_SPRITE } from './prd-style.ts';
/**
* 静态阶梯 —— **判据的写法与沉淀的去向不是数据**,它们随包冻结,不走 `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/A4/A6 的 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 ARCHIVE_LOGGED = viewShell({
title: '结果登记',
kicker: 'A7 · 情报归档 · S1 回写',
tone: 'archive',
icon: 'database',
stamp: { kind: 'live', text: '回写中' },
sign: 'A7 情报归档 · 结果登记(共享域)',
/* 序号由 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-dbadge { 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-won { background: var(--ok); }
.proto-root .jc-tbadge.v-lost { background: var(--ink-4); }
.proto-root .jc-segmeter > i.v-won { background: var(--ok); }
.proto-root .jc-segmeter > i.v-lost { background: var(--ink-4); }
/* 归因徽:价格红 / 交期橙 / 认证蓝 / 规格深蓝 / 关系灰 / 说不清灰虚 —— 值是机器码,人读名交给样式表 */
.proto-root .jc-obadge {
display: inline-flex; align-items: center; justify-content: center;
min-width: 62px; height: 20px; padding: 0 7px; border-radius: 6px;
font-size: 10px; font-weight: 800; letter-spacing: .02em;
color: var(--ink-4); border: 1.5px dashed var(--ink-4); background: #fff;
}
.proto-root .jc-obadge::after { content: "待归因"; }
.proto-root .jc-obadge.v-price { color: var(--danger); border-color: var(--danger); border-style: solid; }
.proto-root .jc-obadge.v-price::after { content: "输在价格"; }
.proto-root .jc-obadge.v-leadtime { color: var(--warn-accent); border-color: var(--warn-accent); border-style: solid; }
.proto-root .jc-obadge.v-leadtime::after { content: "输在交期"; }
.proto-root .jc-obadge.v-cert { color: var(--brand); border-color: var(--brand); border-style: solid; }
.proto-root .jc-obadge.v-cert::after { content: "认证资质"; }
.proto-root .jc-obadge.v-spec { color: var(--brand-deep); border-color: var(--brand-deep); border-style: solid; }
.proto-root .jc-obadge.v-spec::after { content: "规格不符"; }
.proto-root .jc-obadge.v-relation { color: var(--ink-3); border-color: var(--ink-4); border-style: solid; }
.proto-root .jc-obadge.v-relation::after { content: "既有供应"; }
.proto-root .jc-obadge.v-other { color: var(--ink-3); border-color: var(--ink-4); border-style: dashed; }
.proto-root .jc-obadge.v-other::after { content: "说不清"; }
/* 口径徽:待定橙虚 / 全量绿 / 脱敏蓝 / 只留内部灰 */
.proto-root .jc-dbadge {
display: inline-flex; align-items: center; justify-content: center;
min-width: 66px; 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-dbadge::after { content: "未落"; }
.proto-root .jc-dbadge.v-pending { color: var(--warn-accent); border-color: var(--warn-accent); border-style: dashed; }
.proto-root .jc-dbadge.v-pending::after { content: "待你拍板"; }
.proto-root .jc-dbadge.v-full { color: var(--ok); border-color: var(--ok); }
.proto-root .jc-dbadge.v-full::after { content: "全量入库"; }
.proto-root .jc-dbadge.v-masked { color: var(--brand); border-color: var(--brand); }
.proto-root .jc-dbadge.v-masked::after { content: "脱敏入库"; }
.proto-root .jc-dbadge.v-internal { color: var(--ink-3); border-color: var(--ink-4); }
.proto-root .jc-dbadge.v-internal::after { content: "只留内部"; }
/* 败价幅度(A-01)—— 只在归因落「输在价格」的行上有值 */
.proto-root .jc-cbadge { font-size: 10.5px; font-weight: 700; color: var(--ink-4); }
.proto-root .jc-cbadge.v-slight { color: var(--ok); }
.proto-root .jc-cbadge.v-slight::after { content: "擦肩而过 ≤3%"; }
.proto-root .jc-cbadge.v-moderate { color: var(--warn-accent); }
.proto-root .jc-cbadge.v-moderate::after { content: "够得着 3~10%"; }
.proto-root .jc-cbadge.v-structural { color: var(--danger); }
.proto-root .jc-cbadge.v-structural::after { content: "结构性价差 >10%"; }
/* 分布条:四段按条数分宽 —— 条数投影成 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.r { background: var(--brand); }
.proto-root .jc-distbar > i.k { background: var(--ink-4); }
.proto-root .jc-distbar > i.p::before { content: "全量入库 · "; }
.proto-root .jc-distbar > i.n::before { content: "待拍板 · "; }
.proto-root .jc-distbar > i.r::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; }
/* ── 情报档案卡(工作单独有,照 A6 报价档案卡的骨架)──────────────────────
一颗料一张卡,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-lost { border-color: #f3b6b3; box-shadow: 0 1px 0 rgba(191,60,52,.05); }
.proto-root .jc-selcard.v-won { border-color: #bfe3cd; }
.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-lost .jc-selcard-h { background: #fff6f5; border-bottom-color: #f3d3d0; }
.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: 84px; 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.gk { color: var(--danger); border-color: var(--danger); }
.proto-root .jc-band.gz { color: var(--ink-3); border-color: var(--ink-4); }
/* 一单情报是怎么走完的:报价 → 结果 → 归因 → 口径 → 下一单,内联线稿走位图 */
.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; } }
/* ── 落边图:情报卡清单 → 归档闭环 → 下一单的三个用处(件不算不判)── */
.proto-root .route { margin: 2px 0 14px; padding: 10px 6px 6px; border: 1px solid var(--line); border-radius: 11px; background: #fff; }
.proto-root .route svg { width: 100%; height: auto; display: block; }
.proto-root .route .rspine { stroke: var(--brand); stroke-width: 2; }
.proto-root .route .redge { stroke: var(--line); stroke-width: 2; stroke-dasharray: 5 5; fill: none; }
.proto-root .route .rnode { fill: #fff; stroke: var(--line); stroke-width: 1.5; }
.proto-root .route .rnode.hit { stroke: var(--brand); stroke-width: 2; }
.proto-root .route .rtxt { font-size: 12px; font-weight: 800; fill: var(--ink-3); }
.proto-root .route .rtxt.hit { fill: var(--brand-deep); }
.proto-root .route .rsub { font-size: 10px; fill: var(--ink-4); }
.proto-root .route .rcond { font-size: 10.5px; font-weight: 700; fill: var(--ink-4); }
.proto-root .route .rtip { fill: var(--line); }
.proto-root .route .rtip.hit { fill: var(--brand); }
.proto-root .route .legend { display: flex; gap: 14px; flex-wrap: wrap; padding: 6px 10px 2px; font-size: 10.5px; color: var(--ink-4); }
/* 结论带 */
.proto-root .concl { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; align-items: start; margin: 2px 0 14px; padding: 11px 14px; border: 1px solid var(--line); border-radius: 11px; background: var(--soft); }
.proto-root .concl svg { grid-row: span 2; margin-top: 2px; color: var(--brand); }
.proto-root .concl .cl-main { font-size: 12.5px; color: var(--ink); line-height: 1.6; }
.proto-root .concl .cl-main b { font-weight: 800; }
.proto-root .concl .cl-route { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; font-size: 11.5px; color: var(--ink-3); }
.proto-root .concl .cl-route em { font-style: normal; color: var(--ink-4); }
.proto-root .concl .cl-route b { color: var(--brand-deep); font-weight: 800; }
.proto-root .concl .cl-route code { font-family: var(--mono, ui-monospace, monospace); font-size: 10.5px; color: var(--ink-4); }
`;
/** 一枚图标 —— 与原型同一写法 */
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_archive_intel_missing"></b><span>/ <b data-slot="quote_archive_intel_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_archive.intel">
<i data-class-of="deal_result"></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>报价日 <b data-slot="quoted_at" data-blank></b></span></span>
<span class="jc-fcard-note">下一单的三分类 P-03 看的就是「距上次报价多久」—— 这一格空着,这条情报到下一单就哑了</span>
</div>
</div>`;
/** 一单情报是怎么走完的 —— 走位图(形状写死,不算不判) */
const ARCHIVE_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>输赢归因</b><span>价格 / 交期 / 认证 / 规格 / 关系 / 说不清 —— 这一格由跟单的人落</span></div>
<div class="jc-plarrow">${ico('arrow-right', 18)}</div>
<div class="jc-plnode"><b>败价幅度 A-01</b><span>价差 ≤3% / 3~10% / >10%,决定下一单怎么用这条情报</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>下一单</b><span>三分类取时效 · 定价取基准 · 客户画像取行为</span></div>
</div>`;
/** 缺归因红带 —— 与提交闸、放行门 L1 同一个数同一套算法(基座现数);一格不缺自动收 */
const GAP_BAR = `<div class="jc-gapbar" data-class-of="quote_archive_intel_missing">${ico('alert', 14)}
<span><b>还差 <span data-slot="quote_archive_intel_missing"></span> 行归因没落</b>
—— <span data-slot="quote_archive_intel_missing_keys"></span>。
按右上角「编辑」开表,红框那几格就是要填的:输在价格填 PRICE 并把竞对报的那个价填进「竞对价」,
交期填 LEADTIME,认证资质填 CERT,规格不符填 SPEC,既有供应关系填 RELATION,
问不出来填 OTHER 并在依据里写明未获反馈。补齐了执行单才提交得了。</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>四列:
<b>这四列系统一格都不写</b> —— 客户为什么不下单只有你看得见,别让系统替他解释。
归因落 PRICE 的行务必把竞对报的那个数填上,败价幅度全看这一格。不该归档的料按行首 × 撤掉。
改动只落任务草稿层,<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` 只挂人可改的四列(cause_tag / competitor_price / competitor_name / evidence)
* + 主键 pn —— 编辑面精确等于 BD 的职权面;**主键格必挂 `data-col`**(它不是编辑面,
* 是行的钥匙:不挂,服务端按「全表一致列」补键,两行会对到同一行上,后者盖前者 ——
* A2 走查逮过)。
* 归因挂 `req`:空着出「必填」胶囊、整行标底,与提交闸、放行门同源(都数 `_missing`)。
*/
const INTEL_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:88px">报价日期</th>
<th style="min-width:88px">我方报价</th>
<th style="min-width:80px">成交价</th>
<th class="req" style="width:92px;min-width:92px">输赢归因(你落)</th>
<th style="min-width:80px">竞对价</th>
<th style="min-width:96px">竞对是谁</th>
<th style="width:104px;min-width:104px">败价幅度</th>
<th style="min-width:150px;width:100%">依据 / 客户原话</th>
</tr></thead>
<tbody data-rows="mine:quote_archive.intel" data-edit="quote_archive.intel"><tr>
<td class="jc-rn"><button type="button" class="jc-rowdel" data-edit-del="quote_archive.intel" title="撤掉这一行">×</button></td>
<td class="jc-ty" data-h="果"><span class="jc-tbadge" data-class-of="deal_result" data-slot="deal_result"></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 mono" data-slot="quoted_at" data-blank></span></td>
<td class="dimempty" data-h="我方报价"><span class="jc-vcell num" data-slot="quoted_price" data-blank></span></td>
<td class="dimempty" data-h="成交价"><span class="jc-vcell num" data-slot="final_price" data-blank></span></td>
<td class="req" data-h="输赢归因(你落)">
<span class="jc-obadge" data-class-of="cause_tag"></span>
<span class="jc-vcell" data-slot="cause_tag" data-col="cause_tag" data-blank></span>
</td>
<td class="sug" data-h="竞对价"><span class="jc-vcell num" data-slot="competitor_price" data-col="competitor_price" data-blank></span></td>
<td class="sug" data-h="竞对是谁"><span class="jc-vcell" data-slot="competitor_name" data-col="competitor_name" data-blank></span></td>
<td data-h="败价幅度"><span class="jc-cbadge" data-class-of="price_gap_band"></span></td>
<td class="opt" data-h="依据 / 客户原话"><span class="jc-vcell" data-slot="evidence" data-col="evidence" data-blank></span></td>
</tr></tbody>
</table></div>`;
/**
* 情报清单表(收口件用)—— 编辑面换成**放行时可改的两列**(归因与依据),
* 与 `requiredDeliverables[].editableAtRelease` 逐字对齐。
*/
const ARCHIVED_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:88px">报价日期</th>
<th style="min-width:88px">我方报价</th>
<th style="min-width:80px">成交价</th>
<th style="width:92px;min-width:92px">输赢归因</th>
<th style="min-width:80px">竞对价</th>
<th style="width:104px;min-width:104px">败价幅度</th>
<th style="width:84px;min-width:84px">入库口径</th>
<th style="min-width:150px;width:100%">依据 / 客户原话</th>
</tr></thead>
<tbody data-rows="mine:quote_archive.intel" data-edit="quote_archive.intel"><tr>
<td class="jc-rn"><button type="button" class="jc-rowdel" data-edit-del="quote_archive.intel" title="撤掉这一行">×</button></td>
<td class="jc-ty" data-h="果"><span class="jc-tbadge" data-class-of="deal_result" data-slot="deal_result"></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 mono" data-slot="quoted_at" data-blank></span></td>
<td class="dimempty" data-h="我方报价"><span class="jc-vcell num" data-slot="quoted_price" data-blank></span></td>
<td class="dimempty" data-h="成交价"><span class="jc-vcell num" data-slot="final_price" data-blank></span></td>
<td class="req" data-h="输赢归因">
<span class="jc-obadge" data-class-of="cause_tag"></span>
<span class="jc-vcell" data-slot="cause_tag" data-col="cause_tag" data-blank></span>
</td>
<td class="dimempty" data-h="竞对价"><span class="jc-vcell num" data-slot="competitor_price" data-blank></span></td>
<td class="dimempty" data-h="败价幅度"><span class="jc-cbadge" data-class-of="price_gap_band"></span></td>
<td data-h="入库口径"><span class="jc-dbadge" data-class-of="share_scope"></span></td>
<td class="opt" data-h="依据 / 客户原话"><span class="jc-vcell" data-slot="evidence" data-col="evidence" 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_archive_intel_share_scope_full" data-slot="quote_archive_intel_share_scope_full"></i>
<i class="n" data-class-of="quote_archive_intel_share_scope_pending" data-slot="quote_archive_intel_share_scope_pending"></i>
<i class="r" data-class-of="quote_archive_intel_share_scope_masked" data-slot="quote_archive_intel_share_scope_masked"></i>
<i class="k" data-class-of="quote_archive_intel_share_scope_internal" data-slot="quote_archive_intel_share_scope_internal"></i>
</div>
<div class="jc-distlg">
<span><em class="a"></em>全量入库 · 客户与竞对都留名 <b data-slot="quote_archive_intel_share_scope_full"></b></span>
<span><em class="b"></em>待拍板 · 口径还没定 <b data-slot="quote_archive_intel_share_scope_pending"></b></span>
<span><em class="b"></em>脱敏入库 · 只留价位与败因 <b data-slot="quote_archive_intel_share_scope_masked"></b></span>
<span><em class="c"></em>只留内部 · 不进共享库 <b data-slot="quote_archive_intel_share_scope_internal"></b></span>
</div>
</div>`;
/* ==================== 局面一 · 情报归档工作单(归一 / 逐条归因)==================== */
/**
* 逐颗情报档案 —— 一颗料一张卡(data-rows 逐行复制):左既成事实、右归因。
*
* **容器用 `<section>` 不用 `<div>`**:渲染器的行展开按第一个同名闭合标签截行内容,
* 行模版里嵌着 `<div>` 时 `<div data-rows>` 会把行截成套娃(A2 逮过,记在陷阱表)。
*/
const INTEL_CARDS = `<section class="jc-selcards" data-rows="mine:quote_archive.intel">
<article class="jc-selcard" data-class-of="deal_result">
<div class="jc-selcard-h">
<span class="jc-tbadge" data-class-of="deal_result" data-slot="deal_result"></span>
<code data-slot="pn"></code>
<span class="why" data-slot="evidence" data-blank></span>
<span class="jc-obadge" data-class-of="cause_tag"></span>
<span class="jc-dbadge" data-class-of="share_scope"></span>
</div>
<div class="jc-selcard-b">
<div class="jc-selcol">
<h5>既成事实(报过的价,一个数字都不改)</h5>
<div class="jc-kvrow"><i>客户</i><span data-slot="customer_name" 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="eau" data-blank></span></div>
<div class="jc-kvrow"><i>我方报价</i><span class="num" data-slot="quoted_price" data-blank></span></div>
<div class="jc-kvrow"><i>成交价</i><span class="num" data-slot="final_price" data-blank></span></div>
<div class="jc-kvrow"><i>报价日期</i><span data-slot="quoted_at" data-blank></span></div>
<div class="jc-kvrow"><i>结果</i><span data-slot="deal_result" data-blank></span></div>
</div>
<div class="jc-selcol">
<h5>归因(填回下表,这里回显)</h5>
<div class="jc-kvrow"><i>输赢归因</i><span class="sc-req" data-slot="cause_tag" data-blank></span></div>
<div class="jc-kvrow"><i>竞对价</i><span class="sc-sug num" data-slot="competitor_price" data-blank></span></div>
<div class="jc-kvrow"><i>竞对是谁</i><span class="sc-sug" data-slot="competitor_name" data-blank></span></div>
<div class="jc-kvrow"><i>依据 / 原话</i><span class="sc-opt" data-slot="evidence" data-blank></span></div>
<div class="jc-kvrow"><i>败价幅度</i><span class="jc-cbadge" data-class-of="price_gap_band"></span></div>
<div class="jc-kvrow"><i>入库口径</i><span class="jc-dbadge" data-class-of="share_scope"></span></div>
</div>
</div>
</article>
</section>`;
/** 归因六档 —— 静态规则,尺挂在墙上(素材:MVP 示例工作流 §1.6 结果登记) */
const CAUSE_RULES = `<div class="jc-graderule">
<div class="jc-graderule-r">
<span class="jc-band gk">PRICE</span>
<p><b>输在价格。</b>客户明说了别家更便宜,或直接给出一个我们够不着的数 ——
<b>这一档必须填竞对价</b>:没有那个数,「输在价格」下一单读不出任何东西,
而有了它,败价幅度 A-01 当场算得出来。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gc">LEADTIME</span>
<p><b>输在交期。</b>客户等不起 —— 别家现货、我们八周。
价格再低也没用,下一单同一颗料先问现货商。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gb">CERT</span>
<p><b>认证资质。</b>车规、医疗、军工的门槛没过,或供应商资质不在客户合格名录里 ——
下一单这一类需求先核认证,别报完价才发现进不去。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gb">SPEC</span>
<p><b>规格不匹配。</b>参数、封装、温度范围对不上 —— 这是选型的账,
下一单同一需求直接从替代选型开始,别在原厂料上耗。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gz">RELATION</span>
<p><b>既有供应关系。</b>客户手上已经有稳定的供应商,这一轮只是来比个价 ——
情报照记:下一单知道要拿什么去撬,或者干脆不投这个标。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gz">OTHER</span>
<p><b>说不清。</b><b>「客户没回」不是原因</b> —— 问不出来就落这一档,
并在依据里写明「未获反馈」。<b>不许编一个听着合理的理由</b>:
编出来的原因会一路流进下一单的判据里,而那时已经没人记得它是编的。</p>
</div>
</div>`;
/** 败价幅度 A-01 的三档 —— 静态规则(素材:MVP 示例工作流 §1.6 情报复用) */
const BAND_RULES = `<div class="jc-graderule">
<div class="jc-graderule-r">
<span class="jc-band ga">≤ 3%</span>
<p><b>擦肩而过。</b>三个点以内的价差不足以定输赢 —— 下一单同价位再试,
真正输的多半在交期、认证或关系上,回头看看依据那一栏写了什么。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gc">3% ~ 10%</span>
<p><b>够得着。</b>下一单要么让到这一档,要么换替代料把成本降下来 ——
这是替代选型最值钱的入口:同样的功能,换一颗料就把十个点让出来了。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gk">&gt; 10%</span>
<p><b>结构性价差。</b>一成以上不是让价能填的坑,是成本结构不对 ——
下一单别再报同一价位,先走替代选型或重新寻源。<b>拿同一个价再报一次不叫坚持,叫没记账</b>。</p>
</div>
</div>`;
export const ARCHIVE_WORKSHEET = doc({
badge: 'file-text',
kicker: '统一报价 · A7 情报归档 · 归因作业',
title: '情报归档工作单',
titleRef: 'rfq_ref',
state:
'<span class="jc-rp-wait warn">待归因 <b data-slot="quote_archive_intel_missing"></b> / <b data-slot="quote_archive_intel_rows"></b> 条</span>',
body:
statsBand([
['报价日期', 'quoted_at'],
['竞对价位', 'competitor_price'],
['客户等级', 'grade'],
]) +
section('本单登记的结果',
tableOnly('mine:shared.history_price', [
['型号 PN', 'pn', 'mono'],
['报价日期', 'quoted_at', 'mono'],
['拒绝原因', 'lost_reason'],
['竞对价位', 'competitor_price'],
]) +
noteOnly(
'<b>「客户没回」不是原因。</b>问不出来就写「未获反馈」—— 不许编一个听着合理的理由:' +
'编出来的原因会一路流进下一单的判据里,而那时已经没人记得它是编的。',
),
{ text: '只列本单写下的行' },
) +
section('什么样的原因才算数',
ladder([
['1', '竞对报价多少、差几个百分点', '要数,不要形容词'],
['2', '输在价格、交期,还是资质 / 认证', '把败因归到一个维度上'],
['3', '客户是否明说了可接受区间', '下一单的起点就藏在这句话里'],
]) +
noteOnly(
'<b>「价格高」不是原因,是结论。</b>判据要能被下一单<b>机器读取</b>:' +
'P-03 用「距上次报价 / 行情稳定」,P-01 用客户等级。' +
'写成一段散文,它就只能被人读;写成字段,它才进得了决策表。',
),
{ text: '写成字段' },
) +
section('这一单往哪儿沉 —— 三张共享域表',
ladder([
['1', '历史价格情报 · shared.history_price', '喂给下一单的三分类 P-03'],
['2', '客户资料库 · shared.customer', '喂给下一单的利润模型 P-01'],
['3', '报价落地率统计 · shared.landing_stats', '喂给驾驶舱的落地率与 D+3 回复率'],
]),
{ text: '跨活动的沉淀事实' },
) +
section('这一单的底线',
verdict('不得空归档 —— 每一单不成交的报价也是情报资产', 'wait', '原因未回写,预检当场退回') +
noteOnly(
'归档做得好不好,<b>不在这一单看得出来</b> —— 在下一次报价能不能一眼看到「上次为什么丢」看得出来。',
),
),
part('归档任务', '客户 <b data-slot="customer_name" data-blank></b> · 报价单 <b data-slot="quote_no" data-blank></b> · 共 <b data-slot="intel_total"></b> 条') +
`<div class="jc-derive">${ico('info', 14)}
<span>这一屏是一份情报的作业台:系统已按上游的结果归好情报卡,报价、成交价与日期都在下面 ——
<b>既成事实一个数字都不改</b>。你要做的是<b>逐条落输赢归因</b>:客户到底为什么下单、
为什么不下单。这一列系统一格都不写:那件事只有跟单的人知道。
<b>这一单之后再没有人会回头看它</b> —— 归因写错没有下一道工序纠正,
而它会一路流进下一单的判据里。</span>
</div>` +
FOUR +
part('一单情报是怎么走完的', '既成事实 → 归因 → 败价幅度 → 口径 → 下一单') +
ARCHIVE_LINE +
part('逐颗情报档案', '既成事实在左 · 归因在右') +
INTEL_CARDS +
part('输赢归因六档', '选最主要的那一个 —— 尺挂在墙上') +
CAUSE_RULES +
part('败价幅度 A-01', '价差决定下一单怎么用这条情报') +
BAND_RULES +
part('填回工作表', '归因落在这 —— 补齐了执行单才提交得了') +
GAP_BAR +
editGate('quote_archive.intel', '情报卡填回表') +
INTEL_TABLE +
addRowBar('quote_archive.intel') +
`<div class="jc-drop">${ico('info', 13)}
<span>带红标的<b>输赢归因</b>是必填,而且<b>只由你填</b> —— 缺一行执行单按不下提交,
与放行门是同一个数同一套算法。六个取值一个字母不许差:
<b>PRICE</b> 价格 · <b>LEADTIME</b> 交期 · <b>CERT</b> 认证资质 · <b>SPEC</b> 规格不符 ·
<b>RELATION</b> 既有供应 · <b>OTHER</b> 说不清。
归因落 PRICE 的行务必把竞对报的那个价填进<b>竞对价</b> —— 那一格空着这张单同样提交不了,
败价幅度全看它。<b>依据那一栏尽量记客户原话</b>:「你们比 XX 贵两毛」比「价格偏高」
值钱得多 —— 下一单定价时看的就是这一句。不该归档的料按行首 × 撤掉。</span>
</div>`,
foot: sign('BD 归因 · AI 归一情报卡并按尺算败价幅度,输赢的缘由由你落', '统一报价 · A7 情报归档 · 在办'),
});
/** 局面二:闭环待放行 —— 签完这一笔,整条工作流走到终点 */
export const ARCHIVE_RELEASE = viewShell({
title: '情报归档 · 闭环待放行',
kicker: 'A7 · 情报归档 · 成果待签',
tone: 'case',
icon: 'archive',
stamp: { kind: 'live', text: '待签字封卷' },
sign: 'A7 情报归档 · 闭环成果(待放行)',
/* ==================== 局面二 · 情报归档清单(拍板 / 放行 / 归档)==================== */
/**
* 落边图:情报卡清单 → 归档闭环 → 下一单的三个用处。
*
* A7 是流程包的最后一站,**只有一口且无条件**(E14)—— 这条边不是选出来的,
* 所以画成实线亮是如实,不是替谁宣布。右边三个用处是这份情报的去处,
* 各挂本单条数(基座现数),没有的那一档条数为 0、人一眼看得出。
*/
const ROUTE_FIG = `<div class="route">
<svg viewBox="0 0 660 250" fill="none">
<rect class="rnode hit" x="8" y="95" width="150" height="60" rx="10"/>
<text class="rtxt hit" x="83" y="120" text-anchor="middle">情报卡清单</text>
<text class="rsub" x="83" y="138" text-anchor="middle">共 <tspan data-slot="quote_archive_intel_rows"></tspan> 条</text>
<path class="rspine" d="M158 125 H228"/>
<path class="rtip hit" d="M216 119 l14 6 -14 6 z"/>
<rect class="rnode hit" x="232" y="95" width="128" height="60" rx="10"/>
<text class="rtxt hit" x="296" y="120" text-anchor="middle">归档闭环</text>
<text class="rsub" x="296" y="138" text-anchor="middle">这条流走到终点</text>
<path class="redge" d="M360 125 C420 125 430 40 470 40"/>
<path class="redge" d="M360 125 C420 125 430 125 470 125"/>
<path class="redge" d="M360 125 C420 125 430 210 470 210"/>
<path class="rtip" d="M458 34 l14 6 -14 6 z"/>
<path class="rtip" d="M458 119 l14 6 -14 6 z"/>
<path class="rtip" d="M458 204 l14 6 -14 6 z"/>
<text class="rcond" x="378" y="62">时效 · 报价日期</text>
<text class="rcond" x="378" y="115">价格 <tspan data-slot="quote_archive_intel_cause_tag_price"></tspan> 条</text>
<text class="rcond" x="378" y="232">行为画像</text>
<rect class="rnode" x="470" y="8" width="180" height="64" rx="10"/>
<text class="rtxt" x="560" y="35" text-anchor="middle">下一单 · 三分类 P-03</text>
<text class="rsub" x="560" y="53" text-anchor="middle">距上次报价多久 · 行情稳不稳</text>
<rect class="rnode" x="470" y="93" width="180" height="64" rx="10"/>
<text class="rtxt" x="560" y="120" text-anchor="middle">下一单 · 成本定价</text>
<text class="rsub" x="560" y="138" text-anchor="middle">基准价与竞对价位</text>
<rect class="rnode" x="470" y="178" width="180" height="64" rx="10"/>
<text class="rtxt" x="560" y="205" text-anchor="middle">客户画像 · 利润模型 P-01</text>
<text class="rsub" x="560" y="223" text-anchor="middle">价格敏感度 · 决策周期</text>
</svg>
<div class="legend">
<span>A7 只有一口且无条件 —— 它不是选出来的,是这条流的终点</span>
<span>右边三个用处是这份情报的去处:入库口径定的就是它们读不读得到</span>
<span>只留内部那一档不进共享库 —— 三条虚线到那一单就全断了</span>
</div>
</div>`;
/** 入库口径三档 —— 静态规则,决策卡上那一枚 recommended 由 A-02 标 */
const SCOPE_RULES = `<div class="jc-graderule">
<div class="jc-graderule-r">
<span class="jc-band ga">全量入库</span>
<p><b>客户与竞对都留名。</b>下一单查这颗料时,客户是谁、竞对是谁、报了多少一目了然 ——
情报最值钱的形态。代价是客户身份与第三方报价就此对内公开。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gb">脱敏入库</span>
<p><b>只留价位与败因。</b>竞对与客户的名字抹掉,价位、归因、日期照留 ——
下一单照样判得了价,只是追不到是谁说的。<b>带着第三方报价的情报默认走这一档</b>:
竞对的价是他给客户的,不是给我们的。</p>
</div>
<div class="jc-graderule-r">
<span class="jc-band gz">只留内部</span>
<p><b>不进共享情报库。</b>这一单的情报只在本案卷里躺着,下一单查不到 ——
涉密、客户明说不外传、或项目还在谈时才选这一档。选它等于承认这份情报只值一次。</p>
</div>
</div>`;
export const ARCHIVE_ROSTER = doc({
badge: 'file-check',
kicker: '统一报价 · A7 情报归档 · 交付成果',
title: '情报归档清单',
titleRef: 'rfq_ref',
state:
'<span class="jc-rp-wait">情报 <b data-slot="quote_archive_intel_rows"></b> 条 · 待拍板 <b data-slot="quote_archive_intel_share_scope_pending"></b> 条(要归零)</span>',
body:
statsBand([
['统计周期', 'period'],
['落地率', 'landing_rate'],
['TypeA 命中', 'type_a_hit'],
['D+3 回复率', 'reply_rate_d3'],
]) +
section('历史价格情报回写',
tableOnly('mine:shared.history_price', [
['型号 PN', 'pn', 'mono'],
['报价日期', 'quoted_at', 'mono'],
['拒绝原因', 'lost_reason'],
['竞对价位', 'competitor_price'],
]),
{ text: '下一单 P-03 取它' },
) +
section('客户画像更新',
tableOnly('mine:shared.customer', [
['客户码', 'customer_code', 'mono'],
['客户等级', 'grade'],
['画像标签', 'profile_tags'],
]),
{ text: '下一单 P-01 取它' },
) +
section('报价落地率统计',
tableOnly('mine:shared.landing_stats', [
['月份', 'period', 'mono'],
['落地率', 'landing_rate'],
['TypeA 命中', 'type_a_hit'],
['D+3 回复率', 'reply_rate_d3'],
]) +
noteOnly(
'三张表都写在<b>共享域</b>:它们是跨活动的沉淀事实,不属于这一单 ——' +
'这也是活动之间除了流程边以外,唯一一条合法的信息通路。',
),
{ text: '驾驶舱取它' },
) +
section('放行前要过的三道判据',
ladder([
['1', '拒绝原因 / 竞对价位已回写', 'L1 机检 · reasonLogged = true'],
['2', '客户画像标签已更新', 'L2 机检 · profileUpdated = true'],
['3', 'BD 放行', 'L5 人签 · 签了才算数'],
]),
{ text: '两机检 · 一人签' },
) +
section('封卷之后会发生什么',
verdict('放行即闭环:这条工作流就此收敛为「已完成」', 'wait', '终态封卷触发蒸馏钩子') +
noteOnly(
'蒸馏从任务记忆归纳出<b>活动记忆</b>的候选条目(带证据链),经回放与人放行才成为' +
'下一版业务模型的一部分 —— <b>记忆不载权力</b>。这一单丢了,但它把下一单的地形描出来了。',
),
),
part('归档要素', '情报归档 · 能算的归判定表,要担责的归人') +
FOUR +
part('入库口径分布', '全量 + 待拍板 + 脱敏 + 只留内部 = 总条数') +
DIST +
`<div class="jc-derive">${ico('info', 14)}
<span>四段条数由基座逐行现数,不是谁报上来的。橙段(<b>待拍板</b>)是过渡态 ——
情报归好了而口径还没定,它<b>归零这一单才走得到放行门</b>。
进了共享情报库的东西下一单谁都读得到,<b>这条边界过去了就收不回来</b> ——
所以系统不替你定,只按 A-02 给一枚建议。</span>
</div>` +
part('情报卡清单', '<b data-slot="intel_total"></b> 条 · 每条都有归因') +
GAP_BAR +
editGate('quote_archive.intel', '情报清单表') +
ARCHIVED_TABLE +
`<div class="jc-drop">${ico('info', 13)}
<span>每一行摆的是这一条的<b>来路</b>:报了多少、成没成、输赢归在哪一档、
竞对报了什么价、败价幅度落在哪一级。签字前请重点看两处:
<b>归因列</b>里落了 OTHER 的条目(说不清的情报到下一单就是哑的),
与<b>依据列</b> —— 没有客户原话的归因,下一单只能当传说看。
这一屏可改的是<b>输赢归因</b>与<b>依据</b>两列(你的职权面)。</span>
</div>` +
part('入库口径', '三档 · 建议归尺,结论归你') +
SCOPE_RULES +
part('落边 · 这一签之后谁用得上', '一口且无条件 · 归档闭环') +
ROUTE_FIG +
`<div class="concl">
${ico('git-branch', 15)}
<div class="cl-main"><b>归档结论</b> —— <span data-slot="summary" data-blank></span></div>
<div class="cl-route"><em>客户行为画像</em><b data-slot="buyer_profile" data-blank></b><code data-slot="share_policy" data-blank></code></div>
</div>` +
part('放行后即执行的落地动作', '门前 → 门后') +
`<div class="jc-acts">
<div class="jc-act"><i>${ico('check', 11)}</i>
<div><b>草稿层合并进正式版</b>&#12288;<span>本任务写下的情报卡与归档结论一并落账;
你就地改过的归因与依据随之落定。</span></div>
</div>
<div class="jc-act"><i>${ico('check', 11)}</i>
<div><b>归档闭环那一口此刻才发射</b>&#12288;<span>情报卡
<b data-slot="quote_archive_intel_rows"></b> 条随载荷交出去,这条工作流走到终点 ——
放行之前,世界上什么都没有发生。</span></div>
</div>
<div class="jc-act"><i>${ico('check', 11)}</i>
<div><b>下一单开始受用</b>&#12288;<span>按你定的口径,这几条价位、败因与日期
进入下一次询价的判据面 —— <b>归档做得好不好,不在这一单看得出来</b>,
在下一次报价能不能一眼看到「上次为什么丢」看得出来。</span></div>
</div>
</div>`,
foot: sign('BD 归档放行 · AI 归一与算尺留痕 —— 要改哪一格,在这张情报清单上就地改', '统一报价 · A7 情报归档 → 归档闭环'),
});