update. 完成前端页面展示、列表和详情页
update. 调整融合主菜单与板块 update. 分离新闻模块
This commit is contained in:
@@ -7,8 +7,6 @@ config/supervisord.sock
|
||||
.idea/
|
||||
/.idea/
|
||||
/.idea/*
|
||||
/templates/
|
||||
/templates/*
|
||||
**/__pycache__
|
||||
**/.DS_Store
|
||||
/venv/
|
||||
|
||||
+79
-41
@@ -1,51 +1,56 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
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
|
||||
|
||||
|
||||
# —————— 板块 ——————
|
||||
@admin.register(Section)
|
||||
class SectionAdmin(admin.ModelAdmin):
|
||||
list_display = ('display_name', 'code', 'parent', 'is_enabled', 'order')
|
||||
list_filter = ('parent', 'enabled')
|
||||
search_fields = ('name', 'code')
|
||||
ordering = ('order', 'name')
|
||||
list_editable = ('order',)
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('name', 'code', 'parent', 'enabled', 'order')
|
||||
}),
|
||||
)
|
||||
|
||||
def display_name(self, obj):
|
||||
"""在列表中显示层级缩进"""
|
||||
if obj.is_sub:
|
||||
return f" └─ {obj.name}" # 使用全角空格+符号模拟缩进
|
||||
return obj.name
|
||||
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"] = Section.objects.filter(parent__isnull=True)
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""优化查询,避免 N+1"""
|
||||
return super().get_queryset(request).select_related('parent')
|
||||
|
||||
# @admin.register(ArticleSection)
|
||||
# class SectionAdmin(admin.ModelAdmin):
|
||||
# list_display = ('display_name', 'code', 'parent', 'is_enabled', 'order')
|
||||
# list_filter = ('parent', 'enabled')
|
||||
# search_fields = ('name', 'code')
|
||||
# ordering = ('order', 'name')
|
||||
# list_editable = ('order',)
|
||||
# fieldsets = (
|
||||
# ('基本信息', {
|
||||
# 'fields': ('name', 'code', 'parent', 'enabled', 'order')
|
||||
# }),
|
||||
# )
|
||||
#
|
||||
# def display_name(self, obj):
|
||||
# """在列表中显示层级缩进"""
|
||||
# if obj.is_sub:
|
||||
# return f" └─ {obj.name}" # 使用全角空格+符号模拟缩进
|
||||
# return obj.name
|
||||
#
|
||||
# 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"] = ArticleSection.objects.filter(parent__isnull=True)
|
||||
# return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
#
|
||||
# def get_queryset(self, request):
|
||||
# """优化查询,避免 N+1"""
|
||||
# return super().get_queryset(request).select_related('parent')
|
||||
#
|
||||
|
||||
# —————— 轮播图 ——————
|
||||
@admin.register(Carousel)
|
||||
class CarouselAdmin(admin.ModelAdmin):
|
||||
class CarouselAdmin(ImportExportModelAdmin):
|
||||
list_display = ('title', 'image_thumb', 'link_url', 'order', 'active')
|
||||
list_editable = ('order', 'active')
|
||||
ordering = ('order',)
|
||||
@@ -54,18 +59,49 @@ class CarouselAdmin(admin.ModelAdmin):
|
||||
if obj.image:
|
||||
return format_html('<image src="{}" width="60" height="auto" />', obj.image.url)
|
||||
return "无图"
|
||||
|
||||
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)
|
||||
class ArticleAdmin(admin.ModelAdmin):
|
||||
class ArticleAdmin(ImportExportModelAdmin):
|
||||
form = ArticleForm # <--- 关联自定义表单
|
||||
list_display = ('title', 'section', 'author', 'is_published', 'created_at')
|
||||
list_filter = ('section', 'is_published', 'created_at')
|
||||
search_fields = ('title', 'content')
|
||||
date_hierarchy = 'created_at'
|
||||
autocomplete_fields = ('author',) # 需要 UserAdmin 支持搜索
|
||||
|
||||
# 核心配置:将附件内联注册到文章管理中
|
||||
inlines = [ArticleAttachmentInline]
|
||||
fieldsets = (
|
||||
('基本信息', {'fields': ('title', 'section', 'cover')}),
|
||||
('内容', {'fields': ('content',)}),
|
||||
@@ -75,7 +111,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||
|
||||
# —————— 通知(带签收) ——————
|
||||
@admin.register(Notice)
|
||||
class NoticeAdmin(admin.ModelAdmin):
|
||||
class NoticeAdmin(ImportExportModelAdmin):
|
||||
list_display = (
|
||||
'title',
|
||||
'publisher',
|
||||
@@ -112,6 +148,7 @@ class NoticeAdmin(admin.ModelAdmin):
|
||||
|
||||
def signed_count(self, obj):
|
||||
return obj.receipts.count()
|
||||
|
||||
signed_count.short_description = "已签收人数"
|
||||
|
||||
def total_target_users(self, obj):
|
||||
@@ -123,12 +160,13 @@ class NoticeAdmin(admin.ModelAdmin):
|
||||
else:
|
||||
count = User.objects.filter(is_active=True).count()
|
||||
return count
|
||||
|
||||
total_target_users.short_description = "应签收人数"
|
||||
|
||||
|
||||
# —————— 签收记录 ——————
|
||||
@admin.register(NoticeReceipt)
|
||||
class NoticeReceiptAdmin(admin.ModelAdmin):
|
||||
class NoticeReceiptAdmin(ImportExportModelAdmin):
|
||||
list_display = ('notice', 'user', 'signed_at')
|
||||
list_filter = ('signed_at', 'notice__title')
|
||||
search_fields = ('user__cname', 'user__username', 'notice__title')
|
||||
|
||||
@@ -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
|
||||
from django.db import migrations, models
|
||||
@@ -16,24 +16,40 @@ class Migration(migrations.Migration):
|
||||
name='Article',
|
||||
fields=[
|
||||
('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='创建时间')),
|
||||
('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={
|
||||
'verbose_name': '文章',
|
||||
'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(
|
||||
name='Carousel',
|
||||
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=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='跳转链接')),
|
||||
('order', models.IntegerField(default=0, verbose_name='排序')),
|
||||
('active', models.BooleanField(default=True, verbose_name='启用')),
|
||||
@@ -63,6 +79,8 @@ class Migration(migrations.Migration):
|
||||
name='NoticeReceipt',
|
||||
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='更新时间')),
|
||||
('signed_at', models.DateTimeField(auto_now_add=True, verbose_name='签收时间')),
|
||||
],
|
||||
options={
|
||||
@@ -70,19 +88,4 @@ class Migration(migrations.Migration):
|
||||
'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': '板块管理',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-24 10:31
|
||||
# Generated by Django 6.0.3 on 2026-04-01 00:55
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
@@ -11,6 +11,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('article', '0001_initial'),
|
||||
('base', '0001_initial'),
|
||||
('users', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
@@ -21,6 +22,16 @@ class Migration(migrations.Migration):
|
||||
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='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(
|
||||
model_name='notice',
|
||||
name='publisher',
|
||||
@@ -41,11 +52,6 @@ class Migration(migrations.Migration):
|
||||
name='user',
|
||||
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(
|
||||
name='noticereceipt',
|
||||
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
@@ -1,79 +1,22 @@
|
||||
from django.db import models
|
||||
from django_ckeditor_5.fields import CKEditor5Field
|
||||
|
||||
from base.models import MainMenu
|
||||
from common.base import BaseEntity
|
||||
from users.models import User, Department
|
||||
|
||||
|
||||
# Create your models here.
|
||||
|
||||
from django.db import models
|
||||
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):
|
||||
class Article(BaseEntity):
|
||||
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="作者")
|
||||
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)
|
||||
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
||||
updated_at = models.DateTimeField("更新时间", auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "文章"
|
||||
@@ -83,7 +26,38 @@ class Article(models.Model):
|
||||
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)
|
||||
content = CKEditor5Field("通知内容")
|
||||
publisher = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="发布人")
|
||||
@@ -101,7 +75,7 @@ class Notice(models.Model):
|
||||
return self.title
|
||||
|
||||
|
||||
class NoticeReceipt(models.Model):
|
||||
class NoticeReceipt(BaseEntity):
|
||||
"""签收记录"""
|
||||
notice = models.ForeignKey(Notice, on_delete=models.CASCADE, related_name='receipts')
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="签收人")
|
||||
|
||||
@@ -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
@@ -13,7 +13,7 @@ router = Router(tags=['content'])
|
||||
|
||||
|
||||
@router.get('/article', description="获取文章列表")
|
||||
def article_list(request):
|
||||
def article_details(request):
|
||||
if not user_has_permission(request.user, 'article', 'view'):
|
||||
return HttpResponseForbidden("无权访问")
|
||||
articles = Article.objects.all()
|
||||
|
||||
+54
-11
@@ -1,24 +1,67 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
||||
from article.models import Section, Carousel, Article, Notice, NoticeReceipt
|
||||
from base.models import MainMenu, FooterLink
|
||||
from users.models import User
|
||||
from base.models import MainMenu, FooterLink, LinkCategory, FriendLink
|
||||
|
||||
|
||||
# —————— 主菜单 ——————
|
||||
@admin.register(MainMenu)
|
||||
class MainMenuAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'url', 'parent', 'order', 'visible')
|
||||
class MainMenuAdmin(ImportExportModelAdmin):
|
||||
list_display = ('display_name', 'code', 'url', 'parent', 'order', 'visible')
|
||||
list_editable = ('order', 'visible')
|
||||
list_filter = ('parent', 'visible')
|
||||
search_fields = ('title', 'url')
|
||||
ordering = ('order',)
|
||||
ordering = ('id',)
|
||||
|
||||
def display_name(self, obj):
|
||||
"""在列表中显示层级缩进"""
|
||||
if obj.is_sub:
|
||||
return f" └─ {obj.title}" # 使用全角空格+符号模拟缩进
|
||||
return obj.title
|
||||
|
||||
display_name.short_description = '板块名称'
|
||||
display_name.admin_order_field = 'name'
|
||||
|
||||
def is_enabled(self, obj):
|
||||
return obj.enabled
|
||||
|
||||
is_enabled.boolean = True
|
||||
is_enabled.short_description = '启用'
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
"""限制 parent 只能选择主板块(无父级的板块)"""
|
||||
if db_field.name == "parent":
|
||||
kwargs["queryset"] = MainMenu.objects.filter(parent__isnull=True, is_external=False)
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""优化查询,避免 N+1"""
|
||||
return super().get_queryset(request).select_related('parent')
|
||||
|
||||
|
||||
# 1. 注册分类管理
|
||||
@admin.register(LinkCategory)
|
||||
class LinkCategoryAdmin(ImportExportModelAdmin):
|
||||
list_display = ('name', 'code', 'order', 'is_active')
|
||||
list_editable = ('order', 'is_active')
|
||||
|
||||
|
||||
# 2. 注册链接管理
|
||||
@admin.register(FriendLink)
|
||||
class FriendLinkAdmin(ImportExportModelAdmin):
|
||||
list_display = ('name', 'category', 'url', 'order', 'is_active')
|
||||
list_filter = ('category', 'is_active') # 左侧过滤器,方便筛选
|
||||
search_fields = ('name', 'url')
|
||||
|
||||
# 优化:在添加链接时,只显示启用的分类
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
if db_field.name == "category":
|
||||
kwargs["queryset"] = LinkCategory.objects.filter(is_active=True)
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
|
||||
# —————— 页脚链接 ——————
|
||||
@admin.register(FooterLink)
|
||||
class FooterLinkAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'url', 'order')
|
||||
list_editable = ('order',)
|
||||
ordering = ('order',)
|
||||
class FooterLinkAdmin(ImportExportModelAdmin):
|
||||
list_display = ('title', 'address', 'unit', 'postal', 'telephone', 'fax', 'filing')
|
||||
list_editable = ('postal', 'telephone', 'fax', 'unit', 'filing',)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-24 10:31
|
||||
# Generated by Django 6.0.3 on 2026-04-01 00:55
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
@@ -16,29 +16,77 @@ class Migration(migrations.Migration):
|
||||
name='FooterLink',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=50, verbose_name='链接文字')),
|
||||
('url', models.URLField(verbose_name='链接地址')),
|
||||
('order', models.IntegerField(default=0, verbose_name='排序')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('title', models.CharField(max_length=50, verbose_name='页脚信息')),
|
||||
('address', models.CharField(max_length=200, verbose_name='单位地址')),
|
||||
('postal', models.CharField(default='', verbose_name='邮编')),
|
||||
('telephone', models.CharField(default='', verbose_name='服务热线')),
|
||||
('fax', models.CharField(default='', verbose_name='传真')),
|
||||
('unit', models.CharField(default='', verbose_name='版权单位')),
|
||||
('filing', models.CharField(default='', verbose_name='备案号')),
|
||||
('official_account', models.ImageField(default='', upload_to='carousel/', verbose_name='公众号二维码')),
|
||||
('weibo', models.ImageField(default='', upload_to='carousel/', verbose_name='微博二维码')),
|
||||
('is_active', models.BooleanField(default=False, verbose_name='生效')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '页脚链接',
|
||||
'verbose_name': '页脚信息',
|
||||
'verbose_name_plural': '页脚管理',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LinkCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('name', models.CharField(max_length=50, unique=True, verbose_name='分类名称')),
|
||||
('code', models.SlugField(help_text="用于前端模板调用的标识,如 'gov', 'fire'", unique=True, verbose_name='分类代码')),
|
||||
('order', models.IntegerField(default=0, verbose_name='显示排序')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '友链分类',
|
||||
'verbose_name_plural': '友链分类管理',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FriendLink',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('name', models.CharField(max_length=100, verbose_name='网站名称')),
|
||||
('url', models.URLField(verbose_name='链接地址')),
|
||||
('order', models.IntegerField(default=0, verbose_name='排序')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='base.linkcategory', verbose_name='所属分类')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '友情链接',
|
||||
'verbose_name_plural': '友情链接管理',
|
||||
'ordering': ['order', '-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MainMenu',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=50, verbose_name='菜单标题')),
|
||||
('url', models.CharField(help_text='如 /news/ 或 外链 https://xxx', max_length=200, verbose_name='链接地址')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('title', models.CharField(max_length=100, verbose_name='标题')),
|
||||
('code', models.SlugField(help_text='用于URL或模板识别,建议英文', unique=True, verbose_name='唯一标识')),
|
||||
('url', models.CharField(blank=True, help_text='如 /menu/news.html 或 外链', max_length=200, verbose_name='链接地址')),
|
||||
('is_external', models.BooleanField(default=False, verbose_name='是否外链')),
|
||||
('order', models.IntegerField(default=0, verbose_name='排序')),
|
||||
('visible', models.BooleanField(default=True, verbose_name='是否显示')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.mainmenu', verbose_name='父菜单')),
|
||||
('order', models.PositiveSmallIntegerField(default=0, verbose_name='排序权重')),
|
||||
('visible', models.BooleanField(default=True, verbose_name='显示状态')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='base.mainmenu', verbose_name='父级节点')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '主菜单',
|
||||
'verbose_name_plural': '主菜单管理',
|
||||
'verbose_name': '站点导航/板块',
|
||||
'verbose_name_plural': '站点结构管理',
|
||||
'ordering': ('order', 'title'),
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 6.0.3 on 2026-04-01 01:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='friendlink',
|
||||
options={'ordering': ['order', '-created_at'], 'verbose_name': '友情链接', 'verbose_name_plural': '友链管理'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='linkcategory',
|
||||
options={'ordering': ['order'], 'verbose_name': '友链分类', 'verbose_name_plural': '友链分类'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='mainmenu',
|
||||
options={'ordering': ('order', 'title'), 'verbose_name': '站点导航', 'verbose_name_plural': '站点导航'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mainmenu',
|
||||
name='is_carousel',
|
||||
field=models.BooleanField(default=False, verbose_name='轮播图'),
|
||||
),
|
||||
]
|
||||
+131
-14
@@ -1,27 +1,144 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
|
||||
from common.base import BaseEntity
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class MainMenu(models.Model):
|
||||
title = models.CharField("菜单标题", max_length=50)
|
||||
url = models.CharField("链接地址", max_length=200, help_text="如 /news/ 或 外链 https://xxx")
|
||||
is_external = models.BooleanField("是否外链", default=False)
|
||||
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, verbose_name="父菜单")
|
||||
order = models.IntegerField("排序", default=0)
|
||||
visible = models.BooleanField("是否显示", default=True)
|
||||
|
||||
|
||||
# --- 新增:友情链接分类模型 ---
|
||||
class LinkCategory(BaseEntity):
|
||||
name = models.CharField("分类名称", max_length=50, unique=True) # 分类名,如"政府网站"
|
||||
code = models.SlugField("分类代码", max_length=50, unique=True, help_text="用于前端模板调用的标识,如 'gov', 'fire'")
|
||||
order = models.IntegerField("显示排序", default=0) # 分类本身的排序
|
||||
is_active = models.BooleanField("是否启用", default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "主菜单"
|
||||
verbose_name_plural = "主菜单管理"
|
||||
verbose_name = "友链分类"
|
||||
verbose_name_plural = "友链分类"
|
||||
ordering = ['order']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
return self.name
|
||||
|
||||
|
||||
class FooterLink(models.Model):
|
||||
title = models.CharField("链接文字", max_length=50)
|
||||
url = models.URLField("链接地址")
|
||||
# --- 修改:友情链接模型 ---
|
||||
class FriendLink(BaseEntity):
|
||||
# 1. 关联到新的分类模型(一对多关系:一个分类下有多个链接)
|
||||
category = models.ForeignKey(
|
||||
LinkCategory,
|
||||
on_delete=models.CASCADE, # 如果分类被删,关联的链接也删除
|
||||
verbose_name="所属分类",
|
||||
related_name='links' # 反向查询名称,方便后续调用
|
||||
)
|
||||
|
||||
name = models.CharField("网站名称", max_length=100)
|
||||
url = models.URLField("链接地址", max_length=200)
|
||||
|
||||
# 2. 排序和状态
|
||||
order = models.IntegerField("排序", default=0)
|
||||
is_active = models.BooleanField("是否启用", default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "页脚链接"
|
||||
verbose_name = "友情链接"
|
||||
verbose_name_plural = "友链管理"
|
||||
ordering = ['order', '-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.category.name}] {self.name}"
|
||||
|
||||
|
||||
class MainMenu(BaseEntity):
|
||||
# --- 基础信息 ---
|
||||
title = models.CharField("标题", max_length=100)
|
||||
code = models.SlugField("唯一标识", max_length=50, unique=True, help_text="用于URL或模板识别,建议英文")
|
||||
|
||||
# --- 树形结构 ---
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='children',
|
||||
verbose_name="父级节点"
|
||||
)
|
||||
|
||||
# --- 菜单特有属性 ---
|
||||
# 只有顶级节点(主菜单)才需要填写这个,子节点作为板块时通常不需要跳转链接
|
||||
url = models.CharField("链接地址", max_length=200, blank=True, help_text="如 /menu/news.html 或 外链")
|
||||
is_external = models.BooleanField("是否外链", default=False)
|
||||
is_carousel = models.BooleanField("轮播图", default=False)
|
||||
|
||||
# --- 通用属性 ---
|
||||
order = models.PositiveSmallIntegerField("排序权重", default=0)
|
||||
visible = models.BooleanField("显示状态", default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "站点导航"
|
||||
verbose_name_plural = "站点导航"
|
||||
ordering = ('order', 'title')
|
||||
|
||||
def __str__(self):
|
||||
# 显示层级路径,例如:新闻中心 → 国内新闻
|
||||
if self.parent:
|
||||
return f"{self.parent.title} → {self.title}"
|
||||
return self.title
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
# 限制层级:最多 2 级
|
||||
if self.parent and self.parent.parent:
|
||||
raise ValidationError('层级过深:子节点不能再设置子级,最多只允许两级结构。')
|
||||
|
||||
# 防止自引用
|
||||
if self.pk and self.parent and self.parent.pk == self.pk:
|
||||
raise ValidationError('节点不能以自身为父级。')
|
||||
|
||||
# --- 核心逻辑属性 ---
|
||||
@property
|
||||
def is_sub(self):
|
||||
"""是否为子板块(即是否有父级)"""
|
||||
return self.parent is not None
|
||||
|
||||
@property
|
||||
def is_main(self):
|
||||
"""是否为主菜单(即是否无父级)"""
|
||||
return not self.parent
|
||||
|
||||
@property
|
||||
def active_children(self):
|
||||
"""
|
||||
获取当前节点下可见的子节点(用于前端导航渲染)
|
||||
"""
|
||||
return self.children.filter(visible=True)
|
||||
|
||||
|
||||
class FooterLink(BaseEntity):
|
||||
title = models.CharField("页脚信息", max_length=50)
|
||||
address = models.CharField("单位地址", max_length=200)
|
||||
postal = models.CharField("邮编", default="")
|
||||
telephone = models.CharField("服务热线", default="")
|
||||
fax = models.CharField("传真", default="")
|
||||
unit = models.CharField("版权单位", default="")
|
||||
filing = models.CharField("备案号", default="")
|
||||
official_account = models.ImageField("公众号二维码", default="", upload_to='carousel/')
|
||||
weibo = models.ImageField("微博二维码", default="", upload_to='carousel/')
|
||||
is_active = models.BooleanField("生效", default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "页脚信息"
|
||||
verbose_name_plural = "页脚管理"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} - 状态:{'使用中' if self.is_active else '已停用'}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# 1. 如果当前这条数据被标记为“生效” (is_active=True)
|
||||
if self.is_active:
|
||||
# 2. 将数据库中【除了当前这条】以外的所有 FooterLink 都更新为“不生效”
|
||||
# self.pk 是当前对象的唯一标识
|
||||
FooterLink.objects.exclude(pk=self.pk).update(is_active=False)
|
||||
|
||||
# 3. 执行正常的保存操作
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
+107
@@ -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()
|
||||
@@ -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,
|
||||
}
|
||||
@@ -1,5 +1,45 @@
|
||||
from django.shortcuts import render
|
||||
from ninja import Router
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.conf import settings
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
# Create your views here.
|
||||
router = Router(tags=['content'])
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
# 返回文件 URL(CKEditor 规范格式)
|
||||
file_url = f"{settings.MEDIA_URL}uploads/{filename}"
|
||||
return JsonResponse({
|
||||
"url": file_url,
|
||||
"default": file_url
|
||||
})
|
||||
|
||||
@@ -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
Binary file not shown.
+4
-1
@@ -6,7 +6,9 @@ app_name = 'front' # 可选,用于命名空间(如 {% url 'main:index' %}
|
||||
|
||||
urlpatterns = [
|
||||
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('news/', views.news, name='news'),
|
||||
path('info/', views.info, name='info'),
|
||||
@@ -14,4 +16,5 @@ urlpatterns = [
|
||||
path('party/', views.party, name='party'),
|
||||
path('interaction/', views.interaction, name='interaction'),
|
||||
path('science/', views.science, name='science'),
|
||||
path('article/<int:pk>.html/', views.article_detail, name='article_detail'),
|
||||
]
|
||||
|
||||
+184
-147
@@ -1,14 +1,36 @@
|
||||
import traceback
|
||||
from typing import Dict, List
|
||||
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
|
||||
|
||||
|
||||
# Create your views here.
|
||||
|
||||
def h(request):
|
||||
return render(request, 'front/h.html', )
|
||||
# 2. 新增:文章详情视图函数
|
||||
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):
|
||||
@@ -37,64 +59,91 @@ def index(request):
|
||||
]
|
||||
}
|
||||
}
|
||||
section_data = [
|
||||
{
|
||||
"key": "info",
|
||||
# section_data = [
|
||||
# {
|
||||
# "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": {
|
||||
'信息公开': [
|
||||
{'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'},
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
main.title: all_articles[:7]
|
||||
}
|
||||
})
|
||||
logger.info(section_data)
|
||||
carousel = {
|
||||
"key": "index",
|
||||
"data": [
|
||||
@@ -135,112 +184,100 @@ def about(request):
|
||||
|
||||
|
||||
def news(request):
|
||||
sections = [
|
||||
sections = NewsSection.objects.filter(enabled=True).all().order_by('order')
|
||||
section_data = [
|
||||
{
|
||||
"key": "fire_news",
|
||||
"key": section.code,
|
||||
"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": "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'},
|
||||
]
|
||||
section.name: section.news_set.filter(is_published=True)[:7],
|
||||
}
|
||||
}
|
||||
for section in sections
|
||||
]
|
||||
|
||||
context = {
|
||||
'sections': sections,
|
||||
'sections': section_data,
|
||||
}
|
||||
logger.info(context)
|
||||
return render(request, 'front/news.html', context)
|
||||
|
||||
|
||||
def info(request):
|
||||
def info(request, main):
|
||||
"""信息公开"""
|
||||
|
||||
sections = [
|
||||
logger.info(f"正在访问 {main}")
|
||||
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": {
|
||||
"通知通告": [
|
||||
{'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'},
|
||||
]
|
||||
section.title: section.article_set.filter(is_published=True).order_by('-created_at')[:7],
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
for section in sections]
|
||||
logger.info(sections_data)
|
||||
context = {
|
||||
'sections': sections,
|
||||
'main_section': main_section,
|
||||
'sections': sections_data,
|
||||
}
|
||||
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):
|
||||
"""办事服务"""
|
||||
q = request.GET.get('q') or "办事指南"
|
||||
@@ -286,7 +323,7 @@ def service(request):
|
||||
|
||||
def party(request):
|
||||
"""党建引领"""
|
||||
sections = [
|
||||
sections: List[Dict] = [
|
||||
{
|
||||
"key": "party_work",
|
||||
"data": {
|
||||
@@ -329,7 +366,7 @@ def party(request):
|
||||
},
|
||||
|
||||
]
|
||||
carousel = {
|
||||
carousel: Dict = {
|
||||
"key": "index",
|
||||
"data": [
|
||||
{
|
||||
|
||||
@@ -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',)}
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NewsConfig(AppConfig):
|
||||
name = 'news'
|
||||
verbose_name = '新闻管理'
|
||||
@@ -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'),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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='所属板块'),
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
@@ -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)
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Generated
+209
-7
@@ -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]]
|
||||
name = "annotated-types"
|
||||
@@ -46,7 +46,7 @@ version = "3.11.1"
|
||||
description = "ASGI specs, helper code, and adapters"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"},
|
||||
{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"
|
||||
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]]
|
||||
name = "django"
|
||||
version = "6.0.3"
|
||||
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||
optional = false
|
||||
python-versions = ">=3.12"
|
||||
groups = ["main"]
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "django-6.0.3-py3-none-any.whl", hash = "sha256:2e5974441491ddb34c3f13d5e7a9f97b07ba03bf70234c0a9c68b79bbb235bc3"},
|
||||
{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"
|
||||
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]]
|
||||
name = "django-ninja"
|
||||
version = "1.6.2"
|
||||
@@ -251,6 +304,27 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
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]]
|
||||
name = "django-simpleui"
|
||||
version = "2026.1.13"
|
||||
@@ -270,6 +344,29 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
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]]
|
||||
name = "greenlet"
|
||||
version = "3.3.2"
|
||||
@@ -438,6 +535,30 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
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]]
|
||||
name = "markdown"
|
||||
version = "3.10.2"
|
||||
@@ -844,6 +965,23 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
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]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
@@ -1144,7 +1282,7 @@ version = "0.5.5"
|
||||
description = "A non-validating SQL parser."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"},
|
||||
{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"
|
||||
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]]
|
||||
name = "toml"
|
||||
version = "0.10.2"
|
||||
@@ -1176,6 +1340,23 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
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]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
@@ -1219,12 +1400,12 @@ version = "2025.3"
|
||||
description = "Provider of IANA time zone data"
|
||||
optional = false
|
||||
python-versions = ">=2"
|
||||
groups = ["main"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"},
|
||||
{file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"},
|
||||
]
|
||||
markers = {main = "sys_platform == \"win32\"", dev = "sys_platform == \"win32\" or platform_system == \"Windows\""}
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
@@ -1535,7 +1716,28 @@ type = "legacy"
|
||||
url = "http://192.168.123.5:3141/root/pypi/+simple"
|
||||
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]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.13,<3.14"
|
||||
content-hash = "486f6320097292ce4db7f06f502b35949aaca16bc30356379e2be8e5c17bf774"
|
||||
content-hash = "3e611ebdd242413bf20b6f4f63f2db3238f46614acb7d4e54d315075c7ba0d7a"
|
||||
|
||||
@@ -24,9 +24,15 @@ django-environ = "^0.13.0"
|
||||
django-db-connection-pool = "^1.2.6"
|
||||
pillow = "^12.1.1"
|
||||
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]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
+91
-14
@@ -11,10 +11,34 @@ https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
import environ
|
||||
|
||||
env = environ.Env()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
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')
|
||||
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!
|
||||
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'
|
||||
|
||||
@@ -43,18 +76,21 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'import_export',
|
||||
'django_ckeditor_5',
|
||||
'base.apps.BaseConfig',
|
||||
'article.apps.ArticleConfig',
|
||||
'users.apps.UsersConfig',
|
||||
'front.apps.ContentConfig'
|
||||
'front.apps.ContentConfig',
|
||||
'news.apps.NewsConfig',
|
||||
'django_seed',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
# 'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
@@ -84,12 +120,40 @@ ASGI_APPLICATION = 'server.asgi.application'
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
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': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db/data.sqlite3',
|
||||
'OPTIONS': {
|
||||
'timeout': 120,
|
||||
'check_same_thread': False
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# 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_FILE_STORAGE = "path_to_storage.CustomStorage" # optional
|
||||
# CKEDITOR_5_CUSTOM_CSS = 'path_to.css' # 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 = {
|
||||
'default': {
|
||||
'toolbar': {
|
||||
'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': {
|
||||
'blockToolbar': [
|
||||
@@ -261,12 +335,16 @@ CKEDITOR_5_CONFIGS = {
|
||||
'toolbar': {
|
||||
'items': ['heading', '|', 'outdent', 'indent', '|', 'bold', 'italic', 'link', 'underline', 'strikethrough',
|
||||
'code', 'subscript', 'superscript', 'highlight', '|', 'codeBlock', 'sourceEditing', 'insertImage',
|
||||
'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', '|',
|
||||
'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', 'uploadFile', '|',
|
||||
'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'mediaEmbed', 'removeFormat',
|
||||
'insertTable',
|
||||
],
|
||||
'shouldNotGroupWhenFull': 'true'
|
||||
},
|
||||
# --- 在 extends 配置中也必须加上这个 ---
|
||||
'fileUploader': {
|
||||
'supportedFileTypes': ['pdf', 'doc', 'docx', 'zip', 'txt']
|
||||
},
|
||||
'image': {
|
||||
'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft',
|
||||
'imageStyle:alignRight', 'imageStyle:alignCenter', 'imageStyle:side', '|'],
|
||||
@@ -313,4 +391,3 @@ CKEDITOR_5_CONFIGS = {
|
||||
CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" # Possible values: "staff", "authenticated", "any"
|
||||
|
||||
# CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME = "custom_upload_file"
|
||||
|
||||
|
||||
+6
-3
@@ -17,7 +17,7 @@ Including another URLconf
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
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 os
|
||||
@@ -33,8 +33,9 @@ from ninja import NinjaAPI
|
||||
from ninja.errors import ValidationError
|
||||
from ninja.security import HttpBearer
|
||||
|
||||
from base.views import custom_upload_file
|
||||
from common.common_response import CommonResponse
|
||||
from .settings import SECRET_KEY
|
||||
from utils.common_response import CommonResponse
|
||||
|
||||
logger = logging.getLogger('ptools')
|
||||
|
||||
@@ -68,6 +69,7 @@ class GlobalAuth(HttpBearer):
|
||||
# 2. 获取当前文件所在的目录路径 (假设此文件与各个 app 文件夹同级,或者在特定配置目录下)
|
||||
# 如果你的此文件在项目根目录,而 app 文件夹也在根目录:
|
||||
from django.conf import settings
|
||||
|
||||
project_root = settings.BASE_DIR
|
||||
|
||||
|
||||
@@ -123,6 +125,7 @@ def auto_load_routers(base_path, package_name_prefix=None):
|
||||
except Exception as e:
|
||||
print(f"❌ 注册失败 {modname}: {e}")
|
||||
|
||||
|
||||
api_v1 = NinjaAPI(version='1.0.0', auth=GlobalAuth())
|
||||
auto_load_routers(project_root)
|
||||
|
||||
@@ -152,7 +155,7 @@ urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path("api/", api_v1.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()
|
||||
|
||||
+90
-85
@@ -1,4 +1,6 @@
|
||||
{% load static %}
|
||||
{% load footer_tags %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
@@ -39,32 +41,33 @@
|
||||
<li class="{% if request.resolver_match.url_name == 'about' %}active{% endif %}">
|
||||
<a href="{% url 'front:about' %}">单位简介</a>
|
||||
</li>
|
||||
|
||||
<!-- 新闻中心 -->
|
||||
<li class="{% if request.resolver_match.url_name == 'news' %}active{% endif %}">
|
||||
<a href="{% url 'front:news' %}">新闻中心</a>
|
||||
</li>
|
||||
{% show_main_menu %}
|
||||
|
||||
<!-- 信息公开 -->
|
||||
<li class="{% if request.resolver_match.url_name == 'info' %}active{% endif %}">
|
||||
<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>
|
||||
<!-- 办事服务 -->
|
||||
<li class="{% if request.resolver_match.url_name == 'service' %}active{% endif %}">
|
||||
<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>
|
||||
<!-- 消防科普 -->
|
||||
<li class="{% if request.resolver_match.url_name == 'science' %}active{% endif %}">
|
||||
<a href="{% url 'front:science' %}">消防科普</a>
|
||||
</li>
|
||||
{##}
|
||||
{# <!-- 信息公开 -->#}
|
||||
{# <li class="{% if request.resolver_match.url_name == 'info' %}active{% endif %}">#}
|
||||
{# <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>#}
|
||||
{# <!-- 办事服务 -->#}
|
||||
{# <li class="{% if request.resolver_match.url_name == 'service' %}active{% endif %}">#}
|
||||
{# <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>#}
|
||||
{# <!-- 消防科普 -->#}
|
||||
{# <li class="{% if request.resolver_match.url_name == 'science' %}active{% endif %}">#}
|
||||
{# <a href="{% url 'front:science' %}">消防科普</a>#}
|
||||
{# </li>#}
|
||||
</ul>
|
||||
</div>
|
||||
<span></span>
|
||||
@@ -81,72 +84,74 @@
|
||||
<main class="container main" style="padding: 10px 5px !important;">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<!-- 底部页脚:只需要写这一行,全站生效! -->
|
||||
|
||||
{% show_footer %}
|
||||
<!-- 底部 -->
|
||||
<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>
|
||||
<div style="width: 280px;">
|
||||
<select class="form-control" id="exampleFormControlSelect1"
|
||||
x-placement="市政府网站"
|
||||
>
|
||||
<option>市政府网站</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
<option>3</option>
|
||||
<option>4</option>
|
||||
<option>5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display: block;width: 280px;">
|
||||
<select class="form-control" id="exampleFormControlSelect1"
|
||||
x-placement="国内消防站点"
|
||||
>
|
||||
<option>国内消防站点</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
<option>3</option>
|
||||
<option>4</option>
|
||||
<option>5</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-info">
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-md-6 text-center">
|
||||
<img src="{% static 'images/red.png' %}">
|
||||
<img class="footerImg" src="{% static 'images/jiucuo.png' %}">
|
||||
</div>
|
||||
<div class="col-lg-6 d-none d-xl-block">
|
||||
<p class="margin-top-20 font-size-14 line-height-2 footerColor">
|
||||
地址:某某市某某东路锦海·某某区大厦B栋14楼 邮编:xxxxxx
|
||||
</p>
|
||||
<p class="font-size-14 line-height-2 footerColor">
|
||||
服务热线:0791-00000000 传真:0791-00000000
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6 text-center">
|
||||
<img src="/media/upload/images/2026/03/25/wechat.png" alt="">
|
||||
<img class="footerImg" src="/media/upload/images/2026/03/25/weibo.png" alt="">
|
||||
</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">
|
||||
版权所有:长白山消防救援支队 备案证号:<a href="http://beian.miit.gov.cn/"
|
||||
target="_blank">辽ICP备xxxxxxxx号</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
{#<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>#}
|
||||
{# <div style="width: 280px;">#}
|
||||
{# <select class="form-control" id="exampleFormControlSelect1"#}
|
||||
{# x-placement="市政府网站"#}
|
||||
{# >#}
|
||||
{# <option>市政府网站</option>#}
|
||||
{# <option>1</option>#}
|
||||
{# <option>2</option>#}
|
||||
{# <option>3</option>#}
|
||||
{# <option>4</option>#}
|
||||
{# <option>5</option>#}
|
||||
{# </select>#}
|
||||
{# </div>#}
|
||||
{# <div style="display: block;width: 280px;">#}
|
||||
{# <select class="form-control" id="exampleFormControlSelect1"#}
|
||||
{# x-placement="国内消防站点"#}
|
||||
{# >#}
|
||||
{# <option>国内消防站点</option>#}
|
||||
{# <option>1</option>#}
|
||||
{# <option>2</option>#}
|
||||
{# <option>3</option>#}
|
||||
{# <option>4</option>#}
|
||||
{# <option>5</option>#}
|
||||
{# </select>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# <div class="footer-info">#}
|
||||
{# <div class="row">#}
|
||||
{# <div class="col-lg-3 col-md-6 text-center">#}
|
||||
{# <img src="{% static 'images/red.png' %}">#}
|
||||
{# <img class="footerImg" src="{% static 'images/jiucuo.png' %}">#}
|
||||
{# </div>#}
|
||||
{# <div class="col-lg-6 d-none d-xl-block">#}
|
||||
{# <p class="margin-top-20 font-size-14 line-height-2 footerColor">#}
|
||||
{# 地址:某某市某某东路锦海·某某区大厦B栋14楼 邮编:xxxxxx#}
|
||||
{# </p>#}
|
||||
{# <p class="font-size-14 line-height-2 footerColor">#}
|
||||
{# 服务热线:0791-00000000 传真:0791-00000000#}
|
||||
{# </p>#}
|
||||
{# </div>#}
|
||||
{# <div class="col-lg-3 col-md-6 text-center">#}
|
||||
{# <img src="/media/upload/images/2026/03/25/wechat.png" alt="">#}
|
||||
{# <img class="footerImg" src="/media/upload/images/2026/03/25/weibo.png" alt="">#}
|
||||
{# </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">#}
|
||||
{# 版权所有:长白山消防救援支队 备案证号:<a href="http://beian.miit.gov.cn/"#}
|
||||
{# target="_blank">辽ICP备xxxxxxxx号</a>#}
|
||||
{# </p>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{#</footer>#}
|
||||
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
|
||||
|
||||
|
||||
@@ -47,12 +47,17 @@
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
<li class="news-item">
|
||||
<a href="{% url "front:article_detail" pk=item.id %}"
|
||||
style="text-decoration: none;color: black;">
|
||||
|
||||
<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.date }}</span>
|
||||
</a>
|
||||
<span class="news-date">{{ item.created_at | date:"Y-m-d" }}</span>
|
||||
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -66,7 +66,8 @@
|
||||
<div class="row">
|
||||
{% for section in section_data %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}信息公开 - 长白山消防救援支队{% endblock %}
|
||||
{% 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">信息公开</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ main_section.title }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="row" style="margin: 15px auto ">
|
||||
<div class="row" style="margin: 15px auto ">
|
||||
{% for section in sections %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -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">«</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">»</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 %}
|
||||
@@ -8,7 +8,7 @@
|
||||
<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>
|
||||
<li class="breadcrumb-item active" aria-current="page">新闻中心</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div style="margin: 0 5px;">
|
||||
|
||||
+11
-8
@@ -1,9 +1,14 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
||||
from .models import User
|
||||
from .models import Department
|
||||
from .models import Role, Permission
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
class UserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||
list_display = ('username', 'cname', 'dept', 'is_active', 'is_staff')
|
||||
list_filter = ('is_active', 'is_staff', 'dept')
|
||||
search_fields = ('username', 'cname')
|
||||
@@ -22,31 +27,29 @@ class UserAdmin(BaseUserAdmin):
|
||||
ordering = ('username',)
|
||||
filter_horizontal = ('groups', 'user_permissions', 'roles') # 如果 roles 是 ManyToMany
|
||||
|
||||
from django.contrib import admin
|
||||
from .models import Department
|
||||
|
||||
@admin.register(Department)
|
||||
class DepartmentAdmin(admin.ModelAdmin):
|
||||
class DepartmentAdmin(ImportExportModelAdmin):
|
||||
list_display = ('name', 'parent', 'order')
|
||||
list_filter = ('parent',)
|
||||
search_fields = ('name',)
|
||||
ordering = ('order', 'name')
|
||||
from django.contrib import admin
|
||||
from .models import Role, Permission
|
||||
|
||||
|
||||
@admin.register(Role)
|
||||
class RoleAdmin(admin.ModelAdmin):
|
||||
class RoleAdmin(ImportExportModelAdmin):
|
||||
list_display = ('name', 'permission_count')
|
||||
search_fields = ('name',)
|
||||
filter_horizontal = ('permissions',)
|
||||
|
||||
def permission_count(self, obj):
|
||||
return obj.permissions.count()
|
||||
|
||||
permission_count.short_description = "权限数量"
|
||||
|
||||
|
||||
@admin.register(Permission)
|
||||
class PermissionAdmin(admin.ModelAdmin):
|
||||
class PermissionAdmin(ImportExportModelAdmin):
|
||||
list_display = ('resource', 'action', 'desc')
|
||||
list_filter = ('resource', 'action')
|
||||
search_fields = ('resource', 'desc')
|
||||
|
||||
@@ -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.db.models.deletion
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user