Files
art-design/src/views/crmpc/approval/auth_detil.vue
T
2025-09-11 17:44:37 +08:00

552 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="stocking-show-page">
<div class="boxshadow">
<OrderDetailContent :orderData="orderDialog.orderData" />
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">审批记录</span>
</div>
<div class="approval-timeline">
<div v-for="(rec, idx) in approvalRecords" :key="idx" class="timeline-item">
<div class="left">
<div class="avatar-box">
<span class="initial">{{ rec.name?.slice(0, 1) || '—' }}</span>
<i v-if="rec.approved" class="status-dot iconfont-sys">&#xe621;</i>
<i v-if="rec.rejected" class="status-dot rejected">&#xe83a;</i>
<i v-else-if="rec.pending" class="status-dot pending iconfont-sys">&#xe839;</i>
</div>
</div>
<div class="right">
<div class="row name-role">
<span class="name">{{ rec.name }}</span>
<span class="role">({{ rec.role || rec.type || '审批人' }})</span>
</div>
<div class="row time">{{ rec.time }}</div>
</div>
</div>
<div v-if="!approvalRecords.length" class="empty">暂无审批记录</div>
</div>
</div>
<div class="boxshadow" v-if="status == 'pending'">
<div class="form-title">
<span class="border"></span>
<span class="title">审批</span>
</div>
<div class="">
<ElRow :gutter="12">
<ElCol :span="24">
<ElFormItem label="备注:">
<el-input
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="请输入备注信息"
class="approval-notes"
/>
</ElFormItem>
</ElCol>
</ElRow>
</div>
</div>
<div class="form-card" v-if="status == 'pending'">
<!-- 底部信息 -->
<div class="submit-wrap">
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleSubmit('reject')"
>驳回</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleSubmit('approve')"
>同意</ElButton
>
</div>
</div>
<!-- PDF 预览弹窗 -->
<el-dialog
v-model="pdf.visible"
title="打印预览"
width="80%"
top="40px"
:close-on-click-modal="false"
>
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; justify-content: flex-end; gap: 8px">
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
<el-button @click="download">下载</el-button>
</div>
<iframe
v-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { CrmService } from '@/api/crmApi'
import { commonService } from '@/api/commonApi'
// import { manageService } from '@/api/adminApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { usePdfPreview } from '@/composables/usePdfPreview'
import { router } from '@/router'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
const { pdf, frameRef, download } = usePdfPreview()
const route = useRoute()
const orderDialog = reactive({
orderData: {} as OrderData
})
// const approval_id = route.query.id
const status = route.query.status
const stocking_id = route.query.biz_id
const instance_id = route.query.instance_id
const stockingInfo = ref<any>({})
const approvalNotes = ref('')
const approvalAction = ref('')
const handleSubmit = async (approvalAction: string) => {
try {
// 这里调用审批API
const { message } = await CrmService.stockapprovalpass({
instance_id: instance_id,
action: approvalAction,
comment: approvalNotes.value
})
router.push('/crmpc/approval/stocking_auth')
ElMessage.success(message)
} catch {
ElMessage.error('提交失败')
}
}
// 审批记录(先填充假数据展示)
const approvalRecords = ref<any[]>([])
const getDictItemSelectlist = ref<any[]>([])
const getDictItemSelect = async () => {
const res = await commonService.getDictItemSelect({ dict_id: '18' })
getDictItemSelectlist.value = res
}
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await CrmService.checkStock({ id: stocking_id })
const orderData: OrderData = res
orderDialog.orderData = orderData
stockingInfo.value = res
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
// 审批记录(兼容多种字段)
const resstockapprovallogs = await CrmService.stockapprovallogs({ instance_id: instance_id })
console.log(resstockapprovallogs)
const historyList = resstockapprovallogs
if (historyList.length > 0) {
approvalRecords.value = historyList.map((h: any) => ({
name: h?.initiator_name ? h?.initiator_name : h?.name,
role: h?.index == -1 ? '申请人' : '审批人',
time: h?.finished_at || '',
approved: h?.status === 'approved' || h?.status === 'submitted',
rejected: h?.status === 'rejected',
pending: h?.status != 'approved' && h?.status != 'rejected' && h?.status != 'submitted'
}))
}
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
onMounted(() => {
getDictItemSelect()
getdetilfn()
})
</script>
<style scoped lang="scss">
.stocking-show-page {
min-height: 100vh;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
margin-bottom: 12px;
color: #333333;
.header-left {
flex: 1;
text-align: center;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.sub {
color: #333333;
}
.mr40 {
margin-right: 40px;
}
}
.form-title {
font-weight: bold;
color: #222;
margin-bottom: 15px;
}
.form-title .title {
margin-left: 6px;
}
.items-wrap {
margin-top: 6px;
}
.item-row {
padding: 8px 0;
}
:deep(.el-input.is-disabled .el-input__inner) {
color: var(--el-text-color-regular);
}
.file-preview {
padding: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.contract-file {
width: 100%;
max-width: 400px;
}
.image-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.preview-image:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.document-preview {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: #409eff;
border-radius: 8px;
color: white;
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-name {
font-size: 14px;
color: #333;
word-break: break-all;
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
.submit-wrap {
display: flex;
justify-content: center;
padding: 24px 0 8px;
}
/* 审批记录 时间线 */
.approval-timeline {
padding: 8px 4px 8px 0;
}
.timeline-item {
display: flex;
align-items: flex-start;
gap: 10px;
position: relative;
}
.timeline-item + .timeline-item {
margin-top: 50px;
}
.left {
width: 46px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.avatar-box {
width: 46px;
height: 46px;
background: #3d7bff;
border-radius: 10px;
color: #fff;
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 20px;
}
.status-dot {
position: absolute;
right: -4px;
bottom: -4px;
// width: 14px;
// height: 14px;
font-size: 9px;
padding: 3px 1px;
border-radius: 50%;
background: #22c55e;
border: 2px solid #fff;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
}
.status-dot.pending {
background: #3471ff;
padding: 3px 3px;
}
.status-dot.rejected {
background: #ff0000;
padding: 3px 3px;
}
/* 连接下一个节点的虚线 */
.timeline-item:not(:last-child) .left::after {
content: '';
position: absolute;
top: 52px; /* avatar 46px + 间距 6px */
left: 23px; /* 盒子宽度一半,确保竖线居中 */
width: 0;
height: calc(100% + 16px); /* 加上相邻项之间的间距 */
border-left: 2px dotted #bfbfbf;
}
.right {
flex: 1;
padding-top: 2px;
}
.row.name-role {
color: #222;
}
.name {
font-weight: 600;
}
.role {
color: #666;
margin-left: 4px;
}
.time {
color: #888;
font-size: 12px;
margin-top: 2px;
}
.empty {
color: #999;
padding: 8px 0 4px 4px;
}
.submit-wrap {
display: flex;
justify-content: space-around;
padding: 24px 0 8px;
width: 500px;
margin: auto;
}
.submit-btn {
width: 100px;
height: 44px;
font-size: 16px;
}
</style>