chore: initial commit of Wagtail backend scaffold

This commit is contained in:
2026-08-06 15:56:14 +08:00
commit 7f51a31150
51 changed files with 1239 additions and 0 deletions
View File
+8
View File
@@ -0,0 +1,8 @@
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.blog"
label = "blog"
verbose_name = "技术博客"
+72
View File
@@ -0,0 +1,72 @@
# Generated by Django 6.0.5 on 2026-08-06 05:29
import django.db.models.deletion
import modelcluster.contrib.taggit
import modelcluster.fields
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'),
('wagtailcore', '0097_baselogentry_uuid_action_timestamp_indexes'),
('wagtailimages', '0027_image_description'),
]
operations = [
migrations.CreateModel(
name='BlogIndexPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
('seo_title_override', models.CharField(blank=True, help_text='留空则使用页面标题', max_length=70, verbose_name='SEO 标题')),
('seo_description_override', models.CharField(blank=True, max_length=160, verbose_name='SEO 描述')),
('canonical_url', models.URLField(blank=True, verbose_name='Canonical URL')),
('schema_json', models.JSONField(blank=True, default=dict, verbose_name='结构化数据 (JSON-LD)')),
('intro', models.TextField(blank=True, verbose_name='栏目简介')),
('og_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image', verbose_name='社交分享图')),
],
options={
'verbose_name': '博客栏目页',
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='BlogPage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
('seo_title_override', models.CharField(blank=True, help_text='留空则使用页面标题', max_length=70, verbose_name='SEO 标题')),
('seo_description_override', models.CharField(blank=True, max_length=160, verbose_name='SEO 描述')),
('canonical_url', models.URLField(blank=True, verbose_name='Canonical URL')),
('schema_json', models.JSONField(blank=True, default=dict, verbose_name='结构化数据 (JSON-LD)')),
('published_at', models.DateTimeField(verbose_name='发布时间')),
('intro', models.CharField(blank=True, max_length=250, verbose_name='摘要')),
('body', wagtail.fields.StreamField([('hero', 7), ('stats', 12), ('feature_grid', 19), ('faq', 24), ('cta', 27), ('logo_cloud', 31), ('richtext', 32)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'label': '标题', 'max_length': 120}), 1: ('wagtail.blocks.TextBlock', (), {'label': '副标题', 'required': False}), 2: ('wagtail.images.blocks.ImageChooserBlock', (), {'label': '背景图', 'required': False}), 3: ('wagtail.blocks.CharBlock', (), {'label': '按钮文案', 'max_length': 50}), 4: ('wagtail.blocks.URLBlock', (), {'label': '按钮链接'}), 5: ('wagtail.blocks.StructBlock', [[('text', 3), ('link', 4)]], {}), 6: ('wagtail.blocks.ListBlock', (5,), {'label': '按钮组'}), 7: ('wagtail.blocks.StructBlock', [[('title', 0), ('subtitle', 1), ('background_image', 2), ('buttons', 6)]], {}), 8: ('wagtail.blocks.CharBlock', (), {'label': '数值', 'max_length': 20}), 9: ('wagtail.blocks.CharBlock', (), {'label': '说明文字', 'max_length': 50}), 10: ('wagtail.blocks.StructBlock', [[('value', 8), ('label', 9)]], {}), 11: ('wagtail.blocks.ListBlock', (10,), {'label': '数据项'}), 12: ('wagtail.blocks.StructBlock', [[('items', 11)]], {}), 13: ('wagtail.blocks.CharBlock', (), {'label': '模块标题', 'max_length': 100, 'required': False}), 14: ('wagtail.blocks.CharBlock', (), {'label': '图标', 'max_length': 50, 'required': False}), 15: ('wagtail.blocks.CharBlock', (), {'label': '标题', 'max_length': 100}), 16: ('wagtail.blocks.TextBlock', (), {'label': '描述'}), 17: ('wagtail.blocks.StructBlock', [[('icon', 14), ('title', 15), ('description', 16)]], {}), 18: ('wagtail.blocks.ListBlock', (17,), {'label': '能力列表'}), 19: ('wagtail.blocks.StructBlock', [[('heading', 13), ('items', 18)]], {}), 20: ('wagtail.blocks.CharBlock', (), {'label': '问题', 'max_length': 200}), 21: ('wagtail.blocks.RichTextBlock', (), {'label': '回答'}), 22: ('wagtail.blocks.StructBlock', [[('question', 20), ('answer', 21)]], {}), 23: ('wagtail.blocks.ListBlock', (22,), {'label': '问答列表'}), 24: ('wagtail.blocks.StructBlock', [[('items', 23)]], {}), 25: ('wagtail.blocks.TextBlock', (), {'label': '描述', 'required': False}), 26: ('wagtail.blocks.StructBlock', [[('text', 3), ('link', 4)]], {'label': '按钮'}), 27: ('wagtail.blocks.StructBlock', [[('heading', 0), ('description', 25), ('button', 26)]], {}), 28: ('wagtail.blocks.CharBlock', (), {'label': '标题', 'max_length': 100, 'required': False}), 29: ('wagtail.images.blocks.ImageChooserBlock', (), {}), 30: ('wagtail.blocks.ListBlock', (29,), {'label': 'Logo 列表'}), 31: ('wagtail.blocks.StructBlock', [[('heading', 28), ('logos', 30)]], {}), 32: ('wagtail.blocks.RichTextBlock', (), {'label': '富文本'})})),
('cover_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image', verbose_name='封面图')),
('og_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image', verbose_name='社交分享图')),
],
options={
'verbose_name': '博客文章',
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='BlogPageTag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content_object', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='tagged_items', to='blog.blogpage')),
('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_items', to='taggit.tag')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='blogpage',
name='tags',
field=modelcluster.contrib.taggit.ClusterTaggableManager(blank=True, help_text='A comma-separated list of tags.', through='blog.BlogPageTag', to='taggit.Tag', verbose_name='标签'),
),
]
View File
+75
View File
@@ -0,0 +1,75 @@
from django.db import models
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import TaggedItemBase
from wagtail.admin.panels import FieldPanel
from wagtail.fields import StreamField
from wagtail.search import index
from apps.core.blocks import COMMON_BLOCKS
from apps.core.models import SEOablePage
class BlogIndexPage(SEOablePage):
"""博客列表页,本身不直接管理内容,子页面为 BlogPage。"""
intro = models.TextField(blank=True, verbose_name="栏目简介")
content_panels = SEOablePage.content_panels + [
FieldPanel("intro"),
]
subpage_types = ["blog.BlogPage"]
parent_page_types = ["home.HomePage"]
class Meta:
verbose_name = "博客栏目页"
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
context["posts"] = (
BlogPage.objects.live().descendant_of(self).order_by("-published_at")
)
return context
class BlogPageTag(TaggedItemBase):
content_object = ParentalKey(
"BlogPage", related_name="tagged_items", on_delete=models.CASCADE
)
class BlogPage(SEOablePage):
"""技术博客文章页。"""
published_at = models.DateTimeField(verbose_name="发布时间")
intro = models.CharField(max_length=250, blank=True, verbose_name="摘要")
cover_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
verbose_name="封面图",
)
body = StreamField(COMMON_BLOCKS, use_json_field=True, blank=True)
tags = ClusterTaggableManager(through=BlogPageTag, blank=True, verbose_name="标签")
search_fields = SEOablePage.search_fields + [
index.SearchField("intro"),
index.SearchField("body"),
]
content_panels = SEOablePage.content_panels + [
FieldPanel("published_at"),
FieldPanel("intro"),
FieldPanel("cover_image"),
FieldPanel("body"),
FieldPanel("tags"),
]
parent_page_types = ["blog.BlogIndexPage"]
subpage_types = []
class Meta:
verbose_name = "博客文章"
@@ -0,0 +1,14 @@
{% load wagtailcore_tags %}
<!doctype html>
<html lang="zh-hans">
<head><meta charset="utf-8"><title>{{ page.title }}</title></head>
<body>
<h1>{{ page.title }}</h1>
<p>{{ page.intro }}</p>
<ul>
{% for post in posts %}
<li><a href="{% pageurl post %}">{{ post.title }}</a></li>
{% endfor %}
</ul>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
{% load wagtailcore_tags %}
<!doctype html>
<html lang="zh-hans">
<head><meta charset="utf-8"><title>{{ page.title }}</title></head>
<body>
<h1>{{ page.title }}</h1>
<p>{{ page.published_at }}</p>
{% for block in page.body %}
{% include_block block %}
{% endfor %}
</body>
</html>