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 {