| | |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | getApprovalTemplateDetail, |
| | | listApprovalTemplate, |
| | | TEMPLATE_TYPE_BUILTIN, |
| | | TEMPLATE_TYPE_CUSTOM, |
| | | } from "@/api/officeProcessAutomation/approvalTemplate.js"; |
| | | import { |
| | | approveApprovalInstance, |
| | | deleteApprovalInstance, |
| | | listApprovalInstancePage, |
| | | saveApprovalInstance, |
| | | updateApprovalInstance, |
| | | } from "@/api/officeProcessAutomation/approvalInstance.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { computed, reactive, ref, watch } from "vue"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { |
| | | formatDisplayTime, |
| | | mapEnabledFromApi, |
| | | mapTemplateFromApi, |
| | | unwrapTemplateDetail, |
| | | unwrapTemplateList, |
| | | } from "../approve-template/approveTemplateConstants.js"; |
| | | import { buildSubmitTemplateFromRow } from "../approve-template/formConfigUtils.js"; |
| | | import { |
| | | APPROVAL_TYPE_OPTIONS, |
| | | SUBMIT_TEMPLATES, |
| | | approvalModeLabel, |
| | | approvalStatusLabel, |
| | | approvalStatusTagType, |
| | | approvalTypeLabel, |
| | | buildApprovalInstanceListParams, |
| | | buildApproveInstanceDto, |
| | | buildEditFormFromInstanceRow, |
| | | buildInstanceDto, |
| | | clearLegacyApproveListStorage, |
| | | createEmptySubmitForm, |
| | | createInitialMockRows, |
| | | loadStoredRows, |
| | | saveStoredRows, |
| | | buildDefaultFlowNodes, |
| | | mapInstanceFromApi, |
| | | mapSubmitTemplateCard, |
| | | validateSubmitFlowNodes, |
| | | unwrapInstancePage, |
| | | } from "./approveListConstants.js"; |
| | | |
| | | function advanceFlow(row, result, opinion) { |
| | | const nodes = row.approvalFlowNodes || []; |
| | | const idx = row.currentNodeIndex ?? 0; |
| | | const node = nodes[idx]; |
| | | if (!node) return; |
| | | node.nodeStatus = result === "approved" ? "finish" : "error"; |
| | | node.approveOpinion = opinion || (result === "approved" ? "同意" : "驳回"); |
| | | node.approveTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | row.approvalRecords = row.approvalRecords || []; |
| | | row.approvalRecords.push({ |
| | | operatorName: node.approverName || "审批人", |
| | | result, |
| | | opinion: node.approveOpinion, |
| | | time: node.approveTime, |
| | | }); |
| | | if (result === "rejected") { |
| | | row.approvalStatus = "rejected"; |
| | | row.rejectReason = opinion || node.approveOpinion; |
| | | return; |
| | | } |
| | | const next = idx + 1; |
| | | if (next < nodes.length) { |
| | | row.currentNodeIndex = next; |
| | | nodes[next].nodeStatus = "process"; |
| | | row.approvalStatus = "pending"; |
| | | } else { |
| | | row.approvalStatus = "approved"; |
| | | row.rejectReason = ""; |
| | | } |
| | | } |
| | | |
| | | export function useApproveList() { |
| | | clearLegacyApproveListStorage(); |
| | | const userStore = useUserStore(); |
| | | const stored = loadStoredRows(); |
| | | const allRows = ref(stored?.length ? stored : createInitialMockRows()); |
| | | |
| | | const tableData = ref([]); |
| | | const submitTemplateCards = ref([]); |
| | | const submitTemplatesLoading = ref(false); |
| | | |
| | | const searchForm = reactive({ |
| | | approvalType: "", |
| | |
| | | |
| | | const approveDialog = reactive({ visible: false, row: null }); |
| | | const approveOpinion = ref(""); |
| | | const approveSubmitting = ref(false); |
| | | |
| | | const submitDialog = reactive({ visible: false, step: 1 }); |
| | | const submitDialog = reactive({ visible: false, step: 1, mode: "add" }); |
| | | const submitEditRow = ref(null); |
| | | const submitForm = reactive(createEmptySubmitForm("")); |
| | | const submitFormRef = ref(); |
| | | const submitSaving = ref(false); |
| | | |
| | | const filteredList = computed(() => { |
| | | let list = [...allRows.value]; |
| | | if (searchForm.approvalType) { |
| | | list = list.filter((r) => r.approvalType === searchForm.approvalType); |
| | | const isSubmitEdit = computed(() => submitDialog.mode === "edit"); |
| | | const submitDialogTitle = computed(() => { |
| | | if (submitDialog.mode === "edit") { |
| | | return `修改${activeTemplate.value?.label || submitForm.templateName || "审批"}`; |
| | | } |
| | | const kw = (searchForm.applicantKeyword || "").trim().toLowerCase(); |
| | | if (kw) { |
| | | list = list.filter((r) => { |
| | | const name = (r.applicantName || "").toLowerCase(); |
| | | const no = (r.applicantNo || "").toLowerCase(); |
| | | return name.includes(kw) || no.includes(kw); |
| | | }); |
| | | } |
| | | const range = searchForm.createTimeRange; |
| | | if (range?.length === 2) { |
| | | const [from, to] = range; |
| | | list = list.filter((r) => { |
| | | const t = (r.createTime || "").slice(0, 10); |
| | | return t && t >= from && t <= to; |
| | | }); |
| | | } |
| | | return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 1 : -1)); |
| | | if (submitDialog.step === 1) return "选择审批模板"; |
| | | return `提交${activeTemplate.value?.label || "审批"}`; |
| | | }); |
| | | |
| | | watch( |
| | | filteredList, |
| | | (list) => { |
| | | page.total = list.length; |
| | | const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1); |
| | | if (page.current > maxPage) page.current = maxPage; |
| | | }, |
| | | { immediate: true } |
| | | ); |
| | | const activeTemplate = computed(() => submitForm.templateSnapshot || null); |
| | | |
| | | const tableData = computed(() => { |
| | | const start = (page.current - 1) * page.size; |
| | | return filteredList.value.slice(start, start + page.size); |
| | | /** 填报项定义(新增/修改与 formConfig 一致) */ |
| | | const submitFormFields = computed(() => { |
| | | const tplFields = activeTemplate.value?.fields; |
| | | if (tplFields?.length) return tplFields; |
| | | return submitForm.formFieldDefs || []; |
| | | }); |
| | | |
| | | const activeTemplate = computed(() => SUBMIT_TEMPLATES[submitForm.templateKey] || null); |
| | | |
| | | const submitFormRules = computed(() => { |
| | | const rules = { |
| | | templateKey: [{ required: true, message: "请选择审批类型", trigger: "change" }], |
| | | }; |
| | | (activeTemplate.value?.fields || []).forEach((f) => { |
| | | submitFormFields.value.forEach((f) => { |
| | | if (!f.required) return; |
| | | if (f.type === "number") { |
| | | rules[`formPayload.${f.key}`] = [{ required: true, message: `请填写${f.label}`, trigger: "blur" }]; |
| | |
| | | const tableColumn = ref([ |
| | | { label: "申请人编号", prop: "applicantNo", width: 110 }, |
| | | { label: "申请人名称", prop: "applicantName", minWidth: 100 }, |
| | | { label: "业务类型", prop: "businessName", minWidth: 120 }, |
| | | { |
| | | label: "审批类型", |
| | | prop: "approvalType", |
| | |
| | | slot: "approveType", |
| | | }, |
| | | { |
| | | label: "审批方式", |
| | | prop: "approvalMode", |
| | | width: 90, |
| | | dataType: "slot", |
| | | slot: "approvalMethod", |
| | | }, |
| | | { |
| | | label: "是否未读", |
| | | label: "待我审批", |
| | | prop: "unread", |
| | | width: 90, |
| | | align: "center", |
| | |
| | | formatData: (v) => approvalStatusLabel(v), |
| | | formatType: (v) => approvalStatusTagType(v), |
| | | }, |
| | | { label: "创建时间", prop: "createTime", width: 170 }, |
| | | { |
| | | label: "创建时间", |
| | | prop: "createTime", |
| | | width: 170, |
| | | formatData: (v) => formatDisplayTime(v), |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "操作", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 160, |
| | | width: 240, |
| | | operation: [ |
| | | { name: "详情", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { |
| | | name: "审批", |
| | | name: "修改", |
| | | type: "text", |
| | | disabled: (row) => row.approvalStatus !== "pending", |
| | | clickFun: (row) => openEditDialog(row), |
| | | }, |
| | | { |
| | | name: "审批", |
| | | type: "text", |
| | | disabled: (row) => row.approvalStatus !== "pending" || !row.isApprove, |
| | | clickFun: (row) => openApprove(row), |
| | | }, |
| | | { |
| | | name: "删除", |
| | | type: "danger", |
| | | clickFun: (row) => removeInstance(row), |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | function persist() { |
| | | saveStoredRows(allRows.value); |
| | | async function fetchApprovalList() { |
| | | tableLoading.value = true; |
| | | try { |
| | | const res = await listApprovalInstancePage( |
| | | buildApprovalInstanceListParams({ page, searchForm }) |
| | | ); |
| | | const { records, total } = unwrapInstancePage(res); |
| | | tableData.value = records.map(mapInstanceFromApi); |
| | | page.total = total; |
| | | } catch { |
| | | tableData.value = []; |
| | | page.total = 0; |
| | | ElMessage.error("审批列表加载失败"); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadSubmitTemplates() { |
| | | submitTemplatesLoading.value = true; |
| | | try { |
| | | const [builtinRes, customRes] = await Promise.all([ |
| | | listApprovalTemplate(TEMPLATE_TYPE_BUILTIN), |
| | | listApprovalTemplate(TEMPLATE_TYPE_CUSTOM), |
| | | ]); |
| | | const merged = [ |
| | | ...unwrapTemplateList(builtinRes), |
| | | ...unwrapTemplateList(customRes), |
| | | ].filter((row) => mapEnabledFromApi(row.enabled)); |
| | | submitTemplateCards.value = merged.map(mapSubmitTemplateCard); |
| | | } catch { |
| | | submitTemplateCards.value = []; |
| | | ElMessage.error("加载审批模板失败"); |
| | | } finally { |
| | | submitTemplatesLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleQuery() { |
| | | tableLoading.value = true; |
| | | page.current = 1; |
| | | setTimeout(() => { |
| | | tableLoading.value = false; |
| | | }, 200); |
| | | fetchApprovalList(); |
| | | } |
| | | |
| | | function resetSearch() { |
| | |
| | | function pagination({ page: p, limit }) { |
| | | page.current = p; |
| | | page.size = limit; |
| | | } |
| | | |
| | | function markRead(row) { |
| | | if (!row.unread) return; |
| | | const hit = allRows.value.find((r) => r.id === row.id); |
| | | if (hit) { |
| | | hit.unread = false; |
| | | persist(); |
| | | } |
| | | fetchApprovalList(); |
| | | } |
| | | |
| | | function openDetail(row) { |
| | | markRead(row); |
| | | detailRow.value = { ...row }; |
| | | detailDialog.visible = true; |
| | | } |
| | | |
| | | function openApprove(row) { |
| | | markRead(row); |
| | | approveDialog.row = { ...row }; |
| | | approveOpinion.value = ""; |
| | | approveDialog.visible = true; |
| | | } |
| | | |
| | | function openSubmitDialog() { |
| | | Object.assign(submitForm, createEmptySubmitForm("")); |
| | | function resetSubmitDialogState() { |
| | | submitDialog.mode = "add"; |
| | | submitDialog.step = 1; |
| | | submitEditRow.value = null; |
| | | Object.assign(submitForm, createEmptySubmitForm("")); |
| | | } |
| | | |
| | | function openSubmitDialog() { |
| | | resetSubmitDialogState(); |
| | | submitDialog.visible = true; |
| | | loadSubmitTemplates(); |
| | | } |
| | | |
| | | function openEditDialog(row) { |
| | | if (row?.approvalStatus !== "pending") { |
| | | ElMessage.warning("仅审核中的审批可修改"); |
| | | return; |
| | | } |
| | | if (!row?.id) { |
| | | ElMessage.warning("无法修改:缺少审批实例 ID"); |
| | | return; |
| | | } |
| | | submitDialog.mode = "edit"; |
| | | submitDialog.step = 2; |
| | | submitEditRow.value = { ...row }; |
| | | Object.assign(submitForm, buildEditFormFromInstanceRow(row)); |
| | | submitDialog.visible = true; |
| | | } |
| | | |
| | | function onTemplatePick(key) { |
| | | Object.assign(submitForm, createEmptySubmitForm(key)); |
| | | submitDialog.step = 2; |
| | | async function onTemplatePick(card) { |
| | | if (!card?.id) return; |
| | | 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); |
| | | Object.assign(submitForm, { |
| | | ...base, |
| | | templateName: mapped.templateName || tpl.label || "", |
| | | templateSnapshot: tpl, |
| | | formFieldDefs: tpl.fields || [], |
| | | }); |
| | | submitDialog.step = 2; |
| | | } catch { |
| | | ElMessage.error("加载模板详情失败"); |
| | | } finally { |
| | | submitTemplatesLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function backToTemplatePick() { |
| | | submitDialog.step = 1; |
| | | } |
| | | |
| | | async function submitInstanceForm() { |
| | | if (submitDialog.mode === "edit") return submitEditApproval(); |
| | | return submitNewApproval(); |
| | | } |
| | | |
| | | async function submitNewApproval() { |
| | |
| | | } catch { |
| | | return false; |
| | | } |
| | | const tpl = activeTemplate.value; |
| | | if (!tpl) return false; |
| | | const id = `user_${Date.now()}`; |
| | | const summary = |
| | | submitForm.formPayload.summary || |
| | | submitForm.formPayload.handoverTo || |
| | | `${tpl.label}申请`; |
| | | const row = { |
| | | id, |
| | | bizId: `BIZ${dayjs().format("YYYYMMDDHHmmss")}`, |
| | | applicantNo: userStore.name || String(userStore.id || "当前用户"), |
| | | applicantName: userStore.nickName || userStore.name || "当前用户", |
| | | approvalType: tpl.approvalType, |
| | | approvalMode: submitForm.approvalMode, |
| | | unread: false, |
| | | approvalStatus: "pending", |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | summary, |
| | | formPayload: { ...submitForm.formPayload }, |
| | | approvalFlowNodes: (submitForm.approvalFlowNodes?.length |
| | | ? submitForm.approvalFlowNodes |
| | | : buildDefaultFlowNodes() |
| | | ).map((n, i) => ({ ...n, nodeStatus: i === 0 ? "process" : n.nodeStatus || "wait" })), |
| | | currentNodeIndex: 0, |
| | | approvalRecords: [], |
| | | rejectReason: "", |
| | | }; |
| | | allRows.value.unshift(row); |
| | | persist(); |
| | | submitDialog.visible = false; |
| | | page.current = 1; |
| | | return true; |
| | | if (!activeTemplate.value) return false; |
| | | const flowCheck = validateSubmitFlowNodes(submitForm.flowNodes); |
| | | if (!flowCheck.ok) { |
| | | ElMessage.warning(flowCheck.message); |
| | | return false; |
| | | } |
| | | if (!submitForm.templateId) { |
| | | ElMessage.warning("缺少模板 ID,无法提交"); |
| | | return false; |
| | | } |
| | | if (submitSaving.value) return false; |
| | | submitSaving.value = true; |
| | | try { |
| | | await saveApprovalInstance( |
| | | buildInstanceDto({ |
| | | submitForm, |
| | | activeTemplate: activeTemplate.value, |
| | | userStore, |
| | | flowNodes: flowCheck.nodes, |
| | | }) |
| | | ); |
| | | submitDialog.visible = false; |
| | | page.current = 1; |
| | | await fetchApprovalList(); |
| | | return true; |
| | | } catch { |
| | | return false; |
| | | } finally { |
| | | submitSaving.value = false; |
| | | } |
| | | } |
| | | |
| | | function submitApprove(result) { |
| | | async function submitEditApproval() { |
| | | if (!submitFormRef.value) return false; |
| | | try { |
| | | await submitFormRef.value.validate(); |
| | | } catch { |
| | | return false; |
| | | } |
| | | if (!activeTemplate.value) return false; |
| | | const flowCheck = validateSubmitFlowNodes(submitForm.flowNodes); |
| | | if (!flowCheck.ok) { |
| | | ElMessage.warning(flowCheck.message); |
| | | return false; |
| | | } |
| | | if (!submitForm.instanceId) { |
| | | ElMessage.warning("缺少审批实例 ID,无法保存"); |
| | | return false; |
| | | } |
| | | if (submitSaving.value) return false; |
| | | submitSaving.value = true; |
| | | try { |
| | | await updateApprovalInstance( |
| | | buildInstanceDto({ |
| | | submitForm, |
| | | activeTemplate: activeTemplate.value, |
| | | flowNodes: flowCheck.nodes, |
| | | existingRow: submitEditRow.value, |
| | | }) |
| | | ); |
| | | submitDialog.visible = false; |
| | | await fetchApprovalList(); |
| | | if (detailDialog.visible && detailRow.value?.id === submitForm.instanceId) { |
| | | const hit = tableData.value.find((r) => r.id === submitForm.instanceId); |
| | | if (hit) detailRow.value = { ...hit }; |
| | | else detailDialog.visible = false; |
| | | } |
| | | return true; |
| | | } catch { |
| | | return false; |
| | | } finally { |
| | | submitSaving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function removeInstance(row) { |
| | | if (row?.id == null || row.id === "") { |
| | | ElMessage.warning("无法删除:缺少审批实例 ID"); |
| | | return; |
| | | } |
| | | const title = row.title || row.templateName || row.instanceNo || "该审批"; |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | `确定要删除审批「${title}」吗?删除后不可恢复。`, |
| | | "删除确认", |
| | | { |
| | | type: "warning", |
| | | confirmButtonText: "确定删除", |
| | | cancelButtonText: "取消", |
| | | distinguishCancelAndClose: true, |
| | | autofocus: false, |
| | | } |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | try { |
| | | await deleteApprovalInstance([row.id]); |
| | | ElMessage.success("删除成功"); |
| | | if (detailDialog.visible && detailRow.value?.id === row.id) { |
| | | detailDialog.visible = false; |
| | | } |
| | | if (approveDialog.visible && approveDialog.row?.id === row.id) { |
| | | approveDialog.visible = false; |
| | | } |
| | | await fetchApprovalList(); |
| | | } catch { |
| | | /* 错误由拦截器提示 */ |
| | | } |
| | | } |
| | | |
| | | async function submitApprove(result) { |
| | | const row = approveDialog.row; |
| | | if (!row) return; |
| | | const hit = allRows.value.find((r) => r.id === row.id); |
| | | if (!hit || hit.approvalStatus !== "pending") return; |
| | | if (!row?.id) return { ok: false }; |
| | | if (result === "rejected" && !(approveOpinion.value || "").trim()) { |
| | | return { needOpinion: true }; |
| | | } |
| | | advanceFlow(hit, result, (approveOpinion.value || "").trim()); |
| | | hit.unread = false; |
| | | persist(); |
| | | approveDialog.visible = false; |
| | | if (detailDialog.visible && detailRow.value?.id === hit.id) { |
| | | detailRow.value = { ...hit }; |
| | | if (approveSubmitting.value) return { ok: false }; |
| | | approveSubmitting.value = true; |
| | | try { |
| | | await approveApprovalInstance( |
| | | buildApproveInstanceDto(row, result, approveOpinion.value) |
| | | ); |
| | | approveDialog.visible = false; |
| | | await fetchApprovalList(); |
| | | if (detailDialog.visible && detailRow.value?.id === row.id) { |
| | | const hit = tableData.value.find((r) => r.id === row.id); |
| | | if (hit) detailRow.value = { ...hit }; |
| | | else detailDialog.visible = false; |
| | | } |
| | | return { ok: true, result }; |
| | | } catch { |
| | | ElMessage.error("审批操作失败"); |
| | | return { ok: false }; |
| | | } finally { |
| | | approveSubmitting.value = false; |
| | | } |
| | | return { ok: true }; |
| | | } |
| | | |
| | | function approvalActionLabel(result) { |
| | |
| | | return { |
| | | Search, |
| | | APPROVAL_TYPE_OPTIONS, |
| | | SUBMIT_TEMPLATES, |
| | | approvalTypeLabel, |
| | | approvalModeLabel, |
| | | approvalStatusLabel, |
| | | approvalStatusTagType, |
| | | approvalActionLabel, |
| | |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | | approveSubmitting, |
| | | submitDialog, |
| | | isSubmitEdit, |
| | | submitDialogTitle, |
| | | submitForm, |
| | | submitFormRef, |
| | | submitSaving, |
| | | activeTemplate, |
| | | submitFormFields, |
| | | submitFormRules, |
| | | submitTemplateCards, |
| | | submitTemplatesLoading, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | resetSubmitDialogState, |
| | | openSubmitDialog, |
| | | openEditDialog, |
| | | onTemplatePick, |
| | | backToTemplatePick, |
| | | submitInstanceForm, |
| | | submitNewApproval, |
| | | submitApprove, |
| | | openDetail, |
| | | openApprove, |
| | | fetchApprovalList, |
| | | }; |
| | | } |