247 lines
29 KiB
Vue
247 lines
29 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminWorkbench.css'
|
||
import '@/styles/adminDensity.css'
|
||
import { computed, reactive, ref, watch } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { useAuthStore } from '@/core/auth/auth'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||
import { listOverseasPurchaseWarehouseOptions } from '../api/platformOverseasBusiness'
|
||
import { submitProcurementCommand, uploadProcurementFile, type ProcurementCommand, type ProcurementCommandBody } from '../api/procurementCommands'
|
||
import { commandKey, definitelyRejected, readPendingProcurement, type PendingProcurement } from '../procurementPending'
|
||
import { readCorrectionDraft, type ProcurementCorrectionDraft } from '../procurementCorrectionDraft'
|
||
import type { PlatformProcurement, ProcurementAction, ProcurementFile } from '../procurementTypes'
|
||
import type { OverseasPurchaseWarehouseOption } from '../types'
|
||
import ProcurementControlFields from './ProcurementControlFields.vue'
|
||
import { controlCommands, controlForm, buildControl } from '../procurementControlForm'
|
||
import ProcurementFinanceFields from './ProcurementFinanceFields.vue'
|
||
import { financeCommands, financeForm, buildFinance, feeRetryPermission } from '../procurementFinanceForm'
|
||
import ProcurementTransportFields from './ProcurementTransportFields.vue'
|
||
import { transportCommands, transportForm, buildTransport } from '../procurementTransportForm'
|
||
import ProcurementPhysicalFields from './ProcurementPhysicalFields.vue'
|
||
import { physicalCommands, physicalForm, buildPhysical, managementOptions, managementReason, receiptCorrectionReason, dispositionReason } from '../procurementPhysicalForm'
|
||
const props = defineProps<{ detail: PlatformProcurement; customerId: string }>()
|
||
const emit = defineEmits<{ changed: [] }>()
|
||
const auth = useAuthStore(); const access = useAccessPreviewStore()
|
||
const routes: Record<string, ProcurementCommand> = { REVIEW: 'review', PURCHASE: 'purchase', CONFIRM_PURCHASE: 'purchase/confirm', AUTHORIZE_BUDGET: 'budget-authorizations', PLAN_WAREHOUSE: 'planned-warehouse', CANCEL: 'cancel', RECEIVE_OVERSEAS: 'overseas-receipts', RECEIVE_DOMESTIC: 'domestic-receipts', FINISH_OVERSEAS: 'overseas-receipts/complete', FINISH_DOMESTIC: 'domestic-receipts/complete', PACK: 'packing', CONFIRM_PACKING: 'packing/confirm', MANAGE: 'dispositions', LABELS: 'labels', ESTIMATE: 'international-estimate', APPLY_LABELS: 'labels/apply', SHIP_OVERSEAS: 'overseas-shipment', SHIP_CUSTOMER: 'customer-shipment', REPACK: 'domestic-repack', CONFIRM_CHARGES: 'charges/confirm', FINANCE_ADJUST: 'fees/correct-target', RETRY_FEE: 'fees/retry', EXCEPTION_CLOSE: 'exception-close' }
|
||
const retryFees = computed(() => props.detail.fees.filter(fee => fee.active && fee.walletStatus.endsWith('_FAILED') && access.can(feeRetryPermission(fee))))
|
||
const actions = computed(() => [...props.detail.actions.filter(item => routes[item.key] && access.can(item.permission)), ...(retryFees.value.length ? [{ key: 'RETRY_FEE', label: '重试失败资金请求', permission: feeRetryPermission(retryFees.value[0]), allowed: true, reason: null }] : [])])
|
||
const secondaryKeys = new Set(['AUTHORIZE_BUDGET', 'PLAN_WAREHOUSE', 'MANAGE', 'FINANCE_ADJUST', 'CANCEL', 'EXCEPTION_CLOSE'])
|
||
const currentActions = computed(() => actions.value.filter(action => action.allowed && !secondaryKeys.has(action.key)))
|
||
const secondaryActions = computed(() => actions.value.filter(action => secondaryKeys.has(action.key)))
|
||
const unavailableActions = computed(() => actions.value.filter(action => !action.allowed && !secondaryKeys.has(action.key)))
|
||
const opened = ref(false); const active = ref<ProcurementAction | null>(null); const saving = ref(false); const uploading = ref(false)
|
||
const error = ref<ApiErrorDetails | null>(null); const storageError = ref(''); const pending = ref<PendingProcurement | null>(null)
|
||
const editingDraft = ref(false); const correctionDraft = ref<ProcurementCorrectionDraft | null>(null); const draftError = ref('')
|
||
const warehouses = ref<OverseasPurchaseWarehouseOption[]>([]); const warehouseLoading = ref(false); const warehouseError = ref('')
|
||
const extraFiles = ref<ProcurementFile[]>([])
|
||
const form = reactive({ decision: 'APPROVE' as 'APPROVE' | 'REJECT', warehouseId: '', reason: '', amount: '', currency: 'CNY' as 'CNY' | 'USD', purchasedAt: '', estimatedArrivalAt: '', voucherFileId: '' })
|
||
const control = ref(controlForm(props.detail))
|
||
const finance = ref(financeForm(props.detail))
|
||
const financialCommand = ref<ProcurementCommand>('fees/correct-target')
|
||
const transport = ref(transportForm(props.detail))
|
||
const physical = ref(physicalForm())
|
||
const manageCommand = ref<ProcurementCommand>('dispositions')
|
||
const key = computed(() => auth.user?.id ? `procurement-command:${auth.user.id}:${props.customerId}:${props.detail.customerView.order.orderNo}` : '')
|
||
const draftKey = computed(() => key.value ? `${key.value}:correction-draft` : '')
|
||
const canDraft = computed(() => !!key.value && access.can('platform:overseas-business:purchase:finance-adjust') && props.detail.fees.some(fee => !['INTERNATIONAL_ESTIMATE', 'ADJUSTMENT', 'REFUND'].includes(fee.purpose)))
|
||
const commandPermissions: Record<ProcurementCommand, string> = { review: 'review', purchase: 'buy', 'purchase/confirm': 'buy', 'budget-authorizations': 'review', 'planned-warehouse': 'review', cancel: 'cancel', 'overseas-receipts': 'receive-overseas', 'domestic-receipts': 'receive-domestic', 'overseas-receipts/complete': 'receive-overseas', 'domestic-receipts/complete': 'receive-domestic', packing: 'pack', 'packing/confirm': 'pack', 'packing/withdraw': 'manage', dispositions: 'manage', 'receipts/correct': 'manage', 'dispositions/revoke': 'manage', labels: 'label', 'international-estimate': 'estimate', 'labels/apply': 'apply-label', 'overseas-shipment': 'ship-overseas', 'customer-shipment': 'ship-customer', 'domestic-repack': 'receive-domestic', 'charges/confirm': 'charge', 'fees/retry': 'charge', 'fees/terminate': 'finance-adjust', 'fees/release-estimate': 'finance-adjust', 'fees/correct-target': 'finance-adjust', 'fees/additional': 'finance-adjust', 'fees/replace-failed': 'finance-adjust', 'fees/refund': 'finance-adjust', 'control-notes': 'manage', 'control-notes/resolve': 'manage', 'actual-warehouse/correct': 'manage', 'exception-close': 'manage' }
|
||
const command = computed(() => editingDraft.value ? 'fees/correct-target' : pending.value?.command ?? (active.value?.key === 'MANAGE' ? manageCommand.value : active.value?.key === 'FINANCE_ADJUST' ? financialCommand.value : active.value ? routes[active.value.key] : undefined))
|
||
const manageOptions = computed(() => managementOptions.map(option => ({ ...option, reason: managementReason(option.value, props.detail) })))
|
||
const managementBlock = computed(() => {
|
||
if (active.value?.key !== 'MANAGE' || !command.value || pending.value) return null
|
||
const reason = managementReason(command.value, props.detail)
|
||
if (reason) return reason
|
||
if (command.value === 'dispositions') return dispositionReason(props.detail, physical.value.leg)
|
||
const row = command.value === 'receipts/correct'
|
||
? props.detail.physical.receipts.find(item => item.id === physical.value.receiptId)
|
||
: command.value === 'dispositions/revoke' ? props.detail.physical.dispositions.find(item => item.id === physical.value.dispositionId) : null
|
||
return row ? receiptCorrectionReason(props.detail, row.leg) : null
|
||
})
|
||
const isControl = computed(() => !!command.value && controlCommands.has(command.value))
|
||
const isFinance = computed(() => !!command.value && financeCommands.has(command.value))
|
||
const isTransport = computed(() => !!command.value && transportCommands.has(command.value))
|
||
const isPhysical = computed(() => !!command.value && physicalCommands.has(command.value))
|
||
const needsEvidence = computed(() => ['dispositions', 'receipts/correct', 'dispositions/revoke', 'domestic-repack', 'fees/correct-target', 'fees/refund', 'fees/replace-failed', 'control-notes', 'control-notes/resolve', 'actual-warehouse/correct'].includes(command.value || ''))
|
||
const needsWarehouse = computed(() => command.value === 'actual-warehouse/correct' || command.value === 'overseas-receipts' || (command.value === 'receipts/correct' && physical.value.leg === 'OVERSEAS') || command.value === 'planned-warehouse' || (command.value === 'review' && form.decision === 'APPROVE'))
|
||
const needsAmount = computed(() => ['purchase', 'budget-authorizations'].includes(command.value || ''))
|
||
const fileCategory = computed(() => isControl.value ? 'EXCEPTION' : isFinance.value ? 'VOUCHER' : command.value === 'labels' ? 'LABEL' : command.value === 'domestic-repack' ? 'RECEIPT' : needsEvidence.value ? 'EXCEPTION' : command.value === 'purchase' ? 'PURCHASE' : 'BUDGET')
|
||
const allFiles = computed(() => [...props.detail.files, ...extraFiles.value].filter((file, index, all) => all.findIndex(other => other.fileId === file.fileId) === index))
|
||
const files = computed(() => allFiles.value.filter(file => file.category === fileCategory.value))
|
||
const { captureBaseline } = useUnsavedWorkRegistration({ key: 'procurement-purchase-action', label: computed(() => active.value?.label || '采购节点操作'), isOpen: opened, model: reactive({ purchase: form, physical, transport, finance, control }), requiredPermission: computed(() => active.value?.permission ?? null) })
|
||
watch(key, () => {
|
||
opened.value = false; pending.value = null; storageError.value = ''; extraFiles.value = []; active.value = null; editingDraft.value = false; correctionDraft.value = null; draftError.value = ''
|
||
if (!key.value) return
|
||
try { pending.value = readPendingProcurement(sessionStorage.getItem(key.value)) }
|
||
catch { storageError.value = '上次操作记录无法读取,请先联系管理员核对该订单的处理结果。' }
|
||
try { correctionDraft.value = readCorrectionDraft(sessionStorage.getItem(draftKey.value)) }
|
||
catch { draftError.value = '本浏览器的费用更正草稿无法读取,请核对后重新填写。' }
|
||
}, { immediate: true })
|
||
async function loadWarehouses() {
|
||
if (warehouseLoading.value) return
|
||
warehouseLoading.value = true; warehouseError.value = ''
|
||
try { warehouses.value = await listOverseasPurchaseWarehouseOptions() }
|
||
catch { warehouseError.value = '采购仓候选加载失败,请重试'; warehouses.value = [] }
|
||
finally { warehouseLoading.value = false }
|
||
}
|
||
function open(action: ProcurementAction) {
|
||
if (!action.allowed || pending.value || storageError.value || saving.value) return
|
||
editingDraft.value = false; active.value = action; error.value = null; manageCommand.value = manageOptions.value.find(option => !option.reason)?.value || 'control-notes'; physical.value = physicalForm(props.detail)
|
||
control.value = controlForm(props.detail)
|
||
finance.value = financeForm(props.detail); financialCommand.value = 'fees/correct-target'
|
||
transport.value = transportForm(props.detail, ['SHIP_CUSTOMER', 'REPACK'].includes(action.key) ? 'DOMESTIC' : action.key === 'LABELS' ? undefined : 'OVERSEAS')
|
||
if (action.key.includes('DOMESTIC')) physical.value.leg = 'DOMESTIC'
|
||
Object.assign(form, { decision: 'APPROVE', warehouseId: props.detail.purchaseFacts.actual_warehouse_id || '', reason: '', amount: '', currency: props.detail.customerView.budget.currency === 'USD' ? 'USD' : 'CNY', purchasedAt: '', estimatedArrivalAt: '', voucherFileId: '' })
|
||
opened.value = true; captureBaseline()
|
||
if (needsWarehouse.value) void loadWarehouses()
|
||
}
|
||
function openCorrectionDraft() {
|
||
if (!canDraft.value || saving.value || uploading.value) return
|
||
editingDraft.value = true
|
||
active.value = { key: 'CORRECTION_DRAFT', label: '费用更正草稿', permission: 'platform:overseas-business:purchase:finance-adjust', allowed: true, reason: null }
|
||
finance.value = financeForm(props.detail)
|
||
const draft = correctionDraft.value
|
||
finance.value.feeId = draft?.feeId || ''; finance.value.targetOriginalAmount = draft?.targetOriginalAmount || ''
|
||
form.reason = draft?.reason || ''; form.voucherFileId = draft?.voucherFileId || ''
|
||
error.value = null; opened.value = true; captureBaseline()
|
||
}
|
||
function saveCorrectionDraft() {
|
||
if (!editingDraft.value || !canDraft.value || saving.value || uploading.value) return
|
||
try {
|
||
const draft = { feeId: finance.value.feeId, targetOriginalAmount: finance.value.targetOriginalAmount, reason: form.reason, voucherFileId: form.voucherFileId, orderVersion: props.detail.customerView.order.version }
|
||
sessionStorage.setItem(draftKey.value, JSON.stringify(draft)); correctionDraft.value = draft; draftError.value = ''; captureBaseline()
|
||
ElMessage.success('更正草稿已保存在本浏览器标签页,尚未确认费用或发起资金请求')
|
||
} catch { draftError.value = '草稿保存失败,请保留当前输入并重试' }
|
||
}
|
||
function useCorrectionDraft() {
|
||
if (!editingDraft.value || pending.value || storageError.value || !canDraft.value) return
|
||
const action = actions.value.find(item => item.key === 'FINANCE_ADJUST' && item.allowed)
|
||
if (!action) { error.value = { message: '当前业务状态不允许财务确认,请刷新订单后核对' }; return }
|
||
const fee = props.detail.fees.find(row => row.id === finance.value.feeId)
|
||
active.value = action; financialCommand.value = fee && !fee.active && ['TERMINATED', 'RELEASED'].includes(fee.walletStatus) ? 'fees/replace-failed' : 'fees/correct-target'; editingDraft.value = false
|
||
}
|
||
async function retryPending() {
|
||
if (editingDraft.value) { saveCorrectionDraft(); if (draftError.value) return }
|
||
editingDraft.value = false; opened.value = false
|
||
await submit()
|
||
}
|
||
watch(financialCommand, () => { if (editingDraft.value) return; finance.value.feeId = ''; finance.value.targetOriginalAmount = ''; finance.value.refundAmount = ''; form.voucherFileId = ''; error.value = null }, { flush: 'sync' })
|
||
watch(needsWarehouse, value => { if (value && opened.value) void loadWarehouses() })
|
||
function build(): ProcurementCommandBody {
|
||
const order = props.detail.customerView.order
|
||
if (!form.reason.trim()) throw new Error('请填写操作说明')
|
||
const base = { version: order.version, idempotencyKey: commandKey(), reason: form.reason.trim() }
|
||
if (needsWarehouse.value && !warehouses.value.some(row => row.warehouseId === form.warehouseId)) throw new Error('请选择有效采购仓')
|
||
if (isControl.value && command.value) return buildControl(command.value, control.value, props.detail, allFiles.value, base, form.warehouseId, form.voucherFileId)
|
||
if (isFinance.value && command.value) return buildFinance(command.value, finance.value, props.detail, allFiles.value, base, form.voucherFileId)
|
||
if (isTransport.value && command.value) return buildTransport(command.value, transport.value, props.detail, allFiles.value, base, form.voucherFileId)
|
||
if (isPhysical.value && command.value) {
|
||
if (needsEvidence.value && !files.value.some(file => file.fileId === form.voucherFileId)) throw new Error('请选择本单异常处置凭证')
|
||
return buildPhysical(command.value, physical.value, props.detail, base, form.warehouseId, form.voucherFileId)
|
||
}
|
||
if (command.value === 'review') return { version: base.version, idempotencyKey: base.idempotencyKey, decision: form.decision, remark: base.reason, ...(form.decision === 'APPROVE' ? { purchaseWarehouseId: form.warehouseId } : {}) }
|
||
if (command.value === 'planned-warehouse') return { ...base, warehouseId: form.warehouseId }
|
||
if (needsAmount.value) {
|
||
if (!/^\d{1,16}(?:\.\d{1,2})?$/.test(form.amount.trim())) throw new Error('请明确填写非负金额,最多两位小数;免费样品请填 0')
|
||
if (!files.value.some(file => file.fileId === form.voucherFileId)) throw new Error('请选择本单对应用途的凭证')
|
||
const amount = { ...base, amount: form.amount.trim(), currency: form.currency, voucherFileId: form.voucherFileId }
|
||
if (command.value === 'budget-authorizations') return amount
|
||
if (!form.purchasedAt || !form.estimatedArrivalAt) throw new Error('请填写真实购买时间及预计到仓时间')
|
||
return { ...amount, productName: order.productName, specification: order.productSpecification ?? '', quantity: order.purchaseQuantity, purchasedAt: form.purchasedAt, estimatedArrivalAt: form.estimatedArrivalAt }
|
||
}
|
||
return base
|
||
}
|
||
async function submit() {
|
||
if (editingDraft.value) { saveCorrectionDraft(); return }
|
||
if (saving.value || uploading.value || storageError.value || !key.value) return
|
||
const resuming = !!pending.value
|
||
const scope = key.value; const orderNo = props.detail.customerView.order.orderNo; const customer = props.customerId
|
||
if (!pending.value) {
|
||
if (!active.value?.allowed || !access.can(active.value.permission) || !command.value) return
|
||
try {
|
||
if (managementBlock.value) throw new Error(managementBlock.value)
|
||
const request = { command: command.value, payload: build() }
|
||
const serialized = JSON.stringify(request); readPendingProcurement(serialized)
|
||
sessionStorage.setItem(scope, serialized); pending.value = request
|
||
} catch (issue) { error.value = { message: issue instanceof Error ? issue.message : '提交信息保存失败,请重试' }; return }
|
||
}
|
||
const retryFee = pending.value.command === 'fees/retry' ? props.detail.fees.find(fee => fee.id === pending.value?.payload.feeId) : null
|
||
const requiredPermission = pending.value.command === 'fees/retry' ? (retryFee ? feeRetryPermission(retryFee) : '') : `platform:overseas-business:purchase:${commandPermissions[pending.value.command]}`
|
||
if (!requiredPermission || !access.can(requiredPermission)) { error.value = { message: '当前账号无此节点操作权限' }; return }
|
||
saving.value = true; error.value = null
|
||
const confirmsCorrection = !resuming && ['fees/correct-target', 'fees/replace-failed'].includes(pending.value.command)
|
||
try {
|
||
await submitProcurementCommand(orderNo, customer, pending.value.command, pending.value.payload)
|
||
sessionStorage.removeItem(scope)
|
||
if (key.value !== scope) return
|
||
if (confirmsCorrection) { sessionStorage.removeItem(draftKey.value); correctionDraft.value = null }
|
||
pending.value = null; opened.value = false; captureBaseline(); ElMessage.success('操作已保存,请核对最新业务及资金结果'); emit('changed')
|
||
} catch (issue) {
|
||
if (key.value !== scope) return
|
||
if (!resuming && definitelyRejected(issue)) { sessionStorage.removeItem(scope); pending.value = null }
|
||
error.value = getApiErrorDetails(issue, '结果尚未确认,请使用原请求重试')
|
||
} finally { saving.value = false }
|
||
}
|
||
async function upload(event: Event) {
|
||
const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = ''
|
||
if (!file || uploading.value || (pending.value && !editingDraft.value)) return
|
||
if (file.size > 10 * 1024 * 1024 || !['application/pdf', 'image/jpeg', 'image/png'].includes(file.type)) { ElMessage.warning('请上传不超过 10MB 的 PDF、JPG 或 PNG'); return }
|
||
const scope = key.value; uploading.value = true
|
||
try {
|
||
const saved = await uploadProcurementFile(props.detail.customerView.order.orderNo, props.customerId, fileCategory.value, file)
|
||
if (scope !== key.value) return
|
||
extraFiles.value.push(saved); form.voucherFileId = saved.fileId
|
||
} catch (issue) { if (scope === key.value) error.value = getApiErrorDetails(issue, '凭证上传失败') }
|
||
finally { uploading.value = false }
|
||
}
|
||
</script>
|
||
<template>
|
||
<section class="admin-density" aria-label="采购履约节点操作">
|
||
<div class="density-actions purchase-actions" data-testid="current-procurement-actions"><el-tooltip v-for="action in currentActions" :key="action.key" :content="action.reason || action.label"><span><el-button class="density-action" :disabled="!!pending || !!storageError || saving" @click="open(action)">{{ action.label }}</el-button></span></el-tooltip></div>
|
||
<details v-if="secondaryActions.length || canDraft" class="purchase-secondary-actions">
|
||
<summary>更正、财务及异常处理</summary>
|
||
<div class="density-actions purchase-actions"><el-tooltip v-for="action in secondaryActions" :key="action.key" :content="action.reason || action.label"><span><el-button class="density-action" :disabled="!action.allowed || !!pending || !!storageError || saving" @click="open(action)">{{ action.label }}</el-button></span></el-tooltip>
|
||
<el-button v-if="canDraft" :disabled="saving || uploading" @click="openCorrectionDraft">{{ correctionDraft ? '恢复费用更正草稿' : '准备费用更正草稿' }}</el-button>
|
||
</div>
|
||
</details>
|
||
<details v-if="unavailableActions.length" class="purchase-secondary-actions">
|
||
<summary>其他履约节点及不可操作原因</summary>
|
||
<dl><template v-for="action in unavailableActions" :key="action.key"><dt>{{ action.label }}</dt><dd>{{ action.reason || '当前不可操作' }}</dd></template></dl>
|
||
</details>
|
||
<el-alert v-if="draftError" :title="draftError" type="error" :closable="false" /><el-alert v-if="storageError" :title="storageError" type="error" :closable="false" />
|
||
<el-alert v-if="pending" title="上次提交结果尚未确认,重试将使用原始内容与原请求编号。" type="warning" :closable="false"><p>原操作:{{ pending.command === 'review' ? (pending.payload.decision === 'APPROVE' ? '审核通过' : '审核拒绝') : actions.find(action => routes[action.key] === pending?.command)?.label || '采购节点操作' }} · {{ pending.payload.remark || pending.payload.reason }}</p><p v-if="pending.payload.amount !== undefined">原提交金额:{{ pending.payload.amount }} {{ pending.payload.currency || (['international-estimate', 'fees/refund'].includes(pending.command) ? 'CNY' : '') }}</p><el-button :loading="saving" @click="retryPending">确认上次提交结果</el-button></el-alert>
|
||
<ApiErrorMeta v-if="error && !opened" :details="error" />
|
||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="opened" :title="active?.label || '采购节点操作'" :width="['packing', 'labels', 'charges/confirm', 'exception-close'].includes(command || '') ? 'min(1120px, calc(100vw - 28px))' : 'min(760px, calc(100vw - 28px))'" :confirm-loading="saving || uploading" :confirm-disabled="!!managementBlock || uploading || warehouseLoading || (!editingDraft && !!storageError) || !key" :confirm-text="editingDraft ? '保存本地草稿' : pending ? '重试原请求' : '确认提交'" @confirm="submit">
|
||
<template #default><el-alert v-if="editingDraft" title="草稿仅保存在当前浏览器标签页,关闭标签页后可能丢失;不代表已确认费用。原请求未决时只能保存,结果明确后请刷新订单再核对确认。" type="info" :closable="false" /><el-alert v-if="command === 'purchase'" title="记录已实际购买的商品和供应商实付总额。资金未完成时,真实购买事实仍保留。" type="info" :closable="false" />
|
||
<el-alert v-if="managementBlock" :title="managementBlock" type="warning" :closable="false" />
|
||
<el-form class="density-form" label-position="top" :disabled="saving || uploading || (!!pending && !editingDraft)">
|
||
<el-form-item v-if="active?.key === 'MANAGE'" label="实物更正操作"><el-select v-model="manageCommand" placeholder="选择处置或更正操作"><el-option v-for="option in manageOptions" :key="option.value" :value="option.value" :disabled="!!option.reason" :label="option.reason ? `${option.label}(${option.reason})` : option.label" /></el-select></el-form-item>
|
||
<el-form-item v-if="active?.key === 'FINANCE_ADJUST'" label="财务处理方式"><el-select v-model="financialCommand" placeholder="选择财务处理方式"><el-option value="fees/additional" label="登记独立新发生费用" /><el-option value="fees/correct-target" label="更正原费用链目标总额" /><el-option value="fees/refund" label="关联原扣款退款" /><el-option value="fees/terminate" label="终止明确失败的费用版本" /><el-option value="fees/replace-failed" label="重新核定已终止的未扣款费用" /><el-option value="fees/release-estimate" label="释放有效预估冻结" /></el-select></el-form-item>
|
||
<ProcurementControlFields v-if="isControl && command" v-model="control" :command="command" :detail="detail" :files="allFiles" />
|
||
<template v-if="editingDraft"><el-form-item label="原费用"><el-select v-model="finance.feeId" placeholder="选择本单原实际费用"><el-option v-for="fee in detail.fees.filter(row => !['INTERNATIONAL_ESTIMATE', 'ADJUSTMENT', 'REFUND'].includes(row.purpose))" :key="fee.id" :value="fee.id" :label="`${fee.feeNo} · ${fee.feeItemName}`" /></el-select></el-form-item><el-form-item label="正确原币总额"><el-input v-model="finance.targetOriginalAmount" placeholder="填写原费用链正确总额,最多两位小数" /></el-form-item></template>
|
||
<ProcurementFinanceFields v-else-if="isFinance && command" v-model="finance" :command="command" :detail="detail" :files="allFiles" :can="access.can" />
|
||
<ProcurementTransportFields v-if="isTransport && command" v-model="transport" :command="command" :detail="detail" :files="allFiles" />
|
||
<ProcurementPhysicalFields v-if="isPhysical && command" v-model="physical" :command="command" :detail="detail" />
|
||
<el-form-item v-if="command === 'review'" label="审核决定"><el-radio-group v-model="form.decision"><el-radio value="APPROVE">通过</el-radio><el-radio value="REJECT">拒绝</el-radio></el-radio-group></el-form-item>
|
||
<el-form-item v-if="needsWarehouse" :label="command === 'actual-warehouse/correct' ? '更正后的实际仓' : isPhysical ? '实际海外收货仓' : '采购仓'" required><el-select v-model="form.warehouseId" :disabled="isPhysical && !!detail.purchaseFacts.actual_warehouse_id" filterable :loading="warehouseLoading" placeholder="选择有效海外收货仓"><el-option v-for="item in warehouses" :key="item.warehouseId" :value="item.warehouseId" :label="`${item.warehouseName} · ${item.warehouseCode}`" /></el-select><span v-if="warehouseError">{{ warehouseError }}<el-button link @click="loadWarehouses">重试</el-button></span></el-form-item>
|
||
<div v-if="needsAmount" class="purchase-form-grid"><el-form-item :label="command === 'purchase' ? '供应商实付总额' : '客户授权预算总额'" required><el-input v-model="form.amount" placeholder="明确填写金额,包括零元" /></el-form-item><el-form-item label="原币" required><el-select v-model="form.currency"><el-option value="CNY" label="人民币 CNY" /><el-option value="USD" label="美元 USD" /></el-select></el-form-item></div>
|
||
<div v-if="command === 'purchase'" class="purchase-form-grid"><el-form-item label="真实购买时间" required><el-date-picker v-model="form.purchasedAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item><el-form-item label="预计到海外仓时间" required><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item></div>
|
||
<el-form-item v-if="command === 'exception-close'" label="上传异常处置或客户确认凭证"><input aria-label="上传异常关闭凭证" type="file" accept="application/pdf,image/jpeg,image/png" :disabled="uploading || (!!pending && !editingDraft) || saving" @change="upload" /></el-form-item>
|
||
<el-form-item v-if="['charges/confirm', 'fees/additional'].includes(command || '')" label="上传费用凭证"><input aria-label="上传费用凭证" type="file" accept="application/pdf,image/jpeg,image/png" :disabled="uploading || (!!pending && !editingDraft) || saving" @change="upload" /></el-form-item>
|
||
<el-form-item v-if="command === 'labels'" label="上传本单面单"><input aria-label="上传快递面单" type="file" accept="application/pdf,image/jpeg,image/png" :disabled="uploading || (!!pending && !editingDraft) || saving" @change="upload" /></el-form-item>
|
||
<el-form-item v-if="needsAmount || needsEvidence" label="本单凭证" required><el-select v-model="form.voucherFileId" placeholder="选择已上传凭证"><el-option v-for="file in files" :key="file.fileId" :value="file.fileId" :label="file.fileName" /></el-select><input aria-label="上传本单凭证" type="file" accept="application/pdf,image/jpeg,image/png" :disabled="uploading || (!!pending && !editingDraft) || saving" @change="upload" /></el-form-item>
|
||
<el-form-item label="操作说明" required><el-input v-model="form.reason" type="textarea" :rows="3" maxlength="1000" show-word-limit /></el-form-item>
|
||
</el-form><el-alert v-if="error" :title="error.message" type="error" :closable="false" /><ApiErrorMeta v-if="error" :details="error" /></template>
|
||
<template #footer><div class="density-actions density-actions--footer"><el-button :disabled="saving || uploading" @click="opened = false">关闭</el-button><el-button v-if="editingDraft" :disabled="!!pending || !!storageError || saving || uploading" @click="useCorrectionDraft">核对并继续确认</el-button><el-button type="primary" :loading="saving" :disabled="uploading || warehouseLoading || (!editingDraft && !!storageError) || !key" @click="submit">{{ editingDraft ? '保存本地草稿' : pending ? '重试原请求' : '确认提交' }}</el-button></div></template>
|
||
</AppDialog>
|
||
</section>
|
||
</template>
|
||
<style scoped>
|
||
.purchase-actions { flex-wrap: wrap; }
|
||
.purchase-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--admin-space-4); }
|
||
.purchase-form-grid :deep(.el-date-editor), :deep(.el-select) { width: 100%; }
|
||
</style>
|