| | |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="申请部门:"> |
| | | <el-select |
| | | disabled |
| | | v-model="form.approveDeptId" |
| | | placeholder="选择部门" |
| | | > |
| | | <el-option |
| | | v-for="user in productOptions" |
| | | :key="user.deptId" |
| | | :label="user.deptName" |
| | | :value="user.deptId" |
| | | /> |
| | | </el-select> |
| | | <el-input v-model="form.approveDeptName" placeholder="申请部门" disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | <el-col :span="24"> |
| | | <el-form-item :label="props.approveType == 5 ? '采购合同号:' : '审批事由:'" prop="approveReason"> |
| | | <el-input v-model="form.approveReason" placeholder="请输入" clearable type="textarea" disabled/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <!-- 审批人选择(动态节点) --> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="申请人:" prop="approveUser"> |
| | | <el-select |
| | | v-model="form.approveUser" |
| | | placeholder="选择人员" |
| | | disabled |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="申请日期:" prop="approveTime"> |
| | | <el-date-picker |
| | | v-model="form.approveTime" |
| | | type="date" |
| | | placeholder="请选择日期" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | disabled |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | <div v-if="currentQuotation.remark" style="margin-top: 20px;"> |
| | | <h4>备注</h4> |
| | | <p>{{ currentQuotation.remark }}</p> |
| | | </div> |
| | | |
| | | <div v-if="quotationAttachments.length > 0" style="margin-top: 20px;"> |
| | | <h4>附件</h4> |
| | | <el-table :data="quotationAttachments" border style="width: 100%"> |
| | | <el-table-column prop="name" label="附件名称" min-width="360" show-overflow-tooltip /> |
| | | <el-table-column label="操作" width="160" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="primary" size="small" @click="downloadAttachment(row)">下载</el-button> |
| | | <el-button link type="primary" size="small" @click="previewAttachment(row)">预览</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </template> |
| | | </template> |
| | |
| | | </el-skeleton> |
| | | </div> |
| | | |
| | | <!-- 用印审批:展示用印申请详情 --> |
| | | <div v-if="isSealApproval" style="margin: 10px 0 18px;"> |
| | | <el-divider content-position="left">用印详情</el-divider> |
| | | <el-skeleton :loading="sealLoading" animated> |
| | | <template #template> |
| | | <el-skeleton-item variant="h3" style="width: 30%" /> |
| | | <el-skeleton-item variant="text" style="width: 100%" /> |
| | | </template> |
| | | <template #default> |
| | | <el-empty v-if="!currentSeal || !currentSeal.applicationNum" description="未查询到对应用印申请详情" /> |
| | | <template v-else> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="申请编号">{{ currentSeal.applicationNum }}</el-descriptions-item> |
| | | <el-descriptions-item label="申请标题">{{ currentSeal.title }}</el-descriptions-item> |
| | | <el-descriptions-item label="用印类型">{{ getSealTypeText(currentSeal.sealType) }}</el-descriptions-item> |
| | | <el-descriptions-item label="紧急程度">{{ getUrgencyText(currentSeal.urgency) }}</el-descriptions-item> |
| | | <el-descriptions-item label="申请原因" :span="2">{{ currentSeal.reason }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <div v-if="currentSeal.commonFileList?.length" style="margin-top: 20px;"> |
| | | <h4>附件</h4> |
| | | <el-table :data="currentSeal.commonFileList" border style="width: 100%"> |
| | | <el-table-column label="附件名称" show-overflow-tooltip> |
| | | <template #default="scope">{{ scope.row.name }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="150" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="previewSealFile(scope.row)">预览</el-button> |
| | | <el-button link type="primary" size="small" @click="downloadSealFile(scope.row)">下载</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </template> |
| | | </template> |
| | | </el-skeleton> |
| | | </div> |
| | | |
| | | <el-form :model="{ activities }" ref="formRef" label-position="top"> |
| | | <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical"> |
| | | <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical"> |
| | | <el-step |
| | | v-for="(activity, index) in activities" |
| | | :key="index" |
| | | finish-status="success" |
| | | :title="getNodeTitle(index, activities.length)" |
| | | :description="activity.approveNodeUser" |
| | | :icon="getNodeIcon(activity, index)" |
| | | v-for="(group, gIndex) in stepGroups" |
| | | :key="gIndex" |
| | | finish-status="success" |
| | | :title="getNodeTitle(gIndex, stepGroups.length)" |
| | | :icon="getNodeIcon(group)" |
| | | > |
| | | <template #icon> |
| | | <el-icon v-if="activity.approveNodeStatus === 2" color="red" :size="22"><WarningFilled /></el-icon> |
| | | <el-icon v-else-if="activity.isShen" color="#1890ff" :size="22"><Edit /></el-icon> |
| | | <el-icon v-else-if="activity.approveNodeStatus === 1" color="#67C23A" :size="26"><Check /></el-icon> |
| | | <el-icon v-else color="#C0C4CC" :size="22"><MoreFilled /></el-icon> |
| | | </template> |
| | | <template #icon> |
| | | <el-icon v-if="group.some(a => a.approveNodeStatus === 2)" color="red" :size="22"><WarningFilled /></el-icon> |
| | | <el-icon v-else-if="group.some(a => a.isShen)" color="#1890ff" :size="22"><Edit /></el-icon> |
| | | <el-icon v-else-if="group.every(a => a.approveNodeStatus === 1)" color="#67C23A" :size="26"><Check /></el-icon> |
| | | <el-icon v-else color="#C0C4CC" :size="22"><MoreFilled /></el-icon> |
| | | </template> |
| | | <template #title> |
| | | <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span> |
| | | <span style="color: #000000">{{ getNodeTitle(gIndex, stepGroups.length) }}</span> |
| | | <el-tag :type="group[0]?.approveMode === 'and' ? 'warning' : 'info'" size="small" style="margin-left: 6px">{{ group[0]?.approveMode === 'and' ? '会签' : '或签' }}</el-tag> |
| | | </template> |
| | | <template #description> |
| | | <div class="node-user"> |
| | | <div class="avatar-wrapper"> |
| | | <img :src="userStore.avatar" class="user-avatar" alt=""/> |
| | | <div v-for="(activity, aIndex) in group" :key="activity.id || aIndex" class="node-user-item"> |
| | | <div class="node-user"> |
| | | <div class="avatar-wrapper"> |
| | | <img :src="userStore.avatar" class="user-avatar" alt=""/> |
| | | </div> |
| | | <span style="color: #000000">{{ activity.approveNodeUser }}({{ activity.isApproval }})</span> |
| | | </div> |
| | | <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span> |
| | | </div> |
| | | <div v-if="!activity.isShen" class="node-reason"> |
| | | <span>审批意见:</span>{{ activity.approveNodeReason }} |
| | | </div> |
| | | <div v-else-if="activity.isShen"> |
| | | <el-form-item |
| | | :prop="'activities.' + index + '.approveNodeReason'" |
| | | :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]" |
| | | > |
| | | <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input> |
| | | </el-form-item> |
| | | <div v-if="!activity.isShen" class="node-reason"> |
| | | <span>审批意见:</span>{{ activity.approveNodeReason }} |
| | | </div> |
| | | <div v-else-if="activity.isShen"> |
| | | <el-form-item |
| | | :prop="'activities.' + getActivityIndex(activity) + '.approveNodeReason'" |
| | | :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]" |
| | | > |
| | | <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input> |
| | | </el-form-item> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </el-step> |
| | |
| | | updateApproveNode |
| | | } from "@/api/collaborativeApproval/approvalProcess.js"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue' |
| | | import { getQuotationDetail, getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js"; |
| | | import filePreview from "@/components/filePreview/index.vue"; |
| | | import { getSealDetail } from "@/api/collaborativeApproval/sealManagement.js"; |
| | | import FilePreview from '@/components/filePreview/index.vue' |
| | | const emit = defineEmits(['close']) |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | |
| | | const formRef = ref(null); |
| | | const userStore = useUserStore() |
| | | const productOptions = ref([]); |
| | | const userList = ref([]) |
| | | const quotationLoading = ref(false) |
| | | const currentQuotation = ref({}) |
| | | const purchaseLoading = ref(false) |
| | | const currentPurchase = ref({}) |
| | | const sealLoading = ref(false) |
| | | const currentSeal = ref({}) |
| | | const filePreviewRef = ref() |
| | | const isQuotationApproval = computed(() => Number(props.approveType) === 6) |
| | | const isPurchaseApproval = computed(() => Number(props.approveType) === 5) |
| | | |
| | | const normalizeQuotationFiles = (raw) => { |
| | | const list = |
| | | (raw && Array.isArray(raw.salesLedgerFiles) && raw.salesLedgerFiles) || |
| | | (raw && Array.isArray(raw.quotationFiles) && raw.quotationFiles) || |
| | | (raw && Array.isArray(raw.fileList) && raw.fileList) || |
| | | (raw && Array.isArray(raw.files) && raw.files) || |
| | | [] |
| | | return list |
| | | .map((item) => ({ |
| | | id: item?.id, |
| | | name: item?.fileName || item?.name || item?.originalName || item?.filename || "附件", |
| | | url: item?.fileUrl || item?.url || item?.path || item?.tempPath, |
| | | })) |
| | | .filter((i) => i.url) |
| | | } |
| | | |
| | | const quotationAttachments = computed(() => normalizeQuotationFiles(currentQuotation.value)) |
| | | |
| | | const downloadAttachment = (row) => { |
| | | proxy.$download.name(row.url) |
| | | } |
| | | const previewAttachment = (row) => { |
| | | filePreviewRef.value?.open?.(row.url) |
| | | } |
| | | const isSealApproval = computed(() => Number(props.approveType) === 9) |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | approveTime: "", |
| | | approveId: "", |
| | | approveUser: "", |
| | | approveDeptId: "", |
| | | approveReason: "", |
| | | checkResult: "", |
| | |
| | | return '审批'; |
| | | }; |
| | | |
| | | // 获取当前激活步骤 |
| | | // 获取当前激活步骤(分组后:含待审人的组下标,无则返回组总数=全部完成) |
| | | const getActiveStep = () => { |
| | | // 如果所有 isShen 都为 false,返回最后一个步骤(全部完成) |
| | | const hasActive = activities.value.some(a => a.isShen === true); |
| | | if (!hasActive) return activities.value.length; |
| | | // 当前节点索引 |
| | | return activities.value.findIndex(a => a.isShen == true); |
| | | const idx = stepGroups.value.findIndex(group => group.some(a => a.isShen === true)); |
| | | if (idx === -1) return stepGroups.value.length; |
| | | return idx; |
| | | }; |
| | | // 步骤icon |
| | | const getNodeIcon = (activity, index) => { |
| | | if (activity.approveNodeStatus === 2) return 'el-icon-warning'; // 不通过 |
| | | if (activity.isShen) return 'Edit'; |
| | | // 步骤icon(组级:任一人驳回/任一人待审) |
| | | const getNodeIcon = (group) => { |
| | | if (group.some(a => a.approveNodeStatus === 2)) return 'el-icon-warning'; // 不通过 |
| | | if (group.some(a => a.isShen)) return 'Edit'; |
| | | return ''; |
| | | }; |
| | | // 审批节点分组(同一 approveNodeOrder = 同一节点的多名审批人,或签/会签) |
| | | const stepGroups = computed(() => { |
| | | const map = new Map(); |
| | | (activities.value || []).forEach(a => { |
| | | const order = a.approveNodeOrder; |
| | | if (!map.has(order)) map.set(order, []); |
| | | map.get(order).push(a); |
| | | }); |
| | | return Array.from(map.values()); |
| | | }); |
| | | // 获取节点成员在 activities 中的索引(用于表单校验 prop) |
| | | const getActivityIndex = (activity) => { |
| | | return activities.value.findIndex(a => a.id === activity.id); |
| | | }; |
| | | |
| | | // 打开弹框 |
| | |
| | | dialogFormVisible.value = true; |
| | | currentQuotation.value = {} |
| | | currentPurchase.value = {} |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | form.value = {...row} |
| | | // 立即清除表单验证状态(因为字段是disabled的,不需要验证) |
| | | nextTick(() => { |
| | |
| | | const quotationNo = row?.approveReason; |
| | | if (quotationNo) { |
| | | quotationLoading.value = true |
| | | getQuotationList({ quotationNo }).then(async (res) => { |
| | | getQuotationList({ quotationNo }).then((res) => { |
| | | const records = res?.data?.records || [] |
| | | const first = records[0] || {} |
| | | currentQuotation.value = first |
| | | if (first?.id && normalizeQuotationFiles(first).length === 0) { |
| | | try { |
| | | const detailRes = await getQuotationDetail({ id: first.id }) |
| | | const detail = detailRes?.data || detailRes || {} |
| | | currentQuotation.value = { ...first, ...detail } |
| | | } catch (e) { |
| | | } |
| | | } |
| | | currentQuotation.value = records[0] || {} |
| | | }).finally(() => { |
| | | quotationLoading.value = false |
| | | }) |
| | |
| | | purchaseLoading.value = false |
| | | }) |
| | | } |
| | | } |
| | | |
| | | // 用印审批:按审批流程 DB 主键(row.id)查用印申请详情 |
| | | if (isSealApproval.value) { |
| | | sealLoading.value = true |
| | | getSealDetail(row.id).then((res) => { |
| | | currentSeal.value = res.data || {} |
| | | }).catch((err) => { |
| | | console.error('查询用印详情失败:', err) |
| | | proxy.$modal.msgError('查询用印详情失败') |
| | | }).finally(() => { |
| | | sealLoading.value = false |
| | | }) |
| | | } |
| | | |
| | | approveProcessDetails(row.approveId).then((res) => { |
| | |
| | | productOptions.value = res.data; |
| | | }); |
| | | }; |
| | | // 用印类型文本 |
| | | const getSealTypeText = (sealType) => { |
| | | const map = { official: '公章', contract: '合同专用章', finance: '财务专用章', legal: '法人章' } |
| | | return map[sealType] || sealType || '未知' |
| | | } |
| | | // 紧急程度文本 |
| | | const getUrgencyText = (urgency) => { |
| | | const map = { normal: '普通', urgent: '紧急', 'very-urgent': '特急' } |
| | | return map[urgency] || urgency || '未知' |
| | | } |
| | | // 预览附件(URL 为完整地址,原样打开,不拼 host) |
| | | const previewSealFile = (row) => { |
| | | const url = row.previewURL || row.previewUrl || row.url |
| | | if (url && filePreviewRef.value) { |
| | | filePreviewRef.value.openUrl(url) |
| | | } else { |
| | | proxy.$modal.msgWarning('文件地址无效,无法预览') |
| | | } |
| | | } |
| | | // 下载附件(锚点方式,URL 为完整地址) |
| | | const downloadSealFile = (row) => { |
| | | const url = row.downloadURL || row.downloadUrl || row.url |
| | | if (url) { |
| | | const filename = row.originalFilename || row.name || row.fileName || 'download' |
| | | const a = document.createElement('a') |
| | | a.href = url |
| | | a.download = filename |
| | | a.target = '_blank' |
| | | a.rel = 'noopener noreferrer' |
| | | document.body.appendChild(a) |
| | | a.click() |
| | | document.body.removeChild(a) |
| | | } else { |
| | | proxy.$modal.msgWarning('文件地址无效,无法下载') |
| | | } |
| | | } |
| | | // 提交审批 |
| | | const submitForm = (status) => { |
| | | const filteredActivities = activities.value.filter(activity => activity.isShen); |
| | |
| | | return; |
| | | } |
| | | currentActivity.approveNodeStatus = status; |
| | | // 判断是否为最后一步 |
| | | const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length-1; |
| | | updateApproveNode({ ...currentActivity, isLast }).then(() => { |
| | | // 流转判断由后端完成,前端不再传 isLast |
| | | updateApproveNode({ ...currentActivity }).then(() => { |
| | | proxy.$modal.msgSuccess("提交成功"); |
| | | closeDia(); |
| | | }); |
| | |
| | | currentQuotation.value = {} |
| | | purchaseLoading.value = false |
| | | currentPurchase.value = {} |
| | | sealLoading.value = false |
| | | currentSeal.value = {} |
| | | emit('close') |
| | | }; |
| | | defineExpose({ |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .node-user-item { |
| | | margin: 8px 0; |
| | | } |
| | | .node-status { |
| | | color: #1890ff; |
| | |
| | | width: 200px; |
| | | height: 60px; |
| | | } |
| | | </style> |
| | | </style> |