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 进度清单
This commit is contained in:
+233
-2
@@ -1,11 +1,19 @@
|
||||
"""
|
||||
公共抽象模型。所有业务页面应继承 SEOablePage 而非直接继承 wagtail.models.Page,
|
||||
以统一获得 SEO 字段(详见 documents/设计方案分析与完善版.md §2.5)。
|
||||
|
||||
本文件同时存放跨页面复用的全局 Snippet:TeamMember/Testimonial/Partner/
|
||||
NavigationMenu/SiteSettings(详见 documents/设计方案分析与完善版.md §2.5、§2.16)。
|
||||
"""
|
||||
from django.db import models
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
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 Page
|
||||
from wagtail.models import Orderable, Page
|
||||
from wagtail.snippets.models import register_snippet
|
||||
|
||||
|
||||
class SEOablePage(Page):
|
||||
@@ -53,3 +61,226 @@ class SimpleContentPage(SEOablePage):
|
||||
|
||||
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 = "站点设置"
|
||||
|
||||
Reference in New Issue
Block a user