正在定位当前批次的工作事项;已保存的内容无需重复提交。
+{{ syncing ? '预约工作清单同步中,正在自动读取当前批次下一步;已保存内容无需重复提交。' : '正在定位当前批次的工作事项;已保存的内容无需重复提交。' }}
本批有多个客户,请选择本次继续办理的货物。
当前没有可定位的送仓事项,请核对服务端同步结果。
正在读取当前事项与操作资格…
+{{ syncing ? '预约工作清单同步中,正在自动读取最新事项;已保存内容无需重复提交。' : '正在读取当前事项与操作资格…' }}
{{ typeLabels[detail.item.type] }} · {{ flowLabels[detail.item.flow] }} · {{ workStatus(detail.item) }}
{{ workDate(detail.item) }} · {{ detail.item.timezone || '地点时区待核实' }}
{{ detail.item.destinationName || detail.item.originName || '地点待核实' }} · {{ detail.item.owner?.name || '待分派' }}
diff --git a/src/modules/appointment-calendar/useWorkSyncRead.test.ts b/src/modules/appointment-calendar/useWorkSyncRead.test.ts new file mode 100644 index 00000000..be6875b2 --- /dev/null +++ b/src/modules/appointment-calendar/useWorkSyncRead.test.ts @@ -0,0 +1,45 @@ +import { afterEach, expect, it, vi } from 'vitest' +import { ApiResponseError } from '@/shared/api/http' +import { useWorkSyncRead } from './useWorkSyncRead' +const pending = () => new ApiResponseError('预约批次或运输关联已变化,服务端正在协调完整工作清单', 'A0409', 409, undefined, 'request-sync', 'trace-sync') +afterEach(() => vi.useRealTimers()) +it('waits only for the full-work synchronization guard and returns the fresh result', async () => { + vi.useFakeTimers() + const reader = useWorkSyncRead(), load = vi.fn().mockRejectedValueOnce(pending()).mockResolvedValue('fresh') + const result = reader.read(load, new AbortController().signal) + await vi.advanceTimersByTimeAsync(0) + expect(reader.syncing.value).toBe(true) + expect(load).toHaveBeenCalledTimes(1) + await vi.advanceTimersByTimeAsync(2000) + expect(await result).toBe('fresh') + expect(reader.syncing.value).toBe(false) +}) +it.each([new ApiResponseError('事项版本已变化', 'A0409', 409), new ApiResponseError('无权查看', 'A0403', 403), new Error('network')])('does not retry other failures: %s', async error => { + const reader = useWorkSyncRead(), load = vi.fn().mockRejectedValue(error) + await expect(reader.read(load, new AbortController().signal)).rejects.toBe(error) + expect(load).toHaveBeenCalledTimes(1) + expect(reader.syncing.value).toBe(false) +}) +it('stops waiting at the limit and preserves diagnostic IDs on the original error', async () => { + vi.useFakeTimers() + const error = pending(), reader = useWorkSyncRead(), load = vi.fn().mockRejectedValue(error) + const result = expect(reader.read(load, new AbortController().signal)).rejects.toBe(error) + await vi.advanceTimersByTimeAsync(30000) + await result + expect(load).toHaveBeenCalledTimes(16) + expect(reader.syncing.value).toBe(false) + expect(error.traceId).toBe('trace-sync') + expect(vi.getTimerCount()).toBe(0) +}) +it('cancels a waiting read without sending another request', async () => { + vi.useFakeTimers() + const controller = new AbortController(), reader = useWorkSyncRead(), load = vi.fn().mockRejectedValue(pending()) + const result = expect(reader.read(load, controller.signal)).rejects.toHaveProperty('name', 'AbortError') + await vi.advanceTimersByTimeAsync(0) + controller.abort() + await result + await vi.advanceTimersByTimeAsync(30000) + expect(load).toHaveBeenCalledTimes(1) + expect(reader.syncing.value).toBe(false) + expect(vi.getTimerCount()).toBe(0) +}) diff --git a/src/modules/appointment-calendar/useWorkSyncRead.ts b/src/modules/appointment-calendar/useWorkSyncRead.ts new file mode 100644 index 00000000..62c1a74e --- /dev/null +++ b/src/modules/appointment-calendar/useWorkSyncRead.ts @@ -0,0 +1,30 @@ +import { ref } from 'vue' +import { getApiErrorDetails } from '@/shared/api/http' + +// 只等待工作清单同步守卫;版本冲突、权限失败和写请求均不重试。 +export function useWorkSyncRead() { + const syncing = ref(false) + let generation = 0 + async function read