登录注册

03-01 登录注册页面

一. 模态登录组件

Login.vue

复制代码
<template>
    <div class="login">
        <div class="box">
            <i class="el-icon-close" @click="close_login"></i>
            <div class="content">
                <div class="nav">
                    <span :class="{active: login_method === 'is_pwd'}"
                          @click="change_login_method('is_pwd')">密码登录</span>
                    <span :class="{active: login_method === 'is_sms'}"
                          @click="change_login_method('is_sms')">短信登录</span>
                </div>
                <el-form v-if="login_method === 'is_pwd'">
                    <el-input
                            placeholder="用户名/手机号/邮箱"
                            prefix-icon="el-icon-user"
                            v-model="username"
                            clearable>
                    </el-input>
                    <el-input
                            placeholder="密码"
                            prefix-icon="el-icon-key"
                            v-model="password"
                            clearable
                            show-password>
                    </el-input>
                    <el-button type="primary">登录</el-button>
                </el-form>
                <el-form v-if="login_method === 'is_sms'">
                    <el-input
                            placeholder="手机号"
                            prefix-icon="el-icon-phone-outline"
                            v-model="mobile"
                            clearable
                            @blur="check_mobile">
                    </el-input>
                    <el-input
                            placeholder="验证码"
                            prefix-icon="el-icon-chat-line-round"
                            v-model="sms"
                            clearable>
                        <template slot="append">
                            <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                        </template>
                    </el-input>
                    <el-button type="primary">登录</el-button>
                </el-form>
                <div class="foot">
                    <span @click="go_register">立即注册</span>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Login",
        data() {
            return {
                username: '',
                password: '',
                mobile: '',
                sms: '',
                login_method: 'is_pwd',
                sms_interval: '获取验证码',
                is_send: false,
            }
        },
        methods: {
            close_login() {
                this.$emit('close')
            },
            go_register() {
                this.$emit('go')
            },
            change_login_method(method) {
                this.login_method = method;
            },
            check_mobile() {
                if (!this.mobile) return;
                if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                    this.$message({
                        message: '手机号有误',
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                            this.mobile = '';
                        }
                    });
                    return false;
                }
                this.is_send = true;
            },
            send_sms() {

                if (!this.is_send) return;
                this.is_send = false;
                let sms_interval_time = 60;
                this.sms_interval = "发送中...";
                let timer = setInterval(() => {
                    if (sms_interval_time <= 1) {
                        clearInterval(timer);
                        this.sms_interval = "获取验证码";
                        this.is_send = true; // 重新回复点击发送功能的条件
                    } else {
                        sms_interval_time -= 1;
                        this.sms_interval = `${sms_interval_time}秒后再发`;
                    }
                }, 1000);
            }
        }
    }
</script>

<style scoped>
    .login {
        width: 100vw;
        height: 100vh;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        background-color: rgba(0, 0, 0, 0.3);
    }

    .box {
        width: 400px;
        height: 420px;
        background-color: white;
        border-radius: 10px;
        position: relative;
        top: calc(50vh - 210px);
        left: calc(50vw - 200px);
    }

    .el-icon-close {
        position: absolute;
        font-weight: bold;
        font-size: 20px;
        top: 10px;
        right: 10px;
        cursor: pointer;
    }

    .el-icon-close:hover {
        color: darkred;
    }

    .content {
        position: absolute;
        top: 40px;
        width: 280px;
        left: 60px;
    }

    .nav {
        font-size: 20px;
        height: 38px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span {
        margin: 0 20px 0 35px;
        color: darkgrey;
        user-select: none;
        cursor: pointer;
        padding-bottom: 10px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span.active {
        color: black;
        border-bottom: 3px solid black;
        padding-bottom: 9px;
    }

    .el-input, .el-button {
        margin-top: 40px;
    }

    .el-button {
        width: 100%;
        font-size: 18px;
    }

    .foot > span {
        float: right;
        margin-top: 20px;
        color: orange;
        cursor: pointer;
    }

    .sms {
        color: orange;
        cursor: pointer;
        display: inline-block;
        width: 70px;
        text-align: center;
        user-select: none;
    }
</style>
复制代码

二. 模态注册组件

Register.vue

复制代码
<template>
    <div class="register">
        <div class="box">
            <i class="el-icon-close" @click="close_register"></i>
            <div class="content">
                <div class="nav">
                    <span class="active">新用户注册</span>
                </div>
                <el-form>
                    <el-input
                            placeholder="手机号"
                            prefix-icon="el-icon-phone-outline"
                            v-model="mobile"
                            clearable
                            @blur="check_mobile">
                    </el-input>
                    <el-input
                            placeholder="密码"
                            prefix-icon="el-icon-key"
                            v-model="password"
                            clearable
                            show-password>
                    </el-input>
                    <el-input
                            placeholder="验证码"
                            prefix-icon="el-icon-chat-line-round"
                            v-model="sms"
                            clearable>
                        <template slot="append">
                            <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                        </template>
                    </el-input>
                    <el-button type="primary">注册</el-button>
                </el-form>
                <div class="foot">
                    <span @click="go_login">立即登录</span>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Register",
        data() {
            return {
                mobile: '',
                password: '',
                sms: '',
                sms_interval: '获取验证码',
                is_send: false,
            }
        },
        methods: {
            close_register() {
                this.$emit('close', false)
            },
            go_login() {
                this.$emit('go')
            },
            check_mobile() {
                if (!this.mobile) return;
                if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                    this.$message({
                        message: '手机号有误',
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                            this.mobile = '';
                        }
                    });
                    return false;
                }
                this.is_send = true;
            },
            send_sms() {
                if (!this.is_send) return;
                this.is_send = false;
                let sms_interval_time = 60;
                this.sms_interval = "发送中...";
                let timer = setInterval(() => {
                    if (sms_interval_time <= 1) {
                        clearInterval(timer);
                        this.sms_interval = "获取验证码";
                        this.is_send = true; // 重新回复点击发送功能的条件
                    } else {
                        sms_interval_time -= 1;
                        this.sms_interval = `${sms_interval_time}秒后再发`;
                    }
                }, 1000);
            }
        }
    }
</script>

<style scoped>
    .register {
        width: 100vw;
        height: 100vh;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        background-color: rgba(0, 0, 0, 0.3);
    }

    .box {
        width: 400px;
        height: 480px;
        background-color: white;
        border-radius: 10px;
        position: relative;
        top: calc(50vh - 240px);
        left: calc(50vw - 200px);
    }

    .el-icon-close {
        position: absolute;
        font-weight: bold;
        font-size: 20px;
        top: 10px;
        right: 10px;
        cursor: pointer;
    }

    .el-icon-close:hover {
        color: darkred;
    }

    .content {
        position: absolute;
        top: 40px;
        width: 280px;
        left: 60px;
    }

    .nav {
        font-size: 20px;
        height: 38px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span {
        margin-left: 90px;
        color: darkgrey;
        user-select: none;
        cursor: pointer;
        padding-bottom: 10px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span.active {
        color: black;
        border-bottom: 3px solid black;
        padding-bottom: 9px;
    }

    .el-input, .el-button {
        margin-top: 40px;
    }

    .el-button {
        width: 100%;
        font-size: 18px;
    }

    .foot > span {
        float: right;
        margin-top: 20px;
        color: orange;
        cursor: pointer;
    }

    .sms {
        color: orange;
        cursor: pointer;
        display: inline-block;
        width: 70px;
        text-align: center;
        user-select: none;
    }
</style>
复制代码

流程

1. components中新建Register.vue文件, 写入如上代码
2. components中的Header.vue中. 导入组件, 注册组件, 使用组件
3. 登录注册绑定点击事件, 为Register组件使用条件渲染指令v-if, 指定一个值is_Register控制Register组件的展示状态. 
    在点击z注册按钮时is_register为True
    在点击登录组件中×时, Login组件应该控制传递数据, 控制父组件的展示或者隐藏. 子组件对×绑定事件的传递this.$emit('close')
5. 父组件中的Register组件中获取到传递过来的事件@close. 绑定一个父组件中的触发该事的方法, 一当该事件触发, 表示用户×了登录界面. 此时is_register因该为false.

四. 登录业务分析

1. 多方式登录

"""
1)前台提供账号密码,账号可能是 用户名、手机号、邮箱等

接口:
后台只需要提供一个多方式登录接口即可 - 多方式登录接口
"""

2. 验证码登录

"""
1)前台提供手机号和验证码完成登录

接口:
前台填完手机号,往后台发送校验手机号的请求,如果存在继续,不存在提示注册 - 手机号存在与否接口
前台点击发送验证码,将手机再次发送给后台,后台将手机号通知给第三方,发送短信 - 手机验证码接口
前台点击登录提交手机号与验证码,完成验证码登录 - 验证码登录接口
"""

五. 注册业务分析

1. 验证码注册

"""
1)前台提供手机号、验证码、密码完成注册

接口:
前台填完手机号,往后台发送校验手机号的请求,如果不存在继续,存在提示登录 - 手机号存在与否接口
前台点击发送验证码,将手机再次发送给后台,后台将手机号通知给第三方,发送短信 - 手机验证码接口
前台点击注册提交手机号、验证码及密码,完成验证码注册 - 验证码注册接口
"""

六. 总结

"""
1. 多方式登录接口
2. 手机号存在与否接口
3. 手机验证码接口
4. 验证码登录接口
5. 验证码注册接口
"""

03-02 多方式登录

一. 后台

1. 插件

pip install djangorestframework-jwt

2. urls.py

复制代码
from django.urls import path, include
from rest_framework.routers import SimpleRouter
from . import views

router = SimpleRouter()
# '',action装饰器,自动将login作为路径
router.register('', views.LoginAPIView, 'login')

urlpatterns = [
    path('', include(router.urls)),
]
复制代码

3. dev.py

import datetime

JWT_AUTH = {
    # 过期时间七天
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
}

4. views.py

复制代码
import re
from rest_framework.viewsets import ViewSet, GenericViewSet
from rest_framework.mixins import CreateModelMixin
from rest_framework.decorators import action
from . import serializers
from . import models
from luffyapi.utils.response import APIResponse


# Create your views here.
class LoginView(ViewSet):
    # 多方式登录,jwt
    @action(methods=['POST'], detail=False)
    def login(self, request, *args, **kwargs):
        ser = serializers.UserInfoModelSerializer(data=request.data)
        if ser.is_valid():
            token = ser.context['token']
            username = ser.context['user_obj'].username
            return APIResponse(token=token, username=username)
        else:
            return APIResponse(code=0, msg=ser.errors)
复制代码

5. serializers.py

复制代码
import re
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

from . import models


class UserInfoModelSerializer(serializers.ModelSerializer):
    # 提示: 因为继承了AbstractUser的表源码中username字段指定的是unique字段. 在视图中进行序列化data=request.data时, 如果不重定义
    # 在序列化中就会被认为存储数据, 因为data本质就是存储数据, 而我们这里因该重定义该字段, 去除这种影响
    sername = serializers.CharField()

    class Meta:
        model = models.UserInfo
        fields = ['username', 'password', 'id']
        extra_kwargs = {
            'id': {'read_only': True},
            'password': {'write_only': True},
        }

    def validate(self, attrs):
        # 多种登录方式
        user_obj = self._get_user(attrs)
        # 签发token
        token = self._get_token(user_obj)
        # 放在context中,在视图类中可以取出来
        self.context['token'] = token
        self.context['user_obj'] = user_obj
        # 钩子函数,记得返回值
        return attrs

    # 内部规范,
    def _get_user(self, attrs):
        username = attrs.get('username')
        password = attrs.get('password')
        # 多种方式登录
        if re.match(r'^1[3-9][0-9]{9}$', username):
            user_obj = models.UserInfo.objects.filter(phone=username).first()
        elif re.match(r'^.*?@.*?\.com$', username):
            user_obj = models.UserInfo.objects.filter(email=username).first()
        else:
            user_obj = models.UserInfo.objects.filter(username=username).first()
        if user_obj:
            # 用户存在,校验密码
            # 因校验的密文,需要check_password
            res = user_obj.check_password(password)
            if res:
                return user_obj
            else:
                raise ValidationError('密码错误')
        else:
            raise ValidationError('用户不存在')

    def _get_token(self, user_obj):
        from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
        payload = jwt_payload_handler(user_obj)  # 通过user对象获得payload
        token = jwt_encode_handler(payload)  # 通过payload获得token
        return token
    
复制代码

二. 前台(cookies修改页面登陆状态)

1. Login.vue

复制代码
<template>
    <div class="login">
        <div class="box">
            <i class="el-icon-close" @click="close_login"></i>
            <div class="content">
                <div class="nav">
                    <span :class="{active: login_method === 'is_pwd'}"
                          @click="change_login_method('is_pwd')">密码登录</span>
                    <span :class="{active: login_method === 'is_sms'}"
                          @click="change_login_method('is_sms')">短信登录</span>
                </div>
                <el-form v-if="login_method === 'is_pwd'">
                    <el-input
                            placeholder="用户名/手机号/邮箱"
                            prefix-icon="el-icon-user"
                            v-model="username"
                            clearable>
                    </el-input>
                    <el-input
                            placeholder="密码"
                            prefix-icon="el-icon-key"
                            v-model="password"
                            clearable
                            show-password>
                    </el-input>
                    <el-button type="primary" @click="login">登录</el-button>
                </el-form>
                <el-form v-if="login_method === 'is_sms'">
                    <el-input
                            placeholder="手机号"
                            prefix-icon="el-icon-phone-outline"
                            v-model="mobile"
                            clearable
                            @blur="check_mobile">
                    </el-input>
                    <el-input
                            placeholder="验证码"
                            prefix-icon="el-icon-chat-line-round"
                            v-model="sms"
                            clearable>
                        <template slot="append">
                            <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                        </template>
                    </el-input>
                    <el-button type="primary">登录</el-button>
                </el-form>
                <div class="foot">
                    <span @click="go_register">立即注册</span>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Login",
        data() {
            return {
                username: '',
                password: '',
                mobile: '',
                sms: '',
                login_method: 'is_pwd',
                sms_interval: '获取验证码',
                is_send: false,
            }
        },
        methods: {
            close_login() {
                this.$emit('close')
            },
            go_register() {
                this.$emit('go')
            },
            change_login_method(method) {
                this.login_method = method;
            },
            check_mobile() {
                if (!this.mobile) return;
                if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                    this.$message({
                        message: '手机号有误',
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                            this.mobile = '';
                        }
                    });
                    return false;
                }
                this.is_send = true;
            },
            send_sms() {

                if (!this.is_send) return;
                this.is_send = false;
                let sms_interval_time = 60;
                this.sms_interval = "发送中...";
                let timer = setInterval(() => {
                    if (sms_interval_time <= 1) {
                        clearInterval(timer);
                        this.sms_interval = "获取验证码";
                        this.is_send = true; // 重新回复点击发送功能的条件
                    } else {
                        sms_interval_time -= 1;
                        this.sms_interval = `${sms_interval_time}秒后再发`;
                    }
                }, 1000);
            },
            login() {
                if (this.username && this.password) {
                    this.$axios.post(this.$settings.base_url + '/user/login/', {
                        'username': this.username,
                        'password': this.password,
                    }).then(response => {
                        console.log(response.data);
                        // 把用户信息保存到cookie中
                        // this.$cookies.set('key','value','过期时间,按s计')
                        this.$cookies.set('token', response.data.token, '7d');
                        this.$cookies.set('username', response.data.username, '7d');

                        // 关闭登录窗口(子传父)
                        // 给父组件,Head传递一个事件,让它从cookie中取出token和username
                        this.$emit('close');
                        this.$emit('loginSuccess');
                    }).catch(error => {
                        console.log(error.response);
                    })
                } else {
                    this.$message({
                        message: '用户名或密码输入不能为空!',
                        type: 'warning',
                    });
                }
            }
        }
    }
</script>

<style scoped>
    .login {
        width: 100vw;
        height: 100vh;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        background-color: rgba(0, 0, 0, 0.3);
    }

    .box {
        width: 400px;
        height: 420px;
        background-color: white;
        border-radius: 10px;
        position: relative;
        top: calc(50vh - 210px);
        left: calc(50vw - 200px);
    }

    .el-icon-close {
        position: absolute;
        font-weight: bold;
        font-size: 20px;
        top: 10px;
        right: 10px;
        cursor: pointer;
    }

    .el-icon-close:hover {
        color: darkred;
    }

    .content {
        position: absolute;
        top: 40px;
        width: 280px;
        left: 60px;
    }

    .nav {
        font-size: 20px;
        height: 38px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span {
        margin: 0 20px 0 35px;
        color: darkgrey;
        user-select: none;
        cursor: pointer;
        padding-bottom: 10px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span.active {
        color: black;
        border-bottom: 3px solid black;
        padding-bottom: 9px;
    }

    .el-input, .el-button {
        margin-top: 40px;
    }

    .el-button {
        width: 100%;
        font-size: 18px;
    }

    .foot > span {
        float: right;
        margin-top: 20px;
        color: orange;
        cursor: pointer;
    }

    .sms {
        color: orange;
        cursor: pointer;
        display: inline-block;
        width: 70px;
        text-align: center;
        user-select: none;
    }
</style>
复制代码

2. Header.vue

复制代码
<template>
    <div class="header">
        <div class="slogan">
            <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
        </div>
        <div class="nav">
            <ul class="left-part">
                <li class="logo">
                    <router-link to="/">
                        <img src="../assets/img/head-logo.svg" alt="">
                    </router-link>
                </li>
                <li class="ele">
                    <span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
                </li>
                <li class="ele">
                    <span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
                </li>
                <li class="ele">
                    <span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
                </li>
            </ul>

            <div class="right-part">
                <div v-if="!username">
                    <span @click="put_login">登录</span>
                    <span class="line">|</span>
                    <span @click="put_register">注册</span>
                </div>
                <div v-else>
                    <span>{{username}}</span>
                    <span class="line">|</span>
                    <span @click="logout">注销</span>
                </div>
                <Login v-if="is_login" @close="close_login" @go="put_register" @loginSuccess="loginSuccess"/>
                <Register v-if="is_register" @close="close_register" @go="put_login"/>
            </div>
        </div>
    </div>

</template>

<script>
    import Login from './Login'
    import Register from './Register'

    export default {
        name: "Header",
        data() {
            return {
                url_path: sessionStorage.url_path || '/',
                is_login: false,
                is_register: false,
                token: '',
                username: ''
            }
        },
        methods: {
            goPage(url_path) {
                // 已经是当前路由就没有必要重新跳转
                if (this.url_path !== url_path) {
                    this.$router.push(url_path);
                }
                sessionStorage.url_path = url_path;
            },
            put_login() {
                this.is_login = true;
                this.is_register = false;
            },
            put_register() {
                this.is_login = false;
                this.is_register = true;
            },
            close_login() {
                this.is_login = false;
            },
            close_register() {
                this.is_register = false;
            },
            loginSuccess() {
                this.username = this.$cookies.get('username');
                this.token = this.$cookies.get('token');
            },
            logout() {
                // 把两个变量值为空
                this.username = '';
                this.token = '';

                // 清除cookie
                this.$cookies.remove('username');
                this.$cookies.remove('token');
            }
        },
        created() {
            sessionStorage.url_path = this.$route.path;
            this.url_path = this.$route.path;

            // 当页面一创建,我就去cookie中取token和username
            this.username = this.$cookies.get('username');
            this.token = this.$cookies.get('token');
        },
        components: {
            Login,
            Register
        }
    }
</script>

<style scoped>
    .header {
        background-color: white;
        box-shadow: 0 0 5px 0 #aaa;
    }

    .header:after {
        content: "";
        display: block;
        clear: both;
    }

    .slogan {
        background-color: #eee;
        height: 40px;
    }

    .slogan p {
        width: 1200px;
        margin: 0 auto;
        color: #aaa;
        font-size: 13px;
        line-height: 40px;
    }

    .nav {
        background-color: white;
        user-select: none;
        width: 1200px;
        margin: 0 auto;

    }

    .nav ul {
        padding: 15px 0;
        float: left;
    }

    .nav ul:after {
        clear: both;
        content: '';
        display: block;
    }

    .nav ul li {
        float: left;
    }

    .logo {
        margin-right: 20px;
    }

    .ele {
        margin: 0 20px;
    }

    .ele span {
        display: block;
        font: 15px/36px '微软雅黑';
        border-bottom: 2px solid transparent;
        cursor: pointer;
    }

    .ele span:hover {
        border-bottom-color: orange;
    }

    .ele span.active {
        color: orange;
        border-bottom-color: orange;
    }

    .right-part {
        float: right;
    }

    .right-part .line {
        margin: 0 10px;
    }

    .right-part span {
        line-height: 68px;
        cursor: pointer;
    }
</style>
复制代码

03-03 手机是否存在验证接口

urls.py
router = SimpleRouter()
router.register('', views.LoginAPIView, 'login')
urlpatterns = [
    path('', include(router.urls)),
]
views.py
复制代码
class LoginView(ViewSet):
    # 多方式登录,jwt
    @action(methods=['POST'], detail=False)
    def login(self, request, *args, **kwargs):
        ser = serializers.UserInfoModelSerializer(data=request.data)
        if ser.is_valid():
            token = ser.context['token']
            username = ser.context['user_obj'].username
            return APIResponse(token=token, username=username)
        else:
            return APIResponse(code=0, msg=ser.errors)

    @action(methods=['GET'], detail=False)
    def verify_telephone(self, request, *args, **kwargs):
        # 校验手机号
        telephone = request.query_params.get('telephone')

        if not re.match(r'^1[3-9][0-9]{9}$', telephone):
            return APIResponse(code=0, msg='手机号不合法 ')
        try:
            models.UserInfo.objects.get(phone=telephone)
            return APIResponse(result=True)
        except:
            return APIResponse(code=0, msg='手机号不存在')
复制代码

03-04 腾讯云短信开发

一. 短信服务应用申请

""" 
准备工作
1)创建短信应用 - 应用管理
2)申请短信签名 - 国内短信 > 签名管理
3)申请短信模块 - 国内短信 > 正文模板管理
"""

二. python中开发腾讯云短信服务

"""
1)API文档,接口的使用说吧
2)SDK,基于开发语言封装的可以直接调用的功能(工具)集合
    官网sdk使用文档中找到安装命令:pip install qcloudsms_py
    按照sdk使用说明进行开发:https://cloud.tencent.com/document/product/382/11672
"""

sdk使用说明: https://cloud.tencent.com/document/product/382/11672

t_sms.py
复制代码
# 所有配置换成申请的数据

# 申请的短信应用 SDK AppID
appid = 1400
# 申请的短信应用 SDK AppKey
appkey = "ba81"
# 申请的短信模板ID,需要在短信控制台中申请
template_id = 5447
# 申请的签名,参数使用的是`签名内容`,而不是`签名ID`
sms_sign = "Owen的技术栈"


from qcloudsms_py import SmsSingleSender
sender = SmsSingleSender(appid, appkey)

import random
def get_code():
    code = ''
    for i in range(4):
        code += str(random.randint(0, 9))
    return code

mobile = 13344556677
# 模板所需参数,和申请的模板中占位符要保持一致
code = get_code()
print(code)
params = [code, 5]
try:
    result = sender.send_with_param(86, mobile, template_id, params, sign=sms_sign, extend="", ext="")
    if result and result.get('result') == 0:
        print('发送成功')
except Exception as e:
    print('短信发送失败:%s' % e)
复制代码

三. 短信服务二次封装

在libs下创造tx_sms包

初始化

from .send import send_sms
from .send import make_code

settings.py

复制代码
# 短信应用 SDK AppID
APPID = 1400123123  # SDK AppID 以1400开头

# 短信应用 SDK AppKey
APPKEY = "2025dfawefwfjkiokjg984100"

# 需要发送短信的手机号码
# phone_numbers = ["21212313123", "12345678902", "12345678903"]
# 短信模板ID,需要在短信控制台中申请

TEMPLATE_ID = 7839  # NOTE: 这里的模板 ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请
# 签名

SMS_SIGN = "腾讯云"  # NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台中申请
复制代码

短信

复制代码
import random
from qcloudsms_py import SmsSingleSender

from libs.sms import settings
import utils


def make_code():
    code_list = []
    for i in range(6):
        code_list.append(str(random.randint(0, 9)))
    return ''.join(code_list)


def send_sms(code, telephone):
    ssender = SmsSingleSender(settings.APPID, settings.APPKEY)
    params = [code]  # 当模板没有参数时,`params = []`
    try:
        result = ssender.send_with_param(86, telephone,
                                         settings.TEMPLATE_ID, params, sign=settings.SMS_SIGN, extend="", ext="")
        """
        'result': 0表示成功
        result: {'result': 0, 'errmsg': 'OK', 'ext': '', 'sid': '2025dfawefwfjkiokjg984100', 'fee': 1, 'isocode': 'CN'}
        """
        # print('result:', result)
        if not result.get('result'):
            return True
        return False

    except Exception as e:
        utils.log.error(f'手机号:{telephone},短信发送失败,错误为:{e}')


if __name__ == '__main__':
    test_phone = ['13333333333']
    for phone in range(3):
        send_sms(make_code(), telephone=test_phone[0])
复制代码

03-05 短信验证码接口

码缓存时间s
SMS_CACHE_TIME = 300

utils/throttles.py

复制代码
from rest_framework.throttling import SimpleRateThrottle


class SmsThrotting(SimpleRateThrottle):
    scope = 'sms'

    def get_cache_key(self, request, view):
        telephone = request.query_params.get('telephone')
        # 唯一性
        # cache_format = 'throttle_%(scope)s_%(ident)s'字符串替换
        return self.cache_format % {'scope': self.scope, 'ident': telephone}
复制代码

settings/dev.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_RATES': {
        # 一分钟,发送一次验证码
        'sms': '1/m'  # key要跟类中的scope对应
    },
}

user/views.py

复制代码
from luffyapi.utils.throttlings import SmsThrotting


class SendSmsView(ViewSet):
    # 对手机号发送验证码频率进行限制,一分钟1次
    throttle_classes = [SmsThrotting, ]

    @action(methods=['GET'], detail=False)
    def send_code(self, request, *args, **kwargs):
        '''
        发送验证码接口
        :return:
        '''
        from luffyapi.libs.tx_sms import get_code, send_message
        from django.core.cache import cache
        from django.conf import settings
        telephone = request.query_params.get('telephone')
        # 后端校验手机号是否合法
        if not re.match(r'^1[3-9][0-9]{9}$', telephone):
            return APIResponse(code=0, msg='手机号不合法 ')
        # code = get_code()
        code = '1234'
        # result = send_message(telephone, code)
        result = True
        # 验证码保存
        # 'sms_cache_{}'加字符串,是为了唯一性,180,是缓存保存180s
        cache.set(settings.PHONE_CACHE_KEY.format(telephone), code, 180)
        if result:
            return APIResponse()
        else:
            return APIResponse(code=0, msg='验证码发送失败')
复制代码

user/urls.py

router = SimpleRouter()
router.register('', views.SendSmsView, 'sms')
urlpatterns = [
    path('', include(router.urls)),
]

03-06 短信登录接口

user/urls.py

router = SimpleRouter()
router.register('', views.LoginAPIView, 'login')
urlpatterns = [
    path('', include(router.urls)),
]

serializers.py

复制代码
# 手机验证码登录
class CodeUserInfoSerializer(serializers.ModelSerializer):
    code = serializers.CharField()

    class Meta:
        model = models.UserInfo
        fields = ['phone', 'code']

    def validate(self, attrs):
        user_obj = self._get_user(attrs)
        # 用户存在,签发token
        token = self._get_token(user_obj)
        self.context['token'] = token
        self.context['user_obj'] = user_obj
        return attrs

    def _get_user(self, attrs):
        """
        校验获取用户对象:
        1. 判断用户手机号码是否符合格式
        2. 判断用户手机号码是否存在数据中
        3. 通过手机号码从缓存中获取用户的正确验证码
        """
        print(attrs)
        telephone = attrs.get('phone')
        print(telephone)
        code = attrs.get('code')
        # 取出原来的code

        cache_code = cache.get(settings.PHONE_CACHE_KEY.format(telephone))
        print(cache_code)
        if code == cache_code:
            if re.match(r'^1[3-9][0-9]{9}$', telephone):
                user_obj = models.UserInfo.objects.filter(phone=telephone).first()

                if user_obj:
                    # 把使用过的验证码删除
                    cache.set(settings.PHONE_CACHE_KEY.format(telephone), '')
                    return user_obj
                else:
                    raise ValidationError('用户不存在')
            else:
                raise ValidationError('手机号不合法')
        else:
            raise ValidationError('验证码错误')

    def _get_token(self, user_obj):
        from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
        payload = jwt_payload_handler(user_obj)  # 通过user对象获得payload
        token = jwt_encode_handler(payload)  # 通过payload获得token
        return token
复制代码

views.py

复制代码
class LoginView(ViewSet):
    @action(methods=['POST'], detail=False)
    def code_login(self, request, *args, **kwargs):
        ser = serializers.CodeUserInfoSerializer(data=request.data)
        if ser.is_valid():
            token = ser.context['token']
            username = ser.context['user_obj'].username
            return APIResponse(token=token, username=username)
        else:
            return APIResponse(code=0, msg=ser.errors)
复制代码

03-07 后端短信注册接口

一. urls.py

router = SimpleRouter()
router.register('register', views.RegisterAPIView, 'register')  # -> actions={post: create}
urlpatterns = [
    path('', include(router.urls)),
]

二. serializers.py

复制代码
# 注册
class UserRegisterSerilaizer(serializers.ModelSerializer):
    code = serializers.CharField(max_length=4, min_length=4, write_only=True)
    """
    提示: 如果code不指定 write_only. 在序列化的时候, 库中没有, 当然会抛出异常
    Got AttributeError when attempting to get a value for field `code` on serializer `RegisterModelSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `User` instance.
Original exception text was: 'User' object has no attribute 'code'.
    """
    class Meta:
        model = models.UserInfo
        fields = ['phone', 'password', 'code','username']
        extra_kwargs = {
            'username': {'read_only': True},
        }

    def validate(self, attrs):
        telephone = attrs.get('phone')
        code = attrs.get('code')
        cache_code = cache.get(settings.PHONE_CACHE_KEY.format(telephone))
        if code == cache_code:
            if re.match(r'^1[3-9][0-9]{9}$', telephone):
                attrs['username'] = telephone
                attrs.pop('code')
                return attrs
            else:
                raise ValidationError('手机号不合法')
        else:
            raise ValidationError('验证码错误')

    def create(self, validated_data):
        user_obj = models.UserInfo.objects.create_user(**validated_data)
        return user_obj
复制代码

三. views.py

复制代码
# 注册
class RegisterView(GenericViewSet, CreateModelMixin):
    queryset = models.UserInfo.objects.all()
    serializer_class = serializers.UserRegisterSerilaizer

    def create(self, request, *args, **kwargs):
        response = super().create(request, *args, **kwargs)
        username = response.data.get('username')
        return APIResponse(code=1, msg='注册成功', username=username)
复制代码

03-08 前台登录注册修订

一. Login.vue

复制代码
<template>
    <div class="login">
        <div class="box">
            <i class="el-icon-close" @click="close_login"></i>
            <div class="content">
                <div class="nav">
                    <span :class="{active: login_method === 'is_pwd'}"
                          @click="change_login_method('is_pwd')">密码登录</span>
                    <span :class="{active: login_method === 'is_sms'}"
                          @click="change_login_method('is_sms')">短信登录</span>
                </div>
                <el-form v-if="login_method === 'is_pwd'">
                    <el-input
                            placeholder="用户名/手机号/邮箱"
                            prefix-icon="el-icon-user"
                            v-model="username"
                            clearable>
                    </el-input>
                    <el-input
                            placeholder="密码"
                            prefix-icon="el-icon-key"
                            v-model="password"
                            clearable
                            show-password>
                    </el-input>
                    <el-button type="primary" @click="login">登录</el-button>
                </el-form>
                <el-form v-if="login_method === 'is_sms'">
                    <el-input
                            placeholder="手机号"
                            prefix-icon="el-icon-phone-outline"
                            v-model="mobile"
                            clearable
                            @blur="check_mobile">
                    </el-input>
                    <el-input
                            placeholder="验证码"
                            prefix-icon="el-icon-chat-line-round"
                            v-model="sms"
                            clearable>
                        <template slot="append">
                            <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                        </template>
                    </el-input>
                    <el-button type="primary" @click="code_login">登录</el-button>
                </el-form>
                <div class="foot">
                    <span @click="go_register">立即注册</span>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Login",
        data() {
            return {
                username: '',
                password: '',
                mobile: '',
                sms: '',
                login_method: 'is_pwd',
                sms_interval: '获取验证码',
                is_send: false,
            }
        },
        methods: {
            close_login() {
                this.$emit('close')
            },
            go_register() {
                this.$emit('go')
            },
            change_login_method(method) {
                this.login_method = method;
            },
            check_mobile() {
                if (!this.mobile) return;
                if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                    this.$message({
                        message: '手机号有误!',
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                            this.mobile = '';
                        }
                    });
                    return false;
                }

                this.$axios.get(this.$settings.base_url + '/user/verify_telephone/', {
                    params: {
                        // 15870638044
                        'telephone': this.mobile,
                    }
                }).then(response => {
                    // console.log('response.data:', response.data);
                    if (response.data.code) {
                        this.is_send = true;
                    } else {
                        this.$message({
                            message: '手机号码不存在!',
                            type: 'warning',
                            duration: 1000,
                        });
                    }
                }).catch(error => {
                    this.$message({
                        message: '未知错误!',
                        type: 'warning',
                    });
                });
            },
            send_sms() {
                if (!this.is_send) return;
                this.is_send = false;
                let sms_interval_time = 60;
                this.sms_interval = "发送中...";

                this.$axios.get(this.$settings.base_url + '/user/send/', {
                    params: {
                        'telephone': this.mobile,
                    }
                }).then(response => {
                    // console.log('request.data:', response.data);
                    if (response.data.code) {
                        this.$message({
                            message: '验证码发送成功!',
                            type: 'success',
                            duration: 1000,
                        });
                    } else {
                        this.$message({
                            message: '验证码发送失败!',
                            type: 'warning',
                        });
                    }
                }).catch(error => {
                });

                let timer = setInterval(() => {
                    if (sms_interval_time <= 1) {
                        clearInterval(timer);
                        this.sms_interval = "获取验证码";
                        this.is_send = true; // 重新回复点击发送功能的条件
                    } else {
                        sms_interval_time -= 1;
                        this.sms_interval = `${sms_interval_time}秒后再发`;
                    }
                }, 1000);
            },
            login() {
                if (this.username && this.password) {
                    this.$axios.post(this.$settings.base_url + '/user/login/', {
                        'username': this.username,
                        'password': this.password,
                    }).then(response => {
                        console.log(response.data);
                        // 把用户信息保存到cookie中
                        // this.$cookies.set('key','value','过期时间,按s计')
                        this.$cookies.set('token', response.data.token, '7d');
                        this.$cookies.set('username', response.data.username, '7d');

                        // 关闭登录窗口(子传父)
                        // 给父组件,Head传递一个事件,让它从cookie中取出token和username
                        this.$emit('close');
                        this.$emit('loginSuccess');
                    }).catch(error => {
                        console.log(error.response);
                    })
                } else {
                    this.$message({
                        message: '用户名或密码输入不能为空!',
                        type: 'warning',
                    });
                }
            },
            code_login() {
                if (this.mobile && this.sms) {
                    this.$axios.post(this.$settings.base_url + '/user/code_login/', {
                        'telephone': this.mobile,
                        'code': this.sms,
                    }).then(response => {
                        if (response.data.code) {
                            console.log(response.data);
                            // 获取token, 获取username
                            let username = response.data.username;
                            let token = response.data.token;
                            // 将获取的token和username存入cookies中
                            this.$cookies.set('token', token, '7d');
                            this.$cookies.set('username', username, '7d');
                            // 向父组件传递事件
                            this.$emit('close');
                            this.$emit('loginSuccess');
                        } else {
                            this.$message({
                                message: '未知错误!',
                                type: 'warning',
                            });
                        }
                    }).catch(error => {
                        this.$message({
                            message: '未知错误!',
                            type: 'warning',
                        });
                    })
                } else {
                    this.$message({
                        message: '手机号码 或者 验证码输入不能为空!',
                        type: 'warning',
                    });
                }
            }
        }
    }
</script>

<style scoped>
    .login {
        width: 100vw;
        height: 100vh;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        background-color: rgba(0, 0, 0, 0.3);
    }

    .box {
        width: 400px;
        height: 420px;
        background-color: white;
        border-radius: 10px;
        position: relative;
        top: calc(50vh - 210px);
        left: calc(50vw - 200px);
    }

    .el-icon-close {
        position: absolute;
        font-weight: bold;
        font-size: 20px;
        top: 10px;
        right: 10px;
        cursor: pointer;
    }

    .el-icon-close:hover {
        color: darkred;
    }

    .content {
        position: absolute;
        top: 40px;
        width: 280px;
        left: 60px;
    }

    .nav {
        font-size: 20px;
        height: 38px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span {
        margin: 0 20px 0 35px;
        color: darkgrey;
        user-select: none;
        cursor: pointer;
        padding-bottom: 10px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span.active {
        color: black;
        border-bottom: 3px solid black;
        padding-bottom: 9px;
    }

    .el-input, .el-button {
        margin-top: 40px;
    }

    .el-button {
        width: 100%;
        font-size: 18px;
    }

    .foot > span {
        float: right;
        margin-top: 20px;
        color: orange;
        cursor: pointer;
    }

    .sms {
        color: orange;
        cursor: pointer;
        display: inline-block;
        width: 70px;
        text-align: center;
        user-select: none;
    }
</style>
复制代码

二. Register.vue

复制代码
<template>
    <div class="register">
        <div class="box">
            <i class="el-icon-close" @click="close_register"></i>
            <div class="content">
                <div class="nav">
                    <span class="active">新用户注册</span>
                </div>
                <el-form>
                    <el-input
                            placeholder="手机号"
                            prefix-icon="el-icon-phone-outline"
                            v-model="mobile"
                            clearable
                            @blur="check_mobile">
                    </el-input>
                    <el-input
                            placeholder="密码"
                            prefix-icon="el-icon-key"
                            v-model="password"
                            clearable
                            show-password>
                    </el-input>
                    <el-input
                            placeholder="验证码"
                            prefix-icon="el-icon-chat-line-round"
                            v-model="sms"
                            clearable>
                        <template slot="append">
                            <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                        </template>
                    </el-input>
                    <el-button type="primary" @click="register">注册</el-button>
                </el-form>
                <div class="foot">
                    <span @click="go_login">立即登录</span>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Register",
        data() {
            return {
                mobile: '',
                password: '',
                sms: '',
                sms_interval: '获取验证码',
                is_send: false,
            }
        },
        methods: {
            close_register() {
                this.$emit('close', false)
            },
            go_login() {
                this.$emit('go')
            },
            check_mobile() {
                if (!this.mobile) return;
                if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                    this.$message({
                        message: '手机号有误!',
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                            this.mobile = '';
                        }
                    });
                    return false;
                } else {
                    this.$axios.get(this.$settings.base_url + '/user/verify_telephone/', {
                        params: {
                            'telephone': this.mobile,
                        }
                    }).then(response => {
                        if (response.data.code) {
                            // 手机号码存在, 用户已经注册
                            this.$message({
                                message: '该手机号码已经注册!',
                                type: 'warning',
                            });
                        } else {
                            // 手机号码不存在, 让用户可以点击发送验证码
                            this.is_send = true;
                        }
                    }).catch(error => {
                        // 异常
                        this.$message({
                            message: '未知错误!',
                            type: 'warning',
                        });
                    })
                }
            },
            send_sms() {
                if (!this.is_send) return;
                this.is_send = false;
                let sms_interval_time = 60;
                this.sms_interval = "发送中...";

                this.$axios.get(this.$settings.base_url + '/user/send/', {
                    params: {
                        'telephone': this.mobile
                    }
                }).then(response => {
                    if (response.data.code) {
                        // 手机号码发送成功
                        this.$message({
                            message: '验证码发送成功!',
                            type: 'success',
                        });
                    } else {
                        // 手机号码发送失败
                        this.$message({
                            message: '验证码发送失败!',
                            type: 'warning',
                        });
                    }
                }).catch(error => {
                    // 异常.
                    this.$message({
                        message: '未知错误!',
                        type: 'warning',
                    });
                });

                let timer = setInterval(() => {
                    if (sms_interval_time <= 1) {
                        clearInterval(timer);
                        this.sms_interval = "获取验证码";
                        this.is_send = true; // 重新回复点击发送功能的条件
                    } else {
                        sms_interval_time -= 1;
                        this.sms_interval = `${sms_interval_time}秒后再发`;
                    }
                }, 1000);
            },
            register() {
                if (this.mobile && this.sms && this.password) {
                    this.$axios.post(this.$settings.base_url + '/user/register/', {
                        'telephone': this.mobile,
                        'code': this.sms,
                        'password': this.password,
                    }).then(response => {
                        if (response.data.code) {
                            // 注册成功, 跳转到登录页面
                            this.$message({
                                message: '注册成功!',
                                type: 'success',
                                duration: 1000,
                                onClose: () => {
                                    this.go_login();
                                }
                            });
                        } else {
                            // 注册失败
                            this.$message({
                                message: '注册失败!',
                                type: 'warning',
                            });
                        }
                    }).catch(error => {
                        // 异常
                        this.$message({
                            message: '未知错误!',
                            type: 'warning',
                        });
                    })
                } else {
                    // 以上内容有空的
                    this.$message({
                        message: '手机号码, 密码, 验证码不能含有空值!',
                        type: 'warning',
                    });
                }
            }
        }
    }
</script>

<style scoped>
    .register {
        width: 100vw;
        height: 100vh;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        background-color: rgba(0, 0, 0, 0.3);
    }

    .box {
        width: 400px;
        height: 480px;
        background-color: white;
        border-radius: 10px;
        position: relative;
        top: calc(50vh - 240px);
        left: calc(50vw - 200px);
    }

    .el-icon-close {
        position: absolute;
        font-weight: bold;
        font-size: 20px;
        top: 10px;
        right: 10px;
        cursor: pointer;
    }

    .el-icon-close:hover {
        color: darkred;
    }

    .content {
        position: absolute;
        top: 40px;
        width: 280px;
        left: 60px;
    }

    .nav {
        font-size: 20px;
        height: 38px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span {
        margin-left: 90px;
        color: darkgrey;
        user-select: none;
        cursor: pointer;
        padding-bottom: 10px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span.active {
        color: black;
        border-bottom: 3px solid black;
        padding-bottom: 9px;
    }

    .el-input, .el-button {
        margin-top: 40px;
    }

    .el-button {
        width: 100%;
        font-size: 18px;
    }

    .foot > span {
        float: right;
        margin-top: 20px;
        color: orange;
        cursor: pointer;
    }

    .sms {
        color: orange;
        cursor: pointer;
        display: inline-block;
        width: 70px;
        text-align: center;
        user-select: none;
    }
</style>
复制代码

三. Header.vue

复制代码
<template>
    <div class="header">
        <div class="slogan">
            <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
        </div>
        <div class="nav">
            <ul class="left-part">
                <li class="logo">
                    <router-link to="/">
                        <img src="../assets/img/head-logo.svg" alt="">
                    </router-link>
                </li>
                <li class="ele">
                    <span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
                </li>
                <li class="ele">
                    <span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
                </li>
                <li class="ele">
                    <span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
                </li>
            </ul>

            <div class="right-part">
                <div v-if="!username">
                    <span @click="put_login">登录</span>
                    <span class="line">|</span>
                    <span @click="put_register">注册</span>
                </div>
                <div v-else>
                    <span>{{username}}</span>
                    <span class="line">|</span>
                    <span @click="logout">注销</span>
                </div>
                <Login v-if="is_login" @close="close_login" @go="put_register" @loginSuccess="loginSuccess"/>
                <Register v-if="is_register" @close="close_register" @go="put_login"/>
            </div>
        </div>
    </div>

</template>

<script>
    import Login from './Login'
    import Register from './Register'

    export default {
        name: "Header",
        data() {
            return {
                url_path: sessionStorage.url_path || '/',
                is_login: false,
                is_register: false,
                token: '',
                username: ''
            }
        },
        methods: {
            goPage(url_path) {
                // 已经是当前路由就没有必要重新跳转
                if (this.url_path !== url_path) {
                    this.$router.push(url_path);
                }
                sessionStorage.url_path = url_path;
            },
            put_login() {
                this.is_login = true;
                this.is_register = false;
            },
            put_register() {
                this.is_login = false;
                this.is_register = true;
            },
            close_login() {
                this.is_login = false;
            },
            close_register() {
                this.is_register = false;
            },
            loginSuccess() {
                this.username = this.$cookies.get('username');
                this.token = this.$cookies.get('token');
            },
            logout() {
                // 把两个变量值为空
                this.username = '';
                this.token = '';

                // 清除cookie
                this.$cookies.remove('username');
                this.$cookies.remove('token');
            }
        },
        created() {
            sessionStorage.url_path = this.$route.path;
            this.url_path = this.$route.path;

            // 当页面一创建,我就去cookie中取token和username
            this.username = this.$cookies.get('username');
            this.token = this.$cookies.get('token');
        },
        components: {
            Login,
            Register
        }
    }
</script>

<style scoped>
    .header {
        background-color: white;
        box-shadow: 0 0 5px 0 #aaa;
    }

    .header:after {
        content: "";
        display: block;
        clear: both;
    }

    .slogan {
        background-color: #eee;
        height: 40px;
    }

    .slogan p {
        width: 1200px;
        margin: 0 auto;
        color: #aaa;
        font-size: 13px;
        line-height: 40px;
    }

    .nav {
        background-color: white;
        user-select: none;
        width: 1200px;
        margin: 0 auto;

    }

    .nav ul {
        padding: 15px 0;
        float: left;
    }

    .nav ul:after {
        clear: both;
        content: '';
        display: block;
    }

    .nav ul li {
        float: left;
    }

    .logo {
        margin-right: 20px;
    }

    .ele {
        margin: 0 20px;
    }

    .ele span {
        display: block;
        font: 15px/36px '微软雅黑';
        border-bottom: 2px solid transparent;
        cursor: pointer;
    }

    .ele span:hover {
        border-bottom-color: orange;
    }

    .ele span.active {
        color: orange;
        border-bottom-color: orange;
    }

    .right-part {
        float: right;
    }

    .right-part .line {
        margin: 0 10px;
    }

    .right-part span {
        line-height: 68px;
        cursor: pointer;
    }
</style>
复制代码

 

posted @   coder雪山  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
点击右上角即可分享
微信分享提示