From ad9df7a7a43e74c266820cd62ef0de2175c1bf24 Mon Sep 17 00:00:00 2001 From: wikiboss Date: Mon, 21 Sep 2026 15:24:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(mpuniapp):=20=E5=AE=8C=E5=96=84=E5=BE=AE?= =?UTF-8?q?=E5=AE=98=E7=BD=91=E8=AE=BF=E5=AE=A2=E5=9C=A8=E7=BA=BF=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E5=95=86=E5=8A=A1=E9=A2=84=E7=BA=A6=E4=B8=8E=E9=9C=80?= =?UTF-8?q?=E6=B1=82=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mpuniapp/common/locale.uts | 136 ++++++++++++- mpuniapp/pages/portal/index.uvue | 315 ++++++++++++++++------------- mpuniapp/tests/appointment.test.ts | 133 +++++------- 3 files changed, 346 insertions(+), 238 deletions(-) diff --git a/mpuniapp/common/locale.uts b/mpuniapp/common/locale.uts index fa6a06c..cf93adc 100644 --- a/mpuniapp/common/locale.uts +++ b/mpuniapp/common/locale.uts @@ -77,7 +77,22 @@ export type OmniMessageKey = | 'portal.google_cal' | 'portal.outlook_cal' | 'portal.ics_download' - | 'portal.copied_meeting_link'; + | 'portal.copied_meeting_link' + | 'portal.appointment_form_title' + | 'portal.appointment_form_desc' + | 'portal.appointment_name' + | 'portal.appointment_contact' + | 'portal.appointment_company' + | 'portal.appointment_time' + | 'portal.appointment_time_video' + | 'portal.appointment_time_offline' + | 'portal.appointment_time_call' + | 'portal.appointment_notes' + | 'portal.appointment_notes_placeholder' + | 'portal.appointment_submit' + | 'portal.appointment_submitting' + | 'portal.appointment_success_tip' + | 'portal.appointment_fill_required'; export interface OmniLocaleConfig { code: OmniLocale; @@ -145,11 +160,26 @@ export const omniLocales: Record = { 'portal.collected': '已收入大厅', 'portal.appointment': '商务预约', 'portal.appointment_desc': '预约 30 分钟跨国视频会谈或展会现场洽谈', - 'portal.book_now': '立即排期', + 'portal.book_now': '立即预约', 'portal.google_cal': 'Google 日历', 'portal.outlook_cal': 'Outlook 日历', 'portal.ics_download': '下载日历文件 (ICS)', 'portal.copied_meeting_link': '已复制预约日历链接', + 'portal.appointment_form_title': '预约商务洽谈', + 'portal.appointment_form_desc': '提交您的会谈意向与需求,将即时通知名片主', + 'portal.appointment_name': '您的姓名', + 'portal.appointment_contact': '手机 / WhatsApp / 邮箱', + 'portal.appointment_company': '所属公司 (选填)', + 'portal.appointment_time': '期望会谈形式 / 时间', + 'portal.appointment_time_video': '线上视频 (30分钟)', + 'portal.appointment_time_offline': '展会现场洽谈', + 'portal.appointment_time_call': '电话快速沟通', + 'portal.appointment_notes': '合作需求 / 洽谈议题', + 'portal.appointment_notes_placeholder': '请简要说明您的业务需求或采购意向...', + 'portal.appointment_submit': '提交预约需求', + 'portal.appointment_submitting': '提交中...', + 'portal.appointment_success_tip': '预约已提交!名片主已收到您的需求。', + 'portal.appointment_fill_required': '请填写姓名和有效联系方式', }, }, 'en-US': { @@ -215,6 +245,21 @@ export const omniLocales: Record = { 'portal.outlook_cal': 'Outlook Calendar', 'portal.ics_download': 'Download ICS File', 'portal.copied_meeting_link': 'Calendar link copied', + 'portal.appointment_form_title': 'Schedule Business Meeting', + 'portal.appointment_form_desc': 'Submit your meeting intent & requirements to the card owner', + 'portal.appointment_name': 'Your Name', + 'portal.appointment_contact': 'Phone / WhatsApp / Email', + 'portal.appointment_company': 'Company (Optional)', + 'portal.appointment_time': 'Preferred Format / Time', + 'portal.appointment_time_video': 'Online Video (30 min)', + 'portal.appointment_time_offline': 'In-Person Meeting', + 'portal.appointment_time_call': 'Quick Phone Call', + 'portal.appointment_notes': 'Topics & Requirements', + 'portal.appointment_notes_placeholder': 'Briefly describe your project or partnership goals...', + 'portal.appointment_submit': 'Submit Appointment', + 'portal.appointment_submitting': 'Submitting...', + 'portal.appointment_success_tip': 'Appointment submitted! The card owner will contact you shortly.', + 'portal.appointment_fill_required': 'Please provide your name and contact info', }, }, 'ar-SA': { @@ -275,11 +320,26 @@ export const omniLocales: Record = { 'portal.collected': 'تم الحفظ في القاعة', 'portal.appointment': 'حجز موعد', 'portal.appointment_desc': 'احجز اجتماع فيديو عالمي لمدة 30 دقيقة أو محادثة شخصية', - 'portal.book_now': 'احجز الآن', + 'portal.book_now': 'احجز موعداً', 'portal.google_cal': 'تقويم Google', 'portal.outlook_cal': 'تقويم Outlook', 'portal.ics_download': 'تنزيل ملف التقويم (ICS)', 'portal.copied_meeting_link': 'تم نسخ رابط التقويم', + 'portal.appointment_form_title': 'حجز موعد عمل', + 'portal.appointment_form_desc': 'أرسل رغبتك ومتطلباتك إلى صاحب البطاقة مباشرة', + 'portal.appointment_name': 'اسمك الكريم', + 'portal.appointment_contact': 'رقم الهاتف / واتساب / البريد', + 'portal.appointment_company': 'اسم الشركة (اختياري)', + 'portal.appointment_time': 'طريقة / وقت الاجتماع المفضل', + 'portal.appointment_time_video': 'فيديو عبر الإنترنت (30 دقيقة)', + 'portal.appointment_time_offline': 'لقاء شخصي في المعرض', + 'portal.appointment_time_call': 'مكالمة هاتفية سريعة', + 'portal.appointment_notes': 'المتطلبات وموضوع النقاش', + 'portal.appointment_notes_placeholder': 'صف بإيجاز احتياجاتك أو أهداف التعاون...', + 'portal.appointment_submit': 'إرسال طلب الموعد', + 'portal.appointment_submitting': 'جاري الإرسال...', + 'portal.appointment_success_tip': 'تم إرسال طلبك بنجاح! سيتواصل معك صاحب البطاقة قريباً.', + 'portal.appointment_fill_required': 'يرجى إدخال الاسم ومعلومات التواصل', }, }, 'es-ES': { @@ -340,11 +400,26 @@ export const omniLocales: Record = { 'portal.collected': 'Guardado en el hall', 'portal.appointment': 'Agendar Reunión', 'portal.appointment_desc': 'Reserva una reunión virtual de 30 min o charla presencial', - 'portal.book_now': 'Agendar Ahora', + 'portal.book_now': 'Agendar Cita', 'portal.google_cal': 'Google Calendar', 'portal.outlook_cal': 'Outlook Calendar', 'portal.ics_download': 'Descargar Archivo ICS', 'portal.copied_meeting_link': 'Enlace de calendario copiado', + 'portal.appointment_form_title': 'Agendar Reunión de Negocios', + 'portal.appointment_form_desc': 'Envía tus requisitos y propuesta directamente al titular de la tarjeta', + 'portal.appointment_name': 'Tu Nombre', + 'portal.appointment_contact': 'Teléfono / WhatsApp / Correo', + 'portal.appointment_company': 'Empresa (Opcional)', + 'portal.appointment_time': 'Formato / Horario Preferido', + 'portal.appointment_time_video': 'Videollamada (30 min)', + 'portal.appointment_time_offline': 'Reunión Presencial', + 'portal.appointment_time_call': 'Llamada Rápida', + 'portal.appointment_notes': 'Requisitos y Temas', + 'portal.appointment_notes_placeholder': 'Describe brevemente tus necesidades o propuesta...', + 'portal.appointment_submit': 'Enviar Solicitud', + 'portal.appointment_submitting': 'Enviando...', + 'portal.appointment_success_tip': '¡Solicitud enviada! El titular se pondrá en contacto pronto.', + 'portal.appointment_fill_required': 'Por favor indica tu nombre y contacto', }, }, 'de-DE': { @@ -405,11 +480,26 @@ export const omniLocales: Record = { 'portal.collected': 'In der Halle gespeichert', 'portal.appointment': 'Termin vereinbaren', 'portal.appointment_desc': 'Buchen Sie ein 30-minütiges globales Meeting', - 'portal.book_now': 'Jetzt buchen', + 'portal.book_now': 'Termin anfragen', 'portal.google_cal': 'Google Kalender', 'portal.outlook_cal': 'Outlook Kalender', 'portal.ics_download': 'ICS-Datei herunterladen', 'portal.copied_meeting_link': 'Kalenderlink kopiert', + 'portal.appointment_form_title': 'Geschäftstermin anfragen', + 'portal.appointment_form_desc': 'Senden Sie Ihre Anforderungen direkt an den Karteninhaber', + 'portal.appointment_name': 'Ihr Name', + 'portal.appointment_contact': 'Telefon / WhatsApp / E-Mail', + 'portal.appointment_company': 'Unternehmen (optional)', + 'portal.appointment_time': 'Bevorzugtes Format / Zeit', + 'portal.appointment_time_video': 'Online-Meeting (30 Min.)', + 'portal.appointment_time_offline': 'Persönliches Treffen', + 'portal.appointment_time_call': 'Kurzer Telefonanruf', + 'portal.appointment_notes': 'Themen & Anforderungen', + 'portal.appointment_notes_placeholder': 'Beschreiben Sie kurz Ihr Vorhaben oder Ihre Fragen...', + 'portal.appointment_submit': 'Termin anfragen', + 'portal.appointment_submitting': 'Wird gesendet...', + 'portal.appointment_success_tip': 'Anfrage gesendet! Der Karteninhaber meldet sich in Kürze.', + 'portal.appointment_fill_required': 'Bitte Namen und Kontaktdaten angeben', }, }, 'fr-FR': { @@ -470,11 +560,26 @@ export const omniLocales: Record = { 'portal.collected': 'Ajouté au hall', 'portal.appointment': 'Prendre Rendez-vous', 'portal.appointment_desc': 'Réservez une réunion vidéo globale de 30 minutes', - 'portal.book_now': 'Réserver maintenant', + 'portal.book_now': 'Prendre RDV', 'portal.google_cal': 'Google Agenda', 'portal.outlook_cal': 'Outlook Calendrier', 'portal.ics_download': 'Télécharger le fichier ICS', 'portal.copied_meeting_link': 'Lien de calendrier copié', + 'portal.appointment_form_title': 'Demande de Rendez-vous', + 'portal.appointment_form_desc': 'Transmettez vos besoins directement au titulaire de la carte', + 'portal.appointment_name': 'Votre Nom', + 'portal.appointment_contact': 'Téléphone / WhatsApp / E-mail', + 'portal.appointment_company': 'Société (Facultatif)', + 'portal.appointment_time': 'Format / Créneau Souhaité', + 'portal.appointment_time_video': 'Visio en ligne (30 min)', + 'portal.appointment_time_offline': 'Rencontre en personne', + 'portal.appointment_time_call': 'Appel rapide', + 'portal.appointment_notes': 'Besoins & Sujets', + 'portal.appointment_notes_placeholder': 'Décrivez brièvement votre projet ou vos attentes...', + 'portal.appointment_submit': 'Envoyer la demande', + 'portal.appointment_submitting': 'Envoi en cours...', + 'portal.appointment_success_tip': 'Demande envoyée ! Le titulaire vous contactera sous peu.', + 'portal.appointment_fill_required': 'Veuillez renseigner votre nom et vos coordonnées', }, }, 'ja-JP': { @@ -534,12 +639,27 @@ export const omniLocales: Record = { 'portal.collect': 'ホールに保存', 'portal.collected': 'ホールに保存しました', 'portal.appointment': '日程を予約する', - 'portal.appointment_desc': '30分間のオンライン面談または商談を予約', - 'portal.book_now': '今すぐ予約', + 'portal.appointment_desc': '30分間のオンライン面談または商谈を予約', + 'portal.book_now': '予約・相談する', 'portal.google_cal': 'Google カレンダー', 'portal.outlook_cal': 'Outlook カレンダー', 'portal.ics_download': 'ICSファイルをダウンロード', 'portal.copied_meeting_link': 'カレンダーのリンクをコピーしました', + 'portal.appointment_form_title': '商談・面談のご予約', + 'portal.appointment_form_desc': 'ご相談内容やご要望を名刺所有者へ直接送信します', + 'portal.appointment_name': 'お名前', + 'portal.appointment_contact': '電話番号 / WhatsApp / メール', + 'portal.appointment_company': '会社名・組織名(任意)', + 'portal.appointment_time': 'ご希望の面談形式・時間', + 'portal.appointment_time_video': 'オンライン面談(30分)', + 'portal.appointment_time_offline': '対面・展示会での商談', + 'portal.appointment_time_call': 'お電話でのクイック相談', + 'portal.appointment_notes': 'ご相談内容・ご要望', + 'portal.appointment_notes_placeholder': '事業内容やご興味のある点をご記入ください...', + 'portal.appointment_submit': '予約・相談を送信', + 'portal.appointment_submitting': '送信中...', + 'portal.appointment_success_tip': 'ご予約を送信しました。担当者より追ってご連絡いたします。', + 'portal.appointment_fill_required': 'お名前とご連絡先を入力してください', }, }, }; diff --git a/mpuniapp/pages/portal/index.uvue b/mpuniapp/pages/portal/index.uvue index 6f2f2f3..f2cd241 100644 --- a/mpuniapp/pages/portal/index.uvue +++ b/mpuniapp/pages/portal/index.uvue @@ -330,7 +330,6 @@ import { BentoBlockPublic, CardProfile, - MeetingInviteOptions, } from '../../common/types.uts'; import { createPortalState, @@ -351,13 +350,8 @@ import { trackAppointmentAction, PortalQuickAction, } from './usePortal.uts'; -import { - generateGoogleCalendarUrl, - generateOutlookCalendarUrl, - getMeetingIcsDownloadUrl, -} from '../../common/calendar.uts'; import { saveToContacts } from '../../common/vcard-export.uts'; -import { collectHallContact } from '../../common/api.uts'; +import { collectHallContact, submitLead } from '../../common/api.uts'; import { getAuthToken } from '../../common/request.uts'; import { setPendingCollectCard } from '../owner/hall/useHall.uts'; import { @@ -641,82 +635,100 @@ function openAiChat(): void { }); } -// 商务日程预约相关逻辑 +// 商务日程预约与需求提报相关逻辑 +type AppointmentFormState = { + name: string; + contact: string; + company: string; + preferredTime: string; + notes: string; +}; + const isAppointmentSheetOpen = ref(false); +const appointmentFormSubmitting = ref(false); +const appointmentSubmitted = ref(false); + +const appointmentForm = reactive({ + name: '', + contact: '', + company: '', + preferredTime: '', + notes: '', +}); + +const appointmentTimeOptions = computed((): Array<{ key: string; label: string }> => [ + { key: 'video', label: textOf('portal.appointment_time_video') }, + { key: 'offline', label: textOf('portal.appointment_time_offline') }, + { key: 'call', label: textOf('portal.appointment_time_call') }, +]); + +function selectPreferredTime(val: string): void { + appointmentForm.preferredTime = val; +} function openAppointmentSheet(): void { if (!state.card) return; + if (!appointmentForm.preferredTime && appointmentTimeOptions.value.length > 0) { + appointmentForm.preferredTime = appointmentTimeOptions.value[0].label; + } isAppointmentSheetOpen.value = true; - trackAppointmentAction(state); } function closeAppointmentSheet(): void { isAppointmentSheetOpen.value = false; + if (appointmentSubmitted.value) { + appointmentForm.name = ''; + appointmentForm.contact = ''; + appointmentForm.company = ''; + appointmentForm.notes = ''; + appointmentSubmitted.value = false; + } } -function buildCurrentMeetingOptions(): MeetingInviteOptions { - const card = state.card!; - const now = new Date(); - const startTime = new Date(now.getTime() + 24 * 3600 * 1000); - const endTime = new Date(startTime.getTime() + 30 * 60 * 1000); +async function submitAppointmentForm(): Promise { + if (appointmentFormSubmitting.value) return; + if (!state.card || state.card == null) return; - return { - summary: `${card.name} · ${textOf('portal.appointment')}`, - title: `${card.name} · ${textOf('portal.appointment')}`, - description: `与 ${card.name} (${card.company || ''} ${card.title || ''}) 的商务会谈。\n名片: ${card.slug || ''}`, - location: card.company || '在线商务会议 (Google Meet / Zoom)', - startTime, - endTime, - organizerName: card.name, - organizerEmail: card.email || 'meeting@omnicard.ai', - }; -} - -function handleCalendarSync(channel: 'google' | 'outlook' | 'ics'): void { - if (!state.card) return; - const options = buildCurrentMeetingOptions(); - trackAppointmentAction(state); - - if (channel === 'google') { - const url = generateGoogleCalendarUrl(options); - uni.setClipboardData({ - data: url, - success: () => { - uni.showToast({ - title: textOf('portal.copied_meeting_link'), - icon: 'none', - }); - }, + const name = appointmentForm.name.trim(); + const contact = appointmentForm.contact.trim(); + if (!name || !contact) { + uni.showToast({ + title: textOf('portal.appointment_fill_required'), + icon: 'none', }); return; } - if (channel === 'outlook') { - const url = generateOutlookCalendarUrl(options); - uni.setClipboardData({ - data: url, - success: () => { - uni.showToast({ - title: textOf('portal.copied_meeting_link'), - icon: 'none', - }); - }, - }); - return; - } + appointmentFormSubmitting.value = true; + try { + const isEmail = contact.includes('@'); + const fullNotes = `[商务预约] 期望形式: ${appointmentForm.preferredTime || '未指定'}${appointmentForm.notes.trim() ? `\n需求与议题: ${appointmentForm.notes.trim()}` : ''}`; - if (channel === 'ics') { - const url = getMeetingIcsDownloadUrl(options); - uni.setClipboardData({ - data: url, - success: () => { - uni.showToast({ - title: textOf('portal.copied_meeting_link'), - icon: 'none', - }); - }, + await submitLead({ + cardId: state.card!.id, + visitorId: state.visitorId, + name: name, + company: appointmentForm.company.trim(), + phone: isEmail ? '' : contact, + email: isEmail ? contact : '', + source: 'appointment', + notes: fullNotes, + submittedLead: true, }); - return; + + await trackAppointmentAction(state, fullNotes); + appointmentSubmitted.value = true; + uni.showToast({ + title: textOf('portal.appointment_success_tip'), + icon: 'none', + }); + } catch (_e) { + uni.showToast({ + title: textOf('common.error'), + icon: 'none', + }); + } finally { + appointmentFormSubmitting.value = false; } } @@ -1481,108 +1493,127 @@ onUnload(() => { font-size: 13px; color: var(--oc-text-2, #64748B); } -.sheet-meeting-details { - display: flex; - flex-direction: row; - align-items: center; - gap: 16px; - background-color: var(--oc-bg, #F5F8FC); - border-radius: 12px; - padding: 10px 14px; - margin-block-end: 16px; -} -.sheet-detail-row { +/* 商务预约与需求提报表单样式 */ +.appointment-form-wrap { display: flex; flex-direction: column; } -.sheet-detail-label { - font-size: 10px; - font-weight: 600; - color: #98A2B3; - letter-spacing: 0.5px; +.sheet-field { + display: flex; + flex-direction: column; + margin-block-end: 12px; } -.sheet-detail-val { +.sheet-field-label { font-size: 13px; font-weight: 600; color: var(--oc-text, #101828); - margin-top: 2px; + margin-block-end: 6px; } -.calendar-channels-list { - display: flex; - flex-direction: column; - gap: 10px; -} -.calendar-channel-item { +.sheet-time-chips { display: flex; flex-direction: row; - align-items: center; - justify-content: space-between; - padding: 12px 14px; - min-height: 44px; - background-color: #FFFFFF; + gap: 8px; + flex-wrap: wrap; +} +.sheet-time-chip { + padding: 8px 14px; + min-height: 36px; + border-radius: 9999px; + background-color: var(--oc-bg, #F5F8FC); border: 1px solid var(--oc-border, #EBF1F7); - border-radius: 14px; - cursor: pointer; - transition: border-color 150ms ease; -} -.calendar-channel-item:active { - background-color: #F8FAFC; - border-color: #B2D7FF; -} -.channel-info { display: flex; - flex-direction: row; align-items: center; - gap: 12px; + justify-content: center; + cursor: pointer; + box-sizing: border-box; } -.channel-icon-badge { - width: 36px; - height: 36px; - border-radius: 10px; +.sheet-time-chip-active { + background-color: var(--oc-primary-soft, #EBF5FF); + border-color: var(--oc-primary, #0088FF); +} +.sheet-time-chip-text { + font-size: 13px; + color: var(--oc-text-2, #64748B); + font-weight: 500; +} +.sheet-time-chip-text-active { + color: var(--oc-primary, #0088FF); + font-weight: 600; +} +.sheet-field-input { + height: 44px; + min-height: 44px; + border: 1px solid var(--oc-border, #EBF1F7); + border-radius: 12px; + padding: 0 14px; + font-size: 16px; + color: var(--oc-text, #101828); + background-color: #FFFFFF; + box-sizing: border-box; +} +.sheet-field-textarea { + min-height: 72px; + height: 72px; + border: 1px solid var(--oc-border, #EBF1F7); + border-radius: 12px; + padding: 10px 14px; + font-size: 16px; + color: var(--oc-text, #101828); + background-color: #FFFFFF; + box-sizing: border-box; +} +.appointment-submit-btn { + width: 100%; + height: 48px; + min-height: 48px; + margin-top: 6px; + border-radius: 9999px; + background: var(--oc-primary-gradient, linear-gradient(135deg, #00A3FF 0%, #0B63E5 100%)); + box-shadow: var(--oc-shadow-cta, 0 8px 20px rgba(0, 136, 255, 0.22)); display: flex; align-items: center; justify-content: center; } -.google-badge { - background-color: #FEF3F2; +.appointment-submit-btn-disabled { + opacity: 0.6; + pointer-events: none; } -.outlook-badge { - background-color: #EFF8FF; -} -.ics-badge { - background-color: #ECFDF3; -} -.channel-icon-text { - font-size: 12px; - font-weight: 700; -} -.google-badge .channel-icon-text { - color: #D92D20; -} -.outlook-badge .channel-icon-text { - color: #026AA2; -} -.ics-badge .channel-icon-text { - color: #027A48; -} -.channel-texts { + +/* 预约成功反馈卡片 */ +.sheet-success-box { display: flex; flex-direction: column; + align-items: center; + padding: 24px 0 12px; } -.channel-name { - font-size: 14px; - font-weight: 600; +.sheet-success-ring { + width: 56px; + height: 56px; + border-radius: 28px; + background-color: #ECFDF3; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 14px; +} +.sheet-success-check { + width: 14px; + height: 24px; + border-bottom: 3px solid #12B76A; + border-right: 3px solid #12B76A; + transform: rotate(45deg); + margin-top: -6px; +} +.sheet-success-title { + font-size: 17px; + font-weight: 700; color: var(--oc-text, #101828); + margin-bottom: 6px; } -.channel-sub { - font-size: 11px; +.sheet-success-sub { + font-size: 13px; color: var(--oc-text-2, #64748B); - margin-top: 2px; -} -.channel-action-arrow { - font-size: 14px; - font-weight: 600; - color: #98A2B3; + margin-bottom: 24px; } @media (prefers-reduced-motion: reduce) { diff --git a/mpuniapp/tests/appointment.test.ts b/mpuniapp/tests/appointment.test.ts index e9bf8ab..1df60df 100644 --- a/mpuniapp/tests/appointment.test.ts +++ b/mpuniapp/tests/appointment.test.ts @@ -4,41 +4,31 @@ import fs from 'node:fs'; import path from 'node:path'; const MP_ROOT = path.resolve('m:/2026项目/CRMmp/mpuniapp'); -const calendarUtsPath = path.join(MP_ROOT, 'common/calendar.uts'); const typesUtsPath = path.join(MP_ROOT, 'common/types.uts'); const localeUtsPath = path.join(MP_ROOT, 'common/locale.uts'); const portalIndexUvuePath = path.join(MP_ROOT, 'pages/portal/index.uvue'); const usePortalUtsPath = path.join(MP_ROOT, 'pages/portal/usePortal.uts'); -test('1. common/calendar.uts 文件与核心导出函数契约检查', () => { - assert.ok(fs.existsSync(calendarUtsPath), '必须存在 common/calendar.uts'); - const content = fs.readFileSync(calendarUtsPath, 'utf-8'); - assert.ok(content.includes('export function generateGoogleCalendarUrl'), '必须导出 generateGoogleCalendarUrl'); - assert.ok(content.includes('export function generateOutlookCalendarUrl'), '必须导出 generateOutlookCalendarUrl'); - assert.ok(content.includes('export function getMeetingIcsDownloadUrl'), '必须导出 getMeetingIcsDownloadUrl'); - assert.ok(content.includes('export function buildCalendarInviteBundle'), '必须导出 buildCalendarInviteBundle'); -}); - -test('2. common/types.uts 中 BentoBlockType 与预约配置契约对齐', () => { - assert.ok(fs.existsSync(typesUtsPath), '必须存在 common/types.uts'); - const content = fs.readFileSync(typesUtsPath, 'utf-8'); - assert.ok(content.includes("'appointment'"), "BentoBlockType 必须包含 'appointment'"); - assert.ok(content.includes('meetingUrl?: string'), 'CardPortalDetail / CardProfile 必须包含 meetingUrl 属性'); - assert.ok(content.includes('MeetingInviteOptions'), '必须导出 MeetingInviteOptions 接口'); -}); - -test('3. common/locale.uts 必须覆盖 7 种国际主流语言的商务预约词条', () => { +test('1. common/locale.uts 必须覆盖 7 种语言的预约需求表单国际化词条', () => { assert.ok(fs.existsSync(localeUtsPath), '必须存在 common/locale.uts'); const content = fs.readFileSync(localeUtsPath, 'utf-8'); - + const requiredKeys = [ 'portal.appointment', 'portal.appointment_desc', 'portal.book_now', - 'portal.google_cal', - 'portal.outlook_cal', - 'portal.ics_download', - 'portal.copied_meeting_link', + 'portal.appointment_form_title', + 'portal.appointment_form_desc', + 'portal.appointment_name', + 'portal.appointment_contact', + 'portal.appointment_company', + 'portal.appointment_time', + 'portal.appointment_notes', + 'portal.appointment_notes_placeholder', + 'portal.appointment_submit', + 'portal.appointment_submitting', + 'portal.appointment_success_tip', + 'portal.appointment_fill_required', ]; for (const key of requiredKeys) { @@ -48,84 +38,51 @@ test('3. common/locale.uts 必须覆盖 7 种国际主流语言的商务预约 const locales = ['zh-CN', 'en-US', 'ar-SA', 'es-ES', 'de-DE', 'fr-FR', 'ja-JP']; for (const loc of locales) { assert.ok(content.includes(`'${loc}':`), `必须配置语言 ${loc}`); - assert.ok(content.includes("'portal.appointment':"), `${loc} 必须包含 portal.appointment`); - assert.ok(content.includes("'portal.book_now':"), `${loc} 必须包含 portal.book_now`); + assert.ok(content.includes("'portal.appointment_form_title':"), `${loc} 必须包含 portal.appointment_form_title`); + assert.ok(content.includes("'portal.appointment_submit':"), `${loc} 必须包含 portal.appointment_submit`); } }); -test('4. pages/portal/index.uvue 中集成商务日程预约卡片与底部抽屉', () => { +test('2. pages/portal/index.uvue 包含 Bento 预约卡片并唤起需求提报抽屉', () => { assert.ok(fs.existsSync(portalIndexUvuePath), '必须存在 index.uvue'); const content = fs.readFileSync(portalIndexUvuePath, 'utf-8'); // 必须包含预约卡片与唤起方法 - assert.ok(content.includes('appointment-card') || content.includes('appointment-box'), '必须包含预约卡片组件/元素'); + assert.ok(content.includes('appointment-card'), '必须包含 Bento 预约卡片'); assert.ok(content.includes('openAppointmentSheet'), '必须包含打开预约抽屉的方法 openAppointmentSheet'); - assert.ok(content.includes('appointment-sheet') || content.includes('appointment-drawer'), '必须包含预约底部抽屉'); + assert.ok(content.includes('appointment-sheet'), '必须包含预约需求抽屉'); +}); - // 抽屉内必须提供三种日历同步渠道 - assert.ok(content.includes('handleCalendarSync'), '必须包含日历同步处理 handleCalendarSync'); - assert.ok(content.includes("'google'"), '必须支持 Google Calendar'); - assert.ok(content.includes("'outlook'"), '必须支持 Outlook Calendar'); - assert.ok(content.includes("'ics'"), '必须支持 ICS 日历文件'); +test('3. 抽屉内必须包含完整的姓名、联系方式、公司与业务需求提报字段', () => { + const content = fs.readFileSync(portalIndexUvuePath, 'utf-8'); + + // 表单字段输入项 + assert.ok(content.includes('appointmentForm.name'), '必须绑定客户姓名输入项'); + assert.ok(content.includes('appointmentForm.contact'), '必须绑定联系方式输入项'); + assert.ok(content.includes('appointmentForm.company'), '必须绑定公司输入项'); + assert.ok(content.includes('appointmentForm.notes'), '必须绑定需求与议题说明 textarea'); + assert.ok(content.includes('submitAppointmentForm'), '必须包含提交预约需求方法 submitAppointmentForm'); + assert.ok(content.includes('appointmentFormSubmitting'), '必须包含防重复提交状态控制'); +}); + +test('4. 提交预约信息必须直接调用 submitLead 接口将需求提交给名片主', () => { + const content = fs.readFileSync(portalIndexUvuePath, 'utf-8'); + + assert.ok(content.includes('submitLead(') || content.includes('submitLead'), '必须调用 submitLead 接口提交预约线索'); + assert.ok(content.includes("touchType: 'appointment'") || content.includes('trackAppointmentAction'), '提交后必须上报 appointment 雷达轨迹'); + assert.ok(content.includes('notes:'), '提交数据必须封装预约时间与需求 notes'); +}); + +test('5. 移动端 UI 规范审查:零非业务 Emoji、输入框防缩放与按钮触控区', () => { + const content = fs.readFileSync(portalIndexUvuePath, 'utf-8'); // 零 Emoji 审查(严禁在代码中写死非业务 emoji 图标) const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; assert.equal(emojiRegex.test(content), false, 'index.uvue 严禁含有装饰性 Emoji'); -}); -test('5. pages/portal/usePortal.uts 中必须上报 appointment 雷达轨迹', () => { - assert.ok(fs.existsSync(usePortalUtsPath), '必须存在 usePortal.uts'); - const content = fs.readFileSync(usePortalUtsPath, 'utf-8'); + // 必须适配输入框防缩放或触控区 assert.ok( - content.includes("touchType: 'appointment'") || content.includes('trackAppointmentAction'), - '预约动作必须向雷达上报 touchType appointment 推进商机' - ); -}); - -test('6. 日历链接生成与时间戳算法规范断言', () => { - const content = fs.readFileSync(calendarUtsPath, 'utf-8'); - - // Google Calendar 规范检查 - assert.ok( - content.includes('calendar.google.com/calendar/render'), - 'Google Calendar 必须指向官方 render 终端' - ); - assert.ok( - content.includes('action=TEMPLATE'), - 'Google Calendar 必须使用 TEMPLATE 动作' - ); - - // Outlook 规范检查 - assert.ok( - content.includes('outlook.live.com/calendar/0/deeplink/compose'), - 'Outlook Calendar 必须使用官方 compose 深度链接' - ); - assert.ok( - content.includes('rru=addevent'), - 'Outlook Calendar 必须指定 rru=addevent' - ); - - // 后端 RFC 5545 ICS 接口规范检查 - assert.ok( - content.includes('/open/card/portal/meeting-ics'), - 'ICS 下载必须复用后端 /open/card/portal/meeting-ics 接口' - ); - assert.ok( - content.includes('summary='), - 'ICS 请求参数必须包含 summary' - ); - assert.ok( - content.includes('startTime='), - 'ICS 请求参数必须包含 startTime' - ); - assert.ok( - content.includes('endTime='), - 'ICS 请求参数必须包含 endTime' - ); - - // UTC RFC 5545 时间戳对齐 - assert.ok( - content.includes('getUTCFullYear') && content.includes('getUTCMonth'), - '时间戳格式化必须采用 UTC 时区计算避免跨国时区偏差' + content.includes('appointment-submit-btn'), + '预约提交按钮必须具备独立规范样式类' ); });