feat(fleet): connect operations quotes costs and handover workspaces
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import FleetQuotePartySelect from './FleetQuotePartySelect.vue'
|
||||
import { createFleetQuote, updateFleetQuote, type FleetQuote, type FleetQuoteDraft, type FleetQuoteKind } from '../api/fleetQuotes'
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
const props = defineProps<{ quote: FleetQuote | null; readonly?: boolean; kind: FleetQuoteKind }>()
|
||||
const emit = defineEmits<{ saved: [] }>()
|
||||
type QuoteForm = Omit<FleetQuoteDraft, 'distanceMiles'> & { distanceMiles: number | null }
|
||||
const empty = (): QuoteForm => ({ kind: props.kind, partyId: '', originCity: '', destinationCode: '', distanceMiles: null, currency: 'USD', selfPickupPrice: null, fullDeliveryPrice: null, looseDeliveryPrice: null, validFrom: new Date().toISOString().slice(0, 10), validTo: '', remark: null })
|
||||
const form = reactive<QuoteForm>(empty()), saving = ref(false), error = ref<ApiErrorDetails | null>(null)
|
||||
let pending: { idempotencyKey: string; details: FleetQuoteDraft } | null = null
|
||||
const uncertain = ref(false)
|
||||
watch([open, () => props.quote?.quoteNo], () => {
|
||||
if (!open.value) return
|
||||
uncertain.value = !props.quote && pending !== null
|
||||
Object.assign(form, props.quote?.details || pending?.details || empty()); error.value = null
|
||||
})
|
||||
async function save() {
|
||||
if (props.readonly || saving.value) return
|
||||
const editing = props.quote
|
||||
saving.value = true; error.value = null
|
||||
try {
|
||||
if (!form.partyId || !form.originCity.trim() || !form.destinationCode.trim() || !form.validFrom || !form.validTo) throw new Error('请选择往来单位并填写路线和有效期')
|
||||
if (form.distanceMiles == null || !Number.isFinite(form.distanceMiles)) throw new Error('请填写有效的预估距离')
|
||||
if (form.selfPickupPrice == null && form.fullDeliveryPrice == null && form.looseDeliveryPrice == null) throw new Error('至少填写一种价格,零表示明确零价')
|
||||
const details: FleetQuoteDraft = { ...form, distanceMiles: form.distanceMiles }
|
||||
if (editing) await updateFleetQuote(editing, details)
|
||||
else { pending ??= { idempotencyKey: crypto.randomUUID(), details }; await createFleetQuote(pending.idempotencyKey, pending.details) }
|
||||
if (!editing) pending = null
|
||||
uncertain.value = false; open.value = false; emit('saved')
|
||||
} catch (e) { error.value = getApiErrorDetails(e, '报价保存失败'); if (!editing && e instanceof ApiResponseError && ([400, 409, 422].includes(e.status || 0) || ['400', '409', '422', 'VALIDATION_FAILED', 'CONFLICT'].includes(e.code))) pending = null; uncertain.value = !editing && pending !== null }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
</script>
|
||||
<template><AppDialog v-model="open" :title="readonly ? '报价详情' : quote ? '编辑报价' : '新增报价'" width="780px" class="admin-density-dialog" :close-on-click-modal="false">
|
||||
<el-alert v-if="error" :title="error.message" type="error" :closable="false"><ApiErrorMeta :details="error" /></el-alert>
|
||||
<p v-if="uncertain">保存结果尚未确认,重试将发送原请求和原内容。</p>
|
||||
<el-form label-position="top" class="density-form quote-form" :disabled="readonly || saving || uncertain">
|
||||
<el-form-item label="用途"><el-input :model-value="form.kind === 'PURCHASE' ? '车队采购报价' : '外接销售报价'" disabled /></el-form-item>
|
||||
<el-form-item :label="form.kind === 'PURCHASE' ? '海外车队供应商' : '委托客户'" required><FleetQuotePartySelect v-model="form.partyId" :kind="form.kind" :party-name="quote?.partyName" :disabled="readonly || saving || uncertain" /></el-form-item>
|
||||
<el-form-item label="发货城市" required><el-input v-model="form.originCity" maxlength="128" placeholder="请输入发货城市" /></el-form-item>
|
||||
<el-form-item label="目的仓码" required><el-input v-model="form.destinationCode" maxlength="64" placeholder="请输入目的仓码,如 ONT8" /></el-form-item>
|
||||
<el-form-item label="预估距离(mile)" required><el-input-number :model-value="form.distanceMiles" @update:model-value="form.distanceMiles = $event ?? null" :min="0" :precision="1" placeholder="请输入预估英里数" /></el-form-item>
|
||||
<el-form-item label="币种"><el-select v-model="form.currency"><el-option label="USD" value="USD" /><el-option label="CNY" value="CNY" /></el-select></el-form-item>
|
||||
<el-form-item label="整车自提 / 每单"><el-input-number :model-value="form.selfPickupPrice" @update:model-value="form.selfPickupPrice = $event ?? null" :min="0" :precision="2" placeholder="选填,留空表示未报价" /></el-form-item>
|
||||
<el-form-item label="整车派送 / 每单"><el-input-number :model-value="form.fullDeliveryPrice" @update:model-value="form.fullDeliveryPrice = $event ?? null" :min="0" :precision="2" placeholder="选填,留空表示未报价" /></el-form-item>
|
||||
<el-form-item label="散板派送 / 每托"><el-input-number :model-value="form.looseDeliveryPrice" @update:model-value="form.looseDeliveryPrice = $event ?? null" :min="0" :precision="2" placeholder="选填,留空表示未报价" /></el-form-item>
|
||||
<el-form-item label="生效日" required><el-date-picker v-model="form.validFrom" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="失效日(含当天)" required><el-date-picker v-model="form.validTo" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="备注" class="full"><el-input :model-value="form.remark || ''" @update:model-value="form.remark = $event || null" type="textarea" maxlength="1000" placeholder="选填,补充报价说明" /></el-form-item>
|
||||
</el-form><p>留空表示未报价;报价变更不自动覆盖已确认承运价格,也不生成应收或应付。</p>
|
||||
<template #footer><el-button @click="open = false">关闭</el-button><el-button v-if="!readonly" type="primary" :loading="saving" @click="save">{{ uncertain ? '按原请求重试' : '保存报价' }}</el-button></template>
|
||||
</AppDialog></template>
|
||||
<style scoped>.quote-form { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:0 var(--admin-space-4); }.full { grid-column:1/-1; }</style>
|
||||
Reference in New Issue
Block a user