DRF_Admin项目笔记
一、 自定义全局异常
drf_admin.utils.exceptions
"""
@author : Wang Meng
@github : https://github.com/tianpangji
@software : PyCharm
@file : exceptions.py
@create : 2020/7/11 13:44
"""
import logging
import traceback
from django.core.exceptions import PermissionDenied
from django.db import DatabaseError
from django.http import Http404
from redis.exceptions import RedisError
from rest_framework import status, exceptions
from rest_framework.exceptions import ErrorDetail
from rest_framework.response import Response
from rest_framework.views import set_rollback
# 获取在配置文件中定义的logger,用来记录日志
from monitor.models import ErrorLogs
from oauth.utils import get_request_ip
logger = logging.getLogger('error')
def errors_handler(exc):
"""
# 处理DRF异常的消息格式,确保返回统一的错误格式
# 处理列表类型的错误详情
# 处理字典类型的错误详情(包括嵌套结构)
# 返回格式: {'detail': '错误消息'}
"""
try:
if isinstance(exc.detail, list):
msg = ''.join([str(x) for x in exc.detail])
elif isinstance(exc.detail, dict):
def search_error(detail: dict, message: str):
for k, v in detail.items():
if k == 'non_field_errors':
if isinstance(v, list) and isinstance(v[0], ErrorDetail):
message += ''.join([str(x) for x in v])
else:
message += str(v)
else:
if isinstance(v, list) and isinstance(v[0], ErrorDetail):
message += str(k)
message += ''.join([str(x) for x in v])
elif isinstance(v, list) and isinstance(v[0], dict):
for value_dict in v:
message = search_error(value_dict, message)
return message
msg = ''
msg = search_error(exc.detail, msg)
else:
msg = exc.detail
if not msg:
msg = exc.detail
except Exception:
msg = exc.detail
data = {'detail': msg}
return data
def exception_handler(exc, context):
"""
# 主异常处理逻辑
# 1. 将Django的Http404和PermissionDenied转换为DRF对应异常
# 2. 处理DRF APIException
# 3. 处理数据库和Redis异常
# 4. 处理其他未知异常
"""
if isinstance(exc, Http404):
exc = exceptions.NotFound()
elif isinstance(exc, PermissionDenied):
exc = exceptions.PermissionDenied()
if isinstance(exc, exceptions.APIException):
headers = {}
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait
data = errors_handler(exc)
set_rollback()
response = Response(data, status=exc.status_code, headers=headers)
elif isinstance(exc, AssertionError):
# assert断言异常捕获
headers = {}
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait
data = errors_handler(exc)
set_rollback()
response = Response(data, status=status.HTTP_400_BAD_REQUEST, headers=headers)
elif isinstance(exc, DatabaseError) or isinstance(exc, RedisError):
# 数据库异常
view = context['view']
# 数据库记录异常
detail = traceback.format_exc()
write_error_logs(exc, context, detail)
logger.error('[%s] %s' % (view, detail))
response = Response({'detail': '服务器内部错误'}, status=status.HTTP_507_INSUFFICIENT_STORAGE)
else:
# 未知错误
view = context['view']
# 数据库记录异常
detail = traceback.format_exc()
write_error_logs(exc, context, detail)
logger.error('[%s] %s' % (view, detail))
response = Response({'detail': '服务端未知错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return response
def write_error_logs(exc, context, detail):
"""
# 记录错误日志到数据库
# 包含用户名、视图名称、错误描述、IP地址和详细堆栈跟踪
"""
data = {
'username': context['request'].user.username if context['request'].user.username else 'AnonymousUser',
'view': context['view'].get_view_name(),
'desc': exc.__str__(),
'ip': get_request_ip(context['request']),
'detail': detail
}
ErrorLogs.objects.create(**data)

- 最后,在
配置文件中应用
# drf_admin/settings/dev.py
......
REST_FRAMEWORK = {
# 异常处理
'EXCEPTION_HANDLER': 'drf_admin.utils.exceptions.exception_handler',
}
二、全局分页配置
-
全局分页配置→官方文档
-
drf_admin/settings/dev.py配置如下
-
REST_FRAMEWORK = { # 全局分页 'DEFAULT_PAGINATION_CLASS': 'drf_admin.utils.pagination.GlobalPagination', } -
pagination.py配置如下
-
from rest_framework.pagination import PageNumberPagination class GlobalPagination(PageNumberPagination): page_query_param = 'page' # 前端发送的页数关键字名,默认为page page_size = 10 # 每页数目 page_size_query_param = 'size' # 前端发送的每页数目关键字名,默认为None max_page_size = 1000 # 前端最多能设置的每页数量
三、全局响应消息体格式化配置
-
前后端分离项目,规范响应消息体格式,如下:
-
{'msg': msg, 'errors': detail, 'code': code, 'data': data} -
格式化采用Django中间件处理:
-
drf_admin/settings/dev.py配置如下:
-
MIDDLEWARE = [ 'drf_admin.utils.middleware.ResponseMiddleware', ] -
middleware.py配置如下:
-
class ResponseMiddleware(MiddlewareMixin): """ 自定义响应数据格式 """ def process_request(self, request): pass def process_view(self, request, view_func, view_args, view_kwargs): pass def process_exception(self, request, exception): pass def process_response(self, request, response): if isinstance(response, Response) and response.get('content-type') == 'application/json': if response.status_code >= 400: msg = '请求失败' detail = response.data.get('detail') code = 1 data = {} elif response.status_code == 200 or response.status_code == 201: msg = '成功' detail = '' code = 200 data = response.data else: return response response.data = {'msg': msg, 'errors': detail, 'code': code, 'data': data} response.content = response.rendered_content return response
四、日志配置
- 参考drf_admin/settings/dev.py 下的LOGGING配置:
......
# 日志配置
LOGS_DIR = os.path.join(os.path.dirname(BASE_DIR), 'logs')
if not os.path.exists(LOGS_DIR):
os.makedirs(LOGS_DIR)
LOGGING = {
'version': 1,
'disable_existing_loggers': False, # 是否禁用已经存在的日志器
'formatters': { # 日志信息显示的格式
'standard': {
'format': '[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d]==>[%(message)s]'
},
'simple': {
'format': '[%(asctime)s][%(levelname)s]==>[%(message)s]'
},
},
'filters': { # 对日志进行过滤
'require_debug_true': { # django在debug模式下才输出日志
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'default': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(LOGS_DIR, 'admin_info.log'),
'maxBytes': 1024 * 1024 * 50,
'backupCount': 5,
'formatter': 'standard',
'encoding': 'utf-8',
},
# 向终端中输出日志
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'standard'
},
'operation': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(LOGS_DIR, 'admin_operation.log'),
'maxBytes': 1024 * 1024 * 50, # 50 MB
'backupCount': 5,
'formatter': 'simple',
'encoding': 'utf-8',
},
'query': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(LOGS_DIR, 'admin_query.log'),
'maxBytes': 1024 * 1024 * 50, # 50 MB
'backupCount': 5,
'formatter': 'simple',
'encoding': 'utf-8',
},
'error': {
'level': 'ERROR',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(LOGS_DIR, 'admin_error.log'),
'maxBytes': 1024 * 1024 * 50, # 50 MB
'backupCount': 5,
'formatter': 'standard',
'encoding': 'utf-8',
},
},
'loggers': {
# 记录视图中手动info日志
'info': {
'handlers': ['default', 'console'],
'level': 'INFO',
'propagate': True,
},
# 非GET方法操作日志
'operation': {
'handlers': ['operation', 'console'],
'level': 'INFO',
'propagate': True,
},
# GET方法查询日志
'query': {
'handlers': ['query', 'console'],
'level': 'INFO',
'propagate': True,
},
# 记录视图异常日志
'error': {
'handlers': ['error', 'console'],
'level': 'ERROR',
'propagate': True,
}
}
}
......
五、API文档 Swagger配置
- 参考→官方文档
- 其中使用Token登录配置,参考→drf-yasg issues58
- drf_admin/settings/dev.py配置如下:
SWAGGER_SETTINGS = {
'USE_SESSION_AUTH': False,
'SECURITY_DEFINITIONS': {
'api_key': {
'type': 'apiKey',
'in': 'header',
'name': 'Authorization'
}
},
}
RABC权限篇
- 访问地址:
https://blog.csdn.net/Mr_w_ang/article/details/114898401 - 流程图如下


自定义滑块验证码
生成滑块验证码
class GenerateSliderView(APIView):
"""
生成滑块验证码的API视图
"""
permission_classes = ()
authentication_classes = ()
def get_images_list(self):
"""
获取所有验证图片文件名列表
"""
from drf_admin.settings.dev import BASE_DIR
base_image_directory = os.path.join(BASE_DIR, 'static', 'verify_images')
return [
# 只返回文件名,不包含完整路径
entry.name
for entry in os.scandir(base_image_directory)
if entry.is_file()
]
def get(self,request,*args, **kwargs):
"""
处理GET请求,生成滑块验证码
"""
conn = get_redis_connection('captcha')
token = hashlib.sha256(f"{time.time()}{random.randint(1000, 9999)}".encode()).hexdigest()
bg_images = self.get_images_list()
bg_image = random.choice(bg_images)
# 生成滑块目标距离(100-300px),并在生成时就确定
expected_distance = random.randint(100, 300)
# 存储验证状态到缓存
cache_data = {
'created_at': time.time(),
'status': 'pending', # 验证状态:pending/success/failed
'bg_image': bg_image, # 只存储文件名
'attempts': 0, # 尝试次数
'expected_distance': expected_distance # 提前存入期望距离
}
# conn.set(f"slider:{token}", cache_data, timeout=300)
conn.set(f"slider:{token}", json.dumps(cache_data), ex=300)
# 构建前端可访问的图片URL
bg_image_url = f"/static/verify_images/{bg_image}"
return Response({
'token': token,
'bg_image': bg_image_url,
'expires_in': 300, # 过期时间(秒)
'expected_distance': expected_distance # 传递给前端
})

- 校验验证码
class VerifySliderView(APIView):
"""
验证滑块结果的API视图
"""
permission_classes = ()
authentication_classes = ()
def post(self,request,*args, **kwargs):
# 获取前端数据
token = request.data.get('token')
distance = request.data.get('distance') # 用户滑动距离
duration = request.data.get('duration',0) # 滑动耗时(毫秒)
print(f'本次用户操作耗时为{ duration }毫秒')
# 验证必要参数
if not token or distance is None:
return Response(
{'detail': '缺少必要参数: token 或 distance'},
status=status.HTTP_400_BAD_REQUEST
)
# 获取Redis连接
conn = get_redis_connection('captcha')
cache_key = f"slider:{token}"
# 从Redis获取验证数据
cache_data_json = conn.get(cache_key)
if not cache_data_json:
return Response(
{'detail': '验证码已过期或不存在'},
status=status.HTTP_400_BAD_REQUEST
)
# 解析json数据
try:
cache_data = json.loads(cache_data_json.decode('utf-8'))
except json.JSONDecodeError:
return Response(
{'detail': '验证码数据格式错误'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 更新尝试次数
cache_data['attempts'] = cache_data.get('attempts',0) + 1
# 防止暴力破解(最多5次尝试)
if cache_data['attempts'] > 5:
# 删除过期验证码
conn.delete(cache_key)
return Response(
{ 'detail': '尝试次数过多'},
status=status.HTTP_403_FORBIDDEN
)
# 获取期望滑动距离(如果没有则生成一个)
expected_distance = cache_data.get('expected_distance')
if expected_distance is None:
# 如果没有存储期望距离,随机生成一个(100-300px之间)
expected_distance = cache_data['expected_distance'] = random.randint(100, 300)
# 验证滑动距离(容差范围±5px)
try:
user_distance = float(distance)
print(f'用户滑动距离为{user_distance},后端期望距离为{expected_distance}')
print(f'计算结果为: {abs(user_distance - expected_distance)>10}')
if abs(user_distance - expected_distance) > 10:
# 保存更新后的尝试次数
conn.set(cache_key, json.dumps(cache_data), ex=300)
return Response(
{'detail': '验证失败'},
status=status.HTTP_400_BAD_REQUEST
)
except (TypeError, ValueError):
return Response(
{'detail': '无效的滑动距离'},
status=status.HTTP_400_BAD_REQUEST
)
# 验证滑动时间(正常人类操作时间应在500ms以上)
try:
if int(duration) < 500:
# 保存更新后的尝试次数
conn.set(cache_key, json.dumps(cache_data), ex=300)
return Response(
{'detail': '操作过快'},
status=status.HTTP_400_BAD_REQUEST
)
except (TypeError, ValueError):
pass # 如果时间格式错误,跳过时间验证
# 验证通过,更新状态
cache_data['status'] = 'success'
cache_data['verified_at'] = time.time()
# 成功后有效期缩短为1分钟
conn.set(cache_key, json.dumps(cache_data), ex=60)
return Response({'msg': 'Success'})

- 登录逻辑补充
class UserLoginView(ObtainJSONWebToken):
"""
post:
用户登录, status: 200(成功), return: Token信息
"""
throttle_classes = [AnonRateThrottle]
def post(self, request, *args, **kwargs):
# 验证滑块验证码
captcha_token = request.data.get('captcha_token')
if not captcha_token:
return Response(
{'detail': '请先完成滑块验证'},
status=status.HTTP_400_BAD_REQUEST
)
# 检查验证码状态
conn = get_redis_connection('captcha')
cache_key = f"slider:{captcha_token}"
cache_data_json = conn.get(cache_key)
if not cache_data_json:
return Response(
{'detail': '验证码已过期,请重新验证'},
status=status.HTTP_400_BAD_REQUEST
)
try:
cache_data = json.loads(cache_data_json.decode('utf-8'))
except json.JSONDecodeError:
return Response(
{'detail': '验证码数据错误'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if cache_data.get('status') != 'success':
return Response(
{'detail': '验证码未通过,请重新验证'},
status=status.HTTP_400_BAD_REQUEST
)
# 验证码验证通过,执行登录逻辑
try:
response = super().post(request, *args, **kwargs)
except Exception as e:
# 登录失败时删除验证码缓存
conn.delete(cache_key)
return Response(
{'detail': '用户名或密码错误'},
status=status.HTTP_400_BAD_REQUEST
)
if response.status_code == 200:
# 登录成功后删除验证码缓存
conn.delete(cache_key)
user_conn = get_redis_connection('user_info')
user_conn.incr('visits')
return response
else:
# 登录失败时删除验证码缓存
conn.delete(cache_key)
if response.data.get('non_field_errors'):
if isinstance(response.data.get('non_field_errors'), list) and len(
response.data.get('non_field_errors')) > 0:
if response.data.get('non_field_errors')[0].strip() == '无法使用提供的认证信息登录。':
return Response(data={'detail': '用户名或密码错误'}, status=status.HTTP_400_BAD_REQUEST)
# 确保所有分支都返回Response对象
return Response(data=response.data, status=response.status_code)

- 路由
urlpatterns = [
......
path('login/', oauth.UserLoginView.as_view()),
path('captcha/slider/', oauth.GenerateSliderView.as_view(), name='generate_slider'),
path('slider/verify/', oauth.VerifySliderView.as_view(), name='verify_slider'),
]
- 前端代码
// components.SliderCaptcha.vue
<template>
<div class="slider-captcha">
<!-- 滑块容器 -->
<div class="slider-container" ref="sliderContainer">
<!-- 背景图 -->
<div class="slider-bg">
<img :src="bgImage" alt="验证背景" class="bg-img">
<!-- 滑块凹槽 -->
<div class="slider-groove" :style="{ left: grooveLeft + 'px' }"></div>
<!-- 滑块 -->
<div
class="slider-block"
:style="{ left: sliderLeft + 'px' }"
@mousedown="handleMouseDown"
@touchstart="handleTouchStart"
>
<svg-icon icon-class="right" class="slider-icon" />
</div>
</div>
<!-- 滑块底部提示 -->
<div class="slider-info">
<span :class="{ success: isSuccess, error: isError }">
{{ infoText }}
</span>
</div>
</div>
</div>
</template>
<script>
import { getSliderCaptcha, verifySliderCaptcha } from '@/api/user'
export default {
name: 'SliderCaptcha',
data() {
return {
// 滑块相关
sliderLeft: 0, // 滑块当前位置
grooveLeft: 0, // 凹槽位置
startX: 0, // 起始位置
isDragging: false, // 是否正在拖动
maxLeft: 0, // 最大可拖动距离
startTimestamp: 0, // 开始拖动时间戳
// 验证码相关
token: '', // 验证码token
bgImage: '', // 背景图片URL
distance: 0, // 滑动距离
duration: 0, // 滑动耗时
// 状态相关
isSuccess: false, // 验证成功
isError: false, // 验证失败
infoText: '向右拖动滑块完成验证', // 提示文本
isVerifying: false, // 正在验证中
expectedDistance: 0, // 新增:存储后端返回的期望距离
}
},
mounted() {
// 初始化滑块
this.initSlider()
// 获取验证码
this.fetchCaptcha()
},
methods: {
// 初始化滑块
initSlider() {
const container = this.$refs.sliderContainer
if (container) {
// 计算最大可拖动距离(容器宽度 - 滑块宽度)
this.maxLeft = container.clientWidth - 50
// 随机生成凹槽位置(100-300px之间,与后端逻辑对应)
this.grooveLeft = Math.floor(Math.random() * 200) + 100
}
// 绑定鼠标和触摸事件
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp)
document.addEventListener('touchmove', this.handleTouchMove)
document.addEventListener('touchend', this.handleTouchEnd)
},
// 获取验证码
async fetchCaptcha() {
try {
this.reset()
const response = await getSliderCaptcha()
this.token = response.data.token
this.bgImage = "http://127.0.0.1:8769" + response.data.bg_image
// 关键:使用后端返回的期望距离设置凹槽位置
this.expectedDistance = response.data.expected_distance
this.grooveLeft = this.expectedDistance // 凹槽位置与后端期望距离一致
console.log("背景地址为: ",this.bgImage)
this.reset()
} catch (error) {
this.infoText = '验证码加载失败,请刷新重试'
this.isError = true
setTimeout(() => this.reset(), 2000)
}
},
// 重置滑块
reset() {
this.sliderLeft = 0
this.isDragging = false
this.isSuccess = false
this.isError = false
this.infoText = '向右拖动滑块完成验证'
},
// 鼠标按下/触摸开始
handleMouseDown(e) {
this.startDrag(e.clientX)
},
handleTouchStart(e) {
this.startDrag(e.touches[0].clientX)
},
// 开始拖动
startDrag(x) {
if (this.isSuccess || this.isVerifying) return
this.startX = x
this.isDragging = true
this.startTimestamp = Date.now()
this.isError = false
this.infoText = '向右拖动滑块完成验证'
},
// 鼠标移动/触摸移动
handleMouseMove(e) {
if (this.isDragging) {
this.moveSlider(e.clientX)
}
},
handleTouchMove(e) {
if (this.isDragging) {
this.moveSlider(e.touches[0].clientX)
}
},
// 移动滑块
moveSlider(x) {
// 计算移动距离
let distance = x - this.startX
// 限制在有效范围内
distance = Math.max(0, Math.min(distance, this.maxLeft))
this.sliderLeft = distance
},
// 鼠标释放/触摸结束
handleMouseUp() {
this.endDrag()
},
handleTouchEnd() {
this.endDrag()
},
// 结束拖动
async endDrag() {
if (!this.isDragging) return
this.isDragging = false
this.distance = this.sliderLeft
this.duration = Date.now() - this.startTimestamp
// 验证滑动结果
if (this.distance < 10) {
// 移动距离过小,重置
this.sliderLeft = 0
return
}
await this.verify()
},
// 验证滑块
async verify() {
if (!this.token) {
this.infoText = '验证码已过期,请刷新重试'
this.isError = true
setTimeout(() => this.fetchCaptcha(), 1500)
return
}
this.isVerifying = true
this.infoText = '验证中...'
try {
const response = await verifySliderCaptcha({
token: this.token,
distance: this.distance,
duration: this.duration
})
if (response.data.msg === 'Success') {
// 验证成功
this.isSuccess = true
this.isError = false
this.infoText = '验证成功'
// 通知父组件验证成功
this.$emit('success', this.token)
} else {
this.handleVerifyError('验证失败,请重试')
}
} catch (error) {
console.log("错误为",error)
const message = error.response?.data?.detail || '验证失败,请重试'
this.handleVerifyError(message)
} finally {
this.isVerifying = false
}
},
// 处理验证失败
handleVerifyError(message) {
this.isError = true
this.infoText = message
// 滑块回到起始位置
this.sliderLeft = 0
// 如果是尝试次数过多或过期,重新获取验证码
if (message.includes('次数过多') || message.includes('过期')) {
setTimeout(() => this.fetchCaptcha(), 1500)
}
}
},
beforeDestroy() {
// 移除事件监听
document.removeEventListener('mousemove', this.handleMouseMove)
document.removeEventListener('mouseup', this.handleMouseUp)
document.removeEventListener('touchmove', this.handleTouchMove)
document.removeEventListener('touchend', this.handleTouchEnd)
}
}
</script>
<style scoped>
.slider-captcha {
width: 100%;
margin-bottom: 20px;
}
.slider-container {
width: 100%;
position: relative;
}
.slider-bg {
width: 100%;
height: 160px;
position: relative;
border-radius: 8px;
overflow: hidden;
background-color: #f7f9fa;
border: 1px solid #e4e7ed;
}
.bg-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.slider-groove {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
background-color: rgba(255, 255, 255, 0.5);
border: 1px dashed #ccc;
box-sizing: border-box;
}
.slider-block {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
background-color: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.slider-block:hover {
background-color: #f5f7fa;
}
.slider-icon {
width: 20px;
height: 20px;
color: #409eff;
}
.slider-info {
height: 36px;
line-height: 36px;
font-size: 14px;
text-align: center;
margin-top: 10px;
}
.success {
color: #67c23a;
}
.error {
color: #f56c6c;
}
</style>
// login.index.vue
<template>
<div class="login" :style="'background-image:url('+ Background +');'">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" label-position="left" label-width="0px" class="login-form">
<h3 class="title">
ADMIN 后台管理系统
</h3>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" show-password type="password" auto-complete="off" placeholder="密码" @keyup.enter.native="handleLogin">
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<!-- 滑块验证码 -->
<el-form-item>
<slider-captcha ref="sliderCaptcha" @success="handleCaptchaSuccess" />
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0 0 25px 0;">
记住我
</el-checkbox>
<el-form-item style="width:100%;">
<el-button :loading="loading" size="medium" type="primary" style="width:100%;" @click.native.prevent="handleLogin">
<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
</el-button>
</el-form-item>
</el-form>
<!-- 底部 -->
<div id="el-login-footer">
<span />
<a href="https://github.com/TianPangJi" target="_blank">个人练习项目 @ 点击查看GitHub</a>
</div>
</div>
</template>
<script>
import { encrypt, decrypt } from '@/utils/rsaEncrypt'
import Config from '@/settings'
import Cookies from 'js-cookie'
import Background from '@/assets/images/login-bg.jpg'
import { getSliderCaptcha, verifySliderCaptcha } from '@/api/user'
import SliderCaptcha from '@/components/SliderCaptcha'
export default {
name: 'Login',
components: {
SliderCaptcha
},
data() {
return {
Background: Background,
cookiePass: '',
loginForm: {
username: '',
password: '',
rememberMe: false
},
loginRules: {
username: [{ required: true, trigger: 'blur', message: '用户名不能为空' }],
password: [{ required: true, trigger: 'blur', message: '密码不能为空' }]
},
loading: false,
redirect: undefined,
otherQuery: {},
captchaToken: '' // 滑块验证成功后的token
}
},
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect
this.otherQuery = this.getOtherQuery(route)
},
immediate: true
}
},
created() {
// 获取用户名密码等Cookie
this.getCookie()
},
methods: {
getOtherQuery(route) {
const query = { ...route.query }
delete query.redirect
return query
},
getCookie() {
const username = Cookies.get('username')
let password = Cookies.get('password')
const rememberMe = Cookies.get('rememberMe')
// 保存cookie里面的加密后的密码
this.cookiePass = password === undefined ? '' : password
password = password === undefined ? this.loginForm.password : password
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password,
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
}
},
handleCaptchaSuccess(token) {
// 保存验证成功的token,登录时需要提交
this.captchaToken = token
},
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
// 检查验证码是否已通过
if (!this.captchaToken) {
this.$message.error('请先完成滑块验证')
return
}
const user = {
username: this.loginForm.username,
password: this.loginForm.password,
rememberMe: this.loginForm.rememberMe,
captcha_token: this.captchaToken // 添加滑块验证token
}
if (user.password !== this.cookiePass) {
user.password = encrypt(user.password)
} else {
this.loginForm.password = decrypt(user.password)
}
this.loading = true
if (user.rememberMe) {
Cookies.set('username', user.username, { expires: Config.passCookieExpires })
Cookies.set('password', user.password, { expires: Config.passCookieExpires })
Cookies.set('rememberMe', user.rememberMe, { expires: Config.passCookieExpires })
} else {
Cookies.remove('username')
Cookies.remove('password')
Cookies.remove('rememberMe')
}
user.password = decrypt(user.password)
console.log("发送的数据是",user)
this.$store.dispatch('user/login', user).then(() => {
// this.$store.dispatch('user/login', this.loginForm).then(() => {
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
this.loading = false
}).catch(() => {
this.loading = false
// 登录失败后刷新验证码
this.captchaToken = ''
this.resetSliderCaptcha() // 刷新验证码
})
} else {
return false
}
})
},
// 重置滑块验证码
resetSliderCaptcha() {
if (this.$refs.sliderCaptcha) {
this.$refs.sliderCaptcha.fetchCaptcha()
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss">
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: relative;
overflow: hidden;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 0;
}
}
.title {
margin: 0 auto 30px auto;
text-align: center;
color: #333;
font-size: 24px;
font-weight: bold;
}
.login-form {
border-radius: 8px;
background: #ffffff;
width: 400px;
padding: 30px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
position: relative;
z-index: 1;
.el-input {
height: 42px;
margin-bottom: 18px;
input {
height: 42px;
border-radius: 4px;
padding-left: 36px;
font-size: 14px;
}
.el-input__prefix {
left: 10px;
}
}
.input-icon {
height: 16px;
width: 16px;
color: #999;
}
/* 错误提示 */
.slider-error {
color: #f56c6c;
margin-top: 12px;
text-align: center;
font-size: 13px;
font-weight: 500;
background: #fef0f0;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #fde2e2;
}
/* 记住我复选框 */
.el-checkbox {
margin-bottom: 20px;
.el-checkbox__label {
color: #606266;
}
}
/* 登录按钮 */
.el-button {
height: 44px;
font-size: 16px;
font-weight: 500;
letter-spacing: 1px;
border-radius: 4px;
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
&:active {
transform: translateY(0);
}
}
}
#el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: rgba(255, 255, 255, 0.8);
font-family: Arial, sans-serif;
font-size: 13px;
letter-spacing: 0.5px;
z-index: 1;
a {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
transition: all 0.3s;
&:hover {
color: white;
text-decoration: underline;
}
}
}
</style>

浙公网安备 33010602011771号