|
|
@@ -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)
|