style(admin): unify dialog layouts and dense readonly presentation

This commit is contained in:
2026-10-20 10:11:31 +08:00
parent 7ff7c9914f
commit 3267679758
214 changed files with 10931 additions and 2654 deletions
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import ElementPlus from 'element-plus'
import Panel from './ContainerPortSchedulePanel.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import { getPortSchedule, createPortSchedule, updatePortSchedule } from '../api/platformPortSchedules'
import { ApiResponseError } from '@/shared/api/http'
const can = vi.hoisted(() => vi.fn(() => true))
@@ -11,18 +12,25 @@ const container = { id: 'C1', customerId: '0', customerName: '平台订舱', oce
const original = { id: 'S1', scheduleNo: 'PS1', version: 5, customerId: '0', oceanBookingId: 'B1', containerBookingId: 'C1', sourceType: 'CARRIER', sourceName: '官网', sourceRef: 'REF', vesselName: 'OLD', voyageNo: 'V1', departurePortCode: 'CNSHA', destinationPortCode: 'USLAX', etdAt: container.estimatedSailingAt, etaAt: container.estimatedPortArrivalAt, observedAt: '2026-10-01T00:00:00', expiresAt: '2026-10-10T00:00:00', portCutoffAt: '2026-10-03T00:00:00', remark: '原备注' }
const result = () => ({ portSchedule: { schedule: { ...original }, expired: false }, histories: [] })
interface VM { begin(): Promise<void>; save(): Promise<void>; form: Record<string, string>; error: string; open: boolean; schedule: ReturnType<typeof result> | null }
function setup(value = container) {
const wrapper = mount(Panel, { props: { container: value }, global: { plugins: [ElementPlus], stubs: { AppDialog: { props: ['modelValue'], template: '<div v-if="modelValue"><slot /></div>' } } } })
function setup(value = container, realDialog = false) {
const wrapper = mount(Panel, { props: { container: value }, global: { plugins: [ElementPlus], stubs: realDialog ? {} : { AppDialog: { props: ['modelValue'], template: '<div v-if="modelValue"><slot /></div>' } } } })
return { wrapper, vm: wrapper.vm as unknown as VM }
}
describe('original container schedule', () => {
beforeEach(() => { vi.clearAllMocks(); can.mockReturnValue(true); vi.mocked(getPortSchedule).mockResolvedValue(result()); vi.mocked(updatePortSchedule).mockResolvedValue(result()); vi.mocked(createPortSchedule).mockResolvedValue(result()) })
it('loads only the server selected ID, preserves original fields, and keeps key/body after failure', async () => {
const { wrapper, vm } = setup(); await vm.begin()
const { wrapper, vm } = setup(container, true); await vm.begin()
expect(getPortSchedule).toHaveBeenCalledExactlyOnceWith('S1')
vm.form.vesselName = 'NEW'; vm.form.voyageNo = 'V2'; vm.form.reason = '原船满载改配'
vi.mocked(updatePortSchedule).mockRejectedValueOnce(new Error('timeout'))
await vm.save(); expect(vm.form.vesselName).toBe('NEW'); expect(vm.error).toContain('timeout')
await flushPromises()
const dialog = wrapper.getComponent(AppDialog)
expect(dialog.props('title')).toBe('原柜船期与修改历史 · D1')
expect(dialog.getComponent({ name: 'ElForm' }).props('disabled')).toBe(true)
await dialog.get('[aria-label="展开全屏"]').trigger('click')
expect(dialog.find('.el-dialog.is-fullscreen').exists()).toBe(true)
expect(dialog.get<HTMLInputElement>('input[placeholder="请输入船名"]').element.value).toBe('NEW')
await vm.save()
expect(vi.mocked(updatePortSchedule).mock.calls[1]).toEqual(vi.mocked(updatePortSchedule).mock.calls[0])
expect(updatePortSchedule).toHaveBeenLastCalledWith('S1', { expectedVersion: 5, reason: '原船满载改配', data: expect.objectContaining({ vesselName: 'NEW', voyageNo: 'V2', sourceType: 'CARRIER', sourceName: '官网', sourceRef: 'REF', observedAt: original.observedAt, expiresAt: original.expiresAt, portCutoffAt: original.portCutoffAt, remark: '原备注', containerBookingId: 'C1' }) }, expect.any(String))
@@ -36,12 +44,23 @@ describe('original container schedule', () => {
it('list-only account can see four-field history but cannot write', async () => {
can.mockImplementation((...args: unknown[]) => args[0] === 'platform:firstleg:port-schedule:list')
vi.mocked(getPortSchedule).mockResolvedValue({ ...result(), histories: [{ id: 'H1', action: 'UPDATE', reason: '改配', operatorName: '操作员', createdAt: '2026-10-03T00:00:00', beforeSnapshot: JSON.stringify(original), afterSnapshot: JSON.stringify({ ...original, vesselName: 'NEW' }) }] })
const { wrapper, vm } = setup(); await vm.begin(); await flushPromises(); await vm.save()
const { wrapper, vm } = setup(container, true); await vm.begin(); await flushPromises(); await vm.save()
expect(wrapper.text()).toContain('NEW'); expect(wrapper.text()).toContain('操作员'); expect(wrapper.text()).toContain('改配')
const dialog = wrapper.getComponent(AppDialog)
expect(dialog.get('table[aria-label="当前船期只读资料"]').text()).toContain('OLD')
expect(dialog.findAll('input, textarea')).toHaveLength(0)
expect(dialog.props('showConfirm')).toBe(false)
expect(updatePortSchedule).not.toHaveBeenCalled(); wrapper.unmount()
})
it('creation needs explicit source and dates and locks original ownership', async () => {
const { wrapper, vm } = setup({ ...container, currentPortScheduleId: '' }); await vm.begin(); vm.form.reason = '首次登记'
const { wrapper, vm } = setup({ ...container, currentPortScheduleId: '' }, true); await vm.begin(); vm.form.reason = '首次登记'
await flushPromises()
const dialog = wrapper.getComponent(AppDialog)
expect(dialog.props('title')).toBe('登记原柜船期 · D1')
expect(dialog.findAllComponents({ name: 'ElFormItem' })).toHaveLength(12)
expect(dialog.get('table[aria-label="原柜船期归属"]').text()).toContain('平台订舱')
await dialog.get('[aria-label="展开全屏"]').trigger('click')
expect(vm.form.reason).toBe('首次登记')
await vm.save(); expect(createPortSchedule).not.toHaveBeenCalled(); expect(vm.error).toContain('来源')
Object.assign(vm.form, { sourceName: '平台人工确认', sourceRef: '船司邮件号', observedAt: '2026-10-03T00:00:00', expiresAt: '2026-10-05T00:00:00', portCutoffAt: '2026-10-03T01:00:00' })
await vm.save(); expect(getPortSchedule).not.toHaveBeenCalled()