/** * OmniCard AI (CRMmp) - 深度洞见文章详情业务驱动层 * 严格遵循 uts 强类型规范与 AGENTS.md 约束 */ import { BentoBlockPublic, CardPortalDetail, RadarTrack } from '../../common/types.uts'; import { fetchPortalCard, recordRadarTrack } from '../../common/api.uts'; import { getCurrentLocale, getOmniText, OmniLocale } from '../../common/locale.uts'; export type ArticleDetailPageOptions = { cardId?: string; blockId?: string; id?: string; }; export type ArticleOpenGraph = { title: string; description: string; image: string; }; export type ArticleShareFields = { ctaLabel: string; copiedToast: string; shareUrl: string; title: string; description: string; image: string; }; export type ArticleDetailState = { cardId: string; blockId: string; loading: boolean; article: BentoBlockPublic | null; title: string; content: string; mediaUrl: string; linkUrl: string; isHtmlContent: boolean; empty: boolean; errorMessage: string; openGraph: ArticleOpenGraph; authorName: string; authorTitle: string; authorCompany: string; authorAvatar: string; readTimeMinutes: number; summaryText: string; scrollDepth: number; enterTime: number; visitorId: string; }; const BACK_LABEL = '返回'; const OG_DESC_MAX = 160; const VISITOR_STORAGE_KEY = 'omni_visitor_id'; /** * 创建文章详情初始状态 */ export function createArticleDetailState(): ArticleDetailState { return { cardId: '', blockId: '', loading: false, article: null, title: '', content: '', mediaUrl: '', linkUrl: '', isHtmlContent: false, empty: false, errorMessage: '', openGraph: { title: '', description: '', image: '', }, authorName: '', authorTitle: '', authorCompany: '', authorAvatar: '', readTimeMinutes: 1, summaryText: '', scrollDepth: 0, enterTime: Date.now(), visitorId: '', }; } /** * 将详情状态复位为空 */ export function applyEmptyArticle(state: ArticleDetailState): void { state.article = null; state.title = ''; state.content = ''; state.mediaUrl = ''; state.linkUrl = ''; state.isHtmlContent = false; state.empty = true; state.openGraph = { title: '', description: '', image: '', }; state.authorName = ''; state.authorTitle = ''; state.authorCompany = ''; state.authorAvatar = ''; state.readTimeMinutes = 1; state.summaryText = ''; state.scrollDepth = 0; } /** * 从 onLoad options 写入 cardId / blockId */ export function applyArticleDetailOptions( state: ArticleDetailState, options: ArticleDetailPageOptions | null ): boolean { const cardIdRaw = options != null ? options.cardId || options.id || '' : ''; const blockIdRaw = options != null ? options.blockId || '' : ''; state.cardId = String(cardIdRaw).trim(); state.blockId = String(blockIdRaw).trim(); if (state.cardId === '' || state.blockId === '') { applyEmptyArticle(state); return false; } return true; } /** * 从 bentoBlocks 中选出指定 article 块 */ export function pickArticleBlock( blocks: BentoBlockPublic[] | null | undefined, blockId: string ): BentoBlockPublic | null { if (blocks == null || !Array.isArray(blocks) || blocks.length === 0) { return null; } const targetId = blockId != null ? String(blockId).trim() : ''; if (targetId === '') { return null; } for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; if (block != null && String(block.id) === targetId && block.type === 'article') { return block; } } return null; } /** * 正文是否像 HTML,供 rich-text / text 分流 */ export function looksLikeHtml(content: string): boolean { if (content == null || content.trim() === '') { return false; } return /<\s*\/?\s*[a-zA-Z][^>]*>/.test(content); } /** * 去掉 HTML 标签,折叠空白 */ export function stripHtmlToText(raw: string): string { if (raw == null || raw === '') { return ''; } return raw.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); } /** * 供后续 H5 meta 使用的 OpenGraph 三件套 */ export function buildArticleOpenGraph(article: BentoBlockPublic | null): ArticleOpenGraph { if (article == null) { return { title: '', description: '', image: '', }; } const desc = stripHtmlToText(article.content != null ? article.content : ''); let description = desc; if (description.length > OG_DESC_MAX) { description = description.substring(0, OG_DESC_MAX - 3) + '...'; } return { title: article.title != null ? article.title : '', description, image: article.mediaUrl != null ? article.mediaUrl : '', }; } /** * 分享链接:优先块 linkUrl,否则当前页 URL */ export function resolveShareUrl(linkUrl: string, pageUrl: string): string { const fromBlock = linkUrl != null ? linkUrl.trim() : ''; if (fromBlock !== '') { return fromBlock; } return pageUrl != null ? pageUrl.trim() : ''; } /** * 构造当前页路径兜底 */ export function buildArticlePagePath(cardId: string, blockId: string): string { return `/pages/portal/article-detail?cardId=${cardId}&blockId=${blockId}`; } type UniStackPage = { route?: string; }; /** * 读取当前页 URL:优先页面栈,否则构造本地路径 */ export function getCurrentPageUrl(state: ArticleDetailState): string { try { if (typeof getCurrentPages !== 'function') { return buildArticlePagePath(state.cardId, state.blockId); } const pages = getCurrentPages(); if (pages != null && pages.length > 0) { const page = pages[pages.length - 1] as UniStackPage; const route = page != null && page.route != null ? String(page.route) : ''; if (route !== '') { const path = route.startsWith('/') ? route : `/${route}`; return `${path}?cardId=${state.cardId}&blockId=${state.blockId}`; } } } catch (_e) {} return buildArticlePagePath(state.cardId, state.blockId); } /** * 分享文案与 OpenGraph 字段(CTA / Toast / URL / title / description / image) */ export function getArticleShareFields( state: ArticleDetailState, pageUrl: string ): ArticleShareFields { const og = state.openGraph; return { ctaLabel: resolveArticleDetailText('shareCta'), copiedToast: getOmniText(getCurrentLocale(), 'portal.copied_link'), shareUrl: resolveShareUrl(state.linkUrl, pageUrl), title: og.title, description: og.description, image: og.image, }; } export function getArticleBackLabel(): string { return BACK_LABEL; } export function getArticleEmptyMessage(): string { return getOmniText(getCurrentLocale(), 'common.empty'); } export type ArticleDetailTextKey = | 'kicker' | 'shareCta' | 'shareSuccess' | 'shareFailed' | 'copyFailed' | 'back' | 'empty' | 'emptyHint' | 'summary' | 'readTime' | 'minRead' | 'author' | 'viewPortal'; /** * 文章详情展示文案。严格遵循 AGENTS.md 多语言自然表达规范。 */ export function resolveArticleDetailText( key: ArticleDetailTextKey, customLocale?: OmniLocale ): string { const locale = customLocale != null ? customLocale : getCurrentLocale(); if (key === 'empty') { return getOmniText(locale, 'common.empty'); } if (locale === 'zh-CN') { if (key === 'kicker') { return '深度洞见'; } if (key === 'summary') { return '核心导读'; } if (key === 'readTime') { return '预计阅读'; } if (key === 'minRead') { return '分钟'; } if (key === 'author') { return '专栏作者'; } if (key === 'viewPortal') { return '查看微官网'; } if (key === 'shareCta') { return '复制分享链接'; } if (key === 'shareSuccess') { return '已分享'; } if (key === 'shareFailed') { return '分享失败'; } if (key === 'copyFailed') { return '复制失败'; } if (key === 'back') { return '返回'; } return '返回上一页继续浏览'; } if (locale === 'ar-SA') { if (key === 'kicker') { return 'رؤى متعمقة'; } if (key === 'summary') { return 'أبرز النقاط'; } if (key === 'readTime') { return 'وقت القراءة'; } if (key === 'minRead') { return 'دقائق'; } if (key === 'author') { return 'كاتب المقال'; } if (key === 'viewPortal') { return 'عرض الملف التعريفي'; } if (key === 'shareCta') { return 'نسخ رابط المشاركة'; } if (key === 'shareSuccess') { return 'تمت المشاركة'; } if (key === 'shareFailed') { return 'فشلت المشاركة'; } if (key === 'copyFailed') { return 'فشل النسخ'; } if (key === 'back') { return 'رجوع'; } return 'ارجع للصفحة السابقة للمتابعة'; } if (locale === 'es-ES') { if (key === 'kicker') { return 'Perspectivas'; } if (key === 'summary') { return 'Puntos clave'; } if (key === 'readTime') { return 'Tiempo de lectura'; } if (key === 'minRead') { return 'min'; } if (key === 'author') { return 'Autor'; } if (key === 'viewPortal') { return 'Ver perfil'; } if (key === 'shareCta') { return 'Copiar enlace para compartir'; } if (key === 'shareSuccess') { return 'Compartido'; } if (key === 'shareFailed') { return 'Error al compartir'; } if (key === 'copyFailed') { return 'Error al copiar'; } if (key === 'back') { return 'Volver'; } return 'Vuelve para seguir leyendo'; } if (locale === 'de-DE') { if (key === 'kicker') { return 'Brancheneinblick'; } if (key === 'summary') { return 'Kernaussagen'; } if (key === 'readTime') { return 'Lesezeit'; } if (key === 'minRead') { return 'Min.'; } if (key === 'author') { return 'Autor'; } if (key === 'viewPortal') { return 'Profil ansehen'; } if (key === 'shareCta') { return 'Freigabelink kopieren'; } if (key === 'shareSuccess') { return 'Geteilt'; } if (key === 'shareFailed') { return 'Teilen fehlgeschlagen'; } if (key === 'copyFailed') { return 'Kopieren fehlgeschlagen'; } if (key === 'back') { return 'Zurück'; } return 'Gehen Sie zurück, um weiterzulesen'; } if (locale === 'fr-FR') { if (key === 'kicker') { return 'Éclairage d’expert'; } if (key === 'summary') { return 'Points clés'; } if (key === 'readTime') { return 'Temps de lecture'; } if (key === 'minRead') { return 'min'; } if (key === 'author') { return 'Auteur'; } if (key === 'viewPortal') { return 'Voir le profil'; } if (key === 'shareCta') { return 'Copier le lien de partage'; } if (key === 'shareSuccess') { return 'Partagé'; } if (key === 'shareFailed') { return 'Échec du partage'; } if (key === 'copyFailed') { return 'Échec de la copie'; } if (key === 'back') { return 'Retour'; } return 'Revenez en arrière pour continuer'; } if (locale === 'ja-JP') { if (key === 'kicker') { return 'インサイト'; } if (key === 'summary') { return '要約'; } if (key === 'readTime') { return '読了目安'; } if (key === 'minRead') { return '分'; } if (key === 'author') { return '執筆者'; } if (key === 'viewPortal') { return 'プロフィールを見る'; } if (key === 'shareCta') { return '共有リンクをコピー'; } if (key === 'shareSuccess') { return '共有しました'; } if (key === 'shareFailed') { return '共有に失敗しました'; } if (key === 'copyFailed') { return 'コピーに失敗しました'; } if (key === 'back') { return '戻る'; } return '前のページに戻って続きを見る'; } if (key === 'kicker') { return 'Thought Leadership'; } if (key === 'summary') { return 'Key Takeaways'; } if (key === 'readTime') { return 'Read time'; } if (key === 'minRead') { return 'min'; } if (key === 'author') { return 'Author'; } if (key === 'viewPortal') { return 'View Profile'; } if (key === 'shareCta') { return 'Copy share link'; } if (key === 'shareSuccess') { return 'Shared'; } if (key === 'shareFailed') { return 'Share failed'; } if (key === 'copyFailed') { return 'Copy failed'; } if (key === 'back') { return 'Back'; } return 'Go back to keep reading'; } function applyArticle( state: ArticleDetailState, article: BentoBlockPublic, card?: CardPortalDetail | null ): void { state.article = article; state.title = article.title != null ? article.title : ''; state.content = article.content != null ? article.content : ''; state.mediaUrl = article.mediaUrl != null ? article.mediaUrl : ''; state.linkUrl = article.linkUrl != null ? article.linkUrl : ''; state.isHtmlContent = looksLikeHtml(state.content); state.empty = false; state.openGraph = buildArticleOpenGraph(article); const cleanText = stripHtmlToText(state.content); state.readTimeMinutes = Math.max(1, Math.ceil(cleanText.length / 350)); state.summaryText = state.openGraph.description; if (card != null) { state.authorName = card.name != null ? String(card.name) : ''; state.authorTitle = card.title != null ? String(card.title) : ''; state.authorCompany = card.company != null ? String(card.company) : ''; state.authorAvatar = card.avatarUrl != null ? String(card.avatarUrl) : ''; } } /** * fetchPortalCard 后从 bentoBlocks 选出 article 并绑定作者与元信息 */ export async function loadArticleDetail( state: ArticleDetailState ): Promise { if (state.cardId === '' || state.blockId === '') { applyEmptyArticle(state); return null; } state.loading = true; state.errorMessage = ''; state.enterTime = Date.now(); try { const card = await fetchPortalCard(state.cardId); const blocks = card != null && card.bentoBlocks != null ? card.bentoBlocks : []; const article = pickArticleBlock(blocks, state.blockId); state.loading = false; if (article == null) { applyEmptyArticle(state); return null; } applyArticle(state, article, card); return article; } catch (err) { state.loading = false; applyEmptyArticle(state); if (err instanceof Error) { state.errorMessage = err.message; } else { state.errorMessage = getOmniText(getCurrentLocale(), 'portal.unavailable'); } return null; } } /** * 获取或初始化访客唯一识别码 (持久化在本地 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; } /** * 记录并更新滑屏深度 (0 - 100%) */ export function updateArticleScrollDepth( state: ArticleDetailState, scrollTop: number, scrollHeight: number, windowHeight: number ): number { if (scrollHeight <= 0) { return state.scrollDepth; } const maxScroll = Math.max(1, scrollHeight - windowHeight); const currentRatio = Math.min(1, Math.max(0, scrollTop / maxScroll)); const depth = Math.round(currentRatio * 100); if (depth > state.scrollDepth) { state.scrollDepth = depth; } return state.scrollDepth; } /** * 上报文章阅读停留时长与滑屏深度至访客雷达 CRM */ export async function reportArticleStay( state: ArticleDetailState, scrollDepth?: number ): Promise { if (state.cardId === '' || state.blockId === '') { return null; } const currentDepth = scrollDepth != null ? scrollDepth : state.scrollDepth; const enter = state.enterTime > 0 ? state.enterTime : Date.now(); const stayDurationMs = Math.max(0, Date.now() - enter); const visitorId = state.visitorId !== '' ? state.visitorId : getOrCreateVisitorId(); try { const track = await recordRadarTrack({ cardId: state.cardId, visitorId, touchType: 'link', stayDurationMs, scrollDepth: currentDepth, viewedBlocks: [state.blockId], }); return track; } catch (_e) { return null; } } /** * 导航至专栏作者的名片主微官网 */ export function navToAuthorPortal(state: ArticleDetailState): void { if (state.cardId === '') { return; } uni.navigateTo({ url: `/pages/portal/index?id=${state.cardId}`, fail: () => { uni.reLaunch({ url: `/pages/portal/index?id=${state.cardId}`, }); }, }); } type ShareWithSystemOption = { type?: string; summary?: string; href?: string; success?: () => void; fail?: (err: { errCode?: number; errMsg?: string } | null) => void; }; type UniWithSystemShare = { shareWithSystem?: (options: ShareWithSystemOption) => void; }; /** * 是否具备系统分享 API(uni.shareWithSystem) */ export function hasArticleSystemShare(): boolean { const uniShare = uni as UniWithSystemShare; return uniShare.shareWithSystem != null; } /** * 系统分享:仅 uni.shareWithSystem 明确 success 才提示「已分享」; * 失败 / 用户取消不得提示已分享。 */ export function shareArticleViaSystem( state: ArticleDetailState, pageUrl?: string ): Promise { const fallback = pageUrl != null ? pageUrl : getCurrentPageUrl(state); const shareUrl = resolveShareUrl(state.linkUrl, fallback); if (shareUrl === '') { return Promise.resolve(false); } const uniShare = uni as UniWithSystemShare; if (uniShare.shareWithSystem == null) { return Promise.resolve(false); } const summary = state.title !== '' ? state.title : shareUrl; return new Promise(resolve => { uniShare.shareWithSystem!({ type: 'text', summary: summary, href: shareUrl, success: () => { uni.showToast({ title: resolveArticleDetailText('shareSuccess'), icon: 'none', }); resolve(true); }, fail: (err: { errCode?: number; errMsg?: string } | null) => { // 取消(1310600)静默;其它失败只提示 shareFailed const code = err != null && err.errCode != null ? err.errCode : 0; if (code !== 1310600) { uni.showToast({ title: resolveArticleDetailText('shareFailed'), icon: 'none', }); } resolve(false); }, }); }); } /** * 主 CTA:复制 linkUrl 或当前页 URL。 * 仅剪贴板 success 才提示直链已复制(P22);失败只提示 copyFailed。 */ export function copyArticleShareLink(state: ArticleDetailState, pageUrl?: string): boolean { const fallback = pageUrl != null ? pageUrl : getCurrentPageUrl(state); const shareUrl = resolveShareUrl(state.linkUrl, fallback); if (shareUrl === '') { return false; } uni.setClipboardData({ data: shareUrl, success: () => { uni.showToast({ title: getOmniText(getCurrentLocale(), 'portal.copied_link'), icon: 'none', }); }, fail: () => { uni.showToast({ title: resolveArticleDetailText('copyFailed'), icon: 'none', }); }, }); return true; } /** * 底栏主动作:有系统分享则走系统分享;否则回退复制链接。 * 分享失败/取消不回退复制,避免把失败伪装成已分享或已复制。 */ export async function performArticleShareAction( state: ArticleDetailState, pageUrl?: string ): Promise { if (hasArticleSystemShare()) { return await shareArticleViaSystem(state, pageUrl); } return copyArticleShareLink(state, pageUrl); } /** * 空状态返回:优先回退上一页 */ export function goBackFromArticle(state: ArticleDetailState): void { uni.navigateBack({ delta: 1, fail: () => { const query = state.cardId !== '' ? `?id=${state.cardId}` : ''; uni.reLaunch({ url: `/pages/portal/index${query}`, }); }, }); }