chore: initial commit of Wagtail backend scaffold
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.api"
|
||||
label = "api"
|
||||
verbose_name = "API"
|
||||
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Wagtail API v2 路由配置。
|
||||
详见 documents/设计方案分析与完善版.md §2.7 Headless API 设计规范。
|
||||
"""
|
||||
from wagtail.api.v2.views import PagesAPIViewSet
|
||||
from wagtail.api.v2.router import WagtailAPIRouter
|
||||
from wagtail.images.api.v2.views import ImagesAPIViewSet
|
||||
from wagtail.documents.api.v2.views import DocumentsAPIViewSet
|
||||
|
||||
api_router = WagtailAPIRouter("wagtailapi")
|
||||
|
||||
api_router.register_endpoint("pages", PagesAPIViewSet)
|
||||
api_router.register_endpoint("images", ImagesAPIViewSet)
|
||||
api_router.register_endpoint("documents", DocumentsAPIViewSet)
|
||||
@@ -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 = "技术博客"
|
||||
@@ -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='标签'),
|
||||
),
|
||||
]
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,11 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.core"
|
||||
label = "core"
|
||||
verbose_name = "核心 / 公共基础"
|
||||
|
||||
def ready(self):
|
||||
from apps.core import signals # noqa: F401
|
||||
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
可复用的 StreamField Block 库。
|
||||
参见 documents/设计方案分析与完善版.md §2.6 组件清单。
|
||||
"""
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from wagtail import blocks
|
||||
from wagtail.images.blocks import ImageChooserBlock
|
||||
|
||||
|
||||
class CTAButtonBlock(blocks.StructBlock):
|
||||
text = blocks.CharBlock(max_length=50, label=_("按钮文案"))
|
||||
link = blocks.URLBlock(label=_("按钮链接"))
|
||||
|
||||
class Meta:
|
||||
icon = "link"
|
||||
label = _("按钮")
|
||||
|
||||
|
||||
class HeroBlock(blocks.StructBlock):
|
||||
title = blocks.CharBlock(max_length=120, label=_("标题"))
|
||||
subtitle = blocks.TextBlock(required=False, label=_("副标题"))
|
||||
background_image = ImageChooserBlock(required=False, label=_("背景图"))
|
||||
buttons = blocks.ListBlock(CTAButtonBlock(), label=_("按钮组"))
|
||||
|
||||
class Meta:
|
||||
icon = "image"
|
||||
label = _("首屏 Hero")
|
||||
template = "core/blocks/hero_block.html"
|
||||
|
||||
|
||||
class StatItemBlock(blocks.StructBlock):
|
||||
value = blocks.CharBlock(max_length=20, label=_("数值"))
|
||||
label = blocks.CharBlock(max_length=50, label=_("说明文字"))
|
||||
|
||||
|
||||
class StatsBlock(blocks.StructBlock):
|
||||
items = blocks.ListBlock(StatItemBlock(), label=_("数据项"))
|
||||
|
||||
class Meta:
|
||||
icon = "site"
|
||||
label = _("数据展示")
|
||||
template = "core/blocks/stats_block.html"
|
||||
|
||||
|
||||
class FeatureBlock(blocks.StructBlock):
|
||||
icon = blocks.CharBlock(max_length=50, required=False, label=_("图标"))
|
||||
title = blocks.CharBlock(max_length=100, label=_("标题"))
|
||||
description = blocks.TextBlock(label=_("描述"))
|
||||
|
||||
class Meta:
|
||||
icon = "pick"
|
||||
label = _("能力/优势")
|
||||
template = "core/blocks/feature_block.html"
|
||||
|
||||
|
||||
class FeatureGridBlock(blocks.StructBlock):
|
||||
heading = blocks.CharBlock(max_length=100, required=False, label=_("模块标题"))
|
||||
items = blocks.ListBlock(FeatureBlock(), label=_("能力列表"))
|
||||
|
||||
class Meta:
|
||||
icon = "grip"
|
||||
label = _("能力网格")
|
||||
|
||||
|
||||
class FAQItemBlock(blocks.StructBlock):
|
||||
question = blocks.CharBlock(max_length=200, label=_("问题"))
|
||||
answer = blocks.RichTextBlock(label=_("回答"))
|
||||
|
||||
|
||||
class FAQBlock(blocks.StructBlock):
|
||||
items = blocks.ListBlock(FAQItemBlock(), label=_("问答列表"))
|
||||
|
||||
class Meta:
|
||||
icon = "help"
|
||||
label = _("常见问题")
|
||||
template = "core/blocks/faq_block.html"
|
||||
|
||||
|
||||
class CTABlock(blocks.StructBlock):
|
||||
heading = blocks.CharBlock(max_length=120, label=_("标题"))
|
||||
description = blocks.TextBlock(required=False, label=_("描述"))
|
||||
button = CTAButtonBlock(label=_("按钮"))
|
||||
|
||||
class Meta:
|
||||
icon = "plus"
|
||||
label = _("行动号召 CTA")
|
||||
template = "core/blocks/cta_block.html"
|
||||
|
||||
|
||||
class LogoCloudBlock(blocks.StructBlock):
|
||||
heading = blocks.CharBlock(max_length=100, required=False, label=_("标题"))
|
||||
logos = blocks.ListBlock(ImageChooserBlock(), label=_("Logo 列表"))
|
||||
|
||||
class Meta:
|
||||
icon = "image"
|
||||
label = _("客户 Logo 墙")
|
||||
template = "core/blocks/logo_cloud_block.html"
|
||||
|
||||
|
||||
COMMON_BLOCKS = [
|
||||
("hero", HeroBlock()),
|
||||
("stats", StatsBlock()),
|
||||
("feature_grid", FeatureGridBlock()),
|
||||
("faq", FAQBlock()),
|
||||
("cta", CTABlock()),
|
||||
("logo_cloud", LogoCloudBlock()),
|
||||
("richtext", blocks.RichTextBlock(label=_("富文本"))),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
公共抽象模型。所有业务页面应继承 SEOablePage 而非直接继承 wagtail.models.Page,
|
||||
以统一获得 SEO 字段(详见 documents/设计方案分析与完善版.md §2.5)。
|
||||
"""
|
||||
from django.db import models
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Wagtail signal handlers:发布内容后通知 Next.js 前端失效 ISR 缓存。
|
||||
详见 documents/设计方案分析与完善版.md §2.7。
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
from wagtail.signals import page_published
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(page_published)
|
||||
def notify_frontend_revalidate(sender, instance, **kwargs):
|
||||
if not settings.FRONTEND_REVALIDATE_URL:
|
||||
return
|
||||
try:
|
||||
requests.post(
|
||||
settings.FRONTEND_REVALIDATE_URL,
|
||||
json={"tags": [f"page:{instance.id}", instance.url_path]},
|
||||
headers={"Authorization": f"Bearer {settings.REVALIDATE_SECRET}"},
|
||||
timeout=3,
|
||||
)
|
||||
except requests.RequestException:
|
||||
logger.warning("Failed to notify frontend revalidate webhook", exc_info=True)
|
||||
@@ -0,0 +1,5 @@
|
||||
<section class="cta">
|
||||
<h2>{{ value.heading }}</h2>
|
||||
{% if value.description %}<p>{{ value.description }}</p>{% endif %}
|
||||
<a href="{{ value.button.link }}">{{ value.button.text }}</a>
|
||||
</section>
|
||||
@@ -0,0 +1,9 @@
|
||||
{% load wagtailcore_tags %}
|
||||
<section class="faq">
|
||||
{% for item in value.items %}
|
||||
<div class="faq-item">
|
||||
<h4>{{ item.question }}</h4>
|
||||
<div>{{ item.answer|richtext }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
@@ -0,0 +1,4 @@
|
||||
<section class="feature">
|
||||
<h3>{{ value.title }}</h3>
|
||||
<p>{{ value.description }}</p>
|
||||
</section>
|
||||
@@ -0,0 +1,8 @@
|
||||
{% load wagtailcore_tags %}
|
||||
<section class="hero">
|
||||
<h1>{{ value.title }}</h1>
|
||||
{% if value.subtitle %}<p>{{ value.subtitle }}</p>{% endif %}
|
||||
{% for button in value.buttons %}
|
||||
<a href="{{ button.link }}">{{ button.text }}</a>
|
||||
{% endfor %}
|
||||
</section>
|
||||
@@ -0,0 +1,10 @@
|
||||
{% load wagtailimages_tags %}
|
||||
<section class="logo-cloud">
|
||||
{% if value.heading %}<h3>{{ value.heading }}</h3>{% endif %}
|
||||
<div class="logos">
|
||||
{% for logo in value.logos %}
|
||||
{% image logo width-160 as logo_img %}
|
||||
<img src="{{ logo_img.url }}" alt="">
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,5 @@
|
||||
<section class="stats">
|
||||
{% for item in value.items %}
|
||||
<div class="stat"><strong>{{ item.value }}</strong><span>{{ item.label }}</span></div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
健康检查端点,供 Kubernetes liveness/readiness 探针使用。
|
||||
详见 documents/设计方案分析与完善版.md §2.12。
|
||||
"""
|
||||
from django.db import connection
|
||||
from django.http import JsonResponse
|
||||
|
||||
|
||||
def healthz(request):
|
||||
"""存活探针:仅确认进程可响应请求。"""
|
||||
return JsonResponse({"status": "ok"})
|
||||
|
||||
|
||||
def readyz(request):
|
||||
"""就绪探针:确认数据库连接可用。"""
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT 1")
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
return JsonResponse({"status": "error", "detail": str(exc)}, status=503)
|
||||
return JsonResponse({"status": "ok"})
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class HomeConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.home"
|
||||
label = "home"
|
||||
verbose_name = "首页"
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 6.0.5 on 2026-08-06 05:29
|
||||
|
||||
import django.db.models.deletion
|
||||
import wagtail.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0097_baselogentry_uuid_action_timestamp_indexes'),
|
||||
('wagtailimages', '0027_image_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HomePage',
|
||||
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)')),
|
||||
('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': '富文本'})})),
|
||||
('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',),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.fields import StreamField
|
||||
|
||||
from apps.core.blocks import COMMON_BLOCKS
|
||||
from apps.core.models import SEOablePage
|
||||
|
||||
|
||||
class HomePage(SEOablePage):
|
||||
"""企业官网首页,使用通用组件化 StreamField 拼装页面内容。"""
|
||||
|
||||
body = StreamField(COMMON_BLOCKS, use_json_field=True, blank=True)
|
||||
|
||||
content_panels = SEOablePage.content_panels + [
|
||||
FieldPanel("body"),
|
||||
]
|
||||
|
||||
subpage_types = [
|
||||
"blog.BlogIndexPage",
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = "首页"
|
||||
@@ -0,0 +1,11 @@
|
||||
{% load wagtailcore_tags %}
|
||||
<!doctype html>
|
||||
<html lang="zh-hans">
|
||||
<head><meta charset="utf-8"><title>{{ page.title }}</title></head>
|
||||
<body>
|
||||
<h1>{{ page.title }}</h1>
|
||||
{% for block in page.body %}
|
||||
{% include_block block %}
|
||||
{% endfor %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user