<template>
|
<div>
|
<el-dialog
|
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>
|
<!-- 报销金额(仅当 approveType 为 4 时显示) -->
|
<el-row v-if="props.approveType == 4">
|
<el-col :span="24">
|
<el-form-item label="报销金额:" prop="price">
|
<el-input-number
|
v-model="form.price"
|
placeholder="请输入报销金额"
|
:min="0"
|
:precision="2"
|
:step="0.01"
|
style="width: 100%"
|
disabled
|
clearable
|
/>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row v-if="!isPurchaseApproval && !isSalesApproval">
|
<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>
|
<div v-if="isPurchaseApproval" style="margin: 10px 0 18px;">
|
<el-divider content-position="left">采购详情</el-divider>
|
<el-skeleton :loading="purchaseLoading" animated>
|
<template #template>
|
<el-skeleton-item variant="h3" style="width: 30%" />
|
<el-skeleton-item variant="text" style="width: 100%" />
|
<el-skeleton-item variant="text" style="width: 100%" />
|
</template>
|
<template #default>
|
<el-empty v-if="!purchaseDetail || !purchaseDetail.purchaseContractNumber" description="未查询到对应采购详情" />
|
<template v-else>
|
<el-descriptions :column="2" border>
|
<el-descriptions-item label="采购合同号">{{ purchaseDetail.purchaseContractNumber }}</el-descriptions-item>
|
<el-descriptions-item label="供应商名称">{{ purchaseDetail.supplierName }}</el-descriptions-item>
|
<el-descriptions-item label="项目名称">{{ purchaseDetail.projectName }}</el-descriptions-item>
|
<el-descriptions-item label="销售合同号">{{ purchaseDetail.salesContractNo }}</el-descriptions-item>
|
<el-descriptions-item label="签订日期">{{ purchaseDetail.executionDate }}</el-descriptions-item>
|
<el-descriptions-item label="录入日期">{{ purchaseDetail.entryDate }}</el-descriptions-item>
|
<el-descriptions-item label="付款方式">{{ purchaseDetail.paymentMethod }}</el-descriptions-item>
|
<el-descriptions-item label="合同金额" :span="2">
|
<span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
|
¥{{ Number(purchaseDetail.contractAmount ?? 0).toFixed(2) }}
|
</span>
|
</el-descriptions-item>
|
</el-descriptions>
|
|
<div style="margin-top: 20px;">
|
<h4>产品明细</h4>
|
<el-table :data="purchaseProducts" border style="width: 100%">
|
<el-table-column prop="productCategory" label="产品名称" />
|
<el-table-column prop="specificationModel" label="规格型号" />
|
<el-table-column prop="unit" label="单位" />
|
<el-table-column prop="quantity" label="数量" />
|
<el-table-column prop="taxInclusiveUnitPrice" label="含税单价">
|
<template #default="scope">¥{{ Number(scope.row.taxInclusiveUnitPrice ?? 0).toFixed(2) }}</template>
|
</el-table-column>
|
<el-table-column prop="taxInclusiveTotalPrice" label="含税总价">
|
<template #default="scope">¥{{ Number(scope.row.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
</template>
|
</template>
|
</el-skeleton>
|
</div>
|
<div v-if="isSalesApproval" style="margin: 10px 0 18px;">
|
<el-divider content-position="left">销售详情</el-divider>
|
<el-skeleton :loading="salesLoading" animated>
|
<template #template>
|
<el-skeleton-item variant="h3" style="width: 30%" />
|
<el-skeleton-item variant="text" style="width: 100%" />
|
<el-skeleton-item variant="text" style="width: 100%" />
|
</template>
|
<template #default>
|
<el-empty v-if="!salesDetail || !salesDetail.salesContractNo" description="未查询到对应销售详情" />
|
<template v-else>
|
<el-descriptions :column="2" border>
|
<el-descriptions-item label="销售合同号">{{ salesDetail.salesContractNo }}</el-descriptions-item>
|
<el-descriptions-item label="客户合同号">{{ salesDetail.customerContractNo }}</el-descriptions-item>
|
<el-descriptions-item label="客户名称">{{ salesDetail.customerName }}</el-descriptions-item>
|
<el-descriptions-item label="业务员">{{ salesDetail.salesman }}</el-descriptions-item>
|
<el-descriptions-item label="项目名称">{{ salesDetail.projectName }}</el-descriptions-item>
|
<el-descriptions-item label="付款方式">{{ salesDetail.paymentMethod }}</el-descriptions-item>
|
<el-descriptions-item label="录入人">{{ salesEntryPersonDisplay }}</el-descriptions-item>
|
<el-descriptions-item label="录入日期">{{ salesDetail.entryDate }}</el-descriptions-item>
|
<el-descriptions-item label="签订日期">{{ salesDetail.executionDate }}</el-descriptions-item>
|
<el-descriptions-item label="合同金额" :span="2">
|
<span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
|
¥{{ Number(salesDetail.contractAmount ?? 0).toFixed(2) }}
|
</span>
|
</el-descriptions-item>
|
</el-descriptions>
|
|
<div style="margin-top: 20px;">
|
<h4>产品明细</h4>
|
<el-table :data="salesProducts" border style="width: 100%">
|
<el-table-column prop="productCategory" label="产品名称" />
|
<el-table-column prop="specificationModel" label="规格型号" />
|
<el-table-column prop="unit" label="单位" />
|
<el-table-column prop="quantity" label="数量" />
|
<el-table-column prop="taxInclusiveUnitPrice" label="含税单价">
|
<template #default="scope">¥{{ Number(scope.row.taxInclusiveUnitPrice ?? 0).toFixed(2) }}</template>
|
</el-table-column>
|
<el-table-column prop="taxInclusiveTotalPrice" label="含税总价">
|
<template #default="scope">¥{{ Number(scope.row.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</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-step
|
v-for="(activity, index) in activities"
|
:key="index"
|
finish-status="success"
|
:title="getNodeTitle(index, activities.length)"
|
:description="activity.approveNodeUser"
|
:icon="getNodeIcon(activity, index)"
|
>
|
<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 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
|
: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>
|
</template>
|
</el-step>
|
</el-steps>
|
</el-form>
|
<template #footer v-if="operationType === 'approval'">
|
<div class="dialog-footer">
|
<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 { computed, getCurrentInstance, reactive, ref, toRefs } from "vue";
|
import vueEsign from "vue-esign";
|
import {
|
approveProcessDetails,
|
getDept,
|
updateApproveNode
|
} from "@/api/collaborativeApproval/approvalProcess.js";
|
import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
|
import { getSalesByCode } from "@/api/salesManagement/salesLedger.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([])
|
const formRef = ref(null);
|
const userStore = useUserStore()
|
const productOptions = ref([]);
|
const userList = ref([])
|
const purchaseLoading = ref(false)
|
const currentPurchase = ref({})
|
const isPurchaseApproval = computed(() => Number(props.approveType) === 5)
|
const salesLoading = ref(false)
|
const currentSales = ref({})
|
const isSalesApproval = computed(() => Number(props.approveType) === 6)
|
const purchaseDetail = computed(() => currentPurchase.value?.data || currentPurchase.value || {})
|
const purchaseProducts = computed(() => {
|
const detail = purchaseDetail.value || {}
|
return detail.productData || detail.products || detail.children || []
|
})
|
const salesDetail = computed(() => currentSales.value?.data || currentSales.value || {})
|
const salesProducts = computed(() => {
|
const detail = salesDetail.value || {}
|
return detail.productData || detail.products || detail.children || []
|
})
|
/** 销售详情:录入人(接口可能只返 entryPerson 用户ID,需用 userList 反查昵称) */
|
const salesEntryPersonDisplay = computed(() => {
|
const d = salesDetail.value || {}
|
if (d.entryPersonName) return d.entryPersonName
|
if (d.recorderName) return d.recorderName
|
if (d.createByName) return d.createByName
|
if (d.createUserName) return d.createUserName
|
const id = d.entryPerson
|
if (id != null && id !== "" && userList.value?.length) {
|
const u = userList.value.find(
|
(x) => x.userId == id || String(x.userId) === String(id)
|
)
|
if (u) return u.nickName
|
}
|
return id != null && id !== "" ? String(id) : "-"
|
})
|
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 props = defineProps({
|
approveType: {
|
type: [Number, String],
|
default: 0
|
}
|
})
|
|
// 上传配置
|
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;
|
currentPurchase.value = {}
|
currentSales.value = {}
|
userListNoPageByTenantId().then((res) => {
|
userList.value = res.data;
|
});
|
form.value = {...row}
|
getProductOptions()
|
if (isPurchaseApproval.value) {
|
const purchaseContractNumber = row?.approveReason
|
if (purchaseContractNumber) {
|
purchaseLoading.value = true
|
getPurchaseByCode({ purchaseContractNumber }).then((res) => {
|
// 兼容后端返回 {code,data} 或直接返回详情对象两种结构
|
currentPurchase.value = res || {}
|
}).catch(() => {
|
proxy.$modal.msgError("查询采购详情失败")
|
}).finally(() => {
|
purchaseLoading.value = false
|
})
|
}
|
}
|
if (isSalesApproval.value) {
|
const salesContractNo = row?.approveReason
|
if (salesContractNo) {
|
salesLoading.value = true
|
getSalesByCode({ salesContractNo }).then((res) => {
|
currentSales.value = res || {}
|
}).catch(() => {
|
proxy.$modal.msgError("查询销售详情失败")
|
}).finally(() => {
|
salesLoading.value = false
|
})
|
}
|
}
|
approveProcessDetails(row.approveId).then((res) => {
|
activities.value = res.data
|
// 增加isApproval字段
|
activities.value.forEach(item => {
|
if (item.url && 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");
|
dialogFormVisible.value = false;
|
purchaseLoading.value = false
|
currentPurchase.value = {}
|
salesLoading.value = false
|
currentSales.value = {}
|
emit('close')
|
};
|
defineExpose({
|
openDialog,
|
});
|
</script>
|
|
<style scoped>
|
|
.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>
|