Files
admin-web/src/modules/overseas-master-data/pages/CapacityOfferDetailView.vue
T

71 lines
8.4 KiB
Vue
Raw Normal View History

<script setup lang="ts">
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'
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'
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)
let sequence = 0, inquirySequence = 0, resetting = false
const canCreate = computed(() => detail.value?.offer.canCreateInquiry && (access.can('platform:fleet:capacity-offer:manage') || access.can('platform:fleet:capacity-offer:list') && access.can('platform:fleet:capacity-offer:inquiry-create')))
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
if (editing.value && editorOpen.value) checked = value.inquiries.records.find(row => row.bookingNo === editing.value?.bookingNo) || await getCapacityInquiry(editing.value.bookingNo)
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)
async function action(key: string, row: CapacityInquiry) { if (key === 'confirm' || key === 'cancel') await commands.run(row, key); else { editing.value = row; readonly.value = key !== 'edit'; editorOpen.value = true } }
function create() { if (!canCreate.value) return; editing.value = null; readonly.value = false; editorOpen.value = true }
watch(() => route.params.offerNo, () => { detail.value = null; editorOpen.value = false; void load() }, { immediate: true })
watch([page, size], loadInquiries)
onBeforeUnmount(() => { sequence++; inquirySequence++ })
</script>
<template><AdminPage class="admin-density"><DetailPageLayout class="density-detail-layout surface-panel" aria-label="运力发布详情">
<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="canCreate" type="primary" :disabled="loading" @click="create">登记意向</el-button></div></div></template>
<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">
<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>
<p>{{ detail.offer.blockReason || '按所选卸货站核对每段余位;登记意向不占位,调度确认后占位。' }}</p><p v-if="detail.offer.remark">{{ detail.offer.remark }}</p>
<h2 class="capacity-section-title">分段运力</h2><DataTable :data="detail.offer.segments" :columns="columns" :stripe="false" 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>
<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>
<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></div>
<CapacityInquiryDialog v-model="editorOpen" :offer="detail?.offer || null" :inquiry="editing" :readonly="readonly" @saved="accept" @refresh="load" />
</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>