This commit is contained in:
zlwu committed 2026-08-17 15:25:28 +08:00
1 parent 6b2147aa46
commit 8558d9439e
31 files changed
+596719 -4174

No files matched your search

+100 -34
View File
@@ -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) {
+543 -376
View File
@@ -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>
+531 -358
View File
@@ -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);
}
+29 -14
View File
@@ -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
}