From 4ebc2dbe82e2da943e1c2fc8662e0cb95b6c153a Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Thu, 30 Jul 2026 17:54:48 +0800 Subject: [PATCH] feat(oa): add owner person/dept fields with initiator defaults Co-authored-by: Cursor --- backend/app/services/oa_dept_select.py | 14 +- backend/app/services/oa_owner_fields.py | 81 +++++++++ backend/app/services/oa_person_select.py | 15 +- backend/app/services/oa_related_approval.py | 2 +- backend/tests/test_oa_owner_fields.py | 106 ++++++++++++ .../plans/2026-07-30-oa-owner-fields.md | 50 ++++++ .../2026-07-30-oa-owner-fields-design.md | 112 ++++++++++++ .../src/assets/styles/core/oa-fc-runtime.scss | 11 ++ .../oa-approval/OaOwnerDeptSelect.vue | 45 +++++ .../oa-approval/OaOwnerPersonSelect.vue | 45 +++++ .../flow-tree/ConditionRuleEditor.vue | 13 +- .../oa-approval/oa-condition-fields.ts | 3 +- .../src/utils/oa-form-create-owner-dept.ts | 19 +++ .../src/utils/oa-form-create-owner-person.ts | 19 +++ .../src/utils/oa-owner-dept-select-rule.ts | 52 ++++++ .../src/utils/oa-owner-field-defaults.ts | 160 ++++++++++++++++++ .../src/utils/oa-owner-person-select-rule.ts | 52 ++++++ .../views/oa-approval/instances/detail.vue | 7 +- 18 files changed, 791 insertions(+), 15 deletions(-) create mode 100644 backend/app/services/oa_owner_fields.py create mode 100644 backend/tests/test_oa_owner_fields.py create mode 100644 docs/superpowers/plans/2026-07-30-oa-owner-fields.md create mode 100644 docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md create mode 100644 web-admin/src/components/oa-approval/OaOwnerDeptSelect.vue create mode 100644 web-admin/src/components/oa-approval/OaOwnerPersonSelect.vue create mode 100644 web-admin/src/utils/oa-form-create-owner-dept.ts create mode 100644 web-admin/src/utils/oa-form-create-owner-person.ts create mode 100644 web-admin/src/utils/oa-owner-dept-select-rule.ts create mode 100644 web-admin/src/utils/oa-owner-field-defaults.ts create mode 100644 web-admin/src/utils/oa-owner-person-select-rule.ts diff --git a/backend/app/services/oa_dept_select.py b/backend/app/services/oa_dept_select.py index 947f9c5e..ff302c2e 100644 --- a/backend/app/services/oa_dept_select.py +++ b/backend/app/services/oa_dept_select.py @@ -125,11 +125,17 @@ def _validate_dept_rule(rule: dict, data: Dict[str, Any]) -> List[str]: def validate_dept_form_values(schema: Any, form_data: Optional[Dict[str, Any]]) -> List[str]: - """校验 form_data 中部门选择控件的值格式,返回中文错误列表(空表示通过)。""" + """校验 form_data 中部门 / 归属部门控件的值格式,返回中文错误列表(空表示通过)。""" data = form_data or {} errors: List[str] = [] for rule in _schema_rules(schema): - if str(rule.get("type") or "") != "dept": - continue - errors.extend(_validate_dept_rule(rule, data)) + typ = str(rule.get("type") or "") + if typ == "dept": + errors.extend(_validate_dept_rule(rule, data)) + elif typ == "ownerDept": + forced = dict(rule) + props = dict(_rule_props(rule)) + props["multiple"] = False + forced["props"] = props + errors.extend(_validate_dept_rule(forced, data)) return errors diff --git a/backend/app/services/oa_owner_fields.py b/backend/app/services/oa_owner_fields.py new file mode 100644 index 00000000..0c2232d3 --- /dev/null +++ b/backend/app/services/oa_owner_fields.py @@ -0,0 +1,81 @@ +"""OA 归属人 / 归属部门:只读强制回填与校验辅助""" +from __future__ import annotations + +from typing import Any, Dict, List, Optional + + +def _walk_rules(rules: Any, out: List[dict]) -> None: + if not isinstance(rules, list): + return + for r in rules: + if not isinstance(r, dict): + continue + out.append(r) + children = r.get("children") or (r.get("props") or {}).get("rule") or [] + if isinstance(children, list): + _walk_rules(children, out) + + +def _schema_rules(schema: Any) -> List[dict]: + if isinstance(schema, list): + out: List[dict] = [] + _walk_rules(schema, out) + return out + if not isinstance(schema, dict): + return [] + out: List[dict] = [] + _walk_rules(schema.get("rule") or schema.get("rules") or [], out) + return out + + +def _rule_props(rule: dict) -> dict: + props = rule.get("props") + return props if isinstance(props, dict) else {} + + +def _allow_modify(rule: dict) -> bool: + return _rule_props(rule).get("allowModify") is True + + +def apply_owner_field_defaults( + schema: Any, + form_data: Optional[Dict[str, Any]], + *, + user_id: str, + user_name: str, + dept_id: Optional[str], + dept_name: Optional[str], +) -> Dict[str, Any]: + """ + 归属字段处理: + - allowModify 为假:强制写入当前发起人 / 主部门(防篡改) + - allowModify 为真且字段为空:补默认值 + """ + data = dict(form_data or {}) + uid = (user_id or "").strip() + uname = (user_name or "").strip() or uid + did = (dept_id or "").strip() + dname = (dept_name or "").strip() or did + + person_val = {"id": uid, "name": uname} if uid else None + dept_val = {"id": did, "name": dname} if did else None + + for rule in _schema_rules(schema): + typ = str(rule.get("type") or "") + field = str(rule.get("field") or "").strip() + if not field: + continue + allow = _allow_modify(rule) + cur = data.get(field) + empty = cur is None or cur == "" or cur == [] or ( + isinstance(cur, dict) and not str(cur.get("id") or "").strip() + ) + + if typ == "ownerPerson" and person_val: + if not allow or empty: + data[field] = dict(person_val) + elif typ == "ownerDept" and dept_val: + if not allow or empty: + data[field] = dict(dept_val) + + return data diff --git a/backend/app/services/oa_person_select.py b/backend/app/services/oa_person_select.py index d143c7c2..47e300e7 100644 --- a/backend/app/services/oa_person_select.py +++ b/backend/app/services/oa_person_select.py @@ -125,13 +125,20 @@ def _validate_person_rule(rule: dict, data: Dict[str, Any]) -> List[str]: def validate_person_form_values(schema: Any, form_data: Optional[Dict[str, Any]]) -> List[str]: - """校验 form_data 中人员选择控件的值格式,返回中文错误列表(空表示通过)。""" + """校验 form_data 中人员 / 归属人控件的值格式,返回中文错误列表(空表示通过)。""" data = form_data or {} errors: List[str] = [] for rule in _schema_rules(schema): - if str(rule.get("type") or "") != "person": - continue - errors.extend(_validate_person_rule(rule, data)) + typ = str(rule.get("type") or "") + if typ == "person": + errors.extend(_validate_person_rule(rule, data)) + elif typ == "ownerPerson": + # 归属人固定单选 + forced = dict(rule) + props = dict(_rule_props(rule)) + props["multiple"] = False + forced["props"] = props + errors.extend(_validate_person_rule(forced, data)) return errors diff --git a/backend/app/services/oa_related_approval.py b/backend/app/services/oa_related_approval.py index 26361445..a522c3ed 100644 --- a/backend/app/services/oa_related_approval.py +++ b/backend/app/services/oa_related_approval.py @@ -104,7 +104,7 @@ def _format_value(typ: str, raw: Any) -> Tuple[str, bool]: else: n = 0 return f"已关联 {n} 条", False - if (typ == "dept" or typ == "person" or typ == "post"): + if (typ == "dept" or typ == "person" or typ == "post" or typ == "ownerDept" or typ == "ownerPerson"): if isinstance(raw, list): names = [] for x in raw: diff --git a/backend/tests/test_oa_owner_fields.py b/backend/tests/test_oa_owner_fields.py new file mode 100644 index 00000000..9a320fad --- /dev/null +++ b/backend/tests/test_oa_owner_fields.py @@ -0,0 +1,106 @@ +import unittest + +from app.services.oa_dept_select import validate_dept_form_values +from app.services.oa_owner_fields import apply_owner_field_defaults +from app.services.oa_person_select import validate_person_form_values + + +class TestOaOwnerFields(unittest.TestCase): + def test_force_overwrite_when_not_allow_modify(self): + schema = { + "rule": [ + { + "type": "ownerPerson", + "field": "op1", + "title": "归属人", + "props": {"allowModify": False}, + }, + { + "type": "ownerDept", + "field": "od1", + "title": "归属部门", + "props": {"allowModify": False}, + }, + ] + } + data = apply_owner_field_defaults( + schema, + { + "op1": {"id": "hack", "name": "黑客"}, + "od1": {"id": "d-hack", "name": "假部门"}, + }, + user_id="u1", + user_name="张三", + dept_id="d1", + dept_name="研发部", + ) + self.assertEqual(data["op1"], {"id": "u1", "name": "张三"}) + self.assertEqual(data["od1"], {"id": "d1", "name": "研发部"}) + + def test_fill_empty_when_allow_modify(self): + schema = { + "rule": [ + { + "type": "ownerPerson", + "field": "op1", + "props": {"allowModify": True}, + } + ] + } + data = apply_owner_field_defaults( + schema, + {}, + user_id="u1", + user_name="张三", + dept_id="d1", + dept_name="研发部", + ) + self.assertEqual(data["op1"], {"id": "u1", "name": "张三"}) + + def test_keep_user_value_when_allow_modify(self): + schema = { + "rule": [ + { + "type": "ownerPerson", + "field": "op1", + "props": {"allowModify": True}, + } + ] + } + data = apply_owner_field_defaults( + schema, + {"op1": {"id": "u2", "name": "李四"}}, + user_id="u1", + user_name="张三", + dept_id="d1", + dept_name="研发部", + ) + self.assertEqual(data["op1"], {"id": "u2", "name": "李四"}) + + def test_validate_owner_person_single(self): + schema = { + "rule": [{"type": "ownerPerson", "field": "op1", "title": "归属人"}] + } + self.assertEqual( + validate_person_form_values( + schema, {"op1": {"id": "u1", "name": "张三"}} + ), + [], + ) + errs = validate_person_form_values( + schema, {"op1": [{"id": "u1", "name": "张三"}]} + ) + self.assertTrue(any("单个" in e for e in errs)) + + def test_validate_owner_dept_single(self): + schema = { + "rule": [{"type": "ownerDept", "field": "od1", "title": "归属部门"}] + } + self.assertEqual( + validate_dept_form_values(schema, {"od1": {"id": "d1", "name": "研发"}}), + [], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/superpowers/plans/2026-07-30-oa-owner-fields.md b/docs/superpowers/plans/2026-07-30-oa-owner-fields.md new file mode 100644 index 00000000..d8f5e0ad --- /dev/null +++ b/docs/superpowers/plans/2026-07-30-oa-owner-fields.md @@ -0,0 +1,50 @@ +# 归属人 / 归属部门 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 表单设计器新增「归属人」「归属部门」,发起时默认当前登录人/主部门,属性「允许修改」默认关闭。 + +**Architecture:** 新控件类型 `ownerPerson` / `ownerDept` 包装现有人员/部门选择器;发起页注入默认值;后端发布白名单 + 只读强制回填。 + +**Tech Stack:** Vue 3、FormCreate Designer、FastAPI + +**Spec:** `docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md` + +--- + +### Task 1: 前端控件与注册 + +**Files:** +- Create: `web-admin/src/utils/oa-owner-person-select-rule.ts` +- Create: `web-admin/src/utils/oa-owner-dept-select-rule.ts` +- Create: `web-admin/src/utils/oa-form-create-owner-person.ts` +- Create: `web-admin/src/utils/oa-form-create-owner-dept.ts` +- Create: `web-admin/src/components/oa-approval/OaOwnerPersonSelect.vue` +- Create: `web-admin/src/components/oa-approval/OaOwnerDeptSelect.vue` +- Create: `web-admin/src/utils/oa-owner-field-defaults.ts` +- Modify: `web-admin/src/main.ts`, `web-admin/src/oa-fc-host/main.ts` +- Modify: `web-admin/src/views/oa-approval/start/index.vue` +- Modify: print / condition / related display as needed + +- [x] 拖拽规则:label 归属人/归属部门;props.allowModify 默认 false;单选 +- [x] 组件:`disabled || !allowModify`;包装 OaPersonSelect / OaDeptSelect +- [x] `applyOwnerFieldDefaults` + 发起页调用 +- [x] 注册 setup;打印识别新 type;条件候选含 ownerPerson + +### Task 2: 后端白名单与强制回填 + +**Files:** +- Modify: `backend/app/services/oa_publish.py` +- Modify: `backend/app/services/oa_person_select.py` / `oa_dept_select.py` 或新增 apply +- Modify: `backend/app/services/oa_engine.py` start_instance +- Modify: `backend/app/services/oa_related_approval.py` +- Test: `backend/tests/test_oa_owner_fields.py` + +- [x] FILLABLE + CONDITION(ownerPerson) +- [x] 校验 owner* 同单选 person/dept +- [x] allowModify 为假时强制当前用户/部门 +- [x] 单测覆盖 + +### Task 3: 图谱更新 + +- [x] `./scripts/update-graph.sh frontend`(backend 因节点过多 HTML 失败,服务代码已落地) diff --git a/docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md b/docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md new file mode 100644 index 00000000..bf7d85af --- /dev/null +++ b/docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md @@ -0,0 +1,112 @@ +# OA 表单设计 · 归属人 / 归属部门 设计文档 + +**日期**: 2026-07-30 +**状态**: 已实现(计划见 `docs/superpowers/plans/2026-07-30-oa-owner-fields.md`) +**范围**: `web-admin` 审批设计器「2 表单设计」组件 Tab;发起页默认值;`backend` 发布白名单与提交时只读强制回填 +**界面模式**: 对齐现有「人员」「部门」控件;视觉 Fresh Mint + +--- + +## 1. 背景与目标 + +表单设计「组件」Tab 已有 **人员**、**部门**。业务需要两个带默认值的同类控件: + +- **归属人**:默认当前登录人 +- **归属部门**:默认当前登录人主部门 + +### 1.1 目标 + +- 左侧「组件」新增「归属人」「归属部门」,与人员/部门并列。 +- 发起时自动填入当前登录人 / 主部门。 +- 设计器属性「允许修改」:默认 **关**(只读);开则可改选。 +- 值结构与人员/部门一致,便于打印、详情、条件(归属人)复用。 + +### 1.2 非目标 + +- 不改现有「人员」「部门」行为。 +- 不放入「套件」Tab。 +- 不支持多选。 + +--- + +## 2. 产品决策(已确认) + +| 项 | 决策 | +|----|------| +| 控件类型 | `ownerPerson` / `ownerDept` | +| 默认值时机 | 发起审批表单加载时写入;设计器画布不依赖设计者登录身份作为业务默认 | +| 默认来源 | 归属人 = 当前用户 id + 姓名;归属部门 = 主部门 id + 名称(全路径优先) | +| 允许修改 | 属性开关 `allowModify`,**默认 false** | +| 选型 | 固定单选 | +| 值格式 | `{ id, name }`(与 person / dept 一致) | +| 只读防篡改 | `allowModify === false` 时,后端发起提交强制回填为当前用户 / 主部门 | + +--- + +## 3. Schema + +```json +{ + "type": "ownerPerson", + "field": "Fxxxx", + "title": "归属人", + "props": { + "allowModify": false + } +} +``` + +```json +{ + "type": "ownerDept", + "field": "Fyyyy", + "title": "归属部门", + "props": { + "allowModify": false + } +} +``` + +--- + +## 4. 运行时 + +### 4.1 前端(发起页) + +- 加载 schema 后:对空字段写入默认值。 +- `allowModify === false`:控件 `disabled`(只读展示)。 +- `allowModify === true`:复用人员/部门选择器,可改选。 +- 详情页:展示已存 `formData`,不再按当前查看者覆盖。 + +### 4.2 后端(发起) + +- 发布白名单纳入 `ownerPerson` / `ownerDept`。 +- 校验值格式同单选 person / dept。 +- `allowModify` 为假时:提交前强制写入发起人 id/姓名、主部门 id/名称。 +- 条件分支:`ownerPerson` 与 `person` 同属人员类(`contains`);`ownerDept` 暂不进条件候选(与现有 `dept` 一致)。 + +### 4.3 打印 / 关联展示 + +- 打印与关联审批摘要:按 person / dept 同路径格式化展示。 + +--- + +## 5. 登录用户字段约定 + +web-admin `userInfo`(见 `backend/app/api/public.py` `_build_user_info_payload`): + +| 用途 | 字段 | +|------|------| +| 用户 id | `id`(兼容 `userId`) | +| 姓名 | `realName`(兼容 username) | +| 部门 id | `regionId`(即 `sys_user.dept_id`) | +| 部门名称 | 登录态常无 `regionName`;前端用部门树解析全路径,后端用库表 `dept_name` | + +--- + +## 6. 自检 + +- [x] 无未决占位符 +- [x] 与「人员/部门」值格式一致 +- [x] 默认不允许修改 + 后端强制回填 +- [x] 范围不含套件、不多选 diff --git a/web-admin/src/assets/styles/core/oa-fc-runtime.scss b/web-admin/src/assets/styles/core/oa-fc-runtime.scss index 0d827c3e..7191905f 100644 --- a/web-admin/src/assets/styles/core/oa-fc-runtime.scss +++ b/web-admin/src/assets/styles/core/oa-fc-runtime.scss @@ -76,4 +76,15 @@ ._fc-exceed .el-upload { display: none; } + + /* 请假/加班套件时长:左对齐(EP 默认 center) */ + .oa-leave-kit .el-input-number .el-input__inner, + .oa-leave-duration.el-input-number .el-input__inner, + .oa-leave-duration .el-input__inner, + .oa-overtime-kit .el-input-number .el-input__inner, + .oa-overtime-duration.el-input-number .el-input__inner, + .oa-overtime-duration .el-input__inner, + .el-input-number.is-left .el-input__inner { + text-align: left !important; + } } diff --git a/web-admin/src/components/oa-approval/OaOwnerDeptSelect.vue b/web-admin/src/components/oa-approval/OaOwnerDeptSelect.vue new file mode 100644 index 00000000..58863c15 --- /dev/null +++ b/web-admin/src/components/oa-approval/OaOwnerDeptSelect.vue @@ -0,0 +1,45 @@ + + + + diff --git a/web-admin/src/components/oa-approval/OaOwnerPersonSelect.vue b/web-admin/src/components/oa-approval/OaOwnerPersonSelect.vue new file mode 100644 index 00000000..529d9b74 --- /dev/null +++ b/web-admin/src/components/oa-approval/OaOwnerPersonSelect.vue @@ -0,0 +1,45 @@ + + + + diff --git a/web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue b/web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue index 028ef886..f6de90cd 100644 --- a/web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue +++ b/web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue @@ -172,7 +172,7 @@ function isPersonLikeField(field: string) { const t = meta(field)?.type - return t === 'person' || t === 'initiator' + return t === 'person' || t === 'ownerPerson' || t === 'initiator' } function isPostField(field: string) { @@ -186,7 +186,9 @@ } function defaultValueForType(type: string, opts?: ConditionCandidate['options']) { - if (type === 'person' || type === 'post' || type === 'initiator') return [] as string[] + if (type === 'person' || type === 'ownerPerson' || type === 'post' || type === 'initiator') { + return [] as string[] + } return opts?.[0]?.value ?? '' } @@ -335,7 +337,7 @@ } onMounted(() => { - if (props.candidates.some((c) => c.type === 'person' || c.type === 'initiator')) { + if (props.candidates.some((c) => c.type === 'person' || c.type === 'ownerPerson' || c.type === 'initiator')) { loadUsers() } if (props.candidates.some((c) => c.type === 'post')) { @@ -344,7 +346,10 @@ }) watch( - () => props.candidates.some((c) => c.type === 'person' || c.type === 'initiator'), + () => + props.candidates.some( + (c) => c.type === 'person' || c.type === 'ownerPerson' || c.type === 'initiator' + ), (need) => { if (need && !userOptions.value.length) loadUsers() } diff --git a/web-admin/src/components/oa-approval/oa-condition-fields.ts b/web-admin/src/components/oa-approval/oa-condition-fields.ts index 7c13cef9..99a4588f 100644 --- a/web-admin/src/components/oa-approval/oa-condition-fields.ts +++ b/web-admin/src/components/oa-approval/oa-condition-fields.ts @@ -5,6 +5,7 @@ export const CONDITION_ELIGIBLE_TYPES = new Set([ 'number', 'money', 'person', + 'ownerPerson', 'post', 'radio', 'select', @@ -32,7 +33,7 @@ export interface ConditionCandidate { } export function opsForFieldType(type: string): Array<{ value: ConditionOp; label: string }> { - if (type === 'person' || type === 'post' || type === 'initiator') { + if (type === 'person' || type === 'ownerPerson' || type === 'post' || type === 'initiator') { return [{ value: 'contains', label: '包含' }] } // 单选/下拉为离散选项,仅等于/不等于;输入框常填数字参与区间比较,与数字/金额/日期同套运算符 diff --git a/web-admin/src/utils/oa-form-create-owner-dept.ts b/web-admin/src/utils/oa-form-create-owner-dept.ts new file mode 100644 index 00000000..c9609524 --- /dev/null +++ b/web-admin/src/utils/oa-form-create-owner-dept.ts @@ -0,0 +1,19 @@ +import FcDesigner from '@form-create/designer' +import OaOwnerDeptSelect from '@/components/oa-approval/OaOwnerDeptSelect.vue' +import { ownerDeptSelectDragRule } from '@/utils/oa-owner-dept-select-rule' + +const FLAG = '__oaOwnerDeptSelectConfigured' + +export function setupOaOwnerDeptSelect(formCreate?: { + component?: (name: string, c: unknown) => void +}) { + if ((FcDesigner as any)[FLAG]) return + FcDesigner.component('ownerDept', OaOwnerDeptSelect) + if (typeof formCreate?.component === 'function') { + formCreate.component('ownerDept', OaOwnerDeptSelect) + } + if (typeof FcDesigner.addDragRule === 'function') { + FcDesigner.addDragRule(ownerDeptSelectDragRule) + } + ;(FcDesigner as any)[FLAG] = true +} diff --git a/web-admin/src/utils/oa-form-create-owner-person.ts b/web-admin/src/utils/oa-form-create-owner-person.ts new file mode 100644 index 00000000..75ae6abe --- /dev/null +++ b/web-admin/src/utils/oa-form-create-owner-person.ts @@ -0,0 +1,19 @@ +import FcDesigner from '@form-create/designer' +import OaOwnerPersonSelect from '@/components/oa-approval/OaOwnerPersonSelect.vue' +import { ownerPersonSelectDragRule } from '@/utils/oa-owner-person-select-rule' + +const FLAG = '__oaOwnerPersonSelectConfigured' + +export function setupOaOwnerPersonSelect(formCreate?: { + component?: (name: string, c: unknown) => void +}) { + if ((FcDesigner as any)[FLAG]) return + FcDesigner.component('ownerPerson', OaOwnerPersonSelect) + if (typeof formCreate?.component === 'function') { + formCreate.component('ownerPerson', OaOwnerPersonSelect) + } + if (typeof FcDesigner.addDragRule === 'function') { + FcDesigner.addDragRule(ownerPersonSelectDragRule) + } + ;(FcDesigner as any)[FLAG] = true +} diff --git a/web-admin/src/utils/oa-owner-dept-select-rule.ts b/web-admin/src/utils/oa-owner-dept-select-rule.ts new file mode 100644 index 00000000..cf0153f4 --- /dev/null +++ b/web-admin/src/utils/oa-owner-dept-select-rule.ts @@ -0,0 +1,52 @@ +/** 与 FormCreate 官方 unique 同类:生成不重复 field */ +let _uidSeq = 0 +function uniqueId() { + const num = 370 + ++_uidSeq + return ( + 'F' + + Math.random().toString(36).slice(3, 6) + + Number(`${Date.now()}`).toString(36) + + num.toString(36) + + 'od' + ) +} + +/** FormCreate Designer DragRule:归属部门(默认当前登录人主部门) */ +export const ownerDeptSelectDragRule = { + menu: 'main' as const, + icon: 'icon-tree', + label: '归属部门', + name: 'ownerDept', + input: true, + mask: true, + validate: ['string', 'array', 'object'], + event: ['change'], + rule() { + return { + type: 'ownerDept', + field: uniqueId(), + title: '归属部门', + info: '', + $required: false, + props: { + allowModify: false + } + } + }, + props() { + return [ + { + type: 'switch', + field: 'allowModify', + title: '允许修改', + value: false, + props: { activeText: '是', inactiveText: '否' } + }, + { + type: 'input', + field: 'formCreateInfo', + title: '提示信息' + } + ] + } +} diff --git a/web-admin/src/utils/oa-owner-field-defaults.ts b/web-admin/src/utils/oa-owner-field-defaults.ts new file mode 100644 index 00000000..3d51d209 --- /dev/null +++ b/web-admin/src/utils/oa-owner-field-defaults.ts @@ -0,0 +1,160 @@ +import { fetchDeptTree } from '@/api/admin' +import { isDeptValueItem, type OaDeptValueItem } from '@/utils/oa-dept-value' +import { isPersonValueItem, type OaPersonValueItem } from '@/utils/oa-person-value' +import { buildPathLabelMapFromTreeRoots } from '@/utils/tree-path' + +export type OwnerUserSnapshot = { + id?: string + userId?: string + realName?: string + userName?: string + username?: string + regionId?: string + deptId?: string + regionName?: string + deptName?: string +} + +function walkRules(rules: unknown[], out: Record[]) { + for (const r of rules) { + if (!r || typeof r !== 'object') continue + const rule = r as Record + out.push(rule) + const children = rule.children || rule.props?.rule || [] + if (Array.isArray(children)) walkRules(children, out) + } +} + +function flattenRules(schemaOrRules: unknown): Record[] { + if (Array.isArray(schemaOrRules)) { + const out: Record[] = [] + walkRules(schemaOrRules, out) + return out + } + if (schemaOrRules && typeof schemaOrRules === 'object') { + const root = schemaOrRules as any + const rules = root.rule || root.rules || [] + if (Array.isArray(rules)) { + const out: Record[] = [] + walkRules(rules, out) + return out + } + } + return [] +} + +function isEmptyOwnerValue(raw: unknown): boolean { + if (raw == null || raw === '') return true + if (Array.isArray(raw)) return raw.length === 0 + if (typeof raw === 'object') { + const o = raw as Record + return !String(o.id || '').trim() + } + return false +} + +export function buildCurrentOwnerPerson(user: OwnerUserSnapshot | null | undefined): OaPersonValueItem | null { + if (!user) return null + const id = String(user.id || user.userId || '').trim() + if (!id) return null + const name = String(user.realName || user.userName || user.username || '').trim() || id + return { id, name } +} + +export function buildCurrentOwnerDeptSync( + user: OwnerUserSnapshot | null | undefined +): OaDeptValueItem | null { + if (!user) return null + const id = String(user.regionId || user.deptId || '').trim() + if (!id) return null + const name = String(user.regionName || user.deptName || '').trim() || id + return { id, name } +} + +/** 用部门树解析全路径名称;失败则回退 id / 登录态名称 */ +export async function resolveCurrentOwnerDept( + user: OwnerUserSnapshot | null | undefined +): Promise { + const base = buildCurrentOwnerDeptSync(user) + if (!base) return null + try { + const tree = (await fetchDeptTree()) || [] + const map = buildPathLabelMapFromTreeRoots(tree as any, { nameKey: 'deptName' }) + const path = map.get(base.id) + if (path) return { id: base.id, name: path } + } catch { + /* 无部门树权限时仍用 id/名称回退 */ + } + return base +} + +/** + * 发起页:写入当前登录人 / 主部门。 + * - allowModify 为假:始终覆盖为当前用户(防展示旧值 / 复制发起) + * - allowModify 为真:仅空字段补默认 + */ +export async function applyOwnerFieldDefaults( + schemaOrRules: unknown, + formData: Record, + user: OwnerUserSnapshot | null | undefined +): Promise> { + const rules = flattenRules(schemaOrRules) + const hasPerson = rules.some((r) => String(r.type || '') === 'ownerPerson') + const hasDept = rules.some((r) => String(r.type || '') === 'ownerDept') + if (!hasPerson && !hasDept) return { ...formData } + + const person = hasPerson ? buildCurrentOwnerPerson(user) : null + const dept = hasDept ? await resolveCurrentOwnerDept(user) : null + const next = { ...formData } + + for (const rule of rules) { + const type = String(rule.type || '') + const field = String(rule.field || rule.name || '').trim() + if (!field) continue + const allowModify = rule.props?.allowModify === true + if (type === 'ownerPerson' && person) { + if (!allowModify || isEmptyOwnerValue(next[field])) { + next[field] = person + } + } + if (type === 'ownerDept' && dept) { + if (!allowModify || isEmptyOwnerValue(next[field])) { + next[field] = dept + } + } + } + return next +} + +/** 确保 props.allowModify 有明确布尔(缺省 false) */ +export function ensureOwnerFieldRuleProps(rules: unknown): any[] { + if (!Array.isArray(rules)) return [] + return rules.map((raw) => { + if (!raw || typeof raw !== 'object') return raw + const rule = { ...(raw as any) } + const type = String(rule.type || '') + if (type === 'ownerPerson' || type === 'ownerDept') { + const props = { ...(rule.props || {}) } + if (props.allowModify !== true) props.allowModify = false + rule.props = props + } + if (Array.isArray(rule.children)) { + rule.children = ensureOwnerFieldRuleProps(rule.children) + } + if (rule.props && Array.isArray(rule.props.rule)) { + rule.props = { + ...rule.props, + rule: ensureOwnerFieldRuleProps(rule.props.rule) + } + } + return rule + }) +} + +export function isOwnerPersonValue(v: unknown): v is OaPersonValueItem { + return isPersonValueItem(v) +} + +export function isOwnerDeptValue(v: unknown): v is OaDeptValueItem { + return isDeptValueItem(v) +} diff --git a/web-admin/src/utils/oa-owner-person-select-rule.ts b/web-admin/src/utils/oa-owner-person-select-rule.ts new file mode 100644 index 00000000..8e50466d --- /dev/null +++ b/web-admin/src/utils/oa-owner-person-select-rule.ts @@ -0,0 +1,52 @@ +/** 与 FormCreate 官方 unique 同类:生成不重复 field */ +let _uidSeq = 0 +function uniqueId() { + const num = 370 + ++_uidSeq + return ( + 'F' + + Math.random().toString(36).slice(3, 6) + + Number(`${Date.now()}`).toString(36) + + num.toString(36) + + 'op' + ) +} + +/** FormCreate Designer DragRule:归属人(默认当前登录人) */ +export const ownerPersonSelectDragRule = { + menu: 'main' as const, + icon: 'icon-input', + label: '归属人', + name: 'ownerPerson', + input: true, + mask: true, + validate: ['string', 'array', 'object'], + event: ['change'], + rule() { + return { + type: 'ownerPerson', + field: uniqueId(), + title: '归属人', + info: '', + $required: false, + props: { + allowModify: false + } + } + }, + props() { + return [ + { + type: 'switch', + field: 'allowModify', + title: '允许修改', + value: false, + props: { activeText: '是', inactiveText: '否' } + }, + { + type: 'input', + field: 'formCreateInfo', + title: '提示信息' + } + ] + } +} diff --git a/web-admin/src/views/oa-approval/instances/detail.vue b/web-admin/src/views/oa-approval/instances/detail.vue index 3447af97..1bf58b40 100644 --- a/web-admin/src/views/oa-approval/instances/detail.vue +++ b/web-admin/src/views/oa-approval/instances/detail.vue @@ -211,6 +211,7 @@ ensureOaUploadRuleProps } from '@/utils/oa-form-create-upload' import { ensureLeaveKitRuleProps } from '@/utils/oa-leave-kit-runtime' + import { ensureOvertimeKitRuleProps } from '@/utils/oa-overtime-kit-runtime' import { ensureConfirmationKitVisibility } from '@/utils/oa-confirmation-kit-runtime' import { ensureOwnerFieldRuleProps } from '@/utils/oa-owner-field-defaults' @@ -344,7 +345,11 @@ viewFormData.value = { ...(data || {}) } const rule = Array.isArray(schema?.rule) ? schema.rule : Array.isArray(schema) ? schema : [] viewRule.value = ensureConfirmationKitVisibility( - ensureOwnerFieldRuleProps(ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rule))) + ensureOwnerFieldRuleProps( + ensureOvertimeKitRuleProps( + ensureLeaveKitRuleProps(ensureOaUploadRuleProps(rule)) + ) + ) ) const savedGutter = Number(schema?.option?.row?.gutter) viewOption.value = {