Compare commits

...

31 Commits

Author SHA1 Message Date
丁紫晴 0d78de44ac 博客视频.mp4
1 month ago
unknown 618cf56bae feat: 最新版代码注释
1 month ago
丁紫晴 7064685492 实践考评-开源软件大作业项目的自评报告.xlsx
1 month ago
丁紫晴 e6eb8f32f1 开源软件泛读、标注和维护报告文档.docx
1 month ago
p9l5ps7xj b09029de28 Delete 'doc/开源软件泛读、标注和维护报告文档.docx'
1 month ago
丁紫晴 f6cd05a0d1 软件需求规格说明书.docx
2 months ago
丁紫晴 5c04c5b73e 开源软件泛读、标注和维护报告文档.docx
2 months ago
unknown 6e99d6dc3b Merge branch 'master' of https://bdgit.educoder.net/ple6vqxn8/DjangoBlog
2 months ago
unknown 70bb5b4cf1 feat: 新增用户相关功能
2 months ago
ple6vqxn8 fce63f91a4 Delete 'doc/金陵非遗博客系统泛读报告.docx'
2 months ago
丁紫晴 c2fa14933d 金陵非遗博客系统报告.docx
2 months ago
丁紫晴 17a9b6de47 开源软件的质量分析报告文档.docx
2 months ago
丁紫晴 1206e423ae 编码规范.docx
2 months ago
unknown 5d2ebbb8ea feat: 代码精读与注释
2 months ago
unknown 54cadcf84d feat: 完成评论功能独立app拆分
2 months ago
unknown 42751f7223 feat: 实现登录评论功能
2 months ago
unknown 38309147d3 Merge branch 'master' of https://bdgit.educoder.net/ple6vqxn8/DjangoBlog
2 months ago
unknown b19624aa92 docs: 添加项目依赖文件 requirements.txt
2 months ago
unknown af8f327a37 feat: 完整DjangoBlog项目提交
2 months ago
丁紫晴 212880fb51 金陵非遗博客系统泛读报告.docx
3 months ago
丁紫晴 83a24e4911 Merge branch 'master' of https://bdgit.educoder.net/ple6vqxn8/DjangoBlog
3 months ago
丁紫晴 b8833cda10 数据模型设计文档.docx
3 months ago
赵烨 cec8f3966f Merge branch 'master' of https://bdgit.educoder.net/ple6vqxn8/DjangoBlog
3 months ago
赵烨 e8bed1544a feat: 添加金陵书盟介绍文档
3 months ago
李新晨 8b889e700e docs: 同步文档到 master
3 months ago
丁紫晴 b020032691 文档模板-软件界面设计说明书模板
3 months ago
丁紫晴 8b1a2ce8f2 用户界面分析文档
3 months ago
unknown 73cc742716 Merge develop into master: complete Django blog project integration
3 months ago
unknown fa8314391f Resolve merge conflict in src directory
3 months ago
unknown fca17f9106 创建项目文件夹结构:src和doc
4 months ago
dzq302 43f7366174 初始提交:创建项目结构,添加DjangoBlog源代码
4 months ago

BIN
.gitignore vendored

Binary file not shown.

@ -0,0 +1,21 @@
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')}),
)

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

@ -0,0 +1,62 @@
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

@ -0,0 +1,39 @@
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/*'}),
}

@ -0,0 +1,47 @@
# 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()),
],
),
]

@ -0,0 +1,30 @@
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

@ -0,0 +1,326 @@
{% 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 %}

@ -0,0 +1,244 @@
{% 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 %}

@ -0,0 +1,507 @@
{% 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 %}

@ -0,0 +1,515 @@
{% 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 %}

@ -0,0 +1,110 @@
{% 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 %}

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

@ -0,0 +1,17 @@
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'),
]

@ -0,0 +1,218 @@
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,44 +1,59 @@
from django.contrib import admin
from .models import Category, PrimaryTag, SecondaryTag, Post, Comment
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')
}),
)
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ['author', 'post', 'created_time']
list_filter = ['created_time']
search_fields = ['author', 'content']
)

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

@ -0,0 +1,62 @@
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()

@ -3,25 +3,27 @@ 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': '感受南京传统表演艺术的独特韵味与舞台魅力'},
@ -30,15 +32,18 @@ def category_view(request, category_id):
5: {'name': '传承人物', 'label': '匠心传承·人物', 'desc': '认识南京非物质文化遗产的传承人与守护者'},
}
# LXC: 根据分类ID获取对应的分类信息如果不存在则使用默认值
category_info = category_map.get(category_id, {'name': '非遗传承', 'label': '', 'desc': '探索南京非物质文化遗产的独特魅力'})
# 根据分类ID获取对应的文章
# 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)

@ -0,0 +1,21 @@
# 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='评论者'),
),
]

@ -0,0 +1,16 @@
# 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,35 +1,49 @@
from django.db import models
from django.contrib.auth.models import User
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', '主题维度'),
@ -38,56 +52,64 @@ class SecondaryTag(models.Model):
('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="文章摘要")
author = models.ForeignKey(User, on_delete=models.CASCADE, 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):
return self.title
class Comment(models.Model):
"""文章评论"""
post = models.ForeignKey(Post, on_delete=models.CASCADE, verbose_name="所属文章")
author = models.CharField(max_length=100, verbose_name="评论者")
content = models.TextField(verbose_name="评论内容")
created_time = models.DateTimeField(auto_now_add=True, verbose_name="评论时间")
class Meta:
verbose_name = "评论"
verbose_name_plural = "评论"
ordering = ['-created_time']
def __str__(self):
return f'{self.author} 评论了《{self.post.title}'
# HJH定义对象的字符串表示形式返回文章标题
return self.title

@ -0,0 +1,462 @@
{% 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,20 +1,275 @@
<!-- blog/templates/blog/detail.html -->
{% extends 'base.html' %}
{% block content %}
<article class="post-detail">
<h1>{{ post.title }}</h1>
<div class="post-meta">
<span>{{ post.comment_set.count }} 评论</span> | <span>{{ post.views }} views</span>
<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-footer">
发布于 {{ post.category.name }} 并标记为
{% for tag in post.tags.all %}
{{ tag.name }}{% if not forloop.last %}, {% endif %}
<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 %}
由 {{ post.author.username }} 在 {{ post.created_time|date:"Y-m-d" }}
</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 %}

@ -0,0 +1,561 @@
{% 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 %}

@ -0,0 +1,375 @@
{% 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 %}

@ -5,4 +5,17 @@ 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,13 +1,39 @@
# blog/views.py
from django.shortcuts import render, get_object_or_404
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):
"""首页视图"""
post_list = Post.objects.filter(status='published').order_by('-created_time')[:10]
"""首页视图 - 显示最新发布的文章"""
# 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': '',
@ -16,61 +42,357 @@ def index(request):
'current_path': request.path,
'current_category_id': None
}
# LXC渲染首页模板并返回响应
return render(request, 'blog/index.html', context)
def category_view(request, category_id):
"""分类页面视图"""
print(f"=== 调试信息: 访问分类ID {category_id} ===")
"""分类页面视图 - 显示指定分类的文章"""
# LXC根据分类ID获取分类对象如果不存在则返回404错误
category = get_object_or_404(Category, id=category_id)
print(f"数据库中的分类名称: '{category.name}'")
# 根据实际分类名称进行映射
if "工艺" in category.name:
label = '巧夺天工·工艺'
desc = '探索南京传统手工艺的精湛技艺与匠心传承'
print("映射到: 工艺分类")
elif "表演" in category.name or "梨园" in category.name:
label = '梨园雅韵·表演'
desc = '感受南京传统表演艺术的独特韵味与舞台魅力'
print("映射到: 表演分类")
elif "民俗" in category.name or "烟火" in category.name:
label = '人间烟火·民俗'
desc = '体验南京丰富多彩的民俗活动与民间传统'
print("映射到: 民俗分类")
elif "文学" in category.name or "口传" in category.name:
label = '口传心授·文学'
desc = '领略南京口传文学的语言艺术与文化内涵'
print("映射到: 文学分类")
elif "人物" in category.name or "匠心" in category.name:
label = '匠心传承·人物'
desc = '认识南京非物质文化遗产的传承人与守护者'
print("映射到: 人物分类")
else:
label = category.name
desc = f'探索南京{category.name}的独特魅力'
print(f"未匹配,使用默认: {category.name}")
posts = Post.objects.filter(category=category, status='published').order_by('-created_time')
# 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': label, # 使用映射后的显示标签
'desc': desc # 使用映射后的描述
'name': category.name,
'label': category_info['label'],
'desc': category_info['desc']
},
'current_path': request.path,
'current_category_id': category_id
}
print(f"传递给模板的数据: name='{context['category_info']['name']}', label='{context['category_info']['label']}'")
# 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()
context = {'post': post}
return render(request, 'blog/detail.html', context)
# 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)

@ -0,0 +1,30 @@
#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']

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

@ -0,0 +1,31 @@
# 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字段的标签设为空不显示标签文本
}

@ -0,0 +1,33 @@
# 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'],
},
),
]

@ -0,0 +1,39 @@
# 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}'

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

@ -0,0 +1,32 @@
# 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'
),
]

@ -0,0 +1,38 @@
# 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.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -38,6 +38,8 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'blog',
'accounts',
'comments',
]
MIDDLEWARE = [
@ -63,6 +65,7 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.media', # 新增:媒体文件处理器
],
},
},
@ -104,7 +107,6 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
# 修改这里:语言改为中文,时区改为上海
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
@ -121,10 +123,75 @@ 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'
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,23 +1,15 @@
"""
URL configuration for mysite project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
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)

Binary file not shown.

@ -4,8 +4,358 @@
--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;
@ -13,4 +363,352 @@
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;
}

@ -11,17 +11,16 @@
{% endif %}
</title>
<style>
/* 所有CSS样式保持不变 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 南京非遗主题自定义样式 */
:root {
--primary-color: #8b4513;
--secondary-color: #d2691e;
--accent-color: #cd853f;
--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;
@ -29,6 +28,12 @@
--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;
@ -43,7 +48,8 @@
padding: 0;
box-shadow: 0 4px 20px rgba(139, 69, 19, 0.3);
position: relative;
overflow: hidden;
overflow: visible; /* 修复改为visible */
min-height: 180px; /* 确保有足够高度 */
}
.header::before {
@ -53,7 +59,7 @@
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #ffd700, #ff6b6b, #4ecdc4, #ffd700);
background: linear-gradient(90deg, var(--nj-gold), var(--nj-red), var(--nj-gold));
animation: shimmer 3s infinite;
}
@ -66,6 +72,8 @@
max-width: 1400px;
margin: 0 auto;
padding: 0 30px;
position: relative; /* 确保相对定位 */
min-height: 160px; /* 增加最小高度 */
}
.site-brand {
@ -83,7 +91,7 @@
}
.site-subtitle {
color: #ffeb3b;
color: var(--nj-gold);
font-size: 1.1em;
margin-top: 8px;
font-style: italic;
@ -134,12 +142,219 @@
/* 当前分类高亮样式 */
.current-category {
background: rgba(255, 255, 255, 0.25);
border: 2px solid rgba(255, 255, 255, 0.6);
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;
@ -174,7 +389,7 @@
border-radius: 20px;
font-size: 0.9em;
margin-bottom: 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
border: 1px solid var(--nj-gold);
font-weight: 500;
}
@ -326,6 +541,22 @@
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;
@ -465,6 +696,35 @@
.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;
}
}
/* 页脚样式 */
@ -480,6 +740,21 @@
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>
@ -497,13 +772,71 @@
<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 %}
@ -520,25 +853,28 @@
<div class="sidebar-widget">
<h3 class="widget-title">热门文章</h3>
<ul class="views-list">
<li>南京云锦织造技艺 <span class="view-count">156</span></li>
<li>金陵金箔制作工艺 <span class="view-count">128</span></li>
<li>秦淮灯会千年传承 <span class="view-count">95</span></li>
<li>南京白局艺术特色 <span class="view-count">87</span></li>
<li>金陵剪纸非遗大师 <span class="view-count">76</span></li>
{% 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>
<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' 3 %}" class="cloud-tag">秦淮灯会</a>
<a href="{% url 'category' 2 %}" 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' 2 %}" class="cloud-tag">古琴艺术</a>
<a href="{% url 'category' 4 %}" 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>

@ -0,0 +1,283 @@
{% 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 %}

@ -9,7 +9,7 @@
<div class="post-meta">
<span><i>👁️</i> {{ post.views }} 次阅读</span>
<span><i>💬</i> {{ post.comment_set.count }} 条评论</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>

Loading…
Cancel
Save