update. 完成前端页面展示、列表和详情页

update. 调整融合主菜单与板块
update. 分离新闻模块
This commit is contained in:
ngfchl
2026-04-01 14:17:03 +08:00
parent af9d7a676f
commit 777ca50328
55 changed files with 2190 additions and 567 deletions
+54 -11
View File
@@ -1,24 +1,67 @@
from django.contrib import admin
from django.utils.html import format_html
from import_export.admin import ImportExportModelAdmin
from article.models import Section, Carousel, Article, Notice, NoticeReceipt
from base.models import MainMenu, FooterLink
from users.models import User
from base.models import MainMenu, FooterLink, LinkCategory, FriendLink
# —————— 主菜单 ——————
@admin.register(MainMenu)
class MainMenuAdmin(admin.ModelAdmin):
list_display = ('title', 'url', 'parent', 'order', 'visible')
class MainMenuAdmin(ImportExportModelAdmin):
list_display = ('display_name', 'code', 'url', 'parent', 'order', 'visible')
list_editable = ('order', 'visible')
list_filter = ('parent', 'visible')
search_fields = ('title', 'url')
ordering = ('order',)
ordering = ('id',)
def display_name(self, obj):
"""在列表中显示层级缩进"""
if obj.is_sub:
return f" └─ {obj.title}" # 使用全角空格+符号模拟缩进
return obj.title
display_name.short_description = '板块名称'
display_name.admin_order_field = 'name'
def is_enabled(self, obj):
return obj.enabled
is_enabled.boolean = True
is_enabled.short_description = '启用'
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""限制 parent 只能选择主板块(无父级的板块)"""
if db_field.name == "parent":
kwargs["queryset"] = MainMenu.objects.filter(parent__isnull=True, is_external=False)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
def get_queryset(self, request):
"""优化查询,避免 N+1"""
return super().get_queryset(request).select_related('parent')
# 1. 注册分类管理
@admin.register(LinkCategory)
class LinkCategoryAdmin(ImportExportModelAdmin):
list_display = ('name', 'code', 'order', 'is_active')
list_editable = ('order', 'is_active')
# 2. 注册链接管理
@admin.register(FriendLink)
class FriendLinkAdmin(ImportExportModelAdmin):
list_display = ('name', 'category', 'url', 'order', 'is_active')
list_filter = ('category', 'is_active') # 左侧过滤器,方便筛选
search_fields = ('name', 'url')
# 优化:在添加链接时,只显示启用的分类
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "category":
kwargs["queryset"] = LinkCategory.objects.filter(is_active=True)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
# —————— 页脚链接 ——————
@admin.register(FooterLink)
class FooterLinkAdmin(admin.ModelAdmin):
list_display = ('title', 'url', 'order')
list_editable = ('order',)
ordering = ('order',)
class FooterLinkAdmin(ImportExportModelAdmin):
list_display = ('title', 'address', 'unit', 'postal', 'telephone', 'fax', 'filing')
list_editable = ('postal', 'telephone', 'fax', 'unit', 'filing',)
+60 -12
View File
@@ -1,4 +1,4 @@
# Generated by Django 6.0.3 on 2026-03-24 10:31
# Generated by Django 6.0.3 on 2026-04-01 00:55
import django.db.models.deletion
from django.db import migrations, models
@@ -16,29 +16,77 @@ class Migration(migrations.Migration):
name='FooterLink',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50, verbose_name='链接文字')),
('url', models.URLField(verbose_name='链接地址')),
('order', models.IntegerField(default=0, verbose_name='排序')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('title', models.CharField(max_length=50, verbose_name='页脚信息')),
('address', models.CharField(max_length=200, verbose_name='单位地址')),
('postal', models.CharField(default='', verbose_name='邮编')),
('telephone', models.CharField(default='', verbose_name='服务热线')),
('fax', models.CharField(default='', verbose_name='传真')),
('unit', models.CharField(default='', verbose_name='版权单位')),
('filing', models.CharField(default='', verbose_name='备案号')),
('official_account', models.ImageField(default='', upload_to='carousel/', verbose_name='公众号二维码')),
('weibo', models.ImageField(default='', upload_to='carousel/', verbose_name='微博二维码')),
('is_active', models.BooleanField(default=False, verbose_name='生效')),
],
options={
'verbose_name': '页脚链接',
'verbose_name': '页脚信息',
'verbose_name_plural': '页脚管理',
},
),
migrations.CreateModel(
name='LinkCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=50, unique=True, verbose_name='分类名称')),
('code', models.SlugField(help_text="用于前端模板调用的标识,如 'gov', 'fire'", unique=True, verbose_name='分类代码')),
('order', models.IntegerField(default=0, verbose_name='显示排序')),
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
],
options={
'verbose_name': '友链分类',
'verbose_name_plural': '友链分类管理',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='FriendLink',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=100, verbose_name='网站名称')),
('url', models.URLField(verbose_name='链接地址')),
('order', models.IntegerField(default=0, verbose_name='排序')),
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='base.linkcategory', verbose_name='所属分类')),
],
options={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接管理',
'ordering': ['order', '-created_at'],
},
),
migrations.CreateModel(
name='MainMenu',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50, verbose_name='菜单标题')),
('url', models.CharField(help_text='如 /news/ 或 外链 https://xxx', max_length=200, verbose_name='链接地址')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('title', models.CharField(max_length=100, verbose_name='标题')),
('code', models.SlugField(help_text='用于URL或模板识别,建议英文', unique=True, verbose_name='唯一标识')),
('url', models.CharField(blank=True, help_text='如 /menu/news.html 或 外链', max_length=200, verbose_name='链接地址')),
('is_external', models.BooleanField(default=False, verbose_name='是否外链')),
('order', models.IntegerField(default=0, verbose_name='排序')),
('visible', models.BooleanField(default=True, verbose_name='是否显示')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.mainmenu', verbose_name='菜单')),
('order', models.PositiveSmallIntegerField(default=0, verbose_name='排序权重')),
('visible', models.BooleanField(default=True, verbose_name='显示状态')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='base.mainmenu', verbose_name='级节点')),
],
options={
'verbose_name': '主菜单',
'verbose_name_plural': '主菜单管理',
'verbose_name': '站点导航/板块',
'verbose_name_plural': '站点结构管理',
'ordering': ('order', 'title'),
},
),
]
@@ -0,0 +1,30 @@
# Generated by Django 6.0.3 on 2026-04-01 01:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('base', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='friendlink',
options={'ordering': ['order', '-created_at'], 'verbose_name': '友情链接', 'verbose_name_plural': '友链管理'},
),
migrations.AlterModelOptions(
name='linkcategory',
options={'ordering': ['order'], 'verbose_name': '友链分类', 'verbose_name_plural': '友链分类'},
),
migrations.AlterModelOptions(
name='mainmenu',
options={'ordering': ('order', 'title'), 'verbose_name': '站点导航', 'verbose_name_plural': '站点导航'},
),
migrations.AddField(
model_name='mainmenu',
name='is_carousel',
field=models.BooleanField(default=False, verbose_name='轮播图'),
),
]
+131 -14
View File
@@ -1,27 +1,144 @@
from django.core.exceptions import ValidationError
from django.db import models
from common.base import BaseEntity
# Create your models here.
class MainMenu(models.Model):
title = models.CharField("菜单标题", max_length=50)
url = models.CharField("链接地址", max_length=200, help_text="如 /news/ 或 外链 https://xxx")
is_external = models.BooleanField("是否外链", default=False)
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, verbose_name="父菜单")
order = models.IntegerField("排序", default=0)
visible = models.BooleanField("是否显示", default=True)
# --- 新增:友情链接分类模型 ---
class LinkCategory(BaseEntity):
name = models.CharField("分类名称", max_length=50, unique=True) # 分类名,如"政府网站"
code = models.SlugField("分类代码", max_length=50, unique=True, help_text="用于前端模板调用的标识,如 'gov', 'fire'")
order = models.IntegerField("显示排序", default=0) # 分类本身的排序
is_active = models.BooleanField("是否启用", default=True)
class Meta:
verbose_name = "主菜单"
verbose_name_plural = "主菜单管理"
verbose_name = "友链分类"
verbose_name_plural = "友链分类"
ordering = ['order']
def __str__(self):
return self.title
return self.name
class FooterLink(models.Model):
title = models.CharField("链接文字", max_length=50)
url = models.URLField("链接地址")
# --- 修改:友情链接模型 ---
class FriendLink(BaseEntity):
# 1. 关联到新的分类模型(一对多关系:一个分类下有多个链接)
category = models.ForeignKey(
LinkCategory,
on_delete=models.CASCADE, # 如果分类被删,关联的链接也删除
verbose_name="所属分类",
related_name='links' # 反向查询名称,方便后续调用
)
name = models.CharField("网站名称", max_length=100)
url = models.URLField("链接地址", max_length=200)
# 2. 排序和状态
order = models.IntegerField("排序", default=0)
is_active = models.BooleanField("是否启用", default=True)
class Meta:
verbose_name = "页脚链接"
verbose_name = "友情链接"
verbose_name_plural = "友链管理"
ordering = ['order', '-created_at']
def __str__(self):
return f"[{self.category.name}] {self.name}"
class MainMenu(BaseEntity):
# --- 基础信息 ---
title = models.CharField("标题", max_length=100)
code = models.SlugField("唯一标识", max_length=50, unique=True, help_text="用于URL或模板识别,建议英文")
# --- 树形结构 ---
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children',
verbose_name="父级节点"
)
# --- 菜单特有属性 ---
# 只有顶级节点(主菜单)才需要填写这个,子节点作为板块时通常不需要跳转链接
url = models.CharField("链接地址", max_length=200, blank=True, help_text="如 /menu/news.html 或 外链")
is_external = models.BooleanField("是否外链", default=False)
is_carousel = models.BooleanField("轮播图", default=False)
# --- 通用属性 ---
order = models.PositiveSmallIntegerField("排序权重", default=0)
visible = models.BooleanField("显示状态", default=True)
class Meta:
verbose_name = "站点导航"
verbose_name_plural = "站点导航"
ordering = ('order', 'title')
def __str__(self):
# 显示层级路径,例如:新闻中心 → 国内新闻
if self.parent:
return f"{self.parent.title}{self.title}"
return self.title
def clean(self):
super().clean()
# 限制层级:最多 2 级
if self.parent and self.parent.parent:
raise ValidationError('层级过深:子节点不能再设置子级,最多只允许两级结构。')
# 防止自引用
if self.pk and self.parent and self.parent.pk == self.pk:
raise ValidationError('节点不能以自身为父级。')
# --- 核心逻辑属性 ---
@property
def is_sub(self):
"""是否为子板块(即是否有父级)"""
return self.parent is not None
@property
def is_main(self):
"""是否为主菜单(即是否无父级)"""
return not self.parent
@property
def active_children(self):
"""
获取当前节点下可见的子节点(用于前端导航渲染)
"""
return self.children.filter(visible=True)
class FooterLink(BaseEntity):
title = models.CharField("页脚信息", max_length=50)
address = models.CharField("单位地址", max_length=200)
postal = models.CharField("邮编", default="")
telephone = models.CharField("服务热线", default="")
fax = models.CharField("传真", default="")
unit = models.CharField("版权单位", default="")
filing = models.CharField("备案号", default="")
official_account = models.ImageField("公众号二维码", default="", upload_to='carousel/')
weibo = models.ImageField("微博二维码", default="", upload_to='carousel/')
is_active = models.BooleanField("生效", default=False)
class Meta:
verbose_name = "页脚信息"
verbose_name_plural = "页脚管理"
def __str__(self):
return f"{self.title} - 状态:{'使用中' if self.is_active else '已停用'}"
def save(self, *args, **kwargs):
# 1. 如果当前这条数据被标记为“生效” (is_active=True)
if self.is_active:
# 2. 将数据库中【除了当前这条】以外的所有 FooterLink 都更新为“不生效”
# self.pk 是当前对象的唯一标识
FooterLink.objects.exclude(pk=self.pk).update(is_active=False)
# 3. 执行正常的保存操作
super().save(*args, **kwargs)
+107
View File
@@ -0,0 +1,107 @@
import os
import sys
import django
from loguru import logger
from pypinyin import lazy_pinyin, Style
# 配置 Django 环境
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") # 修改为你的 settings 路径
django.setup()
from faker import Faker
from base.models import MainMenu
# 初始化 Faker,设置中文(如果需要中文名)
# 初始化 Faker (zh_CN 生成中文数据)
fake = Faker('zh_CN')
# --- 核心数据结构:定义你的菜单层级 ---
MENU_STRUCTURE = {
"信息公开": [
"通知通告", "法律法规", "政策文件", "政策解读"
],
"党建引领": [
"党建工作", "党建轮播", "党风廉政", "先进典型"
],
"互动交流": [
"专家访谈", "调查征集", "在线咨询"
],
"消防科普": [
"数字消防", "火灾预防", "自救逃生"
],
"办事服务": [
"办事指南", "政务服务", "资料下载", "办事统计"
]
}
def get_slug(text):
"""
将中文转换为拼音 Slug
例如: "信息公开" -> "xin-xi-gong-kai"
"""
# lazy_pinyin 返回拼音列表,join 将其用 '-' 连接
return "-".join(lazy_pinyin(text, style=Style.NORMAL))
def seed_main_menu():
logger.info("🚀 开始生成站点导航数据...")
# 1. 清空旧数据 (可选,如果需要重置数据请取消注释)
MainMenu.objects.all().delete()
logger.info("🧹 已清空旧数据")
created_count = 0
# 2. 循环创建主菜单和子板块
for main_title, sub_titles in MENU_STRUCTURE.items():
# --- 创建主菜单 ---
# 生成唯一的 code,例如 "xin-xi-gong-kai"
main_code = get_slug(text=main_title)
logger.info(f"主菜单: [{main_title}] Code: [{main_code}]")
main_menu, created = MainMenu.objects.get_or_create(
title=main_title,
defaults={
'code': main_code,
'url': main_code, # 模拟 URL
'is_external': False,
'order': created_count,
'visible': True
}
)
if created:
logger.info(f"✅ 创建主菜单: [{main_title}]")
else:
logger.info(f"⏭️ 主菜单已存在: [{main_title}]")
# --- 创建子板块 ---
for idx, sub_title in enumerate(sub_titles):
sub_code = get_slug(text=sub_title)
logger.info(f"子菜单: [{sub_title}] Code: [{sub_code}]")
sub_menu, sub_created = MainMenu.objects.get_or_create(
title=sub_title,
parent=main_menu, # 关键:关联父级
defaults={
'code': sub_code,
'url': sub_code,
'is_external': False,
'order': idx, # 子菜单排序
'visible': True
}
)
if sub_created:
logger.info(f" └─ ✅ 创建子板块: [{sub_title}]")
else:
logger.info(f" └─ ⏭️ 子板块已存在: [{sub_title}]")
created_count += 1
logger.info(f"\n🎉 导航数据生成完毕!共处理 {len(MENU_STRUCTURE)} 个主菜单。")
if __name__ == "__main__":
seed_main_menu()
View File
+36
View File
@@ -0,0 +1,36 @@
from django import template
from loguru import logger
from ..models import FooterLink, LinkCategory, MainMenu # 假设你的模型在当前目录的上一级 models.py 中
register = template.Library()
# inclusion_tag 装饰器会自动帮你渲染指定的模板
@register.inclusion_tag('components/footer.html')
def show_footer():
# 1. 获取当前生效的那一条页脚数据
# 根据你之前的模型,is_active=True 的只有一条
footer = FooterLink.objects.filter(is_active=True).first()
link_categories = LinkCategory.objects.filter(is_active=True)
logger.info(footer)
logger.info(link_categories)
# 3. 将数据传递给模板
return {
'footer': footer,
'categories': link_categories
}
@register.inclusion_tag('components/main_menu.html')
def show_main_menu():
# 1. 获取当前生效的那一条页脚数据
# 根据你之前的模型,is_active=True 的只有一条
menus = MainMenu.objects.filter(visible=True, parent__isnull=True).order_by('order')
logger.info(menus)
# 3. 将数据传递给模板
return {
'menus': menus,
}
+41 -1
View File
@@ -1,5 +1,45 @@
from django.shortcuts import render
from ninja import Router
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
import os
import uuid
from datetime import datetime
# Create your views here.
router = Router(tags=['content'])
router = Router(tags=['content'])
@csrf_exempt
@require_http_methods(["POST"])
def custom_upload_file(request):
"""
自定义 CKEditor 5 上传视图
支持:图片 + PDF / Word / Excel / ZIP 等所有文件
"""
file = request.FILES.get('upload')
if not file:
return JsonResponse({"error": "请选择文件"}, status=400)
# 安全处理文件名(避免中文/特殊字符报错)
ext = file.name.split('.')[-1].lower()
filename = f"{uuid.uuid4().hex}_{datetime.now().strftime('%Y%m%d%H%M%S')}.{ext}"
# 保存路径
upload_dir = os.path.join(settings.MEDIA_ROOT, 'uploads')
os.makedirs(upload_dir, exist_ok=True)
file_path = os.path.join(upload_dir, filename)
# 保存文件
with open(file_path, 'wb+') as f:
for chunk in file.chunks():
f.write(chunk)
# 返回文件 URLCKEditor 规范格式)
file_url = f"{settings.MEDIA_URL}uploads/{filename}"
return JsonResponse({
"url": file_url,
"default": file_url
})