parent
6d9d624da2
commit
2087f67fa5
@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.2.6 on 2025-11-22 23:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0006_alter_blogsettings_options'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Favorite',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='blog.article')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_articles', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '收藏',
|
||||
'verbose_name_plural': '收藏',
|
||||
'unique_together': {('article', 'user')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,37 @@
|
||||
# Generated by Django 5.2.6 on 2025-11-23 00:03
|
||||
|
||||
import blog.models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0007_favorite'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('avatar', models.ImageField(blank=True, help_text='Upload your avatar image (recommended size: 100x100px)', null=True, upload_to=blog.models.user_avatar_path, verbose_name='Avatar')),
|
||||
('bio', models.TextField(blank=True, help_text='Tell us a little about yourself', null=True, verbose_name='Biography')),
|
||||
('website', models.URLField(blank=True, null=True, verbose_name='Website')),
|
||||
('github', models.URLField(blank=True, null=True, verbose_name='GitHub')),
|
||||
('twitter', models.URLField(blank=True, null=True, verbose_name='Twitter')),
|
||||
('weibo', models.URLField(blank=True, null=True, verbose_name='Weibo')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User Profile',
|
||||
'verbose_name_plural': 'User Profiles',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 755 KiB |
@ -0,0 +1,97 @@
|
||||
<!-- blog/templates/blog/user_favorites.html -->
|
||||
{% extends 'share_layout/base.html' %}
|
||||
{% load static %}
|
||||
{% load blog_tags %}
|
||||
|
||||
{% block header %}
|
||||
<title>我的收藏 | {{ SITE_NAME }}</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="primary" class="site-content">
|
||||
<div id="content" role="main">
|
||||
|
||||
<article class="hentry">
|
||||
<header class="entry-header">
|
||||
<h1 class="entry-title">我的收藏</h1>
|
||||
<p>这里是你收藏的所有文章。</p>
|
||||
</header>
|
||||
|
||||
<div class="entry-content">
|
||||
{% if favorite_articles %}
|
||||
<ul class="article-list">
|
||||
{% for article in favorite_articles %}
|
||||
<li>
|
||||
<h2><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h2>
|
||||
<div class="meta">
|
||||
<time datetime="{{ article.pub_time|date:"c" }}">
|
||||
{{ article.pub_time|date:"F j, Y" }}
|
||||
</time>
|
||||
<span>作者: <a href="{% url 'blog:author_detail' article.author.username %}">{{ article.author.username }}</a></span>
|
||||
<!-- 修正第 31 行的 URL 名称 -->
|
||||
<span>分类:<a href="{% url 'blog:category_detail' article.category.slug %}">{{ article.category.name }}</a></span>
|
||||
</div>
|
||||
<div class="summary">
|
||||
{{ article.body|striptags|truncatechars:150 }}
|
||||
</div>
|
||||
<a href="{{ article.get_absolute_url }}" class="read-more">阅读全文</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="no-favorites">你还没有收藏任何文章。快去 <a href="{% url 'blog:index' %}">文章列表</a> 看看吧!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% load_sidebar profile_user "p" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_footer %}
|
||||
<style>
|
||||
.article-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.article-list li {
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.article-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.article-list h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.meta {
|
||||
color: #777;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.summary {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.read-more {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-radius: 3px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.read-more:hover {
|
||||
background-color: #0056b3;
|
||||
color: white;
|
||||
}
|
||||
.no-favorites {
|
||||
text-align: center;
|
||||
padding: 50px 0;
|
||||
color: #555;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,230 @@
|
||||
<!-- blog/templates/blog/user_profile_detail.html -->
|
||||
{% extends 'share_layout/base.html' %}
|
||||
{% load static %}
|
||||
{% load blog_tags %} <!-- 添加这一行,加载自定义标签库 -->
|
||||
|
||||
{% block header %}
|
||||
<title>{{ profile.user.username }}'s Profile | {{ SITE_NAME }}</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="primary" class="site-content">
|
||||
<div id="content" role="main">
|
||||
|
||||
<article class="hentry">
|
||||
<header class="entry-header profile-header">
|
||||
<div class="profile-avatar">
|
||||
{% if profile.avatar %}
|
||||
<img src="{{ profile.avatar.url }}" alt="{{ profile.user.username }}'s avatar">
|
||||
{% else %}
|
||||
<img src="{% static 'blog/images/default-avatar.png' %}" alt="Default Avatar">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
<h1 class="entry-title">{{ profile.user.username }}</h1>
|
||||
<div class="profile-meta">
|
||||
<span>Joined on {{ profile.created_at|date:"F j, Y" }}</span>
|
||||
{% if user.is_authenticated and user == profile.user %}
|
||||
<a href="{% url 'blog:user_profile_update' %}" class="edit-profile-btn">
|
||||
Edit Profile
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="entry-content profile-content">
|
||||
{% if profile.bio %}
|
||||
<section class="profile-bio">
|
||||
<h3>About Me</h3>
|
||||
<p>{{ profile.bio|linebreaks }}</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if profile.website or profile.github or profile.twitter or profile.weibo %}
|
||||
<section class="profile-links">
|
||||
<h3>Connect with Me</h3>
|
||||
<ul>
|
||||
{% if profile.website %}
|
||||
<li><a href="{{ profile.website }}" target="_blank" rel="noopener noreferrer"><i class="fas fa-globe"></i> {{ profile.website }}</a></li>
|
||||
{% endif %}
|
||||
{% if profile.github %}
|
||||
<li><a href="{{ profile.github }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-github"></i> GitHub</a></li>
|
||||
{% endif %}
|
||||
{% if profile.twitter %}
|
||||
<li><a href="{{ profile.twitter }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-twitter"></i> Twitter</a></li>
|
||||
{% endif %}
|
||||
{% if profile.weibo %}
|
||||
<li><a href="{{ profile.weibo }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-weibo"></i> Weibo</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ==================== 新增:我的收藏链接 ==================== -->
|
||||
{% if user.is_authenticated and user == profile.user %}
|
||||
<section class="profile-actions">
|
||||
<a href="{% url 'blog:user_favorites' %}" class="btn-favorite">
|
||||
<i class="fas fa-heart"></i> 我的收藏 ({{ user.favorite_articles.count }})
|
||||
</a>
|
||||
</section>
|
||||
{% endif %}
|
||||
<!-- ========================================================== -->
|
||||
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{% if user_articles %}
|
||||
<section class="profile-articles">
|
||||
<h2>Articles by {{ profile.user.username }} <span>({{ user_articles|length }})</span></h2>
|
||||
<ul>
|
||||
{% for article in user_articles %}
|
||||
<li>
|
||||
<a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
|
||||
<time datetime="{{ article.pub_time|date:"c" }}">{{ article.pub_time|date:"F j, Y" }}</time>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% load_sidebar user "p" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_footer %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
|
||||
<style>
|
||||
/* 个人资料头部布局 */
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 30px; /* 增加了下边距,与内容区隔开 */
|
||||
}
|
||||
|
||||
/* 头像样式调整 */
|
||||
.profile-avatar img {
|
||||
width: 80px; /* 从 120px 调整为 80px */
|
||||
height: 80px; /* 从 120px 调整为 80px */
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
margin-right: 20px; /* 稍微减少了右边距 */
|
||||
border: 3px solid #fff;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1); /* 增强了一点阴影 */
|
||||
}
|
||||
|
||||
/* 个人信息区域 */
|
||||
.profile-info h1 {
|
||||
margin-bottom: 5px; /* 减少了标题下边距 */
|
||||
}
|
||||
|
||||
.edit-profile-btn {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
padding: 5px 15px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-size: 0.9em; /* 字体稍小 */
|
||||
}
|
||||
|
||||
.edit-profile-btn:hover {
|
||||
background-color: #0056b3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 内容区块通用样式 */
|
||||
.profile-bio, .profile-links, .profile-articles, .profile-actions { /* 新增 .profile-actions */
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #eee; /* 增加了一个边框 */
|
||||
}
|
||||
|
||||
/* ==================== 新增:我的收藏链接样式 ==================== */
|
||||
.btn-favorite {
|
||||
display: inline-flex; /* 使用 flex 布局让图标和文字垂直居中 */
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background-color: #f0ad4e; /* 使用一个醒目的颜色,如橙色 */
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-favorite i {
|
||||
margin-right: 8px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.btn-favorite:hover {
|
||||
background-color: #ec971f; /* hover 时颜色加深 */
|
||||
color: white;
|
||||
}
|
||||
/* ================================================================ */
|
||||
|
||||
/* 链接列表 */
|
||||
.profile-links ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.profile-links li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.profile-links a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s; /* 增加了过渡效果 */
|
||||
}
|
||||
|
||||
.profile-links a:hover {
|
||||
text-decoration: underline;
|
||||
color: #0056b3; /* hover 时颜色加深 */
|
||||
}
|
||||
|
||||
.profile-links i {
|
||||
margin-right: 10px;
|
||||
font-size: 1.2em;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
color: #555; /* 图标颜色稍微暗一点 */
|
||||
}
|
||||
|
||||
/* 文章列表 */
|
||||
.profile-articles ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.profile-articles li {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px dotted #ddd;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center; /* 垂直居中对齐 */
|
||||
}
|
||||
|
||||
.profile-articles li:last-child {
|
||||
border-bottom: none; /* 去掉最后一个的边框 */
|
||||
}
|
||||
|
||||
.profile-articles li time {
|
||||
color: #777;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,104 @@
|
||||
<!-- blog/templates/blog/user_profile_update.html -->
|
||||
{% extends 'share_layout/base.html' %}
|
||||
{% load static %}
|
||||
{% load blog_tags %} <!-- 加载自定义标签库 -->
|
||||
|
||||
{% block header %}
|
||||
<title>Edit Profile | {{ SITE_NAME }}</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="primary" class="site-content">
|
||||
<div id="content" role="main">
|
||||
|
||||
<article class="hentry">
|
||||
<header class="entry-header">
|
||||
<h1 class="entry-title">Edit Your Profile</h1>
|
||||
</header>
|
||||
|
||||
<div class="entry-content">
|
||||
<form enctype="multipart/form-data" method="post" action="{% url 'blog:user_profile_update' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger">
|
||||
<strong>Please correct the errors below.</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.avatar.id_for_label }}">Current Avatar:</label><br>
|
||||
{% if user.profile.avatar %}
|
||||
<img src="{{ user.profile.avatar.url }}" alt="Current Avatar" style="width: 100px; border-radius: 50%; margin-bottom: 10px;">
|
||||
{% else %}
|
||||
<img src="{% static 'blog/images/default-avatar.png' %}" alt="Default Avatar" style="width: 100px; border-radius: 50%; margin-bottom: 10px;">
|
||||
{% endif %}
|
||||
<br>
|
||||
{{ form.avatar.label_tag }} {{ form.avatar }}
|
||||
<small class="form-text text-muted">{{ form.avatar.help_text }}</small>
|
||||
{{ form.avatar.errors }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.bio.label_tag }}
|
||||
{{ form.bio }}
|
||||
{{ form.bio.errors }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.website.label_tag }}
|
||||
{{ form.website }}
|
||||
{{ form.website.errors }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.github.label_tag }}
|
||||
{{ form.github }}
|
||||
{{ form.github.errors }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.twitter.label_tag }}
|
||||
{{ form.twitter }}
|
||||
{{ form.twitter.errors }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ form.weibo.label_tag }}
|
||||
{{ form.weibo }}
|
||||
{{ form.weibo.errors }}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<a href="{% url 'blog:user_profile' username=user.username %}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% load_sidebar user "p" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_footer %}
|
||||
<style>
|
||||
.form-group { margin-bottom: 20px; }
|
||||
label { font-weight: bold; display: block; margin-bottom: 5px; }
|
||||
input[type="text"], input[type="url"], textarea {
|
||||
width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;
|
||||
}
|
||||
textarea { resize: vertical; }
|
||||
.btn {
|
||||
display: inline-block; padding: 10px 16px; margin-bottom: 0; font-size: 14px; font-weight: 400; line-height: 1.42857143; text-align: center; white-space: nowrap; vertical-align: middle; cursor: pointer; background-image: none; border: 1px solid transparent; border-radius: 4px; text-decoration: none;
|
||||
}
|
||||
.btn-primary { color: #fff; background-color: #337ab7; border-color: #2e6da4; }
|
||||
.btn-primary:hover { background-color: #286090; border-color: #204d74; color: #fff; }
|
||||
.btn-secondary { color: #333; background-color: #fff; border-color: #ccc; }
|
||||
.btn-secondary:hover { background-color: #e6e6e6; border-color: #adadad; color: #333; }
|
||||
.alert { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; }
|
||||
.alert-danger { color: #a94442; background-color: #f2dede; border-color: #ebccd1; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
Loading…
Reference in new issue