feat(miniprogram): show invoice thumbnails in reimburse apply picker.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
01553ef0f7
commit
ffec3ecd94
1 file changed
+172
-75
@@ -139,17 +139,27 @@
|
||||
</text>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-sm" @click="openInvoicePicker">从发票夹选择</button>
|
||||
<view v-if="selectedInvoicePicks.length" class="invoice-picked-row">
|
||||
<view v-if="selectedInvoicePicks.length" class="invoice-thumb-grid">
|
||||
<view
|
||||
v-for="pick in selectedInvoicePicks"
|
||||
:key="pick.key"
|
||||
class="invoice-picked-item"
|
||||
class="invoice-thumb"
|
||||
>
|
||||
<view class="invoice-picked-item__meta">
|
||||
<text class="invoice-picked-item__name">{{ pick.fileName }}</text>
|
||||
<text class="invoice-picked-item__amt">整票 ¥{{ money(pick.amount) }}</text>
|
||||
<view class="invoice-thumb__del" @click.stop="removeInvoicePick(pick.key)">×</view>
|
||||
<view class="invoice-thumb__media" @click="previewInvoicePick(pick)">
|
||||
<image
|
||||
v-if="isImageFileType(pick.fileType) && pick.previewUrl"
|
||||
:src="pick.previewUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="invoice-thumb__doc">
|
||||
<text class="invoice-thumb__doc-ext">{{ fileExtLabel(pick.fileName) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="invoice-thumb__meta">
|
||||
<text class="invoice-thumb__amt">¥{{ money(pick.amount) }}</text>
|
||||
<text class="invoice-thumb__date">{{ formatInvoiceDateYmd(pick.createTime) }}</text>
|
||||
</view>
|
||||
<view class="invoice-picked-item__remove" @click="removeInvoicePick(pick.key)">×</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-tip">请先在「我的发票夹」上传发票;提交时按合计扣减余额。</view>
|
||||
@@ -217,24 +227,52 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 发票多选(扁平列表) -->
|
||||
<!-- 发票多选(缩略图网格 + 分页) -->
|
||||
<view v-if="showInvoicePicker" class="bx-sheet-mask" @click="showInvoicePicker = false">
|
||||
<view class="bx-sheet" @click.stop>
|
||||
<view class="bx-sheet__title">选择发票附件</view>
|
||||
<scroll-view scroll-y class="bx-sheet__body">
|
||||
<view v-if="!attachItems.length" class="bx-sheet__empty">暂无发票附件</view>
|
||||
<view
|
||||
v-for="item in attachItems"
|
||||
:key="item.key"
|
||||
class="bx-sheet__item"
|
||||
:class="{ 'is-selected': pickerDraftKeys.includes(item.key) }"
|
||||
@click="togglePickerKey(item.key)"
|
||||
>
|
||||
<view>
|
||||
<text>{{ item.fileName }}</text>
|
||||
<view class="bx-sheet__item-sub">整票 ¥{{ money(item.amount) }}</view>
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="bx-sheet__body bx-sheet__body--invoice-thumbs"
|
||||
@scrolltolower="onInvoicePickerLoadMore"
|
||||
lower-threshold="80"
|
||||
>
|
||||
<view v-if="!attachItems.length && !invLoading" class="bx-sheet__empty">
|
||||
暂无发票附件
|
||||
</view>
|
||||
<view v-else class="invoice-pick-grid">
|
||||
<view
|
||||
v-for="item in attachItems"
|
||||
:key="item.key"
|
||||
class="invoice-pick-card"
|
||||
:class="{ 'is-selected': pickerDraftKeys.includes(item.key) }"
|
||||
@click="togglePickerKey(item.key)"
|
||||
>
|
||||
<view class="invoice-pick-card__check">
|
||||
<text v-if="pickerDraftKeys.includes(item.key)">✓</text>
|
||||
</view>
|
||||
<view class="invoice-pick-card__preview">
|
||||
<image
|
||||
v-if="isImageFileType(item.fileType) && item.previewUrl"
|
||||
:src="item.previewUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="invoice-pick-card__doc">
|
||||
<text class="invoice-thumb__doc-ext">{{ fileExtLabel(item.fileName) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="invoice-pick-card__meta">
|
||||
<view class="invoice-pick-card__amt">¥{{ money(item.amount) }}</view>
|
||||
<view class="invoice-pick-card__date">
|
||||
{{ formatInvoiceDateYmd(item.createTime) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="pickerDraftKeys.includes(item.key)">✓</text>
|
||||
</view>
|
||||
<view v-if="attachItems.length" class="invoice-pick-footer-tip">
|
||||
<text v-if="invLoading">加载中…</text>
|
||||
<text v-else-if="invFinished">没有更多了</text>
|
||||
<text v-else>上拉加载更多</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="bx-sheet__footer">
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user