forked from ChaiHao/BPOS-MVP
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:
@@ -0,0 +1,499 @@
|
||||
/**
|
||||
* A4 成本定价的包内自测 —— **只准 import @sdk / @contracts / @sfeel**。
|
||||
* 案例活动没有 `run`,自测不跑回合;测的是清单本身与四处对齐
|
||||
* (判据谓词 / 出口载荷 / 剧本声明 / 两把尺的档位)。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { activityManifestSchema } from '@contracts';
|
||||
import { evaluateCondition, evaluateUnaryTest } from '@sfeel';
|
||||
import { createMockHost } from '@sdk';
|
||||
import { P01_DMN, manifest } from '../src/manifest.ts';
|
||||
import activity from '../src/index.ts';
|
||||
|
||||
describe('清单本身合规', () => {
|
||||
it('过 @contracts 的 schema', () => {
|
||||
expect(() => activityManifestSchema.parse(manifest)).not.toThrow();
|
||||
});
|
||||
|
||||
it('监听时限已配 —— 无悬空监听', () => {
|
||||
expect(manifest.declarations.listeners.every((l) => /^P/.test(l.deadline))).toBe(true);
|
||||
});
|
||||
|
||||
it('L1/L2 判据都给了可机检的谓词 —— 验收等级不得谎报', () => {
|
||||
for (const c of manifest.contract.acceptanceCriteria) {
|
||||
if (c.level === 'L1' || c.level === 'L2') expect(c.predicate ?? c.modelRef, c.key).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('禁区短而硬,授权边界给了调人预算', () => {
|
||||
expect(manifest.contract.prohibitions.length).toBeGreaterThan(0);
|
||||
expect(manifest.contract.authorization.humanCallBudget).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('每件必交付物都有台账地址', () => {
|
||||
for (const d of manifest.contract.requiredDeliverables) {
|
||||
expect(d.ledger, d.key).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* **宽进严出**:起单一个必填都不设 —— 上游(A3 的 quotation 口)发的是
|
||||
* `{rfqRef, rows}`,而上一版必填 `rfqRef + customerCode + records`,
|
||||
* 门口拦的恰是主路。归一在活动内部做。
|
||||
*/
|
||||
it('起单宽进:input.required 是空的,三种来路的键都声明着', () => {
|
||||
expect(manifest.contract.input.data['required']).toEqual([]);
|
||||
const properties = manifest.contract.input.data['properties'] as Record<string, unknown>;
|
||||
for (const key of ['rfqRef', 'rows', 'records', 'subject', 'body']) {
|
||||
expect(properties[key], `入参少了 ${key}`).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* **拟报单价是必填** —— 这是 A4 的门面判据:件上的缺价红带、执行卡的提交闸、
|
||||
* 放行门 L1 三处同源(都数基座的 `_missing`)。声明丢了 required,三处一起失明。
|
||||
*/
|
||||
it('拟报单价标了 required —— 红带 / 提交闸 / 放行门三处同源的那个数才存在', () => {
|
||||
const draft = manifest.declarations.ledger.find((d) => d.object === 'draft');
|
||||
expect(draft?.fields.find((f) => f.name === 'quoted_price')?.required).toBe(true);
|
||||
});
|
||||
|
||||
/*
|
||||
* **建议价与拟报单价分两列是结构治,不是措辞治**(2026-08-03 单飞案例逼出来的):
|
||||
* 只有一列价时,剧本写「算不出的留空」,模型照样给库里查不到的料编了价 ——
|
||||
* 一条要它分辨「算得出/算不出」的规矩会走岔;「这一列你不写」没有分支可走岔。
|
||||
*/
|
||||
it('建议价与拟报单价是两列 —— AI 写建议价,人写拟报单价', () => {
|
||||
const draft = manifest.declarations.ledger.find((d) => d.object === 'draft');
|
||||
const suggested = draft?.fields.find((f) => f.name === 'suggested_price');
|
||||
expect(suggested, '缺 suggested_price 这一列').toBeDefined();
|
||||
expect(suggested?.required, '建议价不该是必填 —— 算不出就该空着').toBeUndefined();
|
||||
for (const step of ['S1_price', 'S3_gate']) {
|
||||
const guide = manifest.assets.businessModel.steps?.find((s) => s.item === step)?.guide ?? '';
|
||||
expect(guide, `${step} 没写死「quoted_price 你一格都不写」`).toMatch(/quoted_price[^\n]*一格都不写|一格都不写[^\n]*quoted_price/);
|
||||
}
|
||||
});
|
||||
|
||||
it('门禁处置给了取值域 —— 三档条数由基座现数,不由活动自报', () => {
|
||||
const draft = manifest.declarations.ledger.find((d) => d.object === 'draft');
|
||||
expect(draft?.fields.find((f) => f.name === 'disposition')?.tally).toEqual(['PASS', 'NOTE', 'BLOCK']);
|
||||
});
|
||||
|
||||
/*
|
||||
* 我方写的注不标 `expect` —— 标了 `_gaps` 恒大于 0,读它的判据永远绿不了(A1 的账)。
|
||||
* 客户目标价才是「客户该给而没给的」那一问。
|
||||
*/
|
||||
it('expect 只标客户该给的那一格,不标我方写的注', () => {
|
||||
const draft = manifest.declarations.ledger.find((d) => d.object === 'draft');
|
||||
expect(draft?.fields.find((f) => f.name === 'target_price')?.expect).toBe(true);
|
||||
expect(draft?.fields.find((f) => f.name === 'price_note')?.expect).toBeUndefined();
|
||||
});
|
||||
|
||||
it('三条常备案例形状真不同:寻源转来 / 客户压价超限 / 单飞无基准', () => {
|
||||
expect(manifest.samples.length).toBe(3);
|
||||
const [flowIn, overLimit, solo] = manifest.samples;
|
||||
expect((flowIn?.input as { rows?: unknown[] }).rows?.length).toBeGreaterThan(0);
|
||||
expect((overLimit?.input as { rows?: Array<Record<string, number>> }).rows?.[0]?.['target_price']).toBeLessThan(
|
||||
(overLimit?.input as { rows?: Array<Record<string, number>> }).rows?.[0]?.['base_price'] ?? 0,
|
||||
);
|
||||
expect((solo?.input as { rows?: unknown[] }).rows).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('案例模型 —— 双表示同版本', () => {
|
||||
const model = manifest.assets.businessModel;
|
||||
const cmmn = parseCmmnLite(model.cmmnXml);
|
||||
|
||||
it('CMMN 与说明书同在,不允许分叉', () => {
|
||||
expect(model.cmmnXml).toContain('<casePlanModel');
|
||||
expect(model.narrative.length).toBeGreaterThan(400);
|
||||
});
|
||||
|
||||
/* 照 MVP 示例工作流 §1.2 的 S1 取价基准 / S2 逐条定价 / S3 底价自检,一步一项 */
|
||||
it('计划项照 SOP 一步一项:算建议价 + 交人定价 + 底价自检 + 超限拍板 + 放行', () => {
|
||||
expect(items(model.cmmnXml)).toEqual(['S1_price', 'S2_review', 'S3_gate', 'T_approve', 'T_release']);
|
||||
});
|
||||
|
||||
it('三处人的门都标了人工激活:BD 定价 · 超限拍板 · 草案放行(底价自检是算术,不标)', () => {
|
||||
expect(cmmn.manualActivation).toEqual(['S2_review', 'T_approve', 'T_release']);
|
||||
});
|
||||
|
||||
it('不用阶段 / 里程碑 / 裁量项', () => {
|
||||
for (const tag of ['<stage', '<milestone', '<planningTable', '<discretionaryItem']) {
|
||||
expect(model.cmmnXml, `不该出现 ${tag}`).not.toContain(tag);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* **超限才叫人** —— 这个活动的要害:哨兵读基座按值现数的拦截档条数,
|
||||
* 而不是活动自报的 `over_total`;且要等 BD 核过价(他自己回调掉的不必再惊动第二个人)。
|
||||
*/
|
||||
it('超限拍板的哨兵读基座现数,且等人核过价 —— 不是每条都叫人,也不是一条都不叫', () => {
|
||||
const condition = sentryIf(model.cmmnXml, 's_over');
|
||||
expect(condition).toContain('quote_pricing_draft_disposition_block > 0');
|
||||
expect(condition).toContain('confirmed_total');
|
||||
// 有拦截档且人核过价 → 叫人
|
||||
expect(safeEval(condition, { quote_pricing_draft_disposition_block: 1, confirmed_total: 2 })).toBe(true);
|
||||
// 拦截档清零 → 不叫人(这一步整个不入场)
|
||||
expect(safeEval(condition, { quote_pricing_draft_disposition_block: 0, confirmed_total: 2 })).toBe(false);
|
||||
// 人还没核价 → 先别叫(他自己回调掉的那几条不该再惊动一个人)
|
||||
expect(safeEval(condition, { quote_pricing_draft_disposition_block: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
/*
|
||||
* 入场哨兵也归「机检只读基座数」管(第四次违反就藏在哨兵里)——
|
||||
* 上一版 `s_priced` 读活动自报的 `draft_total`:模型不记这个账,门就永不开。
|
||||
*/
|
||||
it('交人核价的入场哨兵读基座行数,不读自报的草案条数', () => {
|
||||
const condition = sentryIf(model.cmmnXml, 's_priced');
|
||||
expect(condition).toContain('quote_pricing_draft_rows');
|
||||
expect(condition).not.toContain('draft_total');
|
||||
expect(safeEval(condition, { line_total: 2, quote_pricing_draft_rows: 2, priced_total: 2 })).toBe(true);
|
||||
// 行落了但算价那一轮还没走完(priced_total 没写过)→ 门不开
|
||||
expect(safeEval(condition, { line_total: 2, quote_pricing_draft_rows: 2 })).toBe(false);
|
||||
});
|
||||
|
||||
it('放行门哨兵带 ifPart:每行有价 + 处置守恒 + 拦截清零 + 定盘记过,全读基座数', () => {
|
||||
const condition = sentryIf(model.cmmnXml, 's_ready');
|
||||
expect(condition).toContain('quote_pricing_draft_missing = 0');
|
||||
expect(condition).toContain('quote_pricing_draft_disposition_block = 0');
|
||||
const done = {
|
||||
route_decided_total: 1,
|
||||
quote_pricing_draft_rows: 3,
|
||||
quote_pricing_draft_missing: 0,
|
||||
quote_pricing_draft_disposition_pass: 2,
|
||||
quote_pricing_draft_disposition_note: 1,
|
||||
quote_pricing_draft_disposition_block: 0,
|
||||
};
|
||||
expect(safeEval(condition, done)).toBe(true);
|
||||
// 还有一行缺拟报价 → 门不开
|
||||
expect(safeEval(condition, { ...done, quote_pricing_draft_missing: 1 })).toBe(false);
|
||||
// 还有条目压在拦截档 → 门不开(哪怕守恒仍成立)
|
||||
expect(
|
||||
safeEval(condition, { ...done, quote_pricing_draft_note: 0, quote_pricing_draft_disposition_note: 0, quote_pricing_draft_disposition_block: 1 }),
|
||||
).toBe(false);
|
||||
// 处置没记齐(守恒不成立)→ 门不开
|
||||
expect(safeEval(condition, { ...done, quote_pricing_draft_disposition_note: 0 })).toBe(false);
|
||||
});
|
||||
|
||||
it('每个计划项都有剧本', () => {
|
||||
const ids = items(model.cmmnXml);
|
||||
expect((model.steps ?? []).map((s) => s.item)).toEqual(ids);
|
||||
for (const step of model.steps ?? []) {
|
||||
expect(step.guide.length, `${step.item} 的做法`).toBeGreaterThan(80);
|
||||
expect(step.intent.length, `${step.item} 的意图`).toBeGreaterThan(6);
|
||||
}
|
||||
});
|
||||
|
||||
it('说明书不点名计划项', () => {
|
||||
const named = [...model.narrative.matchAll(/\b(?:S\d+_[A-Za-z]+|T_[a-z]+)\b/g)].map((m) => m[0]);
|
||||
const ids = new Set(items(model.cmmnXml));
|
||||
for (const name of named) {
|
||||
expect(ids.has(name), `说明书提到的 ${name} 不在案例模型里`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('说明书只讲整单的事:成品、怎么说话、怎么收口', () => {
|
||||
for (const topic of ['开案', '要说话', '一拍连做', '结案']) {
|
||||
expect(model.narrative, topic).toContain(topic);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('声明与契约对得上', () => {
|
||||
const columns = new Set(
|
||||
manifest.declarations.ledger.flatMap((d) => [`${d.domain}.${d.object}`, ...d.fields.map((f) => f.name)]),
|
||||
);
|
||||
|
||||
it('必交付物的台账地址都声明过', () => {
|
||||
for (const d of manifest.contract.requiredDeliverables) {
|
||||
expect(columns.has(d.ledger ?? ''), `${d.key} → ${d.ledger}`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('editableAtRelease 的列都在成果表的声明里', () => {
|
||||
const draft = manifest.declarations.ledger.find((d) => d.object === 'draft');
|
||||
const names = new Set((draft?.fields ?? []).map((f) => f.name));
|
||||
for (const d of manifest.contract.requiredDeliverables) {
|
||||
for (const col of d.editableAtRelease ?? []) {
|
||||
expect(names.has(col), `editableAtRelease 里的 ${col}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const rowReadings = new Set([
|
||||
...manifest.declarations.ledger.map((d) => `${d.domain}_${d.object}_rows`),
|
||||
...manifest.declarations.ledger
|
||||
.filter((d) => d.fields.some((f) => f.required === true))
|
||||
.map((d) => `${d.domain}_${d.object}_missing`),
|
||||
...manifest.declarations.ledger.flatMap((d) =>
|
||||
d.fields.flatMap((f) =>
|
||||
(f.tally ?? []).map((v) => `${d.domain}_${d.object}_${f.name}_${v.toLowerCase().replace(/[^a-z0-9_]/g, '')}`),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
it('L1/L2 谓词读的名字都有承载(台账列或基座读数)', () => {
|
||||
for (const c of manifest.contract.acceptanceCriteria) {
|
||||
if (c.level !== 'L1' && c.level !== 'L2') continue;
|
||||
for (const identifier of (c.predicate ?? '').match(/[a-z_][a-z0-9_]*/g) ?? []) {
|
||||
if (['and', 'or', 'not', 'true', 'false', 'null', 'in', 'between'].includes(identifier)) continue;
|
||||
if (rowReadings.has(identifier)) continue;
|
||||
expect(columns.has(identifier), `${c.key} 的谓词引用了 ${identifier}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* **机检必须至少有一边是基座数**(违反史已记四次,每次都是同一个死法:
|
||||
* 那个数要模型自己重算,漏算/改小一次门就废了)。
|
||||
*/
|
||||
it('每条 L1/L2 都至少读一个基座读数 —— 不许两边都由活动自报', () => {
|
||||
for (const c of manifest.contract.acceptanceCriteria) {
|
||||
if (c.level !== 'L1' && c.level !== 'L2') continue;
|
||||
const names = (c.predicate ?? '').match(/[a-z_][a-z0-9_]*/g) ?? [];
|
||||
expect(names.some((n) => rowReadings.has(n)), `${c.key} 一个基座读数都没读`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('出口载荷的标量字段都有台账列承载', () => {
|
||||
const snake = (name: string) => name.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
|
||||
for (const port of manifest.contract.output.mandatory) {
|
||||
const properties = (port.payload as { properties?: Record<string, { type?: string }> }).properties ?? {};
|
||||
for (const [field, spec] of Object.entries(properties)) {
|
||||
if (spec.type === 'array') continue;
|
||||
expect(columns.has(snake(field)), `${port.name}.${field} → ${snake(field)}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('技能引用的工具都在白名单声明内 —— 载方法不载权力', () => {
|
||||
const declared = new Set(manifest.declarations.tools.map((t) => t.name));
|
||||
for (const skill of manifest.assets.skills) {
|
||||
for (const fragment of skill.crystallized) {
|
||||
expect(declared.has(fragment.tool), `${skill.id} 用了 ${fragment.tool}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('L3 判据的整句出现在放行剧本里 —— judge 才对得上名', () => {
|
||||
const l3 = manifest.contract.acceptanceCriteria.find((c) => c.level === 'L3');
|
||||
const release = manifest.assets.businessModel.steps?.find((s) => s.item === 'T_release');
|
||||
expect(l3).toBeDefined();
|
||||
expect(release?.guide, 'T_release 的 guide 要整句抄 L3 判据名').toContain(l3?.label ?? '');
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* 两把尺逐行求值 —— 能算的不问 LLM(规范 §5.6)。
|
||||
* 出参就是台账 `disposition` 的取值:模型算完照抄,不必再翻译一道。
|
||||
*/
|
||||
describe('利润模型 P-01 —— 客户等级 × 类别 → 毛利档', () => {
|
||||
const table = P01_DMN.tables.find((t) => t.id === 'p01-margin');
|
||||
|
||||
it('战略客户 + A 类 → 8%~10%(量换价那一档)', () => {
|
||||
expect(decideBy(table, { customer_grade: '战略客户', type_tag: 'A' })).toMatchObject({ outputs: [0.08, 0.1] });
|
||||
});
|
||||
|
||||
it('普通客户 + A 类 → 12%~15%', () => {
|
||||
expect(decideBy(table, { customer_grade: '普通客户', type_tag: 'A' })).toMatchObject({ rule: 'p01-2' });
|
||||
});
|
||||
|
||||
it('B 类不看客户等级 → 15%~20%(战略客户的 B 类也走这一档)', () => {
|
||||
expect(decideBy(table, { customer_grade: '战略客户', type_tag: 'B' })).toMatchObject({ outputs: [0.15, 0.2] });
|
||||
});
|
||||
|
||||
it('C 类替代/停产件 → 20%~50% 一事一议', () => {
|
||||
expect(decideBy(table, { customer_grade: '普通客户', type_tag: 'C' })).toMatchObject({ rule: 'p01-4' });
|
||||
});
|
||||
|
||||
it('等级与类别都认不出 → 兜底按普通客户标准件档,不是拒绝求值', () => {
|
||||
expect(decideBy(table, {})).toMatchObject({ rule: 'p01-default', outputs: [0.12, 0.15] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('底价门禁 P-02 —— 偏离底价线两成即止', () => {
|
||||
const table = P01_DMN.tables.find((t) => t.id === 'p02-floor');
|
||||
|
||||
it('在底价线之上 → PASS 直接放行', () => {
|
||||
expect(decideBy(table, { deviation: 0.12 })).toMatchObject({ outputs: ['PASS', true] });
|
||||
expect(decideBy(table, { deviation: 0 })).toMatchObject({ outputs: ['PASS', true] });
|
||||
});
|
||||
|
||||
it('低于底价线两成以内 → NOTE 附理由放行', () => {
|
||||
expect(decideBy(table, { deviation: -0.08 })).toMatchObject({ outputs: ['NOTE', true] });
|
||||
});
|
||||
|
||||
it('低于两成以上 → BLOCK 拦截,人拍板', () => {
|
||||
expect(decideBy(table, { deviation: -0.25 })).toMatchObject({ outputs: ['BLOCK', false] });
|
||||
});
|
||||
|
||||
it('三个处置码与台账取值域逐字一致 —— 模型照抄即可写回', () => {
|
||||
const tally = manifest.declarations.ledger
|
||||
.find((d) => d.object === 'draft')
|
||||
?.fields.find((f) => f.name === 'disposition')?.tally;
|
||||
expect(new Set(table?.rules.map((r) => String(r.then[0])))).toEqual(new Set(tally));
|
||||
});
|
||||
});
|
||||
|
||||
describe('两把尺的档位三处同一份 —— 判定表 / 剧本 / 工作单', () => {
|
||||
const guide = manifest.assets.businessModel.steps?.find((s) => s.item === 'S1_price')?.guide ?? '';
|
||||
const sheet = manifest.assets.viewTemplates.find((t) => t.name === 'pricing-worksheet')?.html ?? '';
|
||||
|
||||
it('毛利四档三处都写着同样的数', () => {
|
||||
for (const band of ['8%~10%', '12%~15%', '15%~20%', '20%~50%']) {
|
||||
expect(guide, `剧本少了 ${band}`).toContain(band);
|
||||
expect(sheet, `工作单少了 ${band}`).toContain(band);
|
||||
}
|
||||
});
|
||||
|
||||
/* 门禁在人定完价之后才算得出来,所以三个码写死在底价自检那一步的 guide 里 */
|
||||
it('三个处置码剧本里写死(大写,一个字母不许差)', () => {
|
||||
const gate = manifest.assets.businessModel.steps?.find((s) => s.item === 'S3_gate')?.guide ?? '';
|
||||
for (const code of ['PASS', 'NOTE', 'BLOCK']) expect(gate).toContain(code);
|
||||
});
|
||||
});
|
||||
|
||||
describe('报告模版 —— 活动持有,容器无脚本执行权', () => {
|
||||
it('两个局面各一张:定价工作单(干活)与报价草案清单(收口)', () => {
|
||||
expect(manifest.assets.viewTemplates.map((t) => t.name)).toEqual(['pricing-worksheet', 'pricing-roster']);
|
||||
});
|
||||
|
||||
it('剧本每一步的 view 都在模版名单里 —— 换不到画面屏上就不动', () => {
|
||||
const names = new Set(manifest.assets.viewTemplates.map((t) => t.name));
|
||||
for (const step of manifest.assets.businessModel.steps ?? []) {
|
||||
expect(names.has(step.view ?? ''), `${step.item} 的 view=${step.view}`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('放行那一步摆的是验收目标件 —— 最后摆上去的那张就是归档件', () => {
|
||||
const release = manifest.assets.businessModel.steps?.find((s) => s.item === 'T_release');
|
||||
expect(release?.view).toBe('pricing-roster');
|
||||
});
|
||||
|
||||
it('模版里没有脚本、没有事件处理器、没有外链 —— 样式自带', () => {
|
||||
for (const template of manifest.assets.viewTemplates) {
|
||||
expect(template.html, template.name).not.toMatch(/<script|\son[a-z]+=|https?:\/\//i);
|
||||
expect(template.html, template.name).toContain('<style>');
|
||||
}
|
||||
});
|
||||
|
||||
it('定价工作单是干活的件:逐颗档案 + 两把尺 + 填回工作表', () => {
|
||||
const sheet = manifest.assets.viewTemplates.find((t) => t.name === 'pricing-worksheet')?.html ?? '';
|
||||
expect(sheet).toContain('jc-selcard');
|
||||
expect(sheet).toContain('底价线');
|
||||
expect(sheet).toContain('利润模型 P-01');
|
||||
expect(sheet).toContain('底价门禁 P-02');
|
||||
expect(sheet).toContain('data-edit="quote_pricing.draft"');
|
||||
});
|
||||
|
||||
it('缺价红带与提交闸同源 —— 件上摆的是基座的 `_missing` 与主键名单', () => {
|
||||
for (const name of ['pricing-worksheet', 'pricing-roster']) {
|
||||
const html = manifest.assets.viewTemplates.find((t) => t.name === name)?.html ?? '';
|
||||
expect(html, name).toContain('quote_pricing_draft_missing');
|
||||
expect(html, name).toContain('quote_pricing_draft_missing_keys');
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* 编辑面精确等于 BD 的职权面(定价与让价理由)+ **行的钥匙 pn**:
|
||||
* 主键格不挂 `data-col`,服务端按「全表一致列」补键,两行会对到同一行上
|
||||
* (A2 浏览器走查逮过,e2e 走 API 自带主键测不出来)。
|
||||
*/
|
||||
it('编辑面 = 拟报单价 + 让价理由 + 行的钥匙 pn', () => {
|
||||
for (const name of ['pricing-worksheet', 'pricing-roster']) {
|
||||
const html = manifest.assets.viewTemplates.find((t) => t.name === name)?.html ?? '';
|
||||
const cols = [...html.matchAll(/data-col="([^"]+)"/g)].map((m) => m[1]);
|
||||
expect(new Set(cols), name).toEqual(new Set(['pn', 'quoted_price', 'price_note']));
|
||||
}
|
||||
});
|
||||
|
||||
it('门禁分布读的是基座按值现数的三段 —— 守恒等式的可视形', () => {
|
||||
const roster = manifest.assets.viewTemplates.find((t) => t.name === 'pricing-roster')?.html ?? '';
|
||||
for (const code of ['pass', 'note', 'block']) {
|
||||
expect(roster).toContain(`quote_pricing_draft_disposition_${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* A4 只有一个出口(E8 无条件边),**没有去向要判** —— 件上不摆判定表,
|
||||
* 也不许残留 `case:route*` 的空壳(没有选择组它永远是空的,一屏灰行像判定没跑)。
|
||||
*/
|
||||
it('单出口活动:没有选择组,件上也不摆去向判定', () => {
|
||||
expect(manifest.contract.output.selective).toEqual([]);
|
||||
const roster = manifest.assets.viewTemplates.find((t) => t.name === 'pricing-roster')?.html ?? '';
|
||||
expect(roster).not.toContain('case:routeKey');
|
||||
});
|
||||
|
||||
it('模版只做取值 —— data-class-of 不以 data-slot 为前提(纯色块曾整条静默失色)', () => {
|
||||
const roster = manifest.assets.viewTemplates.find((t) => t.name === 'pricing-roster')?.html ?? '';
|
||||
expect(roster).toContain('<i data-class-of="disposition"></i>');
|
||||
});
|
||||
|
||||
it('宿主拒推含脚本的模版', async () => {
|
||||
const host = createMockHost({ contract: CONTRACT });
|
||||
await expect(host.base.interaction.pushHtml('<div><script>x()</script></div>')).rejects.toThrow(/无脚本执行权/);
|
||||
});
|
||||
});
|
||||
|
||||
const CONTRACT = {
|
||||
activityId: 'quote-pricing',
|
||||
version: '1.0.0',
|
||||
data: { rfqRef: 'RFQ MD14.1', rows: [] },
|
||||
message: undefined,
|
||||
outputPorts: { mandatory: ['draft'], selective: [] },
|
||||
prohibitions: [],
|
||||
discretion: [],
|
||||
} as const;
|
||||
|
||||
describe('代码入口 —— 案例活动没有 run,不是残包', () => {
|
||||
it('默认导出是一个有名字的活动定义', () => {
|
||||
expect(activity.name).toBe('quote-pricing');
|
||||
});
|
||||
|
||||
it('mock 宿主如实报「没有 run」,不静默返回一个假的 outcome', async () => {
|
||||
const host = createMockHost({ contract: CONTRACT });
|
||||
await expect(host.run(activity)).rejects.toThrow(/没有 run/);
|
||||
});
|
||||
});
|
||||
|
||||
/** 按 DMN 语义逐行求值:FIRST 命中即定,单元格谓词复用 @sfeel */
|
||||
function decideBy(
|
||||
table: (typeof P01_DMN)['tables'][number] | undefined,
|
||||
scope: Record<string, unknown>,
|
||||
): { outputs: readonly unknown[]; rule: string } | undefined {
|
||||
if (table === undefined) return undefined;
|
||||
for (const rule of table.rules) {
|
||||
const hit = table.inputs.every((input, i) => evaluateUnaryTest(rule.when[i] ?? '-', scope[input.expression], scope));
|
||||
if (hit) return { outputs: rule.then, rule: rule.id };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function items(xml: string): string[] {
|
||||
return [...xml.matchAll(/<(?:humanTask|decisionTask|processTask|caseTask) id="([^"]+)"/g)].map((m) => m[1] ?? '');
|
||||
}
|
||||
|
||||
function parseCmmnLite(xml: string): { manualActivation: string[]; repetition: string[] } {
|
||||
const manualActivation: string[] = [];
|
||||
const repetition: string[] = [];
|
||||
for (const m of xml.matchAll(/<humanTask id="([^"]+)"[\s\S]*?<\/humanTask>/g)) {
|
||||
if (m[0].includes('manualActivationRule')) manualActivation.push(m[1] ?? '');
|
||||
if (m[0].includes('repetitionRule')) repetition.push(m[1] ?? '');
|
||||
}
|
||||
return { manualActivation, repetition };
|
||||
}
|
||||
|
||||
function sentryIf(xml: string, sentryId: string): string {
|
||||
const block = xml.slice(xml.indexOf(`<sentry id="${sentryId}"`));
|
||||
const raw = /<condition>([\s\S]*?)<\/condition>/.exec(block.slice(0, block.indexOf('</sentry>')))?.[1] ?? '';
|
||||
return raw.replaceAll('>', '>').replaceAll('<', '<').replaceAll('&', '&');
|
||||
}
|
||||
|
||||
function safeEval(expression: string, scope: Record<string, unknown>): boolean {
|
||||
try {
|
||||
return evaluateCondition(expression, scope);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* 步骤剧本 —— **这段错了不报错,只是活动开始转圈**(A1 的账)。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { evaluateCondition } from '@sfeel';
|
||||
import { CMMN_XML, PRICING_PHASES, PRICING_STEPS } from '../src/case-model.ts';
|
||||
import { manifest } from '../src/manifest.ts';
|
||||
|
||||
const ITEM_IDS = [...CMMN_XML.matchAll(/<(?:humanTask|decisionTask|stage|milestone)\s+id="([^"]+)"/g)].map(
|
||||
(m) => m[1] ?? '',
|
||||
);
|
||||
|
||||
describe('剧本与案例模型对得上 —— 对不上的条目等于没写', () => {
|
||||
it('每一条剧本都指向一个真实存在的计划项', () => {
|
||||
for (const step of PRICING_STEPS) {
|
||||
expect(ITEM_IDS, `剧本里的 ${step.item} 在 CMMN 里不存在`).toContain(step.item);
|
||||
}
|
||||
});
|
||||
|
||||
it('图上每一步都有剧本', () => {
|
||||
for (const id of ITEM_IDS) {
|
||||
expect(PRICING_STEPS.map((s) => s.item), `计划项 ${id} 没有剧本`).toContain(id);
|
||||
}
|
||||
});
|
||||
|
||||
it('清单里挂的就是这份剧本(不是另抄一份)', () => {
|
||||
expect(manifest.assets.businessModel.steps).toBe(PRICING_STEPS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('doneWhen 是「成果已存在」的判据,不是「该不该做」的判据', () => {
|
||||
/** 开案那一刻的三种局面:寻源转来 / 受理直达 / 单飞 —— 台账一行没有 */
|
||||
const FRESH_SOURCED: Record<string, unknown> = {
|
||||
rfqRef: 'RFQ MD14.1',
|
||||
rows: [{ pn: 'BBPY00201209601Y00', type_tag: 'B', base_price: 0.058 }],
|
||||
};
|
||||
const FRESH_INTAKE: Record<string, unknown> = {
|
||||
rfqRef: 'RFQ-2026-0555',
|
||||
records: [{ pn: 'PESD2ETH-XR', type_tag: 'B' }],
|
||||
};
|
||||
const FRESH_SOLO: Record<string, unknown> = { subject: '两颗料报个价', body: 'XCJ-7788T 8K……' };
|
||||
|
||||
it('**开局一步都不该被自动收掉** —— 三种开局都要 false', () => {
|
||||
for (const fresh of [FRESH_SOURCED, FRESH_INTAKE, FRESH_SOLO]) {
|
||||
for (const step of PRICING_STEPS) {
|
||||
if (step.doneWhen === undefined) continue;
|
||||
expect(
|
||||
safeEval(step.doneWhen, fresh),
|
||||
`${step.item} 的 doneWhen「${step.doneWhen}」开局就为真`,
|
||||
).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* ── 算价那一步 ─────────────────────────────────────────────── */
|
||||
|
||||
const priced = (over: Record<string, unknown>): Record<string, unknown> => ({
|
||||
line_total: 2,
|
||||
quote_pricing_draft_rows: 2,
|
||||
priced_total: 2,
|
||||
...over,
|
||||
});
|
||||
|
||||
it('整份落表、行数对上、算价那一轮走完 → 收得掉', () => {
|
||||
expect(safeEval(doneOf('S1_price'), priced({}))).toBe(true);
|
||||
});
|
||||
|
||||
it('**全是无基准的单子也收得掉** —— 一条都算不出时 priced_total 是写过的 0', () => {
|
||||
expect(safeEval(doneOf('S1_price'), priced({ priced_total: 0 }))).toBe(true);
|
||||
});
|
||||
|
||||
it('**第一拍写完行就收步是不许的** —— priced_total 没写过,未绑定的名字比大小为假', () => {
|
||||
const scope = priced({});
|
||||
delete scope['priced_total'];
|
||||
expect(safeEval(doneOf('S1_price'), scope)).toBe(false);
|
||||
});
|
||||
|
||||
it('行数两边对不上 → 收不掉(漏落一行当场现形,不等到第 10 拍)', () => {
|
||||
expect(safeEval(doneOf('S1_price'), priced({ quote_pricing_draft_rows: 1 }))).toBe(false);
|
||||
});
|
||||
|
||||
/* ── 三处不给 doneWhen ──────────────────────────────────────── */
|
||||
|
||||
it('**交人定价、超限拍板、放行三项都不给 doneWhen** —— 给了等于给引擎一把替人签字的钥匙', () => {
|
||||
for (const item of ['S2_review', 'T_approve', 'T_release']) {
|
||||
expect(PRICING_STEPS.find((s) => s.item === item)?.doneWhen, `${item} 不该有 doneWhen`).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
/* ── 底价自检:判完了才算办完(收步判据与验收 L2 同源)────────────── */
|
||||
|
||||
const gated = (over: Record<string, unknown>): Record<string, unknown> => ({
|
||||
quote_pricing_draft_rows: 3,
|
||||
quote_pricing_draft_disposition_pass: 2,
|
||||
quote_pricing_draft_disposition_note: 1,
|
||||
quote_pricing_draft_disposition_block: 0,
|
||||
...over,
|
||||
});
|
||||
|
||||
it('三档处置的条数加起来等于行数 → 底价自检收得掉', () => {
|
||||
expect(safeEval(doneOf('S3_gate'), gated({}))).toBe(true);
|
||||
});
|
||||
|
||||
it('**有拦截档也照样收得掉** —— 判出来了就是判完了,拦截档由下一步叫人拍板', () => {
|
||||
expect(
|
||||
safeEval(doneOf('S3_gate'), gated({ quote_pricing_draft_disposition_note: 0, quote_pricing_draft_disposition_block: 1 })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('还有行没判 → 收不掉(松出来的那一截就是放行门前的返工)', () => {
|
||||
expect(safeEval(doneOf('S3_gate'), gated({ quote_pricing_draft_disposition_note: 0 }))).toBe(false);
|
||||
});
|
||||
|
||||
it('**一行都还没写时 0+0+0 = 0 不算判完** —— 全零相等那一坑,靠 rows > 0 挡住', () => {
|
||||
expect(
|
||||
safeEval(doneOf('S3_gate'), {
|
||||
quote_pricing_draft_rows: 0,
|
||||
quote_pricing_draft_disposition_pass: 0,
|
||||
quote_pricing_draft_disposition_note: 0,
|
||||
quote_pricing_draft_disposition_block: 0,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('归一那一步声明了 reads: rows 与 records —— 上游载荷置前不截断', () => {
|
||||
const reads = PRICING_STEPS.find((s) => s.item === 'S1_price')?.reads ?? [];
|
||||
expect(reads).toContain('rows');
|
||||
expect(reads).toContain('records');
|
||||
});
|
||||
|
||||
/*
|
||||
* **一单同一张卡只发一次**,且缺信息不再走补全卡(2026-08-02 全局退役)——
|
||||
* 剧本里不许残留「还缺就再发一张卡」这条已经不存在的路。
|
||||
*/
|
||||
it('剧本不教补全卡,也不留「还缺就再发一张」的死路', () => {
|
||||
for (const step of PRICING_STEPS) {
|
||||
expect(step.guide, `${step.item} 的 guide 提到了补全卡`).not.toMatch(/补全卡|"kind":"ask"/);
|
||||
expect(step.guide, `${step.item} 的 guide 教了再发一张卡`).not.toMatch(/再发一张卡|重新发一张卡/);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* **一单只发一张卡**:人点完之后这一步会被重新念一遍,而 guide 的头一段说什么,
|
||||
* 模型就先做什么 —— 头一段若是「发一张决策卡」,它就再发一张(实测连发四张,
|
||||
* 调人预算烧光 exhausted)。所以拍板那一步的 guide 必须**先分岔再发卡**。
|
||||
*/
|
||||
it('拍板那一步先问「人点过没有」,再谈发卡 —— 分岔要排在发卡骨架前面', () => {
|
||||
const guide = PRICING_STEPS.find((s) => s.item === 'T_approve')?.guide ?? '';
|
||||
const branch = guide.indexOf('已经有');
|
||||
const skeleton = guide.indexOf('【发一张决策卡');
|
||||
expect(branch, 'T_approve 的 guide 没有「人点过没有」这一岔').toBeGreaterThanOrEqual(0);
|
||||
expect(skeleton, 'T_approve 的 guide 没有发卡骨架').toBeGreaterThan(branch);
|
||||
expect(guide).toMatch(/不许再发|别再发第二张卡/);
|
||||
});
|
||||
|
||||
/*
|
||||
* start 墙已拆(2026-08-02):动它就算启动。剧本里再教 start 只是白烧 token,
|
||||
* 而且学不稳的礼仪就是死法。
|
||||
*/
|
||||
it('剧本不教 start 礼仪 —— 那道墙已经拆了(说「不必单发 start」可以,教它先按一下不行)', () => {
|
||||
for (const step of PRICING_STEPS) {
|
||||
expect(step.guide, `${step.item} 的 guide 还在教 start`).not.toMatch(/先\s*start|务必\s*start|要先发\s*start/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('剧本声明的东西活动真有', () => {
|
||||
const declaredLedger = manifest.declarations.ledger.map((l) => `${l.domain}.${l.object}`);
|
||||
const declaredTools = manifest.declarations.tools.map((t) => t.name);
|
||||
|
||||
it('每一步要写的台账都在清单的台账声明里', () => {
|
||||
for (const step of PRICING_STEPS) {
|
||||
for (const address of step.writes) {
|
||||
expect(declaredLedger, `${step.item} 要写 ${address}`).toContain(address);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('每一步要调的工具都在清单的工具声明里', () => {
|
||||
for (const step of PRICING_STEPS) {
|
||||
for (const tool of step.tools) {
|
||||
expect(declaredTools, `${step.item} 要调 ${tool}`).toContain(tool);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('每一步都定死了摆哪张件,且模版真在包里', () => {
|
||||
const names = new Set(manifest.assets.viewTemplates.map((t) => t.name));
|
||||
for (const step of PRICING_STEPS) {
|
||||
expect(step.view, `${step.item} 没定画面`).toBeDefined();
|
||||
expect(names.has(step.view ?? ''), `${step.item} 的 view「${step.view}」不在模版名单里`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('每一步都说得出它交出什么', () => {
|
||||
for (const step of PRICING_STEPS) {
|
||||
expect(step.intent.trim(), `${step.item} 没写 intent`).not.toBe('');
|
||||
expect(step.guide.trim().length, `${step.item} 的 guide 太短`).toBeGreaterThan(20);
|
||||
}
|
||||
});
|
||||
|
||||
/* 卡面是纯文本:星号记号不会变粗体,只会原样摆在人眼前(规范 §5.5-10) */
|
||||
it('发卡骨架里不写星号记号 —— 卡面是纯文本', () => {
|
||||
for (const step of PRICING_STEPS) {
|
||||
for (const line of step.guide.split('\n')) {
|
||||
if (!/"steps"|"standard"|"ask"|"label"|"consequence"/.test(line)) continue;
|
||||
expect(line.includes('**'), `${step.item} 的卡面文案带了星号:${line.slice(0, 60)}`).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('阶段刻度:每一项都有格可待', () => {
|
||||
const inPhase = new Set(PRICING_PHASES.flatMap((p) => p.items));
|
||||
|
||||
it('**除放行外每一项恰好属于一个阶段**', () => {
|
||||
const planned = PRICING_STEPS.map((s) => s.item).filter((id) => id !== 'T_release');
|
||||
expect(planned.filter((id) => !inPhase.has(id))).toEqual([]);
|
||||
});
|
||||
|
||||
it('一项不进两个阶段', () => {
|
||||
const all = PRICING_PHASES.flatMap((p) => p.items);
|
||||
expect(all.length).toBe(new Set(all).size);
|
||||
});
|
||||
|
||||
it('**放行不在名单里** —— 收口那一格由基座统一加', () => {
|
||||
expect(inPhase.has('T_release')).toBe(false);
|
||||
});
|
||||
|
||||
it('阶段里不许出现案例模型没有的项', () => {
|
||||
const ids = new Set(PRICING_STEPS.map((s) => s.item));
|
||||
expect([...inPhase].filter((id) => !ids.has(id))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function doneOf(item: string): string {
|
||||
const found = PRICING_STEPS.find((s) => s.item === item)?.doneWhen;
|
||||
if (found === undefined) throw new Error(`${item} 没有 doneWhen`);
|
||||
return found;
|
||||
}
|
||||
|
||||
function safeEval(expression: string, scope: Record<string, unknown>): boolean {
|
||||
try {
|
||||
return evaluateCondition(expression, scope);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,137 +1,387 @@
|
||||
/**
|
||||
* A4 成本定价的案例模型 —— **算价归判定表,超限那一下归人**(2026-07-31 照 A1 的状态机模式重做)。
|
||||
* A4 成本定价的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。
|
||||
*
|
||||
* 与 A2/A3 的分工:那两个是「一次专家判断」,派一张执行单收回结论就够了;
|
||||
* 定价不同 —— 它**有确定性规则可依**(P-01 利润模型、P-02 底价门禁两支 DMN),
|
||||
* 算价这件事该算不该问。所以它照 A1 的样子分步建模,而不是甩一张单给人。
|
||||
* 业务出处:`docs/MVP示例工作流.html` §1.2(S1 取价基准 → S2 逐条定价 → S3 底价自检),
|
||||
* 按 A3 同一条形状建模(2026-08-03 重写,@3.0.0):
|
||||
*
|
||||
* 三项:
|
||||
* T_price —— 逐条算价并落草案(拟报单价 / 毛利 / 偏离度 / 门禁处置);
|
||||
* T_approve —— **只在门禁判超限时入场**:触达客户利益的一下,人工激活,派决策卡;
|
||||
* T_release —— 放行(人工激活)。
|
||||
* 上游(A3 的目标报价清单 / A2 的询价受理清单 / A1 直达的受理记录)进来 → 归一成
|
||||
* **报价草案**底表,逐条按 P-01 利润模型算出底价线与拟报单价、过 P-02 底价门禁 →
|
||||
* **一张执行单**交 BD 核价(拟报单价可校可改,无基准的那几行由他定价)→
|
||||
* **只有压在拦截档上的那几条才叫人拍板**(决策卡)→ 放行,整份报价草案交定价审批。
|
||||
*
|
||||
* 「超限才叫人」是这个活动的要害:每条都叫人,人就成了橡皮图章;一条都不叫,
|
||||
* 底价门禁就是摆设。所以那一项挂前提(`s_over`),由判定表的结论决定它入不入场。
|
||||
* 上一版(@2.0.0)三处不合现行口径,这一版整个置换:
|
||||
* ① 入参必填 `rfqRef + customerCode + records` —— 而 A3 发的是 `{rfqRef, rows}`,
|
||||
* 门口拦的恰是主路(现在宽进到底,归一在活动内做);
|
||||
* ② 台账一个 `required` 都没有 —— 执行卡的闸没牙,人不填也能交卷;
|
||||
* ③ 入场哨兵 `s_priced` 读活动自报的 `draft_total`(「机检只读基座数」第 5 类违反)——
|
||||
* 模型不记这个账,门就永不开,而它一步也没做错。
|
||||
*/
|
||||
|
||||
export const CMMN_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL">
|
||||
<case id="pricing">
|
||||
<!--
|
||||
案卷项的 id **就是任务视图里的键名**:
|
||||
draft_total 是草案条数(算完没算完),over_total 是超限条数(要不要叫人)。
|
||||
两个都由 T_price 写进台账,摊平到顶层后驱动前提。
|
||||
案卷项的 id **就是任务视图里的键名**。上游载荷(rows / records)与本活动台账列
|
||||
都摊平到任务视图顶层,照它们命名;quote_pricing_* 那几个是**基座读数**
|
||||
(行数 / 缺必填 / 按值现数),由基座现算,模型碰不着。
|
||||
-->
|
||||
<caseFileModel>
|
||||
<caseFileItem id="pn" name="型号(开案即有)"/>
|
||||
<caseFileItem id="draft_total" name="草案条数"/>
|
||||
<caseFileItem id="over_total" name="超限条数"/>
|
||||
<caseFileItem id="rows" name="上游清单行(寻源/选型转来,流内开案即有)"/>
|
||||
<caseFileItem id="records" name="上游受理记录(受理直达时有)"/>
|
||||
<caseFileItem id="line_total" name="草案条目数(自报)"/>
|
||||
<caseFileItem id="priced_total" name="已算出拟报价的条数"/>
|
||||
<caseFileItem id="confirmed_total" name="BD 已核价条数(人环走完的凭据)"/>
|
||||
<caseFileItem id="route_decided_total" name="已定盘"/>
|
||||
<caseFileItem id="quote_pricing_draft_rows" name="草案行数(基座现数)"/>
|
||||
<caseFileItem id="quote_pricing_draft_missing" name="缺拟报单价的行数(基座现数)"/>
|
||||
<caseFileItem id="quote_pricing_draft_disposition_block" name="压在拦截档的条数(基座现数)"/>
|
||||
</caseFileModel>
|
||||
<casePlanModel id="plan" name="成本定价" autoComplete="true">
|
||||
<!--
|
||||
① 算完了 —— 交 BD 核价那一步的入场前提。
|
||||
行数两边对上(自报 = 基座现数),且算价那一轮走完(priced_total 是算完之后
|
||||
显式写下的数;「大于等于 0」对「写过的 0」为真、对「没写过」为假)。
|
||||
-->
|
||||
<sentry id="s_priced">
|
||||
<onPart id="op_pr" caseFileItemRef="draft_total"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_pr"><condition>draft_total > 0</condition></ifPart>
|
||||
<onPart id="op_pr" caseFileItemRef="priced_total"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_pr"><condition>line_total > 0 and quote_pricing_draft_rows = line_total and priced_total >= 0</condition></ifPart>
|
||||
</sentry>
|
||||
<!-- 超限才叫人:门禁判出有超限条目,这一项才入场 -->
|
||||
<!--
|
||||
② 人定完价了 —— 底价自检那一步的入场前提。
|
||||
confirmed_total 只在人交回执行单之后才产生,它就是「经过人了」的凭据。
|
||||
-->
|
||||
<sentry id="s_confirmed">
|
||||
<onPart id="op_cf" caseFileItemRef="confirmed_total"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_cf"><condition>confirmed_total > 0</condition></ifPart>
|
||||
</sentry>
|
||||
<!--
|
||||
③ **超限才叫人** —— 这个活动的要害。
|
||||
「压在拦截档上的条数」由基座按 disposition 的取值域现数(不是活动自报),
|
||||
且要等 BD 核过价(confirmed_total 只在人交回执行单之后才产生)——
|
||||
他自己回调掉的那几条不必再惊动第二个人。
|
||||
-->
|
||||
<sentry id="s_over">
|
||||
<onPart id="op_ov" caseFileItemRef="over_total"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_ov"><condition>over_total > 0</condition></ifPart>
|
||||
<onPart id="op_ov" caseFileItemRef="quote_pricing_draft_disposition_block"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_ov"><condition>quote_pricing_draft_disposition_block > 0 and confirmed_total > 0</condition></ifPart>
|
||||
</sentry>
|
||||
<!--
|
||||
── 放行门的入场条件:**认「办完了没有」,且读基座数**────────────────
|
||||
· 每行都有拟报单价(quote_pricing_draft_missing 由基座按 required 现数,
|
||||
与执行卡的提交闸、验收 L1 同一个数同一套算法);
|
||||
· 门禁处置逐条落定且守恒(三档相加 = 行数,两边都是基座按值现数);
|
||||
· **没有还压在拦截档上的条目**(要么回调、要么经人拍板改记附理由放行);
|
||||
· 定盘过了(route_decided_total 是定盘那一步显式写下的记账数)。
|
||||
-->
|
||||
<sentry id="s_ready">
|
||||
<onPart id="op_rd" caseFileItemRef="route_decided_total"><standardEvent>update</standardEvent></onPart>
|
||||
<ifPart id="if_rd"><condition>route_decided_total > 0 and quote_pricing_draft_rows > 0 and quote_pricing_draft_missing = 0 and quote_pricing_draft_disposition_block = 0 and quote_pricing_draft_disposition_pass + quote_pricing_draft_disposition_note + quote_pricing_draft_disposition_block = quote_pricing_draft_rows</condition></ifPart>
|
||||
</sentry>
|
||||
|
||||
<!-- 算价:判定表说了算,不是临场判 -->
|
||||
<humanTask id="T_price" name="逐条算价:取基准、算毛利与偏离度、过底价门禁">
|
||||
<itemControl><repetitionRule id="r_pr"/></itemControl>
|
||||
<!-- ① 归一草案 + 取基准 + 逐条算价过门禁(能算的不问人) -->
|
||||
<humanTask id="S1_price" name="归一报价草案:取成本基准、按利润模型算底价线与建议价">
|
||||
<itemControl><repetitionRule id="r_s1"/></itemControl>
|
||||
</humanTask>
|
||||
|
||||
<!-- 超限审批:触达客户利益的一下,必须人工激活 -->
|
||||
<humanTask id="T_approve" name="超限条目决策:低于底价的那几条要人拍板">
|
||||
<!--
|
||||
② 交 BD 核价 —— **一张执行单**(拟报价可校可改,无基准的行由他定价)。
|
||||
manualActivation + 不给 doneWhen(发卡触达真人,引擎不替人按)。
|
||||
-->
|
||||
<humanTask id="S2_review" name="交BD定价:工作单上逐条定价,收回后过底价门禁">
|
||||
<entryCriterion id="ec_s2" sentryRef="s_priced"/>
|
||||
<itemControl><manualActivationRule id="m_s2"/></itemControl>
|
||||
</humanTask>
|
||||
|
||||
<!--
|
||||
③ 底价自检 —— SOP 里的 S3,一件独立的事:按人定的价算毛利、偏离度、门禁处置。
|
||||
它不触达世界,所以给 doneWhen(三档处置的条数加起来等于行数就算判完了),
|
||||
引擎自己收 —— 从前这一段藏在「交人核价」的收尾里,模型写完 confirmed_total
|
||||
就把那一项收了,三列处置一格没写,放行门当场判「处置没落定」,而拒回只会说
|
||||
「数不对/行不对」—— 两扇打不开的门(实测 8 次拒回熔断)。
|
||||
-->
|
||||
<humanTask id="S3_gate" name="底价自检:按定下的价算毛利与偏离度,逐条过底价门禁">
|
||||
<entryCriterion id="ec_s3" sentryRef="s_confirmed"/>
|
||||
<itemControl><repetitionRule id="r_s3"/></itemControl>
|
||||
</humanTask>
|
||||
|
||||
<!-- ④ 超限拍板:触达客户利益的那一下,必须人工激活;只在拦截档还有条目时入场 -->
|
||||
<humanTask id="T_approve" name="超限拍板:压在拦截档的条目要人定怎么办">
|
||||
<entryCriterion id="ec_ap" sentryRef="s_over"/>
|
||||
<itemControl><manualActivationRule id="m_ap"/><repetitionRule id="r_ap"/></itemControl>
|
||||
</humanTask>
|
||||
|
||||
<!-- 放行:触达世界,必须人工激活 -->
|
||||
<!-- 草案放行:终局唯放行 —— 触达世界,必须人工激活 -->
|
||||
<humanTask id="T_release" name="报价草案验收放行">
|
||||
<entryCriterion id="ec_rel" sentryRef="s_priced"/>
|
||||
<entryCriterion id="ec_rel" sentryRef="s_ready"/>
|
||||
<itemControl><manualActivationRule id="m_rl"/></itemControl>
|
||||
</humanTask>
|
||||
</casePlanModel>
|
||||
</case>
|
||||
</definitions>`;
|
||||
|
||||
export const NARRATIVE = `§0 开案:拿到一批待定价条目即开案。成品是**一份全条目带偏离度的报价草案** ——
|
||||
人签的不是一个数字,是**这个数字的来路**:基准从哪来、毛利怎么算、偏离多少、门禁怎么判。
|
||||
/**
|
||||
* 案例说明书 —— **只写整单一以贯之的事**(逐步做法归剧本,两处不重复写同一件事)。
|
||||
*/
|
||||
export const NARRATIVE = `§0 开案:**一批料要定出各自的拟报单价**。来路三种,形状都收(宽进):
|
||||
· **寻源转来**:任务数据里有 \`rows\` 数组 —— 目标报价清单的落地行,每行带底价(base_price)
|
||||
与价源;这是最全的一种来路;
|
||||
· **受理/选型直达**:有 \`records\` 或 \`rows\` 而行上没有底价 —— 只有料号与年用量;
|
||||
· **单飞**:只有一段需求文字,列了几颗料要报价。
|
||||
|
||||
**能算的不问人**:P-01(利润模型)与 P-02(底价门禁)两支判定表说了算,你不临场拍价。
|
||||
你的活是把判据取齐、把结论落进台账;**只有超限的那几条才叫人**。
|
||||
**成品只有一样**:一份**报价草案** —— 整份行都在、**每一行都有拟报单价**,
|
||||
而且每个价都说得出来路:基准从哪来、毛利档是哪一档、偏离底价线多少、门禁怎么判。
|
||||
人签的不是一个数字,是**这个数字的来路**。它是定价审批的直接输入。
|
||||
|
||||
【T_price 逐条算价】基准价取自历史价情报(A3 的比价结论 / CRM 历史价)。新料号无历史时
|
||||
基准记 0,由底价门禁按「无基准」处置 —— **不凭空编一个偏离率出来**。
|
||||
逐条写进 \`quote_pricing.draft\`:拟报单价 / 目标毛利 / 相对基准偏离 / 门禁处置。
|
||||
同一拍接着把 draft_total(条数)与 over_total(门禁判「超限」的条数)写上 ——
|
||||
这两个数一个是完成判据,一个决定要不要叫人。写完 finish。
|
||||
**能算的不问人,该人签的不代签**:P-01(利润模型)给毛利档、P-02(底价门禁)给处置,
|
||||
两支判定表说了算,你不临场拍价;而**拟报单价那一列是 BD 定的,你一格都不写** ——
|
||||
你给的是**建议价**,他确认或改成自己的数。你的活是把判据取齐、把结论落进台账。
|
||||
|
||||
【T_approve 超限决策】**只在 over_total > 0 时走这一步**。它触达客户利益,
|
||||
所以标了人工激活:**先 start,再发决策卡**(decide,itemId 填 T_approve)。
|
||||
选项按门禁的处置给(照原价报 / 按底价报 / 退回重算),consequence 写清各自的后果;
|
||||
basis 摆上是哪几条、偏离多少、底价是多少(裸卡基座拒发)。
|
||||
人点完,轨迹上会出现「decision 卡回传 —— 人选了:xxx」—— **看到它就照那条路把 draft 改掉**,
|
||||
别再发第二张卡。改完 finish。
|
||||
**四步走**:归一取基准算建议价 → 交 BD 定价 → 底价自检(按他定的价算毛利、偏离度、
|
||||
门禁处置)→ 定盘放行。
|
||||
|
||||
【提交放行】草案有行了 —— **先 start T_release**(人工激活),然后 submit,BD 验收。
|
||||
提交前把两条裁量判据裁掉(judge,criterion 照判据名原样写);终局唯放行,你不能自己宣布完成。
|
||||
**这一单要经过人一到两次**:BD 在定价工作单上逐条定价(那张单绑着整份草案,
|
||||
拟报单价缺一行按不下提交 —— 基座的闸看着,他填的格子直接落账);
|
||||
**只有还压在拦截档上的条目才另外叫人拍板**(一张决策卡)。每条都叫人,人就成了橡皮图章;
|
||||
一条都不叫,底价门禁就是摆设。最后 BD 放行签字。
|
||||
|
||||
【一路上要说话】每一拍带一句 say,说**业务的话**:这批料算下来什么价、哪几条压线、
|
||||
要不要找人拍板。一两句、口语。别报「写草稿 N 行」这种系统词。`;
|
||||
【一路上要说话】负责人正看着这一单。**每一拍都带一句 say** —— 说业务的话:
|
||||
这批料算下来什么价、哪几条压线、要不要找人拍板;不说系统词。
|
||||
到了要找人的那一拍,先说清这一轮办完了什么、为什么现在需要他。
|
||||
|
||||
【怎么把一项干完 · 一拍连做】write 和 tool 都不会自动收步 —— 做完用 finish 宣告。
|
||||
动作排进同一拍的 actions 串一口气给;只有调工具(出参下一拍才看得见)与
|
||||
发卡 / 提交(挂起)必须停拍。**这一单常路 7-9 拍就该收口**。
|
||||
|
||||
§Z 结案:BD 在验收单上签字,系统把草稿合并成正式草案并发出 draft 口的载荷 ——
|
||||
整份报价草案交给定价审批。`;
|
||||
|
||||
/**
|
||||
* 步骤剧本 —— 与 A1 同一套:`doneWhen` 为真,引擎自己收步、**不问模型**。
|
||||
* 步骤剧本 —— 一个计划项一条;`doneWhen` 是「成果已存在」的判据,不是「该不该做」。
|
||||
*
|
||||
* 两处不给 `doneWhen`:`T_approve`(人工激活,要人拍板)与 `T_release`(触达世界)。
|
||||
* 给了就等于给引擎一把替人签字的钥匙。
|
||||
* 三处不给 `doneWhen`:`S2_review`(发卡触达真人)、`T_approve`(要人拍板)、
|
||||
* `T_release`(签下去触达世界)—— 给了等于给引擎一把替人签字的钥匙。
|
||||
*/
|
||||
export const PRICING_STEPS = [
|
||||
{
|
||||
item: 'T_price',
|
||||
intent: '逐条算价并落草案:拟报单价 / 毛利 / 偏离度 / 门禁处置,并结算条数与超限数',
|
||||
item: 'S1_price',
|
||||
intent: '整份归一成报价草案:取成本基准 → 按利润模型算出底价线与建议价,逐条落表',
|
||||
writes: ['quote_pricing.draft', 'quote_pricing.verdict'],
|
||||
reads: [],
|
||||
/* 上游载荷数组不声明就会被整表段的截断刀砍掉(砍掉的恰是要归一的行) */
|
||||
reads: ['rows', 'records'],
|
||||
view: 'pricing-worksheet',
|
||||
tools: ['crm.history-price-read'],
|
||||
/*
|
||||
* 归一 + 算价两轮都走完才算办完:行数两边对上(一边是基座现数),
|
||||
* 且 priced_total 写过(全是无基准的单子这个数就是 0;S-FEEL 未绑定的名字
|
||||
* 参与比较为假,所以「没写过」进不来)。
|
||||
*/
|
||||
doneWhen: 'line_total > 0 and quote_pricing_draft_rows = line_total and priced_total >= 0',
|
||||
guide: `第一件事:把这一单的**报价草案**落出来,并把能算的价都算出来。
|
||||
|
||||
【料从哪儿来】
|
||||
· 寻源转来(\`rows\` 里的行带 base_price / price_basis):照抄 —— 底价就是成本基准,
|
||||
一个数字都别改;带 quote_line_total 这类总数的是结论行,不是料,别落进草案。
|
||||
· 受理/选型直达(\`records\` 或没有底价的 \`rows\`):料号、品类、制造商、年用量、
|
||||
客户目标价、类别(type_tag)照抄,**底价一栏留空**。
|
||||
· 单飞:照正文归一,列了几颗落几行;没说类别的按 B。
|
||||
**按 pn 归并,一颗料一行**;pn 一个字符不许改(它是主键,改一个字就是新开一行)。
|
||||
|
||||
【第一拍:只做两件事 —— 落草案行 + 串尾发一次查价】
|
||||
① 写 \`quote_pricing.draft\`,一颗料一行(rows 收数组):
|
||||
rfq_ref(入参有就原样抄;没有从主题认;认不出才自己造一个,**造出来的必须以 \`PRC-\` 开头**
|
||||
(\`PRC-<主题或第一颗料的型号>\`)。两条硬规矩:
|
||||
**① 整单只有一个单号,不是一颗料一个** —— 它是这一单的案号,草案所有行、verdict 那一行
|
||||
用的都得是同一个串(实测栽过:两颗料造出两个号,一单裂成两单);
|
||||
**② 只从本单来件取材** —— 台账里躺着的 RFQ-xxxx 是别人的单,借它的号等于把两单的行
|
||||
混进同一个案卷(实测栽过一轮))、pn、customer_pn、category、
|
||||
brand、eau、target_price(客户目标价)、lead_time、remark(客户约束/急料,一个字别丢)、
|
||||
type_tag(照上游现标;单飞按 B)、base_price(上游给了就抄,没有留空)。
|
||||
suggested_price / margin_band / floor_price **这一拍全部留空** —— 基准还没取齐。
|
||||
**quoted_price / margin / deviation / disposition 这四列从头到尾都不是你写的**(见下)。
|
||||
② 串尾调 \`crm.history-price-read\`:pns 收数组,把**底价还空着的那几颗料**一次传完
|
||||
(上游已经给了底价的不必查)。
|
||||
**这一拍不写 \`quote_pricing.verdict\`,一列都不写** —— 尤其 priced_total:
|
||||
它是「算价走完了」的凭据,提前写等于把闸提前打开,引擎会在下一拍开头当场收掉这一步,
|
||||
**算价从此不会发生**(A3 实测栽过:嘴上说预填了,台账里没有)。
|
||||
|
||||
【**这一列你一格都不写**:quoted_price】
|
||||
拟报单价是 **BD 在工作单上定的那个数** —— 人签的就是它。你给的是**建议价**
|
||||
(suggested_price),他确认或改成自己的数;margin / deviation / disposition 三列
|
||||
要等他定完价才算得出来,这一步也别写。
|
||||
这不是客气话:从前只有一列价、剧本写着「算不出的留空」,实测跑单时两颗库里查不到的料
|
||||
照样被编出了价(一颗抄客户目标价、一颗凭空写了个数),而交代里还说着「价空着等你定」。
|
||||
**一条要你分辨「这行算得出/算不出」的规矩会走岔;「这一列不写」没有分支可走岔。**
|
||||
|
||||
【第二拍:算建议价 + 记账 + 收步,一拍连做】
|
||||
① 补基准:**只补上一拍 base_price 空着的那几行** —— 工具出参里 hit=true 的写
|
||||
base_price = lastPrice、price_basis 写 **HIST**;hit=false 的**别编价**,base_price 留空,
|
||||
它就是「无基准」那一档。
|
||||
**上游已经给了 base_price 的行:工具回什么都不采用,那一格一个数字都别改** ——
|
||||
上游给的是本单问回来的货源底价,工具给的是库内历史价;这一单的成本是前者
|
||||
(实测栽过:两行上游底价被历史价盖掉,毛利从 20% 变成 115%,数字全对而结论是假的)。
|
||||
② 逐条算**建议价**,规矩写死,照做即可(**这些是算术,不是判断**):
|
||||
· 毛利档 margin_band 按利润模型 P-01 取(客户等级 × 类别):
|
||||
战略客户 + A 类(大批量长单)→ 8%~10%;普通客户 + A 类 → 12%~15%;
|
||||
B 类 → 15%~20%;C 类 → 20%~50%;拿不准按 12%~15%。写成「12%~15%」这样的文本。
|
||||
· **底价线** floor_price = base_price ×(1 + 档下限)。保留四位小数。
|
||||
· **建议价** suggested_price:
|
||||
- 客户没给目标价(target_price 空)→ 取 base_price ×(1 + 档上限);
|
||||
- 客户目标价 ≥ 底价线 → 就取客户目标价(接得住,毛利还在档内);
|
||||
- 客户目标价 < 底价线 → 仍取客户目标价(贴着客户报),这一条到门禁那儿会亮灯;
|
||||
- **base_price 空(无基准)→ margin_band / floor_price / suggested_price 三列一起留空**。
|
||||
不要拿一个不相干的价去凑一个数出来 —— 那比没有建议更危险,它看起来像有依据。
|
||||
· **「没有」要给空串 \`""\`,不许写 0** —— 0 是一个数,底价线写 0 看起来像「成本为零」,
|
||||
偏离度写 0 看起来像「正好压在底价线上」。空串基座会当成「这一列不写」。
|
||||
③ 这时才写 \`quote_pricing.verdict\`(一次写全):rfq_ref、customer_code / customer_name
|
||||
(顶层有就抄)、customer_grade(来件认得出就写,认不出写「普通客户」)、
|
||||
**line_total** = 草案行数、**basis_total** = 有成本基准的条数、
|
||||
**priced_total** = 这一拍算出建议价的条数(0 也要写)。
|
||||
④ finish 这一项。`,
|
||||
},
|
||||
{
|
||||
item: 'S2_review',
|
||||
intent: '一张执行单交 BD:工作单上逐条定价(拟报单价归他);收回后过底价门禁并定盘',
|
||||
writes: ['quote_pricing.draft', 'quote_pricing.verdict'],
|
||||
reads: ['quote_pricing.draft'],
|
||||
view: 'pricing-worksheet',
|
||||
tools: [],
|
||||
// 草案有行了才算算完。不写 `!= null` —— 未绑定的名字那样求值为 true(A1 踩过)
|
||||
doneWhen: 'draft_total > 0',
|
||||
guide: `基准价取自历史价情报(A3 的比价结论 / CRM 历史价)。新料号无历史时基准记 0,
|
||||
由底价门禁按「无基准」处置 —— **不凭空编一个偏离率**。
|
||||
逐条写进 \`quote_pricing.draft\`:拟报单价 / 目标毛利 / 相对基准偏离 / 门禁处置。
|
||||
**同一拍接着**把 draft_total 与 over_total(门禁判超限的条数)写上 ——
|
||||
前者是完成判据,后者决定要不要叫人。
|
||||
**同一拍再写一行 \`quote_pricing.verdict\`**(验收机检读的就是它):
|
||||
· draft_total / unpriced_total = 草案条数 / 还没定出价的条数(后者要 = 0);
|
||||
· over_total = 门禁判超限的条数;
|
||||
· unresolved_over_total = 超限里**还没处置**的条数(要 = 0 —— 回调了或注明了理由都算已处置);
|
||||
· summary 一句人话结论。写不对 submit 会被门挡回来。三笔写完再 finish。
|
||||
**每一行都要带 \`rfq_ref\`** —— 它是这一单的单标识(起单入参里就有,原样抄进去)。
|
||||
台账主键第一列就是它:不写,这一行会和**别的单**同料号的那一行撞主键,把人家覆盖掉
|
||||
(实测栽过:A4 的报价草案主键只有 pn,跑四轮只剩最后一轮的 3 行)。`,
|
||||
/* 不给 doneWhen —— 发卡触达真人(manualActivation),引擎不替人按 */
|
||||
guide: `建议价算完了,**该 BD 出场了** —— 这是本活动唯一的一张执行单,
|
||||
**拟报单价是他定的,不是你定的**。工作区摆的是**定价工作单**:逐颗料一张定价档案卡
|
||||
(成本基准、毛利档、客户目标价、底价线、系统建议价),利润模型与底价门禁两把尺挂在上面,
|
||||
填回区长在同一屏 —— 他在「拟报单价」那一列逐行填(照建议价确认,或改成自己的数)。
|
||||
|
||||
这一步标了人工激活:发卡就算启动,不必单发 start;引擎也不会替你收 —— 人交回后要你自己 finish。
|
||||
|
||||
【照这个骨架发,别少字段】(卡面是纯文本,steps/standard 里**别写星号记号**):
|
||||
{"kind":"human","itemId":"S2_review","workOrder":"execution","card":{
|
||||
"title":"报价定价:<单号> 共 <N> 条待你定价",
|
||||
"ask":"系统已按成本基准与利润模型算出每一颗的建议价,请在工作单的「拟报单价」一列逐行定价:认可建议价就照抄,不认可就改成你的数;库内没有成本基准的那几行没有建议价,由你定。",
|
||||
"steps":["拟报单价每一行都要有 —— 缺一行这张单按不下提交;它是这一单最后签下去的那个数,系统不替你填",
|
||||
"建议价是从成本基准按利润模型的毛利档推出来的:照抄就是认可这个毛利,改高改低都可以,改了在备注里写一句缘由",
|
||||
"低于底价线的价会被底价门禁标出来:两成以内附理由即可放行,超过两成的要另外找人拍板 —— 你在这里往上调一点,就不必再惊动一个人",
|
||||
"库内查不到成本的料没有建议价:按你的经验定价,并在备注写明依据;实在定不了的按行首 × 把这一条撤出本轮"],
|
||||
"standard":"每一行都有拟报单价,而且每个价都说得出来路(基准 / 毛利档 / 让价理由)—— 缺一行这张单按不下提交(系统的闸看着)。",
|
||||
"basis":[{"k":"<PN>","v":"<类别 · 成本基准 · 毛利档 · 建议价>"}]}}
|
||||
· \`basis\` 逐条列本单的料:PN + 基准与建议价。几颗写几条,别铺全表。
|
||||
· **卡上不摆表、不摆格子**(执行卡零输入):整份草案就在工作单那份件上,
|
||||
拟报单价与备注两列都能就地改、行能撤 —— **别往卡里塞 \`blocks\` 或 \`fields\`**。
|
||||
· **这张卡带提交闸**:拟报单价(必填列)还空着,他按不下提交 —— 缺几格、缺在哪行,
|
||||
闸上与件上的红带都写着(与放行门 L1 同一个数同一套算法)。
|
||||
发完卡这一拍就结束了(发卡挂起)。
|
||||
|
||||
【人交回之后 —— 他填的价基座已经替你写回台账了】这一步你只做两件事,然后 finish:
|
||||
① 写 verdict:**confirmed_total** = 他定过价的条数(照草案行数记);
|
||||
他在件上撤掉了行的话,**line_total 跟着改成撤后的实数**。
|
||||
② finish 这一项。
|
||||
**别在这一拍算门禁** —— 底价自检是下一步的活(它有自己的一段做法),这里算了也白算。
|
||||
草案行你一列都不用重写:他填的值基座已经落进台账了,重写会被「写了等于没写」拒回。
|
||||
|
||||
【这张卡一单只发一次】人交回来之后不许再发同一张卡 —— 必填有提交闸兜着,
|
||||
卡回来时拟报单价一定是齐的(或那几行已被他撤掉)。`,
|
||||
},
|
||||
{
|
||||
item: 'S3_gate',
|
||||
intent: '底价自检:按人定下的价逐条算毛利、偏离度与门禁处置,判完即定盘',
|
||||
writes: ['quote_pricing.draft', 'quote_pricing.verdict'],
|
||||
reads: ['quote_pricing.draft'],
|
||||
view: 'pricing-worksheet',
|
||||
tools: [],
|
||||
/*
|
||||
* **收步判据与验收判据同源**:三档处置的条数加起来等于行数(两边都是基座现数)——
|
||||
* 判完了才算办完。`_rows > 0` 那一半不能省:开案时行数是 0,而 0+0+0 = 0
|
||||
* 会让这一步开局即收(未绑定名字的坑之外,还有「全零相等」这一坑)。
|
||||
*/
|
||||
doneWhen:
|
||||
'quote_pricing_draft_rows > 0 and quote_pricing_draft_disposition_pass + quote_pricing_draft_disposition_note + quote_pricing_draft_disposition_block = quote_pricing_draft_rows',
|
||||
guide: `人把价定完了,**现在才轮到算门禁** —— 这一步是纯算术,一拍连做:
|
||||
|
||||
① **逐行按他定的 quoted_price 算三列**(**quoted_price 你一格都不写,只写这三列**):
|
||||
· margin =(quoted_price − base_price)/ base_price,四位小数;
|
||||
· **偏离度** deviation =(quoted_price − floor_price)/ floor_price,四位小数;
|
||||
· **门禁处置** disposition 按底价门禁 P-02 判,只许写这三个大写码之一,一个字母不许差:
|
||||
- deviation ≥ 0 → **PASS**(在底价线之上,直接放行)
|
||||
- −0.20 ≤ deviation < 0 → **NOTE**(低于底价线两成以内,附理由放行)
|
||||
- deviation < −0.20 → **BLOCK**(低于两成以上,压在拦截档,要人拍板)
|
||||
· **base_price 空(无基准)的行**:margin / deviation 给空串 \`""\`(**不许写 0** ——
|
||||
0 看起来像「正好压在底价线上」,而这一行压根没有底价线)、disposition 记 **NOTE**,
|
||||
price_note 里保留他写的定价依据(他没写就补一句「库内无成本基准,由 BD 定价」)。
|
||||
**一行都不能漏**:三档处置的条数加起来必须等于 quote_pricing_draft_rows(基座数的行数)。
|
||||
现在三档都是 0,就是一行都还没判 —— 这一步没办完,放行门开不了。
|
||||
② 看这一拍写完之后的**拦截档条数**(disposition = BLOCK 的行):
|
||||
· 一条都没有 → 同一拍把 verdict 的 **route_decided_total 记 1**(定盘)、
|
||||
summary 写一句人话(几条直接放行、几条附理由),然后 finish 这一项;
|
||||
· 还有 → **别写 route_decided_total**,只 finish 这一项 —— 下一拍会有一步专门叫人拍板。`,
|
||||
},
|
||||
{
|
||||
item: 'T_approve',
|
||||
intent: '超限条目要人拍板 —— 发决策卡,照人选的那条路改草案',
|
||||
writes: ['quote_pricing.draft'],
|
||||
reads: [],
|
||||
intent: '压在拦截档的条目要人拍板 —— 发一张决策卡,照他选的那条路把草案改掉',
|
||||
writes: ['quote_pricing.draft', 'quote_pricing.verdict'],
|
||||
reads: ['quote_pricing.draft'],
|
||||
view: 'pricing-roster',
|
||||
tools: [],
|
||||
// 人工激活 + 要人拍板 —— 不给 doneWhen,引擎不许替人收
|
||||
guide: `**只在 over_total > 0 时走这一步**。它触达客户利益,标了人工激活:
|
||||
**先 start T_approve**(与发卡排同一拍即可),再发决策卡(decide,itemId 填 T_approve)。
|
||||
选项按门禁处置给(照原价报 / 按底价报 / 退回重算),consequence 写清后果;
|
||||
basis 摆上哪几条、偏离多少、底价多少(**裸卡基座拒发**)。
|
||||
人点完轨迹上会出现「decision 卡回传 —— 人选了:xxx」,**照那条路改 draft,别再发第二张卡**。`,
|
||||
/* 人工激活 + 要人拍板 —— 不给 doneWhen,引擎不许替人收 */
|
||||
guide: `**这一单只发一张决策卡。动手之前先分清你在哪一半:**
|
||||
|
||||
· 轨迹里**已经有**「decision 卡回传」/「人选了:xxx」—— 那就跳到下面【人点完之后】那一段,
|
||||
照他选的那条路改草案、写 route_decided_total、finish。**一个 decide 都不许再发**
|
||||
(实测栽过:人点完了,而这一步又从头念了一遍「发一张决策卡」,同一张卡连发四次,
|
||||
调人预算烧光,单子 exhausted —— 他点过的那一下就白点了);
|
||||
· 轨迹里**还没有** —— 才轮到下面【发一张决策卡】那一段。
|
||||
|
||||
这一步只在还有条目压在拦截档(基座现数 quote_pricing_draft_disposition_block > 0)时入场;
|
||||
它触达客户利益,标了人工激活:发卡就算启动,不必单发 start。
|
||||
工作区这时摆的是**报价草案清单**,整份草案与门禁分布都在上面,他看得见全局再拍板。
|
||||
|
||||
【发一张决策卡,照这个骨架】(选项少于两个基座拒发):
|
||||
{"kind":"decide","itemId":"T_approve","ask":"<N> 条低于底价线两成以上,按哪条路办?",
|
||||
"options":[
|
||||
{"value":"floor","label":"回调到底价线报","consequence":"守住毛利下限,客户可能不接受这个价","recommended":true},
|
||||
{"value":"as-is","label":"照现价破线报,附理由","consequence":"接住客户目标价拿单,毛利跌破下限,理由随草案交审批"},
|
||||
{"value":"drop","label":"这几条本轮不报","consequence":"撤出本单草案,客户拿到的是一份缺项的报价"}],
|
||||
"basis":[{"k":"<PN>","v":"成本基准 <X> · 底价线 <Y> · 拟报 <Z> · 偏离 <W>"}]}
|
||||
· basis 逐条摆压在拦截档的那几颗料:基准、底价线、拟报价、偏离度 —— 裸卡基座拒发。
|
||||
· 发完这一拍就结束了(发卡挂起)。
|
||||
|
||||
【人点完之后】轨迹上会出现「decision 卡回传 —— 人选了:xxx」。**照那条路把草案改掉,
|
||||
别再发第二张卡**,一拍连做:
|
||||
· 选 floor:**这是全活动唯一一次你可以写 quoted_price** —— 那个数不是你定的,
|
||||
是他在卡上点下的(就是该行的底价线),你只是替他落账:
|
||||
那几行 quoted_price 改成本行的 floor_price,margin 记该行毛利档的下限、deviation 记 0、
|
||||
disposition 改 **PASS**,price_note 写「按底价线回调,由负责人拍板」;
|
||||
· 选 as-is:价不动,disposition 改 **NOTE**,price_note 写明破线的理由与拍板人;
|
||||
· 选 drop:用 \`drop\` 把那几行撤出本单草稿(主键照 \`quote_pricing_draft_keys\` 名单**原样粘**),
|
||||
并把 verdict 的 line_total 改成撤后的实数。
|
||||
然后写 verdict:**route_decided_total 记 1**、summary 补一句拍板结果,finish 这一项。
|
||||
拦截档清零之后,放行门才开得了。`,
|
||||
},
|
||||
{
|
||||
item: 'T_release',
|
||||
intent: '草案已齐,提交放行交 BD 验收',
|
||||
intent: '草案已定盘,提交放行交 BD 验收',
|
||||
writes: [],
|
||||
reads: [],
|
||||
view: 'pricing-roster',
|
||||
tools: [],
|
||||
guide: `草案有行了 —— **先 start T_release**(人工激活),提交前用 judge 裁掉裁量判据
|
||||
(criterion 照判据名原样写),然后 submit。终局唯放行:你不能自己宣布完成。`,
|
||||
/* 放行触达世界,必须人按一下 —— 引擎不自动收它(剧本里也不给 doneWhen) */
|
||||
guide: `每行有价、门禁处置守恒、拦截档清零、定盘已记 —— 可以走放行了
|
||||
(T_release 人工激活:动它就算启动,不必单发 start)。先把裁量判据裁掉,然后 submit,同一拍连做:
|
||||
|
||||
· judge criterion="报价的来路说得清:基准价来源、毛利档与让价理由逐条可溯,BD 对报价负责" ——
|
||||
看草案里每一行的基准、毛利档与备注:说得出来路就 satisfied=true;
|
||||
有行说不出的(比如无基准由人定的价没写缘由),satisfied=**false**,rationale 逐行写清,
|
||||
交签字的人定。**裁成不满足照样能提交** —— 理由连着卡一起交给他。
|
||||
criterion 照判据名**整句原样抄**,写别的名字对不上。
|
||||
|
||||
终局唯放行:你不能自己宣布完成。`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 阶段 —— 进度条的刻度,只给人看。除放行外每个计划项恰属一个阶段;
|
||||
* 放行那一格由基座统一加,人看到的是 x/4。
|
||||
*/
|
||||
export const PRICING_PHASES = [
|
||||
{ name: '取基准算建议价', items: ['S1_price'] },
|
||||
{ name: '交BD定价', items: ['S2_review'] },
|
||||
{ name: '底价自检', items: ['S3_gate'] },
|
||||
{ name: '超限拍板', items: ['T_approve'] },
|
||||
];
|
||||
|
||||
@@ -1,79 +1,17 @@
|
||||
/**
|
||||
* 统一报价 A4 · 成本定价 —— 活动包代码入口(ADR-004)。
|
||||
* 统一报价 A4 · 成本定价 —— 活动包代码入口(ADR-004:默认导出 defineActivity 的返回值)。
|
||||
*
|
||||
* flow 活动:下一步由步序声明给定。本文件只做声明装不下的两件事 ——
|
||||
* 视图流控,与「结论不可静默覆写」的呈现:P-02 拦下的条目必须显式呈现给人。
|
||||
* **这个活动没有 `run`,不是缺件。**
|
||||
*
|
||||
* 案例活动的推进由 Loop 驱动,行为全部写在清单里:案例模型 + 说明书 + 步骤剧本 +
|
||||
* 两把判定表 + 两条技能 + 台账声明 + 两张 HTML 报告模版。
|
||||
* 上一版这里有一段 `run()`(读草案、拼 kv/table 块、按拦下条数返回 await/submit)——
|
||||
* 案例活动根本不会调它;摆一个空转的 run 去假装有实现,更坏:
|
||||
* 它写的那套呈现与 `viewTemplates` 里的模版各说各话,而没人看得出以哪份为准。
|
||||
*
|
||||
* 这个文件之所以还存在,是因为清单的 `entry` 指着它 —— 它是**被静态审扫描的产物文本**
|
||||
* (查 import 与全局引用,证明活动包零环境能力)。
|
||||
*/
|
||||
import { defineActivity } from '@sdk';
|
||||
import type { ActivityBase, ActivityOutcome } from '@sdk';
|
||||
|
||||
const DRAFT = 'quote_pricing.draft';
|
||||
|
||||
export default defineActivity({
|
||||
name: 'quote-pricing',
|
||||
|
||||
async run(base: ActivityBase): Promise<ActivityOutcome> {
|
||||
const items = await base.ledger.read(DRAFT);
|
||||
const priced = items.filter((r) => typeof r['quoted_price'] === 'number');
|
||||
// 底价门禁 P-02 拦下的条目 —— DMN 结论是**约束性输入**,不得静默覆写
|
||||
const blocked = items.filter((r) => String(r['disposition'] ?? '').includes('拦截'));
|
||||
|
||||
await base.interaction.pushView([
|
||||
{
|
||||
t: 'kv',
|
||||
title: '定价概览',
|
||||
items: [
|
||||
{ k: '条目', v: `${items.length} 条` },
|
||||
{ k: '已定价', v: `${priced.length} 条` },
|
||||
{ k: '底价门禁拦下', v: `${blocked.length} 条` },
|
||||
],
|
||||
},
|
||||
{
|
||||
t: 'table',
|
||||
title: '报价草案',
|
||||
cols: ['PN', '拟报单价', '毛利率', '偏离度', '门禁处置'],
|
||||
rows: items.map((r) => [
|
||||
String(r['pn'] ?? ''),
|
||||
fmt(r['quoted_price']),
|
||||
pct(r['margin']),
|
||||
pct(r['deviation']),
|
||||
String(r['disposition'] ?? '待判'),
|
||||
]),
|
||||
linkDonut: {
|
||||
col: 4,
|
||||
title: '门禁分布',
|
||||
map: [
|
||||
{ match: '直接放行', label: '直接放行', tone: 'ok' },
|
||||
{ match: 'PM', label: 'PM 附理由', tone: 'warn' },
|
||||
{ match: '拦截', label: '拦截', tone: 'danger' },
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (priced.length < items.length) {
|
||||
await base.interaction.say(`还有 ${items.length - priced.length} 条未定价,继续按利润模型 P-01 求值。`, 'conclusion');
|
||||
return { kind: 'await', reason: '等待定价步完成' };
|
||||
}
|
||||
|
||||
if (blocked.length > 0) {
|
||||
// 超出授权边界 → 走「人 · 决策」,不自行破线
|
||||
await base.interaction.say(
|
||||
`${blocked.length} 条低于底价 20% 以上,已被底价门禁 P-02 拦下 —— 需要人裁决回调还是注明理由。`,
|
||||
'criterion',
|
||||
);
|
||||
return { kind: 'await', reason: '等待超限条目的人裁决' };
|
||||
}
|
||||
|
||||
await base.interaction.say(`${items.length} 条已全部定价且过底价门禁,可提交审批。`, 'criterion');
|
||||
return { kind: 'submit', deliverable: 'quote-draft', rationale: '全条目带偏离度,底价门禁全过' };
|
||||
},
|
||||
});
|
||||
|
||||
function fmt(value: unknown): string {
|
||||
return typeof value === 'number' ? value.toFixed(2) : '—';
|
||||
}
|
||||
|
||||
function pct(value: unknown): string {
|
||||
return typeof value === 'number' ? `${(value * 100).toFixed(1)}%` : '—';
|
||||
}
|
||||
export default defineActivity({ name: 'quote-pricing' });
|
||||
|
||||
@@ -1,27 +1,45 @@
|
||||
/**
|
||||
* 活动包清单 —— 统一报价 A4 · 成本定价。
|
||||
* 活动包清单 —— 统一报价 A4 · 成本定价(案例活动)。
|
||||
*
|
||||
* 业务素材:docs/MVP示例工作流.html §1.2(S1 取价基准 → S2 逐条定价 → S3 底价自检)。
|
||||
* README《已裁定的落地决策》③:**A4 = flow** —— 取价、查表定价、自检的判断
|
||||
* **全部装进 DMN 决策表确定性求值**;一旦出现「由 AI 决定怎么办」,按 AM 2.1 上升。
|
||||
* 业务素材:`docs/MVP示例工作流.html` §1.2(S1 取价基准 → S2 逐条定价 → S3 底价自检):
|
||||
* 「BD 逐条定价:价格 = 底价 × 利润模型。提交审批前先自查底价门禁 ——
|
||||
* 超限的自己先调,不把问题留给 PM。」
|
||||
*
|
||||
* 形状与 A3 同一条(2026-08-03 重写 @3.0.0):归一算价 → 一张执行单交人 → 超限才叫人
|
||||
* 拍板 → 放行。**能算的不问人**(P-01 毛利档、P-02 底价门禁两把尺写死),
|
||||
* **该人负责的不自算**(拟报单价是必填,库内没有基准的那几行系统一个价都不编)。
|
||||
*
|
||||
* 与 @2.0.0 的差别(三处都是踩过的账):
|
||||
* ① `input.required` 从 `['rfqRef','customerCode','records']` 清空 —— 宽进严出,
|
||||
* 上游(A3)发的是 `{rfqRef, rows}`,门口拦的恰是主路;
|
||||
* ② 台账补 `required` —— 从前一个必填都没有,执行卡的闸没牙,人不填也能交卷;
|
||||
* ③ 判据与哨兵全部换成**基座读数**(`_rows` / `_missing` / `_<列>_<值>`)——
|
||||
* 从前 L1 读 `unpriced_total`、哨兵读 `draft_total`,两个都是活动自报的数。
|
||||
*/
|
||||
import type { ActivityManifest, DmnModel } from '@contracts';
|
||||
import { PRICING_SKILLS } from './skills.ts';
|
||||
import { CMMN_XML, NARRATIVE, PRICING_STEPS } from './case-model.ts';
|
||||
import { activityIdSchema } from '@contracts';
|
||||
import { PRICING_BASIS, PRICING_PRICED, PRICING_RELEASE } from './views/report.ts';
|
||||
import { PRICING_SKILLS } from './skills.ts';
|
||||
import { CMMN_XML, NARRATIVE, PRICING_PHASES, PRICING_STEPS } from './case-model.ts';
|
||||
import { PRICING_ROSTER, PRICING_WORKSHEET } from './views/report.ts';
|
||||
|
||||
const DOMAIN = 'quote_pricing';
|
||||
const D = 'quote_pricing';
|
||||
const ACTIVITY_ID = activityIdSchema.parse('quote-pricing');
|
||||
|
||||
/**
|
||||
* 利润模型 P-01 —— 品牌 × 类型 × 客户等级 × 数量档 → 目标毛利。
|
||||
* 「经验」段落不入决策表(那是技能 / 活动记忆种子:载方法不载权力)。
|
||||
* 两把尺 —— **规则本身不是数据**,随包冻结。
|
||||
*
|
||||
* · 利润模型 P-01:客户等级 × 类别 → 目标毛利区间(FIRST,行序即业务优先级);
|
||||
* · 底价门禁 P-02:相对底价线的偏离度 → 处置码。
|
||||
*
|
||||
* 出参就是台账列的取值(`disposition` 的三个大写码),模型算完照抄即可 ——
|
||||
* 「出参键名与台账列同名」是为了它拿回去直接 write(规范 §5.6)。
|
||||
* 两把尺的档位在三处出现:这张表、剧本 guide、工作单上的规则块 ——
|
||||
* 包内自测把三处钉在一起(改一处不改另两处,测就红)。
|
||||
*/
|
||||
export const P01_DMN: DmnModel = {
|
||||
drd: [
|
||||
{ decision: 'p01-margin', requires: [], inputs: ['customerGrade', 'materialType', 'volumeTier'] },
|
||||
{ decision: 'p02-floor', requires: ['p01-margin'], inputs: ['quotedPrice', 'floorPrice'] },
|
||||
{ decision: 'p01-margin', requires: [], inputs: ['customer_grade', 'type_tag'] },
|
||||
{ decision: 'p02-floor', requires: ['p01-margin'], inputs: ['deviation'] },
|
||||
],
|
||||
tables: [
|
||||
{
|
||||
@@ -29,30 +47,29 @@ export const P01_DMN: DmnModel = {
|
||||
label: '利润模型 P-01',
|
||||
hitPolicy: 'FIRST',
|
||||
inputs: [
|
||||
{ name: 'customerGrade', expression: 'customerGrade' },
|
||||
{ name: 'materialType', expression: 'materialType' },
|
||||
{ name: 'volumeTier', expression: 'volumeTier' },
|
||||
{ name: 'customer_grade', expression: 'customer_grade' },
|
||||
{ name: 'type_tag', expression: 'type_tag' },
|
||||
],
|
||||
outputs: [{ name: 'marginLow' }, { name: 'marginHigh' }],
|
||||
outputs: [{ name: 'margin_low' }, { name: 'margin_high' }],
|
||||
rules: [
|
||||
{ id: 'p01-1', when: ['"战略客户"', '"A"', '"大批量"'], then: [0.08, 0.1], annotation: '战略客户 A 类标准件大批量 8%–10%' },
|
||||
{ id: 'p01-2', when: ['"普通客户"', '"A"', '-'], then: [0.12, 0.15], annotation: '普通客户 A 类标准件 12%–15%' },
|
||||
{ id: 'p01-3', when: ['-', '"B"', '-'], then: [0.15, 0.2], annotation: 'B 类波动件 15%–20%' },
|
||||
{ id: 'p01-4', when: ['-', '"C"', '-'], then: [0.2, 0.5], annotation: 'C 类替代/EOL ≥20% 或一事一议' },
|
||||
{ id: 'p01-default', when: ['-', '-', '-'], then: [0.12, 0.15], annotation: '兜底按普通客户标准件' },
|
||||
{ id: 'p01-1', when: ['"战略客户"', '"A"'], then: [0.08, 0.1], annotation: '战略客户 A 类标准件(大批量长单)8%~10%' },
|
||||
{ id: 'p01-2', when: ['"普通客户"', '"A"'], then: [0.12, 0.15], annotation: '普通客户 A 类标准件 12%~15%' },
|
||||
{ id: 'p01-3', when: ['-', '"B"'], then: [0.15, 0.2], annotation: 'B 类波动件 15%~20%' },
|
||||
{ id: 'p01-4', when: ['-', '"C"'], then: [0.2, 0.5], annotation: 'C 类替代/停产件 20%~50%(一事一议)' },
|
||||
{ id: 'p01-default', when: ['-', '-'], then: [0.12, 0.15], annotation: '拿不准按普通客户标准件档' },
|
||||
],
|
||||
},
|
||||
{
|
||||
/** 底价门禁 P-02 —— 偏离底价 ±20% 即止 */
|
||||
/** 底价门禁 P-02 —— 偏离底价线 ±20% 即止;出参就是台账 `disposition` 的取值 */
|
||||
id: 'p02-floor',
|
||||
label: '底价门禁 P-02',
|
||||
hitPolicy: 'FIRST',
|
||||
inputs: [{ name: 'deviation', expression: 'deviation' }],
|
||||
outputs: [{ name: 'disposition' }, { name: 'passable' }],
|
||||
rules: [
|
||||
{ id: 'p02-1', when: ['>= 0'], then: ['直接放行', true], annotation: '≥底价 → 直接放行' },
|
||||
{ id: 'p02-2', when: ['[-0.20..0)'], then: ['PM 附理由放行', true], annotation: '低于底价 ≤20% → PM 附理由' },
|
||||
{ id: 'p02-3', when: ['< -0.20'], then: ['拦截 · BOSS 拍板', false], annotation: '低于底价 >20% → 拦截' },
|
||||
{ id: 'p02-1', when: ['>= 0'], then: ['PASS', true], annotation: '在底价线之上 → 直接放行' },
|
||||
{ id: 'p02-2', when: ['[-0.20..0)'], then: ['NOTE', true], annotation: '低于底价线两成以内 → 附理由放行' },
|
||||
{ id: 'p02-3', when: ['< -0.20'], then: ['BLOCK', false], annotation: '低于两成以上 → 拦截,人拍板' },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -62,95 +79,162 @@ export const manifest: ActivityManifest = {
|
||||
identity: {
|
||||
activityId: ACTIVITY_ID,
|
||||
name: 'A4 · 成本定价',
|
||||
packageVersion: '2.0.0',
|
||||
packageVersion: '3.5.0',
|
||||
category: '统一报价',
|
||||
sdkVersion: '0.1.0-m5',
|
||||
checksum: 'dev-checksum-quote-pricing',
|
||||
},
|
||||
|
||||
/* ── 契约七件 ────────────────────────────────────────────────── */
|
||||
contract: {
|
||||
activityId: ACTIVITY_ID,
|
||||
version: '0.1.0',
|
||||
version: '1.0.0',
|
||||
input: {
|
||||
/*
|
||||
* **宽进到底:一个必填都不设**(与 A1/A2/A3 同口径 —— 宽进严出,
|
||||
* 输入契约在活动内部归一 + 补全)。
|
||||
*
|
||||
* 来路三种形状:寻源转来(rows,行上带 base_price)、受理/选型直达(records/rows,
|
||||
* 没有底价)、单飞(一段文字)。上一版必填 `rfqRef + customerCode + records`,
|
||||
* 而 A3 的 `quotation` 口发的是 `{rfqRef, rows}` —— 门口拦的恰是主路。
|
||||
*/
|
||||
data: {
|
||||
type: 'object',
|
||||
required: ['rfqRef', 'customerCode', 'records'],
|
||||
required: [],
|
||||
properties: {
|
||||
rfqRef: { type: 'string', title: '询价单号(单标识,贯穿全流程)' },
|
||||
customerCode: { type: 'string' },
|
||||
records: { type: 'array' },
|
||||
// B/C 类由 A3/A2 交回的底价与替代方案;A 类直通时为空
|
||||
floorPrices: { type: 'array' },
|
||||
customerCode: { type: 'string', title: '客户码' },
|
||||
rows: { type: 'array', title: '目标报价清单(寻源/选型转来,行上带底价)' },
|
||||
records: { type: 'array', title: '上游受理记录(受理直达)' },
|
||||
subject: { type: 'string', title: '需求主题' },
|
||||
body: { type: 'string', title: '需求正文' },
|
||||
attachments: {
|
||||
type: 'array',
|
||||
format: 'file',
|
||||
title: '附件',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', title: '文件名' },
|
||||
contentType: { type: 'string', title: '类型' },
|
||||
size: { type: 'integer', title: '字节数' },
|
||||
ref: { type: 'string', title: '存档号' },
|
||||
text: { type: 'string', title: '读出来的文本' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
messageAllowed: true,
|
||||
messageHint: '客户等级、急单、竞对价、这一单的让价授权写这里',
|
||||
},
|
||||
output: {
|
||||
/*
|
||||
* 只有一个出口:报价草案交定价审批(流程包 E8 是无条件边)。没有选择组 ——
|
||||
* 没有去向要选,定盘就是把账写齐。
|
||||
*
|
||||
* 载荷键名照下游(A6 议价)的入参写:`rfqRef / customerCode / items` ——
|
||||
* 边读什么、下游要什么,上游就得发什么。标量都有台账列承载(自测钉着)。
|
||||
*/
|
||||
mandatory: [
|
||||
{
|
||||
name: 'draft',
|
||||
payload: {
|
||||
type: 'object',
|
||||
required: ['rfqRef', 'items'],
|
||||
properties: { rfqRef: { type: 'string' }, items: { type: 'array' } },
|
||||
properties: {
|
||||
rfqRef: { type: 'string', title: '询价单号(单标识 —— 下游台账的主键)' },
|
||||
customerCode: { type: 'string', title: '客户码' },
|
||||
items: { type: 'array', title: '报价草案(每条带拟报单价、偏离度与门禁处置)' },
|
||||
},
|
||||
},
|
||||
description: '报价草案(逐条带偏离度)',
|
||||
description: '报价草案(全条目带拟报单价与门禁处置)',
|
||||
},
|
||||
],
|
||||
selective: [],
|
||||
},
|
||||
/*
|
||||
* 必交付物**必须给 `ledger` 地址**(2026-07-31 修)。
|
||||
* 不给的话预检退回 `scope[key]` 去找 —— key 是 `benchmark-table` 这样的短横线别名,
|
||||
* 任务视图里永远没有这个键,于是「必交付物缺一不得提交放行」永远拒,
|
||||
* 活办完了也放不了行,连拒五拍熔断(实测 A2 死于此)。
|
||||
* 必交付物只有一件:**报价草案**。定价结论(verdict)是它的结论栏,
|
||||
* `ledgers` 说清这份成果由哪几张表承载(一份成果,不是两份)。
|
||||
*/
|
||||
requiredDeliverables: [
|
||||
{ key: 'quote-draft', label: '报价草案(全条目带偏离度)', ledger: `${DOMAIN}.draft` },
|
||||
{
|
||||
key: 'quote-draft',
|
||||
label: '报价草案(每条带拟报单价与门禁处置)',
|
||||
ledger: `${D}.draft`,
|
||||
/* 验收页上让 BD 就地改的两列 —— 恰是他的职权面(定价与让价理由) */
|
||||
editableAtRelease: ['quoted_price', 'price_note'],
|
||||
ledgers: [`${D}.draft`, `${D}.verdict`],
|
||||
},
|
||||
],
|
||||
/*
|
||||
* 机检谓词读的是 `verdict` 那张**汇总表**的列(照 A1 的口径)。
|
||||
* 上一版的名字(unpriced;floor-gate 更是连谓词都没有)**台账里根本没有** —— S-FEEL 拿未绑定的名字
|
||||
* 比大小求值为假,判据永远达不了标,submit 门永远拒,连拒五拍熔断。
|
||||
* 判据的名字必须落在台账上,否则「机检」是一句空话(2026-07-31 销账)。
|
||||
* 验收判据 —— **机检(L1/L2)只读基座数得出来的数**:
|
||||
* `_rows` 基座数行数、`_missing` 基座按 required 现数(与执行卡提交闸同一个数
|
||||
* 同一套算法)、`_disposition_*` 基座按值现数。
|
||||
* 上一版两条机检读活动自报的 `unpriced_total` / `unresolved_over_total` ——
|
||||
* 那不是门,是「改哪个数能过」的提示(违反史已记四次,这是第五类)。
|
||||
*/
|
||||
acceptanceCriteria: [
|
||||
{ key: 'all-priced', label: '每条都有拟报单价与毛利率', level: 'L1', predicate: 'unpriced_total = 0' },
|
||||
// 底价门禁沉为 L2 机检 —— 「约束力收缩」的终点
|
||||
{ key: 'floor-gate', label: '偏离底价 ≤±20%,超限者已回调或注明理由', level: 'L2', modelRef: 'p02-floor', predicate: 'unresolved_over_total = 0' },
|
||||
{ key: 'pm-release', label: 'BD 放行', level: 'L5' },
|
||||
{
|
||||
key: 'roster-complete',
|
||||
label: '草案齐:上游条目逐条承继,说了几条草案里就躺着几行',
|
||||
level: 'L1',
|
||||
predicate: `line_total > 0 and ${D}_draft_rows = line_total`,
|
||||
},
|
||||
{
|
||||
key: 'all-priced',
|
||||
label: '每条都有拟报单价:库内无基准价的那几行也由人定过价,缺一行放不了',
|
||||
level: 'L1',
|
||||
predicate: `${D}_draft_rows > 0 and ${D}_draft_missing = 0`,
|
||||
},
|
||||
{
|
||||
key: 'floor-gate',
|
||||
label: '底价门禁已过:处置逐条落定,没有还压在拦截档上的条目 —— 超限的要么回调、要么经人拍板附理由',
|
||||
level: 'L2',
|
||||
modelRef: 'p02-floor',
|
||||
predicate: `${D}_draft_disposition_block = 0 and ${D}_draft_disposition_pass + ${D}_draft_disposition_note + ${D}_draft_disposition_block = ${D}_draft_rows`,
|
||||
},
|
||||
/*
|
||||
* 报价来路可核是**裁量**(L3):基准是不是那个基准、让价理由站不站得住,
|
||||
* 机器数不出来。这句 label 与剧本里 judge 的 criterion **必须逐字一致**。
|
||||
*/
|
||||
{
|
||||
key: 'price-defensible',
|
||||
label: '报价的来路说得清:基准价来源、毛利档与让价理由逐条可溯,BD 对报价负责',
|
||||
level: 'L3',
|
||||
},
|
||||
/* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */
|
||||
{ key: 'bd-release', label: 'BD 放行:签下去这份报价草案即定盘并交定价审批', level: 'L5' },
|
||||
],
|
||||
revisableAtRelease: [],
|
||||
prohibitions: [
|
||||
{ key: 'no-silent-override', statement: '不得静默绕开 P-02 的结论;要破线只能走「人 · 决策」' },
|
||||
{ key: 'no-stale-floor', statement: '底价超一周未更新的,先刷底价再判' },
|
||||
{ key: 'no-silent-override', statement: '不得静默绕开底价门禁的结论;要破线只能走「人 · 决策」并留下理由' },
|
||||
{ key: 'no-made-up-basis', statement: '没有成本基准的料不许编一个偏离率出来:基准空着就空着,价由人定并注明依据' },
|
||||
{ key: 'no-stale-floor', statement: '底价超一周未更新的,先刷底价再判 —— 拿过期的成本算出来的毛利是假的' },
|
||||
],
|
||||
authorization: {
|
||||
discretion: [{ key: 'margin-band', statement: '在 P-01 给出的毛利区间内自决;出区间必须调人决策' }],
|
||||
discretion: [{ key: 'margin-band', statement: '在 P-01 给出的毛利区间内自决;出区间要么回调,要么调人拍板' }],
|
||||
preauthorizedActions: [],
|
||||
humanCallBudget: 3,
|
||||
/* 一张执行单 + 可能一张决策卡 + 放行 = 三次;留一倍余量 */
|
||||
humanCallBudget: 5,
|
||||
},
|
||||
resources: {
|
||||
/*
|
||||
* `quote-pricing.basis` 已从声明里去掉(2026-07-31):工具箱里从没注册过它,
|
||||
* 装包时静态审一直报「运行时转人代办」—— 而重做后的案例模型压根不调它
|
||||
* (算价归判定表 P-01/P-02,取基准读历史价)。声明一件不存在也不用的工具,
|
||||
* 只会让每一单跑到 A4 就弹一张莫名其妙的转人卡。
|
||||
*/
|
||||
tools: [{ name: 'crm.history-price-read' }],
|
||||
skills: PRICING_SKILLS.map((s) => s.id),
|
||||
models: ['deepseek-v4-flash'],
|
||||
roles: ['BD'],
|
||||
budget: { steps: 60, tokens: 150_000, activeMinutes: 120, humanCalls: 3 },
|
||||
/* SLA <2h(MVP示例 §1.2);预算按 A3 实测(6-8 拍 / 40-50k)留一倍余量 */
|
||||
budget: { steps: 60, tokens: 400_000, activeMinutes: 120, humanCalls: 5 },
|
||||
},
|
||||
},
|
||||
|
||||
/* ── 声明 ────────────────────────────────────────────────────── */
|
||||
declarations: {
|
||||
models: ['deepseek-v4-flash'],
|
||||
tools: [{ name: 'crm.history-price-read' }],
|
||||
ledger: [
|
||||
{
|
||||
domain: DOMAIN,
|
||||
domain: D,
|
||||
object: 'draft',
|
||||
kind: 'record',
|
||||
caseKey: 'rfq_ref',
|
||||
@@ -158,82 +242,226 @@ export const manifest: ActivityManifest = {
|
||||
fields: [
|
||||
{ name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
|
||||
{ name: 'pn', type: 'text', primaryKey: true, masked: false, label: 'PN' },
|
||||
{ name: 'quoted_price', type: 'number', primaryKey: false, masked: false, label: '拟报单价' },
|
||||
{ name: 'customer_pn', type: 'text', primaryKey: false, masked: false, label: '客户料号' },
|
||||
{ name: 'category', type: 'text', primaryKey: false, masked: false, label: '品类' },
|
||||
{ name: 'brand', type: 'text', primaryKey: false, masked: false, label: '制造商' },
|
||||
{ name: 'eau', type: 'integer', primaryKey: false, masked: false, label: '年用量 EAU' },
|
||||
{ name: 'lead_time', type: 'text', primaryKey: false, masked: false, label: '客户要求交期' },
|
||||
/* 客户备注承继自上游 —— 认证要求、预授权、急料标注,让价时的第一手依据 */
|
||||
{ name: 'remark', type: 'text', primaryKey: false, masked: false, label: '客户备注(约束/急料)' },
|
||||
{ name: 'type_tag', type: 'text', primaryKey: false, masked: false, label: '类别(承继)', tally: ['A', 'B', 'C'] },
|
||||
/*
|
||||
* **客户目标价标 `expect`** —— 客户该给而没给的那一问不能省(件上出橘色
|
||||
* 「建议填」胶囊)。它不拦单:`_gaps` 只喂 L4,而本活动没有 L4 判据。
|
||||
* 注意边界:我方自己写的注(price_note)**不标 expect** —— 标了 `_gaps`
|
||||
* 恒大于 0,那条判据永远绿不了(A1 的账)。
|
||||
*/
|
||||
{ name: 'target_price', type: 'number', primaryKey: false, masked: false, expect: true, label: '客户目标价' },
|
||||
/* 成本基准:上游底价或库内历史价。**取不到就空着** —— 空着不是异常,是「无基准」那一档 */
|
||||
{ name: 'base_price', type: 'number', primaryKey: false, masked: false, label: '成本基准' },
|
||||
{ name: 'price_basis', type: 'text', primaryKey: false, masked: false, label: '基准来源', tally: ['HIST', 'SOURCED'] },
|
||||
{ name: 'margin_band', type: 'text', primaryKey: false, masked: false, label: '毛利档(P-01)' },
|
||||
{ name: 'floor_price', type: 'number', primaryKey: false, masked: false, label: '底价线' },
|
||||
/*
|
||||
* **系统建议价是 AI 的活,拟报单价是人的活** —— 两列分开是**结构治**,不是措辞治。
|
||||
*
|
||||
* 实测账(2026-08-03,单飞那条案例):从前只有 `quoted_price` 一列,剧本写着
|
||||
* 「无基准的行留空,由 BD 定」—— 模型照样给两颗库里查不到的料编了价
|
||||
* (一颗抄了客户目标价,另一颗凭空写了 0.085),交代里还说着「价空着等你定」。
|
||||
* 一条要模型分辨「这行算得出/算不出」的规矩,它十次里会走岔;
|
||||
* 而「这一列你一格都不写」没有分支可走岔。
|
||||
*
|
||||
* 于是:AI 只写 `suggested_price`(算得出就给,算不出就空着),
|
||||
* **`quoted_price` 永远是人在件上定的那个数** —— 它标 `required`,
|
||||
* 件上的缺价红带、执行卡的提交闸、放行门 L1 三处同源(都数基座的 `_missing`)。
|
||||
* 不用 `requiredWhen`:每一行都要有价,没有「这一行还没轮到」的情形。
|
||||
*/
|
||||
{ name: 'suggested_price', type: 'number', primaryKey: false, masked: false, label: '系统建议价' },
|
||||
{ name: 'quoted_price', type: 'number', primaryKey: false, masked: false, required: true, label: '拟报单价(BD 定)' },
|
||||
{ name: 'margin', type: 'number', primaryKey: false, masked: false, label: '毛利率' },
|
||||
{ name: 'last_price', type: 'number', primaryKey: false, masked: false, label: '基准价' },
|
||||
{ name: 'deviation', type: 'number', primaryKey: false, masked: false, label: '偏离度' },
|
||||
{ name: 'disposition', type: 'text', primaryKey: false, masked: false, label: '门禁处置' },
|
||||
{ name: 'deviation', type: 'number', primaryKey: false, masked: false, label: '偏离底价线' },
|
||||
/*
|
||||
* **门禁处置:取值域给出来,条数由基座现数** —— PASS / NOTE / BLOCK。
|
||||
* 守恒判据、分布条、门禁徽读的全是基座读数;值是机器码,
|
||||
* 人读名(直接放行 / 附理由 / 拦截待拍板)由模版样式表翻译。
|
||||
*/
|
||||
{ name: 'disposition', type: 'text', primaryKey: false, masked: false, label: '门禁处置', tally: ['PASS', 'NOTE', 'BLOCK'] },
|
||||
{ name: 'price_note', type: 'text', primaryKey: false, masked: false, label: '备注 / 让价理由' },
|
||||
],
|
||||
},
|
||||
{
|
||||
/*
|
||||
* 定价结论 —— 一单一行的**汇总表**,验收机检谓词的入参就在这儿。
|
||||
* 定价结论 —— 一单一行的**汇总表**,哨兵与报告读它。
|
||||
* 汇总列与逐行列**故意不同名**:任务视图把草稿行摊平到顶层是「后写的赢」,
|
||||
* 拿逐行列求值只会验到最后写的那一行,那不叫机检、叫抽样(A1 的教训)。
|
||||
*/
|
||||
domain: DOMAIN,
|
||||
domain: D,
|
||||
object: 'verdict',
|
||||
kind: 'record',
|
||||
caseKey: 'rfq_ref',
|
||||
label: '定价结论',
|
||||
fields: [
|
||||
{ name: 'rfq_ref', type: 'text', primaryKey: true, masked: false, label: '询价单号' },
|
||||
{ name: 'pn', type: 'text', primaryKey: false, masked: false, label: '型号' },
|
||||
{ name: 'draft_total', type: 'integer', primaryKey: false, masked: false, label: '草案条数' },
|
||||
{ name: 'unpriced_total', type: 'integer', primaryKey: false, masked: false, label: '未定价条数' },
|
||||
{ name: 'over_total', type: 'integer', primaryKey: false, masked: false, label: '超限条数' },
|
||||
{ name: 'unresolved_over_total', type: 'integer', primaryKey: false, masked: false, label: '超限未处置条数' },
|
||||
{ name: 'customer_code', type: 'text', primaryKey: false, masked: false, label: '客户码' },
|
||||
{ name: 'customer_name', type: 'text', primaryKey: false, masked: false, label: '客户名称' },
|
||||
/* 客户等级是 P-01 的入参之一(战略客户 / 普通客户),认不出按普通客户 */
|
||||
{ name: 'customer_grade', type: 'text', primaryKey: false, masked: false, label: '客户等级' },
|
||||
{ name: 'line_total', type: 'integer', primaryKey: false, masked: false, label: '草案条目数' },
|
||||
{ name: 'basis_total', type: 'integer', primaryKey: false, masked: false, label: '有成本基准的条数' },
|
||||
/* 算价走完的凭据(0 也要写)—— 哨兵按它放交人核价那一步入场 */
|
||||
{ name: 'priced_total', type: 'integer', primaryKey: false, masked: false, label: '已算出建议价条数' },
|
||||
/* 「BD 核过价了」的凭据 —— 只在人环走完才产生 */
|
||||
{ name: 'confirmed_total', type: 'integer', primaryKey: false, masked: false, label: 'BD 已核价条数' },
|
||||
{ name: 'route_decided_total', type: 'integer', primaryKey: false, masked: false, label: '已定盘' },
|
||||
{ name: 'summary', type: 'text', primaryKey: false, masked: false, label: '定价结论' },
|
||||
],
|
||||
},
|
||||
],
|
||||
listeners: [
|
||||
{
|
||||
/** 时限过半:SLA <2h,给 BD 与负责人提个醒 —— 静态监听,时限必配 */
|
||||
origin: 'static',
|
||||
eventType: 'human',
|
||||
keyFilter: { kind: 'decision' },
|
||||
semantic: 'complete-step',
|
||||
deadline: 'PT4H',
|
||||
escalation: ['nudge', 'escalate'],
|
||||
eventType: 'time',
|
||||
keyFilter: { kind: 'half-deadline' },
|
||||
semantic: 'observe',
|
||||
deadline: 'PT1H',
|
||||
escalation: ['nudge'],
|
||||
},
|
||||
],
|
||||
// SLA <2h(MVP示例 §1.2 留痕 · SLA)
|
||||
harness: {
|
||||
memoryWindow: 20,
|
||||
memoryWindow: 22,
|
||||
// SLA <2h(MVP示例 §1.2 留痕 · SLA)
|
||||
stallAfter: 'PT2H',
|
||||
nudge: [{ after: 'PT1H', action: 'nudge' }],
|
||||
retry: { times: 2, backoff: 'PT30S' },
|
||||
},
|
||||
},
|
||||
|
||||
/* ── 资产初始版 ──────────────────────────────────────────────── */
|
||||
assets: {
|
||||
/* 资产初始版:案例模型(2026-07-29 起活动只有这一种业务模型) */
|
||||
businessModel: {
|
||||
cmmnXml: CMMN_XML,
|
||||
narrative: NARRATIVE,
|
||||
dmn: P01_DMN,
|
||||
/* 步骤剧本 —— 与 A1 同一套:doneWhen 真了引擎自己收步,不问模型 */
|
||||
steps: PRICING_STEPS,
|
||||
phases: PRICING_PHASES,
|
||||
},
|
||||
skills: PRICING_SKILLS,
|
||||
/*
|
||||
* 两个局面各一张(与 A2/A3 同款口径):
|
||||
* · `pricing-worksheet` 定价工作单 —— 算价与 BD 核价期间摆它(逐颗定价档案、
|
||||
* 两把尺、填回工作表);
|
||||
* · `pricing-roster` 报价草案清单 —— 拍板、放行、归档摆它。
|
||||
*/
|
||||
viewTemplates: [
|
||||
{ name: 'pricing-basis', html: PRICING_BASIS },
|
||||
{ name: 'pricing-priced', html: PRICING_PRICED },
|
||||
{ name: 'pricing-release', html: PRICING_RELEASE },
|
||||
{ name: 'pricing-worksheet', html: PRICING_WORKSHEET },
|
||||
{ name: 'pricing-roster', html: PRICING_ROSTER },
|
||||
],
|
||||
cardSchemas: {},
|
||||
distillHooks: [
|
||||
{
|
||||
on: 'terminal',
|
||||
prompt: '归纳本单定价的经验:哪些组合的毛利档被人回调过、底价门禁拦下的形态、破线的理由。',
|
||||
prompt: '归纳本单定价:哪些组合的毛利档被人回调过、底价门禁拦下的形态、破线的理由与拍板结果。',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/* ── 样例集 —— 三条形状真不同(常备案例即验收案例)───────────── */
|
||||
samples: [
|
||||
{
|
||||
name: '战略客户 A 类大批量',
|
||||
input: { customerCode: 'C001', records: [{ pn: 'LM358DR', type: 'A' }] },
|
||||
/*
|
||||
* ① 寻源转来 · 常路:A3 `quotation` 口的真实形状 —— rows 里清单行(带底价与价源)
|
||||
* 混着结论行。客户目标价都在底价线之上,全 PASS,不必叫人拍板。
|
||||
*/
|
||||
name: '寻源转来 · 两条料底价齐 · 全在底价线之上(常路)',
|
||||
input: {
|
||||
rfqRef: 'RFQ MD14.1',
|
||||
customerCode: 'CU-0142',
|
||||
customer_name: 'Itelma',
|
||||
rows: [
|
||||
{
|
||||
rfq_ref: 'RFQ MD14.1',
|
||||
pn: 'BBPY00201209601Y00',
|
||||
customer_pn: '4-14-01502',
|
||||
category: '滤波器',
|
||||
brand: 'YAGEO',
|
||||
eau: 30000,
|
||||
target_price: 0.085,
|
||||
lead_time: '8周',
|
||||
type_tag: 'B',
|
||||
base_price: 0.058,
|
||||
price_basis: 'SOURCED',
|
||||
supplier: '深圳华强渠道',
|
||||
},
|
||||
{
|
||||
rfq_ref: 'RFQ MD14.1',
|
||||
pn: 'CJTB0101R6N',
|
||||
customer_pn: '4-00-04102',
|
||||
category: '集成电路',
|
||||
brand: 'Jiangsu Changjing',
|
||||
eau: 60000,
|
||||
target_price: 0.42,
|
||||
lead_time: '6周',
|
||||
type_tag: 'A',
|
||||
base_price: 0.35,
|
||||
price_basis: 'HIST',
|
||||
},
|
||||
{ rfq_ref: 'RFQ MD14.1', quote_line_total: 2, route_decided_total: 1, customer_name: 'Itelma', summary: '两条底价齐,交定价' },
|
||||
],
|
||||
},
|
||||
expect: { ports: ['draft'] },
|
||||
},
|
||||
{
|
||||
/*
|
||||
* ② 有超限条目:客户把目标价压到成本之下(0.06 对成本 0.082)——
|
||||
* 偏离底价线超过两成,门禁判 BLOCK,必须叫人拍板(决策卡)。
|
||||
*/
|
||||
name: '客户压价 · 一条低于底价线两成以上 → 决策卡叫人拍板',
|
||||
input: {
|
||||
rfqRef: 'RFQ-2026-0777',
|
||||
customerCode: 'CU-0311',
|
||||
customer_name: '青岛海洋电子',
|
||||
rows: [
|
||||
{
|
||||
rfq_ref: 'RFQ-2026-0777',
|
||||
pn: 'PESD2ETH-XR',
|
||||
category: '二极管',
|
||||
brand: 'Nexperia',
|
||||
eau: 30000,
|
||||
target_price: 0.06,
|
||||
lead_time: '8周',
|
||||
type_tag: 'B',
|
||||
base_price: 0.082,
|
||||
price_basis: 'SOURCED',
|
||||
remark: '客户明说这个价是竞对给的,要跟',
|
||||
},
|
||||
{
|
||||
rfq_ref: 'RFQ-2026-0777',
|
||||
pn: 'NE555P',
|
||||
category: '定时器',
|
||||
brand: 'TI',
|
||||
eau: 5000,
|
||||
target_price: 1.05,
|
||||
lead_time: '4周',
|
||||
type_tag: 'A',
|
||||
base_price: 0.85,
|
||||
price_basis: 'HIST',
|
||||
},
|
||||
{ rfq_ref: 'RFQ-2026-0777', quote_line_total: 2, route_decided_total: 1, customer_name: '青岛海洋电子' },
|
||||
],
|
||||
},
|
||||
expect: { ports: ['draft'] },
|
||||
},
|
||||
{
|
||||
/*
|
||||
* ③ 单飞宽进:没有单号没有清单,正文列料 —— 归一造案号;库内查不到历史价,
|
||||
* 系统一个价都不编,两行拟报单价空着交 BD 定 → **提交闸真拦**空手交卷。
|
||||
*/
|
||||
name: '单飞需求 · 正文列两颗料 · 库内无基准 → 价由人定(闸真拦)',
|
||||
input: {
|
||||
subject: '两颗料报个价',
|
||||
body: '客户要两颗料的报价:XCJ-7788T 电源模块 8K,目标价 12.5;LMR-3302QQ 稳压器 15K,没给目标价。库里应该没做过这两颗,你先出个草案。',
|
||||
},
|
||||
expect: { ports: ['draft'] },
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/**
|
||||
* A4 技能 —— 定价的方法经验(AM 08)。载方法,不载权力。
|
||||
* A4 技能 —— 定价的方法经验。载方法,不载权力。
|
||||
*
|
||||
* 与剧本的分工:剧本写「这一步做什么、写哪几列」,技能写「为什么这么定、
|
||||
* 拿不准时按哪条走」。剧本改了口径,技能里同一句要跟着改(规范 §5.9)。
|
||||
*/
|
||||
import { skillIdSchema } from '@contracts';
|
||||
import type { Skill } from '@contracts';
|
||||
@@ -8,34 +11,41 @@ export const PRICING_SKILLS: Skill[] = [
|
||||
{
|
||||
id: skillIdSchema.parse('pricing.pick-basis'),
|
||||
label: '选对价格基准',
|
||||
applicability: '定价开始,需要确定「跟谁比」时。',
|
||||
applicability: '定价开始,需要确定「这个价跟谁比」时。',
|
||||
method: [
|
||||
'基准的优先级:① 本客户近期成交价 → ② 同型号他客户成交价 → ③ 供应商底价表 → ④ 无基准。',
|
||||
'基准的优先级:① 上游寻源问回来的货源底价 → ② 本客户近期成交价 → ③ 同型号他客户成交价 → ④ 无基准。',
|
||||
'越靠前越贴近这一单的真实议价空间;越靠后越要留余量。',
|
||||
'',
|
||||
'**无基准不是异常**,是一种基准:新料号、久未报价的老料号都会落到这里。',
|
||||
'此时偏离率记 0(没有可偏离的对象),由底价门禁按「无基准」放行 —— ',
|
||||
'此时成本基准、底价线、偏离度**一律留空**,拟报单价交给人定 ——',
|
||||
'不要拿一个不相干的价去凑一个偏离率出来,那比没有基准更危险:它看起来像有依据。',
|
||||
'',
|
||||
'基准超一周未更新的先刷再判:拿过期成本算出来的毛利,数字是真的,结论是假的。',
|
||||
].join('\n'),
|
||||
crystallized: [{ tool: 'crm.history-price-read', input: { pn: 'pn' } }],
|
||||
knowledgeRefs: ['利润模型 P-01'],
|
||||
version: '1.0.0',
|
||||
version: '2.0.0',
|
||||
},
|
||||
{
|
||||
id: skillIdSchema.parse('pricing.read-gate'),
|
||||
label: '读懂底价门禁的处置',
|
||||
applicability: '底价门禁(P-02)给出处置结论,需要决定下一步时。',
|
||||
applicability: '底价门禁(P-02)给出处置码,需要决定下一步时。',
|
||||
method: [
|
||||
'门禁不是「拦下」而是**分流**:',
|
||||
'· 直接放行 —— 偏离在容许带内,照常提审。',
|
||||
'· 超限待裁 —— 偏离超 ±20%,交人决断:回调价格,还是注明理由硬报。',
|
||||
' 两条路都合法,但都要留下**理由**:这条留痕将来会成为客户等级与利润模型的迭代素材。',
|
||||
'门禁不是「拦下」而是**分流**,三档各有各的下一步:',
|
||||
'· PASS(在底价线之上)—— 照常提审,不必解释。',
|
||||
'· NOTE(低于底价线两成以内)—— 附理由放行:让价的缘由写进备注,',
|
||||
' 理由随草案一起交审批。不写理由的让价,到审批那一关同样过不去。',
|
||||
'· BLOCK(低于两成以上)—— 系统不替任何人放行,交人拍板:回调到底价线、',
|
||||
' 破线报并留理由、或这一条本轮不报。三条路都合法,都要留痕。',
|
||||
'',
|
||||
'不要为了让门禁「过」而回头改毛利参数 —— 那是把判据改成结论,',
|
||||
'决策表就失去了它唯一的价值:同输入必同结论。',
|
||||
'两条别做的事:',
|
||||
'· 不要为了让门禁「过」回头改毛利档或改基准 —— 那是把判据改成结论,',
|
||||
' 决策表就失去了它唯一的价值:同输入必同结论;',
|
||||
'· 不要每一条都叫人。BD 自己能回调掉的,回调掉就是处置 ——',
|
||||
' 每条都叫人,人就成了橡皮图章;一条都不叫,门禁就是摆设。',
|
||||
].join('\n'),
|
||||
crystallized: [],
|
||||
knowledgeRefs: ['底价门禁 P-02'],
|
||||
version: '1.0.0',
|
||||
version: '2.0.0',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* A1 两份执行页的样式表 —— **从 PRD 目标件原样搬过来**,不是我照着复述的。
|
||||
*
|
||||
* 出处:`~/coding/JCAI_PRD/docs/活动执行页/A1-询价受理需求.html` 的整段 `<style>`,
|
||||
* 外加 `A1-询价受理记录.html` 里归档件独有的那几件(分类分布条 / 类徽 / 判定表 /
|
||||
* 结论横幅 / 落地动作 / 完成标)。**一个数值都没改**。
|
||||
*
|
||||
* 为什么是搬而不是抄:前三轮我用自己那套外壳「照着做」,块搬齐了、版面是两套 ——
|
||||
* 章节头成了彩色方块编号、多出摘要与元信息带、字号用 clamp。人一眼就看出不是那份件。
|
||||
* 目标件是**像素级的答案**,搬过来才不会在复述里走样。
|
||||
*
|
||||
* 选择器保留 `.proto-root .app-shell` 前缀,所以模版正文也要照原型那样包两层壳 ——
|
||||
* 少一层壳,整张样式表一条都不生效(踩过)。
|
||||
*/
|
||||
export const PRD_CSS = `
|
||||
/* ============================================================================
|
||||
JCAI 员工面工作台 · A1 受理归一的过程成果「询价受理需求」——报告本体
|
||||
立场:**AI 已从 RFQ 邮件抽取归一**,人在这里审核并就地修订。
|
||||
两态:读态(AI 稿 + 红/灰占位提示缺项) ⇄ 编辑态(全字段可改,保存 / 取消)
|
||||
核心是「有效 PN 清单」;清单之上四件受理要素:客户公司 / 联系人 / 客户属性 / 有效 PN。
|
||||
|
||||
令牌与既有组件取自 workbench/src/app/(proto)/app.css,数值未改。
|
||||
⚠️ 修正一处原型 bug:app.css:2062-2064 的 '.jc-rp-sign span'(权重 0,1,1)
|
||||
盖过了 '.jc-rp-ava'(0,1,0),把头像的 display:grid 覆盖成 block、字色改灰,
|
||||
导致落款头像里的字掉到圆外。本文件把该规则收窄为 '.jc-rp-sign > div > span'。
|
||||
新增件:四要素卡 .jc-four/.jc-fcard · 占位胶囊 .jc-ph · 分段控件 .jc-seg ·
|
||||
来源条 .jc-src · 录入表 .jc-tbl-in
|
||||
========================================================================== */
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body { background: #e6eaf1; -webkit-font-smoothing: antialiased; }
|
||||
|
||||
.proto-root .app-shell {
|
||||
--brand: #3370ff; --brand-deep: #245bdb;
|
||||
--brand-soft: #e1eaff; --brand-wash: #f0f4ff;
|
||||
--ai-grad: linear-gradient(135deg, #3370ff, #7f3bf5);
|
||||
--ink: #1f2329; --ink-2: #41464e; --ink-3: #646a73; --ink-4: #8f959e;
|
||||
--line: #dee0e3; --line-soft: #e8eaed; --soft: #f8f9fb;
|
||||
--jcai-bg: #e6eaf1; --cardline: #e1e6ec; --card-shadow: 0 1px 2px rgba(31, 35, 41, .05);
|
||||
--ok: #278717; --ok-soft: #e8f8e4;
|
||||
--warn: #de7802; --warn-accent: #ff8800; --warn-soft: #fff4e5;
|
||||
--danger: #d83931; --danger-soft: #feeae9;
|
||||
--mono: ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono", Menlo, Consolas, monospace;
|
||||
--shadow-brand: 0 6px 16px rgba(51, 112, 255, .22);
|
||||
--t-fast: .16s cubic-bezier(.3, 0, .2, 1);
|
||||
--t-med: .26s cubic-bezier(.3, 0, .2, 1);
|
||||
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: var(--jcai-bg); min-height: 100vh; color: var(--ink);
|
||||
}
|
||||
.proto-root :where(button, input, select, a, [tabindex]):focus-visible {
|
||||
outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 8px;
|
||||
}
|
||||
.proto-root ::selection { background: var(--brand-soft); }
|
||||
|
||||
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
||||
@keyframes jcBlink { 0%,100% { opacity: 1 } 50% { opacity: .35 } }
|
||||
@keyframes jcStamp { from { opacity: 0; transform: rotate(-6deg) scale(1.7); } to { opacity: .9; transform: rotate(-6deg) scale(1); } }
|
||||
|
||||
/* 放开宽度:1160px 是「9 列全展开、一个字都不裁」的最小容器宽度(见下方列 min-width 之和) */
|
||||
.jc-stage { max-width: 1160px; margin: 0 auto; padding: 22px 16px 52px; }
|
||||
.proto-root .jc-mono { font-family: var(--mono); font-size: 11px; color: var(--ink-3); }
|
||||
.proto-root .jc-pulse-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--brand); display: inline-block; animation: jcBlink 1.1s ease-in-out infinite; }
|
||||
|
||||
/* ── 报告外壳 ───────────────────────────────────────────────────────── */
|
||||
.proto-root .jc-report { background: #fff; border: 1px solid var(--cardline); border-radius: 14px; box-shadow: var(--card-shadow); overflow: hidden; animation: rise var(--t-med) both; }
|
||||
.proto-root .jc-rp-head { position: relative; overflow: hidden; display: flex; align-items: flex-start; gap: 12px; padding: 16px 20px 13px; border-bottom: 2px solid var(--ink); }
|
||||
/* 题头下缘粗细双线 = 正式件版式 */
|
||||
.proto-root .jc-rp-head::after { content: ""; position: absolute; left: 0; right: 0; bottom: -4px; height: 1px; background: var(--line); }
|
||||
/* 题头右上的品牌光晕:与工作台标题区同一签名件 */
|
||||
.proto-root .jc-rp-head::before { content: ""; position: absolute; right: -70px; top: -96px; width: 240px; height: 240px; border-radius: 999px; background: radial-gradient(circle, rgba(51,112,255,.10), transparent 70%); pointer-events: none; }
|
||||
.proto-root .jc-rp-head > * { position: relative; z-index: 1; }
|
||||
.proto-root .jc-rp-badge { flex: none; width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center; color: #fff; background: var(--ai-grad); box-shadow: var(--shadow-brand); }
|
||||
.proto-root .jc-rp-tit { flex: 1; min-width: 0; }
|
||||
.proto-root .jc-rp-tit em { display: block; font-style: normal; font-size: 10px; font-weight: 800; letter-spacing: .08em; color: var(--ink-4); font-family: var(--mono); }
|
||||
.proto-root .jc-rp-tit h3 { margin: 4px 0 0; font-size: 18px; font-weight: 800; color: var(--ink); letter-spacing: -.01em; line-height: 1.3; }
|
||||
.proto-root .jc-rp-wait { display: inline-flex; align-items: center; gap: 6px; flex: none; margin-top: 4px; font-size: 11px; font-weight: 700; border-radius: 999px; padding: 4px 12px; color: var(--brand-deep); background: var(--brand-wash); border: 1px solid var(--brand-soft); }
|
||||
.proto-root .jc-rp-wait.warn { color: var(--warn); background: var(--warn-soft); border-color: #ffd9a8; }
|
||||
.proto-root .jc-rp-wait.edit { color: #fff; background: var(--brand); border-color: var(--brand); }
|
||||
.proto-root .jc-rp-stamp { flex: none; margin-top: 2px; transform: rotate(-6deg); border: 2px solid currentColor; border-radius: 9px; padding: 4px 8px 3px 12px; font-size: 13px; font-weight: 900; letter-spacing: .28em; color: var(--ok); opacity: .9; animation: jcStamp .32s cubic-bezier(.2, 1.4, .4, 1) both; }
|
||||
.proto-root .jc-rp-bd { padding: 18px 20px 12px; }
|
||||
.proto-root .jc-rp-part { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 9px; margin: 20px 0 11px; }
|
||||
.proto-root .jc-rp-part:first-child { margin-top: 0; }
|
||||
.proto-root .jc-rp-part > b { flex: none; font-size: 11px; font-weight: 800; letter-spacing: .12em; color: var(--ink-3); }
|
||||
.proto-root .jc-rp-part > .ln { flex: 1; height: 1px; background: var(--line-soft); }
|
||||
.proto-root .jc-rp-part > em { flex: none; font-style: normal; font-size: 10px; font-weight: 700; color: var(--ink-4); font-family: var(--mono); background: var(--soft); border-radius: 999px; padding: 2px 9px; white-space: nowrap; }
|
||||
.proto-root .jc-rp-part > em.warn { color: var(--warn); background: var(--warn-soft); }
|
||||
|
||||
/* 落款 —— ⚠️ 选择器收窄到 > div > *,别再命中头像那个 span */
|
||||
.proto-root .jc-rp-foot { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 13px 20px 15px; border-top: 2px solid var(--line); background: #fbfcfd; }
|
||||
.proto-root .jc-rp-sign { display: flex; align-items: center; gap: 10px; flex: 1 1 200px; min-width: 0; }
|
||||
.proto-root .jc-rp-ava { flex: 0 0 auto; width: 30px; height: 30px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; line-height: 1; color: #fff; background: var(--brand); }
|
||||
.proto-root .jc-rp-sign > div { min-width: 0; }
|
||||
.proto-root .jc-rp-sign > div > b { display: block; font-size: 12px; font-weight: 700; color: var(--ink-2); }
|
||||
.proto-root .jc-rp-sign > div > span { display: block; margin-top: 1px; font-size: 10.5px; color: var(--ink-4); }
|
||||
.proto-root .jc-rp-acts { display: flex; align-items: center; gap: 8px; margin-left: auto; flex: none; }
|
||||
.proto-root .jc-hintline { font-size: 11px; font-weight: 700; color: var(--warn); line-height: 1.5; }
|
||||
|
||||
.proto-root .jc-btn { display: inline-flex; align-items: center; gap: 5px; border: 0; border-radius: 7px; padding: 8px 14px; font-size: 12px; font-weight: 600; font-family: inherit; background: var(--brand); color: #fff; cursor: pointer; transition: all var(--t-fast); }
|
||||
.proto-root .jc-btn:hover:not(:disabled) { background: var(--brand-deep); }
|
||||
.proto-root .jc-btn:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.proto-root .jc-btn-ghost { background: #fff; color: var(--ink-2); border: 1px solid var(--line); }
|
||||
.proto-root .jc-btn-ghost:hover:not(:disabled) { background: var(--brand-wash); color: var(--brand); border-color: var(--brand-soft); }
|
||||
.proto-root .jc-btn-primary { background: var(--ai-grad); }
|
||||
.proto-root .jc-btn-primary:hover:not(:disabled) { filter: brightness(1.07); box-shadow: var(--shadow-brand); transform: translateY(-1px); }
|
||||
|
||||
/* ── 来源信件:客户 RFQ 邮件原文(复用原型 jc-letter 件,青色 = 收件) ── */
|
||||
.proto-root .jc-letter { position: relative; border: 1px solid var(--cardline); border-left: 3px solid #0aa294; border-radius: 12px; padding: 14px 16px 15px; background: #fff; box-shadow: var(--card-shadow); }
|
||||
.proto-root .jc-letter-h { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 11px; }
|
||||
.proto-root .jc-letter-dir { display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px; font-weight: 800; padding: 3px 9px; border-radius: 999px; background: #e4f8f5; color: #0a7f74; }
|
||||
.proto-root .jc-letter-h .jc-chip { font-size: 10.5px; padding: 3px 9px; border-radius: 999px; background: var(--brand-wash); color: var(--brand-deep); border: 1px solid var(--brand-soft); }
|
||||
.proto-root .jc-letter-h > code { margin-left: auto; font-family: var(--mono); font-size: 10.5px; color: var(--ink-4); }
|
||||
.proto-root .jc-letter-meta { display: flex; flex-direction: column; gap: 4px; margin-bottom: 11px; padding-bottom: 11px; border-bottom: 1px solid var(--line-soft); font-size: 11.5px; color: var(--ink-2); }
|
||||
.proto-root .jc-letter-meta p { margin: 0; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
|
||||
.proto-root .jc-letter-meta i { flex: none; font-style: normal; width: 44px; font-size: 10px; font-weight: 800; letter-spacing: .06em; color: var(--ink-4); }
|
||||
.proto-root .jc-letter-meta code { font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); overflow-wrap: anywhere; }
|
||||
.proto-root .jc-letter-sub { margin: 0 0 9px; font-size: 14px; font-weight: 800; color: var(--ink); line-height: 1.45; overflow-wrap: anywhere; }
|
||||
/* 正文:原文照录,pre-wrap 保原始换行;不截断、不折叠 */
|
||||
.proto-root .jc-letter-body { margin: 0; font-size: 12.5px; line-height: 1.72; color: var(--ink-2); white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.proto-root .jc-letter-body b { color: var(--ink); font-weight: 700; }
|
||||
.proto-root .jc-atts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; padding-top: 11px; border-top: 1px dashed var(--line-soft); }
|
||||
.proto-root .jc-att { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px 5px 8px; border: 1px solid var(--brand-soft); border-radius: 8px; background: var(--brand-wash); font-size: 11.5px; color: var(--brand-deep); }
|
||||
.proto-root .jc-att svg { flex: none; color: var(--brand); }
|
||||
.proto-root .jc-att code { font-family: var(--mono); font-size: 10.5px; }
|
||||
.proto-root .jc-att span { color: var(--ink-4); }
|
||||
/* 抽取指向:从原文到下方要素的一句衔接 */
|
||||
.proto-root .jc-derive { display: flex; align-items: flex-start; gap: 8px; margin-top: 11px; padding: 9px 12px; border-radius: 9px; background: var(--brand-wash); font-size: 11.5px; line-height: 1.7; color: var(--brand-deep); }
|
||||
.proto-root .jc-derive svg { flex: none; margin-top: 2px; }
|
||||
|
||||
/* ── 四要素卡 ───────────────────────────────────────────────────────────
|
||||
色调字典:每张卡带一套 --tone / --tone-soft / --tone-line,组件只读变量。
|
||||
卡 3、卡 4 的色调随数据变(老客户绿 / 新客户橙;齐了蓝 / 有缺橙),
|
||||
所以这条要素带本身就是一行可读的状态,不只是四个格子。 */
|
||||
.proto-root .jc-four { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 11px; }
|
||||
.proto-root .jc-fcard {
|
||||
position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; gap: 10px; min-width: 0;
|
||||
padding: 14px 14px 14px; border: 1px solid var(--cardline); border-radius: 13px; background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(31,35,41,.05), 0 8px 20px -14px rgba(31,35,41,.35);
|
||||
transition: transform var(--t-fast), box-shadow var(--t-fast), border-color var(--t-fast);
|
||||
--tone: var(--brand); --tone-soft: rgba(51,112,255,.13); --tone-line: var(--brand-soft);
|
||||
}
|
||||
/* 顶缘色棱 + 右上角光晕:卡的身份色,一眼分得清四件是四件 */
|
||||
.proto-root .jc-fcard::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px; background: var(--tone); }
|
||||
.proto-root .jc-fcard::after { content: ""; position: absolute; right: -46px; top: -52px; width: 132px; height: 132px; border-radius: 999px; background: radial-gradient(circle, var(--tone-soft), transparent 70%); pointer-events: none; }
|
||||
.proto-root .jc-fcard > * { position: relative; z-index: 1; }
|
||||
.proto-root .jc-fcard:hover { transform: translateY(-2px); border-color: var(--tone-line); box-shadow: 0 2px 4px rgba(31,35,41,.05), 0 14px 26px -16px rgba(31,35,41,.45); }
|
||||
.proto-root .jc-fcard.t-teal { --tone: #0aa294; --tone-soft: rgba(10,162,148,.15); --tone-line: #9fe0d9; }
|
||||
.proto-root .jc-fcard.t-ok { --tone: var(--ok); --tone-soft: rgba(39,135,23,.14); --tone-line: #bfedb4; }
|
||||
.proto-root .jc-fcard.t-warn { --tone: var(--warn-accent); --tone-soft: rgba(255,136,0,.12); --tone-line: #ffd9a8; }
|
||||
|
||||
.proto-root .jc-fcard-k { display: flex; align-items: center; gap: 6px; font-size: 9.5px; font-weight: 800; letter-spacing: .12em; color: var(--tone); }
|
||||
.proto-root .jc-fcard-k > i { flex: none; width: 19px; height: 19px; border-radius: 6px; display: inline-grid; place-items: center; background: var(--tone-soft); color: var(--tone); }
|
||||
.proto-root .jc-fcard-b { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.proto-root .jc-fcard-t { min-width: 0; flex: 1; }
|
||||
/* 一律换行,不省略号——宁可多占一行,也不丢字 */
|
||||
.proto-root .jc-fcard-t > b { display: block; font-size: 14px; font-weight: 800; color: var(--ink); line-height: 1.35; overflow-wrap: anywhere; }
|
||||
.proto-root .jc-fcard-t > span { display: block; margin-top: 2px; font-size: 10.5px; color: var(--ink-4); line-height: 1.55; overflow-wrap: anywhere; }
|
||||
/* 公司标:圆角方 + AI 渐变 + 外圈光环(方=组织) */
|
||||
.proto-root .jc-logo { flex: none; width: 38px; height: 38px; border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 800; line-height: 1; letter-spacing: .04em; color: #fff; background: var(--ai-grad); box-shadow: var(--shadow-brand), 0 0 0 3px rgba(51,112,255,.10); }
|
||||
/* 人像:圆 + 青色系(圆=人,与方形的组织标一眼分开) */
|
||||
.proto-root .jc-face { flex: none; width: 38px; height: 38px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 800; line-height: 1; color: #0a7f74; background: #e4f8f5; border: 1px solid #9fe0d9; box-shadow: 0 0 0 3px rgba(10,162,148,.10); }
|
||||
/* 邮箱做成可点的胶囊,复制按钮嵌在里面——比一行裸文字有抓手 */
|
||||
/* 邮箱:软底胶囊、无描边——有描边会读成输入框,而读态这里不是可填的 */
|
||||
.proto-root .jc-mailrow { display: flex; align-items: center; gap: 4px; margin-top: 5px; min-width: 0; max-width: 100%; padding: 3px 4px 3px 9px; border: 0; border-radius: 8px; background: var(--tone-soft); transition: background var(--t-fast); }
|
||||
.proto-root .jc-mailrow:hover { background: rgba(10,162,148,.2); }
|
||||
/* 编辑态里它包着输入框,软底会变成「框中框」,去掉 */
|
||||
.proto-root .jc-report.editing .jc-mailrow { background: transparent; padding: 0; margin-top: 6px; }
|
||||
.proto-root .jc-mailrow > code { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; line-height: 1.5; font-family: var(--mono); font-size: 10.5px; color: var(--ink-2); }
|
||||
.proto-root .jc-copy { flex: none; width: 22px; height: 22px; display: inline-grid; place-items: center; border: 0; border-radius: 6px; background: #fff; color: var(--ink-4); cursor: pointer; box-shadow: 0 1px 2px rgba(31,35,41,.08); transition: all var(--t-fast); }
|
||||
.proto-root .jc-copy:hover { color: #0aa294; }
|
||||
.proto-root .jc-copy.ok { color: var(--ok); background: var(--ok-soft); box-shadow: none; }
|
||||
/* 客户属性徽章 */
|
||||
.proto-root .jc-tagbig { display: inline-flex; align-items: center; gap: 6px; align-self: flex-start; padding: 7px 14px; border-radius: 999px; font-size: 13.5px; font-weight: 800; line-height: 1; color: var(--tone); background: var(--tone-soft); border: 1px solid var(--tone-line); }
|
||||
.proto-root .jc-fcard-note { font-size: 10.5px; color: var(--ink-4); line-height: 1.55; }
|
||||
.proto-root .jc-fcard-note b { color: var(--ink-2); font-weight: 700; }
|
||||
.proto-root .jc-fcard-note b.bad { color: var(--tone); font-weight: 800; }
|
||||
/* 成交迷你柱:把「台账 6 笔成交」从一句话变成看得见的节奏 */
|
||||
.proto-root .jc-spark { display: flex; align-items: flex-end; gap: 4px; height: 26px; }
|
||||
.proto-root .jc-spark > i { flex: 1 1 0; min-width: 4px; max-width: 15px; border-radius: 3px 3px 1px 1px; background: var(--tone); opacity: .3; transition: opacity var(--t-fast), height var(--t-med); }
|
||||
.proto-root .jc-spark > i:last-child { opacity: 1; }
|
||||
.proto-root .jc-spark:hover > i { opacity: .55; }
|
||||
.proto-root .jc-spark:hover > i:last-child { opacity: 1; }
|
||||
.proto-root .jc-spark.empty { align-items: stretch; }
|
||||
.proto-root .jc-spark.empty > i { height: 100%; opacity: 1; background: repeating-linear-gradient(45deg, var(--tone-soft) 0 5px, transparent 5px 10px); border: 1px dashed var(--tone-line); border-radius: 5px; }
|
||||
/* 有效 PN 计数 */
|
||||
.proto-root .jc-count { display: flex; align-items: baseline; gap: 6px; }
|
||||
.proto-root .jc-count > b { font-family: var(--mono); font-size: 32px; font-weight: 800; line-height: .95; color: var(--ink); letter-spacing: -.04em; }
|
||||
.proto-root .jc-count > span { font-size: 11px; color: var(--ink-4); }
|
||||
/* 分段刻度条:一格 = 一条 PN,缺必填的那格直接标红——比一根实心进度条信息量大 */
|
||||
.proto-root .jc-segmeter { display: flex; gap: 3px; }
|
||||
.proto-root .jc-segmeter > i { flex: 1; height: 7px; border-radius: 2px; background: var(--ok); transition: background var(--t-fast); }
|
||||
.proto-root .jc-segmeter > i.bad { background: var(--danger); }
|
||||
.proto-root .jc-lockhint { display: inline-flex; align-items: center; gap: 4px; font-size: 10px; color: var(--ink-4); }
|
||||
|
||||
/* ── 占位胶囊:必填=红虚线,选填=灰虚线;**都带文字**,不靠颜色单打 ── */
|
||||
.proto-root .jc-ph { display: inline-flex; align-items: center; font-style: normal; font-size: 10px; font-weight: 700; line-height: 1; padding: 3px 7px; border-radius: 5px; border: 1px dashed; }
|
||||
.proto-root .jc-ph.req { color: var(--danger); background: var(--danger-soft); border-color: #f3b6b3; }
|
||||
.proto-root .jc-ph.opt { color: var(--ink-4); background: var(--soft); border-color: var(--line); }
|
||||
|
||||
/* ── 分段控件(新客户 / 老客户)── */
|
||||
.proto-root .jc-seg { display: inline-flex; padding: 2px; gap: 2px; border: 1px solid var(--cardline); border-radius: 9px; background: var(--soft); align-self: flex-start; }
|
||||
.proto-root .jc-seg button { border: 0; border-radius: 7px; padding: 6px 13px; font: inherit; font-size: 11.5px; font-weight: 700; color: var(--ink-3); background: transparent; cursor: pointer; transition: all var(--t-fast); }
|
||||
.proto-root .jc-seg button:hover:not(.on) { color: var(--brand); }
|
||||
.proto-root .jc-seg button.on { background: #fff; color: var(--ink); box-shadow: 0 1px 3px rgba(31,35,41,.12); }
|
||||
|
||||
/* ── 表:读态与编辑态共用骨架 ─────────────────────────────────────── */
|
||||
.proto-root .jc-tbl-wrap { overflow-x: auto; border: 1px solid var(--line-soft); border-radius: 10px; }
|
||||
.proto-root .jc-tbl-in { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 12px; }
|
||||
.proto-root .jc-tbl-in th { background: var(--soft); text-align: left; padding: 8px 9px; font-size: 10.5px; font-weight: 800; letter-spacing: .03em; color: var(--ink-3); white-space: nowrap; border-bottom: 1px solid var(--line-soft); }
|
||||
.proto-root .jc-tbl-in th.req::after { content: "*"; margin-left: 1px; color: var(--danger); font-size: 10px; }
|
||||
.proto-root .jc-tbl-in td { padding: 4px 5px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
|
||||
.proto-root .jc-tbl-in tr:last-child td { border-bottom: 0; }
|
||||
.proto-root .jc-tbl-in tbody tr:hover td { background: #f7f9fd; }
|
||||
/* 缺必填的整行浅红打底 + 行首红标——不用找红胶囊,一眼扫到是哪几行 */
|
||||
.proto-root .jc-tbl-in tr.miss td { background: #fffbfb; }
|
||||
.proto-root .jc-tbl-in tbody tr.miss:hover td { background: #fff5f5; }
|
||||
.proto-root .jc-tbl-in td.jc-rn { position: relative; width: 38px; padding: 4px 6px 4px 13px; white-space: nowrap; font-family: var(--mono); font-size: 10.5px; color: var(--ink-4); }
|
||||
.proto-root .jc-tbl-in tr.miss td.jc-rn { color: var(--danger); font-weight: 800; }
|
||||
/* 行首红标只在表态用 ::after —— 卡片态的 ::after 被「第 N 条」占着,不能抢 */
|
||||
@media (min-width: 961px) {
|
||||
.proto-root .jc-tbl-in tr.miss td.jc-rn::after { content: ""; position: absolute; left: 0; top: 4px; bottom: 4px; width: 3px; border-radius: 0 3px 3px 0; background: var(--danger); }
|
||||
}
|
||||
.proto-root .jc-rndot { display: inline-block; width: 5px; height: 5px; margin-left: 4px; border-radius: 999px; background: var(--danger); vertical-align: 2px; }
|
||||
/* 读态单元格 */
|
||||
.proto-root .jc-vcell { display: block; padding: 6px 6px; font-size: 12px; color: var(--ink); line-height: 1.5; overflow-wrap: anywhere; }
|
||||
.proto-root .jc-vcell.mono { font-family: var(--mono); font-size: 11px; color: var(--ink-2); }
|
||||
.proto-root .jc-vcell.num { font-family: var(--mono); font-size: 11.5px; text-align: right; }
|
||||
/* 编辑态单元格:安静的电子表格 */
|
||||
.proto-root .jc-cell { width: 100%; min-width: 64px; border: 1px solid var(--line); border-radius: 6px; padding: 6px 7px; font: inherit; font-size: 12px; color: var(--ink); background: #fff; transition: border-color var(--t-fast), box-shadow var(--t-fast); }
|
||||
.proto-root .jc-cell.mono { font-family: var(--mono); font-size: 11px; }
|
||||
.proto-root .jc-cell.num { text-align: right; font-family: var(--mono); font-size: 11.5px; }
|
||||
.proto-root .jc-cell::placeholder { color: var(--ink-4); }
|
||||
.proto-root .jc-cell:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-wash); outline: none; }
|
||||
.proto-root .jc-cell.req-empty { border-color: #f3b6b3; background: #fffafa; }
|
||||
.proto-root .jc-cell.req-empty::placeholder { color: var(--danger); }
|
||||
/* 备注这类长文本用文本域:能换行、随内容长高,不裁字 */
|
||||
.proto-root textarea.jc-cell { display: block; resize: vertical; overflow: hidden; min-height: 31px; line-height: 1.55; }
|
||||
|
||||
/* 编辑态的整表提示带 */
|
||||
.proto-root .jc-editbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; padding: 9px 12px; border-radius: 9px; background: var(--brand-wash); border: 1px solid var(--brand-soft); font-size: 11.5px; color: var(--brand-deep); }
|
||||
.proto-root .jc-editbar svg { flex: none; }
|
||||
.proto-root .jc-editbar .sp { flex: 1 1 auto; }
|
||||
|
||||
/* 剔除行说明 */
|
||||
.proto-root .jc-drop { display: flex; align-items: flex-start; gap: 8px; margin-top: 10px; padding: 9px 12px; border-radius: 9px; border: 1px dashed var(--line); background: var(--soft); font-size: 11.5px; color: var(--ink-3); line-height: 1.6; }
|
||||
.proto-root .jc-drop svg { flex: none; margin-top: 2px; color: var(--ink-4); }
|
||||
.proto-root .jc-drop code { font-family: var(--mono); font-size: 10.5px; color: var(--ink-2); }
|
||||
|
||||
.proto-root .jc-callout { display: flex; gap: 8px; align-items: flex-start; border-radius: 9px; padding: 11px 13px; font-size: 12.5px; line-height: 1.75; }
|
||||
.proto-root .jc-callout p { margin: 0; }
|
||||
.proto-root .jc-callout svg { flex: none; margin-top: 2px; }
|
||||
.proto-root .jc-callout.t-mute { background: var(--soft); color: var(--ink-2) }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.proto-root .jc-pulse-dot { animation: none; }
|
||||
.proto-root .jc-report, .proto-root .jc-rp-stamp { animation: none; }
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
响应式:宁可另起一行,也不损失完整度
|
||||
≥961px 六列全展开的表(列 min-width 之和 878px,容器有余量,绝不裁字、绝不横滚)
|
||||
≤960px 表 → 卡片:一条 PN 一张卡、一行一字段(标签 / 值),无横滚、无省略
|
||||
≤560px 字段行再拆成上下两行(标签在上、值在下)
|
||||
══════════════════════════════════════════════════════════════════════════ */
|
||||
@media (max-width: 1040px) { .proto-root .jc-four { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 560px) { .proto-root .jc-four { grid-template-columns: 1fr; } }
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.proto-root .jc-tbl-wrap { overflow: visible; border: 0; border-radius: 0; background: transparent; }
|
||||
.proto-root .jc-tbl-in,
|
||||
.proto-root .jc-tbl-in tbody,
|
||||
.proto-root .jc-tbl-in tr,
|
||||
.proto-root .jc-tbl-in td { display: block; width: 100%; }
|
||||
.proto-root .jc-tbl-in thead { display: none; }
|
||||
|
||||
/* 一条 PN = 一张卡 */
|
||||
.proto-root .jc-tbl-in tr { margin-bottom: 10px; padding: 4px 13px 9px; border: 1px solid var(--line-soft); border-radius: 11px; background: #fff; box-shadow: var(--card-shadow); }
|
||||
.proto-root .jc-tbl-in tr:last-child { margin-bottom: 0; }
|
||||
.proto-root .jc-tbl-in tbody tr:hover td { background: transparent; }
|
||||
.proto-root .jc-tbl-in tr.miss { border-color: #f3b6b3; background: #fffbfb; }
|
||||
|
||||
/* 卡头:第 N 条 */
|
||||
.proto-root .jc-tbl-in td.jc-rn { padding: 8px 0 8px; margin-bottom: 4px; border-bottom: 1px solid var(--line-soft); font-family: inherit; font-size: 12px; font-weight: 800; color: var(--ink-2); }
|
||||
.proto-root .jc-tbl-in td.jc-rn::before { content: "第 "; }
|
||||
.proto-root .jc-tbl-in td.jc-rn::after { content: " 条"; }
|
||||
.proto-root .jc-tbl-in tr.miss td.jc-rn { color: var(--danger); }
|
||||
|
||||
/* 字段行:左标签 + 右值,值超长自动折行 */
|
||||
.proto-root .jc-tbl-in td:not(.jc-rn) { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 10px; align-items: center; padding: 6px 0; border-bottom: 1px dashed var(--line-soft); }
|
||||
.proto-root .jc-tbl-in tr td:last-child { border-bottom: 0; padding-bottom: 2px; }
|
||||
.proto-root .jc-tbl-in td:not(.jc-rn)::before {
|
||||
content: attr(data-h); font-size: 10.5px; font-weight: 700; letter-spacing: .02em; color: var(--ink-4); line-height: 1.5;
|
||||
}
|
||||
.proto-root .jc-tbl-in td[data-req="1"]:not(.jc-rn)::before { content: attr(data-h) " *"; }
|
||||
.proto-root .jc-tbl-in .jc-vcell { padding: 0; overflow-wrap: anywhere; }
|
||||
.proto-root .jc-tbl-in .jc-vcell.num { text-align: left; }
|
||||
.proto-root .jc-tbl-in .jc-cell.num { text-align: left; }
|
||||
.proto-root .jc-tbl-in .jc-cell { min-width: 0; }
|
||||
}
|
||||
|
||||
/* 窄屏:落款三件各占一行,动作按钮撑满一行 */
|
||||
@media (max-width: 560px) {
|
||||
.proto-root .jc-rp-acts { margin-left: 0; width: 100%; flex-wrap: wrap; }
|
||||
.proto-root .jc-rp-acts .jc-btn { flex: 1 1 auto; justify-content: center; }
|
||||
.proto-root .jc-hintline { flex: 1 1 100%; }
|
||||
.proto-root .jc-tbl-in td:not(.jc-rn) { grid-template-columns: 1fr; gap: 3px; align-items: start; }
|
||||
}
|
||||
|
||||
|
||||
/* ══ 以下取自 A1-询价受理记录.html(归档件独有的件)══ */
|
||||
/* ── 料号分类:分布条 + 图例 ───────────────────────────────────────── */
|
||||
.proto-root .jc-dist { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding: 13px 15px; border: 1px solid var(--line-soft); border-radius: 12px; background: linear-gradient(180deg, #fbfcfe, #fff); }
|
||||
.proto-root .jc-distbar { flex: 1 1 320px; min-width: 260px; display: flex; gap: 3px; }
|
||||
.proto-root .jc-distbar > i { height: 30px; border-radius: 4px; display: grid; place-items: center; font-size: 10.5px; font-weight: 800; color: #fff; font-family: var(--mono); }
|
||||
.proto-root .jc-distbar > i.a { background: var(--ok); }
|
||||
.proto-root .jc-distbar > i.b { background: var(--brand); }
|
||||
.proto-root .jc-distbar > i.c { background: var(--warn-accent); }
|
||||
.proto-root .jc-distlg { display: flex; flex-wrap: wrap; gap: 8px 18px; }
|
||||
.proto-root .jc-distlg > span { display: inline-flex; align-items: center; gap: 7px; font-size: 11.5px; color: var(--ink-3); }
|
||||
.proto-root .jc-distlg em { width: 10px; height: 10px; border-radius: 3px; }
|
||||
.proto-root .jc-distlg em.a { background: var(--ok); } .proto-root .jc-distlg em.b { background: var(--brand); } .proto-root .jc-distlg em.c { background: var(--warn-accent); }
|
||||
.proto-root .jc-distlg b { font-family: var(--mono); font-weight: 800; color: var(--ink); }
|
||||
|
||||
/* 类别徽 A / B / C */
|
||||
.proto-root .jc-tbadge { display: inline-grid; place-items: center; width: 20px; height: 20px; border-radius: 6px; font-family: var(--mono); font-size: 11px; font-weight: 800; line-height: 1; color: #fff; }
|
||||
.proto-root .jc-tbadge.a { background: var(--ok); } .proto-root .jc-tbadge.b { background: var(--brand); } .proto-root .jc-tbadge.c { background: var(--warn-accent); }
|
||||
|
||||
/* ── 结论与去向 ─────────────────────────────────────────────────────────
|
||||
G2 是**分支网关**:三条互斥条件按序求值,只命中一条、只落一个活动。
|
||||
所以这里不是三张并列的去向卡,而是一张**判定表 + 一条结论**。 */
|
||||
/* 判定表:一行一条规则,命中行上色留痕(DMN 口径) */
|
||||
.proto-root .jc-dmn { border: 1px solid var(--line-soft); border-radius: 12px; overflow: hidden; }
|
||||
.proto-root .jc-dmn-h, .proto-root .jc-dmn-r { display: grid; grid-template-columns: 40px minmax(0, 1fr) 170px 200px; align-items: center; gap: 12px; padding: 9px 14px; }
|
||||
.proto-root .jc-dmn-h { background: var(--soft); border-bottom: 1px solid var(--line-soft); font-size: 10px; font-weight: 800; letter-spacing: .08em; color: var(--ink-4); }
|
||||
.proto-root .jc-dmn-r { font-size: 12px; color: var(--ink-4); background: #fff; }
|
||||
.proto-root .jc-dmn-r + .jc-dmn-r { border-top: 1px solid var(--line-soft); }
|
||||
.proto-root .jc-dmn-i { font-family: var(--mono); font-size: 11px; font-weight: 800; }
|
||||
.proto-root .jc-dmn-c { line-height: 1.6; overflow-wrap: anywhere; }
|
||||
.proto-root .jc-dmn-t { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; }
|
||||
.proto-root .jc-dmn-t code { font-family: var(--mono); font-size: 10.5px; font-weight: 800; color: var(--ink-4); background: var(--soft); border: 1px solid var(--line); border-radius: 5px; padding: 2px 6px; }
|
||||
.proto-root .jc-dmn-v { font-size: 11px; line-height: 1.55; }
|
||||
/* 命中行:上色 + 左缘棱 + 加粗 */
|
||||
.proto-root .jc-dmn-r.hit { position: relative; background: var(--tone-soft); color: var(--ink-2); }
|
||||
.proto-root .jc-dmn-r.hit::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--tone); }
|
||||
.proto-root .jc-dmn-r.hit .jc-dmn-i { color: var(--tone); }
|
||||
.proto-root .jc-dmn-r.hit .jc-dmn-c { color: var(--ink); }
|
||||
.proto-root .jc-dmn-r.hit .jc-dmn-c b { color: var(--tone); font-weight: 800; }
|
||||
.proto-root .jc-dmn-r.hit .jc-dmn-t { color: var(--ink); }
|
||||
.proto-root .jc-dmn-r.hit .jc-dmn-t code { color: #fff; background: var(--tone); border-color: var(--tone); }
|
||||
.proto-root .jc-dmn-v.y { display: inline-flex; align-items: center; gap: 5px; font-weight: 800; color: var(--tone); }
|
||||
.proto-root .jc-dmn-v.n { color: var(--ink-4); }
|
||||
/* 色调随命中类别 */
|
||||
.proto-root .jc-dmn.k-a { --tone: var(--ok); --tone-soft: rgba(39,135,23,.10); }
|
||||
.proto-root .jc-dmn.k-b { --tone: var(--brand); --tone-soft: rgba(51,112,255,.09); }
|
||||
.proto-root .jc-dmn.k-c { --tone: var(--warn-accent); --tone-soft: rgba(255,136,0,.11); }
|
||||
|
||||
/* 结论横幅:命中条件 → 唯一去向 */
|
||||
.proto-root .jc-verdict {
|
||||
position: relative; overflow: hidden; display: flex; align-items: stretch; flex-wrap: wrap; gap: 16px;
|
||||
margin-top: 11px; padding: 16px 18px; border: 1px solid var(--tone-line); border-radius: 13px;
|
||||
background: linear-gradient(105deg, var(--tone-soft), #fff 62%);
|
||||
box-shadow: 0 1px 2px rgba(31,35,41,.05), 0 10px 24px -16px rgba(31,35,41,.4);
|
||||
--tone: var(--warn-accent); --tone-soft: rgba(255,136,0,.14); --tone-line: #ffd9a8;
|
||||
}
|
||||
.proto-root .jc-verdict.k-a { --tone: var(--ok); --tone-soft: rgba(39,135,23,.13); --tone-line: #bfedb4; }
|
||||
.proto-root .jc-verdict.k-b { --tone: var(--brand); --tone-soft: rgba(51,112,255,.13); --tone-line: var(--brand-soft); }
|
||||
.proto-root .jc-verdict::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px; background: var(--tone); }
|
||||
.proto-root .jc-verdict > * { position: relative; z-index: 1; }
|
||||
.proto-root .jc-vd-l { flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column; gap: 7px; }
|
||||
.proto-root .jc-vd-k { font-size: 9.5px; font-weight: 800; letter-spacing: .12em; color: var(--tone); }
|
||||
.proto-root .jc-vd-cond { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 15px; font-weight: 800; color: var(--ink); }
|
||||
.proto-root .jc-vd-why { margin: 0; font-size: 11.5px; line-height: 1.7; color: var(--ink-3); overflow-wrap: anywhere; }
|
||||
.proto-root .jc-vd-why code { font-family: var(--mono); font-size: 10.5px; color: var(--ink); }
|
||||
.proto-root .jc-vd-arrow { flex: none; align-self: center; color: var(--tone); }
|
||||
.proto-root .jc-vd-r { flex: 1 1 260px; min-width: 0; display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-radius: 11px; background: #fff; border: 1px solid var(--tone-line); }
|
||||
.proto-root .jc-vd-node { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 800; color: var(--ink); }
|
||||
.proto-root .jc-vd-node code { font-family: var(--mono); font-size: 12px; font-weight: 800; color: #fff; background: var(--tone); border-radius: 6px; padding: 3px 8px; }
|
||||
.proto-root .jc-vd-who { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--ink-2); }
|
||||
.proto-root .jc-vd-who > i { width: 20px; height: 20px; border-radius: 999px; display: inline-grid; place-items: center; font-style: normal; font-size: 10px; font-weight: 700; color: #fff; background: var(--tone); }
|
||||
.proto-root .jc-vd-next { margin: 0; padding-top: 8px; border-top: 1px dashed var(--line-soft); font-size: 10.5px; line-height: 1.65; color: var(--ink-4); }
|
||||
.proto-root .jc-vd-next b { color: var(--ink-2); font-weight: 700; }
|
||||
/* 命中分支携带的条目 */
|
||||
.proto-root .jc-pnlist { display: flex; flex-wrap: wrap; gap: 5px; margin: 0; padding: 0; list-style: none; }
|
||||
.proto-root .jc-pnlist > li { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 999px; background: #fff; border: 1px solid var(--tone-line); font-size: 11px; color: var(--ink-2); overflow-wrap: anywhere; }
|
||||
.proto-root .jc-pnlist > li > i { flex: none; width: 5px; height: 5px; border-radius: 999px; background: var(--tone); }
|
||||
.proto-root .jc-pnlist code { font-family: var(--mono); font-size: 10.5px; color: var(--ink); }
|
||||
|
||||
/* ── 落地动作 ───────────────────────────────────────────────────────── */
|
||||
.proto-root .jc-acts { display: flex; flex-direction: column; gap: 0; border: 1px solid var(--line-soft); border-radius: 11px; overflow: hidden; }
|
||||
.proto-root .jc-act { display: flex; align-items: flex-start; gap: 9px; padding: 10px 13px; font-size: 12px; color: var(--ink-2); line-height: 1.6; background: #fff; }
|
||||
.proto-root .jc-act + .jc-act { border-top: 1px solid var(--line-soft); }
|
||||
.proto-root .jc-act > i { flex: none; width: 18px; height: 18px; margin-top: 1px; border-radius: 999px; display: inline-grid; place-items: center; color: #fff; background: var(--ok); }
|
||||
.proto-root .jc-act b { font-weight: 700; color: var(--ink); }
|
||||
.proto-root .jc-act span { color: var(--ink-4); }
|
||||
.proto-root .jc-act code { font-family: var(--mono); font-size: 10.5px; color: var(--brand-deep); background: var(--brand-wash); border-radius: 4px; padding: 1px 5px; }
|
||||
|
||||
|
||||
.proto-root .jc-done-tag { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; flex: none; padding: 6px 13px; border-radius: 999px; font-size: 11.5px; font-weight: 800; color: var(--ok); background: #fff; border: 1px solid #bfedb4; }
|
||||
.proto-root .jc-vcell.dim { color: var(--ink-4); }
|
||||
.proto-root .jc-tbl-in td.jc-ty { width: 44px; text-align: center; }
|
||||
.proto-root .jc-tbl-in td.jc-ty { width: auto; text-align: left; }
|
||||
`;
|
||||
|
||||
/**
|
||||
* 图标精灵 —— 与样式表同一出处,两份目标件的 `<symbol>` 取并集。
|
||||
* 沙箱里取不到图标字体,所以图标一律内联;`<use href="#i-xxx">` 引的就是这一坨。
|
||||
*/
|
||||
export const PRD_SPRITE = `<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs><symbol id="i-alert" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></symbol><symbol id="i-arrow-right" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></symbol><symbol id="i-boxes" viewBox="0 0 24 24"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></symbol><symbol id="i-building" viewBox="0 0 24 24"><path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"/><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/><path d="M10 6h4"/><path d="M10 10h4"/><path d="M10 14h4"/><path d="M10 18h4"/></symbol><symbol id="i-check" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></symbol><symbol id="i-copy" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></symbol><symbol id="i-file-check" viewBox="0 0 24 24"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="m9 15 2 2 4-4"/></symbol><symbol id="i-file-text" viewBox="0 0 24 24"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></symbol><symbol id="i-git-branch" viewBox="0 0 24 24"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-history" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></symbol><symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></symbol><symbol id="i-lock" viewBox="0 0 24 24"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol><symbol id="i-mail-open" viewBox="0 0 24 24"><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/></symbol><symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z"/></symbol><symbol id="i-plus" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></symbol><symbol id="i-sheet" viewBox="0 0 24 24"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M8 13h2"/><path d="M14 13h2"/><path d="M8 17h2"/><path d="M14 17h2"/></symbol><symbol id="i-shield-check" viewBox="0 0 24 24"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></symbol><symbol id="i-user" viewBox="0 0 24 24"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/></symbol><symbol id="i-x" viewBox="0 0 24 24"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></symbol></defs></svg>`;
|
||||
@@ -1,192 +1,579 @@
|
||||
/**
|
||||
* A4 成本定价的 HTML 报告模版 —— **活动自己持有的资产**(§10.3),随包版本冻结。
|
||||
* A4 成本定价的 HTML 报告模版 —— **活动自己持有的资产**,随包版本冻结。
|
||||
*
|
||||
* 定价这件事最需要「把判据摊开」:**人签的不是一个数字,是这个数字的来路**。
|
||||
* 所以三张屏不按「概览 / 明细 / 结论」排,按**一个价是怎么长出来的**排:
|
||||
* ① 取价基准 —— 跟谁比。基准取错,后面每一条偏离率都是假的,而假偏离率长得跟真的一样;
|
||||
* ② 定价与底价门禁 —— 凭什么是这个价。两张决策表的规则原样摆出来,人能逐条倒着核;
|
||||
* ③ 报价草案待放行 —— 签字前最后一次能改的机会,门后的都算数了。
|
||||
* 两个局面各一张(与 A2/A3 同款口径):
|
||||
* · `pricing-worksheet` **定价工作单** —— 算价与 BD 核价期间摆它:逐颗料一张定价档案卡
|
||||
* (题面在左、定价推演在右)、利润模型 P-01 的毛利档、底价门禁 P-02 的三档尺、
|
||||
* 填回工作表就长在同一屏;
|
||||
* · `pricing-roster` **报价草案清单** —— 超限拍板、放行与归档摆它:整份草案只读,
|
||||
* 门禁分布与落地动作在上面。
|
||||
*
|
||||
* 一个局面一张,局面变了让 Agent 用 `view` 换,不在一张里堆条件分支。
|
||||
* 容器是 `sandbox=""` iframe:无脚本、无外链、样式自带;数据由基座推送时填
|
||||
* (`data-slot` 取值、`data-rows` 逐行展开)。
|
||||
* 「1:1」的落点是**每份件与它的办结快照**(在办可编辑 ↔ 回执收下那一刻的 finalHtml),
|
||||
* 不是全程只许一张:作业的件与收口的件是两件工作两份件(规范姊妹篇 §1)。
|
||||
*
|
||||
* 两条贯穿全包的呈现纪律:
|
||||
* · **状态章一律用「在办」灯,不用「已办」印** —— 模版是随包冻结的静态字符串,
|
||||
* 它盖不了一个还没发生的章。三张屏都是放行前的局面,盖「已通过」就是撒谎;
|
||||
* · **`verdict` 的橙档专指「还没发生,等你按」** —— 绿只留给已达标的机检结论。
|
||||
* 放行后果是预告不是既成事实,一律走橙档。
|
||||
* 版面语言沿用 A1 从 PRD 目标件机械抽取的那套(`prd-style.ts` 原样拷入)——
|
||||
* A4 没有自己的 PRD 目标件,与 A2/A3 同一处置:沿用家族版面自造。
|
||||
*
|
||||
* 取值三纪律:顶层只取汇总列与单值列;逐条走 `mine:quote_pricing.draft`;
|
||||
* 列名逐字照 `declarations.ledger` 抄。**模版不算不判** —— 缺价红带、门禁徽、分布条
|
||||
* 全部由基座算好的数经 `data-class-of` 投影,判断留在样式表。
|
||||
*/
|
||||
import { noteOnly, section, stagesOnly, statsBand, tableOnly, verdict, viewShell } from '@sdk';
|
||||
import { PRD_CSS, PRD_SPRITE } from './prd-style.ts';
|
||||
|
||||
/**
|
||||
* 静态阶梯 —— **规则本身不是数据**:P-01 的毛利档、P-02 的三档处置随包冻结,
|
||||
* 它们不该走 `data-rows` 等基座来填(那会让「规则」看起来像「这一单的结果」)。
|
||||
* 沿用外壳已有的 `.steps` 版式,与引擎推的阶段列表同一副面孔 —— 页面上只该有一套样式。
|
||||
*/
|
||||
function ladder(rows: ReadonlyArray<readonly [string, string, string]>): string {
|
||||
return `<ul class="steps">${rows
|
||||
.map(
|
||||
([no, term, gloss]) =>
|
||||
`<li><span class="n">${no}</span><span><span class="t">${term}</span> <span class="d">${gloss}</span></span></li>`,
|
||||
)
|
||||
.join('')}</ul>`;
|
||||
/* ==================== 绑定期补的样式(照 A1/A3 的 BIND_CSS,地址换成本活动)==================== */
|
||||
|
||||
const BIND_CSS = `
|
||||
/* 空占位三级(2026-08-02 全局口径):必填红 / 建议填橘 / 选填灰 —— 值真的空才轮到 */
|
||||
.proto-root .jc-tbl-in td.req > .jc-vcell:empty::after,
|
||||
.proto-root .jc-tbl-in td.sug > .jc-vcell:empty::after,
|
||||
.proto-root .jc-tbl-in td.opt > .jc-vcell:empty::after {
|
||||
display: inline-flex; align-items: center; font-style: normal; font-size: 10px; font-weight: 700;
|
||||
line-height: 1; padding: 3px 7px; border-radius: 5px; border: 1px dashed;
|
||||
}
|
||||
.proto-root .jc-tbl-in td.req > .jc-vcell:empty::after { content: "必填"; color: var(--danger); background: var(--danger-soft); border-color: #f3b6b3; }
|
||||
.proto-root .jc-tbl-in td.sug > .jc-vcell:empty::after { content: "建议填"; color: var(--warn-accent); background: var(--warn-soft); border-color: #f0cf9a; }
|
||||
.proto-root .jc-tbl-in td.opt > .jc-vcell:empty::after { content: "选填"; color: var(--ink-4); background: var(--soft); border-color: var(--line); }
|
||||
/* 归档件里空格子就是「没有」,不再摆「你还能填」的胶囊 */
|
||||
.proto-root .jc-tbl-in td.dimempty > .jc-vcell:empty::after { content: "—"; color: var(--ink-4); border: 0; background: none; padding: 0; }
|
||||
|
||||
/* 缺拟报价的整行浅红 + 行首红条 —— 原型的 tr.miss,这里由 :has() 认 */
|
||||
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty) td { background: #fffbfb; }
|
||||
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty):hover td { background: #fff5f5; }
|
||||
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty) td.jc-rn { color: var(--danger); font-weight: 800; position: relative; }
|
||||
@media (min-width: 1001px) {
|
||||
.proto-root .jc-tbl-in tbody tr:has(td.req > .jc-vcell:empty) td.jc-rn::after {
|
||||
content: ""; position: absolute; left: 0; top: 4px; bottom: 4px; width: 3px; border-radius: 0 3px 3px 0; background: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
/** 局面一:S1 算价刚开场 —— 人此刻要核的是「这一单跟谁比、落在哪一档」 */
|
||||
export const PRICING_BASIS = viewShell({
|
||||
title: '取价基准',
|
||||
kicker: 'A4 · 成本定价 · S1 算价',
|
||||
tone: 'brand',
|
||||
icon: 'coins',
|
||||
stamp: { kind: 'live', text: '算价中' },
|
||||
sign: 'A4 成本定价 · 取价基准(本单草稿)',
|
||||
/* 序号由 CSS 计数器排 */
|
||||
.proto-root .jc-tbl-in tbody { counter-reset: rn; }
|
||||
.proto-root .jc-tbl-in td.jc-rn::before { counter-increment: rn; content: counter(rn); }
|
||||
@media (max-width: 1000px) {
|
||||
.proto-root .jc-tbl-in td.jc-rn::before { content: "第 " counter(rn) " 条"; }
|
||||
.proto-root .jc-tbl-in td.jc-rn::after { content: none; }
|
||||
}
|
||||
|
||||
/* 就地改:可改格子渲了只读与输入两份,默认锁着,表上加 on 才换孪生件 */
|
||||
.proto-root .jc-cell { display: none; }
|
||||
.proto-root .jc-tbl-in.on td > .jc-vcell,
|
||||
.proto-root .jc-tbl-in.on td > .jc-tbadge { display: none; }
|
||||
.proto-root .jc-tbl-in.on td > .jc-cell { display: block; }
|
||||
/* 类徽与门禁徽不进编辑态(承继与引擎产物),on 时也照旧摆着 */
|
||||
.proto-root .jc-tbl-in.on td > .jc-obadge, .proto-root .jc-tbl-in.on td > .jc-gbadge { display: inline-flex; }
|
||||
/* 数值列与表头同侧左对齐(一行一个料,彼此不相加) */
|
||||
.proto-root .jc-vcell.num, .proto-root .jc-cell.num { text-align: left; }
|
||||
/* 编辑态下必填空格子:红框 + 整行浅红(由 :placeholder-shown 认) */
|
||||
.proto-root .jc-tbl-in.on td.req > .jc-cell:placeholder-shown { border-color: #f3b6b3; background: #fffafa; }
|
||||
.proto-root .jc-tbl-in.on tbody tr:has(td.req > .jc-cell:placeholder-shown) td { background: #fffbfb; }
|
||||
|
||||
/* 铺满工作区 */
|
||||
.jc-stage { max-width: none; padding: 0 0 4px; }
|
||||
.proto-root .jc-report { border: 0; border-radius: 0; box-shadow: none; }
|
||||
|
||||
/* 那道闸 —— 默认「已锁定 + 编辑」,开了变成「取消 / 保存」;只在可改屏现身 */
|
||||
.proto-root .jc-editgate { display: none; }
|
||||
html.editable .proto-root .jc-editgate {
|
||||
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||
margin-bottom: 10px; padding: 8px 12px; border-radius: 9px;
|
||||
background: var(--soft); border: 1px solid var(--line-soft);
|
||||
font-size: 11.5px; color: var(--ink-3);
|
||||
}
|
||||
.proto-root .jc-editgate-t { display: inline-flex; align-items: center; gap: 6px; flex: 1 1 auto; min-width: 0; }
|
||||
.proto-root .jc-editgate-t b { color: var(--ink-2); font-weight: 700; }
|
||||
.proto-root .jc-egbtn {
|
||||
flex: none; display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
|
||||
border: 1px solid var(--line); border-radius: 7px; padding: 5px 11px;
|
||||
font: inherit; font-size: 11.5px; font-weight: 700; color: var(--ink-2); background: #fff;
|
||||
transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast);
|
||||
}
|
||||
.proto-root .jc-egbtn:hover { border-color: var(--brand); color: var(--brand); }
|
||||
.proto-root .jc-egbtn.save { color: #fff; background: var(--brand); border-color: var(--brand); }
|
||||
.proto-root .jc-egbtn.save:hover { background: var(--brand-deep); border-color: var(--brand-deep); }
|
||||
.proto-root .jc-editgate .jc-egbtn.ghost, .proto-root .jc-editgate .jc-egbtn.save { display: none; }
|
||||
.proto-root .jc-editgate.on { background: var(--brand-wash); border-color: var(--brand-soft); color: var(--brand-deep); }
|
||||
.proto-root .jc-editgate.on .jc-egbtn.open { display: none; }
|
||||
.proto-root .jc-editgate.on .jc-egbtn.ghost, .proto-root .jc-editgate.on .jc-egbtn.save { display: inline-flex; }
|
||||
.proto-root .jc-editgate.on .jc-editgate-t { visibility: hidden; }
|
||||
|
||||
/* 缺价红带 —— 只在可改屏出,一格不缺自动收(v-0 那一档 display:none) */
|
||||
.proto-root .jc-gapbar { display: none; }
|
||||
html.editable .proto-root .jc-gapbar {
|
||||
display: flex; align-items: flex-start; gap: 8px;
|
||||
margin: 0 0 10px; padding: 9px 12px; border-radius: 9px;
|
||||
background: #fff5f5; border: 1px solid #f3b6b3; color: var(--danger);
|
||||
font-size: 12px; line-height: 1.65;
|
||||
}
|
||||
.proto-root .jc-gapbar svg { flex: 0 0 auto; margin-top: 2px; }
|
||||
.proto-root .jc-gapbar b { font-weight: 800; }
|
||||
.proto-root .jc-gapbar span { color: #9a3b36; }
|
||||
html.editable .proto-root .jc-gapbar.v-0 { display: none; }
|
||||
|
||||
/* 那条「怎么改」的提示只在开了闸之后说 */
|
||||
.proto-root .jc-editbar { display: none; }
|
||||
.proto-root .jc-editgate.on + .jc-editbar { display: flex; }
|
||||
|
||||
/* 增删行 */
|
||||
.proto-root .jc-rowdel { display: none; }
|
||||
.proto-root .jc-tbl-in.on td.jc-rn::before { content: none; }
|
||||
.proto-root .jc-tbl-in.on .jc-rowdel {
|
||||
display: block; width: 20px; height: 20px; margin: 0 auto; cursor: pointer;
|
||||
border: 1px solid var(--line); border-radius: 6px; padding: 0;
|
||||
font: inherit; font-size: 13px; line-height: 1; color: var(--ink-3); background: #fff;
|
||||
transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast);
|
||||
}
|
||||
.proto-root .jc-tbl-in.on .jc-rowdel:hover { border-color: var(--danger); color: var(--danger); background: #fff6f6; }
|
||||
.proto-root .jc-tbl-in.on tr.jc-row-gone td { background: #fff6f6; opacity: .55; }
|
||||
.proto-root .jc-tbl-in.on tr.jc-row-gone .jc-cell { text-decoration: line-through; color: var(--ink-3); }
|
||||
.proto-root .jc-tbl-in.on tr.jc-row-gone .jc-rowdel { border-color: var(--danger); color: var(--danger); background: #fff; }
|
||||
.proto-root .jc-tbl-in.on tr.jc-row-new td { background: #f2fbf7; }
|
||||
.proto-root .jc-addrow { display: none; }
|
||||
html.editable .proto-root .jc-tbl-wrap:has(table.on) + .jc-addrow {
|
||||
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
||||
margin: -2px 0 12px; padding: 7px 12px; border-radius: 9px;
|
||||
background: var(--soft); border: 1px dashed var(--line);
|
||||
}
|
||||
.proto-root .jc-addrow em { font-style: normal; font-size: 11px; color: var(--ink-3); }
|
||||
|
||||
/* 类徽按值上色(承继上游的三分类) */
|
||||
.proto-root .jc-tbadge { background: var(--ink-4); }
|
||||
.proto-root .jc-tbadge.v-a { background: var(--ok); }
|
||||
.proto-root .jc-tbadge.v-b { background: var(--brand); }
|
||||
.proto-root .jc-tbadge.v-c { background: var(--warn-accent); }
|
||||
.proto-root .jc-segmeter > i.v-pass { background: var(--ok); }
|
||||
.proto-root .jc-segmeter > i.v-note { background: var(--warn-accent); }
|
||||
.proto-root .jc-segmeter > i.v-block { background: var(--danger); }
|
||||
|
||||
/* 门禁徽:直接放行绿 / 附理由橙 / 拦截红 —— 值(PASS/NOTE/BLOCK)是机器码,人读名交给样式表 */
|
||||
.proto-root .jc-gbadge {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
min-width: 56px; height: 20px; padding: 0 7px; border-radius: 6px;
|
||||
font-size: 10px; font-weight: 800; letter-spacing: .02em;
|
||||
color: var(--ink-4); border: 1.5px solid var(--ink-4); background: #fff;
|
||||
}
|
||||
.proto-root .jc-gbadge::after { content: "待判"; }
|
||||
.proto-root .jc-gbadge.v-pass { color: var(--ok); border-color: var(--ok); }
|
||||
.proto-root .jc-gbadge.v-pass::after { content: "直接放行"; }
|
||||
.proto-root .jc-gbadge.v-note { color: var(--warn-accent); border-color: var(--warn-accent); }
|
||||
.proto-root .jc-gbadge.v-note::after { content: "附理由"; }
|
||||
.proto-root .jc-gbadge.v-block { color: var(--danger); border-color: var(--danger); }
|
||||
.proto-root .jc-gbadge.v-block::after { content: "拦截待拍板"; }
|
||||
|
||||
/* 分布条:三段按条数分宽 —— 条数投影成 v-<N>,梯子取宽;零条那段自己消失 */
|
||||
.proto-root .jc-distbar > i { flex: 24; min-width: 34px; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.proto-root .jc-distbar > i.v-0, .proto-root .jc-distbar > i:not([class*=" v-"]) { display: none; }
|
||||
.proto-root .jc-distbar > i.v-1 { flex: 1 } .proto-root .jc-distbar > i.v-2 { flex: 2 }
|
||||
.proto-root .jc-distbar > i.v-3 { flex: 3 } .proto-root .jc-distbar > i.v-4 { flex: 4 }
|
||||
.proto-root .jc-distbar > i.v-5 { flex: 5 } .proto-root .jc-distbar > i.v-6 { flex: 6 }
|
||||
.proto-root .jc-distbar > i.v-7 { flex: 7 } .proto-root .jc-distbar > i.v-8 { flex: 8 }
|
||||
.proto-root .jc-distbar > i.v-9 { flex: 9 } .proto-root .jc-distbar > i.v-10 { flex: 10 }
|
||||
.proto-root .jc-distbar > i.v-11 { flex: 11 } .proto-root .jc-distbar > i.v-12 { flex: 12 }
|
||||
.proto-root .jc-distbar > i.v-13 { flex: 13 } .proto-root .jc-distbar > i.v-14 { flex: 14 }
|
||||
.proto-root .jc-distbar > i.v-15 { flex: 15 } .proto-root .jc-distbar > i.v-16 { flex: 16 }
|
||||
.proto-root .jc-distbar > i.v-17 { flex: 17 } .proto-root .jc-distbar > i.v-18 { flex: 18 }
|
||||
.proto-root .jc-distbar > i.v-19 { flex: 19 } .proto-root .jc-distbar > i.v-20 { flex: 20 }
|
||||
.proto-root .jc-distbar > i.p { background: var(--ok); }
|
||||
.proto-root .jc-distbar > i.n { background: var(--warn-accent); }
|
||||
.proto-root .jc-distbar > i.k { background: var(--danger); }
|
||||
.proto-root .jc-distbar > i.p::before { content: "直接放行 · "; }
|
||||
.proto-root .jc-distbar > i.n::before { content: "附理由 · "; }
|
||||
.proto-root .jc-distbar > i.k::before { content: "拦截 · "; }
|
||||
.proto-root .jc-logo svg, .proto-root .jc-face svg, .proto-root .jc-rp-ava svg { width: 17px; height: 17px; }
|
||||
|
||||
/* ── 定价档案卡(工作单独有,照 A3 询价档案卡的骨架)──────────────────────
|
||||
一颗料一张卡,data-rows 逐行复制:左题面、右推演。
|
||||
形态按门禁处置分档(data-class-of):拦截档整卡描红,附理由档描橙 —— 判断在样式表。 */
|
||||
.proto-root .jc-selcards { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; }
|
||||
.proto-root .jc-selcard { border: 1px solid var(--line); border-radius: 11px; background: #fff; overflow: hidden; }
|
||||
.proto-root .jc-selcard.v-note { border-color: #f0cf9a; box-shadow: 0 1px 0 rgba(191,132,32,.06); }
|
||||
.proto-root .jc-selcard.v-block { border-color: #f3b6b3; box-shadow: 0 1px 0 rgba(207,64,58,.08); }
|
||||
.proto-root .jc-selcard-h {
|
||||
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
||||
padding: 9px 14px; background: var(--soft); border-bottom: 1px solid var(--line-soft);
|
||||
}
|
||||
.proto-root .jc-selcard.v-block .jc-selcard-h { background: var(--danger-soft); border-bottom-color: #f3b6b3; }
|
||||
.proto-root .jc-selcard.v-note .jc-selcard-h { background: var(--warn-soft); border-bottom-color: #f0cf9a; }
|
||||
.proto-root .jc-selcard-h code { font-size: 13px; font-weight: 800; color: var(--ink); }
|
||||
.proto-root .jc-selcard-h .why { flex: 1 1 auto; min-width: 0; font-size: 11px; color: var(--ink-3); }
|
||||
.proto-root .jc-selcard-b { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
|
||||
@media (max-width: 1000px) { .proto-root .jc-selcard-b { grid-template-columns: 1fr; } }
|
||||
.proto-root .jc-selcol { padding: 11px 14px; }
|
||||
.proto-root .jc-selcol + .jc-selcol { border-left: 1px solid var(--line-soft); }
|
||||
@media (max-width: 1000px) { .proto-root .jc-selcol + .jc-selcol { border-left: 0; border-top: 1px solid var(--line-soft); } }
|
||||
.proto-root .jc-selcol h5 { margin: 0 0 7px; font-size: 10.5px; font-weight: 800; letter-spacing: .08em; color: var(--ink-3); }
|
||||
.proto-root .jc-kvrow { display: flex; gap: 8px; padding: 3px 0; font-size: 12px; line-height: 1.55; }
|
||||
.proto-root .jc-kvrow i { flex: 0 0 84px; font-style: normal; color: var(--ink-4); font-size: 11px; padding-top: 1px; }
|
||||
.proto-root .jc-kvrow span { flex: 1 1 auto; min-width: 0; color: var(--ink); overflow-wrap: anywhere; }
|
||||
.proto-root .jc-kvrow span:empty::after { content: "—"; color: var(--ink-4); }
|
||||
.proto-root .jc-selcard .sc-req:empty::after {
|
||||
display: inline-flex; align-items: center; font-style: normal; font-size: 10px; font-weight: 700;
|
||||
line-height: 1; padding: 3px 7px; border-radius: 5px; border: 1px dashed;
|
||||
content: "必填"; color: var(--danger); background: var(--danger-soft); border-color: #f3b6b3;
|
||||
}
|
||||
.proto-root .jc-selcard .sc-sug:empty::after {
|
||||
display: inline-flex; align-items: center; font-style: normal; font-size: 10px; font-weight: 700;
|
||||
line-height: 1; padding: 3px 7px; border-radius: 5px; border: 1px dashed;
|
||||
content: "建议填"; color: var(--warn-accent); background: var(--warn-soft); border-color: #f0cf9a;
|
||||
}
|
||||
.proto-root .jc-selcard .sc-opt:empty::after {
|
||||
display: inline-flex; align-items: center; font-style: normal; font-size: 10px; font-weight: 700;
|
||||
line-height: 1; padding: 3px 7px; border-radius: 5px; border: 1px dashed;
|
||||
content: "选填"; color: var(--ink-4); background: var(--soft); border-color: var(--line);
|
||||
}
|
||||
|
||||
/* 两把尺(静态规则表 —— 尺挂在墙上,不是这一单的数据) */
|
||||
.proto-root .jc-graderule { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; margin-bottom: 12px; }
|
||||
.proto-root .jc-graderule-r { display: flex; gap: 12px; align-items: flex-start; padding: 9px 13px; font-size: 12px; line-height: 1.6; }
|
||||
.proto-root .jc-graderule-r + .jc-graderule-r { border-top: 1px solid var(--line-soft); }
|
||||
.proto-root .jc-graderule-r p { margin: 0; color: var(--ink-2); }
|
||||
.proto-root .jc-graderule-r p b { color: var(--ink); }
|
||||
.proto-root .jc-band {
|
||||
flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; margin-top: 1px;
|
||||
min-width: 74px; height: 20px; padding: 0 8px; border-radius: 6px;
|
||||
font-size: 10.5px; font-weight: 800; letter-spacing: .04em; border: 1.5px solid; background: #fff;
|
||||
}
|
||||
.proto-root .jc-band.ga { color: var(--ok); border-color: var(--ok); }
|
||||
.proto-root .jc-band.gb { color: var(--brand); border-color: var(--brand); }
|
||||
.proto-root .jc-band.gc { color: var(--warn-accent); border-color: var(--warn-accent); }
|
||||
.proto-root .jc-band.gp { color: var(--ok); border-color: var(--ok); }
|
||||
.proto-root .jc-band.gn { color: var(--warn-accent); border-color: var(--warn-accent); }
|
||||
.proto-root .jc-band.gk { color: var(--danger); border-color: var(--danger); }
|
||||
|
||||
/* 一个价怎么长出来的:成本 → 底价线 → 拟报价,内联线稿 SVG 走位图 */
|
||||
.proto-root .jc-priceline { display: flex; align-items: center; gap: 0; margin: 2px 0 14px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 11px; background: var(--soft); }
|
||||
.proto-root .jc-plnode { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 3px; align-items: flex-start; }
|
||||
.proto-root .jc-plnode b { font-size: 10.5px; font-weight: 800; letter-spacing: .06em; color: var(--ink-3); }
|
||||
.proto-root .jc-plnode span { font-size: 12px; color: var(--ink-2); line-height: 1.5; }
|
||||
.proto-root .jc-plarrow { flex: 0 0 auto; padding: 0 10px; color: var(--brand-soft); }
|
||||
@media (max-width: 1000px) { .proto-root .jc-priceline { flex-direction: column; align-items: stretch; gap: 8px; } .proto-root .jc-plarrow { display: none; } }
|
||||
`;
|
||||
|
||||
/** 一枚图标 —— 与原型同一写法 */
|
||||
const ico = (name: string, size = 11): string =>
|
||||
`<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-${name}"/></svg>`;
|
||||
|
||||
/** 三层壳照原型:少一层样式表全失效 */
|
||||
const doc = (input: { badge: string; kicker: string; title: string; titleRef: string; state: string; body: string; foot: string }): string =>
|
||||
`<style>${PRD_CSS}${BIND_CSS}</style>
|
||||
${PRD_SPRITE}
|
||||
<div class="proto-root"><div class="app-shell"><div class="jc-stage">
|
||||
<article class="jc-report">
|
||||
<header class="jc-rp-head">
|
||||
<div class="jc-rp-badge">${ico(input.badge, 17)}</div>
|
||||
<div class="jc-rp-tit">
|
||||
<em>${input.kicker}</em>
|
||||
<h3>${input.title} <span data-slot="${input.titleRef}"></span></h3>
|
||||
</div>
|
||||
${input.state}
|
||||
</header>
|
||||
<div class="jc-rp-bd">${input.body}</div>
|
||||
<footer class="jc-rp-foot">${input.foot}</footer>
|
||||
</article>
|
||||
</div></div></div>`;
|
||||
|
||||
const part = (label: string, aside = '', tone = ''): string =>
|
||||
`<div class="jc-rp-part"><b>${label}</b><i class="ln"></i>${aside === '' ? '' : `<em class="${tone}">${aside}</em>`}</div>`;
|
||||
|
||||
const sign = (note: string, tag = ''): string =>
|
||||
`<div class="jc-rp-sign">
|
||||
<span class="jc-rp-ava">${ico('user', 15)}</span>
|
||||
<div><b data-slot="case:owner"></b><span>${note}</span></div>
|
||||
</div>${tag === '' ? '' : `<span class="jc-done-tag">${ico('git-branch', 13)}${tag}</span>`}`;
|
||||
|
||||
/* ==================== 组成件 ==================== */
|
||||
|
||||
/** 要素四卡:客户 / 待定价 / 一格一料的门禁刻度 / 定价的尺 */
|
||||
const FOUR = `<div class="jc-four">
|
||||
<div class="jc-fcard">
|
||||
<span class="jc-fcard-k"><i>${ico('building')}</i>客户公司</span>
|
||||
<div class="jc-fcard-b">
|
||||
<span class="jc-logo">${ico('building', 17)}</span>
|
||||
<div class="jc-fcard-t"><b data-slot="customer_name" data-blank></b><span data-slot="customer_code" data-blank></span></div>
|
||||
</div>
|
||||
<span class="jc-fcard-note">承自上游;单飞起单时此栏可空</span>
|
||||
</div>
|
||||
|
||||
<div class="jc-fcard t-teal">
|
||||
<span class="jc-fcard-k"><i>${ico('boxes')}</i>待定价</span>
|
||||
<div class="jc-count"><b data-slot="quote_pricing_draft_missing"></b><span>/ <b data-slot="quote_pricing_draft_rows"></b> 条草案</span></div>
|
||||
<span class="jc-fcard-note">拟报单价还空着的行 —— 这一列由你落笔,系统只给建议价;补一格少一格,基座现数</span>
|
||||
</div>
|
||||
|
||||
<div class="jc-fcard">
|
||||
<span class="jc-fcard-k"><i>${ico('shield-check')}</i>底价门禁逐条</span>
|
||||
<div class="jc-segmeter" data-rows="mine:quote_pricing.draft">
|
||||
<i data-class-of="disposition"></i>
|
||||
</div>
|
||||
<span class="jc-fcard-note">一格一条料 · 绿=在底价线之上,橙=低于两成内附理由,红=压在拦截档待拍板</span>
|
||||
</div>
|
||||
|
||||
<div class="jc-fcard t-ok">
|
||||
<span class="jc-fcard-k"><i>${ico('history')}</i>取价的尺</span>
|
||||
<span class="jc-tagbig">${ico('check', 13)}<span>成本 × 利润模型 → 底价线</span></span>
|
||||
<span class="jc-fcard-note">基准取上游底价或库内历史价;毛利档由 P-01 定;偏离底价线两成即止(P-02)</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
/** 一个价是怎么长出来的 —— 走位图(形状写死,不算不判) */
|
||||
const PRICE_LINE = `<div class="jc-priceline">
|
||||
<div class="jc-plnode"><b>成本基准</b><span>上游底价 / 库内历史价 —— 取不到就空着,不编</span></div>
|
||||
<div class="jc-plarrow">${ico('arrow-right', 18)}</div>
|
||||
<div class="jc-plnode"><b>毛利档 P-01</b><span>客户等级 × 类别 × 量档 → 一段区间</span></div>
|
||||
<div class="jc-plarrow">${ico('arrow-right', 18)}</div>
|
||||
<div class="jc-plnode"><b>底价线</b><span>成本 ×(1 + 档下限)—— 比它低就要有人负责</span></div>
|
||||
<div class="jc-plarrow">${ico('arrow-right', 18)}</div>
|
||||
<div class="jc-plnode"><b>建议价 → 你定价</b><span>系统给建议,拟报单价那一格由你落笔</span></div>
|
||||
<div class="jc-plarrow">${ico('arrow-right', 18)}</div>
|
||||
<div class="jc-plnode"><b>门禁 P-02</b><span>偏离底价线:≥0 放行 / 两成内附理由 / 超两成拍板</span></div>
|
||||
</div>`;
|
||||
|
||||
/** 缺价红带 —— 与提交闸、放行门 L1 同一个数同一套算法(基座现数);一格不缺自动收 */
|
||||
const GAP_BAR = `<div class="jc-gapbar" data-class-of="quote_pricing_draft_missing">${ico('alert', 14)}
|
||||
<span><b>还差 <span data-slot="quote_pricing_draft_missing"></span> 行拟报单价没定</b>
|
||||
—— <span data-slot="quote_pricing_draft_missing_keys"></span>。
|
||||
按右上角「编辑」开表,红框那几格就是要填的:认可系统建议价就照抄那个数,不认可就改成你的;
|
||||
库内查不到成本的那几行没有建议价,由你按经验定并在备注写明依据。补齐了执行单才提交得了。</span>
|
||||
</div>`;
|
||||
|
||||
const editGate = (address: string, what: string): string => `<div class="jc-editgate" data-edit-for="${address}">
|
||||
<span class="jc-editgate-t">${ico('lock', 12)}${what}<b>已锁定</b> —— 看的时候不误碰,要改按右边</span>
|
||||
<button type="button" class="jc-egbtn open" data-edit-open="${address}">${ico('pen', 12)}编辑</button>
|
||||
<button type="button" class="jc-egbtn ghost" data-edit-cancel="${address}">取消</button>
|
||||
<button type="button" class="jc-egbtn save" data-edit-save="${address}">${ico('check', 12)}保存</button>
|
||||
</div>
|
||||
<div class="jc-editbar">${ico('pen', 14)}
|
||||
<span>可填的是<b>拟报单价</b>与<b>备注</b>两列:<b>拟报单价这一列系统不替你填</b> ——
|
||||
认可建议价就照抄那个数,改高改低都可以,改了在备注里写一句缘由;
|
||||
毛利、偏离度与门禁结论随后按你定的价重算。本轮不报的料按行首 × 撤掉。
|
||||
改动只落任务草稿层,<b>保存不等于放行</b>。</span>
|
||||
</div>`;
|
||||
|
||||
const addRowBar = (address: string): string => `<div class="jc-addrow">
|
||||
<button type="button" class="jc-egbtn ghost" data-edit-add="${address}">${ico('plus', 12)}加一行</button>
|
||||
<em>照末行的样子添一行空的;主键(型号 PN)填了才存得下,不填这一行不落库。</em>
|
||||
</div>`;
|
||||
|
||||
/**
|
||||
* 草案主表 —— 一行一料:承继列 + 定价五列 + 类徽与门禁徽。
|
||||
* `data-col` 只挂人可改的两列(quoted_price / price_note)+ 主键 pn ——
|
||||
* 编辑面精确等于 BD 的职权面;**主键格必挂 `data-col`**(它不是编辑面,是行的钥匙:
|
||||
* 不挂,服务端按「全表一致列」补键,两行会对到同一行上,后者盖前者 —— A2 走查逮过)。
|
||||
* 拟报单价挂 `req`:空着出「必填」胶囊、整行标底,与提交闸、放行门同源(都数 `_missing`)。
|
||||
*/
|
||||
const DRAFT_TABLE = `<div class="jc-tbl-wrap"><table class="jc-tbl-in">
|
||||
<thead><tr>
|
||||
<th style="width:36px;min-width:36px"></th>
|
||||
<th style="width:44px;min-width:44px">类</th>
|
||||
<th style="min-width:170px">PN</th>
|
||||
<th style="min-width:84px">品类</th>
|
||||
<th style="min-width:70px">年用量</th>
|
||||
<th style="min-width:78px">成本基准</th>
|
||||
<th style="min-width:86px">毛利档</th>
|
||||
<th style="min-width:78px">底价线</th>
|
||||
<th style="min-width:78px">客户目标价</th>
|
||||
<th style="min-width:82px">系统建议价</th>
|
||||
<th class="req" style="min-width:96px">拟报单价(你定)</th>
|
||||
<th style="min-width:70px">毛利率</th>
|
||||
<th style="min-width:70px">偏离度</th>
|
||||
<th style="width:78px;min-width:78px">门禁</th>
|
||||
<th style="min-width:150px;width:100%">备注 / 让价理由</th>
|
||||
</tr></thead>
|
||||
<tbody data-rows="mine:quote_pricing.draft" data-edit="quote_pricing.draft"><tr>
|
||||
<td class="jc-rn"><button type="button" class="jc-rowdel" data-edit-del="quote_pricing.draft" title="撤掉这一行">×</button></td>
|
||||
<td class="jc-ty" data-h="类"><span class="jc-tbadge" data-class-of="type_tag" data-slot="type_tag"></span></td>
|
||||
<td data-h="PN"><span class="jc-vcell mono" data-slot="pn" data-col="pn" data-blank></span></td>
|
||||
<td class="dimempty" data-h="品类"><span class="jc-vcell" data-slot="category" data-blank></span></td>
|
||||
<td class="dimempty" data-h="年用量"><span class="jc-vcell mono" data-slot="eau" data-blank></span></td>
|
||||
<td class="dimempty" data-h="成本基准"><span class="jc-vcell num" data-slot="base_price" data-blank></span></td>
|
||||
<td class="dimempty" data-h="毛利档"><span class="jc-vcell" data-slot="margin_band" data-blank></span></td>
|
||||
<td class="dimempty" data-h="底价线"><span class="jc-vcell num" data-slot="floor_price" data-blank></span></td>
|
||||
<td class="sug" data-h="客户目标价"><span class="jc-vcell num" data-slot="target_price" data-blank></span></td>
|
||||
<td class="dimempty" data-h="系统建议价"><span class="jc-vcell num" data-slot="suggested_price" data-blank></span></td>
|
||||
<td class="req" data-h="拟报单价(你定)"><span class="jc-vcell num" data-slot="quoted_price" data-col="quoted_price" data-blank></span></td>
|
||||
<td class="dimempty" data-h="毛利率"><span class="jc-vcell num" data-slot="margin" data-blank></span></td>
|
||||
<td class="dimempty" data-h="偏离度"><span class="jc-vcell num" data-slot="deviation" data-blank></span></td>
|
||||
<td data-h="门禁"><span class="jc-gbadge" data-class-of="disposition"></span></td>
|
||||
<td class="opt" data-h="备注 / 让价理由"><span class="jc-vcell" data-slot="price_note" data-col="price_note" data-blank></span></td>
|
||||
</tr></tbody>
|
||||
</table></div>`;
|
||||
|
||||
/** 门禁分布:三段按条数分宽 —— 守恒等式(三档相加 = 行数)的可视形 */
|
||||
const DIST = `<div class="jc-dist">
|
||||
<div class="jc-distbar">
|
||||
<i class="p" data-class-of="quote_pricing_draft_disposition_pass" data-slot="quote_pricing_draft_disposition_pass"></i>
|
||||
<i class="n" data-class-of="quote_pricing_draft_disposition_note" data-slot="quote_pricing_draft_disposition_note"></i>
|
||||
<i class="k" data-class-of="quote_pricing_draft_disposition_block" data-slot="quote_pricing_draft_disposition_block"></i>
|
||||
</div>
|
||||
<div class="jc-distlg">
|
||||
<span><em class="a"></em>直接放行 · 在底价线之上 <b data-slot="quote_pricing_draft_disposition_pass"></b></span>
|
||||
<span><em class="b"></em>附理由放行 · 低于两成内 <b data-slot="quote_pricing_draft_disposition_note"></b></span>
|
||||
<span><em class="c"></em>拦截待拍板 · 低于两成以上 <b data-slot="quote_pricing_draft_disposition_block"></b></span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
/* ==================== 局面一 · 定价工作单(算价与 BD 核价期间)==================== */
|
||||
|
||||
/**
|
||||
* 逐颗定价档案 —— 一颗料一张卡(data-rows 逐行复制):左题面、右推演。
|
||||
*
|
||||
* **容器用 `<section>` 不用 `<div>`**:渲染器的行展开按第一个同名闭合标签截行内容,
|
||||
* 行模版里嵌着 `<div>` 时 `<div data-rows>` 会把行截成套娃(A2 逮过,记在陷阱表)。
|
||||
*/
|
||||
const PRICE_CARDS = `<section class="jc-selcards" data-rows="mine:quote_pricing.draft">
|
||||
<article class="jc-selcard" data-class-of="disposition">
|
||||
<div class="jc-selcard-h">
|
||||
<span class="jc-tbadge" data-class-of="type_tag" data-slot="type_tag"></span>
|
||||
<code data-slot="pn"></code>
|
||||
<span class="why" data-slot="remark" data-blank></span>
|
||||
<span class="jc-gbadge" data-class-of="disposition"></span>
|
||||
</div>
|
||||
<div class="jc-selcard-b">
|
||||
<div class="jc-selcol">
|
||||
<h5>题面(客户要什么)</h5>
|
||||
<div class="jc-kvrow"><i>客户料号</i><span data-slot="customer_pn" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>品类</i><span data-slot="category" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>制造商</i><span data-slot="brand" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>年用量</i><span data-slot="eau" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>客户目标价</i><span class="sc-sug num" data-slot="target_price" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>交期要求</i><span data-slot="lead_time" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>客户约束</i><span data-slot="remark" data-blank></span></div>
|
||||
</div>
|
||||
<div class="jc-selcol">
|
||||
<h5>定价推演(填回下表,这里回显)</h5>
|
||||
<div class="jc-kvrow"><i>成本基准</i><span class="num" data-slot="base_price" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>毛利档</i><span data-slot="margin_band" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>底价线</i><span class="num" data-slot="floor_price" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>系统建议价</i><span class="num" data-slot="suggested_price" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>拟报单价</i><span class="sc-req num" data-slot="quoted_price" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>毛利率</i><span class="num" data-slot="margin" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>偏离底价线</i><span class="num" data-slot="deviation" data-blank></span></div>
|
||||
<div class="jc-kvrow"><i>让价理由</i><span class="sc-opt" data-slot="price_note" data-blank></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>`;
|
||||
|
||||
/** 利润模型 P-01 的毛利档 —— 静态规则,尺挂在墙上(素材:MVP 示例工作流 §1.2) */
|
||||
const MARGIN_RULES = `<div class="jc-graderule">
|
||||
<div class="jc-graderule-r">
|
||||
<span class="jc-band ga">8%~10%</span>
|
||||
<p><b>战略客户 · A 类标准件 · 大批量。</b>量换价,毛利让到下限也要守住 —— 这一档的价不是拿来谈的,
|
||||
是拿来锁长单的。</p>
|
||||
</div>
|
||||
<div class="jc-graderule-r">
|
||||
<span class="jc-band gb">12%~15%</span>
|
||||
<p><b>普通客户 · A 类标准件。</b>常规档,也是拿不准时的兜底档 —— 行情透明的料按这一档报不吃亏。</p>
|
||||
</div>
|
||||
<div class="jc-graderule-r">
|
||||
<span class="jc-band gc">15%~20%</span>
|
||||
<p><b>B 类波动件。</b>货源与行情都会动,毛利要留出被动让价的余地。</p>
|
||||
</div>
|
||||
<div class="jc-graderule-r">
|
||||
<span class="jc-band gc">20%~50%</span>
|
||||
<p><b>C 类替代 / 停产件。</b>一事一议:找料本身就是成本,替代还带验证风险 ——
|
||||
报低了后面每一次补货都在亏。</p>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
/** 底价门禁 P-02 的三档尺 —— 静态规则(素材:MVP 示例工作流 §1.2 底价自检) */
|
||||
const GATE_RULES = `<div class="jc-graderule">
|
||||
<div class="jc-graderule-r">
|
||||
<span class="jc-band gp">≥ 底价线</span>
|
||||
<p><b>直接放行。</b>报价在底价线之上,毛利还在档内 —— 不必解释,提审即可。</p>
|
||||
</div>
|
||||
<div class="jc-graderule-r">
|
||||
<span class="jc-band gn">低于两成内</span>
|
||||
<p><b>附理由放行。</b>为拿单让到底价线以下两成以内:在备注里写清让价的缘由
|
||||
(客户长单、带单品、竞对压价)—— 理由随草案一起交审批,不写等于没有依据。</p>
|
||||
</div>
|
||||
<div class="jc-graderule-r">
|
||||
<span class="jc-band gk">低于两成以上</span>
|
||||
<p><b>拦截,要人拍板。</b>这一档系统不替任何人放行:要么回调到底价线,要么由负责人
|
||||
拍板破线并留下理由,要么这一条本轮不报。<b>私自绕开门禁不算处置</b>。</p>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export const PRICING_WORKSHEET = doc({
|
||||
badge: 'sheet',
|
||||
kicker: '统一报价 · A4 成本定价 · 定价作业',
|
||||
title: '定价工作单',
|
||||
titleRef: 'rfq_ref',
|
||||
state:
|
||||
'<span class="jc-rp-wait warn">待定价 <b data-slot="quote_pricing_draft_missing"></b> / <b data-slot="quote_pricing_draft_rows"></b> 条</span>',
|
||||
body:
|
||||
/*
|
||||
* 指标带摆的是**利润模型 P-01 的三个入参 + 这一单的工作量**,不是所有字段。
|
||||
* 三个档位键取的是清单里 P-01 声明的决策入参名(customerGrade / materialType /
|
||||
* volumeTier)—— 决策表照这三个名字取值,模版就照这三个名字摆,不另起炉灶。
|
||||
* `records` 是起单入参的数组,基座渲染数组时给的是条数,正好是「这一单要定几条价」。
|
||||
*/
|
||||
statsBand([
|
||||
['待定价条目', 'records'],
|
||||
['客户等级', 'customerGrade'],
|
||||
['料号类型', 'materialType'],
|
||||
['数量档', 'volumeTier'],
|
||||
]) +
|
||||
section('基准从哪来 —— 四级优先级',
|
||||
ladder([
|
||||
['1', '本客户近期成交价', '最贴近这一单的真实议价空间'],
|
||||
['2', '同型号他客户成交价', '价位可比,但议价空间要留余量'],
|
||||
['3', '供应商底价表', '成本侧的锚,不含客户侧信息'],
|
||||
['4', '无基准', '新料号、久未报价的老料号都会落到这里'],
|
||||
]) +
|
||||
noteOnly(
|
||||
'<b>无基准不是异常,它本身就是一种基准。</b>此时偏离率记 0 —— 没有可偏离的对象,' +
|
||||
'由底价门禁按「无基准」处置。不要拿一个不相干的价去凑一个偏离率出来:' +
|
||||
'那比没有基准更危险,因为它<b>看起来像有依据</b>。',
|
||||
),
|
||||
{ text: '越靠前越贴近实情' },
|
||||
) +
|
||||
section('本单待定价条目',
|
||||
tableOnly('mine:quote_pricing.draft', [
|
||||
['型号 PN', 'pn', 'mono'],
|
||||
['基准价', 'last_price'],
|
||||
['拟报单价', 'quoted_price'],
|
||||
['毛利率', 'margin'],
|
||||
]),
|
||||
{ text: '只列本单写下的行' },
|
||||
) +
|
||||
section('案例当下进程', stagesOnly('case:stages'), { text: 'Enabled ≠ Active' }),
|
||||
part('定价任务', '客户 <b data-slot="customer_name" data-blank></b> · 共 <b data-slot="line_total"></b> 条 · 有基准价 <b data-slot="basis_total"></b> 条') +
|
||||
`<div class="jc-derive">${ico('info', 14)}
|
||||
<span>这一屏是一份报价草案的作业台:系统按「成本基准 × 利润模型」给出底价线与
|
||||
<b>建议价</b>,<b>拟报单价那一列由你落笔</b> —— 人签的就是这个数,系统不代签。
|
||||
库内查不到成本的料连建议价都没有(不编),那几行按你的经验定并在备注写明依据。
|
||||
你定完价,毛利、偏离度与底价门禁的结论跟着重算。</span>
|
||||
</div>` +
|
||||
FOUR +
|
||||
part('一个价是怎么长出来的', '成本 → 毛利档 → 底价线 → 拟报价 → 门禁') +
|
||||
PRICE_LINE +
|
||||
part('逐颗定价档案', '题面在左 · 推演在右') +
|
||||
PRICE_CARDS +
|
||||
part('利润模型 P-01 的毛利档', '客户等级 × 类别 × 量档 —— 尺挂在墙上') +
|
||||
MARGIN_RULES +
|
||||
part('底价门禁 P-02', '偏离底价线两成即止') +
|
||||
GATE_RULES +
|
||||
part('填回工作表', '定价落在这 —— 补齐了执行单才提交得了') +
|
||||
GAP_BAR +
|
||||
editGate('quote_pricing.draft', '报价草案填回表') +
|
||||
DRAFT_TABLE +
|
||||
addRowBar('quote_pricing.draft') +
|
||||
`<div class="jc-drop">${ico('info', 13)}
|
||||
<span>带红标的<b>拟报单价</b>是必填,而且<b>只由你填</b> —— 缺一行执行单按不下提交,
|
||||
与放行门是同一个数同一套算法。系统给的是建议价:照抄即认可,改了以你的为准。
|
||||
查不到成本的料没有建议价(系统不编):按你的经验定一个价并在备注写明依据,
|
||||
或者按行首 × 把这一条撤出本轮。压在拦截档(红)的条目在你这儿往上调一点,
|
||||
就不必再惊动第二个人。</span>
|
||||
</div>`,
|
||||
foot: sign('BD 核价 · AI 按利润模型与底价门禁算价,你定最后那个数', '统一报价 · A4 成本定价 · 在办'),
|
||||
});
|
||||
|
||||
/** 局面二:S2 自检 —— 人此刻要核的是「凭什么是这个价、门禁怎么判的」 */
|
||||
export const PRICING_PRICED = viewShell({
|
||||
title: '定价与底价门禁',
|
||||
kicker: 'A4 · 成本定价 · S2 自检',
|
||||
tone: 'brand',
|
||||
icon: 'gauge',
|
||||
stamp: { kind: 'live', text: '自检中' },
|
||||
sign: 'A4 成本定价 · 定价自检(判定表 P-01 / P-02)',
|
||||
body:
|
||||
/* 标签一律照台账声明的列名(基准价 / 拟报单价 / 毛利率 / 偏离度 / 门禁处置):
|
||||
同一个数在指标带上叫一个名、在下面的表里叫另一个名,签字的人就得先花一秒确认它俩是不是一回事 */
|
||||
statsBand([
|
||||
['基准价', 'last_price'],
|
||||
['拟报单价', 'quoted_price'],
|
||||
['毛利率', 'margin'],
|
||||
['偏离度', 'deviation'],
|
||||
]) +
|
||||
section('逐条定价结论',
|
||||
tableOnly('mine:quote_pricing.draft', [
|
||||
['型号 PN', 'pn', 'mono'],
|
||||
['基准价', 'last_price'],
|
||||
['拟报单价', 'quoted_price'],
|
||||
['毛利率', 'margin'],
|
||||
['偏离度', 'deviation'],
|
||||
['门禁处置', 'disposition'],
|
||||
]),
|
||||
{ text: '判据在左 · 结论在右' },
|
||||
) +
|
||||
section('目标毛利怎么定的 —— 利润模型 P-01',
|
||||
ladder([
|
||||
['1', '战略客户 · A 类标准件 · 大批量', '目标毛利 8%–10%'],
|
||||
['2', '普通客户 · A 类标准件', '12%–15%'],
|
||||
['3', 'B 类波动件', '15%–20%'],
|
||||
['4', 'C 类替代 / EOL', '≥20%,或一事一议'],
|
||||
['5', '以上都不命中', '兜底按普通客户标准件 12%–15%'],
|
||||
]) +
|
||||
noteOnly(
|
||||
'自上而下取<b>第一条命中</b>的规则,同输入必得同结论、零方差零 token,命中的规则行随出参留痕。' +
|
||||
'不要为了让门禁「过」而回头改毛利参数 —— 那是把判据改成结论,决策表也就失去了它唯一的价值。',
|
||||
),
|
||||
{ text: '首条命中' },
|
||||
) +
|
||||
section('偏离怎么判的 —— 底价门禁 P-02',
|
||||
ladder([
|
||||
['1', '拟报价 ≥ 底价', '直接放行'],
|
||||
['2', '低于底价 ≤ 20%', 'PM 附理由放行'],
|
||||
['3', '低于底价 超 20%', '拦截 · BOSS 拍板'],
|
||||
]) +
|
||||
noteOnly(
|
||||
'门禁不是「拦下」而是<b>分流</b>:超限的一律调人 —— 要么回调价格,要么写明为什么这样报,' +
|
||||
'两条路都合法,但都要留下理由。这条留痕将来会成为客户等级与利润模型的迭代素材。',
|
||||
),
|
||||
{ text: '±20% 即止' },
|
||||
) +
|
||||
section('本单门禁结论',
|
||||
/*
|
||||
* 结论章里嵌 `data-slot`:处置与偏离都是**这一单算出来的事实**,
|
||||
* 写死一句「已通过」就是替判定表下结论。橙档在此的意思是「还没定,等人处置」。
|
||||
*/
|
||||
verdict('底价门禁 P-02 处置:<b data-slot="disposition"></b>', 'wait', '偏离度 <b data-slot="deviation"></b>') +
|
||||
noteOnly(
|
||||
'禁区两条随本单同行:<b>不得静默绕开 P-02 的结论</b> —— 要破线只能走「人 · 决策」;' +
|
||||
'<b>底价超一周未更新的,先刷底价再判</b> —— 拿旧底价判新行情等于没判。',
|
||||
),
|
||||
),
|
||||
});
|
||||
/* ==================== 局面二 · 报价草案清单(拍板 / 放行 / 归档)==================== */
|
||||
|
||||
/** 局面三:待放行 —— 验收单上人要签的那份成果报告 */
|
||||
export const PRICING_RELEASE = viewShell({
|
||||
title: '报价草案 · 待放行',
|
||||
kicker: 'A4 · 成本定价 · 成果待签',
|
||||
tone: 'case',
|
||||
icon: 'shield-check',
|
||||
stamp: { kind: 'live', text: '待签字放行' },
|
||||
sign: 'A4 成本定价 · 报价草案(待放行)',
|
||||
export const PRICING_ROSTER = doc({
|
||||
badge: 'file-check',
|
||||
kicker: '统一报价 · A4 成本定价 · 交付成果',
|
||||
title: '报价草案',
|
||||
titleRef: 'rfq_ref',
|
||||
state:
|
||||
'<span class="jc-rp-wait">草案 <b data-slot="quote_pricing_draft_rows"></b> 条 · 待拍板 <b data-slot="quote_pricing_draft_disposition_block"></b></span>',
|
||||
body:
|
||||
statsBand([
|
||||
['拟报单价', 'quoted_price'],
|
||||
['毛利率', 'margin'],
|
||||
['偏离度', 'deviation'],
|
||||
['门禁处置', 'disposition'],
|
||||
]) +
|
||||
section('本单交付:报价草案',
|
||||
tableOnly('mine:quote_pricing.draft', [
|
||||
['型号 PN', 'pn', 'mono'],
|
||||
['基准价', 'last_price'],
|
||||
['拟报单价', 'quoted_price'],
|
||||
['毛利率', 'margin'],
|
||||
['偏离度', 'deviation'],
|
||||
['门禁处置', 'disposition'],
|
||||
]) +
|
||||
noteOnly('只列<b>本单</b>写下的行 —— 别单放行过的记录不混进来。逐条列出,便于放行前逐条核对。'),
|
||||
{ text: '本单草稿' },
|
||||
) +
|
||||
section('放行前要过的三道判据',
|
||||
ladder([
|
||||
['1', '每条都有拟报单价与毛利率', 'L1 机检 · unpriced = 0'],
|
||||
['2', '偏离底价 ≤ ±20%,超限者已回调或注明理由', 'L2 机检 · 底价门禁 P-02'],
|
||||
['3', 'BD 放行', 'L5 人签 · 签了才算数'],
|
||||
]),
|
||||
{ text: '两机检 · 一人签' },
|
||||
) +
|
||||
section('放行之后会发生什么',
|
||||
verdict('放行即落地:草稿合并入正式版,草案提交 A5 定价审批', 'wait', '审批点卡在 PM 工位') +
|
||||
noteOnly(
|
||||
'审批驳回按<b>声明</b>走向,不由临场决定:<b>回 A4 重开新实例</b> —— 新任务 ID、留痕关联,' +
|
||||
'不复活旧任务。所以这一屏上还改得动的每一格,过了这道门就要花一整个新实例才改得回来。',
|
||||
),
|
||||
),
|
||||
part('定价要素', '成本定价 · 能算的归判定表,超限的归人') +
|
||||
FOUR +
|
||||
part('底价门禁分布', '直接放行 + 附理由 + 拦截 = 总条数') +
|
||||
DIST +
|
||||
`<div class="jc-derive">${ico('info', 14)}
|
||||
<span>三段条数由基座逐行现数,不是谁报上来的:红段一条不剩,这份草案才走得到放行门。
|
||||
压在拦截档的条目只有三条出路 —— 回调到底价线、由负责人拍板破线并留理由、
|
||||
或者本轮不报。</span>
|
||||
</div>` +
|
||||
part('报价草案', '<b data-slot="line_total"></b> 条 · 每条都带来路') +
|
||||
GAP_BAR +
|
||||
editGate('quote_pricing.draft', '报价草案') +
|
||||
DRAFT_TABLE +
|
||||
`<div class="jc-drop">${ico('info', 13)}
|
||||
<span>每一行摆的是这个价的<b>来路</b>:成本基准从哪来、毛利档是哪一档、底价线在哪、
|
||||
偏离多少、门禁怎么判。签字前请重点看两处:<b>门禁列</b>里非绿的条目,
|
||||
与<b>备注列</b>里的让价理由 —— 没有理由的让价,到了审批那一关同样过不去。</span>
|
||||
</div>` +
|
||||
part('放行后即执行的落地动作', '门前 → 门后') +
|
||||
`<div class="jc-acts">
|
||||
<div class="jc-act"><i>${ico('check', 11)}</i>
|
||||
<div><b>草稿层合并进正式版</b> <span>本任务写下的报价草案与定价结论一并落账;
|
||||
你就地改过的那几个价随之落定。</span></div>
|
||||
</div>
|
||||
<div class="jc-act"><i>${ico('check', 11)}</i>
|
||||
<div><b>草案此刻才对外发出</b> <span>整份 <b data-slot="quote_pricing_draft_rows"></b> 条
|
||||
(每条带拟报单价与门禁处置)交出 —— 放行之前,世界上什么都没有发生。</span></div>
|
||||
</div>
|
||||
<div class="jc-act"><i>${ico('check', 11)}</i>
|
||||
<div><b>转入定价审批</b> <span>本活动只有一个出口 —— 报价草案是审批环节的直接输入,
|
||||
已进对方的任务清单。</span></div>
|
||||
</div>
|
||||
</div>`,
|
||||
foot: sign('BD 定价 · AI 按判定表算价并留痕 —— 要改哪一格,在这张单的草案表上就地改', '统一报价 · A4 成本定价 → 交审批'),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user