/** * OmniCard AI (CRMmp) - 微官网 Bento 首页与 Hero 身份区业务驱动层 * 严格遵循 uts 强类型规范与 AGENTS.md 约束 */ import { CardPortalDetail, BentoBlockPublic, RadarTrack, } from '../../common/types.uts'; import { fetchPortalCard, recordRadarTrack, } from '../../common/api.uts'; export type PortalState = { card: CardPortalDetail | null; loading: boolean; errorMessage: string; visitorId: string; enterTime: number; touchType: string; isPlayingAudio: boolean; audioContext: any | null; }; const VISITOR_STORAGE_KEY = 'omni_visitor_id'; /** * 获取或初始化访客唯一识别码 (持久化在本地 Storage) */ export function getOrCreateVisitorId(): string { let visitorId = ''; try { visitorId = uni.getStorageSync(VISITOR_STORAGE_KEY) as string; } catch (e) { visitorId = ''; } if (!visitorId || visitorId.trim() === '') { const randomPart = Math.random().toString(36).substring(2, 10); visitorId = `vis_${Date.now()}_${randomPart}`; try { uni.setStorageSync(VISITOR_STORAGE_KEY, visitorId); } catch (e) {} } return visitorId; } /** * 创建微官网响应式状态初始对象 */ export function createPortalState(): PortalState { return { card: null, loading: false, errorMessage: '', visitorId: getOrCreateVisitorId(), enterTime: Date.now(), touchType: 'link', isPlayingAudio: false, audioContext: null, }; } /** * 加载微官网名片聚合数据并上报首次触碰 */ export async function loadCardPortal( state: PortalState, cardIdOrSlug: string, source: string = 'link' ): Promise { state.loading = true; state.errorMessage = ''; state.touchType = source; state.enterTime = Date.now(); try { const cardData = await fetchPortalCard(cardIdOrSlug); state.card = cardData; state.loading = false; // 首次载入成功后上报雷达初次触碰轨迹 if (cardData && cardData.id) { recordRadarTrack({ cardId: cardData.id, visitorId: state.visitorId, touchType: state.touchType, stayDurationMs: 0, scrollDepth: 0, }).catch((_e: any) => { // 轨迹上报容错,不阻断主流程 }); } return cardData; } catch (err: any) { state.loading = false; state.card = null; state.errorMessage = err?.message || '获取名片微官网详情失败'; return null; } } /** * 上报访客停留时长与阅读深度 */ export async function reportPortalStay( state: PortalState, scrollDepth: number = 0 ): Promise { if (!state.card || !state.card.id) { return null; } const stayDurationMs = Math.max(0, Date.now() - state.enterTime); try { const track = await recordRadarTrack({ cardId: state.card.id, visitorId: state.visitorId, touchType: state.touchType, stayDurationMs, scrollDepth, }); return track; } catch (e) { return null; } } /** * 名字语音发音试听控制 */ export function toggleAudioPronunciation(state: PortalState, audioUrl?: string): void { if (!audioUrl || audioUrl.trim() === '') { return; } if (state.isPlayingAudio && state.audioContext) { try { state.audioContext.stop(); } catch (e) {} state.isPlayingAudio = false; return; } if (!state.audioContext) { state.audioContext = uni.createInnerAudioContext(); state.audioContext.onPlay(() => { state.isPlayingAudio = true; }); state.audioContext.onEnded(() => { state.isPlayingAudio = false; }); state.audioContext.onError(() => { state.isPlayingAudio = false; }); } try { state.audioContext.src = audioUrl; state.audioContext.play(); state.isPlayingAudio = true; } catch (e) { state.isPlayingAudio = false; } } /** * 快捷动作条呼出处理 */ export function triggerQuickAction( type: 'whatsapp' | 'linkedin' | 'email' | 'phone', value?: string ): boolean { if (!value || value.trim() === '') { uni.showToast({ title: '未设置该联系方式', icon: 'none', }); return false; } if (type === 'phone') { uni.makePhoneCall({ phoneNumber: value, }); return true; } if (type === 'whatsapp') { const cleanNumber = value.replace(/[^0-9+]/g, ''); const waUrl = `https://wa.me/${cleanNumber.replace('+', '')}`; uni.setClipboardData({ data: waUrl, success: () => { uni.showToast({ title: '已复制 WhatsApp 链接', icon: 'none', }); }, }); return true; } if (type === 'linkedin') { uni.setClipboardData({ data: value, success: () => { uni.showToast({ title: '已复制 LinkedIn 链接', icon: 'none', }); }, }); return true; } if (type === 'email') { uni.setClipboardData({ data: value, success: () => { uni.showToast({ title: '已复制邮箱地址', icon: 'none', }); }, }); return true; } return true; } /** * 根据 colSpan 计算 Bento Grid 列宽样式类 */ export function getBentoColSpanClass(colSpan: number): string { if (colSpan === 2) { return 'bento-col-2'; } return 'bento-col-1'; } /** * 过滤非 Hero 的 Bento 卡片并按 sortOrder 升序排列 */ export function filterVisibleBentoBlocks(blocks?: BentoBlockPublic[]): BentoBlockPublic[] { if (!blocks || !Array.isArray(blocks)) { return []; } return blocks .filter((b) => b.type !== 'hero') .sort((a, b) => a.sortOrder - b.sortOrder); }