feat(oa): add owner person/dept fields with initiator defaults
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
3b46b10604
commit
4ebc2dbe82
18 files changed
+791
-15
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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 失败,服务代码已落地)
|
||||
@@ -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] 范围不含套件、不多选
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- FormCreate:归属部门(默认当前登录人主部门;allowModify 控制可否改选) -->
|
||||
<template>
|
||||
<OaDeptSelect
|
||||
:model-value="modelValue"
|
||||
:disabled="effectiveDisabled"
|
||||
:multiple="false"
|
||||
:form-create-inject="formCreateInject"
|
||||
@update:model-value="onUpdate"
|
||||
@change="onChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import OaDeptSelect from '@/components/oa-approval/OaDeptSelect.vue'
|
||||
import type { OaDeptValueItem } from '@/utils/oa-dept-value'
|
||||
|
||||
defineOptions({ name: 'ownerDept' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: unknown
|
||||
disabled?: boolean
|
||||
/** 是否允许改选;默认 false(只读展示默认值) */
|
||||
allowModify?: boolean
|
||||
formCreateInject?: Record<string, unknown>
|
||||
}>(),
|
||||
{ allowModify: false }
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: OaDeptValueItem | OaDeptValueItem[] | null): void
|
||||
(e: 'change', v: OaDeptValueItem | OaDeptValueItem[] | null): void
|
||||
}>()
|
||||
|
||||
const effectiveDisabled = computed(() => !!props.disabled || !props.allowModify)
|
||||
|
||||
function onUpdate(v: OaDeptValueItem | OaDeptValueItem[] | null) {
|
||||
emit('update:modelValue', v)
|
||||
}
|
||||
|
||||
function onChange(v: OaDeptValueItem | OaDeptValueItem[] | null) {
|
||||
emit('change', v)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- FormCreate:归属人(默认当前登录人;allowModify 控制可否改选) -->
|
||||
<template>
|
||||
<OaPersonSelect
|
||||
:model-value="modelValue"
|
||||
:disabled="effectiveDisabled"
|
||||
:multiple="false"
|
||||
:form-create-inject="formCreateInject"
|
||||
@update:model-value="onUpdate"
|
||||
@change="onChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import OaPersonSelect from '@/components/oa-approval/OaPersonSelect.vue'
|
||||
import type { OaPersonValueItem } from '@/utils/oa-person-value'
|
||||
|
||||
defineOptions({ name: 'ownerPerson' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: unknown
|
||||
disabled?: boolean
|
||||
/** 是否允许改选;默认 false(只读展示默认值) */
|
||||
allowModify?: boolean
|
||||
formCreateInject?: Record<string, unknown>
|
||||
}>(),
|
||||
{ allowModify: false }
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: OaPersonValueItem | OaPersonValueItem[] | null): void
|
||||
(e: 'change', v: OaPersonValueItem | OaPersonValueItem[] | null): void
|
||||
}>()
|
||||
|
||||
const effectiveDisabled = computed(() => !!props.disabled || !props.allowModify)
|
||||
|
||||
function onUpdate(v: OaPersonValueItem | OaPersonValueItem[] | null) {
|
||||
emit('update:modelValue', v)
|
||||
}
|
||||
|
||||
function onChange(v: OaPersonValueItem | OaPersonValueItem[] | null) {
|
||||
emit('change', v)
|
||||
}
|
||||
</script>
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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: '包含' }]
|
||||
}
|
||||
// 单选/下拉为离散选项,仅等于/不等于;输入框常填数字参与区间比较,与数字/金额/日期同套运算符
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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: '提示信息'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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<string, any>[]) {
|
||||
for (const r of rules) {
|
||||
if (!r || typeof r !== 'object') continue
|
||||
const rule = r as Record<string, any>
|
||||
out.push(rule)
|
||||
const children = rule.children || rule.props?.rule || []
|
||||
if (Array.isArray(children)) walkRules(children, out)
|
||||
}
|
||||
}
|
||||
|
||||
function flattenRules(schemaOrRules: unknown): Record<string, any>[] {
|
||||
if (Array.isArray(schemaOrRules)) {
|
||||
const out: Record<string, any>[] = []
|
||||
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<string, any>[] = []
|
||||
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<string, unknown>
|
||||
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<OaDeptValueItem | null> {
|
||||
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<string, any>,
|
||||
user: OwnerUserSnapshot | null | undefined
|
||||
): Promise<Record<string, any>> {
|
||||
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)
|
||||
}
|
||||
@@ -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: '提示信息'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user