优化优化
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
import AppDrawer from '@/shared/components/AppDrawer.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
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
|
||||
}
|
||||
|
||||
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 loading = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const drawerOpen = ref(false)
|
||||
const editing = ref(false)
|
||||
const saving = ref(false)
|
||||
const selected = ref<AccountRow | 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 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: 'status', label: '客户状态', width: 110, slotName: 'status' },
|
||||
]
|
||||
|
||||
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
|
||||
loading.value = true
|
||||
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 }]
|
||||
} catch (error) { loadError.value = getApiErrorDetails(error, '客户账户加载失败,请重试') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
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, '客户账期加载失败,请重试') }
|
||||
}
|
||||
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) }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
function money(value: unknown) { return Number(value || 0).toFixed(2) }
|
||||
|
||||
onMounted(() => void loadCustomers())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="finance-account-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="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 #status="{ row }"><el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'">{{ row.status === 'ACTIVE' ? '正常' : '停用' }}</el-tag></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>
|
||||
</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>
|
||||
</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%; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user