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