/** * OmniCard AI (CRMmp) - 案例作品集详情业务驱动层 * 严格遵循 uts 强类型规范与 AGENTS.md 约束 */ import { CardPortalDetail, BentoBlockPublic } from '../../common/types.uts'; import { fetchPortalCard } from '../../common/api.uts'; import { getCurrentLocale, getOmniText } from '../../common/locale.uts'; export type CaseKpi = { label: string; value: string; }; type KpiLoose = { label?: string; name?: string; title?: string; value?: string | number; }; export type CaseMetaRecord = { images?: string[]; kpis?: KpiLoose[]; metrics?: KpiLoose[]; pdfUrl?: string; pdf?: string; youtubeUrl?: string; youtube?: string; videoUrl?: string; video?: string; poster?: string; url?: string; type?: string; client?: string; industry?: string; region?: string; challenge?: string; solution?: string; }; export type CaseDetailState = { cardId: string; blockId: string; loading: boolean; empty: boolean; errorMessage: string; title: string; content: string; images: string[]; kpis: CaseKpi[]; pdfUrl: string; youtubeUrl: string; videoUrl: string; blockType: string; client?: string; industry?: string; region?: string; challenge?: string; solution?: string; }; export type CaseDetailPageOptions = { cardId?: string; blockId?: string; id?: string; }; export type CaseDetailCopyKey = | 'empty' | 'emptyDesc' | 'back' | 'cta' | 'pdf' | 'youtube' | 'loading' | 'badge' | 'videoBadge' | 'overview' | 'challenge' | 'solution' | 'kpis' | 'resources' | 'pdfAction' | 'videoAction' | 'clientLabel' | 'industryLabel' | 'regionLabel'; export const CASE_DETAIL_COPY: Record = { empty: '未找到该案例', emptyDesc: '返回上一页继续浏览', back: '返回', cta: '咨询', pdf: 'PDF', youtube: '视频', loading: '加载中', badge: '标杆商务实践', videoBadge: '视频演示实况', overview: '案例详情与方案', challenge: '业务挑战与背景', solution: '落地解决方案', kpis: '核心商业化成果', resources: '项目资料与演示', pdfAction: '打开白皮书', videoAction: '观看演示视频', clientLabel: '标杆客户', industryLabel: '行业赛道', regionLabel: '业务覆盖区域', }; function isZhLocale(): boolean { return getCurrentLocale() === 'zh-CN'; } /** * 案例详情文案:模板只调本函数,禁止直接绑中文。词典能复用的走 getOmniText。 */ export function resolveCaseDetailText(key: CaseDetailCopyKey): string { const locale = getCurrentLocale(); if (key === 'loading') { return getOmniText(locale, 'common.loading'); } const zh = isZhLocale(); if (key === 'empty') { return zh ? CASE_DETAIL_COPY.empty : 'This case is unavailable'; } if (key === 'emptyDesc') { return zh ? CASE_DETAIL_COPY.emptyDesc : 'Go back to keep browsing'; } if (key === 'back') { return zh ? CASE_DETAIL_COPY.back : 'Back'; } if (key === 'cta') { return zh ? CASE_DETAIL_COPY.cta : 'Inquire'; } if (key === 'badge') { return zh ? CASE_DETAIL_COPY.badge : 'GLOBAL CASE STUDY'; } if (key === 'videoBadge') { return zh ? CASE_DETAIL_COPY.videoBadge : 'FEATURED VIDEO DEMO'; } if (key === 'overview') { return zh ? CASE_DETAIL_COPY.overview : 'Case Overview'; } if (key === 'challenge') { return zh ? CASE_DETAIL_COPY.challenge : 'Business Challenge'; } if (key === 'solution') { return zh ? CASE_DETAIL_COPY.solution : 'Solution & Architecture'; } if (key === 'kpis') { return zh ? CASE_DETAIL_COPY.kpis : 'Key Business Results'; } if (key === 'resources') { return zh ? CASE_DETAIL_COPY.resources : 'Project Resources'; } if (key === 'pdfAction') { return zh ? CASE_DETAIL_COPY.pdfAction : 'Open whitepaper'; } if (key === 'videoAction') { return zh ? CASE_DETAIL_COPY.videoAction : 'Watch demo video'; } if (key === 'pdf') { return 'PDF'; } if (key === 'youtube') { return zh ? '视频' : 'Video'; } if (key === 'clientLabel') { return zh ? CASE_DETAIL_COPY.clientLabel : 'Client'; } if (key === 'industryLabel') { return zh ? CASE_DETAIL_COPY.industryLabel : 'Industry'; } if (key === 'regionLabel') { return zh ? CASE_DETAIL_COPY.regionLabel : 'Region'; } return CASE_DETAIL_COPY[key]; } /** * 打开案例外链:优先 window.open(对齐 useLogin / useLeadDetail), * 无 H5 open 能力时回退复制;空 url 不渲染、不打开。 */ export function openCaseExternalLink(url: string): boolean { const safeUrl = url != null ? url.trim() : ''; if (safeUrl === '') { return false; } let opened = false; try { if (typeof window !== 'undefined' && typeof window.open === 'function') { window.open(safeUrl, '_blank'); opened = true; } } catch (_e) {} if (opened) { return true; } try { uni.setClipboardData({ data: safeUrl, success: () => { uni.showToast({ title: getOmniText(getCurrentLocale(), 'portal.copied_link'), icon: 'none', }); }, }); return true; } catch (_e) { return false; } } /** * 创建案例详情初始状态 */ export function createCaseDetailState(): CaseDetailState { return { cardId: '', blockId: '', loading: false, empty: false, errorMessage: '', title: '', content: '', images: [], kpis: [], pdfUrl: '', youtubeUrl: '', videoUrl: '', blockType: 'case', client: '', industry: '', region: '', challenge: '', solution: '', }; } /** * 从 onLoad options 读取 cardId / blockId */ export function applyCaseDetailOptions( state: CaseDetailState, options: CaseDetailPageOptions | 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(); return state.cardId !== '' && state.blockId !== ''; } /** * 从门户聚合数据中选出 id 匹配且 type 为 case 或 video 的块;缺块返回 null */ export function pickCaseBlock( card: CardPortalDetail | null, blockId: string ): BentoBlockPublic | null { if (card == null || card.bentoBlocks == null || blockId === '') { return null; } const target = String(blockId); const blocks = card.bentoBlocks; for (let i = 0; i < blocks.length; i++) { const block = blocks[i]; if (String(block.id) === target && (block.type === 'case' || block.type === 'video')) { return block; } } return null; } /** * 解析 metaJson(字符串或对象) */ export function parseCaseMetaJson(raw: string | object | null | undefined): CaseMetaRecord { if (raw == null) { return {}; } if (typeof raw === 'string') { const trimmed = raw.trim(); if (trimmed === '') { return {}; } try { return JSON.parse(trimmed) as CaseMetaRecord; } catch (_e) { return {}; } } return raw as CaseMetaRecord; } function readTrimmed(value: string | undefined): string { return value != null ? String(value).trim() : ''; } function normalizeKpis(items: KpiLoose[] | undefined): CaseKpi[] { if (items == null || !Array.isArray(items)) { return []; } const result: CaseKpi[] = []; for (let i = 0; i < items.length; i++) { const item = items[i]; if (item == null) { continue; } const label = readTrimmed(item.label) || readTrimmed(item.name) || readTrimmed(item.title); const value = item.value != null ? String(item.value).trim() : ''; if (label === '' && value === '') { continue; } result.push({ label: label !== '' ? label : value, value: value !== '' ? value : label, }); } return result; } function collectImages(mediaUrl: string, meta: CaseMetaRecord): string[] { const result: string[] = []; if (meta.images != null && Array.isArray(meta.images)) { for (let i = 0; i < meta.images.length; i++) { const src = String(meta.images[i]).trim(); if (src !== '') { result.push(src); } } } if (result.length === 0) { const poster = readTrimmed(meta.poster); if (poster !== '') { result.push(poster); } } if (result.length === 0 && mediaUrl !== '') { result.push(mediaUrl); } return result; } function resolvePdfUrl(meta: CaseMetaRecord): string { const pdfUrl = readTrimmed(meta.pdfUrl); if (pdfUrl !== '') { return pdfUrl; } const pdf = readTrimmed(meta.pdf); if (pdf !== '') { return pdf; } const url = readTrimmed(meta.url); const kind = readTrimmed(meta.type).toLowerCase(); if (url !== '' && (kind === 'pdf' || url.toLowerCase().indexOf('.pdf') >= 0)) { return url; } return ''; } function resolveVideoDirectUrl(meta: CaseMetaRecord, block: BentoBlockPublic): string { const metaVideo = readTrimmed(meta.videoUrl) || readTrimmed(meta.video); if (metaVideo !== '') { const lower = metaVideo.toLowerCase(); if (lower.indexOf('youtube.com') < 0 && lower.indexOf('youtu.be') < 0) { return metaVideo; } } const link = block.linkUrl != null ? String(block.linkUrl).trim() : ''; if (link !== '') { const lower = link.toLowerCase(); if (lower.indexOf('youtube.com') < 0 && lower.indexOf('youtu.be') < 0) { return link; } } return ''; } function resolveYoutubeUrl(meta: CaseMetaRecord, block?: BentoBlockPublic): string { const youtubeUrl = readTrimmed(meta.youtubeUrl); if (youtubeUrl !== '') { return youtubeUrl; } const youtube = readTrimmed(meta.youtube); if (youtube !== '') { return youtube; } const url = readTrimmed(meta.url) || readTrimmed(meta.videoUrl); const kind = readTrimmed(meta.type).toLowerCase(); const lower = url.toLowerCase(); if ( url !== '' && (kind === 'youtube' || lower.indexOf('youtube.com') >= 0 || lower.indexOf('youtu.be') >= 0) ) { return url; } if (block != null && block.linkUrl != null) { const link = String(block.linkUrl).trim(); const linkLower = link.toLowerCase(); if (linkLower.indexOf('youtube.com') >= 0 || linkLower.indexOf('youtu.be') >= 0) { return link; } } return ''; } function clearCaseContent(state: CaseDetailState): void { state.title = ''; state.content = ''; state.images = []; state.kpis = []; state.pdfUrl = ''; state.youtubeUrl = ''; state.videoUrl = ''; state.blockType = 'case'; state.client = ''; state.industry = ''; state.region = ''; state.challenge = ''; state.solution = ''; } function applyEmpty(state: CaseDetailState): void { state.loading = false; state.empty = true; clearCaseContent(state); } function fillFromBlock(state: CaseDetailState, block: BentoBlockPublic): void { const meta = parseCaseMetaJson(block.metaJson); const mediaUrl = block.mediaUrl != null ? block.mediaUrl.trim() : ''; state.empty = false; state.loading = false; state.errorMessage = ''; state.blockType = block.type != null ? String(block.type) : 'case'; state.title = block.title != null ? block.title : ''; state.content = block.content != null ? block.content : ''; state.images = collectImages(mediaUrl, meta); const kpis = normalizeKpis(meta.kpis); state.kpis = kpis.length > 0 ? kpis : normalizeKpis(meta.metrics); state.pdfUrl = resolvePdfUrl(meta); state.videoUrl = resolveVideoDirectUrl(meta, block); state.youtubeUrl = resolveYoutubeUrl(meta, block); state.client = readTrimmed(meta.client); state.industry = readTrimmed(meta.industry); state.region = readTrimmed(meta.region); state.challenge = readTrimmed(meta.challenge); state.solution = readTrimmed(meta.solution); } /** * 用已拉取的门户数据填充案例详情;缺块返空 */ export function applyPortalCase( state: CaseDetailState, card: CardPortalDetail | null ): boolean { const block = pickCaseBlock(card, state.blockId); if (block == null) { applyEmpty(state); return false; } fillFromBlock(state, block); return true; } /** * 拉取门户聚合详情并选出 case 块 */ export async function loadCaseDetail(state: CaseDetailState): Promise { if (state.cardId === '' || state.blockId === '') { applyEmpty(state); return false; } state.loading = true; state.empty = false; state.errorMessage = ''; try { const card = await fetchPortalCard(state.cardId); return applyPortalCase(state, card); } catch (err) { applyEmpty(state); if (err instanceof Error) { state.errorMessage = err.message; } else { state.errorMessage = '获取案例详情失败'; } return false; } } /** * 咨询 / 互换页路径 */ export function buildExchangePath(cardId: string): string { return `/pages/portal/exchange?cardId=${cardId}`; } /** * 跳转无摩擦互换名片 */ export function openCaseExchange(cardId: string): void { const id = cardId != null ? cardId.trim() : ''; if (id === '') { return; } uni.navigateTo({ url: buildExchangePath(id), }); } /** * 空状态返回上一页 */ export function goCaseBack(): void { uni.navigateBack(); }