| | |
| | | <!--OA模块:审批模板(系统常用 + 自定义多节点流程)--> |
| | | <!--OA模块:审批模板--> |
| | | <template> |
| | | <div class="app-container approve-template-page"> |
| | | <el-tabs v-model="activeTab" class="template-tabs"> |
| | |
| | | 以下为 OA 模块内置的常用审批类型,填报字段与默认审批方式由系统维护;提交审批时可直接选用。 |
| | | </template> |
| | | </el-alert> |
| | | <div class="builtin-grid"> |
| | | <div v-for="item in builtinTemplates" :key="item.key" class="builtin-card"> |
| | | <div v-loading="builtinLoading" class="builtin-grid"> |
| | | <template v-if="builtinTemplates.length"> |
| | | <div v-for="item in builtinTemplates" :key="item.key" class="builtin-card"> |
| | | <span class="builtin-label">{{ item.label }}</span> |
| | | <p class="builtin-summary">{{ item.summary }}</p> |
| | | <div class="builtin-meta"> |
| | |
| | | </el-tag> |
| | | <el-tag size="small" type="info" effect="plain">只读</el-tag> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <el-empty v-else-if="!builtinLoading" description="暂无系统常用审批模板" :image-size="80" /> |
| | | </div> |
| | | </el-tab-pane> |
| | | |
| | |
| | | <el-dialog |
| | | v-model="formDialog.visible" |
| | | :title="formDialog.title" |
| | | width="960px" |
| | | width="1020px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="template-form-dialog" |
| | |
| | | > |
| | | <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="模板名称" prop="templateName"> |
| | | <el-input v-model="form.templateName" placeholder="如:项目立项审批" maxlength="50" show-word-limit /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="模板类型" prop="templateType"> |
| | | <el-select v-model="form.templateType" placeholder="请选择" style="width: 100%"> |
| | | <el-option |
| | | v-for="opt in TEMPLATE_TYPE_OPTIONS" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="启用状态"> |
| | | <el-switch v-model="form.enabled" active-text="启用" inactive-text="停用" /> |
| | | </el-form-item> |
| | |
| | | maxlength="200" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="填报配置"> |
| | | <FormConfigEditor v-model="form.formConfigData" /> |
| | | <p class="flow-tip">配置提交审批时需填写的表单项,保存后写入 formConfig(JSON)。</p> |
| | | </el-form-item> |
| | | <el-form-item label="审批流程" required> |
| | | <TemplateFlowEditor v-model="form.flowNodes" :user-options="flowUserOptions" /> |
| | |
| | | |
| | | <!-- 详情 --> |
| | | <el-dialog v-model="detailDialog.visible" title="模板详情" width="880px" append-to-body destroy-on-close> |
| | | <div v-loading="detailLoading" class="detail-dialog-body"> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="模板名称">{{ detailRow.templateName }}</el-descriptions-item> |
| | | <el-descriptions-item label="模板类型">{{ templateTypeLabel(detailRow.templateType) }}</el-descriptions-item> |
| | | <el-descriptions-item label="状态"> |
| | | <el-tag :type="detailRow.enabled !== false ? 'success' : 'info'" size="small"> |
| | | {{ detailRow.enabled !== false ? "启用" : "停用" }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="说明" :span="2">{{ detailRow.description || "—" }}</el-descriptions-item> |
| | | <el-descriptions-item label="创建时间">{{ detailRow.createTime || "—" }}</el-descriptions-item> |
| | | <el-descriptions-item label="更新时间">{{ detailRow.updateTime || "—" }}</el-descriptions-item> |
| | | <el-descriptions-item label="填报提示" :span="2"> |
| | | {{ detailFormConfig.summaryPlaceholder || "—" }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="创建人">{{ detailRow.createdUserName || "—" }}</el-descriptions-item> |
| | | <el-descriptions-item label="创建时间">{{ formatDisplayTime(detailRow.createdTime) }}</el-descriptions-item> |
| | | <el-descriptions-item label="更新时间">{{ formatDisplayTime(detailRow.updatedTime) }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <el-divider content-position="left">填报项({{ detailFormConfig.fields?.length || 0 }} 项)</el-divider> |
| | | <el-table |
| | | v-if="detailFormConfig.fields?.length" |
| | | :data="detailFormConfig.fields" |
| | | border |
| | | size="small" |
| | | class="mb16" |
| | | > |
| | | <el-table-column prop="label" label="显示名称" min-width="120" /> |
| | | <el-table-column prop="key" label="字段标识" min-width="100" /> |
| | | <el-table-column label="类型" width="100"> |
| | | <template #default="{ row }">{{ formFieldTypeLabel(row.type) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="必填" width="70" align="center"> |
| | | <template #default="{ row }">{{ row.required !== false ? "是" : "否" }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="默认值" min-width="120" show-overflow-tooltip> |
| | | <template #default="{ row }">{{ formatDefaultValueDisplay(row) }}</template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-empty v-else description="未配置填报项" :image-size="48" class="mb16" /> |
| | | <el-divider content-position="left">审批流程({{ detailRow.flowNodes?.length || 0 }} 个节点)</el-divider> |
| | | <div v-if="detailRow.flowNodes?.length" class="detail-flow"> |
| | | <div v-for="(node, index) in detailRow.flowNodes" :key="index" class="detail-node"> |
| | |
| | | </div> |
| | | </div> |
| | | <el-empty v-else description="暂无流程节点" :image-size="60" /> |
| | | </div> |
| | | <template #footer> |
| | | <el-button @click="detailDialog.visible = false">关 闭</el-button> |
| | | <el-button type="primary" @click="editFromDetail">编 辑</el-button> |
| | |
| | | <script setup> |
| | | import { ArrowRight, Document, Plus, RefreshRight } from "@element-plus/icons-vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { onMounted, ref } from "vue"; |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import FormConfigEditor from "./components/FormConfigEditor.vue"; |
| | | import TemplateFlowEditor from "./components/TemplateFlowEditor.vue"; |
| | | import { formatDisplayTime } from "./approveTemplateConstants.js"; |
| | | import { formatDefaultValueDisplay, formFieldTypeLabel, parseFormConfigToData } from "./formConfigUtils.js"; |
| | | import { useApproveTemplate } from "./useApproveTemplate.js"; |
| | | |
| | | const at = useApproveTemplate(); |
| | | const { |
| | | Search, |
| | | TEMPLATE_TYPE_OPTIONS, |
| | | templateTypeLabel, |
| | | activeTab, |
| | | builtinTemplates, |
| | | builtinLoading, |
| | | loadBuiltinTemplates, |
| | | nodeSignModeLabel, |
| | | searchForm, |
| | | tableLoading, |
| | |
| | | formRules, |
| | | detailDialog, |
| | | detailRow, |
| | | detailLoading, |
| | | fetchTemplateList, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | |
| | | } = at; |
| | | |
| | | const flowUserOptions = ref([]); |
| | | |
| | | const detailFormConfig = computed(() => |
| | | parseFormConfigToData(detailRow.value?.formConfigData ?? detailRow.value?.formConfig) |
| | | ); |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | |
| | | |
| | | onMounted(() => { |
| | | loadUsers(); |
| | | handleQuery(); |
| | | loadBuiltinTemplates(); |
| | | fetchTemplateList(); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | } |
| | | .mb16 { |
| | | margin-bottom: 16px; |
| | | } |
| | | .mb16.el-empty { |
| | | padding: 8px 0; |
| | | } |
| | | .ml10 { |
| | | margin-left: 10px; |
| | |
| | | transform: translateY(-50%); |
| | | color: var(--el-text-color-placeholder); |
| | | } |
| | | .detail-dialog-body { |
| | | min-height: 120px; |
| | | } |
| | | .text-muted { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-placeholder); |