guzhi/web/src/views/valuation/audit/components/CertificateModal.vue

437 lines
9.6 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.

<script setup>
import { ref, watch, computed } from 'vue'
import {
NModal,
NCard,
NButton,
NUpload,
NText,
NImage,
NImageGroup,
useMessage
} from 'naive-ui'
// 临时移除图标导入以解决模块解析问题
// import { DownloadIcon } from '@vicons/tabler'
import { getToken } from '@/utils/auth/token'
import { generateReport } from '@/utils/report'
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
certificateData: {
type: Object,
default: () => ({}),
},
mode: {
type: String,
default: 'upload', // 'upload' 或 'view'
},
})
const emit = defineEmits(['update:visible', 'confirm'])
const message = useMessage()
const reportFileList = ref([])
const certificateFileList = ref([])
const uploadUrl = `${import.meta.env.VITE_BASE_API}/upload/file`
const uploadHeaders = computed(() => ({
Authorization: `Bearer ${getToken()}`,
}))
// 监听弹窗打开,初始化数据
// 监听弹窗打开,初始化数据
watch(
() => props.visible,
(val) => {
if (val) {
if (props.mode === 'view') {
// 查看模式,加载已有数据
reportFileList.value = (props.certificateData?.reportFiles || []).map(f => ({ ...f, status: 'finished' }))
certificateFileList.value = (props.certificateData?.certificateFiles || []).map(f => ({ ...f, status: 'finished' }))
} else {
// 上传模式,清空数据
reportFileList.value = []
certificateFileList.value = []
}
}
}
)
// 关闭弹窗
const handleClose = () => {
emit('update:visible', false)
}
// 确认操作
// 确认操作
const handleConfirm = () => {
const getFiles = (list) => list.map(f => ({
id: f.id,
name: f.name,
url: f.url,
type: f.type
})).filter(f => f.url)
emit('confirm', {
reportFiles: getFiles(reportFileList.value),
certificateFiles: getFiles(certificateFileList.value)
})
handleClose()
}
// 文件上传前的处理
const beforeUpload = (data) => {
const { file } = data
const isImage = file.type.startsWith('image/')
const isPdf = file.type === 'application/pdf'
const isWord = file.type.includes('word') || file.type.includes('document')
const isVideo = file.type.startsWith('video/')
if (!isImage && !isPdf && !isWord && !isVideo) {
message.error('只能上传图片、PDF、Word文档或视频文件')
return false
}
const isLt50M = file.size / 1024 / 1024 < 50
if (!isLt50M) {
message.error('文件大小不能超过50MB')
return false
}
return true
}
// 报告上传完成
// 报告上传完成
const handleReportUploadFinish = ({ file, event }) => {
try {
const res = JSON.parse(event.target.response)
if (res.code === 200 && res.data?.url) {
file.url = res.data.url
file.name = res.data.filename || file.name
file.status = 'finished'
message.success('报告上传成功')
return file
} else {
message.error(res.message || '上传失败')
const index = reportFileList.value.findIndex((item) => item.id === file.id)
if (index > -1) reportFileList.value.splice(index, 1)
}
} catch (error) {
console.error('上传响应解析失败:', error)
message.error('上传失败:服务器响应异常')
const index = reportFileList.value.findIndex((item) => item.id === file.id)
if (index > -1) reportFileList.value.splice(index, 1)
}
}
// 证书上传完成
// 证书上传完成
const handleCertificateUploadFinish = ({ file, event }) => {
try {
const res = JSON.parse(event.target.response)
if (res.code === 200 && res.data?.url) {
file.url = res.data.url
file.name = res.data.filename || file.name
file.status = 'finished'
message.success('证书上传成功')
return file
} else {
message.error(res.message || '上传失败')
const index = certificateFileList.value.findIndex((item) => item.id === file.id)
if (index > -1) certificateFileList.value.splice(index, 1)
}
} catch (error) {
console.error('上传响应解析失败:', error)
message.error('上传失败:服务器响应异常')
const index = certificateFileList.value.findIndex((item) => item.id === file.id)
if (index > -1) certificateFileList.value.splice(index, 1)
}
}
const modalTitle = computed(() => {
return props.mode === 'upload' ? '上传' : '查看'
})
const isUploadMode = computed(() => props.mode === 'upload')
// 下载报告
const handleDownloadReport = async () => {
try {
if (!props.certificateData?.detailData) {
message.error('缺少详情数据,无法生成报告')
return
}
message.loading('正在生成报告...')
await generateReport(props.certificateData.detailData)
message.success('报告生成并下载成功')
} catch (error) {
console.error(error)
message.error('报告生成失败')
}
}
// 文件预览
const handlePreview = (file) => {
// 对于非图片文件,显示提示
if (!file.type?.startsWith('image/')) {
message.info('此文件类型不支持预览,请下载查看')
return false
}
// 图片文件返回 true让 NUpload 使用内置预览
return true
}
</script>
<template>
<NModal
:show="visible"
:mask-closable="false"
preset="card"
:title="modalTitle"
class="certificate-modal"
style="width: 700px"
@update:show="handleClose"
>
<!-- 上传/查看模式 -->
<div class="certificate-content">
<!-- 报告上传部分 -->
<div class="upload-section">
<div class="section-header">
<div class="section-title">报告:</div>
<NButton text type="primary" @click="handleDownloadReport">
点击下载原版报告
</NButton>
</div>
<div class="upload-content">
<NUpload
v-model:file-list="reportFileList"
max="1"
list-type="image-card"
:action="uploadUrl"
:headers="uploadHeaders"
:before-upload="beforeUpload"
@finish="handleReportUploadFinish"
:disabled="!isUploadMode"
show-preview-button
show-download-button
@download="handleDownload"
>
</NUpload>
</div>
</div>
<!-- 证书上传部分 -->
<div class="upload-section">
<div class="section-title">证书:</div>
<div class="upload-content">
<NUpload
v-model:file-list="certificateFileList"
list-type="image-card"
:action="uploadUrl"
:headers="uploadHeaders"
:before-upload="beforeUpload"
@finish="handleCertificateUploadFinish"
max="1"
:disabled="!isUploadMode"
show-preview-button
show-download-button
@download="handleDownload"
>
</NUpload>
</div>
</div>
</div>
<template #footer>
<div class="modal-footer">
<NButton @click="handleClose">取消</NButton>
<NButton v-if="isUploadMode" type="primary" @click="handleConfirm">
上传并通知
</NButton>
<NButton v-else type="primary" @click="handleConfirm">
确定
</NButton>
</div>
</template>
</NModal>
</template>
<style scoped>
.certificate-modal {
max-width: 90vw;
}
.certificate-content {
display: flex;
flex-direction: column;
gap: 24px;
padding: 16px 0;
}
/* 上传模式样式 */
.upload-section {
margin-bottom: 20px;
}
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.section-title {
font-size: 14px;
color: #666;
font-weight: normal;
margin: 0;
}
.upload-content {
min-height: 60px;
}
.report-upload-area,
.certificate-upload-area {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.download-section {
flex: 0 0 auto;
}
.file-section {
flex: 1;
min-width: 200px;
}
.uploaded-file {
padding: 6px 12px;
background: #f5f5f5;
border-radius: 4px;
display: inline-block;
margin-bottom: 8px;
}
.file-name {
font-size: 14px;
color: #333;
font-weight: 500;
}
.certificate-preview {
width: 60px;
height: 60px;
border-radius: 6px;
overflow: hidden;
position: relative;
border: 1px solid #e8e8e8;
flex: 0 0 auto;
}
.file-preview-icon {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
color: #666;
font-size: 12px;
font-weight: 600;
border-radius: 6px;
}
.remove-button {
position: absolute;
top: -8px;
right: -8px;
width: 20px;
height: 20px;
background: #ff4d4f;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 12px;
line-height: 1;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.remove-button:hover {
background: #ff7875;
}
/* 查看模式样式 */
.view-section {
margin-bottom: 24px;
}
.view-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.download-area {
display: flex;
justify-content: flex-start;
}
.file-info {
padding: 8px 12px;
background: #f5f5f5;
border-radius: 4px;
display: inline-block;
}
/* 底部按钮 */
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 缩略图上传组件样式 */
:deep(.n-upload) {
width: 100%;
}
:deep(.n-upload-file-list) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 8px;
}
:deep(.n-upload-file-card) {
aspect-ratio: 1;
border-radius: 6px;
overflow: hidden;
}
/* 响应式调整 */
@media (max-width: 768px) {
.certificate-modal {
width: 95vw !important;
}
}
</style>