From 6a9135770b5d986aa4db46ccd1e08e4c85fac266 Mon Sep 17 00:00:00 2001 From: zoujing Date: Fri, 31 Jul 2026 09:38:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B8=B8=E7=94=A8=E5=87=BA=E8=A1=8C?= =?UTF-8?q?=E4=BA=BA=E7=9A=84=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tickets/components/TravelerManager.vue | 95 ++++++++++++------- src/pages-service/tickets/store.js | 11 +++ src/pages-service/tickets/travelers.vue | 1 + .../tickets/view-contract.test.mjs | 27 +++++- 4 files changed, 100 insertions(+), 34 deletions(-) diff --git a/src/pages-service/tickets/components/TravelerManager.vue b/src/pages-service/tickets/components/TravelerManager.vue index 2f610bb..22e7336 100644 --- a/src/pages-service/tickets/components/TravelerManager.vue +++ b/src/pages-service/tickets/components/TravelerManager.vue @@ -1,42 +1,67 @@ @@ -49,11 +74,9 @@ const props = defineProps({ selectionMode: { type: Boolean, default: false }, }) -const emit = defineEmits(['toggle', 'save']) -const idTypes = ['身份证', '护照'] -const showForm = ref(false) +const emit = defineEmits(['toggle', 'save', 'delete']) const formError = ref('') -const form = reactive({ name: '', idType: '身份证', idNumber: '', phone: '' }) +const form = reactive({ name: '', idNumber: '', phone: '' }) const isSelected = (id) => props.selectedIds.includes(id) const selectTraveler = (id) => { @@ -74,22 +97,30 @@ const setFormField = (key, value) => { } const resetForm = () => { form.name = '' - form.idType = '身份证' form.idNumber = '' form.phone = '' formError.value = '' - showForm.value = false } const saveTraveler = () => { if (!form.name.trim() || !form.idNumber.trim()) { formError.value = '请填写姓名和证件号码' return } + if (!/^\d{17}[\dX]$/.test(form.idNumber.trim().toUpperCase())) { + formError.value = '请填写正确的18位身份证号码' + return + } if (form.phone && !/^1[3-9]\d{9}$/.test(form.phone)) { formError.value = '请填写正确的手机号' return } - emit('save', { id: `traveler-${Date.now()}`, name: form.name.trim(), idType: form.idType, idNumber: form.idNumber.trim(), phone: form.phone }) + emit('save', { + id: `traveler-${Date.now()}`, + name: form.name.trim(), + idType: '身份证', + idNumber: form.idNumber.trim().toUpperCase(), + phone: form.phone, + }) resetForm() } diff --git a/src/pages-service/tickets/store.js b/src/pages-service/tickets/store.js index ad12142..4f0161c 100644 --- a/src/pages-service/tickets/store.js +++ b/src/pages-service/tickets/store.js @@ -70,6 +70,17 @@ export const useTicketStore = defineStore("ticket-client", { }; } }, + deleteTraveler(travelerId) { + this.travelers = this.travelers.filter( + (traveler) => traveler.id !== travelerId + ); + this.bookingDraft = { + ...this.bookingDraft, + travelerIds: (this.bookingDraft.travelerIds || []).filter( + (id) => id !== travelerId + ), + }; + }, submitBooking() { const product = this.currentProduct; const draft = this.bookingDraft; diff --git a/src/pages-service/tickets/travelers.vue b/src/pages-service/tickets/travelers.vue index d93300f..f3d4d58 100644 --- a/src/pages-service/tickets/travelers.vue +++ b/src/pages-service/tickets/travelers.vue @@ -13,6 +13,7 @@ :selection-mode="selectionMode" @toggle="ticketStore.toggleTraveler" @save="saveTraveler" + @delete="ticketStore.deleteTraveler" /> diff --git a/src/pages-service/tickets/view-contract.test.mjs b/src/pages-service/tickets/view-contract.test.mjs index 1e7f6fe..99f6e23 100644 --- a/src/pages-service/tickets/view-contract.test.mjs +++ b/src/pages-service/tickets/view-contract.test.mjs @@ -53,8 +53,6 @@ test("TopNavBar exposes an optional subtitle", async () => { test("storefront synchronizes the left category from right-column scrolling", async () => { const source = await readTicketFile("components/TicketHome.vue"); - assert.match(source, /right-\[100px\]/); - assert.match(source, /z-30/); assert.match(source, /@tap\.stop="emit\('open-travelers'\)"/); assert.match(source, /@tap\.stop="emit\('open-orders'\)"/); assert.match(source, /@scroll="handleProductScroll"/); @@ -78,3 +76,28 @@ test("order content uses bounded box-model wrappers", async () => { assert.match(source, /min-w-0/); } }); + +test("traveler manager matches the approved prototype details", async () => { + const component = await readTicketFile("components/TravelerManager.vue"); + const page = await readTicketFile("travelers.vue"); + const store = await readTicketFile("store.js"); + + assert.match(component, /gap-\[12px\]/); + assert.match(component, /type="trash"/); + assert.match(component, /type="phone"/); + assert.match(component, /@tap\.stop="emit\('delete', traveler\.id\)"/); + assert.match(component, /身份证信息仅用于|出行人信息仅用于实名购票、第三方出票和身份核验。/); + assert.match(component, /18位身份证号码/); + assert.match(component, /placeholder="手机号(选填)"/); + assert.match(component, /请填写正确的手机号/); + assert.match(component, /bg-blue-600/); + assert.match(component, /已保存的出行人/); + assert.doesNotMatch(component, /showForm/); + assert.doesNotMatch(component, /取消/); + assert.equal((component.match(/新增出行人/g) || []).length, 1); + assert.ok( + component.indexOf("新增出行人") < component.indexOf('v-if="travelers.length"') + ); + assert.match(page, /@delete="ticketStore\.deleteTraveler"/); + assert.match(store, /deleteTraveler\(travelerId\)/); +});