Files
shanxia2.0/miniprogram/pages/bx/reimburse/apply.vue
T

907 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="bx-page bx-page--form">
<view class="topbar" :style="safeAreaStyle">
<view class="topbar-left">
<text class="topbar-back" @click="goBack">←</text>
<text class="topbar-title">报销申请</text>
</view>
<view class="topbar-right" />
</view>
<view class="page-padded">
<view class="form-card">
<view class="form-header">
<text class="form-header-title">报销信息</text>
<text class="form-badge">{{ isEdit ? '编辑' : '新建' }}</text>
</view>
<view class="field">
<text class="field-label">申请人</text>
<view class="form-input" style="display: flex; align-items: center">
{{ applicantLabel || '当前用户' }}
</view>
</view>
<view class="field">
<text class="field-label">报销日期<text class="req">*</text></text>
<picker mode="date" :value="form.reimburseDate" @change="onDateChange">
<view class="picker-trigger">
<text class="value" :class="{ ph: !form.reimburseDate }">
{{ form.reimburseDate || '请选择报销日期' }}
</text>
<text class="arrow">›</text>
</view>
</picker>
</view>
<view class="field">
<text class="field-label">报销标题</text>
<input
class="form-input"
:value="form.title"
placeholder="不填则自动生成"
@input="onTitleInput"
/>
</view>
</view>
<view class="form-card">
<view class="form-header">
<text class="form-header-title">报销明细</text>
<text class="form-badge">{{ form.details.length }}行</text>
</view>
<view v-for="(line, idx) in form.details" :key="idx" class="detail-line">
<view v-if="idx > 0" class="detail-divider" />
<view class="field">
<text class="field-label">
费用项目
<text v-if="form.details.length > 1" class="detail-line-no">
(第 {{ idx + 1 }} 条)
</text>
<text class="req">*</text>
</text>
<view class="picker-trigger" @click="openExpensePicker(idx)">
<text
class="value"
:class="{ ph: !lineExpenseDisplay(line.expenseItemId) }"
>
{{ lineExpenseDisplay(line.expenseItemId) || '请选择费用项目' }}
</text>
<text class="arrow">›</text>
</view>
</view>
<view class="field">
<text class="field-label">金额(元)<text class="req">*</text></text>
<view class="form-input-prefix">
<input
class="form-input"
type="digit"
:value="line.amount"
placeholder="0.00"
:data-idx="idx"
@input="onLineAmount"
/>
</view>
</view>
<view class="field">
<text class="field-label">报销事由<text class="req">*</text></text>
<input
class="form-input"
:value="line.remark"
placeholder="请输入报销事由"
:data-idx="idx"
@input="onLineRemark"
/>
</view>
<view class="field">
<text class="field-label">发生时间</text>
<picker
mode="date"
:value="line.occurDate || form.reimburseDate"
:data-idx="idx"
@change="onOccurDate"
>
<view class="picker-trigger">
<text class="value" :class="{ ph: !line.occurDate }">
{{ line.occurDate || '选填' }}
</text>
<text class="arrow">›</text>
</view>
</picker>
</view>
<view v-if="form.details.length > 1" class="detail-line-actions">
<button class="btn btn-sm btn-danger" @click="removeDetailLine(idx)">
删除此行
</button>
</view>
</view>
<view class="detail-actions">
<button class="btn btn-primary btn-sm" @click="addDetailLine">+ 新增明细行</button>
<text class="total-amount">合计:<text style="font-weight: 700">¥{{ detailTotalAmount }}</text></text>
</view>
</view>
<view class="form-card">
<view class="form-header">
<text class="form-header-title">关联发票(可选)</text>
</view>
<view class="invoice-balance-tip">
发票余额 ¥{{ money(walletBalance) }} · 本次合计 ¥{{ detailTotalAmount }}
<text v-if="balanceInsufficient" class="invoice-balance-tip--warn">
(余额不足,提交后余额将为负,请后续补发票)
</text>
</view>
<button class="btn btn-primary btn-sm" @click="openInvoicePicker">从发票夹选择</button>
<view v-if="selectedInvoicePicks.length" class="invoice-thumb-grid">
<view
v-for="pick in selectedInvoicePicks"
:key="pick.key"
class="invoice-thumb"
>
<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>
</view>
<view class="form-tip">请先在「我的发票夹」上传发票;提交时按合计扣减余额。</view>
</view>
<view class="form-card">
<view class="form-header">
<text class="form-header-title">申请附件(可选)</text>
</view>
<button
class="btn btn-primary btn-sm"
:disabled="uploading"
@click="chooseAttachments"
>
{{ uploading ? '上传中…' : '上传图片' }}
</button>
<view v-if="files.length" class="att-grid">
<view v-for="(file, idx) in files" :key="file.id" class="att-img att-img--with-name">
<image :src="file.previewUrl || file.localPath" mode="aspectFill" />
<view v-if="!file._pending" class="del" @click.stop="removeFile(idx)">×</view>
<text class="att-img__name">{{ attachmentDisplayName(file, idx) }}</text>
</view>
</view>
<view v-else class="detail-section-empty" style="margin-top: 8px">暂无附件(可选)</view>
</view>
<view class="form-card">
<view class="form-header">
<text class="form-header-title">报销说明</text>
</view>
<view class="field">
<textarea
class="form-textarea"
:value="form.remark"
placeholder="请输入报销说明"
:maxlength="500"
@input="onRemarkInput"
/>
</view>
</view>
</view>
<view class="bottom-action-bar">
<button class="btn btn-primary btn-block" :disabled="submitting" @click="onSubmit">
{{ submitting ? '保存中…' : '保存' }}
</button>
</view>
<!-- 费用项目选择 -->
<view v-if="showExpensePicker" class="bx-sheet-mask" @click="showExpensePicker = false">
<view class="bx-sheet" @click.stop>
<view class="bx-sheet__title">选择费用项目</view>
<scroll-view scroll-y class="bx-sheet__body">
<view v-if="!expenseOptions.length" class="bx-sheet__empty">暂无可用费用项目</view>
<view
v-for="opt in expenseOptions"
:key="opt.id"
class="bx-sheet__item"
:class="{ 'is-selected': String(form.details[pickingLineIndex]?.expenseItemId) === opt.id }"
@click="confirmExpensePick(opt.id)"
>
{{ opt.label }}
</view>
</scroll-view>
</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 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>
</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">
<button class="btn btn-primary btn-block" @click="confirmInvoicePicker">
确定(已选 {{ pickerDraftKeys.length }})
</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { PAGE_PATHS } from '@/config/api'
import { useUserStore } from '@/stores/user'
import {
reimburseApi,
attachmentApi,
expenseItemApi,
invoiceWalletApi,
invoiceApi
} from '@/api/bx'
import {
normalizeExpenseItem,
flattenExpenseTree,
formatExpenseName
} from '@/utils/bxFormat'
import {
isImageFileType,
fileExtLabel,
formatInvoiceDateYmd,
buildAttachItemsFromInvoiceRows,
mergeInvoicePicksByDraftKeys
} from '@/utils/bxInvoiceThumb'
import { getSafeAreaStyle } from '@/utils/safeArea'
import { buildApplyAttachmentDisplayName } from '@/utils/bxAttachmentDisplayName'
const safeAreaStyle = getSafeAreaStyle()
const MAX_ATTACHMENTS = 10
const userStore = useUserStore()
const editId = ref('')
const savedId = ref('')
const submitting = ref(false)
const uploading = ref(false)
const files = ref([])
function todayYmd() {
const d = new Date()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${d.getFullYear()}-${m}-${day}`
}
const form = reactive({
title: '',
remark: '',
applicantId: '',
reimburseDate: todayYmd(),
details: [{ expenseItemId: '', amount: '', remark: '', occurDate: '' }]
})
const flatList = ref([])
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)
const isEdit = computed(() => !!editId.value || !!savedId.value)
const applicantLabel = computed(() => {
const u = userStore.userInfo || {}
return u.realName || u.username || u.nickName || u.name || ''
})
const expenseOptions = computed(() => {
return (flatList.value || [])
.filter((i) => i.status === 1 || i.status === '1')
.map((i) => ({
id: String(i.id),
label: formatExpenseName(i.id, flatList.value)
}))
})
const detailTotalNumber = computed(() =>
form.details.reduce((sum, d) => sum + Number(d.amount || 0), 0)
)
const detailTotalAmount = computed(() =>
detailTotalNumber.value.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
)
const balanceInsufficient = computed(
() => detailTotalNumber.value > 0 && walletBalance.value < detailTotalNumber.value
)
function money(v) {
return Number(v || 0).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
function attachmentDisplayName(file, index) {
return buildApplyAttachmentDisplayName({
remark: form.remark || '',
originalFileName: file.fileName || '',
index1: index + 1
})
}
function lineExpenseDisplay(id) {
if (id === '' || id == null) return ''
const name = formatExpenseName(id, flatList.value)
return name === '-' ? '' : name
}
function onTitleInput(e) {
form.title = e.detail.value
}
function onRemarkInput(e) {
form.remark = e.detail.value
}
function onLineAmount(e) {
const idx = Number(e.currentTarget.dataset.idx)
if (Number.isNaN(idx) || !form.details[idx]) return
form.details[idx].amount = e.detail.value
}
function onLineRemark(e) {
const idx = Number(e.currentTarget.dataset.idx)
if (Number.isNaN(idx) || !form.details[idx]) return
form.details[idx].remark = e.detail.value
}
function onOccurDate(e) {
const idx = Number(e.currentTarget.dataset.idx)
if (Number.isNaN(idx) || !form.details[idx]) return
form.details[idx].occurDate = e.detail.value
}
function onDateChange(e) {
form.reimburseDate = e.detail.value
refreshTitle()
}
function goBack() {
uni.navigateBack({ fail: () => uni.redirectTo({ url: PAGE_PATHS.bxReimburseApply }) })
}
function addDetailLine() {
form.details.push({ expenseItemId: '', amount: '', remark: '', occurDate: '' })
}
function removeDetailLine(index) {
form.details.splice(index, 1)
}
function openExpensePicker(idx) {
pickingLineIndex.value = idx
showExpensePicker.value = true
}
function confirmExpensePick(id) {
form.details[pickingLineIndex.value].expenseItemId = String(id)
showExpensePicker.value = false
}
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 }
}
async function refreshTitle() {
if (!form.reimburseDate) return
try {
const params = { reimburseDate: form.reimburseDate }
const exclude = editId.value || savedId.value
if (exclude) params.excludeApplyId = exclude
if (form.applicantId) params.applicantId = form.applicantId
const res = await reimburseApi.generateTitle(params)
const t = res?.data
form.title = typeof t === 'string' ? t : t?.title || form.title
} catch (e) {
// keep
}
}
async function refreshWallet() {
try {
const owner = form.applicantId || undefined
const sumRes = await invoiceWalletApi.mineSummary(
owner ? { userId: owner } : undefined
)
walletBalance.value = Number(sumRes?.data?.balance ?? 0)
} catch (e) {
// ignore
}
}
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) {
const i = pickerDraftKeys.value.indexOf(key)
if (i >= 0) pickerDraftKeys.value.splice(i, 1)
else pickerDraftKeys.value.push(key)
}
function confirmInvoicePicker() {
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)
}
function validateReimburseApplyForm() {
for (let i = 0; i < form.details.length; i++) {
const d = form.details[i]
const row = i + 1
const hasExpense = d.expenseItemId !== '' && d.expenseItemId != null
const amountStr = String(d.amount ?? '').trim()
const hasAmount = amountStr !== ''
const amt = Number(amountStr)
const amountOk = hasAmount && !Number.isNaN(amt) && amt > 0
const hasRemark = !!d.remark?.trim()
if (!hasExpense && !hasAmount && !hasRemark) continue
if (!hasExpense) {
uni.showToast({ title: `第 ${row} 行:请选择费用项目`, icon: 'none' })
return null
}
if (!amountOk) {
uni.showToast({ title: `第 ${row} 行:请输入大于 0 的金额`, icon: 'none' })
return null
}
if (!d.remark?.trim()) {
uni.showToast({ title: `第 ${row} 行:请填写报销事由`, icon: 'none' })
return null
}
}
const validDetails = form.details.filter((d) => {
const hasExpense = d.expenseItemId !== '' && d.expenseItemId != null
const amt = Number(String(d.amount ?? '').trim())
return hasExpense && !Number.isNaN(amt) && amt > 0
})
if (validDetails.length === 0) {
uni.showToast({ title: '请至少填写一条完整的报销明细', icon: 'none' })
return null
}
if (!form.reimburseDate) {
uni.showToast({ title: '请选择报销日期', icon: 'none' })
return null
}
return validDetails
}
async function chooseAttachments() {
const remain = MAX_ATTACHMENTS - files.value.length
if (remain <= 0) {
uni.showToast({ title: `最多上传 ${MAX_ATTACHMENTS} 个附件`, icon: 'none' })
return
}
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const paths = res.tempFilePaths || []
for (const path of paths) {
await uploadOne(path)
}
}
})
}
async function uploadOne(filePath) {
const pendingId = `pending_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`
files.value.push({
id: pendingId,
previewUrl: filePath,
localPath: filePath,
_pending: true,
_existing: false
})
uploading.value = true
try {
const res = await attachmentApi.upload(filePath, '', 0)
const data = res?.data || {}
const id = String(data.id || '')
let previewUrl = filePath
if (id) {
try {
const pRes = await attachmentApi.preview(id)
if (pRes?.data?.url) previewUrl = pRes.data.url
} catch (e) {
// keep local
}
}
const idx = files.value.findIndex((f) => f.id === pendingId)
if (idx >= 0) {
files.value[idx] = {
id: id || pendingId,
previewUrl,
localPath: filePath,
fileName: data.fileName || data.file_name || '',
_pending: false,
_existing: false
}
}
} catch (e) {
files.value = files.value.filter((f) => f.id !== pendingId)
} finally {
uploading.value = files.value.some((f) => f._pending)
}
}
function removeFile(idx) {
const file = files.value[idx]
if (!file) return
if (file._existing && file.id && !String(file.id).startsWith('pending_')) {
attachmentApi.delete(file.id).catch(() => {})
}
files.value.splice(idx, 1)
}
async function bindNewAttachments(bizId) {
const newIds = files.value
.filter((f) => f.id && !f._existing && !f._pending && !String(f.id).startsWith('pending_'))
.map((f) => f.id)
if (!newIds.length) return
await attachmentApi.bindBiz(newIds, 'reimburse_apply', bizId)
files.value.forEach((f) => {
if (newIds.includes(f.id)) f._existing = true
})
}
async function loadExpenseItems() {
try {
const res = await expenseItemApi.tree()
const tree = (res.data || []).map(normalizeExpenseItem)
flatList.value = flattenExpenseTree(tree)
} catch (e) {
flatList.value = []
}
}
async function loadEditData(id) {
try {
const res = await reimburseApi.getById(id)
const data = res?.data || {}
form.title = data.title || ''
form.remark = data.remark || ''
form.applicantId = String(data.applicantId || userStore.userInfo?.id || '')
form.reimburseDate = data.reimburseDate || todayYmd()
form.details = (data.details || []).map((d) => ({
expenseItemId: d.expenseId ?? d.expenseItemId ?? '',
amount: String(d.amount ?? ''),
remark: d.remark || '',
occurDate: d.occurDate || ''
}))
if (!form.details.length) {
form.details = [{ expenseItemId: '', amount: '', remark: '', occurDate: '' }]
}
await refreshWallet()
const linked = data.invoices || []
const picks = []
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 = attachItems.value.find((w) => String(w.invoiceId) === invoiceId)
if (src) {
attachmentId = src.attachmentId
fileName = src.fileName
fileType = src.fileType
}
}
if (!attachmentId) continue
picks.push({
key: `${invoiceId}:${attachmentId}`,
invoiceId,
attachmentId,
amount: inv.amount,
createTime,
fileName,
fileType,
previewUrl: '',
previewLoading: false
})
}
selectedInvoicePicks.value = picks
kickAttachPreviewLoads(selectedInvoicePicks.value)
try {
const attRes = await attachmentApi.listByBiz('reimburse_apply', id)
const list = attRes?.data || []
const mapped = []
for (const a of list) {
let previewUrl = ''
if ((a.fileType || '').startsWith('image/')) {
try {
const pRes = await attachmentApi.preview(a.id)
previewUrl = pRes?.data?.url || ''
} catch (e) {
// skip
}
}
mapped.push({
id: String(a.id),
previewUrl,
fileName: a.fileName || a.file_name || '',
_pending: false,
_existing: true
})
}
files.value = mapped
} catch (e) {
files.value = []
}
} catch (e) {
uni.showToast({ title: '加载数据失败', icon: 'none' })
}
}
async function onSubmit() {
const validDetails = validateReimburseApplyForm()
if (!validDetails) return
if (uploading.value) {
uni.showToast({ title: '附件上传中,请稍候', icon: 'none' })
return
}
submitting.value = true
const isEditingAction = !!editId.value || !!savedId.value
try {
const invoicePayload = buildInvoicePayload()
const payload = {
title: form.title || undefined,
remark: form.remark || undefined,
applicantId: form.applicantId || undefined,
reimburseDate: form.reimburseDate || undefined,
details: validDetails.map((d) => ({
expenseItemId: String(d.expenseItemId),
amount: Number(d.amount),
remark: String(d.remark).trim(),
occurDate: d.occurDate ? String(d.occurDate) : undefined
})),
...invoicePayload
}
let id
if (editId.value || savedId.value) {
id = savedId.value || editId.value
await reimburseApi.update(id, payload)
} else {
const res = await reimburseApi.create(payload)
id = String(res?.data?.id ?? res?.id ?? '')
if (!id) throw new Error('保存成功但未返回报销单ID')
savedId.value = id
editId.value = id
}
try {
await bindNewAttachments(id)
} catch (e) {
uni.showToast({ title: '报销单已保存,但附件关联失败', icon: 'none' })
}
uni.showModal({
title: isEditingAction ? '保存成功' : '创建成功',
content: '报销单已保存为草稿',
confirmText: '提交审批',
confirmColor: '#00a874',
cancelText: '继续编辑',
success: async (modalRes) => {
if (!modalRes.confirm) return
try {
await reimburseApi.submit(id, invoicePayload)
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => goBack(), 500)
} catch (e) {
// toasted
}
}
})
} catch (e) {
// toasted by bxRequest
} finally {
submitting.value = false
}
}
onLoad(async (query) => {
const uid = String(userStore.userInfo?.id || userStore.userInfo?.userId || '')
form.applicantId = uid
await loadExpenseItems()
await refreshWallet()
const id = query?.id ? String(query.id) : ''
if (id) {
editId.value = id
await loadEditData(id)
} else {
await refreshTitle()
}
})
</script>
<style lang="scss">
@import '@/styles/bx-theme.scss';
</style>