Skip to content

Commit fbedebc

Browse files
committed
feat(account): 重构用户认证和个人资料功能
- 将注册路由从 sign-up 改为 signup 并更新相关模板和测试 - 新增用户资料字段(头像、职位、个人简介)及相关表单和模板 - 创建统一的认证基础模板和设置基础模板 - 优化登录、注册和个人资料页面的UI设计 - 添加用户资料相关的数据库迁移文件
1 parent 5c20a1f commit fbedebc

15 files changed

Lines changed: 562 additions & 403 deletions

File tree

src/apps/account/forms.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,31 @@
55
class UserProfileForm(forms.ModelForm):
66
class Meta:
77
model = UserProfile
8-
fields = ['full_name', 'gender', 'phone']
8+
fields = ['avatar', 'full_name', 'title', 'bio', 'gender', 'phone']
99
labels = {
10+
'avatar': '头像',
1011
'full_name': '姓名',
12+
'title': '职位/头衔',
13+
'bio': '个人简介',
1114
'gender': '性别',
1215
'phone': '手机号',
1316
}
1417
widgets = {
18+
'avatar': forms.FileInput(attrs={
19+
'class': 'file-input file-input-bordered w-full',
20+
'accept': 'image/*',
21+
}),
1522
'full_name': forms.TextInput(attrs={
1623
'class': 'input input-bordered w-full',
1724
'autocomplete': 'name',
1825
}),
26+
'title': forms.TextInput(attrs={
27+
'class': 'input input-bordered w-full',
28+
'autocomplete': 'organization-title',
29+
}),
30+
'bio': forms.Textarea(attrs={
31+
'class': 'textarea textarea-bordered w-full h-24',
32+
}),
1933
'gender': forms.Select(attrs={
2034
'class': 'select select-bordered w-full',
2135
}),
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 6.0.1 on 2026-01-22 20:38
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('account', '0004_historicaluserprofile'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='historicaluserprofile',
15+
name='avatar',
16+
field=models.TextField(blank=True, max_length=100, null=True, verbose_name='头像'),
17+
),
18+
migrations.AddField(
19+
model_name='historicaluserprofile',
20+
name='bio',
21+
field=models.TextField(blank=True, default='', verbose_name='个人简介'),
22+
),
23+
migrations.AddField(
24+
model_name='historicaluserprofile',
25+
name='title',
26+
field=models.CharField(blank=True, default='', max_length=100, verbose_name='职位/头衔'),
27+
),
28+
migrations.AddField(
29+
model_name='userprofile',
30+
name='avatar',
31+
field=models.ImageField(blank=True, null=True, upload_to='avatars/%Y/%m/', verbose_name='头像'),
32+
),
33+
migrations.AddField(
34+
model_name='userprofile',
35+
name='bio',
36+
field=models.TextField(blank=True, default='', verbose_name='个人简介'),
37+
),
38+
migrations.AddField(
39+
model_name='userprofile',
40+
name='title',
41+
field=models.CharField(blank=True, default='', max_length=100, verbose_name='职位/头衔'),
42+
),
43+
]

src/apps/account/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.db import models
12
from django.db.models import signals
23
from django.dispatch import receiver
34
from django.contrib.auth.models import User
@@ -8,6 +9,10 @@
89

910
# Create your models here.
1011
class UserProfile(UserProfileAbstract):
12+
avatar = models.ImageField('头像', upload_to='avatars/%Y/%m/', blank=True, null=True)
13+
title = models.CharField('职位/头衔', max_length=100, blank=True, default='')
14+
bio = models.TextField('个人简介', blank=True, default='')
15+
1116
class Meta:
1217
db_table = table_name_wrapper('user_profile')
1318
verbose_name = '用户资料'
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{% extends '_base.html' %}
2+
{% load page_tags %}
3+
4+
{% block content %}
5+
<div class="container mx-auto max-w-6xl">
6+
{% page_header title breadcrumbs %}
7+
8+
<div class="flex flex-col lg:flex-row gap-6">
9+
<!-- Sidebar -->
10+
<div class="lg:w-1/4">
11+
<ul class="menu bg-base-100 rounded-box shadow-sm border border-base-200 p-2">
12+
<li>
13+
<a href="{% url 'account:profile' %}" class="{% if request.resolver_match.url_name == 'profile' %}active{% endif %}">
14+
<i class="fa-solid fa-user w-5 text-center opacity-70"></i>
15+
个人资料
16+
</a>
17+
</li>
18+
<li>
19+
<a href="{% url 'account:settings' %}" class="{% if request.resolver_match.url_name == 'settings' %}active{% endif %}">
20+
<i class="fa-solid fa-shield-halved w-5 text-center opacity-70"></i>
21+
安全中心
22+
</a>
23+
</li>
24+
<li>
25+
<a href="{% url 'account:settings-developer' %}" class="{% if 'developer' in request.resolver_match.url_name %}active{% endif %}">
26+
<i class="fa-solid fa-code w-5 text-center opacity-70"></i>
27+
开发者设置
28+
</a>
29+
</li>
30+
</ul>
31+
</div>
32+
33+
<!-- Content -->
34+
<div class="lg:w-3/4 space-y-6">
35+
{% block settings_content %}{% endblock %}
36+
</div>
37+
</div>
38+
</div>
39+
{% endblock %}
Lines changed: 63 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,77 @@
1-
{% extends '_base.html' %}
2-
{% load page_tags %}
1+
{% extends '_auth_base.html' %}
32

43
{% block title %}登录{% endblock %}
54

65
{% block content %}
7-
{% page_header title breadcrumbs %}
6+
<div class="space-y-6">
7+
<div class="space-y-2">
8+
<h2 class="text-3xl font-bold">欢迎回来</h2>
9+
<p class="text-base-content/60">请输入您的账号信息以登录。</p>
10+
</div>
811

9-
<div class="mx-auto w-full max-w-md">
10-
<div class="card bg-base-100 shadow-sm border border-base-200">
11-
<div class="card-body gap-6">
12-
<div class="space-y-1">
13-
<h1 class="card-title text-2xl">登录</h1>
14-
<p class="text-sm opacity-70">使用你的账号登录并继续。</p>
12+
{% if sso_url %}
13+
<a class="btn btn-outline btn-block gap-2 h-12" href="{{ sso_url }}">
14+
<i class="fa-solid fa-building-shield"></i>
15+
使用企业账号登录 (SSO)
16+
</a>
17+
18+
<div class="relative py-2">
19+
<div class="absolute inset-0 flex items-center">
20+
<div class="w-full border-t border-base-300"></div>
1521
</div>
22+
<div class="relative flex justify-center text-sm">
23+
<span class="px-2 bg-base-100 text-base-content/50">或者使用邮箱登录</span>
24+
</div>
25+
</div>
26+
{% endif %}
1627

17-
{% if sso_url %}
18-
<a class="btn btn-outline btn-block" href="{{ sso_url }}">
19-
<i class="fa-solid fa-building-shield"></i>
20-
企业单点登录
21-
</a>
22-
<div class="divider"></div>
23-
{% endif %}
24-
25-
<form method="post" class="space-y-4" novalidate>
26-
{% csrf_token %}
28+
<form method="post" class="space-y-4" novalidate>
29+
{% csrf_token %}
2730

28-
{% include 'account/_components/form_field.html' with field=form.username %}
31+
{% include 'account/_components/form_field.html' with field=form.username %}
2932

30-
<div class="space-y-1" x-data="{ show: false }">
31-
<div class="flex items-center justify-between">
32-
<label class="label" for="{{ form.password.id_for_label }}">
33-
<span class="label-text">{{ form.password.label }}</span>
34-
</label>
35-
<a class="link link-hover text-sm" href="{% url 'account:password-forgot' %}">忘记密码?</a>
36-
</div>
37-
<div class="join w-full">
38-
<div class="flex-1">
39-
{{ form.password }}
40-
</div>
41-
<button type="button" class="btn btn-ghost join-item" @click="show = !show"
42-
:aria-pressed="show.toString()"
43-
:title="show ? '隐藏密码' : '显示密码'"
44-
x-init="$nextTick(() => { $el.closest('.join').querySelector('input').type = show ? 'text' : 'password' })"
45-
x-effect="$el.closest('.join').querySelector('input').type = show ? 'text' : 'password'">
46-
<i class="fa-solid" :class="show ? 'fa-eye-slash' : 'fa-eye'"></i>
47-
</button>
48-
</div>
49-
{% if form.password.errors %}
50-
<p class="text-sm text-error">{{ form.password.errors|join:', ' }}</p>
51-
{% endif %}
52-
</div>
33+
<div class="form-control w-full space-y-1" x-data="{ show: false }">
34+
<div class="flex items-center justify-between">
35+
<label class="label p-0" for="{{ form.password.id_for_label }}">
36+
<span class="label-text font-medium">{{ form.password.label }}</span>
37+
</label>
38+
<a class="link link-primary link-hover text-sm font-medium" href="{% url 'account:password-forgot' %}">忘记密码?</a>
39+
</div>
40+
<div class="relative">
41+
<input :type="show ? 'text' : 'password'"
42+
name="{{ form.password.name }}"
43+
id="{{ form.password.id_for_label }}"
44+
class="input input-bordered w-full pr-10 {% if form.password.errors %}input-error{% endif %}"
45+
required
46+
{% if form.password.value %}value="{{ form.password.value }}"{% endif %}>
47+
<button type="button"
48+
class="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/50 hover:text-base-content"
49+
@click="show = !show"
50+
tabindex="-1">
51+
<i class="fa-solid" :class="show ? 'fa-eye-slash' : 'fa-eye'"></i>
52+
</button>
53+
</div>
54+
{% if form.password.errors %}
55+
<p class="text-sm text-error mt-1">{{ form.password.errors|join:', ' }}</p>
56+
{% endif %}
57+
</div>
5358

54-
{% if form.non_field_errors %}
55-
<div role="alert" class="alert alert-error">
56-
<span>{{ form.non_field_errors|join:', ' }}</span>
57-
</div>
58-
{% endif %}
59+
{% if form.non_field_errors %}
60+
<div role="alert" class="alert alert-error text-sm py-2">
61+
<i class="fa-solid fa-circle-exclamation"></i>
62+
<span>{{ form.non_field_errors|join:', ' }}</span>
63+
</div>
64+
{% endif %}
5965

60-
<button type="submit" class="btn btn-primary btn-block">
61-
登录
62-
</button>
63-
</form>
66+
<button type="submit" class="btn btn-primary btn-block btn-lg mt-6">
67+
登 录
68+
<i class="fa-solid fa-arrow-right ml-2"></i>
69+
</button>
70+
</form>
6471

65-
<p class="text-sm opacity-80">
66-
还没有账号?
67-
<a class="link link-primary" href="{% url 'account:sign-up' %}">注册</a>
68-
</p>
69-
</div>
70-
</div>
72+
<p class="text-center text-sm text-base-content/70">
73+
还没有账号?
74+
<a class="link link-primary font-bold" href="{% url 'account:signup' %}">立即注册</a>
75+
</p>
7176
</div>
7277
{% endblock %}
Lines changed: 42 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,56 @@
1-
{% extends '_base.html' %}
2-
{% load page_tags %}
1+
{% extends 'account/_settings_base.html' %}
32

43
{% block title %}个人资料{% endblock %}
54

6-
{% block content %}
7-
{% page_header title breadcrumbs %}
8-
9-
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
10-
<div class="card bg-base-100 shadow-sm border border-base-200 lg:col-span-2">
11-
<div class="card-body">
12-
<h2 class="card-title">基本信息</h2>
13-
<form method="post" class="space-y-4" enctype="multipart/form-data" novalidate>
14-
{% csrf_token %}
15-
16-
<div class="space-y-1">
17-
<label class="label" for="{{ form.full_name.id_for_label }}">
18-
<span class="label-text">{{ form.full_name.label }}</span>
19-
</label>
20-
{{ form.full_name }}
21-
{% if form.full_name.errors %}
22-
<p class="text-sm text-error">{{ form.full_name.errors|join:', ' }}</p>
23-
{% endif %}
5+
{% block settings_content %}
6+
<div class="card bg-base-100 shadow-sm border border-base-200">
7+
<div class="card-body">
8+
<h2 class="card-title text-xl mb-4">基本信息</h2>
9+
10+
<form method="post" enctype="multipart/form-data" novalidate class="space-y-6">
11+
{% csrf_token %}
12+
13+
<!-- Avatar Section -->
14+
<div class="flex flex-col sm:flex-row items-center gap-6">
15+
<div class="avatar placeholder">
16+
<div class="w-24 rounded-full bg-neutral text-neutral-content ring ring-base-200 ring-offset-base-100 ring-offset-2 overflow-hidden">
17+
{% if user.profile.avatar %}
18+
<img src="{{ user.profile.avatar.url }}" alt="{{ user.username }}" class="object-cover" />
19+
{% else %}
20+
<span class="text-3xl">{{ user.username|slice:":1"|upper }}</span>
21+
{% endif %}
22+
</div>
2423
</div>
25-
26-
<div class="space-y-1">
27-
<label class="label" for="{{ form.gender.id_for_label }}">
28-
<span class="label-text">{{ form.gender.label }}</span>
24+
<div class="form-control w-full max-w-xs">
25+
<label class="label">
26+
<span class="label-text font-medium">更换头像</span>
2927
</label>
30-
{{ form.gender }}
31-
{% if form.gender.errors %}
32-
<p class="text-sm text-error">{{ form.gender.errors|join:', ' }}</p>
28+
{{ form.avatar }}
29+
{% if form.avatar.errors %}
30+
<p class="text-sm text-error mt-1">{{ form.avatar.errors|join:', ' }}</p>
3331
{% endif %}
32+
<p class="text-xs text-base-content/50 mt-1">支持 JPG, PNG, GIF 格式。</p>
3433
</div>
34+
</div>
3535

36-
<div class="space-y-1">
37-
<label class="label" for="{{ form.phone.id_for_label }}">
38-
<span class="label-text">{{ form.phone.label }}</span>
39-
</label>
40-
{{ form.phone }}
41-
{% if form.phone.errors %}
42-
<p class="text-sm text-error">{{ form.phone.errors|join:', ' }}</p>
43-
{% endif %}
44-
</div>
36+
<div class="divider"></div>
4537

46-
<div class="card-actions justify-end">
47-
<button type="submit" class="btn btn-primary">保存</button>
48-
</div>
49-
</form>
50-
</div>
51-
</div>
38+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
39+
{% include 'account/_components/form_field.html' with field=form.full_name %}
40+
{% include 'account/_components/form_field.html' with field=form.title %}
41+
</div>
5242

53-
<div class="card bg-base-100 shadow-sm border border-base-200">
54-
<div class="card-body">
55-
<h2 class="card-title">账号信息</h2>
56-
<div class="space-y-3 text-sm">
57-
<div class="flex items-center justify-between gap-4">
58-
<span class="opacity-70">用户名</span>
59-
<span class="font-medium">{{ request.user.username }}</span>
60-
</div>
61-
<div class="flex items-center justify-between gap-4">
62-
<span class="opacity-70">邮箱</span>
63-
<span class="font-medium">{{ request.user.email|default:"—" }}</span>
64-
</div>
65-
<div class="flex items-center justify-between gap-4">
66-
<span class="opacity-70">加入时间</span>
67-
<span class="font-medium">{{ request.user.date_joined|date:"Y-m-d H:i" }}</span>
68-
</div>
43+
{% include 'account/_components/form_field.html' with field=form.bio %}
44+
45+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
46+
{% include 'account/_components/form_field.html' with field=form.phone %}
47+
{% include 'account/_components/form_field.html' with field=form.gender %}
6948
</div>
70-
<div class="divider"></div>
71-
<a class="btn btn-ghost btn-block" href="{% url 'account:settings' %}">
72-
<i class="fa-solid fa-gear"></i>
73-
打开设置
74-
</a>
75-
</div>
49+
50+
<div class="card-actions justify-end mt-4">
51+
<button type="submit" class="btn btn-primary">保存更改</button>
52+
</div>
53+
</form>
7654
</div>
7755
</div>
7856
{% endblock %}
79-

0 commit comments

Comments
 (0)