| | |
| | | |
| | | <!-- 新增 / 编辑 --> |
| | | <el-dialog |
| | | v-if="formDialog.visible" |
| | | v-model="formDialog.visible" |
| | | :title="formDialog.title" |
| | | width="960px" |
| | |
| | | @closed="onFormClosed" |
| | | > |
| | | <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" class="regular-apply-form"> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="申请人" prop="applicantName"> |
| | | <el-input v-model="form.applicantName" placeholder="请输入申请人" maxlength="50" show-word-limit /> |
| | | <el-form-item v-if="form.templateSnapshot" label="审批模板"> |
| | | <span class="template-name">{{ form.templateSnapshot.label || form.templateName }}</span> |
| | | <el-button |
| | | v-if="formDialog.mode === 'add'" |
| | | type="primary" |
| | | link |
| | | class="ml12" |
| | | @click="reopenTemplateBind" |
| | | > |
| | | 更换模板 |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="申请日期" prop="applyDate"> |
| | | <el-date-picker |
| | | v-model="form.applyDate" |
| | | type="date" |
| | | placeholder="请选择申请日期" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="转正日期" prop="regularizationDate"> |
| | | <el-date-picker |
| | | v-model="form.regularizationDate" |
| | | type="date" |
| | | placeholder="请选择转正日期" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <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-radio value="countersign">会签</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <template v-if="form.hasTemplateBinding"> |
| | | <FormPayloadFields :fields="form.formFieldDefs" :form-payload="form.formPayload" /> |
| | | <ApprovalTemplateFormSection |
| | | :active-template="form.templateSnapshot" |
| | | :fields="form.formFieldDefs" |
| | |
| | | v-model:attachments="form.storageBlobDTOs" |
| | | :template-attachments="form.templateAttachments" |
| | | :user-options="flowUserOptions" |
| | | :allow-change-template="formDialog.mode === 'add'" |
| | | @change-template="reopenTemplateBind" |
| | | flow-attachments-only |
| | | hide-template-name |
| | | :allow-change-template="false" |
| | | /> |
| | | </template> |
| | | <el-row v-else :gutter="24"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="审批人" prop="approverIds"> |
| | | <el-tree-select |
| | | v-model="form.approverIds" |
| | | :data="approverTreeData" |
| | | multiple |
| | | collapse-tags |
| | | collapse-tags-tooltip |
| | | :max-collapse-tags="2" |
| | | :render-after-expand="false" |
| | | placeholder="请选择审批人(可多选)" |
| | | style="width: 100%" |
| | | :props="{ value: 'id', label: 'label', children: 'children', disabled: 'disabled' }" |
| | | check-strictly |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="试用期工作总结" prop="probationSummary"> |
| | | <el-input |
| | | v-model="form.probationSummary" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="请填写试用期工作总结" |
| | | maxlength="2000" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row v-if="!form.hasTemplateBinding" :gutter="24"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="附件"> |
| | | <div class="upload-block"> |
| | | <FileUpload v-model:file-list="form.attachmentList" :limit="10" button-text="点击选择文件" /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | |
| | | <ApprovalTemplateBindDialog |
| | | v-model:visible="templateBindVisible" |
| | | :module-key="APPROVAL_MODULE_KEYS.REGULAR" |
| | | skip-form-confirm |
| | | @confirm="onTemplateBound" |
| | | @closed="onTemplateBindClosed" |
| | | /> |
| | | |
| | | <!-- 详情(只读) --> |
| | |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | 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 FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.vue"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; |
| | | import { |
| | | applyBindingToForm, |
| | | buildFormPayloadRules, |
| | | validateTemplateBinding, |
| | | } from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js"; |
| | | import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js"; |
| | | |
| | |
| | | applyDate: "", |
| | | regularizationDate: "", |
| | | probationSummary: "", |
| | | approvalMode: "parallel", |
| | | approverIds: [], |
| | | approverNames: "", |
| | | attachmentList: [], |
| | | hasTemplateBinding: false, |
| | | templateId: "", |
| | | templateName: "", |
| | |
| | | }); |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | /** 审批人树:部门树 + 系统用户(与 staff-archive / user-manage 同源接口) */ |
| | | const approverTreeData = ref([]); |
| | | const approverLabelMap = ref({}); |
| | | |
| | | /** 接口返回统一拆成数组(兼容 axios 拦截器已解包为 { data } 或直接数组等情况) */ |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | | if (payload && Array.isArray(payload.data)) return payload.data; |
| | | if (payload && Array.isArray(payload.rows)) return payload.rows; |
| | | return []; |
| | | } |
| | | |
| | | 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; |
| | | }); |
| | | } |
| | | |
| | | function getUserDeptId(u) { |
| | | return ( |
| | | u.deptId ?? |
| | | u.sysDeptId ?? |
| | | u.dept?.deptId ?? |
| | | u.dept?.id ?? |
| | | u.dept_id |
| | | ); |
| | | } |
| | | |
| | | /** 部门树节点主键(若依一般为 id,部分场景为 value) */ |
| | | function getDeptNodeKey(node) { |
| | | const k = node?.id ?? node?.value ?? node?.deptId; |
| | | if (k == null || k === "") return null; |
| | | return k; |
| | | } |
| | | |
| | | function isActiveUser(u) { |
| | | if (u.delFlag === "2" || u.delFlag === 2) return false; |
| | | if (u.status == null) return true; |
| | | return String(u.status) === "0"; |
| | | } |
| | | |
| | | function userToTreeLeaf(u) { |
| | | return { |
| | | id: String(u.userId ?? u.id), |
| | | label: u.nickName || u.userName || `用户${u.userId ?? u.id}`, |
| | | }; |
| | | } |
| | | |
| | | /** 按部门 id 分组;无部门或 id 为 0 的用户进入未分配列表 */ |
| | | function buildUsersByDeptId(users) { |
| | | const map = new Map(); |
| | | const unassigned = []; |
| | | for (const u of users) { |
| | | if (!isActiveUser(u)) continue; |
| | | const did = getUserDeptId(u); |
| | | if (did == null || did === "" || did === 0 || did === "0") { |
| | | unassigned.push(u); |
| | | continue; |
| | | } |
| | | const k = String(did); |
| | | if (!map.has(k)) map.set(k, []); |
| | | map.get(k).push(u); |
| | | } |
| | | return { map, unassigned }; |
| | | } |
| | | |
| | | function collectUserLabels(nodes, map) { |
| | | (nodes || []).forEach((n) => { |
| | | if (n.children?.length) { |
| | | collectUserLabels(n.children, map); |
| | | } else if (n.id != null && !String(n.id).startsWith("dept_")) { |
| | | map[String(n.id)] = n.label; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** 部门节点 id 加前缀,避免与 userId 数值冲突;可选节点为真实 userId 字符串 */ |
| | | function mergeDeptTreeWithUsers(nodes, usersByDept) { |
| | | if (!Array.isArray(nodes)) return []; |
| | | const out = []; |
| | | for (const node of nodes) { |
| | | const deptIdRaw = getDeptNodeKey(node); |
| | | if (deptIdRaw == null) continue; |
| | | const sub = mergeDeptTreeWithUsers(node.children || [], usersByDept); |
| | | const usersHere = usersByDept.get(String(deptIdRaw)) || []; |
| | | const userChildren = usersHere.map(userToTreeLeaf); |
| | | const children = [...sub, ...userChildren]; |
| | | if (!children.length) continue; |
| | | out.push({ |
| | | id: `dept_${deptIdRaw}`, |
| | | label: node.label ?? node.deptName ?? "部门", |
| | | disabled: true, |
| | | children, |
| | | }); |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | function buildFlatApproverTree(users) { |
| | | const list = users.filter(isActiveUser).map(userToTreeLeaf); |
| | | if (!list.length) return []; |
| | | return [ |
| | | { |
| | | id: "dept_all_users", |
| | | label: "系统用户", |
| | | disabled: true, |
| | | children: list, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | async function loadApproverTree() { |
| | | try { |
| | | const [deptRes, userRes] = await Promise.all([deptTreeSelect(), userListNoPageByTenantId()]); |
| | | let rawTree = unwrapArray(deptRes); |
| | | rawTree = rawTree.length ? JSON.parse(JSON.stringify(rawTree)) : []; |
| | | let deptTree = filterDisabledDept(JSON.parse(JSON.stringify(rawTree))); |
| | | if (!deptTree.length && rawTree.length) { |
| | | deptTree = JSON.parse(JSON.stringify(rawTree)); |
| | | } |
| | | const users = unwrapArray(userRes); |
| | | const { map: usersByDept, unassigned } = buildUsersByDeptId(users); |
| | | let merged = mergeDeptTreeWithUsers(deptTree, usersByDept); |
| | | if (unassigned.length) { |
| | | merged.push({ |
| | | id: "dept_unassigned", |
| | | label: "未分配部门", |
| | | disabled: true, |
| | | children: unassigned.map(userToTreeLeaf), |
| | | }); |
| | | } |
| | | if (!merged.length && users.length) { |
| | | merged = buildFlatApproverTree(users); |
| | | } |
| | | approverTreeData.value = merged; |
| | | const map = {}; |
| | | collectUserLabels(merged, map); |
| | | approverLabelMap.value = map; |
| | | } catch { |
| | | approverTreeData.value = []; |
| | | approverLabelMap.value = {}; |
| | | proxy?.$modal?.msgWarning?.("审批人数据加载失败,请检查网络或稍后重试"); |
| | | } |
| | | } |
| | | |
| | | function resolveApproverNames(ids) { |
| | | if (!ids?.length) return ""; |
| | | const map = approverLabelMap.value; |
| | | return ids.map((id) => map[String(id)] || id).join("、"); |
| | | } |
| | | |
| | | function approvalModeLabel(mode) { |
| | | if (mode === "countersign") return "会签"; |
| | |
| | | const formRef = ref(); |
| | | const form = reactive(createEmptyForm()); |
| | | const templateBindVisible = ref(false); |
| | | const pendingTemplateBinding = ref(null); |
| | | const { flowUserOptions, loadFlowUsers } = useFlowUserOptions(); |
| | | |
| | | 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 formRules = computed(() => buildFormPayloadRules(form.formFieldDefs)); |
| | | |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailRow = ref({}); |
| | |
| | | } |
| | | |
| | | function openAddWithTemplate() { |
| | | formDialog.visible = false; |
| | | pendingTemplateBinding.value = null; |
| | | templateBindVisible.value = true; |
| | | } |
| | | |
| | | function onTemplateBound(binding) { |
| | | pendingTemplateBinding.value = binding; |
| | | } |
| | | |
| | | function onTemplateBindClosed() { |
| | | const binding = pendingTemplateBinding.value; |
| | | if (!binding) return; |
| | | pendingTemplateBinding.value = null; |
| | | openFormWithBinding(binding); |
| | | } |
| | | |
| | | function openFormWithBinding(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; |
| | | pendingTemplateBinding.value = null; |
| | | templateBindVisible.value = true; |
| | | } |
| | | |
| | | function openFormDialog(mode, row) { |
| | | if (mode === "edit" && row && !row.hasTemplateBinding) { |
| | | proxy?.$modal?.msgWarning?.("该记录为旧版数据,请重新通过模板发起申请"); |
| | | return; |
| | | } |
| | | formDialog.mode = mode; |
| | | formDialog.title = mode === "add" ? "新增转正申请" : "编辑转正申请"; |
| | | loadApproverTree(); |
| | | formDialog.title = "编辑转正申请"; |
| | | Object.assign(form, createEmptyForm()); |
| | | if (mode === "edit" && row) { |
| | | Object.assign(form, { |
| | |
| | | applyDate: row.applyDate, |
| | | regularizationDate: row.regularizationDate, |
| | | probationSummary: row.probationSummary, |
| | | approvalMode: row.approvalMode, |
| | | approverIds: (row.approverIds || []).map((id) => String(id)), |
| | | approverNames: row.approverNames, |
| | | attachmentList: JSON.parse(JSON.stringify(row.attachmentList || [])), |
| | | hasTemplateBinding: true, |
| | | templateId: row.templateId, |
| | | templateName: row.templateName, |
| | | templateSnapshot: row.templateSnapshot, |
| | | formFieldDefs: row.formFieldDefs || [], |
| | | formPayload: JSON.parse(JSON.stringify(row.formPayload || {})), |
| | | flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [])), |
| | | templateAttachments: JSON.parse(JSON.stringify(row.templateAttachments || [])), |
| | | storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])), |
| | | }); |
| | | loadFlowUsers(); |
| | | } |
| | | formDialog.visible = true; |
| | | nextTick(() => formRef.value?.clearValidate?.()); |
| | |
| | | } catch { |
| | | return; |
| | | } |
| | | form.approverNames = resolveApproverNames(form.approverIds); |
| | | const flowCheck = validateTemplateBinding({ flowNodes: form.flowNodes }); |
| | | if (!flowCheck.ok) { |
| | | proxy?.$modal?.msgWarning?.(flowCheck.message || "请完善审批流程"); |
| | | return; |
| | | } |
| | | form.flowNodes = flowCheck.nodes; |
| | | syncRegularFieldsFromPayload(); |
| | | const payload = { |
| | | applicantName: form.applicantName, |
| | | applyDate: form.applyDate, |
| | | regularizationDate: form.regularizationDate, |
| | | probationSummary: form.probationSummary, |
| | | approvalMode: form.approvalMode, |
| | | approverIds: [...form.approverIds], |
| | | approverNames: form.approverNames, |
| | | attachmentList: JSON.parse(JSON.stringify(form.attachmentList || [])), |
| | | hasTemplateBinding: true, |
| | | templateId: form.templateId, |
| | | templateName: form.templateName, |
| | | templateSnapshot: form.templateSnapshot, |
| | | formFieldDefs: form.formFieldDefs, |
| | | formPayload: JSON.parse(JSON.stringify(form.formPayload || {})), |
| | | flowNodes: JSON.parse(JSON.stringify(form.flowNodes || [])), |
| | | templateAttachments: JSON.parse(JSON.stringify(form.templateAttachments || [])), |
| | | storageBlobDTOs: JSON.parse(JSON.stringify(form.storageBlobDTOs || [])), |
| | | }; |
| | | if (formDialog.mode === "add") { |
| | | const id = `local_${Date.now()}`; |
| | |
| | | handleQuery(); |
| | | } |
| | | |
| | | /** 从模板填报项同步列表展示字段 */ |
| | | function syncRegularFieldsFromPayload() { |
| | | const defs = form.formFieldDefs || []; |
| | | const payload = form.formPayload || {}; |
| | | for (const f of defs) { |
| | | const label = String(f.label || ""); |
| | | const val = payload[f.key]; |
| | | if (label.includes("申请人") && !label.includes("日期")) { |
| | | form.applicantName = val != null && val !== "" ? String(val) : form.applicantName; |
| | | } |
| | | if (label.includes("申请日期") && f.type === "date") { |
| | | form.applyDate = val || ""; |
| | | } |
| | | if (label.includes("转正") && (label.includes("日期") || label.includes("时间")) && f.type === "date") { |
| | | form.regularizationDate = val || ""; |
| | | } |
| | | if (label.includes("试用期") || label.includes("工作总结")) { |
| | | form.probationSummary = val != null ? String(val) : ""; |
| | | } |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadApproverTree(); |
| | | loadFlowUsers(); |
| | | }); |
| | | </script> |
| | |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular); |
| | | } |
| | | .upload-block { |
| | | width: 100%; |
| | | } |
| | | .mr6 { |
| | | margin-right: 6px; |
| | | } |
| | |
| | | .regular-apply-form :deep(.el-form-item) { |
| | | margin-bottom: 18px; |
| | | } |
| | | .template-name { |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary); |
| | | } |
| | | .ml12 { |
| | | margin-left: 12px; |
| | | } |
| | | .regular-apply-form-dialog :deep(.el-dialog__body) { |
| | | padding-top: 12px; |
| | | } |