文件
OmniCard/mpuniapp/pages/owner/useRadar.uts

574 行
14 KiB
Plaintext

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

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

/**
* OmniCard AI (CRMmp) - 名片主移动实时雷达业务驱动层
* 分页拉取访客轨迹,解析列表 / 空态 / 错误文案 / 关键指标聚合
*/
import { OwnerPageData, OwnerPageQuery, RadarTrack } from '../../common/types.uts';
import { fetchOwnerRadarPage } from '../../common/api.uts';
import { getCurrentLocale, getOmniText } from '../../common/locale.uts';
export type RadarCopyKey =
| 'title'
| 'desc'
| 'cardIdLabel'
| 'cardIdPlaceholder'
| 'refresh'
| 'empty'
| 'emptyHint'
| 'error'
| 'stay'
| 'depth'
| 'totalVisits'
| 'intentRate'
| 'intentTrend'
| 'highIntent'
| 'exploring'
| 'normalIntent'
| 'touchNfc'
| 'touchQr'
| 'touchLink'
| 'justNow'
| 'liveRadar'
| 'activeNow'
| 'blocksViewed'
| 'filterAll'
| 'avgStay';
export type LoadTracksInput = {
cardId?: string | number | null;
page?: number;
size?: number;
};
export type RadarFetchApi = {
fetchOwnerRadarPage: (query: OwnerPageQuery) => Promise<OwnerPageData<RadarTrack>>;
};
export type LoadTracksResult = {
list: RadarTrack[];
empty: boolean;
errorMessage: string;
page: number;
size: number;
total: number;
};
export type RadarStats = {
totalVisits: number;
highIntentCount: number;
intentRateText: string;
avgStayText: string;
topLocation: string;
};
export type RadarState = {
cardIdRaw: string;
page: number;
size: number;
total: number;
loading: boolean;
empty: boolean;
errorMessage: string;
tracks: RadarTrack[];
};
export type RadarPageOptions = {
cardId?: string;
id?: string;
};
const DEFAULT_PAGE = 1;
const DEFAULT_SIZE = 20;
function isZhLocale(): boolean {
return getCurrentLocale() === 'zh-CN';
}
/**
* 页面文案:标题与轨迹走词典,其余中英本地词典
*/
export function resolveRadarText(key: RadarCopyKey): string {
const locale = getCurrentLocale();
if (key === 'title') {
return getOmniText(locale, 'radar.online_visitor');
}
if (key === 'desc') {
return getOmniText(locale, 'radar.visitor_trace');
}
if (key === 'empty') {
return getOmniText(locale, 'common.empty');
}
if (key === 'emptyHint') {
return isZhLocale()
? '刷新后查看最新访客停留与触达'
: 'Refresh to see the latest stay and touch points';
}
if (key === 'error') {
return getOmniText(locale, 'common.error');
}
if (key === 'activeNow') {
return getOmniText(locale, 'radar.active_now');
}
const zh = isZhLocale();
if (key === 'cardIdLabel') {
return zh ? '名片编号' : 'Card ID';
}
if (key === 'cardIdPlaceholder') {
return zh ? '可选,输入名片编号' : 'Optional card ID';
}
if (key === 'refresh') {
return zh ? '刷新轨迹' : 'Refresh trail';
}
if (key === 'stay') {
return zh ? '停留' : 'Stay';
}
if (key === 'depth') {
return zh ? '滚动深度' : 'Scroll depth';
}
if (key === 'totalVisits') {
return zh ? '访问总量' : 'Total Visits';
}
if (key === 'intentRate') {
return zh ? '留资意向率' : 'Intent Rate';
}
if (key === 'intentTrend') {
return zh ? '意向深度走势' : 'Intent Trend';
}
if (key === 'highIntent') {
return zh ? '高意向' : 'High Intent';
}
if (key === 'exploring') {
return zh ? '深度浏览' : 'Exploring';
}
if (key === 'normalIntent') {
return zh ? '常规触达' : 'Standard';
}
if (key === 'touchNfc') {
return zh ? 'NFC 触碰' : 'NFC Tap';
}
if (key === 'touchQr') {
return zh ? '展台扫码' : 'QR Scan';
}
if (key === 'touchLink') {
return zh ? '直链访问' : 'Direct Link';
}
if (key === 'justNow') {
return zh ? '刚刚' : 'Just Now';
}
if (key === 'liveRadar') {
return zh ? '实时雷达监测中' : 'Live Radar Active';
}
if (key === 'blocksViewed') {
return zh ? '深度查看模块' : 'Viewed Blocks';
}
if (key === 'filterAll') {
return zh ? '全部名片' : 'All Cards';
}
if (key === 'avgStay') {
return zh ? '平均停留' : 'Avg Stay';
}
return '';
}
/**
* 解析为正整数。非正整数返回 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 normalizePage(raw: number | null | undefined): number {
if (raw == null || !Number.isFinite(raw) || raw < 1) {
return DEFAULT_PAGE;
}
return Math.floor(raw);
}
function normalizeSize(raw: number | null | undefined): number {
if (raw == null || !Number.isFinite(raw) || raw < 1) {
return DEFAULT_SIZE;
}
return Math.floor(raw);
}
/**
* 停留时长:不足 1 秒用 ms,否则保留一位小数的秒
*/
export function formatStay(ms: number | null | undefined): string {
if (ms == null || !Number.isFinite(ms) || ms <= 0) {
return '0ms';
}
const safe = Math.round(ms);
if (safe < 1000) {
return `${safe}ms`;
}
return `${(safe / 1000).toFixed(1)}s`;
}
/**
* 滚动深度:纠偏到 0~100 后以百分比展示
*/
export function formatDepth(depth: number | null | undefined): string {
if (depth == null || !Number.isFinite(depth)) {
return '0%';
}
const clamped = Math.min(100, Math.max(0, Math.round(depth)));
return `${clamped}%`;
}
/**
* 国家 / 城市一行展示
*/
export function formatLocation(country?: string, city?: string): string {
const countryText = country != null ? String(country).trim() : '';
const cityText = city != null ? String(city).trim() : '';
if (countryText !== '' && cityText !== '') {
return `${countryText} / ${cityText}`;
}
if (countryText !== '') {
return countryText;
}
if (cityText !== '') {
return cityText;
}
return '';
}
/**
* 触碰类型标签简写
*/
export function formatTouchTag(touchType: string | null | undefined): string {
if (touchType === 'nfc') {
return 'NFC';
}
if (touchType === 'qr') {
return 'QR';
}
return 'LINK';
}
/**
* 判定是否为高意向信号
*/
export function isHighIntent(stayDurationMs: number, scrollDepth: number): boolean {
const longStay = stayDurationMs >= 180000;
const ratio = scrollDepth > 1 ? scrollDepth / 100 : scrollDepth;
return longStay || ratio >= 0.6;
}
/**
* 获取意向等级: high / medium / normal
*/
export function getIntentLevel(stayDurationMs: number, scrollDepth: number): string {
if (isHighIntent(stayDurationMs, scrollDepth)) {
return 'high';
}
const ratio = scrollDepth > 1 ? scrollDepth / 100 : scrollDepth;
if (stayDurationMs >= 30000 || ratio >= 0.3) {
return 'medium';
}
return 'normal';
}
/**
* 获取意向语义徽标文字
*/
export function getIntentBadgeText(level: string): string {
if (level === 'high') {
return resolveRadarText('highIntent');
}
if (level === 'medium') {
return resolveRadarText('exploring');
}
return resolveRadarText('normalIntent');
}
/**
* 访客活动主通知流文案(如 "Alex tapped your NFC card")
*/
export function formatVisitorHeadline(item: RadarTrack): string {
const zh = isZhLocale();
const vid = item.visitorId != null && item.visitorId.trim() !== '' ? item.visitorId.trim() : '';
let name = 'Visitor';
if (vid !== '') {
const shortId = vid.replace(/^vis_([a-z0-9]+_)?/i, '');
name = shortId !== '' ? `Visitor #${shortId}` : 'Visitor';
}
if (item.touchType === 'nfc') {
return zh ? `${name} 通过 NFC 碰一碰接入名片` : `${name} tapped your NFC card`;
}
if (item.touchType === 'qr') {
return zh ? `${name} 扫描了展台名片二维码` : `${name} scanned your QR showcase`;
}
return zh ? `${name} 浏览了你的数字微官网` : `${name} saved your contact card`;
}
/**
* 相对时间格式化
*/
export function formatRelativeTime(createdAt?: string | Date): string {
const zh = isZhLocale();
if (createdAt == null) {
return zh ? '刚刚' : 'Just Now';
}
let time = 0;
if (typeof createdAt === 'string') {
const parsed = Date.parse(createdAt);
if (!Number.isNaN(parsed) && parsed > 0) {
time = parsed;
}
} else if (createdAt instanceof Date) {
time = createdAt.getTime();
}
if (time <= 0) {
return zh ? '刚刚' : 'Just Now';
}
const diffMs = Math.max(0, Date.now() - time);
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 2) {
return zh ? '刚刚' : 'Just Now';
}
if (diffMin < 60) {
return zh ? `${diffMin}分钟前` : `${diffMin}m ago`;
}
const diffHour = Math.floor(diffMin / 60);
if (diffHour < 24) {
return zh ? `${diffHour}小时前` : `${diffHour}h ago`;
}
const diffDay = Math.floor(diffHour / 24);
return zh ? `${diffDay}天前` : `${diffDay}d ago`;
}
/**
* 格式化模块列表
*/
export function formatBlocksList(blocks?: string[] | null): string {
if (blocks == null || !Array.isArray(blocks) || blocks.length === 0) {
return '';
}
return blocks.join(', ');
}
/**
* 聚合统计雷达数据
*/
export function computeRadarStats(tracks: RadarTrack[], total: number): RadarStats {
const count = total > 0 ? total : tracks.length;
let highCount = 0;
let totalStay = 0;
const countryMap: Record<string, number> = {};
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (isHighIntent(t.stayDurationMs, t.scrollDepth)) {
highCount += 1;
}
totalStay += Math.max(0, t.stayDurationMs);
if (t.country != null && t.country.trim() !== '') {
const c = t.country.trim();
countryMap[c] = (countryMap[c] || 0) + 1;
}
}
const rate = tracks.length > 0 ? Math.round((highCount / tracks.length) * 100) : (count > 0 ? 32 : 0);
const avgStay = tracks.length > 0 ? Math.round(totalStay / tracks.length) : 0;
let topCountry = '';
let maxC = 0;
for (const c in countryMap) {
if (countryMap[c] > maxC) {
maxC = countryMap[c];
topCountry = c;
}
}
return {
totalVisits: count,
highIntentCount: highCount,
intentRateText: `${rate}%`,
avgStayText: formatStay(avgStay),
topLocation: topCountry !== '' ? topCountry : 'Global',
};
}
/**
* 计算走势高度 (7 个数据柱)
*/
export function computeTrendHeights(tracks: RadarTrack[]): number[] {
const defaultHeights: number[] = [35, 60, 45, 80, 55, 90, 75];
if (tracks.length === 0) {
return defaultHeights;
}
const heights: number[] = [];
const sample = tracks.slice(0, 7);
for (let i = 0; i < 7; i++) {
if (i < sample.length) {
const d = sample[i].scrollDepth;
const pct = d > 1 ? Math.min(100, Math.max(15, Math.round(d))) : Math.min(100, Math.max(15, Math.round(d * 100)));
heights.push(pct);
} else {
heights.push(defaultHeights[i]);
}
}
return heights;
}
function readErrorMessage(err: unknown): string {
if (err instanceof Error && err.message !== '') {
return err.message;
}
return resolveRadarText('error');
}
function parseTrackList(payload: OwnerPageData<RadarTrack> | null | undefined): RadarTrack[] {
if (payload == null || payload.list == null) {
return [];
}
if (!Array.isArray(payload.list)) {
return [];
}
return payload.list;
}
/**
* 拉取雷达分页。可注入 fetchOwnerRadarPage,缺省走 common/api
*/
export async function loadTracks(
input: LoadTracksInput,
api?: RadarFetchApi
): Promise<LoadTracksResult> {
const page = normalizePage(input.page);
const size = normalizeSize(input.size);
const query: OwnerPageQuery = {
page,
size,
};
const cardId = parseCardId(input.cardId);
if (cardId != null) {
query.cardId = cardId;
}
const fetchPage =
api != null && api.fetchOwnerRadarPage != null
? api.fetchOwnerRadarPage
: fetchOwnerRadarPage;
try {
const payload = await fetchPage(query);
const list = parseTrackList(payload);
const pagination =
payload != null && payload.pagination != null
? payload.pagination
: { page, size, total: list.length };
return {
list,
empty: list.length === 0,
errorMessage: '',
page: normalizePage(pagination.page),
size: normalizeSize(pagination.size),
total:
pagination.total != null && Number.isFinite(pagination.total)
? Math.max(0, pagination.total)
: list.length,
};
} catch (err) {
return {
list: [],
empty: true,
errorMessage: readErrorMessage(err),
page,
size,
total: 0,
};
}
}
export function createRadarState(): RadarState {
return {
cardIdRaw: '',
page: DEFAULT_PAGE,
size: DEFAULT_SIZE,
total: 0,
loading: false,
empty: true,
errorMessage: '',
tracks: [],
};
}
export function applyRadarOptions(state: RadarState, options: RadarPageOptions | null): void {
if (options == null) {
return;
}
const cardRaw =
options.cardId != null && options.cardId !== ''
? options.cardId
: options.id != null
? options.id
: '';
if (cardRaw !== '') {
state.cardIdRaw = String(cardRaw).trim();
}
}
/**
* 页面主 CTA:刷新当前页轨迹
*/
export async function refreshTracks(
state: RadarState,
api?: RadarFetchApi
): Promise<LoadTracksResult> {
if (state.loading) {
return {
list: state.tracks,
empty: state.empty,
errorMessage: state.errorMessage,
page: state.page,
size: state.size,
total: state.total,
};
}
state.loading = true;
state.errorMessage = '';
const result = await loadTracks(
{
cardId: state.cardIdRaw,
page: state.page,
size: state.size,
},
api
);
state.loading = false;
state.tracks = result.list;
state.empty = result.empty;
state.errorMessage = result.errorMessage;
state.page = result.page;
state.size = result.size;
state.total = result.total;
return result;
}