Compare commits

..
Author SHA1 Message Date
ouyangxq da4cb9c8ad fix(appointments): open canonical dispatch details from calendar 2026-10-20 16:36:21 +08:00
ouyangxq 291535e242 style(admin): 放大订单、费用、测量与库位弹框宽度
- 订单:审核/扣货/合柜报关/同柜组成员 720→880,编辑备注 880→1000,导入旧系统订单 760→880
- 入库费用:新增/编辑费用 880→1000,驳回费用 720→880
- 测量:手动录入箱规 880→1000,确认称重图片 920→1080
- 库位:新增/编辑库位 880→1000
- 同步 InboundFeeDetailManager、LocationsView 的宽度断言
2026-10-20 15:26:18 +08:00
14 changed files with 91 additions and 39 deletions
@@ -88,10 +88,10 @@ describe('simplified appointment entry', () => {
wrapper.unmount() wrapper.unmount()
}) })
it('does not request unreadable domains and describes the incomplete scope', async () => { it('does not request unreadable domains and describes the incomplete scope', async () => {
routing.denied.add('platform:fleet:trip:list'); routing.denied.add('platform:overseas-warehouse:last-mile-order:list') routing.denied.add('platform:overseas-warehouse:dispatch:list'); routing.denied.add('platform:overseas-warehouse:last-mile-order:list')
const wrapper = renderSchedule(); await flushPromises() const wrapper = renderSchedule(); await flushPromises()
expect(listTripCalendar).not.toHaveBeenCalled(); expect(getWarehouseArrivals).not.toHaveBeenCalled() expect(listTripCalendar).not.toHaveBeenCalled(); expect(getWarehouseArrivals).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('缺少车次读取权限') expect(wrapper.text()).toContain('缺少排车读取权限')
expect(wrapper.text()).toContain('缺少尾程订单读取权限') expect(wrapper.text()).toContain('缺少尾程订单读取权限')
wrapper.unmount() wrapper.unmount()
}) })
@@ -143,4 +143,17 @@ describe('simplified appointment entry', () => {
expect(routing.push).toHaveBeenCalledWith({ path: '/container-loading/overseas/1', query: { appointmentReturn: '/appointment-calendar?date=2026-10-21&mode=week' } }) expect(routing.push).toHaveBeenCalledWith({ path: '/container-loading/overseas/1', query: { appointmentReturn: '/appointment-calendar?date=2026-10-21&mode=week' } })
wrapper.unmount() wrapper.unmount()
}) })
it('opens the real dispatch detail with calendar context without requiring retired trip-list access', async () => {
routing.denied.add('platform:fleet:trip:list')
vi.mocked(listTripCalendar).mockResolvedValueOnce({ records: [{ sourceType: 'DISPATCH', dispatchNo: 'DS/1', status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'US-W', originWarehouseName: '美国仓', originCountryCode: 'US', originTimezone: 'America/Los_Angeles', originWarehouseStatus: 'ACTIVE', plannedDepartureAt: '2026-10-21T16:00:00Z', expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [], incompleteReasons: [] }], unresolvedRecords: [] })
const wrapper = renderSchedule(); await flushPromises()
const panel = wrapper.getComponent({ name: 'AppointmentCalendarPanel' })
const event = panel.props('events').find((value: ScheduleEvent) => value.type === 'dispatch')
expect(listTripCalendar).toHaveBeenCalledOnce()
expect(event.sourcePath).toBe('/overseas-warehouse/dispatches/DS%2F1')
panel.vm.$emit('select', '2026-10-21', 'week')
panel.vm.$emit('open', event); await flushPromises()
expect(routing.push).toHaveBeenCalledWith({ path: '/overseas-warehouse/dispatches/DS%2F1', query: { appointmentReturn: '/appointment-calendar?date=2026-10-21&mode=week' } })
wrapper.unmount()
})
}) })
@@ -64,12 +64,12 @@ async function load(next = range.value) {
} catch (cause) { fail('计划到仓读取失败', cause) } } catch (cause) { fail('计划到仓读取失败', cause) }
})(), })(),
(async () => { (async () => {
if (!access.can('platform:fleet:trip:list')) { notices.value.push('缺少车次读取权限,当前未显示排车安排。'); return } if (!access.can('platform:overseas-warehouse:dispatch:list')) { notices.value.push('缺少排车读取权限,当前未显示排车安排。'); return }
try { try {
const result = projectTripCalendar(await listTripCalendar(next, signal), next) const result = projectTripCalendar(await listTripCalendar(next, signal), next)
if (request !== generation) return if (request !== generation) return
trips.value = result.events trips.value = result.events
if (result.unresolved.length) notices.value.push(`${result.unresolved.length} 个车次缺少起运仓或时区资料,请到车辆管理核对。`) if (result.unresolved.length) notices.value.push(`${result.unresolved.length} 条排车资料不完整,请到排车列表核对。`)
} catch (cause) { fail('排车读取失败', cause) } } catch (cause) { fail('排车读取失败', cause) }
})(), })(),
]) ])
@@ -1,5 +1,11 @@
# 预约工作日历逐操作矩阵 # 预约工作日历逐操作矩阵
## 2026-10-20 排车入口收敛
用户指出预约日历仍跳旧车次详情。排车标签改用服务端关联真实整车主单返回的排车单号,打开 `/overseas-warehouse/dispatches/:dispatchNo` 独立排车详情;返回按钮显示“返回预约日历”并保留原日期与月/周视图。无合法日历返回参数时继续走原列表或拼车登记返回逻辑,外部返回地址拒绝。读取权限与排车列表一致,不要求旧车次列表权限;独立旧车次、取消排车主单不进入活动日历,不按TR号猜DS号。原四类、地点日期和实际发车归属规则保持。自动检查与发布/真实HTTP分别记录,1920×1080、100%人工操作截图待确认。
自动验证:相关3文件42项、Admin全量453文件4086项、类型检查与生产构建通过;后端最终常规Maven流程完成生产/测试源码编译,`FleetTripCalendarServiceTest`及真实H2查询的`FleetTripCalendarMapperTest`共9项通过,覆盖真实排车号与内部车次号不同、独立旧车次/取消主单/取货排除、跨日期和查询上限。Gateway路由合同5项通过。均经共享`local-check.ps1`执行;早先其他自有业务在途编译错误及失效生成物尝试不作为通过证据,以最终常规Maven回归为准。接口路径复用、无网关配置或数据库结构变更,匹配发布单元为Admin与Lastleg。
## 2026-10-19 四类日历恢复(当前结论) ## 2026-10-19 四类日历恢复(当前结论)
依据:用户本轮确认四类与日期规则,以及截图 `C:/Users/BY-OY/AppData/Local/Temp/codex-clipboard-840bcc07-6b54-4afa-9dc7-666d09ff04f2.png`。当前结论覆盖下文旧工作台首页/明细承载描述;原业务写入规则仍按正式业务契约。 依据:用户本轮确认四类与日期规则,以及截图 `C:/Users/BY-OY/AppData/Local/Temp/codex-clipboard-840bcc07-6b54-4afa-9dc7-666d09ff04f2.png`。当前结论覆盖下文旧工作台首页/明细承载描述;原业务写入规则仍按正式业务契约。
@@ -3,16 +3,24 @@ import { listTripCalendar, projectTripCalendar, tripQueryWindow, type TripCalend
import { http } from '@/shared/api/http' import { http } from '@/shared/api/http'
vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn() }, unwrapApiResponse: (value: {data: unknown}) => value.data })) vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn() }, unwrapApiResponse: (value: {data: unknown}) => value.data }))
const range = { from: '2026-10-01', to: '2026-11-01' } const range = { from: '2026-10-01', to: '2026-11-01' }
function trip(tripNo: string, plannedDepartureAt: string, originTimezone = 'America/Los_Angeles'): TripCalendarEntry { function trip(dispatchNo: string, plannedDepartureAt: string, originTimezone = 'America/Los_Angeles'): TripCalendarEntry {
return { sourceType: 'TRIP_PLAN', tripNo, status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'US-W', originWarehouseName: 'US warehouse', originCountryCode: 'US', originTimezone, originWarehouseStatus: 'ACTIVE', plannedDepartureAt, expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [{name:'US warehouse'}, {name:'FBA'}], incompleteReasons: [] } return { sourceType: 'DISPATCH', dispatchNo, status: 'PLANNED', originWarehouseId: '1', originWarehouseCode: 'US-W', originWarehouseName: 'US warehouse', originCountryCode: 'US', originTimezone, originWarehouseStatus: 'ACTIVE', plannedDepartureAt, expectedDriverEndAt: null, departedAt: null, completedAt: null, stops: [{name:'US warehouse'}, {name:'FBA'}], incompleteReasons: [] }
} }
describe('location-based trip calendar', () => { describe('location-based trip calendar', () => {
it('routes the canonical dispatch number to dispatch detail and does not invent a link without it', () => {
const row = trip('DS/REAL', '2026-10-10T12:00:00Z')
const result = projectTripCalendar({ records: [row], unresolvedRecords: [] }, range)
expect(result.events[0]).toMatchObject({ key: 'dispatch-DS/REAL', title: 'DS/REAL 排车', sourcePath: '/overseas-warehouse/dispatches/DS%2FREAL' })
const missing = projectTripCalendar({ records: [{ ...row, dispatchNo: '' }], unresolvedRecords: [] }, range)
expect(missing.events).toEqual([])
expect(missing.unresolved[0]!.incompleteReasons).toContain('DISPATCH_REFERENCE_UNKNOWN')
})
it('counts a shared trip once and excludes cancelled arrangements', () => { it('counts a shared trip once and excludes cancelled arrangements', () => {
const shared = trip('TR-SHARED', '2026-10-10T12:00:00Z') const shared = trip('DS-SHARED', '2026-10-10T12:00:00Z')
const cancelled = { ...trip('TR-CANCELLED', '2026-10-10T12:00:00Z'), status: 'CANCELLED' } const cancelled = { ...trip('DS-CANCELLED', '2026-10-10T12:00:00Z'), status: 'CANCELLED' }
const result = projectTripCalendar({ records: [shared, shared, cancelled], unresolvedRecords: [] }, range) const result = projectTripCalendar({ records: [shared, shared, cancelled], unresolvedRecords: [] }, range)
expect(result.events).toHaveLength(1) expect(result.events).toHaveLength(1)
expect(result.events[0]).toMatchObject({ type: 'dispatch', title: 'TR-SHARED 排车' }) expect(result.events[0]).toMatchObject({ type: 'dispatch', title: 'DS-SHARED 排车' })
}) })
it('queries one padded UTC window for 42 location dates without frontend page scanning', async () => { it('queries one padded UTC window for 42 location dates without frontend page scanning', async () => {
const localRange = {from:'2026-09-28',to:'2026-11-09'} const localRange = {from:'2026-09-28',to:'2026-11-09'}
@@ -25,23 +33,23 @@ describe('location-based trip calendar', () => {
it('includes the first and last local day and removes padded rows outside each origin date range', () => { it('includes the first and last local day and removes padded rows outside each origin date range', () => {
const records=[trip('LA-before','2026-10-01T06:30:00Z'),trip('NY-start','2026-10-01T06:30:00Z','America/New_York'),trip('LA-end','2026-11-01T06:30:00Z'),trip('NY-after','2026-11-01T06:30:00Z','America/New_York')] const records=[trip('LA-before','2026-10-01T06:30:00Z'),trip('NY-start','2026-10-01T06:30:00Z','America/New_York'),trip('LA-end','2026-11-01T06:30:00Z'),trip('NY-after','2026-11-01T06:30:00Z','America/New_York')]
const projected=projectTripCalendar({records,unresolvedRecords:[]},range) const projected=projectTripCalendar({records,unresolvedRecords:[]},range)
expect(projected.events.map(event=>[event.key,event.date,event.time])).toEqual([['trip-NY-start','2026-10-01','02:30'],['trip-LA-end','2026-10-31','23:30']]) expect(projected.events.map(event=>[event.key,event.date,event.time])).toEqual([['dispatch-NY-start','2026-10-01','02:30'],['dispatch-LA-end','2026-10-31','23:30']])
}) })
it('does not guess a slot for absent, fixed-offset, invalid or browser-unsupported time zones', () => { it('does not guess a slot for absent, fixed-offset, invalid or browser-unsupported time zones', () => {
const unknown={...trip('unknown','2026-10-10T12:00:00Z'),originTimezone:null,incompleteReasons:['ORIGIN_TIMEZONE_UNKNOWN']} const unknown={...trip('unknown','2026-10-10T12:00:00Z'),originTimezone:null,incompleteReasons:['ORIGIN_TIMEZONE_UNKNOWN']}
const result=projectTripCalendar({records:[trip('offset','2026-10-10T12:00:00Z','UTC-5'),trip('bad','2026-10-10T12:00:00Z','Mars/Base'),trip('naive','2026-10-10T12:00:00')],unresolvedRecords:[unknown]},range) const result=projectTripCalendar({records:[trip('offset','2026-10-10T12:00:00Z','UTC-5'),trip('bad','2026-10-10T12:00:00Z','Mars/Base'),trip('naive','2026-10-10T12:00:00')],unresolvedRecords:[unknown]},range)
expect(result.events).toEqual([]);expect(result.unresolved.map(row=>row.tripNo)).toEqual(['unknown','offset','bad','naive']) expect(result.events).toEqual([]);expect(result.unresolved.map(row=>row.dispatchNo)).toEqual(['unknown','offset','bad','naive'])
}) })
it('uses actual local departure through DST changes while retaining the original plan for comparison', () => { it('uses actual local departure through DST changes while retaining the original plan for comparison', () => {
const row={...trip('TR-DST','2026-11-01T05:30:00Z','America/New_York'),departedAt:'2026-11-02T05:00:00Z',stops:Array.from({length:20},(_,i)=>({name:`Stop ${i}`}))} const row={...trip('DS-DST','2026-11-01T05:30:00Z','America/New_York'),departedAt:'2026-11-02T05:00:00Z',stops:Array.from({length:20},(_,i)=>({name:`Stop ${i}`}))}
const events=projectTripCalendar({records:[row,trip('TR-DST-2','2026-11-01T06:30:00Z','America/New_York')],unresolvedRecords:[]},{from:'2026-11-01',to:'2026-11-08'}).events const events=projectTripCalendar({records:[row,trip('DS-DST-2','2026-11-01T06:30:00Z','America/New_York')],unresolvedRecords:[]},{from:'2026-11-01',to:'2026-11-08'}).events
expect(events.map(event=>event.time)).toEqual(['00:00','01:30']) expect(events.map(event=>event.time)).toEqual(['00:00','01:30'])
expect(events[0]).toMatchObject({date:'2026-11-02',title:'TR-DST 实际发车'}) expect(events[0]).toMatchObject({date:'2026-11-02',title:'DS-DST 实际发车'})
expect(events[0]!.fields).toContainEqual({label:'计划出发(UTC)',value:'2026-11-01T05:30:00Z'}) expect(events[0]!.fields).toContainEqual({label:'计划出发(UTC)',value:'2026-11-01T05:30:00Z'})
expect(events[0]!.fields).toContainEqual({label:'路线',value:'Stop 0 → Stop 1 → Stop 2 等20站'}) expect(events[0]!.fields).toContainEqual({label:'路线',value:'Stop 0 → Stop 1 → Stop 2 等20站'})
}) })
it('keeps executed trips in the actual month after replanning and excludes their old plan month', () => { it('keeps executed trips in the actual month after replanning and excludes their old plan month', () => {
const executed = {...trip('TR-ACTUAL', '2026-10-31T12:00:00Z'), departedAt:'2026-11-01T12:00:00Z'} const executed = {...trip('DS-ACTUAL', '2026-10-31T12:00:00Z'), departedAt:'2026-11-01T12:00:00Z'}
expect(projectTripCalendar({records:[executed],unresolvedRecords:[]}, range).events).toEqual([]) expect(projectTripCalendar({records:[executed],unresolvedRecords:[]}, range).events).toEqual([])
const november = {from:'2026-11-01',to:'2026-12-01'} const november = {from:'2026-11-01',to:'2026-12-01'}
const event = projectTripCalendar({records:[executed],unresolvedRecords:[]}, november).events[0] const event = projectTripCalendar({records:[executed],unresolvedRecords:[]}, november).events[0]
@@ -50,9 +58,9 @@ describe('location-based trip calendar', () => {
expect(projectTripCalendar({records:[changed],unresolvedRecords:[]},november).events[0]).toMatchObject({date:event!.date,time:event!.time}) expect(projectTripCalendar({records:[changed],unresolvedRecords:[]},november).events[0]).toMatchObject({date:event!.date,time:event!.time})
}) })
it('does not fall back to the plan when an actual departure cannot be resolved', () => { it('does not fall back to the plan when an actual departure cannot be resolved', () => {
const invalid = {...trip('TR-INVALID', '2026-10-15T12:00:00Z'), departedAt:'2026-10-16T12:00:00'} const invalid = {...trip('DS-INVALID', '2026-10-15T12:00:00Z'), departedAt:'2026-10-16T12:00:00'}
const result = projectTripCalendar({records:[invalid],unresolvedRecords:[]},range) const result = projectTripCalendar({records:[invalid],unresolvedRecords:[]},range)
expect(result.events).toEqual([]) expect(result.events).toEqual([])
expect(result.unresolved.map(row=>row.tripNo)).toEqual(['TR-INVALID']) expect(result.unresolved.map(row=>row.dispatchNo)).toEqual(['DS-INVALID'])
}) })
}) })
@@ -2,8 +2,8 @@ import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { ScheduleEvent } from './schedule' import type { ScheduleEvent } from './schedule'
export interface TripCalendarEntry { export interface TripCalendarEntry {
sourceType: 'TRIP_PLAN' sourceType: 'DISPATCH'
tripNo: string dispatchNo: string
status: string status: string
originWarehouseId: string | null originWarehouseId: string | null
originWarehouseCode: string | null originWarehouseCode: string | null
@@ -23,6 +23,7 @@ export const tripIssueLabels: Record<string, string> = {
ORIGIN_WAREHOUSE_UNKNOWN: '起运仓资料缺失', ORIGIN_COUNTRY_UNKNOWN: '起运国家待核实', ORIGIN_WAREHOUSE_UNKNOWN: '起运仓资料缺失', ORIGIN_COUNTRY_UNKNOWN: '起运国家待核实',
ORIGIN_TIMEZONE_UNKNOWN: '起运仓时区待核实', ROUTE_INCOMPLETE: '路线资料不完整', ORIGIN_TIMEZONE_UNKNOWN: '起运仓时区待核实', ROUTE_INCOMPLETE: '路线资料不完整',
LOCAL_DATE_UNRESOLVED: '无法确定当地日期', LOCAL_DATE_UNRESOLVED: '无法确定当地日期',
DISPATCH_REFERENCE_UNKNOWN: '排车单号缺失',
} }
export function tripIssues(row: TripCalendarEntry) { export function tripIssues(row: TripCalendarEntry) {
return row.incompleteReasons.map(reason => tripIssueLabels[reason] || '资料待核实').join(';') return row.incompleteReasons.map(reason => tripIssueLabels[reason] || '资料待核实').join(';')
@@ -56,7 +57,13 @@ function localStamp(value: string | null, zone: string | null) {
export function projectTripCalendar(result: TripCalendarResult, range: { from: string; to: string }) { export function projectTripCalendar(result: TripCalendarResult, range: { from: string; to: string }) {
const events: ScheduleEvent[] = [], unresolved = [...result.unresolvedRecords] const events: ScheduleEvent[] = [], unresolved = [...result.unresolvedRecords]
const labels: Record<string, string> = { PLANNED: '计划中', IN_TRANSIT: '运输中', COMPLETED: '货物已结案', CANCELLED: '已取消' } const labels: Record<string, string> = { PLANNED: '计划中', IN_TRANSIT: '运输中', COMPLETED: '货物已结案', CANCELLED: '已取消' }
for (const row of new Map(result.records.map(row => [row.tripNo, row])).values()) { const seen = new Set<string>()
for (const row of result.records) {
if (!row.dispatchNo?.trim()) {
unresolved.push({ ...row, incompleteReasons: [...row.incompleteReasons, 'DISPATCH_REFERENCE_UNKNOWN'] }); continue
}
if (seen.has(row.dispatchNo)) continue
seen.add(row.dispatchNo)
if (row.status === 'CANCELLED') continue if (row.status === 'CANCELLED') continue
// Executed work stays on its actual local date even if the plan changes later. // Executed work stays on its actual local date even if the plan changes later.
const occurrence = localStamp(row.departedAt ?? row.plannedDepartureAt, row.originTimezone) const occurrence = localStamp(row.departedAt ?? row.plannedDepartureAt, row.originTimezone)
@@ -66,11 +73,11 @@ export function projectTripCalendar(result: TripCalendarResult, range: { from: s
if (occurrence.date < range.from || occurrence.date >= range.to) continue if (occurrence.date < range.from || occurrence.date >= range.to) continue
const actual = localStamp(row.departedAt, row.originTimezone) const actual = localStamp(row.departedAt, row.originTimezone)
const driverEnd = localStamp(row.expectedDriverEndAt, row.originTimezone) const driverEnd = localStamp(row.expectedDriverEndAt, row.originTimezone)
events.push({ key: `trip-${row.tripNo}`, type: 'dispatch', date: occurrence.date, time: occurrence.time, events.push({ key: `dispatch-${row.dispatchNo}`, type: 'dispatch', date: occurrence.date, time: occurrence.time,
title: `${row.tripNo} ${row.departedAt ? '实际发车' : '排车'}`, subtitle: row.departedAt ? '按起运仓当地实际发车时间展示' : '海外仓送往 FBA/客户仓的车辆安排', title: `${row.dispatchNo} ${row.departedAt ? '实际发车' : '排车'}`, subtitle: row.departedAt ? '按起运仓当地实际发车时间展示' : '海外仓送往 FBA/客户仓的车辆安排',
sourcePath: `/overseas-master-data/trips/${encodeURIComponent(row.tripNo)}`, sourcePath: `/overseas-warehouse/dispatches/${encodeURIComponent(row.dispatchNo)}`,
fields: [ fields: [
{ label: '车次状态', value: labels[row.status] || '待核实' }, { label: '运输状态', value: labels[row.status] || '待核实' },
{ label: '起运仓', value: row.originWarehouseName || row.originWarehouseCode }, { label: '起运仓', value: row.originWarehouseName || row.originWarehouseCode },
{ label: '地点时区', value: row.originTimezone }, { label: '地点时区', value: row.originTimezone },
{ label: '计划出发(UTC)', value: row.plannedDepartureAt }, { label: '计划出发(UTC)', value: row.plannedDepartureAt },
@@ -885,7 +885,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
</div> </div>
</AppDialog> </AppDialog>
<el-dialog class="admin-density-dialog" v-model="manualEntryOpen" :title="detail?.inboundCode ? `手动录入箱规 · ${detail.inboundCode}` : '手动录入箱规'" width="min(880px, 92vw)" destroy-on-close append-to-body> <el-dialog class="admin-density-dialog" v-model="manualEntryOpen" :title="detail?.inboundCode ? `手动录入箱规 · ${detail.inboundCode}` : '手动录入箱规'" width="min(1000px, 92vw)" destroy-on-close append-to-body>
<el-alert v-if="manualEntryErrors.length" type="error" :closable="false" show-icon class="manual-entry-alert" title="请修正以下问题"> <el-alert v-if="manualEntryErrors.length" type="error" :closable="false" show-icon class="manual-entry-alert" title="请修正以下问题">
<ul><li v-for="error in manualEntryErrors" :key="error">{{ error }}</li></ul> <ul><li v-for="error in manualEntryErrors" :key="error">{{ error }}</li></ul>
</el-alert> </el-alert>
@@ -656,7 +656,7 @@ defineExpose({ reload: loadWorkspace })
:show-confirm="Boolean(previewResult && previewCanConfirm && (previewResult.recognitionStatus !== 'CONFIRMED' || reviewCorrectionEnabled))" :show-confirm="Boolean(previewResult && previewCanConfirm && (previewResult.recognitionStatus !== 'CONFIRMED' || reviewCorrectionEnabled))"
:confirm-text="previewConfirmText" :confirm-text="previewConfirmText"
confirm-type="success" confirm-type="success"
width="min(920px, calc(100vw - 28px))" width="min(1080px, calc(100vw - 28px))"
fullscreenable fullscreenable
@confirm="previewResult && confirmResult(previewResult, true)" @confirm="previewResult && confirmResult(previewResult, true)"
> >
@@ -19,6 +19,21 @@ vi.mock('@/modules/overseas-master-data', async () => ({
const options = { global: { plugins: [ElementPlus], stubs: { DataTable: DataTableStub, DispatchWorkflowDialog: true } } } const options = { global: { plugins: [ElementPlus], stubs: { DataTable: DataTableStub, DispatchWorkflowDialog: true } } }
beforeEach(() => { vi.clearAllMocks(); vi.mocked(getDispatch).mockResolvedValue(detail()); can.mockReturnValue(false); push.mockReset(); Object.keys(query).forEach(key => delete query[key]) }) beforeEach(() => { vi.clearAllMocks(); vi.mocked(getDispatch).mockResolvedValue(detail()); can.mockReturnValue(false); push.mockReset(); Object.keys(query).forEach(key => delete query[key]) })
describe('whole-dispatch four-tab detail', () => { describe('whole-dispatch four-tab detail', () => {
it('returns to the original appointment date and view from the dispatch detail', async () => {
query.appointmentReturn = '/appointment-calendar?date=2026-10-21&mode=week'
const wrapper = mount(DispatchDetailView, options); await flushPromises()
expect(wrapper.get('button.density-back').text()).toContain('返回预约日历')
await wrapper.get('button.density-back').trigger('click')
expect(push).toHaveBeenCalledWith('/appointment-calendar?date=2026-10-21&mode=week')
wrapper.unmount()
})
it('ignores an external calendar return URL and keeps the normal dispatch-list return', async () => {
query.appointmentReturn = 'https://outside.invalid/appointment-calendar?date=2026-10-21'
const wrapper = mount(DispatchDetailView, options); await flushPromises()
await wrapper.get('button.density-back').trigger('click')
expect(push).toHaveBeenCalledWith({ name: 'overseas-dispatches' })
wrapper.unmount()
})
it('uses only four accessible colored tabs and switches without another network read', async () => { it('uses only four accessible colored tabs and switches without another network read', async () => {
const value = detail(); value.dispatchInfo.fbaAppointments = [{ batchKey: 'FBA_APPOINTMENT:1', batchNo: 'BATCH-1', appointmentNo: 'FBA-AP-1', destinationKey: 'FBA:ONT8', destinationWarehouseCode: 'ONT8', status: 'CONFIRMED', confirmedAt: null, blockReason: null }] const value = detail(); value.dispatchInfo.fbaAppointments = [{ batchKey: 'FBA_APPOINTMENT:1', batchNo: 'BATCH-1', appointmentNo: 'FBA-AP-1', destinationKey: 'FBA:ONT8', destinationWarehouseCode: 'ONT8', status: 'CONFIRMED', confirmedAt: null, blockReason: null }]
vi.mocked(getDispatch).mockResolvedValueOnce(value) vi.mocked(getDispatch).mockResolvedValueOnce(value)
@@ -3,6 +3,7 @@ import '@/styles/adminDensity.css'
import { computed, onBeforeUnmount, ref, watch } from 'vue' import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { inquiryReturnTarget, vehicleStatusLabels } from '@/modules/overseas-master-data' import { inquiryReturnTarget, vehicleStatusLabels } from '@/modules/overseas-master-data'
import { appointmentReturnTarget } from '@/modules/appointment-calendar'
import { useAccessPreviewStore } from '@/core/access/accessPreview' import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -65,7 +66,9 @@ async function load() {
catch (error) { if (current === generation) loadError.value = getApiErrorDetails(error, '排车详情加载失败,请重试') } catch (error) { if (current === generation) loadError.value = getApiErrorDetails(error, '排车详情加载失败,请重试') }
finally { if (current === generation) loading.value = false } finally { if (current === generation) loading.value = false }
} }
function back() { void router.push(inquiryReturnTarget(route.query) || { name: 'overseas-dispatches' }) } const appointmentReturn = computed(() => appointmentReturnTarget(route.query))
const backLabel = computed(() => appointmentReturn.value ? '返回预约日历' : inquiryReturnTarget(route.query) ? '返回原拼车登记' : '返回排车列表')
function back() { void router.push(appointmentReturn.value || inquiryReturnTarget(route.query) || { name: 'overseas-dispatches' }) }
function openAction(value: DispatchAction, requestedOperation?: 'ROUTE') { if (!canDispatchAction(detail.value?.actions, value, access.can)) return; rejectionError.value = null; operation.value = requestedOperation; action.value = value; actionOpen.value = true } function openAction(value: DispatchAction, requestedOperation?: 'ROUTE') { if (!canDispatchAction(detail.value?.actions, value, access.can)) return; rejectionError.value = null; operation.value = requestedOperation; action.value = value; actionOpen.value = true }
async function handleRejection(details: ApiErrorDetails) { rejectionError.value = details; await load() } async function handleRejection(details: ApiErrorDetails) { rejectionError.value = details; await load() }
async function handleSaved() { rejectionError.value = null; await load() } async function handleSaved() { rejectionError.value = null; await load() }
@@ -74,7 +77,7 @@ onBeforeUnmount(() => { generation++ })
</script> </script>
<template> <template>
<OverseasDetailShell class="admin-density" dense title="排车详情" reference-label="排车单号" :reference-value="detail?.dispatchNo || dispatchNo" :back-text="inquiryReturnTarget(route.query) ? '返回原拼车登记' : undefined" :back-label="inquiryReturnTarget(route.query) ? '返回原拼车登记' : '返回排车列表'" domain="排车详情" :pending="false" :status-text="detail ? detail.status === 'CANCELLED' ? '已取消' : stageLabel(detail.stage) : '加载中'" @back="back"> <OverseasDetailShell class="admin-density" dense title="排车详情" reference-label="排车单号" :reference-value="detail?.dispatchNo || dispatchNo" :back-text="appointmentReturn || inquiryReturnTarget(route.query) ? backLabel : undefined" :back-label="backLabel" domain="排车详情" :pending="false" :status-text="detail ? detail.status === 'CANCELLED' ? '已取消' : stageLabel(detail.stage) : '加载中'" @back="back">
<LoadErrorState v-if="rejectionError" title="排车操作未完成" :message="rejectionError.message" :show-retry="false"><template #details><ApiErrorMeta :details="rejectionError" /></template></LoadErrorState> <LoadErrorState v-if="rejectionError" title="排车操作未完成" :message="rejectionError.message" :show-retry="false"><template #details><ApiErrorMeta :details="rejectionError" /></template></LoadErrorState>
<LoadErrorState v-if="loadError" title="排车详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState> <LoadErrorState v-if="loadError" title="排车详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
<div v-else v-loading="loading" class="dispatch-detail" :aria-busy="loading"> <div v-else v-loading="loading" class="dispatch-detail" :aria-busy="loading">
@@ -194,7 +194,7 @@ describe('InboundFeeDetailManager', () => {
const dialog = document.querySelector<HTMLElement>('.el-dialog.admin-density-dialog')! const dialog = document.querySelector<HTMLElement>('.el-dialog.admin-density-dialog')!
expect(dialog).not.toBeNull() expect(dialog).not.toBeNull()
expect(dialog.querySelector('.density-form')).not.toBeNull() expect(dialog.querySelector('.density-form')).not.toBeNull()
expect(dialog.style.getPropertyValue('--el-dialog-width')).toBe(action === '编辑' ? 'min(880px, calc(100vw - 28px))' : 'min(720px, calc(100vw - 28px))') expect(dialog.style.getPropertyValue('--el-dialog-width')).toBe(action === '编辑' ? 'min(1000px, calc(100vw - 28px))' : 'min(880px, calc(100vw - 28px))')
if (action === '驳回') { if (action === '驳回') {
const confirm = [...dialog.querySelectorAll<HTMLButtonElement>('button')].find(button => button.textContent?.trim() === '确认驳回')! const confirm = [...dialog.querySelectorAll<HTMLButtonElement>('button')].find(button => button.textContent?.trim() === '确认驳回')!
confirm.click() confirm.click()
@@ -336,7 +336,7 @@ onMounted(() => void load())
</div> </div>
</template> </template>
<AppDialog class="admin-density-dialog" v-model="editorOpen" :title="editorMode === 'create' ? '新增费用' : editorMode === 'adjust' ? '调整已审核费用' : '编辑费用'" width="min(880px, calc(100vw - 28px))" :confirm-loading="saving" confirm-text="保存" @confirm="save"> <AppDialog class="admin-density-dialog" v-model="editorOpen" :title="editorMode === 'create' ? '新增费用' : editorMode === 'adjust' ? '调整已审核费用' : '编辑费用'" width="min(1000px, calc(100vw - 28px))" :confirm-loading="saving" confirm-text="保存" @confirm="save">
<el-alert v-if="editorMode === 'adjust'" title="调整将生成新版本并重新进入待审核状态" type="warning" :closable="false" show-icon /> <el-alert v-if="editorMode === 'adjust'" title="调整将生成新版本并重新进入待审核状态" type="warning" :closable="false" show-icon />
<el-alert v-if="selectedFeeItem?.feeCode === privateDeliveryFeeCode" title="私人仓尾程派送费仅适用于目的仓类型为“私人仓”的订单,最终资格由服务端校验。" type="info" :closable="false" show-icon /> <el-alert v-if="selectedFeeItem?.feeCode === privateDeliveryFeeCode" title="私人仓尾程派送费仅适用于目的仓类型为“私人仓”的订单,最终资格由服务端校验。" type="info" :closable="false" show-icon />
<el-alert v-if="!canViewFeeItems" title="当前账号缺少费用项目查看权限,无法新增或修改费用" type="warning" :closable="false" show-icon /> <el-alert v-if="!canViewFeeItems" title="当前账号缺少费用项目查看权限,无法新增或修改费用" type="warning" :closable="false" show-icon />
@@ -369,7 +369,7 @@ onMounted(() => void load())
</el-form> </el-form>
</section> </section>
</AppDialog> </AppDialog>
<AppDialog class="admin-density-dialog" v-model="rejectOpen" title="驳回费用" :confirm-loading="saving" confirm-text="确认驳回" confirm-type="warning" width="min(720px, calc(100vw - 28px))" @confirm="reject"> <AppDialog class="admin-density-dialog" v-model="rejectOpen" title="驳回费用" :confirm-loading="saving" confirm-text="确认驳回" confirm-type="warning" width="min(880px, calc(100vw - 28px))" @confirm="reject">
<el-alert v-if="actionError" type="error" :closable="true" @close="actionError = null"><template #title>{{ actionError.message }}</template><ApiErrorMeta :details="actionError" /></el-alert> <el-alert v-if="actionError" type="error" :closable="true" @close="actionError = null"><template #title>{{ actionError.message }}</template><ApiErrorMeta :details="actionError" /></el-alert>
<el-form class="density-form" label-position="top"><el-form-item label="驳回原因" required><el-input v-model="rejectReason" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入驳回原因" /></el-form-item></el-form> <el-form class="density-form" label-position="top"><el-form-item label="驳回原因" required><el-input v-model="rejectReason" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入驳回原因" /></el-form-item></el-form>
</AppDialog> </AppDialog>
@@ -2931,7 +2931,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</template> </template>
</ListPageLayout> </ListPageLayout>
<AppDialog class="admin-density-dialog" v-model="legacyImportOpen" title="导入旧系统订单" width="min(760px, calc(100vw - 28px))" confirm-text="开始导入" :confirm-loading="legacyImportSaving" :confirm-disabled="!legacyImportFile || legacyImportSaving || !!legacyImportResult" destroy-on-close @confirm="submitLegacyImport"> <AppDialog class="admin-density-dialog" v-model="legacyImportOpen" title="导入旧系统订单" width="min(880px, calc(100vw - 28px))" confirm-text="开始导入" :confirm-loading="legacyImportSaving" :confirm-disabled="!legacyImportFile || legacyImportSaving || !!legacyImportResult" destroy-on-close @confirm="submitLegacyImport">
<div class="legacy-import-content density-dialog-sections"> <div class="legacy-import-content density-dialog-sections">
<section class="density-dialog-section" data-tone="source"> <section class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">导入规则</h3> <h3 class="density-dialog-section__title">导入规则</h3>
@@ -2981,7 +2981,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</section> </section>
</template> </template>
</AppDialog> </AppDialog>
<AppDialog class="admin-density-dialog" v-model="coLoadCreateOpen" title="合柜报关" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="!canCreateCoLoadGroup" width="min(720px, calc(100vw - 28px))" @confirm="createCoLoadGroup"> <AppDialog class="admin-density-dialog" v-model="coLoadCreateOpen" title="合柜报关" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="!canCreateCoLoadGroup" width="min(880px, calc(100vw - 28px))" @confirm="createCoLoadGroup">
<el-alert type="warning" :closable="false" show-icon :title="`所选 ${selectedOrders.length} 票须属于同一国内仓、同一渠道,并全量进入同一柜;目的仓或目的港可以不同。资格由服务端按完整入仓片段校验。`" /> <el-alert type="warning" :closable="false" show-icon :title="`所选 ${selectedOrders.length} 票须属于同一国内仓、同一渠道,并全量进入同一柜;目的仓或目的港可以不同。资格由服务端按完整入仓片段校验。`" />
<section class="density-dialog-section" data-tone="source"> <section class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">所选入仓码</h3> <h3 class="density-dialog-section__title">所选入仓码</h3>
@@ -2989,7 +2989,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</section> </section>
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div> <div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
</AppDialog> </AppDialog>
<AppDialog class="admin-density-dialog" v-model="coLoadDetailOpen" title="同柜组成员" :show-confirm="false" cancel-text="关闭" width="min(720px, 94vw)" :loading="coLoadDetailLoading" :confirm-loading="coLoadCancelSaving"> <AppDialog class="admin-density-dialog" v-model="coLoadDetailOpen" title="同柜组成员" :show-confirm="false" cancel-text="关闭" width="min(880px, 94vw)" :loading="coLoadDetailLoading" :confirm-loading="coLoadCancelSaving">
<LoadErrorState v-if="coLoadDetailError" title="同柜组成员加载失败" :message="coLoadDetailError.message" :loading="coLoadDetailLoading" @retry="loadCoLoadDetail"><template #details><ApiErrorMeta :details="coLoadDetailError" /></template></LoadErrorState> <LoadErrorState v-if="coLoadDetailError" title="同柜组成员加载失败" :message="coLoadDetailError.message" :loading="coLoadDetailLoading" @retry="loadCoLoadDetail"><template #details><ApiErrorMeta :details="coLoadDetailError" /></template></LoadErrorState>
<template v-else-if="coLoadDetailSummary"> <template v-else-if="coLoadDetailSummary">
<div class="coload-detail-summary"><el-tag size="small" :type="coLoadDetailSummary.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroupSummary(coLoadDetailSummary) }}</el-tag><span>{{ coLoadDetailSummary.memberCount }} 个成员</span></div> <div class="coload-detail-summary"><el-tag size="small" :type="coLoadDetailSummary.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroupSummary(coLoadDetailSummary) }}</el-tag><span>{{ coLoadDetailSummary.memberCount }} 个成员</span></div>
@@ -3019,7 +3019,7 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
/> />
</AppDrawer> </AppDrawer>
<AppDialog class="admin-density-dialog" v-model="noteOpen" :title="orderDialogTitle('编辑订单备注', noteOrder?.inboundCode)" width="min(880px, calc(100vw - 28px))" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote"> <AppDialog class="admin-density-dialog" v-model="noteOpen" :title="orderDialogTitle('编辑订单备注', noteOrder?.inboundCode)" width="min(1000px, calc(100vw - 28px))" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
<el-form label-position="top" class="order-remarks-form"> <el-form label-position="top" class="order-remarks-form">
<el-form-item label="客户备注(只读)"><el-input :model-value="noteOrder?.customerRemark || '—'" disabled /></el-form-item> <el-form-item label="客户备注(只读)"><el-input :model-value="noteOrder?.customerRemark || '—'" disabled /></el-form-item>
<el-form-item label="内部备注"><el-input v-model="noteForm.internalRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit /></el-form-item> <el-form-item label="内部备注"><el-input v-model="noteForm.internalRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit /></el-form-item>
@@ -3028,12 +3028,12 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
</el-form> </el-form>
</AppDialog> </AppDialog>
<AppDialog class="admin-density-dialog" v-model="holdDialogOpen" :title="orderDialogTitle(holdAction === 'HOLD' ? '确认扣货' : '确认解除扣货', detail?.summary.inboundCode)" :confirm-text="holdAction === 'HOLD' ? '确认扣货' : '确认解除'" :confirm-type="holdAction === 'HOLD' ? 'danger' : 'primary'" :confirm-loading="holdSaving" :confirm-disabled="!holdReason.trim() || holdReason.trim().length > 1000" width="min(720px, calc(100vw - 28px))" destroy-on-close @confirm="submitHoldAction"> <AppDialog class="admin-density-dialog" v-model="holdDialogOpen" :title="orderDialogTitle(holdAction === 'HOLD' ? '确认扣货' : '确认解除扣货', detail?.summary.inboundCode)" :confirm-text="holdAction === 'HOLD' ? '确认扣货' : '确认解除'" :confirm-type="holdAction === 'HOLD' ? 'danger' : 'primary'" :confirm-loading="holdSaving" :confirm-disabled="!holdReason.trim() || holdReason.trim().length > 1000" width="min(880px, calc(100vw - 28px))" destroy-on-close @confirm="submitHoldAction">
<el-alert :title="holdAction === 'HOLD' ? '扣货后服务端将阻止后续实物履约操作。' : '解除后是否可继续履约仍以服务端状态为准。'" :type="holdAction === 'HOLD' ? 'warning' : 'info'" :closable="false" show-icon /> <el-alert :title="holdAction === 'HOLD' ? '扣货后服务端将阻止后续实物履约操作。' : '解除后是否可继续履约仍以服务端状态为准。'" :type="holdAction === 'HOLD' ? 'warning' : 'info'" :closable="false" show-icon />
<el-form label-position="top" class="hold-command-form"><el-form-item :label="holdAction === 'HOLD' ? '扣货原因' : '解除原因'" required><el-input v-model="holdReason" type="textarea" :rows="4" maxlength="1000" show-word-limit :placeholder="holdAction === 'HOLD' ? '请输入扣货原因' : '请输入解除扣货原因'" aria-label="扣货操作原因" /></el-form-item><el-form-item label="关联文件(选填)"><el-upload :key="holdEvidenceUploadKey" :auto-upload="false" :limit="1" accept=".txt,.pdf,.jpg,.jpeg,.png,text/plain,application/pdf,image/jpeg,image/png" :disabled="holdSaving" :on-change="selectHoldEvidenceFile" :on-remove="removeHoldEvidenceFile"><el-button :disabled="holdSaving"><FileUp :size="15" />选择文件</el-button><template #tip><div class="el-upload__tip">仅支持 TXT、PDF、JPG、PNG,单文件不超过 10MB。</div></template></el-upload></el-form-item><LoadErrorState v-if="holdEvidenceError" compact title="关联文件处理失败" :message="holdEvidenceError.message" :show-retry="false"><template #details><ApiErrorMeta :details="holdEvidenceError" /></template></LoadErrorState></el-form> <el-form label-position="top" class="hold-command-form"><el-form-item :label="holdAction === 'HOLD' ? '扣货原因' : '解除原因'" required><el-input v-model="holdReason" type="textarea" :rows="4" maxlength="1000" show-word-limit :placeholder="holdAction === 'HOLD' ? '请输入扣货原因' : '请输入解除扣货原因'" aria-label="扣货操作原因" /></el-form-item><el-form-item label="关联文件(选填)"><el-upload :key="holdEvidenceUploadKey" :auto-upload="false" :limit="1" accept=".txt,.pdf,.jpg,.jpeg,.png,text/plain,application/pdf,image/jpeg,image/png" :disabled="holdSaving" :on-change="selectHoldEvidenceFile" :on-remove="removeHoldEvidenceFile"><el-button :disabled="holdSaving"><FileUp :size="15" />选择文件</el-button><template #tip><div class="el-upload__tip">仅支持 TXT、PDF、JPG、PNG,单文件不超过 10MB。</div></template></el-upload></el-form-item><LoadErrorState v-if="holdEvidenceError" compact title="关联文件处理失败" :message="holdEvidenceError.message" :show-retry="false"><template #details><ApiErrorMeta :details="holdEvidenceError" /></template></LoadErrorState></el-form>
</AppDialog> </AppDialog>
<AppDialog class="admin-density-dialog" v-model="approvalOpen" :title="orderDialogTitle('审核订单', approvalOrder?.inboundCode)" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" width="min(720px, calc(100vw - 28px))" destroy-on-close @confirm="approveOrder" @cancel="closeApproval"> <AppDialog class="admin-density-dialog" v-model="approvalOpen" :title="orderDialogTitle('审核订单', approvalOrder?.inboundCode)" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" width="min(880px, calc(100vw - 28px))" destroy-on-close @confirm="approveOrder" @cancel="closeApproval">
<section v-if="approvalOrder" class="density-dialog-section" data-tone="summary"> <section v-if="approvalOrder" class="density-dialog-section" data-tone="summary">
<h3 class="density-dialog-section__title">订单审核资料</h3> <h3 class="density-dialog-section__title">订单审核资料</h3>
<dl class="approval-facts"> <dl class="approval-facts">
@@ -162,7 +162,7 @@ describe('LocationsView', () => {
it('uses the prototype-sized centered editor without a detail drawer or section cards', () => { it('uses the prototype-sized centered editor without a detail drawer or section cards', () => {
expect(locationsSource).not.toContain('AppDrawer') expect(locationsSource).not.toContain('AppDrawer')
expect(locationsSource).not.toContain('FormSection') expect(locationsSource).not.toContain('FormSection')
expect(locationsSource).toContain('width="min(880px, calc(100vw - 28px))"') expect(locationsSource).toContain('width="min(1000px, calc(100vw - 28px))"')
expect(densitySource).toContain('.admin-density-dialog.el-dialog .el-dialog__body { min-height: 0; overflow-y: auto;') expect(densitySource).toContain('.admin-density-dialog.el-dialog .el-dialog__body { min-height: 0; overflow-y: auto;')
expect(locationsSource).toContain('.editor-field-grid { grid-template-columns: minmax(0, 1fr); }') expect(locationsSource).toContain('.editor-field-grid { grid-template-columns: minmax(0, 1fr); }')
}) })
@@ -600,7 +600,7 @@ onMounted(() => {
<AppDialog <AppDialog
v-model="editorOpen" v-model="editorOpen"
:title="editorMode === 'edit' && form.locationCode ? `${editorTitle} · ${form.locationCode}` : editorTitle" :title="editorMode === 'edit' && form.locationCode ? `${editorTitle} · ${form.locationCode}` : editorTitle"
width="min(880px, calc(100vw - 28px))" width="min(1000px, calc(100vw - 28px))"
class="location-editor-dialog admin-density-dialog" class="location-editor-dialog admin-density-dialog"
:confirm-loading="saving" :confirm-loading="saving"
:close-on-click-modal="false" :close-on-click-modal="false"