文件
OmniCard/mpuniapp/pages/portal/useCaseDetail.uts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

528 行
13 KiB
Plaintext

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* 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<CaseDetailCopyKey, string> = {
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<boolean> {
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();
}