Files
wagtailcms/apps/core/models.py
T
Zhengen 8fbbd2e6b0 feat: 新增Snippet全局内容模型(TeamMember/Testimonial/Partner/NavigationMenu/SiteSettings)
- 新增 apps.core.TeamMember/Testimonial/Partner:简单 register_snippet,含 order 排序字段
- 新增 apps.core.NavigationMenu+NavigationMenuItem:ClusterableModel+Orderable+InlinePanel,
  支持内部页面或外部链接二选一
- 新增 apps.core.SiteSettings:接入 wagtail.contrib.settings + BaseSiteSetting,
  存放公司信息/ICP备案号/社交账号等全局配置
- 新增只读 API:/api/v1/custom/core/{team,testimonials,partners,navigation,site-settings}/
- 补充 apps/core/tests.py 单元测试,pytest 用例由 11 个增至 24 个,全部通过
- 更新设计文档 §2.16 Phase 1 进度清单
2026-08-07 13:06:22 +08:00

287 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
公共抽象模型。所有业务页面应继承 SEOablePage 而非直接继承 wagtail.models.Page
以统一获得 SEO 字段(详见 documents/设计方案分析与完善版.md §2.5)。
本文件同时存放跨页面复用的全局 SnippetTeamMember/Testimonial/Partner/
NavigationMenu/SiteSettings(详见 documents/设计方案分析与完善版.md §2.5、§2.16)。
"""
from django.db import models
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
from wagtail.contrib.settings.models import BaseSiteSetting
from wagtail.contrib.settings.registry import register_setting
from wagtail.fields import RichTextField
from wagtail.models import Orderable, Page
from wagtail.snippets.models import register_snippet
class SEOablePage(Page):
seo_title_override = models.CharField(
max_length=70, blank=True, verbose_name="SEO 标题",
help_text="留空则使用页面标题",
)
seo_description_override = models.CharField(
max_length=160, blank=True, verbose_name="SEO 描述",
)
og_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
verbose_name="社交分享图",
)
canonical_url = models.URLField(blank=True, verbose_name="Canonical URL")
schema_json = models.JSONField(blank=True, default=dict, verbose_name="结构化数据 (JSON-LD)")
promote_panels = Page.promote_panels + [
FieldPanel("seo_title_override"),
FieldPanel("seo_description_override"),
FieldPanel("og_image"),
FieldPanel("canonical_url"),
FieldPanel("schema_json"),
]
class Meta:
abstract = True
class SimpleContentPage(SEOablePage):
"""通用富文本内容页,用于隐私政策、服务条款等法务/说明类页面。"""
body = RichTextField(blank=True, verbose_name="正文内容")
content_panels = SEOablePage.content_panels + [
FieldPanel("body"),
]
parent_page_types = ["home.HomePage"]
subpage_types = []
class Meta:
verbose_name = "通用内容页"
@register_snippet
class TeamMember(models.Model):
"""团队成员,用于关于我们/团队介绍等场景复用。"""
name = models.CharField(max_length=50, verbose_name="姓名")
role = models.CharField(max_length=100, blank=True, verbose_name="职位")
photo = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
verbose_name="照片",
)
bio = models.TextField(blank=True, verbose_name="简介")
order = models.IntegerField(default=0, verbose_name="排序")
panels = [
FieldPanel("name"),
FieldPanel("role"),
FieldPanel("photo"),
FieldPanel("bio"),
FieldPanel("order"),
]
class Meta:
ordering = ["order", "id"]
verbose_name = "团队成员"
verbose_name_plural = "团队成员"
def __str__(self):
return self.name
@register_snippet
class Testimonial(models.Model):
"""客户证言/推荐语,用于首页、案例页等场景复用。"""
quote = models.TextField(verbose_name="推荐语")
author_name = models.CharField(max_length=50, verbose_name="客户姓名")
author_title = models.CharField(
max_length=100, blank=True, verbose_name="客户职位/公司"
)
author_photo = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
verbose_name="客户头像",
)
order = models.IntegerField(default=0, verbose_name="排序")
panels = [
FieldPanel("quote"),
FieldPanel("author_name"),
FieldPanel("author_title"),
FieldPanel("author_photo"),
FieldPanel("order"),
]
class Meta:
ordering = ["order", "id"]
verbose_name = "客户证言"
verbose_name_plural = "客户证言"
def __str__(self):
return f"{self.author_name} - {self.quote[:20]}"
@register_snippet
class Partner(models.Model):
"""合作伙伴/客户 Logo,用于 Logo 墙展示。"""
name = models.CharField(max_length=100, verbose_name="名称")
logo = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
verbose_name="Logo",
)
website_url = models.URLField(blank=True, verbose_name="官网链接")
order = models.IntegerField(default=0, verbose_name="排序")
panels = [
FieldPanel("name"),
FieldPanel("logo"),
FieldPanel("website_url"),
FieldPanel("order"),
]
class Meta:
ordering = ["order", "id"]
verbose_name = "合作伙伴"
verbose_name_plural = "合作伙伴"
def __str__(self):
return self.name
@register_snippet
class NavigationMenu(ClusterableModel):
"""导航菜单容器,通过 name 区分主导航/页脚导航等多套菜单。"""
name = models.CharField(
max_length=50,
unique=True,
verbose_name="菜单标识",
help_text="前端按此标识获取菜单,如 main(主导航)、footer(页脚导航)",
)
panels = [
FieldPanel("name"),
InlinePanel("items", label="菜单项"),
]
class Meta:
verbose_name = "导航菜单"
verbose_name_plural = "导航菜单"
def __str__(self):
return self.name
class NavigationMenuItem(Orderable):
"""导航菜单项:可链接到内部页面,也可填写外部链接。"""
menu = ParentalKey(
NavigationMenu, on_delete=models.CASCADE, related_name="items"
)
label = models.CharField(max_length=50, verbose_name="文案")
link_page = models.ForeignKey(
Page,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
verbose_name="内部页面",
)
link_url = models.CharField(
max_length=255,
blank=True,
verbose_name="外部链接",
help_text="留空则使用内部页面链接",
)
open_in_new_tab = models.BooleanField(default=False, verbose_name="新标签页打开")
panels = [
FieldPanel("label"),
FieldPanel("link_page"),
FieldPanel("link_url"),
FieldPanel("open_in_new_tab"),
]
@property
def url(self):
if self.link_url:
return self.link_url
if self.link_page_id:
return self.link_page.url
return "#"
def __str__(self):
return self.label
@register_setting(icon="cogs")
class SiteSettings(BaseSiteSetting):
"""站点全局配置:公司信息/联系方式/备案号/社交账号,供 Footer 等全局位置消费。"""
company_name = models.CharField(max_length=100, blank=True, verbose_name="公司全称")
contact_phone = models.CharField(max_length=30, blank=True, verbose_name="联系电话")
contact_email = models.EmailField(blank=True, verbose_name="联系邮箱")
address = models.CharField(max_length=255, blank=True, verbose_name="公司地址")
icp_number = models.CharField(max_length=50, blank=True, verbose_name="ICP备案号")
icp_url = models.URLField(
default="https://beian.miit.gov.cn/", verbose_name="备案查询链接"
)
wechat_qrcode = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
verbose_name="微信公众号二维码",
)
wechat_account = models.CharField(
max_length=100, blank=True, verbose_name="微信公众号名称"
)
weibo_url = models.URLField(blank=True, verbose_name="微博主页")
panels = [
MultiFieldPanel(
[
FieldPanel("company_name"),
FieldPanel("contact_phone"),
FieldPanel("contact_email"),
FieldPanel("address"),
],
heading="联系方式",
),
MultiFieldPanel(
[FieldPanel("icp_number"), FieldPanel("icp_url")],
heading="备案信息",
),
MultiFieldPanel(
[
FieldPanel("wechat_qrcode"),
FieldPanel("wechat_account"),
FieldPanel("weibo_url"),
],
heading="社交账号",
),
]
class Meta:
verbose_name = "站点设置"