From 5b248a9716688d8132cfb02b4ba0abecd4060b06 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期三, 20 五月 2026 11:49:08 +0800
Subject: [PATCH] 审批模板流程化
---
src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue | 130 ++++-
src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue | 83 --
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalTemplateBindingUtils.js | 91 +++
src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue | 44 +
src/views/officeProcessAutomation/ApproveManage/approve-template/selectOptionSource.js | 140 +++++
src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js | 30 +
src/views/officeProcessAutomation/ApproveManage/approve-shared/useFlowUserOptions.js | 35 +
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js | 1
src/views/officeProcessAutomation/HrManage/regular-apply/index.vue | 101 +++
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js | 61 -
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js | 102 +++
src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js | 16
src/views/officeProcessAutomation/ApproveManage/approve-template/useSelectOptionSources.js | 45 +
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js | 23
src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalTemplateBinding.js | 229 ++++++++
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue | 158 ++++++
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplatePicker.vue | 85 +++
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue | 95 +++
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue | 79 +++
19 files changed, 1,392 insertions(+), 156 deletions(-)
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
index 8974cf4..090dbb1 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -8,6 +8,7 @@
nodeSignModeLabel,
} from "../approve-template/approveTemplateConstants.js";
import { buildFormPayloadFromFields, parseFormConfigToData } from "../approve-template/formConfigUtils.js";
+import { isDynamicOptionSource, selectOptionSourceLabel } from "../approve-template/selectOptionSource.js";
/** 瀹℃壒绫诲瀷锛堜笌鍚庣瀛楁 approvalType 瀵归綈锛屽悗鏈熷彲鍚屾锛� */
export const APPROVAL_TYPE_OPTIONS = [
@@ -176,6 +177,9 @@
/** 鍗曞瓧娈靛睍绀哄�硷紙璇︽儏鍙锛� */
export function formatFieldDisplayValue(field, val) {
if (val == null || val === "" || (Array.isArray(val) && !val.length)) return "鈥�";
+ if (field?.type === "select" && isDynamicOptionSource(field.optionSource)) {
+ return `${selectOptionSourceLabel(field.optionSource)}锛�${String(val)}`;
+ }
if (field?.type === "select" && field.options?.length) {
const hit = field.options.find((o) => String(o.value) === String(val));
return hit?.label || String(val);
@@ -286,6 +290,12 @@
formConfig: buildInstanceFormConfigJson({ ...tpl, fields: tpl.fields || submitForm?.formFieldDefs }, payload),
tasks: taskList,
};
+
+ const attachments =
+ (Array.isArray(submitForm?.storageBlobDTOs) && submitForm.storageBlobDTOs.length
+ ? submitForm.storageBlobDTOs
+ : null) || tpl.storageBlobDTOs;
+ if (attachments?.length) dto.storageBlobDTOs = attachments;
if (isUpdate) {
dto.id = existingRow?.id ?? submitForm?.instanceId;
@@ -487,6 +497,10 @@
formFieldDefs: fields,
formPayload,
flowNodes,
+ templateAttachments: initTemplateAttachmentsFromSnapshot(templateSnapshot),
+ storageBlobDTOs: row?.storageBlobDTOs?.length
+ ? JSON.parse(JSON.stringify(row.storageBlobDTOs))
+ : [],
};
}
@@ -511,5 +525,14 @@
formFieldDefs: tpl?.fields || [],
formPayload: payload,
flowNodes,
+ templateAttachments: tpl?.storageBlobDTOs
+ ? JSON.parse(JSON.stringify(tpl.storageBlobDTOs))
+ : [],
+ storageBlobDTOs: [],
};
}
+
+export function initTemplateAttachmentsFromSnapshot(templateSnapshot) {
+ const list = templateSnapshot?.storageBlobDTOs;
+ return list?.length ? JSON.parse(JSON.stringify(list)) : [];
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue
index 6cdc627..7933db5 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue
@@ -17,12 +17,17 @@
</el-descriptions-item>
</el-descriptions>
- <el-form v-else label-width="120px" class="form-payload-edit">
+ <div
+ v-else
+ class="form-payload-edit"
+ v-loading="optionSourceLoading"
+ >
<el-form-item
v-for="field in fields"
:key="field.key"
:label="field.label"
:prop="`formPayload.${field.key}`"
+ :required="Boolean(field.required)"
>
<el-input
v-if="field.type === 'text'"
@@ -73,17 +78,25 @@
:placeholder="`璇烽�夋嫨${field.label}`"
style="width: 100%"
clearable
+ filterable
>
- <el-option v-for="o in field.options" :key="o.value" :label="o.label" :value="o.value" />
+ <el-option
+ v-for="o in getOptions(field)"
+ :key="String(o.value)"
+ :label="o.label"
+ :value="o.value"
+ />
</el-select>
<span v-else class="field-value">{{ displayValue(field) }}</span>
</el-form-item>
- </el-form>
+ </div>
</template>
<el-empty v-else description="鏆傛棤濉姤椤�" :image-size="48" />
</template>
<script setup>
+import { onMounted, watch } from "vue";
+import { useSelectOptionSources } from "../../approve-template/useSelectOptionSources.js";
import { formatFieldDisplayValue } from "../approveListConstants.js";
const props = defineProps({
@@ -92,8 +105,31 @@
readonly: { type: Boolean, default: false },
});
+const { loading: optionSourceLoading, ensureForFields, getOptions, getDisplayLabel } =
+ useSelectOptionSources();
+
+async function loadOptionCaches() {
+ await ensureForFields(props.fields);
+}
+
+onMounted(() => {
+ loadOptionCaches();
+});
+
+watch(
+ () => props.fields,
+ () => {
+ loadOptionCaches();
+ },
+ { deep: true }
+);
+
function displayValue(field) {
- return formatFieldDisplayValue(field, props.formPayload?.[field.key]);
+ const val = props.formPayload?.[field.key];
+ if (field.type === "select" && field.optionSource && field.optionSource !== "static") {
+ return getDisplayLabel(field, val);
+ }
+ return formatFieldDisplayValue(field, val);
}
</script>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
index 5fce871..b87a964 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
@@ -105,54 +105,33 @@
褰撳墠绫诲瀷锛歿{ selectedBusinessTypeLabel || "鈥�" }}锛岃閫夋嫨鍏蜂綋瀹℃壒妯℃澘銆�
<el-button type="primary" link class="ml8" @click="backToBusinessTypePick">鏇存崲绫诲瀷</el-button>
</p>
- <div v-loading="submitTemplatesLoading" class="template-grid">
- <div
- v-for="card in submitTemplateCards"
- :key="card.key"
- class="template-card"
- @click="onTemplatePick(card)"
- >
- <span class="template-card-type" :style="approvalTypeStyle(card.approvalType)">
- {{ card.label }}
- </span>
- <span class="template-card-desc">{{ card.summaryPlaceholder }}</span>
- </div>
- <el-empty
- v-if="!submitTemplatesLoading && !submitTemplateCards.length"
- description="璇ョ被鍨嬩笅鏆傛棤鍙敤瀹℃壒妯℃澘"
- :image-size="80"
- class="template-empty"
- />
- </div>
+ <ApprovalTemplatePicker
+ :cards="submitTemplateCards"
+ :loading="submitTemplatesLoading"
+ @pick="onTemplatePick"
+ />
</template>
<template v-else>
<div v-loading="submitTemplatesLoading && !isSubmitEdit">
<el-form ref="submitFormRef" :model="submitForm" :rules="submitFormRules" label-width="120px">
- <el-form-item label="瀹℃壒绫诲瀷">
+ <el-form-item v-if="isSubmitEdit" label="瀹℃壒绫诲瀷">
<span class="approve-type-cell" :style="approvalTypeStyle(activeTemplate.approvalType)">
{{ activeTemplate.label }}
</span>
- <el-button
- v-if="!isSubmitEdit"
- type="primary"
- link
- class="ml12"
- @click="backToTemplatePick"
- >
- 鏇存崲妯℃澘
- </el-button>
</el-form-item>
- <FormPayloadFields
+ <ApprovalTemplateFormSection
+ :active-template="activeTemplate"
:fields="submitFormFields"
:form-payload="submitForm.formPayload"
+ v-model:flow-nodes="submitForm.flowNodes"
+ v-model:attachments="submitForm.storageBlobDTOs"
+ :template-attachments="submitForm.templateAttachments"
+ :user-options="flowUserOptions"
+ :show-template-name="!isSubmitEdit"
+ :allow-change-template="!isSubmitEdit"
+ @change-template="backToTemplatePick"
/>
- <el-form-item label="瀹℃壒娴佺▼" required>
- <TemplateFlowEditor v-model="submitForm.flowNodes" :user-options="flowUserOptions" />
- <p class="flow-tip">
- 鎸夐『搴忔祦杞細鍙负姣忎釜鑺傜偣娣诲姞澶氬悕瀹℃壒浜猴紱浼氱闇�鍏ㄩ儴閫氳繃锛屾垨绛句换涓�浜洪�氳繃鍗冲彲杩涘叆涓嬩竴鑺傜偣銆�
- </p>
- </el-form-item>
</el-form>
</div>
</template>
@@ -294,9 +273,9 @@
import { Plus, RefreshRight } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { onMounted, ref } from "vue";
-import { userListNoPageByTenantId } from "@/api/system/user.js";
-import TemplateFlowEditor from "../approve-template/components/TemplateFlowEditor.vue";
-import FormPayloadFields from "./components/FormPayloadFields.vue";
+import ApprovalTemplateFormSection from "../approve-shared/components/ApprovalTemplateFormSection.vue";
+import ApprovalTemplatePicker from "../approve-shared/components/ApprovalTemplatePicker.vue";
+import { useFlowUserOptions } from "../approve-shared/useFlowUserOptions.js";
import { formatDisplayTime } from "../approve-template/approveTemplateConstants.js";
import { approvalTypeStyle } from "./approveListConstants.js";
import ApproveDetailPanel from "./components/ApproveDetailPanel.vue";
@@ -349,29 +328,7 @@
openApprove,
} = al;
-const flowUserOptions = ref([]);
-
-function unwrapArray(payload) {
- if (Array.isArray(payload)) return payload;
- if (payload?.data && Array.isArray(payload.data)) return payload.data;
- if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
- return [];
-}
-
-function isActiveUser(u) {
- if (u.delFlag === "2" || u.delFlag === 2) return false;
- if (u.status == null) return true;
- return String(u.status) === "0";
-}
-
-async function loadUsers() {
- try {
- const res = await userListNoPageByTenantId();
- flowUserOptions.value = unwrapArray(res).filter(isActiveUser);
- } catch {
- flowUserOptions.value = [];
- }
-}
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
async function onSubmitInstance() {
const ok = await submitInstanceForm();
@@ -406,7 +363,7 @@
}
onMounted(() => {
- loadUsers();
+ loadFlowUsers();
handleQuery();
});
</script>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
index 4b8510a..27706b9 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -18,11 +18,13 @@
fetchBusinessTypeOptions,
formatDisplayTime,
mapEnabledFromApi,
- mapTemplateFromApi,
- unwrapTemplateDetail,
unwrapTemplateList,
} from "../approve-template/approveTemplateConstants.js";
-import { buildSubmitTemplateFromRow } from "../approve-template/formConfigUtils.js";
+import {
+ buildFormPayloadRules,
+ buildTemplateBindingFromDetail,
+ validateTemplateBinding,
+} from "../approve-shared/approvalTemplateBindingUtils.js";
import {
APPROVAL_TYPE_OPTIONS,
approvalStatusLabel,
@@ -37,7 +39,6 @@
mapInstanceFromApi,
mapSubmitTemplateCard,
matchBusinessTypeValue,
- validateSubmitFlowNodes,
unwrapInstancePage,
} from "./approveListConstants.js";
@@ -111,22 +112,10 @@
return submitForm.formFieldDefs || [];
});
- const submitFormRules = computed(() => {
- const rules = {
- templateKey: [{ required: true, message: "璇烽�夋嫨瀹℃壒绫诲瀷", trigger: "change" }],
- };
- submitFormFields.value.forEach((f) => {
- if (!f.required) return;
- if (f.type === "number") {
- rules[`formPayload.${f.key}`] = [{ required: true, message: `璇峰~鍐�${f.label}`, trigger: "blur" }];
- } else if (f.type === "datetimerange") {
- rules[`formPayload.${f.key}`] = [{ required: true, message: `璇烽�夋嫨${f.label}`, trigger: "change" }];
- } else {
- rules[`formPayload.${f.key}`] = [{ required: true, message: `璇峰~鍐�${f.label}`, trigger: "blur" }];
- }
- });
- return rules;
- });
+ const submitFormRules = computed(() => ({
+ templateKey: [{ required: true, message: "璇烽�夋嫨瀹℃壒绫诲瀷", trigger: "change" }],
+ ...buildFormPayloadRules(submitFormFields.value),
+ }));
const tableColumn = ref([
{ label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 110 },
@@ -291,18 +280,12 @@
submitTemplatesLoading.value = true;
try {
const res = await getApprovalTemplateDetail(card.id);
- const mapped = mapTemplateFromApi(unwrapTemplateDetail(res));
- const tpl = {
- ...buildSubmitTemplateFromRow(mapped),
- templateId: mapped.id,
- };
- const base = createEmptySubmitForm(String(card.id), tpl, mapped.flowNodes);
+ const applied = buildTemplateBindingFromDetail(res);
Object.assign(submitForm, {
- ...base,
- templateName: mapped.templateName || tpl.label || "",
- businessType: mapped.businessType ?? card.businessType ?? selectedBusinessType.value,
- templateSnapshot: tpl,
- formFieldDefs: tpl.fields || [],
+ templateKey: String(card.id),
+ ...applied,
+ businessType:
+ applied.businessType ?? card.businessType ?? selectedBusinessType.value,
});
submitDialog.step = 3;
} catch {
@@ -343,9 +326,9 @@
return false;
}
if (!activeTemplate.value) return false;
- const flowCheck = validateSubmitFlowNodes(submitForm.flowNodes);
- if (!flowCheck.ok) {
- ElMessage.warning(flowCheck.message);
+ const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
+ if (!bindingCheck.ok) {
+ ElMessage.warning(bindingCheck.message);
return false;
}
if (!submitForm.templateId) {
@@ -360,7 +343,7 @@
submitForm,
activeTemplate: activeTemplate.value,
userStore,
- flowNodes: flowCheck.nodes,
+ flowNodes: bindingCheck.nodes,
})
);
submitDialog.visible = false;
@@ -382,9 +365,9 @@
return false;
}
if (!activeTemplate.value) return false;
- const flowCheck = validateSubmitFlowNodes(submitForm.flowNodes);
- if (!flowCheck.ok) {
- ElMessage.warning(flowCheck.message);
+ const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
+ if (!bindingCheck.ok) {
+ ElMessage.warning(bindingCheck.message);
return false;
}
if (!submitForm.instanceId) {
@@ -398,7 +381,7 @@
buildInstanceDto({
submitForm,
activeTemplate: activeTemplate.value,
- flowNodes: flowCheck.nodes,
+ flowNodes: bindingCheck.nodes,
existingRow: submitEditRow.value,
})
);
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js
new file mode 100644
index 0000000..09d68fd
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js
@@ -0,0 +1,102 @@
+/**
+ * 鍚勪笟鍔℃ā鍧椾笌瀹℃壒妯℃澘绫诲瀷鐨勬槧灏勶紙閰嶇疆鍖栧叆鍙o級
+ *
+ * 浣跨敤鏂瑰紡锛�
+ * 1. 鍦ㄤ笟鍔¢〉寮曞叆 ApprovalTemplateBindDialog锛屼紶鍏� moduleKey
+ * 2. 鎴栧湪琛ㄥ崟鍐呭祵 ApprovalTemplateFormSection + useApprovalTemplateBinding({ moduleKey })
+ *
+ * businessType锛氳嫢鍚庣 TypeEnums 宸插浐瀹� code锛屽彲鐩存帴鍐欐 value锛涘惁鍒欑敤 typeLabels 鎸夊悕绉板尮閰�
+ */
+export const APPROVAL_MODULE_KEYS = {
+ REGULAR: "regular",
+ TRANSFER: "transfer",
+ RESIGN: "resign",
+ WORK_HANDOVER: "work_handover",
+ LEAVE: "leave",
+ OVERTIME: "overtime",
+ TRAVEL_REIMBURSE: "travel_reimburse",
+ COST_REIMBURSE: "cost_reimburse",
+};
+
+/** @type {Record<string, import('./approvalModuleRegistry.js').ApprovalModuleConfig>} */
+export const APPROVAL_MODULE_REGISTRY = {
+ [APPROVAL_MODULE_KEYS.REGULAR]: {
+ label: "杞鐢宠",
+ approvalType: "regular",
+ typeLabels: ["杞", "杞鐢宠"],
+ },
+ [APPROVAL_MODULE_KEYS.TRANSFER]: {
+ label: "璋冨矖鐢宠",
+ approvalType: "transfer",
+ typeLabels: ["璋冨矖", "璋冨姩", "璋冨矖鐢宠", "璋冨姩鐢宠"],
+ },
+ [APPROVAL_MODULE_KEYS.RESIGN]: {
+ label: "绂昏亴鐢宠",
+ approvalType: "resign",
+ typeLabels: ["绂昏亴", "绂昏亴鐢宠"],
+ },
+ [APPROVAL_MODULE_KEYS.WORK_HANDOVER]: {
+ label: "宸ヤ綔浜ゆ帴",
+ approvalType: "work_handover",
+ typeLabels: ["宸ヤ綔浜ゆ帴", "浜ゆ帴"],
+ },
+ [APPROVAL_MODULE_KEYS.LEAVE]: {
+ label: "璇峰亣鐢宠",
+ approvalType: "leave",
+ typeLabels: ["璇峰亣", "璇峰亣鐢宠"],
+ },
+ [APPROVAL_MODULE_KEYS.OVERTIME]: {
+ label: "鍔犵彮鐢宠",
+ approvalType: "overtime",
+ typeLabels: ["鍔犵彮", "鍔犵彮鐢宠"],
+ },
+ [APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE]: {
+ label: "宸梾鎶ラ攢",
+ approvalType: "travel_reimburse",
+ typeLabels: ["宸梾", "宸梾鎶ラ攢"],
+ },
+ [APPROVAL_MODULE_KEYS.COST_REIMBURSE]: {
+ label: "璐圭敤鎶ラ攢",
+ approvalType: "cost_reimburse",
+ typeLabels: ["璐圭敤", "璐圭敤鎶ラ攢"],
+ },
+};
+
+/**
+ * @typedef {object} ApprovalModuleConfig
+ * @property {string} label
+ * @property {string} [approvalType] 鍒楄〃鏍峰紡鐢�
+ * @property {string|number} [businessType] 涓� TypeEnums value 涓�鑷存椂鍙啓姝�
+ * @property {string[]} [typeLabels] 涓� TypeEnums label 妯$硦鍖归厤
+ */
+
+export function getApprovalModuleConfig(moduleKey) {
+ if (!moduleKey) return null;
+ return APPROVAL_MODULE_REGISTRY[moduleKey] || null;
+}
+
+export function listApprovalModuleEntries() {
+ return Object.entries(APPROVAL_MODULE_REGISTRY).map(([moduleKey, cfg]) => ({
+ moduleKey,
+ ...cfg,
+ }));
+}
+
+/** 浠� TypeEnums 閫夐」涓В鏋愭湰妯″潡鐨� businessType */
+export function resolveModuleBusinessType(moduleKey, typeOptions = []) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return null;
+ if (cfg.businessType != null && cfg.businessType !== "") return cfg.businessType;
+
+ const labels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean);
+ const hit = (typeOptions || []).find((opt) => {
+ const optLabel = String(opt?.label || "").trim();
+ if (!optLabel) return false;
+ return labels.some(
+ (l) => optLabel === l || optLabel.includes(l) || l.includes(optLabel)
+ );
+ });
+ if (hit?.value != null && hit.value !== "") return hit.value;
+
+ return cfg.approvalType || null;
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalTemplateBindingUtils.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalTemplateBindingUtils.js
new file mode 100644
index 0000000..d68016f
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalTemplateBindingUtils.js
@@ -0,0 +1,91 @@
+import {
+ mapAttachmentsFromApi,
+ mapTemplateFromApi,
+ unwrapTemplateDetail,
+} from "../approve-template/approveTemplateConstants.js";
+import { buildSubmitTemplateFromRow } from "../approve-template/formConfigUtils.js";
+import {
+ createEmptySubmitForm,
+ validateSubmitFlowNodes,
+} from "../approve-list/approveListConstants.js";
+
+export function attachmentDisplayName(file) {
+ return (
+ file?.fileName ||
+ file?.originalFilename ||
+ file?.name ||
+ file?.blobName ||
+ "闄勪欢"
+ );
+}
+
+/** 鎺ュ彛璇︽儏 鈫� 鎻愪氦缁戝畾蹇収锛堝惈娴佺▼銆侀檮浠躲�佸~鎶ラ」锛� */
+export function buildTemplateBindingFromDetail(detailRow) {
+ const mapped = mapTemplateFromApi(unwrapTemplateDetail(detailRow));
+ const templateAttachments = mapAttachmentsFromApi(mapped);
+ const tpl = {
+ ...buildSubmitTemplateFromRow(mapped),
+ templateId: mapped.id,
+ businessType: mapped.businessType,
+ storageBlobDTOs: templateAttachments,
+ };
+ const base = createEmptySubmitForm(String(mapped.id ?? ""), tpl, mapped.flowNodes);
+ return {
+ templateId: mapped.id,
+ templateName: mapped.templateName || tpl.label || "",
+ businessType: mapped.businessType ?? "",
+ templateSnapshot: tpl,
+ formFieldDefs: tpl.fields || [],
+ formPayload: base.formPayload,
+ flowNodes: base.flowNodes,
+ templateAttachments: JSON.parse(JSON.stringify(templateAttachments)),
+ storageBlobDTOs: [],
+ };
+}
+
+/** 鏍规嵁妯℃澘 fields 鐢熸垚 el-form rules锛坧rop 涓� formPayload.xxx锛� */
+export function buildFormPayloadRules(fields = []) {
+ const rules = {};
+ (fields || []).forEach((f) => {
+ if (!f.required || !f.key) return;
+ const prop = `formPayload.${f.key}`;
+ if (f.type === "number") {
+ rules[prop] = [{ required: true, message: `璇峰~鍐�${f.label}`, trigger: "blur" }];
+ } else if (f.type === "datetimerange" || f.type === "date" || f.type === "select") {
+ rules[prop] = [{ required: true, message: `璇烽�夋嫨${f.label}`, trigger: "change" }];
+ } else {
+ rules[prop] = [{ required: true, message: `璇峰~鍐�${f.label}`, trigger: "blur" }];
+ }
+ });
+ return rules;
+}
+
+/** 鏍¢獙妯℃澘缁戝畾锛氬鎵规祦绋嬶紙闄勪欢閫夊~锛岀敱鐢ㄦ埛鑷涓婁紶锛� */
+export function validateTemplateBinding({ flowNodes }) {
+ const flowCheck = validateSubmitFlowNodes(flowNodes);
+ if (!flowCheck.ok) return flowCheck;
+ return { ok: true, nodes: flowCheck.nodes };
+}
+
+/** 鍚堝苟缁戝畾缁撴灉鍒颁笟鍔¤〃鍗曞璞★紙瀛楁鍚嶅彲鎸変笟鍔¤鐩栵級 */
+export function applyBindingToForm(target, binding, fieldMap = {}) {
+ if (!target || !binding) return target;
+ const map = {
+ templateId: "templateId",
+ templateName: "templateName",
+ businessType: "businessType",
+ templateSnapshot: "templateSnapshot",
+ formFieldDefs: "formFieldDefs",
+ formPayload: "formPayload",
+ flowNodes: "flowNodes",
+ templateAttachments: "templateAttachments",
+ storageBlobDTOs: "storageBlobDTOs",
+ ...fieldMap,
+ };
+ Object.entries(map).forEach(([srcKey, destKey]) => {
+ if (binding[srcKey] !== undefined) {
+ target[destKey] = binding[srcKey];
+ }
+ });
+ return target;
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue
new file mode 100644
index 0000000..c05374d
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue
@@ -0,0 +1,158 @@
+<!--
+ 涓氬姟妯″潡銆屾柊澧炪�嶆椂瀵煎叆瀹℃壒妯℃澘锛堝浐瀹� moduleKey锛屼粎灞曠ず璇ョ被鍨嬩笅妯℃澘锛�
+
+ 鐢ㄦ硶锛�
+ <ApprovalTemplateBindDialog
+ v-model:visible="visible"
+ module-key="regular"
+ @confirm="onTemplateBound"
+ />
+-->
+<template>
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ :width="step === formStep ? 720 : 640"
+ append-to-body
+ destroy-on-close
+ class="approval-template-bind-dialog"
+ @closed="onClosed"
+ >
+ <template v-if="step === 1">
+ <ApprovalTemplatePicker
+ :cards="templateCards"
+ :loading="templatesLoading"
+ :hint="pickerHint"
+ @pick="onPickTemplate"
+ />
+ </template>
+
+ <template v-else>
+ <div v-loading="templatesLoading">
+ <el-form
+ ref="formRef"
+ :model="bindingForm"
+ :rules="mergedRules"
+ label-width="120px"
+ >
+ <ApprovalTemplateFormSection
+ :active-template="activeTemplate"
+ :fields="formFields"
+ :form-payload="bindingForm.formPayload"
+ v-model:flow-nodes="bindingForm.flowNodes"
+ v-model:attachments="bindingForm.storageBlobDTOs"
+ :template-attachments="bindingForm.templateAttachments"
+ :user-options="flowUserOptions"
+ allow-change-template
+ @change-template="step = 1"
+ />
+ </el-form>
+ </div>
+ </template>
+
+ <template #footer>
+ <el-button v-if="step === formStep" type="primary" :loading="confirming" @click="onConfirm">
+ 纭� 瀹�
+ </el-button>
+ <el-button v-if="step === formStep" @click="step = 1">閲嶉�夋ā鏉�</el-button>
+ <el-button @click="dialogVisible = false">{{ step === 1 ? "鍙� 娑�" : "鍏� 闂�" }}</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { computed, ref, watch } from "vue";
+import { ElMessage } from "element-plus";
+import ApprovalTemplatePicker from "./ApprovalTemplatePicker.vue";
+import ApprovalTemplateFormSection from "./ApprovalTemplateFormSection.vue";
+import { useApprovalTemplateBinding } from "../useApprovalTemplateBinding.js";
+import { useFlowUserOptions } from "../useFlowUserOptions.js";
+import { getApprovalModuleConfig } from "../approvalModuleRegistry.js";
+
+const props = defineProps({
+ visible: { type: Boolean, default: false },
+ /** approvalModuleRegistry 涓殑 moduleKey */
+ moduleKey: { type: String, required: true },
+});
+
+const emit = defineEmits(["update:visible", "confirm"]);
+
+const dialogVisible = computed({
+ get: () => props.visible,
+ set: (v) => emit("update:visible", v),
+});
+
+const {
+ step,
+ bindingForm,
+ templateCards,
+ activeTemplate,
+ formFields,
+ formRules,
+ templatesLoading,
+ loadTemplates,
+ resetBinding,
+ pickTemplate,
+ validateBinding,
+ getBindingPayload,
+ moduleConfig,
+} = useApprovalTemplateBinding({ moduleKey: props.moduleKey, mode: "module" });
+
+const formStep = 2;
+const formRef = ref();
+const confirming = ref(false);
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
+
+const mergedRules = computed(() => ({ ...formRules.value }));
+
+const dialogTitle = computed(() => {
+ const label = moduleConfig.value?.label || "瀹℃壒";
+ return step.value === 1 ? `閫夋嫨${label}妯℃澘` : `纭${label}瀹℃壒淇℃伅`;
+});
+
+const pickerHint = computed(
+ () => `璇烽�夋嫨銆�${moduleConfig.value?.label || "鈥�"}銆嶇被鍨嬩笅宸插惎鐢ㄧ殑瀹℃壒妯℃澘锛屽鎵规祦绋嬪皢鑷姩甯﹀叆銆俙
+);
+
+watch(
+ () => props.visible,
+ async (v) => {
+ if (!v) return;
+ resetBinding();
+ step.value = 1;
+ await Promise.all([loadTemplates(), loadFlowUsers()]);
+ const cfg = getApprovalModuleConfig(props.moduleKey);
+ if (!cfg) ElMessage.warning(`鏈厤缃ā鍧椼��${props.moduleKey}銆嶏紝璇锋鏌� approvalModuleRegistry`);
+ }
+);
+
+async function onPickTemplate(card) {
+ const ok = await pickTemplate(card);
+ if (ok) step.value = formStep;
+}
+
+async function onConfirm() {
+ confirming.value = true;
+ try {
+ const check = await validateBinding(formRef.value);
+ if (!check.ok) {
+ if (check.message) ElMessage.warning(check.message);
+ return;
+ }
+ emit("confirm", { ...getBindingPayload(), flowNodes: check.nodes });
+ dialogVisible.value = false;
+ } finally {
+ confirming.value = false;
+ }
+}
+
+function onClosed() {
+ resetBinding();
+}
+</script>
+
+<style scoped>
+.approval-template-bind-dialog :deep(.el-dialog__body) {
+ padding-top: 12px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue
new file mode 100644
index 0000000..47ad787
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue
@@ -0,0 +1,95 @@
+<!-- 妯℃澘缁戝畾琛ㄥ崟鍖猴細濉姤椤� + 瀹℃壒娴佺▼ + 闄勪欢锛堥』鎸傚湪澶栧眰 el-form 涓嬶級 -->
+<template>
+ <template v-if="activeTemplate">
+ <el-form-item v-if="showTemplateName" label="瀹℃壒妯℃澘">
+ <span class="template-name">{{ activeTemplate.label }}</span>
+ <el-button v-if="allowChangeTemplate" type="primary" link class="ml12" @click="emit('change-template')">
+ 鏇存崲妯℃澘
+ </el-button>
+ </el-form-item>
+
+ <FormPayloadFields :fields="fields" :form-payload="formPayload" />
+
+ <el-form-item label="瀹℃壒娴佺▼" required>
+ <TemplateFlowEditor v-model="flowNodesModel" :user-options="userOptions" />
+ <p class="section-tip">娴佺▼涓庡鎵逛汉鐢辨ā鏉块缃紝鍙寜闇�寰皟鑺傜偣瀹℃壒浜恒��</p>
+ </el-form-item>
+
+ <el-form-item v-if="templateAttachments.length" label="妯℃澘鍙傝��">
+ <el-tag
+ v-for="(f, i) in templateAttachments"
+ :key="`tpl-${i}`"
+ class="attachment-tag"
+ type="info"
+ effect="plain"
+ >
+ {{ attachmentDisplayName(f) }}
+ </el-tag>
+ <p class="section-tip">浠ヤ笂涓烘ā鏉块檮甯︽枃浠讹紝浠呬緵鍙傝�冿紱鎻愪氦闄勪欢璇峰湪涓嬫柟涓婁紶銆�</p>
+ </el-form-item>
+
+ <el-form-item label="闄勪欢">
+ <FileUpload
+ v-model:file-list="attachmentsModel"
+ :limit="uploadLimit"
+ button-text="鐐瑰嚮閫夋嫨鏂囦欢"
+ />
+ <p class="section-tip">閫夊~锛屽彲涓婁紶涓庣敵璇风浉鍏崇殑璇存槑鏉愭枡銆�</p>
+ </el-form-item>
+ </template>
+ <el-empty v-else description="璇峰厛閫夋嫨瀹℃壒妯℃澘" :image-size="64" />
+</template>
+
+<script setup>
+import { computed } from "vue";
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+import TemplateFlowEditor from "../../approve-template/components/TemplateFlowEditor.vue";
+import FormPayloadFields from "../../approve-list/components/FormPayloadFields.vue";
+import { attachmentDisplayName } from "../approvalTemplateBindingUtils.js";
+
+const props = defineProps({
+ activeTemplate: { type: Object, default: null },
+ fields: { type: Array, default: () => [] },
+ formPayload: { type: Object, required: true },
+ flowNodes: { type: Array, default: () => [] },
+ /** 鐢ㄦ埛鑷涓婁紶鐨勯檮浠� */
+ attachments: { type: Array, default: () => [] },
+ /** 妯℃澘棰勭疆闄勪欢锛堝彧璇诲睍绀猴級 */
+ templateAttachments: { type: Array, default: () => [] },
+ userOptions: { type: Array, default: () => [] },
+ showTemplateName: { type: Boolean, default: true },
+ allowChangeTemplate: { type: Boolean, default: true },
+ uploadLimit: { type: Number, default: 10 },
+});
+
+const emit = defineEmits(["update:flowNodes", "update:attachments", "change-template"]);
+
+const flowNodesModel = computed({
+ get: () => props.flowNodes,
+ set: (v) => emit("update:flowNodes", v),
+});
+
+const attachmentsModel = computed({
+ get: () => props.attachments,
+ set: (v) => emit("update:attachments", v),
+});
+</script>
+
+<style scoped>
+.template-name {
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+}
+.ml12 {
+ margin-left: 12px;
+}
+.section-tip {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ margin: 8px 0 0;
+ line-height: 1.5;
+}
+.attachment-tag {
+ margin: 0 8px 8px 0;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplatePicker.vue b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplatePicker.vue
new file mode 100644
index 0000000..8adfebc
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplatePicker.vue
@@ -0,0 +1,85 @@
+<!-- 瀹℃壒妯℃澘鍗$墖閫夋嫨锛堟寜 businessType 杩囨护锛� -->
+<template>
+ <div class="approval-template-picker">
+ <p v-if="hint" class="picker-hint">{{ hint }}</p>
+ <div v-loading="loading" class="template-grid">
+ <div
+ v-for="card in cards"
+ :key="card.key || card.id"
+ class="template-card"
+ @click="emit('pick', card)"
+ >
+ <span class="template-card-type" :style="typeStyle(card.approvalType)">
+ {{ card.label }}
+ </span>
+ <span class="template-card-desc">{{ card.summaryPlaceholder }}</span>
+ </div>
+ <el-empty
+ v-if="!loading && !cards.length"
+ :description="emptyText"
+ :image-size="80"
+ class="template-empty"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { approvalTypeStyle } from "../../approve-list/approveListConstants.js";
+
+defineProps({
+ cards: { type: Array, default: () => [] },
+ loading: { type: Boolean, default: false },
+ hint: { type: String, default: "" },
+ emptyText: { type: String, default: "璇ョ被鍨嬩笅鏆傛棤鍙敤瀹℃壒妯℃澘" },
+});
+
+const emit = defineEmits(["pick"]);
+
+function typeStyle(approvalType) {
+ return approvalTypeStyle(approvalType);
+}
+</script>
+
+<style scoped>
+.picker-hint {
+ font-size: 13px;
+ color: var(--el-text-color-secondary);
+ margin: 0 0 16px;
+}
+.template-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 12px;
+ min-height: 120px;
+}
+.template-empty {
+ grid-column: 1 / -1;
+}
+.template-card {
+ padding: 14px 16px;
+ border: 1px solid var(--el-border-color-lighter);
+ border-radius: var(--radius-md, 8px);
+ cursor: pointer;
+ transition: border-color 0.2s, box-shadow 0.2s;
+ background: var(--el-fill-color-blank);
+}
+.template-card:hover {
+ border-color: var(--el-color-primary);
+ box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.06));
+}
+.template-card-type {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 13px;
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+.template-card-desc {
+ display: block;
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ line-height: 1.5;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalTemplateBinding.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalTemplateBinding.js
new file mode 100644
index 0000000..de056d2
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalTemplateBinding.js
@@ -0,0 +1,229 @@
+import {
+ getApprovalTemplateDetail,
+ listApprovalTemplate,
+ TEMPLATE_TYPE_CUSTOM,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import { computed, reactive, ref } from "vue";
+import { ElMessage } from "element-plus";
+import {
+ fetchBusinessTypeOptions,
+ mapEnabledFromApi,
+ unwrapTemplateList,
+} from "../approve-template/approveTemplateConstants.js";
+import {
+ createEmptySubmitForm,
+ mapSubmitTemplateCard,
+ matchBusinessTypeValue,
+} from "../approve-list/approveListConstants.js";
+import {
+ getApprovalModuleConfig,
+ resolveModuleBusinessType,
+} from "./approvalModuleRegistry.js";
+import {
+ buildFormPayloadRules,
+ buildTemplateBindingFromDetail,
+ validateTemplateBinding,
+} from "./approvalTemplateBindingUtils.js";
+
+/**
+ * 瀹℃壒妯℃澘缁戝畾锛堜笟鍔℃ā鍧楀浐瀹氱被鍨� / 瀹℃壒鍒楄〃閫氱敤锛�
+ *
+ * @param {object} options
+ * @param {string} [options.moduleKey] 涓氬姟妯″潡 key锛岃 approvalModuleRegistry
+ * @param {string|number} [options.businessType] 鐩存帴鎸囧畾绫诲瀷锛堜紭鍏堢骇楂樹簬 moduleKey锛�
+ * @param {'module'|'universal'} [options.mode] module=浠呮湰绫诲瀷妯℃澘锛泆niversal=闇�鍏堥�夌被鍨�
+ */
+export function useApprovalTemplateBinding(options = {}) {
+ const { moduleKey = null, businessType: fixedBusinessType = null, mode = moduleKey ? "module" : "universal" } =
+ options;
+
+ const isUniversal = mode === "universal" && !moduleKey && fixedBusinessType == null;
+
+ const allTemplates = ref([]);
+ const businessTypeOptions = ref([]);
+ const selectedBusinessType = ref(fixedBusinessType ?? "");
+ const templatesLoading = ref(false);
+ const step = ref(isUniversal ? 1 : 1);
+
+ const bindingForm = reactive(createEmptySubmitForm(""));
+
+ const moduleConfig = computed(() => getApprovalModuleConfig(moduleKey));
+
+ const resolvedBusinessType = computed(() => {
+ if (fixedBusinessType != null && fixedBusinessType !== "") return fixedBusinessType;
+ if (selectedBusinessType.value != null && selectedBusinessType.value !== "") {
+ return selectedBusinessType.value;
+ }
+ if (moduleKey) {
+ return resolveModuleBusinessType(moduleKey, businessTypeOptions.value);
+ }
+ return "";
+ });
+
+ const templateCards = computed(() => {
+ const type = resolvedBusinessType.value;
+ if (type == null || type === "") return [];
+ return allTemplates.value.filter((card) => matchBusinessTypeValue(card.businessType, type));
+ });
+
+ const activeTemplate = computed(() => bindingForm.templateSnapshot || null);
+
+ const formFields = computed(() => {
+ const tplFields = activeTemplate.value?.fields;
+ if (tplFields?.length) return tplFields;
+ return bindingForm.formFieldDefs || [];
+ });
+
+ const formRules = computed(() => buildFormPayloadRules(formFields.value));
+
+ const hasTemplateBound = computed(() => Boolean(activeTemplate.value?.templateId || bindingForm.templateId));
+
+ function businessTypeLabel(type) {
+ if (type == null || type === "") return "";
+ const hit = businessTypeOptions.value.find((x) => matchBusinessTypeValue(x.value, type));
+ return hit?.label || moduleConfig.value?.label || "";
+ }
+
+ const selectedBusinessTypeLabel = computed(() => businessTypeLabel(resolvedBusinessType.value));
+
+ function countTemplatesByBusinessType(type) {
+ return allTemplates.value.filter((card) => matchBusinessTypeValue(card.businessType, type)).length;
+ }
+
+ async function loadTemplates() {
+ templatesLoading.value = true;
+ try {
+ const [typeOptions, customRes] = await Promise.all([
+ fetchBusinessTypeOptions(),
+ listApprovalTemplate(TEMPLATE_TYPE_CUSTOM),
+ ]);
+ businessTypeOptions.value = typeOptions;
+ allTemplates.value = unwrapTemplateList(customRes)
+ .filter((row) => mapEnabledFromApi(row.enabled))
+ .map(mapSubmitTemplateCard);
+
+ if (moduleKey && !fixedBusinessType) {
+ const resolved = resolveModuleBusinessType(moduleKey, typeOptions);
+ if (resolved != null && resolved !== "") selectedBusinessType.value = resolved;
+ }
+ } catch {
+ businessTypeOptions.value = [];
+ allTemplates.value = [];
+ ElMessage.error("鍔犺浇瀹℃壒妯℃澘澶辫触");
+ } finally {
+ templatesLoading.value = false;
+ }
+ }
+
+ function resetBinding() {
+ step.value = isUniversal ? 1 : 1;
+ if (!fixedBusinessType && !moduleKey) selectedBusinessType.value = "";
+ else if (moduleKey) {
+ selectedBusinessType.value =
+ fixedBusinessType ?? resolveModuleBusinessType(moduleKey, businessTypeOptions.value) ?? "";
+ }
+ Object.assign(bindingForm, createEmptySubmitForm(""));
+ }
+
+ function pickBusinessType(type) {
+ if (!countTemplatesByBusinessType(type)) {
+ ElMessage.warning("璇ョ被鍨嬩笅鏆傛棤鍙敤瀹℃壒妯℃澘");
+ return;
+ }
+ selectedBusinessType.value = type;
+ step.value = 2;
+ }
+
+ function backToBusinessTypePick() {
+ selectedBusinessType.value = "";
+ step.value = 1;
+ }
+
+ function backToTemplatePick() {
+ step.value = isUniversal ? 2 : 1;
+ }
+
+ async function pickTemplate(card) {
+ if (!card?.id) return false;
+ templatesLoading.value = true;
+ try {
+ const res = await getApprovalTemplateDetail(card.id);
+ const applied = buildTemplateBindingFromDetail(res);
+ Object.assign(bindingForm, {
+ templateKey: String(card.id),
+ ...applied,
+ });
+ step.value = isUniversal ? 3 : 2;
+ return true;
+ } catch {
+ ElMessage.error("鍔犺浇妯℃澘璇︽儏澶辫触");
+ return false;
+ } finally {
+ templatesLoading.value = false;
+ }
+ }
+
+ /** 鐩存帴浠ヨ鎯呰缁戝畾锛堢紪杈戝洖鏄撅級 */
+ function applyBindingState(state) {
+ if (!state) return;
+ Object.assign(bindingForm, createEmptySubmitForm(""), state);
+ step.value = isUniversal ? 3 : 2;
+ }
+
+ async function validateBinding(formRef) {
+ if (formRef?.validate) {
+ try {
+ await formRef.validate();
+ } catch {
+ return { ok: false };
+ }
+ }
+ if (!hasTemplateBound.value) {
+ return { ok: false, message: "璇烽�夋嫨瀹℃壒妯℃澘" };
+ }
+ return validateTemplateBinding({ flowNodes: bindingForm.flowNodes });
+ }
+
+ function getBindingPayload() {
+ return {
+ templateId: bindingForm.templateId,
+ templateName: bindingForm.templateName,
+ businessType: bindingForm.businessType ?? resolvedBusinessType.value,
+ templateSnapshot: bindingForm.templateSnapshot,
+ formFieldDefs: bindingForm.formFieldDefs,
+ formPayload: bindingForm.formPayload,
+ flowNodes: bindingForm.flowNodes,
+ templateAttachments: bindingForm.templateAttachments,
+ storageBlobDTOs: bindingForm.storageBlobDTOs,
+ };
+ }
+
+ return {
+ isUniversal,
+ moduleConfig,
+ step,
+ bindingForm,
+ allTemplates,
+ businessTypeOptions,
+ selectedBusinessType,
+ resolvedBusinessType,
+ selectedBusinessTypeLabel,
+ templateCards,
+ activeTemplate,
+ formFields,
+ formRules,
+ hasTemplateBound,
+ templatesLoading,
+ loadTemplates,
+ resetBinding,
+ pickBusinessType,
+ backToBusinessTypePick,
+ backToTemplatePick,
+ pickTemplate,
+ applyBindingState,
+ validateBinding,
+ getBindingPayload,
+ countTemplatesByBusinessType,
+ businessTypeLabel,
+ };
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/useFlowUserOptions.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useFlowUserOptions.js
new file mode 100644
index 0000000..2788ac7
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useFlowUserOptions.js
@@ -0,0 +1,35 @@
+import { ref } from "vue";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+
+function unwrapArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (payload?.data && Array.isArray(payload.data)) return payload.data;
+ if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
+ return [];
+}
+
+function isActiveUser(u) {
+ if (u.delFlag === "2" || u.delFlag === 2) return false;
+ if (u.status == null) return true;
+ return String(u.status) === "0";
+}
+
+/** 瀹℃壒娴佺▼閫変汉涓嬫媺锛堟ā鏉�/瀹炰緥鍏辩敤锛� */
+export function useFlowUserOptions() {
+ const flowUserOptions = ref([]);
+ const loading = ref(false);
+
+ async function loadFlowUsers() {
+ loading.value = true;
+ try {
+ const res = await userListNoPageByTenantId();
+ flowUserOptions.value = unwrapArray(res).filter(isActiveUser);
+ } catch {
+ flowUserOptions.value = [];
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ return { flowUserOptions, loading, loadFlowUsers };
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
index 517e01c..4d7d7a6 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
@@ -178,6 +178,18 @@
return data;
}
+/** 鍚庣闄勪欢瀛楁 鈫� 椤甸潰 storageBlobDTOs */
+export function mapAttachmentsFromApi(row) {
+ const list =
+ row?.storageBlobDTOs ||
+ row?.storageBlobDTOS ||
+ row?.storageBlobVOS ||
+ row?.storageBlobVOList ||
+ row?.attachmentList ||
+ [];
+ return Array.isArray(list) ? list : [];
+}
+
/** 鍒嗛〉鍒楄〃椤� 鈫� 椤甸潰琛屾暟鎹紙涓昏〃 + 鑺傜偣锛� */
export function mapTemplateFromApi(row) {
if (!row) return {};
@@ -193,6 +205,7 @@
businessType: row.businessType ?? "",
formConfig: row.formConfig,
formConfigData: parseFormConfigToData(row.formConfig),
+ storageBlobDTOs: mapAttachmentsFromApi(row),
createdUser: row.createdUser,
createdUserName: row.createdUserName,
...times,
@@ -236,6 +249,8 @@
}),
};
if (templateId) dto.id = templateId;
+ const attachments = Array.isArray(form.storageBlobDTOs) ? form.storageBlobDTOs : [];
+ if (attachments.length) dto.storageBlobDTOs = attachments;
return dto;
}
@@ -280,6 +295,7 @@
formConfigData: createEmptyFormConfigData(),
enabled: true,
flowNodes: [createEmptyNode(1)],
+ storageBlobDTOs: [],
};
}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
index 1881f60..b63383f 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
@@ -219,10 +219,12 @@
placeholder="閫夊~"
style="width: 100%"
clearable
+ filterable
+ :loading="optionSourceLoading"
@change="emitOut"
>
<el-option
- v-for="o in field.options.filter((x) => x.value !== '' && x.value != null)"
+ v-for="o in resolvedSelectOptions(field)"
:key="String(o.value)"
:label="o.label || o.value"
:value="o.value"
@@ -231,28 +233,52 @@
</div>
<div v-if="field.type === 'select'" class="fce-section fce-section--options">
- <div class="fce-options-head">
- <span class="fce-section-title">涓嬫媺閫夐」</span>
- <el-button type="primary" link size="small" :icon="Plus" @click="addOption(field)">
- 娣诲姞閫夐」
- </el-button>
- </div>
- <div
- v-for="(opt, oi) in field.options"
- :key="oi"
- class="fce-option-row"
- >
- <span class="fce-option-index">{{ oi + 1 }}</span>
- <el-input v-model="opt.label" placeholder="鏄剧ず鏂囨湰" @input="emitOut" />
- <el-input v-model="opt.value" placeholder="閫夐」鍊�" class="fce-option-value" @input="emitOut" />
- <el-button
- type="danger"
- link
- :icon="Delete"
- :disabled="field.options.length <= 1"
- @click="removeOption(field, oi)"
- />
- </div>
+ <span class="fce-section-title">涓嬫媺閫夐」</span>
+ <el-row :gutter="16" class="fce-source-row">
+ <el-col :span="12">
+ <el-form-item label="閫夐」鏉ユ簮" class="fce-field-item">
+ <el-select
+ v-model="field.optionSource"
+ style="width: 100%"
+ @change="onOptionSourceChange(field)"
+ >
+ <el-option
+ v-for="s in SELECT_OPTION_SOURCE_OPTIONS"
+ :key="s.value"
+ :label="s.label"
+ :value="s.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <p v-if="isDynamicOptionSource(field.optionSource)" class="fce-source-tip">
+ {{ optionSourceDesc(field.optionSource) }}銆傛彁浜ゅ鎵规椂灏嗚嚜鍔ㄥ姞杞芥渶鏂版暟鎹紝鏃犻渶鎵嬪姩缁存姢閫夐」銆�
+ </p>
+ <template v-if="!isDynamicOptionSource(field.optionSource)">
+ <div class="fce-options-head">
+ <span class="fce-section-subtitle">鎵嬪姩閫夐」</span>
+ <el-button type="primary" link size="small" :icon="Plus" @click="addOption(field)">
+ 娣诲姞閫夐」
+ </el-button>
+ </div>
+ <div
+ v-for="(opt, oi) in field.options"
+ :key="oi"
+ class="fce-option-row"
+ >
+ <span class="fce-option-index">{{ oi + 1 }}</span>
+ <el-input v-model="opt.label" placeholder="鏄剧ず鏂囨湰" @input="emitOut" />
+ <el-input v-model="opt.value" placeholder="閫夐」鍊�" class="fce-option-value" @input="emitOut" />
+ <el-button
+ type="danger"
+ link
+ :icon="Delete"
+ :disabled="field.options.length <= 1"
+ @click="removeOption(field, oi)"
+ />
+ </div>
+ </template>
</div>
</div>
</div>
@@ -271,6 +297,12 @@
createEmptyFormField,
formFieldTypeLabel,
} from "../formConfigUtils.js";
+import {
+ SELECT_OPTION_SOURCE,
+ SELECT_OPTION_SOURCE_OPTIONS,
+ isDynamicOptionSource,
+} from "../selectOptionSource.js";
+import { useSelectOptionSources } from "../useSelectOptionSources.js";
const props = defineProps({
modelValue: { type: Object, default: () => createEmptyFormConfigData() },
@@ -279,6 +311,8 @@
const emit = defineEmits(["update:modelValue"]);
const inner = reactive(createEmptyFormConfigData());
+
+const { loading: optionSourceLoading, ensureForFields, getOptions } = useSelectOptionSources();
function typeLabel(type) {
return formFieldTypeLabel(type);
@@ -289,6 +323,15 @@
return `閫夊~锛岄�夋嫨妯℃澘鏃跺皢棰勫~${name}`;
}
+function optionSourceDesc(source) {
+ return SELECT_OPTION_SOURCE_OPTIONS.find((x) => x.value === source)?.desc || "";
+}
+
+function resolvedSelectOptions(field) {
+ if (field.type !== "select") return [];
+ return getOptions(field);
+}
+
function syncFromProps(v) {
const src = v || createEmptyFormConfigData();
inner.summaryPlaceholder = src.summaryPlaceholder || "";
@@ -296,8 +339,10 @@
...createEmptyFormField(),
...f,
_uid: f._uid || createEmptyFormField()._uid,
+ optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC,
options: (f.options || [{ label: "", value: "" }]).map((o) => ({ ...o })),
}));
+ ensureForFields(inner.fields);
}
function emitOut() {
@@ -313,6 +358,7 @@
min: f.min,
precision: f.precision,
defaultValue: cloneDefaultValue(f),
+ optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC,
options: (f.options || []).map((o) => ({ label: o.label, value: o.value })),
})),
});
@@ -333,6 +379,7 @@
function addField() {
inner.fields.push(createEmptyFormField());
+ ensureForFields(inner.fields);
emitOut();
}
@@ -357,10 +404,23 @@
}
function onTypeChange(field) {
- if (field.type === "select" && (!field.options || !field.options.length)) {
- field.options = [{ label: "", value: "" }];
+ if (field.type === "select") {
+ if (!field.optionSource) field.optionSource = SELECT_OPTION_SOURCE.STATIC;
+ if (!field.options || !field.options.length) {
+ field.options = [{ label: "", value: "" }];
+ }
+ ensureForFields(inner.fields);
}
resetDefaultValueForType(field);
+ emitOut();
+}
+
+function onOptionSourceChange(field) {
+ field.defaultValue = "";
+ if (!isDynamicOptionSource(field.optionSource) && (!field.options || !field.options.length)) {
+ field.options = [{ label: "", value: "" }];
+ }
+ ensureForFields(inner.fields);
emitOut();
}
@@ -585,10 +645,28 @@
margin-bottom: 10px;
}
-.fce-options-head .fce-section-title {
+.fce-options-head .fce-section-title,
+.fce-options-head .fce-section-subtitle {
margin-bottom: 0;
}
+.fce-section-subtitle {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--el-text-color-secondary);
+}
+
+.fce-source-row {
+ margin-bottom: 4px;
+}
+
+.fce-source-tip {
+ margin: 0 0 10px;
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ line-height: 1.5;
+}
+
.fce-option-row {
display: flex;
align-items: center;
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js
index 733f463..c1f66bd 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js
@@ -1,3 +1,12 @@
+import { mapAttachmentsFromApi } from "./approveTemplateConstants.js";
+import {
+ isDynamicOptionSource,
+ SELECT_OPTION_SOURCE,
+ selectOptionSourceLabel,
+} from "./selectOptionSource.js";
+
+export { selectOptionSourceLabel };
+
/** 濉姤椤圭被鍨嬶紙涓庡鎵规彁浜ら〉 field.type 涓�鑷达級 */
export const FORM_FIELD_TYPE_OPTIONS = [
{ value: "text", label: "鍗曡鏂囨湰" },
@@ -67,6 +76,7 @@
min: 0,
precision: 0,
defaultValue: "",
+ optionSource: SELECT_OPTION_SOURCE.STATIC,
options: [{ label: "", value: "" }],
};
}
@@ -154,6 +164,7 @@
min: f.min ?? 0,
precision: f.precision ?? 0,
defaultValue: normalizeDefaultValueFromApi(f),
+ optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC,
options: (f.options || []).length
? f.options.map((o) => ({ label: o.label || "", value: o.value ?? "" }))
: [{ label: "", value: "" }],
@@ -180,9 +191,13 @@
item.precision = f.precision ?? 0;
}
if (item.type === "select") {
- item.options = (f.options || [])
- .filter((o) => (o.label || "").trim() || o.value !== "" && o.value != null)
- .map((o) => ({ label: (o.label || "").trim(), value: o.value }));
+ const source = f.optionSource || SELECT_OPTION_SOURCE.STATIC;
+ item.optionSource = source;
+ if (!isDynamicOptionSource(source)) {
+ item.options = (f.options || [])
+ .filter((o) => (o.label || "").trim() || (o.value !== "" && o.value != null))
+ .map((o) => ({ label: (o.label || "").trim(), value: o.value }));
+ }
}
if (hasDefaultValue(f)) {
item.defaultValue =
@@ -223,6 +238,8 @@
if (keys.has(key)) return { ok: false, message: `瀛楁鏍囪瘑銆�${key}銆嶉噸澶嶏紝璇蜂慨鏀筦 };
keys.add(key);
if (f.type === "select") {
+ const source = f.optionSource || SELECT_OPTION_SOURCE.STATIC;
+ if (isDynamicOptionSource(source)) continue;
const opts = (f.options || []).filter((o) => (o.label || "").trim() && o.value !== "" && o.value != null);
if (!opts.length) return { ok: false, message: `璇蜂负銆�${label}銆嶉厤缃嚦灏戜竴涓笅鎷夐�夐」` };
}
@@ -241,13 +258,16 @@
return dv.length === 2 ? `${dv[0]} ~ ${dv[1]}` : "鈥�";
}
if (field?.type === "select") {
+ if (isDynamicOptionSource(field.optionSource)) {
+ return `${selectOptionSourceLabel(field.optionSource)}锛�${String(dv)}`;
+ }
const opt = (field.options || []).find((o) => String(o.value) === String(dv));
return opt?.label || String(dv);
}
return String(dv);
}
-/** 灏嗗悗绔ā鏉胯杞负鎻愪氦椤垫ā鏉跨粨鏋勶紙鍚� fields 榛樿鍊硷級 */
+/** 灏嗗悗绔ā鏉胯杞负鎻愪氦椤垫ā鏉跨粨鏋勶紙鍚� fields 榛樿鍊笺�侀檮浠讹級 */
export function buildSubmitTemplateFromRow(row) {
const cfg = parseFormConfigToData(row?.formConfig);
const fields = (cfg.fields || []).map(({ _uid, ...rest }) => ({
@@ -260,6 +280,7 @@
min: rest.min,
precision: rest.precision,
defaultValue: rest.defaultValue,
+ optionSource: rest.optionSource,
options: rest.options,
}));
return {
@@ -269,6 +290,7 @@
summaryPlaceholder: cfg.summaryPlaceholder || "",
approvalMode: cfg.approvalMode || "parallel",
fields,
+ storageBlobDTOs: mapAttachmentsFromApi(row),
};
}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
index 5bc65dd..59850e2 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
@@ -196,6 +196,18 @@
</el-form-item>
+ <el-form-item label="闄勪欢">
+
+ <div class="upload-block">
+
+ <FileUpload v-model:file-list="form.storageBlobDTOs" :limit="10" button-text="鐐瑰嚮閫夋嫨鏂囦欢" />
+
+ </div>
+
+ <p class="flow-tip">鍙笂浼犳ā鏉胯鏄庢枃妗c�佸埗搴︽枃浠剁瓑锛堥�夊~锛夈��</p>
+
+ </el-form-item>
+
</el-form>
<template #footer>
@@ -274,6 +286,16 @@
</el-table-column>
+ <el-table-column label="閫夐」鏉ユ簮" width="100">
+
+ <template #default="{ row }">
+
+ {{ row.type === 'select' ? selectOptionSourceLabel(row.optionSource) : '鈥�' }}
+
+ </template>
+
+ </el-table-column>
+
<el-table-column label="蹇呭~" width="70" align="center">
<template #default="{ row }">{{ row.required !== false ? "鏄�" : "鍚�" }}</template>
@@ -338,6 +360,32 @@
<el-empty v-else description="鏆傛棤娴佺▼鑺傜偣" :image-size="60" />
+ <el-divider content-position="left">闄勪欢锛坽{ detailAttachments.length }} 涓級</el-divider>
+
+ <template v-if="detailAttachments.length">
+
+ <el-tag
+
+ v-for="(f, i) in detailAttachments"
+
+ :key="i"
+
+ class="detail-attachment-tag"
+
+ type="info"
+
+ effect="plain"
+
+ >
+
+ {{ attachmentDisplayName(f) }}
+
+ </el-tag>
+
+ </template>
+
+ <el-empty v-else description="鏆傛棤闄勪欢" :image-size="48" />
+
</div>
<template #footer>
@@ -366,13 +414,16 @@
import { userListNoPageByTenantId } from "@/api/system/user.js";
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+
import FormConfigEditor from "./components/FormConfigEditor.vue";
import TemplateFlowEditor from "./components/TemplateFlowEditor.vue";
-import { formatDisplayTime } from "./approveTemplateConstants.js";
+import { formatDisplayTime, mapAttachmentsFromApi } from "./approveTemplateConstants.js";
import { formatDefaultValueDisplay, formFieldTypeLabel, parseFormConfigToData } from "./formConfigUtils.js";
+import { selectOptionSourceLabel } from "./selectOptionSource.js";
import { useApproveTemplate } from "./useApproveTemplate.js";
@@ -441,6 +492,20 @@
parseFormConfigToData(detailRow.value?.formConfigData ?? detailRow.value?.formConfig)
);
+
+
+
+const detailAttachments = computed(() => mapAttachmentsFromApi(detailRow.value));
+
+
+
+function attachmentDisplayName(file) {
+
+ if (!file) return "鏈懡鍚�";
+
+ return file.name || file.originalFilename || file.fileName || "鏈懡鍚�";
+
+}
@@ -706,6 +771,18 @@
}
+.upload-block {
+
+ width: 100%;
+
+}
+
+.detail-attachment-tag {
+
+ margin: 0 8px 8px 0;
+
+}
+
.text-muted {
font-size: 12px;
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/selectOptionSource.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/selectOptionSource.js
new file mode 100644
index 0000000..99706b4
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/selectOptionSource.js
@@ -0,0 +1,140 @@
+import { deptTreeSelect, userListNoPageByTenantId } from "@/api/system/user.js";
+
+/** 涓嬫媺閫夐」鏉ユ簮锛堝啓鍏� formConfig锛屾彁浜ら〉鎸夋潵婧愭媺鍙栨暟鎹級 */
+export const SELECT_OPTION_SOURCE = {
+ STATIC: "static",
+ USER: "user",
+ DEPT: "dept",
+};
+
+export const SELECT_OPTION_SOURCE_OPTIONS = [
+ { value: SELECT_OPTION_SOURCE.STATIC, label: "鎵嬪姩閰嶇疆", desc: "鍦ㄦā鏉夸腑鑷畾涔夐�夐」鏂囨湰涓庡��" },
+ { value: SELECT_OPTION_SOURCE.USER, label: "浜哄憳鍒楄〃", desc: "浠庣郴缁熺敤鎴蜂腑閫夋嫨锛屽�间负鐢ㄦ埛 ID" },
+ { value: SELECT_OPTION_SOURCE.DEPT, label: "閮ㄩ棬鍒楄〃", desc: "浠庣粍缁囨灦鏋勪腑閫夋嫨锛屽�间负閮ㄩ棬 ID" },
+];
+
+export function selectOptionSourceLabel(source) {
+ return SELECT_OPTION_SOURCE_OPTIONS.find((x) => x.value === source)?.label || "鈥�";
+}
+
+export function isDynamicOptionSource(source) {
+ return source === SELECT_OPTION_SOURCE.USER || source === SELECT_OPTION_SOURCE.DEPT;
+}
+
+function unwrapArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (payload?.data && Array.isArray(payload.data)) return payload.data;
+ if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
+ return [];
+}
+
+function isActiveUser(u) {
+ if (u.delFlag === "2" || u.delFlag === 2) return false;
+ if (u.status == null) return true;
+ return String(u.status) === "0";
+}
+
+/** 鐢ㄦ埛 鈫� 涓嬫媺 option */
+export function mapUserToSelectOption(u) {
+ const value = u.userId ?? u.id;
+ return {
+ label: u.nickName || u.userName || `鐢ㄦ埛${value}`,
+ value,
+ };
+}
+
+/** 閮ㄩ棬鏍戞媿骞充负涓嬫媺 option */
+export function flattenDeptToSelectOptions(nodes, result = []) {
+ (nodes || []).forEach((node) => {
+ const value = node.id ?? node.deptId ?? node.value;
+ if (value != null && value !== "") {
+ result.push({
+ label: node.label ?? node.deptName ?? node.name ?? String(value),
+ value,
+ });
+ }
+ if (node.children?.length) flattenDeptToSelectOptions(node.children, result);
+ });
+ return result;
+}
+
+function filterDisabledDept(deptList) {
+ if (!Array.isArray(deptList)) return [];
+ return deptList.filter((dept) => {
+ if (dept.disabled) return false;
+ if (dept.children?.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+}
+
+/** 鎸夊瓧娈甸厤缃В鏋愪笅鎷� options锛堥渶浼犲叆宸插姞杞界殑缂撳瓨锛� */
+export function resolveFieldSelectOptions(field, caches = {}) {
+ const source = field?.optionSource || SELECT_OPTION_SOURCE.STATIC;
+ if (source === SELECT_OPTION_SOURCE.USER) {
+ return (caches.users || []).map(mapUserToSelectOption);
+ }
+ if (source === SELECT_OPTION_SOURCE.DEPT) {
+ return caches.deptOptions || [];
+ }
+ return (field?.options || []).filter((o) => o.value !== "" && o.value != null);
+}
+
+/** 鏍规嵁宸茶В鏋愮殑 options 鍙嶆煡灞曠ず鏂囨湰 */
+export function resolveSelectDisplayLabel(field, val, caches = {}) {
+ if (val == null || val === "") return "鈥�";
+ const options = resolveFieldSelectOptions(field, caches);
+ const hit = options.find((o) => String(o.value) === String(val));
+ return hit?.label || String(val);
+}
+
+/** 鍔犺浇浜哄憳 / 閮ㄩ棬缂撳瓨锛堝澶勫鐢級 */
+export async function fetchSelectOptionCaches(sources = []) {
+ const needUser = sources.includes(SELECT_OPTION_SOURCE.USER);
+ const needDept = sources.includes(SELECT_OPTION_SOURCE.DEPT);
+ const caches = { users: [], deptOptions: [] };
+
+ if (!needUser && !needDept) return caches;
+
+ const tasks = [];
+ if (needUser) {
+ tasks.push(
+ userListNoPageByTenantId()
+ .then((res) => {
+ caches.users = unwrapArray(res).filter(isActiveUser);
+ })
+ .catch(() => {
+ caches.users = [];
+ })
+ );
+ }
+ if (needDept) {
+ tasks.push(
+ deptTreeSelect()
+ .then((res) => {
+ let tree = unwrapArray(res);
+ tree = tree.length ? filterDisabledDept(JSON.parse(JSON.stringify(tree))) : [];
+ if (!tree.length) tree = unwrapArray(res);
+ caches.deptOptions = flattenDeptToSelectOptions(tree);
+ })
+ .catch(() => {
+ caches.deptOptions = [];
+ })
+ );
+ }
+
+ await Promise.all(tasks);
+ return caches;
+}
+
+/** 浠庡瓧娈靛垪琛ㄦ敹闆嗛渶瑕侀鍔犺浇鐨勫姩鎬佹潵婧� */
+export function collectOptionSourcesFromFields(fields) {
+ const set = new Set();
+ (fields || []).forEach((f) => {
+ if (f?.type === "select" && isDynamicOptionSource(f.optionSource)) {
+ set.add(f.optionSource);
+ }
+ });
+ return [...set];
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
index 95eac7b..4b2abe4 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
@@ -210,6 +210,7 @@
),
enabled: row.enabled !== false,
flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [base.flowNodes[0]])),
+ storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])),
});
}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/useSelectOptionSources.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/useSelectOptionSources.js
new file mode 100644
index 0000000..8397288
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/useSelectOptionSources.js
@@ -0,0 +1,45 @@
+import { reactive, ref } from "vue";
+import {
+ collectOptionSourcesFromFields,
+ fetchSelectOptionCaches,
+ resolveFieldSelectOptions,
+ resolveSelectDisplayLabel,
+} from "./selectOptionSource.js";
+
+/** 涓嬫媺鍔ㄦ�侀�夐」锛氫汉鍛� / 閮ㄩ棬缂撳瓨涓庤В鏋� */
+export function useSelectOptionSources() {
+ const loading = ref(false);
+ const caches = reactive({
+ users: [],
+ deptOptions: [],
+ });
+
+ async function ensureForFields(fields) {
+ const sources = collectOptionSourcesFromFields(fields);
+ if (!sources.length) return;
+ loading.value = true;
+ try {
+ const next = await fetchSelectOptionCaches(sources);
+ caches.users = next.users;
+ caches.deptOptions = next.deptOptions;
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ function getOptions(field) {
+ return resolveFieldSelectOptions(field, caches);
+ }
+
+ function getDisplayLabel(field, val) {
+ return resolveSelectDisplayLabel(field, val, caches);
+ }
+
+ return {
+ loading,
+ caches,
+ ensureForFields,
+ getOptions,
+ getDisplayLabel,
+ };
+}
diff --git a/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue b/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
index b95b6e7..a807e6b 100644
--- a/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
+++ b/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
@@ -28,7 +28,7 @@
<el-button @click="resetSearch">閲嶇疆</el-button>
</div>
<div>
- <el-button type="primary" @click="openFormDialog('add')">鏂板杞鐢宠</el-button>
+ <el-button type="primary" @click="openAddWithTemplate">鏂板杞鐢宠</el-button>
</div>
</div>
<div class="table_list">
@@ -87,7 +87,7 @@
/>
</el-form-item>
</el-col>
- <el-col :span="12">
+ <el-col v-if="!form.hasTemplateBinding" :span="12">
<el-form-item label="瀹℃壒鏂瑰紡" prop="approvalMode">
<el-radio-group v-model="form.approvalMode">
<el-radio value="parallel">涓庣</el-radio>
@@ -96,7 +96,20 @@
</el-form-item>
</el-col>
</el-row>
- <el-row :gutter="24">
+ <template v-if="form.hasTemplateBinding">
+ <ApprovalTemplateFormSection
+ :active-template="form.templateSnapshot"
+ :fields="form.formFieldDefs"
+ :form-payload="form.formPayload"
+ v-model:flow-nodes="form.flowNodes"
+ v-model:attachments="form.storageBlobDTOs"
+ :template-attachments="form.templateAttachments"
+ :user-options="flowUserOptions"
+ :allow-change-template="formDialog.mode === 'add'"
+ @change-template="reopenTemplateBind"
+ />
+ </template>
+ <el-row v-else :gutter="24">
<el-col :span="24">
<el-form-item label="瀹℃壒浜�" prop="approverIds">
<el-tree-select
@@ -129,7 +142,7 @@
</el-form-item>
</el-col>
</el-row>
- <el-row :gutter="24">
+ <el-row v-if="!form.hasTemplateBinding" :gutter="24">
<el-col :span="24">
<el-form-item label="闄勪欢">
<div class="upload-block">
@@ -146,6 +159,12 @@
</div>
</template>
</el-dialog>
+
+ <ApprovalTemplateBindDialog
+ v-model:visible="templateBindVisible"
+ :module-key="APPROVAL_MODULE_KEYS.REGULAR"
+ @confirm="onTemplateBound"
+ />
<!-- 璇︽儏锛堝彧璇伙級 -->
<el-dialog v-model="detailDialog.visible" title="杞鐢宠璇︽儏" width="640px" append-to-body>
@@ -204,6 +223,14 @@
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import { deptTreeSelect, userListNoPageByTenantId } from "@/api/system/user.js";
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
+import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
+import ApprovalTemplateFormSection from "../../ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue";
+import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
+import {
+ applyBindingToForm,
+ buildFormPayloadRules,
+} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js";
+import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
/** 涓庡悗绔害瀹氬瓧娈碉紙鍗犱綅锛� */
const createEmptyForm = () => ({
@@ -216,6 +243,15 @@
approverIds: [],
approverNames: "",
attachmentList: [],
+ hasTemplateBinding: false,
+ templateId: "",
+ templateName: "",
+ templateSnapshot: null,
+ formFieldDefs: [],
+ formPayload: {},
+ flowNodes: [],
+ templateAttachments: [],
+ storageBlobDTOs: [],
});
const { proxy } = getCurrentInstance();
@@ -511,22 +547,27 @@
});
const formRef = ref();
const form = reactive(createEmptyForm());
+const templateBindVisible = ref(false);
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
-const formRules = {
- applicantName: [{ required: true, message: "璇疯緭鍏ョ敵璇蜂汉", trigger: "blur" }],
- applyDate: [{ required: true, message: "璇烽�夋嫨鐢宠鏃ユ湡", trigger: "change" }],
- regularizationDate: [{ required: true, message: "璇烽�夋嫨杞鏃ユ湡", trigger: "change" }],
- probationSummary: [{ required: true, message: "璇峰~鍐欒瘯鐢ㄦ湡宸ヤ綔鎬荤粨", trigger: "blur" }],
- approvalMode: [{ required: true, message: "璇烽�夋嫨瀹℃壒鏂瑰紡", trigger: "change" }],
- approverIds: [
- {
- type: "array",
- required: true,
- message: "璇烽�夋嫨瀹℃壒浜�",
- trigger: "change",
- },
- ],
-};
+const formRules = computed(() => {
+ const base = {
+ applicantName: [{ required: true, message: "璇疯緭鍏ョ敵璇蜂汉", trigger: "blur" }],
+ applyDate: [{ required: true, message: "璇烽�夋嫨鐢宠鏃ユ湡", trigger: "change" }],
+ regularizationDate: [{ required: true, message: "璇烽�夋嫨杞鏃ユ湡", trigger: "change" }],
+ probationSummary: [{ required: true, message: "璇峰~鍐欒瘯鐢ㄦ湡宸ヤ綔鎬荤粨", trigger: "blur" }],
+ };
+ if (form.hasTemplateBinding) {
+ return { ...base, ...buildFormPayloadRules(form.formFieldDefs) };
+ }
+ return {
+ ...base,
+ approvalMode: [{ required: true, message: "璇烽�夋嫨瀹℃壒鏂瑰紡", trigger: "change" }],
+ approverIds: [
+ { type: "array", required: true, message: "璇烽�夋嫨瀹℃壒浜�", trigger: "change" },
+ ],
+ };
+});
const detailDialog = reactive({ visible: false });
const detailRow = ref({});
@@ -569,6 +610,27 @@
return;
}
proxy?.$modal?.msgSuccess?.(`宸叉ā鎷熶笅杞斤細${row.name}`);
+}
+
+function openAddWithTemplate() {
+ templateBindVisible.value = true;
+}
+
+function onTemplateBound(binding) {
+ Object.assign(form, createEmptyForm());
+ applyBindingToForm(form, binding);
+ form.hasTemplateBinding = true;
+ formDialog.mode = "add";
+ formDialog.title = "鏂板杞鐢宠";
+ loadApproverTree();
+ loadFlowUsers();
+ formDialog.visible = true;
+ nextTick(() => formRef.value?.clearValidate?.());
+}
+
+function reopenTemplateBind() {
+ formDialog.visible = false;
+ templateBindVisible.value = true;
}
function openFormDialog(mode, row) {
@@ -637,6 +699,7 @@
onMounted(() => {
loadApproverTree();
+ loadFlowUsers();
});
</script>
--
Gitblit v1.9.3