- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
555 行
15 KiB
Plaintext
555 行
15 KiB
Plaintext
/**
|
|
* OmniCard AI (CRMmp) - 名片主 NFC 场景模式切换业务驱动层
|
|
* 日间展会 business 与晚宴社交 social;挂失设备拒绝切换
|
|
*/
|
|
|
|
import {
|
|
NFCMaterial,
|
|
NFCRecord,
|
|
NFCSceneMode,
|
|
NFCStatus,
|
|
NfcBindParams,
|
|
} from '../../../common/types.uts';
|
|
import { switchNfcCard, updateNfcDevice } from '../../../common/api.uts';
|
|
import {
|
|
getCurrentLocale,
|
|
getOmniText,
|
|
OmniLocale,
|
|
} from '../../../common/locale.uts';
|
|
|
|
export type ModeCopyKey =
|
|
| 'title'
|
|
| 'desc'
|
|
| 'uidLabel'
|
|
| 'cardIdLabel'
|
|
| 'business'
|
|
| 'social'
|
|
| 'apply'
|
|
| 'statusLabel'
|
|
| 'sceneLabel'
|
|
| 'statusActive'
|
|
| 'statusSuspended'
|
|
| 'statusUnbound'
|
|
| 'suspendedError'
|
|
| 'invalidMode'
|
|
| 'missingUid'
|
|
| 'missingCardId'
|
|
| 'missingDevice'
|
|
| 'applySuccess'
|
|
| 'applyError'
|
|
| 'loading';
|
|
|
|
export type ModePageOptions = {
|
|
uid?: string;
|
|
cardId?: string;
|
|
id?: string;
|
|
status?: string;
|
|
sceneMode?: string;
|
|
tenantId?: string;
|
|
batchNo?: string;
|
|
material?: string;
|
|
};
|
|
|
|
export type NfcModeState = {
|
|
uid: string;
|
|
cardId: string;
|
|
selectedMode: NFCSceneMode;
|
|
status: string;
|
|
sceneMode: string;
|
|
device: NFCRecord | null;
|
|
submitting: boolean;
|
|
errorMessage: string;
|
|
};
|
|
|
|
export type ApplySceneParams = {
|
|
uid: string;
|
|
cardId: string | number;
|
|
sceneMode: string;
|
|
device: NFCRecord | null;
|
|
};
|
|
|
|
export type ApplySceneApi = {
|
|
switchNfcCard: (params: NfcBindParams) => Promise<NFCRecord>;
|
|
updateNfcDevice: (
|
|
payload: NFCRecord & { id: string | number }
|
|
) => Promise<NFCRecord>;
|
|
};
|
|
|
|
export type ApplySceneResult = {
|
|
ok: boolean;
|
|
error: string;
|
|
device: NFCRecord | null;
|
|
};
|
|
|
|
const MODE_COPY: Record<OmniLocale, Record<ModeCopyKey, string>> = {
|
|
'zh-CN': {
|
|
title: '场景模式',
|
|
desc: '日间展会用商务档,晚宴社交用社交档。挂失设备不可切换。',
|
|
uidLabel: '芯片 UID',
|
|
cardIdLabel: '名片 ID',
|
|
business: '日间展会',
|
|
social: '晚宴社交',
|
|
apply: '应用场景',
|
|
statusLabel: '当前状态',
|
|
sceneLabel: '当前场景',
|
|
statusActive: '已激活',
|
|
statusSuspended: '已挂失',
|
|
statusUnbound: '未绑定',
|
|
suspendedError: '设备已挂失,无法切换场景',
|
|
invalidMode: '仅支持展会或社交场景',
|
|
missingUid: '请填写芯片 UID',
|
|
missingCardId: '请填写名片 ID',
|
|
missingDevice: '缺少设备标识,无法更新场景',
|
|
applySuccess: '操作成功',
|
|
applyError: '操作失败',
|
|
loading: '加载中...',
|
|
},
|
|
'en-US': {
|
|
title: 'Scene mode',
|
|
desc: 'Use Business for daytime expos and Social for evening events. Suspended cards cannot be switched.',
|
|
uidLabel: 'Chip UID',
|
|
cardIdLabel: 'Card ID',
|
|
business: 'Daytime expo',
|
|
social: 'Evening social',
|
|
apply: 'Apply scene',
|
|
statusLabel: 'Current status',
|
|
sceneLabel: 'Current scene',
|
|
statusActive: 'Active',
|
|
statusSuspended: 'Suspended',
|
|
statusUnbound: 'Unbound',
|
|
suspendedError: 'This card is suspended and cannot change scenes',
|
|
invalidMode: 'Only business or social scenes are available',
|
|
missingUid: 'Enter the chip UID',
|
|
missingCardId: 'Enter the card ID',
|
|
missingDevice: 'A device ID is required to update the scene',
|
|
applySuccess: 'Done',
|
|
applyError: 'Something went wrong',
|
|
loading: 'Loading...',
|
|
},
|
|
'ar-SA': {
|
|
title: 'وضع المشهد',
|
|
desc: 'استخدم وضع الأعمال للمعارض النهارية ووضع التواصل للأمسيات. لا يمكن تبديل البطاقة الموقوفة.',
|
|
uidLabel: 'معرّف الشريحة',
|
|
cardIdLabel: 'معرّف البطاقة',
|
|
business: 'معرض نهاري',
|
|
social: 'أمسية اجتماعية',
|
|
apply: 'تطبيق المشهد',
|
|
statusLabel: 'الحالة الحالية',
|
|
sceneLabel: 'المشهد الحالي',
|
|
statusActive: 'نشط',
|
|
statusSuspended: 'موقوف',
|
|
statusUnbound: 'غير مرتبط',
|
|
suspendedError: 'البطاقة موقوفة ولا يمكن تغيير المشهد',
|
|
invalidMode: 'يتوفر وضع الأعمال أو التواصل فقط',
|
|
missingUid: 'أدخل معرّف الشريحة',
|
|
missingCardId: 'أدخل معرّف البطاقة',
|
|
missingDevice: 'يلزم معرّف الجهاز لتحديث المشهد',
|
|
applySuccess: 'تم',
|
|
applyError: 'تعذّر الإكمال',
|
|
loading: 'جارٍ التحميل...',
|
|
},
|
|
'es-ES': {
|
|
title: 'Modo de escena',
|
|
desc: 'Usa Negocios en ferias diurnas y Social en eventos nocturnos. Las tarjetas suspendidas no se pueden cambiar.',
|
|
uidLabel: 'UID del chip',
|
|
cardIdLabel: 'ID de tarjeta',
|
|
business: 'Feria diurna',
|
|
social: 'Evento social',
|
|
apply: 'Aplicar escena',
|
|
statusLabel: 'Estado actual',
|
|
sceneLabel: 'Escena actual',
|
|
statusActive: 'Activa',
|
|
statusSuspended: 'Suspendida',
|
|
statusUnbound: 'Sin vincular',
|
|
suspendedError: 'La tarjeta está suspendida y no puede cambiar de escena',
|
|
invalidMode: 'Solo están disponibles las escenas de negocios o social',
|
|
missingUid: 'Introduce el UID del chip',
|
|
missingCardId: 'Introduce el ID de la tarjeta',
|
|
missingDevice: 'Se necesita el ID del dispositivo para actualizar la escena',
|
|
applySuccess: 'Listo',
|
|
applyError: 'No se pudo completar',
|
|
loading: 'Cargando...',
|
|
},
|
|
'de-DE': {
|
|
title: 'Szenenmodus',
|
|
desc: 'Business für Tagesmessen, Social für Abendveranstaltungen. Gesperrte Karten lassen sich nicht wechseln.',
|
|
uidLabel: 'Chip-UID',
|
|
cardIdLabel: 'Karten-ID',
|
|
business: 'Tagesmesse',
|
|
social: 'Abendempfang',
|
|
apply: 'Szene anwenden',
|
|
statusLabel: 'Aktueller Status',
|
|
sceneLabel: 'Aktuelle Szene',
|
|
statusActive: 'Aktiv',
|
|
statusSuspended: 'Gesperrt',
|
|
statusUnbound: 'Nicht gebunden',
|
|
suspendedError: 'Die Karte ist gesperrt und kann die Szene nicht wechseln',
|
|
invalidMode: 'Nur Business- oder Social-Szenen sind verfügbar',
|
|
missingUid: 'Chip-UID eingeben',
|
|
missingCardId: 'Karten-ID eingeben',
|
|
missingDevice: 'Zum Aktualisieren der Szene ist eine Geräte-ID erforderlich',
|
|
applySuccess: 'Erledigt',
|
|
applyError: 'Vorgang fehlgeschlagen',
|
|
loading: 'Wird geladen...',
|
|
},
|
|
'fr-FR': {
|
|
title: 'Mode scène',
|
|
desc: 'Business pour les salons de jour, Social pour les soirées. Les cartes suspendues ne peuvent pas être changées.',
|
|
uidLabel: 'UID de la puce',
|
|
cardIdLabel: 'ID de carte',
|
|
business: 'Salon de jour',
|
|
social: 'Soirée',
|
|
apply: 'Appliquer la scène',
|
|
statusLabel: 'Statut actuel',
|
|
sceneLabel: 'Scène actuelle',
|
|
statusActive: 'Active',
|
|
statusSuspended: 'Suspendue',
|
|
statusUnbound: 'Non liée',
|
|
suspendedError: 'Cette carte est suspendue et ne peut pas changer de scène',
|
|
invalidMode: 'Seules les scènes business ou social sont disponibles',
|
|
missingUid: 'Saisissez l’UID de la puce',
|
|
missingCardId: 'Saisissez l’ID de la carte',
|
|
missingDevice: 'Un identifiant d’appareil est requis pour mettre à jour la scène',
|
|
applySuccess: 'Terminé',
|
|
applyError: 'Échec de l’opération',
|
|
loading: 'Chargement...',
|
|
},
|
|
'ja-JP': {
|
|
title: 'シーンモード',
|
|
desc: '日中の展示会はビジネス、夜の交流はソーシャル。利用停止中のカードは切り替えできません。',
|
|
uidLabel: 'チップ UID',
|
|
cardIdLabel: '名刺 ID',
|
|
business: '日中展示会',
|
|
social: '夜の交流',
|
|
apply: 'シーンを適用',
|
|
statusLabel: '現在の状態',
|
|
sceneLabel: '現在のシーン',
|
|
statusActive: '有効',
|
|
statusSuspended: '利用停止',
|
|
statusUnbound: '未連携',
|
|
suspendedError: 'このカードは利用停止中のため、シーンを切り替えできません',
|
|
invalidMode: 'ビジネスまたはソーシャルのみ選択できます',
|
|
missingUid: 'チップ UID を入力してください',
|
|
missingCardId: '名刺 ID を入力してください',
|
|
missingDevice: 'シーンを更新するにはデバイス ID が必要です',
|
|
applySuccess: '完了しました',
|
|
applyError: '操作に失敗しました',
|
|
loading: '読み込み中...',
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 场景文案:能复用 locale.uts 的走词典,其余走本地七语表
|
|
*/
|
|
export function resolveModeText(key: ModeCopyKey): string {
|
|
if (key === 'applySuccess') {
|
|
return getOmniText(getCurrentLocale(), 'common.success');
|
|
}
|
|
if (key === 'applyError') {
|
|
return getOmniText(getCurrentLocale(), 'common.error');
|
|
}
|
|
if (key === 'loading') {
|
|
return getOmniText(getCurrentLocale(), 'common.loading');
|
|
}
|
|
const locale = getCurrentLocale();
|
|
const bag = MODE_COPY[locale] != null ? MODE_COPY[locale] : MODE_COPY['en-US'];
|
|
return bag[key];
|
|
}
|
|
|
|
/**
|
|
* 只放行 business | social
|
|
*/
|
|
export function parseSceneMode(raw: string | null | undefined): NFCSceneMode | null {
|
|
if (raw === 'business' || raw === 'social') {
|
|
return raw;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function parseStatus(raw: string | null | undefined): NFCStatus | null {
|
|
if (raw === 'active' || raw === 'suspended' || raw === 'unbound') {
|
|
return raw;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function parseMaterial(raw: string | null | undefined): NFCMaterial | null {
|
|
if (raw === 'wood' || raw === 'metal' || raw === 'pvc') {
|
|
return raw;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function trimText(value: string | number | null | undefined): string {
|
|
if (value == null) {
|
|
return '';
|
|
}
|
|
return String(value).trim();
|
|
}
|
|
|
|
function toCardNumber(cardId: string | number): number {
|
|
const n = typeof cardId === 'number' ? cardId : Number(String(cardId).trim());
|
|
if (n !== n || n === 0) {
|
|
return 0;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
function sameCardId(
|
|
left: string | number | null | undefined,
|
|
right: string | number | null | undefined
|
|
): boolean {
|
|
const a = trimText(left);
|
|
const b = trimText(right);
|
|
if (a === '' || b === '') {
|
|
return false;
|
|
}
|
|
return a === b;
|
|
}
|
|
|
|
function hasDeviceId(device: NFCRecord | null): boolean {
|
|
if (device == null) {
|
|
return false;
|
|
}
|
|
return trimText(device.id) !== '';
|
|
}
|
|
|
|
function fail(error: string, device: NFCRecord | null): ApplySceneResult {
|
|
return {
|
|
ok: false,
|
|
error,
|
|
device,
|
|
};
|
|
}
|
|
|
|
function resolveApi(api?: ApplySceneApi): ApplySceneApi {
|
|
if (api != null) {
|
|
return api;
|
|
}
|
|
return {
|
|
switchNfcCard,
|
|
updateNfcDevice,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 切换场景:挂失拒绝;换绑仅 active 且 cardId 不同;始终 update 写 sceneMode
|
|
*/
|
|
export async function applyScene(
|
|
params: ApplySceneParams,
|
|
api?: ApplySceneApi
|
|
): Promise<ApplySceneResult> {
|
|
const uid = trimText(params.uid);
|
|
if (uid === '') {
|
|
return fail(resolveModeText('missingUid'), params.device);
|
|
}
|
|
|
|
const mode = parseSceneMode(params.sceneMode);
|
|
if (mode == null) {
|
|
return fail(resolveModeText('invalidMode'), params.device);
|
|
}
|
|
|
|
const cardNumber = toCardNumber(params.cardId);
|
|
if (cardNumber === 0) {
|
|
return fail(resolveModeText('missingCardId'), params.device);
|
|
}
|
|
|
|
const device = params.device;
|
|
if (device != null && device.status === 'suspended') {
|
|
return fail(resolveModeText('suspendedError'), device);
|
|
}
|
|
|
|
if (!hasDeviceId(device)) {
|
|
return fail(resolveModeText('missingDevice'), device);
|
|
}
|
|
|
|
const readyDevice = device as NFCRecord;
|
|
const client = resolveApi(api);
|
|
let working = readyDevice;
|
|
|
|
const needSwitch =
|
|
readyDevice.status === 'active' && !sameCardId(readyDevice.cardId, params.cardId);
|
|
if (needSwitch) {
|
|
working = await client.switchNfcCard({
|
|
uid,
|
|
cardId: cardNumber,
|
|
});
|
|
}
|
|
|
|
const payload: NFCRecord & { id: string | number } = {
|
|
id: working.id,
|
|
uid: working.uid,
|
|
tenantId: working.tenantId,
|
|
cardId: working.cardId,
|
|
batchNo: working.batchNo,
|
|
material: working.material,
|
|
status: working.status,
|
|
sceneMode: mode,
|
|
lastScannedAt: working.lastScannedAt,
|
|
};
|
|
const updated = await client.updateNfcDevice(payload);
|
|
return {
|
|
ok: true,
|
|
error: '',
|
|
device: updated,
|
|
};
|
|
}
|
|
|
|
export function createModeState(): NfcModeState {
|
|
return {
|
|
uid: '',
|
|
cardId: '',
|
|
selectedMode: 'business',
|
|
status: '',
|
|
sceneMode: '',
|
|
device: null,
|
|
submitting: false,
|
|
errorMessage: '',
|
|
};
|
|
}
|
|
|
|
function buildDeviceFromOptions(
|
|
state: NfcModeState,
|
|
options: ModePageOptions
|
|
): NFCRecord | null {
|
|
const id = trimText(options.id);
|
|
if (id === '') {
|
|
return null;
|
|
}
|
|
const status = parseStatus(state.status);
|
|
const sceneMode = parseSceneMode(state.sceneMode);
|
|
const material = parseMaterial(options.material);
|
|
return {
|
|
id,
|
|
uid: state.uid,
|
|
tenantId: trimText(options.tenantId) !== '' ? trimText(options.tenantId) : 0,
|
|
cardId: state.cardId === '' ? null : state.cardId,
|
|
batchNo: trimText(options.batchNo),
|
|
material: material != null ? material : 'pvc',
|
|
status: status != null ? status : 'unbound',
|
|
sceneMode: sceneMode != null ? sceneMode : 'business',
|
|
lastScannedAt: null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 从页面 query 写入 uid / cardId / 当前状态与场景
|
|
*/
|
|
export function applyModeOptions(
|
|
state: NfcModeState,
|
|
options: ModePageOptions | null
|
|
): void {
|
|
if (options == null) {
|
|
return;
|
|
}
|
|
const uid = trimText(options.uid);
|
|
if (uid !== '') {
|
|
state.uid = uid;
|
|
}
|
|
const cardId = trimText(options.cardId);
|
|
if (cardId !== '') {
|
|
state.cardId = cardId;
|
|
}
|
|
const status = parseStatus(options.status);
|
|
if (status != null) {
|
|
state.status = status;
|
|
}
|
|
const sceneMode = parseSceneMode(options.sceneMode);
|
|
if (sceneMode != null) {
|
|
state.sceneMode = sceneMode;
|
|
state.selectedMode = sceneMode;
|
|
}
|
|
state.device = buildDeviceFromOptions(state, options);
|
|
}
|
|
|
|
export function selectSceneMode(state: NfcModeState, raw: string): boolean {
|
|
const mode = parseSceneMode(raw);
|
|
if (mode == null) {
|
|
return false;
|
|
}
|
|
state.selectedMode = mode;
|
|
return true;
|
|
}
|
|
|
|
export function formatStatusLabel(status: string): string {
|
|
if (status === 'active') {
|
|
return resolveModeText('statusActive');
|
|
}
|
|
if (status === 'suspended') {
|
|
return resolveModeText('statusSuspended');
|
|
}
|
|
if (status === 'unbound') {
|
|
return resolveModeText('statusUnbound');
|
|
}
|
|
return status;
|
|
}
|
|
|
|
export function formatSceneLabel(mode: string): string {
|
|
if (mode === 'business') {
|
|
return resolveModeText('business');
|
|
}
|
|
if (mode === 'social') {
|
|
return resolveModeText('social');
|
|
}
|
|
return mode;
|
|
}
|
|
|
|
function toastNone(title: string): void {
|
|
uni.showToast({
|
|
title,
|
|
icon: 'none',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 页面提交:调用 applyScene,成功后回写状态
|
|
*/
|
|
export async function submitApplyScene(
|
|
state: NfcModeState,
|
|
api?: ApplySceneApi
|
|
): Promise<ApplySceneResult> {
|
|
if (state.submitting) {
|
|
return {
|
|
ok: false,
|
|
error: '',
|
|
device: state.device,
|
|
};
|
|
}
|
|
|
|
if (state.device != null) {
|
|
state.device.uid = state.uid;
|
|
state.device.cardId = state.cardId === '' ? null : state.cardId;
|
|
}
|
|
|
|
state.submitting = true;
|
|
state.errorMessage = '';
|
|
const result = await applyScene(
|
|
{
|
|
uid: state.uid,
|
|
cardId: state.cardId,
|
|
sceneMode: state.selectedMode,
|
|
device: state.device,
|
|
},
|
|
api
|
|
);
|
|
state.submitting = false;
|
|
|
|
if (!result.ok) {
|
|
state.errorMessage = result.error;
|
|
toastNone(result.error);
|
|
return result;
|
|
}
|
|
|
|
if (result.device != null) {
|
|
state.device = result.device;
|
|
state.status = result.device.status;
|
|
state.sceneMode = result.device.sceneMode;
|
|
state.uid = result.device.uid;
|
|
if (result.device.cardId != null && trimText(result.device.cardId) !== '') {
|
|
state.cardId = String(result.device.cardId);
|
|
}
|
|
}
|
|
toastNone(resolveModeText('applySuccess'));
|
|
return result;
|
|
}
|