文件
OmniCard/mpuniapp/pages/portal/useExchange.uts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

591 行
21 KiB
Plaintext

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* 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<string, unknown>): boolean {
if (lead == null || typeof lead !== 'object') {
return false;
}
const record = lead as Record<string, unknown>;
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<LeadEntity | null> {
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<string, Record<string, string>> = {
'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;
}