refactor(bx-mobile): use attachment-level invoice picks in reimburse apply logic
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
38383b35d9
commit
9f92012e43
1 file changed
+199
-49
@@ -161,42 +161,40 @@
|
||||
<button type="button" class="btn btn-primary btn-sm" @click="openInvoicePicker">
|
||||
从发票夹选择
|
||||
</button>
|
||||
<div v-if="selectedInvoiceIds.length" class="invoice-picked">
|
||||
<div v-if="selectedInvoicePicksSorted.length" class="invoice-picked">
|
||||
<div
|
||||
v-for="inv in selectedInvoiceRows"
|
||||
:key="inv.id"
|
||||
v-for="pick in selectedInvoicePicksSorted"
|
||||
:key="pick.key"
|
||||
class="invoice-picked__item"
|
||||
>
|
||||
<span>¥{{ money(inv.amount) }} · {{ inv.remark || '发票' }}</span>
|
||||
<button type="button" class="del" @click="removeInvoice(inv.id)">×</button>
|
||||
<span>¥{{ money(pick.amount) }} · {{ pick.fileName }}</span>
|
||||
<button type="button" class="del" @click="removeInvoicePick(pick.key)">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="form-tip">请先在「我的发票夹」上传发票;提交时按合计扣减余额。</p>
|
||||
<p class="form-tip">请先在「我的发票夹」上传发票;提交时按合计扣减余额。选择对象为附件文件。</p>
|
||||
</div>
|
||||
|
||||
<van-popup v-model:show="showInvoicePicker" position="bottom" round :style="{ height: '70%' }">
|
||||
<div class="invoice-picker">
|
||||
<div class="invoice-picker__title">选择发票</div>
|
||||
<van-checkbox-group v-model="pickerCheckedIds">
|
||||
<van-cell-group>
|
||||
<van-cell
|
||||
v-for="inv in walletInvoices"
|
||||
:key="inv.id"
|
||||
clickable
|
||||
@click="togglePickerId(inv.id)"
|
||||
>
|
||||
<template #title>
|
||||
¥{{ money(inv.amount) }} · {{ inv.remark || '发票' }}
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<van-checkbox :name="String(inv.id)" @click.stop />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</van-checkbox-group>
|
||||
<div class="invoice-picker__title">选择发票附件</div>
|
||||
<van-cell-group>
|
||||
<van-cell
|
||||
v-for="item in attachItems"
|
||||
:key="item.key"
|
||||
clickable
|
||||
@click="togglePickerKey(item.key)"
|
||||
>
|
||||
<template #title>
|
||||
¥{{ money(item.amount) }} · {{ item.fileName }}
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<span v-if="pickerDraftKeys.includes(item.key)">✓</span>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
<div class="invoice-picker__footer">
|
||||
<button type="button" class="btn btn-primary btn-block" @click="confirmInvoicePicker">
|
||||
确定(已选 {{ pickerCheckedIds.length }})
|
||||
确定(已选 {{ pickerDraftKeys.length }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -296,7 +294,7 @@
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { showToast, showDialog } from 'vant'
|
||||
import { invoiceApi, invoiceWalletApi, reimburseApi } from '@/api'
|
||||
import { attachmentApi, invoiceApi, invoiceWalletApi, reimburseApi } from '@/api'
|
||||
import { BizError } from '@/api/client'
|
||||
import {
|
||||
useExpenseItemTree,
|
||||
@@ -361,6 +359,7 @@ function onApplicantConfirm({ selectedOptions }: any) {
|
||||
const opt = selectedOptions?.[0]
|
||||
if (opt?.value) {
|
||||
form.applicantId = String(opt.value)
|
||||
selectedInvoicePicks.value = []
|
||||
refreshTitle()
|
||||
refreshWallet()
|
||||
}
|
||||
@@ -382,11 +381,23 @@ async function refreshTitle() {
|
||||
}
|
||||
}
|
||||
|
||||
type InvoiceAttachPick = {
|
||||
key: string
|
||||
invoiceId: string
|
||||
attachmentId: string
|
||||
amount: number | string
|
||||
createTime: string
|
||||
fileName: string
|
||||
fileType: string
|
||||
previewUrl: string
|
||||
}
|
||||
|
||||
const walletBalance = ref(0)
|
||||
const walletInvoices = ref<any[]>([])
|
||||
const selectedInvoiceIds = ref<string[]>([])
|
||||
const selectedInvoicePicks = ref<InvoiceAttachPick[]>([])
|
||||
const pickerDraftKeys = ref<string[]>([])
|
||||
const attachItems = ref<InvoiceAttachPick[]>([])
|
||||
const showInvoicePicker = ref(false)
|
||||
const pickerCheckedIds = ref<string[]>([])
|
||||
|
||||
const detailTotalNumber = computed(() =>
|
||||
form.details.reduce((sum: number, d) => sum + Number(d.amount || 0), 0),
|
||||
@@ -403,8 +414,37 @@ const balanceInsufficient = computed(
|
||||
() => detailTotalNumber.value > 0 && walletBalance.value < detailTotalNumber.value,
|
||||
)
|
||||
|
||||
const selectedInvoiceRows = computed(() =>
|
||||
walletInvoices.value.filter((inv) => selectedInvoiceIds.value.includes(String(inv.id))),
|
||||
const pickerMonthGroups = computed(() => {
|
||||
const map = new Map<string, InvoiceAttachPick[]>()
|
||||
for (const item of attachItems.value) {
|
||||
const k = monthKey(item.createTime)
|
||||
const list = map.get(k) || []
|
||||
list.push(item)
|
||||
map.set(k, list)
|
||||
}
|
||||
return Array.from(map.entries())
|
||||
.sort((a, b) => b[0].localeCompare(a[0]))
|
||||
.map(([key, items]) => {
|
||||
const sorted = items
|
||||
.slice()
|
||||
.sort((a, b) => String(b.createTime).localeCompare(String(a.createTime)))
|
||||
return {
|
||||
monthKey: key,
|
||||
monthLabel: monthLabel(key),
|
||||
items: sorted.map((item, idx) => ({ ...item, serial: idx + 1 })),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const selectedInvoicePicksSorted = computed(() =>
|
||||
selectedInvoicePicks.value
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const ta = String(a.createTime || '')
|
||||
const tb = String(b.createTime || '')
|
||||
if (ta !== tb) return tb.localeCompare(ta)
|
||||
return String(b.attachmentId).localeCompare(String(a.attachmentId))
|
||||
}),
|
||||
)
|
||||
|
||||
function money(v: unknown) {
|
||||
@@ -414,6 +454,89 @@ function money(v: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
function isImageFileType(ft: string) {
|
||||
return String(ft || '').startsWith('image/')
|
||||
}
|
||||
|
||||
function monthKey(raw: string) {
|
||||
const text = String(raw || '').trim()
|
||||
const m = text.match(/^(\d{4})[-/]?(\d{2})/)
|
||||
if (m) return `${m[1]}-${m[2]}`
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
function monthLabel(key: string) {
|
||||
if (key === 'unknown') return '未知月份'
|
||||
const [y, m] = key.split('-')
|
||||
return `${y}年${Number(m)}月`
|
||||
}
|
||||
|
||||
function invoiceFileList(inv: any): Array<{ id: string; fileName: string; fileType: string }> {
|
||||
const raw = Array.isArray(inv?.attachments) ? inv.attachments : []
|
||||
if (raw.length) {
|
||||
return raw.map((a: any) => ({
|
||||
id: String(a.id || a.attachmentId),
|
||||
fileName: a.fileName || a.file_name || '发票附件',
|
||||
fileType: a.fileType || a.file_type || '',
|
||||
}))
|
||||
}
|
||||
const aid = inv?.attachmentId || inv?.attachment_id
|
||||
if (aid) {
|
||||
return [
|
||||
{
|
||||
id: String(aid),
|
||||
fileName: inv.fileName || inv.file_name || '发票附件',
|
||||
fileType: inv.fileType || inv.file_type || '',
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function buildAttachItems(rows: any[]): InvoiceAttachPick[] {
|
||||
const items: InvoiceAttachPick[] = []
|
||||
for (const row of rows || []) {
|
||||
const createTime = row.createTime || row.create_time || ''
|
||||
for (const file of invoiceFileList(row)) {
|
||||
if (!file.id) continue
|
||||
items.push({
|
||||
key: `${row.id}:${file.id}`,
|
||||
invoiceId: String(row.id),
|
||||
attachmentId: file.id,
|
||||
amount: row.amount,
|
||||
createTime,
|
||||
fileName: file.fileName,
|
||||
fileType: file.fileType,
|
||||
previewUrl: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
function buildInvoicePayload() {
|
||||
const invoiceLinks = selectedInvoicePicks.value.map((p) => ({
|
||||
invoiceId: String(p.invoiceId),
|
||||
attachmentId: String(p.attachmentId),
|
||||
}))
|
||||
const invoiceIds = Array.from(new Set(invoiceLinks.map((p) => p.invoiceId)))
|
||||
return { invoiceIds, invoiceLinks }
|
||||
}
|
||||
|
||||
function kickAttachPreviews(items: InvoiceAttachPick[]) {
|
||||
for (const item of items) {
|
||||
if (!isImageFileType(item.fileType) || item.previewUrl) continue
|
||||
attachmentApi
|
||||
.preview(item.attachmentId)
|
||||
.then((pRes: any) => {
|
||||
item.previewUrl = pRes?.data?.url || ''
|
||||
})
|
||||
.catch(() => {
|
||||
/* optional */
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWallet() {
|
||||
try {
|
||||
const owner = form.applicantId || undefined
|
||||
@@ -424,37 +547,34 @@ async function refreshWallet() {
|
||||
walletBalance.value = Number(sumRes?.data?.balance ?? 0)
|
||||
const d = invRes?.data
|
||||
walletInvoices.value = d?.rows ?? d?.items ?? d?.list ?? (Array.isArray(d) ? d : [])
|
||||
attachItems.value = buildAttachItems(walletInvoices.value)
|
||||
kickAttachPreviews(attachItems.value)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function openInvoicePicker() {
|
||||
pickerCheckedIds.value = [...selectedInvoiceIds.value]
|
||||
pickerDraftKeys.value = selectedInvoicePicks.value.map((p) => p.key)
|
||||
showInvoicePicker.value = true
|
||||
}
|
||||
|
||||
function togglePickerId(id: string | number) {
|
||||
const sid = String(id)
|
||||
const i = pickerCheckedIds.value.indexOf(sid)
|
||||
if (i >= 0) pickerCheckedIds.value.splice(i, 1)
|
||||
else pickerCheckedIds.value.push(sid)
|
||||
function togglePickerKey(key: string) {
|
||||
const i = pickerDraftKeys.value.indexOf(key)
|
||||
if (i >= 0) pickerDraftKeys.value.splice(i, 1)
|
||||
else pickerDraftKeys.value.push(key)
|
||||
}
|
||||
|
||||
function confirmInvoicePicker() {
|
||||
selectedInvoiceIds.value = [...pickerCheckedIds.value]
|
||||
const set = new Set(pickerDraftKeys.value)
|
||||
selectedInvoicePicks.value = attachItems.value
|
||||
.filter((i) => set.has(i.key))
|
||||
.map((i) => ({ ...i }))
|
||||
showInvoicePicker.value = false
|
||||
}
|
||||
|
||||
function removeInvoice(id: string | number) {
|
||||
const sid = String(id)
|
||||
selectedInvoiceIds.value = selectedInvoiceIds.value.filter((x) => x !== sid)
|
||||
}
|
||||
|
||||
function buildInvoicePayload() {
|
||||
const invoiceIds = [...selectedInvoiceIds.value]
|
||||
const invoiceLinks = invoiceIds.map((invoiceId) => ({ invoiceId }))
|
||||
return { invoiceIds, invoiceLinks }
|
||||
function removeInvoicePick(key: string) {
|
||||
selectedInvoicePicks.value = selectedInvoicePicks.value.filter((p) => p.key !== key)
|
||||
}
|
||||
|
||||
function lineExpenseDisplay(id: number | string): string {
|
||||
@@ -664,7 +784,7 @@ async function loadEditData() {
|
||||
const id = route.query.id
|
||||
if (!id) {
|
||||
editingId.value = null
|
||||
selectedInvoiceIds.value = []
|
||||
selectedInvoicePicks.value = []
|
||||
return
|
||||
}
|
||||
editingId.value = String(id)
|
||||
@@ -685,8 +805,38 @@ async function loadEditData() {
|
||||
form.details = [{ expenseItemId: '', amount: '', remark: '' }]
|
||||
}
|
||||
const linked = data.invoices || []
|
||||
selectedInvoiceIds.value = linked.map((x: any) => String(x.id || x.invoiceId)).filter(Boolean)
|
||||
await refreshWallet()
|
||||
const picks: InvoiceAttachPick[] = []
|
||||
for (const inv of linked) {
|
||||
const invoiceId = String(inv.id || inv.invoiceId || '')
|
||||
if (!invoiceId) continue
|
||||
let attachmentId = String(inv.attachmentId || inv.attachment_id || '')
|
||||
let fileName = inv.fileName || inv.file_name || '发票附件'
|
||||
let fileType = inv.fileType || inv.file_type || ''
|
||||
const createTime = inv.createTime || inv.create_time || ''
|
||||
if (!attachmentId) {
|
||||
const src = walletInvoices.value.find((w) => String(w.id) === invoiceId)
|
||||
const first = src ? invoiceFileList(src)[0] : undefined
|
||||
if (first) {
|
||||
attachmentId = first.id
|
||||
fileName = first.fileName
|
||||
fileType = first.fileType
|
||||
}
|
||||
}
|
||||
if (!attachmentId) continue
|
||||
picks.push({
|
||||
key: `${invoiceId}:${attachmentId}`,
|
||||
invoiceId,
|
||||
attachmentId,
|
||||
amount: inv.amount,
|
||||
createTime,
|
||||
fileName,
|
||||
fileType,
|
||||
previewUrl: '',
|
||||
})
|
||||
}
|
||||
selectedInvoicePicks.value = picks
|
||||
kickAttachPreviews(selectedInvoicePicks.value)
|
||||
} catch (e: any) {
|
||||
showToast(e?.message || '加载数据失败')
|
||||
}
|
||||
@@ -697,7 +847,7 @@ watch(
|
||||
async (id, prev) => {
|
||||
if (!id) {
|
||||
editingId.value = null
|
||||
selectedInvoiceIds.value = []
|
||||
selectedInvoicePicks.value = []
|
||||
return
|
||||
}
|
||||
if (String(id) === String(prev ?? '')) return
|
||||
|
||||
Reference in new issue
Block a user