Compare commits

..

8 Commits

@ -0,0 +1,10 @@
[run]
source = .
include = *.py
omit =
*migrations*
*tests*
*.html
*whoosh_cn_backend*
*settings.py*
*venv*

@ -0,0 +1,11 @@
bin/data/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
uploads/
settings_production.py
*.md
docs/
logs/
static/

6
.gitattributes vendored

@ -0,0 +1,6 @@
blog/static/* linguist-vendored
*.js linguist-vendored
*.css linguist-vendored
* text=auto
*.sh text eol=lf
*.conf text eol=lf

5
.gitignore vendored

@ -1,5 +0,0 @@
__pycache__/
*.pyc
db.sqlite3
*.log
media/

@ -0,0 +1,15 @@
FROM python:3.11
ENV PYTHONUNBUFFERED 1
WORKDIR /code/djangoblog/
RUN apt-get update && \
apt-get install default-libmysqlclient-dev gettext -y && \
rm -rf /var/lib/apt/lists/*
ADD requirements.txt requirements.txt
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt && \
pip install --no-cache-dir gunicorn[gevent] && \
pip cache purge
ADD . .
RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2025 车亮亮
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -1,2 +1,158 @@
# DjangoBlog
<p align="center">
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml/badge.svg" alt="Django CI"></a>
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml/badge.svg" alt="CodeQL"></a>
<a href="https://codecov.io/gh/liangliangyy/DjangoBlog"><img src="https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg" alt="codecov"></a>
<a href="https://github.com/liangliangyy/DjangoBlog/blob/master/LICENSE"><img src="https://img.shields.io/github/license/liangliangyy/djangoblog.svg" alt="license"></a>
</p>
<p align="center">
<b>一款功能强大、设计优雅的现代化博客系统</b>
<br>
<a href="/docs/README-en.md">English</a><b>简体中文</b>
</p>
---
DjangoBlog 是一款基于 Python 3.10 和 Django 4.0 构建的高性能博客平台。它不仅提供了传统博客的所有核心功能还通过一个灵活的插件系统让您可以轻松扩展和定制您的网站。无论您是个人博主、技术爱好者还是内容创作者DjangoBlog 都旨在为您提供一个稳定、高效且易于维护的写作和发布环境。
## ✨ 特性亮点
- **强大的内容管理**: 支持文章、独立页面、分类和标签的完整管理。内置强大的 Markdown 编辑器,支持代码语法高亮。
- **全文搜索**: 集成搜索引擎,提供快速、精准的文章内容搜索。
- **互动评论系统**: 支持回复、邮件提醒等功能,评论内容同样支持 Markdown。
- **灵活的侧边栏**: 可自定义展示最新文章、最多阅读、标签云等模块。
- **社交化登录**: 内置 OAuth 支持,已集成 Google, GitHub, Facebook, 微博, QQ 等主流平台。
- **高性能缓存**: 原生支持 Redis 缓存,并提供自动刷新机制,确保网站高速响应。
- **SEO 友好**: 具备基础 SEO 功能,新内容发布后可自动通知 Google 和百度。
- **便捷的插件系统**: 通过创建独立的插件来扩展博客功能代码解耦易于维护。我们已经通过插件实现了文章浏览计数、SEO 优化等功能!
- **集成图床**: 内置简单的图床功能,方便图片上传和管理。
- **自动化前端**: 集成 `django-compressor`,自动压缩和优化 CSS 及 JavaScript 文件。
- **健壮的运维**: 内置网站异常邮件提醒和微信公众号管理功能。
## 🛠️ 技术栈
- **后端**: Python 3.10, Django 4.0
- **数据库**: MySQL, SQLite (可配置)
- **缓存**: Redis
- **前端**: HTML5, CSS3, JavaScript
- **搜索**: Whoosh, Elasticsearch (可配置)
- **编辑器**: Markdown (mdeditor)
## 🚀 快速开始
### 1. 环境准备
确保您的系统中已安装 Python 3.10+ 和 MySQL/MariaDB。
### 2. 克隆与安装
```bash
# 克隆项目到本地
git clone https://github.com/liangliangyy/DjangoBlog.git
cd DjangoBlog
# 安装依赖
pip install -r requirements.txt
```
### 3. 项目配置
- **数据库**:
打开 `djangoblog/settings.py` 文件,找到 `DATABASES` 配置项,修改为您的 MySQL 连接信息。
```python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangoblog',
'USER': 'root',
'PASSWORD': 'your_password',
'HOST': '127.0.0.1',
'PORT': 3306,
}
}
```
在 MySQL 中创建数据库:
```sql
CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
- **更多配置**:
关于邮件发送、OAuth 登录、缓存等更多高级配置,请参阅我们的 [详细配置文档](/docs/config.md)。
### 4. 初始化数据库
```bash
python manage.py makemigrations
python manage.py migrate
# 创建一个超级管理员账户
python manage.py createsuperuser
```
### 5. 运行项目
```bash
# (可选) 生成一些测试数据
python manage.py create_testdata
# (可选) 收集和压缩静态文件
python manage.py collectstatic --noinput
python manage.py compress --force
# 启动开发服务器
python manage.py runserver
```
现在,在您的浏览器中访问 `http://127.0.0.1:8000/`,您应该能看到 DjangoBlog 的首页了!
## 部署
- **传统部署**: 我们为您准备了非常详细的 [服务器部署教程](https://www.lylinux.net/article/2019/8/5/58.html)。
- **Docker 部署**: 项目已全面支持 Docker。如果您熟悉容器化技术请参考 [Docker 部署文档](/docs/docker.md) 来快速启动。
- **Kubernetes 部署**: 我们也提供了完整的 [Kubernetes 部署指南](/docs/k8s.md),助您轻松上云。
## 🧩 插件系统
插件系统是 DjangoBlog 的核心特色之一。它允许您在不修改核心代码的情况下,通过编写独立的插件来为您的博客添加新功能。
- **工作原理**: 插件通过在预定义的“钩子”上注册回调函数来工作。例如,当一篇文章被渲染时,`after_article_body_get` 钩子会被触发,所有注册到此钩子的函数都会被执行。
- **现有插件**: `view_count`(浏览计数), `seo_optimizer`SEO优化等都是通过插件系统实现的。
- **开发您自己的插件**: 只需在 `plugins` 目录下创建一个新的文件夹,并编写您的 `plugin.py`。欢迎探索并为 DjangoBlog 社区贡献您的创意!
## 🤝 贡献指南
我们热烈欢迎任何形式的贡献!如果您有好的想法或发现了 Bug请随时提交 Issue 或 Pull Request。
## 📄 许可证
本项目基于 [MIT License](LICENSE) 开源。
---
## ❤️ 支持与赞助
如果您觉得这个项目对您有帮助,并且希望支持我继续维护和开发新功能,欢迎请我喝杯咖啡!您的每一份支持都是我前进的最大动力。
<p align="center">
<img src="/docs/imgs/alipay.jpg" width="150" alt="支付宝赞助">
<img src="/docs/imgs/wechat.jpg" width="150" alt="微信赞助">
</p>
<p align="center">
<i>(左) 支付宝 / (右) 微信</i>
</p>
## 🙏 鸣谢
特别感谢 **JetBrains** 为本项目提供的免费开源许可证。
<p align="center">
<a href="https://www.jetbrains.com/?from=DjangoBlog">
<img src="/docs/imgs/pycharm_logo.png" width="150" alt="JetBrains Logo">
</a>
</p>
---
> 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。

@ -1,21 +0,0 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser
# HJJ: 使用装饰器注册CustomUser模型到Django管理后台
@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
# HJJ: 指定管理的模型为CustomUser
model = CustomUser
# HJJ: 定义管理后台列表页显示的字段
list_display = ['username', 'email', 'is_staff', 'date_joined']
# HJJ: 定义管理后台可用的过滤器
list_filter = ['is_staff', 'is_active', 'date_joined']
# HJJ: 在原有字段集基础上添加扩展信息字段集
fieldsets = UserAdmin.fieldsets + (
('扩展信息', {'fields': ('bio', 'avatar', 'location', 'website')}),
)
# HJJ: 在添加用户表单中添加扩展信息字段集
add_fieldsets = UserAdmin.add_fieldsets + (
('扩展信息', {'fields': ('bio', 'avatar', 'location', 'website')}),
)

@ -1,6 +0,0 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'

@ -1,62 +0,0 @@
from django.core.mail import send_mail
from django.core.cache import cache
import random
from django.conf import settings
class EmailService:
"""邮箱验证服务"""
@staticmethod
def send_verification_code(email):
"""发送邮箱验证码"""
# HJJ生成6位随机数字验证码
verification_code = ''.join([str(random.randint(0, 9)) for _ in range(6)])
# HJJ将验证码存入缓存设置10分钟有效期
cache_key = f'email_verification_{email}'
cache.set(cache_key, verification_code, 600)
# HJJ构造邮件主题和内容
subject = '🔐 金陵非遗 - 密码重置验证码'
message = f'''
尊敬的金陵非遗用户
您正在申请重置账户密码验证码为
{verification_code}
验证码有效期10分钟请尽快完成操作
注意事项
如非本人操作请立即忽略此邮件
请勿将验证码透露给他人
如有疑问请联系我们
感谢您对南京非物质文化遗产的关注与支持
🎭 金陵非遗团队
'''
# HJJ尝试发送邮件处理可能的异常
try:
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[email],
fail_silently=False,
)
return True, '验证码已发送到您的邮箱'
except Exception as e:
print(f"邮件发送失败: {e}")
return False, '邮件发送失败,请检查邮箱地址'
@staticmethod
def verify_code(email, code):
"""验证邮箱验证码"""
# HJJ从缓存中获取对应邮箱的验证码
cache_key = f'email_verification_{email}'
stored_code = cache.get(cache_key)
# HJJ验证用户输入的验证码是否与缓存中的一致
return stored_code and stored_code == code

@ -1,39 +0,0 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
"""自定义用户创建表单,用于用户注册"""
class Meta:
# HJJ指定此表单对应的数据模型为CustomUser
model = CustomUser
# HJJ定义表单中要包含的字段包括用户名、邮箱、个人简介等
fields = ('username', 'email', 'bio', 'avatar', 'location', 'website')
# HJJ为表单字段配置自定义小部件用于控制前端显示效果
widgets = {
# HJJ为个人简介字段设置文本区域指定4行高度和占位符文本
'bio': forms.Textarea(attrs={'rows': 4, 'placeholder': '介绍一下自己...'}),
# HJJ为头像字段设置文件输入限制只能选择图片文件
'avatar': forms.FileInput(attrs={'accept': 'image/*'}),
}
class CustomUserChangeForm(UserChangeForm):
"""自定义用户信息修改表单,用于用户编辑个人资料"""
# HJJ将密码字段设置为None从表单中移除密码相关字段
# HJJ这样用户在编辑个人信息时不会看到密码输入框
password = None
class Meta:
# HJJ指定此表单对应的数据模型为CustomUser
model = CustomUser
# HJJ定义表单中要包含的字段与创建表单相同但不包含密码字段
fields = ('username', 'email', 'bio', 'avatar', 'location', 'website')
# HJJ为表单字段配置自定义小部件用于控制前端显示效果
widgets = {
# HJJ为个人简介字段设置文本区域指定4行高度
'bio': forms.Textarea(attrs={'rows': 4}),
# HJJ为头像字段设置文件输入限制只能选择图片文件
'avatar': forms.FileInput(attrs={'accept': 'image/*'}),
}

@ -1,47 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-01 09:09
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('bio', models.TextField(blank=True, max_length=500, verbose_name='个人简介')),
('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/%Y/%m/', verbose_name='头像')),
('location', models.CharField(blank=True, max_length=100, verbose_name='所在地')),
('website', models.URLField(blank=True, verbose_name='个人网站')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': '用户',
'verbose_name_plural': '用户',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -1,30 +0,0 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
"""自定义用户模型
继承Django内置的AbstractUser类扩展用户基本信息
"""
# MYT个人简介字段最大长度500字符可为空
bio = models.TextField(max_length=500, blank=True, verbose_name="个人简介")
# MYT头像字段图片将上传到avatars/年/月/目录,可为空
avatar = models.ImageField(upload_to='avatars/%Y/%m/', blank=True, null=True, verbose_name="头像")
# MYT所在地字段最大长度100字符可为空
location = models.CharField(max_length=100, blank=True, verbose_name="所在地")
# MYT个人网站字段URL格式可为空
website = models.URLField(blank=True, verbose_name="个人网站")
class Meta:
# MYT 在Django admin中显示的单数名称
verbose_name = "用户"
# MYT在Django admin中显示的复数名称
verbose_name_plural = "用户"
def __str__(self):
"""字符串表示方法,返回用户名用于显示"""
return self.username

@ -1,326 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="auth-page">
<div class="auth-container">
<div class="auth-card">
<!-- 非遗主题装饰 -->
<div class="auth-header">
<div class="auth-icon">
<span>🎭</span>
</div>
<h2>登录金陵非遗</h2>
<p>探索南京非物质文化遗产的魅力</p>
</div>
<!-- 登录表单 -->
<form method="post" class="auth-form">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-error">
<span>⚠️</span>
用户名或密码错误,请重试
</div>
{% endif %}
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
<span class="alert-icon">
{% if message.tags == 'success' %}✅{% else %}⚠️{% endif %}
</span>
<span class="alert-content">{{ message }}</span>
</div>
{% endfor %}
</div>
{% endif %}
<div class="form-group">
<label for="id_username">用户名</label>
<div class="input-with-icon">
<span class="input-icon">👤</span>
<input type="text" name="username" id="id_username"
placeholder="请输入用户名" required
value="{{ form.username.value|default:'' }}">
</div>
</div>
<div class="form-group">
<label for="id_password">密码</label>
<div class="input-with-icon">
<span class="input-icon">🔒</span>
<input type="password" name="password" id="id_password"
placeholder="请输入密码" required>
</div>
</div>
<button type="submit" class="auth-btn primary-btn">
<span>登录</span>
<span class="btn-icon"></span>
</button>
</form>
<!-- 其他选项 -->
<div class="auth-footer">
<div class="divider">
<span></span>
</div>
<div class="auth-links">
<p>还没有账号?<a href="{% url 'register' %}" class="link">立即注册</a></p>
<p><a href="{% url 'password_reset_request' %}" class="link">忘记密码?</a></p>
</div>
</div>
</div>
</div>
</div>
<style>
.auth-page {
min-height: 80vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
background: linear-gradient(135deg, #faf3e8 0%, #fffaf0 100%);
}
.auth-container {
width: 100%;
max-width: 420px;
}
.auth-card {
background: var(--bg-white);
padding: 40px 35px;
border-radius: 20px;
box-shadow: 0 15px 35px rgba(139, 69, 19, 0.15);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
}
.auth-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--nj-red), var(--nj-gold), var(--nj-brown));
}
.auth-header {
text-align: center;
margin-bottom: 35px;
}
.auth-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
background: linear-gradient(135deg, var(--nj-gold), var(--nj-brown));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
box-shadow: 0 8px 20px rgba(212, 175, 55, 0.3);
}
.auth-header h2 {
color: var(--text-dark);
font-size: 1.8em;
margin-bottom: 8px;
font-weight: 600;
}
.auth-header p {
color: var(--text-light);
font-size: 1em;
opacity: 0.8;
}
.auth-form {
margin-bottom: 25px;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--text-dark);
font-weight: 500;
font-size: 14px;
}
.input-with-icon {
position: relative;
display: flex;
align-items: center;
}
.input-icon {
position: absolute;
left: 15px;
color: var(--text-light);
font-size: 16px;
z-index: 2;
}
.input-with-icon input {
width: 100%;
padding: 15px 15px 15px 45px;
border: 2px solid var(--border-color);
border-radius: 12px;
font-size: 15px;
transition: all 0.3s ease;
background: var(--bg-white);
}
.input-with-icon input:focus {
outline: none;
border-color: var(--nj-gold);
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.1);
transform: translateY(-1px);
}
.alert {
padding: 12px 16px;
border-radius: 10px;
margin-bottom: 20px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.alert-error {
background: rgba(198, 47, 47, 0.1);
color: var(--nj-red);
border: 1px solid rgba(198, 47, 47, 0.2);
}
.alert-success {
background: rgba(76, 175, 80, 0.1);
color: #4caf50;
border: 1px solid rgba(76, 175, 80, 0.2);
}
.alert-info {
background: rgba(33, 150, 243, 0.1);
color: #2196f3;
border: 1px solid rgba(33, 150, 243, 0.2);
}
.alert-icon {
font-size: 1.2em;
}
.alert-content {
flex: 1;
}
.primary-btn {
width: 100%;
background: linear-gradient(135deg, var(--nj-brown), var(--nj-light-brown));
color: white;
border: none;
padding: 16px 24px;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s ease;
margin-top: 10px;
}
.primary-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.3);
background: linear-gradient(135deg, var(--nj-light-brown), var(--nj-brown));
}
.btn-icon {
font-size: 1.2em;
transition: transform 0.3s ease;
}
.primary-btn:hover .btn-icon {
transform: translateX(3px);
}
.auth-footer {
text-align: center;
}
.divider {
position: relative;
margin: 25px 0;
color: var(--text-light);
font-size: 14px;
}
.divider::before {
content: "";
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: var(--border-color);
z-index: 1;
}
.divider span {
background: var(--bg-white);
padding: 0 15px;
position: relative;
z-index: 2;
}
.auth-links p {
margin: 12px 0;
color: var(--text-light);
font-size: 14px;
}
.link {
color: var(--nj-brown);
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease;
}
.link:hover {
color: var(--nj-red);
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 480px) {
.auth-card {
padding: 30px 25px;
margin: 0 10px;
}
.auth-icon {
width: 70px;
height: 70px;
font-size: 1.8em;
}
.auth-header h2 {
font-size: 1.6em;
}
}
</style>
{% endblock %}

@ -1,244 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="auth-page">
<div class="auth-container">
<div class="auth-card">
<div class="auth-header">
<div class="auth-icon">🔐</div>
<h2>重置密码</h2>
<p>请输入您注册时使用的邮箱地址</p>
</div>
<form method="post" class="auth-form">
{% csrf_token %}
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
<div class="alert-icon">
{% if message.tags == 'success' %}✅{% else %}⚠️{% endif %}
</div>
<div class="alert-content">{{ message }}</div>
</div>
{% endfor %}
</div>
{% endif %}
<div class="form-group">
<label for="email" class="form-label">邮箱地址</label>
<div class="input-with-icon">
<span class="input-icon">📧</span>
<input type="email" name="email" id="email"
placeholder="请输入注册时使用的邮箱" required
class="form-control">
</div>
</div>
<button type="submit" class="auth-btn primary-btn">
<span>发送验证码</span>
<span class="btn-icon"></span>
</button>
</form>
<div class="auth-footer">
<div class="auth-links">
<p><a href="{% url 'login' %}" class="link">返回登录</a></p>
</div>
</div>
</div>
</div>
</div>
<style>
/* 复用之前的登录页面样式 */
.auth-page {
min-height: 80vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
background: linear-gradient(135deg, #faf3e8 0%, #fffaf0 100%);
}
.auth-container {
width: 100%;
max-width: 420px;
}
.auth-card {
background: var(--bg-white);
padding: 40px 35px;
border-radius: 20px;
box-shadow: 0 15px 35px rgba(139, 69, 19, 0.15);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
}
.auth-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--nj-red), var(--nj-gold), var(--nj-brown));
}
.auth-header {
text-align: center;
margin-bottom: 35px;
}
.auth-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
background: linear-gradient(135deg, var(--nj-gold), var(--nj-brown));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
box-shadow: 0 8px 20px rgba(212, 175, 55, 0.3);
}
.auth-header h2 {
color: var(--text-dark);
font-size: 1.8em;
margin-bottom: 8px;
font-weight: 600;
}
.auth-header p {
color: var(--text-light);
font-size: 1em;
opacity: 0.8;
}
.auth-form {
margin-bottom: 25px;
}
.form-group {
margin-bottom: 25px;
}
.form-label {
display: block;
margin-bottom: 8px;
color: var(--text-dark);
font-weight: 500;
font-size: 14px;
}
.input-with-icon {
position: relative;
display: flex;
align-items: center;
}
.input-icon {
position: absolute;
left: 15px;
color: var(--text-light);
font-size: 16px;
z-index: 2;
}
.form-control {
width: 100%;
padding: 15px 15px 15px 45px;
border: 2px solid var(--border-color);
border-radius: 12px;
font-size: 15px;
transition: all 0.3s ease;
background: var(--bg-white);
}
.form-control:focus {
outline: none;
border-color: var(--nj-gold);
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.1);
transform: translateY(-1px);
}
.alert {
padding: 12px 16px;
border-radius: 10px;
margin-bottom: 20px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.alert-error {
background: rgba(198, 47, 47, 0.1);
color: var(--nj-red);
border: 1px solid rgba(198, 47, 47, 0.2);
}
.alert-success {
background: rgba(76, 175, 80, 0.1);
color: #4caf50;
border: 1px solid rgba(76, 175, 80, 0.2);
}
.primary-btn {
width: 100%;
background: linear-gradient(135deg, var(--nj-brown), var(--nj-light-brown));
color: white;
border: none;
padding: 16px 24px;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s ease;
margin-top: 10px;
}
.primary-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.3);
background: linear-gradient(135deg, var(--nj-light-brown), var(--nj-brown));
}
.btn-icon {
font-size: 1.2em;
transition: transform 0.3s ease;
}
.primary-btn:hover .btn-icon {
transform: translateX(3px);
}
.auth-footer {
text-align: center;
}
.auth-links p {
margin: 12px 0;
color: var(--text-light);
font-size: 14px;
}
.link {
color: var(--nj-brown);
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease;
}
.link:hover {
color: var(--nj-red);
text-decoration: underline;
}
</style>
{% endblock %}

@ -1,507 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="profile-page">
<div class="profile-container">
<!-- 用户信息卡片 -->
<div class="profile-card">
<div class="profile-header">
<div class="avatar-section">
{% if user.avatar %}
<img src="{{ user.avatar.url }}" alt="头像" class="profile-avatar">
{% else %}
<div class="avatar-placeholder">
{{ user.username|first|upper }}
</div>
{% endif %}
<div class="avatar-overlay">
<a href="{% url 'profile_edit' %}" class="edit-avatar-btn">更换头像</a>
</div>
</div>
<div class="profile-info">
<h1 class="profile-name">{{ user.username }}</h1>
<p class="profile-bio">
{% if user.bio %}
{{ user.bio }}
{% else %}
这个人很懒,什么都没有写...
{% endif %}
</p>
<div class="profile-meta">
<div class="meta-item">
<span class="meta-icon">📅</span>
<span class="meta-text">加入时间:{{ user.date_joined|date:"Y年m月d日" }}</span>
</div>
{% if user.location %}
<div class="meta-item">
<span class="meta-icon">📍</span>
<span class="meta-text">{{ user.location }}</span>
</div>
{% endif %}
{% if user.website %}
<div class="meta-item">
<span class="meta-icon">🌐</span>
<a href="{{ user.website }}" class="meta-link" target="_blank">个人网站</a>
</div>
{% endif %}
</div>
</div>
<a href="{% url 'profile_edit' %}" class="edit-profile-btn">
<span class="btn-icon">⚙️</span>
编辑资料
</a>
</div>
</div>
<!-- 数据统计 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">📝</div>
<div class="stat-content">
<div class="stat-number">{{ user.post_set.count }}</div>
<div class="stat-label">发表文章</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">💬</div>
<div class="stat-content">
<div class="stat-number">{{ user.comments.count }}</div>
<div class="stat-label">评论数量</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">❤️</div>
<div class="stat-content">
<div class="stat-number">{{ user.post_likes.count }}</div>
<div class="stat-label">获赞数量</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-content">
<div class="stat-number">{{ user.post_favorites.count }}</div>
<div class="stat-label">文章收藏</div>
</div>
</div>
</div>
<!-- 快速操作 -->
<div class="actions-section">
<h2 class="section-title">快速操作</h2>
<div class="action-buttons">
<a href="{% url 'create_post' %}" class="action-btn primary">
<span class="action-icon">✍️</span>
<span class="action-text">发表文章</span>
<span class="action-desc">分享非遗故事</span>
</a>
<a href="{% url 'user_posts' %}" class="action-btn secondary">
<span class="action-icon">📚</span>
<span class="action-text">我的文章</span>
<span class="action-desc">管理我的创作</span>
</a>
<a href="{% url 'index' %}" class="action-btn tertiary">
<span class="action-icon">🏠</span>
<span class="action-text">返回首页</span>
<span class="action-desc">浏览更多内容</span>
</a>
</div>
</div>
<!-- 最近活动 -->
<div class="activity-section">
<h2 class="section-title">最近活动</h2>
<div class="activity-list">
{% if user.comments.all %}
{% for comment in user.comments.all|slice:":5" %}
<div class="activity-item">
<div class="activity-icon">💬</div>
<div class="activity-content">
<p>评论了文章 <a href="{% url 'detail' comment.post.id %}">{{ comment.post.title }}</a></p>
<span class="activity-time">{{ comment.created_time|timesince }}前</span>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<div class="empty-icon">📝</div>
<p>暂无活动记录</p>
<p class="empty-desc">快去发表文章或评论吧!</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<style>
.profile-page {
min-height: 100vh;
background: linear-gradient(135deg, #faf3e8 0%, #fffaf0 100%);
padding: 30px 20px;
}
.profile-container {
max-width: 1000px;
margin: 0 auto;
}
/* 用户信息卡片 */
.profile-card {
background: var(--bg-white);
border-radius: 20px;
box-shadow: 0 15px 35px rgba(139, 69, 19, 0.1);
border: 1px solid var(--border-color);
overflow: hidden;
margin-bottom: 30px;
}
.profile-header {
padding: 40px;
display: flex;
align-items: center;
gap: 30px;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
position: relative;
}
.avatar-section {
position: relative;
flex-shrink: 0;
}
.profile-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 4px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}
.avatar-placeholder {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, var(--nj-gold), var(--nj-red));
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 2.5em;
font-weight: bold;
border: 4px solid rgba(255, 255, 255, 0.3);
}
.avatar-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
padding: 8px;
text-align: center;
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 0 0 60px 60px;
}
.avatar-section:hover .avatar-overlay {
opacity: 1;
}
.edit-avatar-btn {
color: white;
font-size: 12px;
text-decoration: none;
}
.profile-info {
flex: 1;
}
.profile-name {
font-size: 2.2em;
margin-bottom: 10px;
font-weight: 700;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
}
.profile-bio {
font-size: 1.1em;
opacity: 0.9;
margin-bottom: 20px;
line-height: 1.5;
font-style: italic;
}
.profile-meta {
display: flex;
flex-direction: column;
gap: 8px;
}
.meta-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
}
.meta-link {
color: var(--nj-gold);
text-decoration: none;
}
.meta-link:hover {
text-decoration: underline;
}
.edit-profile-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
padding: 12px 20px;
border-radius: 25px;
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
}
.edit-profile-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
/* 数据统计 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: var(--bg-white);
padding: 25px;
border-radius: 15px;
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.1);
border: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 20px;
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-icon {
font-size: 2.5em;
opacity: 0.8;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 2em;
font-weight: bold;
color: var(--primary-color);
margin-bottom: 5px;
}
.stat-label {
color: var(--text-light);
font-size: 14px;
}
/* 快速操作 */
.actions-section {
margin-bottom: 30px;
}
.section-title {
color: var(--primary-color);
font-size: 1.5em;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--accent-color);
}
.action-buttons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.action-btn {
background: var(--bg-white);
padding: 25px;
border-radius: 15px;
text-decoration: none;
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.1);
border: 1px solid var(--border-color);
transition: all 0.3s ease;
display: block;
}
.action-btn:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(139, 69, 19, 0.15);
}
.action-btn.primary {
border-left: 4px solid var(--nj-red);
}
.action-btn.secondary {
border-left: 4px solid var(--nj-gold);
}
.action-btn.tertiary {
border-left: 4px solid var(--nj-brown);
}
.action-icon {
font-size: 2em;
display: block;
margin-bottom: 10px;
}
.action-text {
display: block;
font-size: 1.2em;
font-weight: 600;
color: var(--text-dark);
margin-bottom: 5px;
}
.action-desc {
display: block;
color: var(--text-light);
font-size: 0.9em;
}
/* 最近活动 */
.activity-section {
background: var(--bg-white);
padding: 30px;
border-radius: 15px;
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.1);
border: 1px solid var(--border-color);
}
.activity-list {
max-height: 300px;
overflow-y: auto;
}
.activity-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 0;
border-bottom: 1px solid var(--border-color);
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
font-size: 1.2em;
flex-shrink: 0;
}
.activity-content {
flex: 1;
}
.activity-content p {
margin: 0;
color: var(--text-dark);
}
.activity-content a {
color: var(--primary-color);
text-decoration: none;
}
.activity-content a:hover {
text-decoration: underline;
}
.activity-time {
color: var(--text-light);
font-size: 0.9em;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text-light);
}
.empty-icon {
font-size: 3em;
margin-bottom: 15px;
opacity: 0.5;
}
.empty-desc {
font-size: 0.9em;
margin-top: 5px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.profile-header {
flex-direction: column;
text-align: center;
padding: 30px 20px;
}
.profile-meta {
align-items: center;
}
.stats-grid {
grid-template-columns: 1fr 1fr;
}
.action-buttons {
grid-template-columns: 1fr;
}
.profile-name {
font-size: 1.8em;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.profile-avatar,
.avatar-placeholder {
width: 100px;
height: 100px;
}
}
</style>
{% endblock %}

@ -1,515 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="edit-profile-page">
<div class="edit-profile-container">
<div class="edit-profile-header">
<div class="header-icon">⚙️</div>
<h1>编辑个人资料</h1>
<p>完善您的个人信息,让更多人了解您</p>
</div>
<div class="edit-profile-card">
<form method="post" enctype="multipart/form-data" class="edit-profile-form">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-error">
<div class="alert-icon">⚠️</div>
<div class="alert-content">
<strong>请修正以下错误:</strong>
<ul>
{% for field in form %}
{% for error in field.errors %}
<li>{{ field.label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
<div class="alert-icon"></div>
<div class="alert-content">{{ message }}</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- 头像上传区域 -->
<div class="form-section">
<h3 class="section-title">
<span class="title-icon">🖼️</span>
头像设置
</h3>
<div class="avatar-upload-section">
<div class="current-avatar">
{% if user.avatar %}
<img src="{{ user.avatar.url }}" alt="当前头像" class="avatar-preview">
{% else %}
<div class="avatar-preview placeholder">
{{ user.username|first|upper }}
</div>
{% endif %}
</div>
<div class="avatar-upload">
<label for="{{ form.avatar.id_for_label }}" class="upload-label">
<span class="upload-icon">📁</span>
<span class="upload-text">选择新头像</span>
</label>
{{ form.avatar }}
<div class="upload-help">支持 JPG、PNG 格式,最大 5MB</div>
</div>
</div>
</div>
<!-- 基本信息 -->
<div class="form-section">
<h3 class="section-title">
<span class="title-icon">👤</span>
基本信息
</h3>
<div class="form-grid">
<div class="form-group">
<label for="{{ form.username.id_for_label }}" class="form-label">
{{ form.username.label }}
<span class="required">*</span>
</label>
{{ form.username }}
{% if form.username.help_text %}
<div class="help-text">{{ form.username.help_text }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.email.id_for_label }}" class="form-label">
{{ form.email.label }}
<span class="required">*</span>
</label>
{{ form.email }}
{% if form.email.help_text %}
<div class="help-text">{{ form.email.help_text }}</div>
{% endif %}
</div>
</div>
</div>
<!-- 个人介绍 -->
<div class="form-section">
<h3 class="section-title">
<span class="title-icon">📝</span>
个人介绍
</h3>
<div class="form-group">
<label for="{{ form.bio.id_for_label }}" class="form-label">
{{ form.bio.label }}
</label>
{{ form.bio }}
<div class="help-text">
用一段话介绍自己让其他用户更好地了解您最多500字
</div>
</div>
</div>
<!-- 联系信息 -->
<div class="form-section">
<h3 class="section-title">
<span class="title-icon">📍</span>
联系信息
</h3>
<div class="form-grid">
<div class="form-group">
<label for="{{ form.location.id_for_label }}" class="form-label">
{{ form.location.label }}
</label>
{{ form.location }}
<div class="help-text">例如:江苏南京</div>
</div>
<div class="form-group">
<label for="{{ form.website.id_for_label }}" class="form-label">
{{ form.website.label }}
</label>
{{ form.website }}
<div class="help-text">请输入完整的网址https://example.com</div>
</div>
</div>
</div>
<!-- 表单操作 -->
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="btn-icon">💾</span>
保存更改
</button>
<a href="{% url 'profile' %}" class="btn btn-secondary">
<span class="btn-icon">↩️</span>
取消返回
</a>
</div>
</form>
</div>
<!-- 安全提示 -->
<div class="security-notice">
<div class="notice-icon">🔒</div>
<div class="notice-content">
<h4>账户安全提示</h4>
<p>请妥善保管您的账户信息,不要与他人分享您的登录凭证。</p>
</div>
</div>
</div>
</div>
<style>
.edit-profile-page {
min-height: 100vh;
background: linear-gradient(135deg, #faf3e8 0%, #fffaf0 100%);
padding: 30px 20px;
}
.edit-profile-container {
max-width: 800px;
margin: 0 auto;
}
.edit-profile-header {
text-align: center;
margin-bottom: 40px;
}
.header-icon {
font-size: 4em;
margin-bottom: 20px;
}
.edit-profile-header h1 {
color: var(--primary-color);
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 700;
}
.edit-profile-header p {
color: var(--text-light);
font-size: 1.1em;
}
.edit-profile-card {
background: var(--bg-white);
border-radius: 20px;
box-shadow: 0 15px 35px rgba(139, 69, 19, 0.1);
border: 1px solid var(--border-color);
overflow: hidden;
margin-bottom: 30px;
}
.edit-profile-form {
padding: 40px;
}
.form-section {
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 1px solid var(--border-color);
}
.form-section:last-child {
border-bottom: none;
margin-bottom: 0;
}
.section-title {
color: var(--primary-color);
font-size: 1.3em;
margin-bottom: 25px;
display: flex;
align-items: center;
gap: 10px;
}
.title-icon {
font-size: 1.2em;
}
/* 头像上传区域 */
.avatar-upload-section {
display: flex;
align-items: center;
gap: 30px;
}
.current-avatar {
flex-shrink: 0;
}
.avatar-preview {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--accent-color);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.avatar-preview.placeholder {
background: linear-gradient(135deg, var(--nj-gold), var(--nj-red));
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 2em;
font-weight: bold;
}
.avatar-upload {
flex: 1;
}
.upload-label {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--primary-color);
color: white;
padding: 12px 20px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 10px;
}
.upload-label:hover {
background: var(--secondary-color);
transform: translateY(-2px);
}
.upload-icon {
font-size: 1.2em;
}
#id_avatar {
display: none;
}
.upload-help {
color: var(--text-light);
font-size: 0.9em;
}
/* 表单布局 */
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-group {
margin-bottom: 25px;
}
.form-label {
display: block;
margin-bottom: 8px;
color: var(--text-dark);
font-weight: 600;
font-size: 14px;
}
.required {
color: var(--nj-red);
}
.form-control {
width: 100%;
padding: 15px;
border: 2px solid var(--border-color);
border-radius: 10px;
font-size: 15px;
transition: all 0.3s ease;
background: var(--bg-white);
font-family: inherit;
}
.form-control:focus {
outline: none;
border-color: var(--nj-gold);
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.1);
transform: translateY(-1px);
}
textarea.form-control {
resize: vertical;
min-height: 120px;
line-height: 1.5;
}
.help-text {
font-size: 12px;
color: var(--text-light);
margin-top: 5px;
line-height: 1.4;
}
/* 警告和消息 */
.alert {
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
display: flex;
align-items: flex-start;
gap: 15px;
}
.alert-error {
background: rgba(198, 47, 47, 0.1);
border: 1px solid rgba(198, 47, 47, 0.2);
color: var(--nj-red);
}
.alert-success {
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.2);
color: #4caf50;
}
.alert-icon {
font-size: 1.5em;
flex-shrink: 0;
}
.alert-content {
flex: 1;
}
.alert-content ul {
margin: 10px 0 0 20px;
}
.alert-content li {
margin: 5px 0;
}
/* 表单操作 */
.form-actions {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 40px;
padding-top: 30px;
border-top: 1px solid var(--border-color);
}
.btn {
padding: 15px 30px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
min-width: 140px;
justify-content: center;
}
.btn-primary {
background: linear-gradient(135deg, var(--nj-brown), var(--nj-light-brown));
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(139, 69, 19, 0.3);
}
.btn-secondary {
background: var(--border-color);
color: var(--text-dark);
}
.btn-secondary:hover {
background: var(--text-light);
color: white;
}
/* 安全提示 */
.security-notice {
background: var(--bg-white);
padding: 25px;
border-radius: 15px;
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.1);
border: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 20px;
}
.notice-icon {
font-size: 2.5em;
flex-shrink: 0;
}
.notice-content h4 {
color: var(--primary-color);
margin-bottom: 8px;
font-size: 1.2em;
}
.notice-content p {
color: var(--text-light);
margin: 0;
line-height: 1.5;
}
/* 响应式设计 */
@media (max-width: 768px) {
.edit-profile-form {
padding: 25px;
}
.form-grid {
grid-template-columns: 1fr;
gap: 0;
}
.avatar-upload-section {
flex-direction: column;
text-align: center;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
.edit-profile-header h1 {
font-size: 2em;
}
}
@media (max-width: 480px) {
.edit-profile-form {
padding: 20px;
}
.edit-profile-header h1 {
font-size: 1.8em;
}
.header-icon {
font-size: 3em;
}
}
</style>
{% endblock %}

@ -1,110 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="auth-container">
<div class="auth-form">
<h2>用户注册</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<div class="error">{{ field.errors }}</div>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="auth-btn">注册</button>
</form>
<p class="auth-link">已有账号?<a href="{% url 'login' %}">立即登录</a></p>
</div>
</div>
<style>
.auth-container {
max-width: 500px;
margin: 50px auto;
padding: 0 20px;
}
.auth-form {
background: var(--bg-white);
padding: 40px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(139, 69, 19, 0.1);
border: 1px solid var(--border-color);
}
.auth-form h2 {
text-align: center;
color: var(--primary-color);
margin-bottom: 30px;
font-size: 1.8em;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--text-dark);
font-weight: 500;
}
.form-group input, .form-group textarea, .form-group select {
width: 100%;
padding: 12px 15px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
.form-group input:focus, .form-group textarea:focus {
outline: none;
border-color: var(--primary-color);
}
.error {
color: #e74c3c;
font-size: 14px;
margin-top: 5px;
}
.auth-btn {
width: 100%;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
border: none;
padding: 15px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.3s;
}
.auth-btn:hover {
transform: translateY(-2px);
}
.auth-link {
text-align: center;
margin-top: 20px;
color: var(--text-light);
}
.auth-link a {
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
}
.auth-link a:hover {
text-decoration: underline;
}
</style>
{% endblock %}

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

@ -1,17 +0,0 @@
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
urlpatterns = [
path('register/', views.register, name='register'),
path('login/', views.user_login, name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('profile/', views.profile, name='profile'),
path('profile/edit/', views.profile_edit, name='profile_edit'),
# 邮箱验证密码重置
path('password-reset/', views.password_reset_request, name='password_reset_request'),
path('password-reset/verify/', views.password_reset_verify, name='password_reset_verify'),
path('password-reset/confirm/', views.password_reset_confirm, name='password_reset_confirm'),
path('password-reset/complete/', views.password_reset_complete, name='password_reset_complete'),
]

@ -1,218 +0,0 @@
from django.shortcuts import render, redirect
from django.contrib.auth import login, authenticate
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .forms import CustomUserCreationForm, CustomUserChangeForm
from django.contrib import messages
from django.contrib.auth import get_user_model
from .email_service import EmailService
def register(request):
"""用户注册视图"""
# MYT检查请求方法是否为POST即用户提交注册表单
if request.method == 'POST':
# MYT使用用户提交的数据和文件初始化注册表单
form = CustomUserCreationForm(request.POST, request.FILES)
# MYT验证表单数据是否有效
if form.is_valid():
# MYT保存表单数据创建新用户
user = form.save()
# MYT自动登录新创建的用户
login(request, user)
# MYT显示成功注册消息
messages.success(request, '注册成功!欢迎来到金陵非遗。')
# MYT获取重定向URL如果没有则默认到首页
next_url = request.GET.get('next', 'index')
return redirect(next_url)
else:
# MYT如果是GET请求创建空表单实例
form = CustomUserCreationForm()
# MYT渲染注册页面并传递表单对象
return render(request, 'accounts/register.html', {'form': form})
def user_login(request):
"""用户登录视图"""
# MYT检查请求方法是否为POST即用户提交登录表单
if request.method == 'POST':
# MYT使用请求数据和用户提交的数据初始化登录表单
form = AuthenticationForm(request, data=request.POST)
# MYT验证表单数据是否有效
if form.is_valid():
# MYT从验证后的表单数据中获取用户名
username = form.cleaned_data.get('username')
# MYT从验证后的表单数据中获取密码
password = form.cleaned_data.get('password')
# MYT验证用户凭据是否正确
user = authenticate(username=username, password=password)
# MYT检查用户是否存在且有效
if user is not None:
# MYT登录用户并建立会话
login(request, user)
# MYT显示欢迎登录消息
messages.success(request, f'欢迎回来,{username}')
# MYT获取重定向URL如果没有则默认到首页
next_url = request.GET.get('next', 'index')
return redirect(next_url)
else:
# MYT如果表单验证失败显示错误消息
messages.error(request, '用户名或密码错误,请重试。')
else:
# MYT如果是GET请求创建空登录表单实例
form = AuthenticationForm()
# MYT渲染登录页面并传递表单对象
return render(request, 'accounts/login.html', {'form': form})
@login_required
def profile(request):
"""用户个人资料页面"""
# MYT渲染用户个人资料页面传递当前用户对象
return render(request, 'accounts/profile.html', {'user': request.user})
@login_required
def profile_edit(request):
"""编辑用户资料"""
# MYT检查请求方法是否为POST即用户提交编辑表单
if request.method == 'POST':
# MYT使用用户提交的数据和当前用户实例初始化编辑表单
form = CustomUserChangeForm(request.POST, request.FILES, instance=request.user)
# MYT验证表单数据是否有效
if form.is_valid():
# MYT保存表单数据更新用户信息
form.save()
# MYT显示更新成功消息
messages.success(request, '资料更新成功!')
# MYT重定向到个人资料页面
return redirect('profile')
else:
# MYT如果是GET请求使用当前用户数据预填充表单
form = CustomUserChangeForm(instance=request.user)
# MYT渲染资料编辑页面并传递表单对象
return render(request, 'accounts/profile_edit.html', {'form': form})
def password_reset_request(request):
"""密码重置请求 - 发送验证码"""
# MYT检查请求方法是否为POST即用户提交重置请求
if request.method == 'POST':
# MYT从POST数据中获取邮箱地址
email = request.POST.get('email')
# MYT检查邮箱是否为空
if not email:
messages.error(request, '请输入邮箱地址')
return render(request, 'accounts/password_reset_request.html')
# MYT获取用户模型类
User = get_user_model()
try:
# MYT根据邮箱查找用户是否存在
user = User.objects.get(email=email)
except User.DoesNotExist:
# MYT如果用户不存在显示错误消息
messages.error(request, '该邮箱未注册')
return render(request, 'accounts/password_reset_request.html')
# MYT调用邮箱服务发送验证码
success, message = EmailService.send_verification_code(email)
# MYT检查验证码是否发送成功
if success:
# MYT将重置邮箱存入session供后续步骤使用
request.session['reset_email'] = email
# MYT显示发送成功消息
messages.success(request, message)
# MYT重定向到验证码验证页面
return redirect('password_reset_verify')
else:
# MYT如果发送失败显示错误消息
messages.error(request, message)
# MYT渲染密码重置请求页面
return render(request, 'accounts/password_reset_request.html')
def password_reset_verify(request):
"""验证邮箱验证码"""
# MYT从session中获取之前存储的重置邮箱
email = request.session.get('reset_email')
# MYT检查邮箱是否存在防止直接访问此页面
if not email:
messages.error(request, '请先请求密码重置')
return redirect('password_reset_request')
# MYT检查请求方法是否为POST即用户提交验证码
if request.method == 'POST':
# MYT从POST数据中获取用户输入的验证码
verification_code = request.POST.get('verification_code')
# MYT验证邮箱和验证码是否匹配
if EmailService.verify_code(email, verification_code):
# MYT验证成功在session中标记已验证状态
request.session['verified'] = True
# MYT显示验证成功消息
messages.success(request, '验证成功,请设置新密码')
# MYT重定向到密码确认页面
return redirect('password_reset_confirm')
else:
# MYT验证失败显示错误消息
messages.error(request, '验证码错误或已过期')
# MYT渲染验证码验证页面传递邮箱信息
return render(request, 'accounts/password_reset_verify.html', {'email': email})
def password_reset_confirm(request):
"""设置新密码"""
# MYT从session中获取重置邮箱和验证状态
email = request.session.get('reset_email')
verified = request.session.get('verified')
# MYT检查是否已完成前面的验证步骤
if not email or not verified:
messages.error(request, '请先完成验证')
return redirect('password_reset_request')
# MYT检查请求方法是否为POST即用户提交新密码
if request.method == 'POST':
# MYT从POST数据中获取新密码
new_password = request.POST.get('new_password')
# MYT从POST数据中获取确认密码
confirm_password = request.POST.get('confirm_password')
# MYT检查密码是否为空
if not new_password or not confirm_password:
messages.error(request, '请输入密码')
# MYT检查两次输入的密码是否一致
elif new_password != confirm_password:
messages.error(request, '两次输入的密码不一致')
# MYT检查密码长度是否符合要求
elif len(new_password) < 8:
messages.error(request, '密码长度至少8位')
else:
# MYT获取用户模型类
User = get_user_model()
try:
# MYT根据邮箱查找用户
user = User.objects.get(email=email)
# MYT设置新密码会自动加密
user.set_password(new_password)
# MYT保存用户信息
user.save()
# MYT清理session中的重置相关数据
request.session.pop('reset_email', None)
request.session.pop('verified', None)
# MYT显示密码重置成功消息
messages.success(request, '密码重置成功,请使用新密码登录')
# MYT重定向到重置完成页面
return redirect('password_reset_complete')
except User.DoesNotExist:
# MYT如果用户不存在显示错误消息
messages.error(request, '用户不存在')
# MYT渲染密码重置确认页面
return render(request, 'accounts/password_reset_confirm.html')
def password_reset_complete(request):
"""密码重置完成页面"""
# MYT渲染密码重置完成页面
return render(request, 'accounts/password_reset_complete.html')

@ -1,59 +0,0 @@
from django.contrib import admin
from .models import Category, PrimaryTag, SecondaryTag, Post
# HJH使用装饰器注册Category模型到Django管理后台
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
# HJH定义在管理后台列表页面显示的字段
list_display = ['name', 'icon', 'order', 'description']
# HJH定义可以在列表页面直接编辑的字段无需进入编辑页面
list_editable = ['order', 'icon']
# HJH定义可以通过搜索框搜索的字段
search_fields = ['name']
# HJH使用装饰器注册PrimaryTag模型到Django管理后台
@admin.register(PrimaryTag)
class PrimaryTagAdmin(admin.ModelAdmin):
# HJH定义在管理后台列表页面显示的字段
list_display = ['name', 'color']
# HJH定义可以通过搜索框搜索的字段
search_fields = ['name']
# HJH使用装饰器注册SecondaryTag模型到Django管理后台
@admin.register(SecondaryTag)
class SecondaryTagAdmin(admin.ModelAdmin):
# HJH定义在管理后台列表页面显示的字段
list_display = ['name', 'tag_type', 'parent_tag']
# HJH定义在列表页面右侧的过滤器可以按这些字段快速筛选
list_filter = ['tag_type', 'parent_tag']
# HJH定义可以通过搜索框搜索的字段
search_fields = ['name']
# HJH使用装饰器注册Post模型到Django管理后台
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
# HJH定义在管理后台列表页面显示的字段
list_display = ['title', 'category', 'author', 'status', 'views', 'created_time']
# HJH定义在列表页面右侧的过滤器可以按这些字段快速筛选
list_filter = ['category', 'status', 'created_time']
# HJH定义可以通过搜索框搜索的字段支持标题和内容搜索
search_fields = ['title', 'content']
# HJH使用水平选择器显示多对多字段方便用户选择多个标签
filter_horizontal = ['primary_tags', 'secondary_tags']
# HJH定义在编辑页面中只读的字段用户不能修改这些字段
readonly_fields = ['views', 'created_time', 'updated_time']
# HJH定义字段分组显示使编辑页面更加清晰有序
fieldsets = (
# HJH基本信息分组包含文章的核心信息
('基本信息', {
'fields': ('title', 'content', 'excerpt', 'author', 'category', 'featured_image')
}),
# HJH标签管理分组包含一级和二级标签选择
('标签管理', {
'fields': ('primary_tags', 'secondary_tags')
}),
# HJH状态信息分组包含文章状态和统计信息
('状态信息', {
'fields': ('status', 'views', 'created_time', 'updated_time')
}),
)

@ -1,8 +0,0 @@
from django.apps import AppConfig
class BlogConfig(AppConfig):
# HJH设置默认主键字段类型为BigAutoField支持更大的自增ID
default_auto_field = 'django.db.models.BigAutoField'
# HJH定义应用的名称需要与settings.py中INSTALLED_APPS的配置一致
name = 'blog'

@ -1,62 +0,0 @@
from django import forms
from .models import Post, Category, PrimaryTag, SecondaryTag
from comments.models import Comment # 从comments应用导入
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['content']
widgets = {
'content': forms.Textarea(attrs={
'rows': 4,
'placeholder': '请输入您的评论...',
'class': 'comment-textarea'
})
}
labels = {
'content': ''
}
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'excerpt', 'category', 'primary_tags', 'secondary_tags', 'featured_image']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '请输入文章标题'
}),
'content': forms.Textarea(attrs={
'class': 'form-control',
'rows': 15,
'placeholder': '请输入文章内容...'
}),
'excerpt': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': '请输入文章摘要(可选)'
}),
'category': forms.Select(attrs={'class': 'form-control'}),
'primary_tags': forms.SelectMultiple(attrs={'class': 'form-control'}),
'secondary_tags': forms.SelectMultiple(attrs={'class': 'form-control'}),
'featured_image': forms.FileInput(attrs={'class': 'form-control'})
}
labels = {
'title': '文章标题',
'content': '文章内容',
'excerpt': '文章摘要',
'category': '分类',
'primary_tags': '一级标签',
'secondary_tags': '二级标签',
'featured_image': '特色图片'
}
def __init__(self, *args, **kwargs):
# HJH调用父类的初始化方法
super().__init__(*args, **kwargs)
# HJH设置分类字段的查询集限制为所有已存在的分类
self.fields['category'].queryset = Category.objects.all()
# HJH设置一级标签字段的查询集限制为所有已存在的一级标签
self.fields['primary_tags'].queryset = PrimaryTag.objects.all()
# HJH设置二级标签字段的查询集限制为所有已存在的二级标签
self.fields['secondary_tags'].queryset = SecondaryTag.objects.all()

@ -1,49 +0,0 @@
# views.py
from django.shortcuts import render
from .models import Post, Category
def index(request):
# LXC: 获取所有文章按创建时间倒序排列取前10篇
post_list = Post.objects.all().order_by('-created_time')[:10]
# LXC: 定义分类信息字典
category_info = {
'name': '非遗传承',
'label': '',
'desc': '探索南京非物质文化遗产的独特魅力与传承故事'
}
# LXC: 构建上下文数据
context = {
'post_list': post_list,
'category_info': category_info,
'current_path': request.path
}
# LXC: 渲染首页模板并返回响应
return render(request, 'blog/index.html', context)
def category_view(request, category_id):
# LXC: 定义分类映射字典,包含每个分类的详细信息
category_map = {
1: {'name': '传统工艺', 'label': '巧夺天工·工艺', 'desc': '探索南京传统手工艺的精湛技艺与匠心传承'},
2: {'name': '表演艺术', 'label': '梨园雅韵·表演', 'desc': '感受南京传统表演艺术的独特韵味与舞台魅力'},
3: {'name': '民俗文化', 'label': '人间烟火·民俗', 'desc': '体验南京丰富多彩的民俗活动与民间传统'},
4: {'name': '口头文学', 'label': '口传心授·文学', 'desc': '领略南京口传文学的语言艺术与文化内涵'},
5: {'name': '传承人物', 'label': '匠心传承·人物', 'desc': '认识南京非物质文化遗产的传承人与守护者'},
}
# LXC: 根据分类ID获取对应的分类信息如果不存在则使用默认值
category_info = category_map.get(category_id, {'name': '非遗传承', 'label': '', 'desc': '探索南京非物质文化遗产的独特魅力'})
# LXC: 根据分类ID获取对应的文章按创建时间倒序排列
posts = Post.objects.filter(category_id=category_id).order_by('-created_time')
# LXC: 构建上下文数据
context = {
'post_list': posts,
'category_info': category_info,
'current_path': request.path,
'current_category_id': category_id
}
# LXC: 渲染分类页面模板并返回响应
return render(request, 'blog/index.html', context)

@ -1,56 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-08 15:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
],
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
],
),
migrations.CreateModel(
name='Post',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('content', models.TextField()),
('excerpt', models.TextField(blank=True, max_length=200)),
('views', models.PositiveIntegerField(default=0)),
('created_time', models.DateTimeField(auto_now_add=True)),
('updated_time', models.DateTimeField(auto_now=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category')),
('tags', models.ManyToManyField(blank=True, to='blog.tag')),
],
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('author', models.CharField(max_length=100)),
('content', models.TextField()),
('created_time', models.DateTimeField(auto_now_add=True)),
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.post')),
],
),
]

@ -1,160 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-09 09:21
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='PrimaryTag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True, verbose_name='标签名称')),
('color', models.CharField(default='#8b4513', max_length=7, verbose_name='标签颜色')),
],
options={
'verbose_name': '一级标签',
'verbose_name_plural': '一级标签',
},
),
migrations.RemoveField(
model_name='post',
name='tags',
),
migrations.AlterModelOptions(
name='category',
options={'ordering': ['order'], 'verbose_name': '非遗分类', 'verbose_name_plural': '非遗分类'},
),
migrations.AlterModelOptions(
name='comment',
options={'ordering': ['-created_time'], 'verbose_name': '评论', 'verbose_name_plural': '评论'},
),
migrations.AlterModelOptions(
name='post',
options={'ordering': ['-created_time'], 'verbose_name': '非遗文章', 'verbose_name_plural': '非遗文章'},
),
migrations.AddField(
model_name='category',
name='description',
field=models.TextField(blank=True, verbose_name='分类描述'),
),
migrations.AddField(
model_name='category',
name='icon',
field=models.CharField(default='🏮', max_length=50, verbose_name='分类图标'),
),
migrations.AddField(
model_name='category',
name='order',
field=models.IntegerField(default=0, verbose_name='显示顺序'),
),
migrations.AddField(
model_name='post',
name='featured_image',
field=models.ImageField(blank=True, null=True, upload_to='posts/%Y/%m/', verbose_name='特色图片'),
),
migrations.AddField(
model_name='post',
name='status',
field=models.CharField(choices=[('draft', '草稿'), ('published', '已发布')], default='published', max_length=10, verbose_name='状态'),
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=100, unique=True, verbose_name='分类名称'),
),
migrations.AlterField(
model_name='comment',
name='author',
field=models.CharField(max_length=100, verbose_name='评论者'),
),
migrations.AlterField(
model_name='comment',
name='content',
field=models.TextField(verbose_name='评论内容'),
),
migrations.AlterField(
model_name='comment',
name='created_time',
field=models.DateTimeField(auto_now_add=True, verbose_name='评论时间'),
),
migrations.AlterField(
model_name='comment',
name='post',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.post', verbose_name='所属文章'),
),
migrations.AlterField(
model_name='post',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者'),
),
migrations.AlterField(
model_name='post',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类'),
),
migrations.AlterField(
model_name='post',
name='content',
field=models.TextField(verbose_name='文章内容'),
),
migrations.AlterField(
model_name='post',
name='created_time',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='post',
name='excerpt',
field=models.TextField(blank=True, max_length=300, verbose_name='文章摘要'),
),
migrations.AlterField(
model_name='post',
name='title',
field=models.CharField(max_length=200, verbose_name='文章标题'),
),
migrations.AlterField(
model_name='post',
name='updated_time',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
migrations.AlterField(
model_name='post',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='阅读量'),
),
migrations.AddField(
model_name='post',
name='primary_tags',
field=models.ManyToManyField(blank=True, to='blog.primarytag', verbose_name='一级标签'),
),
migrations.CreateModel(
name='SecondaryTag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='标签名称')),
('tag_type', models.CharField(choices=[('geo', '地理空间'), ('theme', '主题维度'), ('project', '非遗项目'), ('person', '人物传承'), ('time', '时间节庆')], max_length=20, verbose_name='标签类型')),
('parent_tag', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.primarytag', verbose_name='父级标签')),
],
options={
'verbose_name': '二级标签',
'verbose_name_plural': '二级标签',
},
),
migrations.AddField(
model_name='post',
name='secondary_tags',
field=models.ManyToManyField(blank=True, to='blog.secondarytag', verbose_name='二级标签'),
),
migrations.DeleteModel(
name='Tag',
),
]

@ -1,21 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-01 19:43
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0002_primarytag_remove_post_tags_alter_category_options_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='评论者'),
),
]

@ -1,16 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-01 20:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0003_alter_comment_author'),
]
operations = [
migrations.DeleteModel(
name='Comment',
),
]

@ -1,115 +0,0 @@
from django.db import models
from django.conf import settings # 新增导入
class Category(models.Model):
"""非遗分类"""
# HJH分类名称字段最大长度100字符必须唯一用于后台显示名称
name = models.CharField(max_length=100, unique=True, verbose_name="分类名称")
# HJH分类描述字段Text类型允许长文本blank=True表示可为空
description = models.TextField(blank=True, verbose_name="分类描述")
# HJH分类图标字段存储图标字符或表情默认使用🏮表情
icon = models.CharField(max_length=50, default="🏮", verbose_name="分类图标")
# HJH显示顺序字段整数类型用于控制分类在前端的显示顺序
order = models.IntegerField(default=0, verbose_name="显示顺序")
class Meta:
# HJH设置模型在Django管理后台的单数显示名称
verbose_name = "非遗分类"
# HJH设置模型在Django管理后台的复数显示名称
verbose_name_plural = "非遗分类"
# HJH默认按order字段升序排列数字小的排在前面
ordering = ['order']
def __str__(self):
# HJH定义对象的字符串表示形式在管理后台和shell中显示分类名称
return self.name
class PrimaryTag(models.Model):
"""一级标签"""
# HJH一级标签名称字段最大长度100字符必须唯一
name = models.CharField(max_length=100, unique=True, verbose_name="标签名称")
# HJH标签颜色字段存储十六进制颜色值默认值为棕色#8b4513
color = models.CharField(max_length=7, default="#8b4513", verbose_name="标签颜色")
class Meta:
# HJH设置模型在Django管理后台的单数显示名称
verbose_name = "一级标签"
# HJH设置模型在Django管理后台的复数显示名称
verbose_name_plural = "一级标签"
def __str__(self):
# HJH定义对象的字符串表示形式返回标签名称
return self.name
class SecondaryTag(models.Model):
"""二级标签"""
# HJH定义二级标签的类型选择项每个元组包含(数据库值, 显示名称)
TAG_TYPE_CHOICES = [
('geo', '地理空间'),
('theme', '主题维度'),
('project', '非遗项目'),
('person', '人物传承'),
('time', '时间节庆'),
]
# HJH二级标签名称字段最大长度100字符
name = models.CharField(max_length=100, verbose_name="标签名称")
# HJH标签类型字段使用预定义的选择项max_length=20足够存储所有选项
tag_type = models.CharField(max_length=20, choices=TAG_TYPE_CHOICES, verbose_name="标签类型")
# HJH外键关联到一级标签on_delete=CASCADE表示父标签删除时子标签也删除
parent_tag = models.ForeignKey(PrimaryTag, on_delete=models.CASCADE, null=True, blank=True, verbose_name="父级标签")
class Meta:
# HJH设置模型在Django管理后台的单数显示名称
verbose_name = "二级标签"
# HJH设置模型在Django管理后台的复数显示名称
verbose_name_plural = "二级标签"
def __str__(self):
# HJH定义对象的字符串表示形式返回标签名称和类型显示名称
return f"{self.name} ({self.get_tag_type_display()})"
class Post(models.Model):
"""非遗文章"""
# HJH定义文章状态的选择项用于status字段
STATUS_CHOICES = [
('draft', '草稿'),
('published', '已发布'),
]
# HJH文章标题字段最大长度200字符
title = models.CharField(max_length=200, verbose_name="文章标题")
# HJH文章内容字段Text类型支持长文本内容
content = models.TextField(verbose_name="文章内容")
# HJH文章摘要字段最大长度300字符blank=True表示可为空
excerpt = models.TextField(max_length=300, blank=True, verbose_name="文章摘要")
# HJH外键关联到用户模型使用Django设置中的AUTH_USER_MODEL确保兼容性
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="作者")
# HJH外键关联到分类模型文章必须属于一个分类
category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name="分类")
# HJH多对多关联到一级标签blank=True表示可以为空标签集合
primary_tags = models.ManyToManyField(PrimaryTag, blank=True, verbose_name="一级标签")
# HJH多对多关联到二级标签blank=True表示可以为空标签集合
secondary_tags = models.ManyToManyField(SecondaryTag, blank=True, verbose_name="二级标签")
# HJH特色图片字段图片上传到posts/年/月/目录,可为空
featured_image = models.ImageField(upload_to='posts/%Y/%m/', blank=True, null=True, verbose_name="特色图片")
# HJH阅读量字段PositiveIntegerField确保非负整数默认值为0
views = models.PositiveIntegerField(default=0, verbose_name="阅读量")
# HJH文章状态字段使用选择项默认状态为'published'已发布
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='published', verbose_name="状态")
# HJH创建时间字段auto_now_add=True在对象创建时自动设置当前时间
created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
# HJH更新时间字段auto_now=True在对象保存时自动更新为当前时间
updated_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
# HJH设置模型在Django管理后台的单数显示名称
verbose_name = "非遗文章"
# HJH设置模型在Django管理后台的复数显示名称
verbose_name_plural = "非遗文章"
# HJH默认按创建时间降序排列最新的文章显示在最前面
ordering = ['-created_time']
def __str__(self):
# HJH定义对象的字符串表示形式返回文章标题
return self.title

@ -1,15 +0,0 @@
{% extends 'base.html' %}
{% block title %}{{ category.name }} - 我的博客{% endblock %}
{% block content %}
<h1>分类: {{ category.name }}</h1>
{% for post in post_list %}
<article class="post">
<h2><a href="{% url 'detail' post.id %}">{{ post.title }}</a></h2>
<!-- 和首页相同的文章显示 -->
</article>
{% empty %}
<p>该分类下还没有文章。</p>
{% endfor %}
{% endblock %}

@ -1,462 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="create-post-page">
<div class="create-post-container">
<div class="create-post-header">
<div class="header-icon">✍️</div>
<h1>发表非遗文章</h1>
<p>分享您对南京非物质文化遗产的独特见解和故事</p>
</div>
<div class="create-post-card">
<form method="post" enctype="multipart/form-data" class="create-post-form">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-error">
<div class="alert-icon">⚠️</div>
<div class="alert-content">
<strong>请修正以下错误:</strong>
<ul>
{% for field in form %}
{% for error in field.errors %}
<li>{{ field.label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<!-- 基本信息 -->
<div class="form-section">
<h3 class="section-title">
<span class="title-icon">📄</span>
文章基本信息
</h3>
<div class="form-group">
<label for="{{ form.title.id_for_label }}" class="form-label">
文章标题
<span class="required">*</span>
</label>
{{ form.title }}
<div class="help-text">请输入吸引人的文章标题</div>
</div>
<div class="form-group">
<label for="{{ form.excerpt.id_for_label }}" class="form-label">
文章摘要
</label>
{{ form.excerpt }}
<div class="help-text">简要描述文章内容,吸引读者阅读(可选)</div>
</div>
</div>
<!-- 分类和标签 -->
<div class="form-section">
<h3 class="section-title">
<span class="title-icon">🏷️</span>
分类与标签
</h3>
<div class="form-grid">
<div class="form-group">
<label for="{{ form.category.id_for_label }}" class="form-label">
文章分类
<span class="required">*</span>
</label>
{{ form.category }}
</div>
<div class="form-group">
<label for="{{ form.featured_image.id_for_label }}" class="form-label">
特色图片
</label>
<div class="file-upload">
{{ form.featured_image }}
<div class="file-help">支持 JPG、PNG 格式,最大 5MB</div>
</div>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="{{ form.primary_tags.id_for_label }}" class="form-label">
一级标签
</label>
{{ form.primary_tags }}
<div class="help-text">可多选,按住 Ctrl 键选择多个标签</div>
</div>
<div class="form-group">
<label for="{{ form.secondary_tags.id_for_label }}" class="form-label">
二级标签
</label>
{{ form.secondary_tags }}
<div class="help-text">可多选,按住 Ctrl 键选择多个标签</div>
</div>
</div>
</div>
<!-- 文章内容 -->
<div class="form-section">
<h3 class="section-title">
<span class="title-icon">📝</span>
文章内容
<span class="required">*</span>
</h3>
<div class="form-group">
<label for="{{ form.content.id_for_label }}" class="form-label">
详细内容
</label>
{{ form.content }}
<div class="help-text">
<strong>写作提示:</strong>
<ul>
<li>详细描述非遗项目的起源、特点和现状</li>
<li>可以插入图片、视频等多媒体内容</li>
<li>分享个人见解和传承故事</li>
<li>确保内容真实准确</li>
</ul>
</div>
</div>
</div>
<!-- 表单操作 -->
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="btn-icon">💾</span>
保存草稿
</button>
<a href="{% url 'index' %}" class="btn btn-secondary">
<span class="btn-icon">↩️</span>
取消返回
</a>
</div>
</form>
</div>
<!-- 写作提示 -->
<div class="writing-tips">
<div class="tips-icon">💡</div>
<div class="tips-content">
<h4>写作提示</h4>
<ul>
<li>确保内容真实准确,尊重传统文化</li>
<li>可以结合图片、视频等多媒体素材</li>
<li>文章保存为草稿后,可以在"我的文章"中编辑和发布</li>
<li>发布前请仔细检查内容</li>
</ul>
</div>
</div>
</div>
</div>
<style>
.create-post-page {
min-height: 100vh;
background: linear-gradient(135deg, #faf3e8 0%, #fffaf0 100%);
padding: 30px 20px;
}
.create-post-container {
max-width: 900px;
margin: 0 auto;
}
.create-post-header {
text-align: center;
margin-bottom: 40px;
}
.header-icon {
font-size: 4em;
margin-bottom: 20px;
}
.create-post-header h1 {
color: var(--primary-color);
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 700;
}
.create-post-header p {
color: var(--text-light);
font-size: 1.1em;
}
.create-post-card {
background: var(--bg-white);
border-radius: 20px;
box-shadow: 0 15px 35px rgba(139, 69, 19, 0.1);
border: 1px solid var(--border-color);
overflow: hidden;
margin-bottom: 30px;
}
.create-post-form {
padding: 40px;
}
/* 复用编辑资料页面的样式,这里简化显示 */
.form-section {
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 1px solid var(--border-color);
}
.form-section:last-child {
border-bottom: none;
margin-bottom: 0;
}
.section-title {
color: var(--primary-color);
font-size: 1.3em;
margin-bottom: 25px;
display: flex;
align-items: center;
gap: 10px;
}
.title-icon {
font-size: 1.2em;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-group {
margin-bottom: 25px;
}
.form-label {
display: block;
margin-bottom: 8px;
color: var(--text-dark);
font-weight: 600;
font-size: 14px;
}
.required {
color: var(--nj-red);
}
.form-control {
width: 100%;
padding: 15px;
border: 2px solid var(--border-color);
border-radius: 10px;
font-size: 15px;
transition: all 0.3s ease;
background: var(--bg-white);
font-family: inherit;
}
.form-control:focus {
outline: none;
border-color: var(--nj-gold);
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.1);
transform: translateY(-1px);
}
textarea.form-control {
resize: vertical;
min-height: 120px;
line-height: 1.5;
}
#id_content {
min-height: 300px;
}
.help-text {
font-size: 12px;
color: var(--text-light);
margin-top: 5px;
line-height: 1.4;
}
.help-text ul {
margin: 5px 0 0 20px;
}
.help-text li {
margin: 3px 0;
}
.file-upload {
border: 2px dashed var(--border-color);
border-radius: 10px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
}
.file-upload:hover {
border-color: var(--nj-gold);
background: rgba(212, 175, 55, 0.05);
}
.file-help {
font-size: 12px;
color: var(--text-light);
margin-top: 10px;
}
.alert {
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
display: flex;
align-items: flex-start;
gap: 15px;
}
.alert-error {
background: rgba(198, 47, 47, 0.1);
border: 1px solid rgba(198, 47, 47, 0.2);
color: var(--nj-red);
}
.alert-icon {
font-size: 1.5em;
flex-shrink: 0;
}
.alert-content {
flex: 1;
}
.alert-content ul {
margin: 10px 0 0 20px;
}
.alert-content li {
margin: 5px 0;
}
.form-actions {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 40px;
padding-top: 30px;
border-top: 1px solid var(--border-color);
}
.btn {
padding: 15px 30px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
min-width: 140px;
justify-content: center;
}
.btn-primary {
background: linear-gradient(135deg, var(--nj-brown), var(--nj-light-brown));
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(139, 69, 19, 0.3);
}
.btn-secondary {
background: var(--border-color);
color: var(--text-dark);
}
.btn-secondary:hover {
background: var(--text-light);
color: white;
}
.writing-tips {
background: var(--bg-white);
padding: 25px;
border-radius: 15px;
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.1);
border: 1px solid var(--border-color);
display: flex;
align-items: flex-start;
gap: 20px;
}
.tips-icon {
font-size: 2.5em;
flex-shrink: 0;
}
.tips-content h4 {
color: var(--primary-color);
margin-bottom: 15px;
font-size: 1.2em;
}
.tips-content ul {
margin: 0;
padding-left: 20px;
color: var(--text-light);
}
.tips-content li {
margin: 8px 0;
line-height: 1.5;
}
/* 响应式设计 */
@media (max-width: 768px) {
.create-post-form {
padding: 25px;
}
.form-grid {
grid-template-columns: 1fr;
gap: 0;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
.create-post-header h1 {
font-size: 2em;
}
}
@media (max-width: 480px) {
.create-post-form {
padding: 20px;
}
.create-post-header h1 {
font-size: 1.8em;
}
.header-icon {
font-size: 3em;
}
}
</style>
{% endblock %}

@ -1,275 +0,0 @@
<!-- blog/templates/blog/detail.html -->
{% extends 'base.html' %}
{% block content %}
<article class="post-detail">
<div class="post-header">
<h1 class="post-title">{{ post.title }}</h1>
<div class="post-meta">
<span><i>👤</i> {{ post.author.username }}</span>
<span><i>🏷️</i> {{ post.category.name }}</span>
<span><i>👁️</i> {{ post.views }} 次阅读</span>
<span><i>📅</i> {{ post.created_time|date:"Y-m-d H:i" }}</span>
</div>
</div>
{% if post.featured_image %}
<div class="post-image">
<img src="{{ post.featured_image.url }}" alt="{{ post.title }}">
</div>
{% endif %}
<div class="post-content">
{{ post.content|linebreaks }}
</div>
<div class="post-tags">
{% for tag in post.primary_tags.all %}
<span class="tag">{{ tag.name }}</span>
{% endfor %}
{% for tag in post.secondary_tags.all %}
<span class="tag">#{{ tag.name }}</span>
{% endfor %}
</div>
</article>
<!-- 评论区域 -->
<section class="comments-section">
<h3 class="comments-title">评论({{ comments.count }}</h3>
<!-- 评论表单 -->
<div class="comment-form-container">
{% if user.is_authenticated %}
<form method="post" action="{% url 'comments:add_comment' post.id %}" class="comment-form">
{% csrf_token %}
{{ comment_form.content }}
<button type="submit" class="submit-comment-btn">发表评论</button>
</form>
{% else %}
<div class="login-prompt">
<p><a href="{% url 'login' %}">登录</a>后发表评论</p>
</div>
{% endif %}
</div>
<!-- 评论列表 -->
<div class="comments-list">
{% for comment in comments %}
<div class="comment-item">
<div class="comment-header">
<span class="comment-author">{{ comment.author.username }}</span>
<span class="comment-time">{{ comment.created_time|date:"Y-m-d H:i" }}</span>
</div>
<div class="comment-content">
{{ comment.content|linebreaks }}
</div>
</div>
{% empty %}
<div class="no-comments">
<p>暂无评论,快来发表第一条评论吧!</p>
</div>
{% endfor %}
</div>
</section>
<style>
.post-detail {
background: var(--bg-white);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(139, 69, 19, 0.1);
padding: 40px;
margin-bottom: 30px;
border: 1px solid var(--border-color);
}
.post-header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid var(--border-color);
padding-bottom: 20px;
}
.post-title {
color: var(--text-dark);
font-size: 2.2em;
margin-bottom: 15px;
line-height: 1.3;
}
.post-meta {
color: var(--text-light);
font-size: 14px;
display: flex;
justify-content: center;
gap: 25px;
flex-wrap: wrap;
}
.post-meta span {
display: flex;
align-items: center;
gap: 6px;
}
.post-image {
margin: 30px 0;
text-align: center;
}
.post-image img {
max-width: 100%;
height: auto;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.post-content {
color: var(--text-light);
font-size: 16px;
line-height: 1.8;
text-align: justify;
}
.post-tags {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
display: flex;
flex-wrap: wrap;
gap: 10px;
}
/* 评论区域样式 */
.comments-section {
background: var(--bg-white);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(139, 69, 19, 0.1);
padding: 30px;
border: 1px solid var(--border-color);
}
.comments-title {
color: var(--primary-color);
font-size: 1.5em;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 2px solid var(--accent-color);
}
.comment-form-container {
margin-bottom: 30px;
}
.comment-textarea {
width: 100%;
padding: 15px;
border: 2px solid var(--border-color);
border-radius: 10px;
font-size: 14px;
font-family: inherit;
resize: vertical;
min-height: 100px;
transition: border-color 0.3s ease;
}
.comment-textarea:focus {
outline: none;
border-color: var(--primary-color);
}
.submit-comment-btn {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 15px;
}
.submit-comment-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(139, 69, 19, 0.3);
}
.login-prompt {
background: var(--bg-light);
padding: 20px;
border-radius: 10px;
text-align: center;
border: 1px solid var(--border-color);
}
.login-prompt a {
color: var(--primary-color);
font-weight: 600;
text-decoration: none;
}
.login-prompt a:hover {
text-decoration: underline;
}
.comment-item {
padding: 20px;
border-bottom: 1px solid var(--border-color);
background: var(--bg-light);
border-radius: 10px;
margin-bottom: 15px;
}
.comment-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.comment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.comment-author {
color: var(--primary-color);
font-weight: 600;
}
.comment-time {
color: var(--text-light);
font-size: 12px;
}
.comment-content {
color: var(--text-dark);
line-height: 1.6;
}
.no-comments {
text-align: center;
color: var(--text-light);
padding: 40px 20px;
}
@media (max-width: 768px) {
.post-detail {
padding: 25px 20px;
}
.post-title {
font-size: 1.8em;
}
.post-meta {
gap: 15px;
}
.comments-section {
padding: 20px;
}
}
</style>
{% endblock %}

@ -1,561 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="edit-post-page">
<div class="edit-post-container">
<div class="edit-post-header">
<div class="header-icon">✏️</div>
<h1>编辑文章</h1>
<p>修改您的非遗文章内容</p>
</div>
<div class="edit-post-card">
<form method="post" enctype="multipart/form-data" class="edit-post-form">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-error">
<div class="alert-icon">⚠️</div>
<div class="alert-content">
<strong>请修正以下错误:</strong>
<ul>
{% for field in form %}
{% for error in field.errors %}
<li>{{ field.label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
<div class="alert-icon"></div>
<div class="alert-content">{{ message }}</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- 文章状态信息 -->
<div class="post-status-info">
<div class="status-item">
<span class="status-label">当前状态:</span>
<span class="status-value {% if post.status == 'published' %}published{% else %}draft{% endif %}">
{% if post.status == 'published' %}已发布{% else %}草稿{% endif %}
</span>
</div>
<div class="status-item">
<span class="status-label">创建时间:</span>
<span class="status-value">{{ post.created_time|date:"Y-m-d H:i" }}</span>
</div>
<div class="status-item">
<span class="status-label">最后更新:</span>
<span class="status-value">{{ post.updated_time|date:"Y-m-d H:i" }}</span>
</div>
<div class="status-item">
<span class="status-label">阅读量:</span>
<span class="status-value">{{ post.views }}</span>
</div>
</div>
<!-- 基本信息 -->
<div class="form-section">
<h3 class="section-title">
<span class="title-icon">📄</span>
文章基本信息
</h3>
<div class="form-group">
<label for="{{ form.title.id_for_label }}" class="form-label">
文章标题
<span class="required">*</span>
</label>
{{ form.title }}
<div class="help-text">请输入吸引人的文章标题</div>
</div>
<div class="form-group">
<label for="{{ form.excerpt.id_for_label }}" class="form-label">
文章摘要
</label>
{{ form.excerpt }}
<div class="help-text">简要描述文章内容,吸引读者阅读(可选)</div>
</div>
</div>
<!-- 分类和标签 -->
<div class="form-section">
<h3 class="section-title">
<span class="title-icon">🏷️</span>
分类与标签
</h3>
<div class="form-grid">
<div class="form-group">
<label for="{{ form.category.id_for_label }}" class="form-label">
文章分类
<span class="required">*</span>
</label>
{{ form.category }}
</div>
<div class="form-group">
<label for="{{ form.featured_image.id_for_label }}" class="form-label">
特色图片
</label>
<div class="file-upload">
{% if post.featured_image %}
<div class="current-image">
<img src="{{ post.featured_image.url }}" alt="当前图片" class="image-preview">
<div class="image-info">当前图片</div>
</div>
{% endif %}
{{ form.featured_image }}
<div class="file-help">支持 JPG、PNG 格式,最大 5MB</div>
</div>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="{{ form.primary_tags.id_for_label }}" class="form-label">
一级标签
</label>
{{ form.primary_tags }}
<div class="help-text">可多选,按住 Ctrl 键选择多个标签</div>
</div>
<div class="form-group">
<label for="{{ form.secondary_tags.id_for_label }}" class="form-label">
二级标签
</label>
{{ form.secondary_tags }}
<div class="help-text">可多选,按住 Ctrl 键选择多个标签</div>
</div>
</div>
</div>
<!-- 文章内容 -->
<div class="form-section">
<h3 class="section-title">
<span class="title-icon">📝</span>
文章内容
<span class="required">*</span>
</h3>
<div class="form-group">
<label for="{{ form.content.id_for_label }}" class="form-label">
详细内容
</label>
{{ form.content }}
<div class="help-text">
<strong>写作提示:</strong>
<ul>
<li>详细描述非遗项目的起源、特点和现状</li>
<li>可以插入图片、视频等多媒体内容</li>
<li>分享个人见解和传承故事</li>
<li>确保内容真实准确</li>
</ul>
</div>
</div>
</div>
<!-- 表单操作 -->
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="btn-icon">💾</span>
保存更改
</button>
{% if post.status == 'draft' %}
<form method="post" action="{% url 'publish_post' post.id %}" class="action-form">
{% csrf_token %}
<button type="submit" class="btn btn-success">
<span class="btn-icon">🚀</span>
立即发布
</button>
</form>
{% endif %}
<a href="{% url 'user_posts' %}" class="btn btn-secondary">
<span class="btn-icon">↩️</span>
返回列表
</a>
<a href="{% url 'detail' post.id %}" class="btn btn-info">
<span class="btn-icon">👀</span>
查看文章
</a>
</div>
</form>
</div>
</div>
</div>
<style>
.edit-post-page {
min-height: 100vh;
background: linear-gradient(135deg, #faf3e8 0%, #fffaf0 100%);
padding: 30px 20px;
}
.edit-post-container {
max-width: 900px;
margin: 0 auto;
}
.edit-post-header {
text-align: center;
margin-bottom: 40px;
}
.header-icon {
font-size: 4em;
margin-bottom: 20px;
}
.edit-post-header h1 {
color: var(--primary-color);
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 700;
}
.edit-post-header p {
color: var(--text-light);
font-size: 1.1em;
}
.edit-post-card {
background: var(--bg-white);
border-radius: 20px;
box-shadow: 0 15px 35px rgba(139, 69, 19, 0.1);
border: 1px solid var(--border-color);
overflow: hidden;
margin-bottom: 30px;
}
.edit-post-form {
padding: 40px;
}
/* 文章状态信息 */
.post-status-info {
background: var(--bg-light);
padding: 20px;
border-radius: 10px;
margin-bottom: 30px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.status-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.status-label {
font-size: 0.9em;
color: var(--text-light);
font-weight: 600;
}
.status-value {
font-size: 1.1em;
font-weight: 600;
}
.status-value.published {
color: #4caf50;
}
.status-value.draft {
color: var(--text-light);
}
/* 表单样式(复用之前的样式) */
.form-section {
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 1px solid var(--border-color);
}
.form-section:last-child {
border-bottom: none;
margin-bottom: 0;
}
.section-title {
color: var(--primary-color);
font-size: 1.3em;
margin-bottom: 25px;
display: flex;
align-items: center;
gap: 10px;
}
.title-icon {
font-size: 1.2em;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-group {
margin-bottom: 25px;
}
.form-label {
display: block;
margin-bottom: 8px;
color: var(--text-dark);
font-weight: 600;
font-size: 14px;
}
.required {
color: var(--nj-red);
}
.form-control {
width: 100%;
padding: 15px;
border: 2px solid var(--border-color);
border-radius: 10px;
font-size: 15px;
transition: all 0.3s ease;
background: var(--bg-white);
font-family: inherit;
}
.form-control:focus {
outline: none;
border-color: var(--nj-gold);
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.1);
transform: translateY(-1px);
}
textarea.form-control {
resize: vertical;
min-height: 120px;
line-height: 1.5;
}
#id_content {
min-height: 300px;
}
.help-text {
font-size: 12px;
color: var(--text-light);
margin-top: 5px;
line-height: 1.4;
}
.help-text ul {
margin: 5px 0 0 20px;
}
.help-text li {
margin: 3px 0;
}
/* 图片上传 */
.file-upload {
border: 2px dashed var(--border-color);
border-radius: 10px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
}
.file-upload:hover {
border-color: var(--nj-gold);
background: rgba(212, 175, 55, 0.05);
}
.current-image {
margin-bottom: 15px;
}
.image-preview {
max-width: 200px;
max-height: 150px;
border-radius: 8px;
border: 2px solid var(--border-color);
}
.image-info {
font-size: 0.9em;
color: var(--text-light);
margin-top: 5px;
}
.file-help {
font-size: 12px;
color: var(--text-light);
margin-top: 10px;
}
/* 警告和消息 */
.alert {
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
display: flex;
align-items: flex-start;
gap: 15px;
}
.alert-error {
background: rgba(198, 47, 47, 0.1);
border: 1px solid rgba(198, 47, 47, 0.2);
color: var(--nj-red);
}
.alert-success {
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.2);
color: #4caf50;
}
.alert-icon {
font-size: 1.5em;
flex-shrink: 0;
}
.alert-content {
flex: 1;
}
.alert-content ul {
margin: 10px 0 0 20px;
}
.alert-content li {
margin: 5px 0;
}
/* 表单操作 */
.form-actions {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 40px;
padding-top: 30px;
border-top: 1px solid var(--border-color);
flex-wrap: wrap;
}
.btn {
padding: 15px 25px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
min-width: 120px;
justify-content: center;
}
.btn-primary {
background: linear-gradient(135deg, var(--nj-brown), var(--nj-light-brown));
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(139, 69, 19, 0.3);
}
.btn-success {
background: #4caf50;
color: white;
}
.btn-success:hover {
background: #45a049;
transform: translateY(-2px);
}
.btn-secondary {
background: var(--border-color);
color: var(--text-dark);
}
.btn-secondary:hover {
background: var(--text-light);
color: white;
}
.btn-info {
background: var(--primary-color);
color: white;
}
.btn-info:hover {
background: var(--secondary-color);
transform: translateY(-2px);
}
.action-form {
margin: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.edit-post-form {
padding: 25px;
}
.form-grid {
grid-template-columns: 1fr;
gap: 0;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
.edit-post-header h1 {
font-size: 2em;
}
.post-status-info {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.edit-post-form {
padding: 20px;
}
.edit-post-header h1 {
font-size: 1.8em;
}
.header-icon {
font-size: 3em;
}
}
</style>
{% endblock %}

@ -1,24 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<h1>最新文章</h1>
{% for post in post_list %}
<article class="post">
<h2><a href="{% url 'detail' post.id %}">{{ post.title }}</a></h2>
<div class="post-meta">
<span>{{ post.comment_set.count }} 评论</span> | <span>{{ post.views }} views</span>
</div>
<p>{{ post.excerpt|default:post.content|truncatewords:30 }}</p>
<a href="{% url 'detail' post.id %}" class="read-more">Read more</a>
<div class="post-footer">
发布于 {{ post.category.name }} 并标记为
{% for tag in post.tags.all %}
{{ tag.name }}{% if not forloop.last %}, {% endif %}
{% endfor %}
由 {{ post.author.username }} 在 {{ post.created_time|date:"Y-m-d" }}
</div>
</article>
{% empty %}
<p>还没有发布任何文章。</p>
{% endfor %}
{% endblock %}

@ -1,375 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="user-posts-page">
<div class="user-posts-container">
<div class="user-posts-header">
<div class="header-icon">📚</div>
<h1>我的文章</h1>
<p>管理您发表的非遗文章</p>
</div>
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
<div class="alert-icon"></div>
<div class="alert-content">{{ message }}</div>
</div>
{% endfor %}
</div>
{% endif %}
<div class="posts-actions">
<a href="{% url 'create_post' %}" class="btn btn-primary">
<span class="btn-icon">✍️</span>
发表新文章
</a>
</div>
{% if posts %}
<div class="posts-list">
{% for post in posts %}
<div class="post-card {% if post.status == 'draft' %}draft{% endif %}">
<div class="post-header">
<h3 class="post-title">
<a href="{% url 'detail' post.id %}">{{ post.title }}</a>
{% if post.status == 'draft' %}
<span class="status-badge draft">草稿</span>
{% else %}
<span class="status-badge published">已发布</span>
{% endif %}
</h3>
<div class="post-meta">
<span class="meta-item">
<span class="meta-icon">📅</span>
{{ post.created_time|date:"Y-m-d H:i" }}
</span>
<span class="meta-item">
<span class="meta-icon">👁️</span>
{{ post.views }} 阅读
</span>
<span class="meta-item">
<span class="meta-icon">💬</span>
{{ post.comments.count }} 评论
</span>
</div>
</div>
{% if post.excerpt %}
<div class="post-excerpt">
{{ post.excerpt }}
</div>
{% endif %}
<div class="post-actions">
<a href="{% url 'edit_post' post.id %}" class="action-btn edit">
<span class="action-icon">✏️</span>
编辑
</a>
{% if post.status == 'draft' %}
<form method="post" action="{% url 'publish_post' post.id %}" class="action-form">
{% csrf_token %}
<button type="submit" class="action-btn publish">
<span class="action-icon">🚀</span>
发布
</button>
</form>
{% endif %}
<form method="post" action="{% url 'delete_post' post.id %}" class="action-form"
onsubmit="return confirm('确定要删除这篇文章吗?此操作不可恢复。');">
{% csrf_token %}
<button type="submit" class="action-btn delete">
<span class="action-icon">🗑️</span>
删除
</button>
</form>
<a href="{% url 'detail' post.id %}" class="action-btn view">
<span class="action-icon">👀</span>
查看
</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="empty-icon">📝</div>
<h3>还没有发表过文章</h3>
<p>分享您对非物质文化遗产的见解,让更多人了解传统文化的魅力</p>
<a href="{% url 'create_post' %}" class="btn btn-primary">
<span class="btn-icon">✍️</span>
开始创作第一篇文章
</a>
</div>
{% endif %}
</div>
</div>
<style>
.user-posts-page {
min-height: 100vh;
background: linear-gradient(135deg, #faf3e8 0%, #fffaf0 100%);
padding: 30px 20px;
}
.user-posts-container {
max-width: 900px;
margin: 0 auto;
}
.user-posts-header {
text-align: center;
margin-bottom: 40px;
}
.header-icon {
font-size: 4em;
margin-bottom: 20px;
}
.user-posts-header h1 {
color: var(--primary-color);
font-size: 2.5em;
margin-bottom: 10px;
font-weight: 700;
}
.user-posts-header p {
color: var(--text-light);
font-size: 1.1em;
}
.posts-actions {
margin-bottom: 30px;
text-align: center;
}
/* 复用之前的按钮样式 */
.btn {
padding: 15px 30px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
min-width: 140px;
justify-content: center;
}
.btn-primary {
background: linear-gradient(135deg, var(--nj-brown), var(--nj-light-brown));
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(139, 69, 19, 0.3);
}
/* 文章卡片 */
.posts-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.post-card {
background: var(--bg-white);
border-radius: 15px;
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.1);
border: 1px solid var(--border-color);
padding: 25px;
transition: all 0.3s ease;
}
.post-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(139, 69, 19, 0.15);
}
.post-card.draft {
border-left: 4px solid var(--text-light);
opacity: 0.8;
}
.post-header {
margin-bottom: 15px;
}
.post-title {
margin: 0 0 10px 0;
font-size: 1.4em;
}
.post-title a {
color: var(--text-dark);
text-decoration: none;
}
.post-title a:hover {
color: var(--primary-color);
text-decoration: underline;
}
.status-badge {
font-size: 0.7em;
padding: 4px 8px;
border-radius: 12px;
margin-left: 10px;
font-weight: 600;
}
.status-badge.draft {
background: var(--text-light);
color: white;
}
.status-badge.published {
background: var(--nj-gold);
color: var(--text-dark);
}
.post-meta {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
color: var(--text-light);
font-size: 0.9em;
}
.post-excerpt {
color: var(--text-light);
line-height: 1.6;
margin-bottom: 20px;
padding: 15px;
background: var(--bg-light);
border-radius: 8px;
border-left: 3px solid var(--accent-color);
}
.post-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.action-btn {
padding: 8px 15px;
border: none;
border-radius: 6px;
font-size: 14px;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 5px;
transition: all 0.3s ease;
cursor: pointer;
}
.action-btn.edit {
background: var(--nj-gold);
color: var(--text-dark);
}
.action-btn.publish {
background: #4caf50;
color: white;
}
.action-btn.delete {
background: var(--nj-red);
color: white;
}
.action-btn.view {
background: var(--primary-color);
color: white;
}
.action-btn:hover {
transform: translateY(-2px);
opacity: 0.9;
}
.action-form {
margin: 0;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
background: var(--bg-white);
border-radius: 15px;
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.1);
}
.empty-icon {
font-size: 4em;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-state h3 {
color: var(--text-dark);
margin-bottom: 10px;
font-size: 1.5em;
}
.empty-state p {
color: var(--text-light);
margin-bottom: 30px;
font-size: 1.1em;
}
/* 响应式设计 */
@media (max-width: 768px) {
.user-posts-header h1 {
font-size: 2em;
}
.post-actions {
flex-direction: column;
}
.action-btn {
width: 100%;
justify-content: center;
}
.post-meta {
flex-direction: column;
gap: 5px;
}
}
@media (max-width: 480px) {
.user-posts-header h1 {
font-size: 1.8em;
}
.header-icon {
font-size: 3em;
}
.post-card {
padding: 20px;
}
}
</style>
{% endblock %}

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

@ -1,21 +0,0 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('category/<int:category_id>/', views.category_view, name='category'),
path('post/<int:post_id>/', views.detail, name='detail'),
path('search/', views.search, name='search'),
path('about/', views.about, name='about'),
# 文章管理路由
path('create/', views.create_post, name='create_post'),
path('my-posts/', views.user_posts, name='user_posts'),
path('edit/<int:post_id>/', views.edit_post, name='edit_post'),
path('publish/<int:post_id>/', views.publish_post, name='publish_post'),
path('delete/<int:post_id>/', views.delete_post, name='delete_post'),
# 点赞收藏路由
path('post/<int:post_id>/like/', views.like_post, name='like_post'),
path('post/<int:post_id>/favorite/', views.favorite_post, name='favorite_post'),
]

@ -1,398 +0,0 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db.models import Q
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .models import Post, Category
from .forms import PostForm
from comments.forms import CommentForm # 从comments应用导入CommentForm
def index(request):
"""首页视图 - 显示最新发布的文章"""
# LXC获取所有已发布的文章按照创建时间倒序排列
post_list_all = Post.objects.filter(status='published').order_by('-created_time')
# LXC创建分页器对象每页显示10篇文章
paginator = Paginator(post_list_all, 10)
page_number = request.GET.get('page')
try:
post_list = paginator.page(page_number)
except PageNotAnInteger:
# LXC当页码不是整数时显示第一页内容
post_list = paginator.page(1)
except EmptyPage:
# LXC当页码超出范围时显示最后一页内容
post_list = paginator.page(paginator.num_pages)
# LXC获取热门文章按浏览量降序排列取前5篇
hot_posts = Post.objects.filter(status='published').order_by('-views')[:5]
# LXC构建页面上下文数据
context = {
'post_list': post_list,
'hot_posts': hot_posts,
'category_info': {
'name': '非遗传承',
'label': '',
'desc': '探索南京非物质文化遗产的独特魅力与传承故事'
},
'current_path': request.path,
'current_category_id': None
}
# LXC渲染首页模板并返回响应
return render(request, 'blog/index.html', context)
def category_view(request, category_id):
"""分类页面视图 - 显示指定分类的文章"""
# LXC根据分类ID获取分类对象如果不存在则返回404错误
category = get_object_or_404(Category, id=category_id)
# LXC获取该分类下所有已发布的文章按创建时间倒序排列
posts_all = Post.objects.filter(category=category, status='published').order_by('-created_time')
# LXC创建分页器对象每页显示10篇文章
paginator = Paginator(posts_all, 10)
page_number = request.GET.get('page')
try:
posts = paginator.page(page_number)
except PageNotAnInteger:
posts = paginator.page(1)
except EmptyPage:
posts = paginator.page(paginator.num_pages)
# LXC获取热门文章按浏览量降序排列取前5篇
hot_posts = Post.objects.filter(status='published').order_by('-views')[:5]
# LXC定义分类信息映射字典包含每个分类的显示标签和描述
category_map = {
1: {'label': '巧夺天工·工艺', 'desc': '探索南京传统手工艺的精湛技艺与匠心传承'},
2: {'label': '梨园雅韵·表演', 'desc': '感受南京传统表演艺术的独特韵味与舞台魅力'},
3: {'label': '人间烟火·民俗', 'desc': '体验南京丰富多彩的民俗活动与民间传统'},
4: {'label': '口传心授·文学', 'desc': '领略南京口传文学的语言艺术与文化内涵'},
5: {'label': '匠心传承·人物', 'desc': '认识南京非物质文化遗产的传承人与守护者'},
}
# LXC根据分类ID获取对应的分类信息如果不存在则使用默认值
category_info = category_map.get(category_id, {
'label': category.name,
'desc': f'探索南京{category.name}的独特魅力'
})
# LXC构建页面上下文数据
context = {
'post_list': posts,
'hot_posts': hot_posts,
'category_info': {
'name': category.name,
'label': category_info['label'],
'desc': category_info['desc']
},
'current_path': request.path,
'current_category_id': category_id
}
# LXC渲染分类页面模板并返回响应
return render(request, 'blog/index.html', context)
def detail(request, post_id):
"""文章详情页 - 显示单篇文章的完整内容"""
# LXC根据文章ID获取已发布的文章对象如果不存在则返回404错误
post = get_object_or_404(Post, id=post_id, status='published')
# LXC增加文章浏览量并保存到数据库
post.views += 1
post.save()
# LXC导入评论模型
from comments.models import Comment
# LXC获取该文章的所有评论按创建时间倒序排列
comments = post.comments.all().order_by('-created_time')
# LXC创建评论表单实例
comment_form = CommentForm()
# LXC构建页面上下文数据
context = {
'post': post,
'comments': comments,
'comment_form': comment_form,
}
# LXC渲染文章详情页模板并返回响应
return render(request, 'blog/detail.html', context)
def search(request):
"""搜索文章 - 根据关键词搜索文章内容"""
# LXC获取搜索关键词并去除首尾空格
query = request.GET.get('q', '').strip()
# LXC获取热门文章按浏览量降序排列取前5篇
hot_posts = Post.objects.filter(status='published').order_by('-views')[:5]
# LXC初始化空的搜索结果集
results = Post.objects.none()
result_count = 0
if query:
# LXC如果有关键词执行多字段搜索
results = Post.objects.filter(
Q(title__icontains=query) |
Q(content__icontains=query) |
Q(excerpt__icontains=query) |
Q(primary_tags__name__icontains=query) |
Q(secondary_tags__name__icontains=query),
status='published'
).distinct().order_by('-created_time')
# LXC统计搜索结果数量
result_count = results.count()
# LXC如果没有搜索结果添加提示消息
if result_count == 0:
messages.info(request, f'没有找到包含"{query}"的文章,请尝试其他关键词。')
else:
# LXC如果没有输入关键词显示提示信息
messages.info(request, '请输入搜索关键词。')
# LXC创建分页器对象每页显示10篇文章
paginator = Paginator(results, 10)
page_number = request.GET.get('page')
try:
page_obj = paginator.page(page_number)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
# LXC构建页面上下文数据
context = {
'post_list': page_obj,
'hot_posts': hot_posts,
'category_info': {
'name': f'搜索"{query}"' if query else '搜索',
'label': '搜索结果',
'desc': f'找到 {result_count} 篇相关文章' if query else '请输入搜索关键词'
},
'current_path': request.path,
'current_category_id': None,
'search_query': query
}
return render(request, 'blog/index.html', context)
def about(request):
"""关于页面 - 网站介绍"""
context = {
'category_info': {
'name': '关于我们',
'label': '关于金陵非遗',
'desc': '了解南京非物质文化遗产保护与传承的使命'
},
'current_path': request.path,
}
return render(request, 'blog/about.html', context)
# ========== 文章管理视图函数 ==========
@login_required
def create_post(request):
"""发表新文章"""
if request.method == 'POST':
# LXC使用POST数据和文件数据初始化表单
form = PostForm(request.POST, request.FILES)
if form.is_valid():
# LXC保存文章但不提交到数据库以便设置作者
post = form.save(commit=False)
# LXC设置文章作者为当前登录用户
post.author = request.user
# LXC新文章默认为草稿状态
post.status = 'draft'
post.save()
# LXC保存多对多关系标签
form.save_m2m()
messages.success(request, '文章创建成功!已保存为草稿。')
return redirect('user_posts')
else:
messages.error(request, '文章创建失败,请检查表单内容。')
else:
# LXCGET请求时创建空表单
form = PostForm()
context = {
'form': form,
'category_info': {
'name': '发表文章',
'label': '创作非遗',
'desc': '分享您对南京非物质文化遗产的见解和故事'
},
'current_path': request.path,
}
return render(request, 'blog/create_post.html', context)
@login_required
def user_posts(request):
"""用户个人文章管理"""
# LXC获取当前用户的所有文章按创建时间倒序排列
posts = Post.objects.filter(author=request.user).order_by('-created_time')
context = {
'posts': posts,
'category_info': {
'name': '我的文章',
'label': '个人创作',
'desc': '管理您发表的非遗文章'
},
'current_path': request.path,
}
return render(request, 'blog/user_posts.html', context)
@login_required
def edit_post(request, post_id):
"""编辑文章"""
# LXC获取文章对象确保作者是当前用户
post = get_object_or_404(Post, id=post_id, author=request.user)
if request.method == 'POST':
# LXC使用POST数据和文件数据初始化表单并绑定到现有文章实例
form = PostForm(request.POST, request.FILES, instance=post)
if form.is_valid():
form.save()
messages.success(request, '文章更新成功!')
return redirect('user_posts')
else:
messages.error(request, '文章更新失败,请检查表单内容。')
else:
# LXCGET请求时使用现有文章数据初始化表单
form = PostForm(instance=post)
context = {
'form': form,
'post': post,
'category_info': {
'name': '编辑文章',
'label': '修改创作',
'desc': '修改您的非遗文章内容'
},
'current_path': request.path,
}
return render(request, 'blog/edit_post.html', context)
@login_required
@require_POST
def publish_post(request, post_id):
"""发布文章(将草稿状态改为已发布)"""
# LXC获取文章对象确保作者是当前用户
post = get_object_or_404(Post, id=post_id, author=request.user)
if post.status == 'draft':
# LXC将文章状态从草稿改为已发布
post.status = 'published'
post.save()
messages.success(request, '文章已成功发布!')
else:
messages.warning(request, '文章状态未改变。')
return redirect('user_posts')
@login_required
@require_POST
def delete_post(request, post_id):
"""删除文章"""
# LXC获取文章对象确保作者是当前用户
post = get_object_or_404(Post, id=post_id, author=request.user)
post_title = post.title
# LXC删除文章
post.delete()
messages.success(request, f'文章 "{post_title}" 已删除。')
return redirect('user_posts')
@login_required
@require_POST
def like_post(request, post_id):
"""点赞文章"""
# LXC获取文章对象
post = get_object_or_404(Post, id=post_id)
# LXC实现点赞功能逻辑
if post.likes.filter(id=request.user.id).exists():
# LXC如果已经点赞取消点赞
post.likes.remove(request.user)
liked = False
else:
# LXC如果没有点赞添加点赞
post.likes.add(request.user)
liked = True
# LXC返回JSON响应包含点赞状态和点赞数量
return JsonResponse({
'liked': liked,
'likes_count': post.likes.count()
})
@login_required
@require_POST
def favorite_post(request, post_id):
"""收藏文章"""
# LXC获取文章对象
post = get_object_or_404(Post, id=post_id)
# LXC实现收藏功能逻辑
if post.favorites.filter(id=request.user.id).exists():
# LXC如果已经收藏取消收藏
post.favorites.remove(request.user)
favorited = False
else:
# LXC如果没有收藏添加收藏
post.favorites.add(request.user)
favorited = True
# LXC返回JSON响应包含收藏状态和收藏数量
return JsonResponse({
'favorited': favorited,
'favorites_count': post.favorites.count()
})
@login_required
def edit_post(request, post_id):
"""编辑文章"""
try:
# LXC获取文章对象确保作者是当前用户否则重定向
post = get_object_or_404(Post, id=post_id, author=request.user)
except:
messages.error(request, '您没有权限编辑这篇文章或文章不存在。')
return redirect('user_posts')
if request.method == 'POST':
# LXC使用POST数据和文件数据初始化表单并绑定到现有文章实例
form = PostForm(request.POST, request.FILES, instance=post)
if form.is_valid():
form.save()
messages.success(request, '文章更新成功!')
return redirect('user_posts')
else:
messages.error(request, '文章更新失败,请检查表单内容。')
else:
# LXCGET请求时使用现有文章数据初始化表单
form = PostForm(instance=post)
context = {
'form': form,
'post': post,
'category_info': {
'name': '编辑文章',
'label': '修改创作',
'desc': '修改您的非遗文章内容'
},
'current_path': request.path,
}
return render(request, 'blog/edit_post.html', context)

@ -1,30 +0,0 @@
#zy:comments/admin.py
#zy: 评论(Comment)模型的管理后台配置文件
#zy: 该文件用于配置Django管理后台中评论模型的显示和行为
#zy: 导入Django管理后台模块用于注册模型和管理配置
from django.contrib import admin
#zy: 从当前包(models模块)导入Comment模型
from .models import Comment
#zy: 使用装饰器注册Comment模型到管理后台并指定使用CommentAdmin作为管理类
@admin.register(Comment)
#zy: 定义评论模型的管理配置类继承自admin.ModelAdmin
class CommentAdmin(admin.ModelAdmin):
#zy: 设置在管理后台列表页面显示的字段列
# 将显示:作者(author)、关联文章(post)、创建时间(created_time)
list_display = ['author', 'post', 'created_time']
# zy:设置右侧过滤侧边栏的过滤条件
#zy: 允许用户按照创建时间(created_time)对评论进行筛选
list_filter = ['created_time']
# zy:设置搜索字段,在列表页顶部显示搜索框
# zy:支持按作者用户名(author__username)和评论内容(content)进行搜索
# author__username表示通过外键关系搜索作者的用户名字段
search_fields = ['author__username', 'content']
# zy:设置只读字段,在编辑页面中这些字段将显示为不可编辑状态
# zy:创建时间(created_time)通常应该设为只读,防止用户修改
readonly_fields = ['created_time']

@ -1,7 +0,0 @@
# comments/apps.py
from django.apps import AppConfig
class CommentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'comments'
verbose_name = '评论管理'

@ -1,31 +0,0 @@
# zy:comments/forms.py
#zy:评论(Comment)模型的表单定义文件
#zy: 该文件定义了用于创建和编辑评论的表单类
#zy: 导入Django表单模块提供表单相关的基类和功能
from django import forms
#zy: 从当前包(models模块)导入Comment模型
from .models import Comment
#zy: 定义评论表单类继承自ModelForm自动根据模型字段生成表单
class CommentForm(forms.ModelForm):
# zy:定义表单的元数据类,用于配置表单与模型的关联和行为
class Meta:
# zy:指定表单关联的模型为Comment
model = Comment
#zy: 指定表单中包含的字段,这里只包含评论内容字段
fields = ['content']
#zy: 配置字段的小部件(Widget)属性控制表单字段的HTML渲染
widgets = {
# 为content字段配置Textarea文本域小部件
'content': forms.Textarea(attrs={
'rows': 4, # 设置文本域行数为4行
'placeholder': '请输入您的评论...', # 设置占位符文本
'class': 'comment-textarea' # 设置CSS类名用于样式控制
})
}
# zy:配置字段的标签显示
labels = {
'content': '' # zy:将content字段的标签设为空不显示标签文本
}

@ -1,33 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-01 20:12
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('blog', '0004_delete_comment'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(verbose_name='评论内容')),
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='评论时间')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL, verbose_name='评论者')),
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='blog.post', verbose_name='所属文章')),
],
options={
'verbose_name': '评论',
'verbose_name_plural': '评论',
'ordering': ['-created_time'],
},
),
]

@ -1,39 +0,0 @@
# comments/models.py
# DZQ: 评论模块的数据模型定义文件
from django.db import models
from django.conf import settings
# DZQ: 评论数据模型类 - 用于存储博客文章的评论信息
class Comment(models.Model):
# DZQ: 外键关联到博客文章CASCADE表示文章删除时评论也删除
post = models.ForeignKey(
'blog.Post',
on_delete=models.CASCADE,
verbose_name="所属文章",
related_name="comments" #DZQ: 通过related_name可从文章对象反向查询评论
)
# DZQ: 外键关联到用户模型CASCADE表示用户删除时评论也删除
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name="评论者",
related_name="comments" # DZQ: 通过related_name可从用户对象反向查询评论
)
# DZQ: 评论内容字段使用TextField支持长文本
content = models.TextField(verbose_name="评论内容")
# DZQ: 评论时间字段auto_now_add=True表示创建时自动设置当前时间
created_time = models.DateTimeField(auto_now_add=True, verbose_name="评论时间")
# DZQ: 模型元数据配置类
class Meta:
verbose_name = "评论" # DZQ: 单数形式的显示名称
verbose_name_plural = "评论" # DZQ: 复数形式的显示名称
ordering = ['-created_time'] #DZQ: 默认按评论时间倒序排列
#DZQ: 对象字符串表示方法用于在admin等界面显示
def __str__(self):
return f'{self.author.username} 评论了《{self.post.title}'

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

@ -1,32 +0,0 @@
# zy:comments/urls.py
# zy:评论(Comment)应用的URL路由配置文件
# zy:该文件定义了评论功能的所有URL模式和对应的视图处理
#zy: 导入Django的URL路由配置模块path函数用于定义URL模式
from django.urls import path
#zy: 从当前包(views模块)导入所有视图函数或类
from . import views
# zy:定义应用的命名空间用于URL反向解析时区分不同应用的相同名称URL
#zy: 当在模板中使用{% url 'comments:add_comment' post_id=1 %}时会指向此应用的add_comment URL
app_name = 'comments'
# zy:定义URL模式列表Django会按顺序匹配这些模式
# zy:每个path()函数定义一个URL模式及其对应的视图
urlpatterns = [
# zy:定义添加评论的URL模式
path(
# URL路径模式使用尖括号定义路径参数
# - 'post/' 固定文本部分
# - '<int:post_id>' 路径转换器匹配整数并将其作为post_id参数传递给视图
# - '/comment/' 固定文本部分
'post/<int:post_id>/comment/',
# zy:对应的视图函数,处理添加评论的请求
views.add_comment,
# zy:URL模式的名称用于在模板和代码中进行反向解析
name='add_comment'
),
]

@ -1,38 +0,0 @@
# comments/views.py
# DZQ: 评论模块的视图函数文件处理评论相关的HTTP请求
from django.shortcuts import get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Comment
from .forms import CommentForm
from blog.models import Post
# DZQ: 添加评论视图函数 - 处理用户提交评论的请求
@login_required # DZQ: 登录要求装饰器,确保只有登录用户才能发表评论
def add_comment(request, post_id):
"""添加评论"""
# DZQ: 获取指定ID且状态为已发布的文章如果不存在则返回404错误
post = get_object_or_404(Post, id=post_id, status='published')
# DZQ: 只处理POST请求GET请求直接重定向
if request.method == 'POST':
# DZQ: 实例化评论表单传入POST数据
form = CommentForm(request.POST)
# DZQ: 表单验证通过的处理逻辑
if form.is_valid():
# DZQ: commit=False表示先不保存到数据库允许设置额外字段
comment = form.save(commit=False)
comment.post = post # DZQ: 设置评论关联的文章
comment.author = request.user # DZQ: 设置评论作者为当前登录用户
comment.save() # DZQ: 将评论保存到数据库
# DZQ: 添加成功消息,将在下次请求时显示给用户
messages.success(request, '评论发表成功!')
else:
# DZQ: 表单验证失败,添加错误消息
messages.error(request, '评论发表失败,请检查输入内容。')
# DZQ: 重定向回文章详情页面,无论评论成功与否都返回文章页面
return redirect('detail', post_id=post_id)

Binary file not shown.

@ -1,2 +0,0 @@
# DjangoBlog 项目文档
基于Django框架的博客平台

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,4 @@
# DjangoBlog
<p align="center">
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml"><img

@ -1,22 +0,0 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

@ -1,16 +0,0 @@
"""
ASGI config for mysite project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
application = get_asgi_application()

@ -1,197 +0,0 @@
"""
Django settings for mysite project.
Generated by 'django-admin startproject' using Django 5.2.7.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-rw(t8vmsn=h3=+(4@$ns2tx*mr+$c5tkn(=_vn&*j%#hfdf63w'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog',
'accounts',
'comments',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'mysite.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.media', # 新增:媒体文件处理器
],
},
},
]
WSGI_APPLICATION = 'mysite.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
# 生产环境静态文件收集目录
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# 媒体文件配置
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# 使用自定义用户模型
AUTH_USER_MODEL = 'accounts.CustomUser'
# 登录/登出重定向设置
LOGIN_REDIRECT_URL = 'index' # 登录后重定向到首页
LOGOUT_REDIRECT_URL = 'index' # 登出后重定向到首页
LOGIN_URL = 'login' # 登录页面URL
# 消息框架设置
from django.contrib.messages import constants as messages
MESSAGE_TAGS = {
messages.DEBUG: 'secondary',
messages.INFO: 'info',
messages.SUCCESS: 'success',
messages.WARNING: 'warning',
messages.ERROR: 'danger',
}
# 文件上传设置
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
# 会话设置
SESSION_COOKIE_AGE = 1209600 # 2周单位秒
SESSION_SAVE_EVERY_REQUEST = True
# 安全设置(开发环境)
if DEBUG:
# 开发环境下允许内联CSS用于base.html中的样式
CSP_STYLE_SRC = ["'self'", "'unsafe-inline'"]
# 邮件设置(开发环境)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# 日志设置
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'root': {
'handlers': ['console'],
'level': 'INFO',
},
}
# 邮箱配置在settings.py末尾添加
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.qq.com' # QQ邮箱SMTP服务器
EMAIL_PORT = 587 # QQ邮箱SMTP端口
EMAIL_USE_TLS = True # 启用TLS加密
EMAIL_HOST_USER = 'your-email@qq.com' # 您的QQ邮箱
EMAIL_HOST_PASSWORD = 'your-authorization-code' # QQ邮箱授权码不是密码
DEFAULT_FROM_EMAIL = '金陵非遗 <your-email@qq.com>' # 默认发件人
# 密码重置设置
PASSWORD_RESET_TIMEOUT = 3600 # 密码重置链接有效期1小时

@ -1,15 +0,0 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls')),
path('accounts/', include('accounts.urls')),
path('comments/', include('comments.urls', namespace='comments')),
]
# 开发环境服务媒体文件
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

@ -1,16 +0,0 @@
"""
WSGI config for mysite project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
application = get_wsgi_application()

Binary file not shown.

@ -0,0 +1 @@
Subproject commit 76918f2c7f4bd4db4ad3877be1ee20c256446d21

714
static/css/style.css vendored

@ -1,714 +0,0 @@
/* 南京非遗主题自定义样式 */
:root {
--nj-red: #c62f2f;
--nj-gold: #d4af37;
--nj-brown: #8b4513;
--nj-light-brown: #a0522d;
--primary-color: var(--nj-brown);
--secondary-color: var(--nj-light-brown);
--accent-color: var(--nj-gold);
--text-dark: #2c1810;
--text-light: #5d4037;
--bg-light: #faf3e8;
--bg-white: #fffaf0;
--border-color: #e8d5c4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Noto Serif SC", "Source Han Serif SC", "Microsoft YaHei", serif;
line-height: 1.7;
color: var(--text-dark);
background: linear-gradient(135deg, #faf3e8 0%, #fffaf0 100%);
min-height: 100vh;
}
/* 头部样式 - 中国风设计 */
.header {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
padding: 0;
box-shadow: 0 4px 20px rgba(139, 69, 19, 0.3);
position: relative;
overflow: hidden;
}
.header::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--nj-gold), var(--nj-red), var(--nj-gold));
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.nav-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 30px;
}
.site-brand {
text-align: center;
padding: 20px 0;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
.site-title {
color: #fff;
font-size: 2.5em;
font-weight: 700;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
letter-spacing: 4px;
}
.site-subtitle {
color: var(--nj-gold);
font-size: 1.1em;
margin-top: 8px;
font-style: italic;
}
.main-nav {
display: flex;
justify-content: center;
list-style: none;
padding: 15px 0;
}
.main-nav a {
display: block;
padding: 12px 25px;
color: #fff;
text-decoration: none;
font-size: 16px;
font-weight: 500;
transition: all 0.3s ease;
border-radius: 25px;
margin: 0 8px;
position: relative;
overflow: hidden;
}
.main-nav a::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s;
}
.main-nav a:hover::before {
left: 100%;
}
.main-nav a:hover {
background: rgba(255,255,255,0.1);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* 当前分类高亮样式 */
.current-category {
background: rgba(255, 255, 255, 0.25);
border: 2px solid var(--nj-gold);
font-weight: 600;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
/* 搜索框样式 */
.search-box {
text-align: center;
padding: 15px 0;
border-top: 1px solid rgba(255,255,255,0.2);
}
.search-box form {
display: inline-flex;
max-width: 500px;
width: 100%;
}
.search-box input {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 25px 0 0 25px;
font-size: 14px;
outline: none;
}
.search-box button {
background: var(--nj-gold);
color: var(--text-dark);
border: none;
padding: 12px 25px;
border-radius: 0 25px 25px 0;
cursor: pointer;
font-weight: 600;
transition: background 0.3s;
}
.search-box button:hover {
background: #ffd700;
}
/* 主要内容布局 */
.main-container {
max-width: 1400px;
margin: 40px auto;
padding: 0 30px;
display: grid;
grid-template-columns: 1fr 350px;
gap: 40px;
}
/* 文章内容区域 */
.content {
background: var(--bg-white);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(139, 69, 19, 0.1);
overflow: hidden;
border: 1px solid var(--border-color);
}
.content-header {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 25px 30px;
text-align: center;
}
/* 分类标签样式 */
.category-label {
display: inline-block;
background: rgba(255, 255, 255, 0.2);
padding: 6px 16px;
border-radius: 20px;
font-size: 0.9em;
margin-bottom: 12px;
border: 1px solid var(--nj-gold);
font-weight: 500;
}
.content-header h1 {
font-size: 2em;
margin-bottom: 8px;
text-shadow: 1px 1px 3px rgba(0,0,0,0.3);
}
.content-intro {
font-size: 1.1em;
opacity: 0.95;
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
padding: 0 20px;
}
.posts-list {
padding: 0;
}
/* 文章项样式 */
.post-item {
padding: 35px 30px;
border-bottom: 1px solid var(--border-color);
transition: all 0.3s ease;
position: relative;
}
.post-item::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(to bottom, var(--primary-color), var(--accent-color));
opacity: 0;
transition: opacity 0.3s ease;
}
.post-item:hover {
background: linear-gradient(135deg, #fffaf0, #faf3e8);
transform: translateX(5px);
}
.post-item:hover::before {
opacity: 1;
}
.post-item:last-child {
border-bottom: none;
}
.post-title {
margin-bottom: 20px;
}
.post-title a {
color: var(--text-dark);
text-decoration: none;
font-size: 1.8em;
font-weight: 600;
line-height: 1.3;
transition: color 0.3s ease;
display: block;
}
.post-title a:hover {
color: var(--primary-color);
text-decoration: underline;
text-underline-offset: 3px;
}
.post-meta {
color: var(--text-light);
font-size: 14px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 25px;
flex-wrap: wrap;
}
.post-meta span {
display: flex;
align-items: center;
gap: 6px;
}
.post-meta i {
font-style: normal;
opacity: 0.7;
}
.post-content {
color: var(--text-light);
font-size: 16px;
line-height: 1.8;
margin-bottom: 25px;
text-align: justify;
}
.read-more {
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
font-size: 15px;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
padding: 8px 16px;
border: 2px solid var(--primary-color);
border-radius: 25px;
}
.read-more:hover {
background: var(--primary-color);
color: white;
transform: translateX(5px);
}
.post-footer {
color: var(--text-light);
font-size: 13px;
margin-top: 25px;
padding-top: 20px;
border-top: 1px dashed var(--border-color);
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
background: linear-gradient(135deg, var(--accent-color), var(--secondary-color));
color: white;
padding: 4px 12px;
border-radius: 15px;
font-size: 12px;
font-weight: 500;
}
/* 非遗徽章样式 */
.heritage-badge {
background: linear-gradient(135deg, var(--nj-red), var(--nj-brown));
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
border: 1px solid var(--nj-gold);
}
.heritage-item {
border-left: 3px solid var(--nj-red);
background: linear-gradient(135deg, #fffaf0, #faf3e8);
}
/* 侧边栏样式 */
.sidebar {
display: flex;
flex-direction: column;
gap: 25px;
}
.sidebar-widget {
background: var(--bg-white);
border-radius: 15px;
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.1);
padding: 25px;
border: 1px solid var(--border-color);
}
.widget-title {
color: var(--primary-color);
font-size: 1.3em;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid var(--accent-color);
display: flex;
align-items: center;
gap: 10px;
}
.widget-title::before {
content: "◆";
color: var(--secondary-color);
}
.views-list {
list-style: none;
}
.views-list li {
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
color: var(--text-light);
font-size: 14px;
transition: all 0.3s ease;
display: flex;
justify-content: space-between;
align-items: center;
}
.views-list li:hover {
color: var(--primary-color);
transform: translateX(5px);
}
.views-list li:last-child {
border-bottom: none;
}
.view-count {
background: var(--accent-color);
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
}
/* 分类标签云 */
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.cloud-tag {
background: linear-gradient(135deg, #e8d5c4, #d7ccc8);
color: var(--text-light);
padding: 6px 12px;
border-radius: 15px;
font-size: 12px;
text-decoration: none;
transition: all 0.3s ease;
}
.cloud-tag:hover {
background: linear-gradient(135deg, var(--accent-color), var(--secondary-color));
color: white;
transform: translateY(-2px);
}
/* 文章详情页特定样式 */
.post-detail {
padding: 40px;
margin: 0;
}
.post-header {
text-align: center;
margin-bottom: 40px;
border-bottom: 2px solid var(--border-color);
padding-bottom: 30px;
}
.post-detail .post-title {
font-size: 2.2em;
color: var(--text-dark);
margin-bottom: 20px;
line-height: 1.3;
}
.post-detail .post-meta {
color: var(--text-light);
font-size: 14px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 25px;
flex-wrap: wrap;
justify-content: center;
}
.post-image img {
max-width: 100%;
height: auto;
border-radius: 10px;
margin-top: 20px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.post-detail .post-content {
font-size: 16px;
line-height: 1.8;
color: var(--text-light);
margin-bottom: 30px;
}
.post-detail .post-content p {
margin-bottom: 20px;
text-align: justify;
}
.post-detail .post-footer {
color: var(--text-light);
font-size: 13px;
margin-top: 25px;
padding-top: 20px;
border-top: 1px dashed var(--border-color);
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
}
/* 评论区域样式 */
.comments-section {
margin-top: 50px;
padding-top: 30px;
border-top: 2px solid var(--border-color);
}
.comment-form {
margin-bottom: 30px;
}
.comment-form textarea {
width: 100%;
padding: 15px;
border: 1px solid var(--border-color);
border-radius: 8px;
resize: vertical;
min-height: 100px;
font-family: inherit;
font-size: 14px;
transition: border-color 0.3s;
}
.comment-form textarea:focus {
outline: none;
border-color: var(--primary-color);
}
.submit-btn {
background: var(--primary-color);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
cursor: pointer;
margin-top: 15px;
font-weight: 600;
transition: all 0.3s;
}
.submit-btn:hover {
background: var(--secondary-color);
transform: translateY(-2px);
}
.comment-item {
background: var(--bg-light);
padding: 20px;
border-radius: 8px;
margin-bottom: 15px;
border-left: 4px solid var(--accent-color);
transition: transform 0.3s;
}
.comment-item:hover {
transform: translateX(5px);
}
.comment-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 14px;
}
.comment-author {
font-weight: 600;
color: var(--primary-color);
}
.comment-time {
color: var(--text-light);
}
.comment-content {
line-height: 1.6;
color: var(--text-dark);
}
.no-comments {
text-align: center;
color: var(--text-light);
padding: 40px;
font-style: italic;
background: var(--bg-light);
border-radius: 8px;
}
.login-prompt {
text-align: center;
padding: 20px;
background: var(--bg-light);
border-radius: 8px;
margin-bottom: 20px;
}
.login-prompt a {
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
}
.login-prompt a:hover {
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.main-container {
grid-template-columns: 1fr;
gap: 25px;
}
.sidebar {
width: 100%;
}
}
@media (max-width: 768px) {
.nav-container {
padding: 0 15px;
}
.main-nav {
flex-wrap: wrap;
}
.main-nav a {
padding: 10px 15px;
font-size: 14px;
margin: 4px;
}
.site-title {
font-size: 2em;
}
.main-container {
padding: 0 15px;
margin: 20px auto;
}
.post-item {
padding: 25px 20px;
}
.post-title a {
font-size: 1.5em;
}
.content-header {
padding: 20px;
}
.content-header h1 {
font-size: 1.6em;
}
.post-detail {
padding: 20px;
}
.post-detail .post-title {
font-size: 1.8em;
}
.post-detail .post-meta {
gap: 15px;
font-size: 12px;
}
.comment-header {
flex-direction: column;
gap: 5px;
}
}
/* 页脚样式 */
.footer {
background: linear-gradient(135deg, var(--primary-color), var(--text-dark));
color: white;
text-align: center;
padding: 30px 20px;
margin-top: 60px;
}
.footer p {
opacity: 0.8;
font-size: 14px;
}

@ -1,887 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
{% if category_info.name == '非遗传承' %}
金陵非遗 - 南京非物质文化遗产传承与创新
{% else %}
{{ category_info.name }} - 金陵非遗
{% endif %}
</title>
<style>
/* 南京非遗主题自定义样式 */
:root {
--nj-red: #c62f2f;
--nj-gold: #d4af37;
--nj-brown: #8b4513;
--nj-light-brown: #a0522d;
--primary-color: var(--nj-brown);
--secondary-color: var(--nj-light-brown);
--accent-color: var(--nj-gold);
--text-dark: #2c1810;
--text-light: #5d4037;
--bg-light: #faf3e8;
--bg-white: #fffaf0;
--border-color: #e8d5c4;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Noto Serif SC", "Source Han Serif SC", "Microsoft YaHei", serif;
line-height: 1.7;
color: var(--text-dark);
background: linear-gradient(135deg, #faf3e8 0%, #fffaf0 100%);
min-height: 100vh;
}
/* 头部样式 - 中国风设计 */
.header {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
padding: 0;
box-shadow: 0 4px 20px rgba(139, 69, 19, 0.3);
position: relative;
overflow: visible; /* 修复改为visible */
min-height: 180px; /* 确保有足够高度 */
}
.header::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--nj-gold), var(--nj-red), var(--nj-gold));
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.nav-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 30px;
position: relative; /* 确保相对定位 */
min-height: 160px; /* 增加最小高度 */
}
.site-brand {
text-align: center;
padding: 20px 0;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
.site-title {
color: #fff;
font-size: 2.5em;
font-weight: 700;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
letter-spacing: 4px;
}
.site-subtitle {
color: var(--nj-gold);
font-size: 1.1em;
margin-top: 8px;
font-style: italic;
}
.main-nav {
display: flex;
justify-content: center;
list-style: none;
padding: 15px 0;
}
.main-nav a {
display: block;
padding: 12px 25px;
color: #fff;
text-decoration: none;
font-size: 16px;
font-weight: 500;
transition: all 0.3s ease;
border-radius: 25px;
margin: 0 8px;
position: relative;
overflow: hidden;
}
.main-nav a::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s;
}
.main-nav a:hover::before {
left: 100%;
}
.main-nav a:hover {
background: rgba(255,255,255,0.1);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* 当前分类高亮样式 */
.current-category {
background: rgba(255, 255, 255, 0.25);
border: 2px solid var(--nj-gold);
font-weight: 600;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
/* 搜索框样式 */
.search-box {
text-align: center;
padding: 15px 0;
border-top: 1px solid rgba(255,255,255,0.2);
}
.search-box form {
display: inline-flex;
max-width: 500px;
width: 100%;
}
.search-box input {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 25px 0 0 25px;
font-size: 14px;
outline: none;
}
.search-box button {
background: var(--nj-gold);
color: var(--text-dark);
border: none;
padding: 12px 25px;
border-radius: 0 25px 25px 0;
cursor: pointer;
font-weight: 600;
transition: background 0.3s;
}
.search-box button:hover {
background: #ffd700;
}
/* 用户菜单样式 - 修复版 */
.user-menu {
position: absolute;
top: 20px;
right: 30px;
z-index: 9999;
}
.user-dropdown {
position: relative;
display: inline-block;
}
.user-btn {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 10px 18px;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
font-family: inherit;
}
.user-btn:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--nj-gold);
}
.user-avatar-placeholder {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, var(--nj-gold), var(--nj-red));
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 16px;
border: 2px solid var(--nj-gold);
}
.username {
font-size: 14px;
font-weight: 500;
}
.dropdown-arrow {
font-size: 10px;
transition: transform 0.3s ease;
margin-left: 5px;
}
.user-dropdown:hover .dropdown-arrow {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background: var(--bg-white);
min-width: 220px;
border-radius: 12px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
padding: 10px 0;
margin-top: 8px;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
z-index: 10001;
border: 1px solid var(--border-color);
}
.user-dropdown:hover .dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.dropdown-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
color: var(--text-dark);
text-decoration: none;
font-size: 14px;
transition: all 0.3s ease;
border: none;
background: none;
width: 100%;
text-align: left;
font-family: inherit;
}
.dropdown-item:hover {
background: var(--bg-light);
color: var(--primary-color);
}
.dropdown-item.logout {
color: var(--nj-red);
}
.dropdown-item.logout:hover {
background: rgba(198, 47, 47, 0.1);
}
.item-icon {
font-size: 16px;
width: 20px;
text-align: center;
}
.dropdown-divider {
height: 1px;
background: var(--border-color);
margin: 8px 15px;
}
/* 未登录状态样式 */
.auth-links {
display: flex;
gap: 15px;
align-items: center;
}
.auth-link {
color: white;
text-decoration: none;
font-size: 14px;
font-weight: 500;
padding: 10px 20px;
border-radius: 20px;
transition: all 0.3s ease;
font-family: inherit;
}
.auth-link:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
.auth-link.register {
background: var(--nj-gold);
color: var(--text-dark);
font-weight: 600;
}
.auth-link.register:hover {
background: #ffd700;
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.3);
}
/* 主要内容布局 */
.main-container {
max-width: 1400px;
margin: 40px auto;
padding: 0 30px;
display: grid;
grid-template-columns: 1fr 350px;
gap: 40px;
}
/* 文章内容区域 */
.content {
background: var(--bg-white);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(139, 69, 19, 0.1);
overflow: hidden;
border: 1px solid var(--border-color);
}
.content-header {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 25px 30px;
text-align: center;
}
/* 分类标签样式 */
.category-label {
display: inline-block;
background: rgba(255, 255, 255, 0.2);
padding: 6px 16px;
border-radius: 20px;
font-size: 0.9em;
margin-bottom: 12px;
border: 1px solid var(--nj-gold);
font-weight: 500;
}
.content-header h1 {
font-size: 2em;
margin-bottom: 8px;
text-shadow: 1px 1px 3px rgba(0,0,0,0.3);
}
.content-intro {
font-size: 1.1em;
opacity: 0.95;
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
padding: 0 20px;
}
.posts-list {
padding: 0;
}
/* 文章项样式 */
.post-item {
padding: 35px 30px;
border-bottom: 1px solid var(--border-color);
transition: all 0.3s ease;
position: relative;
}
.post-item::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(to bottom, var(--primary-color), var(--accent-color));
opacity: 0;
transition: opacity 0.3s ease;
}
.post-item:hover {
background: linear-gradient(135deg, #fffaf0, #faf3e8);
transform: translateX(5px);
}
.post-item:hover::before {
opacity: 1;
}
.post-item:last-child {
border-bottom: none;
}
.post-title {
margin-bottom: 20px;
}
.post-title a {
color: var(--text-dark);
text-decoration: none;
font-size: 1.8em;
font-weight: 600;
line-height: 1.3;
transition: color 0.3s ease;
display: block;
}
.post-title a:hover {
color: var(--primary-color);
text-decoration: underline;
text-underline-offset: 3px;
}
.post-meta {
color: var(--text-light);
font-size: 14px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 25px;
flex-wrap: wrap;
}
.post-meta span {
display: flex;
align-items: center;
gap: 6px;
}
.post-meta i {
font-style: normal;
opacity: 0.7;
}
.post-content {
color: var(--text-light);
font-size: 16px;
line-height: 1.8;
margin-bottom: 25px;
text-align: justify;
}
.read-more {
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
font-size: 15px;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
padding: 8px 16px;
border: 2px solid var(--primary-color);
border-radius: 25px;
}
.read-more:hover {
background: var(--primary-color);
color: white;
transform: translateX(5px);
}
.post-footer {
color: var(--text-light);
font-size: 13px;
margin-top: 25px;
padding-top: 20px;
border-top: 1px dashed var(--border-color);
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
background: linear-gradient(135deg, var(--accent-color), var(--secondary-color));
color: white;
padding: 4px 12px;
border-radius: 15px;
font-size: 12px;
font-weight: 500;
}
/* 非遗徽章样式 */
.heritage-badge {
background: linear-gradient(135deg, var(--nj-red), var(--nj-brown));
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
border: 1px solid var(--nj-gold);
}
.heritage-item {
border-left: 3px solid var(--nj-red);
background: linear-gradient(135deg, #fffaf0, #faf3e8);
}
/* 侧边栏样式 */
.sidebar {
display: flex;
flex-direction: column;
gap: 25px;
}
.sidebar-widget {
background: var(--bg-white);
border-radius: 15px;
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.1);
padding: 25px;
border: 1px solid var(--border-color);
}
.widget-title {
color: var(--primary-color);
font-size: 1.3em;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid var(--accent-color);
display: flex;
align-items: center;
gap: 10px;
}
.widget-title::before {
content: "◆";
color: var(--secondary-color);
}
.views-list {
list-style: none;
}
.views-list li {
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
color: var(--text-light);
font-size: 14px;
transition: all 0.3s ease;
display: flex;
justify-content: space-between;
align-items: center;
}
.views-list li:hover {
color: var(--primary-color);
transform: translateX(5px);
}
.views-list li:last-child {
border-bottom: none;
}
.view-count {
background: var(--accent-color);
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
}
/* 分类标签云 */
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.cloud-tag {
background: linear-gradient(135deg, #e8d5c4, #d7ccc8);
color: var(--text-light);
padding: 6px 12px;
border-radius: 15px;
font-size: 12px;
text-decoration: none;
transition: all 0.3s ease;
}
.cloud-tag:hover {
background: linear-gradient(135deg, var(--accent-color), var(--secondary-color));
color: white;
transform: translateY(-2px);
}
/* 响应式设计 */
@media (max-width: 1024px) {
.main-container {
grid-template-columns: 1fr;
gap: 25px;
}
.sidebar {
width: 100%;
}
}
@media (max-width: 768px) {
.nav-container {
padding: 0 15px;
}
.main-nav {
flex-wrap: wrap;
}
.main-nav a {
padding: 10px 15px;
font-size: 14px;
margin: 4px;
}
.site-title {
font-size: 2em;
}
.main-container {
padding: 0 15px;
margin: 20px auto;
}
.post-item {
padding: 25px 20px;
}
.post-title a {
font-size: 1.5em;
}
.content-header {
padding: 20px;
}
.content-header h1 {
font-size: 1.6em;
}
/* 移动端用户菜单调整 */
.user-menu {
position: static;
margin-top: 15px;
text-align: center;
}
.user-btn {
justify-content: center;
width: 100%;
max-width: 200px;
margin: 0 auto;
}
.dropdown-menu {
right: 50%;
transform: translateX(50%) translateY(-10px);
min-width: 180px;
}
.user-dropdown:hover .dropdown-menu {
transform: translateX(50%) translateY(0);
}
.auth-links {
justify-content: center;
flex-wrap: wrap;
}
}
/* 页脚样式 */
.footer {
background: linear-gradient(135deg, var(--primary-color), var(--text-dark));
color: white;
text-align: center;
padding: 30px 20px;
margin-top: 60px;
}
.footer p {
opacity: 0.8;
font-size: 14px;
}
.logout-form {
margin: 0;
padding: 0;
}
.logout-form button {
width: 100%;
text-align: left;
background: none;
border: none;
cursor: pointer;
font-family: inherit;
font-size: inherit;
}
</style>
</head>
<body>
<header class="header">
<div class="nav-container">
<div class="site-brand">
<h1 class="site-title">金陵非遗</h1>
<div class="site-subtitle">南京非物质文化遗产传承与创新</div>
</div>
<nav class="main-nav">
<a href="{% url 'index' %}" class="{% if current_path == '/' %}current-category{% endif %}">首页</a>
<a href="{% url 'category' 1 %}" class="{% if current_category_id == 1 %}current-category{% endif %}">巧夺天工·工艺</a>
<a href="{% url 'category' 2 %}" class="{% if current_category_id == 2 %}current-category{% endif %}">梨园雅韵·表演</a>
<a href="{% url 'category' 3 %}" class="{% if current_category_id == 3 %}current-category{% endif %}">人间烟火·民俗</a>
<a href="{% url 'category' 4 %}" class="{% if current_category_id == 4 %}current-category{% endif %}">口传心授·文学</a>
<a href="{% url 'category' 5 %}" class="{% if current_category_id == 5 %}current-category{% endif %}">匠心传承·人物</a>
</nav>
<!-- 搜索框 -->
<div class="search-box">
<form action="{% url 'search' %}" method="get">
<input type="text" name="q" placeholder="搜索非遗文章..." value="{{ search_query|default:'' }}">
<button type="submit">搜索</button>
</form>
</div>
<!-- 用户菜单 -->
<div class="user-menu">
{% if user.is_authenticated %}
<div class="user-dropdown">
<button class="user-btn">
{% if user.avatar %}
<img src="{{ user.avatar.url }}" alt="头像" class="user-avatar">
{% else %}
<div class="user-avatar-placeholder">
{{ user.username|first|upper }}
</div>
{% endif %}
<span class="username">{{ user.username }}</span>
<span class="dropdown-arrow"></span>
</button>
<div class="dropdown-menu">
<a href="{% url 'profile' %}" class="dropdown-item">
<span class="item-icon">👤</span>
个人资料
</a>
<a href="{% url 'profile_edit' %}" class="dropdown-item">
<span class="item-icon">⚙️</span>
编辑资料
</a>
<a href="{% url 'create_post' %}" class="dropdown-item">
<span class="item-icon">✍️</span>
发表文章
</a>
<a href="{% url 'user_posts' %}" class="dropdown-item">
<span class="item-icon">📚</span>
我的文章
</a>
<!-- 修改退出登录链接为表单 -->
<div class="dropdown-divider"></div>
<form method="post" action="{% url 'logout' %}" class="logout-form">
{% csrf_token %}
<button type="submit" class="dropdown-item logout">
<span class="item-icon">🚪</span>
退出登录
</button>
</form>
</div>
</div>
{% else %}
<div class="auth-links">
<a href="{% url 'login' %}" class="auth-link">登录</a>
<a href="{% url 'register' %}" class="auth-link register">注册</a>
</div>
{% endif %}
</div>
</div>
</header>
<div class="main-container">
<main class="content">
<div class="content-header">
{% if category_info.label %}
<div class="category-label">{{ category_info.label }}</div>
{% endif %}
<h1>{{ category_info.name }}</h1>
<div class="content-intro">{{ category_info.desc }}</div>
</div>
<div class="posts-list">
{% block content %}
{% endblock %}
</div>
</main>
<aside class="sidebar">
<div class="sidebar-widget">
<h3 class="widget-title">热门文章</h3>
<ul class="views-list">
{% for post in hot_posts %}
<li>
<a href="{% url 'detail' post.id %}" style="color: inherit; text-decoration: none;">
{{ post.title }}
</a>
<span class="view-count">{{ post.views }}</span>
</li>
{% empty %}
<li>暂无热门文章</li>
{% endfor %}
</ul>
</div>
<div class="sidebar-widget">
<h3 class="widget-title">非遗项目</h3>
<div class="tag-cloud">
<a href="{% url 'category' 1 %}" class="cloud-tag">南京云锦</a>
<a href="{% url 'category' 1 %}" class="cloud-tag">南京白局</a>
<a href="{% url 'category' 1 %}" class="cloud-tag">南京评话</a>
<a href="{% url 'category' 1 %}" class="cloud-tag">秦淮灯会</a>
<a href="{% url 'category' 1 %}" class="cloud-tag">南京板鸭</a>
<a href="{% url 'category' 1 %}" class="cloud-tag">南京白话</a>
</div>
</div>
</aside>
</div>
<footer class="footer">
<p>© 2025 金陵非遗文化传承中心 | 传承非物质文化遗产,弘扬中华优秀传统文化</p>
</footer>
</body>
</html>

@ -1,283 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<article class="post-detail heritage-item">
<header class="post-header">
<h1 class="post-title">{{ post.title }}</h1>
<div class="post-meta">
<span><i>👁️</i> {{ post.views }} 次阅读</span>
<span><i>💬</i> {{ post.comments.count }} 条评论</span>
<span><i>📅</i> {{ post.created_time|date:"Y-m-d" }}</span>
<span><i>🏷️</i> {{ post.category.name }}</span>
<span><i>✍️</i> {{ post.author.username }}</span>
</div>
{% if post.featured_image %}
<div class="post-image">
<img src="{{ post.featured_image.url }}" alt="{{ post.title }}" loading="lazy">
</div>
{% endif %}
<!-- 非遗徽章 -->
<div class="post-tags" style="margin-top: 15px;">
<span class="heritage-badge">非遗传承</span>
{% for tag in post.primary_tags.all %}
<span class="tag">{{ tag.name }}</span>
{% endfor %}
</div>
</header>
<div class="post-content">
{{ post.content|linebreaks }}
</div>
<div class="post-footer">
<div class="post-tags">
<span class="heritage-badge">{{ post.category.name }}</span>
{% for tag in post.primary_tags.all %}
<span class="tag">{{ tag.name }}</span>
{% endfor %}
{% for tag in post.secondary_tags.all %}
<span class="tag">#{{ tag.name }}</span>
{% endfor %}
</div>
</div>
</article>
<!-- 评论区域 -->
<section class="comments-section">
<h3 style="color: var(--primary-color); margin-bottom: 25px; font-size: 1.5em;">
<i>💬</i> 评论 ({{ comments.count }})
</h3>
<!-- 评论表单 -->
{% if user.is_authenticated %}
<form method="post" action="{% url 'comments:add_comment' post.id %}" class="comment-form">
{% csrf_token %}
<div class="form-group">
{{ comment_form.content }}
</div>
<button type="submit" class="submit-btn">发表评论</button>
</form>
{% else %}
<div class="login-prompt">
<p><a href="{% url 'login' %}?next={{ request.path }}">登录</a> 后发表评论,或 <a href="{% url 'register' %}?next={{ request.path }}">注册</a> 账号</p>
</div>
{% endif %}
<!-- 评论列表 -->
<div class="comments-list">
{% for comment in comments %}
<div class="comment-item">
<div class="comment-header">
<span class="comment-author">{{ comment.author.username }}</span>
<span class="comment-time">{{ comment.created_time|date:"Y-m-d H:i" }}</span>
</div>
<div class="comment-content">
{{ comment.content|linebreaks }}
</div>
</div>
{% empty %}
<div class="no-comments">
<p>暂无评论,快来抢沙发吧~</p>
</div>
{% endfor %}
</div>
</section>
<style>
.post-detail {
padding: 40px;
margin: 0;
}
.post-header {
text-align: center;
margin-bottom: 40px;
border-bottom: 2px solid var(--border-color);
padding-bottom: 30px;
}
.post-title {
font-size: 2.2em;
color: var(--text-dark);
margin-bottom: 20px;
line-height: 1.3;
}
.post-meta {
color: var(--text-light);
font-size: 14px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 25px;
flex-wrap: wrap;
justify-content: center;
}
.post-meta span {
display: flex;
align-items: center;
gap: 6px;
}
.post-image img {
max-width: 100%;
height: auto;
border-radius: 10px;
margin-top: 20px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.post-content {
font-size: 16px;
line-height: 1.8;
color: var(--text-light);
margin-bottom: 30px;
}
.post-content p {
margin-bottom: 20px;
text-align: justify;
}
.post-footer {
color: var(--text-light);
font-size: 13px;
margin-top: 25px;
padding-top: 20px;
border-top: 1px dashed var(--border-color);
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
}
.comments-section {
margin-top: 50px;
padding-top: 30px;
border-top: 2px solid var(--border-color);
}
.comment-form {
margin-bottom: 30px;
}
.comment-form textarea {
width: 100%;
padding: 15px;
border: 1px solid var(--border-color);
border-radius: 8px;
resize: vertical;
min-height: 100px;
font-family: inherit;
font-size: 14px;
transition: border-color 0.3s;
}
.comment-form textarea:focus {
outline: none;
border-color: var(--primary-color);
}
.submit-btn {
background: var(--primary-color);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
cursor: pointer;
margin-top: 15px;
font-weight: 600;
transition: all 0.3s;
}
.submit-btn:hover {
background: var(--secondary-color);
transform: translateY(-2px);
}
.comment-item {
background: var(--bg-light);
padding: 20px;
border-radius: 8px;
margin-bottom: 15px;
border-left: 4px solid var(--accent-color);
transition: transform 0.3s;
}
.comment-item:hover {
transform: translateX(5px);
}
.comment-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 14px;
}
.comment-author {
font-weight: 600;
color: var(--primary-color);
}
.comment-time {
color: var(--text-light);
}
.comment-content {
line-height: 1.6;
color: var(--text-dark);
}
.no-comments {
text-align: center;
color: var(--text-light);
padding: 40px;
font-style: italic;
background: var(--bg-light);
border-radius: 8px;
}
.login-prompt {
text-align: center;
padding: 20px;
background: var(--bg-light);
border-radius: 8px;
margin-bottom: 20px;
}
.login-prompt a {
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
}
.login-prompt a:hover {
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 768px) {
.post-detail {
padding: 20px;
}
.post-title {
font-size: 1.8em;
}
.post-meta {
gap: 15px;
font-size: 12px;
}
.comment-header {
flex-direction: column;
gap: 5px;
}
}
</style>
{% endblock %}

@ -1,70 +0,0 @@
{% extends 'base.html' %}
{% block content %}
{% for post in post_list %}
<article class="post-item">
<div class="post-title">
<h2><a href="{% url 'detail' post.id %}">{{ post.title }}</a></h2>
</div>
<div class="post-meta">
<span><i>👁️</i> {{ post.views }} 次阅读</span>
<span><i>💬</i> {{ post.comments.count }} 条评论</span>
<span><i>📅</i> {{ post.created_time|date:"Y-m-d" }}</span>
<!-- 显示文章所属分类 -->
<span><i>🏷️</i> {{ post.category.name }}</span>
</div>
<div class="post-content">
{{ post.excerpt|default:post.content|truncatewords:35 }}
</div>
<a href="{% url 'detail' post.id %}" class="read-more">
阅读全文 →
</a>
<div class="post-footer">
<div class="post-tags">
<span class="tag">{{ post.category.name }}</span>
{% for tag in post.primary_tags.all %}
<span class="tag">{{ tag.name }}</span>
{% endfor %}
{% for tag in post.secondary_tags.all|slice:":3" %}
<span class="tag">#{{ tag.name }}</span>
{% endfor %}
</div>
<div class="post-author">
作者:{{ post.author.username }}
</div>
</div>
</article>
{% empty %}
<div class="post-item" style="text-align: center; padding: 60px 30px;">
<h3 style="color: var(--text-light); margin-bottom: 15px;">
{% if current_category_id %}
该分类下暂无文章
{% else %}
暂无文章
{% endif %}
</h3>
<p style="color: var(--text-light);">欢迎关注南京非物质文化遗产,精彩内容即将呈现...</p>
</div>
{% endfor %}
<!-- 分页功能(可选) -->
{% if post_list.has_other_pages %}
<div class="pagination" style="padding: 30px; text-align: center;">
{% if post_list.has_previous %}
<a href="?page={{ post_list.previous_page_number }}" class="read-more">上一页</a>
{% endif %}
<span style="margin: 0 20px; color: var(--text-light);">
第 {{ post_list.number }} 页,共 {{ post_list.paginator.num_pages }} 页
</span>
{% if post_list.has_next %}
<a href="?page={{ post_list.next_page_number }}" class="read-more">下一页</a>
{% endif %}
</div>
{% endif %}
{% endblock %}
Loading…
Cancel
Save