63 lines
7.6 KiB
Vue
63 lines
7.6 KiB
Vue
<script setup lang="ts">
|
||
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import { listCapacityOffers, saveCapacityOffer, commandCapacityOffer } from '../api/capacityOffers'
|
||
import { offerStatusLabels, type CapacityOffer, type OfferWrite, type OfferCommand } from '../capacityOfferTypes'
|
||
import type { FleetTrip } from '../tripTypes'
|
||
import { useFleetDraftGuard } from '../useFleetDraftGuard'
|
||
const open = defineModel<boolean>({ default: false })
|
||
const props = defineProps<{ trip: FleetTrip }>()
|
||
const emit = defineEmits<{ saved: [version: number] }>()
|
||
const access = useAccessPreviewStore(), offer = ref<CapacityOffer | null>(null), loading = ref(false), busy = ref(false), error = ref<ApiErrorDetails | null>(null)
|
||
const loaded = ref(false)
|
||
const form = reactive({ cutoffAt: '', contactName: '', contactPhone: '', remark: '' })
|
||
const pending = ref<{ action: 'save'; offerNo: string | null; body: OfferWrite } | { action: 'publish' | 'pause' | 'resume' | 'close'; offerNo: string; body: OfferCommand } | null>(null)
|
||
let generation = 0
|
||
const writable = computed(() => access.can('platform:fleet:capacity-offer:manage'))
|
||
const guard = useFleetDraftGuard('capacity-offer-settings', open, form, busy, computed(() => !!pending.value))
|
||
async function load() {
|
||
if (!writable.value || !open.value || pending.value) return
|
||
const current = ++generation; loading.value = true; error.value = null; loaded.value = false
|
||
try { const value = await listCapacityOffers({ scope: 'ALL', tripNo: props.trip.tripNo, page: 1, size: 1 }); if (current !== generation) return; offer.value = value.records[0] || null; loaded.value = true; Object.assign(form, { cutoffAt: offer.value?.cutoffAt || '', contactName: offer.value?.contactName || '', contactPhone: offer.value?.contactPhone || '', remark: offer.value?.remark || '' }); await nextTick(); guard.capture() }
|
||
catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, '发布设置读取失败') }
|
||
finally { if (current === generation) loading.value = false }
|
||
}
|
||
watch(open, value => { generation++; if (value) { if (pending.value) return; void load() } })
|
||
async function execute() {
|
||
if (!pending.value || busy.value || !writable.value) return
|
||
const current = generation; busy.value = true; error.value = null
|
||
try { const p = pending.value; const result = p.action === 'save' ? await saveCapacityOffer(p.offerNo, p.body) : await commandCapacityOffer(p.offerNo, p.action, p.body); if (current !== generation) return; offer.value = result.offer; pending.value = null; emit('saved', result.offer.tripRecordVersion); guard.capture(); ElMessage.success('发布设置已更新') }
|
||
catch (cause) { if (current === generation) { error.value = getApiErrorDetails(cause, '发布操作未完成'); if (cause instanceof ApiResponseError) pending.value = null } }
|
||
finally { if (current === generation) busy.value = false }
|
||
}
|
||
async function save() {
|
||
if (pending.value) { await execute(); return }
|
||
if (loading.value || !loaded.value || !writable.value || offer.value && !offer.value.actions.canEdit) return
|
||
if (!form.cutoffAt) { ElMessage.warning('请填写交货截止时间'); return }
|
||
pending.value = { action: 'save', offerNo: offer.value?.offerNo || null, body: { tripNo: props.trip.tripNo, tripRecordVersion: offer.value?.tripRecordVersion ?? props.trip.recordVersion, offerRecordVersion: offer.value?.recordVersion ?? null, cutoffAt: form.cutoffAt, contactName: form.contactName.trim() || null, contactPhone: form.contactPhone.trim() || null, remark: form.remark.trim() || null, idempotencyKey: crypto.randomUUID() } }
|
||
await execute()
|
||
}
|
||
async function command(action: 'publish' | 'pause' | 'resume' | 'close') {
|
||
if (!offer.value || loading.value || !loaded.value || busy.value || pending.value || !writable.value) return
|
||
if (guard.dirty.value) { ElMessage.warning('请先保存发布信息,再执行发布状态操作'); return }
|
||
const labels = { publish: '发布运力', pause: '暂停接单', resume: '恢复接单', close: '关闭发布' }
|
||
try { await ElMessageBox.confirm(`${labels[action]} ${offer.value.offerNo}?已确认预订及客户订单不会被取消。`, labels[action], { customClass: 'admin-density-confirm' }) } catch { return }
|
||
pending.value = { action, offerNo: offer.value.offerNo, body: { tripRecordVersion: offer.value.tripRecordVersion, offerRecordVersion: offer.value.recordVersion, idempotencyKey: crypto.randomUUID() } }; await execute()
|
||
}
|
||
onBeforeUnmount(() => { generation++ })
|
||
</script>
|
||
<template><AppDialog v-model="open" :before-close="guard.beforeClose" class="admin-density-dialog" title="发布设置" width="1080px" :show-confirm="writable && (!offer || offer.actions.canEdit || !!pending)" :confirm-text="pending ? '重试原请求' : '保存发布信息'" :confirm-loading="busy" :confirm-disabled="loading || !loaded" @confirm="save">
|
||
<LoadErrorState v-if="error" title="发布操作未完成" :message="error.message" @retry="pending ? execute() : load()"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||
<p>{{ trip.tripNo }} · {{ offer ? `${offer.offerNo} · ${offerStatusLabels[offer.status]}` : '尚未建立发布' }}</p><p v-if="offer?.blockReason">{{ offer.blockReason }}</p><p v-if="pending">上次结果尚未确定,原请求已保留,请重试核对。</p>
|
||
<el-form v-loading="loading" label-position="top" class="density-form offer-settings-form" :disabled="busy || loading || !!pending || !writable || !!offer && !offer.actions.canEdit">
|
||
<el-form-item label="交货截止时间" required><el-date-picker v-model="form.cutoffAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="选择交货截止时间" /></el-form-item><el-form-item label="公开给销售的调度联系人"><el-input v-model="form.contactName" maxlength="128" placeholder="填写公开给销售的联系人(选填)" /></el-form-item><el-form-item label="公开联系方式"><el-input v-model="form.contactPhone" maxlength="128" placeholder="填写公开电话或其他联系方式(选填)" /></el-form-item><el-form-item label="公开说明" class="offer-settings-wide"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="1000" placeholder="补充交货要求或运力说明(选填)" /></el-form-item>
|
||
</el-form>
|
||
<div v-if="offer && writable" class="density-actions"><el-button v-if="offer.actions.canPublish" :disabled="busy || loading || !loaded || !!pending || guard.dirty.value" type="primary" @click="command('publish')">发布运力</el-button><el-button v-if="offer.actions.canPause" :disabled="busy || loading || !loaded || !!pending || guard.dirty.value" @click="command('pause')">暂停接单</el-button><el-button v-if="offer.actions.canResume" :disabled="busy || loading || !loaded || !!pending || guard.dirty.value" type="primary" @click="command('resume')">重新核对并恢复</el-button><el-button v-if="offer.actions.canClose" :disabled="busy || loading || !loaded || !!pending || guard.dirty.value" type="danger" plain @click="command('close')">关闭发布</el-button></div>
|
||
</AppDialog></template>
|
||
<style scoped>.offer-settings-form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--admin-space-4) var(--admin-space-6)}.offer-settings-wide{grid-column:1/-1}.offer-settings-form :deep(.el-date-editor){width:100%}</style>
|