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
View File
-2
View File
@@ -7,8 +7,6 @@ config/supervisord.sock
.idea/ .idea/
/.idea/ /.idea/
/.idea/* /.idea/*
/templates/
/templates/*
**/__pycache__ **/__pycache__
**/.DS_Store **/.DS_Store
/venv/ /venv/
+79 -41
View File
@@ -1,51 +1,56 @@
from django import forms
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
from import_export.admin import ImportExportModelAdmin
from article.models import NoticeReceipt, Notice, Article, Carousel, Section from article.models import NoticeReceipt, Notice, Article, Carousel, ArticleAttachment
from base.models import MainMenu
from users.models import User from users.models import User
# —————— 板块 —————— # —————— 板块 ——————
@admin.register(Section) # @admin.register(ArticleSection)
class SectionAdmin(admin.ModelAdmin): # class SectionAdmin(admin.ModelAdmin):
list_display = ('display_name', 'code', 'parent', 'is_enabled', 'order') # list_display = ('display_name', 'code', 'parent', 'is_enabled', 'order')
list_filter = ('parent', 'enabled') # list_filter = ('parent', 'enabled')
search_fields = ('name', 'code') # search_fields = ('name', 'code')
ordering = ('order', 'name') # ordering = ('order', 'name')
list_editable = ('order',) # list_editable = ('order',)
fieldsets = ( # fieldsets = (
('基本信息', { # ('基本信息', {
'fields': ('name', 'code', 'parent', 'enabled', 'order') # 'fields': ('name', 'code', 'parent', 'enabled', 'order')
}), # }),
) # )
#
def display_name(self, obj): # def display_name(self, obj):
"""在列表中显示层级缩进""" # """在列表中显示层级缩进"""
if obj.is_sub: # if obj.is_sub:
return f" └─ {obj.name}" # 使用全角空格+符号模拟缩进 # return f" └─ {obj.name}" # 使用全角空格+符号模拟缩进
return obj.name # return obj.name
display_name.short_description = '板块名称' #
display_name.admin_order_field = 'name' # display_name.short_description = '板块名称'
# display_name.admin_order_field = 'name'
def is_enabled(self, obj): #
return obj.enabled # def is_enabled(self, obj):
is_enabled.boolean = True # return obj.enabled
is_enabled.short_description = '启用' #
# is_enabled.boolean = True
def formfield_for_foreignkey(self, db_field, request, **kwargs): # is_enabled.short_description = '启用'
"""限制 parent 只能选择主板块(无父级的板块)""" #
if db_field.name == "parent": # def formfield_for_foreignkey(self, db_field, request, **kwargs):
kwargs["queryset"] = Section.objects.filter(parent__isnull=True) # """限制 parent 只能选择主板块(无父级的板块)"""
return super().formfield_for_foreignkey(db_field, request, **kwargs) # if db_field.name == "parent":
# kwargs["queryset"] = ArticleSection.objects.filter(parent__isnull=True)
def get_queryset(self, request): # return super().formfield_for_foreignkey(db_field, request, **kwargs)
"""优化查询,避免 N+1""" #
return super().get_queryset(request).select_related('parent') # def get_queryset(self, request):
# """优化查询,避免 N+1"""
# return super().get_queryset(request).select_related('parent')
#
# —————— 轮播图 —————— # —————— 轮播图 ——————
@admin.register(Carousel) @admin.register(Carousel)
class CarouselAdmin(admin.ModelAdmin): class CarouselAdmin(ImportExportModelAdmin):
list_display = ('title', 'image_thumb', 'link_url', 'order', 'active') list_display = ('title', 'image_thumb', 'link_url', 'order', 'active')
list_editable = ('order', 'active') list_editable = ('order', 'active')
ordering = ('order',) ordering = ('order',)
@@ -54,18 +59,49 @@ class CarouselAdmin(admin.ModelAdmin):
if obj.image: if obj.image:
return format_html('<image src="{}" width="60" height="auto" />', obj.image.url) return format_html('<image src="{}" width="60" height="auto" />', obj.image.url)
return "无图" return "无图"
image_thumb.short_description = "预览" image_thumb.short_description = "预览"
# --- 附件内联配置 ---
class ArticleAttachmentInline(admin.TabularInline):
model = ArticleAttachment
extra = 1 # 默认显示 1 个空白的附件上传框
verbose_name = "附件"
verbose_name_plural = "附件列表"
# 如果附件较多,可以限制每页显示数量
# max_num = 5
# 1. 定义自定义表单
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 核心代码:过滤 section 字段,只保留有父级的分类(即二级分类)
# 这里的 queryset 会直接作用于 Admin 的下拉选择框
self.fields['section'].queryset = MainMenu.objects.filter(parent__isnull=False, enabled=True)
# 可选:优化显示格式,让用户知道是哪个主板块下的子板块
# 例如显示:"[新闻中心] 国内新闻"
self.fields['section'].label_from_instance = lambda obj: f"[{obj.parent.name}] {obj.name}"
# —————— 文章 —————— # —————— 文章 ——————
@admin.register(Article) @admin.register(Article)
class ArticleAdmin(admin.ModelAdmin): class ArticleAdmin(ImportExportModelAdmin):
form = ArticleForm # <--- 关联自定义表单
list_display = ('title', 'section', 'author', 'is_published', 'created_at') list_display = ('title', 'section', 'author', 'is_published', 'created_at')
list_filter = ('section', 'is_published', 'created_at') list_filter = ('section', 'is_published', 'created_at')
search_fields = ('title', 'content') search_fields = ('title', 'content')
date_hierarchy = 'created_at' date_hierarchy = 'created_at'
autocomplete_fields = ('author',) # 需要 UserAdmin 支持搜索 autocomplete_fields = ('author',) # 需要 UserAdmin 支持搜索
# 核心配置:将附件内联注册到文章管理中
inlines = [ArticleAttachmentInline]
fieldsets = ( fieldsets = (
('基本信息', {'fields': ('title', 'section', 'cover')}), ('基本信息', {'fields': ('title', 'section', 'cover')}),
('内容', {'fields': ('content',)}), ('内容', {'fields': ('content',)}),
@@ -75,7 +111,7 @@ class ArticleAdmin(admin.ModelAdmin):
# —————— 通知(带签收) —————— # —————— 通知(带签收) ——————
@admin.register(Notice) @admin.register(Notice)
class NoticeAdmin(admin.ModelAdmin): class NoticeAdmin(ImportExportModelAdmin):
list_display = ( list_display = (
'title', 'title',
'publisher', 'publisher',
@@ -112,6 +148,7 @@ class NoticeAdmin(admin.ModelAdmin):
def signed_count(self, obj): def signed_count(self, obj):
return obj.receipts.count() return obj.receipts.count()
signed_count.short_description = "已签收人数" signed_count.short_description = "已签收人数"
def total_target_users(self, obj): def total_target_users(self, obj):
@@ -123,12 +160,13 @@ class NoticeAdmin(admin.ModelAdmin):
else: else:
count = User.objects.filter(is_active=True).count() count = User.objects.filter(is_active=True).count()
return count return count
total_target_users.short_description = "应签收人数" total_target_users.short_description = "应签收人数"
# —————— 签收记录 —————— # —————— 签收记录 ——————
@admin.register(NoticeReceipt) @admin.register(NoticeReceipt)
class NoticeReceiptAdmin(admin.ModelAdmin): class NoticeReceiptAdmin(ImportExportModelAdmin):
list_display = ('notice', 'user', 'signed_at') list_display = ('notice', 'user', 'signed_at')
list_filter = ('signed_at', 'notice__title') list_filter = ('signed_at', 'notice__title')
search_fields = ('user__cname', 'user__username', 'notice__title') search_fields = ('user__cname', 'user__username', 'notice__title')
+24 -21
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_ckeditor_5.fields import django_ckeditor_5.fields
from django.db import migrations, models from django.db import migrations, models
@@ -16,24 +16,40 @@ class Migration(migrations.Migration):
name='Article', name='Article',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200, verbose_name='标题')),
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='正文')),
('cover', models.ImageField(blank=True, upload_to='articles/', verbose_name='封面图')),
('is_published', models.BooleanField(default=True, verbose_name='已发布')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('title', models.CharField(max_length=200, verbose_name='标题')),
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='正文')),
('cover', models.ImageField(blank=True, upload_to='articles/%Y/%m/%d/', verbose_name='封面图')),
('is_contribution', models.BooleanField(default=False, verbose_name='是否投稿')),
('is_published', models.BooleanField(default=True, verbose_name='已发布')),
], ],
options={ options={
'verbose_name': '文章', 'verbose_name': '文章',
'verbose_name_plural': '文章管理', 'verbose_name_plural': '文章管理',
}, },
), ),
migrations.CreateModel(
name='ArticleAttachment',
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='更新时间')),
('file', models.FileField(upload_to='articles/attachments/%Y/%m/%d/', verbose_name='附件文件')),
],
options={
'verbose_name': '文章附件',
'verbose_name_plural': '文章附件管理',
},
),
migrations.CreateModel( migrations.CreateModel(
name='Carousel', name='Carousel',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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='更新时间')),
('title', models.CharField(max_length=100, verbose_name='轮播标题')), ('title', models.CharField(max_length=100, verbose_name='轮播标题')),
('image', models.ImageField(upload_to='carousel/', verbose_name='图片')), ('image', models.ImageField(upload_to='carousel/%Y/%m/%d/', verbose_name='图片')),
('link_url', models.URLField(blank=True, verbose_name='跳转链接')), ('link_url', models.URLField(blank=True, verbose_name='跳转链接')),
('order', models.IntegerField(default=0, verbose_name='排序')), ('order', models.IntegerField(default=0, verbose_name='排序')),
('active', models.BooleanField(default=True, verbose_name='启用')), ('active', models.BooleanField(default=True, verbose_name='启用')),
@@ -63,6 +79,8 @@ class Migration(migrations.Migration):
name='NoticeReceipt', name='NoticeReceipt',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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='更新时间')),
('signed_at', models.DateTimeField(auto_now_add=True, verbose_name='签收时间')), ('signed_at', models.DateTimeField(auto_now_add=True, verbose_name='签收时间')),
], ],
options={ options={
@@ -70,19 +88,4 @@ class Migration(migrations.Migration):
'verbose_name_plural': '签收管理', 'verbose_name_plural': '签收管理',
}, },
), ),
migrations.CreateModel(
name='Section',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True, verbose_name='板块名称')),
('code', models.SlugField(help_text='用于模板识别,如 news, notice', unique=True, verbose_name='板块代码')),
('description', models.TextField(blank=True, verbose_name='描述')),
('order', models.IntegerField(default=0, verbose_name='排序')),
('enabled', models.BooleanField(default=True, verbose_name='启用')),
],
options={
'verbose_name': '板块',
'verbose_name_plural': '板块管理',
},
),
] ]
+12 -6
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 import django.db.models.deletion
from django.conf import settings from django.conf import settings
@@ -11,6 +11,7 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('article', '0001_initial'), ('article', '0001_initial'),
('base', '0001_initial'),
('users', '0001_initial'), ('users', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
@@ -21,6 +22,16 @@ class Migration(migrations.Migration):
name='author', name='author',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='作者'), field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='作者'),
), ),
migrations.AddField(
model_name='article',
name='section',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.mainmenu', verbose_name='所属板块'),
),
migrations.AddField(
model_name='articleattachment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='article.article', verbose_name='所属文章'),
),
migrations.AddField( migrations.AddField(
model_name='notice', model_name='notice',
name='publisher', name='publisher',
@@ -41,11 +52,6 @@ class Migration(migrations.Migration):
name='user', name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='签收人'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='签收人'),
), ),
migrations.AddField(
model_name='article',
name='section',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='article.section', verbose_name='所属板块'),
),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='noticereceipt', name='noticereceipt',
unique_together={('notice', 'user')}, unique_together={('notice', 'user')},
@@ -1,47 +0,0 @@
# Generated by Django 6.0.3 on 2026-03-24 13:24
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('article', '0002_initial'),
]
operations = [
migrations.AlterModelOptions(
name='section',
options={'ordering': ('order', 'name'), 'verbose_name': '板块', 'verbose_name_plural': '板块管理'},
),
migrations.RemoveField(
model_name='section',
name='description',
),
migrations.AddField(
model_name='section',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subsections', to='article.section', verbose_name='所属主板块'),
),
migrations.AlterField(
model_name='section',
name='code',
field=models.SlugField(help_text='用于模板或URL识别,建议英文', unique=True, verbose_name='唯一标识'),
),
migrations.AlterField(
model_name='section',
name='enabled',
field=models.BooleanField(default=True, verbose_name='启用状态'),
),
migrations.AlterField(
model_name='section',
name='name',
field=models.CharField(max_length=100, unique=True, verbose_name='板块名称'),
),
migrations.AlterField(
model_name='section',
name='order',
field=models.PositiveSmallIntegerField(default=0, verbose_name='排序权重'),
),
]
+40 -66
View File
@@ -1,79 +1,22 @@
from django.db import models from django.db import models
from django_ckeditor_5.fields import CKEditor5Field from django_ckeditor_5.fields import CKEditor5Field
from base.models import MainMenu
from common.base import BaseEntity
from users.models import User, Department from users.models import User, Department
# Create your models here. # Create your models here.
from django.db import models class Article(BaseEntity):
from django.core.exceptions import ValidationError
class Section(models.Model):
name = models.CharField('板块名称', max_length=100, unique=True)
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='subsections',
verbose_name='所属主板块'
)
order = models.PositiveSmallIntegerField('排序权重', default=0)
enabled = models.BooleanField('启用状态', default=True)
class Meta:
verbose_name = '板块'
verbose_name_plural = '板块管理'
ordering = ('order', 'name')
def __str__(self):
if self.parent:
return f"{self.parent.name}{self.name}"
return self.name
def clean(self):
super().clean()
# 禁止子板块再设父级(即最多两级)
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 self.parent is None
class Carousel(models.Model):
title = models.CharField("轮播标题", max_length=100)
image = models.ImageField("图片", upload_to='carousel/')
link_url = models.URLField("跳转链接", blank=True)
order = models.IntegerField("排序", default=0)
active = models.BooleanField("启用", default=True)
class Meta:
verbose_name = "轮播图"
verbose_name_plural = "轮播管理"
class Article(models.Model):
title = models.CharField("标题", max_length=200) title = models.CharField("标题", max_length=200)
section = models.ForeignKey(Section, on_delete=models.CASCADE, verbose_name="所属板块") section = models.ForeignKey(MainMenu, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属板块")
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="作者") author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="作者")
content = CKEditor5Field("正文") # 需安装 django-ckeditor content = CKEditor5Field("正文") # 需安装 django-ckeditor
cover = models.ImageField("封面图", upload_to='articles/', blank=True) cover = models.ImageField("封面图", upload_to='articles/%Y/%m/%d/', blank=True)
# 标记是否为投稿(决定是否需要审核)
is_contribution = models.BooleanField("是否投稿", default=False)
is_published = models.BooleanField("已发布", default=True) is_published = models.BooleanField("已发布", default=True)
created_at = models.DateTimeField("创建时间", auto_now_add=True)
updated_at = models.DateTimeField("更新时间", auto_now=True)
class Meta: class Meta:
verbose_name = "文章" verbose_name = "文章"
@@ -83,7 +26,38 @@ class Article(models.Model):
return self.title return self.title
class Notice(models.Model): # --- 新增:附件模型 ---
class ArticleAttachment(BaseEntity):
# 关联文章:related_name='attachments' 允许我们通过 article.attachments.all() 访问
article = models.ForeignKey(
Article,
on_delete=models.CASCADE,
related_name='attachments',
verbose_name="所属文章"
)
file = models.FileField("附件文件", upload_to='articles/attachments/%Y/%m/%d/')
class Meta:
verbose_name = "文章附件"
verbose_name_plural = "文章附件管理"
def __str__(self):
return f"{self.article.title} - {self.file.name}"
class Carousel(BaseEntity):
title = models.CharField("轮播标题", max_length=100)
image = models.ImageField("图片", upload_to='carousel/%Y/%m/%d/')
link_url = models.URLField("跳转链接", blank=True)
order = models.IntegerField("排序", default=0)
active = models.BooleanField("启用", default=True)
class Meta:
verbose_name = "轮播图"
verbose_name_plural = "轮播管理"
class Notice(BaseEntity):
title = models.CharField("通知标题", max_length=200) title = models.CharField("通知标题", max_length=200)
content = CKEditor5Field("通知内容") content = CKEditor5Field("通知内容")
publisher = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="发布人") publisher = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="发布人")
@@ -101,7 +75,7 @@ class Notice(models.Model):
return self.title return self.title
class NoticeReceipt(models.Model): class NoticeReceipt(BaseEntity):
"""签收记录""" """签收记录"""
notice = models.ForeignKey(Notice, on_delete=models.CASCADE, related_name='receipts') notice = models.ForeignKey(Notice, on_delete=models.CASCADE, related_name='receipts')
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="签收人") user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="签收人")
+84
View File
@@ -0,0 +1,84 @@
import os
import random
import sys
import tempfile
import django
# 配置 Django 环境
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") # 修改为你的 settings 路径
django.setup()
# 2. 导入 Faker 和 模型
from faker import Faker
from article.models import Article, ArticleAttachment
from base.models import MainMenu
from users.models import User
from django.core.files import File # 引入 Django 的文件处理类
fake = Faker('zh_CN')
def seed_articles(count=20):
print(f"🚀 开始生成 {count} 篇文章...")
subsections = list(MainMenu.objects.filter(parent__isnull=False, visible=True))
users = list(User.objects.all())
if not subsections:
print("❌ 错误:数据库中没有找到任何子板块!")
return
if not users:
print("⚠️ 警告:数据库中没有用户,作者将设为空。")
created_count = 0
for i in range(count):
section = random.choice(subsections)
author = random.choice(users) if users else None
paragraphs = fake.paragraphs(nb=random.randint(3, 8))
content_html = "\n".join([f"<p>{p}</p>" for p in paragraphs])
article = Article.objects.create(
title=fake.sentence(nb_words=random.randint(5, 10))[:-1],
section=section,
author=author,
content=content_html,
is_published=True,
)
# --- 生成真实附件 ---
if random.random() < 0.3:
fake_filename = f"附件资料_{fake.word()}.pdf"
# 创建临时文件
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp_file:
tmp_file.write(b'%PDF-1.4 Fake PDF Content for Testing')
tmp_file_path = tmp_file.name
try:
# 打开并保存
with open(tmp_file_path, 'rb') as f:
# 关键点:使用 file.save 方法,这会触发文件上传机制
attachment = ArticleAttachment(article=article)
attachment.file.save(fake_filename, File(f), save=True)
print(f" - [{section}] {article.title} (含真实附件)")
finally:
# 删除临时文件
if os.path.exists(tmp_file_path):
os.remove(tmp_file_path)
else:
print(f" - [{section}] {article.title}")
created_count += 1
print(f"✅ 成功生成 {created_count} 篇文章!")
if __name__ == "__main__":
seed_articles(30)
+1 -1
View File
@@ -13,7 +13,7 @@ router = Router(tags=['content'])
@router.get('/article', description="获取文章列表") @router.get('/article', description="获取文章列表")
def article_list(request): def article_details(request):
if not user_has_permission(request.user, 'article', 'view'): if not user_has_permission(request.user, 'article', 'view'):
return HttpResponseForbidden("无权访问") return HttpResponseForbidden("无权访问")
articles = Article.objects.all() articles = Article.objects.all()
+54 -11
View File
@@ -1,24 +1,67 @@
from django.contrib import admin 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, LinkCategory, FriendLink
from base.models import MainMenu, FooterLink
from users.models import User
# —————— 主菜单 —————— # —————— 主菜单 ——————
@admin.register(MainMenu) @admin.register(MainMenu)
class MainMenuAdmin(admin.ModelAdmin): class MainMenuAdmin(ImportExportModelAdmin):
list_display = ('title', 'url', 'parent', 'order', 'visible') list_display = ('display_name', 'code', 'url', 'parent', 'order', 'visible')
list_editable = ('order', 'visible') list_editable = ('order', 'visible')
list_filter = ('parent', 'visible') list_filter = ('parent', 'visible')
search_fields = ('title', 'url') 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) @admin.register(FooterLink)
class FooterLinkAdmin(admin.ModelAdmin): class FooterLinkAdmin(ImportExportModelAdmin):
list_display = ('title', 'url', 'order') list_display = ('title', 'address', 'unit', 'postal', 'telephone', 'fax', 'filing')
list_editable = ('order',) list_editable = ('postal', 'telephone', 'fax', 'unit', 'filing',)
ordering = ('order',)
+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 import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@@ -16,29 +16,77 @@ class Migration(migrations.Migration):
name='FooterLink', name='FooterLink',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50, verbose_name='链接文字')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('url', models.URLField(verbose_name='链接地址')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('order', models.IntegerField(default=0, 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={ options={
'verbose_name': '页脚链接', 'verbose_name': '页脚信息',
'verbose_name_plural': '页脚管理', '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( migrations.CreateModel(
name='MainMenu', name='MainMenu',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50, verbose_name='菜单标题')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('url', models.CharField(help_text='如 /news/ 或 外链 https://xxx', max_length=200, 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='是否外链')), ('is_external', models.BooleanField(default=False, verbose_name='是否外链')),
('order', models.IntegerField(default=0, verbose_name='排序')), ('order', models.PositiveSmallIntegerField(default=0, verbose_name='排序权重')),
('visible', models.BooleanField(default=True, 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='菜单')), ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='base.mainmenu', verbose_name='级节点')),
], ],
options={ options={
'verbose_name': '主菜单', 'verbose_name': '站点导航/板块',
'verbose_name_plural': '主菜单管理', '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 django.db import models
from common.base import BaseEntity
# Create your models here. # 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) class LinkCategory(BaseEntity):
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, verbose_name="父菜单") name = models.CharField("分类名称", max_length=50, unique=True) # 分类名,如"政府网站"
order = models.IntegerField("排序", default=0) code = models.SlugField("分类代码", max_length=50, unique=True, help_text="用于前端模板调用的标识,如 'gov', 'fire'")
visible = models.BooleanField("是否显示", default=True) order = models.IntegerField("显示排序", default=0) # 分类本身的排序
is_active = models.BooleanField("是否启用", default=True)
class Meta: class Meta:
verbose_name = "主菜单" verbose_name = "友链分类"
verbose_name_plural = "主菜单管理" verbose_name_plural = "友链分类"
ordering = ['order']
def __str__(self): def __str__(self):
return self.title return self.name
class FooterLink(models.Model): # --- 修改:友情链接模型 ---
title = models.CharField("链接文字", max_length=50) class FriendLink(BaseEntity):
url = models.URLField("链接地址") # 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) order = models.IntegerField("排序", default=0)
is_active = models.BooleanField("是否启用", default=True)
class Meta: 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 = "页脚管理" 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,
}
+40
View File
@@ -1,5 +1,45 @@
from django.shortcuts import render from django.shortcuts import render
from ninja import Router 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. # 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
})
View File
+17
View File
@@ -0,0 +1,17 @@
# import django
from django.db import models
class BaseEntity(models.Model):
created_at = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
updated_at = models.DateTimeField(verbose_name='更新时间', auto_now=True)
class Meta:
abstract = True
class DownloaderCategory(models.TextChoices):
# 下载器名称
# Deluge = 'De', 'Deluge'
Transmission = 'Tr', 'Transmission'
qBittorrent = 'Qb', 'qBittorrent'
BIN
View File
Binary file not shown.
+4 -1
View File
@@ -6,7 +6,9 @@ app_name = 'front' # 可选,用于命名空间(如 {% url 'main:index' %}
urlpatterns = [ urlpatterns = [
path('', views.index, name='index'), path('', views.index, name='index'),
path('h/', views.index, name='h'), path('<str:main>.html', views.info, name='menu'),
path('<str:main>/list.html', views.main_article_list, name='main_list'),
path('<str:main>/list/<str:sub>.html', views.sub_article_list, name='sub_list'),
path('about/', views.about, name='about'), path('about/', views.about, name='about'),
path('news/', views.news, name='news'), path('news/', views.news, name='news'),
path('info/', views.info, name='info'), path('info/', views.info, name='info'),
@@ -14,4 +16,5 @@ urlpatterns = [
path('party/', views.party, name='party'), path('party/', views.party, name='party'),
path('interaction/', views.interaction, name='interaction'), path('interaction/', views.interaction, name='interaction'),
path('science/', views.science, name='science'), path('science/', views.science, name='science'),
path('article/<int:pk>.html/', views.article_detail, name='article_detail'),
] ]
+183 -146
View File
@@ -1,14 +1,36 @@
import traceback
from typing import Dict, List
from unicodedata import category from unicodedata import category
from django.shortcuts import render from django.db.models import Prefetch
from django.http import Http404, HttpResponse
from django.shortcuts import render, get_object_or_404
from loguru import logger
from article.models import Article
from base.models import MainMenu
from news.models import NewsSection
from utils.format import get_now_strftime_format from utils.format import get_now_strftime_format
# Create your views here. # Create your views here.
def h(request): # 2. 新增:文章详情视图函数
return render(request, 'front/h.html', ) def article_detail(request, pk: int):
"""
根据主键 (pk) 获取文章详情
"""
try:
# 使用 get_object_or_404 简化代码:如果文章不存在,自动返回 404 页面
article = get_object_or_404(Article, pk=pk)
# 如果是渲染 HTML 模板
return render(request, 'front/details/article.html', {'article': article})
except (Article.DoesNotExist, Http404):
return render(request, 'front/error/404.html')
except Exception as e:
logger.error(e)
logger.error(traceback.format_exc())
return render(request, 'front/error/500.html')
def index(request): def index(request):
@@ -37,64 +59,91 @@ def index(request):
] ]
} }
} }
section_data = [ # section_data = [
{ # {
"key": "info", # "key": "info",
# "data": {
# '信息公开': [
# {'title': '习近平会见全国应急管理系统先进模范和消防忠诚卫士表彰...', 'date': '2021-11-06'},
# {'title': '北京市消防救援局挂牌', 'date': '2024-06-19'},
# {'title': '市消防救援局即日起开展专项行动,推出一揽子“便民利企”措施', 'date': '2025-07-01'},
# {'title': '北京开展石油化工生产装置灭火救援实战演练', 'date': '2025-09-30'},
# {'title': '北京市全面启动 2025 年度消防宣传月活动', 'date': '2025-11-11'},
# {'title': '逛庙会学消防!厂甸大观园庙会打造消防“打卡点”', 'date': '2026-02-25'},
# {'title': '“火焰蓝”守护年味!消防员坚守景区庙会护平安', 'date': '2026-02-21'},
# ],
# },
# },
# {
# "key": "party",
# "data": {
# '党建引领': [
# {'title': '习近平会见全国应急管理系统先进模范和消防忠诚卫士表彰...', 'date': '2021-11-06'},
# {'title': '北京市消防救援局挂牌', 'date': '2024-06-19'},
# {'title': '市消防救援局即日起开展专项行动,推出一揽子“便民利企”措施', 'date': '2025-07-01'},
# {'title': '北京开展石油化工生产装置灭火救援实战演练', 'date': '2025-09-30'},
# {'title': '北京市全面启动 2025 年度消防宣传月活动', 'date': '2025-11-11'},
# {'title': '逛庙会学消防!厂甸大观园庙会打造消防“打卡点”', 'date': '2026-02-25'},
# {'title': '“火焰蓝”守护年味!消防员坚守景区庙会护平安', 'date': '2026-02-21'},
# ],
# },
# },
# {
# "key": "interaction",
# "data": {
# '互动交流': [
# {'title': '习近平会见全国应急管理系统先进模范和消防忠诚卫士表彰...', 'date': '2021-11-06'},
# {'title': '北京市消防救援局挂牌', 'date': '2024-06-19'},
# {'title': '市消防救援局即日起开展专项行动,推出一揽子“便民利企”措施', 'date': '2025-07-01'},
# {'title': '北京开展石油化工生产装置灭火救援实战演练', 'date': '2025-09-30'},
# {'title': '北京市全面启动 2025 年度消防宣传月活动', 'date': '2025-11-11'},
# {'title': '逛庙会学消防!厂甸大观园庙会打造消防“打卡点”', 'date': '2026-02-25'},
# {'title': '“火焰蓝”守护年味!消防员坚守景区庙会护平安', 'date': '2026-02-21'},
# ],
# },
# },
# {
# "key": "science",
# "data": {
# '消防科普': [
# {'title': '习近平会见全国应急管理系统先进模范和消防忠诚卫士表彰...', 'date': '2021-11-06'},
# {'title': '北京市消防救援局挂牌', 'date': '2024-06-19'},
# {'title': '市消防救援局即日起开展专项行动,推出一揽子“便民利企”措施', 'date': '2025-07-01'},
# {'title': '北京开展石油化工生产装置灭火救援实战演练', 'date': '2025-09-30'},
# {'title': '北京市全面启动 2025 年度消防宣传月活动', 'date': '2025-11-11'},
# {'title': '逛庙会学消防!厂甸大观园庙会打造消防“打卡点”', 'date': '2026-02-25'},
# {'title': '“火焰蓝”守护年味!消防员坚守景区庙会护平安', 'date': '2026-02-21'},
# ],
# },
# }
# ]
articles_qs = Article.objects.filter(is_published=True).order_by('-created_at')
main_sections = MainMenu.objects.filter(parent__isnull=True, visible=True).order_by('order').prefetch_related(
# 预取可见的子板块
Prefetch('children', queryset=MainMenu.objects.filter(visible=True).order_by('order')),
# 预取子板块下的文章,并将结果存入 'latest_articles' 属性
# 注意:这个属性是属于 'children' (即下面的 sub) 的
Prefetch('children__article_set', queryset=articles_qs, to_attr='latest_articles')
)
# 3. 构建数据 (纯内存操作,速度极快)
section_data = []
for main in main_sections:
# 收集该主板块下所有子板块的文章
all_articles = []
for sub in main.children.all():
# 使用预取好的 children
# 使用预取好的 latest_articles,并取前7条
all_articles.extend(sub.latest_articles[:7])
all_articles.sort(key=lambda x: x.created_at, reverse=True)
# 截取总列表的前7条(如果需要限制总数)
section_data.append({
"key": main.code, # 建议使用 code 作为 key,比对象本身更安全
"data": { "data": {
'信息公开': [ main.title: all_articles[:7]
{'title': '习近平会见全国应急管理系统先进模范和消防忠诚卫士表彰...', 'date': '2021-11-06'},
{'title': '北京市消防救援局挂牌', 'date': '2024-06-19'},
{'title': '市消防救援局即日起开展专项行动,推出一揽子“便民利企”措施', 'date': '2025-07-01'},
{'title': '北京开展石油化工生产装置灭火救援实战演练', 'date': '2025-09-30'},
{'title': '北京市全面启动 2025 年度消防宣传月活动', 'date': '2025-11-11'},
{'title': '逛庙会学消防!厂甸大观园庙会打造消防“打卡点”', 'date': '2026-02-25'},
{'title': '“火焰蓝”守护年味!消防员坚守景区庙会护平安', 'date': '2026-02-21'},
],
},
},
{
"key": "party",
"data": {
'党建引领': [
{'title': '习近平会见全国应急管理系统先进模范和消防忠诚卫士表彰...', 'date': '2021-11-06'},
{'title': '北京市消防救援局挂牌', 'date': '2024-06-19'},
{'title': '市消防救援局即日起开展专项行动,推出一揽子“便民利企”措施', 'date': '2025-07-01'},
{'title': '北京开展石油化工生产装置灭火救援实战演练', 'date': '2025-09-30'},
{'title': '北京市全面启动 2025 年度消防宣传月活动', 'date': '2025-11-11'},
{'title': '逛庙会学消防!厂甸大观园庙会打造消防“打卡点”', 'date': '2026-02-25'},
{'title': '“火焰蓝”守护年味!消防员坚守景区庙会护平安', 'date': '2026-02-21'},
],
},
},
{
"key": "interaction",
"data": {
'互动交流': [
{'title': '习近平会见全国应急管理系统先进模范和消防忠诚卫士表彰...', 'date': '2021-11-06'},
{'title': '北京市消防救援局挂牌', 'date': '2024-06-19'},
{'title': '市消防救援局即日起开展专项行动,推出一揽子“便民利企”措施', 'date': '2025-07-01'},
{'title': '北京开展石油化工生产装置灭火救援实战演练', 'date': '2025-09-30'},
{'title': '北京市全面启动 2025 年度消防宣传月活动', 'date': '2025-11-11'},
{'title': '逛庙会学消防!厂甸大观园庙会打造消防“打卡点”', 'date': '2026-02-25'},
{'title': '“火焰蓝”守护年味!消防员坚守景区庙会护平安', 'date': '2026-02-21'},
],
},
},
{
"key": "science",
"data": {
'消防科普': [
{'title': '习近平会见全国应急管理系统先进模范和消防忠诚卫士表彰...', 'date': '2021-11-06'},
{'title': '北京市消防救援局挂牌', 'date': '2024-06-19'},
{'title': '市消防救援局即日起开展专项行动,推出一揽子“便民利企”措施', 'date': '2025-07-01'},
{'title': '北京开展石油化工生产装置灭火救援实战演练', 'date': '2025-09-30'},
{'title': '北京市全面启动 2025 年度消防宣传月活动', 'date': '2025-11-11'},
{'title': '逛庙会学消防!厂甸大观园庙会打造消防“打卡点”', 'date': '2026-02-25'},
{'title': '“火焰蓝”守护年味!消防员坚守景区庙会护平安', 'date': '2026-02-21'},
],
},
} }
] })
logger.info(section_data)
carousel = { carousel = {
"key": "index", "key": "index",
"data": [ "data": [
@@ -135,112 +184,100 @@ def about(request):
def news(request): def news(request):
sections = [ sections = NewsSection.objects.filter(enabled=True).all().order_by('order')
section_data = [
{ {
"key": "fire_news", "key": section.code,
"data": { "data": {
"消防要闻": [ section.name: section.news_set.filter(is_published=True)[:7],
{'title': '习近平会见全国应急管理系统先进模范和消防忠诚卫士表彰...', 'date': '2021-11-06'},
{'title': '北京市消防救援局挂牌', 'date': '2024-06-19'},
{'title': '市消防救援局即日起开展专项行动,推出一揽子“便民利企”措施', 'date': '2025-07-01'},
{'title': '北京开展石油化工生产装置灭火救援实战演练', 'date': '2025-09-30'},
{'title': '北京市全面启动 2025 年度消防宣传月活动', 'date': '2025-11-11'},
{'title': '逛庙会学消防!厂甸大观园庙会打造消防“打卡点”', 'date': '2026-02-25'},
{'title': '“火焰蓝”守护年味!消防员坚守景区庙会护平安', 'date': '2026-02-21'},
]
}
},
{
"key": "local_dynamic",
"data": {
"基层动态": [
{'title': '某支队开展冬季练兵比武活动', 'date': '2023-12-01'},
{'title': '社区微型消防站进行应急演练', 'date': '2023-12-05'},
{'title': '朝阳区举办消防安全知识讲座', 'date': '2024-01-15'},
]
}
},
{
"key": "state_council",
"data": {
"国务院新闻": [
{'title': '国务院安委会部署全国安全生产大检查', 'date': '2023-11-20'},
]
} }
} }
for section in sections
] ]
context = { context = {
'sections': sections, 'sections': section_data,
} }
logger.info(context)
return render(request, 'front/news.html', context) return render(request, 'front/news.html', context)
def info(request): def info(request, main):
"""信息公开""" """信息公开"""
logger.info(f"正在访问 {main}")
sections = [ main_section = MainMenu.objects.filter(code=main).first()
if not main_section:
raise Http404("不存在的板块")
sections = MainMenu.objects.filter(parent__isnull=False, parent__code=main).order_by('order')
sections_data = [
{ {
"key": "notice", "key": section.code,
"data": { "data": {
"通知通告": [ section.title: section.article_set.filter(is_published=True).order_by('-created_at')[:7],
{'title': '关于第二轮中央生态环境保护督察第四十一项整改任务销号...', 'date': '2026-03-11'},
{'title': '关于第二轮中央生态环境保护督察第三十八项整改任务销号...', 'date': '2026-03-11'},
{'title': '公告送达通知', 'date': '2026-03-10'},
{'title': '吉林省长白山保护开发区管理委员会规划和自然资源局行政...', 'date': '2026-03-06'},
{'title': '吉林省长白山保护开发区管理委员会规划和自然资源局行政...', 'date': '2026-03-06'},
{'title': '吉林省长白山保护开发区管理委员会规划和自然资源局行政...', 'date': '2026-03-06'},
]
}
},
{
"key": "law",
"data": {
"法律法规": [
# ⚠️ 请替换为真实的法律法规数据
{'title': '关于第二轮中央生态环境保护督察第四十一项整改任务销号...', 'date': '2026-03-11'},
{'title': '关于第二轮中央生态环境保护督察第三十八项整改任务销号...', 'date': '2026-03-11'},
{'title': '公告送达通知', 'date': '2026-03-10'},
{'title': '吉林省长白山保护开发区管理委员会规划和自然资源局行政...', 'date': '2026-03-06'},
{'title': '吉林省长白山保护开发区管理委员会规划和自然资源局行政...', 'date': '2026-03-06'},
{'title': '吉林省长白山保护开发区管理委员会规划和自然资源局行政...', 'date': '2026-03-06'},
]
}
},
{
"key": "policy_doc",
"data": {
"政策文件": [
{'title': '中共长白山保护开发区工委办公室关于印发杨文慧同志在 2026...', 'date': '2026-03-05'},
{'title': '长白山管委会办公室关于印发《长白山保护开发区落实<吉林省...', 'date': '2026-02-28'},
{'title': '长白山保护开发区管理委员会 2025 年度行政执法工作报告', 'date': '2026-01-30'},
{'title': '长白山管委会办公室关于印发《美丽长白山建设行动方案(202...', 'date': '2025-12-31'},
{'title': '长白山管委会办公室关于印发《长白山保护开发区碳达峰实施...', 'date': '2025-12-31'},
{'title': '《长白山保护开发区防雷减灾管理办法》政策解读', 'date': '2025-12-12'},
]
}
},
{
"key": "policy_interp",
"data": {
"政策解读": [
{'title': '《长白山保护开发区防雷减灾管理办法》政策解读', 'date': '2025-12-12'},
{'title': '《长白山保护开发区旅游漂流活动管理规范》政策解读', 'date': '2025-07-23'},
{'title': '《长白山 40 米射电望远镜电磁波宁静区保护办法》政策解读', 'date': '2025-07-23'},
{'title': '《长白山保护开发区古树名木管理办法(试行)》政策解读', 'date': '2025-06-18'},
{'title': '《长白山保护开发区公共租赁住房管理办法》政策解读', 'date': '2025-04-08'},
{'title': '《长白山保护开发区旅游民宿管理与服务规范(试行)》政策解读', 'date': '2024-12-31'},
]
} }
} }
] for section in sections]
logger.info(sections_data)
context = { context = {
'sections': sections, 'main_section': main_section,
'sections': sections_data,
} }
return render(request, 'front/info.html', context=context) return render(request, 'front/info.html', context=context)
def main_article_list(request, main):
"""信息公开"""
logger.info(f"正在访问 {main}")
main_section = MainMenu.objects.filter(code=main).first()
if not main_section:
raise Http404("不存在的板块")
articles_qs = Article.objects.filter(is_published=True).order_by('-created_at')
sections = main_section.active_children.order_by('order').prefetch_related(
Prefetch('article_set', queryset=articles_qs, to_attr='latest_articles')
)
section_data = {
"key": main_section,
"data": {
main_section.title: [
article
for section in sections
for article in section.latest_articles[:7]
],
}
}
logger.info(section_data)
context = {
'main_section': main_section,
'sections': sections,
'articles': [
article
for section in sections
for article in section.latest_articles[:7]
]
}
return render(request, 'front/list.html', context=context)
def sub_article_list(request, main: str, sub: str):
"""信息公开"""
logger.info(f"正在访问 {main}")
sections = MainMenu.objects.all().order_by('order')
main_section = sections.filter(code=main).first()
sub_section = sections.filter(code=sub, parent__code=main).first()
logger.info(f"主板块 {main_section} 子板块 {sub_section}")
if not sub_section or not main_section:
raise Http404("不存在的板块")
context = {
'main_section': main_section,
'sub_section': sub_section,
'sections': sections.filter(parent__code=main),
'articles': sub_section.article_set.filter(is_published=True).order_by('-created_at')[:21]
}
return render(request, 'front/list.html', context=context)
def service(request): def service(request):
"""办事服务""" """办事服务"""
q = request.GET.get('q') or "办事指南" q = request.GET.get('q') or "办事指南"
@@ -286,7 +323,7 @@ def service(request):
def party(request): def party(request):
"""党建引领""" """党建引领"""
sections = [ sections: List[Dict] = [
{ {
"key": "party_work", "key": "party_work",
"data": { "data": {
@@ -329,7 +366,7 @@ def party(request):
}, },
] ]
carousel = { carousel: Dict = {
"key": "index", "key": "index",
"data": [ "data": [
{ {
View File
+52
View File
@@ -0,0 +1,52 @@
from django.contrib import admin
from django.utils.html import format_html
from import_export.admin import ImportExportModelAdmin
from .models import NewsSection, News
# Register your models here.
# --- 1. 板块管理配置 ---
@admin.register(NewsSection)
class NewsSectionAdmin(ImportExportModelAdmin):
list_display = ('name', 'code', 'order', 'enabled') # 列表显示的字段
list_editable = ('order', 'enabled') # 允许直接在列表页修改排序和状态
search_fields = ('name', 'code') # 搜索框
prepopulated_fields = {'code': ('name',)} # 输入名称时自动生成 Slug
ordering = ('order', 'name') # 默认排序
# --- 2. 文章管理配置 ---
@admin.register(News)
class NewsAdmin(ImportExportModelAdmin):
# 列表页显示
list_display = ('title', 'section', 'author', 'is_published', 'created_at', 'updated_at')
list_filter = ('section', 'is_published', 'created_at', 'author') # 右侧过滤器
search_fields = ('title', 'content') # 搜索框(支持搜标题和内容)
date_hierarchy = 'created_at' # 顶部日期层级导航
ordering = ('-created_at',) # 按创建时间倒序
# 编辑页布局
fieldsets = (
('基本信息', {
'fields': ('title', 'section', 'author', 'is_published')
}),
('内容', {
# CKEditor5Field 会自动渲染为富文本编辑器,无需额外配置 widget
'fields': ('content', 'cover'),
'classes': ('wide',) # wide 类可以让编辑器占据更多宽度
}),
('系统信息', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse', 'extrapretty'), # collapse 默认折叠,extrapretty 美化
'description': '只读信息,自动生成'
}),
)
# 只读字段(系统自动生成的字段)
readonly_fields = ('created_at', 'updated_at')
# 自动填充 Slug(如果以后给 News 加 slug 字段可以用)
# prepopulated_fields = {'slug': ('title',)}
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class NewsConfig(AppConfig):
name = 'news'
verbose_name = '新闻管理'
+48
View File
@@ -0,0 +1,48 @@
# Generated by Django 6.0.3 on 2026-04-01 00:55
import django_ckeditor_5.fields
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='News',
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='更新时间')),
('title', models.CharField(max_length=200, verbose_name='标题')),
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='正文')),
('cover', models.ImageField(blank=True, upload_to='articles/%Y/%m/%d/', verbose_name='封面图')),
('is_published', models.BooleanField(default=True, verbose_name='已发布')),
],
options={
'verbose_name': '新闻管理',
'verbose_name_plural': '新闻管理',
},
),
migrations.CreateModel(
name='NewsSection',
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, unique=True, verbose_name='模块名称')),
('code', models.SlugField(help_text='用于模板或URL识别,建议英文', unique=True, verbose_name='唯一标识')),
('order', models.PositiveSmallIntegerField(default=0, verbose_name='排序权重')),
('enabled', models.BooleanField(default=True, verbose_name='启用状态')),
],
options={
'verbose_name': '新闻模块',
'verbose_name_plural': '新闻模块',
'ordering': ('order', 'name'),
},
),
]
+28
View File
@@ -0,0 +1,28 @@
# Generated by Django 6.0.3 on 2026-04-01 00:55
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('news', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='news',
name='author',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='作者'),
),
migrations.AddField(
model_name='news',
name='section',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='news.newssection', verbose_name='所属板块'),
),
]
View File
+37
View File
@@ -0,0 +1,37 @@
from django.db import models
from django_ckeditor_5.fields import CKEditor5Field
from common.base import BaseEntity
from users.models import User
# Create your models here.
class NewsSection(BaseEntity):
name = models.CharField('模块名称', max_length=100, unique=True)
code = models.SlugField('唯一标识', max_length=50, unique=True, help_text='用于模板或URL识别,建议英文')
order = models.PositiveSmallIntegerField('排序权重', default=0)
enabled = models.BooleanField('启用状态', default=True)
class Meta:
verbose_name = '新闻模块'
verbose_name_plural = '新闻模块'
ordering = ('order', 'name')
def __str__(self):
return self.name
class News(BaseEntity):
title = models.CharField("标题", max_length=200)
section = models.ForeignKey(NewsSection, on_delete=models.CASCADE, verbose_name="所属板块")
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="作者")
content = CKEditor5Field("正文") # 需安装 django-ckeditor
cover = models.ImageField("封面图", upload_to='articles/%Y/%m/%d/', blank=True)
is_published = models.BooleanField("已发布", default=True)
class Meta:
verbose_name = "新闻管理"
verbose_name_plural = "新闻管理"
def __str__(self):
return f"{self.title} -> {self.section}"
+67
View File
@@ -0,0 +1,67 @@
import os
import random
import sys
import django
# 配置 Django 环境
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") # 修改为你的 settings 路径
django.setup()
# 2. 导入 Faker 和 模型
from faker import Faker
from news.models import News, NewsSection
from users.models import User
# 初始化 Faker (zh_CN 表示生成中文数据)
fake = Faker('zh_CN')
def seed_news(count=20):
print(f"🚀 开始生成 {count} 条新闻数据...")
# --- 1. 获取关联数据 ---
# 获取所有板块
sections = list(NewsSection.objects.all())
# 获取所有用户
users = list(User.objects.all())
# 检查基础数据是否存在
if not sections:
print("❌ 错误:数据库中没有找到任何板块 (NewsSection)!请先添加板块。")
return
if not users:
print("⚠️ 警告:数据库中没有用户 (User),作者字段将设为空。")
# --- 2. 生成数据 ---
created_count = 0
for i in range(count):
# 随机选择一个板块
section = random.choice(sections)
# 随机选择一个作者 (如果有用户)
author = random.choice(users) if users else None
# 生成富文本内容 (模拟 CKEditor 输出的 HTML)
# paragrahs 生成多段文本,join 拼接
paragraphs = fake.paragraphs(nb=random.randint(3, 6))
content_html = "\n".join([f"<p>{p}</p>" for p in paragraphs])
# 创建新闻对象
news = News.objects.create(
title=fake.sentence(nb_words=random.randint(4, 8))[:-1], # 生成标题并去掉末尾句号
section=section,
author=author,
content=content_html,
is_published=True, # 默认已发布
# 封面图留空,因为自动生成图片文件比较复杂,建议手动上传测试图
)
created_count += 1
print(f" - [{section.name}] {news.title}")
print(f"✅ 成功生成 {created_count} 条新闻数据!")
if __name__ == "__main__":
seed_news(30)
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
Generated
+209 -7
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
@@ -46,7 +46,7 @@ version = "3.11.1"
description = "ASGI specs, helper code, and adapters" description = "ASGI specs, helper code, and adapters"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"] groups = ["main", "dev"]
files = [ files = [
{file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"}, {file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"},
{file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"}, {file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"},
@@ -98,13 +98,33 @@ type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple" url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors" reference = "mirrors"
[[package]]
name = "diff-match-patch"
version = "20241021"
description = "Repackaging of Google's Diff Match and Patch libraries."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "diff_match_patch-20241021-py3-none-any.whl", hash = "sha256:93cea333fb8b2bc0d181b0de5e16df50dd344ce64828226bda07728818936782"},
{file = "diff_match_patch-20241021.tar.gz", hash = "sha256:beae57a99fa48084532935ee2968b8661db861862ec82c6f21f4acdd6d835073"},
]
[package.extras]
dev = ["attribution (==1.8.0)", "black (==24.8.0)", "build (>=1)", "flit (==3.9.0)", "mypy (==1.12.1)", "ufmt (==2.7.3)", "usort (==1.0.8.post1)"]
[package.source]
type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors"
[[package]] [[package]]
name = "django" name = "django"
version = "6.0.3" version = "6.0.3"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false optional = false
python-versions = ">=3.12" python-versions = ">=3.12"
groups = ["main"] groups = ["main", "dev"]
files = [ files = [
{file = "django-6.0.3-py3-none-any.whl", hash = "sha256:2e5974441491ddb34c3f13d5e7a9f97b07ba03bf70234c0a9c68b79bbb235bc3"}, {file = "django-6.0.3-py3-none-any.whl", hash = "sha256:2e5974441491ddb34c3f13d5e7a9f97b07ba03bf70234c0a9c68b79bbb235bc3"},
{file = "django-6.0.3.tar.gz", hash = "sha256:90be765ee756af8a6cbd6693e56452404b5ad15294f4d5e40c0a55a0f4870fe1"}, {file = "django-6.0.3.tar.gz", hash = "sha256:90be765ee756af8a6cbd6693e56452404b5ad15294f4d5e40c0a55a0f4870fe1"},
@@ -201,6 +221,39 @@ type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple" url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors" reference = "mirrors"
[[package]]
name = "django-import-export"
version = "4.4.0"
description = "Django application and library for importing and exporting data with included admin integration."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "django_import_export-4.4.0-py3-none-any.whl", hash = "sha256:2d9b234c0f024d3377167f4d9c5a506e095c5bad98e06d30700e1d0752829e3d"},
{file = "django_import_export-4.4.0.tar.gz", hash = "sha256:9900e99c89027594941074fb4cd63a5f2964975e239021765c0f066003fcd412"},
]
[package.dependencies]
diff-match-patch = "20241021"
Django = ">=4.2"
tablib = ">=3.7.0"
[package.extras]
all = ["tablib[all]"]
cli = ["tablib[cli]"]
docs = ["openpyxl (==3.1.5)", "psycopg[binary] (>=3.2.9)", "sphinx (==8.1.3)", "sphinx-rtd-theme (==3.0.1)"]
ods = ["tablib[ods]"]
pandas = ["tablib[pandas]"]
tests = ["chardet (==5.2.0)", "coverage (==7.6.4)", "django-extensions (==3.2.3)", "memory-profiler (==0.61.0)", "mysqlclient (==2.2.5)", "psycopg[binary] (>=3.2.9)", "pytz (==2024.2)", "setuptools-scm (==8.1.0)", "tablib[all] (>=3.7.0)"]
xls = ["tablib[xls]"]
xlsx = ["tablib[xlsx]"]
yaml = ["tablib[yaml]"]
[package.source]
type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors"
[[package]] [[package]]
name = "django-ninja" name = "django-ninja"
version = "1.6.2" version = "1.6.2"
@@ -251,6 +304,27 @@ type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple" url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors" reference = "mirrors"
[[package]]
name = "django-seed"
version = "0.3.1"
description = "Seed your Django project with fake data"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "django-seed-0.3.1.tar.gz", hash = "sha256:93e65d2c10449c464b83e9031be02763ec10e786aa064d649c4dd5ad06fa0eea"},
]
[package.dependencies]
django = ">=1.11"
Faker = ">=0.7.7"
toposort = ">=1.5"
[package.source]
type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors"
[[package]] [[package]]
name = "django-simpleui" name = "django-simpleui"
version = "2026.1.13" version = "2026.1.13"
@@ -270,6 +344,29 @@ type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple" url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors" reference = "mirrors"
[[package]]
name = "faker"
version = "40.11.1"
description = "Faker is a Python package that generates fake data for you."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "faker-40.11.1-py3-none-any.whl", hash = "sha256:3af3a213ba8fb33ce6ba2af7aef2ac91363dae35d0cec0b2b0337d189e5bee2a"},
{file = "faker-40.11.1.tar.gz", hash = "sha256:61965046e79e8cfde4337d243eac04c0d31481a7c010033141103b43f603100c"},
]
[package.dependencies]
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
tzdata = ["tzdata"]
[package.source]
type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors"
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.3.2" version = "3.3.2"
@@ -438,6 +535,30 @@ type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple" url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors" reference = "mirrors"
[[package]]
name = "loguru"
version = "0.7.3"
description = "Python logging made (stupidly) simple"
optional = false
python-versions = ">=3.5,<4.0"
groups = ["main"]
files = [
{file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"},
{file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"},
]
[package.dependencies]
colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
[package.extras]
dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""]
[package.source]
type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors"
[[package]] [[package]]
name = "markdown" name = "markdown"
version = "3.10.2" version = "3.10.2"
@@ -844,6 +965,23 @@ type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple" url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors" reference = "mirrors"
[[package]]
name = "pypinyin"
version = "0.55.0"
description = "汉字拼音转换模块/工具."
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4"
groups = ["dev"]
files = [
{file = "pypinyin-0.55.0-py2.py3-none-any.whl", hash = "sha256:d53b1e8ad2cdb815fb2cb604ed3123372f5a28c6f447571244aca36fc62a286f"},
{file = "pypinyin-0.55.0.tar.gz", hash = "sha256:b5711b3a0c6f76e67408ec6b2e3c4987a3a806b7c528076e7c7b86fcf0eaa66b"},
]
[package.source]
type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors"
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@@ -1144,7 +1282,7 @@ version = "0.5.5"
description = "A non-validating SQL parser." description = "A non-validating SQL parser."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] groups = ["main", "dev"]
files = [ files = [
{file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"}, {file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"},
{file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"}, {file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"},
@@ -1159,6 +1297,32 @@ type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple" url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors" reference = "mirrors"
[[package]]
name = "tablib"
version = "3.9.0"
description = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "tablib-3.9.0-py3-none-any.whl", hash = "sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a"},
{file = "tablib-3.9.0.tar.gz", hash = "sha256:1b6abd8edb0f35601e04c6161d79660fdcde4abb4a54f66cc9f9054bd55d5fe2"},
]
[package.extras]
all = ["odfpy", "openpyxl (>=2.6.0)", "pandas", "pyyaml", "tabulate", "xlrd", "xlwt"]
cli = ["tabulate"]
ods = ["odfpy"]
pandas = ["pandas"]
xls = ["xlrd", "xlwt"]
xlsx = ["openpyxl (>=2.6.0)"]
yaml = ["pyyaml"]
[package.source]
type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors"
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.10.2" version = "0.10.2"
@@ -1176,6 +1340,23 @@ type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple" url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors" reference = "mirrors"
[[package]]
name = "toposort"
version = "1.10"
description = "Implements a topological sort algorithm."
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "toposort-1.10-py3-none-any.whl", hash = "sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87"},
{file = "toposort-1.10.tar.gz", hash = "sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd"},
]
[package.source]
type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors"
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"
@@ -1219,12 +1400,12 @@ version = "2025.3"
description = "Provider of IANA time zone data" description = "Provider of IANA time zone data"
optional = false optional = false
python-versions = ">=2" python-versions = ">=2"
groups = ["main"] groups = ["main", "dev"]
markers = "sys_platform == \"win32\""
files = [ files = [
{file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"},
{file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"},
] ]
markers = {main = "sys_platform == \"win32\"", dev = "sys_platform == \"win32\" or platform_system == \"Windows\""}
[package.source] [package.source]
type = "legacy" type = "legacy"
@@ -1535,7 +1716,28 @@ type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple" url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors" reference = "mirrors"
[[package]]
name = "win32-setctime"
version = "1.2.0"
description = "A small Python utility to set file creation time on Windows"
optional = false
python-versions = ">=3.5"
groups = ["main"]
markers = "sys_platform == \"win32\""
files = [
{file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"},
{file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"},
]
[package.extras]
dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"]
[package.source]
type = "legacy"
url = "http://192.168.123.5:3141/root/pypi/+simple"
reference = "mirrors"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.13,<3.14" python-versions = ">=3.13,<3.14"
content-hash = "486f6320097292ce4db7f06f502b35949aaca16bc30356379e2be8e5c17bf774" content-hash = "3e611ebdd242413bf20b6f4f63f2db3238f46614acb7d4e54d315075c7ba0d7a"
+6
View File
@@ -24,9 +24,15 @@ django-environ = "^0.13.0"
django-db-connection-pool = "^1.2.6" django-db-connection-pool = "^1.2.6"
pillow = "^12.1.1" pillow = "^12.1.1"
django-ckeditor-5 = "^0.2.20" django-ckeditor-5 = "^0.2.20"
loguru = "^0.7.3"
django-import-export = "^4.4.0"
[tool.poetry.group.dev.dependencies]
pypinyin = "^0.55.0"
django-seed = "^0.3.1"
[build-system] [build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"] requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
+89 -12
View File
@@ -11,10 +11,34 @@ https://docs.djangoproject.com/en/6.0/ref/settings/
""" """
import os import os
from pathlib import Path from pathlib import Path
from loguru import logger
import environ
env = environ.Env()
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# 获取 env文件路径
env_file = BASE_DIR / "db/.env"
environ.Env.read_env(env_file)
logger.info(".ENV变量输出", env.ENVIRON)
# 日志文件存储配置
LOGURU_FILE = {
"is_show": "off",
"level": "ERROR",
'path': os.path.join(Path(__file__).parent, 'db/logs', 'test{time:YYYY-MM-DD}.logu'),
"rotation": "10MB",
'retention': "7days",
}
# 日志控制台配置
LOGURU_CONSOLE = {
"is_show": "on", # 配置是否开启控制台打印日志
"level": "INFO", # 最低打印级别是info级别
}
# 日志文件夹不存在时创建 # 日志文件夹不存在时创建
logs_path = os.path.join(BASE_DIR, 'db/logs') logs_path = os.path.join(BASE_DIR, 'db/logs')
if not os.path.exists(logs_path): if not os.path.exists(logs_path):
@@ -29,7 +53,16 @@ SECRET_KEY = 'django-insecure-gr&k$v)_t*ax)yr1v9iz3x%$-j812ajpxs+fe2dn*dgy=a!730
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ["*"] ALLOWED_HOSTS = ['*', "127.0.0.1", "localhost"]
# 2. 【必须添加】这个配置专门用于解决 Origin checking failed 报错
# 注意:必须包含协议头 (http://) 和端口号
CSRF_TRUSTED_ORIGINS = [
'http://127.0.0.1:11111', # 对应你报错的端口
'http://127.0.0.1',
'http://localhost:11111',
'http://localhost',
]
AUTH_USER_MODEL = 'users.User' AUTH_USER_MODEL = 'users.User'
@@ -43,18 +76,21 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'import_export',
'django_ckeditor_5', 'django_ckeditor_5',
'base.apps.BaseConfig', 'base.apps.BaseConfig',
'article.apps.ArticleConfig', 'article.apps.ArticleConfig',
'users.apps.UsersConfig', 'users.apps.UsersConfig',
'front.apps.ContentConfig' 'front.apps.ContentConfig',
'news.apps.NewsConfig',
'django_seed',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
@@ -84,12 +120,40 @@ ASGI_APPLICATION = 'server.asgi.application'
# Database # Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases # https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = { if env('DB_REMOTE', default=False):
# mysql_connect = dj_database_url.parse(os.getenv('MYSQL_CONNECTION'), conn_max_age=0)
# print("数据库连接信息", mysql_connect)
DATABASES = {
'default': {
'ENGINE': env('DB_ENGINE', default='dj_db_conn_pool.backends.postgresql'),
'NAME': env('DB_NAME', default='harvest'),
'USER': env('DB_USER', default='harvest'),
'PASSWORD': env('DB_PASSWORD', default='harvest'),
'HOST': env('DB_HOST', default='harvest_postgres'),
'PORT': env('DB_PORT', default=5432),
'CONN_MAX_AGE': env('DB_CONN_MAX_AGE', default=0), # 必须设为 0,让池管理连接生命周期
'OPTIONS': {
# 'MAX_CONNS': env('DB_MAX_CONNS', default=30), # 池中最大物理连接数 (根据 PG 总限制调整)
# 'REUSE_CONNS': env('DB_REUSE_CONNS', default=10), # 最小保留连接数
},
'POOL_OPTIONS': {
'POOL_SIZE': env('DB_MAX_CONNS', default=100), # 增加连接池大小
'MAX_OVERFLOW': 20, # 增加最大溢出连接数
'TIMEOUT': 30, # 连接超时设置
}
},
}
else:
DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3', 'NAME': BASE_DIR / 'db/data.sqlite3',
'OPTIONS': {
'timeout': 120,
'check_same_thread': False
}
},
} }
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
@@ -240,15 +304,25 @@ customColorPalette = [
}, },
] ]
CKEDITOR_5_CUSTOM_CSS = 'path_to.css' # optional # CKEDITOR_5_CUSTOM_CSS = 'path_to.css' # optional
CKEDITOR_5_FILE_STORAGE = "path_to_storage.CustomStorage" # optional CKEDITOR_5_ALLOW_ALL_FILE_TYPES = True
CKEDITOR_5_UPLOAD_FILE_TYPES = [
'jpeg', 'png', 'jpg', 'gif', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
]
# 允许上传非图片文件(PDF、Word、Excel、ZIP等)
CKEDITOR_ALLOW_NONIMAGE_FILES = True # 必须设为 True
CKEDITOR_5_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' # optional
CKEDITOR_5_CONFIGS = { CKEDITOR_5_CONFIGS = {
'default': { 'default': {
'toolbar': { 'toolbar': {
'items': ['heading', '|', 'bold', 'italic', 'link', 'items': ['heading', '|', 'bold', 'italic', 'link',
'bulletedList', 'numberedList', 'blockQuote', 'imageUpload', ], 'bulletedList', 'numberedList', 'blockQuote', 'imageUpload', 'uploadFile', ],
},
'language': 'zh-cn',
# 2. 允许上传的文件类型 (根据需要扩展)
'fileUploader': {
'supportedFileTypes': ['pdf', 'doc', 'docx', 'zip', 'txt', 'jpg', 'png'],
} }
}, },
'extends': { 'extends': {
'blockToolbar': [ 'blockToolbar': [
@@ -261,12 +335,16 @@ CKEDITOR_5_CONFIGS = {
'toolbar': { 'toolbar': {
'items': ['heading', '|', 'outdent', 'indent', '|', 'bold', 'italic', 'link', 'underline', 'strikethrough', 'items': ['heading', '|', 'outdent', 'indent', '|', 'bold', 'italic', 'link', 'underline', 'strikethrough',
'code', 'subscript', 'superscript', 'highlight', '|', 'codeBlock', 'sourceEditing', 'insertImage', 'code', 'subscript', 'superscript', 'highlight', '|', 'codeBlock', 'sourceEditing', 'insertImage',
'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', '|', 'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', 'uploadFile', '|',
'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'mediaEmbed', 'removeFormat', 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'mediaEmbed', 'removeFormat',
'insertTable', 'insertTable',
], ],
'shouldNotGroupWhenFull': 'true' 'shouldNotGroupWhenFull': 'true'
}, },
# --- 在 extends 配置中也必须加上这个 ---
'fileUploader': {
'supportedFileTypes': ['pdf', 'doc', 'docx', 'zip', 'txt']
},
'image': { 'image': {
'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft', 'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft',
'imageStyle:alignRight', 'imageStyle:alignCenter', 'imageStyle:side', '|'], 'imageStyle:alignRight', 'imageStyle:alignCenter', 'imageStyle:side', '|'],
@@ -313,4 +391,3 @@ CKEDITOR_5_CONFIGS = {
CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" # Possible values: "staff", "authenticated", "any" CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" # Possible values: "staff", "authenticated", "any"
# CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME = "custom_upload_file" # CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME = "custom_upload_file"
+6 -3
View File
@@ -17,7 +17,7 @@ Including another URLconf
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path, include from django.urls import path, include, re_path
import logging import logging
import os import os
@@ -33,8 +33,9 @@ from ninja import NinjaAPI
from ninja.errors import ValidationError from ninja.errors import ValidationError
from ninja.security import HttpBearer from ninja.security import HttpBearer
from base.views import custom_upload_file
from common.common_response import CommonResponse
from .settings import SECRET_KEY from .settings import SECRET_KEY
from utils.common_response import CommonResponse
logger = logging.getLogger('ptools') logger = logging.getLogger('ptools')
@@ -68,6 +69,7 @@ class GlobalAuth(HttpBearer):
# 2. 获取当前文件所在的目录路径 (假设此文件与各个 app 文件夹同级,或者在特定配置目录下) # 2. 获取当前文件所在的目录路径 (假设此文件与各个 app 文件夹同级,或者在特定配置目录下)
# 如果你的此文件在项目根目录,而 app 文件夹也在根目录: # 如果你的此文件在项目根目录,而 app 文件夹也在根目录:
from django.conf import settings from django.conf import settings
project_root = settings.BASE_DIR project_root = settings.BASE_DIR
@@ -123,6 +125,7 @@ def auto_load_routers(base_path, package_name_prefix=None):
except Exception as e: except Exception as e:
print(f"❌ 注册失败 {modname}: {e}") print(f"❌ 注册失败 {modname}: {e}")
api_v1 = NinjaAPI(version='1.0.0', auth=GlobalAuth()) api_v1 = NinjaAPI(version='1.0.0', auth=GlobalAuth())
auto_load_routers(project_root) auto_load_routers(project_root)
@@ -152,7 +155,7 @@ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path("api/", api_v1.urls), path("api/", api_v1.urls),
path("ckeditor5/", include('django_ckeditor_5.urls')), path("ckeditor5/", include('django_ckeditor_5.urls')),
# path("upload/", custom_upload_function, name="custom_upload_file"), path("upload/", custom_upload_file, name="custom_upload_file"),
] ]
urlpatterns += staticfiles_urlpatterns() urlpatterns += staticfiles_urlpatterns()
+90 -85
View File
@@ -1,4 +1,6 @@
{% load static %} {% load static %}
{% load footer_tags %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
@@ -39,32 +41,33 @@
<li class="{% if request.resolver_match.url_name == 'about' %}active{% endif %}"> <li class="{% if request.resolver_match.url_name == 'about' %}active{% endif %}">
<a href="{% url 'front:about' %}">单位简介</a> <a href="{% url 'front:about' %}">单位简介</a>
</li> </li>
<!-- 新闻中心 --> <!-- 新闻中心 -->
<li class="{% if request.resolver_match.url_name == 'news' %}active{% endif %}"> <li class="{% if request.resolver_match.url_name == 'news' %}active{% endif %}">
<a href="{% url 'front:news' %}">新闻中心</a> <a href="{% url 'front:news' %}">新闻中心</a>
</li> </li>
{% show_main_menu %}
<!-- 信息公开 --> {##}
<li class="{% if request.resolver_match.url_name == 'info' %}active{% endif %}"> {# <!-- 信息公开 -->#}
<a href="{% url 'front:info' %}">信息公开</a> {# <li class="{% if request.resolver_match.url_name == 'info' %}active{% endif %}">#}
</li> {# <a href="{% url 'front:info' %}">信息公开</a>#}
<!-- 党建引领 --> {# </li>#}
<li class="{% if request.resolver_match.url_name == 'party' %}active{% endif %}"> {# <!-- 党建引领 -->#}
<a href="{% url 'front:party' %}">党建引领</a> {# <li class="{% if request.resolver_match.url_name == 'party' %}active{% endif %}">#}
</li> {# <a href="{% url 'front:party' %}">党建引领</a>#}
<!-- 办事服务 --> {# </li>#}
<li class="{% if request.resolver_match.url_name == 'service' %}active{% endif %}"> {# <!-- 办事服务 -->#}
<a href="{% url 'front:service' %}">办事服务</a> {# <li class="{% if request.resolver_match.url_name == 'service' %}active{% endif %}">#}
</li> {# <a href="{% url 'front:service' %}">办事服务</a>#}
<!-- 互动交流 --> {# </li>#}
<li class="{% if request.resolver_match.url_name == 'interaction' %}active{% endif %}"> {# <!-- 互动交流 -->#}
<a href="{% url 'front:interaction' %}">互动交流</a> {# <li class="{% if request.resolver_match.url_name == 'interaction' %}active{% endif %}">#}
</li> {# <a href="{% url 'front:interaction' %}">互动交流</a>#}
<!-- 消防科普 --> {# </li>#}
<li class="{% if request.resolver_match.url_name == 'science' %}active{% endif %}"> {# <!-- 消防科普 -->#}
<a href="{% url 'front:science' %}">消防科普</a> {# <li class="{% if request.resolver_match.url_name == 'science' %}active{% endif %}">#}
</li> {# <a href="{% url 'front:science' %}">消防科普</a>#}
{# </li>#}
</ul> </ul>
</div> </div>
<span></span> <span></span>
@@ -81,72 +84,74 @@
<main class="container main" style="padding: 10px 5px !important;"> <main class="container main" style="padding: 10px 5px !important;">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<!-- 底部页脚:只需要写这一行,全站生效! -->
{% show_footer %}
<!-- 底部 --> <!-- 底部 -->
<footer class="footer"> {#<footer class="footer">#}
<div class=" d-none d-md-block"> {# <div class=" d-none d-md-block">#}
<div class="row" {# <div class="row"#}
style="align-items: center;justify-content: space-evenly;padding: 15px 0;background-color: #F5F5F5;width: 100%!important;margin-left: 0 !important;gap: 10px;"> {# style="align-items: center;justify-content: space-evenly;padding: 15px 0;background-color: #F5F5F5;width: 100%!important;margin-left: 0 !important;gap: 10px;">#}
<div style="display: flex;align-items: center;"> {# <div style="display: flex;align-items: center;">#}
<img src="{% static "icons/link.svg" %}" alt="" style="width: 13px; height: 13px; object-fit: contain;"> {# <img src="{% static "icons/link.svg" %}" alt="" style="width: 13px; height: 13px; object-fit: contain;">#}
<a href="#" {# <a href="#"#}
style="margin-left: 6px; text-decoration: none; color: #13227a; font-size: 14px; white-space: nowrap;"> {# style="margin-left: 6px; text-decoration: none; color: #13227a; font-size: 14px; white-space: nowrap;">#}
友情链接 {# 友情链接#}
</a> {# </a>#}
</div> {# </div>#}
<div style="width: 280px;"> {# <div style="width: 280px;">#}
<select class="form-control" id="exampleFormControlSelect1" {# <select class="form-control" id="exampleFormControlSelect1"#}
x-placement="市政府网站" {# x-placement="市政府网站"#}
> {# >#}
<option>市政府网站</option> {# <option>市政府网站</option>#}
<option>1</option> {# <option>1</option>#}
<option>2</option> {# <option>2</option>#}
<option>3</option> {# <option>3</option>#}
<option>4</option> {# <option>4</option>#}
<option>5</option> {# <option>5</option>#}
</select> {# </select>#}
</div> {# </div>#}
<div style="display: block;width: 280px;"> {# <div style="display: block;width: 280px;">#}
<select class="form-control" id="exampleFormControlSelect1" {# <select class="form-control" id="exampleFormControlSelect1"#}
x-placement="国内消防站点" {# x-placement="国内消防站点"#}
> {# >#}
<option>国内消防站点</option> {# <option>国内消防站点</option>#}
<option>1</option> {# <option>1</option>#}
<option>2</option> {# <option>2</option>#}
<option>3</option> {# <option>3</option>#}
<option>4</option> {# <option>4</option>#}
<option>5</option> {# <option>5</option>#}
</select> {# </select>#}
</div> {# </div>#}
</div> {# </div>#}
</div> {# </div>#}
<div class="footer-info"> {# <div class="footer-info">#}
<div class="row"> {# <div class="row">#}
<div class="col-lg-3 col-md-6 text-center"> {# <div class="col-lg-3 col-md-6 text-center">#}
<img src="{% static 'images/red.png' %}"> {# <img src="{% static 'images/red.png' %}">#}
<img class="footerImg" src="{% static 'images/jiucuo.png' %}"> {# <img class="footerImg" src="{% static 'images/jiucuo.png' %}">#}
</div> {# </div>#}
<div class="col-lg-6 d-none d-xl-block"> {# <div class="col-lg-6 d-none d-xl-block">#}
<p class="margin-top-20 font-size-14 line-height-2 footerColor"> {# <p class="margin-top-20 font-size-14 line-height-2 footerColor">#}
地址:某某市某某东路锦海·某某区大厦B栋14楼 邮编:xxxxxx {# 地址:某某市某某东路锦海·某某区大厦B栋14楼 邮编:xxxxxx#}
</p> {# </p>#}
<p class="font-size-14 line-height-2 footerColor"> {# <p class="font-size-14 line-height-2 footerColor">#}
服务热线:0791-00000000 传真:0791-00000000 {# 服务热线:0791-00000000 传真:0791-00000000#}
</p> {# </p>#}
</div> {# </div>#}
<div class="col-lg-3 col-md-6 text-center"> {# <div class="col-lg-3 col-md-6 text-center">#}
<img src="/media/upload/images/2026/03/25/wechat.png" alt=""> {# <img src="/media/upload/images/2026/03/25/wechat.png" alt="">#}
<img class="footerImg" src="/media/upload/images/2026/03/25/weibo.png" alt=""> {# <img class="footerImg" src="/media/upload/images/2026/03/25/weibo.png" alt="">#}
</div> {# </div>#}
</div> {# </div>#}
<div class="copyright text-center line-height-2 padding-bottom-20 padding-top-20"> {# <div class="copyright text-center line-height-2 padding-bottom-20 padding-top-20">#}
<p class="margin-top-20 font-size-14 line-height-2 footerColor"> {# <p class="margin-top-20 font-size-14 line-height-2 footerColor">#}
版权所有:长白山消防救援支队 备案证号:<a href="http://beian.miit.gov.cn/" {# 版权所有:长白山消防救援支队 备案证号:<a href="http://beian.miit.gov.cn/"#}
target="_blank">辽ICP备xxxxxxxx号</a> {# target="_blank">辽ICP备xxxxxxxx号</a>#}
</p> {# </p>#}
</div> {# </div>#}
</div> {# </div>#}
</footer> {#</footer>#}
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script> <script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script> <script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
+6 -1
View File
@@ -47,12 +47,17 @@
{% if items %} {% if items %}
{% for item in items %} {% for item in items %}
<li class="news-item"> <li class="news-item">
<a href="{% url "front:article_detail" pk=item.id %}"
style="text-decoration: none;color: black;">
<div class="news-title-wrapper"> <div class="news-title-wrapper">
<span class="bullet-point"></span> <span class="bullet-point"></span>
<span class="news-title-text" <span class="news-title-text"
title="{{ item.title }}">{{ item.title|truncatechars:30 }}</span> title="{{ item.title }}">{{ item.title|truncatechars:30 }}</span>
</div> </div>
<span class="news-date">{{ item.date }}</span> </a>
<span class="news-date">{{ item.created_at | date:"Y-m-d" }}</span>
</li> </li>
{% endfor %} {% endfor %}
{% else %} {% else %}
+95
View File
@@ -0,0 +1,95 @@
{% load static %}
<footer class="footer">
<!-- 友情链接区域 -->
<div class=" d-none d-md-block">
<div class="row"
style="align-items: center;justify-content: space-evenly;padding: 15px 0;background-color: #F5F5F5;width: 100%!important;margin-left: 0 !important;gap: 10px;">
<div style="display: flex;align-items: center;">
<img src="{% static "icons/link.svg" %}" alt=""
style="width: 13px; height: 13px; object-fit: contain;">
<a href="#"
style="margin-left: 6px; text-decoration: none; color: #13227a; font-size: 14px; white-space: nowrap;">
友情链接
</a>
</div>
{% for category in categories %}
<!-- 市政府网站下拉框 -->
<div style="width: 280px;">
<select class="form-control"
onchange="if(this.value) window.open(this.value)">
<option>{{ category.name }}</option>
{% for link in category.links.all %}
{% if link.is_active %}
<!-- 这里可以动态循环 FooterLink 关联的友情链接对象 -->
<option value="{{ link.url }}">{{ link.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
{% empty %}
<!-- 如果后台没配置分类,显示默认占位 -->
<div style="width: 280px;">
<select class="form-control">
<option>暂无链接</option>
</select>
</div>
{% endfor %}
</div>
</div>
<!-- 页脚主体信息区域 -->
<div class="footer-info">
<div class="row">
<!-- 左侧:Logo 图标 -->
<div class="col-lg-3 col-md-6 text-center">
<img src="{% static 'images/red.png' %}" alt="政府标识">
<img class="footerImg" src="{% static 'images/jiucuo.png' %}" alt="找错">
</div>
<!-- 中间:文字信息 -->
<div class="col-lg-6 d-none d-xl-block">
<!-- 地址与邮编 -->
<p class="margin-top-20 font-size-14 line-height-2 footerColor">
地址:{{ footer.address }} 邮编:{{ footer.postal }}
</p>
<!-- 电话与传真 -->
<p class="font-size-14 line-height-2 footerColor">
服务热线:{{ footer.telephone }} 传真:{{ footer.fax }}
</p>
</div>
<!-- 右侧:公众号/微博二维码 -->
<div class="col-lg-3 col-md-6 text-center">
<!-- 微信二维码 -->
{% if footer.official_account %}
<img src="{{ footer.official_account.url }}" alt="微信公众号"
style="width: 50px; height: 50px; margin-right: 10px;">
{% else %}
<span>暂无二维码</span>
{% endif %}
<!-- 微博二维码 -->
{% if footer.weibo %}
<img class="footerImg" src="{{ footer.weibo.url }}" alt="政务微博"
style="width: 50px; height: 50px;">
{% else %}
<span>暂无微博</span>
{% endif %}
</div>
</div>
<!-- 版权备案信息 -->
<div class="copyright text-center line-height-2 padding-bottom-20 padding-top-20">
<p class="margin-top-20 font-size-14 line-height-2 footerColor">
版权所有:{{ footer.unit }} 备案证号:
<a href="http://beian.miit.gov.cn/" target="_blank">
{{ footer.filing }}
</a>
</p>
</div>
</div>
</footer>
+9
View File
@@ -0,0 +1,9 @@
{% load static %}
{% for menu in menus %}
<!-- 新闻中心 -->
<li class="{% if request.resolver_match.kwargs.url == menu.url %}active{% endif %}">
<a href="{% url 'front:menu' main=menu.code %}">{{ menu.title }}</a>
</li>
{% endfor %}
-45
View File
@@ -1,45 +0,0 @@
{% load static %}
<footer class="site-footer">
<div class="footer-top">
<div class="footer-links">
<span class="link-title">友情链接:</span>
<select name="links" id="links">
<option value="">市政务网站</option>
<option value="">国内消防站点</option>
</select>
</div>
</div>
<div class="footer-bottom">
<div class="footer-left">
<div class="gov-logo">
<img src="{% static 'images/gov_logo.png' %}" alt="政府机关">
<img src="{% static 'images/search_logo.png' %}" alt="政府网站找错">
</div>
<div class="footer-text">
<a href="#">关于我们</a>
<a href="#">版权声明</a>
</div>
</div>
<div class="footer-center">
<p><strong>政府服务热线:</strong> 0433-5312088</p>
<p><strong>地址:</strong> 长白山保护开发区消防救援支队防火监督与政策法规科</p>
</div>
<div class="footer-right">
<div class="qrcode">
<img src="{% static 'images/wechat_qr.png' %}" alt="微信公众号">
<p>微信公众号</p>
</div>
<div class="qrcode">
<img src="{% static 'images/weibo_qr.png' %}" alt="政务微博">
<p>政务微博</p>
</div>
</div>
</div>
<div class="footer-copyright">
<p>版权所有:长白山消防救援支队 | 网站标识码:XXXXXX | 公网安备:XXXXXXXX</p>
</div>
</footer>
+80
View File
@@ -0,0 +1,80 @@
{% extends 'base.html' %}
{% block title %}{{ article.section.title }} - 长白山消防救援支队{% endblock %}
{% block content %}
<div class="container mt-4">
<!-- 1. 面包屑导航 -->
<nav aria-label="breadcrumb" style="margin-left: -10px;">
<ol class="breadcrumb" style="background-color: transparent !important; padding-bottom: 0;">
<li class="breadcrumb-item"><a href="/">网站首页</a></li>
<!-- 动态显示所属板块 -->
<li class="breadcrumb-item"><a href="#">{{ article.section.parent.title }}</a></li>
<li class="breadcrumb-item"><a href="#">{{ article.section.title }}</a></li>
<li class="breadcrumb-item active" aria-current="page">文章详情</li>
</ol>
</nav>
<!-- 2. 文章主体内容 -->
<div class="row justify-content-center mt-4">
<div class="col-lg-10">
<!-- 文章头部信息 -->
<div class="text-center mb-5">
<h1 class="fw-bold mb-3">{{ article.title }}</h1>
<div class="text-muted small">
<span class="me-3"><i class="bi bi-clock"></i> {{ article.created_at|date:"Y-m-d H:i" }}</span>
<span class="me-3"><i class="bi bi-person"></i> {{ article.author.username|default:"管理员" }}</span>
<span><i class="bi bi-eye"></i> 阅读数:{{ article.views|default:0 }}</span>
</div>
</div>
<!-- 封面图 (如果有) -->
{% if article.cover %}
<div class="mb-4 text-center">
<img src="{{ article.cover.url }}" alt="{{ article.title }}" class="img-fluid rounded shadow-sm" style="max-height: 400px; width: 100%; object-fit: cover;">
</div>
{% endif %}
<!-- 正文内容 -->
<div class="article-content mb-5" style="line-height: 1.8; font-size: 1.1rem; color: #333;">
{{ article.content|safe }}
</div>
<!-- 3. 附件列表 (如果有附件) -->
{% if article.attachments_list %}
<div class="card bg-light mb-5">
<div class="card-header">
<i class="bi bi-paperclip"></i> 相关附件
</div>
<ul class="list-group list-group-flush">
{% for attachment in article.attachments_list %}
<li class="list-group-item bg-transparent">
<a href="{{ attachment.file.url }}" target="_blank" class="text-decoration-none">
<i class="bi bi-file-earmark-text"></i>
<!-- 截取文件名显示,防止太长 -->
{{ attachment.file.name|slice:"30:" }}
</a>
<span class="text-muted small float-end">
{{ attachment.uploaded_at|date:"Y-m-d" }}
</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- 4. 底部操作栏 -->
<div class="d-grid gap-4 d-sm-flex justify-content-sm-center border-top pt-4 mt-5">
<a href="javascript:history.back()" class="btn btn-outline-secondary btn px-3">
<i class="bi bi-arrow-left"></i> 返回上一页
</a>
<a href="/" class="btn btn-primary btn px-3">
<i class="bi bi-house-door-fill"></i> 返回首页
</a>
</div>
</div>
</div>
</div>
{% endblock %}
+50
View File
@@ -0,0 +1,50 @@
{% extends 'base.html' %}
{% block title %}页面未找到 - 404 - 长白山消防救援支队{% endblock %}
{% block content %}
<div class="container mt-5">
<!-- 面包屑导航 -->
<nav aria-label="breadcrumb" style="margin-left: -10px;">
<ol class="breadcrumb" style="background-color: transparent !important; padding-bottom: 0;">
<li class="breadcrumb-item"><a href="/">网站首页</a></li>
<li class="breadcrumb-item active" aria-current="page">404</li>
</ol>
</nav>
<!-- 404 核心内容区域 -->
<div class="row justify-content-center mt-5">
<div class="col-md-8 text-center">
<!-- 错误图标与代码 -->
<div class="mb-4">
<!-- 使用简单的 SVG 图标,你也可以换成消防相关的图片 -->
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" fill="#dc3545" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
</div>
<!-- 错误标题 -->
<h1 class="display-1 fw-bold text-danger" style="font-size: 6rem; line-height: 1;">404</h1>
<!-- 错误描述 -->
<h2 class="h3 mb-3">抱歉,您访问的页面不存在</h2>
<p class="lead text-muted mb-4">
您请求的页面可能已被删除、名称已更改或暂时不可用。<br>
请检查网址是否正确,或返回首页查找所需信息。
</p>
<!-- 操作按钮 -->
<div class="d-grid gap-30 d-sm-flex justify-content-sm-center" style="gap: 30px;">
<a href="/" class="btn btn-primary btn-md w-auto px-3">
<i class="bi bi-house-door-fill"></i> 返回首页
</a>
<button onclick="history.back()" class="btn btn-outline-secondary w-auto btn-md px-3">
<i class="bi bi-arrow-left"></i> 返回上一页
</button>
</div>
</div>
</div>
</div>
{% endblock %}
+60
View File
@@ -0,0 +1,60 @@
{% extends 'base.html' %}
{% block title %}服务器内部错误 - 500 - 长白山消防救援支队{% endblock %}
{% block content %}
<div class="container mt-5">
<!-- 面包屑导航 -->
<nav aria-label="breadcrumb" style="margin-left: -10px;">
<ol class="breadcrumb" style="background-color: transparent !important; padding-bottom: 0;">
<li class="breadcrumb-item"><a href="/">网站首页</a></li>
<li class="breadcrumb-item active" aria-current="page">系统错误</li>
</ol>
</nav>
<!-- 500 核心内容区域 -->
<div class="row justify-content-center mt-5">
<div class="col-md-8 text-center">
<!-- 错误图标 -->
<div class="mb-4">
<!-- 使用齿轮/扳手图标,代表系统维护或故障 -->
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" fill="#6c757d" class="bi bi-gear-fill" viewBox="0 0 16 16">
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
</svg>
</div>
<!-- 错误标题 -->
<h1 class="display-1 fw-bold text-secondary" style="font-size: 6rem; line-height: 1;">500</h1>
<!-- 错误描述 -->
<h2 class="h3 mb-3">系统内部错误</h2>
<p class="lead text-muted mb-4">
抱歉,服务器遇到了一些意外情况,无法完成您的请求。<br>
我们的技术人员已经收到了错误报告,正在紧急排查修复中。
</p>
<!-- 操作按钮 -->
<div class="d-grid gap-30 d-sm-flex justify-content-sm-center">
<a href="/" class="btn btn-primary btn-lg px-4">
<i class="bi bi-house-door-fill"></i> 返回首页
</a>
<!-- 刷新按钮 -->
<button onclick="window.location.reload()" class="btn btn-outline-secondary btn-lg px-4">
<i class="bi bi-arrow-clockwise"></i> 重试
</button>
</div>
<!-- 技术支持提示 -->
<div class="mt-5 p-3 bg-light rounded">
<p class="mb-0 text-muted small">
<strong>温馨提示:</strong><br>
如果您急需办理业务,请稍后再试,或联系网站管理员。<br>
联系电话:0433-xxxxxxx (示例)
</p>
</div>
</div>
</div>
</div>
{% endblock %}
-21
View File
@@ -1,21 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>长白山消防救援支队</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<!-- 顶部 -->
<header class="top-header">
<div class="container flex" >
<img src="{% static 'images/logo.png' %}" class="logo" alt="">
<div>
<h1>长白山消防救援支队</h1>
<p>CHANGBAI MOUNTAIN FIRE AND RESCUE BRIGADE</p>
</div>
</div>
</header>
</body>
</html>
+2 -1
View File
@@ -66,7 +66,8 @@
<div class="row"> <div class="row">
{% for section in section_data %} {% for section in section_data %}
<div class="col-md-6 col-sm-12" style="margin: 15px auto"> <div class="col-md-6 col-sm-12" style="margin: 15px auto">
{% include 'components/_news_card.html' with data=section.data unique_id=section.key %} {% url "front:main_list" main=section.key as card_more_url %}
{% include 'components/_news_card.html' with data=section.data unique_id=section.key more_url=card_more_url %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
+5 -3
View File
@@ -1,17 +1,19 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}信息公开 - 长白山消防救援支队{% endblock %} {% block title %}{{ main_section.title }} - 长白山消防救援支队{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<nav aria-label="breadcrumb" style="margin-left: -10px;"> <nav aria-label="breadcrumb" style="margin-left: -10px;">
<ol class="breadcrumb" style="background-color: transparent !important;padding-bottom: 0;"> <ol class="breadcrumb" style="background-color: transparent !important;padding-bottom: 0;">
<li class="breadcrumb-item"><a href="#">网站首页</a></li> <li class="breadcrumb-item"><a href="#">网站首页</a></li>
<li class="breadcrumb-item active" aria-current="page">信息公开</li> <li class="breadcrumb-item active" aria-current="page">{{ main_section.title }}</li>
</ol> </ol>
</nav> </nav>
<div class="row" style="margin: 15px auto "> <div class="row" style="margin: 15px auto ">
{% for section in sections %} {% for section in sections %}
<div class="col-6" style="margin: auto auto 15px"> <div class="col-6" style="margin: auto auto 15px">
{% include 'components/_news_card.html' with data=section.data unique_id=section.key %} {% url "front:sub_list" main=main_section.code sub=section.key as card_more_url %}
{% include 'components/_news_card.html' with data=section.data unique_id=section.key more_url=card_more_url %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
+172
View File
@@ -0,0 +1,172 @@
{% extends 'base.html' %}
{% block title %}{{ main_section.title }} - 长白山消防救援支队{% endblock %}
{% block content %}
<div class="container">
<nav aria-label="breadcrumb" style="margin-left: -10px;">
<ol class="breadcrumb" style="background-color: transparent !important;padding-bottom: 0;">
<li class="breadcrumb-item"><a href="#">网站首页</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ main_section.title }}</li>
</ol>
</nav>
<div class="row" style="height: 100%;margin: 15px auto;margin-right: 0 !important;">
<div class="col-3">
<ul class="list-group">
<li class="list-group-item" style="background: #23418A; color: #f5f5f5">
<a href="{% url "front:main_list" main=main_section.code %}"
style="color: #F5F5F5;text-decoration: none;">
<h5>{{ main_section.title }}</h5>
</a>
</li>
{% for sec in sections %}
<li class="list-group-item">
<a href="{% url "front:sub_list" main=main_section.code sub=sec.code %}"
style="color: #1d2124;text-decoration: none;"> {{ sec.title }}</a>
</li>
{% endfor %}
</ul>
</div>
<div class="col-9" style="width: 100%;padding-right: 0 !important;">
<!-- 组件开始 -->
<div class="section-header-card">
<!-- 左侧文字内容 -->
<div class="header-content">
<div class="header-title">
<span class="icon-circle"></span>
{{ sub_section.title | default:main_section.title }}
</div>
<hr class="divider-line">
</div>
{# <!-- 右侧装饰色块 -->#}
{# <div class="right-decoration">#}
{# <div class="deco-top"></div>#}
{# <div class="deco-bottom"></div>#}
{# </div>#}
<ul class="list-unstyled mb-0" style="width: 100%;padding-right: 38px; margin-left: 0;">
{# items 就是当前 key 对应的列表数据,直接使用 #}
{% if articles %}
{% for item in articles %}
<li class="news-item">
<div class="news-title-wrapper">
<span class="bullet-point"></span>
<span class="news-title-text"
title="{{ item.title }}">{{ item.title|truncatechars:30 }}</span>
</div>
<span class="news-date">{{ item.created_at }}</span>
</li>
{% endfor %}
{% else %}
<li class="text-muted text-center py-2">暂无数据</li>
{% endif %}
</ul>
</div>
<!-- 组件结束 -->
<div style="display: flex; justify-content: center; margin-top: 10px;margin-bottom: -5px;">
<ul class="pagination" style="margin: auto;">
<li class="page-item">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<style>
/* --- 组件核心样式 --- */
.section-header-card {
position: relative;
width: 100%;
{#max-width: 800px; /* 模拟图片中的长宽比 */#} min-height: 720px; /* 设置一个保底高度 */
height: auto; /* 高度随内容增加而增加 */
background-color: #ffffff;
border-radius: 12px; /* 大圆角 */
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); /* 柔和阴影 */
overflow: hidden; /* 确保右侧装饰块不溢出圆角 */
display: flex;
flex-direction: column;
align-items: center;
padding-left: 30px;
{#justify-content: space-between;#}
}
/* 左侧内容区域 */
.header-content {
z-index: 2; /* 确保内容在装饰块之上 */
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
}
/* 标题部分 */
.header-title {
font-size: 20px;
font-weight: 700;
color: #23408A; /* 深蓝色文字 */
margin-bottom: 12px;
display: flex;
align-items: center;
}
/* 空心圆圈图标 */
.icon-circle {
display: inline-block;
width: 10px;
height: 10px;
border: 2px solid #23408A; /* 边框颜色同文字 */
border-radius: 50%;
margin-right: 10px;
box-sizing: border-box; /* 确保边框包含在宽高内 */
}
/* 分割线 */
.divider-line {
width: 95%; /* 线条长度,不完全贯穿 */
height: 2px;
background-color: #23408A; /* 线条颜色比文字稍浅一点或保持一致 */
border: none;
margin: 0;
}
/* --- 右侧装饰块 --- */
.right-decoration {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 60px; /* 装饰块宽度 */
display: flex;
flex-direction: column;
}
/* 上方深色块 */
.deco-top {
flex: 1; /* 占据上半部分 */
background-color: #5a6da3; /* 深蓝灰色 */
min-height: 50%;
}
/* 下方浅色块 */
.deco-bottom {
flex: 1; /* 占据下半部分 */
background-color: #eef4ff; /* 极浅的蓝色 */
min-height: 50%;
}
</style>
{% endblock %}
+1 -1
View File
@@ -8,7 +8,7 @@
<nav aria-label="breadcrumb" style="margin-left: -10px;"> <nav aria-label="breadcrumb" style="margin-left: -10px;">
<ol class="breadcrumb" style="background-color: transparent !important;padding-bottom: 0;"> <ol class="breadcrumb" style="background-color: transparent !important;padding-bottom: 0;">
<li class="breadcrumb-item"><a href="#">网站首页</a></li> <li class="breadcrumb-item"><a href="#">网站首页</a></li>
<li class="breadcrumb-item active" aria-current="page">信息公开</li> <li class="breadcrumb-item active" aria-current="page">新闻中心</li>
</ol> </ol>
</nav> </nav>
<div style="margin: 0 5px;"> <div style="margin: 0 5px;">
+11 -8
View File
@@ -1,9 +1,14 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from import_export.admin import ImportExportModelAdmin
from .models import User from .models import User
from .models import Department
from .models import Role, Permission
@admin.register(User) @admin.register(User)
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin, ImportExportModelAdmin):
list_display = ('username', 'cname', 'dept', 'is_active', 'is_staff') list_display = ('username', 'cname', 'dept', 'is_active', 'is_staff')
list_filter = ('is_active', 'is_staff', 'dept') list_filter = ('is_active', 'is_staff', 'dept')
search_fields = ('username', 'cname') search_fields = ('username', 'cname')
@@ -22,31 +27,29 @@ class UserAdmin(BaseUserAdmin):
ordering = ('username',) ordering = ('username',)
filter_horizontal = ('groups', 'user_permissions', 'roles') # 如果 roles 是 ManyToMany filter_horizontal = ('groups', 'user_permissions', 'roles') # 如果 roles 是 ManyToMany
from django.contrib import admin
from .models import Department
@admin.register(Department) @admin.register(Department)
class DepartmentAdmin(admin.ModelAdmin): class DepartmentAdmin(ImportExportModelAdmin):
list_display = ('name', 'parent', 'order') list_display = ('name', 'parent', 'order')
list_filter = ('parent',) list_filter = ('parent',)
search_fields = ('name',) search_fields = ('name',)
ordering = ('order', 'name') ordering = ('order', 'name')
from django.contrib import admin
from .models import Role, Permission
@admin.register(Role) @admin.register(Role)
class RoleAdmin(admin.ModelAdmin): class RoleAdmin(ImportExportModelAdmin):
list_display = ('name', 'permission_count') list_display = ('name', 'permission_count')
search_fields = ('name',) search_fields = ('name',)
filter_horizontal = ('permissions',) filter_horizontal = ('permissions',)
def permission_count(self, obj): def permission_count(self, obj):
return obj.permissions.count() return obj.permissions.count()
permission_count.short_description = "权限数量" permission_count.short_description = "权限数量"
@admin.register(Permission) @admin.register(Permission)
class PermissionAdmin(admin.ModelAdmin): class PermissionAdmin(ImportExportModelAdmin):
list_display = ('resource', 'action', 'desc') list_display = ('resource', 'action', 'desc')
list_filter = ('resource', 'action') list_filter = ('resource', 'action')
search_fields = ('resource', 'desc') search_fields = ('resource', 'desc')
+1 -1
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.contrib.auth.models import django.contrib.auth.models
import django.db.models.deletion import django.db.models.deletion
+46
View File
@@ -0,0 +1,46 @@
import os
import sys
import django
from faker import Faker
# 配置 Django 环境
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") # 修改为你的 settings 路径
django.setup()
from users.models import User
# 初始化 Faker,设置中文(如果需要中文名)
fake = Faker('zh_CN')
def seed_users(count=5):
print(f"正在生成 {count} 个用户...")
users = []
for i in range(count):
# 生成随机用户名
username = fake.user_name()
# 检查用户名是否已存在,避免冲突
if User.objects.filter(username=username).exists():
continue
user = User.objects.create_user(
username=username,
email=fake.safe_email(),
password='password123', # 统一密码,方便测试登录
is_active=True,
is_staff=False, # 随机是否为管理员
first_name=fake.first_name(),
last_name=fake.last_name()
)
users.append(user)
print(f" - 创建用户: {username} (密码: password123)")
print("用户生成完毕!")
if __name__ == "__main__":
seed_users(10)
+95
View File
@@ -0,0 +1,95 @@
import os
import hashlib
from datetime import datetime
def _get_file_md5(file_obj):
"""
通用辅助函数:计算任意文件对象的 MD5
"""
md5 = hashlib.md5()
# 关键步骤:重置文件指针到开头
# 因为 Django 在处理上传时可能已经读取了一部分文件头
try:
file_obj.seek(0)
except Exception:
pass
# 分块读取计算,防止大文件撑爆内存
for chunk in file_obj.chunks():
md5.update(chunk)
# 计算完后再次重置指针,确保不影响 Django 后续的保存操作
try:
file_obj.seek(0)
except Exception:
pass
return md5.hexdigest()
def upload_image_and_rename(instance, filename):
"""
通用图片上传处理函数
逻辑:存入 images/年/月/日/md5.ext
"""
# 1. 获取后缀
ext = os.path.splitext(filename)[1].lower()
# 2. 生成日期路径
date_str = datetime.now().strftime('%Y/%m/%d')
# 3. 计算 MD5
# 这里直接利用传入的 filename 对应的文件对象(在内存或临时文件中)
# 注意:这里假设 filename 是有效的 UploadedFile 对象名称,
# 但最稳妥的方式其实是直接操作 file_obj。
# 由于 upload_to 签名限制,我们通常通过 instance 获取字段值,
# 但为了通用性,我们尝试从 instance 的 __dict__ 中寻找 FileField 对象。
file_obj = None
# 遍历 instance 的所有属性,找到第一个 FileField 类型的值
for field in instance._meta.get_fields():
if hasattr(field, 'upload_to') and hasattr(instance, field.name):
val = getattr(instance, field.name)
if val and hasattr(val, 'chunks'): # 确认是文件对象
file_obj = val
break
# 如果找不到文件对象(极少见情况),使用随机字符串防止报错
if not file_obj:
import uuid
new_filename = f"{uuid.uuid4().hex}{ext}"
else:
md5_hash = _get_file_md5(file_obj)
new_filename = f"{md5_hash}{ext}"
# 4. 返回路径
return os.path.join('images', date_str, new_filename)
def upload_file_and_rename(instance, filename):
"""
通用文件上传处理函数
逻辑:存入 uploads/年/月/日/md5.ext
"""
ext = os.path.splitext(filename)[1].lower()
date_str = datetime.now().strftime('%Y/%m/%d')
file_obj = None
# 同样的通用查找逻辑
for field in instance._meta.get_fields():
if hasattr(field, 'upload_to') and hasattr(instance, field.name):
val = getattr(instance, field.name)
if val and hasattr(val, 'chunks'):
file_obj = val
break
if not file_obj:
import uuid
new_filename = f"{uuid.uuid4().hex}{ext}"
else:
md5_hash = _get_file_md5(file_obj)
new_filename = f"{md5_hash}{ext}"
return os.path.join('uploads', date_str, new_filename)