/** * OmniCard AI (CRMmp) - 深度洞见文章详情业务驱动层 * 严格遵循 uts 强类型规范与 AGENTS.md 约束 */ import { BentoBlockPublic } from '../../common/types.uts'; import { fetchPortalCard } from '../../common/api.uts'; import { getCurrentLocale, getOmniText } 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; }; const SHARE_CTA_LABEL = '复制链接'; const BACK_LABEL = '返回'; const OG_DESC_MAX = 160; /** * 创建文章详情初始状态 */ export function createArticleDetailState(): ArticleDetailState { return { cardId: '', blockId: '', loading: false, article: null, title: '', content: '', mediaUrl: '', linkUrl: '', isHtmlContent: false, empty: false, errorMessage: '', openGraph: { title: '', description: '', image: '', }, }; } /** * 将详情状态复位为空 */ 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: '', }; } /** * 从 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 { const pages = uni.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: SHARE_CTA_LABEL, 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'); } function applyArticle(state: ArticleDetailState, article: BentoBlockPublic): 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); } /** * fetchPortalCard 后从 bentoBlocks 选出 article */ export async function loadArticleDetail( state: ArticleDetailState ): Promise { if (state.cardId === '' || state.blockId === '') { applyEmptyArticle(state); return null; } state.loading = true; state.errorMessage = ''; 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); 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; } } /** * 主 CTA:复制 linkUrl 或当前页 URL */ export function copyArticleShareLink(state: ArticleDetailState, pageUrl?: string): boolean { const fallback = pageUrl != null && pageUrl !== '' ? 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', }); }, }); return true; } /** * 空状态返回:优先回退上一页 */ 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}`, }); }, }); }