diff --git a/backend/app/services/oa_related_approval.py b/backend/app/services/oa_related_approval.py index 6c3e3b18..7fb1fd0a 100644 --- a/backend/app/services/oa_related_approval.py +++ b/backend/app/services/oa_related_approval.py @@ -104,6 +104,18 @@ def _format_value(typ: str, raw: Any) -> Tuple[str, bool]: else: n = 0 return f"已关联 {n} 条", False + if typ == "dept": + if isinstance(raw, list): + names = [] + for x in raw: + if isinstance(x, dict): + n = str(x.get("name") or "").strip() + if n: + names.append(n) + return "、".join(names) if names else "", False + if isinstance(raw, dict): + return str(raw.get("name") or "").strip(), False + return "", False if isinstance(raw, list): text = "、".join(str(x) for x in raw if x is not None and str(x) != "") elif isinstance(raw, dict): diff --git a/backend/tests/test_oa_related_approval.py b/backend/tests/test_oa_related_approval.py index 91d78873..12dbd410 100644 --- a/backend/tests/test_oa_related_approval.py +++ b/backend/tests/test_oa_related_approval.py @@ -37,6 +37,30 @@ class TestFlatten(unittest.TestCase): self.assertEqual(fields[2].get("relatedIds"), ["id1", "id2"]) self.assertTrue(fields[3].get("fullWidth")) + def test_dept_formats_path_names(self): + schema = { + "rule": [ + {"type": "dept", "field": "dept", "title": "部门"}, + { + "type": "dept", + "field": "depts", + "title": "多部门", + "props": {"multiple": True}, + }, + ] + } + data = { + "dept": {"id": "d1", "name": "中科兴皖 / 研发"}, + "depts": [ + {"id": "d1", "name": "中科兴皖 / 研发"}, + {"id": "d2", "name": "中科兴皖 / 质量"}, + ], + } + fields = flatten_form_fields_for_summary(schema, data) + by_label = {f["label"]: f["value"] for f in fields} + self.assertEqual(by_label["部门"], "中科兴皖 / 研发") + self.assertEqual(by_label["多部门"], "中科兴皖 / 研发、中科兴皖 / 质量") + def test_related_approval_field_includes_related_ids(self): schema = { "rule": [ diff --git a/web-admin/src/components/oa-approval/OaDeptSelect.vue b/web-admin/src/components/oa-approval/OaDeptSelect.vue new file mode 100644 index 00000000..0f9681dc --- /dev/null +++ b/web-admin/src/components/oa-approval/OaDeptSelect.vue @@ -0,0 +1,306 @@ + + + + + + diff --git a/web-admin/src/main.ts b/web-admin/src/main.ts index a7c66c09..547f4bae 100644 --- a/web-admin/src/main.ts +++ b/web-admin/src/main.ts @@ -17,6 +17,7 @@ import FcDesigner from '@form-create/designer' import formCreateAutoImport from '@form-create/element-ui/auto-import' import { setupFormCreateUpload } from '@/utils/oa-form-create-upload' import { setupOaRelatedApproval } from '@/utils/oa-form-create-related-approval' +import { setupOaDeptSelect } from '@/utils/oa-form-create-dept' // FormCreate 按字符串解析 el-input / fc-radio 等,主应用是按需自动导入,不会全局注册; // 必须挂上官方 auto-import,否则发起审批/详情里动态表单控件会残缺(如单选项只剩文字)。 @@ -24,6 +25,7 @@ FcDesigner.formCreate.use(formCreateAutoImport) // 内置「上传」默认 action=/,改为走附件上传 API,避免 POST 前端原点 404 setupFormCreateUpload(FcDesigner.formCreate) setupOaRelatedApproval(FcDesigner.formCreate) +setupOaDeptSelect(FcDesigner.formCreate) function maskSecret(value?: string) { if (!value) return '(empty)' diff --git a/web-admin/src/oa-fc-host/main.ts b/web-admin/src/oa-fc-host/main.ts index 0e1e0b5c..13c60517 100644 --- a/web-admin/src/oa-fc-host/main.ts +++ b/web-admin/src/oa-fc-host/main.ts @@ -9,6 +9,7 @@ import 'element-plus/dist/index.css' import FcDesigner from '@form-create/designer' import { setupFormCreateUpload } from '@/utils/oa-form-create-upload' import { setupOaRelatedApproval } from '@/utils/oa-form-create-related-approval' +import { setupOaDeptSelect } from '@/utils/oa-form-create-dept' import App from './App.vue' const app = createApp(App) @@ -22,4 +23,5 @@ app.use(FcDesigner) app.use(FcDesigner.formCreate) setupFormCreateUpload(FcDesigner.formCreate) setupOaRelatedApproval(FcDesigner.formCreate) +setupOaDeptSelect(FcDesigner.formCreate) app.mount('#app') diff --git a/web-admin/src/utils/oa-approval-print.ts b/web-admin/src/utils/oa-approval-print.ts index e23b46e0..a8ce8acf 100644 --- a/web-admin/src/utils/oa-approval-print.ts +++ b/web-admin/src/utils/oa-approval-print.ts @@ -12,6 +12,7 @@ import { type RelatedPrintBlock } from '@/utils/oa-approval-print-related' import { formatProgressPersonLines } from '@/utils/oa-progress-person-lines' +import { formatDeptDisplay } from '@/utils/oa-dept-value' export const OA_PRINT_COMPANY_NAME = '安徽中科兴皖医学研究有限公司' @@ -237,6 +238,12 @@ async function walkRules( continue } + if (type === 'dept') { + const text = formatDeptDisplay(value) + out.push({ label, text: text || '-' }) + continue + } + if (UPLOAD_TYPES.has(type) || String(rule.name || '').toLowerCase().includes('upload')) { const urls = extractUrls(value) out.push({ label, urls: urls.length ? urls : [], text: urls.length ? undefined : '-' }) diff --git a/web-admin/src/utils/oa-dept-select-rule.ts b/web-admin/src/utils/oa-dept-select-rule.ts new file mode 100644 index 00000000..9a0d921d --- /dev/null +++ b/web-admin/src/utils/oa-dept-select-rule.ts @@ -0,0 +1,60 @@ +/** 与 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) + + 'c' + ) +} + +/** FormCreate Designer DragRule:部门选择 */ +export const deptSelectDragRule = { + menu: 'main' as const, + icon: 'icon-tree', + label: '部门', + name: 'dept', + input: true, + mask: true, + validate: ['string', 'array', 'object'], + event: ['change'], + rule() { + return { + type: 'dept', + field: uniqueId(), + title: '部门', + info: '', + $required: false, + props: { + multiple: true, + max: undefined as number | undefined + } + } + }, + props() { + return [ + { + type: 'switch', + field: 'multiple', + title: '允许多选', + value: true, + props: { activeText: '是', inactiveText: '否' } + }, + { + type: 'inputNumber', + field: 'max', + title: '最大可选数', + value: undefined, + props: { min: 1, precision: 0, placeholder: '不填则不限制' } + }, + { + type: 'input', + field: 'formCreateInfo', + title: '提示信息' + } + ] + } +} diff --git a/web-admin/src/utils/oa-dept-value.ts b/web-admin/src/utils/oa-dept-value.ts new file mode 100644 index 00000000..75c05711 --- /dev/null +++ b/web-admin/src/utils/oa-dept-value.ts @@ -0,0 +1,45 @@ +export type OaDeptValueItem = { id: string; name: string } + +export function isDeptValueItem(v: unknown): v is OaDeptValueItem { + if (!v || typeof v !== 'object') return false + const o = v as Record + return String(o.id || '').trim() !== '' && String(o.name || '').trim() !== '' +} + +/** 单选 → item|null;多选 → items(容错把单对象包成数组) */ +export function normalizeDeptModelValue( + raw: unknown, + multiple: boolean +): OaDeptValueItem | OaDeptValueItem[] | null { + if (multiple) { + if (Array.isArray(raw)) { + return raw.filter(isDeptValueItem).map((x) => ({ + id: String(x.id).trim(), + name: String(x.name).trim() + })) + } + if (isDeptValueItem(raw)) { + return [{ id: String(raw.id).trim(), name: String(raw.name).trim() }] + } + return [] + } + if (isDeptValueItem(raw)) { + return { id: String(raw.id).trim(), name: String(raw.name).trim() } + } + if (Array.isArray(raw) && raw.length === 1 && isDeptValueItem(raw[0])) { + return { id: String(raw[0].id).trim(), name: String(raw[0].name).trim() } + } + return null +} + +export function formatDeptDisplay(raw: unknown): string { + if (Array.isArray(raw)) { + return raw + .filter(isDeptValueItem) + .map((x) => x.name) + .filter(Boolean) + .join('、') + } + if (isDeptValueItem(raw)) return raw.name + return '' +} diff --git a/web-admin/src/utils/oa-form-create-dept.ts b/web-admin/src/utils/oa-form-create-dept.ts new file mode 100644 index 00000000..4ae2cf97 --- /dev/null +++ b/web-admin/src/utils/oa-form-create-dept.ts @@ -0,0 +1,17 @@ +import FcDesigner from '@form-create/designer' +import OaDeptSelect from '@/components/oa-approval/OaDeptSelect.vue' +import { deptSelectDragRule } from '@/utils/oa-dept-select-rule' + +const FLAG = '__oaDeptSelectConfigured' + +export function setupOaDeptSelect(formCreate?: { component?: (name: string, c: unknown) => void }) { + if ((FcDesigner as any)[FLAG]) return + FcDesigner.component('dept', OaDeptSelect) + if (typeof formCreate?.component === 'function') { + formCreate.component('dept', OaDeptSelect) + } + if (typeof FcDesigner.addDragRule === 'function') { + FcDesigner.addDragRule(deptSelectDragRule) + } + ;(FcDesigner as any)[FLAG] = true +}