提交
This commit is contained in:
1 parent
6b2147aa46
commit
8558d9439e
31 files changed
+596719
-4174
No files matched your search
@@ -293,8 +293,22 @@
|
||||
<el-button class="btn-ghost" @click="dialogs.add = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loadingMap.addSubmit" @click="submitAdd">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:loading="loadingMap.addSubmit"
|
||||
:disabled="!!loadingMap.addApprove"
|
||||
@click="submitAdd"
|
||||
>
|
||||
<el-icon><FolderChecked /></el-icon>保存草稿
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="loadingMap.addApprove"
|
||||
:disabled="!!loadingMap.addSubmit"
|
||||
@click="submitAddAndApprove"
|
||||
>
|
||||
<el-icon><Promotion /></el-icon>提交审批
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -432,11 +446,13 @@ import {
|
||||
View,
|
||||
Close,
|
||||
Delete,
|
||||
Check,
|
||||
Search,
|
||||
RefreshLeft
|
||||
RefreshLeft,
|
||||
FolderChecked,
|
||||
Promotion
|
||||
} from '@element-plus/icons-vue'
|
||||
import { advanceApi, paymentApi } from '@/api/bx'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import { canDeleteByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
statusLabel,
|
||||
@@ -444,7 +460,6 @@ import {
|
||||
formatMoney,
|
||||
formatMoneyYuan,
|
||||
moneyTableFormatter,
|
||||
workflowNodeDisplay,
|
||||
workflowNodeLabel,
|
||||
workflowNodeTagType,
|
||||
shouldShowWorkflowNodeTag
|
||||
@@ -552,16 +567,16 @@ const viewData = ref<Record<string, any>>({})
|
||||
const viewMainKvFields = computed<ViewField[]>(() => {
|
||||
const d = viewData.value
|
||||
return [
|
||||
bxViewKv('申请人', d.applicantName),
|
||||
bxViewKv('申请人', d.applicantName, { long: false }),
|
||||
bxViewKv('申请时间', d.applyTime, { long: false }),
|
||||
bxViewKv('申请原因', d.reason, { long: false }),
|
||||
bxViewKvMoney('申请金额(元)', formatMoneyYuan(d.amount || 0), { long: false }),
|
||||
bxViewKvMoney(
|
||||
'实际发放金额(元)',
|
||||
d.actualAmount != null ? formatMoneyYuan(d.actualAmount) : '—'
|
||||
d.actualAmount != null ? formatMoneyYuan(d.actualAmount) : '—',
|
||||
{ long: false }
|
||||
),
|
||||
bxViewKvMoney('申请金额(元)', formatMoneyYuan(d.amount || 0)),
|
||||
bxViewKvMoney('冲抵总金额(元)', formatMoneyYuan(d.offsetAmount || 0)),
|
||||
bxViewKv('当前节点', workflowNodeDisplay(d.currentNode, d.status)),
|
||||
bxViewKv('申请时间', d.applyTime),
|
||||
bxViewKv('申请原因', d.reason, { long: false })
|
||||
bxViewKvMoney('冲抵总金额(元)', formatMoneyYuan(d.offsetAmount || 0), { long: false })
|
||||
]
|
||||
})
|
||||
const selectedRows = ref<any[]>([])
|
||||
@@ -759,43 +774,94 @@ async function handleToolbarSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAdd() {
|
||||
type AddPayload = {
|
||||
reason: string
|
||||
amount: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
async function buildAddPayload(): Promise<AddPayload | null> {
|
||||
const valid = await addFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (!valid) return null
|
||||
const amountNum = Number(String(addForm.amount ?? '').trim())
|
||||
if (Number.isNaN(amountNum) || amountNum <= 0) {
|
||||
ElMessage.warning('请输入大于 0 的申请金额')
|
||||
return
|
||||
return null
|
||||
}
|
||||
return {
|
||||
reason: addForm.reason?.trim() ?? '',
|
||||
amount: amountNum,
|
||||
remark: addForm.remark || undefined
|
||||
}
|
||||
}
|
||||
|
||||
function resetAddForm() {
|
||||
dialogs.add = false
|
||||
Object.assign(addForm, { reason: '', amount: '', remark: '' })
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
/** 保存备用金申请,成功返回 id */
|
||||
async function saveAddForm(payload: AddPayload): Promise<string | null> {
|
||||
if (editingId.value) {
|
||||
await advanceApi.update(editingId.value, payload)
|
||||
return String(editingId.value)
|
||||
}
|
||||
const res: any = await advanceApi.create(payload)
|
||||
const data: any = pickBxData(res) || {}
|
||||
const id = data.id != null ? String(data.id) : ''
|
||||
return id || null
|
||||
}
|
||||
|
||||
/** 保存草稿 */
|
||||
async function submitAdd() {
|
||||
const payload = await buildAddPayload()
|
||||
if (!payload) return
|
||||
const wasEdit = !!editingId.value
|
||||
loadingMap.addSubmit = true
|
||||
try {
|
||||
const reason = addForm.reason?.trim() ?? ''
|
||||
if (editingId.value) {
|
||||
await advanceApi.update(editingId.value, {
|
||||
reason,
|
||||
amount: amountNum,
|
||||
remark: addForm.remark || undefined
|
||||
})
|
||||
ElMessage.success('保存成功')
|
||||
} else {
|
||||
await advanceApi.create({
|
||||
reason,
|
||||
amount: amountNum,
|
||||
remark: addForm.remark || undefined
|
||||
})
|
||||
ElMessage.success('新增成功')
|
||||
const id = await saveAddForm(payload)
|
||||
if (!id) {
|
||||
ElMessage.error('保存成功但未返回申请单 ID,请从列表重新操作')
|
||||
await refreshList()
|
||||
return
|
||||
}
|
||||
dialogs.add = false
|
||||
Object.assign(addForm, { reason: '', amount: '', remark: '' })
|
||||
editingId.value = null
|
||||
ElMessage.success(wasEdit ? '保存成功' : '新增成功')
|
||||
resetAddForm()
|
||||
await refreshList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || (editingId.value ? '保存失败' : '新增失败'))
|
||||
ElMessage.error(e.message || (wasEdit ? '保存失败' : '新增失败'))
|
||||
} finally {
|
||||
loadingMap.addSubmit = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存并提交审批 */
|
||||
async function submitAddAndApprove() {
|
||||
const payload = await buildAddPayload()
|
||||
if (!payload) return
|
||||
loadingMap.addApprove = true
|
||||
try {
|
||||
const id = await saveAddForm(payload)
|
||||
if (!id) {
|
||||
ElMessage.error('保存成功但未返回申请单 ID,请从列表重新提交审批')
|
||||
await refreshList()
|
||||
return
|
||||
}
|
||||
// 保存成功后记下 id,提交失败时再次操作会走更新而非重复新建
|
||||
editingId.value = id
|
||||
await advanceApi.submit(id)
|
||||
ElMessage.success('提交成功')
|
||||
resetAddForm()
|
||||
await refreshList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '提交失败')
|
||||
await refreshList()
|
||||
} finally {
|
||||
loadingMap.addApprove = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openView(row: any) {
|
||||
try {
|
||||
const res = await advanceApi.getById(row.id)
|
||||
|
||||
@@ -492,7 +492,6 @@ import {
|
||||
statusTagType,
|
||||
formatMoneyYuan,
|
||||
moneyTableFormatter,
|
||||
workflowNodeDisplay,
|
||||
workflowNodeLabel,
|
||||
workflowNodeTagType,
|
||||
shouldShowWorkflowNodeTag
|
||||
@@ -613,7 +612,6 @@ const viewMainKvFields = computed<ViewField[]>(() => {
|
||||
bxViewKv('申请人', d.applicantName),
|
||||
bxViewKvLong('申请原因', d.reason),
|
||||
bxViewKvMoney('申请金额(元)', formatMoneyYuan(d.amount || 0)),
|
||||
bxViewKv('当前节点', workflowNodeDisplay(d.currentNode, d.status)),
|
||||
bxViewKv('申请时间', d.applyTime)
|
||||
]
|
||||
})
|
||||
@@ -696,11 +694,12 @@ function buildApprovedQuery() {
|
||||
return q
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
onMounted(() => {
|
||||
loadApplicantOptions()
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
// 列表优先加载;角标刷新勿阻塞列表请求(否则页面已渲染却迟迟不发 pending 列表)
|
||||
searchPending()
|
||||
tabLoaded.pending = true
|
||||
void refreshMenuBadges(menuStore.menuList)
|
||||
})
|
||||
|
||||
function handlePendingSearch() {
|
||||
@@ -851,6 +850,7 @@ async function handleConfirmApprove() {
|
||||
}
|
||||
|
||||
dialogs.approve = false
|
||||
dialogs.view = false
|
||||
await refreshPending()
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
} finally {
|
||||
|
||||
@@ -421,8 +421,22 @@
|
||||
<el-button class="btn-ghost" @click="dialogs.add = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loadingMap.addSubmit" @click="submitAdd">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:loading="loadingMap.addSubmit"
|
||||
:disabled="!!loadingMap.addApprove"
|
||||
@click="submitAdd"
|
||||
>
|
||||
<el-icon><FolderChecked /></el-icon>保存草稿
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="loadingMap.addApprove"
|
||||
:disabled="!!loadingMap.addSubmit"
|
||||
@click="submitAddAndApprove"
|
||||
>
|
||||
<el-icon><Promotion /></el-icon>提交审批
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -584,10 +598,11 @@ import {
|
||||
View,
|
||||
Close,
|
||||
Delete,
|
||||
Check,
|
||||
Search,
|
||||
RefreshLeft,
|
||||
FolderOpened,
|
||||
FolderChecked,
|
||||
Promotion,
|
||||
VideoPlay,
|
||||
Document,
|
||||
Download
|
||||
@@ -1205,23 +1220,33 @@ async function handleToolbarSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAdd() {
|
||||
type AddPayload = {
|
||||
applicantId?: string
|
||||
reimburseDate: string
|
||||
title?: string
|
||||
remark: string
|
||||
invoiceIds: string[]
|
||||
invoiceLinks: { invoiceId: string; attachmentId: string }[]
|
||||
details: { expenseItemId: string; amount: number; remark: string }[]
|
||||
}
|
||||
|
||||
function buildAddPayload(): AddPayload | null {
|
||||
const validDetails = addForm.details.filter((d) => d.expenseItemId && d.amount)
|
||||
if (validDetails.length === 0) {
|
||||
ElMessage.warning('请至少填写一条报销明细')
|
||||
return
|
||||
return null
|
||||
}
|
||||
const missingReason = validDetails.some((d) => !String(d.remark ?? '').trim())
|
||||
if (missingReason) {
|
||||
ElMessage.warning('请为每条报销明细填写报销事由')
|
||||
return
|
||||
return null
|
||||
}
|
||||
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)))
|
||||
const payload = {
|
||||
return {
|
||||
applicantId: addForm.applicantId || undefined,
|
||||
reimburseDate: addForm.reimburseDate,
|
||||
title: addForm.title || undefined,
|
||||
@@ -1234,31 +1259,80 @@ async function submitAdd() {
|
||||
remark: String(d.remark).trim()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
function resetAddForm() {
|
||||
dialogs.add = false
|
||||
addForm.applicantId = currentUserId()
|
||||
addForm.reimburseDate = todayYmd()
|
||||
addForm.title = ''
|
||||
addForm.remark = ''
|
||||
addForm.details = [{ expenseItemId: '', amount: '', remark: '' }]
|
||||
selectedInvoicePicks.value = []
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
/** 保存报销单,成功返回 id;校验失败返回 null */
|
||||
async function saveAddForm(payload: AddPayload): Promise<string | null> {
|
||||
if (editingId.value) {
|
||||
await reimburseApi.update(editingId.value, payload)
|
||||
return String(editingId.value)
|
||||
}
|
||||
const res: any = await reimburseApi.create(payload)
|
||||
const data: any = pickBxData(res) || {}
|
||||
const id = data.id != null ? String(data.id) : ''
|
||||
return id || null
|
||||
}
|
||||
|
||||
/** 保存草稿 */
|
||||
async function submitAdd() {
|
||||
const payload = buildAddPayload()
|
||||
if (!payload) return
|
||||
const wasEdit = !!editingId.value
|
||||
loadingMap.addSubmit = true
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await reimburseApi.update(editingId.value, payload)
|
||||
ElMessage.success('保存成功')
|
||||
} else {
|
||||
await reimburseApi.create(payload)
|
||||
ElMessage.success('新增成功')
|
||||
const id = await saveAddForm(payload)
|
||||
if (!id) {
|
||||
ElMessage.error('保存成功但未返回报销单 ID,请从列表重新操作')
|
||||
await refreshList()
|
||||
return
|
||||
}
|
||||
dialogs.add = false
|
||||
addForm.applicantId = currentUserId()
|
||||
addForm.reimburseDate = todayYmd()
|
||||
addForm.title = ''
|
||||
addForm.remark = ''
|
||||
addForm.details = [{ expenseItemId: '', amount: '', remark: '' }]
|
||||
selectedInvoicePicks.value = []
|
||||
editingId.value = null
|
||||
ElMessage.success(wasEdit ? '保存成功' : '新增成功')
|
||||
resetAddForm()
|
||||
await refreshList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || (editingId.value ? '保存失败' : '新增失败'))
|
||||
ElMessage.error(e.message || (wasEdit ? '保存失败' : '新增失败'))
|
||||
} finally {
|
||||
loadingMap.addSubmit = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存并提交审批 */
|
||||
async function submitAddAndApprove() {
|
||||
const payload = buildAddPayload()
|
||||
if (!payload) return
|
||||
loadingMap.addApprove = true
|
||||
try {
|
||||
const id = await saveAddForm(payload)
|
||||
if (!id) {
|
||||
ElMessage.error('保存成功但未返回报销单 ID,请从列表重新提交审批')
|
||||
await refreshList()
|
||||
return
|
||||
}
|
||||
// 保存成功后记下 id,提交失败时再次操作会走更新而非重复新建
|
||||
editingId.value = id
|
||||
await reimburseApi.submit(id)
|
||||
ElMessage.success('提交成功')
|
||||
resetAddForm()
|
||||
await refreshList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '提交失败')
|
||||
await refreshList()
|
||||
} finally {
|
||||
loadingMap.addApprove = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openView(row: any) {
|
||||
try {
|
||||
const res = await reimburseApi.getById(row.id)
|
||||
|
||||
@@ -886,11 +886,12 @@ function buildApprovedQuery() {
|
||||
return q
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
onMounted(() => {
|
||||
loadApplicantOptions()
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
// 列表优先加载;角标刷新勿阻塞列表请求(否则页面已渲染却迟迟不发 pending 列表)
|
||||
searchPending()
|
||||
tabLoaded.pending = true
|
||||
void refreshMenuBadges(menuStore.menuList)
|
||||
})
|
||||
|
||||
function handleTabChange(tab: string | number) {
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="filter-block">
|
||||
<el-form :model="filterForm" class="filter-form" @submit.prevent style="--filter-label-width: 110px">
|
||||
<el-form
|
||||
:model="filterForm"
|
||||
class="filter-form"
|
||||
@submit.prevent
|
||||
style="--filter-label-width: 110px"
|
||||
>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="单号">
|
||||
<el-input v-model="filterForm.code" placeholder="请输入单号" clearable />
|
||||
@@ -15,7 +20,12 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请人">
|
||||
<el-select v-model="filterForm.applicantId" placeholder="请选择申请人" clearable filterable>
|
||||
<el-select
|
||||
v-model="filterForm.applicantId"
|
||||
placeholder="请选择申请人"
|
||||
clearable
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in applicantOptions"
|
||||
:key="opt.value"
|
||||
@@ -86,7 +96,8 @@
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div ref="allTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-view-all"
|
||||
<ListTable
|
||||
table-key="bx-view-all"
|
||||
class="list-table"
|
||||
:data="pager.pagedRows.value"
|
||||
v-loading="loading"
|
||||
@@ -96,7 +107,13 @@
|
||||
@header-dragend="() => allStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('applyTime')"
|
||||
prop="applyTime"
|
||||
@@ -228,53 +245,119 @@
|
||||
<div class="dlg-view">
|
||||
<div class="dlg-view-hero">
|
||||
<div class="dlg-view-hero-top">
|
||||
<h2 class="dlg-view-title">{{ currentRow?.applyCode || '-' }}</h2>
|
||||
<h2 class="dlg-view-title">{{ viewHero.applyCode || '-' }}</h2>
|
||||
<div class="dlg-view-tags">
|
||||
<el-tag :type="bizTypeTagType(currentRow?.bizType)" size="small">
|
||||
{{ currentRow?.bizType || '-' }}
|
||||
<el-tag :type="bizTypeTagType(viewHero.bizType)" size="small">
|
||||
{{ viewHero.bizType || '-' }}
|
||||
</el-tag>
|
||||
<el-tag v-if="currentRow?.status" :type="statusTagType(currentRow.status)" size="small">
|
||||
{{ statusLabel(currentRow.status) }}
|
||||
<el-tag v-if="viewHero.status" :type="statusTagType(viewHero.status)" size="small">
|
||||
{{ statusLabel(viewHero.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<p class="dlg-view-meta">
|
||||
金额(元):<b class="money-em">{{ formatMoneyYuan(currentRow?.amount || 0) }}</b>
|
||||
· 申请人:{{ currentRow?.applicantName || '-' }}
|
||||
金额(元):<b class="money-em">{{ formatMoneyYuan(viewHero.amount || 0) }}</b> ·
|
||||
申请人:{{ viewHero.applicantName || '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">基础信息</h4>
|
||||
<BxViewKvTable :fields="viewDetailKvFields" mode="triple" />
|
||||
</div>
|
||||
<div
|
||||
v-if="currentRow?.bizType === '报销单' && viewLinkedInvoiceFiles.length"
|
||||
class="dlg-view-section"
|
||||
>
|
||||
<h4 class="dlg-view-section-title">关联发票</h4>
|
||||
<BxAttachmentReadonly :files="viewLinkedInvoiceFiles" />
|
||||
</div>
|
||||
<div
|
||||
v-if="currentRow?.bizType === '报销单' && viewAttachments.length"
|
||||
class="dlg-view-section"
|
||||
>
|
||||
<h4 class="dlg-view-section-title">历史附件</h4>
|
||||
<BxAttachmentReadonly :files="viewAttachments" />
|
||||
</div>
|
||||
<div
|
||||
v-if="currentRow?.bizType === '报销单' && viewPayAttachments.length"
|
||||
class="dlg-view-section"
|
||||
>
|
||||
<h4 class="dlg-view-section-title">发放附件</h4>
|
||||
<BxAttachmentReadonly :files="viewPayAttachments" />
|
||||
</div>
|
||||
<div
|
||||
v-if="currentRow?.bizType === '报销单' && viewOffsetAttachments.length"
|
||||
class="dlg-view-section"
|
||||
>
|
||||
<h4 class="dlg-view-section-title">冲抵附件</h4>
|
||||
<BxAttachmentReadonly :files="viewOffsetAttachments" />
|
||||
</div>
|
||||
<template v-if="viewHero.bizType === '报销单'">
|
||||
<div class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">报销明细</h4>
|
||||
<el-table
|
||||
:data="viewData.details || []"
|
||||
border
|
||||
size="small"
|
||||
show-summary
|
||||
:summary-method="viewDetailSummary"
|
||||
class="view-detail-table"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" />
|
||||
<el-table-column prop="expenseName" label="费用项目" min-width="160" />
|
||||
<el-table-column
|
||||
prop="amount"
|
||||
label="金额(元)"
|
||||
min-width="120"
|
||||
class-name="col-money"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatMoneyYuan(row.amount || 0) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="报销事由" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row.remark || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-if="viewLinkedInvoiceFiles.length" class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">关联发票</h4>
|
||||
<BxAttachmentReadonly :files="viewLinkedInvoiceFiles" />
|
||||
</div>
|
||||
<div v-if="viewAttachments.length" class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">历史附件</h4>
|
||||
<BxAttachmentReadonly :files="viewAttachments" />
|
||||
</div>
|
||||
<div v-if="viewPayAttachments.length" class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">发放附件</h4>
|
||||
<BxAttachmentReadonly :files="viewPayAttachments" />
|
||||
</div>
|
||||
<div v-if="viewOffsetAttachments.length" class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">冲抵附件</h4>
|
||||
<BxAttachmentReadonly :files="viewOffsetAttachments" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="viewHero.bizType === '备用金'">
|
||||
<BaseDetailTableSection
|
||||
title="冲抵记录"
|
||||
:data="viewOffsetRecords"
|
||||
empty-text="暂无冲抵记录"
|
||||
>
|
||||
<el-table :data="viewOffsetRecords" border size="small" class="view-detail-table">
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" />
|
||||
<el-table-column
|
||||
prop="reimburseApplyCode"
|
||||
label="报销单号"
|
||||
min-width="130"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="reimburseApplicantName"
|
||||
label="报销申请人"
|
||||
min-width="110"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="reimburseTitle"
|
||||
label="报销标题"
|
||||
min-width="160"
|
||||
align="left"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="冲抵金额(元)"
|
||||
min-width="120"
|
||||
class-name="col-money"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row: offsetRow }">
|
||||
{{ formatMoneyYuan(offsetRow.offsetAmount || 0) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="冲抵时间" min-width="160" align="center" />
|
||||
<el-table-column prop="operator" label="操作人" min-width="100" align="center" />
|
||||
</el-table>
|
||||
</BaseDetailTableSection>
|
||||
<div v-if="viewIssueAttachments.length" class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">发放附件</h4>
|
||||
<BxAttachmentReadonly :files="viewIssueAttachments" />
|
||||
</div>
|
||||
</template>
|
||||
<BxApprovalFlowTabs
|
||||
:nodes="viewFlowNodes"
|
||||
:history="viewApprovalHistory"
|
||||
@@ -286,366 +369,450 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close, Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
import { advanceApi, reimburseApi } from '@/api/bx'
|
||||
import {
|
||||
bizTypeTagType,
|
||||
statusLabel,
|
||||
statusTagType,
|
||||
formatMoneyYuan,
|
||||
moneyTableFormatter,
|
||||
workflowNodeDisplay,
|
||||
workflowNodeLabel,
|
||||
workflowNodeTagType,
|
||||
shouldShowWorkflowNodeTag
|
||||
} from '@/utils/bx/format'
|
||||
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
|
||||
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
|
||||
import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import { bxViewKv, bxViewKvMoney } from '@/utils/bx/view-kv'
|
||||
import { type BxAttachmentItem, loadBxAttachments } from '@/utils/bx/attachment'
|
||||
import { invoicesToLinkedAttachmentFiles } from '@/utils/bx/invoice-link-files'
|
||||
import { matchesApplyDateRange, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { usePagedTable } from '@/hooks/bx/usePagedTable'
|
||||
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
import { advanceApi, paymentApi, reimburseApi } from '@/api/bx'
|
||||
import {
|
||||
bizTypeTagType,
|
||||
statusLabel,
|
||||
statusTagType,
|
||||
formatMoneyYuan,
|
||||
moneyTableFormatter,
|
||||
workflowNodeDisplay,
|
||||
workflowNodeLabel,
|
||||
workflowNodeTagType,
|
||||
shouldShowWorkflowNodeTag
|
||||
} from '@/utils/bx/format'
|
||||
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
|
||||
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
|
||||
import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
|
||||
import BaseDetailTableSection from '@/components/bx/base/BaseDetailTableSection.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import { bxViewKv, bxViewKvLong, bxViewKvMoney } from '@/utils/bx/view-kv'
|
||||
import {
|
||||
type BxAttachmentItem,
|
||||
fillBxAttachmentPreviews,
|
||||
loadBxAttachments
|
||||
} from '@/utils/bx/attachment'
|
||||
import { invoicesToLinkedAttachmentFiles } from '@/utils/bx/invoice-link-files'
|
||||
import { matchesApplyDateRange, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { usePagedTable } from '@/hooks/bx/usePagedTable'
|
||||
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
|
||||
|
||||
type DialogKey = 'viewDetail'
|
||||
type DialogKey = 'viewDetail'
|
||||
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
viewDetail: false
|
||||
})
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
viewDetail: false
|
||||
})
|
||||
|
||||
const mergedRows = ref<any[]>([])
|
||||
const { applicantOptions, loadApplicantOptions } = useApplicantOptions()
|
||||
const mergedRows = ref<any[]>([])
|
||||
const { applicantOptions, loadApplicantOptions } = useApplicantOptions()
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'applyTime', label: '申请时间' },
|
||||
{ prop: 'bizType', label: '业务类型' },
|
||||
{ prop: 'applicantName', label: '申请人' },
|
||||
{ prop: 'title', label: '申请标题' },
|
||||
{ prop: 'amount', label: '申请金额(元)' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'currentNode', label: '当前节点', defaultVisible: false }
|
||||
]
|
||||
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-view-all-list-columns', listColumnDefs)
|
||||
|
||||
const allTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const allStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const allStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
const listColumnDefs = [
|
||||
{ prop: 'applyTime', label: '申请时间' },
|
||||
{ prop: 'bizType', label: '业务类型' },
|
||||
{ prop: 'applicantName', label: '申请人' },
|
||||
{ prop: 'title', label: '申请标题' },
|
||||
{ prop: 'amount', label: '申请金额(元)' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'currentNode', label: '当前节点', defaultVisible: false }
|
||||
]
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
const quickSearchKeyword = ref('')
|
||||
const appliedQuickSearch = ref('')
|
||||
const { visibleColumns, checkAll, isIndeterminate, isColumnVisible, resetToDefault } =
|
||||
usePersistedListColumns('bx-view-all-list-columns', listColumnDefs)
|
||||
|
||||
const filterForm = reactive({
|
||||
code: '',
|
||||
bizType: '' as string,
|
||||
applicantId: undefined as number | undefined,
|
||||
status: '' as string,
|
||||
applyDateRange: null as ApplyDateRange,
|
||||
amountRange: '',
|
||||
currentNode: '' as string
|
||||
})
|
||||
const allTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const allStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const appliedFilter = reactive({
|
||||
code: '',
|
||||
bizType: '' as string,
|
||||
applicantId: undefined as number | undefined,
|
||||
status: '' as string,
|
||||
applyDateRange: null as ApplyDateRange,
|
||||
amountRange: '',
|
||||
currentNode: '' as string
|
||||
})
|
||||
|
||||
function syncAppliedFilter() {
|
||||
Object.assign(appliedFilter, { ...filterForm })
|
||||
}
|
||||
|
||||
function filterMergedRows(rows: any[]) {
|
||||
let list = [...rows]
|
||||
const quickKw = appliedQuickSearch.value.trim().toLowerCase()
|
||||
if (quickKw) {
|
||||
list = list.filter(
|
||||
(r: any) =>
|
||||
String(r.applyCode || '')
|
||||
.toLowerCase()
|
||||
.includes(quickKw) ||
|
||||
String(r.title || '')
|
||||
.toLowerCase()
|
||||
.includes(quickKw) ||
|
||||
String(r.applicantName || '')
|
||||
.toLowerCase()
|
||||
.includes(quickKw)
|
||||
)
|
||||
}
|
||||
const code = appliedFilter.code.trim()
|
||||
if (code) list = list.filter((r: any) => String(r.applyCode || '').includes(code))
|
||||
if (appliedFilter.bizType && appliedFilter.bizType !== '全部') {
|
||||
list = list.filter((r: any) => r.bizType === appliedFilter.bizType)
|
||||
}
|
||||
if (appliedFilter.applicantId != null) {
|
||||
list = list.filter((r: any) => Number(r.applicantId) === Number(appliedFilter.applicantId))
|
||||
}
|
||||
if (appliedFilter.applyDateRange?.[0] || appliedFilter.applyDateRange?.[1]) {
|
||||
list = list.filter((r: any) => matchesApplyDateRange(r.applyTime, appliedFilter.applyDateRange))
|
||||
}
|
||||
if (appliedFilter.status && appliedFilter.status !== '全部') {
|
||||
if (appliedFilter.status === '已发放') {
|
||||
list = list.filter((r: any) => ['issued', 'paid', 'offset'].includes(r.status))
|
||||
} else {
|
||||
const map: Record<string, string> = {
|
||||
待提交: 'draft',
|
||||
审批中: 'pending',
|
||||
审批通过: 'approved',
|
||||
审批驳回: 'rejected'
|
||||
}
|
||||
const want = map[appliedFilter.status]
|
||||
if (want) list = list.filter((r: any) => r.status === want)
|
||||
const allStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
]
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
}
|
||||
if (appliedFilter.currentNode && appliedFilter.currentNode !== '全部') {
|
||||
list = list.filter((r: any) => (r.currentNode || '') === appliedFilter.currentNode)
|
||||
}
|
||||
if (appliedFilter.amountRange?.trim()) {
|
||||
const parts = appliedFilter.amountRange.split(/[-–]/).map((x: string) => Number(String(x).trim()))
|
||||
if (parts.length >= 2 && !parts.some((n) => Number.isNaN(n))) {
|
||||
const lo = Math.min(parts[0], parts[1])
|
||||
const hi = Math.max(parts[0], parts[1])
|
||||
list = list.filter((r: any) => {
|
||||
const amt = Number(r.amount || 0)
|
||||
return amt >= lo && amt <= hi
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
const filteredMergedRows = computed(() => filterMergedRows(mergedRows.value))
|
||||
|
||||
const pager = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 20 })
|
||||
const currentRow = ref<any>(null)
|
||||
|
||||
const viewDetailKvFields = computed<ViewField[]>(() => {
|
||||
const r = currentRow.value
|
||||
return [
|
||||
bxViewKv('申请单号', r?.applyCode, { long: false }),
|
||||
bxViewKv('业务类型', r?.bizType, { long: false, tagType: bizTypeTagType(r?.bizType) }),
|
||||
bxViewKv('申请人', r?.applicantName, { long: false }),
|
||||
bxViewKv('申请标题', r?.title, { long: false }),
|
||||
bxViewKvMoney('申请金额(元)', formatMoneyYuan(r?.amount || 0), { long: false }),
|
||||
bxViewKv('状态', statusLabel(r?.status), { long: false, tagType: statusTagType(r?.status) }),
|
||||
bxViewKv('当前节点', workflowNodeDisplay(r?.currentNode, r?.status), { long: false }),
|
||||
bxViewKv('申请时间', r?.applyTime, { long: false })
|
||||
]
|
||||
})
|
||||
const viewFlowNodes = ref<any[]>([])
|
||||
const viewApprovalHistory = ref<any[]>([])
|
||||
const viewFlowLoading = ref(false)
|
||||
const viewLinkedInvoiceFiles = ref<BxAttachmentItem[]>([])
|
||||
const viewAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewPayAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewOffsetAttachments = ref<BxAttachmentItem[]>([])
|
||||
|
||||
const bizTypeOptions = ['全部', '报销单', '备用金']
|
||||
const statusOptions = ['全部', '待提交', '审批中', '审批通过', '审批驳回', '已发放']
|
||||
const nodeOptions = ['全部', '草稿', '发起人主管审批', '财务复核', '总经理审批', '已完成', '已驳回', '已发放']
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [advRes, reimRes]: any[] = await Promise.all([
|
||||
advanceApi.list({ page: 1, pageSize: 999 }),
|
||||
reimburseApi.list({ page: 1, pageSize: 999 })
|
||||
])
|
||||
const advRows = (advRes.data?.rows || []).map((r: any) => ({
|
||||
...r,
|
||||
bizType: '备用金',
|
||||
title: r.reason,
|
||||
applyCode: r.applyCode || r.code,
|
||||
applicantName: r.applicantName || r.user,
|
||||
applicantId: r.applicantId
|
||||
}))
|
||||
const reimRows = (reimRes.data?.rows || []).map((r: any) => ({
|
||||
...r,
|
||||
bizType: '报销单',
|
||||
title: r.title,
|
||||
applyCode: r.applyCode || r.code,
|
||||
applicantName: r.applicantName || r.user,
|
||||
applicantId: r.applicantId
|
||||
}))
|
||||
mergedRows.value = [...advRows, ...reimRows]
|
||||
} catch {
|
||||
// error handled by interceptor
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
quickSearchKeyword.value = ''
|
||||
appliedQuickSearch.value = ''
|
||||
syncAppliedFilter()
|
||||
pager.currentPage.value = 1
|
||||
}
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({
|
||||
code: filterForm.code,
|
||||
bizType: filterForm.bizType,
|
||||
applicantId: filterForm.applicantId,
|
||||
status: filterForm.status,
|
||||
applyDateRange: filterForm.applyDateRange,
|
||||
amountRange: filterForm.amountRange,
|
||||
currentNode: filterForm.currentNode
|
||||
}),
|
||||
handleSearch
|
||||
)
|
||||
|
||||
function handleReset() {
|
||||
quickSearchKeyword.value = ''
|
||||
appliedQuickSearch.value = ''
|
||||
Object.assign(filterForm, {
|
||||
code: '',
|
||||
bizType: '',
|
||||
applicantId: undefined,
|
||||
status: '',
|
||||
applyDateRange: null,
|
||||
amountRange: '',
|
||||
currentNode: ''
|
||||
return cols
|
||||
})
|
||||
Object.assign(appliedFilter, {
|
||||
|
||||
const quickSearchKeyword = ref('')
|
||||
const appliedQuickSearch = ref('')
|
||||
|
||||
const filterForm = reactive({
|
||||
code: '',
|
||||
bizType: '',
|
||||
applicantId: undefined,
|
||||
status: '',
|
||||
applyDateRange: null,
|
||||
bizType: '' as string,
|
||||
applicantId: undefined as number | undefined,
|
||||
status: '' as string,
|
||||
applyDateRange: null as ApplyDateRange,
|
||||
amountRange: '',
|
||||
currentNode: ''
|
||||
currentNode: '' as string
|
||||
})
|
||||
pager.currentPage.value = 1
|
||||
}
|
||||
|
||||
function handleQuickSearch() {
|
||||
appliedQuickSearch.value = quickSearchKeyword.value.trim()
|
||||
pager.currentPage.value = 1
|
||||
}
|
||||
const appliedFilter = reactive({
|
||||
code: '',
|
||||
bizType: '' as string,
|
||||
applicantId: undefined as number | undefined,
|
||||
status: '' as string,
|
||||
applyDateRange: null as ApplyDateRange,
|
||||
amountRange: '',
|
||||
currentNode: '' as string
|
||||
})
|
||||
|
||||
function handleQuickSearchClear() {
|
||||
quickSearchKeyword.value = ''
|
||||
appliedQuickSearch.value = ''
|
||||
pager.currentPage.value = 1
|
||||
}
|
||||
function syncAppliedFilter() {
|
||||
Object.assign(appliedFilter, { ...filterForm })
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: any[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function openViewDetail(row: any) {
|
||||
currentRow.value = row
|
||||
viewFlowNodes.value = []
|
||||
viewApprovalHistory.value = []
|
||||
viewLinkedInvoiceFiles.value = []
|
||||
viewAttachments.value = []
|
||||
viewPayAttachments.value = []
|
||||
viewOffsetAttachments.value = []
|
||||
viewFlowLoading.value = true
|
||||
dialogs.viewDetail = true
|
||||
try {
|
||||
const api = row.bizType === '备用金' ? advanceApi : reimburseApi
|
||||
const tasks: Promise<any>[] = [
|
||||
api.getApprovals(row.id),
|
||||
api.getApprovalHistory(row.id)
|
||||
]
|
||||
if (row.bizType === '报销单') {
|
||||
tasks.push(
|
||||
reimburseApi
|
||||
.getById(row.id)
|
||||
.then((res: any) => {
|
||||
const data = res?.data || res || {}
|
||||
viewLinkedInvoiceFiles.value = invoicesToLinkedAttachmentFiles(data.invoices || [])
|
||||
})
|
||||
.catch(() => {
|
||||
viewLinkedInvoiceFiles.value = []
|
||||
}),
|
||||
loadBxAttachments('reimburse_apply', row.id)
|
||||
.then((files) => {
|
||||
viewAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewAttachments.value = []
|
||||
}),
|
||||
loadBxAttachments('reimburse_pay', row.id)
|
||||
.then((files) => {
|
||||
viewPayAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewPayAttachments.value = []
|
||||
}),
|
||||
loadBxAttachments('reimburse_offset', row.id)
|
||||
.then((files) => {
|
||||
viewOffsetAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewOffsetAttachments.value = []
|
||||
})
|
||||
function filterMergedRows(rows: any[]) {
|
||||
let list = [...rows]
|
||||
const quickKw = appliedQuickSearch.value.trim().toLowerCase()
|
||||
if (quickKw) {
|
||||
list = list.filter(
|
||||
(r: any) =>
|
||||
String(r.applyCode || '')
|
||||
.toLowerCase()
|
||||
.includes(quickKw) ||
|
||||
String(r.title || '')
|
||||
.toLowerCase()
|
||||
.includes(quickKw) ||
|
||||
String(r.applicantName || '')
|
||||
.toLowerCase()
|
||||
.includes(quickKw)
|
||||
)
|
||||
}
|
||||
const [flowRes, historyRes]: any[] = await Promise.all(tasks)
|
||||
viewFlowNodes.value = (flowRes.data || []).sort((a: any, b: any) => a.nodeOrder - b.nodeOrder)
|
||||
viewApprovalHistory.value = historyRes.data || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '加载审批流程失败')
|
||||
} finally {
|
||||
viewFlowLoading.value = false
|
||||
const code = appliedFilter.code.trim()
|
||||
if (code) list = list.filter((r: any) => String(r.applyCode || '').includes(code))
|
||||
if (appliedFilter.bizType && appliedFilter.bizType !== '全部') {
|
||||
list = list.filter((r: any) => r.bizType === appliedFilter.bizType)
|
||||
}
|
||||
if (appliedFilter.applicantId != null) {
|
||||
list = list.filter((r: any) => Number(r.applicantId) === Number(appliedFilter.applicantId))
|
||||
}
|
||||
if (appliedFilter.applyDateRange?.[0] || appliedFilter.applyDateRange?.[1]) {
|
||||
list = list.filter((r: any) =>
|
||||
matchesApplyDateRange(r.applyTime, appliedFilter.applyDateRange)
|
||||
)
|
||||
}
|
||||
if (appliedFilter.status && appliedFilter.status !== '全部') {
|
||||
if (appliedFilter.status === '已发放') {
|
||||
list = list.filter((r: any) => ['issued', 'paid', 'offset'].includes(r.status))
|
||||
} else {
|
||||
const map: Record<string, string> = {
|
||||
待提交: 'draft',
|
||||
审批中: 'pending',
|
||||
审批通过: 'approved',
|
||||
审批驳回: 'rejected'
|
||||
}
|
||||
const want = map[appliedFilter.status]
|
||||
if (want) list = list.filter((r: any) => r.status === want)
|
||||
}
|
||||
}
|
||||
if (appliedFilter.currentNode && appliedFilter.currentNode !== '全部') {
|
||||
list = list.filter((r: any) => (r.currentNode || '') === appliedFilter.currentNode)
|
||||
}
|
||||
if (appliedFilter.amountRange?.trim()) {
|
||||
const parts = appliedFilter.amountRange
|
||||
.split(/[-–]/)
|
||||
.map((x: string) => Number(String(x).trim()))
|
||||
if (parts.length >= 2 && !parts.some((n) => Number.isNaN(n))) {
|
||||
const lo = Math.min(parts[0], parts[1])
|
||||
const hi = Math.max(parts[0], parts[1])
|
||||
list = list.filter((r: any) => {
|
||||
const amt = Number(r.amount || 0)
|
||||
return amt >= lo && amt <= hi
|
||||
})
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadApplicantOptions()
|
||||
loadData()
|
||||
})
|
||||
const filteredMergedRows = computed(() => filterMergedRows(mergedRows.value))
|
||||
|
||||
const pager = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 20 })
|
||||
const currentRow = ref<any>(null)
|
||||
const viewData = ref<Record<string, any>>({})
|
||||
|
||||
const viewHero = computed(() => {
|
||||
const row = currentRow.value || {}
|
||||
const detail = viewData.value || {}
|
||||
return {
|
||||
...row,
|
||||
...detail,
|
||||
bizType: row.bizType || detail.bizType,
|
||||
applyCode: detail.applyCode || row.applyCode,
|
||||
applicantName: detail.applicantName || row.applicantName,
|
||||
amount: detail.amount ?? row.amount,
|
||||
currentNode: detail.currentNode ?? row.currentNode,
|
||||
status: detail.status ?? row.status
|
||||
}
|
||||
})
|
||||
|
||||
const viewDetailKvFields = computed<ViewField[]>(() => {
|
||||
const d = viewHero.value
|
||||
if (d.bizType === '备用金') {
|
||||
return [
|
||||
bxViewKv('申请单号', d.applyCode, { long: false }),
|
||||
bxViewKv('业务类型', d.bizType, { long: false, tagType: bizTypeTagType(d.bizType) }),
|
||||
bxViewKv('申请人', d.applicantName, { long: false }),
|
||||
bxViewKvMoney(
|
||||
'实际发放金额(元)',
|
||||
d.actualAmount != null ? formatMoneyYuan(d.actualAmount) : '—',
|
||||
{ long: false }
|
||||
),
|
||||
bxViewKvMoney('申请金额(元)', formatMoneyYuan(d.amount || 0), { long: false }),
|
||||
bxViewKvMoney('冲抵总金额(元)', formatMoneyYuan(d.offsetAmount || 0), { long: false }),
|
||||
bxViewKv('状态', statusLabel(d.status), { long: false, tagType: statusTagType(d.status) }),
|
||||
bxViewKv('申请时间', d.applyTime, { long: false }),
|
||||
bxViewKv('申请原因', d.reason || d.title, { long: false })
|
||||
]
|
||||
}
|
||||
return [
|
||||
bxViewKv('申请单号', d.applyCode, { long: false }),
|
||||
bxViewKv('业务类型', d.bizType, { long: false, tagType: bizTypeTagType(d.bizType) }),
|
||||
bxViewKv('报销日期', d.reimburseDate, { long: false }),
|
||||
bxViewKv('报销标题', d.title, { long: false }),
|
||||
bxViewKv('申请人', d.applicantName, { long: false }),
|
||||
bxViewKvMoney('申请金额(元)', formatMoneyYuan(d.amount || 0), { long: false }),
|
||||
bxViewKv('状态', statusLabel(d.status), { long: false, tagType: statusTagType(d.status) }),
|
||||
bxViewKv('当前节点', workflowNodeDisplay(d.currentNode, d.status), { long: false }),
|
||||
bxViewKv('申请时间', d.applyTime, { long: false }),
|
||||
bxViewKvLong('报销说明', d.remark, { fullSpan: true })
|
||||
]
|
||||
})
|
||||
const viewFlowNodes = ref<any[]>([])
|
||||
const viewApprovalHistory = ref<any[]>([])
|
||||
const viewFlowLoading = ref(false)
|
||||
const viewLinkedInvoiceFiles = ref<BxAttachmentItem[]>([])
|
||||
const viewAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewPayAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewOffsetAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewIssueAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewOffsetRecords = ref<any[]>([])
|
||||
|
||||
function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
const sums: string[] = []
|
||||
columns.forEach((_: any, index: number) => {
|
||||
if (index === 0) {
|
||||
sums[index] = '合计'
|
||||
return
|
||||
}
|
||||
if (index === 2) {
|
||||
const total = data.reduce((sum: number, row: any) => sum + Number(row.amount || 0), 0)
|
||||
sums[index] = formatMoneyYuan(total)
|
||||
return
|
||||
}
|
||||
sums[index] = ''
|
||||
})
|
||||
return sums
|
||||
}
|
||||
|
||||
const bizTypeOptions = ['全部', '报销单', '备用金']
|
||||
const statusOptions = ['全部', '待提交', '审批中', '审批通过', '审批驳回', '已发放']
|
||||
const nodeOptions = [
|
||||
'全部',
|
||||
'草稿',
|
||||
'发起人主管审批',
|
||||
'财务复核',
|
||||
'总经理审批',
|
||||
'已完成',
|
||||
'已驳回',
|
||||
'已发放'
|
||||
]
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [advRes, reimRes]: any[] = await Promise.all([
|
||||
advanceApi.list({ page: 1, pageSize: 999 }),
|
||||
reimburseApi.list({ page: 1, pageSize: 999 })
|
||||
])
|
||||
const advRows = (advRes.data?.rows || []).map((r: any) => ({
|
||||
...r,
|
||||
bizType: '备用金',
|
||||
title: r.reason,
|
||||
applyCode: r.applyCode || r.code,
|
||||
applicantName: r.applicantName || r.user,
|
||||
applicantId: r.applicantId
|
||||
}))
|
||||
const reimRows = (reimRes.data?.rows || []).map((r: any) => ({
|
||||
...r,
|
||||
bizType: '报销单',
|
||||
title: r.title,
|
||||
applyCode: r.applyCode || r.code,
|
||||
applicantName: r.applicantName || r.user,
|
||||
applicantId: r.applicantId
|
||||
}))
|
||||
mergedRows.value = [...advRows, ...reimRows]
|
||||
} catch {
|
||||
// error handled by interceptor
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
quickSearchKeyword.value = ''
|
||||
appliedQuickSearch.value = ''
|
||||
syncAppliedFilter()
|
||||
pager.currentPage.value = 1
|
||||
}
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({
|
||||
code: filterForm.code,
|
||||
bizType: filterForm.bizType,
|
||||
applicantId: filterForm.applicantId,
|
||||
status: filterForm.status,
|
||||
applyDateRange: filterForm.applyDateRange,
|
||||
amountRange: filterForm.amountRange,
|
||||
currentNode: filterForm.currentNode
|
||||
}),
|
||||
handleSearch
|
||||
)
|
||||
|
||||
function handleReset() {
|
||||
quickSearchKeyword.value = ''
|
||||
appliedQuickSearch.value = ''
|
||||
Object.assign(filterForm, {
|
||||
code: '',
|
||||
bizType: '',
|
||||
applicantId: undefined,
|
||||
status: '',
|
||||
applyDateRange: null,
|
||||
amountRange: '',
|
||||
currentNode: ''
|
||||
})
|
||||
Object.assign(appliedFilter, {
|
||||
code: '',
|
||||
bizType: '',
|
||||
applicantId: undefined,
|
||||
status: '',
|
||||
applyDateRange: null,
|
||||
amountRange: '',
|
||||
currentNode: ''
|
||||
})
|
||||
pager.currentPage.value = 1
|
||||
}
|
||||
|
||||
function handleQuickSearch() {
|
||||
appliedQuickSearch.value = quickSearchKeyword.value.trim()
|
||||
pager.currentPage.value = 1
|
||||
}
|
||||
|
||||
function handleQuickSearchClear() {
|
||||
quickSearchKeyword.value = ''
|
||||
appliedQuickSearch.value = ''
|
||||
pager.currentPage.value = 1
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: any[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function openViewDetail(row: any) {
|
||||
currentRow.value = row
|
||||
viewData.value = {}
|
||||
viewFlowNodes.value = []
|
||||
viewApprovalHistory.value = []
|
||||
viewLinkedInvoiceFiles.value = []
|
||||
viewAttachments.value = []
|
||||
viewPayAttachments.value = []
|
||||
viewOffsetAttachments.value = []
|
||||
viewIssueAttachments.value = []
|
||||
viewOffsetRecords.value = []
|
||||
viewFlowLoading.value = true
|
||||
dialogs.viewDetail = true
|
||||
const isAdvance = row.bizType === '备用金'
|
||||
const api = isAdvance ? advanceApi : reimburseApi
|
||||
try {
|
||||
const detailRes: any = await api.getById(row.id)
|
||||
viewData.value = detailRes?.data || detailRes || {}
|
||||
|
||||
const tasks: Promise<any>[] = [api.getApprovals(row.id), api.getApprovalHistory(row.id)]
|
||||
if (isAdvance) {
|
||||
tasks.push(
|
||||
paymentApi
|
||||
.advanceOffsetRecords(row.id)
|
||||
.then((res: any) => {
|
||||
viewOffsetRecords.value = res?.data || []
|
||||
})
|
||||
.catch(() => {
|
||||
viewOffsetRecords.value = []
|
||||
}),
|
||||
loadBxAttachments('advance_issue', row.id)
|
||||
.then((files) => {
|
||||
viewIssueAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewIssueAttachments.value = []
|
||||
})
|
||||
)
|
||||
} else {
|
||||
const linked = invoicesToLinkedAttachmentFiles(viewData.value.invoices || [])
|
||||
viewLinkedInvoiceFiles.value = linked
|
||||
tasks.push(
|
||||
fillBxAttachmentPreviews(linked).catch(() => undefined),
|
||||
loadBxAttachments('reimburse_apply', row.id)
|
||||
.then((files) => {
|
||||
viewAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewAttachments.value = []
|
||||
}),
|
||||
loadBxAttachments('reimburse_pay', row.id)
|
||||
.then((files) => {
|
||||
viewPayAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewPayAttachments.value = []
|
||||
}),
|
||||
loadBxAttachments('reimburse_offset', row.id)
|
||||
.then((files) => {
|
||||
viewOffsetAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewOffsetAttachments.value = []
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const [flowRes, historyRes]: any[] = await Promise.all(tasks)
|
||||
viewFlowNodes.value = (flowRes.data || []).sort((a: any, b: any) => a.nodeOrder - b.nodeOrder)
|
||||
viewApprovalHistory.value = historyRes.data || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '加载详情失败')
|
||||
} finally {
|
||||
viewFlowLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadApplicantOptions()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,12 @@
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="filter-block">
|
||||
<el-form :model="filterForm" class="filter-form" @submit.prevent style="--filter-label-width: 110px">
|
||||
<el-form
|
||||
:model="filterForm"
|
||||
class="filter-form"
|
||||
@submit.prevent
|
||||
style="--filter-label-width: 110px"
|
||||
>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="单号">
|
||||
<el-input v-model="filterForm.code" placeholder="请输入单号" clearable />
|
||||
@@ -45,10 +50,21 @@
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-start" />
|
||||
<div class="filter-actions-end">
|
||||
<el-button native-type="button" class="btn-ghost" :loading="searchLoading" @click="handleReset">
|
||||
<el-button
|
||||
native-type="button"
|
||||
class="btn-ghost"
|
||||
:loading="searchLoading"
|
||||
@click="handleReset"
|
||||
>
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button native-type="button" class="btn-main" type="primary" :loading="searchLoading" @click="handleSearch">
|
||||
<el-button
|
||||
native-type="button"
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="searchLoading"
|
||||
@click="handleSearch"
|
||||
>
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -78,7 +94,8 @@
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div ref="mineTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-view-mine"
|
||||
<ListTable
|
||||
table-key="bx-view-mine"
|
||||
class="list-table"
|
||||
:data="pagedRows"
|
||||
v-loading="tableLoading"
|
||||
@@ -88,7 +105,13 @@
|
||||
@header-dragend="() => mineStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('applyTime')"
|
||||
prop="applyTime"
|
||||
@@ -211,52 +234,118 @@
|
||||
<div class="dlg-view">
|
||||
<div class="dlg-view-hero">
|
||||
<div class="dlg-view-hero-top">
|
||||
<h2 class="dlg-view-title">{{ currentRow?.applyCode || '-' }}</h2>
|
||||
<h2 class="dlg-view-title">{{ viewHero.applyCode || '-' }}</h2>
|
||||
<div class="dlg-view-tags">
|
||||
<el-tag :type="statusTagType(currentRow?.status)" size="small">
|
||||
{{ statusLabel(currentRow?.status) }}
|
||||
<el-tag :type="statusTagType(viewHero.status)" size="small">
|
||||
{{ statusLabel(viewHero.status) }}
|
||||
</el-tag>
|
||||
<el-tag :type="bizTypeTagType(currentRow?.bizType)" size="small">
|
||||
{{ currentRow?.bizType || '-' }}
|
||||
<el-tag :type="bizTypeTagType(viewHero.bizType)" size="small">
|
||||
{{ viewHero.bizType || '-' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<p class="dlg-view-meta">
|
||||
金额(元):<b class="money-em">{{ formatMoneyYuan(currentRow?.amount || 0) }}</b>
|
||||
金额(元):<b class="money-em">{{ formatMoneyYuan(viewHero.amount || 0) }}</b>
|
||||
</p>
|
||||
</div>
|
||||
<div class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">基础信息</h4>
|
||||
<BxViewKvTable :fields="viewDetailKvFields" mode="triple" />
|
||||
</div>
|
||||
<div
|
||||
v-if="currentRow?.bizType === '报销单' && viewLinkedInvoiceFiles.length"
|
||||
class="dlg-view-section"
|
||||
>
|
||||
<h4 class="dlg-view-section-title">关联发票</h4>
|
||||
<BxAttachmentReadonly :files="viewLinkedInvoiceFiles" />
|
||||
</div>
|
||||
<div
|
||||
v-if="currentRow?.bizType === '报销单' && viewAttachments.length"
|
||||
class="dlg-view-section"
|
||||
>
|
||||
<h4 class="dlg-view-section-title">历史附件</h4>
|
||||
<BxAttachmentReadonly :files="viewAttachments" />
|
||||
</div>
|
||||
<div
|
||||
v-if="currentRow?.bizType === '报销单' && viewPayAttachments.length"
|
||||
class="dlg-view-section"
|
||||
>
|
||||
<h4 class="dlg-view-section-title">发放附件</h4>
|
||||
<BxAttachmentReadonly :files="viewPayAttachments" />
|
||||
</div>
|
||||
<div
|
||||
v-if="currentRow?.bizType === '报销单' && viewOffsetAttachments.length"
|
||||
class="dlg-view-section"
|
||||
>
|
||||
<h4 class="dlg-view-section-title">冲抵附件</h4>
|
||||
<BxAttachmentReadonly :files="viewOffsetAttachments" />
|
||||
</div>
|
||||
<template v-if="viewHero.bizType === '报销单'">
|
||||
<div class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">报销明细</h4>
|
||||
<el-table
|
||||
:data="viewData.details || []"
|
||||
border
|
||||
size="small"
|
||||
show-summary
|
||||
:summary-method="viewDetailSummary"
|
||||
class="view-detail-table"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" />
|
||||
<el-table-column prop="expenseName" label="费用项目" min-width="160" />
|
||||
<el-table-column
|
||||
prop="amount"
|
||||
label="金额(元)"
|
||||
min-width="120"
|
||||
class-name="col-money"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatMoneyYuan(row.amount || 0) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="报销事由" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row.remark || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-if="viewLinkedInvoiceFiles.length" class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">关联发票</h4>
|
||||
<BxAttachmentReadonly :files="viewLinkedInvoiceFiles" />
|
||||
</div>
|
||||
<div v-if="viewAttachments.length" class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">历史附件</h4>
|
||||
<BxAttachmentReadonly :files="viewAttachments" />
|
||||
</div>
|
||||
<div v-if="viewPayAttachments.length" class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">发放附件</h4>
|
||||
<BxAttachmentReadonly :files="viewPayAttachments" />
|
||||
</div>
|
||||
<div v-if="viewOffsetAttachments.length" class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">冲抵附件</h4>
|
||||
<BxAttachmentReadonly :files="viewOffsetAttachments" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="viewHero.bizType === '备用金'">
|
||||
<BaseDetailTableSection
|
||||
title="冲抵记录"
|
||||
:data="viewOffsetRecords"
|
||||
empty-text="暂无冲抵记录"
|
||||
>
|
||||
<el-table :data="viewOffsetRecords" border size="small" class="view-detail-table">
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" />
|
||||
<el-table-column
|
||||
prop="reimburseApplyCode"
|
||||
label="报销单号"
|
||||
min-width="130"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="reimburseApplicantName"
|
||||
label="报销申请人"
|
||||
min-width="110"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="reimburseTitle"
|
||||
label="报销标题"
|
||||
min-width="160"
|
||||
align="left"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="冲抵金额(元)"
|
||||
min-width="120"
|
||||
class-name="col-money"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row: offsetRow }">
|
||||
{{ formatMoneyYuan(offsetRow.offsetAmount || 0) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="冲抵时间" min-width="160" align="center" />
|
||||
<el-table-column prop="operator" label="操作人" min-width="100" align="center" />
|
||||
</el-table>
|
||||
</BaseDetailTableSection>
|
||||
<div v-if="viewIssueAttachments.length" class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">发放附件</h4>
|
||||
<BxAttachmentReadonly :files="viewIssueAttachments" />
|
||||
</div>
|
||||
</template>
|
||||
<BxApprovalFlowTabs
|
||||
:nodes="viewFlowNodes"
|
||||
:history="viewApprovalHistory"
|
||||
@@ -268,343 +357,427 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { computed, onActivated, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close, Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
import { advanceApi, reimburseApi } from '@/api/bx'
|
||||
import {
|
||||
bizTypeTagType,
|
||||
statusLabel,
|
||||
statusTagType,
|
||||
formatMoneyYuan,
|
||||
moneyTableFormatter,
|
||||
workflowNodeDisplay,
|
||||
workflowNodeLabel,
|
||||
workflowNodeTagType,
|
||||
shouldShowWorkflowNodeTag
|
||||
} from '@/utils/bx/format'
|
||||
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
|
||||
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
|
||||
import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import { bxViewKv, bxViewKvMoney } from '@/utils/bx/view-kv'
|
||||
import { type BxAttachmentItem, loadBxAttachments } from '@/utils/bx/attachment'
|
||||
import { invoicesToLinkedAttachmentFiles } from '@/utils/bx/invoice-link-files'
|
||||
import { buildApplyDateRangeQuery, matchesApplyDateRange, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { usePagedTable } from '@/hooks/bx/usePagedTable'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { computed, onActivated, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
import { advanceApi, paymentApi, reimburseApi } from '@/api/bx'
|
||||
import {
|
||||
bizTypeTagType,
|
||||
statusLabel,
|
||||
statusTagType,
|
||||
formatMoneyYuan,
|
||||
moneyTableFormatter,
|
||||
workflowNodeDisplay,
|
||||
workflowNodeLabel,
|
||||
workflowNodeTagType,
|
||||
shouldShowWorkflowNodeTag
|
||||
} from '@/utils/bx/format'
|
||||
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
|
||||
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
|
||||
import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
|
||||
import BaseDetailTableSection from '@/components/bx/base/BaseDetailTableSection.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import { bxViewKv, bxViewKvLong, bxViewKvMoney } from '@/utils/bx/view-kv'
|
||||
import {
|
||||
type BxAttachmentItem,
|
||||
fillBxAttachmentPreviews,
|
||||
loadBxAttachments
|
||||
} from '@/utils/bx/attachment'
|
||||
import { invoicesToLinkedAttachmentFiles } from '@/utils/bx/invoice-link-files'
|
||||
import { buildApplyDateRangeQuery, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { usePagedTable } from '@/hooks/bx/usePagedTable'
|
||||
|
||||
type DialogKey = 'viewDetail'
|
||||
type DialogKey = 'viewDetail'
|
||||
|
||||
const tableLoading = ref(false)
|
||||
const searchLoading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
viewDetail: false
|
||||
})
|
||||
const tableLoading = ref(false)
|
||||
const searchLoading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
viewDetail: false
|
||||
})
|
||||
|
||||
const mergedRows = ref<any[]>([])
|
||||
const mergedRows = ref<any[]>([])
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'applyTime', label: '申请时间' },
|
||||
{ prop: 'bizType', label: '业务类型' },
|
||||
{ prop: 'title', label: '申请标题' },
|
||||
{ prop: 'amount', label: '申请金额(元)' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'currentNode', label: '当前节点', defaultVisible: false }
|
||||
]
|
||||
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-view-mine-list-columns', listColumnDefs)
|
||||
|
||||
const mineTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const mineStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const mineStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
const listColumnDefs = [
|
||||
{ prop: 'applyTime', label: '申请时间' },
|
||||
{ prop: 'bizType', label: '业务类型' },
|
||||
{ prop: 'title', label: '申请标题' },
|
||||
{ prop: 'amount', label: '申请金额(元)' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'currentNode', label: '当前节点', defaultVisible: false }
|
||||
]
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
const quickSearchKeyword = ref('')
|
||||
const { visibleColumns, checkAll, isIndeterminate, isColumnVisible, resetToDefault } =
|
||||
usePersistedListColumns('bx-view-mine-list-columns', listColumnDefs)
|
||||
|
||||
const filterForm = reactive({
|
||||
code: '',
|
||||
bizType: '' as string,
|
||||
status: '' as string,
|
||||
applyDateRange: null as ApplyDateRange,
|
||||
amountRange: ''
|
||||
})
|
||||
const mineTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const mineStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const appliedFilter = reactive({
|
||||
code: '',
|
||||
bizType: '' as string,
|
||||
status: '' as string,
|
||||
applyDateRange: null as ApplyDateRange,
|
||||
amountRange: ''
|
||||
})
|
||||
|
||||
function syncAppliedFilter() {
|
||||
Object.assign(appliedFilter, { ...filterForm })
|
||||
}
|
||||
|
||||
function filterMineRows(rows: any[]) {
|
||||
let list = [...rows]
|
||||
const kw = appliedFilter.code.trim()
|
||||
if (kw) {
|
||||
list = list.filter(
|
||||
(r: any) =>
|
||||
String(r.applyCode || '').includes(kw) || String(r.title || '').includes(kw)
|
||||
)
|
||||
}
|
||||
if (appliedFilter.bizType) {
|
||||
list = list.filter((r: any) => r.bizType === appliedFilter.bizType)
|
||||
}
|
||||
if (appliedFilter.amountRange?.trim()) {
|
||||
const parts = appliedFilter.amountRange.split(/[-–]/).map((x: string) => Number(String(x).trim()))
|
||||
if (parts.length >= 2 && !parts.some((n) => Number.isNaN(n))) {
|
||||
const lo = Math.min(parts[0], parts[1])
|
||||
const hi = Math.max(parts[0], parts[1])
|
||||
list = list.filter((r: any) => {
|
||||
const amt = Number(r.amount || 0)
|
||||
return amt >= lo && amt <= hi
|
||||
const mineStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
]
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
const quickSearchKeyword = ref('')
|
||||
|
||||
const filterForm = reactive({
|
||||
code: '',
|
||||
bizType: '' as string,
|
||||
status: '' as string,
|
||||
applyDateRange: null as ApplyDateRange,
|
||||
amountRange: ''
|
||||
})
|
||||
|
||||
const appliedFilter = reactive({
|
||||
code: '',
|
||||
bizType: '' as string,
|
||||
status: '' as string,
|
||||
applyDateRange: null as ApplyDateRange,
|
||||
amountRange: ''
|
||||
})
|
||||
|
||||
function syncAppliedFilter() {
|
||||
Object.assign(appliedFilter, { ...filterForm })
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
const filteredMergedRows = computed(() => filterMineRows(mergedRows.value))
|
||||
|
||||
const {
|
||||
pagedRows,
|
||||
total: filteredTotal,
|
||||
currentPage,
|
||||
pageSize,
|
||||
rangeText,
|
||||
onSizeChange
|
||||
} = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 20 })
|
||||
const currentRow = ref<any>(null)
|
||||
|
||||
const viewDetailKvFields = computed<ViewField[]>(() => {
|
||||
const r = currentRow.value
|
||||
return [
|
||||
bxViewKv('申请单号', r?.applyCode, { long: false }),
|
||||
bxViewKv('业务类型', r?.bizType, { long: false, tagType: bizTypeTagType(r?.bizType) }),
|
||||
bxViewKv('申请标题', r?.title, { long: false }),
|
||||
bxViewKvMoney('申请金额(元)', formatMoneyYuan(r?.amount || 0), { long: false }),
|
||||
bxViewKv('状态', statusLabel(r?.status), { long: false, tagType: statusTagType(r?.status) }),
|
||||
bxViewKv('当前节点', workflowNodeDisplay(r?.currentNode, r?.status), { long: false }),
|
||||
bxViewKv('申请时间', r?.applyTime, { long: false })
|
||||
]
|
||||
})
|
||||
const viewFlowNodes = ref<any[]>([])
|
||||
const viewApprovalHistory = ref<any[]>([])
|
||||
const viewFlowLoading = ref(false)
|
||||
const viewLinkedInvoiceFiles = ref<BxAttachmentItem[]>([])
|
||||
const viewAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewPayAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewOffsetAttachments = ref<BxAttachmentItem[]>([])
|
||||
|
||||
function buildApiQuery() {
|
||||
const q: Record<string, string> = {}
|
||||
const code = appliedFilter.code.trim()
|
||||
if (code) q.keyword = code
|
||||
if (appliedFilter.status) q.status = appliedFilter.status
|
||||
buildApplyDateRangeQuery(appliedFilter.applyDateRange, q)
|
||||
return q
|
||||
}
|
||||
|
||||
function mapAdvanceMineRow(r: any) {
|
||||
return {
|
||||
...r,
|
||||
bizType: '备用金',
|
||||
title: r.reason,
|
||||
applyCode: r.applyCode || r.code
|
||||
}
|
||||
}
|
||||
|
||||
function mapReimburseMineRow(r: any) {
|
||||
return {
|
||||
...r,
|
||||
bizType: '报销单',
|
||||
title: r.title,
|
||||
applyCode: r.applyCode || r.code
|
||||
}
|
||||
}
|
||||
|
||||
function sortMergedRows(rows: any[]) {
|
||||
return [...rows].sort((a, b) => String(b.applyTime || '').localeCompare(String(a.applyTime || '')))
|
||||
}
|
||||
|
||||
async function loadData(options: { buttonLoading?: boolean } = {}) {
|
||||
if (options.buttonLoading) searchLoading.value = true
|
||||
tableLoading.value = true
|
||||
try {
|
||||
const apiQuery = buildApiQuery()
|
||||
const listParams = { page: 1, pageSize: 9999, ...apiQuery }
|
||||
const [advRes, reimRes] = await Promise.allSettled([
|
||||
advanceApi.my(listParams),
|
||||
reimburseApi.my(listParams)
|
||||
])
|
||||
const advRows =
|
||||
advRes.status === 'fulfilled'
|
||||
? (advRes.value.data?.rows || []).map(mapAdvanceMineRow)
|
||||
: []
|
||||
const reimRows =
|
||||
reimRes.status === 'fulfilled'
|
||||
? (reimRes.value.data?.rows || []).map(mapReimburseMineRow)
|
||||
: []
|
||||
mergedRows.value = sortMergedRows([...advRows, ...reimRows])
|
||||
} catch {
|
||||
// error handled by interceptor
|
||||
} finally {
|
||||
if (options.buttonLoading) searchLoading.value = false
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleSearch() {
|
||||
quickSearchKeyword.value = ''
|
||||
syncAppliedFilter()
|
||||
currentPage.value = 1
|
||||
loadData({ buttonLoading: true })
|
||||
}
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({
|
||||
code: filterForm.code,
|
||||
bizType: filterForm.bizType,
|
||||
status: filterForm.status,
|
||||
applyDateRange: filterForm.applyDateRange,
|
||||
amountRange: filterForm.amountRange
|
||||
}),
|
||||
handleSearch
|
||||
)
|
||||
|
||||
function handleReset() {
|
||||
quickSearchKeyword.value = ''
|
||||
Object.assign(filterForm, { code: '', bizType: '', status: '', applyDateRange: null, amountRange: '' })
|
||||
Object.assign(appliedFilter, { code: '', bizType: '', status: '', applyDateRange: null, amountRange: '' })
|
||||
currentPage.value = 1
|
||||
loadData({ buttonLoading: true })
|
||||
}
|
||||
|
||||
function handleQuickSearch() {
|
||||
appliedFilter.code = quickSearchKeyword.value.trim()
|
||||
currentPage.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleQuickSearchClear() {
|
||||
quickSearchKeyword.value = ''
|
||||
appliedFilter.code = ''
|
||||
currentPage.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: any[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function openViewDetail(row: any) {
|
||||
currentRow.value = row
|
||||
viewFlowNodes.value = []
|
||||
viewApprovalHistory.value = []
|
||||
viewLinkedInvoiceFiles.value = []
|
||||
viewAttachments.value = []
|
||||
viewPayAttachments.value = []
|
||||
viewOffsetAttachments.value = []
|
||||
viewFlowLoading.value = true
|
||||
dialogs.viewDetail = true
|
||||
try {
|
||||
const api = row.bizType === '备用金' ? advanceApi : reimburseApi
|
||||
const tasks: Promise<any>[] = [
|
||||
api.getApprovals(row.id),
|
||||
api.getApprovalHistory(row.id)
|
||||
]
|
||||
if (row.bizType === '报销单') {
|
||||
tasks.push(
|
||||
reimburseApi
|
||||
.getById(row.id)
|
||||
.then((res: any) => {
|
||||
const data = res?.data || res || {}
|
||||
viewLinkedInvoiceFiles.value = invoicesToLinkedAttachmentFiles(data.invoices || [])
|
||||
})
|
||||
.catch(() => {
|
||||
viewLinkedInvoiceFiles.value = []
|
||||
}),
|
||||
loadBxAttachments('reimburse_apply', row.id)
|
||||
.then((files) => {
|
||||
viewAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewAttachments.value = []
|
||||
}),
|
||||
loadBxAttachments('reimburse_pay', row.id)
|
||||
.then((files) => {
|
||||
viewPayAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewPayAttachments.value = []
|
||||
}),
|
||||
loadBxAttachments('reimburse_offset', row.id)
|
||||
.then((files) => {
|
||||
viewOffsetAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewOffsetAttachments.value = []
|
||||
})
|
||||
function filterMineRows(rows: any[]) {
|
||||
let list = [...rows]
|
||||
const kw = appliedFilter.code.trim()
|
||||
if (kw) {
|
||||
list = list.filter(
|
||||
(r: any) => String(r.applyCode || '').includes(kw) || String(r.title || '').includes(kw)
|
||||
)
|
||||
}
|
||||
const [flowRes, historyRes]: any[] = await Promise.all(tasks)
|
||||
viewFlowNodes.value = (flowRes.data || []).sort((a: any, b: any) => a.nodeOrder - b.nodeOrder)
|
||||
viewApprovalHistory.value = historyRes.data || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '加载审批流程失败')
|
||||
} finally {
|
||||
viewFlowLoading.value = false
|
||||
if (appliedFilter.bizType) {
|
||||
list = list.filter((r: any) => r.bizType === appliedFilter.bizType)
|
||||
}
|
||||
if (appliedFilter.amountRange?.trim()) {
|
||||
const parts = appliedFilter.amountRange
|
||||
.split(/[-–]/)
|
||||
.map((x: string) => Number(String(x).trim()))
|
||||
if (parts.length >= 2 && !parts.some((n) => Number.isNaN(n))) {
|
||||
const lo = Math.min(parts[0], parts[1])
|
||||
const hi = Math.max(parts[0], parts[1])
|
||||
list = list.filter((r: any) => {
|
||||
const amt = Number(r.amount || 0)
|
||||
return amt >= lo && amt <= hi
|
||||
})
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
const filteredMergedRows = computed(() => filterMineRows(mergedRows.value))
|
||||
|
||||
onActivated(() => {
|
||||
loadData()
|
||||
})
|
||||
const {
|
||||
pagedRows,
|
||||
total: filteredTotal,
|
||||
currentPage,
|
||||
pageSize,
|
||||
rangeText,
|
||||
onSizeChange
|
||||
} = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 20 })
|
||||
const currentRow = ref<any>(null)
|
||||
const viewData = ref<Record<string, any>>({})
|
||||
|
||||
const viewHero = computed(() => {
|
||||
const row = currentRow.value || {}
|
||||
const detail = viewData.value || {}
|
||||
return {
|
||||
...row,
|
||||
...detail,
|
||||
bizType: row.bizType || detail.bizType,
|
||||
applyCode: detail.applyCode || row.applyCode,
|
||||
applicantName: detail.applicantName || row.applicantName,
|
||||
amount: detail.amount ?? row.amount,
|
||||
currentNode: detail.currentNode ?? row.currentNode,
|
||||
status: detail.status ?? row.status
|
||||
}
|
||||
})
|
||||
|
||||
const viewDetailKvFields = computed<ViewField[]>(() => {
|
||||
const d = viewHero.value
|
||||
if (d.bizType === '备用金') {
|
||||
return [
|
||||
bxViewKv('申请单号', d.applyCode, { long: false }),
|
||||
bxViewKv('业务类型', d.bizType, { long: false, tagType: bizTypeTagType(d.bizType) }),
|
||||
bxViewKv('申请人', d.applicantName, { long: false }),
|
||||
bxViewKvMoney('申请金额(元)', formatMoneyYuan(d.amount || 0), { long: false }),
|
||||
bxViewKvMoney(
|
||||
'实际发放金额(元)',
|
||||
d.actualAmount != null ? formatMoneyYuan(d.actualAmount) : '—',
|
||||
{ long: false }
|
||||
),
|
||||
bxViewKvMoney('冲抵总金额(元)', formatMoneyYuan(d.offsetAmount || 0), { long: false }),
|
||||
bxViewKv('状态', statusLabel(d.status), { long: false, tagType: statusTagType(d.status) }),
|
||||
bxViewKv('申请时间', d.applyTime, { long: false }),
|
||||
bxViewKv('申请原因', d.reason || d.title, { long: false })
|
||||
]
|
||||
}
|
||||
return [
|
||||
bxViewKv('申请单号', d.applyCode, { long: false }),
|
||||
bxViewKv('业务类型', d.bizType, { long: false, tagType: bizTypeTagType(d.bizType) }),
|
||||
bxViewKv('报销日期', d.reimburseDate, { long: false }),
|
||||
bxViewKv('报销标题', d.title, { long: false }),
|
||||
bxViewKv('申请人', d.applicantName, { long: false }),
|
||||
bxViewKvMoney('申请金额(元)', formatMoneyYuan(d.amount || 0), { long: false }),
|
||||
bxViewKv('状态', statusLabel(d.status), { long: false, tagType: statusTagType(d.status) }),
|
||||
bxViewKv('当前节点', workflowNodeDisplay(d.currentNode, d.status), { long: false }),
|
||||
bxViewKv('申请时间', d.applyTime, { long: false }),
|
||||
bxViewKvLong('报销说明', d.remark, { fullSpan: true })
|
||||
]
|
||||
})
|
||||
const viewFlowNodes = ref<any[]>([])
|
||||
const viewApprovalHistory = ref<any[]>([])
|
||||
const viewFlowLoading = ref(false)
|
||||
const viewLinkedInvoiceFiles = ref<BxAttachmentItem[]>([])
|
||||
const viewAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewPayAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewOffsetAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewIssueAttachments = ref<BxAttachmentItem[]>([])
|
||||
const viewOffsetRecords = ref<any[]>([])
|
||||
|
||||
function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
const sums: string[] = []
|
||||
columns.forEach((_: any, index: number) => {
|
||||
if (index === 0) {
|
||||
sums[index] = '合计'
|
||||
return
|
||||
}
|
||||
if (index === 2) {
|
||||
const total = data.reduce((sum: number, row: any) => sum + Number(row.amount || 0), 0)
|
||||
sums[index] = formatMoneyYuan(total)
|
||||
return
|
||||
}
|
||||
sums[index] = ''
|
||||
})
|
||||
return sums
|
||||
}
|
||||
|
||||
function buildApiQuery() {
|
||||
const q: Record<string, string> = {}
|
||||
const code = appliedFilter.code.trim()
|
||||
if (code) q.keyword = code
|
||||
if (appliedFilter.status) q.status = appliedFilter.status
|
||||
buildApplyDateRangeQuery(appliedFilter.applyDateRange, q)
|
||||
return q
|
||||
}
|
||||
|
||||
function mapAdvanceMineRow(r: any) {
|
||||
return {
|
||||
...r,
|
||||
bizType: '备用金',
|
||||
title: r.reason,
|
||||
applyCode: r.applyCode || r.code
|
||||
}
|
||||
}
|
||||
|
||||
function mapReimburseMineRow(r: any) {
|
||||
return {
|
||||
...r,
|
||||
bizType: '报销单',
|
||||
title: r.title,
|
||||
applyCode: r.applyCode || r.code
|
||||
}
|
||||
}
|
||||
|
||||
function sortMergedRows(rows: any[]) {
|
||||
return [...rows].sort((a, b) =>
|
||||
String(b.applyTime || '').localeCompare(String(a.applyTime || ''))
|
||||
)
|
||||
}
|
||||
|
||||
async function loadData(options: { buttonLoading?: boolean } = {}) {
|
||||
if (options.buttonLoading) searchLoading.value = true
|
||||
tableLoading.value = true
|
||||
try {
|
||||
const apiQuery = buildApiQuery()
|
||||
const listParams = { page: 1, pageSize: 9999, ...apiQuery }
|
||||
const [advRes, reimRes] = await Promise.allSettled([
|
||||
advanceApi.my(listParams),
|
||||
reimburseApi.my(listParams)
|
||||
])
|
||||
const advRows =
|
||||
advRes.status === 'fulfilled' ? (advRes.value.data?.rows || []).map(mapAdvanceMineRow) : []
|
||||
const reimRows =
|
||||
reimRes.status === 'fulfilled'
|
||||
? (reimRes.value.data?.rows || []).map(mapReimburseMineRow)
|
||||
: []
|
||||
mergedRows.value = sortMergedRows([...advRows, ...reimRows])
|
||||
} catch {
|
||||
// error handled by interceptor
|
||||
} finally {
|
||||
if (options.buttonLoading) searchLoading.value = false
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
quickSearchKeyword.value = ''
|
||||
syncAppliedFilter()
|
||||
currentPage.value = 1
|
||||
loadData({ buttonLoading: true })
|
||||
}
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({
|
||||
code: filterForm.code,
|
||||
bizType: filterForm.bizType,
|
||||
status: filterForm.status,
|
||||
applyDateRange: filterForm.applyDateRange,
|
||||
amountRange: filterForm.amountRange
|
||||
}),
|
||||
handleSearch
|
||||
)
|
||||
|
||||
function handleReset() {
|
||||
quickSearchKeyword.value = ''
|
||||
Object.assign(filterForm, {
|
||||
code: '',
|
||||
bizType: '',
|
||||
status: '',
|
||||
applyDateRange: null,
|
||||
amountRange: ''
|
||||
})
|
||||
Object.assign(appliedFilter, {
|
||||
code: '',
|
||||
bizType: '',
|
||||
status: '',
|
||||
applyDateRange: null,
|
||||
amountRange: ''
|
||||
})
|
||||
currentPage.value = 1
|
||||
loadData({ buttonLoading: true })
|
||||
}
|
||||
|
||||
function handleQuickSearch() {
|
||||
appliedFilter.code = quickSearchKeyword.value.trim()
|
||||
currentPage.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleQuickSearchClear() {
|
||||
quickSearchKeyword.value = ''
|
||||
appliedFilter.code = ''
|
||||
currentPage.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: any[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function openViewDetail(row: any) {
|
||||
currentRow.value = row
|
||||
viewData.value = {}
|
||||
viewFlowNodes.value = []
|
||||
viewApprovalHistory.value = []
|
||||
viewLinkedInvoiceFiles.value = []
|
||||
viewAttachments.value = []
|
||||
viewPayAttachments.value = []
|
||||
viewOffsetAttachments.value = []
|
||||
viewIssueAttachments.value = []
|
||||
viewOffsetRecords.value = []
|
||||
viewFlowLoading.value = true
|
||||
dialogs.viewDetail = true
|
||||
const isAdvance = row.bizType === '备用金'
|
||||
const api = isAdvance ? advanceApi : reimburseApi
|
||||
try {
|
||||
const detailRes: any = await api.getById(row.id)
|
||||
viewData.value = detailRes?.data || detailRes || {}
|
||||
|
||||
const tasks: Promise<any>[] = [api.getApprovals(row.id), api.getApprovalHistory(row.id)]
|
||||
if (isAdvance) {
|
||||
tasks.push(
|
||||
paymentApi
|
||||
.advanceOffsetRecords(row.id)
|
||||
.then((res: any) => {
|
||||
viewOffsetRecords.value = res?.data || []
|
||||
})
|
||||
.catch(() => {
|
||||
viewOffsetRecords.value = []
|
||||
}),
|
||||
loadBxAttachments('advance_issue', row.id)
|
||||
.then((files) => {
|
||||
viewIssueAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewIssueAttachments.value = []
|
||||
})
|
||||
)
|
||||
} else {
|
||||
const linked = invoicesToLinkedAttachmentFiles(viewData.value.invoices || [])
|
||||
viewLinkedInvoiceFiles.value = linked
|
||||
tasks.push(
|
||||
fillBxAttachmentPreviews(linked).catch(() => undefined),
|
||||
loadBxAttachments('reimburse_apply', row.id)
|
||||
.then((files) => {
|
||||
viewAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewAttachments.value = []
|
||||
}),
|
||||
loadBxAttachments('reimburse_pay', row.id)
|
||||
.then((files) => {
|
||||
viewPayAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewPayAttachments.value = []
|
||||
}),
|
||||
loadBxAttachments('reimburse_offset', row.id)
|
||||
.then((files) => {
|
||||
viewOffsetAttachments.value = files
|
||||
})
|
||||
.catch(() => {
|
||||
viewOffsetAttachments.value = []
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const [flowRes, historyRes]: any[] = await Promise.all(tasks)
|
||||
viewFlowNodes.value = (flowRes.data || []).sort((a: any, b: any) => a.nodeOrder - b.nodeOrder)
|
||||
viewApprovalHistory.value = historyRes.data || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '加载详情失败')
|
||||
} finally {
|
||||
viewFlowLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1843,6 +1843,8 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
const subtotal = parseFloat(String(product.subtotal || 0));
|
||||
const taxAmount = parseFloat(String(product.taxAmount || 0));
|
||||
const beforeTax = subtotal - taxAmount;
|
||||
const taxRateNum = Number(product.taxRate);
|
||||
const taxRateText = Number.isFinite(taxRateNum) ? `${taxRateNum}%` : '0%';
|
||||
|
||||
const pricingCells = isFrameworkContract
|
||||
? '<td colspan="4" style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">随行就市</td>'
|
||||
@@ -1859,13 +1861,14 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${getUnitLabel(product.unit)}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
|
||||
${pricingCells}
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${product.packageSpec || ''}</td>
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${product.remark || ''}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join('') ||
|
||||
'<tr><td colspan="11" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">暂无产品信息</td></tr>';
|
||||
'<tr><td colspan="12" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">暂无产品信息</td></tr>';
|
||||
|
||||
// 定义替换映射表
|
||||
const getSettlementClauseContent = (): string =>
|
||||
@@ -1931,9 +1934,17 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
<th style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 8%;">品牌</th>`
|
||||
)
|
||||
}
|
||||
// 旧模板表头无「税率」时,在「用量」后插入
|
||||
if (!/用量(?:(吨))?<\/th>[\s\S]{0,200}?税率<\/th>/i.test(result)) {
|
||||
result = result.replace(
|
||||
/(<th[^>]*>\s*用量(?:(吨))?\s*<\/th>)/i,
|
||||
`$1
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 5%;">税率</th>`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// 合计区(11 列):第 1 行左右对半运费/桶押金;第 2 行合计;第 3 行提示
|
||||
// 合计区(12 列):第 1 行左右对半运费/桶押金;第 2 行合计;第 3 行提示
|
||||
const freightRaw = Number(contractData.freight);
|
||||
const freightAmount = Number.isFinite(freightRaw) && freightRaw > 0 ? freightRaw : 0;
|
||||
const depositRaw = Number(contractData.barrelDeposit);
|
||||
@@ -1953,14 +1964,14 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
<td colspan="2" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">运费(元)</td>
|
||||
<td colspan="4" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${freightText}</td>
|
||||
<td colspan="2" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">桶押金(元)</td>
|
||||
<td colspan="3" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${barrelDepositText}</td>
|
||||
<td colspan="4" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${barrelDepositText}</td>
|
||||
</tr>
|
||||
<tr style="background-color: #fff; font-weight: bold;">
|
||||
<td colspan="2" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">合计(人民币)</td>
|
||||
<td colspan="9" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${totalWithExtrasText}</td>
|
||||
<td colspan="10" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${totalWithExtrasText}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="11" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 11px; background-color: #fff;">
|
||||
<td colspan="12" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 11px; background-color: #fff;">
|
||||
${tipRowText}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -2234,6 +2245,8 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
|
||||
const subtotal = parseFloat(String(product.subtotal || 0));
|
||||
const taxAmount = parseFloat(String(product.taxAmount || 0));
|
||||
const beforeTax = subtotal - taxAmount;
|
||||
const taxRateNum = Number(product.taxRate);
|
||||
const taxRateText = Number.isFinite(taxRateNum) ? `${taxRateNum}%` : '0%';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
@@ -2242,6 +2255,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${getUnitLabel(product.unit)}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(product.price)}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(beforeTax)}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(taxAmount)}</td>
|
||||
@@ -2251,7 +2265,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
|
||||
</tr>`;
|
||||
})
|
||||
.join('') ||
|
||||
'<tr><td colspan="11" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">暂无产品信息</td></tr>';
|
||||
'<tr><td colspan="12" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">暂无产品信息</td></tr>';
|
||||
|
||||
return `
|
||||
<div style="width: 210mm; margin: 0 auto; padding: 0 15mm 20mm 15mm; font-family: '宋体', SimSun, serif; line-height: 1.6; box-sizing: border-box;">
|
||||
@@ -2295,13 +2309,14 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 6%;">规格</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 6%;">品牌</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 5%;">单位</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 10%;">用量</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 7%;">用量</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 5%;">税率</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 12%;">含税单价(元)</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 12%;">未含税金额(元)</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 10%;">税费(元)</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 12%;">含税金额(元)</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 7%;">包装规格</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 10%;">备注</th>
|
||||
<th style="padding: 5px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; width: 8%;">备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -2453,9 +2468,9 @@ const handleSaveTemplate = async () => {
|
||||
);
|
||||
const totalRowHtml = totalRowMatch ? totalRowMatch[0] : '';
|
||||
|
||||
// 3. 提取提示行(兼容一票制 / 客户自提文案,以及 colspan 10/11)
|
||||
// 3. 提取提示行(兼容一票制 / 客户自提文案,以及 colspan 10/11/12)
|
||||
const tipRowMatch = templateWithPlaceholders.match(
|
||||
/<tr>[\s\S]*?<td colspan="1[01]"[\s\S]*?结算以实际到货吨位为准[\s\S]*?<\/td>[\s\S]*?<\/tr>/
|
||||
/<tr>[\s\S]*?<td colspan="1[012]"[\s\S]*?结算以实际到货吨位为准[\s\S]*?<\/td>[\s\S]*?<\/tr>/
|
||||
);
|
||||
const tipRowHtml = tipRowMatch ? tipRowMatch[0] : '';
|
||||
|
||||
@@ -2480,7 +2495,7 @@ const handleSaveTemplate = async () => {
|
||||
// 替换提示行(兼容旧一票制与客户自提提示)
|
||||
if (tipRowHtml) {
|
||||
const tipRowPattern =
|
||||
/<tr>[\s\S]*?<td colspan="1[01]"[\s\S]*?>含税、(?:含运费|客户自提)[\s\S]*?<\/tr>/;
|
||||
/<tr>[\s\S]*?<td colspan="1[012]"[\s\S]*?>含税、(?:含运费|客户自提)[\s\S]*?<\/tr>/;
|
||||
if (tipRowPattern.test(finalTemplate)) {
|
||||
finalTemplate = finalTemplate.replace(tipRowPattern, tipRowHtml);
|
||||
}
|
||||
|
||||
@@ -1587,6 +1587,8 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
const subtotal = parseFloat(String(product.subtotal || 0));
|
||||
const taxAmount = parseFloat(String(product.taxAmount || 0));
|
||||
const beforeTax = subtotal - taxAmount;
|
||||
const taxRateNum = Number(product.taxRate);
|
||||
const taxRateText = Number.isFinite(taxRateNum) ? `${taxRateNum}%` : '0%';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
@@ -1595,6 +1597,7 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
|
||||
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${getUnitLabel(product.unit)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(product.price)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(beforeTax)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(taxAmount)}</td>
|
||||
@@ -1604,7 +1607,7 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
</tr>`;
|
||||
})
|
||||
.join('') ||
|
||||
'<tr><td colspan="11" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">暂无产品信息</td></tr>';
|
||||
'<tr><td colspan="12" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">暂无产品信息</td></tr>';
|
||||
|
||||
// 定义替换映射表
|
||||
const replacements: Record<string, string> = {
|
||||
@@ -1664,8 +1667,16 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 8%;">品牌</th>`
|
||||
)
|
||||
}
|
||||
// 旧模板表头无「税率」时,在「用量」后插入
|
||||
if (!/用量(?:(吨))?<\/th>[\s\S]{0,200}?税率<\/th>/i.test(result)) {
|
||||
result = result.replace(
|
||||
/(<th[^>]*>\s*用量(?:(吨))?\s*<\/th>)/i,
|
||||
`$1
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 5%;">税率</th>`
|
||||
)
|
||||
}
|
||||
|
||||
// 合计区(11 列):第 1 行左右对半运费/桶押金;第 2 行合计;第 3 行提示
|
||||
// 合计区(12 列):第 1 行左右对半运费/桶押金;第 2 行合计;第 3 行提示
|
||||
const freightRaw = Number(contractData.freight);
|
||||
const freightAmount = Number.isFinite(freightRaw) && freightRaw > 0 ? freightRaw : 0;
|
||||
const depositRaw = Number(contractData.barrelDeposit);
|
||||
@@ -1686,14 +1697,14 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">运费(元)</td>
|
||||
<td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${freightText}</td>
|
||||
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">桶押金(元)</td>
|
||||
<td colspan="3" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${barrelDepositText}</td>
|
||||
<td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${barrelDepositText}</td>
|
||||
</tr>
|
||||
<tr style="background-color: #fff; font-weight: bold;">
|
||||
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">合计(人民币)</td>
|
||||
<td colspan="9" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${totalWithExtrasText}</td>
|
||||
<td colspan="10" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${totalWithExtrasText}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="11" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 11px; background-color: #fff;">
|
||||
<td colspan="12" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 11px; background-color: #fff;">
|
||||
${tipRowText}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1929,6 +1940,8 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
|
||||
const subtotal = parseFloat(String(product.subtotal || 0));
|
||||
const taxAmount = parseFloat(String(product.taxAmount || 0));
|
||||
const beforeTax = subtotal - taxAmount;
|
||||
const taxRateNum = Number(product.taxRate);
|
||||
const taxRateText = Number.isFinite(taxRateNum) ? `${taxRateNum}%` : '0%';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
@@ -1936,6 +1949,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
|
||||
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.model || product.productModel || ''}</td>
|
||||
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(product.price)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(beforeTax)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(taxAmount)}</td>
|
||||
@@ -1945,7 +1959,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
|
||||
</tr>`;
|
||||
})
|
||||
.join('') ||
|
||||
'<tr><td colspan="10" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">暂无产品信息</td></tr>';
|
||||
'<tr><td colspan="11" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">暂无产品信息</td></tr>';
|
||||
|
||||
return `
|
||||
<div style="width: 210mm; margin: 0 auto; padding: 0 15mm 20mm 15mm; font-family: '宋体', SimSun, serif; line-height: 1.6; box-sizing: border-box;">
|
||||
@@ -1986,33 +2000,34 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
|
||||
<thead>
|
||||
<tr style="background-color: #fff;">
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 11%;">产品名称</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 9%;">规格</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 8%;">品牌</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 8%;">用量(吨)</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 8%;">规格</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 7%;">品牌</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 7%;">用量(吨)</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 5%;">税率</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 10%;">含税单价(元/吨)</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 10%;">未含税金额(元)</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 8%;">税费(元)</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 11%;">含税金额(元)</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 7%;">包装规格</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 15%;">备注</th>
|
||||
<th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 13%;">备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${productRows}
|
||||
<!-- 运费/桶押金一行(10 列对半),合计单独一行 -->
|
||||
<!-- 运费/桶押金一行(11 列对半),合计单独一行 -->
|
||||
<tr style="background-color: #fff; font-weight: bold;">
|
||||
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">运费(元)</td>
|
||||
<td colspan="3" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${freightText}</td>
|
||||
<td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${freightText}</td>
|
||||
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">桶押金(元)</td>
|
||||
<td colspan="3" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${barrelDepositText}</td>
|
||||
</tr>
|
||||
<tr style="background-color: #fff; font-weight: bold;">
|
||||
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">合计(人民币)</td>
|
||||
<td colspan="8" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${totalWithExtrasText}</td>
|
||||
<td colspan="9" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${totalWithExtrasText}</td>
|
||||
</tr>
|
||||
<!-- 提示行 -->
|
||||
<tr>
|
||||
<td colspan="10" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 11px; background-color: #fff;">
|
||||
<td colspan="11" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 11px; background-color: #fff;">
|
||||
${tipRowText}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -11,9 +11,9 @@ export const SETTLEMENT_CLAUSE_LABELS: Record<string, string> = {
|
||||
}
|
||||
|
||||
const ADVANCE_CLAUSE =
|
||||
'款到发货。需方付款后安排发货,需方收到货物后,供方开具13%增值税专用发票。'
|
||||
'款到发货。需方付款后安排发货,需方收到货物后,供方开具增值税专用发票。'
|
||||
const ARRIVE_CLAUSE =
|
||||
'货到付款,需方收到供方货物后,供方开具13%增值税专用发票,无异议后,以现汇方式月结。'
|
||||
'货到付款,需方收到供方货物后,供方开具增值税专用发票,无异议后,以现汇方式月结。'
|
||||
|
||||
export function defaultSettlementClause(
|
||||
settlementMethod?: string | null,
|
||||
@@ -24,7 +24,7 @@ export function defaultSettlementClause(
|
||||
if (method in SETTLEMENT_CLAUSE_LABELS) {
|
||||
const label =
|
||||
(labelResolver && labelResolver(method)) || SETTLEMENT_CLAUSE_LABELS[method]
|
||||
return `请根据双方约定填写结算方式:${label}。供方开具13%增值税专用发票,无异议后,以现汇方式月结。`
|
||||
return `请根据双方约定的结算方式:${label}。供方开具增值税专用发票,无异议后,以现汇方式月结。`
|
||||
}
|
||||
return ARRIVE_CLAUSE
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user