| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼å®¡æ¹æ¨¡æ¿--> |
| | | |
| | | <template> |
| | | |
| | | <div class="app-container approve-template-page"> |
| | | |
| | | <div class="search_form mb20"> |
| | | |
| | | <div class="search_fields"> |
| | | |
| | | <span class="search_title">模æ¿åç§°ï¼</span> |
| | | |
| | | <el-input |
| | | |
| | | v-model="searchForm.keyword" |
| | | |
| | | style="width: 220px" |
| | | |
| | | placeholder="æç´¢åç§°æè¯´æ" |
| | | |
| | | clearable |
| | | |
| | | :prefix-icon="Search" |
| | | |
| | | @keyup.enter="handleQuery" |
| | | |
| | | /> |
| | | |
| | | <el-checkbox v-model="searchForm.enabledOnly" class="ml12" @change="handleQuery"> |
| | | |
| | | ä»
æ¾ç¤ºå¯ç¨ |
| | | |
| | | </el-checkbox> |
| | | |
| | | <el-button type="primary" :icon="Search" class="ml10" @click="handleQuery">æç´¢</el-button> |
| | | |
| | | <el-button :icon="RefreshRight" @click="resetSearch">éç½®</el-button> |
| | | |
| | | </div> |
| | | |
| | | <div class="search_actions"> |
| | | |
| | | <el-button type="primary" :icon="Plus" @click="openFormDialog('add')">æ°å»ºæ¨¡æ¿</el-button> |
| | | |
| | | </div> |
| | | |
| | | </div> |
| | | |
| | | |
| | | |
| | | <div class="table_list"> |
| | | |
| | | <PIMTable |
| | | |
| | | rowKey="id" |
| | | |
| | | :column="tableColumn" |
| | | |
| | | :tableData="tableData" |
| | | |
| | | :page="page" |
| | | |
| | | :isSelection="false" |
| | | |
| | | :tableLoading="tableLoading" |
| | | |
| | | :total="page.total" |
| | | |
| | | @pagination="pagination" |
| | | |
| | | /> |
| | | |
| | | </div> |
| | | |
| | | |
| | | |
| | | <!-- æ°å»º / ç¼è¾ --> |
| | | |
| | | <el-dialog |
| | | |
| | | v-model="formDialog.visible" |
| | | |
| | | :title="formDialog.title" |
| | | |
| | | width="1020px" |
| | | |
| | | append-to-body |
| | | |
| | | destroy-on-close |
| | | |
| | | class="template-form-dialog" |
| | | |
| | | @closed="onFormDialogClosed" |
| | | |
| | | > |
| | | |
| | | <el-form |
| | | |
| | | v-if="formDialog.visible" |
| | | |
| | | ref="formRef" |
| | | |
| | | :model="form" |
| | | |
| | | :rules="formRules" |
| | | |
| | | label-width="100px" |
| | | |
| | | > |
| | | |
| | | <el-row :gutter="20"> |
| | | |
| | | <el-col :span="8"> |
| | | |
| | | <el-form-item label="模æ¿åç§°" prop="templateName"> |
| | | |
| | | <el-input |
| | | v-model="form.templateName" |
| | | placeholder="å¦ï¼é¡¹ç®ç«é¡¹å®¡æ¹" |
| | | maxlength="50" |
| | | show-word-limit |
| | | :disabled="isEditingBuiltin" |
| | | /> |
| | | |
| | | </el-form-item> |
| | | |
| | | </el-col> |
| | | |
| | | <el-col :span="8"> |
| | | |
| | | <el-form-item label="模æ¿ç±»å" prop="businessType"> |
| | | |
| | | <el-select |
| | | v-model="form.businessType" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | :disabled="isEditingBuiltin" |
| | | > |
| | | |
| | | <el-option |
| | | |
| | | v-for="opt in templateTypeOptions" |
| | | |
| | | :key="`tpl-type-${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> |
| | | |
| | | </el-col> |
| | | |
| | | </el-row> |
| | | |
| | | <el-form-item label="模æ¿è¯´æ"> |
| | | |
| | | <el-input |
| | | |
| | | v-model="form.description" |
| | | |
| | | type="textarea" |
| | | |
| | | :rows="2" |
| | | |
| | | placeholder="ç®è¦è¯´æè¯¥æ¨¡æ¿çéç¨åºæ¯" |
| | | |
| | | maxlength="200" |
| | | |
| | | show-word-limit |
| | | |
| | | /> |
| | | |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="å¡«æ¥é
ç½®"> |
| | | |
| | | <FormConfigEditor |
| | | v-model="form.formConfigData" |
| | | :exclude-template-id="form.id" |
| | | :disable-import="isEditingBuiltin" |
| | | :locked-field-uids="isEditingBuiltin ? form.lockedFormFieldUids : []" |
| | | /> |
| | | |
| | | <p class="flow-tip">é
ç½®æäº¤å®¡æ¹æ¶éå¡«åç表å项ï¼ä¿åååå
¥ formConfigï¼JSONï¼ã</p> |
| | | |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="å®¡æ¹æµç¨" required> |
| | | |
| | | <TemplateFlowEditor v-model="form.flowNodes" :user-options="flowUserOptions" /> |
| | | |
| | | <p class="flow-tip"> |
| | | |
| | | æé¡ºåºæµè½¬ï¼å¯ä¸ºæ¯ä¸ªèç¹æ·»å å¤å审æ¹äººï¼ä¼ç¾éå
¨é¨éè¿ï¼æç¾ä»»ä¸äººéè¿å³å¯è¿å
¥ä¸ä¸èç¹ã |
| | | |
| | | </p> |
| | | |
| | | </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-button type="primary" @click="onSubmitForm">ä¿ å</el-button> |
| | | |
| | | <el-button @click="formDialog.visible = false">å æ¶</el-button> |
| | | |
| | | </template> |
| | | |
| | | </el-dialog> |
| | | |
| | | |
| | | |
| | | <!-- 详æ
--> |
| | | |
| | | <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.businessType) }}</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="å¡«æ¥æç¤º" :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="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-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 class="detail-node-head"> |
| | | |
| | | <span class="detail-node-order">èç¹ {{ index + 1 }}</span> |
| | | |
| | | <el-tag size="small" :type="node.signMode === 'or_sign' ? 'warning' : 'primary'"> |
| | | |
| | | {{ nodeSignModeLabel(node.signMode) }} |
| | | |
| | | </el-tag> |
| | | |
| | | </div> |
| | | |
| | | <div class="detail-approvers"> |
| | | |
| | | <el-tag |
| | | |
| | | v-for="a in node.approvers" |
| | | |
| | | :key="String(a.approverId)" |
| | | |
| | | class="detail-approver-tag" |
| | | |
| | | effect="plain" |
| | | |
| | | > |
| | | |
| | | {{ a.approverName || "â" }} |
| | | |
| | | </el-tag> |
| | | |
| | | <span v-if="!node.approvers?.length" class="text-muted">æªé
置审æ¹äºº</span> |
| | | |
| | | </div> |
| | | |
| | | <el-icon v-if="index < detailRow.flowNodes.length - 1" class="detail-arrow"><ArrowRight /></el-icon> |
| | | |
| | | </div> |
| | | |
| | | </div> |
| | | |
| | | <el-empty v-else description="ææ æµç¨èç¹" :image-size="60" /> |
| | | |
| | | <el-divider content-position="left">éä»¶ï¼{{ detailAttachments.length }} 个ï¼</el-divider> |
| | | |
| | | <template v-if="detailAttachments.length"> |
| | | |
| | | <div class="detail-attachment-list"> |
| | | <div |
| | | v-for="(f, i) in detailAttachments" |
| | | :key="i" |
| | | class="detail-attachment-item" |
| | | @click="openAttachmentFile(f)" |
| | | > |
| | | <el-icon class="attachment-icon"><Document /></el-icon> |
| | | <span class="attachment-name">{{ attachmentDisplayName(f) }}</span> |
| | | <el-icon class="attachment-download"><Download /></el-icon> |
| | | </div> |
| | | </div> |
| | | |
| | | </template> |
| | | |
| | | <el-empty v-else description="ææ éä»¶" :image-size="48" /> |
| | | |
| | | </div> |
| | | |
| | | <template #footer> |
| | | |
| | | <el-button @click="detailDialog.visible = false">å
³ é</el-button> |
| | | |
| | | <el-button type="primary" @click="editFromDetail">ç¼ è¾</el-button> |
| | | |
| | | </template> |
| | | |
| | | </el-dialog> |
| | | |
| | | </div> |
| | | |
| | | </template> |
| | | |
| | | |
| | | |
| | | <script setup> |
| | | |
| | | import { ArrowRight, Document, Download, Plus, RefreshRight } from "@element-plus/icons-vue"; |
| | | |
| | | import { ElMessage } from "element-plus"; |
| | | |
| | | import { computed, nextTick, onMounted, ref } from "vue"; |
| | | |
| | | 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, mapAttachmentsFromApi } from "./approveTemplateConstants.js"; |
| | | |
| | | import { formatDefaultValueDisplay, formFieldTypeLabel, parseFormConfigToData } from "./formConfigUtils.js"; |
| | | import { selectOptionSourceLabel } from "./selectOptionSource.js"; |
| | | |
| | | import { useApproveTemplate } from "./useApproveTemplate.js"; |
| | | |
| | | |
| | | |
| | | const { |
| | | |
| | | Search, |
| | | |
| | | templateTypeOptions, |
| | | |
| | | loadTemplateTypeOptions, |
| | | |
| | | templateTypeLabel, |
| | | |
| | | nodeSignModeLabel, |
| | | |
| | | searchForm, |
| | | |
| | | tableLoading, |
| | | |
| | | page, |
| | | |
| | | tableData, |
| | | |
| | | tableColumn, |
| | | |
| | | formDialog, |
| | | |
| | | form, |
| | | |
| | | formRef, |
| | | |
| | | formRules, |
| | | |
| | | isEditingBuiltin, |
| | | |
| | | detailDialog, |
| | | |
| | | detailRow, |
| | | |
| | | detailLoading, |
| | | |
| | | fetchTemplateList, |
| | | |
| | | handleQuery, |
| | | |
| | | resetSearch, |
| | | |
| | | pagination, |
| | | |
| | | openFormDialog, |
| | | |
| | | openDetail, |
| | | |
| | | submitForm, |
| | | |
| | | } = useApproveTemplate(); |
| | | |
| | | |
| | | |
| | | const flowUserOptions = ref([]); |
| | | |
| | | |
| | | |
| | | const detailFormConfig = computed(() => |
| | | |
| | | 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 || "æªå½å"; |
| | | |
| | | } |
| | | |
| | | function openAttachmentFile(file) { |
| | | const url = file?.url || file?.previewURL || file?.downloadURL || file?.previewUrl || ""; |
| | | if (url) { |
| | | window.open(url, "_blank"); |
| | | } else { |
| | | ElMessage.warning("æ æ³æå¼è¯¥éä»¶"); |
| | | } |
| | | } |
| | | |
| | | |
| | | 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 = []; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | async function onSubmitForm() { |
| | | |
| | | const ret = await submitForm(); |
| | | |
| | | if (ret?.message) { |
| | | |
| | | ElMessage.warning(ret.message); |
| | | |
| | | return; |
| | | |
| | | } |
| | | |
| | | if (ret?.ok) ElMessage.success("ä¿åæå"); |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | function onFormDialogClosed() { |
| | | |
| | | formRef.value?.resetFields?.(); |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | async function editFromDetail() { |
| | | |
| | | const row = detailRow.value; |
| | | |
| | | detailDialog.visible = false; |
| | | |
| | | await nextTick(); |
| | | |
| | | openFormDialog("edit", row); |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | onMounted(() => { |
| | | |
| | | loadUsers(); |
| | | |
| | | loadTemplateTypeOptions(); |
| | | |
| | | fetchTemplateList(); |
| | | |
| | | }); |
| | | |
| | | </script> |
| | | |
| | | |
| | | |
| | | <style scoped> |
| | | |
| | | .mb20 { |
| | | |
| | | margin-bottom: 20px; |
| | | |
| | | } |
| | | |
| | | .mb16 { |
| | | |
| | | margin-bottom: 16px; |
| | | |
| | | } |
| | | |
| | | .mb16.el-empty { |
| | | |
| | | padding: 8px 0; |
| | | |
| | | } |
| | | |
| | | .ml10 { |
| | | |
| | | margin-left: 10px; |
| | | |
| | | } |
| | | |
| | | .ml12 { |
| | | |
| | | margin-left: 12px; |
| | | |
| | | } |
| | | |
| | | .search_form { |
| | | |
| | | display: flex; |
| | | |
| | | flex-wrap: wrap; |
| | | |
| | | align-items: center; |
| | | |
| | | justify-content: space-between; |
| | | |
| | | gap: 12px; |
| | | |
| | | } |
| | | |
| | | .search_fields { |
| | | |
| | | display: flex; |
| | | |
| | | flex-wrap: wrap; |
| | | |
| | | align-items: center; |
| | | |
| | | gap: 4px; |
| | | |
| | | } |
| | | |
| | | .search_actions { |
| | | |
| | | display: flex; |
| | | |
| | | gap: 8px; |
| | | |
| | | } |
| | | |
| | | .flow-tip { |
| | | |
| | | font-size: 12px; |
| | | |
| | | color: var(--el-text-color-secondary); |
| | | |
| | | margin: 8px 0 0; |
| | | |
| | | line-height: 1.5; |
| | | |
| | | } |
| | | |
| | | .detail-flow { |
| | | |
| | | display: flex; |
| | | |
| | | flex-wrap: wrap; |
| | | |
| | | align-items: flex-start; |
| | | |
| | | gap: 8px; |
| | | |
| | | } |
| | | |
| | | .detail-node { |
| | | |
| | | position: relative; |
| | | |
| | | min-width: 180px; |
| | | |
| | | max-width: 240px; |
| | | |
| | | padding: 12px; |
| | | |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | |
| | | border-radius: 8px; |
| | | |
| | | background: var(--el-fill-color-lighter); |
| | | |
| | | } |
| | | |
| | | .detail-node-head { |
| | | |
| | | display: flex; |
| | | |
| | | align-items: center; |
| | | |
| | | justify-content: space-between; |
| | | |
| | | margin-bottom: 8px; |
| | | |
| | | } |
| | | |
| | | .detail-node-order { |
| | | |
| | | font-weight: 600; |
| | | |
| | | font-size: 13px; |
| | | |
| | | } |
| | | |
| | | .detail-approvers { |
| | | |
| | | display: flex; |
| | | |
| | | flex-wrap: wrap; |
| | | |
| | | gap: 4px; |
| | | |
| | | } |
| | | |
| | | .detail-approver-tag { |
| | | |
| | | margin: 0; |
| | | |
| | | } |
| | | |
| | | .detail-arrow { |
| | | |
| | | position: absolute; |
| | | |
| | | right: -20px; |
| | | |
| | | top: 50%; |
| | | |
| | | transform: translateY(-50%); |
| | | |
| | | color: var(--el-text-color-placeholder); |
| | | |
| | | } |
| | | |
| | | .detail-dialog-body { |
| | | |
| | | min-height: 120px; |
| | | |
| | | } |
| | | |
| | | .upload-block { |
| | | |
| | | width: 100%; |
| | | |
| | | } |
| | | |
| | | .detail-attachment-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .detail-attachment-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | padding: 8px 12px; |
| | | background: var(--el-fill-color-light); |
| | | border-radius: 6px; |
| | | cursor: pointer; |
| | | transition: background 0.2s; |
| | | } |
| | | |
| | | .detail-attachment-item:hover { |
| | | background: var(--el-fill-color); |
| | | } |
| | | |
| | | .attachment-icon { |
| | | font-size: 16px; |
| | | color: var(--el-text-color-regular); |
| | | } |
| | | |
| | | .attachment-name { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-primary); |
| | | } |
| | | |
| | | .attachment-download { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-secondary); |
| | | } |
| | | |
| | | .text-muted { |
| | | |
| | | font-size: 12px; |
| | | |
| | | color: var(--el-text-color-placeholder); |
| | | |
| | | } |
| | | |
| | | .template-form-dialog :deep(.el-dialog__body) { |
| | | |
| | | padding-top: 8px; |
| | | |
| | | } |
| | | |
| | | </style> |
| | | |