From fb6b49bb61c8e221c100862892625ada24550177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Sun, 6 Sep 2026 10:38:21 -0700 Subject: [PATCH] =?UTF-8?q?feat(pages,apps):=20=E5=BE=85=E5=8A=9E=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0=E4=B8=89=E4=B8=AA=E9=98=9F=E5=88=97=E5=8C=BA?= =?UTF-8?q?=E5=9D=97=20+=20=E5=BE=85=E5=AE=A1=E6=A0=B8/=E5=BE=85=E5=AE=A1?= =?UTF-8?q?=E6=89=B9/=E5=BE=85=E6=A0=B8=E5=AF=B9=E7=9B=B4=E8=BE=BE?= =?UTF-8?q?=E8=8F=9C=E5=8D=95(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 工作台 kpi_home 在「待填报的指标」「填报单」之外加三个只读网格: 「待我核对」(核对任务 status=pending,行动作 确认无误 / 提出争议)、 「待人力审核」「待领导审批」(填报单 status=hr_reviewing / leader_approving, 行动作 审核通过 / 驳回)。区块不按岗位显隐 —— 页面组件的 visibleWhen 不绑岗位, 且谁看到哪些行本来就由对象 OWD + 方案发布写入的动态共享规则 + 权限集 readScope 决定;空区块保留、pageSize 收到 10。 三个待办区块的 filter 过滤的是本对象自己的 status 列,不跨关系;「待填报的指标」 按「所属填报单为填报中」过滤仍然做不到(那是 kpi_entry_sheet.status,查询引擎 拒绝跨关系过滤,而 kpi_entry_line 上没有任何字段编码填报单状态),按 #36 「明细上若无该字段则如实记录,不加字段」如实记录在注释里,不加字段、不用退化条件。 导航「填报与审核」组新增三项,用 viewName 直指已存在的具名列表视图 (kpi_entry_sheet 的 hr / leader、kpi_check_task 的 pending)。改前分管领导的队列 藏在填报单页签栏的「还有 4 个」溢出菜单里(平台 objectstack-ai/objectstack#14883、 #14952),要 5 击才够得着;改后 3 击直达。三项不加 requiredPermissions:能看到几行 由数据范围决定,加能力闸门会挡掉本该用它的人。 翻译包经 pnpm i18n:extract 重生成(+3 个导航项 label)。 Co-Authored-By: Claude Fable 5.1 --- src/apps/index.ts | 19 ++++ src/pages/index.ts | 101 +++++++++++++++++--- src/translations/zh-CN.objects.generated.ts | 9 ++ 3 files changed, 116 insertions(+), 13 deletions(-) diff --git a/src/apps/index.ts b/src/apps/index.ts index 18ac455..ef92252 100644 --- a/src/apps/index.ts +++ b/src/apps/index.ts @@ -30,6 +30,25 @@ export const KpiApp = App.create({ id: 'group_entry', type: 'group', label: '填报与审核', icon: 'file-spreadsheet', children: [ { id: 'nav_sheets', type: 'object', objectName: 'kpi_entry_sheet', label: '填报单', icon: 'file-spreadsheet' }, + // ── 三个待办直达项(#36)────────────────────────────────────────── + // 队列本来就有(`EntrySheetViews.listViews.hr` / `.leader`、 + // `CheckTaskViews.listViews.pending`),问题在够不着:填报单有 8 个具名列表视图, + // 页签栏放不下就把后几个折进「还有 N 个」溢出菜单,「待领导审批」正好落在里面 —— + // 分管领导是唯一按手册找不到自己队列的角色(平台 + // objectstack-ai/objectstack#14883、#14952)。这里用 `viewName` 从侧边栏直指 + // 那三个视图,把「点组 → 点填报单 → 展开溢出菜单 → 点页签」压成一次点击。 + // + // 用 `viewName` 而不是 `filters`:两者互斥(spec `app.zod.ts` 的 + // `assertNavTargetExclusivity`,`filters` 落到 `/:objectName/data` 裸数据面), + // 而具名视图已经带好了列与过滤,重复一遍只会多一处会跟视图走散的真值。 + // + // 这三项**不加** `requiredPermissions`:它们指向的对象在所有岗位的权限集里都有读权, + // 看得到几行由数据范围决定(部门填报人员点进「待人力审核」是一张空表,不是报错)。 + // 加能力闸门等于把「配置类菜单按岗位裁剪」的口径套到待办上,反而挡掉本该用它的人。 + { id: 'nav_my_checks', type: 'object', objectName: 'kpi_check_task', viewName: 'pending', label: '待我核对', icon: 'clipboard-check' }, + { id: 'nav_hr_queue', type: 'object', objectName: 'kpi_entry_sheet', viewName: 'hr', label: '待人力审核', icon: 'user-check' }, + { id: 'nav_leader_queue', type: 'object', objectName: 'kpi_entry_sheet', viewName: 'leader', label: '待领导审批', icon: 'shield-check' }, + // ── 全量入口 ──────────────────────────────────────────────────── { id: 'nav_lines', type: 'object', objectName: 'kpi_entry_line', label: '填报明细', icon: 'table' }, { id: 'nav_checks', type: 'object', objectName: 'kpi_check_task', label: '分公司核对', icon: 'clipboard-check' }, { id: 'nav_bonus', type: 'object', objectName: 'kpi_bonus', label: '加减分', icon: 'plus-minus' }, diff --git a/src/pages/index.ts b/src/pages/index.ts index 0af53c0..dea9c02 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,10 +1,10 @@ import { definePage } from '@objectstack/spec/ui'; /** - * 工作台 —— 填报人员的待办入口(#34)。 + * 工作台 —— 六个岗位共用的待办入口(#34 建、#36 扩)。 * - * 设计目标是「一屏填数出分提交」:登录后直接落在本页,不再先跳「填报明细」再跳「填报单」。 - * 两个 `object-grid` 区块合起来构成一条不跳页的动线 —— + * 设计目标是「登录即处理」:落地页上直接排开当前该我动手的几摞事,不再「回列表 → 重选页签 → + * 打开 → 审」。五个 `object-grid` 区块合起来构成一条不跳页的动线 —— * * 1. 「待填报的指标」= `kpi_entry_line` 的可编辑网格(`editable: true`),首列是「所属填报单」, * 后面九列是填一行数当场要看的项;实际值与备注可改,其余列在字段定义上就是 `readonly`, @@ -12,10 +12,23 @@ import { definePage } from '@objectstack/spec/ui'; * 同一张表。 * 2. 「填报单」= `kpi_entry_sheet` 的只读网格,行操作挂 `kpi_sheet_submit`(「提交填报」), * 填完即可在同一页提交。 + * 3. 「待我核对」= `kpi_check_task` 中状态为「待核对」的任务,行操作挂「确认无误」「提出争议」。 + * 4. 「待人力审核」= `kpi_entry_sheet` 中状态为「人力审核中」的单,行操作挂「审核通过」「驳回」。 + * 5. 「待领导审批」= 同上,状态为「领导审批中」。 * - * **能看到哪些行**由数据层决定(对象 OWD `private` / `controlled_by_parent` + 方案发布时 - * 写入的动态共享规则 + 权限集 `readScope`)—— 视图筛选是展示范围,不是安全边界,这里不拿 - * `filter` 当权限用。 + * **区块不按岗位显隐**(#36 的实现口径)。两个原因,一个是能力、一个是设计: + * + * - 能力:页面组件的 `visibleWhen` 只绑 `record` / `current_user` / `page.`(spec + * `ui/page.zod.ts`),没有岗位绑定。实测写 `'kpi_dept_reporter' in current_user.positions` + * 后,**连部门填报人员本人都看不到网格**(谓词求值为空 = 对所有人隐藏),已回退。 + * - 设计:**能看到哪些行由数据层决定** —— 对象 OWD `private` / `controlled_by_parent` + + * 方案发布时写入的动态共享规则 + 权限集 `readScope`。部门填报人员在「待人力审核」里 + * 本来就一行都读不到,分公司核对人员在「待我核对」里只读得到本分公司的任务;按岗位再 + * 显隐一次是把同一条边界画两遍,而画在前端的那一遍不是边界。视图筛选是展示范围,不是 + * 安全边界,这里不拿 `filter` 当权限用。 + * + * 空区块**保留**、不隐藏:它给「这摞事现在是空的」一个确定的回答,比区块时有时无好读; + * 三个待办区块 `pageSize` 收到 10(填报动线的两块仍是 25),空态只占一行标题加一句空提示。 * * **屏幕上怎么分辨哪一行是哪一张单**是另一回事(评审 F3),三处按平台实际能到的程度处理: * @@ -26,19 +39,42 @@ import { definePage } from '@objectstack/spec/ui'; * 「filters on 'sheet.status', which follows the relationship 'sheet' into another object * — a filter reaches only columns of 'kpi_entry_line' itself … Denormalise the value onto * 'kpi_entry_line' (a stored field, written when the source changes) and filter that.」 - * 本对象上没有任何字段编码填报单状态,而反规范化要新增字段 + 写入路径,超出本工作项范围。 + * `kpi_entry_line` 上没有任何字段编码填报单状态(#36 复核过一遍字段表:sheet / + * plan_indicator / indicator / indicator_name / unit / direction / scoring_method / + * target_value / weight / actual_value / completion_rate / score_rate / score / + * adjusted_score / final_score / is_adjusted / adjust_type_applied / calc_trace / + * last_adjustment / remark —— 没有一个是填报单状态的副本),而反规范化要新增字段 + + * 写入路径,#36 明写「明细上若无该字段则如实记录,不加字段」。 * 退化的替代条件(如「实际值为空」)会让保存后的行当场从网格里消失,连带毁掉「同页出分」, * 比不过滤更糟,因此不做。已提交 / 已归档的行仍会出现在网格里,改动在保存时被 `sheet.hook` * 的冻结闸门拦下并给出三段式提示 —— 拦得住,只是晚一步。 - * - 「对 org 范围岗位隐藏本区块」**做不到**:页面组件的 `visibleWhen` 只绑 `record` / - * `current_user` / `page.`(spec `ui/page.zod.ts`),没有岗位绑定。实测写 - * `'kpi_dept_reporter' in current_user.positions` 后,**连部门填报人员本人都看不到网格** - * (谓词求值为空 = 对所有人隐藏),已回退。 + * - 三个待办区块**能**过滤,因为过滤的是本对象自己的 `status` 列(`kpi_check_task.status`、 + * `kpi_entry_sheet.status`),不跨关系。 + * + * 行动作用 `object-grid` 的 `rowActions`(spec `component.zod.ts` 记有读点, + * `ObjectGrid.tsx:1927`),挂的是 `actions/index.ts` 里既有的对象级动作 —— 它们本就带 + * `locations: ['record_header', 'list_item']` 与状态谓词,本页只是把它们摆到行上, + * 不新增动作、不改动作定义。驳回原因 / 争议内容的必填口径写在动作参数上,行级入口与 + * 记录页入口共用同一份定义,不会因为入口不同而松动。 + * + * ⚠️ 页面网格行上的「打开 ›」按钮点了没反应(平台 objectstack-ai/objectstack#16276), + * 本页的动线不依赖它:待办都在行操作菜单里就地办完。 * * 区块标题按需求 §1.4 术语表用「填报单」「指标」的表内名词,不用「我的」——`kpi_home` 是 * `isDefault: true` 的默认落地页,六个岗位共用;对管理员 / 人力审核这类 org 范围岗位, - * 网格里本来就不止「我的」行,标题不该替它们说话。 + * 网格里本来就不止「我的」行,标题不该替它们说话。「待我核对」是例外,也只是例外地跟着 + * 设计方案 §5.5 的原文走(该节把三个队列点名为「待我核对」「待人力审核」「待领导审批」)。 */ + +/** 只读待办网格的共同形状:不可编辑、按 10 行压缩、行操作就地办完。 */ +const TODO_GRID = { editable: false, pageSize: 10 } as const; + +/** 三个待办区块共用的填报单列:够认出是哪张单、够看出分,不铺满一屏。 */ +const SHEET_TODO_COLUMNS = ['name', 'subject', 'status', 'line_count', 'weight_total', 'indicator_score', 'bonus_total', 'total_score']; + +/** 待办区块的行操作:审核通过 / 驳回,两者都在 `actions/index.ts` 里定义。 */ +const SHEET_REVIEW_ACTIONS = ['kpi_sheet_approve', 'kpi_sheet_reject']; + export const HomePage = definePage({ name: 'kpi_home', label: '工作台', @@ -51,7 +87,7 @@ export const HomePage = definePage({ { name: 'main', width: 'full', components: [ - { type: 'element:text', properties: { content: '部门填报人员:在下方「待填报的指标」直接填实际值,保存即出分,再到「填报单」点「提交填报」;分公司核对人员:在「分公司核对」确认或提出争议;人力审核 / 分管领导:在「填报单」对应队列里审核通过或驳回(驳回必须填写原因);审批通过后在「结果与归档」查看四个维度的汇总并归档。' } }, + { type: 'element:text', properties: { content: '部门填报人员:在下方「待填报的指标」直接填实际值,保存即出分,再到「填报单」点「提交填报」;分公司核对人员:在「待我核对」确认无误或提出争议(争议内容必填);人力审核 / 分管领导:在「待人力审核」「待领导审批」就地审核通过或驳回(驳回原因必填);审批通过后在「结果与归档」查看四个维度的汇总并归档。当前没有待办的区块会显示为空,不影响其他区块。' } }, { type: 'element:text', properties: { content: '待填报的指标', variant: 'subheading' } }, { type: 'object-grid', @@ -76,6 +112,45 @@ export const HomePage = definePage({ pageSize: 25, }, }, + { type: 'element:divider', properties: {} }, + { type: 'element:text', properties: { content: '待我核对', variant: 'subheading' } }, + { + type: 'object-grid', + properties: { + ...TODO_GRID, + objectName: 'kpi_check_task', + columns: ['name', 'sheet', 'branch', 'status', 'comment'], + filter: [{ field: 'status', operator: 'equals', value: 'pending' }], + rowActions: ['kpi_check_confirm', 'kpi_check_dispute'], + sort: [{ field: 'created_at', order: 'asc' }], + }, + }, + { type: 'element:divider', properties: {} }, + { type: 'element:text', properties: { content: '待人力审核', variant: 'subheading' } }, + { + type: 'object-grid', + properties: { + ...TODO_GRID, + objectName: 'kpi_entry_sheet', + columns: SHEET_TODO_COLUMNS, + filter: [{ field: 'status', operator: 'equals', value: 'hr_reviewing' }], + rowActions: SHEET_REVIEW_ACTIONS, + sort: [{ field: 'submitted_at', order: 'asc' }], + }, + }, + { type: 'element:divider', properties: {} }, + { type: 'element:text', properties: { content: '待领导审批', variant: 'subheading' } }, + { + type: 'object-grid', + properties: { + ...TODO_GRID, + objectName: 'kpi_entry_sheet', + columns: SHEET_TODO_COLUMNS, + filter: [{ field: 'status', operator: 'equals', value: 'leader_approving' }], + rowActions: SHEET_REVIEW_ACTIONS, + sort: [{ field: 'submitted_at', order: 'asc' }], + }, + }, ], }, ], diff --git a/src/translations/zh-CN.objects.generated.ts b/src/translations/zh-CN.objects.generated.ts index 6ccb2d6..04fdfa9 100644 --- a/src/translations/zh-CN.objects.generated.ts +++ b/src/translations/zh-CN.objects.generated.ts @@ -1226,6 +1226,15 @@ export const zhCNTranslations: TranslationData = { nav_sheets: { label: "填报单" }, + nav_my_checks: { + label: "待我核对" + }, + nav_hr_queue: { + label: "待人力审核" + }, + nav_leader_queue: { + label: "待领导审批" + }, nav_lines: { label: "填报明细" },