/** * 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>; }; 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 = {}; 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 | 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 { 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 { 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; }