优化报销管理增加挂靠
This commit is contained in:
1 parent
7b1c94b5ff
commit
ee49ef15b5
24 files changed
+1267
-70
No files matched your search
@@ -7,9 +7,11 @@
|
||||
<el-descriptions-item label="申请日期">{{ detail.apply_date || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="申请人">{{ detailApplicantName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报销金额">¥ {{ money(detail.total_amount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发票金额">{{ formatDetailReceiptTotal }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发票金额">
|
||||
<span title="本单实际上传票据的票面合计;挂靠只是把同一张票分给多条明细,不重复计入">{{ formatDetailReceiptTotal }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发票差额">
|
||||
<span :class="detailReceiptDiffClass">{{ formatDetailReceiptDiff }}</span>
|
||||
<span :class="detailReceiptDiffClass" title="发票金额 − 报销金额(挂靠不重复计入)">{{ formatDetailReceiptDiff }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注说明" :span="3">{{ detail.description || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
@@ -53,6 +55,9 @@
|
||||
<template v-if="compareSummary.unmatchedCount > 0">
|
||||
,{{ compareSummary.unmatchedCount }} 张待填写
|
||||
</template>
|
||||
<template v-if="compareLinkedTotal > 0">
|
||||
,另有发票挂靠 ¥ {{ money(compareLinkedTotal) }}(同一张票不重复计入票据总额)
|
||||
</template>
|
||||
。识别结果可修改,确认后请保存。
|
||||
</p>
|
||||
<div class="table-scroll">
|
||||
@@ -98,9 +103,11 @@
|
||||
<el-table-column prop="amount" label="金额" width="110">
|
||||
<template #default="{ row }">¥ {{ money(row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="receipt_flag" label="发票" width="90">
|
||||
<el-table-column prop="receipt_flag" label="发票" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ itemHasReceipt(row) ? '有' : '无' }}({{ getItemReceiptFiles(row).length }})
|
||||
<span :class="{ 'receipt-status-linked': itemReceiptStatus(row) === '挂靠' }">
|
||||
{{ itemReceiptStatus(row) }}({{ itemReceiptCount(row) }})
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发票金额" width="110">
|
||||
@@ -110,13 +117,36 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="发票差额" width="110">
|
||||
<template #default="{ row }">
|
||||
<span :class="itemReceiptDiffClass(row)">{{ formatItemReceiptDiff(row) }}</span>
|
||||
<span :class="itemReceiptDiffClass(row)" :title="itemReceiptDiffTip(row)">
|
||||
{{ formatItemReceiptDiff(row) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="176" fixed="right">
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="item-actions">
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
:title="isEditable ? '挂靠有余额的发票' : '当前状态不可编辑费用明细'"
|
||||
aria-label="挂靠发票"
|
||||
:disabled="!isEditable"
|
||||
@click="openInvoicePicker(row)"
|
||||
>
|
||||
<i class="fa-solid fa-link"></i>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.invoice_file_id"
|
||||
link
|
||||
type="info"
|
||||
title="取消挂靠"
|
||||
aria-label="取消挂靠"
|
||||
:disabled="!isEditable"
|
||||
@click="clearInvoiceLink(row)"
|
||||
>
|
||||
<i class="fa-solid fa-link-slash"></i>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="itemHasReceipt(row)"
|
||||
link
|
||||
@@ -266,6 +296,14 @@
|
||||
@saved="handleItemsSaved"
|
||||
/>
|
||||
|
||||
<InvoicePicker
|
||||
v-model="pickerVisible"
|
||||
:reimbursement-id="detail?.id"
|
||||
:item-amount="pickerItemAmount"
|
||||
:exclude-item-id="pickerRow?.id || 0"
|
||||
@select="handleInvoiceSelect"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<div class="detail-toolbar">
|
||||
<el-button
|
||||
@@ -312,6 +350,7 @@ import {
|
||||
summarizeReceiptAmountRows
|
||||
} from '../utils/invoiceAmount'
|
||||
import ItemForm from './itemForm.vue'
|
||||
import InvoicePicker from './invoicePicker.vue'
|
||||
// 时间显示统一为:YYYY-MM-DD HH:mm:ss
|
||||
import { formatDateTime } from '@/utils/datetime'
|
||||
|
||||
@@ -379,6 +418,9 @@ const submitting = ref(false)
|
||||
const withdrawing = ref(false)
|
||||
const manualSaving = ref(false)
|
||||
const itemVisible = ref(false)
|
||||
const pickerVisible = ref(false)
|
||||
const pickerRow = ref(null)
|
||||
const pickerItemAmount = ref(0)
|
||||
const batchUploading = ref(false)
|
||||
const invoiceFileInputRef = ref(null)
|
||||
const pendingUploadRow = ref(null)
|
||||
@@ -406,6 +448,14 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
// 本单挂靠发票的已使用额度:仅用于面板提示,不重复计入整单票据金额
|
||||
const compareLinkedTotal = computed(() =>
|
||||
(props.detail?.items || []).reduce(
|
||||
(sum, item) => sum + Number(item.invoice_use_amount || 0),
|
||||
0
|
||||
)
|
||||
)
|
||||
|
||||
const compareSummary = computed(() => {
|
||||
if (!amountCompare.value?.details) {
|
||||
return {
|
||||
@@ -421,6 +471,7 @@ const compareSummary = computed(() => {
|
||||
(sum, item) => sum + Number(item.amount || 0),
|
||||
0
|
||||
)
|
||||
// 与顶部基础信息保持同一口径:挂靠不增加整单票据总额
|
||||
return summarizeReceiptAmountRows(amountCompare.value.details, claimTotal)
|
||||
})
|
||||
|
||||
@@ -571,6 +622,19 @@ const getItemReceiptFiles = item => {
|
||||
|
||||
const itemHasReceipt = item => getItemReceiptFiles(item).length > 0
|
||||
|
||||
// 「发票」列三态:有(本行上传发票)/ 挂靠(用本单其它明细上传的发票)/ 无。
|
||||
// 挂靠优先展示;数量为本行关联的发票凭证张数(上传张数 + 挂靠张数)。
|
||||
const itemLinkedInvoiceCount = item =>
|
||||
Number(item?.invoice_file_id || 0) > 0 && Number(item?.invoice_use_amount || 0) > 0 ? 1 : 0
|
||||
|
||||
const itemReceiptStatus = item => {
|
||||
if (itemLinkedInvoiceCount(item) > 0) return '挂靠'
|
||||
return itemHasReceipt(item) ? '有' : '无'
|
||||
}
|
||||
|
||||
const itemReceiptCount = item =>
|
||||
getItemReceiptFiles(item).length + itemLinkedInvoiceCount(item)
|
||||
|
||||
const getItemReceiptAmountTotal = item => {
|
||||
const amounts = getItemReceiptFiles(item)
|
||||
.map(file => file.amount)
|
||||
@@ -579,14 +643,49 @@ const getItemReceiptAmountTotal = item => {
|
||||
return amounts.reduce((sum, amount) => sum + Number(amount), 0)
|
||||
}
|
||||
|
||||
// 本条明细挂靠本单其它明细上传的发票时,本次分到的额度
|
||||
const getItemLinkedAmount = item => Number(item?.invoice_use_amount || 0)
|
||||
|
||||
// 按发票文件 ID 统计「被挂靠走」的额度。一张票的票面只在上传它的那条
|
||||
// 明细计入一次,挂靠出去的部分要从宿主明细划出,否则同一张票会被重复计算。
|
||||
const linkedOutByFile = computed(() => {
|
||||
const map = {}
|
||||
for (const item of props.detail?.items || []) {
|
||||
const fileId = Number(item?.invoice_file_id || 0)
|
||||
const used = getItemLinkedAmount(item)
|
||||
if (fileId > 0 && used > 0) {
|
||||
map[fileId] = (map[fileId] || 0) + used
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const getItemLinkedOutAmount = item =>
|
||||
getItemReceiptFiles(item).reduce(
|
||||
(sum, file) => sum + Number(linkedOutByFile.value[Number(file.file_id)] || 0),
|
||||
0
|
||||
)
|
||||
|
||||
// 本行可用发票金额 = 上传票面(扣除已划出给挂靠明细的部分)+ 本行挂靠分到的额度
|
||||
const getItemInvoiceAmount = item => {
|
||||
const uploaded = getItemReceiptAmountTotal(item)
|
||||
const linked = getItemLinkedAmount(item)
|
||||
if (uploaded == null && linked <= 0) return null
|
||||
const linkedOut = uploaded == null ? 0 : getItemLinkedOutAmount(item)
|
||||
const own = Math.max(0, (uploaded || 0) - linkedOut)
|
||||
return own + linked
|
||||
}
|
||||
|
||||
// 统一口径:本行发票金额 − 本行明细金额。所有明细、顶部整单完全一致,
|
||||
// 明细行差额之和就等于顶部整单差额。
|
||||
const getItemReceiptDiff = item => {
|
||||
const receiptTotal = getItemReceiptAmountTotal(item)
|
||||
if (receiptTotal == null) return null
|
||||
return receiptTotal - Number(item?.amount || 0)
|
||||
const invoiceTotal = getItemInvoiceAmount(item)
|
||||
if (invoiceTotal == null) return null
|
||||
return invoiceTotal - Number(item?.amount || 0)
|
||||
}
|
||||
|
||||
const formatItemReceiptTotal = item => {
|
||||
const total = getItemReceiptAmountTotal(item)
|
||||
const total = getItemInvoiceAmount(item)
|
||||
return total == null ? '-' : `¥ ${money(total)}`
|
||||
}
|
||||
|
||||
@@ -604,11 +703,27 @@ const itemReceiptDiffClass = item => {
|
||||
return diff > 0 ? 'receipt-diff-over' : 'receipt-diff-short'
|
||||
}
|
||||
|
||||
// 悬停说明差额构成:票面、划出的挂靠额度、分入的挂靠额度
|
||||
const itemReceiptDiffTip = item => {
|
||||
const uploaded = getItemReceiptAmountTotal(item)
|
||||
const linked = getItemLinkedAmount(item)
|
||||
if (uploaded == null && linked <= 0) return '本条明细暂无发票'
|
||||
const linkedOut = uploaded == null ? 0 : getItemLinkedOutAmount(item)
|
||||
const parts = []
|
||||
if (uploaded != null) parts.push(`上传票面 ¥${money(uploaded)}`)
|
||||
if (linkedOut > 0) parts.push(`划出挂靠 ¥${money(linkedOut)}`)
|
||||
if (linked > 0) parts.push(`挂靠分入 ¥${money(linked)}`)
|
||||
parts.push(`本行可用发票 ¥${money(getItemInvoiceAmount(item) || 0)} − 明细 ¥${money(item?.amount || 0)}`)
|
||||
return parts.join(';')
|
||||
}
|
||||
|
||||
// 顶部整单 = 各明细可用发票金额之和。挂靠额度只是同一张票在明细之间划转,
|
||||
// 「划出」与「分入」相互抵消,因此总额等于实际上传票据的票面合计,不重复计算。
|
||||
const detailReceiptTotal = computed(() => {
|
||||
const items = props.detail?.items || []
|
||||
let hasAny = false
|
||||
const total = items.reduce((sum, item) => {
|
||||
const itemTotal = getItemReceiptAmountTotal(item)
|
||||
const itemTotal = getItemInvoiceAmount(item)
|
||||
if (itemTotal != null) {
|
||||
hasAny = true
|
||||
return sum + itemTotal
|
||||
@@ -729,6 +844,48 @@ const handleItemsSaved = reimbursementId => {
|
||||
emit('changed', reimbursementId)
|
||||
}
|
||||
|
||||
// 发票挂靠:对已有明细直接挂靠 / 更换 / 解挂,复用明细更新接口,
|
||||
// 请求里带上原发票附件字段,避免更新时把已上传的票据清掉。
|
||||
const saveInvoiceLink = async (row, fileId, useAmount) => {
|
||||
try {
|
||||
await updateReimbursementItem(props.detail?.id, row.id, {
|
||||
expense_type: row.expense_type,
|
||||
expense_date: row.expense_date,
|
||||
amount: row.amount,
|
||||
quantity: row.quantity || 1,
|
||||
unit_price: row.unit_price || 0,
|
||||
description: row.description || '',
|
||||
receipt_flag: row.receipt_flag || 0,
|
||||
receipt_file_ids: row.receipt_file_ids || '',
|
||||
receipt_urls: row.receipt_urls || '',
|
||||
invoice_file_id: fileId,
|
||||
invoice_use_amount: useAmount
|
||||
})
|
||||
ElMessage.success(fileId ? '发票挂靠成功' : '已取消挂靠')
|
||||
emit('changed', props.detail?.id)
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '保存挂靠失败')
|
||||
}
|
||||
}
|
||||
|
||||
const openInvoicePicker = row => {
|
||||
pickerRow.value = row
|
||||
pickerItemAmount.value = Number(row.amount || 0)
|
||||
pickerVisible.value = true
|
||||
}
|
||||
|
||||
const handleInvoiceSelect = selected => {
|
||||
const row = pickerRow.value
|
||||
if (!row) return
|
||||
saveInvoiceLink(row, selected.file_id, selected.use_amount)
|
||||
}
|
||||
|
||||
const clearInvoiceLink = row => {
|
||||
ElMessageBox.confirm('确认取消这条明细的发票挂靠吗?', '取消挂靠', { type: 'warning' })
|
||||
.then(() => saveInvoiceLink(row, 0, 0))
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!props.detail?.id) return
|
||||
try {
|
||||
@@ -1146,6 +1303,11 @@ const money = value => Number(value || 0).toFixed(2)
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.receipt-status-linked {
|
||||
color: var(--el-color-warning);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择挂靠发票"
|
||||
width="780px"
|
||||
class="invoice-picker-dialog"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="picker-tip"
|
||||
title="一张发票可以分多次报销。这里只列出当前报销单内上传、且仍有余额的发票;本次使用金额不能超过发票余额,也不能超过本条费用明细金额。"
|
||||
/>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
border
|
||||
size="small"
|
||||
highlight-current-row
|
||||
max-height="320"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<el-table-column prop="name" label="发票文件" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column label="票面金额" width="110">
|
||||
<template #default="{ row }">¥ {{ money(row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已用" width="100">
|
||||
<template #default="{ row }">¥ {{ money(row.used_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余可挂靠" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="remain">¥ {{ money(row.remain_amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expense_date" label="上传日期" width="110" />
|
||||
<el-table-column label="操作" width="80" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleSelect(row)">
|
||||
{{ selectedId === row.file_id ? '已选' : '选择' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="当前报销单暂无有余额的发票:请先在本单费用明细上传发票并填写票面金额" :image-size="70" />
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<div v-if="selected" class="picker-use">
|
||||
<span>本次使用金额</span>
|
||||
<el-input-number
|
||||
v-model="useAmount"
|
||||
:min="0.01"
|
||||
:max="maxUse"
|
||||
:precision="2"
|
||||
:step="10"
|
||||
controls-position="right"
|
||||
size="small"
|
||||
/>
|
||||
<span class="picker-hint">最多 ¥ {{ money(maxUse) }}</span>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selected" @click="confirm">确认挂靠</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getAvailableInvoices } from '@/api/reimburse'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
// 当前报销单 ID:发票挂靠只允许在本单范围内进行
|
||||
reimbursementId: { type: [Number, String], default: 0 },
|
||||
// 本条费用明细金额,用于限制本次使用金额上限
|
||||
itemAmount: { type: Number, default: 0 },
|
||||
// 编辑已有明细时传入该明细 ID,排除它自身占用的额度
|
||||
excludeItemId: { type: [Number, String], default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'select'])
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const selectedId = ref(0)
|
||||
const selected = ref(null)
|
||||
const useAmount = ref(0)
|
||||
|
||||
const money = value => Number(value || 0).toFixed(2)
|
||||
const maxUse = computed(() =>
|
||||
Number(
|
||||
Math.min(
|
||||
Number(props.itemAmount) || 0,
|
||||
Number(selected.value?.remain_amount) || 0
|
||||
).toFixed(2)
|
||||
)
|
||||
)
|
||||
|
||||
const load = async () => {
|
||||
if (!props.reimbursementId) {
|
||||
list.value = []
|
||||
ElMessage.warning('缺少报销单信息,无法加载可挂靠发票')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getAvailableInvoices({
|
||||
reimbursement_id: props.reimbursementId,
|
||||
exclude_item_id: props.excludeItemId || undefined
|
||||
})
|
||||
const payload = res?.data ?? {}
|
||||
list.value = Array.isArray(payload.list)
|
||||
? payload.list
|
||||
: (Array.isArray(res?.list) ? res.list : [])
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '加载可挂靠发票失败')
|
||||
list.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelect = row => {
|
||||
if (!row) return
|
||||
selectedId.value = row.file_id
|
||||
selected.value = row
|
||||
useAmount.value = maxUse.value
|
||||
}
|
||||
const handleCurrentChange = row => {
|
||||
if (row) handleSelect(row)
|
||||
}
|
||||
|
||||
const confirm = () => {
|
||||
if (!selected.value) return
|
||||
const amount = Number(useAmount.value) || 0
|
||||
if (amount <= 0 || amount > maxUse.value + 0.001) {
|
||||
ElMessage.warning(`本次使用金额需在 0.01 ~ ${money(maxUse.value)} 之间`)
|
||||
return
|
||||
}
|
||||
emit('select', {
|
||||
file_id: selected.value.file_id,
|
||||
name: selected.value.name || '',
|
||||
remain_amount: selected.value.remain_amount,
|
||||
use_amount: Number(amount.toFixed(2))
|
||||
})
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async isVisible => {
|
||||
if (!isVisible) return
|
||||
selectedId.value = 0
|
||||
selected.value = null
|
||||
useAmount.value = 0
|
||||
await load()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.picker-tip {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.remain {
|
||||
color: var(--el-color-success);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.picker-use {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.picker-hint {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -79,6 +79,25 @@
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="invoice-link-row">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="Number(item.amount) <= 0"
|
||||
@click="openPicker(index)"
|
||||
>
|
||||
<i class="fa-solid fa-link"></i>
|
||||
{{ item.invoice_file_id ? '更换挂靠发票' : '挂靠有余额的发票' }}
|
||||
</el-button>
|
||||
<template v-if="item.invoice_file_id">
|
||||
<span class="invoice-link-info">
|
||||
{{ item.invoice_name || ('发票 #' + item.invoice_file_id) }}
|
||||
· 本次 ¥{{ money(item.invoice_use_amount) }}
|
||||
</span>
|
||||
<el-button link type="danger" @click="clearLink(index)">取消挂靠</el-button>
|
||||
</template>
|
||||
<span v-else class="invoice-link-tip">可选:一张发票分多次报销</span>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -91,6 +110,14 @@
|
||||
{{ editingItemId ? '保存' : `保存 ${items.length} 条` }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<InvoicePicker
|
||||
v-model="pickerVisible"
|
||||
:reimbursement-id="props.reimbursementId"
|
||||
:item-amount="pickerItemAmount"
|
||||
:exclude-item-id="props.editingItemId"
|
||||
@select="handleInvoiceSelect"
|
||||
/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
@@ -98,6 +125,7 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { addReimbursementItem, updateReimbursementItem } from '@/api/reimburse'
|
||||
import InvoicePicker from './invoicePicker.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
@@ -115,6 +143,9 @@ const visible = computed({
|
||||
const saving = ref(false)
|
||||
const items = ref([])
|
||||
let rowKey = 0
|
||||
const pickerVisible = ref(false)
|
||||
const pickerIndex = ref(-1)
|
||||
const pickerItemAmount = ref(0)
|
||||
|
||||
const today = () => new Date().toISOString().slice(0, 10)
|
||||
const createRow = (source = {}) => ({
|
||||
@@ -124,9 +155,40 @@ const createRow = (source = {}) => ({
|
||||
amount: Number(source.amount || 0),
|
||||
description: source.description || '',
|
||||
unit_price: Number(source.unit_price || 0),
|
||||
receipt_urls: source.receipt_urls || ''
|
||||
receipt_urls: source.receipt_urls || '',
|
||||
invoice_file_id: Number(source.invoice_file_id || 0),
|
||||
invoice_name: source.invoice_name || '',
|
||||
invoice_use_amount: Number(source.invoice_use_amount || 0)
|
||||
})
|
||||
|
||||
const money = value => Number(value || 0).toFixed(2)
|
||||
|
||||
const openPicker = index => {
|
||||
const item = items.value[index]
|
||||
if (!item || Number(item.amount) <= 0) {
|
||||
ElMessage.warning('请先填写本条费用明细金额')
|
||||
return
|
||||
}
|
||||
pickerIndex.value = index
|
||||
pickerItemAmount.value = Number(item.amount)
|
||||
pickerVisible.value = true
|
||||
}
|
||||
|
||||
const handleInvoiceSelect = selected => {
|
||||
const index = pickerIndex.value
|
||||
if (index < 0 || !items.value[index]) return
|
||||
items.value[index].invoice_file_id = selected.file_id
|
||||
items.value[index].invoice_name = selected.name
|
||||
items.value[index].invoice_use_amount = selected.use_amount
|
||||
}
|
||||
|
||||
const clearLink = index => {
|
||||
if (!items.value[index]) return
|
||||
items.value[index].invoice_file_id = 0
|
||||
items.value[index].invoice_name = ''
|
||||
items.value[index].invoice_use_amount = 0
|
||||
}
|
||||
|
||||
const rules = {
|
||||
expense_type: [{ required: true, message: '请选择费用类型', trigger: 'change' }],
|
||||
expense_date: [{ required: true, message: '请选择发生日期', trigger: 'change' }],
|
||||
@@ -158,6 +220,15 @@ const submit = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const invalidLinkIndex = items.value.findIndex(
|
||||
item => Number(item.invoice_file_id) > 0 &&
|
||||
Number(item.invoice_use_amount) > Number(item.amount) + 0.001
|
||||
)
|
||||
if (invalidLinkIndex !== -1) {
|
||||
ElMessage.warning(`第 ${invalidLinkIndex + 1} 条明细的挂靠金额超过明细金额,请重新选择挂靠发票`)
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
if (props.editingItemId) {
|
||||
@@ -171,7 +242,9 @@ const submit = async () => {
|
||||
description: item.description || '',
|
||||
receipt_flag: props.editingItem?.receipt_flag || 0,
|
||||
receipt_file_ids: props.editingItem?.receipt_file_ids || '',
|
||||
receipt_urls: props.editingItem?.receipt_urls || ''
|
||||
receipt_urls: props.editingItem?.receipt_urls || '',
|
||||
invoice_file_id: Number(item.invoice_file_id || 0),
|
||||
invoice_use_amount: Number(item.invoice_use_amount || 0)
|
||||
})
|
||||
} else {
|
||||
for (const item of items.value) {
|
||||
@@ -183,7 +256,9 @@ const submit = async () => {
|
||||
unit_price: item.unit_price || 0,
|
||||
description: item.description || '',
|
||||
receipt_flag: 0,
|
||||
receipt_urls: ''
|
||||
receipt_urls: '',
|
||||
invoice_file_id: Number(item.invoice_file_id || 0),
|
||||
invoice_use_amount: Number(item.invoice_use_amount || 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -220,6 +295,24 @@ watch(
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.invoice-link-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
border-top: 1px dashed var(--el-border-color-lighter);
|
||||
padding-top: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.invoice-link-info {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.invoice-link-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.item-list {
|
||||
max-height: 540px;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -50,6 +50,11 @@
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<el-tabs v-model="activeTab" class="reimburse-tabs" @tab-change="handleTabChange">
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
<el-tab-pane label="未报销" name="unpaid" />
|
||||
<el-tab-pane label="已报销" name="paid" />
|
||||
</el-tabs>
|
||||
<div class="table-scroll">
|
||||
<el-table v-loading="loading" :data="list" stripe>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
@@ -207,6 +212,8 @@ const statusOptions = [
|
||||
{ value: 4, label: "已撤回" },
|
||||
{ value: 5, label: "已打款" },
|
||||
];
|
||||
// 默认进入「未报销」:待处理的单据优先展示
|
||||
const activeTab = ref("unpaid");
|
||||
const filters = reactive({ keyword: "", status: undefined });
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
|
||||
const list = ref([]);
|
||||
@@ -251,7 +258,11 @@ const loadList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = responseData(
|
||||
await getReimbursementList({ ...filters, ...pagination }),
|
||||
await getReimbursementList({
|
||||
...filters,
|
||||
...pagination,
|
||||
reimbursed: activeTab.value === "all" ? undefined : activeTab.value,
|
||||
}),
|
||||
);
|
||||
list.value = data.list || [];
|
||||
pagination.total = Number(data.total || 0);
|
||||
@@ -262,9 +273,19 @@ const loadList = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 切换「全部 / 未报销 / 已报销」:回到第一页重新查询。
|
||||
// tab 与具体状态下拉语义有重叠,切换时清掉下拉,避免叠加出空结果。
|
||||
const handleTabChange = () => {
|
||||
filters.status = undefined;
|
||||
pagination.page = 1;
|
||||
loadList();
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
filters.keyword = "";
|
||||
filters.status = undefined;
|
||||
// 重置回到默认视图「未报销」
|
||||
activeTab.value = "unpaid";
|
||||
pagination.page = 1;
|
||||
loadList();
|
||||
};
|
||||
@@ -514,6 +535,17 @@ onBeforeUnmount(() => {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.reimburse-tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
// 三个 pane 只作切换标签,表格共用一份,隐藏空内容区
|
||||
:deep(.el-tabs__content) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user