| | |
| | | {{ formatFileSize(row.size) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="100"> |
| | | <el-table-column label="操作" width="180"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | type="text" |
| | | size="small" |
| | | class="preview-btn" |
| | | @click="handlePreview(row)" |
| | | > |
| | | 预览 |
| | | </el-button> |
| | | <div class="file-actions"> |
| | | <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> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | <div class="title">交易信息备注</div> |
| | | <el-table :data="form.items" border class="mt10 remark-table"> |
| | | <el-table-column label="详情" prop="name" min-width="200" /> |
| | | <el-table-column label="授权开始时间" width="200"> |
| | | <template #default="{ row, $index }"> |
| | | <el-date-picker |
| | | v-model="form.items[$index].start" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="选择日期" |
| | | size="small" |
| | | style="width: 100%" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="授权结束时间" width="200"> |
| | | <template #default="{ row, $index }"> |
| | | <div class="end-time-wrapper"> |
| | | <el-date-picker |
| | | v-model="form.items[$index].end" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="选择日期" |
| | | size="small" |
| | | style="width: 100%" |
| | | :disabled="form.items[$index].forever" |
| | | /> |
| | | <el-checkbox |
| | | v-model="form.items[$index].forever" |
| | | class="forever-checkbox" |
| | | @change="handleForeverChange($index)" |
| | | > |
| | | 永久 |
| | | </el-checkbox> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="备注" min-width="300"> |
| | | <template #default="{ row, $index }"> |
| | | <el-input |
| | | v-model="form.items[$index].remark" |
| | | placeholder="请输入备注" |
| | | size="small" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="授权开始时间" width="200"> |
| | | <template #default="{ row, $index }"> |
| | | <span>{{ form.items[$index].start || '-' }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="授权结束时间" width="200"> |
| | | <template #default="{ row, $index }"> |
| | | <div class="end-time-wrapper"> |
| | | <span v-if="form.items[$index].forever" class="forever-text">永久</span> |
| | | <span v-else>{{ form.items[$index].end || '-' }}</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="备注" min-width="300"> |
| | | <template #default="{ row, $index }"> |
| | | <span>{{ form.items[$index].remark || '-' }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="action-buttons"> |
| | | <el-button @click="goBack">返回</el-button> |
| | |
| | | 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, confirmTrade } from '@/api/tradeManage' |
| | | import { ElMessage } from 'element-plus' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import orderApi from '@/api/orderApi' |
| | | import pointsApi from '@/api/pointsApi' // 新增积分API导入 |
| | | import { useUserInfo } from '@/stores/modules/userInfo' |
| | | import createAxios from '@/utils/axios' |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const userStore = useUserInfo() |
| | | const detail = reactive<any>({ items: [] }) |
| | | const form = reactive<any>({ items: [] }) |
| | | const fileList = ref<any[]>([]) |
| | |
| | | return [...detail.items, summaryRow] |
| | | }) |
| | | |
| | | onMounted(async () => { |
| | | // 使用前端模拟数据以便开发 UI(不改动后端服务) |
| | | const mockDetail = { |
| | | orderNo: '4348442557619205545', |
| | | resourceTypeName: '软件产品', |
| | | status: 'WAIT_CONFIRM', |
| | | 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 statusServerToUi: Record<string, string> = { |
| | | '待上传文件': 'WAIT_UPLOAD', |
| | | '待授权': 'WAIT_AUTHORIZE', |
| | | '待交易确认': 'WAIT_CONFIRM', |
| | | '已完成': 'COMPLETED', |
| | | '已评价': 'EVALUATED', |
| | | } |
| | | |
| | | Object.assign(detail, mockDetail) |
| | | form.items = (detail.items || []).map((item: any, index: number) => ({ |
| | | name: item.name, |
| | | start: '2025-06-01', |
| | | end: index === 0 || index === 3 ? '' : '2025-06-01', |
| | | forever: index === 0 || index === 3, |
| | | remark: index === 3 ? '开通管理员账号1个,账号admin' : '开通管理员账号1个,账号admin,登录管理员账号可管理普通用户' |
| | | })) |
| | | 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 () => { |
| | | const orderId = String(route.params.id || '') |
| | | if (!orderId) return |
| | | |
| | | // 添加模拟文件数据用于展示 |
| | | fileList.value = [ |
| | | { |
| | | name: '签字盖章文件.pdf', |
| | | size: 2621440, // 2.5MB |
| | | uid: '1', |
| | | status: 'success' |
| | | }, |
| | | { |
| | | name: 'API Keys.txt', |
| | | size: 354, // 354 Bytes |
| | | uid: '2', |
| | | status: 'success' |
| | | try { |
| | | const res = (await orderApi.getOrderDetail(orderId)) as any |
| | | const data = res?.data || {} |
| | | |
| | | const statusName: string = data.orderStatus || '' |
| | | const uiStatus = statusServerToUi[statusName] || 'INFO' |
| | | |
| | | // 映射订单详情头部信息 |
| | | 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 || '-', |
| | | } |
| | | ] |
| | | |
| | | // 注释掉原有的API调用,使用模拟数据 |
| | | // const { data } = (await fetchOrderDetail({ id: route.params.id })) as any |
| | | // Object.assign(detail, data || {}) |
| | | // form.items = (detail.items || []).map(() => ({ start: '', end: '', forever: false, remark: '' })) |
| | | |
| | | // 明细项映射 |
| | | 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), |
| | | remarks: d.remarks || '', // 新增 remarks 字段 |
| | | } |
| | | }) |
| | | : [] |
| | | |
| | | // 汇总(简单相加:单价*数量) |
| | | 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(), |
| | | }) |
| | | |
| | | // 初始化表单数据 |
| | | form.items = (detail.items || []).map((item: any, index: number) => { |
| | | // 计算授权结束时间 |
| | | let endDate = '' |
| | | if (item.period > 0) { |
| | | // 如果有期限,计算结束时间 |
| | | const startDate = new Date(data.applyTime || new Date()) |
| | | const endDateObj = new Date(startDate) |
| | | endDateObj.setFullYear(endDateObj.getFullYear() + item.period) |
| | | endDate = endDateObj.toISOString().split('T')[0] // 格式化为 YYYY-MM-DD |
| | | } |
| | | |
| | | return { |
| | | name: item.name, |
| | | start: data.applyTime ? data.applyTime.split('T')[0] : '', // 使用订单申请时间 |
| | | end: endDate, |
| | | forever: item.period === 0, // 期限为0时设置为永久 |
| | | remark: item.remarks || '' // 使用套件信息中的remarks字段 |
| | | } |
| | | }) |
| | | |
| | | // 获取订单附件列表(如果有的话) |
| | | if (data.attachments && Array.isArray(data.attachments)) { |
| | | fileList.value = data.attachments.map((file: any) => ({ |
| | | name: file.fileName || file.originalName, |
| | | size: file.fileSize || 0, |
| | | uid: file.id, |
| | | status: 'success', |
| | | url: file.fileUrl, |
| | | uploaded: true |
| | | })) |
| | | } else { |
| | | fileList.value = [] |
| | | } |
| | | } catch (error) { |
| | | console.error('获取订单详情失败:', error) |
| | | ElMessage.error('获取订单详情失败') |
| | | } |
| | | }) |
| | | |
| | | const goBack = () => router.back() |
| | | const submit = async () => { |
| | | // 模拟提交成功响应 |
| | | const mockResponse = { code: 200 } |
| | | |
| | | // 注释掉原有的API调用,使用模拟数据 |
| | | // const { code } = (await confirmTrade({ id: route.params.id, ...form })) as any |
| | | |
| | | if (mockResponse.code === 200) { |
| | | ElMessage.success('提交成功') |
| | | router.back() |
| | | try { |
| | | const orderId = String(route.params.id || '') |
| | | const userId = userStore.getUserId ? Number(userStore.getUserId) : undefined |
| | | |
| | | if (!orderId || !userId) { |
| | | ElMessage.error('订单ID或用户ID不能为空') |
| | | return |
| | | } |
| | | |
| | | // 检查订单是否涉及积分扣减 |
| | | const hasPointsDeduction = detail.items && detail.items.some((item: any) => Number(item.pricePoint || 0) > 0) |
| | | |
| | | if (hasPointsDeduction) { |
| | | // 计算需要扣减的积分总额 |
| | | const totalPointsToDeduct = detail.items.reduce((sum: number, item: any) => { |
| | | return sum + (Number(item.pricePoint || 0) * Number(item.quantity || 0)) |
| | | }, 0) |
| | | |
| | | if (totalPointsToDeduct > 0) { |
| | | // 获取当前用户积分余额 |
| | | try { |
| | | const userPointsRes = await pointsApi.getUserPoints(userId) |
| | | if (userPointsRes.code !== 200 || !userPointsRes.data) { |
| | | ElMessage.error('获取积分余额失败') |
| | | return |
| | | } |
| | | |
| | | const currentPoints = userPointsRes.data.balance || 0 |
| | | |
| | | if (currentPoints < totalPointsToDeduct) { |
| | | ElMessage.error(`积分余额不足!当前积分:${currentPoints.toLocaleString()},需要积分:${totalPointsToDeduct.toLocaleString()}`) |
| | | return |
| | | } |
| | | |
| | | // 积分余额充足,继续执行 |
| | | console.log(`积分余额检查通过:当前${currentPoints},需要${totalPointsToDeduct}`) |
| | | } catch (error) { |
| | | console.error('获取积分余额失败:', error) |
| | | ElMessage.error('获取积分余额失败,请重试') |
| | | return |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 确认操作 |
| | | await ElMessageBox.confirm('确定要确认交易?', '确认操作', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }) |
| | | |
| | | // 如果涉及积分扣减,先进行积分扣减和流水记录 |
| | | if (hasPointsDeduction) { |
| | | const totalPointsToDeduct = detail.items.reduce((sum: number, item: any) => { |
| | | return sum + (Number(item.pricePoint || 0) * Number(item.quantity || 0)) |
| | | }, 0) |
| | | |
| | | if (totalPointsToDeduct > 0) { |
| | | try { |
| | | // 获取用户单位ID |
| | | const unitId = userStore.getUserInfo?.unitId || userStore.getUserInfo?.unitId || '' |
| | | |
| | | // 获取当前订购的产品名称作为备注(使用产品名称,不是套件名称) |
| | | const productNames = detail.productName || '订单交易扣减积分' |
| | | |
| | | // 先执行积分扣减 |
| | | const deductRes: any = await pointsApi.deductPointsByFlow( |
| | | userId.toString(), |
| | | unitId, |
| | | totalPointsToDeduct, |
| | | orderId, |
| | | productNames || '订单交易扣减积分', // 使用产品名称作为备注 |
| | | '积分交易' // 数据类别 |
| | | ) |
| | | if (!deductRes || deductRes.code !== 200) { |
| | | ElMessage.error(deductRes?.msg || deductRes?.message || '积分扣减失败,交易确认终止') |
| | | return |
| | | } |
| | | console.log(`积分扣减成功:${totalPointsToDeduct}`) |
| | | } catch (error) { |
| | | console.error('积分扣减失败:', error) |
| | | ElMessage.error('积分扣减失败,交易确认终止') |
| | | return |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 积分扣减成功后,更新订单状态进入下一个状态 |
| | | await orderApi.updateOrderStatusToNext(orderId) |
| | | |
| | | ElMessage.success('交易确认成功') |
| | | router.back() |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | console.error('交易确认失败:', error) |
| | | ElMessage.error('交易确认失败') |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| | | }; |
| | | |
| | | // 文件预览处理 |
| | | const handlePreview = (file: any) => { |
| | | ElMessage.info(`预览文件:${file.name}`) |
| | | // 判断文件是否可预览 |
| | | 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 handleForeverChange = (index: number) => { |
| | | if (form.items[index].forever) { |
| | | form.items[index].end = '' |
| | | // 判断文件是否已上传成功 |
| | | const isFileUploaded = (file: any) => { |
| | | // 文件有url且状态为success表示已上传成功 |
| | | return file.url && (file.status === 'success' || file.uploaded) |
| | | } |
| | | |
| | | // 文件预览 |
| | | 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 |
| | | 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 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('下载失败,请重试') |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | // 症结与修复说明: |
| | | // 1) Element Plus 的 el-table 子列 width 百分比是相对于表格容器的像素宽度计算,但只有在表格容器有明确宽度时才生效。 |
| | |
| | | text-align: left !important; |
| | | } |
| | | |
| | | /* 文件操作按钮样式 */ |
| | | .file-actions { |
| | | display: flex; |
| | | gap: 8px; |
| | | align-items: center; |
| | | justify-content: center; |
| | | |
| | | .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; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /* 交易信息备注表格样式 */ |
| | | .remark-table { |
| | | width: 100%; |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | .forever-checkbox { |
| | | margin-left: 10px; |
| | | .forever-text { |
| | | color: #409eff; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |