""" 公共抽象模型。所有业务页面应继承 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 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 = "站点设置"