merge develop & 手动注释

pull/14/head
guqi 3 months ago
parent 7f4f413d9a
commit f641a52a2d

@ -1,142 +1,225 @@
# 导入JSON模块用于将结构化数据转换为JSON格式
import json
# 导入Django工具函数用于移除HTML标签提取纯文本
from django.utils.html import strip_tags
# 导入Django模板过滤器当前未使用预留用于文本截断
from django.template.defaultfilters import truncatewords
# 导入插件基类,所有插件需继承此类实现标准化接口
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入插件钩子管理模块,用于注册插件功能到指定钩子
from djangoblog.plugin_manage import hooks
# 导入博客数据模型,用于获取文章、分类、标签等数据
from blog.models import Article, Category, Tag
# 导入工具函数,用于获取博客站点的基础配置(如站点名称、关键词等)
from djangoblog.utils import get_blog_setting
class SeoOptimizerPlugin(BasePlugin):
# 插件名称,用于在插件管理界面展示
PLUGIN_NAME = 'SEO 优化器'
# 插件功能描述,说明插件的核心作用
PLUGIN_DESCRIPTION = '为文章、页面等提供 SEO 优化,动态生成 meta 标签和 JSON-LD 结构化数据。'
# 插件版本号,用于版本管理
PLUGIN_VERSION = '0.2.0'
# 插件作者信息
PLUGIN_AUTHOR = 'liuangliangyy'
def register_hooks(self):
"""注册插件钩子将SEO处理逻辑绑定到页面<head>区域的meta标签生成环节"""
# 当系统渲染<head>中的meta标签时触发dispatch_seo_generation方法
hooks.register('head_meta', self.dispatch_seo_generation)
def _get_article_seo_data(self, context, request, blog_setting):
"""
生成文章详情页的SEO数据私有方法仅内部调用
参数
context模板上下文包含当前页面的文章对象等数据
requestHTTP请求对象用于构建绝对URL
blog_setting博客站点配置信息如站点名称关键词
返回
包含SEO相关数据的字典若上下文无有效文章对象则返回None
"""
# 从上下文获取文章对象
article = context.get('article')
# 校验是否为有效的Article实例避免非文章页误处理
if not isinstance(article, Article):
return None
# 生成描述信息移除文章内容中的HTML标签截取前150字符符合多数搜索引擎的描述长度建议
description = strip_tags(article.body)[:150]
# 生成关键词:优先使用文章标签,若无标签则使用站点默认关键词
keywords = ",".join([tag.name for tag in article.tags.all()]) or blog_setting.site_keywords
# 生成Open GraphOG协议标签用于优化社交平台分享效果
meta_tags = f'''
<meta property="og:type" content="article"/>
<meta property="og:title" content="{article.title}"/>
<meta property="og:description" content="{description}"/>
<meta property="og:url" content="{request.build_absolute_uri()}"/>
<meta property="article:published_time" content="{article.pub_time.isoformat()}"/>
<meta property="article:modified_time" content="{article.last_modify_time.isoformat()}"/>
<meta property="article:author" content="{article.author.username}"/>
<meta property="article:section" content="{article.category.name}"/>
<meta property="og:type" content="article"/> <!-- 内容类型为文章 -->
<meta property="og:title" content="{article.title}"/> <!-- 社交分享标题 -->
<meta property="og:description" content="{description}"/> <!-- 社交分享描述 -->
<meta property="og:url" content="{request.build_absolute_uri()}"/> <!-- 文章完整URL -->
<meta property="article:published_time" content="{article.pub_time.isoformat()}"/> <!-- 发布时间ISO标准格式 -->
<meta property="article:modified_time" content="{article.last_modify_time.isoformat()}"/> <!-- 最后修改时间 -->
<meta property="article:author" content="{article.author.username}"/> <!-- 作者信息 -->
<meta property="article:section" content="{article.category.name}"/> <!-- 所属分类 -->
'''
# 为文章的每个标签添加OG标签
for tag in article.tags.all():
meta_tags += f'<meta property="article:tag" content="{tag.name}"/>'
# 添加站点名称OG标签关联文章所属站点
meta_tags += f'<meta property="og:site_name" content="{blog_setting.site_name}"/>'
# 生成JSON-LD结构化数据帮助搜索引擎理解页面内容结构
structured_data = {
"@context": "https://schema.org",
"@type": "Article",
"mainEntityOfPage": {"@type": "WebPage", "@id": request.build_absolute_uri()},
"headline": article.title,
"description": description,
"@context": "https://schema.org", # 遵循schema.org的结构化数据标准
"@type": "Article", # 内容类型为文章
"mainEntityOfPage": { # 声明页面的主体内容
"@type": "WebPage",
"@id": request.build_absolute_uri() # 页面唯一标识完整URL
},
"headline": article.title, # 文章标题
"description": description, # 文章描述
# 文章首图通过绝对URL访问增强内容丰富度
"image": request.build_absolute_uri(article.get_first_image_url()),
"datePublished": article.pub_time.isoformat(),
"dateModified": article.last_modify_time.isoformat(),
"author": {"@type": "Person", "name": article.author.username},
"publisher": {"@type": "Organization", "name": blog_setting.site_name}
"datePublished": article.pub_time.isoformat(), # 发布时间
"dateModified": article.last_modify_time.isoformat(), # 修改时间
"author": { # 作者信息
"@type": "Person",
"name": article.author.username
},
"publisher": { # 发布机构信息
"@type": "Organization",
"name": blog_setting.site_name
}
}
# 若文章无图片则移除image字段避免空值影响结构化数据有效性
if not structured_data.get("image"):
del structured_data["image"]
# 返回整合后的文章页SEO数据
return {
"title": f"{article.title} | {blog_setting.site_name}",
"title": f"{article.title} | {blog_setting.site_name}", # 页面标题(文章标题+站点名称,增强品牌关联)
"description": description,
"keywords": keywords,
"meta_tags": meta_tags,
"json_ld": structured_data
"meta_tags": meta_tags, # 包含OG标签的HTML片段
"json_ld": structured_data # JSON-LD结构化数据
}
def _get_category_seo_data(self, context, request, blog_setting):
"""生成分类页面的SEO数据私有方法"""
# 从上下文获取分类名称变量名tag_name可能为笔误实际应为分类名称
category_name = context.get('tag_name')
if not category_name:
return None
# 根据名称查询分类对象
category = Category.objects.filter(name=category_name).first()
if not category:
if not category: # 若分类不存在返回None
return None
# 页面标题:分类名称+站点名称
title = f"{category.name} | {blog_setting.site_name}"
# 描述:使用分类名称或站点默认描述
description = strip_tags(category.name) or blog_setting.site_description
# 关键词:使用分类名称
keywords = category.name
# BreadcrumbList structured data for category page
breadcrumb_items = [{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}]
breadcrumb_items.append({"@type": "ListItem", "position": 2, "name": category.name, "item": request.build_absolute_uri()})
# 生成面包屑导航的JSON-LD数据帮助搜索引擎理解页面在站点中的层级位置
breadcrumb_items = [
# 首页面包屑项位置1
{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}
]
# 当前分类面包屑项位置2
breadcrumb_items.append({
"@type": "ListItem",
"position": 2,
"name": category.name,
"item": request.build_absolute_uri()
})
structured_data = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": breadcrumb_items
"@type": "BreadcrumbList", # 类型为面包屑列表
"itemListElement": breadcrumb_items # 面包屑项列表
}
# 返回分类页SEO数据
return {
"title": title,
"description": description,
"keywords": keywords,
"meta_tags": "",
"meta_tags": "", # 分类页暂无需额外meta标签
"json_ld": structured_data
}
def _get_default_seo_data(self, context, request, blog_setting):
# Homepage and other default pages
"""生成默认页面如首页、未匹配的页面的SEO数据私有方法"""
# 生成网站级别的JSON-LD数据包含站点基本信息和搜索功能描述
structured_data = {
"@context": "https://schema.org",
"@type": "WebSite",
"url": request.build_absolute_uri('/'),
"@type": "WebSite", # 类型为网站
"url": request.build_absolute_uri('/'), # 网站首页URL
# 描述站点的搜索功能(帮助搜索引擎识别并支持站内搜索)
"potentialAction": {
"@type": "SearchAction",
# 搜索结果页URL模板{search_term_string}为搜索关键词占位符)
"target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}",
"query-input": "required name=search_term_string"
"query-input": "required name=search_term_string" # 声明搜索参数为必填项
}
}
# 返回默认页SEO数据
return {
"title": f"{blog_setting.site_name} | {blog_setting.site_description}",
"description": blog_setting.site_description,
"keywords": blog_setting.site_keywords,
"meta_tags": "",
"title": f"{blog_setting.site_name} | {blog_setting.site_description}", # 首页标题(站点名称+描述)
"description": blog_setting.site_description, # 站点描述
"keywords": blog_setting.site_keywords, # 站点默认关键词
"meta_tags": "", # 默认页无需额外meta标签
"json_ld": structured_data
}
def dispatch_seo_generation(self, metas, context):
"""
分发SEO数据生成逻辑核心方法根据当前页面类型调用对应的数据生成方法
参数
metas原始的meta标签内容未使用预留用于扩展
context模板上下文包含请求对象和页面数据
返回
生成的完整SEO标签包含titlemeta标签JSON-LD脚本等
"""
# 从上下文获取请求对象用于判断页面类型和构建URL
request = context.get('request')
if not request:
if not request: # 若无请求对象,返回原始内容
return metas
# 获取当前视图的名称通过Django的URL解析器用于区分页面类型
view_name = request.resolver_match.view_name
# 获取博客站点配置
blog_setting = get_blog_setting()
# 根据页面类型视图名称生成对应的SEO数据
seo_data = None
if view_name == 'blog:detailbyid':
# 文章详情页调用文章SEO数据生成方法
seo_data = self._get_article_seo_data(context, request, blog_setting)
elif view_name == 'blog:category_detail':
# 分类详情页调用分类SEO数据生成方法
seo_data = self._get_category_seo_data(context, request, blog_setting)
# 若未匹配到特定页面类型使用默认SEO数据
if not seo_data:
seo_data = self._get_default_seo_data(context, request, blog_setting)
seo_data = self._get_default_seo_data(context, request, blog_setting)
# 生成JSON-LD脚本标签将结构化数据转换为JSON字符串确保非ASCII字符正常显示
json_ld_script = f'<script type="application/ld+json">{json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}</script>'
# 拼接所有SEO相关标签并返回最终会被插入到页面的<head>区域
return f"""
<title>{seo_data.get("title", "")}</title>
<meta name="description" content="{seo_data.get("description", "")}">
<meta name="keywords" content="{seo_data.get("keywords", "")}">
{seo_data.get("meta_tags", "")}
{json_ld_script}
<title>{seo_data.get("title", "")}</title> <!-- 页面标题SEO核心要素 -->
<meta name="description" content="{seo_data.get("description", "")}"> <!-- 描述标签影响搜索结果展示 -->
<meta name="keywords" content="{seo_data.get("keywords", "")}"> <!-- 关键词标签 -->
{seo_data.get("meta_tags", "")} <!-- 额外meta标签如Open Graph -->
{json_ld_script} <!-- JSON-LD结构化数据脚本提升搜索结果丰富度 -->
"""
plugin = SeoOptimizerPlugin()
# 实例化插件,使插件系统能够识别并加载该插件
plugin = SeoOptimizerPlugin()

@ -1,18 +1,44 @@
# 导入Django博客系统的插件基类所有自定义插件需继承此类以实现标准化接口
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入插件钩子管理模块,用于将插件功能绑定到系统预设的钩子点
from djangoblog.plugin_manage import hooks
# 定义文章浏览次数统计插件类继承自插件基类BasePlugin
class ViewCountPlugin(BasePlugin):
# 插件名称:在插件管理界面展示,用于区分不同插件
PLUGIN_NAME = '文章浏览次数统计'
# 插件功能描述:说明插件的核心作用,方便管理员理解用途
PLUGIN_DESCRIPTION = '统计文章的浏览次数'
# 插件版本号:用于版本管理,便于后续更新和兼容性判断
PLUGIN_VERSION = '0.1.0'
# 插件作者信息:标注开发者,便于维护和沟通
PLUGIN_AUTHOR = 'liangliangyy'
def register_hooks(self):
"""
注册插件钩子将统计逻辑绑定到系统的特定触发点
作用告诉插件系统在哪个时机执行当前插件的功能
"""
# 绑定规则:
# 1. 'after_article_body_get' 是系统预设的钩子名称,代表“文章内容获取完成后”的时机
# 2. self.record_view 是当前插件的核心方法,即钩子触发时要执行的逻辑
# 场景:当用户访问文章详情页,系统成功获取文章内容后,自动触发浏览次数统计
hooks.register('after_article_body_get', self.record_view)
def record_view(self, article, *args, **kwargs):
"""
核心统计方法执行文章浏览次数的记录操作
参数说明
article钩子传递的文章对象即当前被访问的文章必须是Article模型实例
*args, **kwargs预留参数用于接收钩子传递的额外信息如请求对象等保证扩展性
"""
# 调用文章对象的viewed()方法:
# 该方法应由Article模型预先实现通常逻辑为“将view_count字段+1并保存到数据库”
# 插件通过调用模型方法实现统计,解耦插件与数据模型的直接操作,符合设计规范
article.viewed()
plugin = ViewCountPlugin()
# 实例化插件类:
# 插件系统会扫描并加载该实例,使上述注册的钩子和功能生效
plugin = ViewCountPlugin()

@ -1,29 +1,56 @@
{# 1. 模板继承:继承基础账户页面模板,复用头部、底部、样式等公共组件 #}
{# base_account.html 通常包含账户相关页面(登录、注册、密码重置)的通用布局 #}
{% extends 'share_layout/base_account.html' %}
{% load i18n %}
{% load static %}
{# 2. 加载Django内置模板标签库 #}
{% load i18n %} {# 加载国际化标签库,用于实现多语言文本(如英文/中文切换) #}
{% load static %} {# 加载静态文件标签库用于引用CSS、JS、图片等静态资源 #}
{# 3. 重写父模板的content块定义当前页面的核心内容 #}
{# 父模板中会预留content块子模板通过重写该块注入页面专属内容 #}
{% block content %}
<div class="container">
<div class="container"> {# 容器组件使用Bootstrap等样式框架的容器类实现内容居中、响应式布局 #}
{# 页面标题显示“忘记密码”多语言支持实际文本由i18n翻译决定 #}
<h2 class="form-signin-heading text-center">{% trans 'forget the password' %}</h2>
{# 卡片容器:包裹密码重置表单,通过卡片样式提升页面美观度和层次感 #}
<div class="card card-signin">
{# 头像图片:引用静态文件夹中的默认头像,增强页面视觉识别度 #}
{# static标签会自动拼接静态文件根路径避免硬编码URL #}
<img class="img-circle profile-img" src="{% static 'blog/img/avatar.png' %}" alt="">
{# 密码重置表单提交到账户应用的forget_password视图函数 #}
<form class="form-signin" action="{% url 'account:forget_password' %}" method="post">
{# CSRF令牌Django内置安全机制防止跨站请求伪造CSRF攻击 #}
{# 所有POST表单必须包含该标签否则请求会被Django拦截 #}
{% csrf_token %}
{# 显示表单非字段级错误:如验证码错误、邮箱不存在等全局错误 #}
{# form.non_field_errors 存储表单整体验证失败的错误信息 #}
{{ form.non_field_errors }}
{# 循环渲染表单字段:动态生成所有密码重置相关字段(如邮箱输入框、验证码输入框) #}
{# form为视图传递的表单对象field遍历表单中的每个字段自动生成input标签 #}
{% for field in form %}
{{ field }}
{{ field.errors }}
{{ field }} {# 渲染单个字段的输入框(如<input type="email" name="email"> #}
{{ field.errors }} {# 显示当前字段的验证错误(如邮箱格式错误) #}
{% endfor %}
<input type="button" class="button" id="btn" value="{% trans 'get verification code' %}">
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans 'submit' %}</button>
{# 验证码按钮用于触发获取验证码的逻辑需配合JS实现点击发送请求 #}
<input type="button" class="button" id="btn" value="{% trans 'get verification code' %}">
{# 提交按钮点击后将表单数据POST到指定视图 #}
{# 使用Bootstrap样式类btn、btn-lg等实现按钮美化和响应式 #}
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans 'submit' %}</button>
</form>
</div>
<p class="text-center">
<a href="/">Home Page</a>
|
<a href="{% url "account:login" %}">login page</a>
{# 辅助链接:提供首页和登录页的跳转入口,提升用户体验 #}
<p class="text-center"> {# 文本居中显示 #}
<a href="/">Home Page</a> {# 跳转到网站首页(硬编码根路径,也可改用{% url 'home' %} #}
| {# 分隔符,增强链接可读性 #}
<a href="{% url "account:login" %}">login page</a> {# 跳转到登录页使用URL反向解析避免硬编码 #}
</p>
</div> <!-- /container -->

@ -1,42 +1,74 @@
{# 1. 模板继承:复用账户相关页面的公共布局(如头部导航、底部信息、基础样式) #}
{# base_account.html 通常是登录、注册、密码重置等页面的父模板,保证风格统一 #}
{% extends 'share_layout/base_account.html' %}
{% load static %}
{% load i18n %}
{# 2. 加载模板标签库:引入必要的功能支持 #}
{% load static %} {# 加载静态文件标签库用于引用图片、CSS、JS等资源 #}
{% load i18n %} {# 加载国际化标签库,实现多语言文本切换(如中英文) #}
{# 3. 重写父模板的content块定义当前登录页的核心内容 #}
{# 父模板预留content块子模板通过重写注入专属内容实现布局复用 #}
{% block content %}
<div class="container">
<div class="container"> {# 容器组件基于Bootstrap等框架实现内容居中、响应式适配 #}
{# 页面标题:登录页主标题,居中显示,明确页面用途 #}
<h2 class="form-signin-heading text-center">Sign in with your Account</h2>
{# 卡片容器:包裹登录表单,通过卡片样式提升视觉层次和美观度 #}
<div class="card card-signin">
<img class="img-circle profile-img" src="{% static 'blog/img/avatar.png' %}" alt="">
{# 默认头像:引用静态文件夹中的头像图片,增强页面视觉识别性 #}
{# static标签自动拼接静态文件根路径避免硬编码URL导致的路径问题 #}
<img class="img-circle profile-img" src="{% static 'blog/img/avatar.png' %}" alt="默认头像">
{# 登录表单核心交互区域数据提交到account应用的login视图 #}
<form class="form-signin" action="{% url 'account:login' %}" method="post">
{# CSRF令牌Django安全机制防止跨站请求伪造CSRF攻击 #}
{# 所有POST方法的表单必须包含该标签否则请求会被Django拦截拒绝 #}
{% csrf_token %}
{# 显示表单全局错误:非单个字段的错误(如“用户名或密码错误”“账号已锁定”) #}
{{ form.non_field_errors }}
{# 循环渲染表单字段:动态生成所有登录相关输入框(如用户名、密码) #}
{# form为视图传递的表单对象如LoginFormfield自动匹配字段类型text/password #}
{% for field in form %}
{{ field }}
{{ field.errors }}
{{ field }} {# 渲染字段输入框(如<input type="text" name="username"> #}
{{ field.errors }} {# 显示当前字段的验证错误(如“用户名不能为空”“密码格式错误”) #}
{% endfor %}
{# 隐藏字段:登录成功后的跳转地址 #}
{# redirect_to由视图传递通常是用户登录前尝试访问的页面如文章详情页提升体验 #}
<input type="hidden" name="next" value="{{ redirect_to }}">
{# 登录提交按钮使用Bootstrap样式类实现美化蓝色、大尺寸、全屏宽度 #}
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
{# 记住登录状态选项:复选框,允许用户选择是否保持登录(减少重复登录) #}
<div class="checkbox">
{# 注释代码:预留“需要帮助”链接位置,后续可扩展功能 #}
{% comment %}<a class="pull-right">Need help?</a>{% endcomment %}
<label>
{# name="remember"与后端逻辑关联,值"remember-me"用于标识“记住登录”状态 #}
<input type="checkbox" value="remember-me" name="remember"> Stay signed in
</label>
</div>
{% load oauth_tags %}
{% load_oauth_applications request%}
{# 第三方登录功能加载OAuth登录标签库渲染第三方登录按钮如GitHub、Google登录 #}
{% load oauth_tags %} {# 加载自定义OAuth标签库 #}
{% load_oauth_applications request%} {# 调用标签渲染第三方登录入口,依赖请求对象获取配置 #}
</form>
</div>
<p class="text-center">
{# 辅助链接区域:提供注册、首页、密码重置的跳转入口,覆盖用户不同需求 #}
<p class="text-center"> {# 文本居中,提升页面整洁度 #}
{# 注册链接:跳转到账户注册页,使用国际化标签,支持多语言文本 #}
<a href="{% url "account:register" %}">
{% trans 'Create Account' %}
</a>
| {# 分隔符:视觉上区分不同链接,提升可读性 #}
<a href="/">Home Page</a> {# 首页链接:硬编码根路径(也可改用{% url 'home' %}实现反向解析) #}
|
<a href="/">Home Page</a>
|
{# 密码重置链接:跳转到忘记密码页,支持多语言文本 #}
<a href="{% url "account:forget_password" %}">
{% trans 'Forget Password' %}
</a>

@ -1,28 +1,53 @@
{# 1. 模板继承:复用账户相关页面的公共布局 #}
{# base_account.html 通常包含登录、注册、密码重置等页面的通用组件(如头部样式、底部信息)#}
{# 继承后只需专注于当前注册页的专属内容,减少重复代码并保证风格统一 #}
{% extends 'share_layout/base_account.html' %}
{% load static %}
{# 2. 加载模板标签库:引入静态资源处理功能 #}
{% load static %} {# 用于引用图片、CSS、JS等静态文件避免硬编码路径导致的问题 #}
{# 3. 重写父模板的content块定义注册页的核心内容 #}
{# 父模板会预留content块子模板通过重写该块注入当前页面的具体内容 #}
{% block content %}
<div class="container">
<div class="container"> {# 容器组件基于Bootstrap等样式框架实现内容居中、响应式适配适配手机/电脑屏幕) #}
{# 页面标题:明确当前页面为“创建账户”,居中显示以增强视觉引导 #}
<h2 class="form-signin-heading text-center">Create Your Account</h2>
{# 卡片容器:包裹注册表单,通过卡片样式(阴影、圆角)提升页面层次感和美观度 #}
<div class="card card-signin">
<img class="img-circle profile-img" src="{% static 'blog/img/avatar.png' %}" alt="">
{# 注册页默认头像:引用静态文件夹中的头像图片 #}
{# static标签会自动拼接Django配置的静态文件根路径确保图片能正确加载 #}
{# img-circle类使图片显示为圆形符合用户头像的常见设计 #}
<img class="img-circle profile-img" src="{% static 'blog/img/avatar.png' %}" alt="默认用户头像">
{# 注册表单核心交互区域数据提交到account应用的register视图函数 #}
<form class="form-signin" action="{% url 'account:register' %}" method="post">
{# CSRF令牌Django内置的安全机制防止跨站请求伪造CSRF攻击 #}
{# 所有使用POST方法的表单必须包含该标签否则请求会被Django拦截拒绝 #}
{% csrf_token %}
{# 显示表单全局错误:非单个字段的错误(如“用户名已存在”“两次密码不一致”等) #}
{# form.non_field_errors 存储表单整体验证失败的错误信息,直接渲染即可显示 #}
{{ form.non_field_errors }}
{# 循环渲染表单字段:动态生成所有注册所需的输入框(如用户名、密码、邮箱等) #}
{# form为视图函数传递的表单对象如RegisterFormfield会自动匹配字段类型text/password/email #}
{% for field in form %}
{{ field }}
{{ field.errors }}
{{ field }} {# 渲染单个字段的输入框(如<input type="text" name="username"> #}
{{ field.errors }} {# 显示当前字段的验证错误(如“用户名不能为空”“密码长度不足”) #}
{% endfor %}
{# 注册提交按钮使用Bootstrap样式类btn-lg/btn-primary/btn-block实现“大尺寸、蓝色、全屏宽度”的按钮 #}
{# 点击后将表单数据以POST方式提交到register视图完成用户注册逻辑 #}
<button class="btn btn-lg btn-primary btn-block" type="submit">Create Your Account</button>
</form>
</div>
<p class="text-center">
<a href="{% url "account:login" %}">Sign In</a>
{# 辅助链接:提供登录页跳转入口,方便已有账号的用户切换到登录流程 #}
<p class="text-center"> {# 文本居中显示,提升页面整洁度 #}
<a href="{% url "account:login" %}">Sign In</a> {# 通过URL反向解析跳转到登录页避免硬编码URL #}
</p>
</div> <!-- /container -->

Loading…
Cancel
Save