feat(backend): 增强名片高保真种子数据与 SEO 微官网服务端渲染能力
这个提交包含在:
@@ -1,3 +1,4 @@
|
||||
import { Inject, Post } from '@midwayjs/core';
|
||||
import { CoolController, BaseController } from '@cool-midway/core';
|
||||
import { CardInfoEntity } from '../../entity/info';
|
||||
import { CardInfoService } from '../../service/info';
|
||||
@@ -38,4 +39,21 @@ export const adminCardInfoCurdOption = {
|
||||
* 路由前缀:/admin/card/info
|
||||
*/
|
||||
@CoolController(adminCardInfoCurdOption)
|
||||
export class AdminCardInfoController extends BaseController {}
|
||||
export class AdminCardInfoController extends BaseController {
|
||||
@Inject()
|
||||
cardInfoService: CardInfoService;
|
||||
|
||||
/**
|
||||
* 一键播种/同步 6 套全球顶级商务名片预览数据
|
||||
* 接口地址: POST /admin/card/info/seed
|
||||
*/
|
||||
@Post('/seed', { summary: '一键播种高保真名片预览数据' })
|
||||
async seed() {
|
||||
const count = await this.cardInfoService.seedMockCards();
|
||||
return this.ok({
|
||||
count,
|
||||
message: `成功同步 ${count} 套全球高保真商务名片预览数据`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
GeoHeaderMap,
|
||||
resolveSuggestedLocale,
|
||||
} from '../../../../comm/i18n/geo';
|
||||
import { MOCK_CARDS_DATA } from '../../mock/seed-cards';
|
||||
|
||||
export interface PortalRequestContext {
|
||||
headers?: GeoHeaderMap;
|
||||
@@ -89,6 +90,23 @@ export class OpenCardPortalController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部高保真名片预览清单 (供前端预览体验与免登录快速切换)
|
||||
* 接口地址: GET /open/card/portal/mock-cards
|
||||
*/
|
||||
@CoolTag(TagTypes.IGNORE_TOKEN)
|
||||
@Get('/mock-cards', { summary: '获取高保真名片预览清单' })
|
||||
async mockCards() {
|
||||
return this.ok(
|
||||
MOCK_CARDS_DATA.map(item => ({
|
||||
...item.card,
|
||||
bentoBlocksCount: item.bentoBlocks.length,
|
||||
casesCount: item.cases.length,
|
||||
articlesCount: item.articles.length,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出国际商务日程预约排期 ICS 日历订阅文件
|
||||
* 接口地址: GET /open/card/portal/meeting-ics
|
||||
@@ -260,7 +278,8 @@ export class OpenCardPortalController extends BaseController {
|
||||
!slugOrId ||
|
||||
slugOrId === 'detail' ||
|
||||
slugOrId === 'meeting-ics' ||
|
||||
slugOrId === 'preview-html'
|
||||
slugOrId === 'preview-html' ||
|
||||
slugOrId === 'mock-cards'
|
||||
) {
|
||||
return this.fail('缺少名片标识参数~', 1001);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface MockCardBundle {
|
||||
}
|
||||
|
||||
/**
|
||||
* 5 组不同全球主流行业的高保真多媒体名片数据(含配图与视频)
|
||||
* 6 组不同全球主流顶级商业行业的高保真多媒体名片数据(含真实配图、可播视频、全景 Bento 块、量化案例与长文)
|
||||
*/
|
||||
export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
// ==========================================
|
||||
@@ -36,6 +36,7 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
whatsapp: '+8613800138001',
|
||||
linkedin: 'https://linkedin.com/in/alex-zheng-omni',
|
||||
twitter: 'https://x.com/alexzheng_hw',
|
||||
wechat: 'alex_omni_global',
|
||||
timezone: 'Asia/Singapore',
|
||||
audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/alex-zheng.mp3',
|
||||
status: 'published',
|
||||
@@ -51,8 +52,8 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 1,
|
||||
title: '关于我与 OmniCard',
|
||||
content: '专注全球物联网与智能消费电子 12 年,连接全球 60+ 国家和地区的创新生态。',
|
||||
title: '关于我与 OmniCard 国际化版图',
|
||||
content: '专注全球物联网与智能硬件生态 12 年,深度连接亚太供应链与欧美中东主流渠道。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1497366216548-37526070297c.jpg',
|
||||
status: 1,
|
||||
},
|
||||
@@ -64,7 +65,7 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
sortOrder: 2,
|
||||
title: 'OmniCard Next 旗舰实体卡实机开箱演示',
|
||||
content: '超精密阳极氧化铝雕刻,内置双频高灵敏 NFC 芯片与动态感应天线,轻触即达微官网。',
|
||||
content: '超精密航空级阳极氧化铝雕刻,内置双频高灵敏 NFC 芯片与动态射频天线,触卡 0.3 秒直达微官网。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c.jpg',
|
||||
linkUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4',
|
||||
metaJson: {
|
||||
@@ -72,7 +73,7 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
poster: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c.jpg',
|
||||
duration: '01:28',
|
||||
aspectRatio: '16:9',
|
||||
title: 'OmniCard Next 硬件工艺与触卡交互',
|
||||
title: 'OmniCard Next 硬件工艺与触卡交互实景',
|
||||
},
|
||||
status: 1,
|
||||
},
|
||||
@@ -84,7 +85,7 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 3,
|
||||
title: 'GITEX 迪拜展数字化留资',
|
||||
content: '实现 10,000+ 国际买家 0.5 秒即时触卡交互,获客转化提升 320%。',
|
||||
content: '实现 10,000+ 国际买家 0.5 秒即时触卡交互,展会线索转化率较传统名片提升 320%。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1511578314322-379afb476865.jpg',
|
||||
status: 1,
|
||||
},
|
||||
@@ -95,11 +96,34 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 4,
|
||||
title: '2026 全球智能硬件合规与准入白皮书',
|
||||
content: '深度解读 CE/FCC/KC 认证新规与国际电池环保法规应对策略。',
|
||||
title: '2026 全球智能硬件合规白皮书',
|
||||
content: '深度解读 CE/FCC/KC 认证新规与国际电池环保政策,助力中国高精尖硬件顺畅出海。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2005,
|
||||
cardId: 1001,
|
||||
type: 'social' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 5,
|
||||
title: '全球即时商业联络',
|
||||
content: '支持 LinkedIn、WhatsApp 与官方电子邮箱一键添加,全天候跨时区无缝对接。',
|
||||
linkUrl: 'https://linkedin.com/in/alex-zheng-omni',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2006,
|
||||
cardId: 1001,
|
||||
type: 'form' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 6,
|
||||
title: '预约跨国硬件分销与渠道合作',
|
||||
content: '留下您的企业与联系方式,我们将在 2 个工作小时内安排资深全球商务代表与您连线。',
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
@@ -108,10 +132,10 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
title: 'GITEX 迪拜展会智能名片规模化交付',
|
||||
client: '阿联酋国际商业联合会',
|
||||
duration: '2025.10 - 2025.11',
|
||||
metrics: '触卡 12,500+ 次,沉淀高意向商业线索 890 条',
|
||||
metrics: '触卡 12,500+ 次,沉淀高意向商业线索 890 条,获客转化提升 320%',
|
||||
coverImage: 'https://images.unsplash.com/photo-1511578314322-379afb476865.jpg',
|
||||
pdfUrl: 'https://cdn.omnicard.ai/cases/gitex-2025-report.pdf',
|
||||
description: '为 48 家国际参展代表团统一配置智能名片与雷达跟进中枢,完成全程数字化展会获客。',
|
||||
description: '为 48 家国际参展代表团统一配置智能名片与雷达跟进中枢,完成全程数字化展会获客与私域沉淀。',
|
||||
},
|
||||
],
|
||||
articles: [
|
||||
@@ -119,8 +143,8 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
id: 4001,
|
||||
cardId: 1001,
|
||||
title: '智能硬件全球化新范式:从单品突围到全域生态触达',
|
||||
summary: '探讨如何结合 NFC 与端侧 AI 技术在国际展会和商务洽谈中建立第一分钟信任。',
|
||||
content: '在全球消费电子产业加速演进的今天,传统的纸质物料正在迅速被具备实时交互能力的数字名片和微官网替代...',
|
||||
summary: '探讨如何结合 NFC 物理触碰与端侧微官网技术,在国际展会和高端商务洽谈中建立第一分钟深度信任。',
|
||||
content: '在全球消费电子产业加速演进的今天,传统的纸质物料正在迅速被具备实时交互能力的数字名片和微官网替代。通过 NFC 极速交互与访客雷达意向追踪,硬件出海企业能够将每次面对面握手转变为可长期沉淀的高价值商机资产...',
|
||||
coverImage: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa.jpg',
|
||||
isPublished: true,
|
||||
},
|
||||
@@ -154,26 +178,26 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
},
|
||||
bentoBlocks: [
|
||||
{
|
||||
id: 2005,
|
||||
id: 2007,
|
||||
cardId: 1002,
|
||||
type: 'hero' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 1,
|
||||
title: '全球云架构与大模型算力调度',
|
||||
content: '主导超过 5,000+ 节点高可用 Kubernetes 集群与跨大洲大模型推理加速网络设计。',
|
||||
content: '主导超过 5,000+ 节点高可用 Kubernetes 集群与跨大洲大模型推理加速网络设计,可用性达 99.999%。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2006,
|
||||
id: 2008,
|
||||
cardId: 1002,
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
sortOrder: 2,
|
||||
title: 'NebulaScale 分布式大模型推理架构演示',
|
||||
content: '实时观测跨欧美亚太算力流转与动态故障转移,端到端延迟压至 35ms 以内。',
|
||||
content: '实时观测跨欧美亚太算力流转与动态故障转移,端到端全球 P99 延迟压至 35ms 以内。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5.jpg',
|
||||
linkUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||
metaJson: {
|
||||
@@ -186,19 +210,19 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2007,
|
||||
id: 2009,
|
||||
cardId: 1002,
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 3,
|
||||
title: 'FinTech 全球秒级支付架构重构',
|
||||
content: '99.999% 可用性保障,支撑每日 3 亿美元全球交易量。',
|
||||
content: '99.999% 可用性保障,支撑每日 3 亿美元全球跨币种交易清结算。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1563986768609-322da13575f3.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2008,
|
||||
id: 2010,
|
||||
cardId: 1002,
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
@@ -209,6 +233,29 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2011,
|
||||
cardId: 1002,
|
||||
type: 'social' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 5,
|
||||
title: '技术专栏与开源社区',
|
||||
content: 'GitHub 开源项目贡献者,定期分享 Cloud Native 与 GPU 算力优化专题。',
|
||||
linkUrl: 'https://linkedin.com/in/elena-rostova-cloud',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2012,
|
||||
cardId: 1002,
|
||||
type: 'form' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 6,
|
||||
title: '预约云原生架构深度咨询',
|
||||
content: '提供企业异地多活、GPU 资源池化与跨国网络加速的一对一专家诊断。',
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
@@ -217,9 +264,9 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
title: '跨国全球支付金融级容灾架构改造',
|
||||
client: 'PayNova International',
|
||||
duration: '2025.03 - 2025.08',
|
||||
metrics: '交易超时率降低 98.4%,算力成本节省 35%',
|
||||
metrics: '交易超时率降低 98.4%,算力成本节省 35%,支撑每日 3 亿美元交易',
|
||||
coverImage: 'https://images.unsplash.com/photo-1563986768609-322da13575f3.jpg',
|
||||
description: '基于异地多活方案实现 RPO=0, RTO<1s 的高可用架构,顺利通过全球多国金融安全审计。',
|
||||
description: '基于异地多活方案实现 RPO=0, RTO<1s 的高可用架构,顺利通过全球多国金融安全与合规审计。',
|
||||
},
|
||||
],
|
||||
articles: [
|
||||
@@ -227,8 +274,8 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
id: 4002,
|
||||
cardId: 1002,
|
||||
title: '在大模型时代重新思考全球分布式架构演进',
|
||||
summary: '探索算力调度从传统容器编排向 GPU 异构资源池化迁移的核心挑战。',
|
||||
content: '伴随企业全球化战略加速,算力孤岛与网络延迟成为制约 AI 商业落地的最大瓶颈...',
|
||||
summary: '探索算力调度从传统微服务容器编排向 GPU 异构资源池化迁移的核心技术瓶颈与破局之道。',
|
||||
content: '伴随企业全球化战略加速,算力孤岛与跨大洲网络延迟成为制约 AI 商业落地的最大瓶颈。通过结合网络拓扑感知的近端推理调度与智能缓存穿透策略,我们成功在多云混部环境下将推理效率提升了 300%...',
|
||||
coverImage: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d.jpg',
|
||||
isPublished: true,
|
||||
},
|
||||
@@ -261,26 +308,26 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
},
|
||||
bentoBlocks: [
|
||||
{
|
||||
id: 2009,
|
||||
id: 2013,
|
||||
cardId: 1003,
|
||||
type: 'hero' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 1,
|
||||
title: '海湾合作委员会 (GCC) 绿色能源生态',
|
||||
content: '常驻阿布扎比与利雅得,深耕沙特“2030愿景”与国际绿色能源技术对接。',
|
||||
content: '常驻阿布扎比与利雅得,深耕沙特“2030愿景”与国际绿色高端技术与装备对接。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1509391365360-2e959784a276.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2010,
|
||||
id: 2014,
|
||||
cardId: 1003,
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
sortOrder: 2,
|
||||
title: '阿布扎比 2GW 光伏产业示范园落成实景',
|
||||
content: '沙漠极端高温环境下的自动化清扫与高效储能实景纪录片,见证绿色能源奇迹。',
|
||||
content: '沙漠极端高温环境下的自动化清扫与高效储能实景纪录片,见证中东绿色能源奇迹。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1497435334941-8c899ee9e8e9.jpg',
|
||||
linkUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
|
||||
metaJson: {
|
||||
@@ -293,7 +340,7 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2011,
|
||||
id: 2015,
|
||||
cardId: 1003,
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
@@ -305,17 +352,29 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2012,
|
||||
id: 2016,
|
||||
cardId: 1003,
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 4,
|
||||
title: '海湾国家商业投资环境与法律合规指引',
|
||||
content: '从外资持股比例到本地化雇佣比率,全方位规避国际投资法律风险。',
|
||||
title: '海湾国家商业投资环境与法律指引',
|
||||
content: '从外资持股比例到本地化雇佣比率,全方位规避国际跨境投资法律与财税风险。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2017,
|
||||
cardId: 1003,
|
||||
type: 'social' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 5,
|
||||
title: '中东政商直达网络',
|
||||
content: '阿联酋与沙特本地专线,支持 WhatsApp 商务洽谈与阿拉伯语本土商务对接。',
|
||||
linkUrl: 'https://linkedin.com/in/tariq-almansoor-mena',
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
@@ -324,7 +383,7 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
title: '阿布扎比综合智慧能源示范中心一期',
|
||||
client: 'Abu Dhabi Future Energy PJSC',
|
||||
duration: '2024.08 - 2025.04',
|
||||
metrics: '年发电量 32 亿千瓦时,减排二氧化碳 240 万吨',
|
||||
metrics: '年发电量 32 亿千瓦时,减排二氧化碳 240 万吨,供电可靠率 99.98%',
|
||||
coverImage: 'https://images.unsplash.com/photo-1466611653911-95081537e5b7.jpg',
|
||||
description: '整合国际先锋光伏组件与智能追踪支架,打造跨国绿色能源合作的典范工程。',
|
||||
},
|
||||
@@ -334,8 +393,8 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
id: 4003,
|
||||
cardId: 1003,
|
||||
title: '走进中东:沙特与阿联酋新一轮科技能源投资机遇',
|
||||
summary: '解析全球高科技与能源装备企业在海湾市场的本土化突围之道。',
|
||||
content: '随着海湾国家加速摆脱石油依赖,以绿色能源与智能化技术为核心的产业变革正在全面展开...',
|
||||
summary: '解析全球高科技与能源装备企业在海湾市场的本土化突围之道与主权基金战略倾向。',
|
||||
content: '随着海湾国家加速摆脱石油依赖,以绿色能源与智能化技术为核心的产业变革正在全面展开。阿布扎比投资局与沙特 PIF 正积极设立针对亚太高科技与先进制造企业的联合落地基金...',
|
||||
coverImage: 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c.jpg',
|
||||
isPublished: true,
|
||||
},
|
||||
@@ -368,7 +427,7 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
},
|
||||
bentoBlocks: [
|
||||
{
|
||||
id: 2013,
|
||||
id: 2018,
|
||||
cardId: 1004,
|
||||
type: 'hero' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
@@ -380,14 +439,14 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2014,
|
||||
id: 2019,
|
||||
cardId: 1004,
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
sortOrder: 2,
|
||||
title: '巴黎绿色时尚周品牌秀场精选',
|
||||
content: '从可降解生物丝绸到 0 碳足迹工坊生产实拍,展现先锋环保奢品背后的匠心。',
|
||||
content: '从可降解生物丝绸到 0 碳足迹工坊生产实拍,展现先锋环保奢品背后的匠心工艺。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1469334031218-e382a71b716b.jpg',
|
||||
linkUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4',
|
||||
metaJson: {
|
||||
@@ -400,7 +459,7 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2015,
|
||||
id: 2020,
|
||||
cardId: 1004,
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
@@ -412,17 +471,29 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2016,
|
||||
id: 2021,
|
||||
cardId: 1004,
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 4,
|
||||
title: '时尚产业的数字护照 (DPP) 全球实践',
|
||||
content: '如何利用区块链与 NFC 芯片追踪服装从棉花田到衣橱的全生命周期。',
|
||||
content: '如何利用区块链与 NFC 芯片追踪服装从棉花田到衣橱的全生命周期溯源。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1558769132-cb1aea458c5e.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2022,
|
||||
cardId: 1004,
|
||||
type: 'social' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 5,
|
||||
title: '全球先锋买手与媒体联络',
|
||||
content: '直连巴黎与米兰时装周 VIP 媒体公关通道,快速预约媒体样品借拍与合作。',
|
||||
linkUrl: 'https://linkedin.com/in/chloe-dubois-luxury',
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
@@ -431,7 +502,7 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
title: 'Atelier Lumina 欧美全渠道首发战役',
|
||||
client: 'Lumina Paris',
|
||||
duration: '2025.05 - 2025.09',
|
||||
metrics: '欧美 GMV 增长 210%,复购率达到 38.6%',
|
||||
metrics: '欧美 GMV 增长 210%,复购率达 38.6%,全网曝光 3500 万次',
|
||||
coverImage: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8.jpg',
|
||||
description: '通过数字体验名片与 VIP 专属私人定制预约通道,构建高端时尚圈层闭环。',
|
||||
},
|
||||
@@ -441,8 +512,8 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
id: 4004,
|
||||
cardId: 1004,
|
||||
title: '为什么新一代全球 DTC 品牌需要重塑品牌心智',
|
||||
summary: '摆脱流量依赖与单一价格战,以故事叙事和品牌价值赢得国际高净值客群。',
|
||||
content: '全球品牌的竞争已经从单纯的供应链红利转向了文化叙事与品牌价值主张的博弈...',
|
||||
summary: '摆脱低维流量依赖与单一价格战,以真实故事叙事和可持续品牌主张赢得国际高净值客群。',
|
||||
content: '全球品牌的竞争已经从单纯的供应链红利转向了文化叙事与品牌价值主张的博弈。通过透明的原材料碳足迹披露与极具现代美学的数字名片微官网,品牌得以建立超越单次买卖的长期信任感...',
|
||||
coverImage: 'https://images.unsplash.com/photo-1558769132-cb1aea458c5e.jpg',
|
||||
isPublished: true,
|
||||
},
|
||||
@@ -476,19 +547,19 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
},
|
||||
bentoBlocks: [
|
||||
{
|
||||
id: 2017,
|
||||
id: 2023,
|
||||
cardId: 1005,
|
||||
type: 'hero' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 1,
|
||||
title: '跨国汽车产业链智能物流中枢',
|
||||
content: '管理东京、上海、曼谷三大核心转运港口与 12 座保税自动化立体中心仓。',
|
||||
content: '管理东京、上海、曼谷三大核心转运枢纽港口与 12 座保税自动化立体中心仓。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2018,
|
||||
id: 2024,
|
||||
cardId: 1005,
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
@@ -508,29 +579,41 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2019,
|
||||
id: 2025,
|
||||
cardId: 1005,
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 3,
|
||||
title: '中日新能源车配件 48 小时达',
|
||||
content: '通过全链条条码及 RFID 联动,清关及通关提速 60%。',
|
||||
content: '通过全链条条码及 RFID 联动,关务及通关清关时效提速 60%。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1578575437130-527eed3abbec.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2020,
|
||||
id: 2026,
|
||||
cardId: 1005,
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 4,
|
||||
title: '区域全面经济伙伴关系协定 (RCEP) 关务实战指南',
|
||||
content: '深度解读原产地累积规则与跨国物流链条中的税费优化策略。',
|
||||
title: '区域全面经济伙伴关系协定 (RCEP) 关务指南',
|
||||
content: '深度解读原产地累积规则与跨国多式联运链条中的综合税费优化方案。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1542744173-8e7e53415bb0.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2027,
|
||||
cardId: 1005,
|
||||
type: 'social' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 5,
|
||||
title: '亚太产业协同与专线联络',
|
||||
content: '覆盖日本、韩国、中国与东盟制造基地,支持中英日三语无缝商谈。',
|
||||
linkUrl: 'https://linkedin.com/in/kenji-tanaka-logistics',
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
@@ -539,7 +622,7 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
title: '亚太区域核心零部件保税中转枢纽项目',
|
||||
client: 'Global AutoTech Consortium',
|
||||
duration: '2024.11 - 2025.07',
|
||||
metrics: '仓储周转效率提高 42%,破损率控制在 0.001% 以下',
|
||||
metrics: '仓储周转效率提高 42%,破损率控制在 0.001% 以下,准时率 99.9%',
|
||||
coverImage: 'https://images.unsplash.com/photo-1578575437130-527eed3abbec.jpg',
|
||||
description: '成功构建日韩到东盟的跨国零备件绿色走廊,达成准时化(JIT)精益交付。',
|
||||
},
|
||||
@@ -549,13 +632,145 @@ export const MOCK_CARDS_DATA: MockCardBundle[] = [
|
||||
id: 4005,
|
||||
cardId: 1005,
|
||||
title: '全球地缘与贸易新常态下的弹性供应链构建',
|
||||
summary: '面对跨国运价波动与港口堵塞风险,如何通过数字孪生提升全球业务韧性。',
|
||||
content: '跨国供应链的制胜关键已经从单一的价格竞争转向全链路的可视化与动态避险能力...',
|
||||
summary: '面对跨国运价波动与港口堵塞风险,如何通过数字孪生提升跨国供应链业务韧性。',
|
||||
content: '跨国供应链的制胜关键已经从单一的价格竞争转向全链路的可视化与动态避险能力。通过将干线海运与内陆多式联运数据打通,制造企业可实现精准到小时级的主机厂物料准点上线...',
|
||||
coverImage: 'https://images.unsplash.com/photo-1542744173-8e7e53415bb0.jpg',
|
||||
isPublished: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 名片 6: 瑞士数字医疗与生物医药前沿 (Dr. Marcus Weber)
|
||||
// ==========================================
|
||||
{
|
||||
card: {
|
||||
id: 1006,
|
||||
tenantId: 1,
|
||||
slug: 'marcus-weber',
|
||||
name: 'Dr. Marcus Weber',
|
||||
title: 'Managing Director & Co-Founder',
|
||||
company: 'SwissBio Frontier AG',
|
||||
slogan: 'Accelerating Breakthrough Therapeutics with Precision Genomics & AI',
|
||||
avatarUrl: 'https://images.unsplash.com/photo-1622253692010-333f2da6031d.jpg',
|
||||
phone: '+41442113355',
|
||||
email: 'm.weber@swissbiofrontier.ch',
|
||||
whatsapp: '+41442113355',
|
||||
linkedin: 'https://linkedin.com/in/dr-marcus-weber-biotech',
|
||||
twitter: 'https://x.com/marcusweber_bio',
|
||||
timezone: 'Europe/Zurich',
|
||||
audioPronunciationUrl: 'https://cdn.omnicard.ai/audio/marcus-weber.mp3',
|
||||
status: 'published',
|
||||
themeCode: 'cobalt_blue',
|
||||
userId: 106,
|
||||
remark: '苏黎世联邦理工 (ETH Zurich) 特聘研究员,瑞士生物医药创新基金会理事',
|
||||
},
|
||||
bentoBlocks: [
|
||||
{
|
||||
id: 2028,
|
||||
cardId: 1006,
|
||||
type: 'hero' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 1,
|
||||
title: '计算生物学与抗体工程联合研发平台',
|
||||
content: '汇聚苏黎世与巴塞尔顶尖科学家,专注肿瘤免疫微环境与新一代双特异性抗体发现。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1532187863486-abf9dbad1b69.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2029,
|
||||
cardId: 1006,
|
||||
type: 'video' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 2 as BentoRowSpan,
|
||||
sortOrder: 2,
|
||||
title: '巴塞尔自动化基因筛选高通量实验室实况',
|
||||
content: '机械臂集群 7x24 小时全自动化合物微孔板点样与共聚焦显微成像全景解析。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1579154204601-01588f351e67.jpg',
|
||||
linkUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4',
|
||||
metaJson: {
|
||||
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4',
|
||||
poster: 'https://images.unsplash.com/photo-1579154204601-01588f351e67.jpg',
|
||||
duration: '02:05',
|
||||
aspectRatio: '16:9',
|
||||
title: 'SwissBio 自动化实验室高通量筛选演练',
|
||||
},
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2030,
|
||||
cardId: 1006,
|
||||
type: 'case' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 3,
|
||||
title: '跨国三期多中心临床数字试验',
|
||||
content: '受试者基因分型筛查周期提速 4.2 倍,数据质控合规率达 99.98%。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2031,
|
||||
cardId: 1006,
|
||||
type: 'article' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 4,
|
||||
title: 'EMA 与 FDA 新药双报监管策略分析',
|
||||
content: '探讨数字化真实世界证据 (RWE) 在欧美双边加速审评审批通道中的关键作用。',
|
||||
mediaUrl: 'https://images.unsplash.com/photo-1507668077129-56e32842fceb.jpg',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2032,
|
||||
cardId: 1006,
|
||||
type: 'social' as BentoBlockType,
|
||||
colSpan: 1 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 5,
|
||||
title: '学术与药企直通通道',
|
||||
content: '常驻苏黎世与巴塞尔生命科学园区,支持国际顶级期刊同行评审与学术交流。',
|
||||
linkUrl: 'https://linkedin.com/in/dr-marcus-weber-biotech',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2033,
|
||||
cardId: 1006,
|
||||
type: 'form' as BentoBlockType,
|
||||
colSpan: 2 as BentoColSpan,
|
||||
rowSpan: 1 as BentoRowSpan,
|
||||
sortOrder: 6,
|
||||
title: '创新药临床合作与专利授权洽谈',
|
||||
content: '面向全球创新生物制药企业开放靶点许可与联合早期研发,欢迎提交合作意向。',
|
||||
status: 1,
|
||||
},
|
||||
],
|
||||
cases: [
|
||||
{
|
||||
id: 3006,
|
||||
cardId: 1006,
|
||||
title: '瑞士前沿生物医学产业园多中心试验',
|
||||
client: 'Roche-Novartis 创新联合体',
|
||||
duration: '2024.06 - 2025.05',
|
||||
metrics: '受试者基因分型筛查提速 4.2 倍,数据质控合规率 99.98%,临床脱落率降低至 1.8%',
|
||||
coverImage: 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d.jpg',
|
||||
pdfUrl: 'https://cdn.omnicard.ai/cases/swissbio-clinical-report.pdf',
|
||||
description: '成功运用 AI 驱动的多组学分析系统,加速罕见突变肺癌患者的高精度靶向药物入组。',
|
||||
},
|
||||
],
|
||||
articles: [
|
||||
{
|
||||
id: 4006,
|
||||
cardId: 1006,
|
||||
title: '从靶点发现到临床申报:AI 驱动抗体偶联药物研发范式转移',
|
||||
summary: '深度剖析机器学习模型在抗体亲和力成熟、连接子稳定性与毒素载荷释放动力学预测中的实证突破。',
|
||||
content: '传统药物发现往往经历 5 到 10 年的高成本试错。随着生成式大模型在蛋白质三维构象预测与结合能模拟方面的成熟,我们能够将先导化合物的筛选效率提升数个量级,从根本上重塑创新药研发的风险收益曲线...',
|
||||
coverImage: 'https://images.unsplash.com/photo-1507668077129-56e32842fceb.jpg',
|
||||
isPublished: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CardInfoEntity } from '../entity/info';
|
||||
import { CardPortalService } from './portal';
|
||||
import { TeamQuotaService } from '../../team/service/quota';
|
||||
import { getTenantId } from '../../../comm/context/tenant';
|
||||
import { seedCardsToDatabase } from '../mock/seed-cards';
|
||||
|
||||
/**
|
||||
* 名片增删改载荷(Cool CRUD 传入对象、主键或主键列表)
|
||||
@@ -98,4 +99,15 @@ export class CardInfoService extends BaseService {
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键播种/同步 6 套全球主流顶级商业赛道高保真名片数据至数据库
|
||||
*/
|
||||
async seedMockCards(): Promise<number> {
|
||||
const dataSource = this.cardInfoEntity.manager.connection;
|
||||
const count = await seedCardsToDatabase(dataSource);
|
||||
CardPortalService.clearAllCache();
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -388,6 +388,13 @@ export class CardSeoService extends BaseService {
|
||||
const safeJobTitle = escapeHtml(card.title);
|
||||
const safeCompany = escapeHtml(card.company);
|
||||
const safeSlogan = escapeHtml(card.slogan);
|
||||
const safeTimezone = escapeHtml(card.timezone);
|
||||
const safePhone = escapeHtml(card.phone);
|
||||
const safeEmail = escapeHtml(card.email);
|
||||
|
||||
const bentoBlocks = Array.isArray((card as any).bentoBlocks) ? (card as any).bentoBlocks : [];
|
||||
const cases = Array.isArray((card as any).cases) ? (card as any).cases : [];
|
||||
const articles = Array.isArray((card as any).articles) ? (card as any).articles : [];
|
||||
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
@@ -408,11 +415,154 @@ export class CardSeoService extends BaseService {
|
||||
|
||||
const isRtl = this.isRtlLocale(card.suggestedLocale);
|
||||
|
||||
// 渲染 Bento 块
|
||||
const renderedBlocks = bentoBlocks
|
||||
.map((block: any) => {
|
||||
const colClass = block.colSpan === 2 ? 'col-span-2' : 'col-span-1';
|
||||
const rowClass = block.rowSpan === 2 ? 'row-span-2' : 'row-span-1';
|
||||
const title = escapeHtml(block.title);
|
||||
const content = escapeHtml(block.content);
|
||||
const mediaUrl = escapeHtml(block.mediaUrl);
|
||||
const linkUrl = escapeHtml(block.linkUrl);
|
||||
|
||||
if (block.type === 'video') {
|
||||
let videoUrl = block.linkUrl || '';
|
||||
let duration = '01:30';
|
||||
if (block.metaJson) {
|
||||
const meta = typeof block.metaJson === 'string' ? JSON.parse(block.metaJson) : block.metaJson;
|
||||
videoUrl = meta.videoUrl || videoUrl;
|
||||
duration = meta.duration || duration;
|
||||
}
|
||||
const safeVideoUrl = escapeHtml(videoUrl);
|
||||
return `
|
||||
<div class="bento-card bento-video ${colClass} ${rowClass}">
|
||||
<div class="video-container">
|
||||
${safeVideoUrl ? `<video class="video-player" controls preload="metadata" poster="${mediaUrl}">
|
||||
<source src="${safeVideoUrl}" type="video/mp4" />
|
||||
您的浏览器不支持 HTML5 视频播放。
|
||||
</video>` : `<img class="bento-media" src="${mediaUrl}" alt="${title}" />`}
|
||||
<span class="video-duration-pill">${duration}</span>
|
||||
</div>
|
||||
<div class="bento-body">
|
||||
<div class="bento-tag">Featured Video</div>
|
||||
<h3 class="bento-title">${title}</h3>
|
||||
<p class="bento-desc">${content}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (block.type === 'case') {
|
||||
return `
|
||||
<div class="bento-card bento-case ${colClass} ${rowClass}">
|
||||
${mediaUrl ? `<img class="bento-media" src="${mediaUrl}" alt="${title}" />` : ''}
|
||||
<div class="bento-body">
|
||||
<div class="bento-tag tag-success">Success Case</div>
|
||||
<h3 class="bento-title">${title}</h3>
|
||||
<p class="bento-desc">${content}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (block.type === 'article') {
|
||||
return `
|
||||
<div class="bento-card bento-article ${colClass} ${rowClass}">
|
||||
${mediaUrl ? `<img class="bento-media" src="${mediaUrl}" alt="${title}" />` : ''}
|
||||
<div class="bento-body">
|
||||
<div class="bento-tag tag-purple">Insight</div>
|
||||
<h3 class="bento-title">${title}</h3>
|
||||
<p class="bento-desc">${content}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (block.type === 'social') {
|
||||
return `
|
||||
<div class="bento-card bento-social ${colClass} ${rowClass}">
|
||||
<div class="bento-body">
|
||||
<div class="bento-tag">Direct Connect</div>
|
||||
<h3 class="bento-title">${title}</h3>
|
||||
<p class="bento-desc">${content}</p>
|
||||
${linkUrl ? `<a class="bento-link-btn" href="${linkUrl}" target="_blank" rel="noopener">Connect Now ↗</a>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (block.type === 'form') {
|
||||
return `
|
||||
<div class="bento-card bento-form ${colClass} ${rowClass}">
|
||||
<div class="bento-body">
|
||||
<div class="bento-tag tag-primary">Inquiry Channel</div>
|
||||
<h3 class="bento-title">${title}</h3>
|
||||
<p class="bento-desc">${content}</p>
|
||||
<div class="quick-form-mock">
|
||||
<input class="mock-input" type="text" placeholder="Your Name or Company" readonly />
|
||||
<button class="mock-submit-btn" type="button">Book Consultation</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 默认 hero 块
|
||||
return `
|
||||
<div class="bento-card bento-hero ${colClass} ${rowClass}">
|
||||
${mediaUrl ? `<img class="bento-media" src="${mediaUrl}" alt="${title}" />` : ''}
|
||||
<div class="bento-body">
|
||||
<div class="bento-tag tag-blue">About</div>
|
||||
<h3 class="bento-title">${title}</h3>
|
||||
<p class="bento-desc">${content}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
// 渲染关联案例区
|
||||
const renderedCases = cases
|
||||
.map((c: any) => {
|
||||
const cTitle = escapeHtml(c.title);
|
||||
const cClient = escapeHtml(c.client);
|
||||
const cDuration = escapeHtml(c.duration);
|
||||
const cMetrics = escapeHtml(c.metrics);
|
||||
const cCover = escapeHtml(c.coverImage);
|
||||
const cDesc = escapeHtml(c.description);
|
||||
return `
|
||||
<div class="case-card">
|
||||
${cCover ? `<img class="case-cover" src="${cCover}" alt="${cTitle}" />` : ''}
|
||||
<div class="case-content">
|
||||
<div class="case-client-line">
|
||||
<span class="case-client">${cClient}</span>
|
||||
${cDuration ? `<span class="case-duration">${cDuration}</span>` : ''}
|
||||
</div>
|
||||
<h4 class="case-title">${cTitle}</h4>
|
||||
${cMetrics ? `<div class="case-metrics-badge">${cMetrics}</div>` : ''}
|
||||
<p class="case-desc">${cDesc}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
// 渲染深度文章区
|
||||
const renderedArticles = articles
|
||||
.map((a: any) => {
|
||||
const aTitle = escapeHtml(a.title);
|
||||
const aSummary = escapeHtml(a.summary);
|
||||
const aCover = escapeHtml(a.coverImage);
|
||||
return `
|
||||
<div class="article-card">
|
||||
${aCover ? `<img class="article-cover" src="${aCover}" alt="${aTitle}" />` : ''}
|
||||
<div class="article-content">
|
||||
<div class="article-tag">Whitepaper</div>
|
||||
<h4 class="article-title">${aTitle}</h4>
|
||||
<p class="article-summary">${aSummary}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="${escapeHtml(lang)}"${isRtl ? ' dir="rtl"' : ''}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>${safeTitle}</title>
|
||||
<meta name="description" content="${safeDesc}" />
|
||||
<link rel="canonical" href="${safeUrl}" />
|
||||
@@ -436,107 +586,400 @@ export class CardSeoService extends BaseService {
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary: #0B5CFF;
|
||||
--bg: #FFFFFF;
|
||||
--surface: #F7F8FA;
|
||||
--border: #E3E7ED;
|
||||
--primary: #0088FF;
|
||||
--primary-gradient: linear-gradient(135deg, #00A3FF 0%, #0B63E5 100%);
|
||||
--aurora: linear-gradient(135deg, #00D2FF 0%, #0088FF 50%, #6366F1 100%);
|
||||
--bg: #F5F8FC;
|
||||
--surface: #FFFFFF;
|
||||
--border: #EBF1F7;
|
||||
--text: #101828;
|
||||
--text-2: #667085;
|
||||
--text-2: #64748B;
|
||||
--success: #12B76A;
|
||||
--shadow-card: 0 8px 30px rgba(0, 0, 0, 0.04), 0 2px 8px rgba(11, 99, 229, 0.03);
|
||||
--shadow-cta: 0 8px 20px rgba(0, 136, 255, 0.22);
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 24px 16px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background-color: var(--surface);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
.card-preview {
|
||||
background: var(--bg);
|
||||
.aurora-glow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 220px;
|
||||
background: var(--aurora);
|
||||
opacity: 0.08;
|
||||
filter: blur(40px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.portal-shell {
|
||||
position: relative;
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
/* Hero Card */
|
||||
.hero-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
padding: 32px 24px;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
||||
border-radius: 24px;
|
||||
padding: 28px 24px 24px;
|
||||
box-shadow: var(--shadow-card);
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.avatar {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
.hero-avatar-wrap {
|
||||
position: relative;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
.hero-avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid var(--border);
|
||||
margin: 0 auto 16px;
|
||||
display: block;
|
||||
box-shadow: 0 4px 14px rgba(0, 136, 255, 0.16);
|
||||
border: 3px solid #FFFFFF;
|
||||
}
|
||||
.name {
|
||||
.hero-name {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 4px;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.title {
|
||||
.hero-title {
|
||||
font-size: 15px;
|
||||
color: var(--text-2);
|
||||
margin: 0 0 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.company {
|
||||
.hero-company {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
margin: 0 0 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.slogan {
|
||||
font-size: 14px;
|
||||
.hero-slogan {
|
||||
font-size: 13px;
|
||||
color: var(--text-2);
|
||||
line-height: 1.5;
|
||||
margin: 0 0 24px;
|
||||
font-style: italic;
|
||||
line-height: 1.5;
|
||||
padding: 0 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
.hero-meta-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.meta-pill {
|
||||
font-size: 12px;
|
||||
color: var(--text-2);
|
||||
background: #F1F5F9;
|
||||
padding: 4px 12px;
|
||||
border-radius: 9999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.primary-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
padding: 0 24px;
|
||||
background-color: var(--primary);
|
||||
background: var(--primary-gradient);
|
||||
color: #FFFFFF;
|
||||
text-decoration: none;
|
||||
border-radius: 12px;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
transition: opacity 150ms ease;
|
||||
font-size: 14px;
|
||||
box-shadow: var(--shadow-cta);
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
.cta-button:hover {
|
||||
opacity: 0.92;
|
||||
.secondary-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
background: #F1F5F9;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Bento Grid */
|
||||
.bento-section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin: 24px 4px 12px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.bento-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
.col-span-1 { grid-column: span 1; }
|
||||
.col-span-2 { grid-column: span 2; }
|
||||
.row-span-1 { grid-row: span 1; }
|
||||
.row-span-2 { grid-row: span 2; }
|
||||
|
||||
.bento-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
.bento-media {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.video-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: #000;
|
||||
}
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.video-duration-pill {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #FFF;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bento-body {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
.bento-tag {
|
||||
align-self: flex-start;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
background: #F1F5F9;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.tag-success { background: #ECFDF5; color: var(--success); }
|
||||
.tag-blue { background: #EBF5FF; color: var(--primary); }
|
||||
.tag-purple { background: #F3E8FF; color: #7C3AED; }
|
||||
.tag-primary { background: #EBF5FF; color: var(--primary); }
|
||||
.bento-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.bento-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-2);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
.bento-link-btn {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.quick-form-mock {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.mock-input {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
background: #F8FAFC;
|
||||
}
|
||||
.mock-submit-btn {
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
background: var(--primary);
|
||||
color: #FFF;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Cases & Articles */
|
||||
.cases-list, .articles-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.case-card, .article-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
.case-cover, .article-cover {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.case-content, .article-content {
|
||||
padding: 16px;
|
||||
}
|
||||
.case-client-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.case-client {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
.case-duration {
|
||||
font-size: 11px;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.case-title, .article-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.case-metrics-badge {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--success);
|
||||
background: #ECFDF5;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.case-desc, .article-summary {
|
||||
font-size: 13px;
|
||||
color: var(--text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.article-tag {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #7C3AED;
|
||||
background: #F3E8FF;
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.portal-footer {
|
||||
text-align: center;
|
||||
margin-top: 32px;
|
||||
color: var(--text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card-preview">
|
||||
${
|
||||
safeImage
|
||||
? `<img class="avatar" src="${safeImage}" alt="${safeName}" />`
|
||||
: ''
|
||||
}
|
||||
<h1 class="name">${safeName}</h1>
|
||||
${safeJobTitle ? `<div class="title">${safeJobTitle}</div>` : ''}
|
||||
${safeCompany ? `<div class="company">${safeCompany}</div>` : ''}
|
||||
${safeSlogan ? `<p class="slogan">${safeSlogan}</p>` : ''}
|
||||
${
|
||||
card.linkedin
|
||||
? `<div class="social"><a href="${escapeHtml(
|
||||
card.linkedin
|
||||
)}">LinkedIn</a></div>`
|
||||
: ''
|
||||
}
|
||||
<a class="cta-button" href="${safeUrl}">View Full Profile</a>
|
||||
<div class="aurora-glow"></div>
|
||||
<div class="portal-shell">
|
||||
<!-- Hero Showcase -->
|
||||
<main class="hero-card">
|
||||
<div class="hero-avatar-wrap">
|
||||
<img class="hero-avatar" src="${safeImage}" alt="${safeName}" />
|
||||
</div>
|
||||
<h1 class="hero-name">${safeName}</h1>
|
||||
${safeJobTitle ? `<div class="hero-title">${safeJobTitle}</div>` : ''}
|
||||
${safeCompany ? `<div class="hero-company">${safeCompany}</div>` : ''}
|
||||
${safeSlogan ? `<p class="hero-slogan">${safeSlogan}</p>` : ''}
|
||||
|
||||
<div class="hero-meta-row">
|
||||
${safeTimezone ? `<span class="meta-pill">🌐 ${safeTimezone}</span>` : ''}
|
||||
${safeEmail ? `<span class="meta-pill">✉ ${safeEmail}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<a class="primary-btn" href="${safeUrl}">Save Contact (vCard)</a>
|
||||
${safePhone ? `<a class="secondary-btn" href="tel:${safePhone}">Direct Call</a>` : ''}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Bento Grid Showcase -->
|
||||
${bentoBlocks.length > 0 ? `
|
||||
<h2 class="bento-section-title">Highlights & Insights</h2>
|
||||
<div class="bento-grid">
|
||||
${renderedBlocks}
|
||||
</div>` : ''}
|
||||
|
||||
<!-- Cases Showcase -->
|
||||
${cases.length > 0 ? `
|
||||
<h2 class="bento-section-title">Featured Business Cases</h2>
|
||||
<div class="cases-list">
|
||||
${renderedCases}
|
||||
</div>` : ''}
|
||||
|
||||
<!-- Articles Showcase -->
|
||||
${articles.length > 0 ? `
|
||||
<h2 class="bento-section-title">Thought Leadership & Articles</h2>
|
||||
<div class="articles-list">
|
||||
${renderedArticles}
|
||||
</div>` : ''}
|
||||
|
||||
<footer class="portal-footer">
|
||||
<p>OmniCard AI · Next-Gen Global Digital Business Card & Micro Portal</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { MOCK_CARDS_DATA, MockCardBundle } from '../../../src/modules/card/mock/seed-cards';
|
||||
import { CardPortalService } from '../../../src/modules/card/service/portal';
|
||||
import { CardSeoService } from '../../../src/modules/card/service/seo';
|
||||
import { CardInfoEntity } from '../../../src/modules/card/entity/info';
|
||||
import { CardBentoEntity } from '../../../src/modules/card/entity/bento';
|
||||
|
||||
describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试', () => {
|
||||
it('1. 应包含至少 5 组不同国际行业的高保真名片数据包', () => {
|
||||
it('1. 应包含至少 6 组不同国际顶级行业的高保真名片数据包且 Slug 唯一', () => {
|
||||
expect(Array.isArray(MOCK_CARDS_DATA)).toBe(true);
|
||||
expect(MOCK_CARDS_DATA.length).toBeGreaterThanOrEqual(5);
|
||||
expect(MOCK_CARDS_DATA.length).toBeGreaterThanOrEqual(6);
|
||||
|
||||
const slugs = MOCK_CARDS_DATA.map(item => item.card.slug);
|
||||
const uniqueSlugs = new Set(slugs);
|
||||
expect(uniqueSlugs.size).toBe(MOCK_CARDS_DATA.length); // Slug 必须唯一
|
||||
|
||||
// 必须包含涵盖 6 大赛道的关键人物 slug
|
||||
expect(slugs).toContain('alex-zheng'); // 消费电子硬件
|
||||
expect(slugs).toContain('elena-rostova'); // 云原生与大模型架构
|
||||
expect(slugs).toContain('tariq-al-mansoor'); // 中东智慧基建与绿色能源
|
||||
expect(slugs).toContain('chloe-dubois'); // 巴黎可持续时尚奢品
|
||||
expect(slugs).toContain('kenji-tanaka'); // 跨国汽车制造与智能物流
|
||||
expect(slugs).toContain('marcus-weber'); // 瑞士生物医药与数字健康
|
||||
});
|
||||
|
||||
it('2. 每张名片的基础信息必须完整且配有高分辨率头像与国际化联系方式', () => {
|
||||
@@ -30,14 +39,15 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
expect(card.phone).toMatch(/^\+[1-9]\d{1,14}$/); // E.164 规范
|
||||
expect(card.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
|
||||
expect(card.timezone).toBeTruthy();
|
||||
expect(card.audioPronunciationUrl).toMatch(/^https?:\/\/.+\.(mp3|wav|ogg|m4a)$/i);
|
||||
});
|
||||
});
|
||||
|
||||
it('3. 每张名片必须包含有效的视频 Bento 块,且配有可播放视频流与专属海报封面', () => {
|
||||
it('3. 每张名片必须包含丰富的 Bento Grid 块 (>= 5 个),包含视频与社交/留资互动块', () => {
|
||||
MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => {
|
||||
const { bentoBlocks } = bundle;
|
||||
expect(Array.isArray(bentoBlocks)).toBe(true);
|
||||
expect(bentoBlocks.length).toBeGreaterThan(0);
|
||||
expect(bentoBlocks.length).toBeGreaterThanOrEqual(5);
|
||||
|
||||
// 必须包含至少一个 video 类型的 Bento 块
|
||||
const videoBlocks = bentoBlocks.filter(b => b.type === 'video');
|
||||
@@ -45,9 +55,7 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
|
||||
videoBlocks.forEach(videoBlock => {
|
||||
expect(videoBlock.title).toBeTruthy();
|
||||
// mediaUrl 作为视频封面海报,必须是合法图片 URL
|
||||
expect(videoBlock.mediaUrl).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
// linkUrl 或 metaJson.videoUrl 必须是可播放的视频源 (mp4/m3u8/webm)
|
||||
let videoUrl = videoBlock.linkUrl;
|
||||
if (typeof videoBlock.metaJson === 'object' && videoBlock.metaJson !== null) {
|
||||
videoUrl = (videoBlock.metaJson as any).videoUrl || videoUrl;
|
||||
@@ -57,10 +65,14 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
}
|
||||
expect(videoUrl).toMatch(/^https?:\/\/.+\.(mp4|webm|m3u8)(\?.*)?$/i);
|
||||
});
|
||||
|
||||
// 必须包含 social 或 form 等交互块
|
||||
const interactiveBlocks = bentoBlocks.filter(b => b.type === 'social' || b.type === 'form');
|
||||
expect(interactiveBlocks.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('4. 每张名片必须包含图文案例与文章,且均配置了真实多媒体封面配图', () => {
|
||||
it('4. 每张名片必须包含图文案例与文章,且配置了真实多媒体封面配图与量化商业指标', () => {
|
||||
MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => {
|
||||
// 检查案例
|
||||
expect(Array.isArray(bundle.cases)).toBe(true);
|
||||
@@ -68,6 +80,8 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
bundle.cases.forEach(c => {
|
||||
expect(c.title).toBeTruthy();
|
||||
expect(c.client).toBeTruthy();
|
||||
expect(c.metrics).toBeTruthy(); // 必须包含量化商业成果
|
||||
expect(c.description).toBeTruthy();
|
||||
expect(c.coverImage).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
});
|
||||
|
||||
@@ -76,12 +90,14 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
expect(bundle.articles.length).toBeGreaterThanOrEqual(1);
|
||||
bundle.articles.forEach(a => {
|
||||
expect(a.title).toBeTruthy();
|
||||
expect(a.summary).toBeTruthy();
|
||||
expect(a.content).toBeTruthy();
|
||||
expect(a.coverImage).toMatch(/^https?:\/\/.+\.(png|jpe?g|webp)(\?.*)?$/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('5. 微官网服务 (CardPortalService) 能够无缝聚合各名片详情,正确渲染视频与图文块', async () => {
|
||||
it('5. 微官网服务 (CardPortalService) 能够无缝聚合各名片详情,正确渲染多媒体 Bento 块', async () => {
|
||||
const cardInfoRepo: any = {
|
||||
findOne: jest.fn().mockImplementation(async options => {
|
||||
const slug = options?.where?.slug;
|
||||
@@ -118,4 +134,59 @@ describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试
|
||||
expect(videoBlock?.mediaUrl).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('6. 服务端渲染微官网预览 (CardSeoService.renderCardHtml) 包含完整的 Bento Grid 结构与多媒体', async () => {
|
||||
const seoService = new CardSeoService();
|
||||
const firstBundle = MOCK_CARDS_DATA[0];
|
||||
|
||||
const portalDetail = {
|
||||
...firstBundle.card,
|
||||
bentoBlocks: firstBundle.bentoBlocks as any[],
|
||||
cases: firstBundle.cases,
|
||||
articles: firstBundle.articles,
|
||||
} as any;
|
||||
|
||||
const html = seoService.renderCardHtml(portalDetail);
|
||||
expect(html).toContain('<!DOCTYPE html>');
|
||||
expect(html).toContain(firstBundle.card.name);
|
||||
// 应包含 Bento Grid 容器类名
|
||||
expect(html).toContain('bento-grid');
|
||||
// 应包含 Hero 与 Video 相关卡片内容
|
||||
expect(html).toContain(firstBundle.bentoBlocks[0].title);
|
||||
expect(html).toContain(firstBundle.cases[0].title);
|
||||
});
|
||||
|
||||
it('7. 控制器公开路由 /mock-cards 能返回 6 套名片高保真预览清单', async () => {
|
||||
// 动态验证 OpenCardPortalController.mockCards
|
||||
const { OpenCardPortalController } = require('../../../src/modules/card/controller/open/portal');
|
||||
const controller = new OpenCardPortalController();
|
||||
controller.ok = jest.fn((data: any) => ({ code: 1000, data }));
|
||||
|
||||
const res = await controller.mockCards();
|
||||
expect(res.code).toBe(1000);
|
||||
expect(Array.isArray(res.data)).toBe(true);
|
||||
expect(res.data.length).toBe(6);
|
||||
expect(res.data[0].slug).toBe('alex-zheng');
|
||||
expect(res.data[5].slug).toBe('marcus-weber');
|
||||
expect(res.data[0].bentoBlocksCount).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
|
||||
it('8. seedCardsToDatabase 函数能够幂等同步数据到仓库', async () => {
|
||||
const { seedCardsToDatabase } = require('../../../src/modules/card/mock/seed-cards');
|
||||
const mockRepo = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
create: jest.fn((data: any) => ({ ...data, id: 1 })),
|
||||
save: jest.fn((data: any) => Promise.resolve({ ...data, id: 1 })),
|
||||
delete: jest.fn().mockResolvedValue({ affected: 1 }),
|
||||
};
|
||||
|
||||
const mockDataSource = {
|
||||
getRepository: jest.fn().mockReturnValue(mockRepo),
|
||||
};
|
||||
|
||||
const insertedCount = await seedCardsToDatabase(mockDataSource);
|
||||
expect(insertedCount).toBe(6);
|
||||
expect(mockRepo.save).toHaveBeenCalled();
|
||||
expect(mockRepo.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户