优化
This commit is contained in:
@@ -3,7 +3,6 @@ import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Eye, Pencil, Save } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
@@ -13,128 +12,120 @@ import FormSection from '@/shared/components/FormSection.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { getFinanceCustomerAccount, getFinanceCustomerTerms, updateFinanceCustomerTerms } from '../api/finance'
|
||||
import type { FinanceAccountEntry, FinanceCustomerTerms } from '../types'
|
||||
|
||||
interface AccountRow {
|
||||
id: string
|
||||
customerName: string
|
||||
customerCode: string
|
||||
creditDays: number
|
||||
totalReceivable: number
|
||||
totalReceived: number
|
||||
outstandingAmount: number
|
||||
entryCount: number
|
||||
entries: FinanceAccountEntry[]
|
||||
status: string
|
||||
}
|
||||
import { getFinanceCustomerTerms, listFinanceAccountLedger, listFinanceAccounts, updateFinanceCustomerTerms } from '../api/finance'
|
||||
import type { FinanceAccountSummary, FinanceCustomerTerms, FinanceLedgerEntry } from '../types'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const customers = ref<PlatformTenantResource[]>([])
|
||||
const customersLoading = ref(false)
|
||||
const customersError = ref<ApiErrorDetails | null>(null)
|
||||
const filters = reactive<QueryModel>({ customerId: '' })
|
||||
const rows = ref<AccountRow[]>([])
|
||||
const filters = reactive<QueryModel>({ keyword: '', status: '' })
|
||||
const rows = ref<FinanceAccountSummary[]>([])
|
||||
const loading = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const drawerOpen = ref(false)
|
||||
const editing = ref(false)
|
||||
const saving = ref(false)
|
||||
const selected = ref<AccountRow | null>(null)
|
||||
const selected = ref<FinanceAccountSummary | null>(null)
|
||||
const ledger = ref<FinanceLedgerEntry[]>([])
|
||||
const ledgerLoading = ref(false)
|
||||
const ledgerError = ref<ApiErrorDetails | null>(null)
|
||||
const selectedTerms = ref<FinanceCustomerTerms | null>(null)
|
||||
const creditDays = ref(30)
|
||||
|
||||
const canView = computed(() => access.can('platform:finance:bill:view') && access.can('platform:finance:customer-terms:view'))
|
||||
const canUpdate = computed(() => access.can('platform:finance:customer-terms:update'))
|
||||
const stateError = computed(() => customersError.value || loadError.value)
|
||||
const queryFields = computed<QueryField[]>(() => [{ key: 'customerId', type: 'select', label: '客户', width: 280, placeholder: '请选择客户', props: { 'aria-label': '客户', filterable: true, loading: customersLoading.value }, options: customers.value.map((item) => ({ label: `${item.tenantName} (${item.tenantCode})`, value: String(item.id), disabled: item.status !== 'ACTIVE' })) }])
|
||||
const canView = computed(() => access.can('platform:finance:account:view'))
|
||||
const canViewTerms = computed(() => access.can('platform:finance:customer-terms:view'))
|
||||
const canUpdateTerms = computed(() => canViewTerms.value && access.can('platform:finance:customer-terms:update'))
|
||||
const queryFields: QueryField[] = [
|
||||
{ key: 'keyword', type: 'input', label: '关键词', width: 260, placeholder: '客户编号/客户名称/联系人/手机号', props: { 'aria-label': '关键词' } },
|
||||
{ key: 'status', type: 'select', label: '状态', width: 150, placeholder: '全部状态', options: [{ label: '正常', value: 'ACTIVE' }, { label: '停用', value: 'DISABLED' }] },
|
||||
]
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 150, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'customerName', label: '客户', minWidth: 180 }, { key: 'customerCode', label: '客户编号', width: 150 },
|
||||
{ key: 'creditDays', label: '账期(天)', width: 100 }, { key: 'totalReceivable', label: '累计应收(CNY)', width: 140 },
|
||||
{ key: 'totalReceived', label: '累计已收(CNY)', width: 140 }, { key: 'outstandingAmount', label: '未收(CNY)', width: 120 },
|
||||
{ key: 'entryCount', label: '账户流水数', width: 120 },
|
||||
{ key: 'customerName', label: '客户', minWidth: 180 },
|
||||
{ key: 'customerCode', label: '客户编号', width: 150 },
|
||||
{ key: 'contactName', label: '联系人', width: 120 },
|
||||
{ key: 'contactPhone', label: '联系电话', width: 140 },
|
||||
{ key: 'outstandingReceivable', label: '待收(CNY)', width: 130 },
|
||||
{ key: 'pendingBilling', label: '待开账(CNY)', width: 140 },
|
||||
{ key: 'receivedInPeriod', label: '本期已收(CNY)', width: 140 },
|
||||
{ key: 'outstandingPayable', label: '待付(CNY)', width: 130 },
|
||||
{ key: 'status', label: '客户状态', width: 110, slotName: 'status' },
|
||||
{ key: 'lastChangedAt', label: '最近变动', width: 170 },
|
||||
]
|
||||
|
||||
async function loadCustomers() {
|
||||
customersLoading.value = true; customersError.value = null
|
||||
try {
|
||||
const records: PlatformTenantResource[] = []; let currentPage = 1; let pages = 1
|
||||
do { const result = await listPlatformTenants({ page: currentPage, size: 200 }); records.push(...result.records); pages = Math.max(1, result.pages); currentPage += 1 } while (currentPage <= pages)
|
||||
customers.value = records
|
||||
} catch (error) { customersError.value = getApiErrorDetails(error, '客户列表加载失败') }
|
||||
finally { customersLoading.value = false }
|
||||
}
|
||||
|
||||
async function load(model: QueryModel = filters) {
|
||||
Object.assign(filters, model)
|
||||
const customerId = String(filters.customerId || '')
|
||||
rows.value = []; loadError.value = null
|
||||
if (!customerId) return
|
||||
const customer = customers.value.find((item) => String(item.id) === customerId)
|
||||
if (!customer) return
|
||||
if (!canView.value) return
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
try {
|
||||
const [terms, account] = await Promise.all([getFinanceCustomerTerms(customerId), getFinanceCustomerAccount(customerId)])
|
||||
rows.value = [{ id: customerId, customerName: customer.tenantName, customerCode: customer.tenantCode, creditDays: terms.creditDays, totalReceivable: account.totalReceivable, totalReceived: account.totalReceived, outstandingAmount: account.outstandingAmount, entryCount: account.entries.length, entries: account.entries, status: customer.status }]
|
||||
const result = await listFinanceAccounts({ page: page.value, size: pageSize.value, keyword: String(filters.keyword || '').trim(), status: String(filters.status || '') })
|
||||
rows.value = result.records
|
||||
total.value = result.total
|
||||
} catch (error) { loadError.value = getApiErrorDetails(error, '客户账户加载失败,请重试') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
function search(model: QueryModel) { page.value = 1; void load(model) }
|
||||
async function openAccount(row: Record<string, unknown>, edit: boolean) {
|
||||
const account = row as unknown as AccountRow
|
||||
selected.value = account; editing.value = edit; drawerOpen.value = true; loadError.value = null
|
||||
try { selectedTerms.value = await getFinanceCustomerTerms(account.id); creditDays.value = selectedTerms.value.creditDays }
|
||||
catch (error) { loadError.value = getApiErrorDetails(error, '客户账期加载失败,请重试') }
|
||||
selected.value = row as unknown as FinanceAccountSummary
|
||||
editing.value = edit
|
||||
drawerOpen.value = true
|
||||
ledger.value = []
|
||||
ledgerError.value = null
|
||||
selectedTerms.value = null
|
||||
if (edit) {
|
||||
if (!canViewTerms.value) return
|
||||
try { selectedTerms.value = await getFinanceCustomerTerms(selected.value.customerId); creditDays.value = selectedTerms.value.creditDays }
|
||||
catch (error) { ledgerError.value = getApiErrorDetails(error, '客户账期加载失败,请重试') }
|
||||
return
|
||||
}
|
||||
ledgerLoading.value = true
|
||||
try {
|
||||
const result = await listFinanceAccountLedger(selected.value.customerId, { page: 1, size: 200 })
|
||||
ledger.value = result.records
|
||||
} catch (error) { ledgerError.value = getApiErrorDetails(error, '客户账户流水加载失败,请重试') }
|
||||
finally { ledgerLoading.value = false }
|
||||
}
|
||||
async function retryDrawer() { if (selected.value) await openAccount(selected.value as unknown as Record<string, unknown>, editing.value) }
|
||||
async function saveTerms() {
|
||||
if (!selected.value || !Number.isInteger(creditDays.value) || creditDays.value < 0 || creditDays.value > 3650) { ElMessage.warning('账期必须是 0 到 3650 的整数天'); return }
|
||||
saving.value = true
|
||||
try { selectedTerms.value = await updateFinanceCustomerTerms(selected.value.id, { customerName: selected.value.customerName, creditDays: creditDays.value }); editing.value = false; ElMessage.success('客户账期已保存'); await load() }
|
||||
catch (error) { loadError.value = getApiErrorDetails(error, '客户账期保存失败,请重试'); ElMessage.error(loadError.value.message) }
|
||||
ledgerError.value = null
|
||||
try { selectedTerms.value = await updateFinanceCustomerTerms(selected.value.customerId, { customerName: selected.value.customerName, creditDays: creditDays.value }); ElMessage.success('客户账期已保存') }
|
||||
catch (error) { ledgerError.value = getApiErrorDetails(error, '客户账期保存失败,请重试'); ElMessage.error(ledgerError.value.message) }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
function money(value: unknown) { return Number(value || 0).toFixed(2) }
|
||||
|
||||
onMounted(() => void loadCustomers())
|
||||
onMounted(() => void load())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="finance-account-page" spacing="normal">
|
||||
<el-result v-if="!canView" icon="warning" title="无权访问客户账户" sub-title="请联系管理员开通账单和客户账期查看权限" />
|
||||
<el-result v-if="!canView" icon="warning" title="无权访问客户账户" sub-title="请联系管理员开通客户账户查看权限" />
|
||||
<ListPageLayout v-else aria-label="客户账户">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="客户账户筛选" :fields="queryFields" preference-key="finance-customer-accounts-query" @search="load" /></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 :data="rows" :columns="columns" :loading="loading" preference-key="finance-customer-accounts-table" aria-label="客户账户列表" row-key="id" empty-text="请选择客户查询账户">
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button link type="primary" @click="openAccount(row, false)"><Eye :size="15" />查看</el-button><el-button v-if="canUpdate" link type="primary" @click="openAccount(row, true)"><Pencil :size="15" />账期</el-button></div></template>
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="客户账户筛选" :fields="queryFields" preference-key="finance-customer-accounts-query" @search="search" /></template>
|
||||
<template v-if="loadError" #state><LoadErrorState title="客户账户加载失败" :message="loadError.message" :loading="loading" @retry="load()"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
|
||||
<section v-if="!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-accounts-table" aria-label="客户账户列表" row-key="customerId" empty-text="暂无客户账户" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button link type="primary" @click="openAccount(row, false)"><Eye :size="15" />查看</el-button><el-button v-if="canUpdateTerms" link type="primary" @click="openAccount(row, true)"><Pencil :size="15" />账期</el-button></div></template>
|
||||
<template #status="{ row }"><el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'">{{ row.status === 'ACTIVE' ? '正常' : '停用' }}</el-tag></template>
|
||||
<template #pagination-left>币种:CNY · 账户由对账单与收款记录生成</template>
|
||||
<template #pagination-left>币种:CNY · 账户数据由对账单、收付款和费用来源派生</template>
|
||||
</DataTable>
|
||||
</section>
|
||||
</ListPageLayout>
|
||||
<AppDrawer v-model="drawerOpen" :title="editing ? '维护客户账期' : '客户账户详情'" size="min(680px, 94vw)" :confirm-loading="saving">
|
||||
<template v-if="selected">
|
||||
<el-descriptions :column="2" border class="account-detail"><el-descriptions-item label="客户">{{ selected.customerName }}</el-descriptions-item><el-descriptions-item label="客户编号">{{ selected.customerCode }}</el-descriptions-item><el-descriptions-item label="累计应收">{{ money(selected.totalReceivable) }} CNY</el-descriptions-item><el-descriptions-item label="累计已收">{{ money(selected.totalReceived) }} CNY</el-descriptions-item><el-descriptions-item label="未收">{{ money(selected.outstandingAmount) }} CNY</el-descriptions-item><el-descriptions-item label="账户模式">人工应收台账</el-descriptions-item></el-descriptions>
|
||||
<FormSection title="账户流水" description="由对账单和收款记录生成。"><div v-for="entry in selected.entries" :key="`${entry.entryType}-${entry.resourceId}`" class="account-entry"><span>{{ entry.entryType === 'RECEIPT' ? '收款' : '应收' }} · {{ entry.resourceNo }}</span><span>{{ entry.occurredAt }}</span><b>{{ entry.entryType === 'RECEIPT' ? entry.creditAmount : entry.debitAmount }} CNY</b></div><el-empty v-if="!selected.entries.length" description="暂无账户流水" /></FormSection>
|
||||
<FormSection title="账期设置" description="每个客户独立设置,默认 30 天;新账单按该账期计算到期日。"><el-input-number v-model="creditDays" :min="0" :max="3650" :step="1" :disabled="!editing" /><span class="term-unit">天</span></FormSection>
|
||||
<AppDrawer v-model="drawerOpen" :title="editing ? '维护客户账期' : '客户账户详情'" size="min(760px, 94vw)" :loading="ledgerLoading" :confirm-loading="saving">
|
||||
<LoadErrorState v-if="ledgerError" :title="editing ? '客户账期加载失败' : '账户流水加载失败'" :message="ledgerError.message" :loading="ledgerLoading" @retry="retryDrawer"><template #details><ApiErrorMeta :details="ledgerError" /></template></LoadErrorState>
|
||||
<template v-else-if="selected">
|
||||
<el-descriptions :column="2" border class="account-detail"><el-descriptions-item label="客户">{{ selected.customerName }}</el-descriptions-item><el-descriptions-item label="客户编号">{{ selected.customerCode }}</el-descriptions-item><el-descriptions-item label="待收">{{ money(selected.outstandingReceivable) }} CNY</el-descriptions-item><el-descriptions-item label="待开账">{{ money(selected.pendingBilling) }} CNY</el-descriptions-item><el-descriptions-item label="本期已收">{{ money(selected.receivedInPeriod) }} CNY</el-descriptions-item><el-descriptions-item label="待付">{{ money(selected.outstandingPayable) }} CNY</el-descriptions-item></el-descriptions>
|
||||
<FormSection v-if="!editing" title="账户流水" description="按后端正式流水 DTO 展示应收、应付及变动后未收金额。"><div v-for="entry in ledger" :key="entry.ledgerId" class="ledger-entry"><div><strong>{{ entry.documentNo || `单据 ${entry.documentId}` }}</strong><span>{{ entry.businessType }} · {{ entry.documentType }}</span></div><span>{{ entry.occurredAt }}</span><div class="ledger-money"><b>应收 {{ money(entry.receivableChange) }}</b><b>应付 {{ money(entry.payableChange) }}</b><span>变动后未收 {{ money(entry.outstandingReceivableAfter) }} CNY</span></div><small>{{ entry.operatorName }}{{ entry.remark ? ` · ${entry.remark}` : '' }}</small></div><el-empty v-if="!ledger.length" description="暂无账户流水" /></FormSection>
|
||||
<FormSection v-else title="账期设置" description="每个客户独立设置,默认 30 天。"><el-input-number v-model="creditDays" :min="0" :max="3650" :step="1" /><span class="term-unit">天</span></FormSection>
|
||||
</template>
|
||||
<template #footer><div class="drawer-footer"><el-button @click="drawerOpen = false">关闭</el-button><el-button v-if="editing && canUpdate" type="primary" :loading="saving" @click="saveTerms"><Save :size="16" />保存账期</el-button></div></template>
|
||||
<template #footer><div class="drawer-footer"><el-button @click="drawerOpen = false">关闭</el-button><el-button v-if="editing && canUpdateTerms" type="primary" :loading="saving" @click="saveTerms"><Save :size="16" />保存账期</el-button></div></template>
|
||||
</AppDrawer>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.finance-account-page { min-width: 0; }
|
||||
.row-actions, .drawer-footer { display: flex; align-items: center; gap: 8px; }
|
||||
.drawer-footer { justify-content: flex-end; }
|
||||
.row-actions .el-button { margin-left: 0; }
|
||||
.account-detail { margin-bottom: 16px; }
|
||||
.term-unit { margin-left: 8px; color: var(--cc-muted); }
|
||||
.account-entry { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--el-border-color-lighter); }
|
||||
.account-entry span:nth-child(2) { color: var(--cc-muted); }
|
||||
.account-entry b { white-space: nowrap; }
|
||||
@media (max-width: 720px) { .drawer-footer .el-button { min-height: 44px; } :deep(.account-detail .el-descriptions__body .el-descriptions__table) { table-layout: fixed; } }
|
||||
@media (max-width: 480px) { .drawer-footer .el-button { flex: 1 1 120px; } .account-entry { grid-template-columns: 1fr; } :deep(.account-detail .el-descriptions__cell) { display: block; width: 100%; } }
|
||||
.finance-account-page{min-width:0}.row-actions,.drawer-footer{display:flex;align-items:center;gap:8px}.drawer-footer{justify-content:flex-end}.row-actions .el-button{margin-left:0}.account-detail{margin-bottom:16px}.term-unit{margin-left:8px;color:var(--cc-muted)}.ledger-entry{display:grid;grid-template-columns:minmax(160px,1fr) auto minmax(210px,auto);gap:8px 16px;padding:12px 0;border-bottom:1px solid var(--el-border-color-lighter)}.ledger-entry>div:first-child,.ledger-money{display:grid;gap:3px}.ledger-entry span,.ledger-entry small{color:var(--cc-muted)}.ledger-entry small{grid-column:1/-1}.ledger-money{text-align:right}.ledger-money b{white-space:nowrap}@media(max-width:720px){.drawer-footer .el-button{min-height:44px}.ledger-entry{grid-template-columns:1fr 1fr}.ledger-money{grid-column:1/-1;text-align:left}:deep(.account-detail .el-descriptions__body .el-descriptions__table){table-layout:fixed}}@media(max-width:480px){.drawer-footer .el-button{flex:1 1 120px}.ledger-entry{grid-template-columns:1fr}:deep(.account-detail .el-descriptions__cell){display:block;width:100%}}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Eye } from '@lucide/vue'
|
||||
import { Eye, FileCheck2, Link2 } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
@@ -43,6 +43,9 @@ const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const canView = computed(() => access.can('platform:finance:bill:view'))
|
||||
const canViewSource = computed(() => access.can('platform:finance:fee-source:view'))
|
||||
const canReviewSource = computed(() => access.can('platform:finance:fee-source:review'))
|
||||
const canSettleSource = computed(() => access.can('platform:finance:fee-source:settlement'))
|
||||
const stateError = computed(() => customersError.value || loadError.value)
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'customerId', type: 'select', label: '客户', width: 260, placeholder: '请选择客户', props: { 'aria-label': '客户', filterable: true, loading: customersLoading.value }, options: customers.value.map((item) => ({ label: item.tenantName, value: String(item.id), disabled: item.status !== 'ACTIVE' })) },
|
||||
@@ -93,18 +96,20 @@ onMounted(() => void loadCustomers())
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="客户应收应付筛选" :fields="queryFields" preference-key="finance-customer-arap-query" @search="load" /></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="filteredRows.length" preference-key="finance-customer-arap-table" aria-label="客户应收应付列表" row-key="id" empty-text="请选择客户查询应收应付">
|
||||
<template #actions="{ row }"><el-button link type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><el-button link type="primary" :disabled="!canViewSource" :title="canViewSource ? '费用来源详情接口尚未提供' : '无费用来源查看权限'" @click="openDetail(row)"><Eye :size="15" />摘要</el-button><el-button v-if="canReviewSource" link type="primary" disabled title="审核接口尚未提供,暂不可操作"><FileCheck2 :size="15" />审核</el-button><el-button v-if="canSettleSource" link type="primary" disabled title="参与结算接口尚未提供,暂不可操作"><Link2 :size="15" />结算</el-button></div></template>
|
||||
<template #direction="{ row }"><el-tag :type="row.direction === 'RECEIVABLE' ? 'success' : 'warning'">{{ row.direction === 'RECEIVABLE' ? '应收' : '应付' }}</el-tag></template>
|
||||
<template #status="{ row }"><el-tag :type="row.status === 'PAID' ? 'success' : row.status === 'VOIDED' ? 'info' : 'warning'">{{ statusLabel(row.status) }}</el-tag></template>
|
||||
<template #pagination-left>费用来源来自真实账单明细、入库费用快照和成本台账 · CNY</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
<AppDrawer v-model="drawerOpen" title="应收应付详情" size="min(680px, 94vw)"><el-descriptions v-if="selected" :column="2" border class="arap-detail"><el-descriptions-item label="收付方向">{{ selected.direction === 'RECEIVABLE' ? '应收' : '应付' }}</el-descriptions-item><el-descriptions-item label="状态">{{ statusLabel(selected.status) }}</el-descriptions-item><el-descriptions-item label="客户">{{ selected.customerName }}</el-descriptions-item><el-descriptions-item label="费用单据">{{ selected.documentNo }}</el-descriptions-item><el-descriptions-item label="来源类型">{{ selected.sourceType }}</el-descriptions-item><el-descriptions-item label="来源追溯">{{ selected.sourceId }}</el-descriptions-item><el-descriptions-item label="费用项目">{{ selected.feeName }}</el-descriptions-item><el-descriptions-item label="金额">{{ selected.amount }} CNY</el-descriptions-item><el-descriptions-item label="发生日期">{{ selected.occurredAt }}</el-descriptions-item></el-descriptions><template #footer><el-button @click="drawerOpen = false">关闭</el-button></template></AppDrawer>
|
||||
<AppDrawer v-model="drawerOpen" title="费用来源摘要" size="min(680px, 94vw)"><el-descriptions v-if="selected" :column="2" border class="arap-detail"><el-descriptions-item label="收付方向">{{ selected.direction === 'RECEIVABLE' ? '应收' : '应付' }}</el-descriptions-item><el-descriptions-item label="状态">{{ statusLabel(selected.status) }}</el-descriptions-item><el-descriptions-item label="客户">{{ selected.customerName }}</el-descriptions-item><el-descriptions-item label="费用单据">{{ selected.documentNo }}</el-descriptions-item><el-descriptions-item label="来源类型">{{ selected.sourceType }}</el-descriptions-item><el-descriptions-item label="来源追溯">{{ selected.sourceId }}</el-descriptions-item><el-descriptions-item label="费用项目">{{ selected.feeName }}</el-descriptions-item><el-descriptions-item label="金额">{{ selected.amount }} CNY</el-descriptions-item><el-descriptions-item label="发生日期">{{ selected.occurredAt }}</el-descriptions-item></el-descriptions><el-alert type="info" :closable="false" title="费用来源详情、审核和参与结算接口尚未提供,当前仅展示列表摘要。" /><template #footer><el-button @click="drawerOpen = false">关闭</el-button></template></AppDrawer>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.finance-arap-page { min-width: 0; }
|
||||
.row-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
|
||||
.row-actions .el-button { margin-left: 0; }
|
||||
.arap-detail { margin-bottom: 16px; }
|
||||
@media (max-width: 720px) { :deep(.el-drawer__footer .el-button) { min-height: 44px; } :deep(.arap-detail .el-descriptions__body .el-descriptions__table) { table-layout: fixed; } }
|
||||
@media (max-width: 480px) { :deep(.arap-detail .el-descriptions__cell) { display: block; width: 100%; } }
|
||||
|
||||
@@ -1,2 +1,26 @@
|
||||
<script setup lang="ts">import FinanceManualPage from './FinanceManualPage.vue'</script>
|
||||
<template><FinanceManualPage mode="bills" /></template>
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Eye, Plus } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDrawer from '@/shared/components/AppDrawer.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { getFinanceBill, listFinanceStatements } from '../api/finance'
|
||||
import type { FinanceBillDetail, FinanceStatementReadModel } from '../types'
|
||||
const access = useAccessPreviewStore(); const customers = ref<PlatformTenantResource[]>([]); const filters = reactive<QueryModel>({ customerId: '' }); const rows = ref<FinanceStatementReadModel[]>([]); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null); const customerError = ref<ApiErrorDetails | null>(null); const drawerOpen = ref(false); const detail = ref<FinanceBillDetail | null>(null); const detailError = ref<ApiErrorDetails | null>(null)
|
||||
const canView = computed(() => access.can('platform:finance:bill:view')); const fields = computed<QueryField[]>(() => [{ key: 'customerId', type: 'select', label: '客户', width: 280, placeholder: '请选择客户', props: { filterable: true }, options: customers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })) }]); const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 100, fixed: 'left', slotName: 'actions' }, { key: 'billNo', label: '对账单号', minWidth: 180 }, { key: 'customerName', label: '客户', minWidth: 160 }, { key: 'billDate', label: '账单日期', width: 130 }, { key: 'dueDate', label: '到期日', width: 130 }, { key: 'totalAmount', label: '应收(CNY)', width: 140 }, { key: 'receivedAmount', label: '已收(CNY)', width: 140 }, { key: 'outstandingAmount', label: '未收(CNY)', width: 140 }, { key: 'status', label: '状态', width: 120 }]
|
||||
async function loadCustomers() { try { const result = await listPlatformTenants({ page: 1, size: 200 }); customers.value = result.records } catch (e) { customerError.value = getApiErrorDetails(e, '客户列表加载失败') } }
|
||||
async function load(model: QueryModel = filters) { Object.assign(filters, model); rows.value = []; error.value = null; if (!filters.customerId) return; loading.value = true; try { rows.value = await listFinanceStatements(String(filters.customerId)) } catch (e) { error.value = getApiErrorDetails(e, '客户对账单加载失败,请重试') } finally { loading.value = false } }
|
||||
async function openDetail(row: FinanceStatementReadModel) { drawerOpen.value = true; detail.value = null; detailError.value = null; try { detail.value = await getFinanceBill(row.id) } catch (e) { detailError.value = getApiErrorDetails(e, '对账单详情加载失败,请重试') } }
|
||||
onMounted(() => void loadCustomers())
|
||||
</script>
|
||||
<template>
|
||||
<AdminPage as="div" class="finance-statements-page" spacing="normal"><el-result v-if="!canView" icon="warning" title="无权访问客户对账单" sub-title="请联系管理员开通财务查看权限" /><ListPageLayout v-else aria-label="客户对账单"><template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="客户对账单筛选" :fields="fields" preference-key="finance-statements-query" @search="load" /></template><template v-if="customerError || error" #state><LoadErrorState title="客户对账单加载失败" :message="(customerError || error)?.message || ''" :loading="loading" @retry="customerError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="(customerError || error)!" /></template></LoadErrorState></template><section v-if="!customerError && !error" class="surface-panel table-panel"><DataTable :data="rows" :columns="columns" :loading="loading" aria-label="客户对账单列表" row-key="id" empty-text="请选择客户查询对账单"><template #toolbar-actions><el-button type="primary" disabled title="接口尚未提供,暂不可操作"><Plus :size="16" />生成对账单</el-button></template><template #actions="{ row }"><el-button link type="primary" @click="openDetail(row as unknown as FinanceStatementReadModel)"><Eye :size="15" />详情</el-button></template><template #pagination-left>币种:CNY · 生成、出账、调整、作废待后端 G1-G4 契约冻结</template></DataTable></section></ListPageLayout><AppDrawer v-model="drawerOpen" title="客户对账单详情" size="min(760px, 94vw)"><LoadErrorState v-if="detailError" title="详情加载失败" :message="detailError.message" :loading="false"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState><template v-else-if="detail"><el-descriptions :column="2" border><el-descriptions-item label="对账单号">{{ detail.billNo }}</el-descriptions-item><el-descriptions-item label="客户">{{ detail.customerName }}</el-descriptions-item><el-descriptions-item label="状态">{{ detail.status }}</el-descriptions-item><el-descriptions-item label="金额">{{ detail.totalAmount }} CNY</el-descriptions-item></el-descriptions><el-alert class="blocked-alert" type="info" :closable="false" title="对账单生成与状态操作接口尚未提供,当前仅支持真实详情查询。" /></template></AppDrawer></AdminPage>
|
||||
</template>
|
||||
<style scoped>.finance-statements-page{min-width:0}.blocked-alert{margin-top:16px}</style>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Eye, Plus } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDrawer from '@/shared/components/AppDrawer.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { financePhase1Capabilities, listFinancePayableSettlements, listFinanceReceivableSettlements } from '../api/finance'
|
||||
import type { FinancePayableSettlement, FinanceReceivableSettlement } from '../types'
|
||||
|
||||
const props = defineProps<{ kind: 'receivable' | 'payable' }>()
|
||||
const access = useAccessPreviewStore()
|
||||
const customers = ref<PlatformTenantResource[]>([])
|
||||
const filters = reactive<QueryModel>({ customerId: '' })
|
||||
const rows = ref<Array<FinanceReceivableSettlement | FinancePayableSettlement>>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<ApiErrorDetails | null>(null)
|
||||
const customerError = ref<ApiErrorDetails | null>(null)
|
||||
const drawerOpen = ref(false)
|
||||
const selected = ref<FinanceReceivableSettlement | FinancePayableSettlement | null>(null)
|
||||
const viewPermission = computed(() => props.kind === 'receivable' ? 'platform:finance:settlement-ar:view' : 'platform:finance:settlement-ap:view')
|
||||
const canView = computed(() => access.can(viewPermission.value))
|
||||
const title = computed(() => props.kind === 'receivable' ? '结算单应收' : '结算单应付')
|
||||
const blocked = computed(() => props.kind === 'receivable' ? !financePhase1Capabilities.receivableSettlementCreate : !financePhase1Capabilities.payableSettlementCreate)
|
||||
const fields = computed<QueryField[]>(() => [{ key: 'customerId', type: 'select', label: '客户', width: 280, placeholder: '请选择客户', props: { filterable: true, loading: !customers.value.length && !customerError.value }, options: customers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })) }])
|
||||
const columns = computed<DataTableColumn[]>(() => props.kind === 'receivable'
|
||||
? [{ key: 'actions', label: '操作', width: 100, fixed: 'left', slotName: 'actions' }, { key: 'settlementNo', label: '应收结算单号', minWidth: 180 }, { key: 'customerId', label: '客户', minWidth: 140 }, { key: 'receivableAmount', label: '应收(CNY)', width: 140 }, { key: 'receivedAmount', label: '已收(CNY)', width: 140 }, { key: 'outstandingAmount', label: '未收(CNY)', width: 140 }, { key: 'status', label: '状态', width: 120 }]
|
||||
: [{ key: 'actions', label: '操作', width: 100, fixed: 'left', slotName: 'actions' }, { key: 'settlementNo', label: '应付结算单号', minWidth: 180 }, { key: 'customerId', label: '客户', minWidth: 140 }, { key: 'feeItemName', label: '费用项目', minWidth: 160 }, { key: 'payableAmount', label: '应付(CNY)', width: 140 }, { key: 'paidAmount', label: '已付(CNY)', width: 140 }, { key: 'outstandingAmount', label: '未付(CNY)', width: 140 }, { key: 'status', label: '状态', width: 120 }])
|
||||
async function loadCustomers() { try { const result = await listPlatformTenants({ page: 1, size: 200 }); customers.value = result.records } catch (e) { customerError.value = getApiErrorDetails(e, '客户列表加载失败') } }
|
||||
async function load(model: QueryModel = filters) { Object.assign(filters, model); rows.value = []; error.value = null; if (!filters.customerId) return; loading.value = true; try { rows.value = props.kind === 'receivable' ? await listFinanceReceivableSettlements(String(filters.customerId)) : await listFinancePayableSettlements(String(filters.customerId)) } catch (e) { error.value = getApiErrorDetails(e, `${title.value}加载失败,请重试`) } finally { loading.value = false } }
|
||||
function openDetail(row: FinanceReceivableSettlement | FinancePayableSettlement) { selected.value = row; drawerOpen.value = true }
|
||||
function totalAmount(row: FinanceReceivableSettlement | FinancePayableSettlement) { return props.kind === 'receivable' ? (row as FinanceReceivableSettlement).receivableAmount : (row as FinancePayableSettlement).payableAmount }
|
||||
onMounted(() => void loadCustomers())
|
||||
</script>
|
||||
<template>
|
||||
<AdminPage as="div" class="finance-settlement-page" spacing="normal">
|
||||
<el-result v-if="!canView" icon="warning" :title="`无权访问${title}`" sub-title="请联系管理员开通结算单查看权限" />
|
||||
<ListPageLayout v-else :aria-label="title">
|
||||
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" :ariaLabel="`${title}筛选`" :fields="fields" preference-key="finance-settlement-query" @search="load" /></template>
|
||||
<template v-if="customerError || error" #state><LoadErrorState :title="`${title}加载失败`" :message="(customerError || error)?.message || ''" :loading="loading" @retry="customerError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="(customerError || error)!" /></template></LoadErrorState></template>
|
||||
<section v-if="!customerError && !error" class="surface-panel table-panel">
|
||||
<DataTable :data="rows" :columns="columns" :loading="loading" :aria-label="`${title}列表`" row-key="id" empty-text="请选择客户查询结算单">
|
||||
<template #toolbar-actions><el-button type="primary" disabled :title="blocked ? '接口尚未提供,暂不可操作' : ''"><Plus :size="16" />生成{{ title }}</el-button></template>
|
||||
<template #actions="{ row }"><el-button link type="primary" @click="openDetail(row as unknown as FinanceReceivableSettlement | FinancePayableSettlement)"><Eye :size="15" />详情</el-button></template>
|
||||
<template #pagination-left>币种:CNY · 生成接口尚未提供时仅展示真实查询结果</template>
|
||||
</DataTable>
|
||||
</section>
|
||||
</ListPageLayout>
|
||||
<AppDrawer v-model="drawerOpen" :title="`${title}详情`" size="min(680px, 94vw)">
|
||||
<el-descriptions v-if="selected" :column="2" border><el-descriptions-item label="结算单号">{{ selected.settlementNo }}</el-descriptions-item><el-descriptions-item label="客户">{{ selected.customerId }}</el-descriptions-item><el-descriptions-item :label="props.kind === 'receivable' ? '应收金额' : '应付金额'">{{ totalAmount(selected) }} CNY</el-descriptions-item><el-descriptions-item :label="props.kind === 'receivable' ? '未收金额' : '未付金额'">{{ selected.outstandingAmount }} CNY</el-descriptions-item><el-descriptions-item label="状态">{{ selected.status }}</el-descriptions-item></el-descriptions>
|
||||
<el-alert class="blocked-alert" type="info" :closable="false" title="结算单详情/收付款接口尚未提供,当前不显示不可执行的提交按钮。" />
|
||||
</AppDrawer>
|
||||
</AdminPage>
|
||||
</template>
|
||||
<style scoped>.finance-settlement-page{min-width:0}.blocked-alert{margin-top:16px}@media(max-width:720px){:deep(.el-drawer__footer .el-button){min-height:44px}}</style>
|
||||
@@ -1,2 +1,2 @@
|
||||
<script setup lang="ts">import FinanceManualPage from './FinanceManualPage.vue'</script>
|
||||
<template><FinanceManualPage mode="costs" /></template>
|
||||
<script setup lang="ts">import FinanceFormalSettlementPage from './FinanceFormalSettlementPage.vue'</script>
|
||||
<template><FinanceFormalSettlementPage kind="payable" /></template>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<script setup lang="ts">import FinanceManualPage from './FinanceManualPage.vue'</script>
|
||||
<template><FinanceManualPage mode="receipts" /></template>
|
||||
<script setup lang="ts">import FinanceFormalSettlementPage from './FinanceFormalSettlementPage.vue'</script>
|
||||
<template><FinanceFormalSettlementPage kind="receivable" /></template>
|
||||
|
||||
Reference in New Issue
Block a user