update. 完成前端页面展示、列表和详情页
update. 调整融合主菜单与板块 update. 分离新闻模块
This commit is contained in:
+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,
|
||||
}
|
||||
+41
-1
@@ -1,5 +1,45 @@
|
||||
from django.shortcuts import render
|
||||
from ninja import Router
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.conf import settings
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
# Create your views here.
|
||||
router = Router(tags=['content'])
|
||||
router = Router(tags=['content'])
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def custom_upload_file(request):
|
||||
"""
|
||||
自定义 CKEditor 5 上传视图
|
||||
支持:图片 + PDF / Word / Excel / ZIP 等所有文件
|
||||
"""
|
||||
file = request.FILES.get('upload')
|
||||
if not file:
|
||||
return JsonResponse({"error": "请选择文件"}, status=400)
|
||||
|
||||
# 安全处理文件名(避免中文/特殊字符报错)
|
||||
ext = file.name.split('.')[-1].lower()
|
||||
filename = f"{uuid.uuid4().hex}_{datetime.now().strftime('%Y%m%d%H%M%S')}.{ext}"
|
||||
|
||||
# 保存路径
|
||||
upload_dir = os.path.join(settings.MEDIA_ROOT, 'uploads')
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
file_path = os.path.join(upload_dir, filename)
|
||||
|
||||
# 保存文件
|
||||
with open(file_path, 'wb+') as f:
|
||||
for chunk in file.chunks():
|
||||
f.write(chunk)
|
||||
|
||||
# 返回文件 URL(CKEditor 规范格式)
|
||||
file_url = f"{settings.MEDIA_URL}uploads/{filename}"
|
||||
return JsonResponse({
|
||||
"url": file_url,
|
||||
"default": file_url
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user