From 5a6963cade86cd35f311e3c2d4f702cc57ca75cb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 03:03:28 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs(=E6=B1=87=E6=8A=A5):=20=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E6=96=B9=E6=A1=88=E6=B1=87=E6=8A=A5=E7=A8=BF=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E4=B9=9D=E5=BC=A0=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E7=A4=BA=E6=84=8F=E5=9B=BE=20+=2026=20=E5=BC=A0=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E6=88=AA=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 面向客户与 PM 的一份完整汇报材料,补上现有文档缺的一环:《设计方案》是 需求基准、《操作手册》是分角色操作指引,两者都不回答「这套系统整体怎么 转、为什么这么设计」。考核流程本身跨六个岗位、四个审核节点、四种计分 方式、四个汇总维度,纯文字讲不清,所以以图为主、截图为证。 九张图各自回答一个讲不清的点: 图 1 角色动线泳道图 —— 谁在什么阶段做什么,系统在哪五处自动接手 图 2 状态机与闸门 —— 什么条件下走不动、驳回退回到哪 图 3 发布即冻结 —— 八项完整性检查 + 明细行为何是副本不是引用 图 4 计分链路 —— 实际值到最终得分的五步,四种计分方式只在中段分岔 图 5 计分方式曲线 —— 同一完成率下三种方式的得分率差异(线性/区间/阶梯) 图 6 重算传播 —— 源数据调整、结果调整、加减分三条通道如何汇入总分 图 7 四维汇总口径 —— 一个最终得分如何落到部门/分公司/到人/分管领导 图 8 数据范围模型 —— 可见范围由方案发布生成,组织调整不改元数据 图 9 对象模型总图 —— 17 个自建对象分四组,缩进表示主从 截图取操作手册已归档的 26 张,走相对路径引用,不重复入库(遵 CLAUDE.md E2「截图不进代码树」的手册配图例外)。源稿是 Artifact 片段格式(无 html/head/body 外壳),发布用的自包含单文件由 pnpm docs:report 内联图片 生成,6MB+ 且可从源稿重建,已加 .gitignore。 scripts/inline-assets.cjs:把 改写成 data URI; 找不到的文件报错退出,不静默漏图。 数据核对:139 条单元测试、17 个对象、8 项发布前检查、20 个工作项中 15 项待验收,均已对照代码与 issue 列表核实;pnpm verify 通过。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QhMsYoDpoMGeQLf24eTawP --- .gitignore | 3 + README.md | 1 + ...\346\241\210\346\261\207\346\212\245.html" | 1536 +++++++++++++++++ package.json | 3 +- scripts/inline-assets.cjs | 75 + 5 files changed, 1617 insertions(+), 1 deletion(-) create mode 100644 "docs/\346\261\207\346\212\245/KPI\350\200\203\346\240\270\347\256\241\347\220\206\347\263\273\347\273\237-\350\247\243\345\206\263\346\226\271\346\241\210\346\261\207\346\212\245.html" create mode 100644 scripts/inline-assets.cjs diff --git a/.gitignore b/.gitignore index 2d56a59..7d4778c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ dist # 截图与证据不进代码树:挂在工作项评论或孤儿分支 acceptance-evidence docs/evidence/ + +# 汇报稿的自包含副本由 pnpm docs:report 生成(内联图片后 6MB+),可从源稿重建,不入库 +docs/交付/KPI考核管理系统-解决方案汇报.html diff --git a/README.md b/README.md index c0bbdf2..ac39f84 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ | [docs/01-需求解读报告.md](docs/01-需求解读报告.md) | 场景地图、平台能力覆盖度、风险、疑点(疑点已由设计方案 V1.0 关闭) | | [docs/02-总体方案蓝图.md](docs/02-总体方案蓝图.md) | os 能力映射、对象模型总图、模块依赖与开发顺序 | | [docs/手册/KPI考核管理系统-操作手册.md](docs/手册/KPI考核管理系统-操作手册.md) | 分角色操作手册 V0.1(送审稿):通用操作 + 五个业务角色分章,含真实系统截图;Word 成品在 [docs/交付/](docs/交付/),由 `pnpm docs:manual` 从本源稿生成 | +| [docs/汇报/KPI考核管理系统-解决方案汇报.html](docs/汇报/KPI考核管理系统-解决方案汇报.html) | 解决方案汇报稿:业务流程、计分与汇总口径、权限模型九张示意图 + 26 张界面截图。图片走相对路径指向操作手册的截图(不重复入库);发布用的自包含单文件由 `pnpm docs:report` 生成 | | [CLAUDE.md](CLAUDE.md) | 开发约定与 dev-issue 启用清单 | ## 快速开始 diff --git "a/docs/\346\261\207\346\212\245/KPI\350\200\203\346\240\270\347\256\241\347\220\206\347\263\273\347\273\237-\350\247\243\345\206\263\346\226\271\346\241\210\346\261\207\346\212\245.html" "b/docs/\346\261\207\346\212\245/KPI\350\200\203\346\240\270\347\256\241\347\220\206\347\263\273\347\273\237-\350\247\243\345\206\263\346\226\271\346\241\210\346\261\207\346\212\245.html" new file mode 100644 index 0000000..6a98b1e --- /dev/null +++ "b/docs/\346\261\207\346\212\245/KPI\350\200\203\346\240\270\347\256\241\347\220\206\347\263\273\347\273\237-\350\247\243\345\206\263\346\226\271\346\241\210\346\261\207\346\212\245.html" @@ -0,0 +1,1536 @@ +KPI 考核管理系统解决方案 + + + + + +
+
+ + + +
+ +
+

解决方案汇报

+

KPI 考核管理系统

+

把总公司各业务部门与分公司的考核,从 Excel 与聊天工具的来回传递,搬成一条口径唯一、全程留痕、发布即冻结的线上流程:指标库 → 方案配置 → 指标下达 → 数据填报 → 并行核对 → 审核审批 → 实时计分 → 数据调整 → 四维汇总 → 归档快照。

+
+
需求基准
《设计方案》V1.0(客户 2026-09-02 确认)
+
技术底座
ObjectStack 17.x 元数据平台
+
当前阶段
可运行系统 · 全流程已跑通 · PM 验收中
+
汇报日期
2026-09-07
+
+
+
17业务对象
+
6岗位与权限集
+
4计分方式
+
4汇总维度
+
8发布前检查项
+
139单元测试用例
+
+
+ +
+
+

01方案摘要

+

一页纸看懂:这套系统解决什么、靠什么机制解决、现在到哪一步了。

+ +

现状的三个痛点

+

考核本身不复杂,难的是口径留痕。用 Excel 做考核,三件事必然失控:

+
    +
  • 口径不唯一——同一个「完成率」,各部门表里的公式写法不同;表一改,历史周期的分数跟着变,过去算过的账对不上。
  • +
  • 过程不可追——谁在什么时候改了哪个数、为什么改、谁批准的,散落在聊天记录和邮件附件里,复核时拼不回来。
  • +
  • 并行协作靠人盯——分公司核对、人力审核、领导审批的先后顺序靠人催,谁卡在哪一步没有统一视图。
  • +
+ +

系统给出的五个机制

+
+

计分口径唯一真值

四种计分方式(线性 / 阶梯 / 区间插值 / 自定义公式)是数据不是代码。全系统任何一个得分都出自同一个计分引擎 src/lib/scoring.ts,视图和公式字段里不允许二次实现。

+

发布即冻结

方案一发布,目标值、权重、计分规则以副本形式落到每一行填报明细上。之后改指标库,历史周期分毫不动。

+

状态机带闸门

流转不靠人自觉。未填全不能提交、有争议不能推进、驳回必须写原因、归档后全表只读——规则全在服务端 hook,按钮只写「待执行动作」。

+

只增不改的留痕

每一次提交、核对、审核、驳回、调整、归档都写一条审核记录,只增不改;归档快照带 SHA-256 校验和,不可修改删除。

+

数据范围随方案生成

「本部门 / 本分公司 / 分管范围」由方案发布时按参与主体、分管领导、到人分工自动写入共享规则。组织调整或换分管领导,只改数据,不改元数据。

+

保留 Excel 的手感

填报是一屏网格、按行填数、保存即出分;列表支持 Excel 导出与批量导入。变的只有一件事:自由公式与跨表引用不进系统,由指标计分规则统一承担。

+
+ +

当前状态

+

系统已按方案搭出并跑通全流程:发布生成填报单、线性 / 区间 / 阶梯计分结果与设计方案第 7 章示例逐位一致、提交冻结、并行核对、争议阻断、驳回原因必填、退回上一节点、通过后四维汇总、加减分与调整落地重算、归档快照不可改删,以及三类业务账号的数据范围隔离,均已验证。第 11 章的产品化补充设计(第 18~35 项)待客户确认档次后排期,详见第 12 节。

+
+
+ +
+
+

02角色与职责

+

六个岗位,六套权限集。谁能看到什么由数据范围决定,谁能按哪个按钮由当前流程节点决定——两条边界都在服务端。

+
+ +
+ + + + + + + + + + + +
角色在系统里做什么能看到的范围岗位机器名
系统管理员维护组织、用户、岗位全部kpi_admin
人力审核维护指标库与考核方案、指标下达、发布方案、人力节点审核、登记加减分、审批数据调整、归档、审计查询全部kpi_hr_reviewer
人力负责人审批加减分、审批数据调整全部kpi_hr_head
部门填报人员填报本部门数据并提交、发起数据调整申请本部门kpi_dept_reporter
分公司核对人员核对与本分公司相关的数据,只能「确认无误」或「提出争议」,不能改数值本分公司kpi_branch_checker
分管领导分管主体的最终审批、查看分管范围的结果与看板分管范围kpi_exec_leader
表 1角色、职责与数据范围。分公司在方案里既是被考核主体、又是核对方——这两件事由两个人做:分公司填报人员填本分公司那张单,分公司核对人员只对别人的数据表态。
+
+ +
+
+

一条容易被忽略的设计约束

+

核对人员不能改数值。发现数据对不上,只能提出争议,由填报部门经调整流程更正。这是客户 2026-09-02 在第 13 项上选定的口径(选项 A),目的是让「谁报的数谁负责」这条责任线不被核对环节冲淡。

+
+
+
+ +
+
+

03端到端业务流程

+

一个考核周期是一条从左到右的单行道:人力配方案 → 部门填数 → 分公司并行核对 → 人力审核 → 领导审批 → 归档。三张图分别回答:谁在做什么、系统自动做了什么、什么情况下走不动。

+
+ +
+
+ + + + + + + + + + + + + + + ① 配置与发布 + ② 填报 + ③ 并行核对与逐级审核 + ④ 归档 + + + + + + + + + + + + + + + + 人力审核 + 部门填报人员 + 分公司核对人员 + 分管领导 + 系统 · 自动 + + + + 配置方案 + 指标 · 主体 · 权重 + 目标 · 节点 · 分工 + + + 发布方案 + 配置就此冻结 + 要改须复制新版 + + + 人力审核 + 审核通过 + 或驳回(填原因) + + + 归档 + 本期就此封存 + 纠错留到下期 + + + + 填报出分 + 逐行填实际值 + 或 Excel 批量导入 + + + 提交填报 + 有指标未填 + 则当场拦下 + + + + 分公司核对 + 各分公司并行 + 确认 或 提争议 + + + + 领导审批 + 只审分管主体 + 最终把关 + + + + 完整性检查 + 生成填报单与明细 + 冻结目标与规则副本 + + + 保存即计分 + 完成率 · 得分率 · 得分 + 并写出计算说明 + + + 生成核对任务 + 按方案参与的 + 各分公司一条 + + + 生成考核结果 + 四个维度 + 排名 + 可钻取回明细 + + + 生成归档快照 + 带 SHA-256 校验和 + 全表转为只读 + + + + + + + + + + + + + + + + + +
+
图 1一个考核周期的角色动线。实线是单据的流转方向,虚线是人的动作触发的系统动作——发布、保存、提交、审批通过、归档这五处,系统各自做一件不需要人参与的事。驳回路径见图 2。
+
+ +
+
+ + + + + + + + + + + + + 方案发布 + 每个参与主体一张单 + + + + + 填报中 + 部门填报人员 + + + 分公司核对中 + 分公司核对人员 + + + 人力审核中 + 人力审核 + + + 领导审批中 + 分管领导 + + + 已通过 + 结果已生成 + + + 已归档 + 不可改不可删 + + + 提交填报 + 闸门:全部指标已填实际值 + 岗位:部门填报人员 + + + 全部确认无误 + 闸门:有一家提争议就不推进 + 方案未配核对方时自动跳过 + + + 审核通过 + 闸门:岗位 = 人力审核 + 审核意见选填 + + + 审批通过 + 闸门:岗位 = 本主体分管领导 + 通过即生成四维结果 + + + 归档 + 闸门:岗位 = 人力审核 + 快照带 SHA-256 校验和 + + + + + 驳回 · 原因必填 · 退回上一节点 + + + 驳回 → 全部分公司重新核对 + + + + 提交之后实际值冻结 —— 改数只能走「数据调整」申请,备注不受限制 + +
+
图 2填报单状态机与它的闸门。每一次流转都由服务端 hook(src/hooks/sheet.hook.ts)判定,按钮本身只写「待执行动作」——绕过按钮直接写状态字段同样会被拦下。方案未配置分公司核对节点时,提交后直接进入人力审核;此时人力审核驳回退回的是「填报中」。
+
+ +
+

每一步系统保证了什么

+
+
+ + + + + + + + + + + + + +
步骤做什么系统保证
1人力审核新建方案版本(可复制上一版)→ 选指标 → 分配权重与目标 → 配考核关系、分管领导、审核节点 → 到人分工草稿期自由改;发布时逐条校验,不合格发不出去
2人力审核点「发布方案」方案冻结;每个主体生成一张填报单,指标行带目标、权重、计分规则的冻结副本;历史周期不受后续指标库改动影响
3部门填报人员逐行填实际值(或 Excel 导入),点「提交填报」保存即出分并写计算说明;有指标未填不能提交;提交后数据冻结
4分公司核对人员并行核对:「确认无误」或「提出争议」(必须写意见)全部确认才推进;有争议不能推进;方案里没有核对方时自动跳过本节点
5人力审核
分管领导
「审核通过」或「驳回」驳回必须填原因,退回上一节点;每一步谁、何时、从哪到哪、原因全部留痕
6系统每次保存实际值即计分口径唯一、可重复计算;计算过程逐条可复核
7系统审批通过时生成四维结果与排名加减分、数据调整落地后自动重算
8人力审核点「归档」生成带校验和的快照;之后填报单、明细、加减分全部锁定
表 2流程步骤与系统保证。左边四列是人看得见的操作,右边一列是系统在背后不允许发生的事——这一列才是系统相对 Excel 的全部价值。
+
+
+ +
+
+

04方案配置与发布

+

方案是一次考核的全部配置:考谁、考什么指标、各占多少权重、目标是多少、走几个审核环节、谁分管谁、分数怎么落到人头上。发布是整条流程唯一一次不可逆的动作——过了这道门,配置就冻结了。

+ +

发布这道门为什么重要

+

Excel 时代最难查的账是「这个数当时是按什么口径算的」。系统的答案是:发布时把口径复制一份,钉在每一行明细上

+
+ +
+
+ + + + + + + + + + + + + + + + 指标库 + 方向 · 计分方式 · 封顶保底 + 阶梯区间 + kpi_indicator + + + 考核方案 · 草稿 + 流程节点 · 参与主体 + 指标下达(指标 × 主体) + 权重 · 目标值 · 分管领导 + 到人分工 · 个人承接项 + + + + + + + 发布前完整性检查 + 1 流程节点配置合法 + 2 至少一个参与主体 + 3 每个主体至少一条下达 + 4 每主体权重合计 = 100% + 5 目标值齐全 + 6 每个主体已配分管领导 + 7 无下达给非参与主体 + 8 无未关闭的指标争议 + + + 通过 + + + + 不通过 + 方案保持草稿 + 右下角逐条列出问题所在 + + + + 填报单 × N —— 每个参与主体一张 + kpi_entry_sheet + + + + + + + + + + + 指标 + 目标值 + 权重 + 计分规则 + 实际值 + 完成率 + 得分率 + 得分 + + 营业收入完成率 + 1200 + 50 + 线性 + 1320 + 110% + 110% + 55.0 + + 客户满意度 + 90 + 20 + 阶梯 + 87.3 + 97% + 90% + 18.0 + + + 发布时写入的冻结副本 · 只读 + + 填报时由计分引擎算出 + + + 为什么必须是副本,而不是引用 + 明细行上的目标值、权重、计分规则,是发布那一刻从指标库与指标下达复制过来的值。 + 此后调整指标口径、改目标值、停用指标,都不会回头改动已发布周期的任何一行 —— + 去年算过的账,今年再打开还是同一个数。 + + + + + + 时间 + + + T1 · 方案发布 + 明细行写入冻结副本 + + + T2 · 指标库改口径 + 已发布周期分毫不动 + + + T3 · 下期方案发布 + 新周期采用新口径 + +
+
图 3发布即冻结。八项检查任意一项不过,方案原地保持草稿并逐条给出问题;全部通过才生成填报单。检查之外还有一条前置条件:同一考核周期只允许一个已发布版本。
+
+ +
+

指标库:计分规则是数据,不是代码

+

指标只建一次,以后各期方案都从库里选。指标编码全系统唯一,口径变了不改旧指标——新建一条新版本、把旧的停用、用「替代版本」串起版本链,历史周期照旧可查。

+
+ +
+
+
指标库列表页,顶部有全部指标与启用中两个视图页签,右上角是新建、导入、导出按钮
+
图 4指标库列表。支持 Excel 批量导入建库,首次上线时现行考核表可以整批灌进来。
+
+
+
新建指标窗口,上半部分是基本信息,下半部分是计分规则区,含指标方向、计分方式、完成率封顶与保底
+
图 5新建指标的「计分规则」区。方向决定完成率怎么算,计分方式决定得分率怎么取——两个下拉决定这个指标一辈子的算法。
+
+
+ +
+
指标详情页相关页签下的阶梯区间四行,每行含区间名称、完成率下限、完成率上限与得分率
+
图 6阶梯计分的区间表挂在指标下,按行维护。区间是数据行不是代码分支——加一档、改一档得分率,都不需要开发介入。
+
+ +
+

方案:四张明细表配齐一次考核

+

方案详情页把一次考核要配的东西收在四张内嵌明细表里:审核流程节点(走几步、每步哪个岗位审)、参与主体与考核关系(考谁、谁分管、主体权重)、指标下达(指标 × 主体 × 权重 × 目标值)、到人分工(部门得分怎么落到人头上)。第二期开始,「复制自版本」可以把上一期整套配置搬过来再改数。

+
+ +
+
草稿状态的方案详情页,顶部是状态条,右上角是发布方案按钮,下方是相关页签
+
图 7方案详情页(草稿)。顶部状态条:草稿 → 已发布 → 已关闭 → 已归档;右上角是那颗不可逆的「发布方案」。
+
+ +
+
+
方案相关页签下的流程节点四行,含顺序、节点类型、节点名称与审核岗位
+
图 8审核流程节点。四种节点类型可增删、可改审核岗位;本期不需要分公司核对,删掉那一行即可,提交后直接进人力审核。
+
+
+
方案相关页签下的参与主体五行,含考核主体、主体类型、考核部门、主体权重与分管领导
+
图 9参与主体与考核关系。这里配的「分管领导」,既决定谁审这张单,也决定谁看得见这张单——一处配置管两件事。
+
+
+ +
+
指标下达列表处于行内编辑状态,双击目标值单元格改为 380 后工具条出现全部保存按钮
+
图 10指标下达支持整表行内编辑,也支持「导出 XLSX → 在 Excel 里填 → 导入」的批量路径。这是配置阶段工作量最大的一张表,两条路都留着。
+
+ +
+

发布:先拦下,再放行

+

发布不是一个「保存并推进」的按钮,而是一次全量体检。检查不通过时,系统不做部分发布——方案原地不动,把问题逐条摆出来。

+
+ +
+
+
发布前完整性检查未通过时右下角逐条列出的问题提示,含市场部权重合计 90% 与华北分公司培训完成率未设置目标值两条
+
图 11拦下 检查未通过。提示直接点名是哪个主体的哪一条:「市场部的权重合计为 90%,必须等于 100%」「华北分公司的培训完成率未设置目标值」——不是一句「配置有误」。
+
+
+
发布成功后的方案详情页,状态条推进到已发布,出现发布时间与发布人,右上角按钮变成关闭方案
+
图 12放行 发布成功。状态条推进到「已发布」,盖上发布时间与发布人,右上角的按钮换成「关闭方案」——同一个位置不会再出现第二次发布。
+
+
+
+ +
+
+

05填报与实时计分

+

填报保留 Excel 的手感:一屏网格、按行填数、保存即出分。变的是口径的来源——得分不再由每张表自己的公式算出,而是由全系统唯一的计分引擎算出,并把算式写下来备查。

+ +

从实际值到最终得分

+

整条计算链路只有五步,每一步的输入输出都落库,任何一个数都能倒推回它的上一步。

+
+ +
+
+ + + + + + + + + + + + + 实际值 + 人填 / 导入 + + + 完成率 + % + + + 得分率 + % + + + 指标得分 + 单行 + + + 最终得分 + 指标得分合计 + + 已批准加减分 + + + 越高越好 = 实际 ÷ 目标 + 越低越好 = 目标 ÷ 实际 + + + + 按指标声明的计分方式四选一 + + + 完成率线性 + 得分率 = 完成率,限定在保底与封顶之间 + + + 阶梯计分 + 完成率落在哪一档,取该档的得分率 + + + 区间插值 + 下限→0,100%→100%,上限→封顶 + + + 自定义公式 + CEL 表达式直接给出得分 + + + + + + + + + + + + + × 权重 ÷ 100 + + + Σ 全部指标行 + + 已批准的加减分 + + + + + + 示例 · 营业收入完成率:目标 1200 万元,实际 1320 万元,权重 50,完成率线性、封顶 120% + 1320 + 110% + 110% + 55.0 + 与另两行合计 88.0(见表 3) + +
+
图 4计分链路。四种计分方式只在中间一段分岔,前后完全一致——这是「口径唯一」的具体含义:换计分方式改的是一个下拉,不是一套算法。引擎在 src/lib/scoring.ts,是纯函数,可单元测试、可逐条复核。
+
+ +
+

四种计分方式画出来是什么形状

+

同一个完成率,四种方式给出的得分率完全不同。选哪一种取决于指标想鼓励什么:线性鼓励多超额、区间插值容忍未达标但压缩超额收益、阶梯只认档位不认零头。

+
+ +
+
+ + + + + + + + + + 完成率线性(封顶 120%) + + 区间插值(下限 60%) + + 阶梯计分(四档) + + + + + + + + + + + + + + + + + + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + + 完成率(%) + 得分率(%) + + + + 封顶 + + + + + + + + + + 达标点:三种方式在此重合 + 完成率 100% → 得分率 100% + + 自定义公式不在本图:它由 CEL 表达式直接给出得分,形状不固定,用于事故次数一类无法用完成率表达的特殊口径。 + +
+
图 5三种可画出的计分方式,横轴完成率、纵轴得分率。同一个 97% 的完成率:线性给 97 分,区间插值给 92.5 分,阶梯落在「95~100%」这一档给 90 分——差异不是误差,是口径选择。
+
+ +
+

一张填报单算出来是什么样

+
+ +
+ + + + + + + + + +
指标权重目标实际完成率计分方式得分率得分
营业收入完成率501200 万元1320110%线性,封顶 120%110%55.0
利润完成率30180 万元14480%区间 60%~120%50%15.0
客户满意度2090 分87.397%阶梯:95%~100% → 90%90%18.0
合计10088.0
表 3市场部一张填报单的计分示例(权重合计 100)。三行分别用了三种计分方式——同一张表里混用是常态,因为不同指标想鼓励的行为不同。系统的单元测试逐位比对了这张表的每一个数。
+
+ +
+

填报动线:登录即处理

+

工作台是六个岗位共用的落地页。区块不按岗位显隐,按数据范围显示——分公司核对人员在「待我核对」里只读得到本分公司的任务,部门填报人员的待填报区块里只有本部门的行。看得到不等于动得了:越权点动作会被服务端的岗位闸门拒绝。

+
+ +
+
工作台页面与左侧五组功能菜单:填报与审核、考核方案、结果与归档、基础设置、审计查询
+
图 6工作台与左侧菜单五个分组。配置类菜单(考核方案、基础设置)按岗位在服务端裁剪——不满足能力的导航项根本不下发到浏览器,而不是在前端藏起来。
+
+ +
+
+
填报明细列表处于行内编辑状态,双击实际值单元格录入后工具条出现全部保存按钮
+
图 7按行填实际值,保存即出分。完成率、得分率、得分当场回填到同一张表,不用跳页、不用等批处理。
+
+
+
填报明细记录详情中的计算说明字段,写明完成率、得分率与得分的取值过程
+
图 8可复核 每一行都落一段「计算说明」。得分和自己手算的对不上时,打开这一段就能看到完成率怎么来的、封顶保底在哪一步生效、得分率取了哪一档。
+
+
+ +
+
+
有指标未填实际值时点提交填报被拦下的提示,填报单保持填报中状态
+
图 9拦下 有指标未填实际值,提交被拦,填报单原地保持「填报中」。回到明细用「未填实际值」页签就能找出漏的行。
+
+
+
提交成功后右下角的提示与状态条推进到分公司核对中
+
图 10推进 补齐后提交成功,状态条推进到「分公司核对中」,盖上提交时间与提交人。此刻实际值冻结,再改数只能走调整申请。
+
+
+
+ +
+
+

06并行核对与逐级审核

+

提交之后,单据不再属于填报部门。它先摊给所有相关分公司并行核对,全部确认才继续;再逐级过人力审核与分管领导。每一步只有两个动作:放行,或者写明原因退回。

+ +

并行核对:全部确认才推进

+

填报单一提交,系统按方案里参与的分公司各生成一条核对任务。这些任务互不排队、同时进行,但推进条件是「全部确认无误」——有一家提出争议,单据就停在核对节点,不会因为其他家都点了确认而溜过去。方案里没有配置需要核对的分公司时,本节点自动跳过。

+

核对人员只能表态,不能改数。发现数据对不上,写清「哪一项、差多少、依据是什么」提出争议,由填报部门经数据调整流程更正,更正后回到队列点确认,流程继续。

+
+ +
+
+
分公司核对人员的待我核对队列,每行显示任务名称、对应填报单、核对分公司与核对状态
+
图 11「待我核对」队列。只列得出与本分公司相关的任务——别的分公司的任务在数据层就不可见,不是靠视图筛选藏起来的。
+
+
+
提出争议窗口,争议内容为必填项,下方是确认按钮
+
图 12阻断 提出争议,争议内容必填。任务转为「有争议」,在争议解决前填报单不会往下推进。
+
+
+ +
+

逐级审核:放行或退回,没有第三种

+

审核人从队列进入,不需要知道单据在哪个环节——队列本身就是环节。「待人力审核」「待领导审批」「待我核对」三个队列在左侧菜单有直达入口,点一次就到。

+

驳回必须填原因,退回上一节点。退回到分公司核对节点时,全部分公司重新核对——这条口径是客户确认的第 8 项,目的是避免「改了一家的数,其他家的确认还算数」这种半有效状态。

+
+ +
+
+
填报单列表上方的队列页签:全部填报单、填报中、分公司核对中、待人力审核,右侧是还有 4 个的溢出入口
+
图 13填报单按状态分队列。每个人打开看到的行数不同——同一个「待人力审核」页签,人力审核看到全部,部门填报人员只看到本部门那一张。
+
+
+
待领导审批队列中的填报单,状态显示为领导审批中,行右侧是操作菜单
+
图 14分管领导的「待领导审批」队列,只出现本人分管主体的单据。分管关系来自方案里的「参与主体」配置,换人只需改那一格。
+
+
+ +
+
+
驳回窗口,驳回原因为必填项,下方是确认按钮
+
图 15退回 驳回窗口。原因必填,不填点不动;填完退回上一节点,并在审核记录里留下一条带原因的驳回记录。
+
+
+
审核通过窗口,审核意见为选填项,下方是确认按钮
+
图 16放行 审核通过窗口。审核意见选填——通过是常态,不该为了留痕而强迫每个人编一句话;但谁在什么时候通过的,系统一定会记。
+
+
+ +
+
+

按钮不是权限边界

+

流程按钮只写「待执行动作」,是否允许推进由服务端 hook(src/hooks/sheet.hook.ts)按当前节点的审核岗位判定。绕过按钮直接写状态字段,同样会被拦下——这条曾经是一个真实缺陷(动作体以受信任身份写入被误当作系统写入而免检,任何岗位可越节点审批),已修复并由 29 条岗位闸门测试用例守住。

+
+
+
+ +
+
+

07加减分与数据调整

+

考核不可能一次填对。系统给出三条受控的改数通道,每条都要审批、都要留痕、都会触发重算——而不是让人回到表格里悄悄把数字改掉。

+ +

三条通道各自改什么

+
+

源数据调整

报错的是原始数据。批准后直接更正该行实际值,并重新计分——完成率、得分率、得分全部按新实际值重算。

+

计算结果调整

要更正的是得分本身。批准后只改该行得分,该行标记「已调整」,在填报单与汇总结果中都看得见,不悄悄改主体总分之外的数据。

+

加减分

指标之外的专项表彰或扣分。由人力审核按事项登记(分值、依据必填),人力负责人审批后计入最终得分,不封顶不保底。

+

共同的约束

发起人、审批人由系统按登录账号盖章,不能手工填;已批准并落地的调整不可撤销,要再改就重新提一份;归档后三条通道全部关闭。

+
+
+ +
+
+ + + + + + + + + 源数据(实际值)调整 + 部门发起 → 人力审批「批准并落地」 + + + 计算结果(得分)调整 + 部门发起 → 人力审批「批准并落地」 + + + 加减分 + 人力审核登记 → 人力负责人批准 + + + + + + + 改写该行实际值 + → 重算完成率 · 得分率 · 得分 + + + 只改该行得分 + → 该行标记「已调整」,汇总里可见 + + + 计入加减分合计 + → 不封顶、不保底 + + + + + + + 填报单最终得分 + 指标得分合计 + 加减分 + + + + + 四维考核结果 + 自动重新生成 + 重排名 + + + 每一次落地都写一条审核记录:调整前值 / 调整后值 / 原因 / 审批人 / 落地时间 —— 只增不改,不可回改。 + + + 归档之后 + 三条通道全部关闭:填报单、明细、加减分转为只读,快照校验和固定;发现问题在下一周期修正,不回改历史。 + +
+
图 6改数的三条受控通道与它们的重算传播。关键在最右边那一格:任何一条通道落地,四维结果与排名都自动重算,不需要有人记得去「刷新汇总」——这是 Excel 时代最容易漏掉的一步。
+
+ +
+
+
新建加减分窗口,含事项、类型(加分或扣分)、分值与依据字段
+
图 17登记加减分。分值与依据必填,依据要写清制度条款或文件号。登记与审批分属两个岗位——人力审核提出,人力负责人批。
+
+
+
数据调整的待审批队列与行操作菜单入口,列出调整类型、调整前值、调整后值与调整原因
+
图 18数据调整的「待审批」队列。「调整前值」由系统自动带出,不给人手填的机会——留痕的价值取决于原值是否可信。
+
+
+
+ +
+
+

08汇总与结果应用

+

一张填报单只有一个最终得分,但这个分要落到四个不同的问题上:这个部门考得怎么样、这家分公司排第几、张三个人该拿多少、王总分管的这一摊整体如何。四个维度、四套口径,由同一个汇总引擎在审批通过的那一刻一次生成。

+
+ +
+
+ + + + + + + + + 已通过的填报单 + 每个主体一张 · 最终得分 + kpi_entry_sheet + + + 到人分工 · 个人承接项 + 员工 × 板块 × 权重 × 系数 + + + 参与主体配置 + 主体权重 · 分管领导 + + + + + + + 汇总引擎 + 审批通过 · 归档 + 调整落地时重算 + src/lib/aggregate.ts + + + + + + + + 维度一 · 部门 + 口径:该部门填报单的最终得分 + 排名:在全部部门主体内排序 + + + 维度二 · 分公司 + 口径:该分公司填报单的最终得分 + 排名:在全部分公司主体内排序 + + + 维度三 · 到人 + Σ(部门板块得分 × 分工权重% × 个人系数)+ Σ(承接项得分率% × 承接权重%) + 不封顶 —— 承担得多、承担的板块考得好,分就高 + + + 维度四 · 分管领导 + 分管主体的最终得分,按主体权重加权平均 + 主体权重全部留空时,退化为算术平均 + + + 四个维度都能从结果列表钻取回填报单与指标明细;计算明细逐指标给出得分率、得分与「已调整」标记 —— 分数从哪来,一路点得回去。 + +
+
图 7四维汇总口径。口径的唯一真值在 src/lib/aggregate.ts,是纯函数;换口径改的是这一个文件,不是四张报表各改一遍。到人与分管领导两个维度是跨对象加权计算,这也是它们必须由服务端算、而不能由视图公式表达的原因。
+
+ +
+

结果之上还有三张报表与两个看板:组织单元 × 维度矩阵(可钻取)、到人得分指标完成情况,以及结果看板与流程进度看板。看板回答的是「整体什么水平」,报表回答的是「具体哪一格」。

+
+ +
+
+
考核结果列表,按维度列出部门与分管领导两条结果,上方有部门、分公司、到人、分管领导四个页签
+
图 19考核结果列表,四个维度各一个页签。分管领导维度那一行的得分,是它下面几个主体按主体权重加权出来的——点进去能看到是哪几个主体。
+
+
+
考核结果看板的统计卡片与各组织单元平均得分图表,页面上方有汇总维度下拉
+
图 20结果看板:结果条数、平均分、最高最低分与各组织单元平均得分,「汇总维度」下拉可以只看某一个维度。
+
+
+
+ +
+
+

09归档与审计

+

归档是一个周期的句号。快照生成之后,这一期的数据就不再是「当前数据」,而是一份带校验和的历史记录——想改也改不动。

+ +

归档快照

+

人力审核在「已通过」的填报单上点归档,系统把填报单、明细、加减分的当时状态序列化成一份 JSON 载荷,算出 SHA-256 校验和一并存下。此后填报单、明细、加减分全部转为只读,hook 拒绝任何改写与删除。校验和用于核验快照内容未被改动:任何时候都能把快照重新算一遍校验和,与存档值比对。

+

归档之后发现错了怎么办?不回改,在下一周期修正——这是客户确认的第 11 项口径。一份能被追溯修改的历史记录,和没有历史记录差别不大。

+ +

审计查询

+

审核记录对象只增不改,按时间倒序列出每一次流程动作:记录编号、填报单、动作、节点、经办人、原因或意见、时间。加减分与数据调整的审批留痕也走同一张表。支持按填报单、动作、经办人、时间范围筛选并导出,另有「驳回记录」与「时间线」两个视图。

+

经办人一律以登录账号为准:提交人、审批人、归档人、调整发起人这些字段由系统盖章,界面上不给手工填写的入口。

+
+ +
+
+
归档快照列表中的一条快照记录,含方案、考核主体、最终得分、校验和与归档人
+
图 21已封存 归档快照记录:方案、考核主体、最终得分、校验和、归档人。这一行不可修改、不可删除。
+
+
+
审核记录列表,按时间倒序列出流程动作,工具条上有筛选与导出
+
图 22审核记录。一张单从生成、提交、核对、审核、调整到归档的完整轨迹,每条都带经办人与时间,可筛选可导出。
+
+
+
+ +
+
+

10权限与数据范围

+

「本部门 / 本分公司 / 分管范围」这三个词,在系统里不是配置项,而是方案发布时算出来的结果。这决定了一件事:组织调整、换分管领导、增减参与主体,都只改数据,不改系统配置。

+ +

为什么不做成「按组织层级授权」

+

常见做法是按组织架构树给权限:某人属于哪个部门,就能看该部门及以下的数据。这套做法在考核场景下会立刻失效——分管领导分管的主体跨部门、逐期变化,到人分工也跨板块。所以数据范围的真值不在组织树上,而在方案里:方案说了谁参与、谁分管、谁承接,可见范围就照着生成。

+
+ +
+
+ + + + + + + + + 基线 · 不发方案,谁都看不见 + 填报单、填报明细、核对任务、数据调整、考核结果的默认可见性(OWD)= private;部门、分公司、分管领导三类岗位的权限集读范围 = 仅本人。 + 共享规则只能放宽,不能收窄 —— 越权访问在数据层被拒绝,不依赖界面隐藏。 + + + 参与主体 + 主体 = 组织单元 + + + 分管领导 + 主体 → 人 + + + 到人分工 + 员工 × 部门板块 + + + + + + + 方案发布 + 读三类配置 + 写入共享规则 + 条件里带方案标识 + 规则名按方案加前缀 + sharing-service.ts + + + + + + + 部门 / 分公司填报人员 + 看得见:本单元那张填报单及其全部明细,可编辑直至提交 + + + 分公司核对人员 + 看得见:本分公司的核对任务与被核对的填报数据,只读 + + + 分管领导 + 看得见:分管主体的填报单与考核结果,可审批 + + + 换人、撤主体怎么办 + 重新发布时对本方案的规则做一次对账:换掉分管领导、撤掉参与 + 主体、撤掉到人分工,旧授权当场失效;历史方案的授权原样保留。 + + + 方案关闭 / 归档之后 + 规则转为「留读、去写」:填报单、核对任务、数据调整由可编辑 + 降为只读 —— 历史还查得到,但谁也改不动了。 + +
+
图 8数据范围随方案生成。这条设计的实际收益在实施阶段:接入一家新单位的组织架构,或者本单位年中调整部门,都不需要动系统里的任何一条权限配置——重新发布方案,可见范围自己就对了。
+
+
+ +
+
+

11系统架构与对象模型

+

系统建在 ObjectStack 17.x 元数据平台上:组织、账号、权限、审计、导入导出、列表与表单这些通用能力直接复用平台;考核专属的六件事——计分、审核状态机、发布生成、调整落地、快照只读、结果汇总——由服务端代码点承担。

+ +

自建了什么,复用了什么

+
+

直接复用平台

组织单元与用户、岗位与权限集、共享规则、字段历史与审计日志、列表 / 表单 / 内联网格、Excel 导入导出、报表与看板、账号安全策略。这部分一行代码没写。

+

六个服务端代码点

计分引擎、审核状态机、方案发布生成、调整落地重算、归档快照与只读锁、结果汇总。它们都是平台 hook 与函数扩展,不改平台包

+

纯函数 + 单元测试

计分、状态机、汇总、CEL 求值四件事写成不依赖运行时的纯函数,139 条单元测试逐条守住口径;其中 64 条守数据范围、29 条守岗位闸门。

+

平台问题只上报

遇到平台能力边界(导航项按岗位裁剪、视图页签溢出、多租户组织归属)一律向平台仓提 issue,应用侧只允许带环境闸门的临时夹具,并在夹具处注明对应 issue。

+
+
+ +
+
+ + + + + + + + 平台对象 —— 复用,不自建 + + 组织单元 + sys_business_unit + 部门 / 分公司 · 考核主体 · 部门板块 · 核对方 + + + 用户 + sys_user + 到人分工 · 分管领导 · 审核人 · 经办人 + + 六个岗位、六套权限集、密码策略与登录锁定、字段历史与审计日志, + 全部由平台账号与权限体系承担。 + + + + 指标库 + + 考核指标 + kpi_indicator + + 阶梯区间 + kpi_indicator_step + + + + 考核方案 + + 考核方案 + kpi_plan + + 流程节点 + kpi_plan_step + + 参与主体 + kpi_plan_subject + + 指标下达 + kpi_plan_indicator + + 到人分工 + kpi_staff_assignment + + 个人承接项 + kpi_personal_item + + 指标争议 + kpi_dispute + + + + 填报与审核 + + 填报单 + kpi_entry_sheet + + 填报明细 + kpi_entry_line + + 加减分 + kpi_bonus + + 核对任务 + kpi_check_task + + 数据调整 + kpi_adjustment + + 审核记录 + kpi_review_record + + + + 结果与归档 + + 考核结果 + kpi_result + + 归档快照 + kpi_snapshot + + + + 怎么读这张图 + 缩进 = 主从:随主对象一并冻结与删除 + 不缩进 = 独立对象,靠查找关联 + 17 个自建对象 · 9 组主从 · 22 处查找 + + + 模块间的四条关键关系 + + kpi_plan + + 发布时生成 + kpi_entry_sheet + + kpi_plan_indicator + + 发布时复制为冻结副本 + kpi_entry_line + + kpi_entry_sheet + + 审批通过时汇总 + kpi_result + + kpi_entry_sheet + + 归档时生成快照 + kpi_snapshot + +
+
图 9对象模型总图。主从关系(缩进)承担的是「一并冻结、一并删除」的语义:方案一发布,它下面的流程节点、参与主体、指标下达、到人分工全部随之冻结;填报单一归档,它下面的明细与加减分全部随之只读。
+
+
+ +
+
+

12交付状态与后续计划

+

一期已确认口径(设计方案第 1~10 章共 17 项)已全部实现并跑通;第二批产品化补充(第 18~35 项共 18 项)待逐条确认档次后排期。

+ +

已验证的内容

+

用演示数据把完整流程真实跑了一遍,并逐条断言:

+
    +
  • 发布生成填报单与指标明细;发布前完整性检查的每一条都能拦住对应的错误配置;
  • +
  • 线性、区间、阶梯三种计分结果与设计方案第 7 章示例逐位一致;
  • +
  • 提交冻结、并行核对、争议阻断推进、驳回原因必填、退回上一节点;
  • +
  • 审批通过后四维汇总,加减分与数据调整落地后自动重算;
  • +
  • 归档快照不可改、不可删;
  • +
  • 部门填报人员、分公司核对人员、分管领导三类账号的数据范围隔离,以及越节点操作被岗位闸门拒绝。
  • +
+

验证手段有三层:139 条单元测试守计分、状态机、汇总、数据范围与岗位闸门的口径;端到端脚本经 REST 走完整考核链路;两套演示种子档案(通用企业 / 软件公司,后者 24 个指标覆盖四种计分方式全部分支)支撑演示与验收。此外交付了一册分角色操作手册,六个业务角色分章成册,62 张截图全部取自真实界面。

+
+ +
+ + + + + + + + + +
档次含义项数状态
一期(已确认)设计方案第 1~10 章,客户 2026-09-02 逐条确认17已实现并跑通
一期补强不改已确认口径,补齐上线即需要的能力:通知与待办、公式试算器、批量操作与 Excel 直接导入、结果等级、我的考核、统一争议、结果确认与申诉、非功能指标目标值、数据初始化与运维基线9待客户确认档次
二期扩展考核能力:定性指标计分、否决项与不占权重指标、目标三档与派生指标、个人考核单、审批人来源扩展、跨周期汇总、取数配置与定时导入7待客户确认档次
产品化候选面向多单位复用:外部接口与单点登录、指标库与方案模板库2单独立项
表 4范围分级。确认方式是对第 18~35 项逐条回复「同意默认」「改为 X」或「删除」;确认后设计方案升 V2.0,一期补强项纳入本期验收范围。
+
+ +
+

工作项与协作口径

+

开发按工作项(GitHub Issue)推进,当前 20 个工作项中 15 项已开发完成待验收、3 项待细化、2 项已关闭。方案分级、需求符合度清单、测试报告一律挂在对应工作项评论上。

+

遇到平台能力边界一律只上报、不修复:向平台仓提交现象、最小复现、期望能力与平台版本,不在平台仓开 PR。目前已上报四项(导航项按岗位裁剪不生效、视图页签溢出导致队列够不着、多租户下种子组织单元的归属对齐),应用侧只保留带环境闸门的临时夹具,并在夹具处注明对应 issue,平台修复后删除。

+ +

下一步

+
    +
  1. 确认第二批 18 项的档次——这是当前唯一的阻塞项,决定本期验收范围。
  2. +
  3. 界面走查评审与真实数据量性能验证——目前尚未完成,需要贵方参与走查。
  4. +
  5. 现行 Excel 模板的口径翻译——把现有公式逐条翻译成四种计分方式之一,翻译不了的先提出来。这是实施阶段风险最集中的一步,建议配合公式试算器(第 19 项)一并推进。
  6. +
  7. 主数据准备——组织单元、用户与岗位、指标库、首个方案的导入与清洗。
  8. +
  9. 试运行——用一个真实考核周期跑一遍,值守并修复缺陷,出试运行报告。
  10. +
+ +
+

需要贵方注意的一处口径变化

+

本方案与现状最大的差别是:Excel 自由公式与跨表引用不进系统,公式的作用由「指标计分规则」统一承担。这是获得「口径唯一、可复核、可重算」的代价。实施阶段需要贵方业务口径的负责人配合,把现行考核表里的每一条公式对应到四种计分方式之一;对应不上的,请尽早提出来单独讨论。

+
+
+ +
+

本材料依据《KPI 考核管理系统 设计方案》V1.0(客户 2026-09-02 确认版,唯一需求基准)、《功能性需求文档》V1.3 与《操作手册》V0.1 编制,截图全部取自演示环境真实界面。

+

范围分级、待确认事项以设计方案第 11 章为准;方案分级、需求符合度清单与测试报告挂在对应工作项评论上。

+
+
+ +
+
+
diff --git a/package.json b/package.json index 3c3d6e5..6260b99 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "verify": "pnpm validate && pnpm typecheck && pnpm test && pnpm i18n:extract:check", "e2e": "node scripts/e2e-flow.mjs", "docs:docx": "node scripts/md2docx.cjs docs/00-设计方案.md docs/交付/KPI考核管理系统-设计方案-V1.0.docx", - "docs:manual": "node scripts/md2docx.cjs docs/手册/KPI考核管理系统-操作手册.md docs/交付/KPI考核管理系统-操作手册-V0.1.docx" + "docs:manual": "node scripts/md2docx.cjs docs/手册/KPI考核管理系统-操作手册.md docs/交付/KPI考核管理系统-操作手册-V0.1.docx", + "docs:report": "node scripts/inline-assets.cjs docs/汇报/KPI考核管理系统-解决方案汇报.html docs/交付/KPI考核管理系统-解决方案汇报.html" }, "dependencies": { "@objectstack/connector-mcp": "^17.2.0", diff --git a/scripts/inline-assets.cjs b/scripts/inline-assets.cjs new file mode 100644 index 0000000..ffc1e59 --- /dev/null +++ b/scripts/inline-assets.cjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/** + * 把 HTML 里的本地图片改写成内联 data URI,产出可独立发布的单文件页面。 + * + * 源稿(docs/汇报/*.html)里的图片走相对路径,指向操作手册已归档的截图 —— + * 仓库里存的是这一份:图片不重复入库,改图只改一处。发布用的副本需要自包含 + * (Artifact 的内容安全策略只放行 CDN 上的脚本与字体,图片一律拿不到), + * 所以发布前用本脚本把 换成 data URI。 + * + * node scripts/inline-assets.cjs <输入.html> <输出.html> + * + * 只处理 中的相对路径;已经是 data:/http(s): 的原样保留。 + * 找不到的文件直接报错退出 —— 静默漏图比报错难查得多。 + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const MIME = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', +}; + +function main(argv) { + const [input, output] = argv; + if (!input || !output) { + console.error('用法: node scripts/inline-assets.cjs <输入.html> <输出.html>'); + process.exit(2); + } + + const baseDir = path.dirname(path.resolve(input)); + const html = fs.readFileSync(input, 'utf8'); + + let count = 0; + let bytes = 0; + const missing = []; + + const out = html.replace(/(]*?\bsrc=")([^"]+)(")/g, (whole, head, src, tail) => { + if (/^(data:|https?:|\/\/)/.test(src)) return whole; + + const file = path.resolve(baseDir, decodeURIComponent(src)); + if (!fs.existsSync(file)) { + missing.push(src); + return whole; + } + + const mime = MIME[path.extname(file).toLowerCase()]; + if (!mime) { + missing.push(`${src}(不支持的扩展名)`); + return whole; + } + + const buf = fs.readFileSync(file); + count += 1; + bytes += buf.length; + return `${head}data:${mime};base64,${buf.toString('base64')}${tail}`; + }); + + if (missing.length > 0) { + console.error(`找不到 ${missing.length} 个图片文件:\n ${missing.join('\n ')}`); + process.exit(1); + } + + fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true }); + fs.writeFileSync(output, out); + + const mb = (n) => `${(n / 1024 / 1024).toFixed(2)} MB`; + console.log(`内联 ${count} 张图片(原始 ${mb(bytes)});产出 ${output} ${mb(Buffer.byteLength(out))}`); +} + +main(process.argv.slice(2)); From 3a96646cc5eecbc90bd3ec983741bc7bbafbe5e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 03:23:14 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(=E6=B1=87=E6=8A=A5):=20=E8=A1=A5=20PPT?= =?UTF-8?q?=20=E7=94=9F=E6=88=90=E8=84=9A=E6=9C=AC=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E4=B8=8E=E6=B1=87=E6=8A=A5=E7=A8=BF=E5=90=8C=E6=BA=90=E7=9A=84?= =?UTF-8?q?=2019=20=E9=A1=B5=E8=AE=B2=E7=A8=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 汇报稿是给人读的长文档,现场讲需要另一种密度。本脚本从同一批素材 (九张示意图 + 操作手册已归档的截图)出一份 19 页 PPT,每页一个论点、 一句结论、一张图,并带讲稿备注。 配色取应用自身的品牌色(src/apps/index.ts 的 primaryColor #1D4ED8)与 汇报稿同一套语义色(通过绿 / 拦截琥珀 / 驳回红),三份交付物看起来是 一家的。版式按内容分四类:整图页、双截图页、卡片页、数据页,不是同一个 「标题 + 项目符号」模板套 19 遍。 依赖 pptxgenjs,刻意**不进 package.json** —— 一次性成稿工具,不值得让元 数据项目的依赖图为它变长;脚本头注写明了 pnpm add -D 的装法。示意图同理 不入库:它们是汇报稿 HTML 里内联 SVG 的渲染产物,可随时按 figure 重新截取, 入库就多一份会与源稿走散的副本。 已验证:pptx 结构校验通过;逐页几何检查(出界 / 文字溢出 / 文字框重叠) 0 条问题。本次修掉三处真实缺陷 —— 40pt 统计数字撑破 0.62" 文本框、 计分曲线页的脚注压住页码、交付状态页的状态标签与行内文字重叠 1.29"。 LibreOffice 在本会话环境里连纯文本都转换不了,渲染式目视 QA 走的是 从 pptx 真实几何回放的近似预览,不是 PowerPoint 原生渲染。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QhMsYoDpoMGeQLf24eTawP --- README.md | 1 + scripts/build-report-deck.mjs | 657 ++++++++++++++++++++++++++++++++++ 2 files changed, 658 insertions(+) create mode 100644 scripts/build-report-deck.mjs diff --git a/README.md b/README.md index ac39f84..fdbbbfa 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ | [docs/02-总体方案蓝图.md](docs/02-总体方案蓝图.md) | os 能力映射、对象模型总图、模块依赖与开发顺序 | | [docs/手册/KPI考核管理系统-操作手册.md](docs/手册/KPI考核管理系统-操作手册.md) | 分角色操作手册 V0.1(送审稿):通用操作 + 五个业务角色分章,含真实系统截图;Word 成品在 [docs/交付/](docs/交付/),由 `pnpm docs:manual` 从本源稿生成 | | [docs/汇报/KPI考核管理系统-解决方案汇报.html](docs/汇报/KPI考核管理系统-解决方案汇报.html) | 解决方案汇报稿:业务流程、计分与汇总口径、权限模型九张示意图 + 26 张界面截图。图片走相对路径指向操作手册的截图(不重复入库);发布用的自包含单文件由 `pnpm docs:report` 生成 | +| [scripts/build-report-deck.mjs](scripts/build-report-deck.mjs) | 从汇报稿同一批素材生成 19 页汇报 PPT(九张示意图 + 八张界面截图,每页带讲稿备注)。示意图需先从汇报稿 HTML 的内联 SVG 按 `figure` 截取导出;脚本头注说明了用法与依赖 | | [CLAUDE.md](CLAUDE.md) | 开发约定与 dev-issue 启用清单 | ## 快速开始 diff --git a/scripts/build-report-deck.mjs b/scripts/build-report-deck.mjs new file mode 100644 index 0000000..bec2b53 --- /dev/null +++ b/scripts/build-report-deck.mjs @@ -0,0 +1,657 @@ +/** + * 从汇报稿源稿生成 19 页汇报 PPT(pptxgenjs)。 + * + * 内容与 docs/汇报/KPI考核管理系统-解决方案汇报.html 同源:九张业务示意图 + + * 八张界面截图,每页带讲稿备注。配色取应用自身的品牌色(src/apps/index.ts 的 + * primaryColor)与汇报稿同一套语义色,三份交付物看起来是一家的。 + * + * 用法: + * node scripts/build-report-deck.mjs <示意图目录> [输出.pptx] + * + * <示意图目录> 需含九张 PNG(图1-角色动线泳道图.png … 图9-对象模型总图.png), + * 由汇报稿 HTML 里的内联 SVG 渲染导出 —— 图是页面的一部分,不单独入库, + * 需要时用无头浏览器按 figure 截取。截图直接读 docs/手册/图片/ 下操作手册已归档的那批。 + * + * 依赖 pptxgenjs,**未列入 package.json** —— 这是一次性的成稿工具,不值得让 + * 元数据项目的依赖图为它变长。要跑先装:pnpm add -D pptxgenjs + * + * 生成后务必跑一遍校验(pptxgenjs 会写出 PowerPoint 拒绝打开的图表 XML): + * python3 /scripts/office/validate.py <输出.pptx> + */ + +import { createRequire } from 'node:module'; +import fs from 'node:fs'; +import path from 'node:path'; +const require = createRequire(import.meta.url); +const PptxGenJS = require('pptxgenjs'); + +// ── 取自应用自身的品牌色(src/apps/index.ts 的 primaryColor)+ 汇报稿同一套语义色 ── +const NAVY = '111A3E'; // 主色:封面、结尾、强调块 +const BLUE = '1D4ED8'; // 品牌蓝:重点、图标 +const BLUE_D = '12379E'; +const TINT = 'F1F4F8'; // 卡片底 +const ICE = 'C3D2F0'; // 深色底上的次要文字 +const INK = '151A21'; +const MUTED = '5A6675'; +const RULE = 'DBE2EB'; +const OK = '0E7A57'; +const WARN = '96590C'; +const STOP = 'A81F27'; + +const FONT = 'Microsoft YaHei'; + +const HERE = path.dirname(new URL(import.meta.url).pathname); +const REPO = path.resolve(HERE, '..'); +const SHOT = path.join(REPO, 'docs/手册/图片'); + +const DIA = process.argv[2]; +if (!DIA || !fs.existsSync(DIA)) { + console.error('用法: node scripts/build-report-deck.mjs <示意图目录> [输出.pptx]'); + console.error(' <示意图目录> 需含「图1-…png」到「图9-…png」九张图。'); + process.exit(2); +} + +const W = 13.333, H = 7.5, M = 0.62; +const CW = W - M * 2; + +/** 读 PNG 宽高(IHDR:signature 8 + length 4 + "IHDR" 4,之后是 4+4 字节大端宽高)。 */ +function pngSize(file) { + const b = fs.readFileSync(file, { length: 33 }); + return { w: b.readUInt32BE(16), h: b.readUInt32BE(20) }; +} + +/** 等比缩放并在给定框内居中。 */ +function fit(file, bx, by, bw, bh) { + const { w, h } = pngSize(file); + const s = Math.min(bw / w, bh / h); + const dw = w * s, dh = h * s; + return { path: file, x: bx + (bw - dw) / 2, y: by + (bh - dh) / 2, w: dw, h: dh }; +} + +const dia = (n) => path.join(DIA, n); +const shot = (n) => path.join(SHOT, n); + +const pres = new PptxGenJS(); +pres.layout = 'LAYOUT_WIDE'; // 必须在 addSlide 之前 +pres.author = 'KPI 考核管理系统项目组'; +pres.title = 'KPI 考核管理系统 解决方案汇报'; + +let pageNo = 0; + +/** 浅色内容页的统一版头:眉标 + 标题 + 一句话结论。返回内容区起始 y。 */ +function head(s, eyebrow, title, takeaway) { + s.background = { color: 'FFFFFF' }; + s.addShape(pres.ShapeType.rect, { x: M, y: 0.42, w: 0.1, h: 0.1, fill: { color: BLUE }, line: { color: BLUE } }); + s.addText(eyebrow, { + x: M + 0.2, y: 0.32, w: 8, h: 0.3, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12, color: BLUE, bold: true, charSpacing: 1, valign: 'middle', + }); + s.addText(title, { + x: M, y: 0.66, w: CW, h: 0.56, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 30, bold: true, color: INK, valign: 'middle', + }); + if (takeaway) { + s.addText(takeaway, { + x: M, y: 1.24, w: CW, h: 0.34, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 14, color: MUTED, valign: 'top', + }); + return 1.72; + } + return 1.36; +} + +function foot(s) { + pageNo += 1; + s.addText(String(pageNo), { + x: W - M - 0.6, y: H - 0.52, w: 0.6, h: 0.3, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 10, color: MUTED, align: 'right', valign: 'middle', + }); + s.addText('KPI 考核管理系统 · 解决方案汇报', { + x: M, y: H - 0.52, w: 6, h: 0.3, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 10, color: 'A7B0BC', valign: 'middle', + }); +} + +/** 一张图占满内容区的版式。 */ +function diagramSlide({ eyebrow, title, takeaway, image, note }) { + const s = pres.addSlide(); + const top = head(s, eyebrow, title, takeaway); + s.addImage(fit(image, M, top, CW, H - top - 0.72)); + foot(s); + if (note) s.addNotes(note); + return s; +} + +/** 两张界面截图并排,各带一句说明。 */ +function shotsSlide({ eyebrow, title, takeaway, items, note }) { + const s = pres.addSlide(); + const top = head(s, eyebrow, title, takeaway); + const gap = 0.4; + const cw = (CW - gap) / 2; + const imgH = H - top - 1.5; + items.forEach((it, i) => { + const bx = M + i * (cw + gap); + s.addImage(fit(shot(it.file), bx, top, cw, imgH)); + if (it.tag) { + s.addShape(pres.ShapeType.roundRect, { + x: bx, y: top + imgH + 0.14, w: 0.72, h: 0.26, rectRadius: 0.05, + fill: { color: it.tagColor }, line: { color: it.tagColor }, + }); + s.addText(it.tag, { + x: bx, y: top + imgH + 0.14, w: 0.72, h: 0.26, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 10, bold: true, color: 'FFFFFF', align: 'center', valign: 'middle', + }); + } + s.addText(it.caption, { + x: bx + (it.tag ? 0.82 : 0), y: top + imgH + 0.1, w: cw - (it.tag ? 0.82 : 0), h: 0.72, + isTextBox: true, margin: 0, fontFace: FONT, fontSize: 12, color: MUTED, valign: 'top', + }); + }); + foot(s); + if (note) s.addNotes(note); + return s; +} + +// ══════════════════════════════════════════════════════════════════ +// 1 封面 +// ══════════════════════════════════════════════════════════════════ +{ + const s = pres.addSlide(); + s.background = { color: NAVY }; + s.addShape(pres.ShapeType.rect, { x: 1.0, y: 1.72, w: 0.13, h: 0.13, fill: { color: BLUE }, line: { color: BLUE } }); + s.addText('解决方案汇报', { + x: 1.26, y: 1.62, w: 8, h: 0.32, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 14, color: ICE, bold: true, charSpacing: 2, valign: 'middle', + }); + s.addText('KPI 考核管理系统', { + x: 1.0, y: 2.16, w: 11, h: 1.16, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 54, bold: true, color: 'FFFFFF', valign: 'middle', + }); + s.addText('指标库 · 方案配置 · 指标下达 · 数据填报 · 并行核对 · 审核审批 · 实时计分 · 数据调整 · 四维汇总 · 归档快照', { + x: 1.0, y: 3.38, w: 10.6, h: 0.4, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 15, color: ICE, valign: 'middle', + }); + s.addText('把考核从 Excel 与聊天工具的来回传递,搬成一条口径唯一、全程留痕、发布即冻结的线上流程。', { + x: 1.0, y: 3.92, w: 10.6, h: 0.4, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 15, color: 'FFFFFF', valign: 'middle', + }); + const meta = [ + ['需求基准', '《设计方案》V1.0 客户确认版'], + ['技术底座', 'ObjectStack 17.x 元数据平台'], + ['当前阶段', '全流程已跑通 · PM 验收中'], + ['汇报日期', '2026 年 09 月 07 日'], + ]; + meta.forEach(([k, v], i) => { + const x = 1.0 + i * 2.72; + s.addText(k, { + x, y: 5.32, w: 2.5, h: 0.26, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 10.5, color: '8095C4', bold: true, charSpacing: 1, valign: 'middle', + }); + s.addText(v, { + x, y: 5.6, w: 2.6, h: 0.42, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12.5, color: 'FFFFFF', valign: 'top', + }); + }); + s.addNotes('开场:这套系统覆盖一个考核周期的全部环节。今天重点讲三件事 —— 流程怎么转、分怎么算、谁能看见什么。'); +} + +// ══════════════════════════════════════════════════════════════════ +// 2 三个痛点 + 关键数字 +// ══════════════════════════════════════════════════════════════════ +{ + const s = pres.addSlide(); + const top = head(s, '为什么要建', '用 Excel 做考核,三件事必然失控', + '考核本身不复杂,难的是口径与留痕。'); + const pains = [ + ['口径不唯一', '同一个「完成率」,各部门表里的公式写法不同;表一改,历史周期的分数跟着变 —— 过去算过的账对不上。'], + ['过程不可追', '谁在什么时候改了哪个数、为什么改、谁批准的,散落在聊天记录和邮件附件里,复核时拼不回来。'], + ['协作靠人盯', '分公司核对、人力审核、领导审批的先后顺序靠人催,谁卡在哪一步没有统一视图。'], + ]; + const gap = 0.34, cw = (CW - gap * 2) / 3; + pains.forEach(([t, d], i) => { + const x = M + i * (cw + gap); + s.addShape(pres.ShapeType.roundRect, { + x, y: top, w: cw, h: 2.32, rectRadius: 0.08, fill: { color: TINT }, line: { color: RULE }, + }); + s.addShape(pres.ShapeType.ellipse, { + x: x + 0.34, y: top + 0.32, w: 0.42, h: 0.42, fill: { color: STOP }, line: { color: STOP }, + }); + s.addText(String(i + 1), { + x: x + 0.34, y: top + 0.32, w: 0.42, h: 0.42, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 15, bold: true, color: 'FFFFFF', align: 'center', valign: 'middle', + }); + s.addText(t, { + x: x + 0.34, y: top + 0.88, w: cw - 0.68, h: 0.38, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 19, bold: true, color: INK, valign: 'middle', + }); + s.addText(d, { + x: x + 0.34, y: top + 1.3, w: cw - 0.68, h: 0.86, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12.5, color: MUTED, valign: 'top', lineSpacingMultiple: 1.25, + }); + }); + + const stats = [['17', '业务对象'], ['6', '岗位与权限集'], ['4', '计分方式'], ['4', '汇总维度'], ['8', '发布前检查项'], ['139', '单元测试用例']]; + const sy = top + 2.78; + const sw = CW / 6; + s.addText('系统规模', { + x: M, y: sy - 0.4, w: 6, h: 0.3, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12, bold: true, color: BLUE, charSpacing: 1, valign: 'middle', + }); + stats.forEach(([n, l], i) => { + const x = M + i * sw; + s.addText(n, { + x, y: sy, w: sw - 0.2, h: 0.8, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 40, bold: true, color: NAVY, valign: 'middle', + }); + s.addText(l, { + x, y: sy + 0.82, w: sw - 0.2, h: 0.3, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12, color: MUTED, valign: 'top', + }); + }); + foot(s); + s.addNotes('三个痛点是客户自己在需求文档里写的现状。右下六个数字是系统当前的实际规模,不是估算。'); +} + +// ══════════════════════════════════════════════════════════════════ +// 3 五个机制 +// ══════════════════════════════════════════════════════════════════ +{ + const s = pres.addSlide(); + const top = head(s, '解法概览', '系统给出的五个机制', + '每一个机制都对应上一页的某一个痛点,而不是功能清单上的一条。'); + const items = [ + ['计分口径唯一真值', '四种计分方式是数据不是代码。全系统任何一个得分都出自同一个计分引擎,视图与公式字段里不允许二次实现。', BLUE], + ['发布即冻结', '方案一发布,目标值、权重、计分规则以副本形式落到每一行明细上。之后改指标库,历史周期分毫不动。', BLUE], + ['状态机带闸门', '未填全不能提交、有争议不能推进、驳回必须写原因、归档后全表只读 —— 规则全在服务端,按钮只写「待执行动作」。', WARN], + ['只增不改的留痕', '每次提交、核对、审核、驳回、调整、归档都写一条审核记录;归档快照带 SHA-256 校验和,不可修改删除。', OK], + ['数据范围随方案生成', '「本部门 / 分管范围」由方案发布时按参与主体、分管领导、到人分工自动写入。组织调整只改数据,不改元数据。', BLUE], + ]; + const gap = 0.3; + const cw = (CW - gap * 2) / 3; + const ch = 2.28; + items.forEach(([t, d, c], i) => { + const col = i % 3, row = Math.floor(i / 3); + const x = M + col * (cw + gap); + const y = top + row * (ch + gap); + s.addShape(pres.ShapeType.roundRect, { + x, y, w: cw, h: ch, rectRadius: 0.08, fill: { color: TINT }, line: { color: RULE }, + }); + s.addShape(pres.ShapeType.ellipse, { + x: x + 0.3, y: y + 0.3, w: 0.34, h: 0.34, fill: { color: c }, line: { color: c }, + }); + s.addText(t, { + x: x + 0.76, y: y + 0.26, w: cw - 1.06, h: 0.42, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 16, bold: true, color: INK, valign: 'middle', + }); + s.addText(d, { + x: x + 0.3, y: y + 0.82, w: cw - 0.6, h: 1.24, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12, color: MUTED, valign: 'top', lineSpacingMultiple: 1.25, + }); + }); + // 第六格:范围边界的提醒,放在 2×3 网格的空位 + const x6 = M + 2 * (cw + gap), y6 = top + (ch + gap); + s.addShape(pres.ShapeType.roundRect, { + x: x6, y: y6, w: cw, h: ch, rectRadius: 0.08, fill: { color: NAVY }, line: { color: NAVY }, + }); + s.addText('代价:一处口径变化', { + x: x6 + 0.3, y: y6 + 0.3, w: cw - 0.6, h: 0.4, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 16, bold: true, color: 'FFFFFF', valign: 'middle', + }); + s.addText('Excel 自由公式与跨表引用不进系统,公式的作用由「指标计分规则」统一承担。这是换取「口径唯一、可复核、可重算」的代价,实施阶段需逐条翻译现行公式。', { + x: x6 + 0.3, y: y6 + 0.8, w: cw - 0.6, h: 1.26, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12, color: ICE, valign: 'top', lineSpacingMultiple: 1.25, + }); + foot(s); + s.addNotes('深色那一格是要主动讲的:这是本方案与现状最大的差别,也是实施阶段风险最集中的地方,不要等客户自己发现。'); +} + +// ══════════════════════════════════════════════════════════════════ +// 4 角色与职责 +// ══════════════════════════════════════════════════════════════════ +{ + const s = pres.addSlide(); + const top = head(s, '角色', '六个岗位,六套权限集', + '能看到什么由数据范围决定,能按哪个按钮由当前流程节点决定 —— 两条边界都在服务端。'); + const rows = [ + ['系统管理员', '维护组织、用户与岗位', '全部'], + ['人力审核', '维护指标库与方案、指标下达、发布方案、人力节点审核、登记加减分、审批调整、归档、审计查询', '全部'], + ['人力负责人', '审批加减分、审批数据调整', '全部'], + ['部门填报人员', '填报本部门数据并提交、发起数据调整申请', '本部门'], + ['分公司核对人员', '核对与本分公司相关的数据,只能「确认无误」或「提出争议」,不能改数值', '本分公司'], + ['分管领导', '分管主体的最终审批、查看分管范围的结果与看板', '分管范围'], + ]; + const c1 = 2.5, c3 = 1.6, c2 = CW - c1 - c3; + const rh = 0.62; + ['角色', '在系统里做什么', '能看到的范围'].forEach((h, i) => { + const x = M + (i === 0 ? 0 : i === 1 ? c1 : c1 + c2); + const w = i === 0 ? c1 : i === 1 ? c2 : c3; + s.addText(h, { + x: x + 0.14, y: top, w: w - 0.28, h: 0.42, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 11.5, bold: true, color: BLUE, charSpacing: 1, valign: 'middle', + }); + }); + rows.forEach((r, i) => { + const y = top + 0.5 + i * rh; + s.addShape(pres.ShapeType.rect, { + x: M, y, w: CW, h: rh, fill: { color: i % 2 ? 'FFFFFF' : TINT }, line: { color: 'FFFFFF' }, + }); + s.addText(r[0], { + x: M + 0.14, y, w: c1 - 0.28, h: rh, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 13.5, bold: true, color: INK, valign: 'middle', + }); + s.addText(r[1], { + x: M + c1 + 0.14, y, w: c2 - 0.28, h: rh, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12.5, color: MUTED, valign: 'middle', + }); + s.addText(r[2], { + x: M + c1 + c2 + 0.14, y, w: c3 - 0.28, h: rh, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12.5, bold: true, color: r[2] === '全部' ? MUTED : BLUE_D, valign: 'middle', + }); + }); + const ny = top + 0.5 + rows.length * rh + 0.3; + s.addShape(pres.ShapeType.roundRect, { + x: M, y: ny, w: CW, h: 0.76, rectRadius: 0.06, fill: { color: 'F6EAD8' }, line: { color: 'E3CFA8' }, + }); + s.addText([ + { text: '一条容易被忽略的约束:', options: { bold: true, color: WARN } }, + { text: '核对人员不能改数值。发现数据对不上只能提出争议,由填报部门经调整流程更正 —— 让「谁报的数谁负责」这条责任线不被核对环节冲淡。', options: { color: INK } }, + ], { + x: M + 0.28, y: ny, w: CW - 0.56, h: 0.76, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12.5, valign: 'middle', + }); + foot(s); + s.addNotes('分公司在方案里既是被考核主体、又是核对方 —— 这两件事由两个人做:分公司填报人员填本分公司那张单,分公司核对人员只对别人的数据表态。'); +} + +// ══════════════════════════════════════════════════════════════════ +// 5-7 流程三张图 +// ══════════════════════════════════════════════════════════════════ +diagramSlide({ + eyebrow: '业务流程 · 全景', + title: '一个考核周期的角色动线', + takeaway: '实线是单据的流转方向,虚线是人的动作触发的系统动作 —— 发布、保存、提交、审批、归档这五处,系统各自做一件不需要人参与的事。', + image: dia('图1-角色动线泳道图.png'), + note: '讲这张图的顺序:先横着念一遍五个阶段,再指最下面那条「系统 · 自动」泳道 —— 客户最关心的省人力,就在这一条。', +}); + +diagramSlide({ + eyebrow: '业务流程 · 规则', + title: '状态机与它的闸门:什么情况下走不动', + takeaway: '每一次流转都由服务端判定,按钮本身只写「待执行动作」;绕过按钮直接写状态字段,同样会被拦下。', + image: dia('图2-状态机与闸门.png'), + note: '重点讲两条:一是「有一家分公司提争议就不推进」,二是驳回退回核对节点时全部分公司重新核对 —— 避免半有效状态。', +}); + +diagramSlide({ + eyebrow: '方案配置与发布', + title: '发布即冻结:八项检查,以及为什么必须是副本', + takeaway: '八项检查任意一项不过,方案原地保持草稿并逐条给出问题;全部通过才生成填报单。', + image: dia('图3-发布即冻结.png'), + note: '客户最容易问的是「以后指标口径变了怎么办」。答案就在右下的时间轴:T2 改口径,已发布周期分毫不动,新口径从 T3 的下期方案开始生效。', +}); + +shotsSlide({ + eyebrow: '方案配置与发布 · 界面', + title: '发布不是「保存并推进」,而是一次全量体检', + takeaway: '检查不通过时系统不做部分发布 —— 方案原地不动,把问题逐条摆出来。', + items: [ + { file: '02-23-fabu-jiancha-butongguo.png', tag: '拦下', tagColor: STOP, + caption: '提示直接点名是哪个主体的哪一条:「市场部的权重合计为 90%,必须等于 100%」「华北分公司的培训完成率未设置目标值」,不是一句「配置有误」。' }, + { file: '02-24-fabu-chenggong.png', tag: '放行', tagColor: OK, + caption: '发布成功,状态条推进到「已发布」,盖上发布时间与发布人;右上角按钮换成「关闭方案」—— 同一个位置不会再出现第二次发布。' }, + ], + note: '现场可以演示一次失败发布再改到成功 —— 这是操作手册里给人力审核的练习任务之一。', +}); + +// ══════════════════════════════════════════════════════════════════ +// 9-11 计分 +// ══════════════════════════════════════════════════════════════════ +diagramSlide({ + eyebrow: '填报与计分', + title: '从实际值到最终得分,只有五步', + takeaway: '四种计分方式只在中间一段分岔 —— 换计分方式改的是一个下拉,不是一套算法。', + image: dia('图4-计分链路.png'), + note: '强调「口径唯一」的具体含义:计分引擎是纯函数,可单元测试、可逐条复核;每一行都落一段计算说明,得分和手算对不上时打开就能看到差在哪一步。', +}); + +{ + const s = pres.addSlide(); + const top = head(s, '填报与计分', '同一个完成率,四种方式给出的分完全不同', + '选哪一种取决于指标想鼓励什么:线性鼓励多超额,区间插值容忍未达标但压缩超额收益,阶梯只认档位不认零头。'); + const chartW = 7.9; + s.addImage(fit(dia('图5-四种计分方式曲线.png'), M, top, chartW, H - top - 0.72)); + const rx = M + chartW + 0.42; + const rw = CW - chartW - 0.42; + s.addText('完成率 97% 时', { + x: rx, y: top + 0.06, w: rw, h: 0.34, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 13, bold: true, color: BLUE, charSpacing: 1, valign: 'middle', + }); + const cases = [['97', '完成率线性', '得分率 = 完成率,限定在保底与封顶之间', '2563EB'], + ['92.5', '区间插值', '下限 60% 得 0,100% 得满分,中间按直线插值', '0E8A6E'], + ['90', '阶梯计分', '落在「95~100%」这一档,取该档得分率', 'B26A0B']]; + cases.forEach(([n, t, d, c], i) => { + const y = top + 0.52 + i * 1.46; + s.addShape(pres.ShapeType.roundRect, { + x: rx, y, w: rw, h: 1.28, rectRadius: 0.08, fill: { color: TINT }, line: { color: RULE }, + }); + s.addText(n, { + x: rx + 0.24, y: y + 0.12, w: 1.5, h: 0.62, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 34, bold: true, color: c, valign: 'middle', + }); + s.addText(t, { + x: rx + 1.72, y: y + 0.16, w: rw - 1.96, h: 0.54, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 14, bold: true, color: INK, valign: 'middle', + }); + s.addText(d, { + x: rx + 0.24, y: y + 0.74, w: rw - 0.48, h: 0.44, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 11.5, color: MUTED, valign: 'top', + }); + }); + s.addText('第四种「自定义公式」由 CEL 表达式直接给出得分,形状不固定,不在本图。', { + x: rx, y: top + 4.7, w: rw, h: 0.48, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 11, color: MUTED, valign: 'top', lineSpacingMultiple: 1.2, + }); + foot(s); + s.addNotes('这一页是全场最容易引起讨论的:三个数字的差异不是误差,是口径选择。实施阶段翻译现行 Excel 公式,就是在这三条曲线里选一条。'); +} + +shotsSlide({ + eyebrow: '填报与计分 · 界面', + title: '保留 Excel 的手感:按行填数,保存即出分', + takeaway: '变的只有口径来源 —— 得分不再由每张表自己的公式算出,而是由全系统唯一的计分引擎算出,并把算式写下来备查。', + items: [ + { file: '03-03-luru-shijizhi.png', + caption: '双击单元格填实际值,保存后完成率、得分率、得分当场回填到同一张表,不用跳页、不用等批处理。指标条数多时可走「导出 XLSX → 填 → 导入」。' }, + { file: '03-04-jisuan-shuoming.png', tag: '可复核', tagColor: BLUE, + caption: '每一行都落一段「计算说明」:完成率怎么来的、封顶保底在哪一步生效、得分率取了哪一档 —— 逐条可复核。' }, + ], + note: '「保存即出分」是填报人员感知最强的一点,值得现场演示一次。', +}); + +shotsSlide({ + eyebrow: '核对与审核 · 界面', + title: '并行核对与逐级审核:放行,或写明原因退回', + takeaway: '各分公司同时核对,但推进条件是「全部确认无误」—— 有一家提出争议,单据就停在核对节点。', + items: [ + { file: '04-01-daiwo-hedui.png', + caption: '「待我核对」队列只列得出与本分公司相关的任务 —— 别的分公司的任务在数据层就不可见,不是靠视图筛选藏起来的。' }, + { file: '02-26-bohui-duihuakuang.png', tag: '退回', tagColor: STOP, + caption: '驳回原因必填,不填点不动;填完退回上一节点,并在审核记录里留下一条带原因的驳回记录。' }, + ], + note: '审核人从队列进入,不需要知道单据在哪个环节 —— 队列本身就是环节。三个待办队列在左侧菜单有直达入口。', +}); + +// ══════════════════════════════════════════════════════════════════ +// 13-15 调整、汇总 +// ══════════════════════════════════════════════════════════════════ +diagramSlide({ + eyebrow: '加减分与数据调整', + title: '三条受控的改数通道,以及它们的重算传播', + takeaway: '考核不可能一次填对。每条通道都要审批、都要留痕、都会触发重算 —— 而不是让人回到表格里悄悄把数字改掉。', + image: dia('图6-调整与加减分的重算传播.png'), + note: '关键在最右边那一格:任何一条通道落地,四维结果与排名都自动重算,不需要有人记得去「刷新汇总」—— 这是 Excel 时代最容易漏掉的一步。', +}); + +diagramSlide({ + eyebrow: '汇总与结果', + title: '一个最终得分,落到四个不同的问题上', + takeaway: '这个部门考得怎么样、这家分公司排第几、张三个人该拿多少、王总分管的这一摊整体如何 —— 四个维度、四套口径,一次生成。', + image: dia('图7-四维汇总口径.png'), + note: '到人与分管领导两个维度是跨对象加权计算,这也是它们必须由服务端算、而不能由视图公式表达的原因。口径的唯一真值在一个文件里。', +}); + +shotsSlide({ + eyebrow: '汇总与结果 · 界面', + title: '结果、看板与三张报表', + takeaway: '看板回答「整体什么水平」,报表回答「具体哪一格」,两者都能钻取回填报单与指标明细。', + items: [ + { file: '06-04-fenguan-zhuti-jieguo.png', + caption: '考核结果列表,四个维度各一个页签。分管领导维度那一行的得分,是它下面几个主体按主体权重加权出来的 —— 点进去能看到是哪几个主体。' }, + { file: '06-05-jieguo-kanban.png', + caption: '结果看板:结果条数、平均分、最高最低分与各组织单元平均得分,「汇总维度」下拉可以只看某一个维度。' }, + ], + note: '另有三张报表:组织单元 × 维度矩阵(可钻取)、到人得分、指标完成情况。', +}); + +// ══════════════════════════════════════════════════════════════════ +// 16-17 权限、架构 +// ══════════════════════════════════════════════════════════════════ +diagramSlide({ + eyebrow: '权限与数据安全', + title: '「本部门 / 分管范围」不是配置项,是算出来的结果', + takeaway: '分管领导分管的主体跨部门、逐期变化,按组织层级授权会立刻失效 —— 所以可见范围的真值在方案里,不在组织树上。', + image: dia('图8-数据范围模型.png'), + note: '实际收益在实施阶段:接入一家新单位的组织架构,或者本单位年中调整部门,都不需要动系统里的任何一条权限配置 —— 重新发布方案,可见范围自己就对了。', +}); + +diagramSlide({ + eyebrow: '系统架构', + title: '17 个自建对象,六个服务端代码点', + takeaway: '组织、账号、权限、审计、导入导出、列表与表单直接复用平台;考核专属的六件事由服务端代码点承担,不改平台包。', + image: dia('图9-对象模型总图.png'), + note: '六个代码点:计分引擎、审核状态机、方案发布生成、调整落地重算、归档快照与只读锁、结果汇总。计分、状态机、汇总、CEL 求值写成纯函数,139 条单元测试守住口径。', +}); + +// ══════════════════════════════════════════════════════════════════ +// 18 交付状态 +// ══════════════════════════════════════════════════════════════════ +{ + const s = pres.addSlide(); + const top = head(s, '交付状态', '一期已跑通,第二批 18 项待确认档次', + '一期已确认口径(设计方案第 1~10 章共 17 项)已全部实现并逐条验证。'); + const tiers = [ + ['一期(已确认)', '17', '设计方案第 1~10 章,客户 2026-09-02 逐条确认', '已实现并跑通', OK], + ['一期补强', '9', '通知与待办、公式试算器、批量操作与 Excel 直接导入、结果等级、统一争议等', '待确认档次', WARN], + ['二期', '7', '定性指标计分、否决项与不占权重指标、目标三档与派生指标、个人考核单等', '待确认档次', WARN], + ['产品化候选', '2', '外部接口与单点登录、指标库与方案模板库', '单独立项', MUTED], + ]; + const rh = 0.92; + tiers.forEach((t, i) => { + const y = top + i * (rh + 0.10); + s.addShape(pres.ShapeType.roundRect, { + x: M, y, w: 7.55, h: rh, rectRadius: 0.06, fill: { color: TINT }, line: { color: RULE }, + }); + s.addText(t[1], { + x: M + 0.22, y, w: 0.82, h: rh, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 26, bold: true, color: t[4], align: 'center', valign: 'middle', + }); + s.addText(t[0], { + x: M + 1.14, y: y + 0.1, w: 4.66, h: 0.34, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 14, bold: true, color: INK, valign: 'middle', + }); + s.addText(t[2], { + x: M + 1.14, y: y + 0.44, w: 4.66, h: 0.44, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 10.5, color: MUTED, valign: 'top', + }); + s.addText(t[3], { + x: M + 7.55 - 1.5, y, w: 1.34, h: rh, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 11, bold: true, color: t[4], align: 'right', valign: 'middle', + }); + }); + const bx = M + 7.55 + 0.34; + const bw = CW - 7.55 - 0.34; + s.addShape(pres.ShapeType.roundRect, { + x: bx, y: top, w: bw, h: 3.98, rectRadius: 0.08, fill: { color: NAVY }, line: { color: NAVY }, + }); + s.addText('下一步', { + x: bx + 0.32, y: top + 0.24, w: bw - 0.64, h: 0.4, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 17, bold: true, color: 'FFFFFF', valign: 'middle', + }); + const steps = [ + '确认第二批 18 项的档次 —— 当前唯一的阻塞项,决定本期验收范围', + '界面走查评审与真实数据量性能验证,需贵方参与走查', + '现行 Excel 模板的口径翻译,建议配合公式试算器一并推进', + '主数据准备:组织、用户与岗位、指标库、首个方案', + '试运行:一个真实考核周期跑一遍,出试运行报告', + ]; + steps.forEach((t, i) => { + const y = top + 0.78 + i * 0.64; + s.addShape(pres.ShapeType.ellipse, { + x: bx + 0.32, y: y + 0.06, w: 0.3, h: 0.3, fill: { color: BLUE }, line: { color: BLUE }, + }); + s.addText(String(i + 1), { + x: bx + 0.32, y: y + 0.06, w: 0.3, h: 0.3, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 11, bold: true, color: 'FFFFFF', align: 'center', valign: 'middle', + }); + s.addText(t, { + x: bx + 0.74, y, w: bw - 1.06, h: 0.56, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 11.5, color: ICE, valign: 'middle', lineSpacingMultiple: 1.15, + }); + }); + // 底部:验证手段三层 —— 交付状态的证据,不是口号 + const vy = 6.22; + s.addText('验证手段', { + x: M, y: vy - 0.30, w: 4, h: 0.3, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12, bold: true, color: BLUE, charSpacing: 1, valign: 'middle', + }); + const proofs = [ + ['139 条单元测试', '守计分、状态机、汇总、数据范围与岗位闸门的口径'], + ['端到端脚本', '经 REST 走完整考核链路,发布到归档逐条断言'], + ['两套演示种子档案', '通用企业 / 软件公司,四种计分方式全覆盖'], + ]; + const pw = CW / 3; + proofs.forEach(([t, d], i) => { + const x = M + i * pw; + s.addText(t, { + x, y: vy, w: pw - 0.3, h: 0.3, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 14, bold: true, color: INK, valign: 'middle', + }); + s.addText(d, { + x, y: vy + 0.32, w: pw - 0.3, h: 0.42, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 11.5, color: MUTED, valign: 'top', + }); + }); + foot(s); + s.addNotes('另交付一册分角色操作手册,六个业务角色分章成册,62 张截图全部取自真实界面。'); +} + +// ══════════════════════════════════════════════════════════════════ +// 19 结尾 +// ══════════════════════════════════════════════════════════════════ +{ + const s = pres.addSlide(); + s.background = { color: NAVY }; + s.addText('需要贵方决定的一件事', { + x: 1.0, y: 1.72, w: 11, h: 0.5, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 15, color: '8095C4', bold: true, charSpacing: 2, valign: 'middle', + }); + s.addText('对第 18~35 项逐条回复\n「同意默认」「改为 X」或「删除」', { + x: 1.0, y: 2.32, w: 11, h: 1.6, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 40, bold: true, color: 'FFFFFF', valign: 'middle', lineSpacingMultiple: 1.2, + }); + s.addText('确认后设计方案升 V2.0,「一期补强」的 9 项纳入本期验收范围,「二期」的 7 项进入排期。', { + x: 1.0, y: 4.16, w: 11, h: 0.44, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 15, color: ICE, valign: 'middle', + }); + s.addShape(pres.ShapeType.rect, { x: 1.0, y: 5.06, w: 1.2, h: 0.02, fill: { color: '3B4A7A' }, line: { color: '3B4A7A' } }); + s.addText('配套材料:《设计方案》V1.0 · 《操作手册》V0.1(六个业务角色分章,62 张真实界面截图)· 解决方案汇报稿(九张业务示意图)', { + x: 1.0, y: 5.32, w: 11, h: 0.44, isTextBox: true, margin: 0, + fontFace: FONT, fontSize: 12, color: '8095C4', valign: 'middle', + }); + s.addNotes('收尾时把球踢回去:唯一阻塞项是第二批 18 项的档次确认,确认完就能定本期验收范围。'); +} + +const out = process.argv[3] || path.join(REPO, 'docs/交付/KPI考核管理系统-解决方案汇报.pptx'); +fs.mkdirSync(path.dirname(out), { recursive: true }); +await pres.writeFile({ fileName: out }); +console.log('写出', out);