feat(报销/拨款): 发票明细与附件双向联动

- 移除手动添加条目按钮,条目只能通过上传文件产生
- 删除附件时同步移除该文件的所有明细条目
- 删除条目时同步从附件列表移除对应文件(含该文件下全部条目)
- 无明细时隐藏明细表格区域

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 17:49:43 +08:00
parent c412f73b80
commit 1e128cecfe
2 changed files with 60 additions and 29 deletions

View File

@@ -54,8 +54,9 @@
<!-- 拨款单据附件OCR触发区 -->
<el-form-item label="拨款单据附件" prop="accessoryApplyIds">
<file-upload v-model="form.accessoryApplyIds" :limit="50" :file-size="50"
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple />
<div class="hint-text">上传发票收据付款截图等支持 PDF/图片上传后自动识别</div>
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple
@delete="onFileDelete" />
<div class="hint-text">上传发票收据付款截图等支持 PDF/图片上传后自动识别明细</div>
</el-form-item>
<!-- OCR 识别中提示 -->
@@ -67,9 +68,9 @@
<!-- 发票明细条目表 -->
<div class="block-title">
拨款明细
<span class="block-title-hint">上传发票后自动填充也可手动添加</span>
<span class="block-title-hint">上传发票后自动识别删除条目将同步移除对应文件</span>
</div>
<div class="invoice-table">
<div class="invoice-table" v-if="invoiceItems.length">
<div class="invoice-table-header">
<span class="col-reason">事由说明</span>
<span class="col-amount">金额</span>
@@ -95,8 +96,7 @@
</div>
</div>
<div class="invoice-table-footer">
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="addInvoiceItem">添加条目</el-button>
<span class="total-hint" v-if="invoiceItems.length">
<span class="total-hint">
合计<b>¥{{ invoiceTotalFormatted }}</b>已自动更新拨款总金额
</span>
</div>
@@ -350,23 +350,27 @@ export default {
try {
const res = await ocrAppropriationInvoice(ossId)
if (res.code === 200 && res.data) {
const { items, totalAmount } = res.data
const { items, totalAmount, sellerName, invoiceDate, invoiceType } = res.data
// 拼接发票头部信息作为事由前缀:发票类型 · 销售方 · 开票日期
const prefix = [invoiceType, sellerName, invoiceDate].filter(Boolean).join(' · ')
if (items && items.length) {
const startIdx = this.invoiceItems.length
items.forEach((item, i) => {
const reason = [prefix, item.itemName].filter(Boolean).join(' / ')
this.invoiceItems.push({
ossId: Number(ossId),
itemName: item.itemName || '',
reason: item.itemName || '',
reason,
amount: item.amount || 0,
sortNo: startIdx + i
})
})
} else if (totalAmount) {
// 没有明细时用总金额创建一条,事由取发票头部信息
this.invoiceItems.push({
ossId: Number(ossId),
itemName: '',
reason: '',
itemName: sellerName || '',
reason: prefix || '',
amount: totalAmount,
sortNo: this.invoiceItems.length
})
@@ -382,12 +386,24 @@ export default {
}
},
addInvoiceItem () {
this.invoiceItems.push({ ossId: null, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
removeInvoiceItem (idx) {
const item = this.invoiceItems[idx]
if (item.ossId) {
// 删除该文件下所有条目
const ossId = String(item.ossId)
this.invoiceItems = this.invoiceItems.filter(it => String(it.ossId) !== ossId)
// 从附件 CSV 中移除该文件
const ids = (this.form.accessoryApplyIds || '').split(',').filter(id => id && id !== ossId)
this.form.accessoryApplyIds = ids.join(',')
} else {
this.invoiceItems.splice(idx, 1)
}
this.recalcTotal()
},
removeInvoiceItem (idx) {
this.invoiceItems.splice(idx, 1)
onFileDelete (deletedFile) {
const ossId = String(deletedFile.ossId)
this.invoiceItems = this.invoiceItems.filter(item => String(item.ossId) !== ossId)
this.recalcTotal()
},

View File

@@ -54,8 +54,9 @@
<!-- 报销单据附件OCR触发区 -->
<el-form-item label="报销单据附件" prop="accessoryApplyIds">
<file-upload v-model="form.accessoryApplyIds" :limit="200" :file-size="50"
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple />
<div class="hint-text">上传发票收据付款截图等支持 PDF/图片上传后自动识别</div>
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple
@delete="onFileDelete" />
<div class="hint-text">上传发票收据付款截图等支持 PDF/图片上传后自动识别明细</div>
</el-form-item>
<!-- OCR 识别中提示 -->
@@ -67,9 +68,9 @@
<!-- 发票明细条目表 -->
<div class="block-title">
发票明细
<span class="block-title-hint">上传发票后自动填充也可手动添加</span>
<span class="block-title-hint">上传发票后自动识别删除条目将同步移除对应文件</span>
</div>
<div class="invoice-table">
<div class="invoice-table" v-if="invoiceItems.length">
<div class="invoice-table-header">
<span class="col-reason">事由说明</span>
<span class="col-amount">金额</span>
@@ -95,8 +96,7 @@
</div>
</div>
<div class="invoice-table-footer">
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="addInvoiceItem">添加条目</el-button>
<span class="total-hint" v-if="invoiceItems.length">
<span class="total-hint">
合计<b>¥{{ invoiceTotalFormatted }}</b>已自动更新报销总金额
</span>
</div>
@@ -325,24 +325,27 @@ export default {
try {
const res = await ocrReimburseInvoice(ossId)
if (res.code === 200 && res.data) {
const { items, totalAmount } = res.data
const { items, totalAmount, sellerName, invoiceDate, invoiceType } = res.data
// 拼接发票头部信息作为事由前缀:发票类型 · 销售方 · 开票日期
const prefix = [invoiceType, sellerName, invoiceDate].filter(Boolean).join(' · ')
if (items && items.length) {
const startIdx = this.invoiceItems.length
items.forEach((item, i) => {
const reason = [prefix, item.itemName].filter(Boolean).join(' / ')
this.invoiceItems.push({
ossId: Number(ossId),
itemName: item.itemName || '',
reason: item.itemName || '',
reason,
amount: item.amount || 0,
sortNo: startIdx + i
})
})
} else if (totalAmount) {
// 没有明细时用总金额创建一条
// 没有明细时用总金额创建一条,事由取发票头部信息
this.invoiceItems.push({
ossId: Number(ossId),
itemName: '',
reason: '',
itemName: sellerName || '',
reason: prefix || '',
amount: totalAmount,
sortNo: this.invoiceItems.length
})
@@ -358,12 +361,24 @@ export default {
}
},
addInvoiceItem () {
this.invoiceItems.push({ ossId: null, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
removeInvoiceItem (idx) {
const item = this.invoiceItems[idx]
if (item.ossId) {
// 删除该文件下所有条目
const ossId = String(item.ossId)
this.invoiceItems = this.invoiceItems.filter(it => String(it.ossId) !== ossId)
// 从附件 CSV 中移除该文件
const ids = (this.form.accessoryApplyIds || '').split(',').filter(id => id && id !== ossId)
this.form.accessoryApplyIds = ids.join(',')
} else {
this.invoiceItems.splice(idx, 1)
}
this.recalcTotal()
},
removeInvoiceItem (idx) {
this.invoiceItems.splice(idx, 1)
onFileDelete (deletedFile) {
const ossId = String(deletedFile.ossId)
this.invoiceItems = this.invoiceItems.filter(item => String(item.ossId) !== ossId)
this.recalcTotal()
},