From dd1e55aea32ed4e731612bc5ce79fee5442cbd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E5=BC=BA?= <357099073@qq.com> Date: Tue, 21 Jul 2026 11:07:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package-lock.json | 20 +++++ backend/package.json | 1 + backend/src/main.js | 3 + .../apps/oa/reimburse/components/detail.vue | 90 +++++++++++++------ 4 files changed, 88 insertions(+), 26 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index a8fdbb2..cfd78c6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,6 +20,7 @@ "marked": "^16.4.1", "os": "^0.1.2", "pinia": "^3.0.3", + "v-viewer": "^3.0.11", "vue": "^3.5.22", "vue-img-cutter": "^3.0.7", "vue-router": "^4.6.3", @@ -7079,6 +7080,25 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v-viewer": { + "version": "3.0.11", + "resolved": "https://registry.npmmirror.com/v-viewer/-/v-viewer-3.0.11.tgz", + "integrity": "sha512-E8LOdAxhzuktt4HB3PswVCccQ1Q1sYHYnLsS6zaJISpb5EvmAFs5sYNfXnDLFxVb5DQ82v4ZlGxkYlseXwWRJw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "viewerjs": "^1.9.0" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/viewerjs": { + "version": "1.11.8", + "resolved": "https://registry.npmmirror.com/viewerjs/-/viewerjs-1.11.8.tgz", + "integrity": "sha512-PLO477UwNThzMek7EQhr7+YONfbgFKj+E6J/+Vr+USzY6cN1WNGQcft0mlIepQXATquvDmH1dOx7aokNyOx/vw==", + "license": "MIT" + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz", diff --git a/backend/package.json b/backend/package.json index 0923ed9..bf1819e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,6 +21,7 @@ "marked": "^16.4.1", "os": "^0.1.2", "pinia": "^3.0.3", + "v-viewer": "^3.0.11", "vue": "^3.5.22", "vue-img-cutter": "^3.0.7", "vue-router": "^4.6.3", diff --git a/backend/src/main.js b/backend/src/main.js index 5029d26..b4742b1 100644 --- a/backend/src/main.js +++ b/backend/src/main.js @@ -5,6 +5,8 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue' import 'element-plus/dist/index.css' // 导入 Element Plus 暗黑模式样式 import 'element-plus/theme-chalk/dark/css-vars.css' +import 'viewerjs/dist/viewer.css' +import VueViewer from 'v-viewer' import '@/assets/less/index.less' import '@/assets/css/all.min.css' import '@/assets/js/all.min.js' @@ -27,6 +29,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.use(pinia) app.use(router) +app.use(VueViewer) // 初始化主题(必须在挂载前执行) // initTheme() diff --git a/backend/src/views/apps/oa/reimburse/components/detail.vue b/backend/src/views/apps/oa/reimburse/components/detail.vue index a40b59b..8ae957f 100644 --- a/backend/src/views/apps/oa/reimburse/components/detail.vue +++ b/backend/src/views/apps/oa/reimburse/components/detail.vue @@ -91,6 +91,15 @@ /> {{ receipt.name }} + + + @@ -98,17 +107,15 @@ -

审批记录

+

审批记录

- (props.detail?.items || []) - .filter(item => item.receipt_url) - .map(item => { - const url = resolveReceiptUrl(item.receipt_url) - const isImage = /\.(jpe?g|png|gif|bmp|webp)(\?|#|$)/i.test(item.receipt_url || '') - return { - id: item.id, - url, - isImage, - name: item.expense_type || `票据 ${item.id}` - } +const receiptList = computed(() => { + const items = props.detail?.items || [] + const list = [] + items.forEach(item => { + if (!item.receipt_url) return + const urls = item.receipt_url.split(',').filter(Boolean) + urls.forEach((url, index) => { + list.push({ + id: `${item.id}-${index}`, + originalId: item.id, // 记录原始明细ID,用于删除时定位 + url: resolveReceiptUrl(url), + isImage: /\.(jpe?g|png|gif|bmp|webp)(\?|#|$)/i.test(url || ''), + name: `${item.expense_type || '票据'} ${index + 1}` + }) }) -) + }) + return list +}) const isReceiptImage = computed(() => receiptPreviewType.value.startsWith('image/')) @@ -192,10 +203,18 @@ const selectReceipt = receipt => { } const openReceiptPreview = row => { - const receipt = receiptList.value.find(item => item.id === row.id) - if (!receipt) return - selectReceipt(receipt) + // 查找列表项,注意这里的 row.id 可能是原始记录ID,或者是多票据生成出的唯一ID + // 如果是从表格点击查看,row 对应的是明细项,其本身可能包含多个票据 + // 我们直接找到该明细对应的第一张票据并打开预览窗口 + const firstReceipt = receiptList.value.find(item => item.originalId === row.id) + + if (!firstReceipt) { + ElMessage.warning('暂无票据可查看') + return + } + receiptPreviewVisible.value = true + selectReceipt(firstReceipt) } const openAddItem = () => { @@ -234,6 +253,11 @@ const uploadInvoice = async (row, file) => { throw new Error(uploadResponse?.msg || '发票上传失败') } + const newUrl = uploadResponse?.data?.url || uploadResponse?.url || '' + // 获取当前已有的 URL 列表 + const existingUrls = row.receipt_url ? row.receipt_url.split(',').filter(Boolean) : [] + existingUrls.push(newUrl) + await updateReimbursementItem(props.detail.id, row.id, { expense_type: row.expense_type, expense_date: row.expense_date, @@ -242,7 +266,7 @@ const uploadInvoice = async (row, file) => { unit_price: row.unit_price || 0, description: row.description || '', receipt_flag: 1, - receipt_url: uploadResponse?.data?.url || uploadResponse?.url || '' + receipt_url: existingUrls.join(',') }) ElMessage.success('发票上传成功') emit('changed', props.detail.id) @@ -253,14 +277,26 @@ const uploadInvoice = async (row, file) => { } } -const removeItem = async row => { +const removeReceipt = async (receipt) => { try { - await ElMessageBox.confirm(`确认删除“${row.expense_type}”这条费用明细吗?`, '删除确认', { type: 'warning' }) - await deleteReimbursementItem(props.detail.id, row.id) - ElMessage.success('费用明细已删除') + await ElMessageBox.confirm('确认删除该票据吗?', '删除确认', { type: 'warning' }) + // 通过原始 ID 查找对应的明细行 + const row = props.detail.items.find(item => item.id === receipt.originalId) + if (!row) return + + // 获取并过滤掉要删除的 URL + const existingUrls = row.receipt_url ? row.receipt_url.split(',').filter(Boolean) : [] + const updatedUrls = existingUrls.filter(url => resolveReceiptUrl(url) !== receipt.url) + + await updateReimbursementItem(props.detail.id, row.id, { + ...row, + receipt_flag: updatedUrls.length > 0 ? 1 : 0, + receipt_url: updatedUrls.join(',') + }) + ElMessage.success('票据已移除') emit('changed', props.detail.id) } catch (error) { - if (error !== 'cancel' && error !== 'close') ElMessage.error(error?.message || '删除费用明细失败') + if (error !== 'cancel' && error !== 'close') ElMessage.error('移除票据失败') } } @@ -304,13 +340,14 @@ const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN', } .receipt-list { - flex: 0 0 180px; + flex: 0 0 200px; display: flex; flex-direction: column; gap: 10px; padding-right: 12px; border-right: 1px solid var(--el-border-color-light); overflow-y: auto; + max-height: 620px; } .receipt-thumb { @@ -370,6 +407,7 @@ const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN', max-width: 100%; max-height: 620px; cursor: zoom-in; + pointer-events: auto; /* 确保点击事件可以传递 */ } .receipt-pdf {