257 行
5.6 KiB
Plaintext
257 行
5.6 KiB
Plaintext
/**
|
|
* 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<CardPortalDetail | null> {
|
|
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<RadarTrack | null> {
|
|
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);
|
|
}
|