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

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

542 行
13 KiB
Plaintext

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

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

/**
* OmniCard AI (CRMmp) - 名片主 NFC 贴卡激活业务驱动层
* H5 无天线走手动 UID;APP 经 uni.getNFCAdapter 尝试读芯片 UID,失败回退手动输入
*/
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'
| 'writeSuccess'
| 'writeFailed'
| 'writeUnavailable'
| 'writeCancelled';
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';
}
if (key === 'writeSuccess') {
return zh ? '已写入' : 'Written to tag';
}
if (key === 'writeFailed') {
return zh ? '写入失败' : 'Could not write';
}
if (key === 'writeUnavailable') {
return zh ? '暂无可写内容' : 'Nothing to write';
}
if (key === 'writeCancelled') {
return zh ? '已取消写入' : 'Write cancelled';
}
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) === '';
}
/**
* NFC 标签 id(ArrayBuffer)转大写 Hex UID;空则返回空串
*/
function nfcIdToHex(id: ArrayBuffer | null | undefined): string {
if (id == null) {
return '';
}
const bytes = new Uint8Array(id);
let hex = '';
let i = 0;
const len = bytes.length;
while (i < len) {
let part = bytes[i].toString(16);
if (part.length < 2) {
part = '0' + part;
}
hex += part;
i++;
}
return hex.toUpperCase();
}
type NfcFail = {
errMsg?: string;
};
type NfcIdResult = {
id?: ArrayBuffer | null;
};
type NfcCallbackOptions = {
success?: () => void;
fail?: (err: NfcFail) => void;
};
type NdefWriteOptions = {
uris?: string[];
success?: () => void;
fail?: (err: NfcFail) => void;
};
type NdefWriter = {
writeNdefMessage: (options: NdefWriteOptions) => void;
connect?: (options: NfcCallbackOptions) => void;
};
type NfcAdapter = {
getNdef: () => NdefWriter | null;
startDiscovery: (options: NfcCallbackOptions) => void;
onDiscovered: (callback: (res: NfcIdResult) => void) => void;
};
type UniWithNfc = {
getNFCAdapter?: () => NfcAdapter | null;
};
function readNfcAdapter(): NfcAdapter | null {
const runtime = uni as UniWithNfc;
if (runtime.getNFCAdapter == null) {
return null;
}
return runtime.getNFCAdapter();
}
/**
* APP:调用官方风格 uni.getNFCAdapter + startDiscovery / onDiscovered 尝试读 UID。
* 同步路径仅在已读到合法 UID 时返回非空;读不到、API 不存在或抛错返回空串,禁止伪造。
*/
function tryReadUidFromAntenna(state: ScanState): string {
// #ifdef APP
try {
const adapter = readNfcAdapter();
if (adapter == null) {
return '';
}
let syncUid = '';
adapter.startDiscovery({
success: () => {
adapter.onDiscovered((res: NfcIdResult) => {
const uid = parseUid(nfcIdToHex(res.id));
if (uid === '') {
return;
}
// 异步贴卡成功:写入真实 UID,不伪造
state.uid = uid;
state.empty = false;
state.scanning = false;
state.manualInput = false;
state.errorMessage = '';
syncUid = uid;
});
},
fail: (_err: { errMsg?: string }) => {
// 由 startScan 统一走手动输入回退
},
});
return parseUid(syncUid);
} catch (_err) {
return '';
}
// #endif
return '';
}
/**
* APP 尝试官方 NFC Adapter 读 UID;仅合法 UID 才成功。否则回退手动输入,禁止伪造成功
*/
export function startScan(state: ScanState): boolean {
// #ifdef APP
try {
const uid = tryReadUidFromAntenna(state);
if (parseUid(uid) !== '') {
state.uid = parseUid(uid);
state.scanning = false;
state.manualInput = false;
state.empty = false;
state.errorMessage = '';
state.record = null;
return true;
}
} catch (_err) {
// 落入下方失败回退
}
// #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;
}
}
/**
* 可写 NDEF URI:有合法 cardId 才生成;否则空串表示无可写内容
*/
export function buildNdefUri(cardId: string | number | null | undefined): string {
const id = parseCardId(cardId);
if (id == null) {
return '';
}
return `https://omnicard.ai/c/${id}`;
}
function toastWriteUnavailable(): void {
if (typeof uni === 'undefined' || typeof uni.showToast !== 'function') {
return;
}
uni.showToast({
title: resolveScanText('writeUnavailable'),
icon: 'none',
});
}
function toastWriteFailed(): void {
if (typeof uni === 'undefined' || typeof uni.showToast !== 'function') {
return;
}
uni.showToast({
title: resolveScanText('writeFailed'),
icon: 'none',
});
}
function toastWriteCancelled(): void {
if (typeof uni === 'undefined' || typeof uni.showToast !== 'function') {
return;
}
uni.showToast({
title: resolveScanText('writeCancelled'),
icon: 'none',
});
}
function isWriteCancelledError(err: { errMsg?: string } | null | undefined): boolean {
if (err == null || err.errMsg == null) {
return false;
}
const msg = String(err.errMsg).toLowerCase();
return msg.indexOf('cancel') >= 0;
}
/**
* 贴卡写入 NDEF:仅系统写入接口的 success 回调才 toast 已写入。
* 天线未写 / 取消 / 失败 / 无可写内容均不得报写入成功。
*/
export function writeNdefToTag(uri: string | null | undefined): Promise<boolean> {
const payload = uri == null ? '' : String(uri).trim();
if (payload === '') {
toastWriteUnavailable();
return Promise.resolve(false);
}
return new Promise<boolean>(resolve => {
try {
const adapter = readNfcAdapter();
if (adapter == null) {
toastWriteFailed();
resolve(false);
return;
}
const ndef = adapter.getNdef();
if (ndef == null) {
toastWriteFailed();
resolve(false);
return;
}
const runWrite = (): void => {
ndef.writeNdefMessage({
uris: [payload],
success: () => {
uni.showToast({
title: resolveScanText('writeSuccess'),
icon: 'none',
});
resolve(true);
},
fail: (err: { errMsg?: string }) => {
if (isWriteCancelledError(err)) {
toastWriteCancelled();
} else {
toastWriteFailed();
}
resolve(false);
},
});
};
if (typeof ndef.connect === 'function') {
ndef.connect({
success: () => {
runWrite();
},
fail: (err: { errMsg?: string }) => {
if (isWriteCancelledError(err)) {
toastWriteCancelled();
} else {
toastWriteFailed();
}
resolve(false);
},
});
return;
}
runWrite();
} catch (_err) {
toastWriteFailed();
resolve(false);
}
});
}