文件
OmniCard/mpuniapp/pages/owner/nfc/useMode.uts
wikiboss eec259a3a7 feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换
- 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片
- 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转
- 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出
- 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志
- 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票
- 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏
- 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
2026-09-20 23:47:47 +08:00

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;
}