| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | :title="operationType === 'add' ? '新增审批流程' : '编辑审批流程'" |
| | | width="700px" |
| | | @close="closeDia" |
| | | v-model="dialogFormVisible" |
| | | :title="operationType === 'add' ? '新增审批流程' : '编辑审批流程'" |
| | | width="700px" |
| | | @close="closeDia" |
| | | > |
| | | <el-form :model="form" label-width="140px" label-position="top" ref="formRef"> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="流程编号:" prop="approveId"> |
| | | <el-input v-model="form.approveId" placeholder="自动编号" clearable disabled/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="申请部门:" prop="approveDeptId"> |
| | | <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-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="审批事由:" 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> |
| | | </el-form> |
| | | <el-form :model="{ activities }" ref="formRef" label-position="top"> |
| | | <el-timeline style="max-width: 600px"> |
| | | <el-timeline-item |
| | | v-for="(activity, index) in activities" |
| | | :key="index" |
| | | :type="activity.current ? 'primary' : ''" |
| | | :hollow="activity.current" |
| | | :timestamp="activity.timestamp" |
| | | <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)" |
| | | > |
| | | <el-card> |
| | | <span style="font-size: 18px;font-weight: 700">{{activity.content}}</span> |
| | | <div style="margin: 10px 0"> |
| | | <span style="font-size: 16px;font-weight: 600">审批人:{{activity.people}}</span> |
| | | <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 #title> |
| | | <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span> |
| | | </template> |
| | | <template #description> |
| | | <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> |
| | | <div> |
| | | <span style="margin-bottom: 8px;display: inline-block;font-size: 16px;font-weight: 600">审批意见:</span> |
| | | <div v-if="!activity.isShen" class="node-reason"> |
| | | <span>审批意见:</span>{{ activity.approveNodeReason }} |
| | | </div> |
| | | <div v-if="!activity.isShen" class="node-reason"> |
| | | <span>签名:</span> |
| | | <img :src="activity.urlTem" class="signImg" alt="" v-if="activity.urlTem"/> |
| | | </div> |
| | | <div v-else-if="activity.isShen"> |
| | | <el-form-item |
| | | v-if="activity.current" |
| | | :prop="'activities.' + index + '.value'" |
| | | :prop="'activities.' + index + '.approveNodeReason'" |
| | | :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]" |
| | | > |
| | | <el-input v-model="activity.value" clearable type="textarea" :disabled="operationType === 'view'"></el-input> |
| | | </el-form-item> |
| | | <el-form-item v-else> |
| | | <el-input v-model="activity.value" clearable type="textarea" disabled></el-input> |
| | | <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input> |
| | | </el-form-item> |
| | | </div> |
| | | </el-card> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | </template> |
| | | </el-step> |
| | | </el-steps> |
| | | </el-form> |
| | | <template #footer v-if="operationType === 'approval'"> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button type="primary" @click="submitForm(2)">不通过</el-button> |
| | | <el-button type="primary" @click="openSignatureDialog(1)">通过</el-button> |
| | | <el-button @click="closeDia">取消</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 电子签名弹窗(vue3-signature-pad) --> |
| | | <el-dialog v-model="signatureDialogVisible" title="电子签名" width="600px" append-to-body> |
| | | <vueEsign |
| | | ref="esign" |
| | | class="mySign" |
| | | :width="800" |
| | | :height="300" |
| | | :isCrop="isCrop" |
| | | :lineWidth="lineWidth" |
| | | :lineColor="lineColor" |
| | | /> |
| | | <div style="margin-top:10px;"> |
| | | <el-button @click="clearSignature">清除</el-button> |
| | | <el-button type="primary" @click="confirmSignature">确定</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {getCurrentInstance, ref} from "vue"; |
| | | import {approveProcessDetails} from "../../../../api/collaborativeApproval/approvalProcess.js"; |
| | | import { getCurrentInstance, reactive, ref, toRefs } from "vue"; |
| | | import vueEsign from "vue-esign"; |
| | | import { |
| | | approveProcessDetails, |
| | | getDept, |
| | | 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 { getToken } from "@/utils/auth"; |
| | | const emit = defineEmits(['close']) |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const activities = ref([ |
| | | { |
| | | content: '节点1', |
| | | timestamp: '', |
| | | type: 'primary', |
| | | hollow: true, |
| | | people: 'admin', |
| | | value: '' |
| | | }, |
| | | { |
| | | content: '节点2', |
| | | timestamp: '', |
| | | type: '', |
| | | hollow: false, |
| | | current: true, |
| | | people: 'admin', |
| | | value: '' |
| | | }, |
| | | ]) |
| | | const activities = ref([]) |
| | | const formRef = ref(null); |
| | | const userStore = useUserStore() |
| | | const productOptions = ref([]); |
| | | const userList = ref([]) |
| | | const data = reactive({ |
| | | form: { |
| | | approveTime: "", |
| | | approveId: "", |
| | | approveUser: "", |
| | | approveDeptId: "", |
| | | approveReason: "", |
| | | checkResult: "", |
| | | }, |
| | | }); |
| | | const { form } = toRefs(data); |
| | | const signatureDialogVisible = ref(false); |
| | | const signatureImg = ref(''); |
| | | let submitStatus = null; // 临时存储通过/不通过状态 |
| | | const isCrop = ref(""); |
| | | const esign = ref(null); |
| | | const lineWidth = ref(0); |
| | | const lineColor = ref("#000000"); |
| | | |
| | | // 上传配置 |
| | | const upload = reactive({ |
| | | // 上传的地址 |
| | | url: import.meta.env.VITE_APP_BASE_API + "/file/upload", |
| | | // 设置上传的请求头部 |
| | | headers: { Authorization: "Bearer " + getToken() }, |
| | | }); |
| | | |
| | | // 节点标题 |
| | | const getNodeTitle = (index, len) => { |
| | | if (index === len - 1) return '结束'; |
| | | 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); |
| | | }; |
| | | // 步骤icon |
| | | const getNodeIcon = (activity, index) => { |
| | | if (activity.approveNodeStatus === 2) return 'el-icon-warning'; // 不通过 |
| | | if (activity.isShen) return 'Edit'; |
| | | return ''; |
| | | }; |
| | | |
| | | // 打开弹框 |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | approveProcessDetails({id: row.approveId}).then((res) => { |
| | | console.log(res) |
| | | }) |
| | | } |
| | | // 提交审批 |
| | | const submitForm = () => { |
| | | formRef.value.validate(valid => { |
| | | if (valid) { |
| | | // 校验通过后的逻辑 |
| | | } |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | form.value = {...row} |
| | | getProductOptions() |
| | | approveProcessDetails(row.approveId).then((res) => { |
| | | activities.value = res.data |
| | | // 增加isApproval字段 |
| | | activities.value.forEach(item => { |
| | | if (item.url.includes('word')) { |
| | | item.urlTem = item.url.replaceAll('word', 'img') |
| | | } else { |
| | | item.urlTem = item.url |
| | | } |
| | | if (item.approveNodeStatus === 2) { |
| | | item.isApproval = '已驳回'; |
| | | } else if (item.approveNodeStatus === 1) { |
| | | item.isApproval = '已同意'; |
| | | } else { |
| | | item.isApproval = '未审批'; |
| | | } |
| | | }) |
| | | }) |
| | | } |
| | | const getProductOptions = () => { |
| | | getDept().then((res) => { |
| | | productOptions.value = res.data; |
| | | }); |
| | | }; |
| | | // 打开签名弹窗 |
| | | const openSignatureDialog = (status) => { |
| | | submitStatus = status; |
| | | signatureDialogVisible.value = true; |
| | | }; |
| | | // 清除签名 |
| | | const clearSignature = () => { |
| | | esign.value.reset(); |
| | | }; |
| | | // 确认签名 |
| | | const confirmSignature = () => { |
| | | esign.value.generate().then((res) => { |
| | | console.log(res); |
| | | // 将base64转换为二进制 |
| | | const base64Data = res.split(',')[1]; // 移除data:image/png;base64,前缀 |
| | | const binaryString = atob(base64Data); |
| | | const bytes = new Uint8Array(binaryString.length); |
| | | for (let i = 0; i < binaryString.length; i++) { |
| | | bytes[i] = binaryString.charCodeAt(i); |
| | | } |
| | | signatureImg.value = bytes; |
| | | |
| | | // 创建文件对象用于上传 |
| | | const blob = new Blob([bytes], { type: 'image/png' }); |
| | | const file = new File([blob], 'signature.png', { type: 'image/png' }); |
| | | |
| | | // 创建FormData |
| | | const formData = new FormData(); |
| | | formData.append('file', file); |
| | | |
| | | // 上传签名图片 |
| | | fetch(upload.url, { |
| | | method: 'POST', |
| | | headers: upload.headers, |
| | | body: formData |
| | | }) |
| | | .then(response => response.json()) |
| | | .then(data => { |
| | | if (data.code === 200) { |
| | | console.log('data---', data) |
| | | let tempFileIds = []; |
| | | tempFileIds.push(data.data.tempId); |
| | | signatureDialogVisible.value = false; |
| | | clearSignature(); |
| | | // 只有通过时才传递签名文件ID |
| | | if (submitStatus === 1) { |
| | | submitForm(submitStatus, tempFileIds); |
| | | } else { |
| | | submitForm(submitStatus); |
| | | } |
| | | } else { |
| | | proxy.$modal.msgError("签名图片上传失败:" + data.msg); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | console.error('上传失败:', error); |
| | | proxy.$modal.msgError("签名图片上传失败"); |
| | | }); |
| | | }).catch((err) => { |
| | | console.log(err); |
| | | proxy.$modal.msgWarning("请先签名!"); |
| | | }) |
| | | }; |
| | | // 提交审批 |
| | | const submitForm = (status, tempFileIds) => { |
| | | const filteredActivities = activities.value.filter(activity => activity.isShen); |
| | | filteredActivities[0].approveNodeStatus = status; |
| | | // 只有通过时才需要签名 |
| | | if (status === 1 && tempFileIds) { |
| | | filteredActivities[0].tempFileIds = tempFileIds; |
| | | } |
| | | // 判断是否为最后一步 |
| | | const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length-1; |
| | | updateApproveNode({ ...filteredActivities[0], isLast }).then(() => { |
| | | proxy.$modal.msgSuccess("提交成功"); |
| | | closeDia(); |
| | | }); |
| | | }; |
| | | // 关闭弹框 |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .el-timeline { |
| | | padding-left: 10px; |
| | | |
| | | .node-user { |
| | | margin: 10px 0; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .node-status { |
| | | color: #1890ff; |
| | | margin-left: 8px; |
| | | font-size: 14px; |
| | | } |
| | | .node-reason { |
| | | font-size: 15px; |
| | | color: #333; |
| | | margin: 10px 0; |
| | | } |
| | | .user-avatar { |
| | | cursor: pointer; |
| | | width: 30px; |
| | | height: 30px; |
| | | border-radius: 50px; |
| | | } |
| | | .signImg { |
| | | cursor: pointer; |
| | | width: 200px; |
| | | height: 60px; |
| | | } |
| | | </style> |