fix(appointments): handle synchronization and transport continuation

This commit is contained in:
2026-10-18 22:29:06 +08:00
parent a5a1976924
commit 67d917010d
9 changed files with 226 additions and 11 deletions
@@ -1,6 +1,6 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
import { computed, onActivated, onDeactivated, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
import { onBeforeRouteLeave, onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
@@ -18,6 +18,7 @@ import WorkDetailPanel from './WorkDetailPanel.vue'
import SourceAppointmentPanel from './SourceAppointmentPanel.vue'
import type { PlanningSourceLookup } from './planningApi'
import { appointmentExternalTarget } from './workNavigation'
import { useWorkSyncRead } from './useWorkSyncRead'
const route = useRoute(), router = useRouter(), access = useAccessPreviewStore()
const stringQuery = (key: string) => typeof route.query[key] === 'string' ? route.query[key] as string : ''
const savedAnchor = stringQuery('workAnchor')
@@ -49,6 +50,14 @@ const heading = computed(() => {
})
const scopeDescription = computed(() => scope.value?.kind === 'day' ? '按所选日期、流程与类型查询,包含已完成事项' : scope.value?.kind === 'arrivals' ? '独立到港来源,不计工作量;来源未提供流程筛选,展示该日全部参考' : '跨月份、流程与类型,包含未预约和未定日期事项')
const panelOpen = computed(() => !!scope.value || !!detailKey.value || !!sourceLink.value)
// Cached routes must not leave their body-teleported dialog over another workspace.
const pageActive = ref(true)
onActivated(() => { const returning = !pageActive.value; pageActive.value = true; if (returning) void refreshWork() })
onDeactivated(() => {
pageActive.value = false
++calendarRequest; ++summaryRequest; ++itemsRequest
calendarController?.abort(); summaryController?.abort(); itemsController?.abort()
})
const panelBody = ref<HTMLElement | null>(null)
const selectedDate = computed(() => scope.value?.kind === 'day' || scope.value?.kind === 'arrivals' ? scope.value.date : undefined)
const selectedType = computed(() => scope.value?.kind === 'day' && scope.value.types.length === 1 ? scope.value.types[0] : undefined)
@@ -62,19 +71,24 @@ onBeforeRouteLeave(canLeave)
onBeforeRouteUpdate((to, from) => to.query.sourceKey !== from.query.sourceKey || to.query.orderId !== from.query.orderId || to.query.customerId !== from.query.customerId || to.query.destinationKey !== from.query.destinationKey || to.query.workItem !== from.query.workItem ? canLeave() : true)
let calendarRequest = 0, summaryRequest = 0, itemsRequest = 0, referenceRequest = 0
let calendarController: AbortController | null = null, summaryController: AbortController | null = null, itemsController: AbortController | null = null
const calendarRead = useWorkSyncRead(), summaryRead = useWorkSyncRead(), itemsRead = useWorkSyncRead()
const syncing = computed(() => calendarRead.syncing.value || summaryRead.syncing.value || itemsRead.syncing.value)
async function loadCalendar() {
const request = ++calendarRequest; calendarController?.abort(); calendarController = new AbortController(); calendarError.value = null; calendar.value = null
if (!types.value.length) { calendarLoading.value = false; return }
calendarLoading.value = true
try {
const result = await getWorkCalendar({ from: range.value.from, to: addDays(range.value.to, -1), flow: flow.value || undefined, types: [...types.value] }, calendarController.signal)
const query = { from: range.value.from, to: addDays(range.value.to, -1), flow: flow.value || undefined, types: [...types.value] }
const signal = calendarController.signal
const result = await calendarRead.read(() => getWorkCalendar(query, signal), signal)
if (request === calendarRequest) calendar.value = result
} catch (cause) { if (request === calendarRequest) calendarError.value = getApiErrorDetails(cause, '工作日历读取失败') }
finally { if (request === calendarRequest) calendarLoading.value = false }
}
async function loadSummary() {
const request = ++summaryRequest; summaryController?.abort(); summaryController = new AbortController(); summaryLoading.value = true; summaryError.value = null
try { const result = await getWorkSummary(summaryController.signal); if (request === summaryRequest) summary.value = result }
const signal = summaryController.signal
try { const result = await summaryRead.read(() => getWorkSummary(signal), signal); if (request === summaryRequest) summary.value = result }
catch (cause) { if (request === summaryRequest) { summaryError.value = getApiErrorDetails(cause, '全局待办读取失败'); summary.value = null } }
finally { if (request === summaryRequest) summaryLoading.value = false }
}
@@ -91,7 +105,8 @@ async function loadItems() {
const request = ++itemsRequest; itemsController?.abort(); itemsController = new AbortController(); itemsError.value = null; items.value = null
const query = itemQuery(); if (!query) { itemsLoading.value = false; return }
itemsLoading.value = true
try { const result = await getWorkItems(query, itemsController.signal); if (request === itemsRequest) items.value = result }
const signal = itemsController.signal
try { const result = await itemsRead.read(() => getWorkItems(query, signal), signal); if (request === itemsRequest) items.value = result }
catch (cause) { if (request === itemsRequest) itemsError.value = getApiErrorDetails(cause, '工作明细读取失败') }
finally { if (request === itemsRequest) itemsLoading.value = false }
}
@@ -188,6 +203,7 @@ onBeforeUnmount(() => { ++calendarRequest; ++summaryRequest; ++itemsRequest; ++r
<form class="work-search" role="search" @submit.prevent="searchItems"><label class="sr-only" for="appointment-work-search">查找工作事项</label><input id="appointment-work-search" v-model="search" type="search" maxlength="80" placeholder="柜号、送仓批次、车次、目的仓"><button type="submit">查找事项</button><span>跨日期查找未完成事项 · 含未定日期</span></form>
</div>
<p v-if="linkError" role="alert">{{ linkError }}</p>
<p v-if="syncing" role="status">预约工作清单同步中,正在自动读取最新日历与待办;已保存内容无需重复提交。</p>
<LoadErrorState v-if="calendarError" title="工作日历读取失败" :message="calendarError.message" :loading="calendarLoading" @retry="loadCalendar"><template #details><ApiErrorMeta :details="calendarError" /></template></LoadErrorState>
<LoadErrorState v-if="referenceError" title="到港参考不可用" :message="referenceError.message" :loading="referenceLoading" @retry="loadArrivals"><template #details><ApiErrorMeta :details="referenceError" /></template></LoadErrorState>
<div class="work-layout">
@@ -199,7 +215,7 @@ onBeforeUnmount(() => { ++calendarRequest; ++summaryRequest; ++itemsRequest; ++r
<LoadErrorState v-if="summaryError" title="全局待办读取失败" :message="summaryError.message" :loading="summaryLoading" @retry="loadSummary"><template #details><ApiErrorMeta :details="summaryError" /></template></LoadErrorState>
</template>
</WorkCalendarPanel>
<AppDialog :model-value="panelOpen" :title="panelTitle" class="admin-density-dialog admin-density appointment-work work-dialog" width="min(1120px, calc(100vw - 48px))" fullscreenable append-to-body :close-on-click-modal="true" :close-on-press-escape="true" :before-close="closePanel" :show-footer="listVisible && !!items && !itemsError && !itemsLoading" destroy-on-close>
<AppDialog v-if="pageActive" :model-value="panelOpen" :title="panelTitle" class="admin-density-dialog admin-density appointment-work work-dialog" width="min(1120px, calc(100vw - 48px))" fullscreenable append-to-body :close-on-click-modal="true" :close-on-press-escape="true" :before-close="closePanel" :show-footer="listVisible && !!items && !itemsError && !itemsLoading" destroy-on-close>
<template #header>
<header class="panel-header">
<div class="panel-nav"><button v-if="detailKey || sourceLink" type="button" @click="back">‹ {{ origins.length ? '返回原事项' : '返回工作明细' }}</button><h3>{{ panelTitle }}</h3></div>