- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
288 行
6.9 KiB
Plaintext
288 行
6.9 KiB
Plaintext
/**
|
||
* OmniCard AI (CRMmp) - 名片主 NFC 贴卡激活业务驱动层
|
||
* H5 无天线走手动 UID;APP startScan 为占位,调不到原生 NFC 时回退手动输入
|
||
*/
|
||
|
||
import { NFCRecord } from '../../../common/types.uts';
|
||
import { bindNfcDevice, activateNfcDevice } from '../../../common/api.uts';
|
||
import { getCurrentLocale, getOmniText } from '../../../common/locale.uts';
|
||
|
||
export type ScanCopyKey =
|
||
| 'title'
|
||
| 'desc'
|
||
| 'uidLabel'
|
||
| 'uidPlaceholder'
|
||
| 'cardIdLabel'
|
||
| 'cardIdPlaceholder'
|
||
| 'cta'
|
||
| 'empty'
|
||
| 'uidRequired'
|
||
| 'cardIdRequired'
|
||
| 'nfcUnavailable'
|
||
| 'success'
|
||
| 'error'
|
||
| 'retry';
|
||
|
||
export type ScanState = {
|
||
uid: string;
|
||
cardIdRaw: string;
|
||
deviceCardId: number | null;
|
||
submitting: boolean;
|
||
scanning: boolean;
|
||
manualInput: boolean;
|
||
errorMessage: string;
|
||
empty: boolean;
|
||
record: NFCRecord | null;
|
||
};
|
||
|
||
export type ScanPageOptions = {
|
||
cardId?: string;
|
||
uid?: string;
|
||
id?: string;
|
||
};
|
||
|
||
export type ActivateByTapInput = {
|
||
uid: string;
|
||
cardId?: string | number | null;
|
||
deviceCardId?: string | number | null;
|
||
};
|
||
|
||
function isZhLocale(): boolean {
|
||
return getCurrentLocale() === 'zh-CN';
|
||
}
|
||
|
||
/**
|
||
* 页面文案:能复用 locale.uts 的走词典,缺 key 时中英本地词典
|
||
*/
|
||
export function resolveScanText(key: ScanCopyKey): string {
|
||
const locale = getCurrentLocale();
|
||
if (key === 'title') {
|
||
return getOmniText(locale, 'card.scan_nfc');
|
||
}
|
||
if (key === 'success') {
|
||
return getOmniText(locale, 'common.success');
|
||
}
|
||
if (key === 'error') {
|
||
return getOmniText(locale, 'common.error');
|
||
}
|
||
if (key === 'empty') {
|
||
return getOmniText(locale, 'common.empty');
|
||
}
|
||
|
||
const zh = isZhLocale();
|
||
if (key === 'desc') {
|
||
return zh
|
||
? '贴近实体卡,或手动填写芯片 UID 完成绑定'
|
||
: 'Hold the card to this phone, or enter the chip UID to bind it';
|
||
}
|
||
if (key === 'uidLabel') {
|
||
return zh ? '芯片 UID' : 'Chip UID';
|
||
}
|
||
if (key === 'uidPlaceholder') {
|
||
return zh ? '输入芯片 UID' : 'Enter chip UID';
|
||
}
|
||
if (key === 'cardIdLabel') {
|
||
return zh ? '名片编号' : 'Card ID';
|
||
}
|
||
if (key === 'cardIdPlaceholder') {
|
||
return zh ? '输入名片编号' : 'Enter card ID';
|
||
}
|
||
if (key === 'cta') {
|
||
return zh ? '激活并绑定' : 'Activate and bind';
|
||
}
|
||
if (key === 'uidRequired') {
|
||
return zh ? '请填写芯片 UID' : 'Enter the chip UID';
|
||
}
|
||
if (key === 'cardIdRequired') {
|
||
return zh ? '请填写名片编号' : 'Enter a card ID';
|
||
}
|
||
if (key === 'nfcUnavailable') {
|
||
return zh
|
||
? '当前无法读取 NFC,请手动填写 UID'
|
||
: 'NFC is unavailable. Enter the UID instead';
|
||
}
|
||
if (key === 'retry') {
|
||
return zh ? '重试' : 'Try again';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 去空。空串视为非法 UID
|
||
*/
|
||
export function parseUid(raw: string | number | null | undefined): string {
|
||
if (raw == null) {
|
||
return '';
|
||
}
|
||
return String(raw).trim();
|
||
}
|
||
|
||
/**
|
||
* 解析为正整数。非正整数返回 null
|
||
*/
|
||
export function parseCardId(raw: string | number | null | undefined): number | null {
|
||
if (raw == null) {
|
||
return null;
|
||
}
|
||
if (typeof raw === 'number') {
|
||
if (raw <= 0) {
|
||
return null;
|
||
}
|
||
if (Math.floor(raw) !== raw) {
|
||
return null;
|
||
}
|
||
return raw;
|
||
}
|
||
const text = String(raw).trim();
|
||
if (text === '') {
|
||
return null;
|
||
}
|
||
if (!/^[1-9]\d*$/.test(text)) {
|
||
return null;
|
||
}
|
||
return Number(text);
|
||
}
|
||
|
||
function readErrorMessage(err: unknown): string {
|
||
if (err instanceof Error && err.message !== '') {
|
||
return err.message;
|
||
}
|
||
return resolveScanText('error');
|
||
}
|
||
|
||
/**
|
||
* 贴卡激活:有入参 cardId 则 bind;设备已有 cardId 且入参为空则 activate
|
||
*/
|
||
export async function activateByTap(
|
||
input: ActivateByTapInput,
|
||
existing?: { cardId?: string | number | null } | null
|
||
): Promise<NFCRecord> {
|
||
const uid = parseUid(input.uid);
|
||
if (uid === '') {
|
||
throw new Error(resolveScanText('uidRequired'));
|
||
}
|
||
|
||
const incomingCardId = parseCardId(input.cardId);
|
||
const fromInputDevice = parseCardId(input.deviceCardId);
|
||
const fromExisting = existing != null ? parseCardId(existing.cardId) : null;
|
||
const deviceCardId = fromInputDevice != null ? fromInputDevice : fromExisting;
|
||
|
||
try {
|
||
if (incomingCardId == null && deviceCardId != null) {
|
||
return await activateNfcDevice({ uid });
|
||
}
|
||
if (incomingCardId != null) {
|
||
return await bindNfcDevice({ uid, cardId: incomingCardId });
|
||
}
|
||
throw new Error(resolveScanText('cardIdRequired'));
|
||
} catch (err) {
|
||
throw new Error(readErrorMessage(err));
|
||
}
|
||
}
|
||
|
||
export function createScanState(): ScanState {
|
||
return {
|
||
uid: '',
|
||
cardIdRaw: '',
|
||
deviceCardId: null,
|
||
submitting: false,
|
||
scanning: false,
|
||
manualInput: true,
|
||
errorMessage: '',
|
||
empty: true,
|
||
record: null,
|
||
};
|
||
}
|
||
|
||
export function applyScanOptions(state: ScanState, options: ScanPageOptions | null): void {
|
||
if (options == null) {
|
||
return;
|
||
}
|
||
if (options.uid != null) {
|
||
state.uid = parseUid(options.uid);
|
||
}
|
||
const cardRaw =
|
||
options.cardId != null && options.cardId !== ''
|
||
? options.cardId
|
||
: options.id != null
|
||
? options.id
|
||
: '';
|
||
if (cardRaw !== '') {
|
||
state.cardIdRaw = String(cardRaw).trim();
|
||
}
|
||
state.empty = parseUid(state.uid) === '';
|
||
}
|
||
|
||
/**
|
||
* APP 原生贴卡占位。调不到天线时回退手动输入,禁止伪造成功
|
||
*/
|
||
export function startScan(state: ScanState): boolean {
|
||
// #ifdef APP
|
||
// Native NFC antenna is not wired in this skeleton.
|
||
// #endif
|
||
state.scanning = false;
|
||
state.manualInput = true;
|
||
state.record = null;
|
||
if (parseUid(state.uid) === '') {
|
||
state.empty = true;
|
||
state.errorMessage = resolveScanText('nfcUnavailable');
|
||
}
|
||
return false;
|
||
}
|
||
|
||
export function resolveStatusLine(state: ScanState): string {
|
||
if (state.errorMessage !== '') {
|
||
return state.errorMessage;
|
||
}
|
||
if (state.empty && parseUid(state.uid) === '') {
|
||
return resolveScanText('empty');
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 页面主 CTA:校验后走 activateByTap,失败写入一句错误
|
||
*/
|
||
export async function submitScan(state: ScanState): Promise<NFCRecord | null> {
|
||
if (state.submitting) {
|
||
return null;
|
||
}
|
||
|
||
const uid = parseUid(state.uid);
|
||
if (uid === '') {
|
||
state.empty = true;
|
||
state.errorMessage = resolveScanText('uidRequired');
|
||
return null;
|
||
}
|
||
|
||
state.uid = uid;
|
||
state.empty = false;
|
||
state.submitting = true;
|
||
state.errorMessage = '';
|
||
|
||
try {
|
||
const record = await activateByTap(
|
||
{
|
||
uid,
|
||
cardId: state.cardIdRaw,
|
||
deviceCardId: state.deviceCardId,
|
||
},
|
||
{ cardId: state.deviceCardId }
|
||
);
|
||
state.submitting = false;
|
||
state.record = record;
|
||
if (record.cardId != null) {
|
||
const bound = parseCardId(record.cardId);
|
||
if (bound != null) {
|
||
state.deviceCardId = bound;
|
||
}
|
||
}
|
||
return record;
|
||
} catch (err) {
|
||
state.submitting = false;
|
||
state.record = null;
|
||
state.errorMessage = readErrorMessage(err);
|
||
return null;
|
||
}
|
||
}
|