fix: align fleet controls and protect appointment retries
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { computed, provide, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave, onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
|
||||
import { resolvePlanningSource, listUnscheduledAppointments, type ResolvedPlanningSource, type PlanningSource } from './planningApi'
|
||||
import AppointmentFollowupDialog from './AppointmentFollowupDialog.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -35,6 +35,14 @@ const selectedSource = ref<PlanningSource | null>(null)
|
||||
const linkError = ref<ApiErrorDetails | null>(null)
|
||||
const linkLoading = ref(false)
|
||||
let linkRequest = 0
|
||||
const childLeaveGuards = new Set<() => boolean>()
|
||||
provide('appointment-register-leave-guard', (guard: () => boolean) => {
|
||||
childLeaveGuards.add(guard)
|
||||
return () => { childLeaveGuards.delete(guard) }
|
||||
})
|
||||
function switchWorkView(next: 'calendar' | 'cargo' | 'containers') {
|
||||
if (canLeave()) workView.value = next
|
||||
}
|
||||
const workView = ref<'calendar' | 'cargo' | 'containers'>('calendar')
|
||||
const calendarVisited = ref(true)
|
||||
const showTripLayer = ref(false)
|
||||
@@ -203,7 +211,16 @@ function openSource(event: CalendarEvent) {
|
||||
if (event.sourcePath) void router.push(event.sourcePath)
|
||||
}
|
||||
|
||||
function canLeave() {
|
||||
if (saving.value || pendingSave.value) { ElMessage.warning('请先核实本次保存结果,再关闭或切换'); return false }
|
||||
return [...childLeaveGuards].every(guard => guard())
|
||||
}
|
||||
function beforeCloseAppointment(done: () => void) { if (canLeave()) done() }
|
||||
function closeAppointment() { if (canLeave()) dialogOpen.value = false }
|
||||
onBeforeRouteLeave(canLeave)
|
||||
onBeforeRouteUpdate(canLeave)
|
||||
function openCreate(date: string) {
|
||||
if (!access.can(permission.inboundOrderAppointment) || !canLeave()) return
|
||||
++candidateRequest
|
||||
++detailRequest
|
||||
detailLoading.value = false
|
||||
@@ -225,6 +242,7 @@ function openCreate(date: string) {
|
||||
}
|
||||
|
||||
async function openEdit(event: Pick<CalendarEvent, 'key'>) {
|
||||
if (!canLeave()) return
|
||||
const request = ++detailRequest
|
||||
dialogError.value = null
|
||||
editing.value = [...appointments.value, ...unscheduled.value].find(item => item.appointmentNo === event.key) || null
|
||||
@@ -294,7 +312,7 @@ async function loadCandidates() {
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (saving.value || detailLoading.value || !detailReady.value) return
|
||||
if (!access.can(permission.inboundOrderAppointment) || saving.value || detailLoading.value || !detailReady.value) return
|
||||
const validation = appointmentFormError(form)
|
||||
if (!pendingSave.value && validation) { ElMessage.warning(validation); return }
|
||||
if (!editing.value) {
|
||||
@@ -382,9 +400,9 @@ onBeforeUnmount(() => { ++unscheduledRequest; ++calendarRequest; ++candidateRequ
|
||||
<AppointmentFollowupDialog v-if="linkedSource" :source="linkedSource.source" :coverage="linkedSource.coverage" :can-write="access.can('platform:appointment:followup')" :can-arrange="access.can(permission.inboundOrderAppointment)" @arrange="arrangeLinkedSource" @close="linkedSource = null" />
|
||||
<div class="appointment-heading">
|
||||
<div class="density-status-tabs" role="group" aria-label="预约工作视图">
|
||||
<button type="button" :class="{ 'is-active': workView === 'calendar' }" :aria-pressed="workView === 'calendar'" @click="workView = 'calendar'">预约日历</button>
|
||||
<button v-if="access.can(permission.lastMileOrderList)" type="button" :class="{ 'is-active': workView === 'containers' }" :aria-pressed="workView === 'containers'" @click="workView = 'containers'">提柜与到仓</button>
|
||||
<button v-if="access.can(permission.lastMileOrderList)" type="button" :class="{ 'is-active': workView === 'cargo' }" :aria-pressed="workView === 'cargo'" @click="workView = 'cargo'">送仓与排车</button>
|
||||
<button type="button" :class="{ 'is-active': workView === 'calendar' }" :aria-pressed="workView === 'calendar'" @click="switchWorkView('calendar')">预约日历</button>
|
||||
<button v-if="access.can(permission.lastMileOrderList)" type="button" :class="{ 'is-active': workView === 'containers' }" :aria-pressed="workView === 'containers'" @click="switchWorkView('containers')">提柜与到仓</button>
|
||||
<button v-if="access.can(permission.lastMileOrderList)" type="button" :class="{ 'is-active': workView === 'cargo' }" :aria-pressed="workView === 'cargo'" @click="switchWorkView('cargo')">送仓与排车</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="cargoVisited" v-show="workView === 'cargo'"><AppointmentCargoPanel :external-source="selectedSource" @source-accepted="selectedSource = null" @created="load(); loadUnscheduled()" /></div>
|
||||
@@ -405,7 +423,7 @@ onBeforeUnmount(() => { ++unscheduledRequest; ++calendarRequest; ++candidateRequ
|
||||
<template #issues="{ row }">{{ tripIssues(row as TripCalendarEntry) }}</template>
|
||||
</DataTable>
|
||||
</AppDialog>
|
||||
<AppDialog v-model="dialogOpen" class="admin-density-dialog" :title="editing ? '编辑预约' : '新增预约'" width="min(1080px, calc(100vw - 48px))" :confirm-text="pendingSave ? '重试本次保存' : '保存'" :confirm-loading="saving" :confirm-disabled="detailLoading || !detailReady || (!editing && (!warehouse || !selected.length || candidateLoading || !!candidateError))" @confirm="save">
|
||||
<AppDialog :model-value="dialogOpen" @update:model-value="closeAppointment" :before-close="beforeCloseAppointment" :show-confirm="access.can(permission.inboundOrderAppointment)" class="admin-density-dialog" :title="editing ? (access.can(permission.inboundOrderAppointment) ? '编辑预约' : '查看预约') : '新增预约'" width="min(1080px, calc(100vw - 48px))" :confirm-text="pendingSave ? '重试本次保存' : '保存'" :confirm-loading="saving" :confirm-disabled="detailLoading || !detailReady || (!editing && (!warehouse || !selected.length || candidateLoading || !!candidateError))" @confirm="save">
|
||||
<LoadErrorState v-if="dialogError" title="预约操作失败" :message="dialogError.message" :show-retry="false"><template #details><ApiErrorMeta :details="dialogError" /></template></LoadErrorState>
|
||||
<div v-loading="detailLoading">
|
||||
<el-form label-position="top" class="calendar-form density-form">
|
||||
@@ -418,7 +436,7 @@ onBeforeUnmount(() => { ++unscheduledRequest; ++calendarRequest; ++candidateRequ
|
||||
</el-form>
|
||||
<LoadErrorState v-if="!editing && warehouseError" title="仓库加载失败" :message="warehouseError.message" @retry="searchWarehouses(warehouseKeyword)" />
|
||||
<p v-if="!editing && warehouseTotal > warehouseOptions.length && !warehouseError" class="appointment-hint">输入更完整的仓库编码或名称,缩小搜索范围。</p>
|
||||
<FbaAppointmentFields :model="form" :appointment-no="editing?.appointmentNo" :timezone="editing?.timezone || warehouseOptions.find(item => item.warehouseCode === warehouse)?.timezone" :disabled="saving || !!pendingSave" />
|
||||
<FbaAppointmentFields :model="form" :appointment-no="editing?.appointmentNo" :timezone="editing?.timezone || warehouseOptions.find(item => item.warehouseCode === warehouse)?.timezone" :disabled="!access.can(permission.inboundOrderAppointment) || saving || !!pendingSave" />
|
||||
<template v-if="!editing && warehouse">
|
||||
<DataTable :key="selectionEpoch" v-model:page="candidatePage" v-model:page-size="candidateSize" :data="candidates" :columns="columns" :total="candidateTotal" :loading="candidateLoading" row-key="lineId" :results-visible="!candidateError" @selection-change="selectCandidates" @update:page="loadCandidates()" @update:page-size="candidatePage = 1; loadCandidates()">
|
||||
<template #customer="{ row }">{{ row.customerName?.trim() || '客户名称缺失' }}</template>
|
||||
|
||||
Reference in New Issue
Block a user