| | |
| | | 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 = [ |
| | |
| | | /** ååæ®µå±ç¤ºå¼ï¼è¯¦æ
åªè¯»ï¼ */ |
| | | 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); |
| | |
| | | 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; |
| | |
| | | formFieldDefs: fields, |
| | | formPayload, |
| | | flowNodes, |
| | | templateAttachments: initTemplateAttachmentsFromSnapshot(templateSnapshot), |
| | | storageBlobDTOs: row?.storageBlobDTOs?.length |
| | | ? JSON.parse(JSON.stringify(row.storageBlobDTOs)) |
| | | : [], |
| | | }; |
| | | } |
| | | |
| | |
| | | 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)) : []; |
| | | } |
| | |
| | | </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'" |
| | |
| | | :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({ |
| | |
| | | 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> |
| | | |
| | |
| | | å½åç±»åï¼{{ 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> |
| | |
| | | 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"; |
| | |
| | | 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(); |
| | |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadUsers(); |
| | | loadFlowUsers(); |
| | | handleQuery(); |
| | | }); |
| | | </script> |
| | |
| | | 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, |
| | |
| | | mapInstanceFromApi, |
| | | mapSubmitTemplateCard, |
| | | matchBusinessTypeValue, |
| | | validateSubmitFlowNodes, |
| | | unwrapInstancePage, |
| | | } from "./approveListConstants.js"; |
| | | |
| | |
| | | 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 }, |
| | |
| | | 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 { |
| | |
| | | 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) { |
| | |
| | | submitForm, |
| | | activeTemplate: activeTemplate.value, |
| | | userStore, |
| | | flowNodes: flowCheck.nodes, |
| | | flowNodes: bindingCheck.nodes, |
| | | }) |
| | | ); |
| | | submitDialog.visible = false; |
| | |
| | | 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) { |
| | |
| | | buildInstanceDto({ |
| | | submitForm, |
| | | activeTemplate: activeTemplate.value, |
| | | flowNodes: flowCheck.nodes, |
| | | flowNodes: bindingCheck.nodes, |
| | | existingRow: submitEditRow.value, |
| | | }) |
| | | ); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * åä¸å¡æ¨¡åä¸å®¡æ¹æ¨¡æ¿ç±»åçæ å°ï¼é
ç½®åå
¥å£ï¼ |
| | | * |
| | | * ä½¿ç¨æ¹å¼ï¼ |
| | | * 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; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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ï¼prop 为 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; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | ä¸å¡æ¨¡åãæ°å¢ãæ¶å¯¼å
¥å®¡æ¹æ¨¡æ¿ï¼åºå® 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- 模æ¿ç»å®è¡¨ååºï¼å¡«æ¥é¡¹ + å®¡æ¹æµç¨ + éä»¶ï¼é¡»æå¨å¤å± 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å®¡æ¹æ¨¡æ¿å¡çéæ©ï¼æ 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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=ä»
æ¬ç±»å模æ¿ï¼universal=éå
éç±»å |
| | | */ |
| | | 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, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 }; |
| | | } |
| | |
| | | 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 {}; |
| | |
| | | businessType: row.businessType ?? "", |
| | | formConfig: row.formConfig, |
| | | formConfigData: parseFormConfigToData(row.formConfig), |
| | | storageBlobDTOs: mapAttachmentsFromApi(row), |
| | | createdUser: row.createdUser, |
| | | createdUserName: row.createdUserName, |
| | | ...times, |
| | |
| | | }), |
| | | }; |
| | | if (templateId) dto.id = templateId; |
| | | const attachments = Array.isArray(form.storageBlobDTOs) ? form.storageBlobDTOs : []; |
| | | if (attachments.length) dto.storageBlobDTOs = attachments; |
| | | return dto; |
| | | } |
| | | |
| | |
| | | formConfigData: createEmptyFormConfigData(), |
| | | enabled: true, |
| | | flowNodes: [createEmptyNode(1)], |
| | | storageBlobDTOs: [], |
| | | }; |
| | | } |
| | | |
| | |
| | | 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" |
| | |
| | | </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> |
| | |
| | | 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() }, |
| | |
| | | const emit = defineEmits(["update:modelValue"]); |
| | | |
| | | const inner = reactive(createEmptyFormConfigData()); |
| | | |
| | | const { loading: optionSourceLoading, ensureForFields, getOptions } = useSelectOptionSources(); |
| | | |
| | | function typeLabel(type) { |
| | | return formFieldTypeLabel(type); |
| | |
| | | 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 || ""; |
| | |
| | | ...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() { |
| | |
| | | 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 })), |
| | | })), |
| | | }); |
| | |
| | | |
| | | function addField() { |
| | | inner.fields.push(createEmptyFormField()); |
| | | ensureForFields(inner.fields); |
| | | emitOut(); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | 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(); |
| | | } |
| | | |
| | |
| | | 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; |
| | |
| | | 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: "åè¡ææ¬" }, |
| | |
| | | min: 0, |
| | | precision: 0, |
| | | defaultValue: "", |
| | | optionSource: SELECT_OPTION_SOURCE.STATIC, |
| | | options: [{ label: "", value: "" }], |
| | | }; |
| | | } |
| | |
| | | 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: "" }], |
| | |
| | | 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 = |
| | |
| | | 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}ãé
ç½®è³å°ä¸ä¸ªä¸æé项` }; |
| | | } |
| | |
| | | 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 }) => ({ |
| | |
| | | min: rest.min, |
| | | precision: rest.precision, |
| | | defaultValue: rest.defaultValue, |
| | | optionSource: rest.optionSource, |
| | | options: rest.options, |
| | | })); |
| | | return { |
| | |
| | | summaryPlaceholder: cfg.summaryPlaceholder || "", |
| | | approvalMode: cfg.approvalMode || "parallel", |
| | | fields, |
| | | storageBlobDTOs: mapAttachmentsFromApi(row), |
| | | }; |
| | | } |
| | | |
| | |
| | | |
| | | </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">å¯ä¸ä¼ 模æ¿è¯´æææ¡£ãå¶åº¦æä»¶çï¼éå¡«ï¼ã</p> |
| | | |
| | | </el-form-item> |
| | | |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | |
| | | |
| | | </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> |
| | |
| | | |
| | | <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> |
| | |
| | | |
| | | 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"; |
| | | |
| | |
| | | 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 || "æªå½å"; |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | |
| | | |
| | | } |
| | | |
| | | .upload-block { |
| | | |
| | | width: 100%; |
| | | |
| | | } |
| | | |
| | | .detail-attachment-tag { |
| | | |
| | | margin: 0 8px 8px 0; |
| | | |
| | | } |
| | | |
| | | .text-muted { |
| | | |
| | | font-size: 12px; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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]; |
| | | } |
| | |
| | | ), |
| | | enabled: row.enabled !== false, |
| | | flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [base.flowNodes[0]])), |
| | | storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])), |
| | | }); |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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, |
| | | }; |
| | | } |
| | |
| | | <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"> |
| | |
| | | /> |
| | | </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> |
| | |
| | | </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 |
| | |
| | | </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"> |
| | |
| | | </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> |
| | |
| | | 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 = () => ({ |
| | |
| | | approverIds: [], |
| | | approverNames: "", |
| | | attachmentList: [], |
| | | hasTemplateBinding: false, |
| | | templateId: "", |
| | | templateName: "", |
| | | templateSnapshot: null, |
| | | formFieldDefs: [], |
| | | formPayload: {}, |
| | | flowNodes: [], |
| | | templateAttachments: [], |
| | | storageBlobDTOs: [], |
| | | }); |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | |
| | | }); |
| | | 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({}); |
| | |
| | | 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) { |
| | |
| | | |
| | | onMounted(() => { |
| | | loadApproverTree(); |
| | | loadFlowUsers(); |
| | | }); |
| | | </script> |
| | | |