- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
320 行
7.1 KiB
Plaintext
320 行
7.1 KiB
Plaintext
/**
|
||
* 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<OwnerPageData<RadarTrack>>;
|
||
};
|
||
|
||
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<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;
|
||
}
|