| | |
| | | :on-exceed="onExceed" |
| | | :on-remove="handleRemove" |
| | | :show-file-list="false" |
| | | :before-upload="beforeUpload" |
| | | > |
| | | <el-button type="primary">选择文件</el-button> |
| | | </el-upload> |
| | |
| | | {{ formatFileSize(row.size) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="100"> |
| | | <el-table-column label="操作" width="280"> |
| | | <template #default="{ row, $index }"> |
| | | <el-button |
| | | type="text" |
| | | size="small" |
| | | class="delete-btn" |
| | | @click="handleRemove(row, fileList)" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | <div class="file-actions"> |
| | | <el-button |
| | | type="text" |
| | | size="small" |
| | | class="upload-btn" |
| | | @click="handleUpload(row)" |
| | | v-if="!row.status || row.status === 'ready'" |
| | | :loading="row.uploading" |
| | | > |
| | | {{ row.uploading ? '上传中' : '上传' }} |
| | | </el-button> |
| | | <el-button |
| | | type="text" |
| | | size="small" |
| | | class="preview-btn" |
| | | @click="handlePreview(row)" |
| | | v-if="isPreviewable(row)" |
| | | :disabled="!isFileUploaded(row)" |
| | | > |
| | | 预览 |
| | | </el-button> |
| | | <el-button |
| | | type="text" |
| | | size="small" |
| | | class="download-btn" |
| | | @click="handleDownload(row)" |
| | | :disabled="!isFileUploaded(row)" |
| | | > |
| | | 下载 |
| | | </el-button> |
| | | <el-button |
| | | type="text" |
| | | size="small" |
| | | class="delete-btn" |
| | | @click="handleRemove(row, fileList)" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | import { onMounted, reactive, ref, computed, type CSSProperties } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { Document, User, Goods, List } from '@element-plus/icons-vue' |
| | | import { fetchOrderDetail, uploadTradeFile } from '@/api/tradeManage' |
| | | import { ElMessage } from 'element-plus' |
| | | import { uploadTradeFile } from '@/api/tradeManage' |
| | | import orderApi from '@/api/orderApi' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useUserInfo } from '@/stores/modules/userInfo' |
| | | import createAxios from '@/utils/axios' |
| | | import productApi from "@/api/productApi"; |
| | | import sysUserService from "@/api/sysUser"; |
| | | import {queryUserDetail} from "@/api/userInfo"; |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const userStore = useUserInfo() |
| | | const detail = reactive<any>({ items: [] }) |
| | | const fileList = ref<any[]>([]) |
| | | const orderTableWrapRef = ref<HTMLElement | null>(null) |
| | |
| | | return [...detail.items, summaryRow] |
| | | }) |
| | | |
| | | // 状态映射(后端中文 -> 前端枚举) |
| | | const statusServerToUi: Record<string, string> = { |
| | | '待上传文件': 'WAIT_UPLOAD', |
| | | '待授权': 'WAIT_AUTHORIZE', |
| | | '待交易确认': 'WAIT_CONFIRM', |
| | | '已完成': 'COMPLETED', |
| | | '已评价': 'EVALUATED', |
| | | } |
| | | |
| | | const formatDateTime = (val?: string) => (val ? val.replace('T', ' ').slice(0, 19) : '') |
| | | |
| | | const normalizePriceType = (val?: string): 'points' | 'currency' | 'agreement' | 'free' => { |
| | | if (!val) return 'currency' |
| | | const s = String(val) |
| | | if (/(积分|points)/i.test(s)) return 'points' |
| | | if (/(协议|agreement)/i.test(s)) return 'agreement' |
| | | if (/(免费|free)/i.test(s)) return 'free' |
| | | return 'currency' |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | // 使用前端模拟数据以便开发 UI(不改动后端服务) |
| | | const mockDetail = { |
| | | orderNo: '4348442557619205545', |
| | | resourceTypeName: '软件产品', |
| | | status: 'WAIT_UPLOAD', |
| | | statusName: '待上传文件', |
| | | applyTime: '2025-05-21 10:00:00', |
| | | unitName: '中交方远建设有限公司', |
| | | userName: '张静', |
| | | userAccount: 'L20159922', |
| | | userDept: '信息中心', |
| | | userPhone: '13800000000', |
| | | productName: '中交方远智能实测实量管理系统', |
| | | supplier: '中交方远科技有限公司', |
| | | industry: '建筑工程', |
| | | projectUnit: '土建工程', |
| | | productType: '软件库', |
| | | productDesc: |
| | | '面向工程项目的质量实测实量数字化管理系统,支持标准化采集、自动统计与过程管控。', |
| | | items: [ |
| | | { |
| | | id: '1', |
| | | name: '企业私有SaaS版许可', |
| | | saleType: '买断', |
| | | accountCount: 50, |
| | | customerTarget: '企业', |
| | | concurrentNodes: 50, |
| | | pricePoint: 50000, |
| | | priceCash: 0, |
| | | quantity: 1, |
| | | period: 0, |
| | | }, |
| | | { |
| | | id: '2', |
| | | name: '企业私有SaaS版OTA升级服务', |
| | | saleType: 'OTA服务', |
| | | accountCount: 50, |
| | | customerTarget: '企业', |
| | | concurrentNodes: 50, |
| | | pricePoint: 0, |
| | | priceCash: 7500, |
| | | quantity: 1, |
| | | period: 1, |
| | | }, |
| | | { |
| | | id: '3', |
| | | name: '企业私有SaaS版用户增量包', |
| | | saleType: '私有增量包', |
| | | accountCount: 100, |
| | | customerTarget: '企业', |
| | | concurrentNodes: 100, |
| | | pricePoint: 0, |
| | | priceCash: 0, |
| | | priceProtocol: true, |
| | | quantity: 1, |
| | | period: 1, |
| | | }, |
| | | { |
| | | id: '4', |
| | | name: '个人公有SaaS版许可', |
| | | saleType: '私有增量包', |
| | | accountCount: 50, |
| | | customerTarget: '个人', |
| | | concurrentNodes: 50, |
| | | pricePoint: 0, |
| | | priceCash: 0, |
| | | quantity: 1, |
| | | period: 0, |
| | | }, |
| | | ], |
| | | pointTotal: '50,000', |
| | | cashTotal: '7,500', |
| | | const orderId = String(route.params.id || '') |
| | | if (!orderId) { |
| | | ElMessage.error('订单ID不能为空') |
| | | return |
| | | } |
| | | |
| | | Object.assign(detail, mockDetail) |
| | | |
| | | // 添加模拟文件数据用于展示 |
| | | fileList.value = [ |
| | | { |
| | | name: '签字盖章文件.pdf', |
| | | size: 2621440, // 2.5MB |
| | | uid: '1', |
| | | status: 'success' |
| | | }, |
| | | { |
| | | name: 'API Keys.txt', |
| | | size: 354, // 354 Bytes |
| | | uid: '2', |
| | | status: 'success' |
| | | // 获取用户信息 |
| | | if (!userStore.getUserId) { |
| | | try { |
| | | const res: any = await queryUserDetail() |
| | | if (res?.code === 200 && res.data) { |
| | | userStore.updateUserDetail(res.data) |
| | | } else { |
| | | ElMessage.error(res?.msg || '无法获取用户信息,请先登录') |
| | | return |
| | | } |
| | | } catch (e) { |
| | | console.error('获取用户详情失败:', e) |
| | | ElMessage.error('获取用户信息失败,请稍后重试') |
| | | return |
| | | } |
| | | ] |
| | | |
| | | // 注释掉原有的API调用,使用模拟数据 |
| | | // const { data } = (await fetchOrderDetail({ id: route.params.id })) as any |
| | | // Object.assign(detail, data || {}) |
| | | } |
| | | |
| | | try { |
| | | const res = (await orderApi.getOrderDetail(orderId)) as any |
| | | const data = res?.data || {} |
| | | |
| | | const statusName: string = data.orderStatus || '' |
| | | const uiStatus = statusServerToUi[statusName] || 'INFO' |
| | | |
| | | // 根据产品id获取产品信息,更新头部展示 |
| | | try { |
| | | if (data.productId) { |
| | | const detailRes: any = await productApi.getProductById({ id: data.productId }) |
| | | if (detailRes?.code === 200 && detailRes.data) { |
| | | // 用产品详情补全头信息 |
| | | data.productName = detailRes.data.name || data.productName |
| | | data.providerName = detailRes.data.submissionUnit || data.providerName |
| | | data.industry = detailRes.data.industrialChainName || data.industry |
| | | data.productDesc = detailRes.data.describe || data.productDesc |
| | | data.projectUnit = detailRes.data.importantAreaName || data.productDesc |
| | | data.productType = detailRes.data.typeName || data.productDesc |
| | | } |
| | | } |
| | | } catch (e) { |
| | | // 忽略产品详情失败,不阻塞订单详情 |
| | | } |
| | | |
| | | // 获取用户信息 |
| | | try { |
| | | const userRes: any = await sysUserService.getUserdetail({ userId: data.userId }) |
| | | if (userRes?.code === 200 && userRes.data) { |
| | | // 用产品详情补全头信息 |
| | | data.unitName = userRes.data.unitName || data.unitName |
| | | data.userName = userRes.data.name || data.userName |
| | | data.userDept = userRes.data.departmentName || data.userDept |
| | | data.userPhone = userRes.data.phone || data.userPhone |
| | | data.userAccount = userRes.data.username || data.userAccount |
| | | } |
| | | }catch (e){ |
| | | |
| | | } |
| | | |
| | | // 映射订单详情头部信息 |
| | | const head = { |
| | | orderNo: data.orderId, |
| | | resourceTypeName: '软件产品', |
| | | status: uiStatus, |
| | | statusName, |
| | | applyTime: formatDateTime(data.applyTime), |
| | | unitName: data.unitName || '-', |
| | | userName: data.userName || '-', |
| | | userAccount: data.userAccount || '-', |
| | | userDept: data.userDept || '-', |
| | | userPhone: data.userPhone || '-', |
| | | productName: data.productName || '-', |
| | | supplier: data.providerName || '-', |
| | | industry: data.industry || '-', |
| | | projectUnit: data.projectUnit || '-', |
| | | productType: data.productType || '-', |
| | | productDesc: data.productDesc || '-', |
| | | } |
| | | |
| | | // 明细项映射 |
| | | const items: any[] = Array.isArray(data.orderDetails) |
| | | ? data.orderDetails.map((d: any, idx: number) => { |
| | | const pt = normalizePriceType(d.priceType) |
| | | return { |
| | | id: String(d.id ?? idx + 1), |
| | | name: d.suiteName, |
| | | saleType: d.salesForm, |
| | | accountCount: d.accountLimit, |
| | | customerTarget: d.customerType, |
| | | concurrentNodes: d.concurrentNodes, |
| | | pricePoint: pt === 'points' ? Number(d.unitPrice || 0) : 0, |
| | | priceCash: pt === 'currency' ? Number(d.unitPrice || 0) : 0, |
| | | priceProtocol: pt === 'agreement', |
| | | quantity: Number(d.quantity || 0), |
| | | period: Number(d.duration || 0), |
| | | } |
| | | }) |
| | | : [] |
| | | |
| | | // 汇总(简单相加:单价*数量) |
| | | const pointTotalNum = items.reduce((sum, it) => sum + Number(it.pricePoint || 0) * Number(it.quantity || 0), 0) |
| | | const cashTotalNum = items.reduce((sum, it) => sum + Number(it.priceCash || 0) * Number(it.quantity || 0), 0) |
| | | |
| | | Object.assign(detail, head, { |
| | | items, |
| | | pointTotal: pointTotalNum.toLocaleString(), |
| | | cashTotal: cashTotalNum.toLocaleString(), |
| | | }) |
| | | |
| | | // 如果有已上传的文件,显示在文件列表中 |
| | | if (data.attachments && Array.isArray(data.attachments)) { |
| | | fileList.value = data.attachments.map((file: any) => ({ |
| | | name: file.fileName, |
| | | size: file.fileSize, |
| | | uid: file.id, |
| | | status: 'success', |
| | | url: file.fileUrl |
| | | })) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取订单详情失败:', error) |
| | | ElMessage.error('获取订单详情失败') |
| | | } |
| | | }) |
| | | |
| | | const onExceed = () => ElMessage.warning('最多选择5个文件') |
| | | |
| | | // 文件上传前的验证 |
| | | const beforeUpload = (file: File) => { |
| | | // 检查文件大小(限制为100MB) |
| | | const maxSize = 500 * 1024 * 1024 |
| | | if (file.size > maxSize) { |
| | | ElMessage.error('文件大小不能超过500MB') |
| | | return false |
| | | } |
| | | |
| | | // 检查文件名长度 |
| | | if (file.name.length > 100) { |
| | | ElMessage.error('文件名称不能超过100字符') |
| | | return false |
| | | } |
| | | |
| | | // 设置文件初始状态 |
| | | const fileObj = { |
| | | name: file.name, |
| | | size: file.size, |
| | | type: file.type, |
| | | raw: file, |
| | | status: 'ready', // 初始状态为准备上传 |
| | | uploading: false, |
| | | uploaded: false |
| | | } |
| | | |
| | | // 将文件添加到文件列表 |
| | | fileList.value.push(fileObj) |
| | | |
| | | return false // 阻止自动上传,改为手动上传 |
| | | } |
| | | |
| | | // 上传单个文件到服务器 |
| | | const uploadSingleFile = async (file: File) => { |
| | | const formData = new FormData() |
| | | formData.append('file', file) |
| | | formData.append('folder', 'order-attachments') |
| | | |
| | | try { |
| | | console.log('开始上传文件:', file.name, '大小:', file.size) |
| | | |
| | | const response = await createAxios({ |
| | | url: '/admin/file/upload', |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'multipart/form-data' |
| | | }, |
| | | data: formData |
| | | }) |
| | | |
| | | console.log('文件上传响应:', response) |
| | | |
| | | // 检查响应格式 - 根据实际返回格式调整 |
| | | const responseData = response as any |
| | | |
| | | // 根据实际响应格式,直接检查 responseData.code |
| | | if (responseData && responseData.code === 200) { |
| | | console.log('文件上传成功,返回数据:', responseData.data) |
| | | return responseData.data // 返回文件URL |
| | | } else if (responseData && responseData.data && responseData.data.code === 200) { |
| | | // 备用检查:如果响应被包装在 data 中 |
| | | console.log('文件上传成功,返回数据:', responseData.data.data) |
| | | return responseData.data.data // 返回文件URL |
| | | } else { |
| | | // 处理错误情况 |
| | | const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '文件上传失败' |
| | | console.error('文件上传失败,错误信息:', errorMsg) |
| | | throw new Error(errorMsg) |
| | | } |
| | | } catch (error) { |
| | | console.error('文件上传异常:', error) |
| | | throw error |
| | | } |
| | | } |
| | | |
| | | // 保存文件信息到数据库 |
| | | const saveFileInfo = async (fileData: any) => { |
| | | try { |
| | | console.log('开始保存文件信息:', fileData) |
| | | |
| | | // 使用FormData格式,因为后端使用@RequestParam接收参数 |
| | | const formData = new FormData() |
| | | formData.append('orderId', fileData.orderId) |
| | | formData.append('fileName', fileData.fileName) |
| | | formData.append('originalName', fileData.originalName) |
| | | formData.append('fileType', fileData.fileType) |
| | | formData.append('fileSize', fileData.fileSize.toString()) |
| | | formData.append('fileUrl', fileData.fileUrl) |
| | | formData.append('bucketName', fileData.bucketName) |
| | | formData.append('objectName', fileData.objectName) |
| | | formData.append('uploadUserId', fileData.uploadUserId.toString()) |
| | | formData.append('uploadUserName', fileData.uploadUserName) |
| | | formData.append('attachmentType', fileData.attachmentType) |
| | | formData.append('description', fileData.description) |
| | | |
| | | console.log('准备发送的文件信息:', { |
| | | orderId: fileData.orderId, |
| | | fileName: fileData.fileName, |
| | | fileSize: fileData.fileSize, |
| | | fileUrl: fileData.fileUrl, |
| | | uploadUserId: fileData.uploadUserId, |
| | | uploadUserName: fileData.uploadUserName |
| | | }) |
| | | |
| | | const response = await createAxios({ |
| | | url: '/admin/api/order/attachment/upload', |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'multipart/form-data' |
| | | }, |
| | | data: formData |
| | | }) |
| | | |
| | | console.log('保存文件信息响应:', response) |
| | | |
| | | // 使用与文件上传相同的响应判断逻辑 |
| | | const responseData = response as any |
| | | |
| | | if (responseData && responseData.code === 200) { |
| | | console.log('文件信息保存成功,返回的附件ID:', responseData.data) |
| | | const attachmentId = responseData.data |
| | | |
| | | // 验证attachmentId是否为有效的数字 |
| | | if (typeof attachmentId === 'number' && attachmentId > 0) { |
| | | return attachmentId |
| | | } else { |
| | | console.error('返回的附件ID不是有效数字:', attachmentId, typeof attachmentId) |
| | | throw new Error(`无效的附件ID: ${attachmentId}`) |
| | | } |
| | | } else if (responseData && responseData.data && responseData.data.code === 200) { |
| | | // 备用检查:如果响应被包装在 data 中 |
| | | console.log('文件信息保存成功(备用检查),返回的附件ID:', responseData.data.data) |
| | | const attachmentId = responseData.data.data |
| | | |
| | | // 验证attachmentId是否为有效的数字 |
| | | if (typeof attachmentId === 'number' && attachmentId > 0) { |
| | | return attachmentId |
| | | } else { |
| | | console.error('返回的附件ID不是有效数字(备用检查):', attachmentId, typeof attachmentId) |
| | | throw new Error(`无效的附件ID: ${attachmentId}`) |
| | | } |
| | | } else { |
| | | // 处理错误情况 |
| | | const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '保存文件信息失败' |
| | | console.error('文件信息保存失败:', errorMsg) |
| | | throw new Error(errorMsg) |
| | | } |
| | | } catch (error) { |
| | | console.error('保存文件信息异常:', error) |
| | | throw error |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | const goBack = () => router.back() |
| | | |
| | | // 提交文件并更新订单状态 |
| | | const submit = async () => { |
| | | // 模拟提交成功响应 |
| | | const mockResponse = { code: 200 } |
| | | |
| | | // 注释掉原有的API调用,使用模拟数据 |
| | | // const { code } = (await uploadTradeFile({ id: route.params.id, files: fileList.value })) as any |
| | | |
| | | if (mockResponse.code === 200) { |
| | | if (fileList.value.length === 0) { |
| | | ElMessage.warning('请至少上传一个文件') |
| | | return |
| | | } |
| | | |
| | | // 检查是否有未上传的文件 |
| | | const unuploadedFiles = fileList.value.filter(file => !file.uploaded && !file.url) |
| | | if (unuploadedFiles.length > 0) { |
| | | ElMessage.warning('请先上传所有文件') |
| | | return |
| | | } |
| | | |
| | | try { |
| | | const orderId = String(route.params.id || '') |
| | | const userId = userStore.getUserId |
| | | const userName = userStore.username || userStore.name || '未知用户' |
| | | |
| | | // 上传所有未上传的文件 |
| | | const uploadPromises = fileList.value |
| | | .filter(fileItem => fileItem.raw && !fileItem.uploaded) |
| | | .map(async (fileItem) => { |
| | | const fileUrl = await uploadSingleFile(fileItem.raw) |
| | | |
| | | // 保存文件信息到数据库 |
| | | const attachmentData = { |
| | | orderId: orderId, |
| | | fileName: fileItem.name, |
| | | originalName: fileItem.name, |
| | | fileType: fileItem.type || 'application/octet-stream', |
| | | fileSize: fileItem.size, |
| | | fileUrl: fileUrl, |
| | | bucketName: 'dev', |
| | | objectName: fileUrl.split('/').pop(), |
| | | uploadUserId: userId, |
| | | uploadUserName: userName, |
| | | attachmentType: '其他', |
| | | description: '交易文件' |
| | | } |
| | | |
| | | await saveFileInfo(attachmentData) |
| | | }) |
| | | |
| | | await Promise.all(uploadPromises) |
| | | |
| | | // 更新订单状态进入下一个状态 |
| | | await orderApi.updateOrderStatusToNext(orderId) |
| | | |
| | | ElMessage.success('提交成功') |
| | | router.back() |
| | | } catch (error) { |
| | | console.error('提交失败:', error) |
| | | ElMessage.error(error instanceof Error ? error.message : '提交失败') |
| | | } |
| | | } |
| | | |
| | |
| | | // 文件列表表格表体文字大小 |
| | | const fileTableCellStyle: CSSProperties = { fontSize: '12px' }; |
| | | |
| | | // 判断文件是否可预览 |
| | | const isPreviewable = (file: any) => { |
| | | // 首先检查MIME类型 |
| | | const previewableTypes = [ |
| | | 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', |
| | | 'text/plain', 'text/html', 'text/css', 'text/javascript', |
| | | 'application/pdf', |
| | | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx |
| | | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx |
| | | 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx |
| | | ] |
| | | |
| | | // 如果MIME类型匹配,直接返回true |
| | | if (previewableTypes.includes(file.type || '')) { |
| | | return true |
| | | } |
| | | |
| | | // 如果MIME类型为空或不匹配,根据文件扩展名判断 |
| | | const fileName = file.name || '' |
| | | const fileExtension = fileName.toLowerCase().split('.').pop() |
| | | |
| | | const previewableExtensions = [ |
| | | 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', |
| | | 'txt', 'html', 'htm', 'css', 'js', |
| | | 'pdf', |
| | | 'docx', 'xlsx', 'pptx' |
| | | ] |
| | | |
| | | return previewableExtensions.includes(fileExtension) |
| | | } |
| | | |
| | | // 文件预览 |
| | | const handlePreview = async (file: any) => { |
| | | if (!file.url) { |
| | | ElMessage.warning('文件链接不存在') |
| | | return |
| | | } |
| | | |
| | | // 获取文件扩展名 |
| | | const fileName = file.name || '' |
| | | const fileExtension = fileName.toLowerCase().split('.').pop() |
| | | |
| | | let previewUrl = file.url |
| | | |
| | | // 如果文件存储在MinIO,优先使用预览URL |
| | | if (file.url.includes('order-attachments')) { |
| | | try { |
| | | // 首先尝试获取预览URL |
| | | const previewResponse = await createAxios({ |
| | | url: `/admin/file/preview`, |
| | | method: 'GET', |
| | | params: { |
| | | fileName: file.url |
| | | } |
| | | }) |
| | | |
| | | console.log('预览URL响应:', previewResponse) |
| | | |
| | | // 检查响应格式 |
| | | const responseData = previewResponse as any |
| | | if (responseData && responseData.code === 200 && responseData.data) { |
| | | previewUrl = responseData.data.replaceAll("http://192.168.20.52:9000", import.meta.env.VITE_FILE_PREVIEW_URL) |
| | | console.log('使用预览URL:', previewUrl) |
| | | } else { |
| | | console.log('预览URL获取失败,使用下载方式') |
| | | // 如果预览URL获取失败,回退到下载方式 |
| | | const response = await createAxios({ |
| | | url: `/admin/file/download`, |
| | | method: 'GET', |
| | | responseType: 'blob', |
| | | params: { |
| | | fileName: file.url, |
| | | originalName: file.name |
| | | } |
| | | }) |
| | | |
| | | // 创建预览URL |
| | | const blob = new Blob([response as any]) |
| | | previewUrl = window.URL.createObjectURL(blob) |
| | | console.log('使用Blob URL:', previewUrl) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取文件失败:', error) |
| | | ElMessage.error('获取文件失败,无法预览') |
| | | return |
| | | } |
| | | } |
| | | |
| | | // 图片文件直接在新窗口打开 |
| | | if ((file.type && file.type.startsWith('image/')) || |
| | | ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension)) { |
| | | console.log('预览图片文件:', previewUrl) |
| | | window.open(previewUrl, '_blank') |
| | | return |
| | | } |
| | | |
| | | // PDF文件在新窗口打开 |
| | | if (file.type === 'application/pdf' || fileExtension === 'pdf') { |
| | | console.log('预览PDF文件:', previewUrl) |
| | | window.open(previewUrl, '_blank') |
| | | return |
| | | } |
| | | |
| | | // 文本文件在新窗口打开 |
| | | if ((file.type && file.type.startsWith('text/')) || |
| | | ['txt', 'html', 'htm', 'css', 'js'].includes(fileExtension)) { |
| | | console.log('预览文本文件:', previewUrl) |
| | | window.open(previewUrl, '_blank') |
| | | return |
| | | } |
| | | |
| | | // Office文档和其他文件类型,尝试在新窗口打开 |
| | | try { |
| | | console.log('预览其他文件:', previewUrl) |
| | | window.open(previewUrl, '_blank') |
| | | } catch (error) { |
| | | console.error('预览失败:', error) |
| | | ElMessage.error('预览失败,请下载后查看') |
| | | } |
| | | } |
| | | |
| | | // 判断文件是否已上传成功 |
| | | const isFileUploaded = (file: any) => { |
| | | // 文件有url且状态为success表示已上传成功 |
| | | return file.url && (file.status === 'success' || file.uploaded) |
| | | } |
| | | |
| | | // 单个文件上传 |
| | | const handleUpload = async (file: any) => { |
| | | if (!file.raw) { |
| | | ElMessage.warning('文件数据不存在') |
| | | return |
| | | } |
| | | |
| | | // 设置上传状态 |
| | | file.uploading = true |
| | | |
| | | try { |
| | | // 上传文件到服务器 |
| | | const fileUrl = await uploadSingleFile(file.raw) |
| | | |
| | | // 保存文件信息到数据库 |
| | | const orderId = String(route.params.id || '') |
| | | const userId = userStore.getUserId |
| | | const userName = userStore.username || userStore.name || '未知用户' |
| | | |
| | | const attachmentData = { |
| | | orderId: orderId, |
| | | fileName: file.name, |
| | | originalName: file.name, |
| | | fileType: file.type || 'application/octet-stream', |
| | | fileSize: file.size, |
| | | fileUrl: fileUrl, |
| | | bucketName: 'dev', |
| | | objectName: fileUrl.split('/').pop(), |
| | | uploadUserId: userId, |
| | | uploadUserName: userName, |
| | | attachmentType: '其他', |
| | | description: '交易文件' |
| | | } |
| | | |
| | | const attachmentId = await saveFileInfo(attachmentData) |
| | | |
| | | // 更新文件状态 |
| | | file.url = fileUrl |
| | | file.uid = attachmentId // 设置正确的附件ID |
| | | file.status = 'success' |
| | | file.uploaded = true |
| | | file.uploading = false |
| | | |
| | | ElMessage.success('文件上传成功') |
| | | } catch (error) { |
| | | console.error('文件上传失败:', error) |
| | | file.uploading = false |
| | | ElMessage.error(error instanceof Error ? error.message : '文件上传失败') |
| | | } |
| | | } |
| | | |
| | | // 文件下载 |
| | | const handleDownload = async (file: any) => { |
| | | if (!file.url) { |
| | | ElMessage.warning('文件链接不存在') |
| | | return |
| | | } |
| | | |
| | | console.log('开始下载文件:', file.name, 'URL:', file.url) |
| | | |
| | | try { |
| | | // 如果文件存储在MinIO,使用后端直接下载API |
| | | if (file.url.includes('order-attachments')) { |
| | | console.log('使用MinIO下载API') |
| | | |
| | | // 使用axios通过代理访问后端API |
| | | const response = await createAxios({ |
| | | url: `/admin/file/download`, |
| | | method: 'GET', |
| | | responseType: 'blob', |
| | | params: { |
| | | fileName: file.url, |
| | | originalName: file.name |
| | | } |
| | | }) |
| | | |
| | | console.log('下载响应:', response) |
| | | |
| | | // 创建下载链接 |
| | | const blob = new Blob([response as any]) |
| | | const downloadUrl = window.URL.createObjectURL(blob) |
| | | |
| | | console.log('创建下载链接:', downloadUrl) |
| | | |
| | | const link = document.createElement('a') |
| | | link.href = downloadUrl |
| | | link.download = file.name || 'download' |
| | | link.target = '_blank' |
| | | link.rel = 'noopener noreferrer' |
| | | |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | |
| | | // 清理URL对象 |
| | | window.URL.revokeObjectURL(downloadUrl) |
| | | |
| | | ElMessage.success('文件下载成功') |
| | | } else { |
| | | console.log('使用直接URL下载') |
| | | // 其他情况直接使用原URL |
| | | const link = document.createElement('a') |
| | | link.href = file.url |
| | | link.download = file.name || 'download' |
| | | link.target = '_blank' |
| | | link.rel = 'noopener noreferrer' |
| | | |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | |
| | | ElMessage.success('开始下载文件') |
| | | } |
| | | } catch (error) { |
| | | console.error('下载失败:', error) |
| | | ElMessage.error('下载失败,请重试') |
| | | } |
| | | } |
| | | |
| | | // 删除MinIO文件 |
| | | const deleteMinioFile = async (fileName: string) => { |
| | | try { |
| | | console.log('开始删除MinIO文件:', fileName) |
| | | |
| | | const response = await createAxios({ |
| | | url: `/admin/file/delete`, |
| | | method: 'DELETE', |
| | | params: { |
| | | fileName: fileName |
| | | } |
| | | }) |
| | | |
| | | console.log('删除MinIO文件响应:', response) |
| | | |
| | | const responseData = response as any |
| | | if (responseData && responseData.code === 200) { |
| | | console.log('MinIO文件删除成功') |
| | | return true |
| | | } else { |
| | | const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '删除MinIO文件失败' |
| | | console.error('删除MinIO文件失败:', errorMsg) |
| | | throw new Error(errorMsg) |
| | | } |
| | | } catch (error) { |
| | | console.error('删除MinIO文件异常:', error) |
| | | throw error |
| | | } |
| | | } |
| | | |
| | | // 删除数据库附件记录 |
| | | const deleteAttachmentRecord = async (attachmentId: number) => { |
| | | try { |
| | | console.log('开始删除数据库附件记录:', attachmentId) |
| | | |
| | | const response = await createAxios({ |
| | | url: `/admin/api/order/attachment/delete/${attachmentId}`, |
| | | method: 'DELETE' |
| | | }) |
| | | |
| | | console.log('删除数据库附件记录响应:', response) |
| | | |
| | | const responseData = response as any |
| | | if (responseData && responseData.code === 200) { |
| | | console.log('数据库附件记录删除成功') |
| | | return true |
| | | } else { |
| | | const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '删除数据库附件记录失败' |
| | | console.error('删除数据库附件记录失败:', errorMsg) |
| | | throw new Error(errorMsg) |
| | | } |
| | | } catch (error) { |
| | | console.error('删除数据库附件记录异常:', error) |
| | | throw error |
| | | } |
| | | } |
| | | |
| | | // 文件列表移除文件 |
| | | const handleRemove = (file: any, uploadFiles: any) => { |
| | | // 从文件列表中移除指定文件 |
| | | const index = fileList.value.findIndex(item => item.uid === file.uid) |
| | | if (index > -1) { |
| | | fileList.value.splice(index, 1) |
| | | ElMessage.success('文件已删除') |
| | | const handleRemove = async (file: any, uploadFiles: any) => { |
| | | try { |
| | | // 显示确认对话框 |
| | | await ElMessageBox.confirm( |
| | | `确定要删除文件 "${file.name}" 吗?`, |
| | | '删除确认', |
| | | { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning', |
| | | center: true |
| | | } |
| | | ) |
| | | |
| | | // 用户确认后执行删除操作 |
| | | // 如果是已上传的文件(有uid且为数字,表示数据库记录ID) |
| | | if (file.uid && !isNaN(file.uid) && file.url) { |
| | | console.log('删除已上传的文件:', file.name, '附件ID:', file.uid) |
| | | |
| | | // 1. 删除MinIO中的文件 |
| | | if (file.url.includes('order-attachments')) { |
| | | await deleteMinioFile(file.url) |
| | | } |
| | | |
| | | // 2. 删除数据库中的附件记录 |
| | | await deleteAttachmentRecord(file.uid) |
| | | |
| | | // 3. 从文件列表中移除 |
| | | const index = fileList.value.findIndex(item => item.uid === file.uid) |
| | | if (index > -1) { |
| | | fileList.value.splice(index, 1) |
| | | } |
| | | |
| | | ElMessage.success('文件删除成功') |
| | | } else { |
| | | // 如果是未上传的文件(只有raw文件对象) |
| | | console.log('删除未上传的文件:', file.name) |
| | | |
| | | // 直接从文件列表中移除 |
| | | const index = fileList.value.findIndex(item => item.name === file.name && item.size === file.size) |
| | | if (index > -1) { |
| | | fileList.value.splice(index, 1) |
| | | } |
| | | |
| | | ElMessage.success('文件已删除') |
| | | } |
| | | } catch (error) { |
| | | // 如果用户取消删除,error为'cancel',不需要显示错误消息 |
| | | if (error === 'cancel') { |
| | | console.log('用户取消删除操作') |
| | | return |
| | | } |
| | | |
| | | console.error('删除文件失败:', error) |
| | | ElMessage.error(error instanceof Error ? error.message : '删除文件失败') |
| | | } |
| | | } |
| | | </script> |
| | |
| | | color: #409eff; |
| | | } |
| | | } |
| | | .delete-btn { |
| | | color: #f56c6c; |
| | | &:hover { |
| | | text-decoration: underline; |
| | | .file-actions { |
| | | display: flex; |
| | | gap: 8px; |
| | | align-items: center; |
| | | justify-content: center; |
| | | |
| | | .upload-btn { |
| | | color: #e6a23c; |
| | | &:hover { |
| | | text-decoration: underline; |
| | | } |
| | | &:disabled { |
| | | color: #c0c4cc; |
| | | cursor: not-allowed; |
| | | } |
| | | } |
| | | |
| | | .preview-btn { |
| | | color: #409eff; |
| | | &:hover { |
| | | text-decoration: underline; |
| | | } |
| | | &:disabled { |
| | | color: #c0c4cc; |
| | | cursor: not-allowed; |
| | | } |
| | | } |
| | | |
| | | .download-btn { |
| | | color: #67c23a; |
| | | &:hover { |
| | | text-decoration: underline; |
| | | } |
| | | &:disabled { |
| | | color: #c0c4cc; |
| | | cursor: not-allowed; |
| | | } |
| | | } |
| | | |
| | | .delete-btn { |
| | | color: #f56c6c; |
| | | &:hover { |
| | | text-decoration: underline; |
| | | } |
| | | } |
| | | } |
| | | } |