费用优化

This commit is contained in:
2026-10-26 13:43:08 +08:00
parent 090431b409
commit 18a78e5508
74 changed files with 3339 additions and 116 deletions
@@ -0,0 +1,61 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import AppDialog from '@/shared/components/AppDialog.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { actOnOverseasFee, type OverseasFee } from '../api/overseasFees'
import { adjustStorageRentFee } from '../api/storageRent'
import { feePermissions, feeMoney } from '../overseasFeePresentation'
import StorageRentFeeDetails from './StorageRentFeeDetails.vue'
const props = defineProps<{ fee: OverseasFee | null; action: string; readonly?: boolean }>()
const open = defineModel<boolean>({ required: true })
const emit = defineEmits<{ saved: [] }>()
const access = useAccessPreviewStore()
const amount = ref(0), reason = ref(''), busy = ref(false), error = ref<ApiErrorDetails | null>(null)
const key = ref(''), fingerprint = ref('')
const adjusting = computed(() => props.action === 'ADJUST_RENT')
const allowed = (action: string) => !props.readonly && props.fee?.allowedActions.includes(action) && access.can(action === 'ADJUST_RENT' ? feePermissions.edit : feePermissions.review)
function reset() { amount.value = props.fee?.originalAmount ?? 0; reason.value = ''; error.value = null; key.value = ''; fingerprint.value = '' }
watch(open, value => { if (value) reset() }, { immediate: true })
async function submit(decision?: 'APPROVE' | 'REJECT') {
const fee = props.fee, action = adjusting.value ? 'ADJUST_RENT' : decision
if (!fee || !action || !allowed(action) || busy.value) return
if ((adjusting.value || decision === 'REJECT') && !reason.value.trim()) { ElMessage.warning('请填写说明'); return }
if (adjusting.value && (!Number.isFinite(amount.value) || amount.value < 0)) { ElMessage.warning('请输入有效金额'); return }
const body = adjusting.value ? { expectedVersion: fee.sourceVersion, amount: amount.value, reason: reason.value.trim() }
: { decision, reason: reason.value.trim() || undefined, expectedVersion: fee.sourceVersion }
const next = JSON.stringify([fee.sourceFeeId, body])
if (fingerprint.value !== next) { fingerprint.value = next; key.value = crypto.randomUUID() }
busy.value = true; error.value = null
try {
if (adjusting.value) await adjustStorageRentFee(fee.sourceFeeId, { expectedVersion: fee.sourceVersion, amount: amount.value, reason: reason.value.trim(), idempotencyKey: key.value })
else await actOnOverseasFee(fee.sourceFeeId, decision!, { ...body, idempotencyKey: key.value })
open.value = false; ElMessage.success(adjusting.value ? '仓租已调整' : decision === 'APPROVE' ? '审核通过,正在处理扣款' : '仓租已驳回'); emit('saved')
} catch (failure) { error.value = getApiErrorDetails(failure, '仓租操作失败') }
finally { busy.value = false }
}
</script>
<template>
<AppDialog v-model="open" append-to-body :title="adjusting ? '调整仓租' : '审核仓租'" width="760px" class="admin-density-dialog admin-workbench is-dialog" :show-confirm="false" :confirm-loading="busy" lock-while-submitting>
<template v-if="fee">
<p>{{ fee.businessNo }} · 仓租 {{ feeMoney(fee.originalAmount, fee.originalCurrency) }}</p>
<StorageRentFeeDetails :fee="fee" />
<p v-if="fee.feeRemark">费用说明:{{ fee.feeRemark }}</p>
<el-form label-position="top" :disabled="busy" class="density-form">
<el-form-item v-if="adjusting" :label="`仓租金额(${fee.originalCurrency})`" required><el-input-number v-model="amount" :precision="2" :min="0" :max="999999999999.99" /></el-form-item>
<el-form-item :label="adjusting ? '调整说明' : '审核说明(驳回必填)'" :required="adjusting"><el-input v-model="reason" type="textarea" :maxlength="500" show-word-limit /></el-form-item>
</el-form>
<el-alert v-if="error" :title="error.message" type="error" :closable="false"><ApiErrorMeta :details="error" /></el-alert>
</template>
<template #footer>
<el-button :disabled="busy" @click="open = false">关闭</el-button>
<template v-if="adjusting"><el-button v-if="allowed('ADJUST_RENT')" type="primary" :loading="busy" :disabled="!reason.trim()" @click="submit()">保存调整</el-button></template>
<template v-else>
<el-button v-if="allowed('REJECT')" type="danger" :disabled="busy || !reason.trim()" @click="submit('REJECT')">驳回</el-button>
<el-button v-if="allowed('APPROVE')" type="primary" :loading="busy" @click="submit('APPROVE')">审核通过</el-button>
</template>
</template>
</AppDialog>
</template>