This commit is contained in:
2026-09-15 09:58:15 +08:00
parent 3f998bdb88
commit 4bf66d9571
90 changed files with 1599 additions and 1388 deletions
@@ -1,7 +1,8 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Check, FileUp, MoreHorizontal, Pencil, Plus, RefreshCw, Trash2, Undo2, X } from '@lucide/vue'
import { FileUp, MoreHorizontal, Plus, RefreshCw, X } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
@@ -87,6 +88,9 @@ function canDelete(row: InboundFeeDetail) { return isManual(row) && access.can('
function canReview(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:review') && row.reviewStatus === 'PENDING' }
function canAdjust(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:edit') && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' }
function canVoid(row: InboundFeeDetail) { return isManual(row) && access.can('firstleg:inbound:fee-detail:review') && row.reviewStatus === 'APPROVED' && row.publicationStatus !== 'VOIDED' }
function rowActionCount(row: InboundFeeDetail) {
return Number(canEdit(row)) + 2 * Number(canReview(row)) + Number(canAdjust(row)) + Number(canVoid(row)) + Number(canDelete(row))
}
function reviewLabel(value: string) { return ({ PENDING: '待审核', APPROVED: '已审核', REJECTED: '已驳回' } as Record<string, string>)[value] || value }
function publicationLabel(value: string) {
return ({ DRAFT: '草稿', PUBLISHED: '已发布', SUPERSEDED: '已被替代', VOIDED: '已作废' } as Record<string, string>)[value] || value || '未知状态'
@@ -231,7 +235,7 @@ async function save() {
async function remove(row: InboundFeeDetail) {
try {
await ElMessageBox.confirm('删除后无法恢复,是否继续?', '删除费用明细', { type: 'warning', confirmButtonText: '删除' })
await ElMessageBox.confirm('删除后无法恢复,是否继续?', '删除费用明细', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '删除' })
await deleteInboundFeeDetail(props.accountId, normalizedInboundId.value, String(row.id)); await finishAction('费用明细已删除')
} catch (error) { if (error !== 'cancel' && error !== 'close') actionError.value = getApiErrorDetails(error, '费用明细删除失败') }
}
@@ -252,7 +256,7 @@ async function reject() {
async function voidRow(row: InboundFeeDetail) {
try {
await ElMessageBox.confirm('作废会同步撤销尚未结算的财务费用来源,是否继续?', '作废费用明细', { type: 'warning', confirmButtonText: '作废' })
await ElMessageBox.confirm('作废会同步撤销尚未结算的财务费用来源,是否继续?', '作废费用明细', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '作废' })
await voidInboundFeeDetail(props.accountId, normalizedInboundId.value, String(row.id), row.sourceFeeVersion); await finishAction('费用明细已作废')
} catch (error) { if (error !== 'cancel' && error !== 'close') actionError.value = getApiErrorDetails(error, '费用作废失败') }
}
@@ -268,7 +272,7 @@ onMounted(() => void load())
</script>
<template>
<section class="fee-detail-manager" aria-label="运单费用维护">
<section class="fee-detail-manager admin-density" aria-label="运单费用维护">
<div class="fee-toolbar">
<span>共 {{ rows.length }} 条应收应付费用明细</span>
<div>
@@ -284,17 +288,20 @@ onMounted(() => void load())
<el-table :data="rows" border stripe empty-text="暂无费用数据" row-key="id">
<el-table-column label="操作" fixed="left" width="250">
<template #default="{ row }">
<el-button v-if="canEdit(row as InboundFeeDetail)" size="small" plain type="primary" @click="openEditor('edit', row as InboundFeeDetail)"><Pencil :size="15" />编辑</el-button>
<el-button v-if="canReview(row as InboundFeeDetail)" size="small" plain type="success" @click="approve(row as InboundFeeDetail)"><Check :size="15" />审核</el-button>
<el-button v-if="canReview(row as InboundFeeDetail)" size="small" plain type="warning" @click="openReject(row as InboundFeeDetail)"><Undo2 :size="15" />驳回</el-button>
<el-button v-if="canAdjust(row as InboundFeeDetail)" size="small" plain type="primary" @click="openEditor('adjust', row as InboundFeeDetail)"><Pencil :size="15" />调整</el-button>
<el-button v-if="canVoid(row as InboundFeeDetail)" size="small" plain type="danger" @click="voidRow(row as InboundFeeDetail)"><X :size="15" />作废</el-button>
<el-dropdown v-if="canDelete(row as InboundFeeDetail)">
<el-button link aria-label="更多费用操作"><MoreHorizontal :size="17" /></el-button>
<template #dropdown><el-dropdown-menu>
<el-dropdown-item @click="remove(row as InboundFeeDetail)"><Trash2 :size="15" />删除</el-dropdown-item>
</el-dropdown-menu></template>
</el-dropdown>
<div class="density-actions">
<el-button v-if="canEdit(row as InboundFeeDetail)" class="density-action" data-tone="neutral" @click="openEditor('edit', row as InboundFeeDetail)">编辑</el-button>
<el-button v-if="canReview(row as InboundFeeDetail)" class="density-action" data-tone="warning" @click="approve(row as InboundFeeDetail)">审核</el-button>
<el-button v-if="canReview(row as InboundFeeDetail)" class="density-action" data-tone="warning" @click="openReject(row as InboundFeeDetail)">驳回</el-button>
<el-button v-if="canAdjust(row as InboundFeeDetail)" class="density-action" data-tone="neutral" @click="openEditor('adjust', row as InboundFeeDetail)">调整</el-button>
<el-button v-if="canVoid(row as InboundFeeDetail)" class="density-action" data-tone="danger" @click="voidRow(row as InboundFeeDetail)">作废</el-button>
<el-button v-if="canDelete(row as InboundFeeDetail) && rowActionCount(row as InboundFeeDetail) <= 3" class="density-action" data-tone="danger" @click="remove(row as InboundFeeDetail)">删除</el-button>
<el-dropdown v-if="canDelete(row as InboundFeeDetail) && rowActionCount(row as InboundFeeDetail) > 3" trigger="click">
<el-button class="density-action" data-tone="neutral" :aria-label="(row.feeTypeNameSnapshot || '当前费用') + '的更多操作'" title="更多操作"><MoreHorizontal aria-hidden="true" :size="15" /></el-button>
<template #dropdown><el-dropdown-menu>
<el-dropdown-item @click="remove(row as InboundFeeDetail)">删除</el-dropdown-item>
</el-dropdown-menu></template>
</el-dropdown>
</div>
</template>
</el-table-column>
<el-table-column label="审核状态" width="120"><template #default="{ row }"><StatusBadge :status="reviewLabel(row.reviewStatus)" /></template></el-table-column>
@@ -316,7 +323,7 @@ onMounted(() => void load())
</div>
</template>
<AppDialog v-model="editorOpen" :title="editorMode === 'create' ? '新增费用' : editorMode === 'adjust' ? '调整已审核费用' : '编辑费用'" width="min(680px, calc(100vw - 28px))" :confirm-loading="saving" confirm-text="保存" @confirm="save">
<AppDialog class="admin-density-dialog" v-model="editorOpen" :title="editorMode === 'create' ? '新增费用' : editorMode === 'adjust' ? '调整已审核费用' : '编辑费用'" width="min(680px, calc(100vw - 28px))" :confirm-loading="saving" confirm-text="保存" @confirm="save">
<el-alert v-if="editorMode === 'adjust'" title="调整将生成新版本并重新进入待审核状态" type="warning" :closable="false" show-icon />
<el-alert v-if="selectedFeeItem?.feeCode === privateDeliveryFeeCode" title="私人仓尾程派送费仅适用于目的仓类型为“私人仓”的订单,最终资格由服务端校验。" type="info" :closable="false" show-icon />
<el-alert v-if="!canViewFeeItems" title="当前账号缺少费用项目查看权限,无法新增或修改费用" type="warning" :closable="false" show-icon />
@@ -324,7 +331,7 @@ onMounted(() => void load())
<el-alert v-if="feeItemsError" type="error" :closable="true" @close="feeItemsError = null"><template #title>{{ feeItemsError.message }}</template><ApiErrorMeta :details="feeItemsError" /></el-alert>
<el-alert v-if="suppliersError" type="error" :closable="true" @close="suppliersError = null"><template #title>{{ suppliersError.message }}</template><ApiErrorMeta :details="suppliersError" /></el-alert>
<el-alert v-if="actionError" type="error" :closable="true" @close="actionError = null"><template #title>{{ actionError.message }}</template><ApiErrorMeta :details="actionError" /></el-alert>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="fee-form">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="fee-form density-form">
<el-form-item label="应收/应付" prop="direction"><el-segmented v-model="form.direction" :options="directionOptions" /></el-form-item>
<el-form-item label="费用项目" prop="feeItemId"><el-select v-model="form.feeItemId" filterable :loading="feeItemsLoading" :disabled="!canViewFeeItems"><el-option v-for="item in applicableItems" :key="item.id" :label="item.feeTypeName" :value="item.id" /></el-select></el-form-item>
<el-form-item v-if="form.direction === 'PAYABLE'" label="供应商" required><el-select v-model="form.supplierId" filterable :loading="suppliersLoading" :disabled="!canViewSuppliers || !selectedFeeItem || !!suppliersError" placeholder="请先选择费用项目"><el-option v-for="supplier in suppliers" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" /></el-select></el-form-item>
@@ -346,26 +353,26 @@ onMounted(() => void load())
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注(选填)" /></el-form-item>
</el-form>
</AppDialog>
<AppDialog v-model="rejectOpen" title="驳回费用" :confirm-loading="saving" confirm-text="确认驳回" confirm-type="warning" @confirm="reject">
<AppDialog class="admin-density-dialog" v-model="rejectOpen" title="驳回费用" :confirm-loading="saving" confirm-text="确认驳回" confirm-type="warning" @confirm="reject">
<el-alert v-if="actionError" type="error" :closable="true" @close="actionError = null"><template #title>{{ actionError.message }}</template><ApiErrorMeta :details="actionError" /></el-alert>
<el-form label-position="top"><el-form-item label="驳回原因" required><el-input v-model="rejectReason" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入驳回原因" /></el-form-item></el-form>
<el-form class="density-form" label-position="top"><el-form-item label="驳回原因" required><el-input v-model="rejectReason" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入驳回原因" /></el-form-item></el-form>
</AppDialog>
</section>
</template>
<style scoped>
.fee-detail-manager { display: grid; min-width: 0; max-width: 100%; gap: 12px; }
.fee-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--el-text-color-secondary); font-size: 13px; }
.fee-toolbar > div { display: flex; flex-wrap: wrap; gap: 8px; }
.fee-detail-manager { display: grid; min-width: 0; max-width: 100%; gap: var(--admin-space-5); }
.fee-toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-5); color: var(--el-text-color-secondary); font-size: var(--admin-font-body); }
.fee-toolbar > div { display: flex; flex-wrap: wrap; gap: var(--admin-space-3); }
.fee-toolbar :deep(.el-button + .el-button) { margin-left: 0; }
.fee-toolbar .is-spinning { animation: fee-refresh-spin 0.8s linear infinite; }
.fee-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; margin-top: 14px; }
.fee-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--admin-space-6); margin-top: var(--admin-space-5); }
.fee-form :deep(.el-select), .fee-form :deep(.el-date-editor), .fee-form :deep(.el-input-number) { width: 100%; }
.fee-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
.voucher-field { grid-column: 1 / -1; }.voucher-control { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; width: 100%; }.voucher-picker { display: inline-flex; align-items: center; gap: 6px; padding: 7px 12px; border: 1px solid var(--cc-border); border-radius: 6px; color: var(--el-color-primary); cursor: pointer; }.voucher-picker.is-disabled { cursor: not-allowed; opacity: .6; }.voucher-picker input { position: absolute; width: 1px; height: 1px; opacity: 0; }.voucher-hint { color: var(--cc-muted); font-size: 12px; }.voucher-list { display: grid; width: 100%; gap: 6px; margin-top: 8px; }.voucher-list > span { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 9px; border-radius: 5px; background: var(--el-fill-color-light); }.voucher-list > span > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.voucher-list button { display: inline-flex; padding: 2px; border: 0; background: transparent; color: var(--el-color-danger); cursor: pointer; }
.voucher-field { grid-column: 1 / -1; }.voucher-control { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-4); width: 100%; }.voucher-picker { display: inline-flex; align-items: center; gap: var(--admin-space-2); padding: var(--admin-space-2) var(--admin-space-5); border: 1px solid var(--cc-border); border-radius: var(--admin-radius); color: var(--el-color-primary); cursor: pointer; }.voucher-picker.is-disabled { cursor: not-allowed; opacity: .6; }.voucher-picker input { position: absolute; width: 1px; height: 1px; opacity: 0; }.voucher-hint { color: var(--cc-muted); font-size: var(--admin-font-caption); }.voucher-list { display: grid; width: 100%; gap: var(--admin-space-2); margin-top: var(--admin-space-3); }.voucher-list > span { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-3); padding: var(--admin-space-2) var(--admin-space-3); border-radius: var(--admin-radius); background: var(--el-fill-color-light); }.voucher-list > span > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.voucher-list button { display: inline-flex; padding: 2px; border: 0; background: transparent; color: var(--el-color-danger); cursor: pointer; }
.fee-table-shell { min-width: 0; max-width: 100%; overflow: hidden; }
.fee-table-shell :deep(.el-table) { width: 100%; max-width: 100%; }
.private-delivery-tag { margin-left: 6px; vertical-align: middle; }
.private-delivery-tag { margin-left: var(--admin-space-2); vertical-align: middle; }
@keyframes fee-refresh-spin { to { transform: rotate(360deg); } }
@media (max-width: 720px) { .fee-toolbar { align-items: flex-start; flex-direction: column; } .fee-form { grid-template-columns: 1fr; } .fee-form :deep(.el-form-item:last-child) { grid-column: auto; } }
</style>