2026-10-07 16:46:57 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-10-20 23:34:11 +08:00
|
|
|
|
import '@/styles/adminWorkbench.css'
|
2026-10-07 16:46:57 +08:00
|
|
|
|
import '@/styles/adminDensity.css'
|
|
|
|
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
|
|
|
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
|
|
|
|
|
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
|
|
|
|
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
|
|
|
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
|
|
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
|
|
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
|
|
|
|
import CapacityInquiryTable from '../components/CapacityInquiryTable.vue'
|
2026-10-09 22:05:51 +08:00
|
|
|
|
import CapacityInquiryFulfillment from '../components/CapacityInquiryFulfillment.vue'
|
2026-10-07 16:46:57 +08:00
|
|
|
|
import CapacityInquiryDialog from '../components/CapacityInquiryDialog.vue'
|
|
|
|
|
|
import { getCapacityOffer, getCapacityInquiry, listCapacityInquiries } from '../api/capacityOffers'
|
|
|
|
|
|
import { offerStatusLabels, offerTime, loadText, type CapacityOfferDetail, type CapacityInquiry, type InquiryResult } from '../capacityOfferTypes'
|
2026-10-11 15:33:09 +08:00
|
|
|
|
import { inquirySourceQuery, inquiryOrderTarget } from '../inquiryNavigation'
|
2026-10-07 16:46:57 +08:00
|
|
|
|
import { useCapacityInquiryCommands } from '../useCapacityInquiryCommands'
|
|
|
|
|
|
const route = useRoute(), router = useRouter(), access = useAccessPreviewStore()
|
|
|
|
|
|
const detail = ref<CapacityOfferDetail | null>(null), loading = ref(false), error = ref<ApiErrorDetails | null>(null), inquiryError = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
|
const page = ref(1), size = ref(20), inquiryLoading = ref(false), editorOpen = ref(false), editing = ref<CapacityInquiry | null>(null), readonly = ref(false)
|
2026-10-09 22:05:51 +08:00
|
|
|
|
const confirmMode = ref(false)
|
|
|
|
|
|
const fulfillmentOpen = ref(false), fulfillmentRow = ref<CapacityInquiry | null>(null), fulfillmentMode = ref<'incoming' | 'receipt' | 'bind' | 'view'>('view')
|
2026-10-11 15:33:09 +08:00
|
|
|
|
async function fulfillmentSaved() {
|
|
|
|
|
|
const row = fulfillmentRow.value
|
|
|
|
|
|
fulfillmentOpen.value = false
|
|
|
|
|
|
if (!row) return
|
|
|
|
|
|
const current = ++followupSequence
|
|
|
|
|
|
await load()
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updated = await getCapacityInquiry(row.bookingNo)
|
|
|
|
|
|
if (current === followupSequence) await action('view', updated)
|
|
|
|
|
|
} catch (cause) { if (current === followupSequence) error.value = getApiErrorDetails(cause, '操作已完成,下一步进度读取失败,请刷新原登记') }
|
|
|
|
|
|
}
|
|
|
|
|
|
let followupSequence = 0
|
2026-10-09 22:05:51 +08:00
|
|
|
|
function openFulfillment(key: string, row: CapacityInquiry) {
|
2026-10-11 15:33:09 +08:00
|
|
|
|
if (key === 'business-order') { const target = inquiryOrderTarget(row, access.can); if (target) void router.push(target); return true }
|
2026-10-09 22:05:51 +08:00
|
|
|
|
if (key === 'appointment' && access.can('platform:appointment:list') && access.can('platform:overseas-warehouse:last-mile-order:list')) { void router.push({ name: 'appointment-calendar', query: { sourceKey: `BOOKING:${row.bookingNo}`, customerId: row.customerId, destinationKey: row.cargo.destinationKey } }); return true }
|
2026-10-11 15:33:09 +08:00
|
|
|
|
if (key === 'dispatch' && row.dispatchNo && access.can('platform:overseas-warehouse:dispatch:list')) { void router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo: row.dispatchNo }, query: inquirySourceQuery(row) }); return true }
|
|
|
|
|
|
if (key === 'trip' && row.dispatchNo && access.can('platform:fleet:trip:list')) { void router.push({ name: 'overseas-master-data-trip-detail', params: { tripNo: row.tripNo }, query: inquirySourceQuery(row) }); return true }
|
2026-10-09 22:05:51 +08:00
|
|
|
|
if (!['incoming', 'receipt', 'bind', 'fulfillment'].includes(key)) return false
|
|
|
|
|
|
editorOpen.value = false
|
|
|
|
|
|
fulfillmentRow.value = row; fulfillmentMode.value = key === 'fulfillment' ? 'view' : key as 'incoming' | 'receipt' | 'bind'; fulfillmentOpen.value = true
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
2026-10-07 16:46:57 +08:00
|
|
|
|
let sequence = 0, inquirySequence = 0, resetting = false
|
2026-10-08 14:11:54 +08:00
|
|
|
|
const hasCreatePermission = computed(() => (access.can('platform:fleet:capacity-offer:manage') || access.can('platform:fleet:capacity-offer:list') && access.can('platform:fleet:capacity-offer:inquiry-create')))
|
|
|
|
|
|
const canCreate = computed(() => detail.value?.offer.canCreateInquiry && hasCreatePermission.value)
|
2026-10-07 16:46:57 +08:00
|
|
|
|
const columns: DataTableColumn[] = [{ key: 'fromName', label: '起始站', minWidth: 150 }, { key: 'toName', label: '到达站', minWidth: 150 }, { key: 'planned', label: '已关联货物计划占用', minWidth: 260, slotName: 'planned' }, { key: 'reserved', label: '未关联外接预留', minWidth: 260, slotName: 'reserved' }, { key: 'remaining', label: '可接托位 / 重量 / 体积', minWidth: 270, slotName: 'remaining' }]
|
|
|
|
|
|
async function load() {
|
|
|
|
|
|
const current = ++sequence; inquirySequence++; loading.value = true; error.value = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
const value = await getCapacityOffer(String(route.params.offerNo))
|
|
|
|
|
|
let checked: CapacityInquiry | null = null
|
2026-10-21 17:57:02 +08:00
|
|
|
|
// 当前只读展示余位,旧登记深链不再打开登记窗口;原流程保留。
|
2026-10-21 18:05:02 +08:00
|
|
|
|
/*
|
2026-10-11 15:33:09 +08:00
|
|
|
|
if (typeof route.query?.inquiry === 'string' && /^[A-Z][A-Z0-9-]{1,63}$/.test(route.query.inquiry)) {
|
|
|
|
|
|
const returned = await getCapacityInquiry(route.query.inquiry)
|
|
|
|
|
|
if (returned.offerNo !== value.offer.offerNo) throw new Error('登记不属于当前发布')
|
|
|
|
|
|
if (current === sequence) { checked = returned; readonly.value = true; confirmMode.value = false; editorOpen.value = true }
|
2026-10-11 19:33:25 +08:00
|
|
|
|
} else if (editing.value && editorOpen.value) checked = value.inquiries.records.find(row => row.bookingNo === editing.value?.bookingNo) || await getCapacityInquiry(editing.value.bookingNo)
|
2026-10-21 18:05:02 +08:00
|
|
|
|
*/
|
2026-10-07 16:46:57 +08:00
|
|
|
|
if (current === sequence) { resetting = true; detail.value = value; if (checked) editing.value = checked; page.value = value.inquiries.page; size.value = value.inquiries.size; queueMicrotask(() => { resetting = false }) }
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (cause) { if (current === sequence) { detail.value = null; error.value = getApiErrorDetails(cause, '发布详情读取失败') } }
|
|
|
|
|
|
finally { if (current === sequence) loading.value = false }
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadInquiries() {
|
|
|
|
|
|
if (!detail.value || resetting) return
|
|
|
|
|
|
const current = ++inquirySequence; inquiryLoading.value = true; inquiryError.value = null
|
|
|
|
|
|
try { const value = await listCapacityInquiries({ offerNo: detail.value.offer.offerNo, page: page.value, size: size.value, scope: access.can('platform:fleet:capacity-offer:manage') ? 'ALL' : 'MINE' }); if (current === inquirySequence && detail.value) detail.value.inquiries = value }
|
|
|
|
|
|
catch (cause) { if (current === inquirySequence) inquiryError.value = getApiErrorDetails(cause, '意向列表读取失败') }
|
|
|
|
|
|
finally { if (current === inquirySequence) inquiryLoading.value = false }
|
|
|
|
|
|
}
|
|
|
|
|
|
function accept(value: InquiryResult) {
|
|
|
|
|
|
if (!detail.value) return
|
|
|
|
|
|
detail.value.offer = value.offer
|
|
|
|
|
|
const rows = detail.value.inquiries.records, index = rows.findIndex(row => row.bookingNo === value.inquiry.bookingNo)
|
|
|
|
|
|
if (index >= 0) rows[index] = value.inquiry
|
|
|
|
|
|
else if (page.value === 1) { rows.unshift(value.inquiry); if (rows.length > size.value) rows.pop(); detail.value.inquiries.total++ }
|
|
|
|
|
|
else void loadInquiries()
|
|
|
|
|
|
for (const row of rows) { row.offerRecordVersion = value.offer.recordVersion; row.tripRecordVersion = value.offer.tripRecordVersion }
|
|
|
|
|
|
}
|
|
|
|
|
|
const commands = useCapacityInquiryCommands('capacity-offer-command', accept)
|
2026-10-11 15:33:09 +08:00
|
|
|
|
async function action(key: string, row: CapacityInquiry) { followupSequence++; if (openFulfillment(key, row)) return; if (key === 'cancel') await commands.run(row, key); else { if (key === 'confirm' && !access.can('platform:fleet:capacity-offer:manage')) return; confirmMode.value = key === 'confirm'; editing.value = row; readonly.value = key !== 'edit' && key !== 'confirm'; editorOpen.value = true } }
|
2026-10-09 22:05:51 +08:00
|
|
|
|
function create() { if (!canCreate.value) return; confirmMode.value = false; editing.value = null; readonly.value = false; editorOpen.value = true }
|
2026-10-11 15:33:09 +08:00
|
|
|
|
watch(() => [route.name, route.params.offerNo, route.query?.inquiry], ([name, offerNo]) => {
|
2026-10-07 18:53:06 +08:00
|
|
|
|
if (name !== 'overseas-master-data-capacity-offer-detail' || typeof offerNo !== 'string' || !offerNo) return
|
2026-10-11 15:33:09 +08:00
|
|
|
|
followupSequence++; detail.value = null; editorOpen.value = false; void load()
|
2026-10-07 18:53:06 +08:00
|
|
|
|
}, { immediate: true })
|
2026-10-07 16:46:57 +08:00
|
|
|
|
watch([page, size], loadInquiries)
|
2026-10-11 15:33:09 +08:00
|
|
|
|
onBeforeUnmount(() => { followupSequence++; sequence++; inquirySequence++ })
|
2026-10-07 16:46:57 +08:00
|
|
|
|
</script>
|
2026-10-20 23:34:11 +08:00
|
|
|
|
<template><AdminPage class="admin-density admin-workbench"><DetailPageLayout class="density-detail-layout surface-panel workbench-panel is-flush is-page-surface" aria-label="运力发布详情">
|
2026-10-21 18:05:02 +08:00
|
|
|
|
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="router.push({ name: 'overseas-master-data-capacity-offers' })">返回散板需求</el-button><h1 class="density-detail-title">{{ detail?.offer.offerNo || '运力发布详情' }}</h1><span v-if="detail">{{ offerStatusLabels[detail.offer.status] }}</span></div><div class="density-actions"><el-button :loading="loading" @click="load">重新核对</el-button><!-- 当前暂不开放登记与拼车;原代码保留。
|
|
|
|
|
|
<el-button v-if="hasCreatePermission" type="primary" :disabled="loading || !canCreate" @click="create">登记客户货物</el-button>
|
|
|
|
|
|
--></div></div></template>
|
2026-10-07 16:46:57 +08:00
|
|
|
|
<LoadErrorState v-if="error" title="发布详情读取失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
|
|
|
|
|
<div v-loading="loading" :aria-busy="loading"><template v-if="detail">
|
2026-10-09 22:05:51 +08:00
|
|
|
|
<dl class="density-fields"><div><dt>车次号</dt><dd><el-button v-if="access.can('platform:fleet:trip:list')" link type="primary" @click="router.push({ name: 'overseas-master-data-trip-detail', params: { tripNo: detail.offer.tripNo } })">{{ detail.offer.tripNo }}</el-button><span v-else>{{ detail.offer.tripNo }}</span></dd></div><div><dt>起运仓</dt><dd>{{ detail.offer.warehouseName }}</dd></div><div><dt>计划发车</dt><dd>{{ offerTime(detail.offer.plannedDepartureAt) }}</dd></div><div><dt>最晚送仓</dt><dd>{{ offerTime(detail.offer.cutoffAt) }}</dd></div><div><dt>调度联系人</dt><dd>{{ detail.offer.contactName || '—' }}</dd></div><div><dt>调度联系方式</dt><dd>{{ detail.offer.contactPhone || '—' }}</dd></div></dl>
|
2026-10-21 18:05:02 +08:00
|
|
|
|
<!-- 当前暂不开放登记与拼车;原代码保留。
|
|
|
|
|
|
<template><p v-if="hasCreatePermission && !detail.offer.canCreateInquiry" role="status">无法登记客户货物:{{ detail.offer.blockReason || '当前发布不接受新意向' }}</p>
|
|
|
|
|
|
<p v-else>{{ detail.offer.blockReason || '按所选卸货站核对每段余位;登记客户货物不占位,调度确认后占位。' }}</p></template>
|
|
|
|
|
|
--><p v-if="detail.offer.blockReason" role="status">{{ detail.offer.blockReason }}</p><p v-if="detail.offer.remark">{{ detail.offer.remark }}</p>
|
2026-10-21 13:22:42 +08:00
|
|
|
|
<h2 class="capacity-section-title">分段运力</h2><div class="workbench-table"><DataTable :data="detail.offer.segments" :columns="columns" row-key="fromIndex" empty-text="容量待调度核对"><template #planned="{ row }">{{ loadText(row.planned) }}</template><template #reserved="{ row }">{{ loadText(row.reserved) }}</template><template #remaining="{ row }">{{ detail.offer.capacityKnown ? loadText(row.remaining) : '待调度核对' }}</template></DataTable></div>
|
2026-10-21 18:05:02 +08:00
|
|
|
|
<!-- 当前暂不开放登记与拼车;原代码保留。
|
|
|
|
|
|
<template><h2 class="capacity-section-title">{{ access.can('platform:fleet:capacity-offer:manage') ? '本发布全部意向' : '我的登记' }}</h2>
|
|
|
|
|
|
<LoadErrorState v-if="inquiryError || commands.error.value" title="意向操作未完成" :message="(inquiryError || commands.error.value)?.message" @retry="commands.state.pending ? commands.retry() : loadInquiries()"><template #details><ApiErrorMeta :details="(inquiryError || commands.error.value)!" /></template>
|
|
|
|
|
|
</LoadErrorState><el-button v-if="commands.state.pending" :loading="commands.busy.value" @click="commands.retry">重试原操作</el-button>
|
2026-10-21 17:57:02 +08:00
|
|
|
|
<CapacityInquiryTable v-model:page="page" v-model:size="size" :rows="detail.inquiries.records" :total="detail.inquiries.total" :loading="inquiryLoading" :failed="!!inquiryError" :busy="commands.busy.value" @action="action" /></template>
|
2026-10-21 18:05:02 +08:00
|
|
|
|
-->
|
2026-10-07 16:46:57 +08:00
|
|
|
|
</template></div>
|
2026-10-21 18:05:02 +08:00
|
|
|
|
<!-- 当前暂不开放登记与拼车;原代码保留。
|
|
|
|
|
|
<CapacityInquiryFulfillment v-model="fulfillmentOpen" :inquiry="fulfillmentRow" :mode="fulfillmentMode" @saved="fulfillmentSaved" />
|
|
|
|
|
|
-->
|
|
|
|
|
|
<!-- 当前暂不开放登记与拼车;原代码保留。
|
|
|
|
|
|
<CapacityInquiryDialog v-model="editorOpen" :offer="detail?.offer || null" :inquiry="editing" :readonly="readonly" :confirm-mode="confirmMode" @action="action" @saved="accept" @refresh="load" />
|
|
|
|
|
|
-->
|
2026-10-07 16:46:57 +08:00
|
|
|
|
</DetailPageLayout></AdminPage></template>
|
|
|
|
|
|
<style scoped>.capacity-section-title{font-size:var(--admin-font-section);margin:var(--admin-space-6) 0 var(--admin-space-3)}</style>
|