diff --git a/miniprogram/pages/bx/reimburse/apply.vue b/miniprogram/pages/bx/reimburse/apply.vue index e7bee79fa..e53b1a589 100644 --- a/miniprogram/pages/bx/reimburse/apply.vue +++ b/miniprogram/pages/bx/reimburse/apply.vue @@ -139,17 +139,27 @@ - + - - {{ pick.fileName }} - 整票 ¥{{ money(pick.amount) }} + × + + + + {{ fileExtLabel(pick.fileName) }} + + + + ¥{{ money(pick.amount) }} + {{ formatInvoiceDateYmd(pick.createTime) }} - × 请先在「我的发票夹」上传发票;提交时按合计扣减余额。 @@ -217,24 +227,52 @@ - + 选择发票附件 - - 暂无发票附件 - - - {{ item.fileName }} - 整票 ¥{{ money(item.amount) }} + + + 暂无发票附件 + + + + + ✓ + + + + + {{ fileExtLabel(item.fileName) }} + + + + ¥{{ money(item.amount) }} + + {{ formatInvoiceDateYmd(item.createTime) }} + + - ✓ + + + 加载中… + 没有更多了 + 上拉加载更多 @@ -264,6 +302,13 @@ import { flattenExpenseTree, formatExpenseName } from '@/utils/bxFormat' +import { + isImageFileType, + fileExtLabel, + formatInvoiceDateYmd, + buildAttachItemsFromInvoiceRows, + mergeInvoicePicksByDraftKeys +} from '@/utils/bxInvoiceThumb' import { getSafeAreaStyle } from '@/utils/safeArea' import { buildApplyAttachmentDisplayName } from '@/utils/bxAttachmentDisplayName' @@ -298,7 +343,11 @@ const showExpensePicker = ref(false) const pickingLineIndex = ref(0) const walletBalance = ref(0) +const INVOICE_PAGE_SIZE = 10 const attachItems = ref([]) +const invPage = ref(1) +const invLoading = ref(false) +const invFinished = ref(false) const selectedInvoicePicks = ref([]) const pickerDraftKeys = ref([]) const showInvoicePicker = ref(false) @@ -403,48 +452,6 @@ function confirmExpensePick(id) { showExpensePicker.value = false } -function invoiceFileList(inv) { - const raw = Array.isArray(inv?.attachments) ? inv.attachments : [] - if (raw.length) { - return raw.map((a) => ({ - 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) { - const items = [] - 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 - }) - } - } - return items -} - function buildInvoicePayload() { const invoiceLinks = selectedInvoicePicks.value.map((p) => ({ invoiceId: String(p.invoiceId), @@ -472,22 +479,88 @@ async function refreshTitle() { async function refreshWallet() { try { const owner = form.applicantId || undefined - const [sumRes, invRes] = await Promise.all([ - invoiceWalletApi.mineSummary(owner ? { userId: owner } : undefined), - invoiceApi.list({ page: 1, pageSize: 200, userId: owner }) - ]) + const sumRes = await invoiceWalletApi.mineSummary( + owner ? { userId: owner } : undefined + ) walletBalance.value = Number(sumRes?.data?.balance ?? 0) - const d = invRes?.data - const rows = d?.rows ?? d?.items ?? d?.list ?? (Array.isArray(d) ? d : []) - attachItems.value = buildAttachItems(rows) } catch (e) { // ignore } } -function openInvoicePicker() { +function pagedRows(res) { + const d = res?.data + if (!d) return { rows: [], total: 0 } + if (Array.isArray(d)) return { rows: d, total: d.length } + const rows = d.rows ?? d.items ?? d.list ?? [] + const total = typeof d.total === 'number' ? d.total : rows.length + return { rows, total } +} + +function kickAttachPreviewLoads(items) { + for (const item of items) { + if (!isImageFileType(item.fileType) || item.previewUrl || item.previewLoading) continue + item.previewLoading = true + attachmentApi + .preview(item.attachmentId) + .then((pRes) => { + item.previewUrl = pRes?.data?.url || '' + }) + .catch(() => {}) + .finally(() => { + item.previewLoading = false + }) + } +} + +async function fetchInvoiceAttachPage(isRefresh) { + if (invLoading.value) return + invLoading.value = true + try { + const owner = form.applicantId || undefined + const res = await invoiceApi.list({ + page: invPage.value, + pageSize: INVOICE_PAGE_SIZE, + userId: owner + }) + const { rows, total } = pagedRows(res) + const chunk = buildAttachItemsFromInvoiceRows(rows) + if (isRefresh) attachItems.value = chunk + else attachItems.value = attachItems.value.concat(chunk) + kickAttachPreviewLoads(chunk) + const invoiceCountSoFar = + (isRefresh ? 0 : (invPage.value - 1) * INVOICE_PAGE_SIZE) + rows.length + if (rows.length < INVOICE_PAGE_SIZE || invoiceCountSoFar >= total) { + invFinished.value = true + } + } catch (e) { + uni.showToast({ title: '发票列表加载失败', icon: 'none' }) + if (isRefresh) invFinished.value = true + } finally { + invLoading.value = false + } +} + +async function resetAndLoadInvoices() { + invPage.value = 1 + invFinished.value = false + attachItems.value = [] + await fetchInvoiceAttachPage(true) + invPage.value = 2 +} + +async function onInvoicePickerLoadMore() { + if (invFinished.value || invLoading.value) return + await fetchInvoiceAttachPage(false) + invPage.value += 1 +} + +async function openInvoicePicker() { pickerDraftKeys.value = selectedInvoicePicks.value.map((p) => p.key) showInvoicePicker.value = true + if (!attachItems.value.length && !invFinished.value) { + await resetAndLoadInvoices() + } } function togglePickerKey(key) { @@ -497,13 +570,34 @@ function togglePickerKey(key) { } function confirmInvoicePicker() { - const set = new Set(pickerDraftKeys.value) - selectedInvoicePicks.value = attachItems.value - .filter((i) => set.has(i.key)) - .map((i) => ({ ...i })) + selectedInvoicePicks.value = mergeInvoicePicksByDraftKeys( + pickerDraftKeys.value, + attachItems.value, + selectedInvoicePicks.value + ) showInvoicePicker.value = false } +async function previewInvoicePick(pick) { + if (!isImageFileType(pick.fileType)) { + uni.showToast({ title: '非图片附件请在管理端查看', icon: 'none' }) + return + } + let url = pick.previewUrl + if (!url && pick.attachmentId) { + try { + const pRes = await attachmentApi.preview(pick.attachmentId) + url = pRes?.data?.url || '' + if (url) pick.previewUrl = url + } catch (e) { + uni.showToast({ title: '暂无法预览', icon: 'none' }) + return + } + } + if (url) uni.previewImage({ urls: [url], current: url }) + else uni.showToast({ title: '暂无法预览', icon: 'none' }) +} + function removeInvoicePick(key) { selectedInvoicePicks.value = selectedInvoicePicks.value.filter((p) => p.key !== key) } @@ -685,10 +779,13 @@ async function loadEditData(id) { amount: inv.amount, createTime, fileName, - fileType + fileType, + previewUrl: '', + previewLoading: false }) } selectedInvoicePicks.value = picks + kickAttachPreviewLoads(selectedInvoicePicks.value) try { const attRes = await attachmentApi.listByBiz('reimburse_apply', id)