/** * 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' | 'error' | 'stay' | 'depth'; 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 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 === 'error') { return getOmniText(locale, 'common.error'); } 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'; } 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 ''; } 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; }