修复财务页面竞态并整理采购与运输展示
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import '@/styles/adminWorkbench.css'
|
||||
import '@/styles/adminDensity.css'
|
||||
import '../financeDensity.css'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Download, Plus, SlidersHorizontal } from '@lucide/vue'
|
||||
@@ -29,15 +29,6 @@ import { actionReconciliationStatement, createReconciliationStatement, getReconc
|
||||
import { financeBillStatusOptions, financeSourceTypeLabel, financeStatusLabel, financeStatusTone } from '../status'
|
||||
import type { FinanceFeeSourceSummary, FinanceReconciliationDetail, FinanceStatementReadModel, ReconciliationActionPayload } from '../types'
|
||||
|
||||
interface AdjustmentRow {
|
||||
selected: boolean
|
||||
statementLineId: string
|
||||
feeItemName: string
|
||||
originalAmount: number
|
||||
adjustedAmount: number
|
||||
reason: string
|
||||
}
|
||||
|
||||
type StatementRow = Omit<FinanceStatementReadModel, 'creditDays' | 'settled'> & {
|
||||
periodStart: string
|
||||
periodEnd: string
|
||||
@@ -85,12 +76,9 @@ const createForm = ref({
|
||||
remark: '',
|
||||
})
|
||||
let createSourceLoadSequence = 0
|
||||
const adjustOpen = ref(false)
|
||||
const adjustBusy = ref(false)
|
||||
const adjustError = ref<ApiErrorDetails | null>(null)
|
||||
const adjustStatement = ref<FinanceStatementReadModel | null>(null)
|
||||
const adjustReason = ref('')
|
||||
const adjustLines = ref<AdjustmentRow[]>([])
|
||||
let selectedCreateGeneration = 0
|
||||
let pageActive = true
|
||||
let initialLoadCompleted = false
|
||||
|
||||
const canView = computed(() => access.can('platform:finance:customer-statement:list'))
|
||||
const canListCustomers = computed(() => access.can('platform:customer:list'))
|
||||
@@ -100,7 +88,6 @@ const canCreate = computed(() => access.can('platform:finance:reconciliation:cre
|
||||
const activeCustomers = computed(() => customers.value.filter((item) => item.status === 'ACTIVE'))
|
||||
const canReview = computed(() => access.can('platform:finance:reconciliation:review'))
|
||||
const canIssue = computed(() => access.can('platform:finance:reconciliation:issue'))
|
||||
const canAdjust = computed(() => access.can('platform:finance:reconciliation:adjust'))
|
||||
const canVoid = computed(() => access.can('platform:finance:reconciliation:void'))
|
||||
const canExport = computed(() => access.can('platform:finance:customer-statement:export') && access.can('platform:download:list'))
|
||||
const canGenerate = computed(() => canCreate.value && canListCustomers.value)
|
||||
@@ -407,6 +394,7 @@ function defaultCreateForm() {
|
||||
|
||||
function openCreate() {
|
||||
if (!canListCustomers.value) return ElMessage.warning('缺少客户列表权限,无法选择客户生成对账单')
|
||||
invalidateCreateLoads()
|
||||
createForm.value = defaultCreateForm()
|
||||
createSourceOptions.value = []
|
||||
createSourcesError.value = null
|
||||
@@ -440,6 +428,7 @@ async function loadEligibleCreateSources(preferredIds: string[] = createForm.val
|
||||
businessDateFrom: createForm.value.periodStart,
|
||||
businessDateTo: createForm.value.periodEnd,
|
||||
})
|
||||
if (sequence !== createSourceLoadSequence) return
|
||||
records.push(...result.records)
|
||||
pages = Math.max(1, result.pages)
|
||||
currentPage += 1
|
||||
@@ -467,7 +456,9 @@ function queryText(value: unknown) {
|
||||
}
|
||||
|
||||
async function openSelectedCreateFromRoute() {
|
||||
if (route.name !== 'finance-customer-statements' || queryText(route.query.createMode) !== 'SELECTED') return
|
||||
if (!pageActive || route.name !== 'finance-customer-statements' || queryText(route.query.createMode) !== 'SELECTED') return
|
||||
const generation = ++selectedCreateGeneration
|
||||
const sourcePath = route.fullPath
|
||||
if (!canGenerate.value || !canSelectSources.value) {
|
||||
ElMessage.warning('缺少客户或费用来源权限,无法生成所选对账单')
|
||||
return
|
||||
@@ -484,8 +475,12 @@ async function openSelectedCreateFromRoute() {
|
||||
ElMessage.warning('带入费用所属客户当前不可用,请重新选择费用')
|
||||
return
|
||||
}
|
||||
createError.value = null
|
||||
createForm.value = { generationMode: 'SELECTED', customerId, periodStart, periodEnd, feeSourceIds: [], remark: '' }
|
||||
await loadEligibleCreateSources(feeSourceIds)
|
||||
// 带入候选只属于当前激活页面及本次路由,旧响应不能重新打开弹框或覆盖用户导航。
|
||||
if (!pageActive || generation !== selectedCreateGeneration || route.fullPath !== sourcePath
|
||||
|| !canGenerate.value || !canSelectSources.value) return
|
||||
createOpen.value = true
|
||||
await router.replace({ name: 'finance-customer-statements' })
|
||||
}
|
||||
@@ -535,18 +530,6 @@ async function performAction(row: FinanceStatementReadModel, actionName: Reconci
|
||||
finally { actionBusy.value = false }
|
||||
}
|
||||
|
||||
async function openAdjustment(row: FinanceStatementReadModel) {
|
||||
adjustError.value = null
|
||||
adjustBusy.value = true
|
||||
try {
|
||||
const current = await fetchDetail(row.id, true)
|
||||
adjustStatement.value = { ...row, version: current.version }
|
||||
adjustReason.value = ''
|
||||
adjustLines.value = current.lines.flatMap((line) => line.statementLineId ? [{ selected: false, statementLineId: line.statementLineId, feeItemName: line.feeItemName, originalAmount: line.amount, adjustedAmount: line.amount, reason: '' }] : [])
|
||||
adjustOpen.value = true
|
||||
} catch (cause) { error.value = getApiErrorDetails(cause, '调整明细加载失败') }
|
||||
finally { adjustBusy.value = false }
|
||||
}
|
||||
function statementActions(row: FinanceStatementReadModel): RowActionItem[] {
|
||||
return [
|
||||
{ key: 'detail', label: '详情', priority: 300, disabled: loading.value },
|
||||
@@ -554,14 +537,13 @@ function statementActions(row: FinanceStatementReadModel): RowActionItem[] {
|
||||
{ key: 'approve', label: '通过', priority: 250, tone: 'warning', visible: row.status === 'PENDING_REVIEW' && canReview.value, loading: actionBusy.value },
|
||||
{ key: 'reject', label: '驳回', priority: 200, tone: 'danger', visible: row.status === 'PENDING_REVIEW' && canReview.value, loading: actionBusy.value },
|
||||
{ key: 'issue', label: '出账', priority: 200, visible: row.status === 'APPROVED' && canIssue.value, loading: actionBusy.value },
|
||||
{ key: 'adjust', label: '调整', priority: 200, visible: row.status === 'ISSUED' && canAdjust.value, loading: adjustBusy.value },
|
||||
{ key: 'reopen', label: '重开', priority: 150, visible: ['REJECTED', 'VOIDED'].includes(String(row.status)) && canVoid.value, loading: actionBusy.value },
|
||||
{ key: 'void', label: '作废', tone: 'danger', divided: true, visible: ['DRAFT', 'PENDING_REVIEW', 'APPROVED', 'ISSUED'].includes(String(row.status)) && canVoid.value, loading: actionBusy.value },
|
||||
{ key: 'void', label: '作废', tone: 'danger', divided: true, visible: ['DRAFT', 'PENDING_REVIEW'].includes(String(row.status)) && canVoid.value, loading: actionBusy.value },
|
||||
]
|
||||
}
|
||||
function handleStatementAction(command: string, row: FinanceStatementReadModel) {
|
||||
if (!statementActions(row).some(item => item.key === command && item.visible !== false)) return
|
||||
if (command === 'detail') void openDetail(row)
|
||||
else if (command === 'adjust') void openAdjustment(row)
|
||||
else {
|
||||
const actions: Record<string, ReconciliationActionPayload['action']> = {
|
||||
submit: 'SUBMIT', approve: 'APPROVE', reject: 'REJECT', issue: 'ISSUE', reopen: 'REOPEN', void: 'VOID',
|
||||
@@ -571,35 +553,39 @@ function handleStatementAction(command: string, row: FinanceStatementReadModel)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAdjustment() {
|
||||
if (adjustBusy.value) return
|
||||
const statement = adjustStatement.value
|
||||
const selected = adjustLines.value.filter((line) => line.selected)
|
||||
if (!statement || !adjustReason.value.trim() || !selected.length) return ElMessage.warning('请填写调整原因并至少选择一条明细')
|
||||
if (selected.some((line) => line.adjustedAmount <= 0 || !line.reason.trim())) return ElMessage.warning('请填写所选明细的新金额和调整说明')
|
||||
adjustBusy.value = true
|
||||
adjustError.value = null
|
||||
try {
|
||||
await actionReconciliationStatement(statement.id, {
|
||||
action: 'ADJUST', reason: adjustReason.value.trim(), expectedVersion: statement.version,
|
||||
adjustmentLines: selected.map((line) => ({ statementLineId: line.statementLineId, adjustedAmount: line.adjustedAmount, reason: line.reason.trim() })),
|
||||
})
|
||||
adjustOpen.value = false
|
||||
ElMessage.success('对账单调整已保存')
|
||||
await load(appliedFilters.value)
|
||||
await openDetail(statement)
|
||||
} catch (cause) { adjustError.value = getApiErrorDetails(cause, '对账单调整失败'); error.value = adjustError.value }
|
||||
finally { adjustBusy.value = false }
|
||||
function invalidateCreateLoads() {
|
||||
selectedCreateGeneration += 1
|
||||
createSourceLoadSequence += 1
|
||||
createSourcesLoading.value = false
|
||||
}
|
||||
|
||||
function deactivateCreateLoads() {
|
||||
pageActive = false
|
||||
invalidateCreateLoads()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (canListCustomers.value) await loadCustomers()
|
||||
await (canListCustomers.value ? loadCustomers() : undefined)
|
||||
initialLoadCompleted = true
|
||||
await openSelectedCreateFromRoute()
|
||||
})
|
||||
onActivated(() => {
|
||||
pageActive = true
|
||||
if (initialLoadCompleted) void openSelectedCreateFromRoute()
|
||||
})
|
||||
onDeactivated(deactivateCreateLoads)
|
||||
onBeforeUnmount(deactivateCreateLoads)
|
||||
watch(() => route.name, (name) => {
|
||||
if (name !== 'finance-customer-statements') invalidateCreateLoads()
|
||||
}, { flush: 'sync' })
|
||||
watch(
|
||||
() => [createOpen.value, createForm.value.generationMode, createForm.value.customerId, createForm.value.periodStart, createForm.value.periodEnd],
|
||||
([open, mode]) => {
|
||||
if (!open || mode !== 'SELECTED') return
|
||||
([open, mode], [wasOpen]) => {
|
||||
if (!open) {
|
||||
if (wasOpen === true) invalidateCreateLoads()
|
||||
return
|
||||
}
|
||||
if (mode !== 'SELECTED') return
|
||||
void loadEligibleCreateSources()
|
||||
},
|
||||
)
|
||||
@@ -744,16 +730,7 @@ watch(detailRouteId, (statementId) => {
|
||||
</section>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="adjustOpen" :title="`调整客户对账单 · ${adjustStatement?.billNo || '—'}`" width="min(900px, 96vw)" fullscreenable :confirm-loading="adjustBusy" @confirm="submitAdjustment">
|
||||
<OperationErrorToast :details="adjustError" />
|
||||
<el-form class="density-form density-dialog-sections" label-position="left" label-width="110px" :disabled="adjustBusy">
|
||||
<section v-if="adjustStatement" class="density-dialog-section workbench-evidence" data-tone="summary" aria-label="当前对账单"><h3 class="density-dialog-section__title">当前对账单</h3><table class="density-table statement-dialog-facts"><tbody><tr><th scope="row">对账单号</th><td>{{ adjustStatement.billNo || '—' }}</td><th scope="row">客户</th><td>{{ customerDisplayName(adjustStatement.customerId, adjustStatement.customerName) }}</td></tr></tbody></table></section>
|
||||
<section class="density-dialog-section" data-tone="work" aria-label="对账单调整登记"><h3 class="density-dialog-section__title">调整登记</h3><div class="statement-adjust-editor">
|
||||
<div class="workbench-evidence"><table class="density-table density-table--editable"><tbody><tr><td><el-form-item label="整体调整原因"><el-input v-model="adjustReason" type="textarea" maxlength="500" show-word-limit placeholder="请输入整体调整原因" /></el-form-item></td></tr></tbody></table></div>
|
||||
<div class="workbench-table"><el-table :data="adjustLines" size="small" border stripe row-key="statementLineId"><el-table-column label="选择" width="64"><template #default="{ row }"><el-checkbox v-model="row.selected" :aria-label="`选择${row.feeItemName || '费用明细'}`" /></template></el-table-column><el-table-column prop="feeItemName" label="费用项目" min-width="150" show-overflow-tooltip /><el-table-column prop="originalAmount" label="原金额(CNY)" width="130" /><el-table-column label="新金额(CNY)" width="180"><template #default="{ row }"><el-input-number v-model="row.adjustedAmount" :min="0.01" :precision="2" placeholder="请输入新金额" /></template></el-table-column><el-table-column label="明细说明" min-width="220"><template #default="{ row }"><el-input v-model="row.reason" maxlength="500" placeholder="请输入明细说明" /></template></el-table-column></el-table></div>
|
||||
</div></section>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
@@ -761,10 +738,8 @@ watch(detailRouteId, (statementId) => {
|
||||
.finance-statements-page { min-width: 0; }
|
||||
:deep(.el-select), :deep(.el-date-editor) { width: 100%; }
|
||||
.selected-source-summary { width: 100%; margin-top: var(--admin-space-2); text-align: right; }
|
||||
.statement-create-table, .statement-dialog-facts { table-layout: fixed; }
|
||||
.statement-create-table { table-layout: fixed; }
|
||||
.statement-create-table td:not([colspan]) { width: 50%; }
|
||||
.statement-dialog-facts th { width: var(--admin-label-width); }
|
||||
.statement-adjust-editor { display: grid; gap: var(--admin-space-5); }
|
||||
.statement-detail-page { min-width: 0; width: 100%; margin: 0 auto; overflow: hidden; }
|
||||
.statement-summary-section { padding: var(--admin-space-4) var(--admin-space-6); border-bottom: 1px solid var(--workbench-border); }
|
||||
.statement-outstanding { color: var(--el-color-warning-dark-2); }
|
||||
|
||||
Reference in New Issue
Block a user