更新样式

This commit is contained in:
2026-07-21 11:07:33 +08:00
parent a78666177a
commit dd1e55aea3
4 changed files with 88 additions and 26 deletions
+20
View File
@@ -20,6 +20,7 @@
"marked": "^16.4.1", "marked": "^16.4.1",
"os": "^0.1.2", "os": "^0.1.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"v-viewer": "^3.0.11",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-img-cutter": "^3.0.7", "vue-img-cutter": "^3.0.7",
"vue-router": "^4.6.3", "vue-router": "^4.6.3",
@@ -7079,6 +7080,25 @@
"uuid": "dist/bin/uuid" "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": { "node_modules/vite": {
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz",
+1
View File
@@ -21,6 +21,7 @@
"marked": "^16.4.1", "marked": "^16.4.1",
"os": "^0.1.2", "os": "^0.1.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"v-viewer": "^3.0.11",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-img-cutter": "^3.0.7", "vue-img-cutter": "^3.0.7",
"vue-router": "^4.6.3", "vue-router": "^4.6.3",
+3
View File
@@ -5,6 +5,8 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
// 导入 Element Plus 暗黑模式样式 // 导入 Element Plus 暗黑模式样式
import 'element-plus/theme-chalk/dark/css-vars.css' 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/less/index.less'
import '@/assets/css/all.min.css' import '@/assets/css/all.min.css'
import '@/assets/js/all.min.js' import '@/assets/js/all.min.js'
@@ -27,6 +29,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.use(pinia) app.use(pinia)
app.use(router) app.use(router)
app.use(VueViewer)
// 初始化主题(必须在挂载前执行) // 初始化主题(必须在挂载前执行)
// initTheme() // initTheme()
@@ -91,6 +91,15 @@
/> />
<i v-else class="fa-regular fa-file-pdf receipt-thumb-pdf"></i> <i v-else class="fa-regular fa-file-pdf receipt-thumb-pdf"></i>
<span>{{ receipt.name }}</span> <span>{{ receipt.name }}</span>
<el-button
v-if="isDraft"
link
type="danger"
class="receipt-delete-btn"
@click.stop="removeReceipt(receipt)"
>
<i class="fa-solid fa-trash"></i>
</el-button>
</button> </button>
</aside> </aside>
@@ -98,17 +107,15 @@
<el-image <el-image
v-if="isReceiptImage" v-if="isReceiptImage"
:src="receiptPreviewUrl" :src="receiptPreviewUrl"
:preview-src-list="[receiptPreviewUrl]"
fit="contain" fit="contain"
class="receipt-image" class="receipt-image"
preview-teleported
/> />
<iframe v-else :src="receiptPreviewUrl" title="票据 PDF 预览" class="receipt-pdf"></iframe> <iframe v-else :src="receiptPreviewUrl" title="票据 PDF 预览" class="receipt-pdf"></iframe>
</main> </main>
</div> </div>
</el-dialog> </el-dialog>
<h4 class="detail-title">审批记录</h4> <h4 class="detail-title" style="margin-top: 20px;">审批记录</h4>
<el-timeline v-if="records.length"> <el-timeline v-if="records.length">
<el-timeline-item <el-timeline-item
v-for="record in records" v-for="record in records"
@@ -162,20 +169,24 @@ const receiptPreviewUrl = ref('')
const receiptPreviewType = ref('') const receiptPreviewType = ref('')
const receiptPreviewItemId = ref(0) const receiptPreviewItemId = ref(0)
const receiptList = computed(() => const receiptList = computed(() => {
(props.detail?.items || []) const items = props.detail?.items || []
.filter(item => item.receipt_url) const list = []
.map(item => { items.forEach(item => {
const url = resolveReceiptUrl(item.receipt_url) if (!item.receipt_url) return
const isImage = /\.(jpe?g|png|gif|bmp|webp)(\?|#|$)/i.test(item.receipt_url || '') const urls = item.receipt_url.split(',').filter(Boolean)
return { urls.forEach((url, index) => {
id: item.id, list.push({
url, id: `${item.id}-${index}`,
isImage, originalId: item.id, // 记录原始明细ID,用于删除时定位
name: item.expense_type || `票据 ${item.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/')) const isReceiptImage = computed(() => receiptPreviewType.value.startsWith('image/'))
@@ -192,10 +203,18 @@ const selectReceipt = receipt => {
} }
const openReceiptPreview = row => { const openReceiptPreview = row => {
const receipt = receiptList.value.find(item => item.id === row.id) // 查找列表项,注意这里的 row.id 可能是原始记录ID,或者是多票据生成出的唯一ID
if (!receipt) return // 如果是从表格点击查看,row 对应的是明细项,其本身可能包含多个票据
selectReceipt(receipt) // 我们直接找到该明细对应的第一张票据并打开预览窗口
const firstReceipt = receiptList.value.find(item => item.originalId === row.id)
if (!firstReceipt) {
ElMessage.warning('暂无票据可查看')
return
}
receiptPreviewVisible.value = true receiptPreviewVisible.value = true
selectReceipt(firstReceipt)
} }
const openAddItem = () => { const openAddItem = () => {
@@ -234,6 +253,11 @@ const uploadInvoice = async (row, file) => {
throw new Error(uploadResponse?.msg || '发票上传失败') 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, { await updateReimbursementItem(props.detail.id, row.id, {
expense_type: row.expense_type, expense_type: row.expense_type,
expense_date: row.expense_date, expense_date: row.expense_date,
@@ -242,7 +266,7 @@ const uploadInvoice = async (row, file) => {
unit_price: row.unit_price || 0, unit_price: row.unit_price || 0,
description: row.description || '', description: row.description || '',
receipt_flag: 1, receipt_flag: 1,
receipt_url: uploadResponse?.data?.url || uploadResponse?.url || '' receipt_url: existingUrls.join(',')
}) })
ElMessage.success('发票上传成功') ElMessage.success('发票上传成功')
emit('changed', props.detail.id) emit('changed', props.detail.id)
@@ -253,14 +277,26 @@ const uploadInvoice = async (row, file) => {
} }
} }
const removeItem = async row => { const removeReceipt = async (receipt) => {
try { try {
await ElMessageBox.confirm(`确认删除“${row.expense_type}”这条费用明细吗?`, '删除确认', { type: 'warning' }) await ElMessageBox.confirm('确认删除该票据吗?', '删除确认', { type: 'warning' })
await deleteReimbursementItem(props.detail.id, row.id) // 通过原始 ID 查找对应的明细行
ElMessage.success('费用明细已删除') 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) emit('changed', props.detail.id)
} catch (error) { } 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 { .receipt-list {
flex: 0 0 180px; flex: 0 0 200px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
padding-right: 12px; padding-right: 12px;
border-right: 1px solid var(--el-border-color-light); border-right: 1px solid var(--el-border-color-light);
overflow-y: auto; overflow-y: auto;
max-height: 620px;
} }
.receipt-thumb { .receipt-thumb {
@@ -370,6 +407,7 @@ const formatDateTime = value => value ? new Date(value).toLocaleString('zh-CN',
max-width: 100%; max-width: 100%;
max-height: 620px; max-height: 620px;
cursor: zoom-in; cursor: zoom-in;
pointer-events: auto; /* 确保点击事件可以传递 */
} }
.receipt-pdf { .receipt-pdf {