feat(cms): 品牌叙事与结构性文案全站 CMS 化并补齐信任证据阶段 0
- 品牌叙事内核(价值主张/定位/承诺/三支柱/语气)下沉 site-config,经 SiteConfigProvider 注入全站 - 新增 page-copy 内容模型承载结构性文案(章节标题/眉标/描述/CTA/空状态),覆盖首页+服务/方案/产品/案例/新闻列表页,「CMS 优先 + 硬编码兜底」不白屏 - 首页「首批客户共创计划」板块(earlyAccessTitle/CtaLabel/Status 驱动)替代单行标签,如实呈现共创/内测/授权公开三档状态 - 关于页资质区数据空时如实展示「建设中」空态 - 零编造:不虚构客户/数据/资质
This commit is contained in:
+5
-1
@@ -141,7 +141,8 @@ Novalon 的 6 个核心产品,互为互补关系,常以组合形式出现在
|
|||||||
| Hero 视觉方向修正 | 当前深色渐变风格偏离"水墨雅致"品牌定位,需转向浅色/宣纸色底、深色文字、品牌色点缀;通过 web-design-engineer skill 做原型验证后再落地 | ✅ 2026-06-07 确认 |
|
| Hero 视觉方向修正 | 当前深色渐变风格偏离"水墨雅致"品牌定位,需转向浅色/宣纸色底、深色文字、品牌色点缀;通过 web-design-engineer skill 做原型验证后再落地 | ✅ 2026-06-07 确认 |
|
||||||
| 导航体现 HSI 层级 | Products(Hub)视觉权重最高,Solutions(Spoke)次之,Services 最轻;Products 下拉菜单需展示套装区/专业产品区分区 | ✅ 2026-06-07 确认 |
|
| 导航体现 HSI 层级 | Products(Hub)视觉权重最高,Solutions(Spoke)次之,Services 最轻;Products 下拉菜单需展示套装区/专业产品区分区 | ✅ 2026-06-07 确认 |
|
||||||
| 方案页删除服务方式区域 | Solutions 列表页"服务方式"区域职责越界(属 Services 层),应删除;替换为方案→服务的轻量关联推荐链接 | ✅ 2026-06-07 确认 |
|
| 方案页删除服务方式区域 | Solutions 列表页"服务方式"区域职责越界(属 Services 层),应删除;替换为方案→服务的轻量关联推荐链接 | ✅ 2026-06-07 确认 |
|
||||||
| L3 信任层内容策略 | 初创阶段无真实案例/数据/资质,L3 暂不渲染;组件结构预留,数据就绪后启用。三档策略:有案例→正常展示;有意向→"正在服务中"标签;无数据→不显示 L3。当前全部标记为 TODO | ✅ 2026-06-07 确认(TODO) |
|
| L3 信任层内容策略 | 初创阶段无真实案例/数据/资质,L3 暂不渲染;组件结构预留,数据就绪后启用。三档策略:有案例→正常展示;有意向→"正在服务中"标签;无数据→不显示 L3。**阶段 0(2026-08-20)已落地**:信任策略从「结果证据」转向「可验证的过程+能力+治理证据」——首页「共创计划」板块(共创进行中/产品内测中/成果授权公开三档如实状态 + 招募 CTA)、关于页资质区如实「建设中」空态 | ✅ 2026-06-07 确认 / 阶段 0 完成 2026-08-20 |
|
||||||
|
| 首页共创计划板块 | 成立 <1 年无真实案例时,以主动板块替代被动标签:`page-copy` 字段 `earlyAccessTitle`/`earlyAccessCtaLabel`/`earlyAccessStatus` 驱动首页「首批客户共创计划」板块,如实呈现共创/内测/授权公开状态并引导转化(成为共创客户 → /contact)。零编造:不虚构客户/数据/资质 | ✅ 2026-08-20 确认 |
|
||||||
| 组件版本统一 | 独立产品迁移后删除 `detail/`(V1),将 `detail-v2/` 重命名为 `detail/`,组件文件名去掉版本后缀(如 `DetailHeroV3` → `DetailHero`) | ✅ 2026-06-07 确认(待 Phase 0 执行) |
|
| 组件版本统一 | 独立产品迁移后删除 `detail/`(V1),将 `detail-v2/` 重命名为 `detail/`,组件文件名去掉版本后缀(如 `DetailHeroV3` → `DetailHero`) | ✅ 2026-06-07 确认(待 Phase 0 执行) |
|
||||||
| 设计定位 | 纯咨询专业风(Accenture + Bain + Porsche 三位一体),水墨元素已移除 | ✅ 2026-07-10 更新(ADR-0006) |
|
| 设计定位 | 纯咨询专业风(Accenture + Bain + Porsche 三位一体),水墨元素已移除 | ✅ 2026-07-10 更新(ADR-0006) |
|
||||||
| 设计 DNA 框架 | 三维度模型:Design System + Design Style + Visual Effects,基于 Accenture+Bain+Porsche 整合 | ✅ 2026-06-28 确认 |
|
| 设计 DNA 框架 | 三维度模型:Design System + Design Style + Visual Effects,基于 Accenture+Bain+Porsche 整合 | ✅ 2026-06-28 确认 |
|
||||||
@@ -162,6 +163,9 @@ Novalon 的 6 个核心产品,互为互补关系,常以组合形式出现在
|
|||||||
| 品牌故事页品牌名 | 使用与 footer Logo 完全相同的青柳隶书 SVG path(`BrandCalligraphyName`),不加载 4.4MB 的 AoyagiReisho 字体文件,兼顾品牌一致性与性能 | ✅ 2026-08-18 确认 |
|
| 品牌故事页品牌名 | 使用与 footer Logo 完全相同的青柳隶书 SVG path(`BrandCalligraphyName`),不加载 4.4MB 的 AoyagiReisho 字体文件,兼顾品牌一致性与性能 | ✅ 2026-08-18 确认 |
|
||||||
| 风格主词 | Swiss Modernism 2.0(骨架)+ Bento Box Grid(信息组织)+ Hero-Centric & Conversion-Optimized(首屏转化)+ Trust & Authority & Social Proof(信任层)+ Motion-Driven(差异化记忆点)+ Accessible & Ethical(底线) | ✅ 2026-08-18 确认 |
|
| 风格主词 | Swiss Modernism 2.0(骨架)+ Bento Box Grid(信息组织)+ Hero-Centric & Conversion-Optimized(首屏转化)+ Trust & Authority & Social Proof(信任层)+ Motion-Driven(差异化记忆点)+ Accessible & Ethical(底线) | ✅ 2026-08-18 确认 |
|
||||||
| 官网产品模块定位 | **IHG/字节式品牌矩阵**:官网品牌宣传为主,产品矩阵页保留为聚合入口(每产品一句话定位+指标证据+外链独立站);成熟产品 `externalUrl` 外链独立站(NovaVis → novavis.p.novalon.cn 已实践),未成熟产品官网详情页占位,独立站上线后切换外链。参照案例(已核实):IHG 品牌组合页 / 字节跳动 bytedance.com/products / 腾讯 tencent.com/business.html(模式 B:品牌+品牌矩阵聚合页);对照金蝶/用友(模式 C:产品为主,官网承载详情与转化) | ✅ 2026-08-19 确认 |
|
| 官网产品模块定位 | **IHG/字节式品牌矩阵**:官网品牌宣传为主,产品矩阵页保留为聚合入口(每产品一句话定位+指标证据+外链独立站);成熟产品 `externalUrl` 外链独立站(NovaVis → novavis.p.novalon.cn 已实践),未成熟产品官网详情页占位,独立站上线后切换外链。参照案例(已核实):IHG 品牌组合页 / 字节跳动 bytedance.com/products / 腾讯 tencent.com/business.html(模式 B:品牌+品牌矩阵聚合页);对照金蝶/用友(模式 C:产品为主,官网承载详情与转化) | ✅ 2026-08-19 确认 |
|
||||||
|
| 品牌主口号 | **智连未来 · 成长伙伴 —— 您的数字化转型同行者**(整句全站统一采用)。品牌叙事内核(定位声明 / 承诺 / L0 价值主张 / L1 三支柱 / 语气语调)见 `docs/brand-narrative-core.md`,作为全站文案统一唯一锚点 | ✅ 2026-08-19 确认 |
|
||||||
|
| 品牌叙事 CMS 化 | 品牌叙事内核全部下沉到 CMS `site-config` 模型(`slogan` / `valueProposition` / `positioningStatement` / `brandPromise` / `toneOfVoice` / `pillars`),后台可编辑;代码侧 `COMPANY_INFO` 与 `BRAND_NARRATIVE`(`src/lib/constants/company.ts`)作为未配置 CMS 时的兜底唯一真源,`layout.tsx` 读取后经 `SiteConfigProvider` 注入全站 | ✅ 2026-08-19 确认 |
|
||||||
|
| 结构性文案 CMS 化 | 新增 `page-copy` 内容模型承载各业务页面章节标题/眉标/描述/CTA/空状态,覆盖首页 + 服务/方案/产品/案例/新闻列表页,seed 写入 6 条(home/services/solutions/products/cases/news)。实现为「CMS 优先 + 硬编码兜底」:CMS 未配置时回退 `TRUST_SIGNALS`/`EARLY_ACCESS`/`NARRATIVE_ACTS` 等常量,保证任何情况不白屏。数据层新增 `getPageCopy(pageCode)` / `getHomePageCopy()`,各 `page.tsx` 读取后以 `pageCopy` props 传入组件。详情页小节标题、页面内嵌业务常量(服务流程/合作模式/产品组合)、Header/Footer 等 UI 外壳按 UI 职责保留代码。数据实体(服务/产品/方案/案例/新闻/指标/Hero)仍由 `data-server` 从 Prisma 读取 | ✅ 2026-08-19 确认 |
|
||||||
|
|
||||||
## 页面类型与四层映射
|
## 页面类型与四层映射
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,10 @@
|
|||||||
- [x] **方法论 CMS 化(Phase 4 信任证据·方法论骨架)**:新增 `methodology` 内容模型 + `/methodology` 页面骨架(Hero / 四阶段 / 空态 / CTA),首页 CTA「了解我们的方法论」指向 /methodology,sitemap 收录,视觉基线已生成
|
- [x] **方法论 CMS 化(Phase 4 信任证据·方法论骨架)**:新增 `methodology` 内容模型 + `/methodology` 页面骨架(Hero / 四阶段 / 空态 / CTA),首页 CTA「了解我们的方法论」指向 /methodology,sitemap 收录,视觉基线已生成
|
||||||
- [x] **产品外链机制补全(对齐 Google / 字节 / 腾讯对标)**:product 模型补 externalUrl 字段 + 企业套装详情页 redirect(复用 standalone 模板)+ 矩阵卡「外部站点」徽标;清理 3 个无引用 AoyagiReisho 字体文件(性能优化)
|
- [x] **产品外链机制补全(对齐 Google / 字节 / 腾讯对标)**:product 模型补 externalUrl 字段 + 企业套装详情页 redirect(复用 standalone 模板)+ 矩阵卡「外部站点」徽标;清理 3 个无引用 AoyagiReisho 字体文件(性能优化)
|
||||||
- [x] **首页 Hero 口号恢复(2026-08-19)**:应用户偏好,H1 恢复为「让每一家企业都拥有数据驱动的决策能力」(seed hero-banner + home 兜底默认同步,db:seed 已生效),视觉基线三端更新
|
- [x] **首页 Hero 口号恢复(2026-08-19)**:应用户偏好,H1 恢复为「让每一家企业都拥有数据驱动的决策能力」(seed hero-banner + home 兜底默认同步,db:seed 已生效),视觉基线三端更新
|
||||||
- [ ] **信任证据补齐(Phase 4 内容待办)**:首批客户共创案例与可量化成果——需提供真实素材后实施(零编造原则)
|
- [x] **结构性文案 CMS 化(全站完成,2026-08-19)**:`page-copy` 内容模型承载全部营销页面的结构性文案(章节标题/眉标/描述/CTA/空状态),覆盖:首页(信任区/叙事情节/服务案例区标题/CTA)+ 服务页(Hero/区块标题/CTA/空状态)+ 方案页(Hero/区块/CTA)+ 产品页(Hero/双体系区块/CTA)+ 案例页(Hero/筛选区)+ 新闻页(Hero/空状态)。实现为「CMS 优先 + 硬编码兜底」,CMS 未配置时回退 `TRUST_SIGNALS`/`EARLY_ACCESS`/`NARRATIVE_ACTS` 等常量,任何情况不白屏。数据流:`page.tsx → getPageCopy(pageCode) → pageCopy props`;seed 写入 6 条 page-copy(home/services/solutions/products/cases/news),RBAC 权限已含 `page-copy`,本地 db:seed 已生效(条目 33→38)。详情页小节标题、页面内嵌业务常量(服务流程/合作模式/产品组合)、Header/Footer UI 外壳按《AGENTS.md·UI 职责》保留代码。通过 type-check / lint(0 errors) / 单元测试 128 suites / 1623 tests / 运行态冒烟(6 页面 HTTP 200 + CMS 文案渲染确认)
|
||||||
|
- [x] **视觉回归基线更新(CMS 化后,2026-08-19/20)**:CMS 化改造涉及的产品页 CTA/双体系区块品牌红位置、服务页区块标题移动端换行等细微视觉调整已通过 `--update-snapshots` 更新三端(chromium-desktop/tablet/mobile)基线;三端普通视觉验证 66 passed、功能 E2E(@smoke|@critical)45 passed / 9 skipped 全部通过(此前因系统高负载受阻,用户清理 Chrome 进程后完成)
|
||||||
|
- [x] **信任证据补齐·阶段 0(成立 <1 年策略,2026-08-20)**:遵循零编造原则,将信任策略从「结果证据」转向「可验证的过程 + 能力 + 治理证据」。落地:① 首页信任区「首批客户共创中」单行标签升级为「首批客户共创计划」板块(标题/描述/三条如实状态:共创进行中、产品内测中、成果授权公开/招募 CTA,page-copy 字段 earlyAccessTitle/earlyAccessCtaLabel/earlyAccessStatus 驱动);② 关于页资质区由「数据空则隐藏」改为如实「资质建设中,将随业务开展陆续公示」空态。不虚构任何客户案例/数据/资质。通过 type-check / lint(0 errors) / 单元测试 128 suites / 1623 tests / 运行态冒烟(首页共创板块 + about 资质区渲染)/ 三端视觉基线更新 + 66 passed 验证
|
||||||
|
- [ ] **信任证据补齐·阶段 1(待内测/试点客户素材)**:D 类共创证据如实披露「共创中/内测中/已交付」三档状态 + 经授权客户(脱敏);阶段 2 在有交付成果后经授权公示可量化结果(零编造原则)
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# 品牌叙事内核(Brand Narrative Core)
|
||||||
|
|
||||||
|
> 本文档是全站文案(含 CMS seed)统一的唯一锚点。
|
||||||
|
> 所有页面、组件、CMS 内容的文案,都必须以本文档定义的**定位声明、信息层级、语气语调**为生长来源,改动前先回到本文件对齐。
|
||||||
|
|
||||||
|
- 版本:v1.0
|
||||||
|
- 确认日期:2026-08-19
|
||||||
|
- 状态:✅ 已确认
|
||||||
|
|
||||||
|
## 0. 落地实现(CMS 化)
|
||||||
|
|
||||||
|
本文所述的品牌叙事内核**已全部下沉到 CMS**,后台可编辑:
|
||||||
|
|
||||||
|
| 叙事内容 | CMS 模型字段(`site-config`,后台站点配置中编辑) | 代码兜底真源 |
|
||||||
|
|---------|----------------------------------------------|-------------|
|
||||||
|
| 主口号 slogan | `slogan` | `COMPANY_INFO.slogan` |
|
||||||
|
| L0 一句话价值主张 | `valueProposition` | `BRAND_NARRATIVE.valueProposition` |
|
||||||
|
| 品牌定位声明 | `positioningStatement` | `BRAND_NARRATIVE.positioningStatement` |
|
||||||
|
| 品牌承诺 | `brandPromise` | `BRAND_NARRATIVE.brandPromise` |
|
||||||
|
| 三支柱(L1) | `pillars`(数组) | `BRAND_NARRATIVE.pillars` |
|
||||||
|
| 语气语调(写作规范,不展示) | `toneOfVoice` | `BRAND_NARRATIVE.toneOfVoice` |
|
||||||
|
|
||||||
|
- **唯一真源**:`src/lib/constants/company.ts` 的 `COMPANY_INFO` 与 `BRAND_NARRATIVE`。
|
||||||
|
- **注入链路**:`prisma/seed.ts` 将上述常量写入 CMS → `src/app/layout.tsx` 读取 `site-config` → `SiteConfigProvider` 注入全站 → 首页 Hero / Footer 等触点消费(后台改值即全站生效,无需改代码)。
|
||||||
|
- **新增字段定义**:`prisma/seed.ts` 的 `site-config` 模型字段块,执行 `npm run db:seed` 生效。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 品牌定位声明(Positioning Statement)
|
||||||
|
|
||||||
|
**睿新致远(Novalon)是中国成长型企业的数字化转型同行者。**
|
||||||
|
|
||||||
|
对 **面临数字化升级但不具备大型 IT 成本承受力的成长型企业**,
|
||||||
|
我们提供 **从战略诊断到系统落地、再到持续陪跑的一站式服务与产品矩阵**,
|
||||||
|
凭借 **结果导向的方法论 + 自研可组合的产品 + 复合型工程师团队**,
|
||||||
|
让企业在可衡量的前提下,把数字化从「投入」变成「成长」。
|
||||||
|
|
||||||
|
**我们的角色**:不是技术供应商,而是与您同行的伙伴——
|
||||||
|
懂业务、说实话、陪到底,以您的业务改善作为我们交付成功与否的唯一标准。
|
||||||
|
|
||||||
|
**竞争性的差异化**:大型咨询公司「够专业但太贵、太远」;软件厂商「有产品但不管您的业务」。
|
||||||
|
我们在两者之间,提供「咨询的严谨 × 工程的落地 × 并肩的陪伴」。
|
||||||
|
|
||||||
|
> 品牌角色一句话:**我们不只是帮您上线系统,而是陪您把转型做成成长。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 品牌承诺(Brand Promise)
|
||||||
|
|
||||||
|
> 我们承诺:**每一份方案都源于对您业务的理解;每一次交付都以结果说话;每一段合作都以陪您走到成长为终点。**
|
||||||
|
|
||||||
|
三层兑现路径(对应服务方法论):
|
||||||
|
1. **懂** — 用诊断而非推销,先理解您的业务、痛点与约束。
|
||||||
|
2. **做** — 用结果导向的交付,让每一分投入可衡量、可验证。
|
||||||
|
3. **陪** — 不止交付项目,更持续陪跑,助您沉淀团队内生能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 主口号与结构拆解
|
||||||
|
|
||||||
|
**主口号(全站一致采用整句):**
|
||||||
|
|
||||||
|
> ## 智连未来 · 成长伙伴 —— 您的数字化转型同行者
|
||||||
|
|
||||||
|
口号是「品牌角色」的精炼,内含三个语义,叙事时分层使用,但**展示时保持整句**:
|
||||||
|
|
||||||
|
| 词 | 定义(在文案中如何解释它) |
|
||||||
|
|----|--------------------------|
|
||||||
|
| **智连未来** | 用智能技术与数据,连接您当前业务与下一阶段的增长目标——「连接的不是技术,而是您企业的未来」。 |
|
||||||
|
| **成长伙伴** | 我们以一荣俱荣的伙伴身份参与,您的成长即我们的成果。不是供应商,是伙伴。 |
|
||||||
|
| **数字化转型同行者** | 全链路的同路人:从战略、到落地、到陪跑,我们与您并肩而非站在对面。 |
|
||||||
|
|
||||||
|
**展示约束**:
|
||||||
|
- 作为品牌主口号,全站统一呈现为完整句「智连未来 · 成长伙伴 —— 您的数字化转型同行者」。
|
||||||
|
- 在 Hero 标签(badge)、Footer 品牌区、About 愿景区等品牌触点呈现。
|
||||||
|
- 单页内不重复罗列三短语三个定义,仅在「为什么是它」的叙事区块(About 愿景层)展开解释。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 信息层级(Message Map)
|
||||||
|
|
||||||
|
口号向下展开为一套**递进式信息层级**,供不同页面与场景取用。任何文案都应落在其中一层,避免自行发散。
|
||||||
|
|
||||||
|
### L0 一句话价值主张(Elevator Pitch)
|
||||||
|
> **让数据连接未来,让伙伴陪您成长——我们是您数字化转型的同行者。**
|
||||||
|
|
||||||
|
### L1 三支柱(核心支撑信息,对应产品 / 方案 / 服务)
|
||||||
|
1. **战略同行** → 从诊断到蓝图,先想清楚再动手(对应:咨询服务、方法论)。
|
||||||
|
2. **落地同行** → 用自研可组合产品与工程交付,让转型真正落地(对应:产品矩阵、行业方案)。
|
||||||
|
3. **陪跑同行** → 交付后仍持续陪跑,助您优化结果(对应:数据/AI 应用、持续运维、方法迭代)。
|
||||||
|
|
||||||
|
### L2 逐条证据链(每条支柱下的可验证支撑,符合「零编造」)
|
||||||
|
| 支柱 | 证据链(据此撰写具体文案) |
|
||||||
|
|------|------------------------|
|
||||||
|
| 战略同行 | 专业核心团队 / 结果导向方法论(诊断→规划→实施→优化)/ 2026 年成立后的首批客户共创 |
|
||||||
|
| 落地同行 | 6 款自研可组合产品 / 私有化部署 / 本地化服务(成都龙泉驿区)/ 复合型工程师团队 |
|
||||||
|
| 陪跑同行 | 数据与 AI 应用能力 / 持续运维 / 以业务改善为衡量标准 |
|
||||||
|
|
||||||
|
> 原则:**凡出现数字、资质、案例,必须可溯源;未取得真实成绩前,如实标注「共创中」而非虚构。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 语气与语调(Tone of Voice)
|
||||||
|
|
||||||
|
**基调一句话:严谨,但不说教;专业,但不冰冷;同行,但不越位。**
|
||||||
|
|
||||||
|
| 维度 | 我们这么做 | 我们不这么做 |
|
||||||
|
|------|-----------|-------------|
|
||||||
|
| 专业 | 一句话讲清价值主张,答案优先,数据先行 | 堆砌术语、用行话自我感觉良好 |
|
||||||
|
| 诚实 | 可衡量的才说,未成的如实「共创中」 | 虚构案例、夸大产能、贩卖焦虑 |
|
||||||
|
| 伙伴 | 用「我们 / 您」,站在同侧看问题 | 居高临下说教、把您当「客户」对立面 |
|
||||||
|
| 简洁 | 短句、有力的动词、克制的形容词 | 冗长排比、空洞的口号式修饰 |
|
||||||
|
|
||||||
|
**关键词汇偏好**:同行、陪伴、理解业务、结果导向、可衡量、落地、连接未来、共同成长。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 一致性护栏(落地后的自洽检查)
|
||||||
|
|
||||||
|
为保障全站 + CMS 文案自洽,执行时统一遵循以下规则:
|
||||||
|
|
||||||
|
1. **单一锚点**:所有品牌口号、公司简介(slogan/description)、Hero 主张,统一取自 `COMPANY_INFO`(代码唯一真源),CMS `site-config` seed 同步引用同一常量,禁止出现第二套口径。
|
||||||
|
2. **三层对位**:任何页面文案落位后,可映射到信息层级(L1 三支柱 / L2 证据链)之一;无法映射的文案视为冗余,删除或改写。
|
||||||
|
3. **一句话原则**:每页 Hero 只能有一个核心主张,不叠加两套口号。
|
||||||
|
4. **品牌红克制**:品牌红 #C41E3A 仅用于关键触点,面积 ≤10%,不用于段落正文。
|
||||||
|
5. **「同行者」语义闭环**:首页 Hero、About 愿景、Footer 品牌区三个主要触点必须使用同一句主口号,避免各自解释。
|
||||||
|
6. **零编造**:数字、资质、案例必须可溯源;未达成如实标注。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 待评审确认点
|
||||||
|
|
||||||
|
评审通过本文档后,将据此执行全站与 CMS 文案统一。请确认以下两点:
|
||||||
|
|
||||||
|
1. 本文「信息层级(L0/L1/L2)」与「语气语调」是否符合你对品牌的口径?
|
||||||
|
2. 主口号在三支柱中的落位(战略同行 / 落地同行 / 陪跑同行)是否准确?
|
||||||
+115
-1
@@ -11,7 +11,8 @@ import { PRODUCTS } from './seeds/products';
|
|||||||
import { STANDALONE_PRODUCTS } from './seeds/standalone-products';
|
import { STANDALONE_PRODUCTS } from './seeds/standalone-products';
|
||||||
import { SOLUTIONS } from './seeds/solutions';
|
import { SOLUTIONS } from './seeds/solutions';
|
||||||
import { STATS } from '../src/lib/constants/stats';
|
import { STATS } from '../src/lib/constants/stats';
|
||||||
import { COMPANY_INFO } from '../src/lib/constants/company';
|
import { COMPANY_INFO, TRUST_SIGNALS, EARLY_ACCESS } from '../src/lib/constants/company';
|
||||||
|
import { BRAND_NARRATIVE } from '../src/lib/constants/company';
|
||||||
import { METHODOLOGY } from '../src/lib/constants/methodology';
|
import { METHODOLOGY } from '../src/lib/constants/methodology';
|
||||||
import { NAVIGATION_V2, MEGA_DROPDOWN_DATA } from '../src/lib/constants/navigation';
|
import { NAVIGATION_V2, MEGA_DROPDOWN_DATA } from '../src/lib/constants/navigation';
|
||||||
|
|
||||||
@@ -233,6 +234,7 @@ async function main() {
|
|||||||
'standalone-product',
|
'standalone-product',
|
||||||
'media',
|
'media',
|
||||||
'methodology',
|
'methodology',
|
||||||
|
'page-copy',
|
||||||
];
|
];
|
||||||
|
|
||||||
// 为 super_admin 之外的角色创建权限
|
// 为 super_admin 之外的角色创建权限
|
||||||
@@ -471,6 +473,16 @@ async function main() {
|
|||||||
{ name: 'displayName', type: 'text', label: '展示名称', required: true },
|
{ name: 'displayName', type: 'text', label: '展示名称', required: true },
|
||||||
{ name: 'slogan', type: 'text', label: '品牌标语', required: true },
|
{ name: 'slogan', type: 'text', label: '品牌标语', required: true },
|
||||||
{ name: 'description', type: 'textarea', label: '公司简介', required: true },
|
{ name: 'description', type: 'textarea', label: '公司简介', required: true },
|
||||||
|
{ name: 'valueProposition', type: 'textarea', label: '一句话价值主张(L0)', description: '品牌叙事核心,用于首页 Hero / 品牌触点' },
|
||||||
|
{ name: 'positioningStatement', type: 'textarea', label: '品牌定位声明', description: '品牌叙事核心 — 为谁、解决什么、凭什么' },
|
||||||
|
{ name: 'brandPromise', type: 'textarea', label: '品牌承诺', description: '品牌叙事核心 — 我们承诺什么' },
|
||||||
|
{ name: 'toneOfVoice', type: 'textarea', label: '语气语调(写作规范)', description: '内部文案写作参考,不在页面直接展示' },
|
||||||
|
{ name: 'pillars', type: 'array', label: '信息层级三支柱(L1)', description: '战略同行 / 落地同行 / 陪跑同行,对应产品、方案、服务', fields: [
|
||||||
|
{ name: 'key', type: 'text', label: '标识(如 strategy)' },
|
||||||
|
{ name: 'title', type: 'text', label: '标题' },
|
||||||
|
{ name: 'tagline', type: 'text', label: '一句话副标' },
|
||||||
|
{ name: 'description', type: 'textarea', label: '描述' },
|
||||||
|
]},
|
||||||
{ name: 'founded', type: 'text', label: '成立年份' },
|
{ name: 'founded', type: 'text', label: '成立年份' },
|
||||||
{ name: 'location', type: 'text', label: '所在地' },
|
{ name: 'location', type: 'text', label: '所在地' },
|
||||||
{ name: 'email', type: 'text', label: '联系邮箱', required: true },
|
{ name: 'email', type: 'text', label: '联系邮箱', required: true },
|
||||||
@@ -885,6 +897,11 @@ async function main() {
|
|||||||
displayName: COMPANY_INFO.displayName,
|
displayName: COMPANY_INFO.displayName,
|
||||||
slogan: COMPANY_INFO.slogan,
|
slogan: COMPANY_INFO.slogan,
|
||||||
description: COMPANY_INFO.description,
|
description: COMPANY_INFO.description,
|
||||||
|
valueProposition: BRAND_NARRATIVE.valueProposition,
|
||||||
|
positioningStatement: BRAND_NARRATIVE.positioningStatement,
|
||||||
|
brandPromise: BRAND_NARRATIVE.brandPromise,
|
||||||
|
toneOfVoice: BRAND_NARRATIVE.toneOfVoice,
|
||||||
|
pillars: BRAND_NARRATIVE.pillars,
|
||||||
founded: COMPANY_INFO.founded,
|
founded: COMPANY_INFO.founded,
|
||||||
location: COMPANY_INFO.location,
|
location: COMPANY_INFO.location,
|
||||||
email: COMPANY_INFO.email,
|
email: COMPANY_INFO.email,
|
||||||
@@ -906,6 +923,103 @@ async function main() {
|
|||||||
]);
|
]);
|
||||||
console.log(' ✅ 导航菜单');
|
console.log(' ✅ 导航菜单');
|
||||||
|
|
||||||
|
console.log('📝 导入首页文案配置...');
|
||||||
|
await seedItems('page-copy', [
|
||||||
|
{
|
||||||
|
slug: 'home',
|
||||||
|
title: '首页文案配置',
|
||||||
|
trustEyebrow: '值得信赖',
|
||||||
|
trustTitle: '以可衡量的成果\n赢得长期信任',
|
||||||
|
trustSignals: TRUST_SIGNALS.map((s) => ({ value: s.value, label: s.label, desc: s.desc, source: s.source })),
|
||||||
|
earlyAccessLabel: EARLY_ACCESS.label,
|
||||||
|
earlyAccessDesc: EARLY_ACCESS.desc,
|
||||||
|
earlyAccessTitle: '首批客户共创计划',
|
||||||
|
earlyAccessCtaLabel: '成为共创客户',
|
||||||
|
earlyAccessStatus: [
|
||||||
|
{ label: '共创进行中', desc: '我们正与首批客户共同打磨产品与方案,以真实运营数据验证业务改善。' },
|
||||||
|
{ label: '产品内测中', desc: '自研产品(如睿视 NovaVis)处于内测阶段,成熟后正式发布。' },
|
||||||
|
{ label: '成果授权公开', desc: '案例与可量化成果在获得客户授权后公示——经得起衡量。' },
|
||||||
|
],
|
||||||
|
narrativeTitle: '从问题到结果,\n一条可量化的路径',
|
||||||
|
narrativeActs: [
|
||||||
|
{ number: '01', title: '诊断现状', desc: '从业务目标出发,梳理流程断点、数据孤岛与瓶颈环节,形成可量化的诊断结论。', href: '/services/consulting', cta: '了解诊断方法' },
|
||||||
|
{ number: '02', title: '设计路径', desc: '基于诊断结论设计系统架构与实施路线图,明确每一阶段的交付物与衡量指标。', href: '/services/solutions', cta: '查看方案设计' },
|
||||||
|
{ number: '03', title: '交付成果', desc: '以自研产品组合与专业实施团队完成交付,用真实运营数据验证业务改善,持续迭代。', href: '/products', cta: '浏览产品矩阵' },
|
||||||
|
],
|
||||||
|
servicesTitle: '从战略到落地,\n可量化的一站式服务',
|
||||||
|
casesEyebrow: '共创进行时',
|
||||||
|
casesTitle: '每一个项目,\n都以业务改善为衡量标准',
|
||||||
|
casesDescription: '我们正与首批客户共同打磨产品与方案,以真实运营数据验证业务改善。公开案例将在验证后发布——合作成果,经得起衡量。',
|
||||||
|
ctaTitleLine1: '准备好开始了吗?',
|
||||||
|
ctaTitleLine2: '让数据真正转化为业务增长',
|
||||||
|
ctaLabel: '预约咨询',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
console.log(' ✅ 首页文案配置');
|
||||||
|
|
||||||
|
console.log('📝 导入列表页文案配置...');
|
||||||
|
await seedItems('page-copy', [
|
||||||
|
{
|
||||||
|
slug: 'services',
|
||||||
|
title: '服务页文案配置',
|
||||||
|
servicesHeroTitle1: '从规划到运维',
|
||||||
|
servicesHeroTitle2: '全程陪伴',
|
||||||
|
servicesHeroDescription: '不只是技术供应商,更是您数字化转型的同行者。\n从第一次沟通到系统稳定运行,我们始终在您身边。',
|
||||||
|
servicesCtaPrimary: '聊聊您的需求',
|
||||||
|
servicesCtaSecondary: '查看产品矩阵',
|
||||||
|
servicesGridTitle: '四大核心服务,覆盖数字化全链路',
|
||||||
|
servicesEmptyState: '暂无服务数据',
|
||||||
|
servicesCtaTitle: '准备好开始了吗?',
|
||||||
|
servicesCtaDescription: '无论是一个明确的项目,还是一个模糊的想法,我们都愿意坐下来和您一起理清楚。\n首次咨询免费,没有任何销售压力。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'solutions',
|
||||||
|
title: '方案页文案配置',
|
||||||
|
solutionsHeroTitle1: '专注行业场景的',
|
||||||
|
solutionsHeroTitle2: '解决方案',
|
||||||
|
solutionsHeroDescription: '端到端交付可量化的业务成果。\n基于自研产品矩阵,为制造、零售、教育、医疗、金融、物流六大行业沉淀最佳实践。',
|
||||||
|
solutionsCtaPrimary: '立即咨询',
|
||||||
|
solutionsCtaSecondary: '浏览产品',
|
||||||
|
solutionsGridTitle: '行业解决方案',
|
||||||
|
solutionsGridDescription: '每个方案都明确推荐产品组合和服务包,确保落地效果',
|
||||||
|
solutionsCtaTitle: '告诉我们您的行业场景',
|
||||||
|
solutionsCtaDescription: '无论您处于哪个行业、哪个数字化阶段,我们都能为您推荐最合适的产品组合和服务包。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'products',
|
||||||
|
title: '产品页文案配置',
|
||||||
|
productsHeroTitle1: '自研产品矩阵',
|
||||||
|
productsHeroTitle2: '支撑企业',
|
||||||
|
productsHeroTitle3: '数字化核心系统',
|
||||||
|
productsHeroDescription: '从业务系统到数据智能,深度自研、互为补充。\n6 大企业套装与专业产品线,覆盖企业数字化核心场景。',
|
||||||
|
productsCtaPrimary: '了解我们的服务',
|
||||||
|
productsCtaSecondary: '查看行业方案',
|
||||||
|
productsBundleTitle: '两套产品体系\n覆盖不同场景',
|
||||||
|
productsBundleDescription: '企业套装以组合形式解决核心管理问题,专业产品聚焦独立场景深度赋能。',
|
||||||
|
productsCtaTitle: '以自研产品\n支撑数字化落地',
|
||||||
|
productsCtaDescription: '从企业套装到专业产品,以自研能力与专业服务,陪伴企业完成数字化升级。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'cases',
|
||||||
|
title: '案例页文案配置',
|
||||||
|
casePageHeroTitle1: '每一个项目,',
|
||||||
|
casePageHeroTitle2: '都以成果为目标',
|
||||||
|
casePageHeroDescription: '我们正在与首批客户共同创造价值故事。\n从挑战到方案,从过程到成果——用数据说话,用结果证明。',
|
||||||
|
casePageFilterTitle: '全部案例',
|
||||||
|
casePageFilterDescription: '按行业筛选,找到与您业务最相关的成功故事',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'news',
|
||||||
|
title: '新闻页文案配置',
|
||||||
|
newsPageHeroEyebrow: 'News & Insights',
|
||||||
|
newsPageHeroTitle: '新闻与\n洞察',
|
||||||
|
newsPageHeroDescription: '最新动态、产品更新与行业洞察。了解数字化趋势,把握技术脉搏。',
|
||||||
|
newsPageEmptyTitle: '没有找到相关新闻',
|
||||||
|
newsPageEmptyDescription: '试试其他关键词或分类',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
console.log(' ✅ 列表页文案配置');
|
||||||
|
|
||||||
// ============ 5. 创建默认内容区域 ============
|
// ============ 5. 创建默认内容区域 ============
|
||||||
console.log('📝 创建默认内容区域...');
|
console.log('📝 创建默认内容区域...');
|
||||||
await seedHomeZones();
|
await seedHomeZones();
|
||||||
|
|||||||
@@ -302,7 +302,38 @@ function CertificationsSection({ data }: { data: AboutData }) {
|
|||||||
const certifications = data.certifications ?? [];
|
const certifications = data.certifications ?? [];
|
||||||
const partners = data.partners ?? [];
|
const partners = data.partners ?? [];
|
||||||
|
|
||||||
if (certifications.length === 0 && partners.length === 0) return null;
|
// 初创阶段无资质/伙伴数据时,如实呈现「建设中」状态而非隐藏整块(零编造)
|
||||||
|
if (certifications.length === 0 && partners.length === 0) {
|
||||||
|
return (
|
||||||
|
<section className="relative py-20 sm:py-28 md:py-32 overflow-hidden bg-white">
|
||||||
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, margin: '-80px' }}
|
||||||
|
transition={{ duration: 0.8, ease: EASE_OUT }}
|
||||||
|
className="max-w-3xl"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-mono tracking-[0.2em] text-text-muted uppercase mb-6">
|
||||||
|
Certifications
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl sm:text-4xl md:text-5xl font-extrabold text-ink tracking-tight leading-[0.95] mb-10">
|
||||||
|
资质与认证
|
||||||
|
</h2>
|
||||||
|
<div className="p-8 sm:p-10 border border-dashed border-border-primary bg-bg-secondary">
|
||||||
|
<div className="flex items-center gap-2.5 mb-3">
|
||||||
|
<Shield className="w-5 h-5 text-text-muted" />
|
||||||
|
<span className="text-sm font-semibold text-ink">资质建设中</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-text-muted leading-relaxed">
|
||||||
|
公司资质与认证信息建设中,将随业务开展与取得进度陆续公示。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 sm:py-28 md:py-32 overflow-hidden bg-white">
|
<section className="relative py-20 sm:py-28 md:py-32 overflow-hidden bg-white">
|
||||||
|
|||||||
@@ -95,14 +95,23 @@ function CaseCard({ caseItem, index }: { caseItem: CaseStudy; index: number }) {
|
|||||||
export default function CasesContentV3({
|
export default function CasesContentV3({
|
||||||
cases: casesProp,
|
cases: casesProp,
|
||||||
industries: industriesProp,
|
industries: industriesProp,
|
||||||
|
pageCopy,
|
||||||
}: {
|
}: {
|
||||||
cases?: CaseStudy[];
|
cases?: CaseStudy[];
|
||||||
industries?: { id: string; label: string }[];
|
industries?: { id: string; label: string }[];
|
||||||
|
pageCopy?: Record<string, unknown> | null;
|
||||||
}) {
|
}) {
|
||||||
const [selectedIndustry, setSelectedIndustry] = useState('all');
|
const [selectedIndustry, setSelectedIndustry] = useState('all');
|
||||||
|
|
||||||
const industries = industriesProp ?? [];
|
const industries = industriesProp ?? [];
|
||||||
|
|
||||||
|
const heroTitle1 = (pageCopy?.casePageHeroTitle1 as string) || '每一个项目,';
|
||||||
|
const heroTitle2 = (pageCopy?.casePageHeroTitle2 as string) || '都以成果为目标';
|
||||||
|
const heroDescription = (pageCopy?.casePageHeroDescription as string) || '我们正在与首批客户共同创造价值故事。\n从挑战到方案,从过程到成果——用数据说话,用结果证明。';
|
||||||
|
const heroDescriptionLines = heroDescription.split('\n');
|
||||||
|
const filterTitle = (pageCopy?.casePageFilterTitle as string) || '全部案例';
|
||||||
|
const filterDescription = (pageCopy?.casePageFilterDescription as string) || '按行业筛选,找到与您业务最相关的成功故事';
|
||||||
|
|
||||||
const filteredCases = useMemo(() => {
|
const filteredCases = useMemo(() => {
|
||||||
const allCases = casesProp ?? [];
|
const allCases = casesProp ?? [];
|
||||||
if (selectedIndustry === 'all') return allCases;
|
if (selectedIndustry === 'all') return allCases;
|
||||||
@@ -130,11 +139,11 @@ export default function CasesContentV3({
|
|||||||
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
|
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
|
||||||
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
|
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.88] tracking-tightest mb-10 sm:mb-12 md:mb-16"
|
||||||
>
|
>
|
||||||
<div className="block">每一个项目,</div>
|
<div className="block">{heroTitle1}</div>
|
||||||
<div className="block mt-4 sm:mt-6">
|
<div className="block mt-4 sm:mt-6">
|
||||||
<span className="relative inline-block">
|
<span className="relative inline-block">
|
||||||
<span className="relative text-brand font-black">
|
<span className="relative text-brand font-black">
|
||||||
都以成果为目标
|
{heroTitle2}
|
||||||
<motion.span
|
<motion.span
|
||||||
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
|
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
|
||||||
style={{ transformOrigin: 'left' }}
|
style={{ transformOrigin: 'left' }}
|
||||||
@@ -153,9 +162,12 @@ export default function CasesContentV3({
|
|||||||
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
|
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
|
||||||
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12"
|
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12"
|
||||||
>
|
>
|
||||||
我们正在与首批客户共同创造价值故事。
|
{heroDescriptionLines.map((line, i) => (
|
||||||
<br className="hidden sm:block" />
|
<span key={i}>
|
||||||
从挑战到方案,从过程到成果——用数据说话,用结果证明。
|
{i > 0 && <br className="hidden sm:block" />}
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -225,10 +237,10 @@ export default function CasesContentV3({
|
|||||||
All Cases
|
All Cases
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
|
||||||
全部案例
|
{filterTitle}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
|
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
|
||||||
按行业筛选,找到与您业务最相关的成功故事
|
{filterDescription}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import CasesContentV3 from './cases-content-v3';
|
|||||||
interface CasesClientProps {
|
interface CasesClientProps {
|
||||||
cases?: CaseStudy[];
|
cases?: CaseStudy[];
|
||||||
industries?: { id: string; label: string }[];
|
industries?: { id: string; label: string }[];
|
||||||
|
pageCopy?: Record<string, unknown> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CasesClient({ cases, industries }: CasesClientProps) {
|
export function CasesClient({ cases, industries, pageCopy }: CasesClientProps) {
|
||||||
return <CasesContentV3 cases={cases} industries={industries} />;
|
return <CasesContentV3 cases={cases} industries={industries} pageCopy={pageCopy} />;
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import CasesPage from './page';
|
|||||||
|
|
||||||
jest.mock('@/lib/cms/data-server', () => ({
|
jest.mock('@/lib/cms/data-server', () => ({
|
||||||
getPublishedItems: jest.fn(),
|
getPublishedItems: jest.fn(),
|
||||||
|
getPageCopy: jest.fn().mockResolvedValue(null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./cases-content-v3', () => ({
|
jest.mock('./cases-content-v3', () => ({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { COMPANY_INFO } from '@/lib/constants';
|
import { COMPANY_INFO } from '@/lib/constants';
|
||||||
import { CASE_INDUSTRIES } from '@/lib/constants/cases';
|
import { CASE_INDUSTRIES } from '@/lib/constants/cases';
|
||||||
import { getPublishedItems } from '@/lib/cms/data-server';
|
import { getPublishedItems, getPageCopy } from '@/lib/cms/data-server';
|
||||||
import type { CaseStudy } from '@/lib/constants/cases';
|
import type { CaseStudy } from '@/lib/constants/cases';
|
||||||
import { CasesClient } from './client';
|
import { CasesClient } from './client';
|
||||||
|
|
||||||
@@ -13,7 +13,10 @@ export const metadata: Metadata = {
|
|||||||
export const revalidate = 3600;
|
export const revalidate = 3600;
|
||||||
|
|
||||||
export default async function CasesPage() {
|
export default async function CasesPage() {
|
||||||
const items = await getPublishedItems('case-study');
|
const [items, pageCopy] = await Promise.all([
|
||||||
|
getPublishedItems('case-study'),
|
||||||
|
getPageCopy('cases').catch(() => null),
|
||||||
|
]);
|
||||||
const cases = items.map((item) => ({ ...item.data, id: item.data.id || item.id, slug: item.data.slug || item.slug, title: item.data.title || item.title } as unknown as CaseStudy));
|
const cases = items.map((item) => ({ ...item.data, id: item.data.id || item.id, slug: item.data.slug || item.slug, title: item.data.title || item.title } as unknown as CaseStudy));
|
||||||
return <CasesClient cases={cases} industries={CASE_INDUSTRIES} />;
|
return <CasesClient cases={cases} industries={CASE_INDUSTRIES} pageCopy={pageCopy?.data ?? null} />;
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import { ArrowRight, Lightbulb, Database, Layers, Code, Puzzle, Server } from 'l
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { EASE_OUT } from '@/components/ui/page-decoration';
|
import { EASE_OUT } from '@/components/ui/page-decoration';
|
||||||
import { COMPANY_INFO, TRUST_SIGNALS, EARLY_ACCESS } from '@/lib/constants';
|
import { COMPANY_INFO, TRUST_SIGNALS, EARLY_ACCESS } from '@/lib/constants';
|
||||||
|
import { useSiteConfig } from '@/lib/site-config';
|
||||||
|
|
||||||
// ===== 服务图标映射:将字符串键名映射为 Lucide 组件 =====
|
// ===== 服务图标映射:将字符串键名映射为 Lucide 组件 =====
|
||||||
const SERVICE_ICON_MAP: Record<string, React.ReactNode> = {
|
const SERVICE_ICON_MAP: Record<string, React.ReactNode> = {
|
||||||
@@ -71,6 +72,23 @@ const FALLBACK_SERVICES = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ===== 首页结构性文案类型(page-copy CMS 覆盖,未配置时走硬编码兜底)=====
|
||||||
|
type HomePageCopy = Record<string, any>;
|
||||||
|
|
||||||
|
// 多行标题渲染:按 \n 分段,输出 <br/> 换行
|
||||||
|
function StyledTitle({ lines }: { lines: (string | undefined)[] }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{lines.map((line, i) => (
|
||||||
|
<span key={i}>
|
||||||
|
{i > 0 && <br />}
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 品牌 CTA 按钮 =====
|
// ===== 品牌 CTA 按钮 =====
|
||||||
function CTAButton({ children, href, variant = 'primary', className, dataTestId }: {
|
function CTAButton({ children, href, variant = 'primary', className, dataTestId }: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -103,6 +121,7 @@ function HeroSection({ heroData, stats }: {
|
|||||||
stats?: Record<string, any>[];
|
stats?: Record<string, any>[];
|
||||||
}) {
|
}) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const siteConfig = useSiteConfig();
|
||||||
|
|
||||||
const heading = heroData?.heading || heroData?.headingBottom || '让每一家企业都拥有数据驱动的决策能力';
|
const heading = heroData?.heading || heroData?.headingBottom || '让每一家企业都拥有数据驱动的决策能力';
|
||||||
const subheading = heroData?.subheading || heroData?.description || '';
|
const subheading = heroData?.subheading || heroData?.description || '';
|
||||||
@@ -110,6 +129,7 @@ function HeroSection({ heroData, stats }: {
|
|||||||
const ctaHref = heroData?.ctaHref || '/contact';
|
const ctaHref = heroData?.ctaHref || '/contact';
|
||||||
const secondaryCtaLabel = heroData?.secondaryCtaLabel || '查看产品矩阵';
|
const secondaryCtaLabel = heroData?.secondaryCtaLabel || '查看产品矩阵';
|
||||||
const secondaryCtaHref = heroData?.secondaryCtaHref || '/products';
|
const secondaryCtaHref = heroData?.secondaryCtaHref || '/products';
|
||||||
|
const slogan = siteConfig.slogan || COMPANY_INFO.slogan;
|
||||||
|
|
||||||
const hasValidStats = stats?.length && stats[0]?.value;
|
const hasValidStats = stats?.length && stats[0]?.value;
|
||||||
const displayStats = hasValidStats ? stats!.slice(0, 3) : [];
|
const displayStats = hasValidStats ? stats!.slice(0, 3) : [];
|
||||||
@@ -147,6 +167,20 @@ function HeroSection({ heroData, stats }: {
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 品牌口号 Badge:消费站点配置 slogan,后台可改 */}
|
||||||
|
{slogan && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.35, delay: 0.03, ease: EASE_OUT }}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<span className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-brand/5 text-brand text-sm font-medium border border-brand/15">
|
||||||
|
{slogan}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 主标题:大胆、直接 */}
|
{/* 主标题:大胆、直接 */}
|
||||||
<motion.h1
|
<motion.h1
|
||||||
initial={{ opacity: 0, y: 40 }}
|
initial={{ opacity: 0, y: 40 }}
|
||||||
@@ -228,7 +262,23 @@ function HeroSection({ heroData, stats }: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 信任/成果区:可验证信任信号 + 首批客户共创标签 =====
|
// ===== 信任/成果区:可验证信任信号 + 首批客户共创标签 =====
|
||||||
function TrustSection() {
|
// 共创计划默认状态条目(如实呈现,无真实案例时不虚构客户)
|
||||||
|
const DEFAULT_EARLY_ACCESS_STATUS = [
|
||||||
|
{ label: '共创进行中', desc: '我们正与首批客户共同打磨产品与方案,以真实运营数据验证业务改善。' },
|
||||||
|
{ label: '产品内测中', desc: '自研产品(如睿视 NovaVis)处于内测阶段,成熟后正式发布。' },
|
||||||
|
{ label: '成果授权公开', desc: '案例与可量化成果在获得客户授权后公示——经得起衡量。' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function TrustSection({ pageCopy }: { pageCopy?: HomePageCopy | null }) {
|
||||||
|
const eyebrow = pageCopy?.trustEyebrow || '值得信赖';
|
||||||
|
const titleLines = (pageCopy?.trustTitle || '以可衡量的成果\n赢得长期信任').split('\n');
|
||||||
|
const signals = (pageCopy?.trustSignals?.length ? pageCopy.trustSignals : TRUST_SIGNALS) as typeof TRUST_SIGNALS;
|
||||||
|
const earlyLabel = pageCopy?.earlyAccessLabel || EARLY_ACCESS.label;
|
||||||
|
const earlyDesc = pageCopy?.earlyAccessDesc || EARLY_ACCESS.desc;
|
||||||
|
const earlyTitle = (pageCopy?.earlyAccessTitle as string) || '首批客户共创计划';
|
||||||
|
const earlyCtaLabel = (pageCopy?.earlyAccessCtaLabel as string) || '成为共创客户';
|
||||||
|
const earlyStatus = (pageCopy?.earlyAccessStatus?.length ? pageCopy.earlyAccessStatus : DEFAULT_EARLY_ACCESS_STATUS) as typeof DEFAULT_EARLY_ACCESS_STATUS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-bg-secondary" data-testid="trust-section">
|
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-bg-secondary" data-testid="trust-section">
|
||||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
|
||||||
@@ -236,18 +286,16 @@ function TrustSection() {
|
|||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<div className="w-10 h-px bg-brand" />
|
<div className="w-10 h-px bg-brand" />
|
||||||
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand">
|
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand">
|
||||||
值得信赖
|
{eyebrow}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
|
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
|
||||||
以可衡量的成果
|
<StyledTitle lines={titleLines} />
|
||||||
<br />
|
|
||||||
赢得长期信任
|
|
||||||
</h2>
|
</h2>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-10">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-10">
|
||||||
{TRUST_SIGNALS.map((item, i) => (
|
{signals.map((item, i) => (
|
||||||
<StaggerReveal key={i} staggerDelay={0.06}>
|
<StaggerReveal key={i} staggerDelay={0.06}>
|
||||||
<div className="bain-card p-6 sm:p-7 h-full flex flex-col">
|
<div className="bain-card p-6 sm:p-7 h-full flex flex-col">
|
||||||
<div className="flex items-baseline gap-2 mb-3">
|
<div className="flex items-baseline gap-2 mb-3">
|
||||||
@@ -265,24 +313,33 @@ function TrustSection() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 首批客户共创标签:无真实案例时如实标注,不虚构客户 */}
|
{/* 首批客户共创计划:如实呈现共创/内测/授权公开状态,不虚构客户 */}
|
||||||
<StaggerReveal staggerDelay={0.08}>
|
<StaggerReveal staggerDelay={0.08}>
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-6 p-6 sm:p-8 bg-brand-bg border border-[rgba(196,30,58,0.15)]" data-testid="early-access-banner">
|
<div className="p-6 sm:p-8 bg-brand-bg border border-[rgba(196,30,58,0.15)]" data-testid="early-access-banner">
|
||||||
<div>
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="flex items-center gap-3 mb-1">
|
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand">
|
||||||
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand">
|
{earlyLabel}
|
||||||
{EARLY_ACCESS.label}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
<h3 className="text-xl sm:text-2xl font-bold text-ink mb-3">
|
||||||
<p className="text-sm text-text-secondary leading-relaxed">
|
{earlyTitle}
|
||||||
{EARLY_ACCESS.desc}
|
</h3>
|
||||||
</p>
|
<p className="text-sm text-text-secondary leading-relaxed mb-6 max-w-2xl">
|
||||||
|
{earlyDesc}
|
||||||
|
</p>
|
||||||
|
<div className="grid sm:grid-cols-3 gap-4 mb-8">
|
||||||
|
{earlyStatus.map((s) => (
|
||||||
|
<div key={s.label} className="p-4 bg-white border border-brand/10">
|
||||||
|
<div className="text-sm font-semibold text-brand mb-1">{s.label}</div>
|
||||||
|
<div className="text-xs text-text-secondary leading-relaxed">{s.desc}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="/contact"
|
href="/contact"
|
||||||
className="group inline-flex shrink-0 items-center gap-2 text-sm font-semibold text-ink hover:text-brand transition-colors sm:ml-auto"
|
className="group inline-flex shrink-0 items-center gap-2 text-sm font-semibold text-ink hover:text-brand transition-colors"
|
||||||
>
|
>
|
||||||
<span>成为首批共创客户</span>
|
<span>{earlyCtaLabel}</span>
|
||||||
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -317,20 +374,27 @@ const NARRATIVE_ACTS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function NarrativeSection() {
|
function NarrativeSection({ pageCopy }: { pageCopy?: HomePageCopy | null }) {
|
||||||
|
const narrativeTitle = (pageCopy?.narrativeTitle || '从问题到结果,\n一条可量化的路径').split('\n');
|
||||||
|
const NARRATIVE_ACTS_CMS = (pageCopy?.narrativeActs?.length ? pageCopy.narrativeActs : NARRATIVE_ACTS) as Array<{
|
||||||
|
number: string;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
href: string;
|
||||||
|
cta: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-white" data-testid="narrative-section">
|
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-white" data-testid="narrative-section">
|
||||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
|
||||||
<ScrollReveal className="mb-16 sm:mb-20">
|
<ScrollReveal className="mb-16 sm:mb-20">
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
|
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
|
||||||
从问题到结果,
|
<StyledTitle lines={narrativeTitle} />
|
||||||
<br />
|
|
||||||
一条可量化的路径
|
|
||||||
</h2>
|
</h2>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-px bg-border-primary">
|
<div className="grid md:grid-cols-3 gap-px bg-border-primary">
|
||||||
{NARRATIVE_ACTS.map((act) => (
|
{NARRATIVE_ACTS_CMS.map((act) => (
|
||||||
<StaggerReveal key={act.number} staggerDelay={0.08}>
|
<StaggerReveal key={act.number} staggerDelay={0.08}>
|
||||||
<div className="group relative h-full bg-white p-8 sm:p-10 md:p-12 transition-colors duration-300 hover:bg-bg-secondary">
|
<div className="group relative h-full bg-white p-8 sm:p-10 md:p-12 transition-colors duration-300 hover:bg-bg-secondary">
|
||||||
<div className="flex items-start justify-between mb-8">
|
<div className="flex items-start justify-between mb-8">
|
||||||
@@ -363,7 +427,7 @@ function NarrativeSection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 服务区:等宽三列卡片 + 兜底数据 =====
|
// ===== 服务区:等宽三列卡片 + 兜底数据 =====
|
||||||
function ServicesSection({ services }: { services?: Record<string, any>[] }) {
|
function ServicesSection({ services, pageCopy }: { services?: Record<string, any>[]; pageCopy?: HomePageCopy | null }) {
|
||||||
const hasValidData = services?.length && (services![0] as any)?.subtitle;
|
const hasValidData = services?.length && (services![0] as any)?.subtitle;
|
||||||
const SERVICE_ITEMS = hasValidData
|
const SERVICE_ITEMS = hasValidData
|
||||||
? services!.map((s: Record<string, any>) => ({
|
? services!.map((s: Record<string, any>) => ({
|
||||||
@@ -371,6 +435,7 @@ function ServicesSection({ services }: { services?: Record<string, any>[] }) {
|
|||||||
icon: SERVICE_ICON_MAP[s.icon as string] || <Server className="w-7 h-7" />,
|
icon: SERVICE_ICON_MAP[s.icon as string] || <Server className="w-7 h-7" />,
|
||||||
})) as Record<string, any>[]
|
})) as Record<string, any>[]
|
||||||
: FALLBACK_SERVICES;
|
: FALLBACK_SERVICES;
|
||||||
|
const servicesTitle = (pageCopy?.servicesTitle || '从战略到落地,\n可量化的一站式服务').split('\n');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-bg-secondary">
|
<section className="relative py-20 sm:py-28 md:py-36 overflow-hidden bg-bg-secondary">
|
||||||
@@ -378,9 +443,7 @@ function ServicesSection({ services }: { services?: Record<string, any>[] }) {
|
|||||||
<div className="max-w-3xl mb-16 sm:mb-20">
|
<div className="max-w-3xl mb-16 sm:mb-20">
|
||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
|
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
|
||||||
从战略到落地,
|
<StyledTitle lines={servicesTitle} />
|
||||||
<br />
|
|
||||||
可量化的一站式服务
|
|
||||||
</h2>
|
</h2>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
</div>
|
</div>
|
||||||
@@ -434,9 +497,14 @@ function ServicesSection({ services }: { services?: Record<string, any>[] }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 案例区:轻量快照卡片 =====
|
// ===== 案例区:轻量快照卡片 =====
|
||||||
function CasesSection({ cases }: { cases?: Record<string, any>[] }) {
|
function CasesSection({ cases, pageCopy }: { cases?: Record<string, any>[]; pageCopy?: HomePageCopy | null }) {
|
||||||
const hasValidData = cases?.length && cases[0]?.metrics;
|
const hasValidData = cases?.length && cases[0]?.metrics;
|
||||||
const CASE_ITEMS = hasValidData ? cases! : [];
|
const CASE_ITEMS = hasValidData ? cases! : [];
|
||||||
|
const casesEyebrow = pageCopy?.casesEyebrow || '共创进行时';
|
||||||
|
const casesTitleLines = (pageCopy?.casesTitle || '每一个项目,\n都以业务改善为衡量标准').split('\n');
|
||||||
|
const casesDesc = pageCopy?.casesDescription || '我们正与首批客户共同打磨产品与方案,以真实运营数据验证业务改善。';
|
||||||
|
const earlyLabel = pageCopy?.earlyAccessLabel || EARLY_ACCESS.label;
|
||||||
|
const earlyDesc = pageCopy?.earlyAccessDesc || EARLY_ACCESS.desc;
|
||||||
|
|
||||||
if (CASE_ITEMS.length === 0) {
|
if (CASE_ITEMS.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -447,18 +515,14 @@ function CasesSection({ cases }: { cases?: Record<string, any>[] }) {
|
|||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<div className="w-10 h-px bg-brand" />
|
<div className="w-10 h-px bg-brand" />
|
||||||
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand">
|
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand">
|
||||||
共创进行时
|
{casesEyebrow}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05] mb-8">
|
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05] mb-8">
|
||||||
每一个项目,
|
<StyledTitle lines={casesTitleLines} />
|
||||||
<br />
|
|
||||||
都以业务改善为衡量标准
|
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-text-secondary leading-relaxed text-lg mb-8">
|
<p className="text-text-secondary leading-relaxed text-lg mb-8">
|
||||||
我们正与首批客户共同打磨产品与方案,以真实运营数据验证业务改善。
|
{casesDesc}
|
||||||
<br className="hidden sm:block" />
|
|
||||||
公开案例将在验证后发布——合作成果,经得起衡量。
|
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="/contact"
|
href="/contact"
|
||||||
@@ -471,9 +535,9 @@ function CasesSection({ cases }: { cases?: Record<string, any>[] }) {
|
|||||||
<ScrollReveal delay={0.15}>
|
<ScrollReveal delay={0.15}>
|
||||||
<div className="bain-card p-10 sm:p-12 bg-bg-secondary">
|
<div className="bain-card p-10 sm:p-12 bg-bg-secondary">
|
||||||
<div className="text-5xl sm:text-6xl font-black text-brand tabular-nums leading-none mb-4">
|
<div className="text-5xl sm:text-6xl font-black text-brand tabular-nums leading-none mb-4">
|
||||||
{EARLY_ACCESS.label}
|
{earlyLabel}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-text-secondary leading-relaxed">{EARLY_ACCESS.desc}</p>
|
<p className="text-text-secondary leading-relaxed">{earlyDesc}</p>
|
||||||
</div>
|
</div>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
</div>
|
</div>
|
||||||
@@ -488,9 +552,7 @@ function CasesSection({ cases }: { cases?: Record<string, any>[] }) {
|
|||||||
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-8 mb-16 sm:mb-20">
|
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-8 mb-16 sm:mb-20">
|
||||||
<ScrollReveal className="md:max-w-3xl">
|
<ScrollReveal className="md:max-w-3xl">
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
|
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[1.05]">
|
||||||
每一个项目,
|
<StyledTitle lines={casesTitleLines} />
|
||||||
<br />
|
|
||||||
都以业务改善为衡量标准
|
|
||||||
</h2>
|
</h2>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
<ScrollReveal delay={0.1}>
|
<ScrollReveal delay={0.1}>
|
||||||
@@ -553,10 +615,10 @@ function CasesSection({ cases }: { cases?: Record<string, any>[] }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== CTA 区:克制强调,品牌红仅用于按钮 =====
|
// ===== CTA 区:克制强调,品牌红仅用于按钮 =====
|
||||||
function CTASection({ heroData }: { heroData?: Record<string, any> | null }) {
|
function CTASection({ heroData, pageCopy }: { heroData?: Record<string, any> | null; pageCopy?: HomePageCopy | null }) {
|
||||||
const ctaTitleLine1 = heroData?.ctaTitleLine1 || '准备好开始了吗?';
|
const ctaTitleLine1 = heroData?.ctaTitleLine1 || pageCopy?.ctaTitleLine1 || '准备好开始了吗?';
|
||||||
const ctaTitleLine2 = heroData?.ctaTitleLine2 || '让数据真正转化为业务增长';
|
const ctaTitleLine2 = heroData?.ctaTitleLine2 || pageCopy?.ctaTitleLine2 || '让数据真正转化为业务增长';
|
||||||
const ctaLabel = heroData?.ctaLabel || '预约咨询';
|
const ctaLabel = heroData?.ctaLabel || pageCopy?.ctaLabel || '预约咨询';
|
||||||
const ctaHref = heroData?.ctaHref || '/contact';
|
const ctaHref = heroData?.ctaHref || '/contact';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -576,11 +638,12 @@ function CTASection({ heroData }: { heroData?: Record<string, any> | null }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 主页面组件 =====
|
// ===== 主页面组件 =====
|
||||||
export default function HomeContentV14({ services, cases, stats, heroData }: {
|
export default function HomeContentV14({ services, cases, stats, heroData, pageCopy }: {
|
||||||
services?: Record<string, any>[];
|
services?: Record<string, any>[];
|
||||||
cases?: Record<string, any>[];
|
cases?: Record<string, any>[];
|
||||||
stats?: Record<string, any>[];
|
stats?: Record<string, any>[];
|
||||||
heroData?: Record<string, any> | null;
|
heroData?: Record<string, any> | null;
|
||||||
|
pageCopy?: HomePageCopy | null;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
@@ -588,19 +651,19 @@ export default function HomeContentV14({ services, cases, stats, heroData }: {
|
|||||||
<HeroSection heroData={heroData} stats={stats} />
|
<HeroSection heroData={heroData} stats={stats} />
|
||||||
|
|
||||||
{/* 2. 信任区:量化数据成果展示 */}
|
{/* 2. 信任区:量化数据成果展示 */}
|
||||||
<TrustSection />
|
<TrustSection pageCopy={pageCopy} />
|
||||||
|
|
||||||
{/* 3. 章节式叙事:问题 → 方法 → 结果 */}
|
{/* 3. 章节式叙事:问题 → 方法 → 结果 */}
|
||||||
<NarrativeSection />
|
<NarrativeSection pageCopy={pageCopy} />
|
||||||
|
|
||||||
{/* 4. 服务区:等宽卡片 + 兜底数据 */}
|
{/* 4. 服务区:等宽卡片 + 兜底数据 */}
|
||||||
<ServicesSection services={services} />
|
<ServicesSection services={services} pageCopy={pageCopy} />
|
||||||
|
|
||||||
{/* 5. 案例区:轻量快照卡片 */}
|
{/* 5. 案例区:轻量快照卡片 */}
|
||||||
<CasesSection cases={cases} />
|
<CasesSection cases={cases} pageCopy={pageCopy} />
|
||||||
|
|
||||||
{/* 6. CTA:克制强调,品牌红仅用于按钮 */}
|
{/* 6. CTA:克制强调,品牌红仅用于按钮 */}
|
||||||
<CTASection heroData={heroData} />
|
<CTASection heroData={heroData} pageCopy={pageCopy} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -74,12 +74,18 @@ function NewsCard({ newsItem, index }: { newsItem: NewsItem; index: number }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NewsContentV3({ news }: { news?: NewsItem[] }) {
|
export default function NewsContentV3({ news, pageCopy }: { news?: NewsItem[]; pageCopy?: Record<string, unknown> | null }) {
|
||||||
const NEWS = news ?? [];
|
const NEWS = news ?? [];
|
||||||
const [selectedCategory, setSelectedCategory] = useState('全部');
|
const [selectedCategory, setSelectedCategory] = useState('全部');
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
const heroEyebrow = (pageCopy?.newsPageHeroEyebrow as string) || 'News & Insights';
|
||||||
|
const heroTitleLines = ((pageCopy?.newsPageHeroTitle as string) || '新闻与\n洞察').split('\n');
|
||||||
|
const heroDescription = (pageCopy?.newsPageHeroDescription as string) || `${COMPANY_INFO.displayName}最新动态、产品更新与行业洞察。了解数字化趋势,把握技术脉搏。`;
|
||||||
|
const emptyTitle = (pageCopy?.newsPageEmptyTitle as string) || '没有找到相关新闻';
|
||||||
|
const emptyDescription = (pageCopy?.newsPageEmptyDescription as string) || '试试其他关键词或分类';
|
||||||
|
|
||||||
const filteredNews = useMemo(() => {
|
const filteredNews = useMemo(() => {
|
||||||
return NEWS.filter((newsItem) => {
|
return NEWS.filter((newsItem) => {
|
||||||
const matchesCategory = selectedCategory === '全部' || newsItem.category === selectedCategory;
|
const matchesCategory = selectedCategory === '全部' || newsItem.category === selectedCategory;
|
||||||
@@ -118,19 +124,22 @@ export default function NewsContentV3({ news }: { news?: NewsItem[] }) {
|
|||||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
|
||||||
<div className="max-w-4xl">
|
<div className="max-w-4xl">
|
||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<SectionLabel>News & Insights</SectionLabel>
|
<SectionLabel>{heroEyebrow}</SectionLabel>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
|
|
||||||
<ScrollReveal delay={0.1}>
|
<ScrollReveal delay={0.1}>
|
||||||
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-bold leading-[0.92] tracking-tighter mb-10 sm:mb-12 md:mb-16">
|
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-bold leading-[0.92] tracking-tighter mb-10 sm:mb-12 md:mb-16">
|
||||||
新闻与
|
{heroTitleLines.map((line, i) => (
|
||||||
<span className="block mt-4 text-brand">洞察</span>
|
<span key={i} className={i === heroTitleLines.length - 1 ? 'block mt-4 text-brand' : 'block'}>
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</h1>
|
</h1>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
|
|
||||||
<ScrollReveal delay={0.2}>
|
<ScrollReveal delay={0.2}>
|
||||||
<p className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed">
|
<p className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed">
|
||||||
{COMPANY_INFO.displayName}最新动态、产品更新与行业洞察。了解数字化趋势,把握技术脉搏。
|
{heroDescription}
|
||||||
</p>
|
</p>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,8 +182,8 @@ export default function NewsContentV3({ news }: { news?: NewsItem[] }) {
|
|||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<div className="text-center py-24">
|
<div className="text-center py-24">
|
||||||
<Newspaper className="w-16 h-16 text-text-muted/20 mx-auto mb-6" />
|
<Newspaper className="w-16 h-16 text-text-muted/20 mx-auto mb-6" />
|
||||||
<p className="text-xl text-text-secondary">没有找到相关新闻</p>
|
<p className="text-xl text-text-secondary">{emptyTitle}</p>
|
||||||
<p className="mt-2 text-text-muted">试试其他关键词或分类</p>
|
<p className="mt-2 text-text-muted">{emptyDescription}</p>
|
||||||
</div>
|
</div>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import NewsListPage from './page';
|
|||||||
|
|
||||||
jest.mock('@/lib/cms/data-server', () => ({
|
jest.mock('@/lib/cms/data-server', () => ({
|
||||||
getPublishedItems: jest.fn(),
|
getPublishedItems: jest.fn(),
|
||||||
|
getPageCopy: jest.fn().mockResolvedValue(null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./news-content-v3', () => ({
|
jest.mock('./news-content-v3', () => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { COMPANY_INFO } from '@/lib/constants';
|
import { COMPANY_INFO } from '@/lib/constants';
|
||||||
import { getPublishedItems } from '@/lib/cms/data-server';
|
import { getPublishedItems, getPageCopy } from '@/lib/cms/data-server';
|
||||||
import type { NewsItem } from '@/lib/constants/news';
|
import type { NewsItem } from '@/lib/constants/news';
|
||||||
import NewsContentV3 from './news-content-v3';
|
import NewsContentV3 from './news-content-v3';
|
||||||
|
|
||||||
@@ -13,7 +13,10 @@ export const metadata: Metadata = {
|
|||||||
export const revalidate = 3600;
|
export const revalidate = 3600;
|
||||||
|
|
||||||
export default async function NewsListPage() {
|
export default async function NewsListPage() {
|
||||||
const items = await getPublishedItems('news');
|
const [items, pageCopy] = await Promise.all([
|
||||||
|
getPublishedItems('news'),
|
||||||
|
getPageCopy('news').catch(() => null),
|
||||||
|
]);
|
||||||
const news = items.map((item) => ({ ...item.data, title: item.data.title || item.title } as unknown as NewsItem));
|
const news = items.map((item) => ({ ...item.data, title: item.data.title || item.title } as unknown as NewsItem));
|
||||||
return <NewsContentV3 news={news} />;
|
return <NewsContentV3 news={news} pageCopy={pageCopy?.data ?? null} />;
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import HomePage from './page';
|
|||||||
jest.mock('@/lib/cms/data-server', () => ({
|
jest.mock('@/lib/cms/data-server', () => ({
|
||||||
getPublishedItems: jest.fn(),
|
getPublishedItems: jest.fn(),
|
||||||
getResolvedHomeZones: jest.fn(),
|
getResolvedHomeZones: jest.fn(),
|
||||||
|
getHomePageCopy: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./home-content-v14', () => ({
|
jest.mock('./home-content-v14', () => ({
|
||||||
@@ -16,15 +17,17 @@ jest.mock('./home-content-v14', () => ({
|
|||||||
<div data-testid="cases-count">{(props.cases as unknown[])?.length ?? 0}</div>
|
<div data-testid="cases-count">{(props.cases as unknown[])?.length ?? 0}</div>
|
||||||
<div data-testid="stats-count">{(props.stats as unknown[])?.length ?? 0}</div>
|
<div data-testid="stats-count">{(props.stats as unknown[])?.length ?? 0}</div>
|
||||||
<div data-testid="hero-data">{props.heroData ? JSON.stringify(props.heroData) : 'null'}</div>
|
<div data-testid="hero-data">{props.heroData ? JSON.stringify(props.heroData) : 'null'}</div>
|
||||||
|
<div data-testid="page-copy-data">{props.pageCopy ? JSON.stringify(props.pageCopy) : 'null'}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { getPublishedItems, getResolvedHomeZones } from '@/lib/cms/data-server';
|
import { getPublishedItems, getResolvedHomeZones, getHomePageCopy } from '@/lib/cms/data-server';
|
||||||
|
|
||||||
const mockedGetResolvedHomeZones = getResolvedHomeZones as jest.MockedFunction<typeof getResolvedHomeZones>;
|
const mockedGetResolvedHomeZones = getResolvedHomeZones as jest.MockedFunction<typeof getResolvedHomeZones>;
|
||||||
const mockedGetPublishedItems = getPublishedItems as jest.MockedFunction<typeof getPublishedItems>;
|
const mockedGetPublishedItems = getPublishedItems as jest.MockedFunction<typeof getPublishedItems>;
|
||||||
|
const mockedGetHomePageCopy = getHomePageCopy as jest.MockedFunction<typeof getHomePageCopy>;
|
||||||
|
|
||||||
const createMockItem = (modelCode: string, data: Record<string, unknown>) => ({
|
const createMockItem = (modelCode: string, data: Record<string, unknown>) => ({
|
||||||
id: `${modelCode}-id`,
|
id: `${modelCode}-id`,
|
||||||
@@ -48,6 +51,7 @@ describe('HomePage', () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
mockedGetResolvedHomeZones.mockResolvedValue({});
|
mockedGetResolvedHomeZones.mockResolvedValue({});
|
||||||
mockedGetPublishedItems.mockResolvedValue([]);
|
mockedGetPublishedItems.mockResolvedValue([]);
|
||||||
|
mockedGetHomePageCopy.mockResolvedValue(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders home content from resolved zones', async () => {
|
it('renders home content from resolved zones', async () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getPublishedItems, getResolvedHomeZones } from '@/lib/cms/data-server';
|
import { getPublishedItems, getResolvedHomeZones, getHomePageCopy } from '@/lib/cms/data-server';
|
||||||
import HomeContentV14 from './home-content-v14';
|
import HomeContentV14 from './home-content-v14';
|
||||||
|
|
||||||
export const revalidate = 3600;
|
export const revalidate = 3600;
|
||||||
@@ -31,5 +31,14 @@ export default async function HomePage() {
|
|||||||
heroData = heroItems[0]?.data ?? null;
|
heroData = heroItems[0]?.data ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <HomeContentV14 services={services} cases={cases} stats={stats} heroData={heroData} />;
|
// 首页各章节结构性文案(信任信号/叙事情节/标题/CTA),CMS 未配置时为 null(组件内部兜底)
|
||||||
|
const homePageCopy = await getHomePageCopy().catch(() => null);
|
||||||
|
|
||||||
|
return <HomeContentV14
|
||||||
|
services={services}
|
||||||
|
cases={cases}
|
||||||
|
stats={stats}
|
||||||
|
heroData={heroData}
|
||||||
|
pageCopy={homePageCopy?.data ?? null}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ProductsPage from './page';
|
|||||||
|
|
||||||
jest.mock('@/lib/cms/data-server', () => ({
|
jest.mock('@/lib/cms/data-server', () => ({
|
||||||
getPublishedItems: jest.fn(),
|
getPublishedItems: jest.fn(),
|
||||||
|
getPageCopy: jest.fn().mockResolvedValue(null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./products-content-v3', () => ({
|
jest.mock('./products-content-v3', () => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { COMPANY_INFO } from '@/lib/constants';
|
import { COMPANY_INFO } from '@/lib/constants';
|
||||||
import { getPublishedItems } from '@/lib/cms/data-server';
|
import { getPublishedItems, getPageCopy } from '@/lib/cms/data-server';
|
||||||
import type { Product } from '@/lib/constants/products';
|
import type { Product } from '@/lib/constants/products';
|
||||||
import ProductsContentV3 from './products-content-v3';
|
import ProductsContentV3 from './products-content-v3';
|
||||||
|
|
||||||
@@ -12,9 +12,10 @@ export const metadata: Metadata = {
|
|||||||
export const revalidate = 3600;
|
export const revalidate = 3600;
|
||||||
|
|
||||||
export default async function ProductsPage() {
|
export default async function ProductsPage() {
|
||||||
const [items, standaloneItems] = await Promise.all([
|
const [items, standaloneItems, pageCopy] = await Promise.all([
|
||||||
getPublishedItems('product'),
|
getPublishedItems('product'),
|
||||||
getPublishedItems('standalone-product'),
|
getPublishedItems('standalone-product'),
|
||||||
|
getPageCopy('products').catch(() => null),
|
||||||
]);
|
]);
|
||||||
const products = items.map((item) => ({ ...item.data, id: item.data.id || item.id, slug: item.data.slug || item.slug, title: item.data.title || item.title } as unknown as Product));
|
const products = items.map((item) => ({ ...item.data, id: item.data.id || item.id, slug: item.data.slug || item.slug, title: item.data.title || item.title } as unknown as Product));
|
||||||
const standaloneProducts = standaloneItems.map((item) => ({
|
const standaloneProducts = standaloneItems.map((item) => ({
|
||||||
@@ -25,5 +26,5 @@ export default async function ProductsPage() {
|
|||||||
categoryId: 'specialized',
|
categoryId: 'specialized',
|
||||||
bundle: 'standalone',
|
bundle: 'standalone',
|
||||||
} as unknown as Product));
|
} as unknown as Product));
|
||||||
return <ProductsContentV3 products={[...products, ...standaloneProducts]} />;
|
return <ProductsContentV3 products={[...products, ...standaloneProducts]} pageCopy={pageCopy?.data ?? null} />;
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,8 @@ import { cn } from '@/lib/utils';
|
|||||||
|
|
||||||
const EASE_SPRING = { type: 'spring', stiffness: 300, damping: 30 } as const;
|
const EASE_SPRING = { type: 'spring', stiffness: 300, damping: 30 } as const;
|
||||||
|
|
||||||
|
type PageCopy = Record<string, unknown>;
|
||||||
|
|
||||||
const PRODUCT_ICONS: Record<string, React.ReactNode> = {
|
const PRODUCT_ICONS: Record<string, React.ReactNode> = {
|
||||||
'睿新ERP管理系统': <Package className="w-5 h-5" />,
|
'睿新ERP管理系统': <Package className="w-5 h-5" />,
|
||||||
'睿新商业智能分析平台': <TrendingUp className="w-5 h-5" />,
|
'睿新商业智能分析平台': <TrendingUp className="w-5 h-5" />,
|
||||||
@@ -75,10 +77,18 @@ function StatItem({ value, suffix, label, start, delay }: { value: number; suffi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HeroSection() {
|
function HeroSection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||||
const statsRef = useRef(null);
|
const statsRef = useRef(null);
|
||||||
const isInView = useInView(statsRef, { once: true, margin: '-100px' });
|
const isInView = useInView(statsRef, { once: true, margin: '-100px' });
|
||||||
|
|
||||||
|
const heroTitle1 = (pageCopy?.productsHeroTitle1 as string) || '自研产品矩阵';
|
||||||
|
const heroTitle2 = (pageCopy?.productsHeroTitle2 as string) || '支撑企业';
|
||||||
|
const heroTitle3 = (pageCopy?.productsHeroTitle3 as string) || '数字化核心系统';
|
||||||
|
const heroDescriptionLines = ((pageCopy?.productsHeroDescription as string) ||
|
||||||
|
'从业务系统到数据智能,深度自研、互为补充。\n6 大企业套装与专业产品线,覆盖企业数字化核心场景。').split('\n');
|
||||||
|
const ctaPrimary = (pageCopy?.productsCtaPrimary as string) || '了解我们的服务';
|
||||||
|
const ctaSecondary = (pageCopy?.productsCtaSecondary as string) || '查看行业方案';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
|
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
|
||||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-24 sm:py-28 md:py-32 lg:py-40">
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-24 sm:py-28 md:py-32 lg:py-40">
|
||||||
@@ -89,10 +99,10 @@ function HeroSection() {
|
|||||||
transition={{ duration: 1.2, delay: 0.15, ease: EASE_OUT }}
|
transition={{ duration: 1.2, delay: 0.15, ease: EASE_OUT }}
|
||||||
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-ink leading-[0.92] tracking-tighter mb-8 sm:mb-10 md:mb-12"
|
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-ink leading-[0.92] tracking-tighter mb-8 sm:mb-10 md:mb-12"
|
||||||
>
|
>
|
||||||
<div className="block">自研产品矩阵</div>
|
<div className="block">{heroTitle1}</div>
|
||||||
<div className="block mt-5">
|
<div className="block mt-5">
|
||||||
<span className="relative inline-block">
|
<span className="relative inline-block">
|
||||||
<span className="text-brand font-extrabold">支撑企业</span>
|
<span className="text-brand font-extrabold">{heroTitle2}</span>
|
||||||
<motion.span
|
<motion.span
|
||||||
className="absolute -bottom-3 left-0 w-full h-[3px] bg-brand"
|
className="absolute -bottom-3 left-0 w-full h-[3px] bg-brand"
|
||||||
style={{ transformOrigin: 'left' }}
|
style={{ transformOrigin: 'left' }}
|
||||||
@@ -102,7 +112,7 @@ function HeroSection() {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="block mt-5">数字化核心系统</div>
|
<div className="block mt-5">{heroTitle3}</div>
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
|
|
||||||
<motion.p
|
<motion.p
|
||||||
@@ -111,8 +121,12 @@ function HeroSection() {
|
|||||||
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
|
transition={{ duration: 1, delay: 0.45, ease: EASE_OUT }}
|
||||||
className="text-lg sm:text-xl lg:text-2xl text-text-secondary max-w-2xl leading-relaxed mb-8 sm:mb-10 md:mb-12"
|
className="text-lg sm:text-xl lg:text-2xl text-text-secondary max-w-2xl leading-relaxed mb-8 sm:mb-10 md:mb-12"
|
||||||
>
|
>
|
||||||
从业务系统到数据智能,深度自研、互为补充。
|
{heroDescriptionLines.map((line, i) => (
|
||||||
6 大企业套装与专业产品线,覆盖企业数字化核心场景。
|
<span key={i}>
|
||||||
|
{i > 0 && <br className="hidden sm:block" />}
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -123,12 +137,12 @@ function HeroSection() {
|
|||||||
>
|
>
|
||||||
<Button size="xl" asChild>
|
<Button size="xl" asChild>
|
||||||
<a href="/services">
|
<a href="/services">
|
||||||
了解我们的服务
|
{ctaPrimary}
|
||||||
<ArrowRight className="w-5 h-5 ml-2" />
|
<ArrowRight className="w-5 h-5 ml-2" />
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="xl" variant="outline" asChild>
|
<Button size="xl" variant="outline" asChild>
|
||||||
<a href="/solutions">查看行业方案</a>
|
<a href="/solutions">{ctaSecondary}</a>
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -214,9 +228,12 @@ function ProductCard({ product, size = 'small' }: { product: Product; size?: 'la
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BundleOverview({ products }: { products: Product[] }) {
|
function BundleOverview({ products, pageCopy }: { products: Product[]; pageCopy?: PageCopy | null }) {
|
||||||
const enterpriseProducts = products.filter((p) => p.bundle === 'enterprise');
|
const enterpriseProducts = products.filter((p) => p.bundle === 'enterprise');
|
||||||
const standaloneProducts = products.filter((p) => p.bundle === 'standalone');
|
const standaloneProducts = products.filter((p) => p.bundle === 'standalone');
|
||||||
|
const bundleTitle = (pageCopy?.productsBundleTitle as string) || '两套产品体系\n覆盖不同场景';
|
||||||
|
const bundleTitleLines = bundleTitle.split('\n');
|
||||||
|
const bundleDescription = (pageCopy?.productsBundleDescription as string) || '企业套装以组合形式解决核心管理问题,专业产品聚焦独立场景深度赋能。';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
|
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
|
||||||
@@ -225,10 +242,14 @@ function BundleOverview({ products }: { products: Product[] }) {
|
|||||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
|
||||||
<ScrollReveal className="max-w-3xl mb-12 sm:mb-16 md:mb-20">
|
<ScrollReveal className="max-w-3xl mb-12 sm:mb-16 md:mb-20">
|
||||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-6">
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-6">
|
||||||
两套产品体系,<span className="text-brand">覆盖不同场景</span>
|
{bundleTitleLines.map((line, i) => (
|
||||||
|
<span key={i} className={i === bundleTitleLines.length - 1 ? 'block text-brand' : 'block'}>
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-text-secondary leading-relaxed">
|
<p className="text-lg text-text-secondary leading-relaxed">
|
||||||
企业套装以组合形式解决核心管理问题,专业产品聚焦独立场景深度赋能。
|
{bundleDescription}
|
||||||
</p>
|
</p>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
|
|
||||||
@@ -453,7 +474,9 @@ function SuiteCombosSection({ products }: { products: Product[] }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CTASection() {
|
function CTASection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||||
|
const ctaTitleLines = ((pageCopy?.productsCtaTitle as string) || '以自研产品\n支撑数字化落地').split('\n');
|
||||||
|
const ctaDescription = (pageCopy?.productsCtaDescription as string) || '从企业套装到专业产品,以自研能力与专业服务,陪伴企业完成数字化升级。';
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
|
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
|
||||||
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
|
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
|
||||||
@@ -462,13 +485,15 @@ function CTASection() {
|
|||||||
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
|
<div className="max-w-container mx-auto px-6 lg:px-10 relative z-10">
|
||||||
<ScrollReveal className="text-center max-w-3xl mx-auto">
|
<ScrollReveal className="text-center max-w-3xl mx-auto">
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6 sm:mb-8">
|
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6 sm:mb-8">
|
||||||
以<span className="text-brand">自研产品</span><br />
|
{ctaTitleLines.map((line, i) => (
|
||||||
支撑数字化落地
|
<span key={i} className={i === ctaTitleLines.length - 1 ? 'block text-brand' : 'block'}>
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="text-xl text-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
|
<p className="text-xl text-text-secondary mb-12 max-w-2xl mx-auto leading-relaxed">
|
||||||
从企业套装到专业产品,以自研能力与专业服务,
|
{ctaDescription}
|
||||||
陪伴企业完成数字化升级。
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-wrap justify-center gap-6">
|
<div className="flex flex-wrap justify-center gap-6">
|
||||||
@@ -488,17 +513,17 @@ function CTASection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductsContentV3({ products: productsProp }: { products?: Product[] }) {
|
export default function ProductsContentV3({ products: productsProp, pageCopy }: { products?: Product[]; pageCopy?: PageCopy | null }) {
|
||||||
const products = productsProp ?? [];
|
const products = productsProp ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<HeroSection />
|
<HeroSection pageCopy={pageCopy} />
|
||||||
<BundleOverview products={products} />
|
<BundleOverview products={products} pageCopy={pageCopy} />
|
||||||
<EnterpriseProductsSection products={products} />
|
<EnterpriseProductsSection products={products} />
|
||||||
<SpecializedProductsSection products={products} />
|
<SpecializedProductsSection products={products} />
|
||||||
<SuiteCombosSection products={products} />
|
<SuiteCombosSection products={products} />
|
||||||
<CTASection />
|
<CTASection pageCopy={pageCopy} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ServicesPage from './page';
|
|||||||
|
|
||||||
jest.mock('@/lib/cms/data-server', () => ({
|
jest.mock('@/lib/cms/data-server', () => ({
|
||||||
getPublishedItems: jest.fn(),
|
getPublishedItems: jest.fn(),
|
||||||
|
getPageCopy: jest.fn().mockResolvedValue(null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./services-content-v3', () => ({
|
jest.mock('./services-content-v3', () => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { COMPANY_INFO } from '@/lib/constants';
|
import { COMPANY_INFO } from '@/lib/constants';
|
||||||
import { getPublishedItems } from '@/lib/cms/data-server';
|
import { getPublishedItems, getPageCopy } from '@/lib/cms/data-server';
|
||||||
import type { Service } from '@/lib/constants/services';
|
import type { Service } from '@/lib/constants/services';
|
||||||
import ServicesContentV3 from './services-content-v3';
|
import ServicesContentV3 from './services-content-v3';
|
||||||
|
|
||||||
@@ -15,5 +15,7 @@ export const revalidate = 3600;
|
|||||||
export default async function ServicesPage() {
|
export default async function ServicesPage() {
|
||||||
const items = await getPublishedItems('service');
|
const items = await getPublishedItems('service');
|
||||||
const services = items.map((item) => ({ ...item.data, id: item.data.id || item.id, slug: item.data.slug || item.slug, title: item.data.title || item.title } as unknown as Service));
|
const services = items.map((item) => ({ ...item.data, id: item.data.id || item.id, slug: item.data.slug || item.slug, title: item.data.title || item.title } as unknown as Service));
|
||||||
return <ServicesContentV3 services={services} />;
|
// 服务页结构性文案(Hero/区块标题/CTA/空状态),CMS 未配置时组件内部兜底
|
||||||
|
const pageCopy = await getPageCopy('services').catch(() => null);
|
||||||
|
return <ServicesContentV3 services={services} pageCopy={pageCopy?.data ?? null} />;
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,16 @@ const SERVICE_MODES = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function HeroSection() {
|
type PageCopy = Record<string, unknown>;
|
||||||
|
|
||||||
|
function HeroSection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||||
|
const heroTitle1 = (pageCopy?.servicesHeroTitle1 as string) || '从规划到运维';
|
||||||
|
const heroTitle2 = (pageCopy?.servicesHeroTitle2 as string) || '全程陪伴';
|
||||||
|
const heroDescriptionLines = ((pageCopy?.servicesHeroDescription as string) ||
|
||||||
|
'不只是技术供应商,更是您数字化转型的同行者。\n从第一次沟通到系统稳定运行,我们始终在您身边。').split('\n');
|
||||||
|
const ctaPrimary = (pageCopy?.servicesCtaPrimary as string) || '聊聊您的需求';
|
||||||
|
const ctaSecondary = (pageCopy?.servicesCtaSecondary as string) || '查看产品矩阵';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
|
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
|
||||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
|
||||||
@@ -80,11 +89,11 @@ function HeroSection() {
|
|||||||
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
|
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
|
||||||
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.92] tracking-tightest mb-10 sm:mb-12 md:mb-16"
|
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.92] tracking-tightest mb-10 sm:mb-12 md:mb-16"
|
||||||
>
|
>
|
||||||
<div className="block">从规划到运维</div>
|
<div className="block">{heroTitle1}</div>
|
||||||
<div className="block mt-4 sm:mt-6">
|
<div className="block mt-4 sm:mt-6">
|
||||||
<span className="relative inline-block">
|
<span className="relative inline-block">
|
||||||
<span className="relative text-brand font-black">
|
<span className="relative text-brand font-black">
|
||||||
全程陪伴
|
{heroTitle2}
|
||||||
<motion.span
|
<motion.span
|
||||||
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
|
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
|
||||||
style={{ transformOrigin: 'left' }}
|
style={{ transformOrigin: 'left' }}
|
||||||
@@ -103,9 +112,12 @@ function HeroSection() {
|
|||||||
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
|
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
|
||||||
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12 sm:mb-16"
|
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12 sm:mb-16"
|
||||||
>
|
>
|
||||||
不只是技术供应商,更是您数字化转型的同行者。
|
{heroDescriptionLines.map((line, i) => (
|
||||||
<br className="hidden sm:block" />
|
<span key={i}>
|
||||||
从第一次沟通到系统稳定运行,我们始终在您身边。
|
{i > 0 && <br className="hidden sm:block" />}
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -118,14 +130,14 @@ function HeroSection() {
|
|||||||
href="/contact"
|
href="/contact"
|
||||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
|
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
|
||||||
>
|
>
|
||||||
聊聊您的需求
|
{ctaPrimary}
|
||||||
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/products"
|
href="/products"
|
||||||
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
|
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
|
||||||
>
|
>
|
||||||
查看产品矩阵
|
{ctaSecondary}
|
||||||
</a>
|
</a>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -160,15 +172,17 @@ function HeroSection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ServicesGridSection({ services }: { services?: Service[] }) {
|
function ServicesGridSection({ services, pageCopy }: { services?: Service[]; pageCopy?: PageCopy | null }) {
|
||||||
const displayServices = services?.length ? mapServicesToCards(services) : [];
|
const displayServices = services?.length ? mapServicesToCards(services) : [];
|
||||||
|
const gridTitle = (pageCopy?.servicesGridTitle as string) || '四大核心服务,覆盖数字化全链路';
|
||||||
|
const emptyState = (pageCopy?.servicesEmptyState as string) || '暂无服务数据';
|
||||||
if (displayServices.length === 0) {
|
if (displayServices.length === 0) {
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
|
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
|
||||||
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
|
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
|
||||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-border-primary" />
|
<div className="absolute bottom-0 left-0 right-0 h-px bg-border-primary" />
|
||||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 text-center">
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 text-center">
|
||||||
<p className="text-text-muted text-lg">暂无服务数据</p>
|
<p className="text-text-muted text-lg">{emptyState}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -187,7 +201,7 @@ function ServicesGridSection({ services }: { services?: Service[] }) {
|
|||||||
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
|
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
|
||||||
>
|
>
|
||||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
|
||||||
四大核心服务,<br className="sm:hidden" />覆盖数字化全链路
|
{gridTitle}
|
||||||
</h2>
|
</h2>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -321,7 +335,11 @@ function ProcessModesSection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CTASection() {
|
function CTASection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||||
|
const ctaTitle = (pageCopy?.servicesCtaTitle as string) || '准备好开始了吗?';
|
||||||
|
const ctaDescriptionLines = ((pageCopy?.servicesCtaDescription as string) ||
|
||||||
|
'无论是一个明确的项目,还是一个模糊的想法,我们都愿意坐下来和您一起理清楚。\n首次咨询免费,没有任何销售压力。').split('\n');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
|
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
|
||||||
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
|
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
|
||||||
@@ -335,12 +353,15 @@ function CTASection() {
|
|||||||
className="max-w-3xl"
|
className="max-w-3xl"
|
||||||
>
|
>
|
||||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
|
||||||
准备好开始了吗?
|
{ctaTitle}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-text-secondary leading-relaxed mb-10">
|
<p className="text-lg text-text-secondary leading-relaxed mb-10">
|
||||||
无论是一个明确的项目,还是一个模糊的想法,我们都愿意坐下来和您一起理清楚。
|
{ctaDescriptionLines.map((line, i) => (
|
||||||
<br className="hidden sm:block" />
|
<span key={i}>
|
||||||
首次咨询免费,没有任何销售压力。
|
{i > 0 && <br className="hidden sm:block" />}
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||||
<a
|
<a
|
||||||
@@ -363,13 +384,13 @@ function CTASection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ServicesContentV3({ services }: { services?: Service[] }) {
|
export default function ServicesContentV3({ services, pageCopy }: { services?: Service[]; pageCopy?: PageCopy | null }) {
|
||||||
return (
|
return (
|
||||||
<main className="bg-white text-ink">
|
<main className="bg-white text-ink">
|
||||||
<HeroSection />
|
<HeroSection pageCopy={pageCopy} />
|
||||||
<ServicesGridSection services={services} />
|
<ServicesGridSection services={services} pageCopy={pageCopy} />
|
||||||
<ProcessModesSection />
|
<ProcessModesSection />
|
||||||
<CTASection />
|
<CTASection pageCopy={pageCopy} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import SolutionsPage from './page';
|
|||||||
|
|
||||||
jest.mock('@/lib/cms/data-server', () => ({
|
jest.mock('@/lib/cms/data-server', () => ({
|
||||||
getPublishedItems: jest.fn(),
|
getPublishedItems: jest.fn(),
|
||||||
|
getPageCopy: jest.fn().mockResolvedValue(null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./solutions-content-v3', () => ({
|
jest.mock('./solutions-content-v3', () => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { COMPANY_INFO } from '@/lib/constants';
|
import { COMPANY_INFO } from '@/lib/constants';
|
||||||
import { getPublishedItems } from '@/lib/cms/data-server';
|
import { getPublishedItems, getPageCopy } from '@/lib/cms/data-server';
|
||||||
import type { Solution } from '@/lib/constants/solutions';
|
import type { Solution } from '@/lib/constants/solutions';
|
||||||
import type { Product } from '@/lib/constants/products';
|
import type { Product } from '@/lib/constants/products';
|
||||||
import SolutionsContentV3 from './solutions-content-v3';
|
import SolutionsContentV3 from './solutions-content-v3';
|
||||||
@@ -13,11 +13,12 @@ export const metadata: Metadata = {
|
|||||||
export const revalidate = 3600;
|
export const revalidate = 3600;
|
||||||
|
|
||||||
export default async function SolutionsPage() {
|
export default async function SolutionsPage() {
|
||||||
const [solutionItems, productItems] = await Promise.all([
|
const [solutionItems, productItems, pageCopy] = await Promise.all([
|
||||||
getPublishedItems('solution'),
|
getPublishedItems('solution'),
|
||||||
getPublishedItems('product'),
|
getPublishedItems('product'),
|
||||||
|
getPageCopy('solutions').catch(() => null),
|
||||||
]);
|
]);
|
||||||
const solutions = solutionItems.map((item) => ({ ...item.data, id: item.data.id || item.id, slug: item.data.slug || item.slug, title: item.data.title || item.title } as unknown as Solution));
|
const solutions = solutionItems.map((item) => ({ ...item.data, id: item.data.id || item.id, slug: item.data.slug || item.slug, title: item.data.title || item.title } as unknown as Solution));
|
||||||
const products = productItems.map((item) => ({ ...item.data, id: item.data.id || item.id, slug: item.data.slug || item.slug, title: item.data.title || item.title } as unknown as Product));
|
const products = productItems.map((item) => ({ ...item.data, id: item.data.id || item.id, slug: item.data.slug || item.slug, title: item.data.title || item.title } as unknown as Product));
|
||||||
return <SolutionsContentV3 solutions={solutions} products={products} />;
|
return <SolutionsContentV3 solutions={solutions} products={products} pageCopy={pageCopy?.data ?? null} />;
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,8 @@ import type { Product } from '@/lib/constants/products';
|
|||||||
|
|
||||||
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
|
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
|
||||||
|
|
||||||
|
type PageCopy = Record<string, unknown>;
|
||||||
|
|
||||||
const INDUSTRY_ICONS: Record<string, LucideIcon> = {
|
const INDUSTRY_ICONS: Record<string, LucideIcon> = {
|
||||||
制造业: Factory,
|
制造业: Factory,
|
||||||
贸易零售: ShoppingCart,
|
贸易零售: ShoppingCart,
|
||||||
@@ -17,7 +19,14 @@ const INDUSTRY_ICONS: Record<string, LucideIcon> = {
|
|||||||
物流运输: Truck,
|
物流运输: Truck,
|
||||||
};
|
};
|
||||||
|
|
||||||
function HeroSection() {
|
function HeroSection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||||
|
const heroTitle1 = (pageCopy?.solutionsHeroTitle1 as string) || '专注行业场景的';
|
||||||
|
const heroTitle2 = (pageCopy?.solutionsHeroTitle2 as string) || '解决方案';
|
||||||
|
const heroDescriptionLines = ((pageCopy?.solutionsHeroDescription as string) ||
|
||||||
|
'端到端交付可量化的业务成果。\n基于自研产品矩阵,为制造、零售、教育、医疗、金融、物流六大行业沉淀最佳实践。').split('\n');
|
||||||
|
const ctaPrimary = (pageCopy?.solutionsCtaPrimary as string) || '立即咨询';
|
||||||
|
const ctaSecondary = (pageCopy?.solutionsCtaSecondary as string) || '浏览产品';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
|
<section className="relative min-h-[85vh] flex items-center overflow-hidden bg-white">
|
||||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10 w-full py-28 sm:py-36 md:py-44 lg:py-56">
|
||||||
@@ -28,11 +37,11 @@ function HeroSection() {
|
|||||||
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
|
transition={{ duration: 1, delay: 0.2, ease: EASE_OUT }}
|
||||||
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.92] tracking-tightest mb-10 sm:mb-12 md:mb-16"
|
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-black text-ink leading-[0.92] tracking-tightest mb-10 sm:mb-12 md:mb-16"
|
||||||
>
|
>
|
||||||
<div className="block">专注行业场景的</div>
|
<div className="block">{heroTitle1}</div>
|
||||||
<div className="block mt-4 sm:mt-6">
|
<div className="block mt-4 sm:mt-6">
|
||||||
<span className="relative inline-block">
|
<span className="relative inline-block">
|
||||||
<span className="relative text-brand font-black">
|
<span className="relative text-brand font-black">
|
||||||
解决方案
|
{heroTitle2}
|
||||||
<motion.span
|
<motion.span
|
||||||
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
|
className="absolute -bottom-2 left-0 w-full h-[3px] bg-brand"
|
||||||
style={{ transformOrigin: 'left' }}
|
style={{ transformOrigin: 'left' }}
|
||||||
@@ -51,9 +60,12 @@ function HeroSection() {
|
|||||||
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
|
transition={{ duration: 0.8, delay: 0.5, ease: EASE_OUT }}
|
||||||
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12 sm:mb-16"
|
className="text-lg md:text-xl text-text-secondary max-w-2xl leading-relaxed mb-12 sm:mb-16"
|
||||||
>
|
>
|
||||||
端到端交付可量化的业务成果。
|
{heroDescriptionLines.map((line, i) => (
|
||||||
<br className="hidden sm:block" />
|
<span key={i}>
|
||||||
基于自研产品矩阵,为制造、零售、教育、医疗、金融、物流六大行业沉淀最佳实践。
|
{i > 0 && <br className="hidden sm:block" />}
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -66,14 +78,14 @@ function HeroSection() {
|
|||||||
href="/contact"
|
href="/contact"
|
||||||
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
|
className="group inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base bg-brand text-white transition-all duration-300 hover:bg-brand/90 min-h-[52px] touch-manipulation"
|
||||||
>
|
>
|
||||||
立即咨询
|
{ctaPrimary}
|
||||||
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
<ArrowUpRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/products"
|
href="/products"
|
||||||
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
|
className="inline-flex items-center justify-center gap-3 px-10 py-5 font-semibold text-base text-ink border border-border-primary hover:border-ink/30 transition-all duration-300 min-h-[52px] touch-manipulation"
|
||||||
>
|
>
|
||||||
浏览产品
|
{ctaSecondary}
|
||||||
</a>
|
</a>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -174,7 +186,9 @@ function SolutionCard({ solution, index, products, featured = false }: { solutio
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SolutionsGridSection({ solutions, products }: { solutions: Solution[]; products?: Product[] }) {
|
function SolutionsGridSection({ solutions, products, pageCopy }: { solutions: Solution[]; products?: Product[]; pageCopy?: PageCopy | null }) {
|
||||||
|
const gridTitle = (pageCopy?.solutionsGridTitle as string) || '行业解决方案';
|
||||||
|
const gridDescription = (pageCopy?.solutionsGridDescription as string) || '每个方案都明确推荐产品组合和服务包,确保落地效果';
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
|
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-bg-secondary">
|
||||||
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
|
<div className="absolute top-0 left-0 right-0 h-px bg-border-primary" />
|
||||||
@@ -189,10 +203,10 @@ function SolutionsGridSection({ solutions, products }: { solutions: Solution[];
|
|||||||
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
|
className="max-w-3xl mb-16 sm:mb-20 md:mb-24"
|
||||||
>
|
>
|
||||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95]">
|
||||||
行业解决方案
|
{gridTitle}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
|
<p className="mt-6 text-lg text-text-secondary leading-relaxed">
|
||||||
每个方案都明确推荐产品组合和服务包,确保落地效果
|
{gridDescription}
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -206,7 +220,9 @@ function SolutionsGridSection({ solutions, products }: { solutions: Solution[];
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CTASection() {
|
function CTASection({ pageCopy }: { pageCopy?: PageCopy | null }) {
|
||||||
|
const ctaTitle = (pageCopy?.solutionsCtaTitle as string) || '告诉我们您的行业场景';
|
||||||
|
const ctaDescription = (pageCopy?.solutionsCtaDescription as string) || '无论您处于哪个行业、哪个数字化阶段,我们都能为您推荐最合适的产品组合和服务包。';
|
||||||
return (
|
return (
|
||||||
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
|
<section className="relative py-20 sm:py-28 md:py-36 lg:py-44 overflow-hidden bg-white">
|
||||||
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
|
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10 relative z-10">
|
||||||
@@ -218,10 +234,10 @@ function CTASection() {
|
|||||||
className="max-w-3xl"
|
className="max-w-3xl"
|
||||||
>
|
>
|
||||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-ink tracking-tight leading-[0.95] mb-8">
|
||||||
告诉我们您的<br className="sm:hidden" />行业场景
|
{ctaTitle}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-text-secondary leading-relaxed mb-10">
|
<p className="text-lg text-text-secondary leading-relaxed mb-10">
|
||||||
无论您处于哪个行业、哪个数字化阶段,我们都能为您推荐最合适的产品组合和服务包。
|
{ctaDescription}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||||
<a
|
<a
|
||||||
@@ -244,14 +260,14 @@ function CTASection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SolutionsContentV3({ solutions: solutionsProp, products }: { solutions?: Solution[]; products?: Product[] }) {
|
export default function SolutionsContentV3({ solutions: solutionsProp, products, pageCopy }: { solutions?: Solution[]; products?: Product[]; pageCopy?: PageCopy | null }) {
|
||||||
const solutions = solutionsProp ?? [];
|
const solutions = solutionsProp ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="bg-white text-ink">
|
<main className="bg-white text-ink">
|
||||||
<HeroSection />
|
<HeroSection pageCopy={pageCopy} />
|
||||||
<SolutionsGridSection solutions={solutions} products={products} />
|
<SolutionsGridSection solutions={solutions} products={products} pageCopy={pageCopy} />
|
||||||
<CTASection />
|
<CTASection pageCopy={pageCopy} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -15,7 +15,7 @@ import { BackToTop } from "@/components/ui/back-to-top";
|
|||||||
import { ClientLayout } from "@/components/layout/client-layout";
|
import { ClientLayout } from "@/components/layout/client-layout";
|
||||||
import { getPublishedItems } from "@/lib/cms/data-server";
|
import { getPublishedItems } from "@/lib/cms/data-server";
|
||||||
import { SiteConfigProvider, type SiteConfig, type NavigationItem, type MegaDropdownGroup, type MegaDropdownItem } from "@/lib/site-config";
|
import { SiteConfigProvider, type SiteConfig, type NavigationItem, type MegaDropdownGroup, type MegaDropdownItem } from "@/lib/site-config";
|
||||||
import { COMPANY_INFO } from "@/lib/constants/company";
|
import { COMPANY_INFO, BRAND_NARRATIVE, type BrandPillar } from "@/lib/constants/company";
|
||||||
import { NAVIGATION_V2, MEGA_DROPDOWN_DATA } from "@/lib/constants/navigation";
|
import { NAVIGATION_V2, MEGA_DROPDOWN_DATA } from "@/lib/constants/navigation";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -127,6 +127,11 @@ export default async function RootLayout({
|
|||||||
displayName: (configData?.displayName as string) || COMPANY_INFO.displayName,
|
displayName: (configData?.displayName as string) || COMPANY_INFO.displayName,
|
||||||
slogan: (configData?.slogan as string) || COMPANY_INFO.slogan,
|
slogan: (configData?.slogan as string) || COMPANY_INFO.slogan,
|
||||||
description: (configData?.description as string) || COMPANY_INFO.description,
|
description: (configData?.description as string) || COMPANY_INFO.description,
|
||||||
|
valueProposition: (configData?.valueProposition as string) || BRAND_NARRATIVE.valueProposition,
|
||||||
|
positioningStatement: (configData?.positioningStatement as string) || BRAND_NARRATIVE.positioningStatement,
|
||||||
|
brandPromise: (configData?.brandPromise as string) || BRAND_NARRATIVE.brandPromise,
|
||||||
|
toneOfVoice: (configData?.toneOfVoice as string) || BRAND_NARRATIVE.toneOfVoice,
|
||||||
|
pillars: (Array.isArray(configData?.pillars) ? configData.pillars : BRAND_NARRATIVE.pillars) as BrandPillar[],
|
||||||
founded: (configData?.founded as string) || COMPANY_INFO.founded,
|
founded: (configData?.founded as string) || COMPANY_INFO.founded,
|
||||||
location: (configData?.location as string) || COMPANY_INFO.location,
|
location: (configData?.location as string) || COMPANY_INFO.location,
|
||||||
email: (configData?.email as string) || COMPANY_INFO.email,
|
email: (configData?.email as string) || COMPANY_INFO.email,
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ export function Footer() {
|
|||||||
</StaticLink>
|
</StaticLink>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-300 text-sm leading-relaxed max-w-md">
|
<p className="text-gray-300 text-sm leading-relaxed max-w-md">
|
||||||
|
{config.slogan && (
|
||||||
|
<>
|
||||||
|
<span className="block text-gray-200 font-medium mb-2">{config.slogan}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{config.description}
|
{config.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1578,6 +1578,122 @@ const methodologyFields: FieldDefinition[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const pageCopyFields: FieldDefinition[] = [
|
||||||
|
// === 首页信任区 ===
|
||||||
|
{ name: 'trustEyebrow', type: 'text', label: '信任区眉标' },
|
||||||
|
{ name: 'trustTitle', type: 'text', label: '信任区标题' },
|
||||||
|
{ name: 'trustDescription', type: 'textarea', label: '信任区描述' },
|
||||||
|
{
|
||||||
|
name: 'trustSignals',
|
||||||
|
type: 'array',
|
||||||
|
label: '信任信号',
|
||||||
|
description: '可验证信任信号卡片(数值/标签/描述/来源)',
|
||||||
|
fields: [
|
||||||
|
{ name: 'value', type: 'text', label: '数值', required: true },
|
||||||
|
{ name: 'label', type: 'text', label: '标签', required: true },
|
||||||
|
{ name: 'desc', type: 'textarea', label: '描述', required: true },
|
||||||
|
{ name: 'source', type: 'text', label: '来源', required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: 'earlyAccessLabel', type: 'text', label: '共创状态标签' },
|
||||||
|
{ name: 'earlyAccessDesc', type: 'textarea', label: '共创状态说明' },
|
||||||
|
{ name: 'earlyAccessTitle', type: 'text', label: '共创计划标题' },
|
||||||
|
{ name: 'earlyAccessCtaLabel', type: 'text', label: '共创计划 CTA 文案' },
|
||||||
|
{
|
||||||
|
name: 'earlyAccessStatus',
|
||||||
|
type: 'array',
|
||||||
|
label: '共创计划状态条目',
|
||||||
|
description: '如实呈现共创/内测/授权公开等当前状态',
|
||||||
|
fields: [
|
||||||
|
{ name: 'label', type: 'text', label: '状态名称', required: true },
|
||||||
|
{ name: 'desc', type: 'textarea', label: '状态说明', required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// === 首页叙事区 ===
|
||||||
|
{ name: 'narrativeTitle', type: 'textarea', label: '叙事区标题' },
|
||||||
|
{
|
||||||
|
name: 'narrativeActs',
|
||||||
|
type: 'array',
|
||||||
|
label: '叙事情节(问题→方法→结果)',
|
||||||
|
fields: [
|
||||||
|
{ name: 'number', type: 'text', label: '序号', required: true },
|
||||||
|
{ name: 'title', type: 'text', label: '标题', required: true },
|
||||||
|
{ name: 'desc', type: 'textarea', label: '描述', required: true },
|
||||||
|
{ name: 'href', type: 'text', label: '链接' },
|
||||||
|
{ name: 'cta', type: 'text', label: '按钮文案' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// === 首页服务区 ===
|
||||||
|
{ name: 'servicesTitle', type: 'textarea', label: '服务区标题' },
|
||||||
|
// === 首页案例区 ===
|
||||||
|
{ name: 'casesEyebrow', type: 'text', label: '案例区眉标' },
|
||||||
|
{ name: 'casesTitle', type: 'textarea', label: '案例区标题' },
|
||||||
|
{ name: 'casesDescription', type: 'textarea', label: '案例区空状态描述' },
|
||||||
|
// === 首页 CTA ===
|
||||||
|
{ name: 'ctaTitleLine1', type: 'text', label: 'CTA 标题第一行' },
|
||||||
|
{ name: 'ctaTitleLine2', type: 'text', label: 'CTA 标题第二行' },
|
||||||
|
{ name: 'ctaLabel', type: 'text', label: 'CTA 按钮文案' },
|
||||||
|
// === 服务列表页 ===
|
||||||
|
{ name: 'servicesHeroTitle1', type: 'text', label: '服务页 Hero 标题第一行' },
|
||||||
|
{ name: 'servicesHeroTitle2', type: 'text', label: '服务页 Hero 标题第二行(品牌红)' },
|
||||||
|
{ name: 'servicesHeroDescription', type: 'textarea', label: '服务页 Hero 描述' },
|
||||||
|
{ name: 'servicesCtaPrimary', type: 'text', label: '服务页主 CTA 文案' },
|
||||||
|
{ name: 'servicesCtaSecondary', type: 'text', label: '服务页次 CTA 文案' },
|
||||||
|
{ name: 'servicesGridTitle', type: 'text', label: '服务页区块标题' },
|
||||||
|
{ name: 'servicesEmptyState', type: 'text', label: '服务页空状态文案' },
|
||||||
|
{ name: 'servicesCtaTitle', type: 'text', label: '服务页 CTA 标题' },
|
||||||
|
{ name: 'servicesCtaDescription', type: 'textarea', label: '服务页 CTA 描述' },
|
||||||
|
// === 方案列表页 ===
|
||||||
|
{ name: 'solutionsHeroTitle1', type: 'text', label: '方案页 Hero 标题第一行' },
|
||||||
|
{ name: 'solutionsHeroTitle2', type: 'text', label: '方案页 Hero 标题第二行(品牌红)' },
|
||||||
|
{ name: 'solutionsHeroDescription', type: 'textarea', label: '方案页 Hero 描述' },
|
||||||
|
{ name: 'solutionsCtaPrimary', type: 'text', label: '方案页主 CTA 文案' },
|
||||||
|
{ name: 'solutionsCtaSecondary', type: 'text', label: '方案页次 CTA 文案' },
|
||||||
|
{ name: 'solutionsGridTitle', type: 'text', label: '方案页区块标题' },
|
||||||
|
{ name: 'solutionsGridDescription', type: 'textarea', label: '方案页区块描述' },
|
||||||
|
{ name: 'solutionsCtaTitle', type: 'text', label: '方案页 CTA 标题' },
|
||||||
|
{ name: 'solutionsCtaDescription', type: 'textarea', label: '方案页 CTA 描述' },
|
||||||
|
// === 产品列表页 ===
|
||||||
|
{ name: 'productsHeroTitle1', type: 'text', label: '产品页 Hero 标题第一行' },
|
||||||
|
{ name: 'productsHeroTitle2', type: 'text', label: '产品页 Hero 标题第二行(品牌红)' },
|
||||||
|
{ name: 'productsHeroTitle3', type: 'text', label: '产品页 Hero 标题第三行' },
|
||||||
|
{ name: 'productsHeroDescription', type: 'textarea', label: '产品页 Hero 描述' },
|
||||||
|
{ name: 'productsCtaPrimary', type: 'text', label: '产品页主 CTA 文案' },
|
||||||
|
{ name: 'productsCtaSecondary', type: 'text', label: '产品页次 CTA 文案' },
|
||||||
|
{ name: 'productsBundleTitle', type: 'text', label: '产品页双体系区块标题' },
|
||||||
|
{ name: 'productsBundleDescription', type: 'textarea', label: '产品页双体系区块描述' },
|
||||||
|
{ name: 'productsCtaTitle', type: 'text', label: '产品页 CTA 标题' },
|
||||||
|
{ name: 'productsCtaDescription', type: 'textarea', label: '产品页 CTA 描述' },
|
||||||
|
// === 案例列表页 ===
|
||||||
|
{ name: 'casePageHeroTitle1', type: 'text', label: '案例页 Hero 标题第一行' },
|
||||||
|
{ name: 'casePageHeroTitle2', type: 'text', label: '案例页 Hero 标题第二行(品牌红)' },
|
||||||
|
{ name: 'casePageHeroDescription', type: 'textarea', label: '案例页 Hero 描述' },
|
||||||
|
{ name: 'casePageFilterTitle', type: 'text', label: '案例页筛选区块标题' },
|
||||||
|
{ name: 'casePageFilterDescription', type: 'textarea', label: '案例页筛选区块描述' },
|
||||||
|
// === 新闻列表页 ===
|
||||||
|
{ name: 'newsPageHeroEyebrow', type: 'text', label: '新闻页 Hero 眉标' },
|
||||||
|
{ name: 'newsPageHeroTitle', type: 'textarea', label: '新闻页 Hero 标题(多行,最后一行品牌红)' },
|
||||||
|
{ name: 'newsPageHeroDescription', type: 'textarea', label: '新闻页 Hero 描述' },
|
||||||
|
{ name: 'newsPageEmptyTitle', type: 'text', label: '新闻页空状态标题' },
|
||||||
|
{ name: 'newsPageEmptyDescription', type: 'textarea', label: '新闻页空状态描述' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageCopyConfig: ContentTypeConfig = {
|
||||||
|
model: {
|
||||||
|
id: 'model-page-copy',
|
||||||
|
code: 'page-copy',
|
||||||
|
name: '页面文案',
|
||||||
|
description: '各业务页面结构性文案配置(章节标题/眉标/描述/CTA/空状态)',
|
||||||
|
fields: pageCopyFields,
|
||||||
|
isPageType: false,
|
||||||
|
hasVersions: false,
|
||||||
|
hasDraft: true,
|
||||||
|
icon: 'file-text',
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const newsConfig: ContentTypeConfig = {
|
const newsConfig: ContentTypeConfig = {
|
||||||
model: {
|
model: {
|
||||||
id: 'model-news',
|
id: 'model-news',
|
||||||
@@ -1851,4 +1967,5 @@ export const CONTENT_TYPE_CONFIGS = {
|
|||||||
'contact-page': contactPageConfig,
|
'contact-page': contactPageConfig,
|
||||||
'legal-page': legalPageConfig,
|
'legal-page': legalPageConfig,
|
||||||
methodology: methodologyConfig,
|
methodology: methodologyConfig,
|
||||||
|
'page-copy': pageCopyConfig,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -414,11 +414,27 @@ describe('data-server - CMS 数据访问层', () => {
|
|||||||
|
|
||||||
it('getSolutionBySlug should call getPublishedItemBySlug with solution', async () => {
|
it('getSolutionBySlug should call getPublishedItemBySlug with solution', async () => {
|
||||||
const spy = jest.spyOn(dataServer, 'getPublishedItemBySlug');
|
const spy = jest.spyOn(dataServer, 'getPublishedItemBySlug');
|
||||||
spy.mockResolvedValue(null);
|
spy.mockResolvedValue(null as never);
|
||||||
|
|
||||||
await dataServer.getSolutionBySlug('test-solution');
|
await dataServer.getSolutionBySlug('test-solution');
|
||||||
expect(spy).toHaveBeenCalledWith('solution', 'test-solution');
|
expect(spy).toHaveBeenCalledWith('solution', 'test-solution');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('getPageCopy should call getPublishedItemBySlug with page-copy model', async () => {
|
||||||
|
const spy = jest.spyOn(dataServer, 'getPublishedItemBySlug');
|
||||||
|
spy.mockResolvedValue(null as never);
|
||||||
|
|
||||||
|
await dataServer.getPageCopy('home');
|
||||||
|
expect(spy).toHaveBeenCalledWith('page-copy', 'home');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getHomePageCopy should fetch page-copy for home page code', async () => {
|
||||||
|
const spy = jest.spyOn(dataServer, 'getPublishedItemBySlug');
|
||||||
|
spy.mockResolvedValue(null as never);
|
||||||
|
|
||||||
|
await dataServer.getHomePageCopy();
|
||||||
|
expect(spy).toHaveBeenCalledWith('page-copy', 'home');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getResolvedHomeZones', () => {
|
describe('getResolvedHomeZones', () => {
|
||||||
|
|||||||
@@ -157,6 +157,20 @@ export async function getSolutionBySlug(slug: string) {
|
|||||||
return getPublishedItemBySlug('solution', slug);
|
return getPublishedItemBySlug('solution', slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定页面的结构性文案配置(page-copy 模型,按页面 slug)
|
||||||
|
*/
|
||||||
|
export async function getPageCopy(pageCode: string) {
|
||||||
|
return getPublishedItemBySlug('page-copy', pageCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取首页结构性文案配置(章节标题/信任信号/叙事情节/CTA 的 CMS 覆盖)
|
||||||
|
*/
|
||||||
|
export async function getHomePageCopy() {
|
||||||
|
return getPageCopy('home');
|
||||||
|
}
|
||||||
|
|
||||||
export async function getHomePageZones() {
|
export async function getHomePageZones() {
|
||||||
return getPageZones('home');
|
return getPageZones('home');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ describe('Constants', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have slogan', () => {
|
it('should have slogan', () => {
|
||||||
expect(COMPANY_INFO.slogan).toBe('企业数字化转型的同行者');
|
expect(COMPANY_INFO.slogan).toBe('智连未来 · 成长伙伴 —— 您的数字化转型同行者');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have contact information', () => {
|
it('should have contact information', () => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
COMPANY_INFO,
|
COMPANY_INFO,
|
||||||
|
BRAND_NARRATIVE,
|
||||||
|
type BrandPillar,
|
||||||
TRUST_SIGNALS,
|
TRUST_SIGNALS,
|
||||||
EARLY_ACCESS,
|
EARLY_ACCESS,
|
||||||
NAVIGATION,
|
NAVIGATION,
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ export const COMPANY_INFO = {
|
|||||||
name: '四川睿新致远科技有限公司',
|
name: '四川睿新致远科技有限公司',
|
||||||
shortName: '睿新致遠',
|
shortName: '睿新致遠',
|
||||||
displayName: '睿新致远',
|
displayName: '睿新致远',
|
||||||
slogan: '企业数字化转型的同行者',
|
/**
|
||||||
description: '专业核心团队,以结果导向的服务理念,做企业数字化转型的同行者与成长伙伴',
|
* 品牌主口号(整句采用)。
|
||||||
|
* ★ 全站唯一真源:Hero / Footer / About 等品牌触点统一引用本值,
|
||||||
|
* 并通过 CMS `site-config` 的 slogan 字段覆盖(后台可编辑)。
|
||||||
|
*/
|
||||||
|
slogan: '智连未来 · 成长伙伴 —— 您的数字化转型同行者',
|
||||||
|
/** 公司简介:结果导向 + 同行者定位,与 slogan 语义一致。CMS site-config 可覆盖。 */
|
||||||
|
description:
|
||||||
|
'专业核心团队,以结果导向的服务理念,做中国企业数字化转型的同行者与成长伙伴,从战略诊断到系统落地、再到持续陪跑,让每一份技术投入都转化为可衡量的成长。',
|
||||||
founded: '2026',
|
founded: '2026',
|
||||||
foundedFull: '2026 年 1 月',
|
foundedFull: '2026 年 1 月',
|
||||||
location: '四川省成都市',
|
location: '四川省成都市',
|
||||||
@@ -13,6 +20,59 @@ export const COMPANY_INFO = {
|
|||||||
police: '川公网安备51010602003285号',
|
police: '川公网安备51010602003285号',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export interface BrandPillar {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
tagline: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 品牌叙事内核(Brand Narrative Core)—— 代码侧唯一真源。
|
||||||
|
*
|
||||||
|
* 定义见 docs/brand-narrative-core.md。以下字段均通过 CMS `site-config`
|
||||||
|
* 模型在后台可编辑,代码常量作为未配置 CMS 时的兜底默认值。
|
||||||
|
*
|
||||||
|
* 零编造原则延续:定位、承诺、三支柱均围绕「同行者 + 结果导向 + 成长伙伴」
|
||||||
|
* 展开,不虚构资质/案例/数据。
|
||||||
|
*/
|
||||||
|
export const BRAND_NARRATIVE = {
|
||||||
|
/** L0 一句话价值主张(elevator pitch) */
|
||||||
|
valueProposition: '让数据连接未来,让伙伴陪您成长——我们是您数字化转型的同行者。',
|
||||||
|
/** 品牌定位声明 */
|
||||||
|
positioningStatement:
|
||||||
|
'睿新致远是中国成长型企业的数字化转型同行者:对需要数字化升级但缺乏大型 IT 投入的成长型企业,我们以结果导向的方法论、自研可组合的产品与复合型工程师团队,提供从战略诊断到系统落地、再到持续陪跑的一站式服务,让数字化从「投入」真正变成「成长」。',
|
||||||
|
/** 品牌承诺 */
|
||||||
|
brandPromise:
|
||||||
|
'我们承诺:每一份方案都源于对您业务的理解;每一次交付都以结果说话;每一段合作都以陪您走到成长为终点。',
|
||||||
|
/**
|
||||||
|
* L1 三支柱(品牌叙事信息层级),对应产品 / 方案 / 服务。
|
||||||
|
* HTML 提示:CMS 中以数组形式编辑。
|
||||||
|
*/
|
||||||
|
pillars: [
|
||||||
|
{
|
||||||
|
key: 'strategy',
|
||||||
|
title: '战略同行',
|
||||||
|
tagline: '从诊断到蓝图,先想清楚再动手',
|
||||||
|
description: '以诊断代替推销,先理解您的业务、痛点与约束,再给出可落地的数字化转型路线图。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delivery',
|
||||||
|
title: '落地同行',
|
||||||
|
tagline: '让转型真正落地,让投入可衡量',
|
||||||
|
description: '用自研可组合产品与工程化交付,把蓝图变成可运行的系统,每一分投入都可验证。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'accompany',
|
||||||
|
title: '陪跑同行',
|
||||||
|
tagline: '交付不是终点,成长才是成果',
|
||||||
|
description: '交付后仍持续陪跑,助您沉淀团队能力,让数字化持续产生业务价值。',
|
||||||
|
},
|
||||||
|
] satisfies BrandPillar[],
|
||||||
|
/** 语气语调(内部写作规范,供内容/文案参考,不在页面直接展示) */
|
||||||
|
toneOfVoice: '严谨不说教、专业不冰冷、同行不越位;答案优先、数据先行;可衡量的才说,未成的如实「共创中」。',
|
||||||
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 首页信任层可验证信任信号(L3 Trust & Authority)。
|
* 首页信任层可验证信任信号(L3 Trust & Authority)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { COMPANY_INFO, TRUST_SIGNALS, EARLY_ACCESS } from './company';
|
export { COMPANY_INFO, BRAND_NARRATIVE, TRUST_SIGNALS, EARLY_ACCESS } from './company';
|
||||||
|
export type { BrandPillar } from './company';
|
||||||
export { NAVIGATION, NAVIGATION_V2, MEGA_DROPDOWN_DATA } from './navigation';
|
export { NAVIGATION, NAVIGATION_V2, MEGA_DROPDOWN_DATA } from './navigation';
|
||||||
export type { NavigationItem, NavigationItemV2, MegaDropdownItem, MegaDropdownGroup, MegaDropdownData } from './navigation';
|
export type { NavigationItem, NavigationItemV2, MegaDropdownItem, MegaDropdownGroup, MegaDropdownData } from './navigation';
|
||||||
export { STATS, type StatItem } from './stats';
|
export { STATS, type StatItem } from './stats';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useContext, type ReactNode } from 'react';
|
import { createContext, useContext, type ReactNode } from 'react';
|
||||||
|
import type { BrandPillar } from '@/lib/constants';
|
||||||
|
|
||||||
export interface NavigationItem {
|
export interface NavigationItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -40,6 +41,13 @@ export interface SiteConfig {
|
|||||||
icp: string;
|
icp: string;
|
||||||
police: string;
|
police: string;
|
||||||
|
|
||||||
|
/** Brand narrative core (CMS editable via site-config, fallback to BRAND_NARRATIVE) */
|
||||||
|
valueProposition: string;
|
||||||
|
positioningStatement: string;
|
||||||
|
brandPromise: string;
|
||||||
|
pillars: BrandPillar[];
|
||||||
|
toneOfVoice: string;
|
||||||
|
|
||||||
/** Navigation */
|
/** Navigation */
|
||||||
mainNav: NavigationItem[];
|
mainNav: NavigationItem[];
|
||||||
megaDropdown: Record<string, MegaDropdownGroup[]>;
|
megaDropdown: Record<string, MegaDropdownGroup[]>;
|
||||||
@@ -57,6 +65,11 @@ const DEFAULT_CONFIG: SiteConfig = {
|
|||||||
address: '',
|
address: '',
|
||||||
icp: '',
|
icp: '',
|
||||||
police: '',
|
police: '',
|
||||||
|
valueProposition: '',
|
||||||
|
positioningStatement: '',
|
||||||
|
brandPromise: '',
|
||||||
|
pillars: [],
|
||||||
|
toneOfVoice: '',
|
||||||
mainNav: [],
|
mainNav: [],
|
||||||
megaDropdown: {},
|
megaDropdown: {},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user