| | |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import dayjs from "dayjs"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { computed, reactive, ref, watch } from "vue"; |
| | | import { |
| | | addApprovalTemplate, |
| | | deleteApprovalTemplate, |
| | | getApprovalTemplateDetail, |
| | | listApprovalTemplate, |
| | | listApprovalTemplatePage, |
| | | TEMPLATE_TYPE_BUILTIN, |
| | | TEMPLATE_TYPE_CUSTOM, |
| | | TEMPLATE_TYPE_OPTIONS, |
| | | updateApprovalTemplate, |
| | | } from "@/api/officeProcessAutomation/approvalTemplate.js"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { reactive, ref } from "vue"; |
| | | import { |
| | | buildApprovalTemplateListParams, |
| | | createEmptyTemplateForm, |
| | | createInitialMockTemplates, |
| | | flowNodesSummary, |
| | | getBuiltinTemplates, |
| | | loadStoredTemplates, |
| | | mapBuiltinCardFromApi, |
| | | mapTemplateFromApi, |
| | | mapTemplateToApi, |
| | | nodeSignModeLabel, |
| | | saveStoredTemplates, |
| | | templateTypeLabel, |
| | | unwrapTemplateList, |
| | | formatDisplayTime, |
| | | unwrapTemplateDetail, |
| | | validateTemplateForm, |
| | | } from "./approveTemplateConstants.js"; |
| | | import { parseFormConfigToData } from "./formConfigUtils.js"; |
| | | |
| | | const LEGACY_STORAGE_KEY = "oa_approve_template_custom_v1"; |
| | | |
| | | function clearLegacyStorage() { |
| | | try { |
| | | localStorage.removeItem(LEGACY_STORAGE_KEY); |
| | | } catch { |
| | | /* ignore */ |
| | | } |
| | | } |
| | | |
| | | export function useApproveTemplate() { |
| | | const stored = loadStoredTemplates(); |
| | | const allTemplates = ref(stored?.length ? stored : createInitialMockTemplates()); |
| | | clearLegacyStorage(); |
| | | |
| | | const activeTab = ref("custom"); |
| | | const builtinTemplates = getBuiltinTemplates(); |
| | | const builtinTemplates = ref([]); |
| | | const builtinLoading = ref(false); |
| | | |
| | | const searchForm = reactive({ |
| | | keyword: "", |
| | |
| | | |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | | const tableData = ref([]); |
| | | |
| | | const formDialog = reactive({ visible: false, title: "", mode: "add" }); |
| | | const form = reactive(createEmptyTemplateForm()); |
| | |
| | | |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailRow = ref({}); |
| | | |
| | | const filteredList = computed(() => { |
| | | let list = [...allTemplates.value]; |
| | | const kw = (searchForm.keyword || "").trim().toLowerCase(); |
| | | if (kw) { |
| | | list = list.filter((r) => { |
| | | const name = (r.templateName || "").toLowerCase(); |
| | | const desc = (r.description || "").toLowerCase(); |
| | | return name.includes(kw) || desc.includes(kw); |
| | | }); |
| | | } |
| | | if (searchForm.enabledOnly) { |
| | | list = list.filter((r) => r.enabled !== false); |
| | | } |
| | | return list.sort((a, b) => (String(a.updateTime) < String(b.updateTime) ? 1 : -1)); |
| | | }); |
| | | |
| | | 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 tableData = computed(() => { |
| | | const start = (page.current - 1) * page.size; |
| | | return filteredList.value.slice(start, start + page.size); |
| | | }); |
| | | const detailLoading = ref(false); |
| | | |
| | | const formRules = { |
| | | templateName: [{ required: true, message: "请输入模板名称", trigger: "blur" }], |
| | | templateType: [{ required: true, message: "请选择模板类型", trigger: "change" }], |
| | | }; |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "模板名称", prop: "templateName", minWidth: 140 }, |
| | | { |
| | | label: "模板类型", |
| | | prop: "templateType", |
| | | width: 100, |
| | | align: "center", |
| | | formatData: (v) => templateTypeLabel(v), |
| | | }, |
| | | { label: "说明", prop: "description", minWidth: 160, showOverflowTooltip: true }, |
| | | { |
| | | label: "节点数", |
| | |
| | | formatData: (v) => (v !== false ? "启用" : "停用"), |
| | | formatType: (v) => (v !== false ? "success" : "info"), |
| | | }, |
| | | { label: "更新时间", prop: "updateTime", width: 170 }, |
| | | { |
| | | label: "创建时间", |
| | | prop: "createdTime", |
| | | width: 170, |
| | | showOverflowTooltip: true, |
| | | formatData: (v) => formatDisplayTime(v), |
| | | }, |
| | | { |
| | | label: "更新时间", |
| | | prop: "updatedTime", |
| | | width: 170, |
| | | showOverflowTooltip: true, |
| | | formatData: (v) => formatDisplayTime(v), |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "操作", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 200, |
| | | width: 220, |
| | | operation: [ |
| | | { name: "详情", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { name: "编辑", type: "text", clickFun: (row) => openFormDialog("edit", row) }, |
| | | { name: "删除", type: "text", clickFun: (row) => removeTemplate(row) }, |
| | | { |
| | | name: "删除", |
| | | type: "danger", |
| | | link: true, |
| | | clickFun: (row) => removeTemplate(row), |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | function persist() { |
| | | saveStoredTemplates(allTemplates.value); |
| | | async function loadBuiltinTemplates() { |
| | | builtinLoading.value = true; |
| | | try { |
| | | const res = await listApprovalTemplate(TEMPLATE_TYPE_BUILTIN); |
| | | builtinTemplates.value = unwrapTemplateList(res).map(mapBuiltinCardFromApi); |
| | | } catch { |
| | | builtinTemplates.value = []; |
| | | ElMessage.warning("系统常用审批加载失败"); |
| | | } finally { |
| | | builtinLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function fetchTemplateList() { |
| | | tableLoading.value = true; |
| | | try { |
| | | const res = await listApprovalTemplatePage( |
| | | buildApprovalTemplateListParams({ page, searchForm }) |
| | | ); |
| | | const data = res?.data || {}; |
| | | tableData.value = (data.records || []).map(mapTemplateFromApi); |
| | | page.total = Number(data.total || 0); |
| | | } catch { |
| | | tableData.value = []; |
| | | page.total = 0; |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleQuery() { |
| | | tableLoading.value = true; |
| | | page.current = 1; |
| | | setTimeout(() => { |
| | | tableLoading.value = false; |
| | | }, 150); |
| | | fetchTemplateList(); |
| | | } |
| | | |
| | | function resetSearch() { |
| | |
| | | function pagination({ page: p, limit }) { |
| | | page.current = p; |
| | | page.size = limit; |
| | | fetchTemplateList(); |
| | | } |
| | | |
| | | function resetForm(row) { |
| | |
| | | id: row.id, |
| | | templateName: row.templateName || "", |
| | | description: row.description || "", |
| | | templateType: row.templateType ?? TEMPLATE_TYPE_CUSTOM, |
| | | formConfig: row.formConfig || "", |
| | | formConfigData: JSON.parse( |
| | | JSON.stringify(row.formConfigData || parseFormConfigToData(row.formConfig)) |
| | | ), |
| | | enabled: row.enabled !== false, |
| | | flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [base.flowNodes[0]])), |
| | | }); |
| | |
| | | |
| | | function openFormDialog(mode, row) { |
| | | formDialog.mode = mode; |
| | | formDialog.title = mode === "add" ? "新建自定义审批模板" : "编辑自定义审批模板"; |
| | | formDialog.title = mode === "add" ? "新建审批模板" : "编辑审批模板"; |
| | | resetForm(mode === "edit" ? row : null); |
| | | formDialog.visible = true; |
| | | } |
| | | |
| | | function openDetail(row) { |
| | | detailRow.value = { ...row }; |
| | | async function openDetail(row) { |
| | | if (row?.id == null || row.id === "") { |
| | | ElMessage.warning("无法查看详情:缺少模板 ID"); |
| | | return; |
| | | } |
| | | detailDialog.visible = true; |
| | | } |
| | | |
| | | function isNameDuplicate(name, excludeId) { |
| | | const n = (name || "").trim(); |
| | | return allTemplates.value.some((t) => t.templateName?.trim() === n && t.id !== excludeId); |
| | | detailLoading.value = true; |
| | | detailRow.value = {}; |
| | | try { |
| | | const res = await getApprovalTemplateDetail(row.id); |
| | | detailRow.value = mapTemplateFromApi(unwrapTemplateDetail(res)); |
| | | } catch { |
| | | detailDialog.visible = false; |
| | | } finally { |
| | | detailLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function submitForm() { |
| | |
| | | if (!validated.ok) { |
| | | return { message: validated.message }; |
| | | } |
| | | if (isNameDuplicate(validated.name, form.id)) { |
| | | return { message: "模板名称已存在,请更换名称" }; |
| | | if (formDialog.mode === "edit" && !form.id) { |
| | | return { message: "缺少模板 ID,无法保存修改" }; |
| | | } |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | if (formDialog.mode === "add") { |
| | | allTemplates.value.unshift({ |
| | | id: `tpl_${Date.now()}`, |
| | | templateName: validated.name, |
| | | description: (form.description || "").trim(), |
| | | enabled: form.enabled !== false, |
| | | createTime: now, |
| | | updateTime: now, |
| | | flowNodes: validated.nodes, |
| | | }); |
| | | } else { |
| | | const hit = allTemplates.value.find((t) => t.id === form.id); |
| | | if (!hit) return { message: "模板不存在或已删除" }; |
| | | hit.templateName = validated.name; |
| | | hit.description = (form.description || "").trim(); |
| | | hit.enabled = form.enabled !== false; |
| | | hit.flowNodes = validated.nodes; |
| | | hit.updateTime = now; |
| | | const dto = mapTemplateToApi(form); |
| | | try { |
| | | if (formDialog.mode === "add") { |
| | | await addApprovalTemplate(dto); |
| | | } else { |
| | | await updateApprovalTemplate(dto); |
| | | } |
| | | } catch { |
| | | return false; |
| | | } |
| | | persist(); |
| | | formDialog.visible = false; |
| | | page.current = 1; |
| | | await fetchTemplateList(); |
| | | if (dto.templateType === TEMPLATE_TYPE_BUILTIN) { |
| | | await loadBuiltinTemplates(); |
| | | } |
| | | return { ok: true }; |
| | | } |
| | | |
| | | async function removeTemplate(row) { |
| | | if (row?.id == null || row.id === "") { |
| | | ElMessage.warning("无法删除:缺少模板 ID"); |
| | | return; |
| | | } |
| | | const name = row.templateName || "未命名模板"; |
| | | try { |
| | | await ElMessageBox.confirm(`确定删除模板「${row.templateName}」吗?`, "提示", { |
| | | type: "warning", |
| | | confirmButtonText: "删除", |
| | | cancelButtonText: "取消", |
| | | }); |
| | | await ElMessageBox.confirm( |
| | | `确定要删除审批模板「${name}」吗?删除后不可恢复。`, |
| | | "删除确认", |
| | | { |
| | | type: "warning", |
| | | confirmButtonText: "确定删除", |
| | | cancelButtonText: "取消", |
| | | distinguishCancelAndClose: true, |
| | | autofocus: false, |
| | | } |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | const idx = allTemplates.value.findIndex((t) => t.id === row.id); |
| | | if (idx >= 0) { |
| | | allTemplates.value.splice(idx, 1); |
| | | persist(); |
| | | try { |
| | | await deleteApprovalTemplate([row.id]); |
| | | ElMessage.success("删除成功"); |
| | | await fetchTemplateList(); |
| | | if (row.templateType === TEMPLATE_TYPE_BUILTIN) { |
| | | await loadBuiltinTemplates(); |
| | | } |
| | | } catch { |
| | | /* 错误由拦截器提示 */ |
| | | } |
| | | } |
| | | |
| | | function toggleEnabled(row) { |
| | | const hit = allTemplates.value.find((t) => t.id === row.id); |
| | | if (!hit) return; |
| | | hit.enabled = !hit.enabled; |
| | | hit.updateTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | persist(); |
| | | } |
| | | |
| | | return { |
| | | Search, |
| | | TEMPLATE_TYPE_OPTIONS, |
| | | templateTypeLabel, |
| | | activeTab, |
| | | builtinTemplates, |
| | | builtinLoading, |
| | | loadBuiltinTemplates, |
| | | fetchTemplateList, |
| | | nodeSignModeLabel, |
| | | flowNodesSummary, |
| | | searchForm, |
| | |
| | | formRules, |
| | | detailDialog, |
| | | detailRow, |
| | | detailLoading, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | openFormDialog, |
| | | openDetail, |
| | | submitForm, |
| | | toggleEnabled, |
| | | }; |
| | | } |