/** * OmniCard AI (CRMmp) - 无摩擦互换名片留资业务驱动层 * 严格遵循 uts 强类型规范与 AGENTS.md 约束 */ import { LeadEntity, SubmitLeadParams } from '../../common/types.uts'; import { submitLead } from '../../common/api.uts'; import { getCurrentLocale, getOmniText } from '../../common/locale.uts'; export type ExchangeForm = { name: string; company: string; phone: string; email: string; notes: string; }; export type ExchangeState = { form: ExchangeForm; cardId: string; slug: string; submitting: boolean; canSubmit: boolean; errorMessage: string; }; export type ExchangePageOptions = { cardId?: string; slug?: string; id?: string; }; const VISITOR_STORAGE_KEY = 'omni_visitor_id'; /** * 读取本地已有访客标识,不在本页新建 */ function readVisitorId(): string { try { const val = uni.getStorageSync(VISITOR_STORAGE_KEY); return val != null ? String(val) : ''; } catch (e) { return ''; } } /** * 创建互换名片表单初始状态 */ export function createExchangeState(): ExchangeState { return { form: { name: '', company: '', phone: '', email: '', notes: '', }, cardId: '', slug: '', submitting: false, canSubmit: false, errorMessage: '', }; } /** * 解析提交用的名片标识:优先 cardId,其次 slug */ export function resolveExchangeCardId(state: ExchangeState): string { if (state.cardId && state.cardId.trim() !== '') { return state.cardId.trim(); } if (state.slug && state.slug.trim() !== '') { return state.slug.trim(); } return ''; } /** * 从页面 onLoad options 写入名片标识 * 缺少 cardId 与 slug 时 toast 并禁止提交 */ export function applyExchangeOptions( state: ExchangeState, options: ExchangePageOptions | null ): boolean { const cardIdRaw = options != null ? options.cardId || options.id || '' : ''; const slugRaw = options != null ? options.slug || '' : ''; state.cardId = String(cardIdRaw).trim(); state.slug = String(slugRaw).trim(); state.canSubmit = resolveExchangeCardId(state) !== ''; if (!state.canSubmit) { uni.showToast({ title: '缺少名片标识,无法提交', icon: 'none', }); } return state.canSubmit; } /** * 校验姓名 + (WhatsApp/phone 或 email) * 通过返回空字符串 */ export function validateExchangeForm(form: ExchangeForm): string { const name = form.name ? form.name.trim() : ''; if (name === '') { return '请填写姓名'; } const phone = form.phone ? form.phone.trim() : ''; const email = form.email ? form.email.trim() : ''; if (phone === '' && email === '') { return '请填写 WhatsApp 或邮箱'; } return ''; } /** * 仅接口明确成功才视为互换完成: * - persisted === false → 未落库,不成功 * - persisted === true → 成功 * - 无 persisted 字段时,须有非空 id(CRM 留资落库回执) */ export function isExchangeSubmitSuccess(lead: LeadEntity | null | Record): boolean { if (lead == null || typeof lead !== 'object') { return false; } const record = lead as Record; if ('persisted' in record) { return record['persisted'] === true; } const id = record['id']; return id != null && String(id).trim() !== ''; } /** * 清空表单字段,保留名片标识 */ export function resetExchangeForm(state: ExchangeState): void { state.form.name = ''; state.form.company = ''; state.form.phone = ''; state.form.email = ''; state.form.notes = ''; state.errorMessage = ''; } /** * 提交名片交换:校验通过后调用已有 submitLead */ export async function submitExchange(state: ExchangeState): Promise { const cardId = resolveExchangeCardId(state); if (!state.canSubmit || cardId === '') { uni.showToast({ title: '缺少名片标识,无法提交', icon: 'none', }); return null; } const invalidReason = validateExchangeForm(state.form); if (invalidReason !== '') { uni.showToast({ title: invalidReason, icon: 'none', }); return null; } if (state.submitting) { return null; } const name = state.form.name.trim(); const company = state.form.company.trim(); const phone = state.form.phone.trim(); const email = state.form.email.trim(); const notes = state.form.notes.trim(); const visitorId = readVisitorId(); const payload: SubmitLeadParams = { cardId, name, }; if (company !== '') { payload.company = company; } if (phone !== '') { payload.phone = phone; } if (email !== '') { payload.email = email; } if (notes !== '') { payload.notes = notes; } if (visitorId !== '') { payload.visitorId = visitorId; } payload.source = 'link'; state.submitting = true; state.errorMessage = ''; try { const lead = await submitLead(payload); state.submitting = false; if (!isExchangeSubmitSuccess(lead)) { state.errorMessage = '交换未保存,请稍后重试'; uni.showToast({ title: state.errorMessage, icon: 'none', }); return null; } resetExchangeForm(state); uni.showToast({ title: '留资已提交', icon: 'none', }); return lead; } catch (err) { state.submitting = false; if (err instanceof Error) { state.errorMessage = err.message; } else { state.errorMessage = '交换名片失败'; } return null; } } export type ExchangeCopyKey = | 'title' | 'desc' | 'warnMissingId' | 'ocrTitle' | 'ocrSubtitle' | 'ocrScan' | 'ocrScanning' | 'ocrPreviewTitle' | 'ocrPreviewHint' | 'ocrDelete' | 'nameLabel' | 'namePlaceholder' | 'companyLabel' | 'companyPlaceholder' | 'whatsappLabel' | 'whatsappPlaceholder' | 'emailLabel' | 'emailPlaceholder' | 'notesLabel' | 'notesPlaceholder' | 'submit' | 'submitting' | 'badge' | 'badgeAi' | 'contactSectionTitle' | 'contactSectionDesc' | 'intentTitle' | 'intentCooperate' | 'intentPurchase' | 'intentInquiry' | 'intentAgent' | 'intentTech' | 'privacyTitle' | 'privacyDesc' | 'successTitle' | 'successDesc' | 'backPortal' | 'submitAgain' | 'whatsappOrEmailTip' | 'whatsappBadge' | 'requiredMark'; const EXCHANGE_TEXTS: Record> = { 'zh-CN': { desc: '留下姓名与联系方式,双方建立高效商务连接', warnMissingId: '缺少名片标识,无法提交', ocrTitle: '纸质名片识别', ocrSubtitle: '拍照或上传名片,自动识别填入', ocrScan: '拍摄/上传', ocrScanning: '识别中...', ocrPreviewTitle: '名片原图', ocrPreviewHint: '点击放大预览原图', nameLabel: '姓名', namePlaceholder: '请输入姓名', companyLabel: '公司', companyPlaceholder: '选填,例如:ABC Global Ltd', whatsappPlaceholder: 'WhatsApp 号码(例:+86 138... 或 +971 50...)', emailLabel: '邮箱', emailPlaceholder: '工作邮箱 (name@company.com)', notesLabel: '需求留言 / 洽谈备注', notesPlaceholder: '选填,简要说明需求或选上方意向标签', submit: '交换名片', submitting: '提交中...', badge: '双向名片互换', badgeAi: '名片扫描识别', contactSectionTitle: '联系人信息', contactSectionDesc: '留下真实商务联系方式,便于后续及时跟进', intentTitle: '沟通意向 (快捷选择)', intentCooperate: '商务合作', intentPurchase: '产品采购', intentInquiry: '索取资料', intentAgent: '代理加盟', intentTech: '技术交流', privacyTitle: '隐私与数据安全', privacyDesc: '您的信息仅用于双方商务对接与资料发送,遵循严格的国际隐私保护与数据合规标准。', successTitle: '名片交换已完成', successDesc: '名片所有者已收到您的联系方式,将尽快与您取得联系。', backPortal: '返回微官网', submitAgain: '重新填写', whatsappOrEmailTip: 'WhatsApp 或邮箱至少填写一项', whatsappBadge: '全球常用', requiredMark: '必填', }, 'en-US': { desc: 'Leave your contact details to connect and receive business information.', warnMissingId: 'This card is missing an ID, so the form cannot be submitted.', ocrTitle: 'Scan Paper Business Card', ocrSubtitle: 'Snap or upload a card image to autofill the form instantly.', ocrScan: 'Photo / Upload', ocrScanning: 'Scanning...', ocrPreviewTitle: 'Card Photo', ocrPreviewHint: 'Tap to view full size', nameLabel: 'Name', namePlaceholder: 'Your full name', companyLabel: 'Company', companyPlaceholder: 'Optional, e.g. Acme Global Inc.', whatsappPlaceholder: 'WhatsApp number (e.g. +1 555... or +971 50...)', emailLabel: 'Email', emailPlaceholder: 'Work email (name@company.com)', notesLabel: 'Message / Business Notes', notesPlaceholder: 'Optional — briefly describe what you need or tap intent chips above', submit: 'Submit & Exchange', submitting: 'Submitting...', badge: 'Mutual Card Exchange', badgeAi: 'AI Fast Fill', contactSectionTitle: 'Contact Details', contactSectionDesc: 'Provide valid contact info for direct business follow-up', intentTitle: 'Business Intent (Quick Select)', intentCooperate: 'Partnership', intentPurchase: 'Procurement', intentInquiry: 'Request Info', intentAgent: 'Distributor', intentTech: 'Tech Exchange', privacyTitle: 'Enterprise Privacy & Security', privacyDesc: 'Your details are strictly used for direct business communication in compliance with global privacy standards.', successTitle: 'Card Exchanged Successfully', successDesc: 'The card owner has received your contact details and business intent, and will reach out via WhatsApp or email soon.', backPortal: 'Back to Profile', submitAgain: 'Exchange Another Card', whatsappOrEmailTip: 'Provide at least WhatsApp or Email', whatsappBadge: 'Recommended', requiredMark: 'Required', }, 'ar-SA': { desc: 'اترك اسمك وطريقة التواصل لبناء علاقة عمل مباشرة وفعالة.', warnMissingId: 'البطاقة تفتقد للمعرف، لا يمكن إرسال النموذج.', ocrTitle: 'مسح البطاقة الورقية بالذكاء الاصطناعي', ocrSubtitle: 'التقط صورة أو حمّل بطاقة للملء التلقائي الفوري.', ocrScan: 'تصوير / رفع', ocrScanning: 'جاري المسح...', ocrPreviewTitle: 'صورة البطاقة', ocrPreviewHint: 'اضغط لعرض بالحجم الكامل', nameLabel: 'الاسم', namePlaceholder: 'اسمك الكامل', companyLabel: 'الشركة', companyPlaceholder: 'اختياري، مثلاً: شركة النور العالمية', whatsappPlaceholder: 'رقم واتساب (مثلاً: +971 50...)', emailLabel: 'البريد الإلكتروني', emailPlaceholder: 'بريد العمل (name@company.com)', notesLabel: 'ملاحظات أو رسالة', notesPlaceholder: 'اختياري — اكتب موجزاً عن اهتمامك أو اختر أعلاه', submit: 'إرسال وتبادل البطاقة', submitting: 'جاري الإرسال...', badge: 'تبادل بطاقات الأعمال', badgeAi: 'ملء فوري بالذكاء الاصطناعي', contactSectionTitle: 'معلومات الاتصال', contactSectionDesc: 'يرجى تقديم بيانات اتصال صحيحة للمتابعة المباشرة', intentTitle: 'الغرض من التواصل (اختيار سريع)', intentCooperate: 'شراكة أعمال', intentPurchase: 'شراء منتجات', intentInquiry: 'طلب معلومات', intentAgent: 'وكيل معتمد', intentTech: 'تبادل تقني', privacyTitle: 'حماية وأمان البيانات المعتمد', privacyDesc: 'تُستخدم بياناتك حصرياً للتواصل التجاري الثنائي وفق المعايير الدولية لحماية البيانات.', successTitle: 'تم تبادل البطاقة بنجاح', successDesc: 'استلم صاحب البطاقة معلومات اتصالك والغرض، وسيتواصل معك قريباً عبر واتساب أو البريد الإلكتروني.', backPortal: 'العودة للملف الشخصي', submitAgain: 'تبادل بطاقة أخرى', whatsappOrEmailTip: 'يرجى إدخال واتساب أو البريد على الأقل', whatsappBadge: 'المفضل', requiredMark: 'مطلوب', }, 'es-ES': { desc: 'Deje sus datos de contacto para establecer una conexión comercial directa.', warnMissingId: 'Esta tarjeta no tiene identificador y no se puede enviar.', ocrTitle: 'Escaneo de tarjeta física con IA', ocrSubtitle: 'Tome una foto o suba una imagen para autorrellenar el formulario.', ocrScan: 'Foto / Subir', ocrScanning: 'Escaneando...', ocrPreviewTitle: 'Foto de tarjeta', ocrPreviewHint: 'Toque para ampliar', nameLabel: 'Nombre', namePlaceholder: 'Su nombre completo', companyLabel: 'Empresa', companyPlaceholder: 'Opcional, ej. Global Corp', whatsappPlaceholder: 'Número de WhatsApp (ej. +34 600...)', emailLabel: 'Correo electrónico', emailPlaceholder: 'Correo de trabajo (name@company.com)', notesLabel: 'Mensaje / Notas', notesPlaceholder: 'Opcional: describa su requerimiento o seleccione una opción arriba', submit: 'Enviar e intercambiar', submitting: 'Enviando...', badge: 'Intercambio mutuo de tarjetas', badgeAi: 'IA Relleno Rápido', contactSectionTitle: 'Datos de contacto', contactSectionDesc: 'Facilite sus datos válidos para el seguimiento comercial', intentTitle: 'Intención comercial (selección rápida)', intentCooperate: 'Alianza comercial', intentPurchase: 'Adquisición', intentInquiry: 'Solicitar catálogo', intentAgent: 'Distribución', intentTech: 'Consulta técnica', privacyTitle: 'Garantía de privacidad empresarial', privacyDesc: 'Sus datos se utilizan estrictamente para el contacto comercial según los estándares internacionales de privacidad.', successTitle: 'Tarjeta intercambiada con éxito', successDesc: 'El titular de la tarjeta ha recibido sus datos y se comunicará pronto por WhatsApp o correo.', backPortal: 'Volver al perfil', submitAgain: 'Intercambiar otra', whatsappOrEmailTip: 'Ingrese al menos WhatsApp o correo', whatsappBadge: 'Recomendado', requiredMark: 'Obligatorio', }, 'de-DE': { desc: 'Hinterlassen Sie Ihre Kontaktdaten für einen direkten geschäftlichen Austausch.', warnMissingId: 'Dieser Karte fehlt eine Kennung, daher kein Versand möglich.', ocrTitle: 'Visitenkarte mit KI scannen', ocrSubtitle: 'Foto aufnehmen oder Bild hochladen für automatisches Ausfüllen.', ocrScan: 'Foto / Upload', ocrScanning: 'Wird gescannt...', ocrPreviewTitle: 'Visitenkartenfoto', ocrPreviewHint: 'Antippen zum Vergrößern', nameLabel: 'Name', namePlaceholder: 'Ihr vollständiger Name', companyLabel: 'Unternehmen', companyPlaceholder: 'Optional, z. B. Muster GmbH', whatsappPlaceholder: 'WhatsApp-Nummer (z. B. +49 151...)', emailLabel: 'E-Mail', emailPlaceholder: 'Geschäftliche E-Mail (name@company.com)', notesLabel: 'Nachricht / Notiz', notesPlaceholder: 'Optional — kurze Beschreibung Ihres Anliegens', submit: 'Absenden & austauschen', submitting: 'Wird gesendet...', badge: 'Gegenseitiger Austausch', badgeAi: 'KI-Schnellausfüllen', contactSectionTitle: 'Kontaktdaten', contactSectionDesc: 'Geben Sie Ihre Daten für geschäftliche Nachfassaktionen an', intentTitle: 'Geschäftsabsicht (Schnellauswahl)', intentCooperate: 'Kooperation', intentPurchase: 'Beschaffung', intentInquiry: 'Infos anfordern', intentAgent: 'Vertriebspartner', intentTech: 'Technischer Austausch', privacyTitle: 'Unternehmensdatenschutz & Sicherheit', privacyDesc: 'Ihre Daten werden vertraulich und streng nach internationalen Datenschutzstandards verarbeitet.', successTitle: 'Visitenkarte erfolgreich übermittelt', successDesc: 'Der Karteninhaber hat Ihre Kontaktdaten erhalten und meldet sich in Kürze per WhatsApp oder E-Mail.', backPortal: 'Zurück zum Profil', submitAgain: 'Weitere Karte tauschen', whatsappOrEmailTip: 'Mindestens WhatsApp oder E-Mail angeben', whatsappBadge: 'Empfohlen', requiredMark: 'Erforderlich', }, 'fr-FR': { desc: 'Laissez vos coordonnées pour établir un contact professionnel direct.', warnMissingId: 'Identifiant manquant, formulaire impossible à envoyer.', ocrTitle: 'Numérisation de carte par IA', ocrSubtitle: 'Prenez une photo pour préremplir automatiquement le formulaire.', ocrScan: 'Photo / Importer', ocrScanning: 'Numérisation...', ocrPreviewTitle: 'Photo de carte', ocrPreviewHint: 'Appuyez pour agrandir', nameLabel: 'Nom', namePlaceholder: 'Votre nom complet', companyLabel: 'Entreprise', companyPlaceholder: 'Facultatif, ex. Société SAS', whatsappPlaceholder: 'Numéro WhatsApp (ex. +33 6...)', emailLabel: 'E-mail', emailPlaceholder: 'E-mail professionnel (name@company.com)', notesLabel: 'Message / Remarques', notesPlaceholder: 'Facultatif — décrivez brièvement votre projet', submit: 'Envoyer et échanger', submitting: 'Envoi en cours...', badge: 'Échange mutuel de cartes', badgeAi: 'Remplissage IA rapide', contactSectionTitle: 'Coordonnées', contactSectionDesc: 'Indiquez vos coordonnées pour le suivi commercial direct', intentTitle: 'Intention commerciale (sélection rapide)', intentCooperate: 'Partenariat', intentPurchase: 'Approvisionnement', intentInquiry: 'Documentation', intentAgent: 'Distribution', intentTech: 'Échange technique', privacyTitle: 'Confidentialité et sécurité des données', privacyDesc: 'Vos données sont strictement réservées au contact commercial conformément aux normes internationales.', successTitle: 'Carte échangée avec succès', successDesc: 'Le propriétaire a reçu vos coordonnées et vous contactera bientôt par WhatsApp ou e-mail.', backPortal: 'Retour au profil', submitAgain: 'Échanger une autre', whatsappOrEmailTip: 'Indiquez au moins WhatsApp ou e-mail', whatsappBadge: 'Recommandé', requiredMark: 'Requis', }, 'ja-JP': { desc: '連絡先をご入力いただき、双方の円滑なビジネス連絡に役立てます。', warnMissingId: '名刺識別子がないため送信できません。', ocrTitle: 'AI 紙名刺スキャン', ocrSubtitle: '写真を撮るか画像をアップロードして自動入力します。', ocrScan: '撮影 / アップロード', ocrScanning: '認識中...', ocrPreviewTitle: '名刺画像', ocrPreviewHint: 'タップして拡大表示', nameLabel: 'お名前', namePlaceholder: 'お名前を入力してください', companyLabel: '貴社名', companyPlaceholder: '任意(例:株式会社〇〇)', whatsappPlaceholder: 'WhatsApp または電話番号(例:+81 90...)', emailLabel: 'メールアドレス', emailPlaceholder: '勤務先メールアドレス (name@company.com)', notesLabel: 'ご用件 / メモ', notesPlaceholder: '任意 — ご要望の概要または上のタグを選択', submit: '名刺を交換して送信', submitting: '送信中...', badge: '相互名刺交換', badgeAi: 'AI 高速自動入力', contactSectionTitle: '連絡先情報', contactSectionDesc: '今後のビジネスフォローのため正確な連絡先をご入力ください', intentTitle: 'ご用件(クイック選択)', intentCooperate: 'ビジネス提携', intentPurchase: '製品購入', intentInquiry: '資料請求', intentAgent: '代理店希望', intentTech: '技術相談', privacyTitle: 'エンタープライズ情報セキュリティ保護', privacyDesc: 'ご入力情報はビジネス連絡にのみ使用され、国際的なプライバシー基準に準拠します。', successTitle: '名刺交換が完了しました', successDesc: '名刺の持ち主に連絡先が送信されました。WhatsApp またはメールでご連絡いたします。', backPortal: 'プロフィールに戻る', submitAgain: '別の名刺を交換', whatsappOrEmailTip: 'WhatsApp またはメールのいずれかを入力してください', whatsappBadge: '推奨', requiredMark: '必須', }, }; /** * 互换名片页文案:能复用 locale 词典的走 getOmniText,其余走多语言映射表 */ export function resolveExchangeText(key: string): string { const locale = getCurrentLocale(); if (key === 'title') { return getOmniText(locale, 'card.exchange'); } if (key === 'ocrDelete') { return getOmniText(locale, 'common.delete'); } if (key === 'whatsappLabel') { return getOmniText(locale, 'portal.whatsapp'); } const langMap = EXCHANGE_TEXTS[locale] || EXCHANGE_TEXTS['en-US'] || EXCHANGE_TEXTS['zh-CN']; if (langMap != null && langMap[key] != null) { return langMap[key]; } const fallbackMap = EXCHANGE_TEXTS['en-US'] || EXCHANGE_TEXTS['zh-CN']; if (fallbackMap != null && fallbackMap[key] != null) { return fallbackMap[key]; } return key; }