From 4bbd6947da7d465724eebda5d03213b04bbe455f Mon Sep 17 00:00:00 2001 From: Zhengen TANG Date: Fri, 7 Aug 2026 15:59:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=A8=E5=89=A9=E4=BD=99=20?= =?UTF-8?q?6=20=E4=B8=AA=20StreamField=20Block=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=20(ProductCard/Pricing/Timeline/Team/TechSta?= =?UTF-8?q?ck/Video)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - types/wagtail.ts 新增对应 6 个 Block 值类型接口及 StreamFieldBlock 联合类型分支 - 新建 6 个渲染组件:ProductCardGrid/Pricing/Timeline/Team/TechStack/Video,风格与既有 Block 一致 - BlockRenderer.tsx 映射表接入新组件,key 与后端 COMMON_BLOCKS 保持一致 - lint/build 均验证通过 --- blocks/BlockRenderer.tsx | 14 ++++- blocks/pricing/Pricing.tsx | 51 ++++++++++++++++++ blocks/product-card/ProductCardGrid.tsx | 46 ++++++++++++++++ blocks/team/Team.tsx | 40 ++++++++++++++ blocks/tech-stack/TechStack.tsx | 28 ++++++++++ blocks/timeline/Timeline.tsx | 24 +++++++++ blocks/video/Video.tsx | 42 +++++++++++++++ types/wagtail.ts | 70 +++++++++++++++++++++++++ 8 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 blocks/pricing/Pricing.tsx create mode 100644 blocks/product-card/ProductCardGrid.tsx create mode 100644 blocks/team/Team.tsx create mode 100644 blocks/tech-stack/TechStack.tsx create mode 100644 blocks/timeline/Timeline.tsx create mode 100644 blocks/video/Video.tsx diff --git a/blocks/BlockRenderer.tsx b/blocks/BlockRenderer.tsx index 32d05d4..904c26a 100644 --- a/blocks/BlockRenderer.tsx +++ b/blocks/BlockRenderer.tsx @@ -7,13 +7,19 @@ import CTA from "./cta/CTA"; import LogoCloud from "./logo-cloud/LogoCloud"; import CaseStudyCard from "./case-study/CaseStudyCard"; import LeadForm from "./form/LeadForm"; +import ProductCardGrid from "./product-card/ProductCardGrid"; +import Pricing from "./pricing/Pricing"; +import Timeline from "./timeline/Timeline"; +import Team from "./team/Team"; +import TechStack from "./tech-stack/TechStack"; +import Video from "./video/Video"; import type { StreamFieldBlock } from "@/types/wagtail"; /** * StreamField 类型 → 组件映射表。 * 必须与后端 apps/core/blocks.py 的 COMMON_BLOCKS 保持一致, * 否则新增/重命名 Block 时前后端会出现字段不对齐问题 - * (详见 documents/设计方案分析与完善版.md §2.6)。 + * (详见 documents/设计方案分析与完善版.md §2.6)。 */ // eslint-disable-next-line @typescript-eslint/no-explicit-any const components: Record> = { @@ -25,6 +31,12 @@ const components: Record> = { logo_cloud: LogoCloud, case_study: CaseStudyCard, form: LeadForm, + product_card: ProductCardGrid, + pricing: Pricing, + timeline: Timeline, + team: Team, + tech_stack: TechStack, + video: Video, }; export function BlockRenderer({ blocks }: { blocks: StreamFieldBlock[] }) { diff --git a/blocks/pricing/Pricing.tsx b/blocks/pricing/Pricing.tsx new file mode 100644 index 0000000..b5d8e12 --- /dev/null +++ b/blocks/pricing/Pricing.tsx @@ -0,0 +1,51 @@ +import Link from "next/link"; +import type { PricingBlockValue } from "@/types/wagtail"; + +export default function Pricing({ value }: { value: PricingBlockValue }) { + return ( +
+
+ {value.heading && ( +

+ {value.heading} +

+ )} +
+ {value.plans?.map((plan, index) => ( +
+

+ {plan.name} +

+
{plan.price}
+
    + {plan.features?.map((feature, featureIndex) => ( +
  • + {feature} +
  • + ))} +
+ {plan.button && ( + + {plan.button.text} + + )} +
+ ))} +
+
+
+ ); +} diff --git a/blocks/product-card/ProductCardGrid.tsx b/blocks/product-card/ProductCardGrid.tsx new file mode 100644 index 0000000..38f3193 --- /dev/null +++ b/blocks/product-card/ProductCardGrid.tsx @@ -0,0 +1,46 @@ +import Link from "next/link"; +import Image from "next/image"; +import type { ProductCardBlockValue } from "@/types/wagtail"; + +export default function ProductCardGrid({ value }: { value: ProductCardBlockValue }) { + return ( +
+
+ {value.heading && ( +

{value.heading}

+ )} +
+ {value.items?.map((item, index) => { + const card = ( +
+ {item.image && ( + {item.image.title} + )} +

+ {item.title} +

+ {item.description && ( +

{item.description}

+ )} +
+ ); + const key = `${item.title}-${index}`; + return item.link ? ( + + {card} + + ) : ( +
{card}
+ ); + })} +
+
+
+ ); +} diff --git a/blocks/team/Team.tsx b/blocks/team/Team.tsx new file mode 100644 index 0000000..417a180 --- /dev/null +++ b/blocks/team/Team.tsx @@ -0,0 +1,40 @@ +import Image from "next/image"; +import type { TeamBlockValue } from "@/types/wagtail"; + +export default function Team({ value }: { value: TeamBlockValue }) { + return ( +
+
+ {value.heading && ( +

{value.heading}

+ )} +
+ {value.members?.map((member) => ( +
+ {member.photo ? ( + {member.photo.title} + ) : ( +
+ )} +
+ {member.name} +
+ {member.role && ( +
{member.role}
+ )} + {member.bio && ( +

{member.bio}

+ )} +
+ ))} +
+
+
+ ); +} diff --git a/blocks/tech-stack/TechStack.tsx b/blocks/tech-stack/TechStack.tsx new file mode 100644 index 0000000..81985ca --- /dev/null +++ b/blocks/tech-stack/TechStack.tsx @@ -0,0 +1,28 @@ +import type { TechStackBlockValue } from "@/types/wagtail"; + +export default function TechStack({ value }: { value: TechStackBlockValue }) { + return ( +
+
+ {value.heading && ( +

+ {value.heading} +

+ )} +
+ {value.items?.map((item, index) => ( +
+ {item.icon && {item.icon}} + + {item.label} + +
+ ))} +
+
+
+ ); +} diff --git a/blocks/timeline/Timeline.tsx b/blocks/timeline/Timeline.tsx new file mode 100644 index 0000000..21566ba --- /dev/null +++ b/blocks/timeline/Timeline.tsx @@ -0,0 +1,24 @@ +import type { TimelineBlockValue } from "@/types/wagtail"; + +export default function Timeline({ value }: { value: TimelineBlockValue }) { + return ( +
+
+ {value.heading && ( +

{value.heading}

+ )} +
    + {value.items?.map((item, index) => ( +
  1. + +
    + {item.year} +
    +

    {item.event}

    +
  2. + ))} +
+
+
+ ); +} diff --git a/blocks/video/Video.tsx b/blocks/video/Video.tsx new file mode 100644 index 0000000..1315b7b --- /dev/null +++ b/blocks/video/Video.tsx @@ -0,0 +1,42 @@ +import type { VideoBlockValue } from "@/types/wagtail"; + +const DIRECT_VIDEO_EXTENSIONS = [".mp4", ".webm", ".ogg"]; + +export default function Video({ value }: { value: VideoBlockValue }) { + if (!value.video_url) { + return null; + } + + const isDirectFile = DIRECT_VIDEO_EXTENSIONS.some((ext) => + value.video_url.toLowerCase().includes(ext) + ); + + return ( +
+
+ {value.heading && ( +

+ {value.heading} +

+ )} +
+ {isDirectFile ? ( +