112 lines
12 KiB
Vue
112 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import '@/styles/adminWorkbench.css'
|
|
import '@/styles/adminDensity.css'
|
|
import { TransportRouteDialog, routeLocation } from '@/modules/overseas-master-data'
|
|
import '../overseasBusinessDensity.css'
|
|
import { computed, reactive, ref, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { ArrowLeft } from '@lucide/vue'
|
|
import { ElMessage, type FormInstance } from 'element-plus'
|
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
|
import FormSection from '@/shared/components/FormSection.vue'
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
|
import { createLocalLooseboardOrder, createUsTruckOrder, getLocalLooseboardOrder, getUsTruckOrder, updateLocalLooseboardOrder, updateUsTruckOrder } from '../api/platformOverseasBusiness'
|
|
import { overseasBusinessPermissions } from '../permissions'
|
|
import FleetArrangementSummary from '../components/FleetArrangementSummary.vue'
|
|
import TransportDemandForm from '../components/TransportDemandForm.vue'
|
|
import { createTransportDemandForm, transportDemandError, buildTransportDemand, type TransportDemandForm as DemandFormModel } from '../transportDemandForm'
|
|
import type { FleetArrangement } from '../fleetArrangement'
|
|
import type { CreateLocalLooseboardOrderRequest, CreateLocalTransportOrderRequest, LocalLooseboardStatus, LocalTransportStatus, UpdateLocalLooseboardOrderRequest, UpdateLocalTransportOrderRequest } from '../types'
|
|
|
|
const props = defineProps<{ domain: 'truck' | 'looseboard' }>()
|
|
const routeOpen = ref(false)
|
|
const route = useRoute(); const router = useRouter(); const formRef = ref<FormInstance>()
|
|
const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
|
|
const routeCustomerId = computed(() => typeof route.query.customerId === 'string' ? route.query.customerId : '')
|
|
const editing = computed(() => Boolean(orderId.value)); const isLooseboard = computed(() => props.domain === 'looseboard')
|
|
const domainLabel = computed(() => isLooseboard.value ? '本土散板' : '本土卡车'); const listRouteName = computed(() => isLooseboard.value ? 'overseas-business-looseboards' : 'overseas-business-trucks')
|
|
const requiredPermission = computed(() => isLooseboard.value ? (editing.value ? overseasBusinessPermissions.looseboardEdit : overseasBusinessPermissions.looseboardCreate) : (editing.value ? overseasBusinessPermissions.truckEdit : overseasBusinessPermissions.truckCreate))
|
|
type FormStatus = LocalTransportStatus | LocalLooseboardStatus
|
|
interface TransportFormModel extends DemandFormModel {
|
|
customerId: string; orderNo: string; status: FormStatus; version: number; idempotencyKey: string
|
|
}
|
|
function newIdempotencyKey() { return globalThis.crypto?.randomUUID?.() || `admin-${Date.now()}-${Math.random().toString(36).slice(2)}` }
|
|
const initial = (): TransportFormModel => ({ ...createTransportDemandForm(), customerId: '', orderNo: '', status: isLooseboard.value ? 'PENDING_ACCEPTANCE' : 'PENDING_REVIEW', version: 0, idempotencyKey: newIdempotencyKey() })
|
|
const form = reactive<TransportFormModel>(initial()); const loading = ref(false); const saving = ref(false)
|
|
const fleetArrangement = ref<FleetArrangement | null>(null)
|
|
const loadError = ref<ApiErrorDetails | null>(null); const submitError = ref<ApiErrorDetails | null>(null)
|
|
const customers = ref<PlatformTenantResource[]>([]); const customersLoading = ref(false); const customersError = ref('')
|
|
const editableStatus = computed(() => !fleetArrangement.value && (!editing.value || (isLooseboard.value ? form.status === 'PENDING_ACCEPTANCE' : form.status === 'PENDING_REVIEW')))
|
|
const { captureBaseline } = useUnsavedWorkRegistration({ key: `overseas-${props.domain}-editor`, label: computed(() => `${editing.value ? '编辑' : '新增'}${domainLabel.value}订单`), isOpen: computed(() => true), model: form, requiredPermission })
|
|
async function loadCustomers() {
|
|
if (editing.value) return
|
|
customersLoading.value = true; customersError.value = ''
|
|
try { const result = await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }); customers.value = result.records.filter((item) => item.status === 'ACTIVE') }
|
|
catch { customers.value = []; customersError.value = '客户候选加载失败,请稍后重试' }
|
|
finally { customersLoading.value = false }
|
|
}
|
|
async function load() {
|
|
Object.assign(form, initial()); loadError.value = null; submitError.value = null; fleetArrangement.value = null
|
|
if (!editing.value) { captureBaseline(); return }
|
|
if (!routeCustomerId.value) { loadError.value = { message: `缺少客户标识,请从${domainLabel.value}列表重新进入` }; captureBaseline(); return }
|
|
loading.value = true
|
|
try {
|
|
if (isLooseboard.value) {
|
|
const detail = await getLocalLooseboardOrder(orderId.value, routeCustomerId.value)
|
|
fleetArrangement.value = detail.fleetArrangement
|
|
Object.assign(form, { customerId: detail.customerId, orderNo: detail.orderNo, productName: detail.productName || '', estimatedPalletCount: detail.estimatedPalletCount ?? 0, origin: detail.origin, destination: detail.destination, originAddress: detail.originAddress || '', destinationAddress: detail.destinationAddress || '', originContactName: detail.originContactName || '', originContactPhone: detail.originContactPhone || '', destinationContactName: detail.destinationContactName || '', destinationContactPhone: detail.destinationContactPhone || '', packageCount: detail.packageCount, totalWeightKg: Number(detail.totalWeightKg), totalVolumeCbm: Number(detail.totalVolumeCbm), budgetAmount: detail.budgetAmount == null ? undefined : Number(detail.budgetAmount), currency: detail.currency, pickupAt: detail.pickupAt || '', deliveryAt: detail.deliveryAt || '', remark: detail.remark || '', status: detail.status, version: detail.version })
|
|
} else {
|
|
const detail = await getUsTruckOrder(orderId.value, routeCustomerId.value)
|
|
fleetArrangement.value = detail.fleetArrangement
|
|
Object.assign(form, { customerId: detail.customerId, orderNo: detail.orderNo, origin: detail.origin, destination: detail.destination, originAddress: detail.originAddress || '', destinationAddress: detail.destinationAddress || '', vehicleType: detail.vehicleType || '', originContactName: detail.originContactName || '', originContactPhone: detail.originContactPhone || '', destinationContactName: detail.destinationContactName || '', destinationContactPhone: detail.destinationContactPhone || '', pickupAt: detail.pickupAt || '', deliveryAt: detail.deliveryAt || '', packageCount: detail.packageCount, totalWeightKg: Number(detail.totalWeightKg), totalVolumeCbm: Number(detail.totalVolumeCbm), budgetAmount: detail.budgetAmount == null ? undefined : Number(detail.budgetAmount), currency: detail.currency, remark: detail.remark || '', status: detail.status, version: detail.version })
|
|
}
|
|
captureBaseline()
|
|
} catch (error) { loadError.value = getApiErrorDetails(error, `${domainLabel.value}订单详情加载失败`) } finally { loading.value = false }
|
|
}
|
|
async function save() {
|
|
if (!editableStatus.value) { submitError.value = { message: `当前${domainLabel.value}订单状态不可编辑` }; return }
|
|
if (!editing.value && !form.customerId) { submitError.value = { message: '请选择客户' }; return }
|
|
const failure = transportDemandError(isLooseboard.value ? 'looseboard' : 'truck', form)
|
|
if (failure) { submitError.value = { message: failure }; return }
|
|
if (!await formRef.value?.validate().catch(() => false)) return
|
|
saving.value = true; submitError.value = null
|
|
try {
|
|
if (isLooseboard.value) {
|
|
const loose = buildTransportDemand('looseboard', form)
|
|
if (editing.value) await updateLocalLooseboardOrder(orderId.value, routeCustomerId.value, { ...loose, version: form.version } satisfies UpdateLocalLooseboardOrderRequest)
|
|
else await createLocalLooseboardOrder(form.customerId, { ...loose, idempotencyKey: form.idempotencyKey } satisfies CreateLocalLooseboardOrderRequest)
|
|
} else {
|
|
const truck = buildTransportDemand('truck', form)
|
|
if (editing.value) await updateUsTruckOrder(orderId.value, routeCustomerId.value, { ...truck, version: form.version } satisfies UpdateLocalTransportOrderRequest)
|
|
else await createUsTruckOrder(form.customerId, { ...truck, idempotencyKey: form.idempotencyKey } satisfies CreateLocalTransportOrderRequest)
|
|
}
|
|
captureBaseline(); ElMessage.success(`${domainLabel.value}订单已${editing.value ? '保存' : '创建'}`); await router.push({ name: listRouteName.value })
|
|
} catch (error) { submitError.value = getApiErrorDetails(error, `${domainLabel.value}订单保存失败`) } finally { saving.value = false }
|
|
}
|
|
function back() { void router.push({ name: listRouteName.value }) }
|
|
watch([orderId, routeCustomerId, isLooseboard], () => { void load(); void loadCustomers() }, { immediate: true })
|
|
</script>
|
|
|
|
<template>
|
|
<AdminPage class="admin-density overseas-business-editor overseas-transport-editor admin-workbench"><OperationErrorToast :details="submitError" /><DetailPageLayout class="overseas-business-form-layout workbench-panel is-flush is-page-surface" :aria-label="`${editing ? '编辑' : '新增'}${domainLabel}订单`">
|
|
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="back"><ArrowLeft :size="16" />返回列表</el-button><h1 class="density-detail-title">{{ editing ? '编辑' : '新增' }}{{ domainLabel }}订单</h1><span v-if="editing" class="form-reference">{{ form.orderNo || '—' }}</span></div></div></template>
|
|
<LoadErrorState v-if="loadError" :title="`${domainLabel}订单暂时无法加载`" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
|
<FleetArrangementSummary v-else-if="fleetArrangement" :arrangement="fleetArrangement" />
|
|
<section v-else v-loading="loading" class="form-panel">
|
|
<el-alert v-if="editing && !editableStatus" type="warning" :closable="false" show-icon title="当前状态不可普通编辑,请返回列表查看" />
|
|
<TransportDemandForm ref="formRef" :model="form" :kind="isLooseboard ? 'looseboard' : 'truck'" :submitting="saving" :disabled="!editableStatus" :editing="editing" @submit="save" @cancel="back">
|
|
<template v-if="!editing" #customer>
|
|
<FormSection class="transport-customer-section density-form-section" title="客户信息"><el-form-item label="客户" required><el-select v-model="form.customerId" filterable :loading="customersLoading" :disabled="Boolean(customersError)" placeholder="请选择客户"><el-option v-for="item in customers" :key="String(item.id)" :label="item.tenantName" :value="String(item.id)" /></el-select><small v-if="customersError" class="field-error">{{ customersError }}</small></el-form-item></FormSection>
|
|
</template>
|
|
</TransportDemandForm>
|
|
<FormSection v-if="!isLooseboard" class="density-form-section" title="车辆与派车"><el-button @click="routeOpen = true">查看路线</el-button></FormSection>
|
|
</section>
|
|
</DetailPageLayout><TransportRouteDialog v-model="routeOpen" :origin="routeLocation(form.origin, form.originAddress)" :destinations="[routeLocation(form.destination, form.destinationAddress)]" /></AdminPage>
|
|
</template>
|