288 lines
8.6 KiB
Python
288 lines
8.6 KiB
Python
"""
|
||
公共抽象模型。所有业务页面应继承 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 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
|
||
from wagtail_headless_preview.models import HeadlessPreviewMixin
|
||
|
||
|
||
class SEOablePage(HeadlessPreviewMixin, 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 = "站点设置"
|