|
|
|
@@ -1,5 +1,5 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
import { ArrowLeft, Ban, Download, Eye, FileCheck2, FilePlus2, Pencil, Send } from '@lucide/vue'
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
@@ -53,6 +53,7 @@ const router = useRouter()
|
|
|
|
|
const customers = ref<PlatformTenantResource[]>([])
|
|
|
|
|
const customersLoading = ref(false)
|
|
|
|
|
const customersError = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
const customerSelectOpen = ref(false)
|
|
|
|
|
const filters = ref<QueryModel>({ customerId: '', direction: '', status: '', reviewStatus: '', statementGenerated: '', businessRange: [], keyword: '' })
|
|
|
|
|
const allRows = ref<ArapRow[]>([])
|
|
|
|
|
const loading = ref(false)
|
|
|
|
@@ -92,14 +93,40 @@ function isFirstlegSource(source: { sourceSystem?: unknown; sourceType?: unknown
|
|
|
|
|
}
|
|
|
|
|
const stateError = computed(() => customersError.value || loadError.value)
|
|
|
|
|
const activeCustomers = computed(() => customers.value.filter((item) => item.status === 'ACTIVE'))
|
|
|
|
|
function onCustomerSelectVisible(visible: boolean) {
|
|
|
|
|
customerSelectOpen.value = visible
|
|
|
|
|
}
|
|
|
|
|
function closeCustomerSelectOnScroll() {
|
|
|
|
|
if (!customerSelectOpen.value || typeof document === 'undefined') return
|
|
|
|
|
const input = document.querySelector('.finance-customer-select input') as HTMLInputElement | null
|
|
|
|
|
input?.blur()
|
|
|
|
|
customerSelectOpen.value = false
|
|
|
|
|
}
|
|
|
|
|
const queryFields = computed<QueryField[]>(() => [
|
|
|
|
|
{ key: 'customerId', type: 'select', label: '客户名称', width: 260, placeholder: '全部客户', props: { 'aria-label': '客户名称', filterable: true, clearable: true, loading: customersLoading.value }, options: activeCustomers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })) },
|
|
|
|
|
{ key: 'direction', type: 'select', label: '收付方向', width: 140, placeholder: '全部', options: [{ label: '应收', value: 'RECEIVABLE' }, { label: '应付', value: 'PAYABLE' }] },
|
|
|
|
|
{ key: 'status', type: 'select', label: '来源状态', width: 150, placeholder: '全部状态', options: [{ label: '可结算', value: 'AVAILABLE' }, { label: '已占用', value: 'LOCKED' }, { label: '已生成对账单', value: 'BILLED' }, { label: '已作废', value: 'VOIDED' }, { label: '已被调整替代', value: 'SUPERSEDED' }] },
|
|
|
|
|
{ key: 'reviewStatus', type: 'select', label: '审核状态', width: 150, placeholder: '全部状态', options: financeArapStatusOptions },
|
|
|
|
|
{ key: 'statementGenerated', type: 'select', label: '已生成对账单', width: 160, placeholder: '全部', options: [{ label: '已生成', value: 'true' }, { label: '未生成', value: 'false' }] },
|
|
|
|
|
{
|
|
|
|
|
key: 'customerId',
|
|
|
|
|
type: 'select',
|
|
|
|
|
label: '客户名称',
|
|
|
|
|
width: 260,
|
|
|
|
|
placeholder: '请选择客户',
|
|
|
|
|
props: {
|
|
|
|
|
'aria-label': '客户名称',
|
|
|
|
|
class: 'finance-customer-select',
|
|
|
|
|
popperClass: 'finance-customer-arap-popper',
|
|
|
|
|
teleported: false,
|
|
|
|
|
onVisibleChange: onCustomerSelectVisible,
|
|
|
|
|
filterable: true,
|
|
|
|
|
clearable: true,
|
|
|
|
|
loading: customersLoading.value,
|
|
|
|
|
},
|
|
|
|
|
options: activeCustomers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })),
|
|
|
|
|
},
|
|
|
|
|
{ key: 'direction', type: 'select', label: '收付方向', width: 140, placeholder: '请选择收付方向', options: [{ label: '应收', value: 'RECEIVABLE' }, { label: '应付', value: 'PAYABLE' }] },
|
|
|
|
|
{ key: 'status', type: 'select', label: '来源状态', width: 150, placeholder: '请选择来源状态', options: [{ label: '可结算', value: 'AVAILABLE' }, { label: '已占用', value: 'LOCKED' }, { label: '已生成对账单', value: 'BILLED' }, { label: '已作废', value: 'VOIDED' }, { label: '已被调整替代', value: 'SUPERSEDED' }] },
|
|
|
|
|
{ key: 'reviewStatus', type: 'select', label: '审核状态', width: 150, placeholder: '请选择审核状态', options: financeArapStatusOptions },
|
|
|
|
|
{ key: 'statementGenerated', type: 'select', label: '已生成对账单', width: 160, placeholder: '请选择对账单状态', options: [{ label: '已生成', value: 'true' }, { label: '未生成', value: 'false' }] },
|
|
|
|
|
{ key: 'businessRange', type: 'daterange', label: '业务日期', width: 280 },
|
|
|
|
|
{ key: 'keyword', type: 'input', label: '关键词', width: 240, placeholder: '业务单号/费用/客户/供应商', props: { 'aria-label': '关键词' } },
|
|
|
|
|
{ key: 'keyword', type: 'input', label: '关键词', width: 240, placeholder: '请输入业务单号/费用/客户/供应商', props: { 'aria-label': '关键词' } },
|
|
|
|
|
])
|
|
|
|
|
const columns: DataTableColumn[] = [
|
|
|
|
|
{ key: 'actions', label: '操作', width: 260, fixed: 'left', slotName: 'actions' }, { key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: isSelectableReceivable }, { key: 'direction', label: '收付方向', width: 100, slotName: 'direction' },
|
|
|
|
@@ -315,11 +342,15 @@ async function restoreFeeSourceDetail(feeSourceId: string) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
document.addEventListener('scroll', closeCustomerSelectOnScroll, true)
|
|
|
|
|
const feeSourceId = String(route.params.feeSourceId || '')
|
|
|
|
|
if (route.name === 'finance-customer-arap-detail' && feeSourceId) { if (canListCustomers.value) await loadCustomers(); await restoreFeeSourceDetail(feeSourceId); return }
|
|
|
|
|
if (canListCustomers.value) await loadCustomers()
|
|
|
|
|
await load()
|
|
|
|
|
})
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
document.removeEventListener('scroll', closeCustomerSelectOnScroll, true)
|
|
|
|
|
})
|
|
|
|
|
watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSourceId], previous) => {
|
|
|
|
|
if (!previous) return
|
|
|
|
|
if (name === 'finance-customer-arap-detail' && feeSourceId) void restoreFeeSourceDetail(String(feeSourceId))
|
|
|
|
@@ -335,7 +366,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
|
|
|
|
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="客户应收应付筛选" :fields="queryFields" preference-key="finance-customer-arap-query" @search="search" /></template>
|
|
|
|
|
<template v-if="stateError" #state><LoadErrorState title="客户应收应付加载失败" :message="stateError.message" :loading="customersLoading || loading" @retry="customersError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="stateError" /></template></LoadErrorState></template>
|
|
|
|
|
<section v-if="!customersError && !loadError" class="surface-panel table-panel">
|
|
|
|
|
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="finance-customer-arap-table" aria-label="客户应收应付列表" row-key="id" empty-text="暂无客户应收应付数据" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
|
|
|
|
|
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" max-height="calc(100vh - 300px)" preference-key="finance-customer-arap-table" aria-label="客户应收应付列表" row-key="id" empty-text="暂无客户应收应付数据" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
|
|
|
|
|
<template #toolbar-actions><div class="generation-actions"><el-button v-if="canExport" :loading="exportBusy" :disabled="loading || exportBusy" @click="exportFeeSources"><Download :size="16" />导出Excel</el-button><el-button v-if="canCreateStatement" type="primary" :disabled="!selectedRows.length" @click="continueSelectedGeneration"><FilePlus2 :size="16" />生成所选对账单</el-button></div></template>
|
|
|
|
|
<template #actions="{ row }"><div class="row-actions"><template v-if="row.feeSourceId"><el-button v-if="canViewSource" size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />详情</el-button><el-button v-if="canEditSource && !isFirstlegSource(row) && ['DRAFT','REJECTED'].includes(row.reviewStatus) && row.status === 'AVAILABLE'" size="small" plain @click="openMutation(row, 'EDIT')"><Pencil :size="15" />编辑</el-button><el-button v-if="canEditSource && ['DRAFT','REJECTED'].includes(row.reviewStatus) && row.status === 'AVAILABLE'" size="small" plain type="primary" @click="openMutation(row, 'SUBMIT')"><Send :size="15" />提交审核</el-button><el-button v-if="canReviewSource && row.reviewStatus === 'PENDING' && row.status === 'AVAILABLE'" size="small" plain type="success" @click="openReview(row)"><FileCheck2 :size="15" />审核</el-button><el-button v-if="canEditSource && !isFirstlegSource(row) && row.reviewStatus === 'APPROVED' && row.status === 'AVAILABLE' && !row.statementGenerated" size="small" plain @click="openMutation(row, 'ADJUST')"><Pencil :size="15" />调整</el-button><el-button v-if="canEditSource && row.status === 'AVAILABLE' && !row.statementGenerated" size="small" plain type="danger" @click="openMutation(row, 'VOID')"><Ban :size="15" />作废</el-button></template><span v-else class="muted-action">手工成本</span></div></template>
|
|
|
|
|
<template #customerName="{ row }">{{ customerName(row.customerId, row.customerName) }}</template><template #direction="{ row }"><el-tag :type="row.direction === 'RECEIVABLE' ? 'success' : row.direction === 'PAYABLE' ? 'warning' : 'info'">{{ financeDirectionLabel(row.direction) }}</el-tag></template>
|
|
|
|
@@ -369,6 +400,25 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Customer names can contain an order suffix; keep the selector readable without changing the query value. */
|
|
|
|
|
:deep(.finance-customer-select .el-select__selected-item) {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.finance-customer-arap-popper) {
|
|
|
|
|
width: max(260px, min(360px, calc(100vw - 32px))) !important;
|
|
|
|
|
max-width: calc(100vw - 32px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.finance-customer-arap-popper .el-select-dropdown__item) {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.finance-arap-detail {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-width: 0;
|
|
|
|
|