【pip永久换源】

# 1 以后用阿里云
# 2 pip install -i 镜像站
# 3 永久换源
"""
1、文件管理器文件路径地址栏敲:%APPDATA% 回车,快速进入 C:\Users\电脑用户\AppData\Roaming 文件夹中
2、新建 pip 文件夹并在文件夹中新建 pip.ini 配置文件(之前得pip文件要删除)
3、新增 pip.ini 配置文件内容
"""


[global]
index-url = https://mirrors.aliyun.com/pypi/simple
[install]
use-mirrors =true
mirrors =https://mirrors.aliyun.com/pypi/simple
trusted-host =mirrors.aliyun.com

 

【虚拟环境】

#  虚拟环境作用
1、使不同应用开发环境相互独立
2、环境升级不影响其他应用,也不会影响全局的python环境
3、防止出现包管理混乱及包版本冲突

# 什么是虚拟环境,为什么要有它?它解决了什么问题
    -操作系统装了python3.10
        -使用django 2.2.2开发了一个项目
        -使用django 3.x 开发了一个项目
        -把两个项目都打开,同时开发
    -每个项目都用自己独立的环境,装的模块相互不影响
    -两种解决方案:
        Virtualenv
        pipenv

=====================================================
-1 安装两个模块  cmd 命令行执行命令
	pip3 install virtualenv    # 虚拟环境是它
	pip3 install virtualenvwrapper-win
# 该命令是对虚拟环境的加强,以后只需要简单的命令就可以使用和创建虚拟环境


-2 配置虚拟环境管理器工作目录
	-在环境变量中新建:WORKON_HOME: C:\Virtualenvs
	-在C盘创建Virtualenvs文件夹,以后新建的虚拟环境,都会在这个文件夹下


-3 去Python38的安装目录----Scripts文件夹下----
    -----virtualenvwrapper.bat--------双击一下
 

如何使用命令 创建和使用虚拟环境

 1 pycharm能直接创虚拟环境,命令创建虚拟环境的作用是?
 2 后期项目在linux里上线,项目要跑在虚拟环境里,所以也要创建虚拟环境
 3 linux里创建虚拟环境,都是用命令来执行的
 4 
 5 而且pycharm能直接创虚拟环境不通用,但是命令方式创建是通用的
 6 ---------------------------------
 7 
 8 创建并进入到虚拟环境:
 9 
10 创建虚拟环境,会自动放在D:\Virtualenvs目录下
11     mkvirtualenv -p python38 虚拟环境名
12     # 此处的python38是要指定解释器的名字
13 
14 
15 cmd 窗口中带着虚拟环境名字,表示在虚拟环境中,
16 以后安装的所有模块,都是给虚拟环境安装的!!和系统的环境没有关系
17 
18 退出虚拟环境
19     deactivate
20 
21 查看有哪些虚拟环境
22     workon
23 
24 进入虚拟环境
25     workon 虚拟环境名
查看装了哪些模块
      pip list
26 27 删除虚拟环境 28 rmvirtualenv 虚拟环境名 # 或者直接去文件夹下删文件

//命令行创建项目

pycharm中创建虚拟环境

 

【项目创建后台目录调整】

 1 # 推荐的目录结构
 2 """
 3 
 4 ├── luffytest
 5     ├── logs/        # 项目运行时/开发时日志目录 - 包
 6     ├── manage.py        # 脚本文件
 7     ├── luffytest/        # 项目主应用,开发时的代码保存 - 包
 8         ├── apps/        # 开发者的代码保存目录,以模块[子应用]为目录保存 - 包
 9         ├── libs/        # 第三方类库的保存目录[第三方组件、模块] - 包
10         ├── settings/        # 配置目录 - 包
11             ├── dev.py    # 项目开发时的本地配置
12             └── prod.py    # 项目上线时的运行配置
13         ├── urls.py        # 总路由
14         └── utils/        # 多个模块[子应用]的公共函数或类库[自己开发的组件]等
15     └── scripts/    # 保存项目运行时的脚本文件夹,小的测试脚本或文件夹,不提交到git上
16 
17 """
18 
19 ----------------------------------------
20 
21 # 目录调整后运行不了,报错
22 CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False.
23 
24 -原因是 找不到配置文件了---》django项目运行,第一步就要加载配置文件
25 
26 # 1 开发阶段:python manage.py runserver ---->找不到配置文件就报错了---》
27     改manage.py文件里面的代码'luffytesr.settings' 变成'luffytesr.settings.dev'
28     因为原来settings配置文件是在项目的小目录下的,
29     现在被我们放到自己创的settings包下面去了,而且还把名字该成了dev.py
30     所以配置文件路径要改一下!!!变成如下
31     os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffytest.settings.dev')
32 
33 -------------------------------------------------
34 -------------------------------------------------
35 
36 # 2 上线阶段:要用uwsgi 来运行  要改两个文件
37     asgi.py   与    wsgi.py  文件
38 
39 
40 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffytest.settings')
41 要改成
42 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffytest.settings.prod')
43 
44 -------------------------------------------------------
45 -------------------------------------------------------
46 
47 # 3 项目能运行阶段
48 
49     - 创建app
50         -python manage.py startapp home 命令在哪个路径下执行,app就创建在哪个路径下
51         直接创app会创到根路径下,但是我们现在想要的是创的app文件放在apps包下
52 
53         所以 cd 到apps路径下 cd luffytest/apps
54 因为已经切到了apps的路径下了,所以先要找到manage.py要用相对路径的方式找到manage.py的路径
55         -python ../../manage.py startapp home
56 
57         或者直接在根目录下,直接创建app,然后直接拖到apps目录下去就行了,最简单!!!
58 
59 ------
60     - 注册app
61         -先到dev里面注册app的名字比如 'luffytest.apps.home',
62         -其次我们命令在apps目录下创的home,home包里面的apps.py里面的name='home'
63         -不对,需要在home包下的apps.py中修改成  name = 'luffytest.apps.home'
64         -所以就有点烦了,建议直接在根目录下创建app,然后手动拖到apps包里面去
65         -这样系统会自动修改成name = 'luffytest.apps.home' 就方便了,
66         -也不用先切apps目录下 python ../../manage.py startapp app名字
67         -直接python manage.py startapp app名字  创app了
68 
69 
70         但是这样注册还是太烦了,如果就想注册app的时候,就只写app名home怎么办
71         在配置文件的里面最上面把apps包的路径加入到环境变量,
72         这样加载配置文件的一开始就会执行添加环境变量的操作,注册的时候直接写项目名就没问题了
73         import os
74         import sys
75         apps_dir = os.path.join(BASE_DIR, 'apps')
76         sys.path.insert(apps_dir)
77         顺便把项目根目录下的小目录的路径也放到环境变量中,
78         这样以后导apps包下的文件或导项目小目录下的文件都可以直接import 文件名了!!!
79         sys.path.insert(BASE_DIR)
80 
81 
82         导入模块,导入句式会飘红,飘红不一定是真的有问题,运行一下看看就知道了
83         怎么解决导入句式飘红的问题?
84         把加入到环境变量的路径,右键,做为source root

 

asgi.py 与 wsgi.py文件里面要改一下,为了上线阶段用uwsgi来运行项目文件用的!!!

 

 。

【后端数据库创建】

# 之前项目操作数据库,都是使用root用户,root用户权限太高了
# 在公司里,一般不会给你root用户权限
# 如果开发人员是root权限,数据安全性就很差

# 正常开发人员,专门创建一个用户,用户只对当前项目的库有操作权限
----------------------------------------
----------------------------------------

# 面试题:utf8  utf8mb4 有什么区别 ?

----------------------------------------

# 创建一个luffy库,创建luffy用户,luffy用户只对luffy库有操作权限
# 创建luffy数据库
----------------------------------------------------------
----------------------------------------------------------
----------------------------------------------------------

1.管理员连接数据库
>: mysql -uroot -p密码

2.创建数据库
>: create database luffy default charset=utf8;     # charset=utf8mb4也行

3.查看用户
>: select user,host,password from mysql.user;
只有root用户,但是root用户权限太高,所以需要创建luffy用户

# (5.7往后的版本) 查看用户的命令
>: select user,host,authentication_string from mysql.user;



-----------------------------------------------------------
创建普通用户,给这个用户只授权 某个 库的权限
    #1 创建luffy库,给项目使用
    # 在mysql8中的utf8 就是utf8mb3
    create database luffy default charset=utf8mb4;
    #2 查看用户
    SELECT User, Host FROM mysql.user;
    #3 创建用户
    CREATE USER 'luffy'@'localhost' IDENTIFIED BY 'Luffy123?';  
    CREATE USER 'luffy'@'%' IDENTIFIED BY 'Luffy123?';  
    GRANT ALL PRIVILEGES ON luffy.* TO 'luffy'@'localhost' WITH GRANT OPTION;  # 只有本地权限
    GRANT ALL PRIVILEGES ON luffy.* TO 'luffy'@'%' WITH GRANT OPTION;  # 查看所有权限

使用 luffy 用户,密码是:Luffy123? 登录了,只能看到luffy库
 

 

 。

【后端User表】

链接数据库

settings.dev

# 用户名密码写死在代码中了,为了保证安全,可以把用户名与密码写到环境变量中
user = os.environ.get('MYSQL_USER', 'luffy')
password = os.environ.get('MYSQL_PASSWORD', 'Luffy123?')

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'luffy',
        'HOST': '127.0.0.1',
        'PORT': '3306',
        'USER': user,
        'PASSWORD': password,
    }
}

 

User表

 1 from django.db import models
 2 from django.contrib.auth.models import AbstractUser
 3 
 4 
 5 class User(AbstractUser):
 6     mobile = models.CharField(max_length=11, unique=True)
 7     # ImageField需要pillow包的支持
 8     icon = models.ImageField(upload_to='icon', default='icon/default.png')
 9 
10     class Meta:
11         db_table = 'luffy_user'
12         verbose_name = '用户表'
13         verbose_name_plural = verbose_name
14 
15     def __str__(self):
16         return self.username

-----------------------------------------
扩写user表,在setting.dev里配置

# 扩写auth的user表
AUTH_USER_MODEL = 'user.User'

 。

 。

 *******************************************************************************

软件开发模式:

  # 瀑布开发

  # 敏捷开发

 

[开启media访问]

 1 #1 settings.dev
 2 MEDIA_URL = '/media/'
 3 MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
 4 #2 创建文件夹
 5 
 6 # 3 配一个路由
 7 from django.contrib import admin
 8 from django.urls import path
 9 from django.views.static import serve
10 
11 from django.conf import settings
12 
13 urlpatterns = [
14     path('admin/', admin.site.urls),
15     # 127.0.0.1:8000/media/icon/default.png
16     path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),
17  
18 ]
19 
20 ### 补充
21 auth的user表,密码加密方式
22     -使用sha256+使用SECRET_KEY作为密码+23     
24 自定义用户表,参照这种方式设置密码这个字段

2.

 创建超级用户

 。

【项目日志】

(这是我们平常写的记录日志方法)

# 1 配置文件中写
# 真实项目上线后,日志文件打印级别不能过低,因为一次日志记录就是一次文件io操作
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s'
        },
        'simple': {
            'format': '%(levelname)s %(module)s %(lineno)d %(message)s'
        },
    },
    'filters': {
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {
        'console': {
            # 实际开发建议使用WARNING
            'level': 'DEBUG',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'file': {
            # 实际开发建议使用ERROR
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',
            # 日志位置,日志文件名,日志保存目录必须手动创建,注:这里的文件路径要注意BASE_DIR代表的是小luffyapi
            'filename': os.path.join(os.path.dirname(BASE_DIR), "logs", "luffy.log"),
            # 日志文件的最大值,这里我们设置300M
            'maxBytes': 300 * 1024 * 1024,
            # 日志文件的数量,设置最大日志数量为10
            'backupCount': 10,
            # 日志格式:详细格式
            'formatter': 'verbose',
            # 文件内容编码
            'encoding': 'utf-8'
        },
    },
    # 日志对象
    'loggers': {
        'django': {
            'handlers': ['console', 'file'],
            'propagate': True, # 是否让日志信息继续冒泡给其他的日志处理系统
        },
    }
}

#2 写一个py文件common_logger.py
import logging
logger = logging.getLogger('django')
# 3 使用
class LoggerView(APIView):
    def get(self,request):
        logger.info('来了,老弟')
        return Response('ok')

(第三方记录日志方法,样式好看,且功能比我们自己写的好:loguru)

 1 1.安装模块
 2     pip3 install loguru
 3 
 4 项目测试配置文件中配置
 5 
 6 from loguru import logger
 7 
 8 LOG_DIR = os.path.join(os.path.dirname(BASE_DIR), "logss")
 9 LOGGING_CONFIG = None  # 禁用默认的 Django logging 配置
10 
11 # logger.remove(handler_id=None)  # 指定不在控制台输出(默认输出在控制台中)
12 logger.add(os.path.join(LOG_DIR, "debug.log"), level="DEBUG", rotation="1 day", retention="7 days")
13 logger.add(os.path.join(LOG_DIR, "info.log"), level="INFO", rotation="1 day", retention="7 days")
14 logger.add(os.path.join(LOG_DIR, "error.log"), level="ERROR", rotation="1 day", retention="7 days")
15 logger.add(os.path.join(LOG_DIR, "critical.log"), level="CRITICAL", rotation="1 day", retention="7 days")
16 
17 *********************************
18 测试
19 
20 from loguru import logger
21 
22 
23 class LogruView(APIView):
24     def get(self, request):
25         logger.info('info级别')
26         logger.warning('warning级别')
27         logger.error('error级别')
28         return Response('测试日志')
29 
30 **************************************
31 测试路由
32 from luffytest.apps.home.views import LogruView
33 
34 urlpatterns = [
35 
36     path('logs/', LogruView.as_view(), name='logs'),
37 
38 ]
39 路由访问
40     
41 # http://127.0.0.1:8000/api/v1/user/logs/

 。

 【封装全局异常】

 1 from rest_framework.views import exception_handler as drf_exception_handler
 2 from rest_framework.response import Response
 3 
 4 from luffytest.utils.common_logger import logger
 5 
 6 
 7 # 以后可能会自定义自定义异常类
 8 class NoPermissionException(Exception):
 9     pass
10 
11 
12 def exception_handler(exc, context):
13     # 记录日志
14     request = context.get('request')
15     view = context.get('view')
16     ip = request.META.get('REMOTE_ADDR')
17     path = request.get_full_path()
18     method = request.method
19     user_id = request.user.id or '【匿名用户】'
20     logger.error(
21         f'操作出错:{str(exc)},ip地址为:{ip},请求方式是:{method},请求地址是:{path},用户为:{user_id},视图类为:{str(view)}')
22     res = drf_exception_handler(exc, context)
23     if res:
24         # drf的异常 data=['错误1',错误2]    data={detail:'sss'}
25         if isinstance(res.data, dict):
26             err = res.data.get('detail')
27         elif isinstance(res.data, list):
28             err = res.data[0]
29         else:
30             err = '服务异常,请稍后再尝试,-drf'
31         response = Response({'code': 999, 'msg': err})
32     else:
33         # 非drf异常,更细力度的区分异常
34         if isinstance(exc, ZeroDivisionError):
35             err = '数据操作出错,除以0了'
36             code = 909
37         elif isinstance(exc, NoPermissionException):
38             err = f'您没有操作权限:{str(exc)}'
39             code = 906
40         else:
41             err = f'系统错误:{str(exc)}'
42             code = 909
43         response = Response({'code': code, 'msg': err})
44 
45     return response
46 
47 *******************************
48 配置文件里配置
49 REST_FRAMEWORK = {
50     'EXCEPTION_HANDLER': 'luffytest.utils.common_exception.exception_handler',
51 }
52 
53 *******************************
54 测试
55 
56 # 异常出来测试
57 class ExceptionView(APIView):
58     def get(self, request):
59         # drf异常
60         # raise ValidationError('校验失败')
61         # 2.drf异常 AuthenticationFailed的data是字典带detail情况
62         # raise AuthenticationFailed('认证失败')
63         # 3.drf 的APIException
64         # raise APIException('api异常')
65         # 4.
66         5/0
67         return Response('ok')
68 
69 ***************
70 路由
71 path('exception/', ExceptionView.as_view())
72 http://127.0.0.1:8000/api/v1/user/exception/

 -----------------------------------------------------------------------------------------------------------

【二次封装Response】

 1 '''
 2 
 3 要达到的效果,以后使用APIResponse的情况
 4     -return APIResponse()----------前端收到-->{code:100,msg:成功}
 5     -return APIResponse(token=xsdse,username=lqz)----------前端收到-->{code:100,msg:成功,token:xsdse,username:lqz}
 6     -return APIResponse(data=[{},{},{}])----------前端收到-->{code:100,msg:成功,data:[{},{},{}]}
 7     -return APIResponse(code=101,msg=失败)----------前端收到-->{code:101,msg:失败}
 8     -return APIResponse(headers={'xx':'yy'})----------前端收到-->{code:100,msg:成功},但是响应头中有xx=yy
 9     -return APIResponse(status=201)----------前端收到-->{code:100,msg:成功},但是响应状态码是201
10 
11 
12 效果就是:传code msg  status headers 都能正常改,不传有默认值,
13 如果有多余的参数传过来了,都统一放到响应体的字典里面去
14 实际上需要达到的效果就是:传Response里面有的参数,会替换,传传Response里面没有的参数,
15 会给APIResponse添加属性!!!
16 
17 '''
18 common_response.py
19 
20 from rest_framework.response import Response
21 
22 
23 class APIResponse(Response):
24     # data=None, status=None,template_name=None, headers=None,exception=False, content_type=None
25     def __init__(self, code=100, msg='成功', status=None, headers=None, **kwargs):
26         data = {'code': code, 'msg': msg}
27         if kwargs: # 说明除了code,msg这些以外,还传了其他数据
28             data.update(kwargs)
29         super().__init__(data=data, status=status, headers=headers)
30       子类调用父类里面的方法,执行Response里面的双下init方法,实现真正的初始化
31 --------------------------- 
32 测试代码
33 class ResponseView(APIView):
34   def get(self, request, *args, **kwargs):
35   return APIResponse(username='tank',token='abc.e.d')

 。

【前端项目创建】

1 # 1 vue2 创建项目
2 vue create luffy_city
3 # 2 使用pycharm打开
4 
5 # 3 文件调整
6     -删除Helloworld组件
7     -删除AboutView页面组件
8     -App.vue中只留
9         <router-view/>

【前端配置】

 1 # 1 axios 
 2     -cnpm install axios -S
 3     -main.js中
 4     import axios from 'axios'
 5     Vue.prototype.$axios=axios
 6 # 2 elementui
 7     -cnpm install element-ui -S
 8     -main.js
 9     import ElementUI from 'element-ui';
10     import 'element-ui/lib/theme-chalk/index.css';
11     Vue.use(ElementUI);
12 # 3 cookies
13     -cnpm install vue-cookies -S
14     -main.js
15     import cookies from 'vue-cookies'
16     Vue.prototype.$cookies = cookies;
17 
18     
19 # 4 写个settings.js
20     const BASE_URL='http://127.0.0.1:8000/api/v1/'
21     export default {
22         banner:BASE_URL+'/home/banner/',
23     }
24     -main.js注册
25         import settings from "@/assets/js/settings";
26         Vue.prototype.$settings=settings
27    - 再组件中使用
28      methods:{
29      getdata(){
30           async function a(){
31             let response=await this.$axios(this.$settings.banner)
32 
33           }
34         }
35       }
36     
37     
38 # 5 去掉默认样式
39     -写个global.css
40     /* 声明全局样式和项目的初始化样式 */
41     body, h1, h2, h3, h4, h5, h6, p, table, tr, td, ul, li, a, form, input, select, option, textarea {
42         margin: 0;
43         padding: 0;
44         font-size: 15px;
45     }
46 
47     a {
48         text-decoration: none;
49         color: #333;
50     }
51 
52     ul {
53         list-style: none;
54     }
55 
56     table {
57         border-collapse: collapse; /* 合并边框 */
58     }
59     -再main.js中引入
60     // 全局css
61     import '@/assets/css/global.css'

【前台主页功能】

(在后端写)

# 1 轮播图接口--写它

# 2 推荐课程接口(往后放)

# 3 创建home--app

# 4 创建Banner表 -写一个基本 BaseModel,写一些基本字段,以后 轮播图或课程继承这个表

Uitil-Model-BaseModel

 1 from django.db import models
 2 
 3 
 4 class BaseModel(models.Model):
 5     created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
 6     updated_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')
 7     is_delete = models.BooleanField(default=False, verbose_name='是否删除')
 8     is_show = models.BooleanField(default=True, verbose_name='是否上架')
 9     orders = models.IntegerField(verbose_name='优先级')
10 
11     class Meta:
12         abstract = True  # 只用来继承,不在数据库中生成表

Home-Model-Banner

 1 from django.db import models
 2 
 3 from luffytest.utils.common_model import BaseModel
 4 
 5 
 6 # Create your models here.
 7 
 8 class Banner(BaseModel):
 9     title = models.CharField(max_length=16, unique=True, verbose_name='名称')
10     image = models.ImageField(upload_to='banner', verbose_name='图片')
11     # web  , 小程序 ,app --》轮播图--》可以点击--》点击--》跳转到某个地址
12     # 跳转有两种情况:1 外链  2 自己的页面
13     link = models.CharField(max_length=64, verbose_name='跳转链接')
14     info = models.TextField(verbose_name='详情')
15 
16     class Meta:
17         db_table = 'luffy_banner'
18         verbose_name_plural = '轮播图'
19 
20     def __str__(self):
21         return self.title

serializer

1 from rest_framework import serializers
2 
3 from .models import Banner
4 
5 
6 class Bannerserializer(serializers.ModelSerializer):
7     class Meta:
8         model = Banner
9         fields = ['title', 'image', 'link']

home.view

 1 from django.shortcuts import render
 2 from rest_framework.viewsets import GenericViewSet
 3 from rest_framework.mixins import ListModelMixin
 4 from utils.common_mixin import APIListModelMixin
 5 from .models import Banner
 6 from .serializer import Bannerserializer
 7 
 8 
 9 # 轮播图接口  新增,修改,删除---用admin做,不要再写接口
10 class BannerView(GenericViewSet, APIListModelMixin):
11     queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')  # 过滤排序
12     serializer_class = Bannerserializer

common_mixin

 1 from rest_framework.mixins import ListModelMixin, CreateModelMixin, RetrieveModelMixin, DestroyModelMixin, \
 2     UpdateModelMixin
 3 
 4 from utils.common_response import APIResponse
 5 
 6 
 7 class APIListModelMixin(ListModelMixin):
 8     def list(self, request, *args, **kwargs):
 9         res = super().list(request, *args, **kwargs)
10         return APIResponse(results=res.data)
注意如果报错
RuntimeError: Model class apps.home.models.Banner doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS.

原因是 有些地方的导入有问题,虽然apps加入到了环境变量中,但是还是不行,找到从apps下导入的
句式都改成从根目录下开始导入!!!就解决报错了!!!

 。

自定义配置

##1  settings下新建 common_settings.py
# 用户自己配置,以后开发或上线环境都会用
BANNER_COUNT = 3

# 2 以后再dev.py和pro.py都导入
# 导入公共配置
from .common_settings import *

# 3 在任意位置,导入django配置文件
from django.conf import settings
queryset = Banner.objects.all().filter(is_delete=False, is_show=True).order_by('orders')[0:settings.BANNER_COUNT]

admin使用simpleui

# 1 下载
pip install django-simpleui
# 2 app注册
INSTALLED_APPS = [
    'simpleui',
]
# 3 在admin中注册
from .models import Banner
@admin.register(Banner)
class BannerAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'image', 'link')
#4在home.app中
class HomeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'home'
verbose_name = '首页'

 轮播图增加

 。

【跨域问题详解】

# 1 浏览器同源策略
    -同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能
    -请求的url地址,必须与浏览器上的url地址处于同域上,也就是域名(www.xx.com),端口(80,8080),协议(ftp,https,http)相同.
    -浏览器上就会报错,个就是同源策略的保护
    
    
# 2 浏览器的安全策略
    -访问一个跨域[域名,端口,协议不同]接口---》后端正常执行--》浏览器拦截了
    -只有web端需要处理跨域,小程序,app都不需要
    
# 3 解决跨域问题
    -cors :咱们使用的,后端配置
    -JSONP:忽略
    -nginx代理跨域(服务器代理)
    -vue 代理(开发阶段)
    
    
# 4  CORS(跨域资源共享)简介
CORS需要浏览器和服务器【响应头加东西】同时支持。目前,所有浏览器都支持该功能

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信


# 5 cors 两种请求
    -简单请求:请求一次--》直接发送--》服务的处理了cors--》顺利返回--》如果服务的没处理就报错
   
    -非简单请求:先发一次options请求--》服务的如果处理了cors--》再发真正的请求-》如果服务端没处理,它就不发了
  
#  6 简单和非简单区别
    # 符合下面两个条件就是简单请求
    - 请求方法是以下三种方法之一:
        HEAD
        GET
        POST
    -HTTP的头信息不超出以下几种字段:
        Accept
        Accept-Language
        Content-Language
        Last-Event-ID
        Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

 。

自定义中间件处理跨域

 1 # 自定义中间件
 2 from django.utils.deprecation import MiddlewareMixin
 3 
 4 
 5 class CorsMiddleWare(MiddlewareMixin):
 6     def process_response(self, request, response):
 7         if request.method == "OPTIONS":
 8             # 可以加*
 9             response["Access-Control-Allow-Headers"] = "*"
10             response['Access-Control-Allow-Methods'] = '*'
11         response["Access-Control-Allow-Origin"] = "*"
12         return response
13 
14 ---------------------------------
15 #dev 配置文件配置##
16 MIDDLEWARE = [
17     'utils.common_middleware.CorsMiddleWare'
18 ]
19 
20 ---------------------
21 # 测试跨域问题
22 def cors_view(request):
23     res = HttpResponse('ok')
24     # res['Access-Control-Allow-Origin'] = '*'
25     # if request.method == 'OPTIONS':
26     #     res['Access-Control-Allow-Headers'] = '*'
27     return res
路由:

path('cors/', cors_view)

===========================================================================
后端homeview.vue
created(){
this.$axios({
url:'http://127.0.0.1:8000/api/v1/user/cors/',
method:'GET',
headers:{
token:'aa,bb,cc'
}
}).then(res=>{
console.log(res)
}).catch(err=>{
console.log('err:',err)
})
},


第三方app解决跨域问题

 1 # 1、使用pip安装
 2 pip install django-cors-headers
 3 
 4 # 2、添加到setting的app中
 5 INSTALLED_APPS = (
 6     ...
 7     'corsheaders',
 8     ...
 9 )
10 # 3、添加中间件
11 MIDDLEWARE = [  
12     ...
13     'corsheaders.middleware.CorsMiddleware',
14     ...
15 ]
16 
17 ------------------------------
18 在公共的配置文件了设置
19 CORS_ORIGIN_ALLOW_ALL = True
20 CORS_ALLOW_METHODS = (
21     'DELETE',
22     'GET',
23     'OPTIONS',
24     'PATCH',
25     'POST',
26     'PUT',
27     'VIEW',
28 )
29 
30 CORS_ALLOW_HEADERS = (
31     'XMLHttpRequest',
32     'X_FILENAME',
33     'accept-encoding',
34     'authorization',
35     'content-type',
36     'dnt',
37     'origin',
38     'user-agent',
39     'x-csrftoken',
40     'x-requested-with',
41     'Pragma',
42     'token',
43 )

【首页前后端打通】

Header.vue组件

  1 <script>
  2 export default {
  3   name: "Header",
  4   data() {
  5     return {
  6       url_path: sessionStorage.url_path || '/',
  7     }
  8   },
  9   methods: {
 10     goPage(url_path) {
 11       // 已经是当前路由就没有必要重新跳转
 12       if (this.url_path !== url_path) {
 13         this.$router.push(url_path);
 14       }
 15       sessionStorage.url_path = url_path;
 16     },
 17   },
 18   created() {
 19     // this.$route.path 当前路径   /      /about     /course
 20     sessionStorage.url_path = this.$route.path;
 21     this.url_path = this.$route.path;
 22   }
 23 }
 24 </script>
 25 
 26 <template>
 27   <div class="header">
 28     <div class="slogan">
 29       <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
 30     </div>
 31     <div class="nav">
 32       <ul class="left-part">
 33         <li class="logo">
 34           <router-link to="/">
 35             <img src="../assets/img/head-logo.svg" alt="">
 36           </router-link>
 37         </li>
 38         <li class="ele">
 39           <span @click="goPage('/free')" :class="{active: url_path === '/free'}">免费课</span>
 40         </li>
 41         <li class="ele">
 42           <span @click="goPage('/actual')" :class="{active: url_path === '/actual'}">实战课</span>
 43         </li>
 44         <li class="ele">
 45           <span @click="goPage('/light')" :class="{active: url_path === '/light'}">轻课</span>
 46         </li>
 47       </ul>
 48 
 49       <div class="right-part">
 50         <div>
 51           <span>登录</span>
 52           <span class="line">|</span>
 53           <span>注册</span>
 54         </div>
 55       </div>
 56     </div>
 57   </div>
 58 </template>
 59 
 60 <style scoped>
 61 .header {
 62   background-color: white;
 63   box-shadow: 0 0 5px 0 #aaa;
 64 }
 65 
 66 .header:after {
 67   content: "";
 68   display: block;
 69   clear: both;
 70 }
 71 
 72 .slogan {
 73   background-color: #eee;
 74   height: 40px;
 75 }
 76 
 77 .slogan p {
 78   width: 1200px;
 79   margin: 0 auto;
 80   color: #aaa;
 81   font-size: 13px;
 82   line-height: 40px;
 83 }
 84 
 85 .nav {
 86   background-color: white;
 87   user-select: none;
 88   width: 1200px;
 89   margin: 0 auto;
 90 
 91 }
 92 
 93 .nav ul {
 94   padding: 15px 0;
 95   float: left;
 96 }
 97 
 98 .nav ul:after {
 99   clear: both;
100   content: '';
101   display: block;
102 }
103 
104 .nav ul li {
105   float: left;
106 }
107 
108 .logo {
109   margin-right: 20px;
110 }
111 
112 .ele {
113   margin: 0 20px;
114 }
115 
116 .ele span {
117   display: block;
118   font: 15px/36px '微软雅黑';
119   border-bottom: 2px solid transparent;
120   cursor: pointer;
121 }
122 
123 .ele span:hover {
124   border-bottom-color: orange;
125 }
126 
127 .ele span.active {
128   color: orange;
129   border-bottom-color: orange;
130 }
131 
132 .right-part {
133   float: right;
134 }
135 
136 .right-part .line {
137   margin: 0 10px;
138 }
139 
140 .right-part span {
141   line-height: 68px;
142   cursor: pointer;
143 }
144 </style>
Header.vue

Footer.vue组件

 1 <script>
 2 export default {
 3   name: "Footer"
 4 }
 5 </script>
 6 
 7 <template>
 8   <div class="footer">
 9     <ul>
10       <li>关于我们</li>
11       <li>联系我们</li>
12       <li>商务合作</li>
13       <li>帮助中心</li>
14       <li>意见反馈</li>
15       <li>新手指南</li>
16     </ul>
17     <p>Copyright © luffycity.com版权所有 | 京ICP备17072161号-1</p>
18   </div>
19 </template>
20 
21 <style scoped>
22 .footer {
23   width: 100%;
24   height: 128px;
25   background: #25292e;
26   color: #fff;
27 }
28 
29 .footer ul {
30   margin: 0 auto 16px;
31   padding-top: 38px;
32   width: 810px;
33 }
34 
35 .footer ul li {
36   float: left;
37   width: 112px;
38   margin: 0 10px;
39   text-align: center;
40   font-size: 14px;
41 }
42 
43 .footer ul::after {
44   content: "";
45   display: block;
46   clear: both;
47 }
48 
49 .footer p {
50   text-align: center;
51   font-size: 12px;
52 }
53 </style>
Footer.vue

Banner.vue组件

 1 <script>
 2 import api from '../assets/js/settings'
 3 
 4 export default {
 5   name: "Banner",
 6   data() {
 7     return {
 8       bannerList: []
 9     }
10   },
11   created() {
12     this.$axios.get(api.banner).then(res => {
13       console.log(res.data)
14       if (res.data.code == 100) {
15         this.bannerList = res.data.results
16       } else {
17         this.$message({type: "error", message: res.data.msg})
18       }
19     }).catch(err => {
20       this.$message({type: "error", message: '服务器异常,请联系系统管理员'})
21     })
22   }
23 }
24 </script>
25 
26 <template>
27   <div class="banner">
28     <el-carousel height="400px" :interval="3000">
29       <el-carousel-item v-for="(item,index) in bannerList" :key="index">
30         <!-- 跳内部,跳外链接-->
31         <div v-if="item.link.indexOf('http')>=0">
32           <a :href="item.link">
33             <img :src="item.image" :alt="item.title">
34           </a>
35         </div>
36 
37         <div v-else>
38           <router-link :to="item.link">
39             <img :src="item.image" :alt="item.title">
40           </router-link>
41         </div>
42       </el-carousel-item>
43     </el-carousel>
44   </div>
45 </template>
46 
47 <style scoped>
48 .el-carousel__item {
49   height: 400px;
50   min-width: 1200px;
51 }
52 
53 .el-carousel__item img {
54   height: 400px;
55   margin-left: calc(50% - 1920px / 2);
56 }
57 </style>
Banner.vue

HomeView.vue

 1 <template>
 2   <div class="home">
 3     <Header></Header>
 4     <Banner></Banner>
 5     <div class="course">
 6       <el-row>
 7         <el-col :span="6" v-for="(o, index) in 8" :key="o" class="course_detail">
 8           <el-card :body-style="{ padding: '0px' }">
 9             <img src="http://photo.liuqingzheng.top/2023%2002%2022%2021%2057%2011%20/image-20230222215707795.png"
10                  class="image">
11             <div style="padding: 14px;">
12               <span>推荐课程</span>
13               <div class="bottom clearfix">
14                 <time class="time">价格:999</time>
15                 <el-button type="text" class="button">查看详情</el-button>
16               </div>
17             </div>
18           </el-card>
19         </el-col>
20       </el-row>
21     </div>
22     <img src="http://photo.liuqingzheng.top/2023%2003%2001%2016%2010%2034%20/1.png" alt="" width="100%" height="500px">
23     <Footer></Footer>
24   </div>
25 </template>
26 
27 <script>
28 
29 
30 import Header from "@/components/Header.vue";
31 import Footer from "@/components/Footer.vue";
32 import Banner from "@/components/Banner.vue";
33 
34 export default {
35   name: 'HomeView',
36   methods: {
37     getdata() {
38       async function a() {
39         let response = await this.$axios(this.$settings.banner)
40 
41       }
42     }
43   },
44   created() {
45     this.$axios({
46       url: 'http://127.0.0.1:8000/api/v1/user/cors/',
47       method: 'GET',
48       headers: {
49         token: 'aa,bb,cc'
50       }
51     }).then(res => {
52       console.log(res)
53     }).catch(err => {
54       console.log('err:', err)
55     })
56   },
57   components: {
58     Header,
59     Footer,
60     Banner,
61   }
62 }
63 </script>
64 
65 <style scoped>
66 .time {
67   font-size: 13px;
68   color: #999;
69 }
70 
71 .bottom {
72   margin-top: 13px;
73   line-height: 12px;
74 }
75 
76 .button {
77   padding: 0;
78   float: right;
79 }
80 
81 .image {
82   width: 100%;
83   display: block;
84 }
85 
86 .clearfix:before,
87 .clearfix:after {
88   display: table;
89   content: "";
90 }
91 
92 .clearfix:after {
93   clear: both
94 }
95 
96 .course_detail {
97   padding: 50px;
98 }
99 </style>
HomeView.vue

 

 。

#1 登陆和注册的,要写的接口
    -1 发送短信验证码接口
    -2 多方式登陆接口(用户名/邮箱/手机号+密码)
    -3 手机号+验证码登陆
    -4 手机号+验证码+密码 ---》短信注册接口
    -5 校验手机号是否存在接口
    
    
# 2 发送短信:
    阿里大于短信
    腾讯云短信:讲课
    容联云通信
    
    
# 3 注册腾讯云短信
    -1 地址:https://cloud.tencent.com/product/sms
    -2 创建签名
        -公众号:注册个公众号
            -微信公众号
    -3 创建模版
        
    
        
# 4 备案网站
    -1 买域名
    -2 服务器:包年包月
    -3 部署项目:个人博客
    -4 阿里云初审:
    -5 工信部备案
        -30天左右
    -6 备案过的域名

 。

手机号是否存在接口

 1 app.user.view
 2 
 3 from rest_framework.viewsets import ViewSet
 4 from rest_framework.decorators import action
 5 from .models import User
 6 from utils.common_response import APIResponse
 7 
 8 
 9 class UserMobileView(ViewSet):
10     @action(methods=['get'], detail=False)
11     def check_mobile(self, request, *args, **kwargs):
12         try:
13             # 获取手机号  放在地址栏中
14             mobile = request.query_params.get('mobile')
15             # 数据库校验手机号是否存在
16             User.objects.get(mobile=mobile)
17             return APIResponse(msg='手机号已存在')
18         except Exception as e:
19             raise Exception('手机号不存在')
20 
21 ***********************************
22 urls
23 
24 from rest_framework.routers import SimpleRouter
25 
26 router = SimpleRouter()
27 # http://127.0.0.1:8000/api/v1/user/mobile/check_mobile/
28 router.register('mobile', UserMobileView, 'mobile')
29 
30 urlpatterns += router.urls

多方式登录接口

 1 view.py
 2 
 3 from .serializer import LoginSerializer
 4 
 5 
 6 class UserView(GenericViewSet):
 7     serializer_class = LoginSerializer
 8 
 9     @action(methods=['POST'], detail=False)
10     def mul_login(self, request, *args, **kwargs):
11         # 1 把校验逻辑写在序列化类中
12         serializer: LoginSerializer = self.get_serializer(data=request.data)
13         serializer.is_valid(raise_exception=True)
14         token = serializer.context.get('token')
15         username = serializer.context.get('username')
16         icon = serializer.context.get('icon')
17         return APIResponse(token=token, username=username, icon=icon)
18         #  另一种方案,但是 serializer.instance 必须是user对象
19         # serializer.data # {'username.icon}
20         # print(serializer.data)
21         # return APIResponse(token=token, username=serializer.data.get('username'), icon=serializer.data.get('icon'))
22 -----------------------------------
23 序列化类
24 
25 from rest_framework import serializers
26 from .models import User
27 import re
28 from rest_framework.exceptions import ValidationError
29 from rest_framework_simplejwt.tokens import RefreshToken
30 from django.conf import settings
31 
32 
33 class LoginSerializer(serializers.ModelSerializer):
34     # 这个序列化类用来: 1 校验
35 
36     # 坑:必须重写username--》去除它的unique
37     username = serializers.CharField()
38 
39     class Meta:
40         model = User
41         fields = ['username', 'password', 'icon']
42         extra_kwargs = {
43             'password': {'write_only': True}  # 它不做序列化
44         }
45 
46     def _get_user(self, attrs):
47         username = attrs.get('username')
48         password = attrs.get('password')
49         # 2 去数据库校验:正则--》
50         if re.match(r'^1[3-9][0-9]{9}$', username):
51             user = User.objects.filter(mobile=username).first()
52         elif re.match('^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$', username):
53             user = User.objects.filter(email=username).first()
54         else:
55             user = User.objects.filter(username=username).first()
56 
57         if user and user.check_password(password):
58             return user
59         else:
60             raise ValidationError('用户名或密码错误')
61 
62     def _get_token(self, user):
63         refresh = RefreshToken.for_user(user)
64         return str(refresh.access_token)
65 
66     def _pre_data(self, token, user):
67         self.context['token'] = token
68         self.context['username'] = user.username
69         # self.instance=user # 当前用户,放到instance中了
70         self.context['icon'] = settings.BACKEND_URL + "media/" + str(user.icon)  # 不带 域名前缀的
71 
72     def validate(self, attrs):
73         # 1 取出用户名(手机号,邮箱)和密码
74         user = self._get_user(attrs)
75         # 2 如果存在:签发token,返回
76         token = self._get_token(user)
77         # 3 把token,用户名和icon放入context
78         self._pre_data(token, user)
79         return attrs
80 
81 
82 ----------
83 公共配置文件里配置
84 
85 BACKEND_URL = 'http://127.0.0.1:8000/'

异常处理修改

 1 from rest_framework.views import exception_handler as drf_exception_handler
 2 from rest_framework.response import Response
 3 
 4 from luffytest.utils.common_logger import logger
 5 
 6 
 7 # 以后可能会自定义自定义异常类
 8 class NoPermissionException(Exception):
 9     pass
10 
11 
12 def exception_handler(exc, context):
13     # 记录日志
14     request = context.get('request')
15     view = context.get('view')
16     ip = request.META.get('REMOTE_ADDR')
17     path = request.get_full_path()
18     method = request.method
19     user_id = request.user.id or '【匿名用户】'
20     logger.error(
21         f'操作出错:{str(exc)},ip地址为:{ip},请求方式是:{method},请求地址是:{path},用户为:{user_id},视图类为:{str(view)}')
22     res = drf_exception_handler(exc, context)
23     if res:
24         # drf的异常 data=['错误1',错误2]    data={detail:'sss'}
25         if isinstance(res.data, dict):
26             err = res.data.get('detail') or res.data.get('non_field_errors')[0] or '系统错误'
27         elif isinstance(res.data, list):
28             err = res.data[0]
29         else:
30             err = '服务异常,请稍后再尝试,-drf'
31         response = Response({'code': 999, 'msg': err})
32     else:
33         # 非drf异常,更细力度的区分异常
34         if isinstance(exc, ZeroDivisionError):
35             err = '数据操作出错,除以0了'
36             code = 909
37         elif isinstance(exc, NoPermissionException):
38             err = f'您没有操作权限:{str(exc)}'
39             code = 906
40         else:
41             err = f'{str(exc)}'
42             code = 909
43         response = Response({'code': code, 'msg': err})
44 
45     return response

腾讯云短信封装

腾讯云短信申请

# 发送短信接口,借助于第三方短信平台,收费的
    -腾讯云短信
    -阿里 大于短信
    -。。。。

---------------------------------------



# 使用腾讯短信
    https://cloud.tencent.com,微信扫码登录
    先点产品,再在搜索框里面搜索短信,回车调到短信的页面
    https://console.cloud.tencent.com/smsv2

    1 创建短信签名:公众号注册,提交等待审核通过
    2 需要先申请微信公众号,用网站或app认证比较烦

    3 创建短信正文模版
    -等待审核
    -发送短信
        python代码发送昂短信

 # API SDK

-API: 腾讯自己的API接口,我们要带着数据向对应的地址发送post请求,
        然后腾讯会根据我们post携带的对应数据,发送对应的短信给对应的人
       写起来比较麻烦,要自己分析腾讯的接口
------------------------------------------------

-SDK:集成开发工具包,分语言,java,python,go
	-使用python 对api进行封装成包
	-以后我们只需要,安装包,导入包,包名.发送短信,传入参数,就可以发送了

------------------------------------------------

-只要官方提供sdk,优先用sdk
	先下载adk的模块
	pip install tencentcloud-sdk-python
 

python sdk

 安装python项目里面安装sdk模块

 

把发送短信的代码整体复制到,python文件里面去,对应参数改好,右键运行,就行了

 需要修改5个参数

 生成密钥

 

 

 

 

发送短信

 1 # -*- coding: utf-8 -*-
 2 from tencentcloud.common import credential
 3 from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
 4 # 导入对应产品模块的client models。
 5 from tencentcloud.sms.v20210111 import sms_client, models
 6 
 7 # 导入可选配置类
 8 from tencentcloud.common.profile.client_profile import ClientProfile
 9 from tencentcloud.common.profile.http_profile import HttpProfile
10 
11 try:
12     # SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi
13     cred = credential.Credential("AKIDlT7aqG7wkZONVkzEY5lSEB39QqGdFUmz", "SwMkHnEmyIXkv4bKFAFe5c9ZAkT7lXEl")
14     httpProfile = HttpProfile()
15     # httpProfile = HttpProfile(proxy="http://用户名:密码@代理IP:代理端口")
16     httpProfile.reqMethod = "POST"  # post请求(默认为post请求)
17     httpProfile.reqTimeout = 30  # 请求超时时间,单位为秒(默认60秒)
18     httpProfile.endpoint = "sms.tencentcloudapi.com"  # 指定接入地域域名(默认就近接入)
19 
20     clientProfile = ClientProfile()
21     clientProfile.signMethod = "TC3-HMAC-SHA256"  # 指定签名算法
22     clientProfile.language = "en-US"
23     clientProfile.httpProfile = httpProfile
24     client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)
25     req = models.SendSmsRequest()
26     # 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看
27     req.SmsSdkAppId = "1400909757"
28     req.SignName = "阿浩取经公众号"
29     # 模板 ID: 必须填写已审核通过的模板 ID
30     req.TemplateId = "2153916"
31     # 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,,若无模板参数,则设置为空
32     req.TemplateParamSet = ["123456"]
33     # 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号
34     req.PhoneNumberSet = ["+8618451110811"]
35     req.SessionContext = ""
36     req.ExtendCode = ""
37     req.SenderId = ""
38     resp = client.SendSms(req)
39     # 输出json格式的字符串回包
40     print(resp.to_json_string(indent=2))
41 
42 
43 
44 except TencentCloudSDKException as err:
45     print(err)

封装成包

# 1 包结构 libs
	-tx_sms
    	-__init__.py #给外部使用的,在这注册
        -settings.py # 配置
        -sms.py      # 核心
settings


SECRET_ID = 'AKIDlT7aqG7wkZONVkzEY5lSEB39QqGdFUmz'
SECRET_KEY = 'SwMkHnEmyIXkv4bKFAFe5c9ZAkT7lXEl'
SmsSdkAppId = "1400909757"
SIGN_NAME = '阿浩取经公众号'
TemplateId = "2153916"
sms.py

# -*- coding: utf-8 -*-
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sms.v20210111 import sms_client, models
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
import random
from .settings import *


# 生成n位数字的随机验证码
def get_code(num=4):
    code = ''
    for i in range(num):
        r = random.randint(0, 9)
        code += str(r)

    return code


# 发送短信函数
def send_sms(mobile, code):
    try:
        # SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi
        cred = credential.Credential(SECRET_ID, SECRET_KEY)
        httpProfile = HttpProfile()
        # httpProfile = HttpProfile(proxy="http://用户名:密码@代理IP:代理端口")
        httpProfile.reqMethod = "POST"  # post请求(默认为post请求)
        httpProfile.reqTimeout = 30  # 请求超时时间,单位为秒(默认60秒)
        httpProfile.endpoint = "sms.tencentcloudapi.com"  # 指定接入地域域名(默认就近接入)

        clientProfile = ClientProfile()
        clientProfile.signMethod = "TC3-HMAC-SHA256"  # 指定签名算法
        clientProfile.language = "en-US"
        clientProfile.httpProfile = httpProfile
        client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)
        req = models.SendSmsRequest()
        # 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看
        req.SmsSdkAppId = SmsSdkAppId
        req.SignName = SIGN_NAME
        # 模板 ID: 必须填写已审核通过的模板 ID
        req.TemplateId = TemplateId
        # 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,,若无模板参数,则设置为空
        req.TemplateParamSet = [code]
        # 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号
        req.PhoneNumberSet = [f"+86{mobile}"]
        req.SessionContext = ""
        req.ExtendCode = ""
        req.SenderId = ""
        resp = client.SendSms(req)
        # 输出json格式的字符串回包
        print(resp.to_json_string(indent=2))
        res = resp.to_json_string(indent=2)
        if res.get('SendStatusSet')[0].get('Code') == 'Ok':
            return True
        else:
            return False

    except TencentCloudSDKException as err:
        print(err)
        return False


if __name__ == '__main__':
    print(get_code(3))
init

from .sms import get_code,send_sms

发送短信

class UserMobileView(ViewSet):
    # 发送短信接口
    @action(methods=['GET'], detail=False)
    def send_sms(self, request, *args, **kwargs):
        # 后续要保证发送短信接口安全
        # 1 取出手机号
        mobile = request.query_params.get('mobile')
        # 2 生成验证码,保存到 缓存中--》后续要取出来校验
        code = get_code()
        print(code)
        cache.set(f'cache_code_{mobile}', code)  # key 要唯一,根据手机号唯一
        # # 3 发送短信--》同步发送
        # res=sms(mobile,code)
        # # 4 返回给前端
        # if res:
        #     return APIResponse(msg='短信发送成功')
        # else:
        #     return APIResponse(code=101,msg='发送短信失败,请稍后再试')

        # 3 异步发送短信,使用多线程,后期会用别的
        t = Thread(target=sms, args=[mobile, code])
        t.start()
        return APIResponse(msg='短信已发送')

短信登陆

视图类

 1 from .serializer import LoginSerializer,SMSLoginSerializer
 2 
 3 
 4 class UserView(GenericViewSet):
 5     serializer_class = LoginSerializer
 6     @action(methods=['POST'], detail=False)
 7     def mul_login(self, request, *args, **kwargs):
 8         return self._login(request, *args, **kwargs)
 9 
10     @action(methods=['POST'], detail=False)
11     def sms_login(self, request, *args, **kwargs):
12         return self._login(request, *args, **kwargs)
13 
14     def _login(self, request, *args, **kwargs):
15         serializer = self.get_serializer(data=request.data)
16         serializer.is_valid(raise_exception=True)
17         token = serializer.context.get('token')
18         username = serializer.context.get('username')
19         icon = serializer.context.get('icon')
20         return APIResponse(token=token, username=username, icon=icon)
21 
22     def get_serializer_class(self):
23         if self.action == 'sms_login':
24             return SMSLoginSerializer
25         else:
26             return LoginSerializer

序列化类


from rest_framework import serializers
from .models import User
import re
from rest_framework.exceptions import ValidationError
from rest_framework_simplejwt.tokens import RefreshToken
from django.conf import settings
from django.core.cache import cache


class CommonLoginSerializer:
def _get_user(self, attrs):
raise Exception('这个方法必须被重写')

def _get_token(self, user):
refresh = RefreshToken.for_user(user)
return str(refresh.access_token)

def _pre_data(self, token, user):
self.context['token'] = token
self.context['username'] = user.username
# self.instance=user # 当前用户,放到instance中了
self.context['icon'] = settings.BACKEND_URL + "media/" + str(user.icon) # 不带 域名前缀的

def validate(self, attrs):
# 1 取出用户名(手机号,邮箱)和密码
user = self._get_user(attrs)
# 2 如果存在:签发token,返回
token = self._get_token(user)
# 3 把token,用户名和icon放入context
self._pre_data(token, user)
return attrs


class SMSLoginSerializer(CommonLoginSerializer, serializers.Serializer):
# 重写
code = serializers.CharField()
mobile = serializers.CharField()

def _get_user(self, attrs):
# 1 取出手机号,取出验证码
mobile = attrs.get('mobile')
code = attrs.get('code')
# 2 校验验证码
old_code = cache.get(f'cache_code_{mobile}')
assert old_code == code or (settings.DEBUG and code == '8888'), ValidationError('验证码错误')
user = User.objects.filter(mobile=mobile).first()
assert user, ValidationError('该手机号用户没注册')
return user


class LoginSerializer(CommonLoginSerializer,serializers.ModelSerializer):
# 这个序列化类用来: 1 校验

# 坑:必须重写username--》去除它的unique
username = serializers.CharField()

class Meta:
model = User
fields = ['username', 'password', 'icon']
extra_kwargs = {
'password': {'write_only': True} # 它不做序列化
}

def _get_user(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
# 2 去数据库校验:正则--》
if re.match(r'^1[3-9][0-9]{9}$', username):
user = User.objects.filter(mobile=username).first()
elif re.match('^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$', username):
user = User.objects.filter(email=username).first()
else:
user = User.objects.filter(username=username).first()

if user and user.check_password(password):
return user
else:
raise ValidationError('用户名或密码错误')

# def _get_token(self, user):
# refresh = RefreshToken.for_user(user)
# return str(refresh.access_token)

# def _pre_data(self, token, user):
# self.context['token'] = token
# self.context['username'] = user.username
# # self.instance=user # 当前用户,放到instance中了
# self.context['icon'] = settings.BACKEND_URL + "media/" + str(user.icon) # 不带 域名前缀的

# def validate(self, attrs):
# # 1 取出用户名(手机号,邮箱)和密码
# user = self._get_user(attrs)
# # 2 如果存在:签发token,返回
# token = self._get_token(user)
# # 3 把token,用户名和icon放入context
# self._pre_data(token, user)
# return attrs


.

短信注册

  前端携带数据    -{mobile:,code:8888,password:123456}

视图类

 1 # 注册接口
 2 from .serializer import RegisterSerializer
 3 class UserRegisterView(GenericViewSet):
 4     serializer_class =RegisterSerializer
 5 
 6     def create(self,request,*args,**kwargs):
 7         # 逻辑写在序列化类中
 8         serializer=self.get_serializer(data=request.data)
 9         serializer.is_valid(raise_exception=True)
10         serializer.save()
11         return APIResponse(msg='注册成功')

序列化类

 1 class RegisterSerializer(serializers.ModelSerializer):
 2     # 校验和保存(create)
 3     code = serializers.CharField()
 4     class Meta:
 5         model = User
 6         fields = ['mobile', 'password', 'code']
 7 
 8 
 9     def validate(self, attrs):
10         # 1 取出code,验证code
11         code = attrs.pop('code')
12         mobile = attrs.get('mobile')
13         # 2 校验验证码
14         old_code = cache.get(f'cache_code_{mobile}')
15         assert old_code == code or (settings.DEBUG and code == '8888'), APIException('验证码错误')
16         # 2 取出手机号和密码--》创建用户-->有些字段必填
17         # 用户名必填
18         attrs['username'] = mobile
19         # 3 返回
20         return attrs
21 
22     def create(self, validated_data):
23         # validated_data:code,mobile,password,username
24         user = User.objects.create_user(**validated_data)
25         return user

多方式登陆前端

  1 <template>
  2   <div class="login">
  3     <div class="box">
  4       <i class="el-icon-close" @click="close_login"></i>
  5       <div class="content">
  6         <div class="nav">
  7           <span :class="{active: login_method === 'is_pwd'}" @click="change_login_method('is_pwd')">密码登录</span>
  8           <span :class="{active: login_method === 'is_sms'}" @click="change_login_method('is_sms')">短信登录</span>
  9         </div>
 10         <el-form v-if="login_method === 'is_pwd'">
 11           <el-input
 12               placeholder="用户名/手机号/邮箱"
 13               prefix-icon="el-icon-user"
 14               v-model="username"
 15               clearable>
 16           </el-input>
 17           <el-input
 18               placeholder="密码"
 19               prefix-icon="el-icon-key"
 20               v-model="password"
 21               clearable
 22               show-password>
 23           </el-input>
 24           <el-button type="primary" @click="handleLogin">登录</el-button>
 25         </el-form>
 26         <el-form v-if="login_method === 'is_sms'">
 27           <el-input
 28               placeholder="手机号"
 29               prefix-icon="el-icon-phone-outline"
 30               v-model="mobile"
 31               clearable
 32               @blur="check_mobile">
 33           </el-input>
 34           <el-input
 35               placeholder="验证码"
 36               prefix-icon="el-icon-chat-line-round"
 37               v-model="sms"
 38               clearable>
 39             <template slot="append">
 40               <span class="sms" @click="send_sms">{{ sms_interval }}</span>
 41             </template>
 42           </el-input>
 43           <el-button type="primary" @click="handleSMSLogin">登录</el-button>
 44         </el-form>
 45         <div class="foot">
 46           <span @click="go_register">立即注册</span>
 47         </div>
 48       </div>
 49     </div>
 50   </div>
 51 </template>
 52 
 53 
 54 <script>
 55 import api from '../assets/js/settings'
 56 
 57 export default {
 58   name: "Login",
 59   data() {
 60     return {
 61       username: '',
 62       password: '',
 63       mobile: '',
 64       sms: '',
 65       login_method: 'is_pwd',
 66       sms_interval: '获取验证码',
 67       is_send: false,
 68     }
 69   },
 70   methods: {
 71     close_login() {
 72       this.$emit('close')
 73     },
 74     go_register() {
 75       this.$emit('go')
 76     },
 77     change_login_method(method) {
 78       this.login_method = method;
 79     },
 80     check_mobile() {
 81       if (!this.mobile) return;
 82       if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
 83         this.$message({
 84           message: '手机号有误',
 85           type: 'warning',
 86           duration: 1000,
 87           onClose: () => {
 88             this.mobile = '';
 89           }
 90         });
 91         return false;
 92       }
 93       // 校验手机号是否能发送验证码:只有后台存在才能发
 94       this.$axios({
 95         url: api.check_mobile,
 96         params: {'mobile': this.mobile}
 97       }).then(res => {
 98         if (res.data.code == 100) {
 99           this.$message({type: "success", message: '手机号正确,可以正常发送'})
100           this.is_send = true; // 发送验证码可以点击
101         } else {
102           // 手机号,后端没有,让它注册
103           this.$message({type: "error", message: '手机号尚未注册,请先注册'})
104           this.mobile = ''
105         }
106       })
107 
108 
109     },
110     send_sms() {
111       if (!this.is_send) return;
112       this.is_send = false; // 发送短信按钮不能点击了
113       let sms_interval_time = 60;
114       this.sms_interval = "发送中...";
115       let timer = setInterval(() => {
116         if (sms_interval_time <= 1) {
117           clearInterval(timer);
118           this.sms_interval = "获取验证码";
119           this.is_send = true; // 重新回复点击发送功能的条件
120         } else {
121           sms_interval_time -= 1;
122           this.sms_interval = `${sms_interval_time}秒后再发`;
123         }
124       }, 1000);
125       // 发送短信
126       this.$axios({
127         url: api.send_sms,
128         params: {mobile: this.mobile}
129       }).then(res => {
130         if (res.data.code == 100) {
131           this.$message('短信发送成功')
132         }
133       })
134 
135 
136     },
137     // 多方式登陆
138     handleLogin() {
139       // 1 用户名密码框不能为空
140       if (this.username && this.password) {
141         this.$axios({
142           url: api.mul_login,
143           method: 'POST',
144           data: {
145             username: this.username,
146             password: this.password
147           },
148         }).then(res => {
149           if (res.data.code == 100) {
150             // 登陆成功
151             // 1 把token,username,icon,存到cookie中
152             this.$cookies.set('token', res.data.token, '7d')
153             this.$cookies.set('username', res.data.username, '7d')
154             this.$cookies.set('icon', res.data.icon, '7d')
155             // 2 关闭当前模态框
156             this.$emit('close')
157           } else {
158             // 登陆失败
159             this.$message({
160               type: 'error',
161               text: res.data.msg
162             })
163           }
164         }).catch(err => {
165 
166         })
167       } else {
168         this.$message('用户名和密码必须')
169       }
170     },
171 
172     // 短信登陆
173     handleSMSLogin() {
174       if (this.mobile && this.sms) {
175         this.$axios({
176           url: api.sms_login,
177           method: 'POST',
178           data: {
179             mobile: this.mobile,
180             code: this.sms
181           },
182         }).then(res => {
183           if (res.data.code == 100) {
184             // 登陆成功
185             // 1 把token,username,icon,存到cookie中
186             this.$cookies.set('token', res.data.token, '7d')
187             this.$cookies.set('username', res.data.username, '7d')
188             this.$cookies.set('icon', res.data.icon, '7d')
189             // 2 关闭当前模态框
190             this.$emit('close')
191           } else {
192             // 登陆失败
193             this.$message({
194               type: 'error',
195               text: res.data.msg
196             })
197           }
198         }).catch(err => {
199 
200         })
201       } else {
202         this.$message('手机号和验证码不能为空')
203       }
204     }
205   }
206 }
207 </script>
208 
209 <style scoped>
210 .login {
211   width: 100vw;
212   height: 100vh;
213   position: fixed;
214   top: 0;
215   left: 0;
216   z-index: 10;
217   background-color: rgba(0, 0, 0, 0.3);
218 }
219 
220 .box {
221   width: 400px;
222   height: 420px;
223   background-color: white;
224   border-radius: 10px;
225   position: relative;
226   top: calc(50vh - 210px);
227   left: calc(50vw - 200px);
228 }
229 
230 .el-icon-close {
231   position: absolute;
232   font-weight: bold;
233   font-size: 20px;
234   top: 10px;
235   right: 10px;
236   cursor: pointer;
237 }
238 
239 .el-icon-close:hover {
240   color: darkred;
241 }
242 
243 .content {
244   position: absolute;
245   top: 40px;
246   width: 280px;
247   left: 60px;
248 }
249 
250 .nav {
251   font-size: 20px;
252   height: 38px;
253   border-bottom: 2px solid darkgrey;
254 }
255 
256 .nav > span {
257   margin: 0 20px 0 35px;
258   color: darkgrey;
259   user-select: none;
260   cursor: pointer;
261   padding-bottom: 10px;
262   border-bottom: 2px solid darkgrey;
263 }
264 
265 .nav > span.active {
266   color: black;
267   border-bottom: 3px solid black;
268   padding-bottom: 9px;
269 }
270 
271 .el-input, .el-button {
272   margin-top: 40px;
273 }
274 
275 .el-button {
276   width: 100%;
277   font-size: 18px;
278 }
279 
280 .foot > span {
281   float: right;
282   margin-top: 20px;
283   color: orange;
284   cursor: pointer;
285 }
286 
287 .sms {
288   color: orange;
289   cursor: pointer;
290   display: inline-block;
291   width: 70px;
292   text-align: center;
293   user-select: none;
294 }
295 </style>
LoginView

  1 <template>
  2   <div class="header">
  3     <div class="slogan">
  4       <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
  5     </div>
  6     <div class="nav">
  7       <ul class="left-part">
  8         <li class="logo">
  9           <router-link to="/">
 10             <img src="../assets/img/head-logo.svg" alt="">
 11           </router-link>
 12         </li>
 13         <li class="ele">
 14           <span @click="goPage('/free')" :class="{active: url_path === '/free'}">免费课</span>
 15         </li>
 16         <li class="ele">
 17           <span @click="goPage('/actual')" :class="{active: url_path === '/actual'}">实战课</span>
 18         </li>
 19         <li class="ele">
 20           <span @click="goPage('/light')" :class="{active: url_path === '/light'}">轻课</span>
 21         </li>
 22       </ul>
 23 
 24       <div class="right-part">
 25         <div v-if="username && username.length>0">
 26           <span>{{ username }}</span>
 27           <span class="line">|</span>
 28           <span @click="handleLogout">注销</span>
 29         </div>
 30         <div v-else>
 31           <span @click="put_login">登录</span>
 32           <span class="line">|</span>
 33           <span @click="put_register">注册</span>
 34         </div>
 35 
 36       </div>
 37 
 38       <LoginView v-if="is_login" @close="close_login" @go="put_register"></LoginView>
 39       <RegisterView v-if="is_register" @close="close_register" @go="put_login"></RegisterView>
 40 
 41     </div>
 42   </div>
 43 </template>
 44 <script>
 45 import LoginView from "@/views/LoginView.vue";
 46 import RegisterView from "@/views/RegisterView.vue";
 47 
 48 export default {
 49   name: "Header",
 50   data() {
 51     return {
 52       url_path: sessionStorage.url_path || '/',
 53       is_login: false,
 54       is_register: false,
 55       username: ''
 56     }
 57   },
 58   methods: {
 59     handleLogout() {
 60       this.username = ''
 61       this.$cookies.remove('username')
 62       this.$cookies.remove('token')
 63       this.$cookies.remove('icon')
 64     },
 65     put_login() {
 66       this.is_login = true;
 67       this.is_register = false;
 68     },
 69     put_register() {
 70       this.is_login = false;
 71       this.is_register = true;
 72     },
 73     close_login() {
 74       // 子组件登陆成功或点击X,触发我
 75       this.is_login = false;
 76       // 如果是登陆成功,取出用户名
 77       this.username = this.$cookies.get('username')
 78 
 79     },
 80     close_register() {
 81       this.is_register = false;
 82     },
 83 
 84 
 85     goPage(url_path) {
 86       // 已经是当前路由就没有必要重新跳转
 87       if (this.url_path !== url_path) {
 88         this.$router.push(url_path);
 89       }
 90       sessionStorage.url_path = url_path;
 91     },
 92   },
 93   created() {
 94     // this.$route.path 当前路径   /      /about     /course
 95     sessionStorage.url_path = this.$route.path;
 96     this.url_path = this.$route.path;
 97 
 98     //当页面刷新,从cookie中取出用户名
 99     this.username = this.$cookies.get('username')
100   },
101   components: {
102     LoginView, RegisterView
103   }
104 }
105 </script>
106 <style scoped>
107 .header {
108   background-color: white;
109   box-shadow: 0 0 5px 0 #aaa;
110 }
111 
112 .header:after {
113   content: "";
114   display: block;
115   clear: both;
116 }
117 
118 .slogan {
119   background-color: #eee;
120   height: 40px;
121 }
122 
123 .slogan p {
124   width: 1200px;
125   margin: 0 auto;
126   color: #aaa;
127   font-size: 13px;
128   line-height: 40px;
129 }
130 
131 .nav {
132   background-color: white;
133   user-select: none;
134   width: 1200px;
135   margin: 0 auto;
136 
137 }
138 
139 .nav ul {
140   padding: 15px 0;
141   float: left;
142 }
143 
144 .nav ul:after {
145   clear: both;
146   content: '';
147   display: block;
148 }
149 
150 .nav ul li {
151   float: left;
152 }
153 
154 .logo {
155   margin-right: 20px;
156 }
157 
158 .ele {
159   margin: 0 20px;
160 }
161 
162 .ele span {
163   display: block;
164   font: 15px/36px '微软雅黑';
165   border-bottom: 2px solid transparent;
166   cursor: pointer;
167 }
168 
169 .ele span:hover {
170   border-bottom-color: orange;
171 }
172 
173 .ele span.active {
174   color: orange;
175   border-bottom-color: orange;
176 }
177 
178 .right-part {
179   float: right;
180 }
181 
182 .right-part .line {
183   margin: 0 10px;
184 }
185 
186 .right-part span {
187   line-height: 68px;
188   cursor: pointer;
189 }
190 </style>
Header.vue

  1 <template>
  2   <div class="register">
  3     <div class="box">
  4       <i class="el-icon-close" @click="close_register"></i>
  5       <div class="content">
  6         <div class="nav">
  7           <span class="active">新用户注册</span>
  8         </div>
  9         <el-form>
 10           <el-input
 11               placeholder="手机号"
 12               prefix-icon="el-icon-phone-outline"
 13               v-model="mobile"
 14               clearable
 15               @blur="check_mobile">
 16           </el-input>
 17           <el-input
 18               placeholder="密码"
 19               prefix-icon="el-icon-key"
 20               v-model="password"
 21               clearable
 22               show-password>
 23           </el-input>
 24           <el-input
 25               placeholder="验证码"
 26               prefix-icon="el-icon-chat-line-round"
 27               v-model="sms"
 28               clearable>
 29             <template slot="append">
 30               <span class="sms" @click="send_sms">{{ sms_interval }}</span>
 31             </template>
 32           </el-input>
 33           <el-button type="primary" @click="handleRegister">注册</el-button>
 34         </el-form>
 35         <div class="foot">
 36           <span @click="go_login">立即登录</span>
 37         </div>
 38       </div>
 39     </div>
 40   </div>
 41 </template>
 42 
 43 <script>
 44 import api from "@/assets/js/settings";
 45 
 46 export default {
 47   name: "Register",
 48   data() {
 49     return {
 50       mobile: '',
 51       password: '',
 52       sms: '',
 53       sms_interval: '获取验证码',
 54       is_send: false,
 55     }
 56   },
 57   methods: {
 58     close_register() {
 59       this.$emit('close', false)
 60     },
 61     go_login() {
 62       this.$emit('go')
 63     },
 64     check_mobile() {
 65       if (!this.mobile) return;
 66       if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
 67         this.$message({
 68           message: '手机号有误',
 69           type: 'warning',
 70           duration: 1000,
 71           onClose: () => {
 72             this.mobile = '';
 73           }
 74         });
 75         return false;
 76       }
 77 
 78       //跟后端交互,判断手机号是否注册过
 79       this.$axios({
 80         url: api.check_mobile,
 81         params: {'mobile': this.mobile}
 82       }).then(res => {
 83         if (res.data.code == 100) {
 84           this.$message({type: "error", message: '您已经注册过,请直接登陆'})
 85           this.mobile = ''
 86         } else {
 87           // 手机号,后端没有注册
 88           this.$message({type: "success", message: '手机号尚未注册'})
 89           this.is_send = true; // 发送验证码可以点击
 90 
 91         }
 92       })
 93     },
 94     send_sms() {
 95       if (!this.is_send) return;
 96       this.is_send = false;
 97       let sms_interval_time = 60;
 98       this.sms_interval = "发送中...";
 99       let timer = setInterval(() => {
100         if (sms_interval_time <= 1) {
101           clearInterval(timer);
102           this.sms_interval = "获取验证码";
103           this.is_send = true; // 重新回复点击发送功能的条件
104         } else {
105           sms_interval_time -= 1;
106           this.sms_interval = `${sms_interval_time}秒后再发`;
107         }
108       }, 1000);
109       // 发送短信
110       this.$axios({
111         url: api.send_sms,
112         params: {mobile: this.mobile}
113       }).then(res => {
114         if (res.data.code == 100) {
115           this.$message('短信发送成功')
116         }
117       })
118     },
119     handleRegister() {
120       if (this.mobile && this.sms && this.password) {
121         this.$axios({
122           url: api.register,
123           method: 'POST',
124           data: {
125             mobile: this.mobile,
126             code: this.sms,
127             password: this.password
128           }
129         }).then(res => {
130           if (res.data.code == 100) {
131             this.$message({
132               type: "success", message: '注册成功,请去登陆', onClose: () => {
133                 this.$emit('go')
134               }
135             })
136           }
137 
138         })
139       }
140     }
141   }
142 }
143 </script>
144 
145 <style scoped>
146 .register {
147   width: 100vw;
148   height: 100vh;
149   position: fixed;
150   top: 0;
151   left: 0;
152   z-index: 10;
153   background-color: rgba(0, 0, 0, 0.3);
154 }
155 
156 .box {
157   width: 400px;
158   height: 480px;
159   background-color: white;
160   border-radius: 10px;
161   position: relative;
162   top: calc(50vh - 240px);
163   left: calc(50vw - 200px);
164 }
165 
166 .el-icon-close {
167   position: absolute;
168   font-weight: bold;
169   font-size: 20px;
170   top: 10px;
171   right: 10px;
172   cursor: pointer;
173 }
174 
175 .el-icon-close:hover {
176   color: darkred;
177 }
178 
179 .content {
180   position: absolute;
181   top: 40px;
182   width: 280px;
183   left: 60px;
184 }
185 
186 .nav {
187   font-size: 20px;
188   height: 38px;
189   border-bottom: 2px solid darkgrey;
190 }
191 
192 .nav > span {
193   margin-left: 90px;
194   color: darkgrey;
195   user-select: none;
196   cursor: pointer;
197   padding-bottom: 10px;
198   border-bottom: 2px solid darkgrey;
199 }
200 
201 .nav > span.active {
202   color: black;
203   border-bottom: 3px solid black;
204   padding-bottom: 9px;
205 }
206 
207 .el-input, .el-button {
208   margin-top: 40px;
209 }
210 
211 .el-button {
212   width: 100%;
213   font-size: 18px;
214 }
215 
216 .foot > span {
217   float: right;
218   margin-top: 20px;
219   color: orange;
220   cursor: pointer;
221 }
222 
223 .sms {
224   color: orange;
225   cursor: pointer;
226   display: inline-block;
227   width: 70px;
228   text-align: center;
229   user-select: none;
230 }
231 </style>
RegisterView.vue

 

posted on 2024-05-09 23:12  认真的六六  阅读(11)  评论(0编辑  收藏  举报