接入前端真实接口并标记邮件HTML清洗

This commit is contained in:
andy committed 2026-07-08 23:37:03 +08:00
1 parent c83935a26c
commit a0cb3a05d7
25 files changed
+2581 -157

No files matched your search

@@ -8,6 +8,7 @@ import ReservationTaskDetailPanel from '@/components/reservation/ReservationTask
import type {
ReservationTaskAuditLogResult,
ReservationTaskDetailResult,
ReservationTaskFieldResult,
ReservationTaskPayloadMutationResult,
} from '@/types/reservation'
@@ -24,8 +25,8 @@ vi.mock('@/services/reservationService', async (importOriginal) => {
const service = await import('@/services/reservationService')
function createTaskDetail(): ReservationTaskDetailResult {
return {
function createTaskDetail(overrides: Partial<ReservationTaskDetailResult> = {}): ReservationTaskDetailResult {
const detail: ReservationTaskDetailResult = {
task_id: '10001',
order_id: '20001',
source_message_id: '30001',
@@ -52,6 +53,10 @@ function createTaskDetail(): ReservationTaskDetailResult {
fields: [],
opera_operations: [],
}
return {
...detail,
...overrides,
}
}
function createMutationResult(taskStatus = 'PENDING_CONFIRM'): ReservationTaskPayloadMutationResult {
@@ -81,6 +86,34 @@ function createAudit(auditId: string, action: string): ReservationTaskAuditLogRe
}
}
function createRequiredField(overrides: Partial<ReservationTaskFieldResult> = {}): ReservationTaskFieldResult {
return {
row_number: 1,
card_name: 'New Booking',
display_area: '客人信息',
field_path: 'guest.name',
display_name: '客人姓名',
visible: 'Y',
editable: 'Y',
input_editable: 'Y',
select_editable: 'N',
date_picker: 'N',
number_input: 'N',
file_display: 'N',
table_editable: 'N',
enum_options: null,
required_rule: 'Y',
display_condition: null,
validation_rule: null,
write_path: null,
opera_write_participation: 'N',
opera_parameter_mapping: null,
notes: null,
value: '',
...overrides,
}
}
async function mountPanel() {
const i18n = createI18n({
legacy: false,
@@ -107,7 +140,9 @@ async function mountPanel() {
global: {
plugins: [i18n, router],
stubs: {
RouterLink: true,
RouterLink: {
template: '<a><slot /></a>',
},
},
},
})
@@ -158,6 +193,59 @@ describe('ReservationTaskDetailPanel', () => {
expect(findButton(wrapper, '确认').attributes('disabled')).toBeUndefined()
})
it('blocks confirmation when frontend field validation fails', async () => {
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
createTaskDetail({
fields: [createRequiredField()],
}),
)
const wrapper = await mountPanel()
await findButton(wrapper, '确认').trigger('click')
await flushPromises()
expect(service.confirmReservationTask).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('请先修正字段')
expect(wrapper.text()).toContain('客人姓名为必填项')
})
it('keeps remaining validation errors and clears the summary after fields are fixed', async () => {
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
createTaskDetail({
fields: [
createRequiredField(),
createRequiredField({
field_path: 'room.type',
display_name: '房型',
}),
],
}),
)
const wrapper = await mountPanel()
await findButton(wrapper, '确认').trigger('click')
await flushPromises()
expect(wrapper.text()).toContain('请先修正字段')
expect(wrapper.text()).toContain('客人姓名为必填项')
expect(wrapper.text()).toContain('房型为必填项')
const textareas = wrapper.findAll('textarea')
await textareas[0]!.setValue('王建国')
await flushPromises()
expect(wrapper.text()).toContain('请先修正字段')
expect(wrapper.text()).not.toContain('客人姓名为必填项')
expect(wrapper.text()).toContain('房型为必填项')
await textareas[1]!.setValue('豪华 Q1A')
await flushPromises()
expect(wrapper.text()).not.toContain('请先修正字段')
expect(wrapper.text()).not.toContain('客人姓名为必填项')
expect(wrapper.text()).not.toContain('房型为必填项')
})
it('refreshes audit records after saving a draft', async () => {
vi.mocked(service.fetchReservationTaskAudits)
.mockResolvedValueOnce({
@@ -180,4 +268,69 @@ describe('ReservationTaskDetailPanel', () => {
expect(wrapper.text()).toContain('DRAFT_SAVED')
expect(wrapper.text()).not.toContain('TASK_LOADED')
})
it('saves only editable active fields to match backend task field validation', async () => {
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
createTaskDetail({
fields: [
createRequiredField({
field_path: 'visible_reason',
display_name: '生成原因',
editable: 'N',
input_editable: 'N',
required_rule: 'Y',
value: 'AI extracted this task.',
}),
createRequiredField({
field_path: 'case_keys.group_code',
display_name: 'Group Code',
display_condition: '对象为Group Block或Allotment时展示',
value: 'GRP-001',
}),
createRequiredField({
field_path: 'case_keys.confirmation_number',
display_name: 'Confirmation No.',
display_condition: '对象为FIT Reservation时展示',
value: null,
}),
createRequiredField({
field_path: 'extracted_fields.booking_object_type',
display_name: '订单对象类型',
select_editable: 'Y',
input_editable: 'N',
enum_options: 'Group Block, FIT Reservation',
value: 'Group Block',
}),
],
}),
)
vi.mocked(service.saveReservationTaskDraft).mockResolvedValue(createMutationResult())
const wrapper = await mountPanel()
await findButton(wrapper, '保存草稿').trigger('click')
await flushPromises()
expect(service.saveReservationTaskDraft).toHaveBeenCalledWith(
'10001',
{
field_values: {
'case_keys.group_code': 'GRP-001',
'extracted_fields.booking_object_type': 'Group Block',
},
},
)
})
it('opens the source conversation by source message id without external conversation id', async () => {
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
createTaskDetail({
external_conversation_id: null,
}),
)
const wrapper = await mountPanel()
expect(wrapper.text()).toContain('查看邮件会话')
expect(wrapper.text()).not.toContain('接口待补')
})
})