- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
210 行
4.5 KiB
Plaintext
210 行
4.5 KiB
Plaintext
/**
|
|
* OmniCard AI (CRMmp) - 无摩擦互换名片留资业务驱动层
|
|
* 严格遵循 uts 强类型规范与 AGENTS.md 约束
|
|
*/
|
|
|
|
import { LeadEntity, SubmitLeadParams } from '../../common/types.uts';
|
|
import { submitLead } from '../../common/api.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 '';
|
|
}
|
|
|
|
/**
|
|
* 清空表单字段,保留名片标识
|
|
*/
|
|
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;
|
|
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;
|
|
}
|
|
}
|