- apps/core/blocks.py 新增 ProductCardBlock/PricingBlock/TimelineBlock/TeamBlock/TechStackBlock/VideoBlock,COMMON_BLOCKS 扩充至 14 个内容 Block - TeamBlock 新增 TeamMemberChooserBlock(SnippetChooserBlock),复用 FormBlock 已有的 get_api_representation 展开模式,展开 TeamMember 完整字段(含 serialize_image 复用) - 为 blog/cases/home/products/solutions 五个 Page 模型生成并应用 alter field body 迁移(StreamField block 定义变更会写入迁移 state) - apps/core/tests.py 新增 TeamMemberChooserBlock 空值/展开单元测试,pytest 33 个用例全部通过 - 设计文档 §2.6/§2.16 同步更新为已实现
280 lines
9.0 KiB
Python
280 lines
9.0 KiB
Python
"""
|
|
可复用的 StreamField Block 库。
|
|
参见 documents/设计方案分析与完善版.md §2.6 组件清单。
|
|
"""
|
|
from django.utils.translation import gettext_lazy as _
|
|
from wagtail import blocks
|
|
from wagtail.images.blocks import ImageChooserBlock
|
|
from wagtail.snippets.blocks import SnippetChooserBlock
|
|
|
|
from .serializers import serialize_image
|
|
|
|
|
|
class CTAButtonBlock(blocks.StructBlock):
|
|
text = blocks.CharBlock(max_length=50, label=_("按钮文案"))
|
|
link = blocks.URLBlock(label=_("按钮链接"))
|
|
|
|
class Meta:
|
|
icon = "link"
|
|
label = _("按钮")
|
|
|
|
|
|
class HeroBlock(blocks.StructBlock):
|
|
title = blocks.CharBlock(max_length=120, label=_("标题"))
|
|
subtitle = blocks.TextBlock(required=False, label=_("副标题"))
|
|
background_image = ImageChooserBlock(required=False, label=_("背景图"))
|
|
buttons = blocks.ListBlock(CTAButtonBlock(), label=_("按钮组"))
|
|
|
|
class Meta:
|
|
icon = "image"
|
|
label = _("首屏 Hero")
|
|
template = "core/blocks/hero_block.html"
|
|
|
|
|
|
class StatItemBlock(blocks.StructBlock):
|
|
value = blocks.CharBlock(max_length=20, label=_("数值"))
|
|
label = blocks.CharBlock(max_length=50, label=_("说明文字"))
|
|
|
|
|
|
class StatsBlock(blocks.StructBlock):
|
|
items = blocks.ListBlock(StatItemBlock(), label=_("数据项"))
|
|
|
|
class Meta:
|
|
icon = "site"
|
|
label = _("数据展示")
|
|
template = "core/blocks/stats_block.html"
|
|
|
|
|
|
class FeatureBlock(blocks.StructBlock):
|
|
icon = blocks.CharBlock(max_length=50, required=False, label=_("图标"))
|
|
title = blocks.CharBlock(max_length=100, label=_("标题"))
|
|
description = blocks.TextBlock(label=_("描述"))
|
|
|
|
class Meta:
|
|
icon = "pick"
|
|
label = _("能力/优势")
|
|
template = "core/blocks/feature_block.html"
|
|
|
|
|
|
class FeatureGridBlock(blocks.StructBlock):
|
|
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
|
items = blocks.ListBlock(FeatureBlock(), label=_("能力列表"))
|
|
|
|
class Meta:
|
|
icon = "grip"
|
|
label = _("能力网格")
|
|
|
|
|
|
class FAQItemBlock(blocks.StructBlock):
|
|
question = blocks.CharBlock(max_length=200, label=_("问题"))
|
|
answer = blocks.RichTextBlock(label=_("回答"))
|
|
|
|
|
|
class FAQBlock(blocks.StructBlock):
|
|
items = blocks.ListBlock(FAQItemBlock(), label=_("问答列表"))
|
|
|
|
class Meta:
|
|
icon = "help"
|
|
label = _("常见问题")
|
|
template = "core/blocks/faq_block.html"
|
|
|
|
|
|
class CTABlock(blocks.StructBlock):
|
|
heading = blocks.CharBlock(max_length=120, label=_("标题"))
|
|
description = blocks.TextBlock(required=False, label=_("描述"))
|
|
button = CTAButtonBlock(label=_("按钮"))
|
|
|
|
class Meta:
|
|
icon = "plus"
|
|
label = _("行动号召 CTA")
|
|
template = "core/blocks/cta_block.html"
|
|
|
|
|
|
class LogoCloudBlock(blocks.StructBlock):
|
|
heading = blocks.CharBlock(max_length=100, required=False, label=_("标题"))
|
|
logos = blocks.ListBlock(ImageChooserBlock(), label=_("Logo 列表"))
|
|
|
|
class Meta:
|
|
icon = "image"
|
|
label = _("客户 Logo 墙")
|
|
template = "core/blocks/logo_cloud_block.html"
|
|
|
|
|
|
class CaseStudyBlock(blocks.StructBlock):
|
|
case_page = blocks.PageChooserBlock(
|
|
page_type="cases.CaseStudyPage", label=_("案例页面")
|
|
)
|
|
summary = blocks.CharBlock(max_length=250, required=False, label=_("摘要覆盖"))
|
|
|
|
class Meta:
|
|
icon = "doc-full"
|
|
label = _("客户案例卡片")
|
|
|
|
|
|
class FormBlock(blocks.StructBlock):
|
|
"""嵌入可复用的线索表单(apps.forms.FormDefinition Snippet)。"""
|
|
|
|
form = SnippetChooserBlock("forms.FormDefinition", label=_("选择表单"))
|
|
|
|
class Meta:
|
|
icon = "form"
|
|
label = _("线索表单")
|
|
|
|
def get_api_representation(self, value, context=None):
|
|
"""
|
|
默认 SnippetChooserBlock 只会序列化为主键,前端渲染动态表单需要完整字段定义,
|
|
因此这里手动展开 FormDefinition 及其子字段(详见 apps/forms/models.py)。
|
|
"""
|
|
form_def = value.get("form")
|
|
if form_def is None:
|
|
return {"form": None}
|
|
return {
|
|
"form": {
|
|
"id": form_def.pk,
|
|
"name": form_def.name,
|
|
"submit_button_text": form_def.submit_button_text,
|
|
"success_message": form_def.success_message,
|
|
"fields": [
|
|
{
|
|
"label": field.label,
|
|
"field_key": field.field_key,
|
|
"field_type": field.field_type,
|
|
"required": field.required,
|
|
"choices": [
|
|
choice.strip()
|
|
for choice in field.choices.split(",")
|
|
if choice.strip()
|
|
],
|
|
}
|
|
for field in form_def.fields.all()
|
|
],
|
|
}
|
|
}
|
|
|
|
|
|
class ProductCardItemBlock(blocks.StructBlock):
|
|
title = blocks.CharBlock(max_length=100, label=_("标题"))
|
|
description = blocks.TextBlock(required=False, label=_("描述"))
|
|
image = ImageChooserBlock(required=False, label=_("图片"))
|
|
link = blocks.URLBlock(required=False, label=_("链接"))
|
|
|
|
|
|
class ProductCardBlock(blocks.StructBlock):
|
|
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
|
items = blocks.ListBlock(ProductCardItemBlock(), label=_("产品列表"))
|
|
|
|
class Meta:
|
|
icon = "grip"
|
|
label = _("产品矩阵")
|
|
|
|
|
|
class PricingPlanBlock(blocks.StructBlock):
|
|
name = blocks.CharBlock(max_length=50, label=_("方案名称"))
|
|
price = blocks.CharBlock(max_length=50, label=_("价格"))
|
|
features = blocks.ListBlock(
|
|
blocks.CharBlock(max_length=100), label=_("功能列表")
|
|
)
|
|
highlighted = blocks.BooleanBlock(
|
|
required=False, default=False, label=_("突出显示")
|
|
)
|
|
button = CTAButtonBlock(required=False, label=_("按钮"))
|
|
|
|
|
|
class PricingBlock(blocks.StructBlock):
|
|
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
|
plans = blocks.ListBlock(PricingPlanBlock(), label=_("定价方案"))
|
|
|
|
class Meta:
|
|
icon = "tag"
|
|
label = _("定价")
|
|
|
|
|
|
class TimelineItemBlock(blocks.StructBlock):
|
|
year = blocks.CharBlock(max_length=20, label=_("年份/时间"))
|
|
event = blocks.TextBlock(label=_("事件描述"))
|
|
|
|
|
|
class TimelineBlock(blocks.StructBlock):
|
|
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
|
items = blocks.ListBlock(TimelineItemBlock(), label=_("时间线条目"))
|
|
|
|
class Meta:
|
|
icon = "date"
|
|
label = _("发展历程")
|
|
|
|
|
|
class TeamMemberChooserBlock(SnippetChooserBlock):
|
|
"""选择 apps.core.TeamMember Snippet,展开完整字段供前端渲染(而非仅主键)。"""
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__("core.TeamMember", **kwargs)
|
|
|
|
def get_api_representation(self, value, context=None):
|
|
if value is None:
|
|
return None
|
|
return {
|
|
"id": value.pk,
|
|
"name": value.name,
|
|
"role": value.role,
|
|
"bio": value.bio,
|
|
"photo": serialize_image(value.photo),
|
|
}
|
|
|
|
|
|
class TeamBlock(blocks.StructBlock):
|
|
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
|
members = blocks.ListBlock(TeamMemberChooserBlock(), label=_("团队成员"))
|
|
|
|
class Meta:
|
|
icon = "group"
|
|
label = _("团队展示")
|
|
|
|
|
|
class TechStackItemBlock(blocks.StructBlock):
|
|
icon = blocks.CharBlock(max_length=50, required=False, label=_("图标"))
|
|
label = blocks.CharBlock(max_length=50, label=_("名称"))
|
|
|
|
|
|
class TechStackBlock(blocks.StructBlock):
|
|
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
|
items = blocks.ListBlock(TechStackItemBlock(), label=_("技术项"))
|
|
|
|
class Meta:
|
|
icon = "cogs"
|
|
label = _("技术架构")
|
|
|
|
|
|
class VideoBlock(blocks.StructBlock):
|
|
heading = blocks.CharBlock(max_length=100, required=False, label=_("标题"))
|
|
video_url = blocks.URLBlock(
|
|
label=_("视频地址"),
|
|
help_text=_(
|
|
"建议使用自建 OSS/COS 存储的 mp4 直链,或腾讯视频/哔哩哔哩等国内平台的"
|
|
"嵌入地址,避免使用 YouTube/Vimeo(详见 §2.15 国内可访问性要求)"
|
|
),
|
|
)
|
|
poster = ImageChooserBlock(required=False, label=_("封面图"))
|
|
|
|
class Meta:
|
|
icon = "media"
|
|
label = _("视频")
|
|
|
|
|
|
COMMON_BLOCKS = [
|
|
("hero", HeroBlock()),
|
|
("stats", StatsBlock()),
|
|
("feature_grid", FeatureGridBlock()),
|
|
("faq", FAQBlock()),
|
|
("cta", CTABlock()),
|
|
("logo_cloud", LogoCloudBlock()),
|
|
("case_study", CaseStudyBlock()),
|
|
("form", FormBlock()),
|
|
("product_card", ProductCardBlock()),
|
|
("pricing", PricingBlock()),
|
|
("timeline", TimelineBlock()),
|
|
("team", TeamBlock()),
|
|
("tech_stack", TechStackBlock()),
|
|
("video", VideoBlock()),
|
|
("richtext", blocks.RichTextBlock(label=_("富文本"))),
|
|
]
|