Bootstrap5
简单介绍
Bootstrap 5 是最新版本的 Bootstrap(截至2025年),它是一个免费、开源的 前端框架,用于快速构建响应式、移动优先的网站和 Web 应用。
- 官网:https://getbootstrap.com
- 中文文档:https://v5.bootcss.com
- Bootstrap Icons:https://icons.getbootstrap.com
引入Bootstrap5
下载相关资源
文件名 | 作用 | 是否必须 |
---|---|---|
bootstrap.min.css |
Bootstrap 的压缩版 CSS 样式文件 | ✅ 必须 |
bootstrap.min.js |
Bootstrap 的压缩版 JavaScript 功能文件 | ✅ 必须(如需交互组件) |
popper.min.js |
Popper.js(用于下拉菜单、弹出框等定位) | ✅ 必须(Bootstrap 5 依赖它) |
点击 “Compiled CSS and JS” 下载完整包(包含所有文件)。
解压后,在
dist/
文件夹中找到:css/bootstrap.min.css
js/bootstrap.min.js
Bootstrap 5 默认不提供单独
popper.min.js
,而是内置在bootstrap.bundle.min.js
正确做法:直接使用
bootstrap.bundle.min.js
,避免手动管理 Popper。如需下载
popper.min.js
可以访问Popper 官方源码页面: https://unpkg.com/@popperjs/core@2/dist/umd/下载后,放入到
static
的包下
加载静态资源
配置STATICFILES_DIRS
# Django 方式
STATICFILES_DIRS = [
# 这个 static 可以变成 ABC 只是一个名称
BASE_DIR / "static"
]
配置builtins
,这样可以以后不使用{% load static %}
加载静态资源
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
# 加在这里
"builtins": ["django.templatetags.static"],
},
},
]
在模版中引入Bootstrap5
<head>
<meta charset="UTF-8">
<title>LinNote<title/>
{% comment %} 加载CSS样式 {% endcomment %}
<link rel="stylesheet" href="% static 'bootstrap5/bootstrap.min.css' %}">
{% comment %} 加载JS脚本 {% endcomment %}
<script src="{% static 'bootstrap5/popper.min.js'%}"></script>
<script src="{% static 'bootstrap5/bootstrap.min.js'%}"></script>
</head>
导航栏Header
复制导航栏的源码
找到官网导航栏的Example,选一个自己喜欢的。然后右键,然后检查,然后找到elements,找header标签,最后复制,就可以复制源代码。
复制源码之后,放在project/templates/index.html
里面,记得要配置如下:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'] # 要检查这里是否配置
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
否者会报django.template.exceptions.TemplateDoesNotExist: index.html
修改导航栏的源码
修改logo
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<img src="{% static 'image/logo001.png' %}" height="40">
</a>
修改颜色
可以根据官方文档修改颜色
<header class="p-3 text-bg-light">
</header>
添加边框
可以根据官方文档修改边框
<header class="p-3 text-bg-light border-bottom">
</header>
总的效果
<!-- border-bottom: 底部边框 -->
<header class="p-3 text-bg-light border-bottom">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<!-- 设置logo -->
<img src="{% static 'image/logo003.jpg' %}" height="40">
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<!-- secondary 灰色 -->
<li><a href="#" class="nav-link px-2 text-secondary">首页</a></li>
<li><a href="#" class="nav-link px-2 text-secondary">发布博客</a></li>
</ul>
<form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search"> <input type="search"
class="form-control" placeholder="搜索..." aria-label="Search">
</form>
<!-- primary:蓝色 -->
<div class="text-end"> <button type="button" class="btn btn-outline-primary">登录</button> <button
type="button" class="btn btn-primary">注册</button> </div>
</div>
</div>
</header>
以下是效果图:
首页布局
调整背景
在static/css/base.css
下面写入如下css样式
body {
/* rgba :r-红色,g-绿色,b-蓝色,a-透明度 */
/* (0, 0, 0, 0.1) 前面3个0代表黑色,后面0.1的透明度,会把黑色变成灰色 */
background-color: rgba(0, 0, 0, 0.1);
}
在index.html
里面引入css
样式
<!-- 加载CSS样式 -->
<link rel="stylesheet" href="{% static 'bootstrap5/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/base.css' %}">
实现效果:
container
container 是 Bootstrap 5 中最基础的布局容器,用于控制页面内容的宽度、对齐和响应式间距。它的核心作用是创建水平居中的内容区域,并自动适应不同屏幕尺寸。
主要特点
特性 | 说明 |
---|---|
响应式宽度 | 根据屏幕尺寸自动调整最大宽度(如 PC 端 1140px ,平板 720px )。 |
水平居中 | 自动左右外边距 auto ,使内容始终居中。 |
内边距(Gutters) | 默认两侧有 12px 内边距(可通过 g-* 工具类调整)。 |
嵌套支持 | 允许内部再嵌套 container 或 container-fluid 。 |
三种容器类型
类名 | 行为 | 适用场景 |
---|---|---|
.container |
固定宽度,响应式断点调整宽度(默认)。 | 大多数常规布局(如博客、后台)。 |
.container-fluid |
100% 宽度,始终撑满视口。 | 全屏轮播、仪表盘。 |
.container-{breakpoint} |
特定断点前 100% 宽度,达到断点后变为固定宽度(如 .container-md )。 |
需要混合响应的场景。 |
使用
<main class="container bg-white">
<h1>博客列表</h1>
</main>
增加边距
边距相关的文档在spacing里面
Margin(外边距)
控制元素边界外部的透明区域,决定与其他元素的距离。
<!-- border-bottom: 底部边框 -->
<!-- m - for classes that set margin -->
<!-- b - for classes that set margin-bottom or padding-bottom -->
<!-- 3 - (by default) for classes that set the margin or padding to $spacer -->
<header class="p-3 text-bg-light border-bottom mb-3">
</header>
Padding(内边距)
控制元素内容与边框之间的空白区域。
<!-- p - for classes that set padding -->
<main class="container bg-white p-3">
<h1>博客列表</h1>
</main>
设置圆角
在Borders里面,可以设置圆角
<!-- p - for classes that set padding -->
<!-- rounded 添加圆角 -->
<main class="container bg-white p-3 rounded">
<h1>博客列表</h1>
</main>
设置卡片
官网的参考配置如下:
- card
- borders
- Flex:CSS 的 Flexbox(弹性盒子布局) 是一种一维布局模型,通过 弹性容器(flex container) 和 弹性项目(flex item) 的配合,可以轻松实现灵活的对齐、排序和空间分配。它用简单的属性(如
justify-content
、align-items
)即可控制子元素的水平或垂直排列方式,完美适配响应式设计需求。
<main class="container bg-white p-3 rounded">
<h1>博客列表</h1>
<!-- class="row" 指定一行 -->
<!-- row-cols-2: 一行两列 -->
<!-- row-gap-4: 设置行间距,间距为4 -->
<div class="row row-cols-2 row-gap-4">
<!-- class="col":指定 列 -->
<div class="col">
<!-- text-center:文字居中,删除就不会文字居中了 -->
<div class="card">
<div class="card-header">
<!-- 这里展示文章标题 -->
<a href="#">Django入门</a>
</div>
<!-- height: 100px:设置card-body的固定高度 -->
<div class="card-body" style="height: 100px;">
<!-- 原来的标题不用了 -->
<!-- <h5 class="card-title"></h5> -->
<p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
</div>
<!-- d-flex justify-content-between :设置d-flex -->
<div class="card-footer text-muted d-flex justify-content-between">
<div>
<img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
<!-- 用户名 -->
Lin
</div>
<div>
<!-- 先填一个假的发布时间 -->
发布时间:2025年12月12日
</div>
</div>
</div>
</div>
</div>
</main>
详情页布局
初步效果
<!-- p - for classes that set padding -->
<!-- rounded 添加圆角 -->
<main class="container bg-white p-3 rounded">
<h1>Django入门详情</h1>
<!-- hr: 会显示一条横线 -->
<hr />
</main>
作者与发布时间
<!-- p - for classes that set padding -->
<!-- rounded 添加圆角 -->
<main class="container bg-white p-3 rounded">
<h1>Django入门详情</h1>
<!-- hr: 会显示一条横线 -->
<hr />
<!-- mt-2: spacing内容,t - for classes that set margin-top or padding-top -->
<div class="mt-2">
<img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
<!-- s - (start) for classes that set margin-left or padding-left in LTR,
margin-right or padding-right in RTL -->
<span class="ms-2">Lin</span>
<span class="ms-2">于</span>
<span class="ms-2">2025年12月12日</span>发布
</div>
<hr />
</main>
博客详情
<!-- y - for classes that set both *-top and *-bottom -->
<!-- py-2: 上下内边距调整2 -->
<div class="py-2">
这是博客详情
</div>
<hr />
发表评论
<div class="mt-2">
<h3>评论(10)</h3>
<form action="">
<div class="mt-2">
<!-- form-control: bootstrap的表单样式 -->
<input type="text" class="form-control" placeholder="请输入评论">
</div>
<!-- text-end 的本质:相当于 CSS 的 text-align: right,
会将容器内所有行内/行内块元素向右对齐。 -->
<!-- 按钮的默认表现:Bootstrap 的 .btn 按钮本质是 display: inline-block(行内块元素),
因此会受父容器文本对齐方式影响。 -->
<div class="text-end mt-2">
<!-- btn btn-primary:bootstrap的按钮样式 -->
<button type="button" class="btn btn-primary">评论</button>
</div>
</form>
</div>
涉及bootstrap5的内容:
评论列表
更多列表组件查看List group
<div class="mt-2">
<!-- list-group list-group-flush: bootstrap的列表样式 -->
<ul class="list-group list-group-flush">
<li class="list-group-item">
<!-- class="user-info" 和 class="create-time" 是 HTML 元素的类名(CSS 类),它们本身不带任何功能,
但通常被用作前端样式或脚本的“钩子”,让你可以通过 CSS 或 JavaScript 对这些元素进行样式设置或操作控制。 -->
<!-- text-secondary: 设置字体颜色 -->
<div class="d-flex justify-content-between text-secondary">
<!-- class="user-info":不是 Bootstrap 的默认类,而是开发者自定义的 -->
<div class="user-info">
<img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
<span class="ms-2">Lin</span>
</div>
<!-- class="create-time":不是 Bootstrap 的默认类,而是开发者自定义的 -->
<!-- line-height: 40px 是 CSS 属性,用于控制文本行高
该日期文本会在一个 40px 高的透明盒子中垂直居中-->
<div class="create-time" style="line-height: 40px;">2025年12月12日 12:12</div>
</div>
<!-- 这里的目的是为了对齐作者 -->
<div style="padding-left: 30px;">
<div class="ms-2">这是一个评论内容</div>
</div>
</li>
</ul>
</div>
发布博客页面布局
代码
<head>
<meta charset="UTF-8">
<title>LinNote</title>
<!-- 加载CSS样式 -->
<link rel="stylesheet" href="{% static 'bootstrap5/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/base.css' %}">
<!-- 加载JS脚本 -->
<script src="{% static 'bootstrap5/popper.min.js'%}"></script>
<script src="{% static 'bootstrap5/bootstrap.min.js'%}"></script>
<!-- 加载wangeditor 的css 样式 和 js 脚本 -->
<link rel="stylesheet" href="{% static 'wangeditor/style.css' %}">
<script src="{% static 'wangeditor/index.js'%}"></script>
<!-- 定义富文本编辑器的样式 -->
<!-- 这里不用,因为已经下载了,放在wangeditor/style.css下面 -->
<!-- <link href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" rel="stylesheet" /> -->
<style>
/* 工具栏+编辑器 */
#editor—wrapper {
border: 1px solid #ccc;
z-index: 100;
/* 按需定义 */
}
/* 工具栏 */
#toolbar-container {
border-bottom: 1px solid #ccc;
}
/* 编辑器 */
#editor-container {
height: 500px;
}
</style>
<!-- 引入wangeditor的 JS代码 -->
<script src="{% static 'js/pub_blog.js'%}"></script>
</head>
<body>
<!-- border-bottom: 底部边框 -->
<!-- m - for classes that set margin -->
<!-- b - for classes that set margin-bottom or padding-bottom -->
<!-- 3 - (by default) for classes that set the margin or padding to $spacer -->
<header class="p-3 text-bg-light border-bottom mb-3">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<!-- 设置logo -->
<img src="{% static 'image/logo003.jpg' %}" height="40">
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<!-- secondary 灰色 -->
<li><a href="#" class="nav-link px-2 text-secondary">首页</a></li>
<li><a href="#" class="nav-link px-2 text-secondary">发布博客</a></li>
</ul>
<form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search"> <input type="search"
class="form-control" placeholder="搜索..." aria-label="Search">
</form>
<!-- primary:蓝色 -->
<div class="text-end"> <button type="button" class="btn btn-outline-primary">登录</button> <button
type="button" class="btn btn-primary">注册</button> </div>
</div>
</div>
</header>
<!-- p - for classes that set padding -->
<!-- rounded 添加圆角 -->
<main class="container bg-white p-3 rounded">
<h1>发布博客</h1>
<div class="mt-3">
<form action="">
<div class="mb-3">
<!-- 以下是bootstrap的样式 -->
<lable class="form-lable">标题</lable>
<input type="text" name="title" class="form-control">
</div>
<div class="mb-3">
<!-- 以下是bootstrap的样式 -->
<lable class="form-lable">分类</lable>
<select name="category" class="form-select">
<!-- 这里先写两个假的数据,有序写成真的 -->
<option value="1">Python</option>
<option value="2">前端</option>
</select>
</div>
<div class="mb-3">
<lable class="form-lable">内容</lable>
<!--使用wangeditor的富文本编辑器-->
<div id="editor—wrapper">
<div id="toolbar-container"><!-- 工具栏 --></div>
<div id="editor-container"><!-- 编辑器 --></div>
</div>
</div>
<div class="mb-3 text-end">
<button class="btn btn-primary">发布</button>
</div>
</form>
</div>
</main>
</body>
如何引入富文本编辑器
引入 CSS 定义样式。项目中直接下载了文件。
<!-- 定义富文本编辑器的样式 --> <!-- 这里不用,因为已经下载了,放在wangeditor/style.css下面 --> <!-- <link href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" rel="stylesheet" /> --> <style> /* 工具栏+编辑器 */ #editor—wrapper { border: 1px solid #ccc; z-index: 100; /* 按需定义 */ } /* 工具栏 */ #toolbar-container { border-bottom: 1px solid #ccc; } /* 编辑器 */ #editor-container { height: 500px; } </style>
定义 HTML 结构
<!--使用wangeditor的富文本编辑器--> <div id="editor—wrapper"> <div id="toolbar-container"><!-- 工具栏 --></div> <div id="editor-container"><!-- 编辑器 --></div> </div>
引入 JS 创建编辑器
引入js的相关资源
<script src="{% static 'wangeditor/index.js'%}"></script> <!--这个不需要,因为已经下载了--> <!--<script src="https://unpkg.com/@wangeditor/editor@latest/dist/index.js"></script>-->
创建编辑器,放在单独的js文件里面
// 当浏览器完全加载页面中的所有内容(包括 HTML、CSS、图片、脚本、iframe 等外部资源)后,window.onload 会触发绑定的函数 window.onload = function () { const { createEditor, createToolbar } = window.wangEditor const editorConfig = { placeholder: 'Type here...', // 这里有一个监听事件,哟任何变化都会在这里打印 onChange(editor) { const html = editor.getHtml() console.log('editor content', html) // 也可以同步到 <textarea> } } const editor = createEditor({ selector: '#editor-container', html: '<p><br></p>', config: editorConfig, mode: 'default', // or 'simple' }) }
由于把js脚本单独放在一个文件里面,所有还需要再次引入这个js文件
<!-- 引入wangeditor的 JS代码 --> <script src="{% static 'js/pub_blog.js'%}"></script>
登录注册页面的布局
登陆
<!-- p - for classes that set padding -->
<!-- rounded 添加圆角 -->
<main class="container bg-white p-3 rounded">
<!--m-auto:使用 Bootstrap 的工具类,水平居中容器(通过自动左右外边距实现)。 -->
<!--style="max-width: 330px":使用 Bootstrap 的工具类,水平居中容器(通过自动左右外边距实现)。 -->
<div style="max-width: 330px;" class="m-auto">
<h1>登录</h1>
<div class="mb-3">
<input type="email" name="email" class="form-control" placeholder="邮箱">
</div>
<div class="mb-3">
<input type="password" name="password" class="form-control" placeholder="密码">
</div>
<!-- bootstrap的复选框 -->
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="flexCheckDefault">
<label class="form-check-label" for="flexCheckDefault">
请记住我
</label>
</div>
<div class="mb-3">
<!-- w = width(宽度)
100 = 100%(百分比)
组合效果:设置元素的宽度为父容器宽度的 100%(等同于 CSS 的 width: 100%;) -->
<button class="btn btn-primary w-100">立即登录</button>
</div>
</div>
</main>
注册
- 验证码的框:input-group
<!-- p - for classes that set padding -->
<!-- rounded 添加圆角 -->
<main class="container bg-white p-3 rounded">
<div style="max-width: 330px;" class="m-auto">
<h1>注册</h1>
<form action="" method="POST">
<div class="mb-3">
<input type="text" name="username" class="form-control" placeholder="用户名">
</div>
<div class="mb-3">
<input type="email" name="email" class="form-control" placeholder="邮箱">
</div>
<div class="mb-3">
<!-- bootstrap的样式 -->
<div class="input-group">
<input type="text" class="form-control" name="captcha" placeholder="验证码"
aria-label="Recipient's username" aria-describedby="button-addon2">
<button class="btn btn-outline-secondary" type="button" id="">获取验证码</button>
</div>
</div>
<div class="mb-3">
<input type="password" name="password" class="form-control" placeholder="密码">
</div>
<div class="mb-3">
<!-- w = width(宽度)
100 = 100%(百分比)
组合效果:设置元素的宽度为父容器宽度的 100%(等同于 CSS 的 width: 100%;) -->
<button class="btn btn-primary w-100">注册</button>
</div>
</form>
</div>
</main>
Django 模版化
父模版base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{% comment %} 不同页面会有不同标题 {% endcomment %}
<title>{% block title %}{% endblock title %}-LinNote</title>
<!-- 加载CSS样式 -->
<link rel="stylesheet" href="{% static 'bootstrap5/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/base.css' %}">
<!-- 加载JS脚本 -->
<script src="{% static 'bootstrap5/popper.min.js'%}"></script>
<script src="{% static 'bootstrap5/bootstrap.min.js'%}"></script>
{% comment %} 这个位置留给以后加一下CSS 和 js 代码 {% endcomment %}
{% block head %}{% endblock head %}
</head>
<body>
<!-- border-bottom: 底部边框 -->
<!-- m - for classes that set margin -->
<!-- b - for classes that set margin-bottom or padding-bottom -->
<!-- 3 - (by default) for classes that set the margin or padding to $spacer -->
<header class="p-3 text-bg-light border-bottom mb-3">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<!-- 设置logo -->
<img src="{% static 'image/logo003.jpg' %}" height="40">
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<!-- secondary 灰色 -->
<li><a href="#" class="nav-link px-2 text-secondary">首页</a></li>
<li><a href="#" class="nav-link px-2 text-secondary">发布博客</a></li>
</ul>
<form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search"> <input type="search"
class="form-control" placeholder="搜索..." aria-label="Search">
</form>
<!-- primary:蓝色 -->
<div class="text-end"> <button type="button" class="btn btn-outline-primary">登录</button> <button
type="button" class="btn btn-primary">注册</button> </div>
</div>
</div>
</header>
<!-- p - for classes that set padding -->
<!-- rounded 添加圆角 -->
<main class="container bg-white p-3 rounded">
{% comment %} 每个页面的主体部分代码 {% endcomment %}
{% block main %}{% endblock %}
</main>
</body>
</html>
子模板pub_blog.html
{% extends "base.html" %}
{% block title %}发布{% endblock %}
{% block head %}
<!-- 加载wangeditor 的css 样式 he js 脚本 -->
<link rel="stylesheet" href="{% static 'wangeditor/style.css' %}">
<script src="{% static 'wangeditor/index.js'%}"></script>
<!-- 定义富文本编辑器的样式 -->
<!-- 这里不用,因为已经下载了,放在wangeditor/style.css下面 -->
<!-- <link href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" rel="stylesheet" /> -->
<style>
/* 工具栏+编辑器 */
#editor—wrapper {
border: 1px solid #ccc;
z-index: 100;
/* 按需定义 */
}
/* 工具栏 */
#toolbar-container {
border-bottom: 1px solid #ccc;
}
/* 编辑器 */
#editor-container {
height: 500px;
}
</style>
<!-- 引入wangeditor的 JS代码 -->
<script src="{% static 'js/pub_blog.js'%}"></script>
{% endblock %}
{% block main %}
<h1>发布博客</h1>
<div class="mt-3">
<form action="">
<div class="mb-3">
<!-- 以下是bootstrap的样式 -->
<lable class="form-lable">标题</lable>
<input type="text" name="title" class="form-control">
</div>
<div class="mb-3">
<!-- 以下是bootstrap的样式 -->
<lable class="form-lable">分类</lable>
<select name="category" class="form-select">
<!-- 这里先写两个假的数据,有序写成真的 -->
<option value="1">Python</option>
<option value="2">前端</option>
</select>
</div>
<div class="mb-3">
<lable class="form-lable">内容</lable>
<!--使用wangeditor的富文本编辑器-->
<div id="editor—wrapper">
<div id="toolbar-container"><!-- 工具栏 --></div>
<div id="editor-container"><!-- 编辑器 --></div>
</div>
</div>
<div class="mb-3 text-end">
<button class="btn btn-primary">发布</button>
</div>
</form>
</div>
{% endblock %}
注册功能
发送邮箱验证码
开启QQ邮箱服务
登陆QQ邮箱,在设置下面,找到账号
找到POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务
,选择开启服务
点击开启服务之后,QQ邮箱会要求绑定手机,按照他的要求做即可
短信发送完毕之后,QQ邮箱会发送如下授权码,用这个授权码,Django就可以用QQ邮箱向其他人发送验证码了
配置参数
在项目的settings.py
文件下配置以下参数
# SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)是互联网上用于发送电子邮件的核心通信协议。
# 发送邮件:将电子邮件从发件人传输到收件人的邮件服务器(如 QQ 邮箱、Gmail 等)。
# 不涉及接收邮件:接收邮件使用其他协议(如 POP3/IMAP)。
# 指定邮件后端实现,smtp.EmailBackend 表示使用 SMTP 协议发送邮件。
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# 是否启用 TLS 加密(推荐 True,端口 587 时必启用)。
EMAIL_USE_TLS = True
# SMTP 服务器地址,这里是 QQ 邮箱的服务器。
EMAIL_HOST = 'smtp.qq.com'
# SMTP 服务器端口号,QQ 邮箱的 TLS 端口为 587(SSL 端口为 465)。
EMAIL_PORT = 587
# 发件人邮箱地址(需与 SMTP 服务器匹配)。
EMAIL_HOST_USER = 'xxxxxx@qq.com'
# 授权码(非邮箱密码,需在邮箱设置中生成)。
EMAIL_HOST_PASSWORD = 'dxxxxxxg'
# 默认发件人显示名称(可设为与 EMAIL_HOST_USER 相同)
DEFAULT_FROM_EMAIL = 'xxxxx@qq.com'
关于上面相关配置的参数,可以在管理服务里面找到
然后找到配置方法
这是相关文档:SMTP/IMAP服务
编写发送邮箱的后端代码
def send_email_captcha(request):
# 使用get请求传参:?email=xxx
email = request.GET.get("email")
if not email:
return JsonResponse({"code": 400, "message": "必须传递邮箱!"})
# 生成验证码(取随机的4位阿拉伯数字)
# ['0', '2', '9', '8']
captcha = "".join(random.sample(string.digits, 4))
send_mail(
# subject(邮件主题)
"LinNote注册验证码",
# message(邮件正文)
message=f"您的注册验证码是:{captcha}",
# recipient_list(收件人列表)
# recipient_list=["user1@example.com", "user2@example.com"] # 群发
recipient_list=[email],
# from_email(发件人地址),若为 None,则使用 settings.DEFAULT_FROM_EMAIL
from_email=None,
)
return JsonResponse({"code": 200, "message": "邮箱验证码发送成功!"})
存储验证码
生成存储验证码的表
注册APP
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"blog.apps.BlogConfig", # 新注册
"linauth.apps.LinauthConfig", # 新注册
]
创建Model
在linauth下面的model.py
文件下创建models
# Create your models here.
class CaptchaModel(models.Model):
email = models.EmailField(unique=True)
captcha = models.CharField(max_length=4)
create_time = models.DateTimeField(auto_now_add=True)
运行迁移脚本创建表
依次执行以下命令:
python manage.py makemigrations
python manage.py migrate
我使用SQLite作数据库的原因
条件 | SQLite 优势 |
---|---|
🔹 轻量级服务器(只有 2G 内存) | ✅ SQLite 不需要运行守护进程,占内存极小 |
🔹 网站初期流量不大 | ✅ 单文件数据库,足够应对中小型项目 |
🔹 快速开发、部署 | ✅ 零配置,开箱即用,Django 默认就支持 |
🔹 个人项目或学习用途 | ✅ 简单方便,维护成本低 |
修改代码
def send_email_captcha(request):
email = request.GET.get("email")
if not email:
return JsonResponse({"code": 400, "message": "必须传递邮箱!"})
captcha = "".join(random.sample(string.digits, 4))
# 存储到数据库中
"""
email=email: 数据库中查找 email 字段等于变量 email 值的记录
defaults={"captcha": captcha}: 指定要创建或更新的字段值
- 如果找到匹配记录:更新该记录的 captcha 字段值为变量 captcha 的值
- 如果没找到:创建新记录,设置 email 和 captcha 字段的值
"""
CaptchaModel.objects.update_or_create(email=email, defaults={"captcha": captcha})
send_mail(
"LinNote注册验证码",
message=f"您的注册验证码是:{captcha}",
recipient_list=[email],
from_email=None,
)
return JsonResponse({"code": 200, "message": "邮箱验证码发送成功!"})
获取验证码功能
引入JQuery
{% block head %}
<script src="{% static 'jquery/jquery-3.7.1.min.js' %}"></script>
{% endblock %}
给按钮定义ID
<div class="input-group">
<input type="text" class="form-control" name="captcha" placeholder="验证码"
aria-label="Recipient's username" aria-describedby="button-addon2">
{% comment %} captcha-btn 定义按钮的id,用于绑定点击事件 {% endcomment %}
<button class="btn btn-outline-secondary" type="button" id="captcha-btn">获取验证码</button>
</div>
编写JQuery代码
/*
$(function () { ... }) 的含义
这是 jQuery 的文档就绪函数(document ready)的简写形式,完整写法是 $(document).ready(function(){...})。它的作用是:
- 等待 DOM 完全加载:确保页面所有 HTML 元素都加载完成后再执行内部的 JavaScript 代码
- 避免操作未加载的元素:防止脚本在 DOM 元素尚未存在时就尝试操作它们
- 相当于原生 JS 的:window.onload = function(){...},但 jQuery 的版本更高效
*/
$(function () {
// 定义一个绑定函数
function bindCaptchaBtnClick() {
/*
关于 $("#captcha-btn").click(function (event) { ... }) 的执行机制:
- 绑定阶段(立即执行)
- 当代码运行到 $("#captcha-btn").click(...) 时,
jQuery 会立即绑定点击事件处理函数(即 function(event) { ... } 里的代码)。
- 但此时函数内部的代码不会执行,只是注册了一个回调函数,
告诉浏览器:"当 #captcha-btn 被点击时,执行这个函数"。
- 触发阶段(用户点击后执行)
- 只有当用户真正点击了 #captcha-btn 按钮时,绑定的函数才会被执行。
- 此时会传入一个 event 对象(包含点击事件的相关信息,如触发元素、坐标等)。
*/
$("#captcha-btn").click(function (event) {
let $this = $(this);
let email = $("input[name='email']").val();
if (!email) {
alert("请先输入邮箱!");
return;
}
// 取消按钮的点击事件
$this.off('click');
// 发送ajax请求
$.ajax('/auth/captcha?email=' + email, {
// 发送ajax请求
method: 'GET',
// 发送成功的回调函数
success: function (result) {
if (result['code'] == 200) {
alert("验证码发送成功!");
} else {
alert(result['message']);
}
},
// 发送失败的回调函数
fail: function (error) {
// console.log(error);
}
})
// 倒计时
let countdown = 60;
let timer = setInterval(function () {
if (countdown <= 0) {
$this.text('获取验证码');
// 清掉定时器
clearInterval(timer);
// 重新绑定点击事件
bindCaptchaBtnClick();
} else {
countdown--;
$this.text(countdown + "s")
}
}, 1000);
})
}
// 执行邦迪函数
bindCaptchaBtnClick();
});
引入自己编写的JS代码
{% block head %}
<script src="{% static 'jquery/jquery-3.7.1.min.js' %}"></script>
<script src="{% static 'js/register.js' %}"></script>
{% endblock %}
实现注册功能
创建表单模型
from django import forms
from django.contrib.auth import get_user_model
from .models import CaptchaModel
# User = get_user_model() # 也可以启用,但是不推荐
class RegisterForm(forms.Form):
username = forms.CharField(
max_length=20,
min_length=2,
error_messages={
"required": "请传入用户名!",
"max_length": "用户名长度在2~20之间!",
"min_length": "用户名长度在2~20之间!",
},
)
email = forms.EmailField(
error_messages={"required": "请传入邮箱!", "invalid": "请传入一个正确的邮箱!"}
)
captcha = forms.CharField(max_length=4, min_length=4)
password = forms.CharField(max_length=20, min_length=6)
# 验证邮箱是否已经被注册
def clean_email(self):
email = self.cleaned_data.get("email")
# 获取用户模型
User = get_user_model()
exists = User.objects.filter(email=email).exists()
if exists:
raise forms.ValidationError("邮箱已经被注册!")
return email
# 验证码是否和邮箱是匹配的
def clean_captcha(self):
captcha = self.cleaned_data.get("captcha")
email = self.cleaned_data.get("email")
captcha_model = CaptchaModel.objects.filter(
email=email, captcha=captcha
).first()
# 如果数据库里面不存在,说明验证码和邮箱不匹配
if not captcha_model:
raise forms.ValidationError("验证码和邮箱不匹配!")
captcha_model.delete()
return captcha
解释User = get_user_model()
get_user_model()
返回的是什么
get_user_model()
返回的是 Django 项目中配置的 用户模型类(即 settings.AUTH_USER_MODEL
指定的模型类)
User
是一个 Django Model 类(继承自 AbstractUser
或 AbstractBaseUser
),类似于你在 models.py
中定义的其他模型类(如 Product
、Order
等)。
- 不是具体的用户对象(如
User.objects.get(id=1)
返回的实例)。 - 不是数据库中的数据(需要通过
User.objects
查询才能获取数据)。
为什么不直接引用
为什么自定义模型可以直接导入,而用户模型需要 get_user_model()
?
自定义模型(如 CaptchaModel
)
- 明确绑定到你的应用:当你从
.models
导入CaptchaModel
时,你明确知道它是当前项目定义的模型,不存在替换需求。 - 无需动态切换:它的定义是静态的,不会因为 Django 配置而变化。
用户模型(User)
- Django 允许完全替换默认用户模型:通过
settings.AUTH_USER_MODEL
可以指定自定义用户模型(例如添加phone
字段或改用邮箱登录)。 - 需要动态获取当前配置的模型:直接导入
from django.contrib.auth.models import User
会硬编码默认模型,导致无法兼容自定义用户模型。
get_user_model()
的作用
动态获取当前用户模型
from django.contrib.auth import get_user_model
User = get_user_model() # 自动返回 CustomUser 或默认 User
- 如果
AUTH_USER_MODEL = 'myapp.CustomUser'
,则User
指向CustomUser
。 - 如果未配置,则返回默认
django.contrib.auth.models.User
。
为什么不能提前赋值?
User = get_user_model() # 模块级别赋值(不推荐)
class RegisterForm(forms.Form):
username = forms.CharField(
......
)
......
- 问题:Django 启动时模型可能未加载完成,导致
AppRegistryNotReady
错误。
如何auth的User不满足业务需求怎么办
如果项目需要扩展 Django 默认的 User
模型(例如添加 phone
字段),Django 提供了两种推荐方案。以下是详细解决方案:
特性 | AbstractUser | AbstractBaseUser |
---|---|---|
保留默认字段 | (username/password等) | (完全自定义) |
开发复杂度 | 低 | 高(需实现用户管理器) |
适用场景 | 添加字段/微调默认行为 | 完全重构认证逻辑(如手机号登录) |
选择建议:优先使用 AbstractUser
,除非你需要彻底重写用户系统。
auth模块的优势
为什么一定要嵌入auth模块?为什么不自己写一个User类,然后自己校验密码?
auth有以下一些优势:
功能 | 自定义独立模型 | 继承 Django Auth |
---|---|---|
密码加密与验证 | 需手动实现 | 内置 set_password() /check_password() |
登录会话管理(login() /logout() ) |
需手动实现 | 直接使用 django.contrib.auth |
Admin 后台集成 | 需重写 | 自动支持 |
权限系统(Groups/Permissions) | 需重写 | 内置支持 |
第三方包兼容性(如 DRF) | 需适配 | 开箱即用 |
后端代码
@require_http_methods(["GET", "POST"])
def register(request):
if request.method == "GET":
return render(request, "register.html")
else:
form = RegisterForm(request.POST)
if form.is_valid():
email = form.cleaned_data.get("email")
username = form.cleaned_data.get("username")
password = form.cleaned_data.get("password")
User = get_user_model()
# 存入password是密文,是Django的auth模块底层设置的
User.objects.create_user(email=email, username=username, password=password)
# 从定向到登录页面
return redirect(reverse("linauth:login"))
else:
print(form.errors)
# 重新跳转到登录页面
return redirect(reverse("linauth:register"))
添加csrf的token
<form action="" method="POST">
{% csrf_token %}
<div class="mb-3">
<input type="text" name="username" class="form-control" placeholder="用户名">
</div>
......
</form>
实现登陆功能
登陆表单的Form
class LoginForm(forms.Form):
email = forms.EmailField(
error_messages={"required": "请传入邮箱!", "invalid": "请传入一个正确的邮箱!"}
)
password = forms.CharField(max_length=20, min_length=6)
remember = forms.IntegerField(required=False)
添加csrf的token
<form action="" method="POST">
{% csrf_token %}
<div class="mb-3">
<input type="email" name="email" class="form-control" placeholder="邮箱">
</div>
......
</form>
编写后端验证逻辑
@require_http_methods(["GET", "POST"])
def linlogin(request):
if request.method == "GET":
return render(request, "login.html")
else:
form = LoginForm(request.POST)
if form.is_valid():
email = form.cleaned_data.get("email")
password = form.cleaned_data.get("password")
remember = form.cleaned_data.get("remember")
User = get_user_model()
user = User.objects.filter(email=email).first()
# 验证用户输入的明文密码(password)是否与数据库中存储的加密密码匹配。
# 返回值是一个布尔值(True 或 False)
if user and user.check_password(password):
# 登录:Django的登录
login(request, user)
# 判断是否需要记住我:remember=0 为false 其他值为true
if not remember:
# 如果没有点击记住我,那么就要设置过期时间为0,即浏览器关闭后就会过期
request.session.set_expiry(0)
# 如果点击了,那么就什么都不做,使用默认的2周的过期时间
return redirect("/")
else:
print("邮箱或密码错误!")
# form.add_error('email', '邮箱或者密码错误!')
# return render(request, 'login.html', context={"form": form})
return redirect(reverse("linauth:login"))
login(request, user)
干了哪些事情
作用
- 将用户实例(
user
)绑定到当前会话(Session),标记用户为“已认证状态”。 - 使得后续请求可以通过
request.user
获取当前登录用户。
内部执行流程
- 会话创建:
- 在
request.session
中存储用户的唯一标识(通常是user.id
)。 - Django 默认使用数据库存储会话(可通过
settings.SESSION_ENGINE
配置为缓存或其他后端)。
- 在
- 用户标记:
- 将
user
对象赋值给request.user
,供后续请求使用。 - 如果启用了
django.contrib.auth.middleware.AuthenticationMiddleware
(默认启用),每个请求的request.user
会自动从会话中恢复。
- 将
- 可选功能:
- 更新用户的
last_login
字段(如果用户模型继承自AbstractBaseUser
)。 - 触发
user_logged_in
信号(允许其他组件监听登录事件)。
- 更新用户的
登录和注册按钮的跳转
<div class="text-end">
{% comment %} 注意,这里的标签从button变成 a ; 也就是从按钮变成超链接{% endcomment %}
<a href="{% url 'linauth:login' %}" type="button" class="btn btn-outline-primary">登录</a>
<a href="{% url 'linauth:register' %}" type="button" class="btn btn-primary">注册</a>
</div>
登录和非登录状态的切换
{% comment %}
user.is_authenticated 是jango.contrib.auth.models.User类的一个属性
它可以判断,当前用户是否登录
{% endcomment %}
{% if user.is_authenticated %}
{% comment %} 如果登录了,就显示用户的头像 {% endcomment %}
<!-- 下面是bootstrap的一个样式 -->
<div class="dropdown text-end">
<a href="#" class="d-block link-body-emphasis text-decoration-none dropdown-toggle"
data-bs-toggle="dropdown" aria-expanded="false">
<img src="{% static 'image/avatar.jpg' %}" alt="mdo" width="32" height="32" class="rounded-circle">
</a>
<ul class="dropdown-menu text-small" style="">
<li><a class="dropdown-item" href="{% url 'linauth:logout' %}">退出登录</a></li>
</ul>
</div>
{% else %}
{% comment %} 如果没有登录,就显示登录和注册两个按钮 {% endcomment %}
<!-- primary:蓝色 -->
<div class="text-end">
{% comment %} 注意,这里的标签从button变成 a {% endcomment %}
<a href="{% url 'linauth:login' %}" type="button" class="btn btn-outline-primary">登录</a>
<a href="{% url 'linauth:register' %}" type="button" class="btn btn-primary">注册</a>
</div>
{% endif %}
user.is_authenticated
是什么
user.is_authenticated
是 Django 用户模型(User Model)的一个**属性,用于判断当前用户是否已通过认证(即是否已登录)。
返回值:
True
→ 用户已登录(认证通过)。class User(AbstractUser): class Meta(AbstractUser.Meta): swappable = "AUTH_USER_MODEL" class AbstractUser(AbstractBaseUser, PermissionsMixin): username_validator = UnicodeUsernameValidator() ...... class AbstractBaseUser(models.Model): @property def is_authenticated(self): """ Always return True. This is a way to tell if the user has been authenticated in templates. """ return True
False
→ 用户未登录(匿名用户)。class AnonymousUser: @property def is_authenticated(self): return False
为什么能在模板中直接使用 user.is_authenticated
?
Django 模板自动注入 user
对象
只要使用了 django.contrib.auth.context_processors.auth
上下文处理器(默认启用),Django 会在渲染模板时自动将 request.user
注入为模板变量 user
。无需手动传递 user
到模板上下文。
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", ### 这里使用了
"django.contrib.messages.context_processors.messages",
],
# 加在这里
"builtins": ["django.templatetags.static"],
},
},
]
退出登录功能
后端代码
from django.contrib.auth import logout
def linlogout(request):
# 调用Django的退出登录功能
logout(request)
# 重定向到首页
return redirect("/")
前端代码
<ul class="dropdown-menu text-small" style="">
<li><a class="dropdown-item" href="{% url 'linauth:logout' %}">退出登录</a></li>
</ul>
logout干了些什么
作用
- 终止当前用户的会话,清除认证状态。
- 确保用户无法继续通过
request.user
访问受保护资源。
内部执行流程
- 会话销毁:
- 清除
request.session
中的所有数据(包括用户 ID)。 - 如果使用数据库会话,会删除对应的
django_session
表记录。
- 清除
- 用户标记清除:
- 将
request.user
设置为匿名用户(AnonymousUser
实例)。
- 将
- 可选功能:
- 触发
user_logged_out
信号(允许其他组件监听登出事件)。
- 触发
实现博客发布功能
发布博客访问限制
点击发布博客
按钮,后端会验证目前是否登录,如果没有登录,就跳转到登录页面,否则就跳转到博客发布页面。
前端引入跳转的URL
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<li><a href="#" class="nav-link px-2 text-secondary">首页</a></li>
{% comment %} 跳转到发布博客页面 {% endcomment %}
<li><a href="{% url 'blog:pub_blog' %}" class="nav-link px-2 text-secondary">发布博客</a></li>
</ul>
后端校验是否登录
方式一:直接写死url
# 如果没有登录,会跳转到 /auth/login
from django.contrib.auth.decorators import login_required
@login_required(login_url="/auth/login")
@require_http_methods(["GET", "POST"])
def pub_blog(request):
if request.method == "GET":
# categories = BlogCategory.objects.all()
return render(request, "pub_blog.html")
方式二:使用懒加载
from django.contrib.auth.decorators import login_required
from django.urls.base import reverse_lazy
# 这里使用了 App名(linauth)+url名(login)的配置
# 如果不使用懒加载,使用reverse作翻转,
# @login_required(login_url=reverse("linauth:login"))
# 那么,在reverse作翻转的时候,很可能会因为Django无法读到urls.py的配置,而无法获取url
# 那么,就可以使用懒加载reverse_lazy,等到配置都读完了,在加载url
@login_required(login_url=reverse_lazy("linauth:login"))
@require_http_methods(["GET", "POST"])
def pub_blog(request):
if request.method == "GET":
# categories = BlogCategory.objects.all()
return render(request, "pub_blog.html")
方式三:使用@login_required()
+ settings.py
的配置
直接使用@login_required()
from django.contrib.auth.decorators import login_required
@require_http_methods(["GET", "POST"])
@login_required()
def pub_blog(request):
if request.method == "GET":
# categories = BlogCategory.objects.all()
return render(request, "pub_blog.html")
然后在settings.py
的配置
LOGIN_URL = "/auth/login"
@login_required
检查用户登录状态的步骤
在请求到达视图前,Django 的中间件会按顺序处理 request
对象:
SessionMiddleware
- 从请求的 Cookie 中提取
sessionid
(如sessionid=abc123
)。 - 通过
sessionid
查询django_session
表,加载会话数据到request.session
(类似字典)。
- 从请求的 Cookie 中提取
AuthenticationMiddleware
- 从
request.session
中读取用户 ID(键为_auth_user_id
)。 - 用该 ID 从
auth_user
表查询用户,并赋值给request.user
。 - 如果未找到 ID,
request.user
设为AnonymousUser
。
- 从
- 检查
request.user
- 已登录用户 →
request.user
是User
实例(如admin
)。 - 未登录用户 →
request.user
是AnonymousUser
实例。
- 已登录用户 →
- 验证
is_authenticated
- 调用
request.user.is_authenticated
:True
→ 放行,执行视图函数。False
→ 触发跳转。
- 调用
- 未登录则跳转到登录页
- 默认跳转至
settings.LOGIN_URL
(如/auth/login/
)。 - 携带
next
参数(如?next=/profile/
),登录后自动返回原页面。
- 默认跳转至
创建相关模型
from django.db import models
from django.contrib.auth import get_user_model
class BlogCategory(models.Model):
name = models.CharField(max_length=200)
class Blog(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
pub_time = models.DateTimeField(auto_now_add=True)
category = models.ForeignKey(
BlogCategory, on_delete=models.CASCADE
)
User = get_user_model()
author = models.ForeignKey(User, on_delete=models.CASCADE)
class BlogComment(models.Model):
content = models.TextField()
pub_time = models.DateTimeField(auto_now_add=True)
blog = models.ForeignKey(
Blog, on_delete=models.CASCADE
)
User = get_user_model()
author = models.ForeignKey(User, on_delete=models.CASCADE)
python manage.py makemigrations
python manage.py migrate
Django的Admin系统的使用
登录
在auth_user
表中is_staff
字段值为1的用户才可以登录Admin。因此需要把该值改成1
登录之后,发现自己并没有权限
需要把auth_user
表中的is_superuser
的值改成1之后,才会有权限
改成中文:在settings.py
文件下修改LANGUAGE_CODE
LANGUAGE_CODE = "zh-hans"
创建superuser
也可以通过创建superuser来实现这个功能:python manage.py createsuperuser
(.venv) (base) xxx@xxxdeMacBook-Pro LinNote % python manage.py createsuperuser
Username (leave blank to use 'xxx'): xieshaolin
Email address: xxx@163.com
Password:
Password (again):
添加blog的相关配置
添加admin配置
在blog
app下的admin.py
文件里面添加如下配置
from django.contrib import admin
# Register your models here.
from .models import BlogCategory, Blog, BlogComment
class BlogCategoryAdmin(admin.ModelAdmin):
list_display = ["name"]
class BlogAdmin(admin.ModelAdmin):
list_display = ["title", "content", "pub_time", "category", "author"]
class BlogCommentAdmin(admin.ModelAdmin):
list_display = ["content", "pub_time", "author", "blog"]
# 绑定 模型 和 admin 的管理项
admin.site.register(BlogCategory, BlogCategoryAdmin)
admin.site.register(Blog, BlogAdmin)
admin.site.register(BlogComment, BlogCommentAdmin)
把配置项的名字改成中文
from django.db import models
from django.contrib.auth import get_user_model
class BlogCategory(models.Model):
name = models.CharField(max_length=200)
class Meta:
# apple, apples
verbose_name = "博客分类"
# 因为在英语中有复数这个概念,如apple, apples
# 如果不加上这个配置,那么遇到复数的形式,"博客分类"会变成"博客分类s"
verbose_name_plural = verbose_name
class Blog(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
pub_time = models.DateTimeField(auto_now_add=True)
category = models.ForeignKey(BlogCategory, on_delete=models.CASCADE)
User = get_user_model()
author = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
# apple, apples
verbose_name = "博客"
verbose_name_plural = verbose_name
class BlogComment(models.Model):
content = models.TextField()
pub_time = models.DateTimeField(auto_now_add=True)
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
User = get_user_model()
author = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
# apple, apples
verbose_name = "评论"
verbose_name_plural = verbose_name
修改完之后,博客的配置项已经变成中文了,但是博客的配置属性还没有变成中文
把配置属性变成中文
在属性里面添加verbose_name
from django.db import models
from django.contrib.auth import get_user_model
class BlogCategory(models.Model):
name = models.CharField(max_length=200, verbose_name="分类名称")
class Meta:
# apple, apples
verbose_name = "博客分类"
# 因为在英语中有复数这个概念,如apple, apples
# 如果不加上这个配置,那么遇到复数的形式,"博客分类"会变成"博客分类s"
verbose_name_plural = verbose_name
class Blog(models.Model):
title = models.CharField(max_length=200, verbose_name="标题")
content = models.TextField(verbose_name="内容")
pub_time = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
category = models.ForeignKey(
BlogCategory, on_delete=models.CASCADE, verbose_name="分类"
)
User = get_user_model()
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
class Meta:
# apple, apples
verbose_name = "博客"
verbose_name_plural = verbose_name
class BlogComment(models.Model):
content = models.TextField(verbose_name="内容")
pub_time = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
blog = models.ForeignKey(
Blog, on_delete=models.CASCADE, verbose_name="所属博客"
)
User = get_user_model()
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
class Meta:
# apple, apples
verbose_name = "评论"
verbose_name_plural = verbose_name
定义__str__(self)
from django.db import models
from django.contrib.auth import get_user_model
class BlogCategory(models.Model):
name = models.CharField(max_length=200, verbose_name="分类名称")
def __str__(self):
return self.name
class Meta:
# apple, apples
verbose_name = "博客分类"
# 因为在英语中有复数这个概念,如apple, apples
# 如果不加上这个配置,那么遇到复数的形式,"博客分类"会变成"博客分类s"
verbose_name_plural = verbose_name
class Blog(models.Model):
title = models.CharField(max_length=200, verbose_name="标题")
content = models.TextField(verbose_name="内容")
pub_time = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
category = models.ForeignKey(
BlogCategory, on_delete=models.CASCADE, verbose_name="分类"
)
User = get_user_model()
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
def __str__(self):
return self.title
class Meta:
# apple, apples
verbose_name = "博客"
verbose_name_plural = verbose_name
class BlogComment(models.Model):
content = models.TextField(verbose_name="内容")
pub_time = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
blog = models.ForeignKey(
Blog, on_delete=models.CASCADE, verbose_name="所属博客"
)
User = get_user_model()
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
def __str__(self):
return self.content
class Meta:
# apple, apples
verbose_name = "评论"
verbose_name_plural = verbose_name
在 Django Admin 中的表现
后台列表页(List View)
- 默认行为:如果未定义
__str__
,Django Admin 会显示对象的默认字符串表示(如BlogCategory object (1)
),这对管理员毫无意义。 - 定义
__str__
后:在列表页中会直接显示name
字段的值(如"技术"
、"生活"
),清晰易读。
下拉选择框(ForeignKey/ManyToManyField)
- 当其他模型通过
ForeignKey
或ManyToManyField
关联到BlogCategory
时:- 在 Admin 的表单中,下拉选择框会显示
__str__
返回的值(如"技术"
),而非object(1)
。
- 在 Admin 的表单中,下拉选择框会显示
其他 Django 场景
Shell或日志调试
在 Django Shell (
python manage.py shell
) 中查询模型时:>>> category = BlogCategory.objects.first() >>> print(category) # 输出 "技术"(而非无意义的 `BlogCategory object (1)`)
模板渲染
在模板中直接输出对象时:
{{ category }} <!-- 显示分类名称,如 "技术" -->
为什么不用 verbose_name
?
verbose_name
是模型的显示名称(用于 Admin 的标题、表单标签等),而__str__
是对象实例的显示名称。- 例如:
verbose_name
控制的是“博客分类”这个类别的标题。__str__
控制的是具体某个分类(如"技术"
)的显示。
发布博客后端代码
# 以下是form表达的模型
from django import forms
class PubBlogForm(forms.Form):
title = forms.CharField(max_length=200, min_length=2)
content = forms.CharField(min_length=2)
category = forms.IntegerField()
# 以下是业务代码
@require_http_methods(["GET", "POST"])
@login_required()
def pub_blog(request):
# 如果是Get请求,说明只是访问发布博客
if request.method == "GET":
# 查询所有分类
categories = BlogCategory.objects.all()
# 把分类传给模版
return render(request, "pub_blog.html", context={"categories": categories})
# 如果是post请求,说明是在提交表达
else:
form = PubBlogForm(request.POST)
if form.is_valid():
title = form.cleaned_data.get("title")
content = form.cleaned_data.get("content")
category_id = form.cleaned_data.get("category")
blog = Blog.objects.create(
title=title,
content=content,
category_id=category_id,
author=request.user,
)
return JsonResponse(
# 这里给前端返回一个blog.id的目的是为了发生成功之后,跳转的博客详情页
{"code": 200, "message": "博客发布成功!", "data": {"blog_id": blog.id}}
)
else:
print(form.errors)
return JsonResponse({"code": 400, "message": "参数错误!"})
发布博客的前端内容
HTML的内容
分类下拉框
<!-- 引入 这里引入id,那么js就更容易获取数据 -->
<select name="category" class="form-select" id="category-select">
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
引入jquery
在pub_blog.html
里面引入jquery
script src="{% static 'jquery/jquery-3.7.1.min.js' %}"></script>
给按钮绑定Id
<div class="mb-3 text-end">
<button class="btn btn-primary" id="submit-btn">发布</button>
</div>
补充csrf_token
<!-- 引入 csrf_token -->
{% csrf_token %}
Js绑定点击事件
$("#submit-btn").click(function (event) {
/*阻止按钮的默认行为:
如果不阻止默认行为,点击发布按钮,就会直接通过表单的形式发布给后端,
但是富文本框里面的内容,是不能通过表单提交的*/
event.preventDefault();
// 找到input标签里面 name='title' 的 要素,然后 .val() 获取它的值
let title = $("input[name='title']").val();
// 找到id=category-select 的要是,然后 .val() 获取它的值
// 用id的方式,要比前面那种方式快,因为id唯一
let category = $("#category-select").val();
// 这个方法只能获取文本,不能获取样式,如一级标题,二级标题
// let content= editor.getText();
// 这个方法可以获取样式
let content = editor.getHtml();
/**
* 获取CSRF的token:
* 使用{% csrf_token %} 渲染的结果是:
* <input type="hidden" name="csrfmiddlewaretoken" value="4nTc...A">
*/
let csrfmiddlewaretoken = $("input[name='csrfmiddlewaretoken']").val();
// 实际上只有两个参数:参数1 url=/blog/pub'; 参数2 opotion={}
$.ajax('/blog/pub', {
// 请求的方式
method: 'POST',
// 上传的数据
data: { title, category, content, csrfmiddlewaretoken },
success: function (result) {
if (result['code'] == 200) {
// 获取博客id
let blog_id = result['data']['blog_id']
// 跳转到博客详情页面
window.location = '/blog/detail/' + blog_id
} else {
alert(result['message']);
}
}
})
});
展示博客详情
后端
def blog_detail(request, blog_id):
try:
blog = Blog.objects.get(pk=blog_id)
except Exception as e:
print(e)
return render(request, "blog_detail.html", context={"blog": blog})
前端
<h1>{{blog.title}}</h1>
<!-- hr: 会显示一条横线 -->
<hr />
<!-- mt-2: spacing内容,t - for classes that set margin-top or padding-top -->
<div class="mt-2">
<img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
<!-- s - (start) for classes that set margin-left or padding-left in LTR, margin-right or padding-right in RTL -->
<span class="ms-2">{{ blog.author.username }}</span>
<span class="ms-2">于</span>
{% comment %} date过滤器设置时间格式 {% endcomment %}
<span class="ms-2">{{ blog.pub_time|date:"Y年m月d日 h时i分" }}</span>发布
</div>
<hr />
<!-- y - for classes that set both *-top and *-bottom -->
<!-- py-2: 上下内边距调整2 -->
<div class="py-2">
{% comment %} 标记这个字符串是安全的,让他渲染成HTML {% endcomment %}
{{ blog.content|safe }}
</div>
评论功能
后端
# 增加related_name属性
class BlogComment(models.Model):
content = models.TextField(verbose_name="内容")
pub_time = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
blog = models.ForeignKey(
Blog,
on_delete=models.CASCADE,
# related_name="comments":设置这个属性就可以通过 Blog.comments查到这个blog下面的所有评论
related_name="comments",
verbose_name="所属博客",
)
User = get_user_model()
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="作者")
# 发布comment的逻辑
@require_POST
@login_required()
def pub_comment(request):
blog_id = request.POST.get("blog_id")
content = request.POST.get("content")
# author=request.user: 会自动提取user的id
BlogComment.objects.create(content=content, blog_id=blog_id, author=request.user)
# 重新加载博客详情页
# kwargs可以传参:/blog/detail/{blog_id}
return redirect(reverse("blog:blog_detail", kwargs={"blog_id": blog_id}))
前端
<div class="mt-2">
{% comment %} 相当于后端代码的: blog.comments.all() 方法{% endcomment %}
{% comment %}
下面的评论列表是访问/blog/detail/{blog_id}的时候就需要显示
所以只给前端上传了 context={"blog": blog}
{% endcomment %}
<h3>评论({{ blog.comments.all|length }})</h3>
{% comment %} 表单提交地址 {% endcomment %}
<form action="{% url 'blog:pub_comment' %}" method="POST">
{% comment %} 添加csrf_token {% endcomment %}
{% csrf_token %}
{% comment %} 添加blog_id,用于刷新页面 {% endcomment %}
<input type="hidden" name="blog_id" value="{{ blog.id }}">
<div class="mt-2">
{% comment %} 新添 name="content" {% endcomment %}
<input type="text" class="form-control" placeholder="请输入评论" name="content">
</div>
<div class="text-end mt-2">
{% comment %} 改成submit,表单才可以提交 {% endcomment %}
<!-- <button type="submit" class="btn btn-primary">评论</button> -->
<button type="submit" class="btn btn-primary">评论</button>
</div>
</form>
</div>
<div class="mt-2">
<!-- list-group list-group-flush: bootstrap的列表样式 -->
<ul class="list-group list-group-flush">
{% for comme in blog.comments.all %}
<li class="list-group-item">
<div class="d-flex justify-content-between text-secondary">
<div class="user-info">
<img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
<span class="ms-2">{{ comme.author.username }}</span>
</div>
<div class="create-time" style="line-height: 40px;">{{ comme.pub_time|date:"Y年m月d日 h时i分" }}</div>
</div>
<div style="padding-left: 30px;">
<div class="ms-2">{{ comme.content }}</div>
</div>
</li>
{% endfor %}
</ul>
</div>
代码高亮Highlight.js
Highlight.js 是一个轻量级、高性能的代码语法高亮库,用于在网页中高亮显示各种编程语言的代码片段。
映入Highlight
{% block head %} {% comment %} 引入主题 {% endcomment %} <link rel="stylesheet" href="{% static 'highlight/styles/github-dark.min.css' %}"> {% comment %} 引入hightlight.js {% endcomment %} <script src="{% static 'highlight/highlight.min.js' %}"></script> {% endblock %}
高亮代码
{% comment %} hljs Highlight.js 库的全局对象,所有功能通过它调用。 .highlightAll()该方法会: 扫描整个网页。 找到所有 <pre><code> 标签(或带 class="hljs" 的代码块)。 根据代码语言(如 class="language-python")自动应用语法高亮。 如果未指定语言,尝试自动检测。 这行代码是 Highlight.js 的“一键高亮”开关,自动为页面中的所有代码块着色,无需手动逐个处理。 放在代码最后面也是希望所有要素都加载完毕后,高亮所有代码 {% endcomment %} <script>hljs.highlightAll(); </script>
首页功能
后端代码
# 显示所有文章
def index(request):
blogs = Blog.objects.all()
return render(request, "index.html", context={"blogs": blogs})
后端模版
<h1>博客列表</h1>
<div class="row row-cols-2 row-gap-4">
{% for blog in blogs %}
<div class="col">
<!-- text-center:文字居中,删除就不会文字居中了 -->
<div class="card">
<div class="card-header">
<!-- 这里展示文章标题 -->
{% comment %} blog_id=blog.id 传递URL参数 {% endcomment %}
<a href="{% url 'blog:blog_detail' blog_id=blog.id %}">{{ blog.title }}</a>
</div>
<!-- height: 100px:设置card-body的固定高度 -->
<div class="card-body" style="height: 100px;">
<!-- 原来的标题不用了 -->
<!-- <h5 class="card-title"></h5> -->
{% comment %} striptags:去除HTML格式 {% endcomment %}
{% comment %} truncatechars:截取100个字符 {% endcomment %}
<p class="card-text">{{ blog.content|striptags|truncatechars:100 }}</p>
</div>
<!-- d-flex justify-content-between :设置d-flex -->
<div class="card-footer text-muted d-flex justify-content-between">
<div>
<img src="{% static 'image/avatar.jpg'%}" class="rounded-circle" width="30" height="30">
<!-- 用户名 -->
{{ blog.author.username }}
</div>
<div>
<!-- 先填一个假的发布时间 -->
发布时间:{{ blog.pub_time|date:"Y年m月d日 h时i分" }}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
查询功能
后端代码
@require_GET
def search(request):
# /search?q=xxx
q = request.GET.get("q")
# 从博客的标题和内容中查找含有q关键字的博客
blogs = Blog.objects.filter(Q(title__icontains=q) | Q(content__icontains=q)).all()
return render(request, "index.html", context={"blogs": blogs})
前端代码
<form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search" action="{% url 'blog:search' %}" method="GET">
<input type="search" class="form-control" name="q" placeholder="搜索..." aria-label="Search">
</form>
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1909773034@qq.com