forked from ple6vqxn8/DjangoBlog
Compare commits
35 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
0d78de44ac | 1 month ago |
|
|
618cf56bae | 1 month ago |
|
|
7064685492 | 1 month ago |
|
|
e6eb8f32f1 | 1 month ago |
|
|
b09029de28 | 1 month ago |
|
|
f6cd05a0d1 | 2 months ago |
|
|
5c04c5b73e | 2 months ago |
|
|
6e99d6dc3b | 2 months ago |
|
|
70bb5b4cf1 | 2 months ago |
|
|
fce63f91a4 | 2 months ago |
|
|
c2fa14933d | 2 months ago |
|
|
17a9b6de47 | 2 months ago |
|
|
1206e423ae | 2 months ago |
|
|
5d2ebbb8ea | 2 months ago |
|
|
54cadcf84d | 2 months ago |
|
|
42751f7223 | 2 months ago |
|
|
38309147d3 | 2 months ago |
|
|
b19624aa92 | 2 months ago |
|
|
af8f327a37 | 2 months ago |
|
|
212880fb51 | 3 months ago |
|
|
83a24e4911 | 3 months ago |
|
|
b8833cda10 | 3 months ago |
|
|
cec8f3966f | 3 months ago |
|
|
e8bed1544a | 3 months ago |
|
|
8b889e700e | 3 months ago |
|
|
b020032691 | 3 months ago |
|
|
8b1a2ce8f2 | 3 months ago |
|
|
73cc742716 | 3 months ago |
|
|
fa8314391f | 3 months ago |
|
|
fb035f81e7 | 3 months ago |
|
|
e26349f6e9 | 3 months ago |
|
|
c9bce0588d | 3 months ago |
|
|
db412ed30f | 3 months ago |
|
|
43f7366174 | 4 months ago |
|
|
0fc26c566f | 4 months ago |
@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
db.sqlite3
|
||||
*.log
|
||||
media/
|
||||
@ -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,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,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,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,56 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-08 15:36
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Post',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('content', models.TextField()),
|
||||
('excerpt', models.TextField(blank=True, max_length=200)),
|
||||
('views', models.PositiveIntegerField(default=0)),
|
||||
('created_time', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_time', models.DateTimeField(auto_now=True)),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category')),
|
||||
('tags', models.ManyToManyField(blank=True, to='blog.tag')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('author', models.CharField(max_length=100)),
|
||||
('content', models.TextField()),
|
||||
('created_time', models.DateTimeField(auto_now_add=True)),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.post')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,160 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-09 09:21
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PrimaryTag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True, verbose_name='标签名称')),
|
||||
('color', models.CharField(default='#8b4513', max_length=7, verbose_name='标签颜色')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '一级标签',
|
||||
'verbose_name_plural': '一级标签',
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='post',
|
||||
name='tags',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='category',
|
||||
options={'ordering': ['order'], 'verbose_name': '非遗分类', 'verbose_name_plural': '非遗分类'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='comment',
|
||||
options={'ordering': ['-created_time'], 'verbose_name': '评论', 'verbose_name_plural': '评论'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='post',
|
||||
options={'ordering': ['-created_time'], 'verbose_name': '非遗文章', 'verbose_name_plural': '非遗文章'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, verbose_name='分类描述'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='icon',
|
||||
field=models.CharField(default='🏮', max_length=50, verbose_name='分类图标'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='order',
|
||||
field=models.IntegerField(default=0, verbose_name='显示顺序'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='featured_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='posts/%Y/%m/', verbose_name='特色图片'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('draft', '草稿'), ('published', '已发布')], default='published', max_length=10, verbose_name='状态'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100, unique=True, verbose_name='分类名称'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='author',
|
||||
field=models.CharField(max_length=100, verbose_name='评论者'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='content',
|
||||
field=models.TextField(verbose_name='评论内容'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='created_time',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='评论时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='post',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.post', verbose_name='所属文章'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='post',
|
||||
name='author',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='post',
|
||||
name='category',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='post',
|
||||
name='content',
|
||||
field=models.TextField(verbose_name='文章内容'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='post',
|
||||
name='created_time',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='post',
|
||||
name='excerpt',
|
||||
field=models.TextField(blank=True, max_length=300, verbose_name='文章摘要'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='post',
|
||||
name='title',
|
||||
field=models.CharField(max_length=200, verbose_name='文章标题'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='post',
|
||||
name='updated_time',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='post',
|
||||
name='views',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='阅读量'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='primary_tags',
|
||||
field=models.ManyToManyField(blank=True, to='blog.primarytag', verbose_name='一级标签'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SecondaryTag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='标签名称')),
|
||||
('tag_type', models.CharField(choices=[('geo', '地理空间'), ('theme', '主题维度'), ('project', '非遗项目'), ('person', '人物传承'), ('time', '时间节庆')], max_length=20, verbose_name='标签类型')),
|
||||
('parent_tag', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.primarytag', verbose_name='父级标签')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '二级标签',
|
||||
'verbose_name_plural': '二级标签',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='secondary_tags',
|
||||
field=models.ManyToManyField(blank=True, to='blog.secondarytag', verbose_name='二级标签'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Tag',
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ category.name }} - 我的博客{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>分类: {{ category.name }}</h1>
|
||||
{% for post in post_list %}
|
||||
<article class="post">
|
||||
<h2><a href="{% url 'detail' post.id %}">{{ post.title }}</a></h2>
|
||||
<!-- 和首页相同的文章显示 -->
|
||||
</article>
|
||||
{% empty %}
|
||||
<p>该分类下还没有文章。</p>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@ -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 %}
|
||||
@ -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,24 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>最新文章</h1>
|
||||
{% for post in post_list %}
|
||||
<article class="post">
|
||||
<h2><a href="{% url 'detail' post.id %}">{{ post.title }}</a></h2>
|
||||
<div class="post-meta">
|
||||
<span>{{ post.comment_set.count }} 评论</span> | <span>{{ post.views }} views</span>
|
||||
</div>
|
||||
<p>{{ post.excerpt|default:post.content|truncatewords:30 }}</p>
|
||||
<a href="{% url 'detail' post.id %}" class="read-more">Read more</a>
|
||||
<div class="post-footer">
|
||||
发布于 {{ post.category.name }} 并标记为
|
||||
{% for tag in post.tags.all %}
|
||||
{{ tag.name }}{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
由 {{ post.author.username }} 在 {{ post.created_time|date:"Y-m-d" }}
|
||||
</div>
|
||||
</article>
|
||||
{% empty %}
|
||||
<p>还没有发布任何文章。</p>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@ -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 %}
|
||||
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@ -0,0 +1,21 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('category/<int:category_id>/', views.category_view, name='category'),
|
||||
path('post/<int:post_id>/', views.detail, name='detail'),
|
||||
path('search/', views.search, name='search'),
|
||||
path('about/', views.about, name='about'),
|
||||
|
||||
# 文章管理路由
|
||||
path('create/', views.create_post, name='create_post'),
|
||||
path('my-posts/', views.user_posts, name='user_posts'),
|
||||
path('edit/<int:post_id>/', views.edit_post, name='edit_post'),
|
||||
path('publish/<int:post_id>/', views.publish_post, name='publish_post'),
|
||||
path('delete/<int:post_id>/', views.delete_post, name='delete_post'),
|
||||
|
||||
# 点赞收藏路由
|
||||
path('post/<int:post_id>/like/', views.like_post, name='like_post'),
|
||||
path('post/<int:post_id>/favorite/', views.favorite_post, name='favorite_post'),
|
||||
]
|
||||
@ -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,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,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for mysite project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
@ -0,0 +1,15 @@
|
||||
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)
|
||||
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for mysite project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
Binary file not shown.
@ -0,0 +1,714 @@
|
||||
/* 南京非遗主题自定义样式 */
|
||||
:root {
|
||||
--nj-red: #c62f2f;
|
||||
--nj-gold: #d4af37;
|
||||
--nj-brown: #8b4513;
|
||||
--nj-light-brown: #a0522d;
|
||||
|
||||
--primary-color: var(--nj-brown);
|
||||
--secondary-color: var(--nj-light-brown);
|
||||
--accent-color: var(--nj-gold);
|
||||
--text-dark: #2c1810;
|
||||
--text-light: #5d4037;
|
||||
--bg-light: #faf3e8;
|
||||
--bg-white: #fffaf0;
|
||||
--border-color: #e8d5c4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Noto Serif SC", "Source Han Serif SC", "Microsoft YaHei", serif;
|
||||
line-height: 1.7;
|
||||
color: var(--text-dark);
|
||||
background: linear-gradient(135deg, #faf3e8 0%, #fffaf0 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 头部样式 - 中国风设计 */
|
||||
.header {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
padding: 0;
|
||||
box-shadow: 0 4px 20px rgba(139, 69, 19, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--nj-gold), var(--nj-red), var(--nj-gold));
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.site-brand {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.site-title {
|
||||
color: #fff;
|
||||
font-size: 2.5em;
|
||||
font-weight: 700;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.site-subtitle {
|
||||
color: var(--nj-gold);
|
||||
font-size: 1.1em;
|
||||
margin-top: 8px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.main-nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
list-style: none;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.main-nav a {
|
||||
display: block;
|
||||
padding: 12px 25px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 25px;
|
||||
margin: 0 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-nav a::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.main-nav a:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.main-nav a:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* 当前分类高亮样式 */
|
||||
.current-category {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border: 2px solid var(--nj-gold);
|
||||
font-weight: 600;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 搜索框样式 */
|
||||
.search-box {
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.search-box form {
|
||||
display: inline-flex;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 25px 0 0 25px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-box button {
|
||||
background: var(--nj-gold);
|
||||
color: var(--text-dark);
|
||||
border: none;
|
||||
padding: 12px 25px;
|
||||
border-radius: 0 25px 25px 0;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.search-box button:hover {
|
||||
background: #ffd700;
|
||||
}
|
||||
|
||||
/* 主要内容布局 */
|
||||
.main-container {
|
||||
max-width: 1400px;
|
||||
margin: 40px auto;
|
||||
padding: 0 30px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 350px;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
/* 文章内容区域 */
|
||||
.content {
|
||||
background: var(--bg-white);
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(139, 69, 19, 0.1);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.content-header {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
padding: 25px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 分类标签样式 */
|
||||
.category-label {
|
||||
display: inline-block;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--nj-gold);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content-header h1 {
|
||||
font-size: 2em;
|
||||
margin-bottom: 8px;
|
||||
text-shadow: 1px 1px 3px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.content-intro {
|
||||
font-size: 1.1em;
|
||||
opacity: 0.95;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.6;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.posts-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 文章项样式 */
|
||||
.post-item {
|
||||
padding: 35px 30px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.post-item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: linear-gradient(to bottom, var(--primary-color), var(--accent-color));
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.post-item:hover {
|
||||
background: linear-gradient(135deg, #fffaf0, #faf3e8);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.post-item:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.post-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.post-title a {
|
||||
color: var(--text-dark);
|
||||
text-decoration: none;
|
||||
font-size: 1.8em;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
transition: color 0.3s ease;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.post-title a:hover {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.post-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.post-meta i {
|
||||
font-style: normal;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
color: var(--text-light);
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 25px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.read-more {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
padding: 8px 16px;
|
||||
border: 2px solid var(--primary-color);
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.read-more:hover {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.post-footer {
|
||||
color: var(--text-light);
|
||||
font-size: 13px;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px dashed var(--border-color);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.post-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: linear-gradient(135deg, var(--accent-color), var(--secondary-color));
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 15px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 非遗徽章样式 */
|
||||
.heritage-badge {
|
||||
background: linear-gradient(135deg, var(--nj-red), var(--nj-brown));
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border: 1px solid var(--nj-gold);
|
||||
}
|
||||
|
||||
.heritage-item {
|
||||
border-left: 3px solid var(--nj-red);
|
||||
background: linear-gradient(135deg, #fffaf0, #faf3e8);
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.sidebar-widget {
|
||||
background: var(--bg-white);
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 8px 25px rgba(139, 69, 19, 0.1);
|
||||
padding: 25px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.widget-title {
|
||||
color: var(--primary-color);
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid var(--accent-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.widget-title::before {
|
||||
content: "◆";
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.views-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.views-list li {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.views-list li:hover {
|
||||
color: var(--primary-color);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.views-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.view-count {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 分类标签云 */
|
||||
.tag-cloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cloud-tag {
|
||||
background: linear-gradient(135deg, #e8d5c4, #d7ccc8);
|
||||
color: var(--text-light);
|
||||
padding: 6px 12px;
|
||||
border-radius: 15px;
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cloud-tag:hover {
|
||||
background: linear-gradient(135deg, var(--accent-color), var(--secondary-color));
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 文章详情页特定样式 */
|
||||
.post-detail {
|
||||
padding: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.post-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.post-detail .post-title {
|
||||
font-size: 2.2em;
|
||||
color: var(--text-dark);
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post-detail .post-meta {
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.post-image img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
margin-top: 20px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.post-detail .post-content {
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.post-detail .post-content p {
|
||||
margin-bottom: 20px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.post-detail .post-footer {
|
||||
color: var(--text-light);
|
||||
font-size: 13px;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px dashed var(--border-color);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 评论区域样式 */
|
||||
.comments-section {
|
||||
margin-top: 50px;
|
||||
padding-top: 30px;
|
||||
border-top: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.comment-form textarea {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.comment-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
margin-top: 15px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background: var(--secondary-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
background: var(--bg-light);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
border-left: 4px solid var(--accent-color);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.comment-item:hover {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
line-height: 1.6;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.no-comments {
|
||||
text-align: center;
|
||||
color: var(--text-light);
|
||||
padding: 40px;
|
||||
font-style: italic;
|
||||
background: var(--bg-light);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.login-prompt {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: var(--bg-light);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-prompt a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-prompt a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.main-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nav-container {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.main-nav {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.main-nav a {
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
padding: 0 15px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
padding: 25px 20px;
|
||||
}
|
||||
|
||||
.post-title a {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-header h1 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.post-detail {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.post-detail .post-title {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.post-detail .post-meta {
|
||||
gap: 15px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 页脚样式 */
|
||||
.footer {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--text-dark));
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 30px 20px;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
opacity: 0.8;
|
||||
font-size: 14px;
|
||||
}
|
||||
@ -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 %}
|
||||
@ -0,0 +1,70 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
{% for post in post_list %}
|
||||
<article class="post-item">
|
||||
<div class="post-title">
|
||||
<h2><a href="{% url 'detail' post.id %}">{{ post.title }}</a></h2>
|
||||
</div>
|
||||
|
||||
<div class="post-meta">
|
||||
<span><i>👁️</i> {{ post.views }} 次阅读</span>
|
||||
<span><i>💬</i> {{ post.comments.count }} 条评论</span>
|
||||
<span><i>📅</i> {{ post.created_time|date:"Y-m-d" }}</span>
|
||||
<!-- 显示文章所属分类 -->
|
||||
<span><i>🏷️</i> {{ post.category.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="post-content">
|
||||
{{ post.excerpt|default:post.content|truncatewords:35 }}
|
||||
</div>
|
||||
|
||||
<a href="{% url 'detail' post.id %}" class="read-more">
|
||||
阅读全文 →
|
||||
</a>
|
||||
|
||||
<div class="post-footer">
|
||||
<div class="post-tags">
|
||||
<span class="tag">{{ post.category.name }}</span>
|
||||
{% for tag in post.primary_tags.all %}
|
||||
<span class="tag">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
{% for tag in post.secondary_tags.all|slice:":3" %}
|
||||
<span class="tag">#{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="post-author">
|
||||
作者:{{ post.author.username }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% empty %}
|
||||
<div class="post-item" style="text-align: center; padding: 60px 30px;">
|
||||
<h3 style="color: var(--text-light); margin-bottom: 15px;">
|
||||
{% if current_category_id %}
|
||||
该分类下暂无文章
|
||||
{% else %}
|
||||
暂无文章
|
||||
{% endif %}
|
||||
</h3>
|
||||
<p style="color: var(--text-light);">欢迎关注南京非物质文化遗产,精彩内容即将呈现...</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- 分页功能(可选) -->
|
||||
{% if post_list.has_other_pages %}
|
||||
<div class="pagination" style="padding: 30px; text-align: center;">
|
||||
{% if post_list.has_previous %}
|
||||
<a href="?page={{ post_list.previous_page_number }}" class="read-more">上一页</a>
|
||||
{% endif %}
|
||||
|
||||
<span style="margin: 0 20px; color: var(--text-light);">
|
||||
第 {{ post_list.number }} 页,共 {{ post_list.paginator.num_pages }} 页
|
||||
</span>
|
||||
|
||||
{% if post_list.has_next %}
|
||||
<a href="?page={{ post_list.next_page_number }}" class="read-more">下一页</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Loading…
Reference in new issue