mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-25 17:22:55 +00:00
feat(init): 初始化项目
This commit is contained in:
parent
bce0046cf2
commit
dbe8e519a9
|
|
@ -158,3 +158,10 @@ cython_debug/
|
|||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
ui/node_modules
|
||||
ui/dist
|
||||
apps/static
|
||||
data
|
||||
.idea
|
||||
.dev
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: smart-doc
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2023/9/14 16:22
|
||||
@desc:
|
||||
"""
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: smart-doc
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2023/9/14 19:44
|
||||
@desc:
|
||||
"""
|
||||
from .authenticate import *
|
||||
from .authentication import *
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: authenticate.py
|
||||
@date:2023/9/4 11:16
|
||||
@desc: 认证类
|
||||
"""
|
||||
|
||||
from common.constants.permission_constants import Auth, get_permission_list_by_role, RoleConstants
|
||||
from common.exception.app_exception import AppAuthenticationFailed
|
||||
from django.core import cache
|
||||
from django.core import signing
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from smartdoc.settings import JWT_AUTH
|
||||
from users.models.user import User
|
||||
|
||||
token_cache = cache.caches['token_cache']
|
||||
|
||||
|
||||
class AnonymousAuthentication(TokenAuthentication):
|
||||
def authenticate(self, request):
|
||||
return None, None
|
||||
|
||||
|
||||
class TokenAuth(TokenAuthentication):
|
||||
# 重新 authenticate 方法,自定义认证规则
|
||||
def authenticate(self, request):
|
||||
auth = request.META.get('HTTP_AUTHORIZATION', None
|
||||
)
|
||||
# 未认证
|
||||
if auth is None:
|
||||
raise AppAuthenticationFailed(1003, '未登录,请先登录')
|
||||
try:
|
||||
# 解析 token
|
||||
user = signing.loads(auth)
|
||||
if 'id' in user:
|
||||
cache_token = token_cache.get(auth)
|
||||
if cache_token is None:
|
||||
raise AppAuthenticationFailed(1002, "登录过期")
|
||||
user = User.objects.get(id=user['id'])
|
||||
# 续期
|
||||
token_cache.touch(auth, timeout=JWT_AUTH['JWT_EXPIRATION_DELTA'].total_seconds())
|
||||
rule = RoleConstants[user.role]
|
||||
return user, Auth(role_list=[rule],
|
||||
permission_list=get_permission_list_by_role(RoleConstants[user.role]))
|
||||
else:
|
||||
raise AppAuthenticationFailed(1002, "身份验证信息不正确!非法用户")
|
||||
|
||||
except Exception as e:
|
||||
raise AppAuthenticationFailed(1002, "身份验证信息不正确!非法用户")
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: authentication.py
|
||||
@date:2023/9/13 15:00
|
||||
@desc: 鉴权
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from common.constants.permission_constants import ViewPermission, CompareConstants, RoleConstants, PermissionConstants
|
||||
from common.exception.app_exception import AppUnauthorizedFailed
|
||||
|
||||
|
||||
def exist_permissions_by_permission_constants(user_permission: List[PermissionConstants],
|
||||
permission_list: List[PermissionConstants]):
|
||||
"""
|
||||
用户是否拥有 permission_list的权限
|
||||
:param user_permission: 用户权限
|
||||
:param permission_list: 需要的权限
|
||||
:return: 是否拥有
|
||||
"""
|
||||
return any(list(map(lambda up: permission_list.__contains__(up), user_permission)))
|
||||
|
||||
|
||||
def exist_role_by_role_constants(user_role: List[RoleConstants],
|
||||
role_list: List[RoleConstants]):
|
||||
"""
|
||||
用户是否拥有这个角色
|
||||
:param user_role: 用户角色
|
||||
:param role_list: 需要拥有的角色
|
||||
:return: 是否拥有
|
||||
"""
|
||||
return any(list(map(lambda up: role_list.__contains__(up), user_role)))
|
||||
|
||||
|
||||
def exist_permissions_by_view_permission(user_role: List[RoleConstants], user_permission: List[PermissionConstants],
|
||||
permission: ViewPermission):
|
||||
"""
|
||||
用户是否存在这些权限
|
||||
:param user_role: 用户角色
|
||||
:param user_permission: 用户权限
|
||||
:param permission: 所属权限
|
||||
:return: 是否存在 True False
|
||||
"""
|
||||
role_ok = any(list(map(lambda ur: permission.roleList.__contains__(ur), user_role)))
|
||||
permission_ok = any(list(map(lambda up: permission.permissionList.__contains__(up), user_permission)))
|
||||
return role_ok | permission_ok if permission.compare == CompareConstants.OR else role_ok & permission_ok
|
||||
|
||||
|
||||
def exist_permissions(user_role: List[RoleConstants], user_permission: List[PermissionConstants], permission):
|
||||
if isinstance(permission, ViewPermission):
|
||||
return exist_permissions_by_view_permission(user_role, user_permission, permission)
|
||||
elif isinstance(permission, RoleConstants):
|
||||
return exist_role_by_role_constants(user_role, [permission])
|
||||
elif isinstance(permission, PermissionConstants):
|
||||
return exist_permissions_by_permission_constants(user_permission, [permission])
|
||||
return False
|
||||
|
||||
|
||||
def has_permissions(*permission, compare=CompareConstants.OR):
|
||||
"""
|
||||
权限 role or permission
|
||||
:param compare: 比较符号
|
||||
:param permission: 如果是角色 role:roleId
|
||||
:return: 权限装饰器函数,用于判断用户是否有权限访问当前接口
|
||||
"""
|
||||
|
||||
def inner(func):
|
||||
def run(view, request, **kwargs):
|
||||
exit_list = list(
|
||||
map(lambda p: exist_permissions(request.auth.role_list, request.auth.permission_list, p), permission))
|
||||
# 判断是否有权限
|
||||
if any(exit_list) if compare == CompareConstants.OR else all(exit_list):
|
||||
return func(view, request, **kwargs)
|
||||
else:
|
||||
raise AppUnauthorizedFailed(403, "没有权限访问")
|
||||
|
||||
return run
|
||||
|
||||
return inner
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: file_cache.py
|
||||
@date:2023/9/11 15:58
|
||||
@desc: 文件缓存
|
||||
"""
|
||||
import datetime
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
|
||||
from diskcache import Cache
|
||||
from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache
|
||||
|
||||
|
||||
class FileCache(BaseCache):
|
||||
def __init__(self, dir, params):
|
||||
super().__init__(params)
|
||||
self._dir = os.path.abspath(dir)
|
||||
self._createdir()
|
||||
self.cache = Cache(self._dir)
|
||||
|
||||
def _createdir(self):
|
||||
old_umask = os.umask(0o077)
|
||||
try:
|
||||
os.makedirs(self._dir, 0o700, exist_ok=True)
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
expire = timeout if isinstance(timeout, int) or isinstance(timeout,
|
||||
float) else timeout.total_seconds()
|
||||
return self.cache.add(key, value=value, expire=expire)
|
||||
|
||||
def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
expire = timeout if isinstance(timeout, int) or isinstance(timeout,
|
||||
float) else timeout.total_seconds()
|
||||
return self.cache.set(key, value=value, expire=expire)
|
||||
|
||||
def get(self, key, default=None, version=None):
|
||||
return self.cache.get(key, default=default)
|
||||
|
||||
def delete(self, key, version=None):
|
||||
return self.cache.delete(key)
|
||||
|
||||
def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
expire = timeout if isinstance(timeout, int) or isinstance(timeout,
|
||||
float) else timeout.total_seconds()
|
||||
|
||||
return self.cache.touch(key, expire=expire)
|
||||
|
||||
def ttl(self, key):
|
||||
"""
|
||||
获取key的剩余时间
|
||||
:param key: key
|
||||
:return: 剩余时间
|
||||
"""
|
||||
value, expire_time = self.cache.get(key, expire_time=True)
|
||||
if value is None:
|
||||
return None
|
||||
return datetime.timedelta(seconds=math.ceil(expire_time - time.time()))
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: swagger_conf.py
|
||||
@date:2023/9/5 14:01
|
||||
@desc: 用于swagger 分组
|
||||
"""
|
||||
|
||||
from drf_yasg.inspectors import SwaggerAutoSchema
|
||||
|
||||
tags_dict = {
|
||||
'user': '用户'
|
||||
}
|
||||
|
||||
|
||||
class CustomSwaggerAutoSchema(SwaggerAutoSchema):
|
||||
def get_tags(self, operation_keys=None):
|
||||
tags = super().get_tags(operation_keys)
|
||||
if "api" in tags and operation_keys:
|
||||
return [tags_dict.get(operation_keys[1]) if operation_keys[1] in tags_dict else operation_keys[1]]
|
||||
return tags
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: exception_code_constants.py
|
||||
@date:2023/9/4 14:09
|
||||
@desc: 异常常量类
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
from common.exception.app_exception import AppApiException
|
||||
|
||||
|
||||
class ExceptionCodeConstantsValue:
|
||||
def __init__(self, code, message):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
def get_message(self):
|
||||
return self.message
|
||||
|
||||
def get_code(self):
|
||||
return self.code
|
||||
|
||||
def to_app_api_exception(self):
|
||||
return AppApiException(code=self.code, message=self.message)
|
||||
|
||||
|
||||
class ExceptionCodeConstants(Enum):
|
||||
INCORRECT_USERNAME_AND_PASSWORD = ExceptionCodeConstantsValue(1000, "用户名或者密码不正确")
|
||||
NOT_AUTHENTICATION = ExceptionCodeConstantsValue(1001, "请先登录,并携带用户Token")
|
||||
EMAIL_SEND_ERROR = ExceptionCodeConstantsValue(1002, "邮件发送失败")
|
||||
EMAIL_FORMAT_ERROR = ExceptionCodeConstantsValue(1003, "邮箱格式错误")
|
||||
EMAIL_IS_EXIST = ExceptionCodeConstantsValue(1004, "邮箱已经被注册,请勿重复注册")
|
||||
EMAIL_IS_NOT_EXIST = ExceptionCodeConstantsValue(1005, "邮箱尚未注册,请先注册")
|
||||
CODE_ERROR = ExceptionCodeConstantsValue(1005, "验证码不正确,或者验证码过期")
|
||||
USERNAME_IS_EXIST = ExceptionCodeConstantsValue(1006, "用户名已被使用,请使用其他用户名")
|
||||
USERNAME_ERROR = ExceptionCodeConstantsValue(1006, "用户名不能为空,并且长度在6-20")
|
||||
PASSWORD_NOT_EQ_RE_PASSWORD = ExceptionCodeConstantsValue(1007, "密码与确认密码不一致")
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: permission_constants.py
|
||||
@date:2023/9/13 18:23
|
||||
@desc: 权限,角色 常量
|
||||
"""
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
|
||||
class Group(Enum):
|
||||
"""
|
||||
权限组 一个组一般对应前段一个菜单
|
||||
"""
|
||||
USER = "USER"
|
||||
|
||||
|
||||
class Operate(Enum):
|
||||
"""
|
||||
一个权限组的操作权限
|
||||
"""
|
||||
READ = 'READ'
|
||||
EDIT = "EDIT"
|
||||
CREATE = "CREATE"
|
||||
DELETE = "DELETE"
|
||||
|
||||
|
||||
class Role:
|
||||
def __init__(self, name: str, decs: str):
|
||||
self.name = name
|
||||
self.decs = decs
|
||||
|
||||
|
||||
class RoleConstants(Enum):
|
||||
ADMIN = Role("管理员", "管理员,预制目前不会使用")
|
||||
USER = Role("用户", "用户所有权限")
|
||||
SESSION = Role("会话", decs="只拥有应用会话框接口权限")
|
||||
|
||||
|
||||
class Permission:
|
||||
"""
|
||||
权限信息
|
||||
"""
|
||||
|
||||
def __init__(self, group: Group, operate: Operate, roles: List[RoleConstants]):
|
||||
self.group = group
|
||||
self.operate = operate
|
||||
self.roleList = roles
|
||||
|
||||
def __str__(self):
|
||||
return self.group.value + ":" + self.operate.value
|
||||
|
||||
|
||||
class PermissionConstants(Enum):
|
||||
"""
|
||||
权限枚举
|
||||
"""
|
||||
USER_READ = Permission(group=Group.USER, operate=Operate.READ, roles=[RoleConstants.ADMIN, RoleConstants.USER])
|
||||
USER_EDIT = Permission(group=Group.USER, operate=Operate.EDIT, roles=[RoleConstants.ADMIN, RoleConstants.USER])
|
||||
USER_DELETE = Permission(group=Group.USER, operate=Operate.EDIT, roles=[RoleConstants.USER])
|
||||
|
||||
|
||||
def get_permission_list_by_role(role: RoleConstants):
|
||||
"""
|
||||
根据角色 获取角色对应的权限
|
||||
:param role: 角色
|
||||
:return: 权限
|
||||
"""
|
||||
return list(map(lambda k: PermissionConstants[k],
|
||||
list(filter(lambda k: PermissionConstants[k].value.roleList.__contains__(role),
|
||||
PermissionConstants.__members__))))
|
||||
|
||||
|
||||
class Auth:
|
||||
"""
|
||||
用于存储当前用户的角色和权限
|
||||
"""
|
||||
|
||||
def __init__(self, role_list: List[RoleConstants], permission_list: List[PermissionConstants]):
|
||||
self.role_list = role_list
|
||||
self.permission_list = permission_list
|
||||
|
||||
|
||||
class CompareConstants(Enum):
|
||||
# 或者
|
||||
OR = "OR"
|
||||
# 并且
|
||||
AND = "AND"
|
||||
|
||||
|
||||
class ViewPermission:
|
||||
def __init__(self, roleList: List[RoleConstants], permissionList: List[PermissionConstants],
|
||||
compare=CompareConstants.OR):
|
||||
self.roleList = roleList
|
||||
self.permissionList = permissionList
|
||||
self.compare = compare
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: app_exception.py
|
||||
@date:2023/9/4 14:04
|
||||
@desc:
|
||||
"""
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class AppApiException(Exception):
|
||||
"""
|
||||
项目内异常
|
||||
"""
|
||||
status_code = status.HTTP_200_OK
|
||||
|
||||
def __init__(self, code, message):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
|
||||
class AppAuthenticationFailed(AppApiException):
|
||||
"""
|
||||
未认证(未登录)异常
|
||||
"""
|
||||
status_code = status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def __init__(self, code, message):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
|
||||
class AppUnauthorizedFailed(AppApiException):
|
||||
"""
|
||||
未授权(没有权限)异常
|
||||
"""
|
||||
status_code = status.HTTP_403_FORBIDDEN
|
||||
|
||||
def __init__(self, code, message):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class VectorField(models.Field):
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'vector'
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2023/9/6 10:09
|
||||
@desc:
|
||||
"""
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: handle_exception.py
|
||||
@date:2023/9/5 19:29
|
||||
@desc:
|
||||
"""
|
||||
from rest_framework.exceptions import ValidationError, ErrorDetail, APIException
|
||||
from rest_framework.views import exception_handler
|
||||
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.response import result
|
||||
|
||||
|
||||
def to_result(key, args):
|
||||
"""
|
||||
将校验异常 args转换为统一数据
|
||||
:param key: 校验key
|
||||
:param args: 校验异常参数
|
||||
:return: 接口响应对象
|
||||
"""
|
||||
error_detail = (args[0] if len(args) > 0 else {key: [ErrorDetail('未知异常', code='unknown')]}).get(key)[
|
||||
0]
|
||||
|
||||
return result.Result(500 if isinstance(error_detail.code, str) else error_detail.code,
|
||||
message=f"【{key}】为必填参数" if str(
|
||||
error_detail) == "This field is required." else error_detail)
|
||||
|
||||
|
||||
def validation_error_to_result(exc: ValidationError):
|
||||
"""
|
||||
校验异常转响应对象
|
||||
:param exc: 校验异常
|
||||
:return: 接口响应对象
|
||||
"""
|
||||
res = list(map(lambda key: to_result(key, args=exc.args),
|
||||
exc.args[0].keys() if len(exc.args) > 0 else []))
|
||||
if len(res) > 0:
|
||||
return res[0]
|
||||
else:
|
||||
return result.error("未知异常")
|
||||
|
||||
|
||||
def handle_exception(exc, context):
|
||||
exception_class = exc.__class__
|
||||
# 先调用REST framework默认的异常处理方法获得标准错误响应对象
|
||||
response = exception_handler(exc, context)
|
||||
# 在此处补充自定义的异常处理
|
||||
if issubclass(exception_class, ValidationError):
|
||||
return validation_error_to_result(exc)
|
||||
if issubclass(exception_class, AppApiException):
|
||||
return result.Result(exc.code, exc.message, response_status=exc.status_code)
|
||||
if issubclass(exception_class, APIException):
|
||||
return result.error(exc.detail)
|
||||
return response
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: smart-doc
|
||||
@Author:虎
|
||||
@file: api_mixin.py
|
||||
@date:2023/9/14 17:50
|
||||
@desc:
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class ApiMixin(serializers.Serializer):
|
||||
|
||||
def get_request_params_api(self):
|
||||
pass
|
||||
|
||||
def get_request_body_api(self):
|
||||
pass
|
||||
|
||||
def get_response_body_api(self):
|
||||
pass
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
from django.http import JsonResponse
|
||||
from drf_yasg import openapi
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class Result(JsonResponse):
|
||||
"""
|
||||
接口统一返回对象
|
||||
"""
|
||||
|
||||
def __init__(self, code=200, message="成功", data=None, response_status=status.HTTP_200_OK, **kwargs):
|
||||
back_info_dict = {"code": code, "message": message, 'data': data}
|
||||
super().__init__(data=back_info_dict, status=response_status)
|
||||
|
||||
|
||||
def get_api_response(response_data_schema: openapi.Schema, data_examples):
|
||||
"""
|
||||
获取统一返回 响应Api
|
||||
"""
|
||||
return openapi.Responses(responses={200: openapi.Response(description="响应参数",
|
||||
schema=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
properties={
|
||||
'code': openapi.Schema(
|
||||
type=openapi.TYPE_INTEGER,
|
||||
title="响应码",
|
||||
default=200,
|
||||
description="成功:200 失败:其他"),
|
||||
"message": openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="提示",
|
||||
default='成功',
|
||||
description="错误提示"),
|
||||
"data": response_data_schema
|
||||
|
||||
}
|
||||
),
|
||||
examples={'code': 200,
|
||||
'data': data_examples,
|
||||
'message': "成功"})})
|
||||
|
||||
|
||||
def success(data):
|
||||
"""
|
||||
获取一个成功的响应对象
|
||||
:param data: 接口响应数据
|
||||
:return: 请求响应对象
|
||||
"""
|
||||
return Result(data=data)
|
||||
|
||||
|
||||
def error(message):
|
||||
"""
|
||||
获取一个失败的响应对象
|
||||
:param message: 错误提示
|
||||
:return: 接口响应对象
|
||||
"""
|
||||
return Result(code=500, message=message)
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="description" content="email code">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<!--邮箱验证码模板-->
|
||||
<body>
|
||||
<div style="background-color:#ECECEC; padding: 35px;">
|
||||
<table cellpadding="0" align="center"
|
||||
style="width: 800px;height: 100%; margin: 0px auto; text-align: left; position: relative; border-top-left-radius: 5px; border-top-right-radius: 5px; border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; font-size: 14px; font-family:微软雅黑, 黑体; line-height: 1.5; box-shadow: rgb(153, 153, 153) 0px 0px 5px; border-collapse: collapse; background-position: initial initial; background-repeat: initial initial;background:#fff;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th valign="middle"
|
||||
style="height: 25px; line-height: 25px; padding: 15px 35px; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: rgba(51, 112, 255); background-color: rgba(51, 112, 255); border-top-left-radius: 5px; border-top-right-radius: 5px; border-bottom-right-radius: 0px; border-bottom-left-radius: 0px;">
|
||||
<font face="微软雅黑" size="5" style="color: rgb(255, 255, 255); ">智能客服平台验证码</font>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="word-break:break-all">
|
||||
<div style="padding:25px 35px 40px; background-color:#fff;opacity:0.8;">
|
||||
|
||||
<h2 style="margin: 5px 0px; ">
|
||||
<font color="#333333" style="line-height: 20px; ">
|
||||
<font style="line-height: 22px; " size="4">
|
||||
尊敬的用户:</font>
|
||||
</font>
|
||||
</h2>
|
||||
<!-- 中文 -->
|
||||
<p><font color="#ff8c00" style="font-weight: 900;">${code}</font> 为您的动态验证码,请于30分钟内填写,为保障帐户安全,请勿向任何人提供此验证码。
|
||||
</p><br>
|
||||
|
||||
<div style="width:100%;margin:0 auto;">
|
||||
<div style="padding:10px 10px 0;border-top:1px solid #ccc;color:#747474;margin-bottom:20px;line-height:1.3em;font-size:12px;text-align:right">
|
||||
<p>飞致云</p>
|
||||
<br>
|
||||
<p>此为系统邮件,请勿回复<br>
|
||||
Please do not reply to this system email
|
||||
</p>
|
||||
<!--<p>©***</p>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: lock.py
|
||||
@date:2023/9/11 11:45
|
||||
@desc:
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.cache import caches
|
||||
|
||||
memory_cache = caches['default']
|
||||
|
||||
|
||||
def try_lock(key: str):
|
||||
"""
|
||||
获取锁
|
||||
:param key: 获取锁 key
|
||||
:return: 是否获取到锁
|
||||
"""
|
||||
return memory_cache.add(key, 'lock', timeout=timedelta(hours=1).total_seconds())
|
||||
|
||||
|
||||
def un_lock(key: str):
|
||||
"""
|
||||
解锁
|
||||
:param key: 解锁 key
|
||||
:return: 是否解锁成功
|
||||
"""
|
||||
return memory_cache.delete(key)
|
||||
|
||||
|
||||
def lock(lock_key):
|
||||
"""
|
||||
给一个函数上锁
|
||||
:param lock_key: 上锁key 字符串|函数 函数返回值为字符串
|
||||
:return: 装饰器函数 当前装饰器主要限制一个key只能一个线程去调用 相同key只能阻塞等待上一个任务执行完毕 不同key不需要等待
|
||||
"""
|
||||
|
||||
def inner(func):
|
||||
def run(*args, **kwargs):
|
||||
key = lock_key(*args, **kwargs) if callable(lock_key) else lock_key
|
||||
try:
|
||||
if try_lock(key=key):
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
un_lock(key=key)
|
||||
|
||||
return run
|
||||
|
||||
return inner
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: split_model.py
|
||||
@date:2023/9/1 15:12
|
||||
@desc:
|
||||
"""
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
import jieba
|
||||
|
||||
|
||||
def get_level_block(text, level_content_list, level_content_index):
|
||||
"""
|
||||
从文本中获取块数据
|
||||
:param text: 文本
|
||||
:param level_content_list: 拆分的title数组
|
||||
:param level_content_index: 指定的下标
|
||||
:return: 拆分后的文本数据
|
||||
"""
|
||||
start_content: str = level_content_list[level_content_index].get('content')
|
||||
next_content = level_content_list[level_content_index + 1].get("content") if level_content_index + 1 < len(
|
||||
level_content_list) else None
|
||||
start_index = text.index(start_content)
|
||||
end_index = text.index(next_content) if next_content is not None else len(text)
|
||||
return text[start_index:end_index]
|
||||
|
||||
|
||||
def to_tree_obj(content, state='title'):
|
||||
"""
|
||||
转换为树形对象
|
||||
:param content: 文本数据
|
||||
:param state: 状态: title block
|
||||
:return: 转换后的数据
|
||||
"""
|
||||
return {'content': content, 'state': state}
|
||||
|
||||
|
||||
def remove_special_symbol(str_source: str):
|
||||
"""
|
||||
删除特殊字符
|
||||
:param str_source: 需要删除的文本数据
|
||||
:return: 删除后的数据
|
||||
"""
|
||||
return str_source
|
||||
|
||||
|
||||
def filter_special_symbol(content: dict):
|
||||
"""
|
||||
过滤文本中的特殊字符
|
||||
:param content: 需要过滤的对象
|
||||
:return: 过滤后返回
|
||||
"""
|
||||
content['content'] = remove_special_symbol(content['content'])
|
||||
return content
|
||||
|
||||
|
||||
def flat(tree_data_list: List[dict], parent_chain: List[dict], result: List[dict]):
|
||||
"""
|
||||
扁平化树形结构数据
|
||||
:param tree_data_list: 树形接口数据
|
||||
:param parent_chain: 父级数据 传[] 用于递归存储数据
|
||||
:param result: 响应数据 传[] 用于递归存放数据
|
||||
:return: result 扁平化后的数据
|
||||
"""
|
||||
if parent_chain is None:
|
||||
parent_chain = []
|
||||
if result is None:
|
||||
result = []
|
||||
for tree_data in tree_data_list:
|
||||
p = parent_chain.copy()
|
||||
p.append(tree_data)
|
||||
result.append(to_flat_obj(parent_chain, content=tree_data["content"], state=tree_data["state"]))
|
||||
children = tree_data.get('children')
|
||||
if children is not None and len(children) > 0:
|
||||
flat(children, p, result)
|
||||
return result
|
||||
|
||||
|
||||
def to_paragraph(obj: dict):
|
||||
"""
|
||||
转换为段落
|
||||
:param obj: 需要转换的对象
|
||||
:return: 段落对象
|
||||
"""
|
||||
content = obj['content']
|
||||
return {"keywords": get_keyword(content),
|
||||
'parent_chain': list(map(lambda p: p['content'], obj['parent_chain'])),
|
||||
'content': content}
|
||||
|
||||
|
||||
def get_keyword(content: str):
|
||||
"""
|
||||
获取content中的关键词
|
||||
:param content: 文本
|
||||
:return: 关键词数组
|
||||
"""
|
||||
stopwords = [':', '“', '!', '”', '\n', '\\s']
|
||||
cutworms = jieba.lcut(content)
|
||||
return list(set(list(filter(lambda k: (k not in stopwords) | len(k) > 1, cutworms))))
|
||||
|
||||
|
||||
def titles_to_paragraph(list_title: List[dict]):
|
||||
"""
|
||||
将同一父级的title转换为块段落
|
||||
:param list_title: 同父级title
|
||||
:return: 块段落
|
||||
"""
|
||||
if len(list_title) > 0:
|
||||
content = "\n".join(
|
||||
list(map(lambda d: d['content'].strip("\r\n").strip("\n").strip("\\s"), list_title)))
|
||||
|
||||
return {'keywords': '',
|
||||
'parent_chain': list(
|
||||
map(lambda p: p['content'].strip("\r\n").strip("\n").strip("\\s"), list_title[0]['parent_chain'])),
|
||||
'content': content}
|
||||
return None
|
||||
|
||||
|
||||
def parse_group_key(level_list: List[dict]):
|
||||
"""
|
||||
将同级别同父级的title生成段落,加上本身的段落数据形成新的数据
|
||||
:param level_list: title n 级数据
|
||||
:return: 根据title生成的数据 + 段落数据
|
||||
"""
|
||||
result = []
|
||||
group_data = group_by(list(filter(lambda f: f['state'] == 'title' and len(f['parent_chain']) > 0, level_list)),
|
||||
key=lambda d: ",".join(list(map(lambda p: p['content'], d['parent_chain']))))
|
||||
result += list(map(lambda group_data_key: titles_to_paragraph(group_data[group_data_key]), group_data))
|
||||
result += list(map(to_paragraph, list(filter(lambda f: f['state'] == 'block', level_list))))
|
||||
return result
|
||||
|
||||
|
||||
def to_block_paragraph(tree_data_list: List[dict]):
|
||||
"""
|
||||
转换为块段落对象
|
||||
:param tree_data_list: 树数据
|
||||
:return: 块段落
|
||||
"""
|
||||
flat_list = flat(tree_data_list, [], [])
|
||||
level_group_dict: dict = group_by(flat_list, key=lambda f: f['level'])
|
||||
return list(map(lambda level: parse_group_key(level_group_dict[level]), level_group_dict))
|
||||
|
||||
|
||||
def parse_level(text, pattern: str):
|
||||
"""
|
||||
获取正则匹配到的文本
|
||||
:param text: 需要匹配的文本
|
||||
:param pattern: 正则
|
||||
:return: 符合正则的文本
|
||||
"""
|
||||
level_content_list = list(map(to_tree_obj, re.findall(pattern, text, flags=0)))
|
||||
return list(map(filter_special_symbol, level_content_list))
|
||||
|
||||
|
||||
def to_flat_obj(parent_chain: List[dict], content: str, state: str):
|
||||
"""
|
||||
将树形属性转换为扁平对象
|
||||
:param parent_chain:
|
||||
:param content:
|
||||
:param state:
|
||||
:return:
|
||||
"""
|
||||
return {'parent_chain': parent_chain, 'level': len(parent_chain), "content": content, 'state': state}
|
||||
|
||||
|
||||
def flat_map(array: List[List]):
|
||||
"""
|
||||
将二位数组转为一维数组
|
||||
:param array: 二维数组
|
||||
:return: 一维数组
|
||||
"""
|
||||
result = []
|
||||
for e in array:
|
||||
result += e
|
||||
return result
|
||||
|
||||
|
||||
def group_by(list_source: List, key):
|
||||
"""
|
||||
將數組分組
|
||||
:param list_source: 需要分組的數組
|
||||
:param key: 分組函數
|
||||
:return: key->[]
|
||||
"""
|
||||
result = {}
|
||||
for e in list_source:
|
||||
k = key(e)
|
||||
array = result.get(k) if k in result else []
|
||||
array.append(e)
|
||||
result[k] = array
|
||||
return result
|
||||
|
||||
|
||||
class SplitModel:
|
||||
|
||||
def __init__(self, content_level_pattern):
|
||||
self.content_level_pattern = content_level_pattern
|
||||
|
||||
def parse_to_tree(self, text: str, index=0):
|
||||
"""
|
||||
解析文本
|
||||
:param text: 需要解析的文本
|
||||
:param index: 从那个正则开始解析
|
||||
:return: 解析后的树形结果数据
|
||||
"""
|
||||
if len(self.content_level_pattern) == index:
|
||||
return
|
||||
level_content_list = parse_level(text, pattern=self.content_level_pattern[index])
|
||||
for i in range(len(level_content_list)):
|
||||
block = get_level_block(text, level_content_list, i)
|
||||
children = self.parse_to_tree(text=block.replace(level_content_list[i]['content'][:-1], ""),
|
||||
index=index + 1)
|
||||
if children is not None and len(children) > 0:
|
||||
level_content_list[i]['children'] = children
|
||||
else:
|
||||
if len(block) > 0:
|
||||
level_content_list[i]['children'] = [to_tree_obj(block, 'block')]
|
||||
if len(level_content_list) > 0:
|
||||
end_index = text.index(level_content_list[0].get('content'))
|
||||
if end_index == 0:
|
||||
return level_content_list
|
||||
other_content = text[0:end_index]
|
||||
if len(other_content.strip()) > 0:
|
||||
level_content_list.append(to_tree_obj(other_content, 'block'))
|
||||
return level_content_list
|
||||
|
||||
def parse(self, text: str):
|
||||
"""
|
||||
解析文本
|
||||
:param text: 文本数据
|
||||
:return: 解析后数据 {content:段落数据,keywords:[‘段落关键词’],parent_chain:['段落父级链路']}
|
||||
"""
|
||||
result_tree = self.parse_to_tree(text, 0)
|
||||
return flat_map(to_block_paragraph(result_tree))
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartdoc.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for apps project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartdoc.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
# !/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
"""
|
||||
配置分类:
|
||||
1. Django使用的配置文件,写到settings中
|
||||
2. 程序需要, 用户不需要更改的写到settings中
|
||||
3. 程序需要, 用户需要更改的写到本config中
|
||||
"""
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from importlib import import_module
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
import yaml
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
||||
logger = logging.getLogger('smartdoc.conf')
|
||||
|
||||
|
||||
def import_string(dotted_path):
|
||||
try:
|
||||
module_path, class_name = dotted_path.rsplit('.', 1)
|
||||
except ValueError as err:
|
||||
raise ImportError("%s doesn't look like a module path" % dotted_path) from err
|
||||
|
||||
module = import_module(module_path)
|
||||
|
||||
try:
|
||||
return getattr(module, class_name)
|
||||
except AttributeError as err:
|
||||
raise ImportError(
|
||||
'Module "%s" does not define a "%s" attribute/class' %
|
||||
(module_path, class_name)) from err
|
||||
|
||||
|
||||
def is_absolute_uri(uri):
|
||||
""" 判断一个uri是否是绝对地址 """
|
||||
if not isinstance(uri, str):
|
||||
return False
|
||||
|
||||
result = re.match(r'^http[s]?://.*', uri)
|
||||
if result is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def build_absolute_uri(base, uri):
|
||||
""" 构建绝对uri地址 """
|
||||
if uri is None:
|
||||
return base
|
||||
|
||||
if isinstance(uri, int):
|
||||
uri = str(uri)
|
||||
|
||||
if not isinstance(uri, str):
|
||||
return base
|
||||
|
||||
if is_absolute_uri(uri):
|
||||
return uri
|
||||
|
||||
parsed_base = urlparse(base)
|
||||
url = "{}://{}".format(parsed_base.scheme, parsed_base.netloc)
|
||||
path = '{}/{}/'.format(parsed_base.path.strip('/'), uri.strip('/'))
|
||||
return urljoin(url, path)
|
||||
|
||||
|
||||
class DoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Config(dict):
|
||||
defaults = {
|
||||
# 数据库相关配置
|
||||
"DB_HOST": "",
|
||||
"DB_PORT": "",
|
||||
"DB_USER": "",
|
||||
"DB_PASSWORD": "",
|
||||
"DB_ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||
# 邮件相关配置
|
||||
"EMAIL_ADDRESS": "",
|
||||
"EMAIL_USE_TLS": False,
|
||||
"EMAIL_USE_SSL": True,
|
||||
"EMAIL_HOST": "",
|
||||
"EMAIL_PORT": 465,
|
||||
"EMAIL_HOST_USER": "",
|
||||
"EMAIL_HOST_PASSWORD": ""
|
||||
}
|
||||
|
||||
def get_db_setting(self) -> dict:
|
||||
return {
|
||||
"NAME": self.get('DB_NAME'),
|
||||
"HOST": self.get('DB_HOST'),
|
||||
"PORT": self.get('DB_PORT'),
|
||||
"USER": self.get('DB_USER'),
|
||||
"PASSWORD": self.get('DB_PASSWORD'),
|
||||
"ENGINE": self.get('DB_ENGINE')
|
||||
}
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self))
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.get(item)
|
||||
|
||||
def __getattr__(self, item):
|
||||
return self.get(item)
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
config_class = Config
|
||||
|
||||
def __init__(self, root_path=None):
|
||||
self.root_path = root_path
|
||||
self.config = self.config_class()
|
||||
|
||||
def from_mapping(self, *mapping, **kwargs):
|
||||
"""Updates the config like :meth:`update` ignoring items with non-upper
|
||||
keys.
|
||||
|
||||
.. versionadded:: 0.11
|
||||
"""
|
||||
mappings = []
|
||||
if len(mapping) == 1:
|
||||
if hasattr(mapping[0], 'items'):
|
||||
mappings.append(mapping[0].items())
|
||||
else:
|
||||
mappings.append(mapping[0])
|
||||
elif len(mapping) > 1:
|
||||
raise TypeError(
|
||||
'expected at most 1 positional argument, got %d' % len(mapping)
|
||||
)
|
||||
mappings.append(kwargs.items())
|
||||
for mapping in mappings:
|
||||
for (key, value) in mapping:
|
||||
if key.isupper():
|
||||
self.config[key] = value
|
||||
return True
|
||||
|
||||
def from_yaml(self, filename, silent=False):
|
||||
if self.root_path:
|
||||
filename = os.path.join(self.root_path, filename)
|
||||
try:
|
||||
with open(filename, 'rt', encoding='utf8') as f:
|
||||
obj = yaml.safe_load(f)
|
||||
except IOError as e:
|
||||
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
|
||||
return False
|
||||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
|
||||
raise
|
||||
if obj:
|
||||
return self.from_mapping(obj)
|
||||
return True
|
||||
|
||||
def load_from_yml(self):
|
||||
for i in ['config_example.yml', 'config.yaml']:
|
||||
if not os.path.isfile(os.path.join(self.root_path, i)):
|
||||
continue
|
||||
loaded = self.from_yaml(i)
|
||||
if loaded:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def load_user_config(cls, root_path=None, config_class=None):
|
||||
config_class = config_class or Config
|
||||
cls.config_class = config_class
|
||||
if not root_path:
|
||||
root_path = PROJECT_DIR
|
||||
manager = cls(root_path=root_path)
|
||||
if manager.load_from_yml():
|
||||
config = manager.config
|
||||
else:
|
||||
msg = """
|
||||
|
||||
Error: No config file found.
|
||||
|
||||
You can run `cp config_example.yml config_example.yml`, and edit it.
|
||||
"""
|
||||
raise ImportError(msg)
|
||||
return config
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
|
||||
from .conf import ConfigManager
|
||||
|
||||
__all__ = ['BASE_DIR', 'PROJECT_DIR', 'VERSION', 'CONFIG']
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
||||
VERSION = '1.0.0'
|
||||
CONFIG = ConfigManager.load_user_config(root_path=os.path.abspath('/opt/smartdoc/conf'))
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: smart-doc
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2023/9/14 15:45
|
||||
@desc:
|
||||
"""
|
||||
from .base import *
|
||||
from .logging import *
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import datetime
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from ..const import CONFIG, PROJECT_DIR
|
||||
import mimetypes
|
||||
|
||||
mimetypes.add_type("text/css", ".css", True)
|
||||
mimetypes.add_type("text/javascript", ".js", True)
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-g1u*$)1ddn20_3orw^f+g4(i(2dacj^awe*2vh-$icgqwfnbq('
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
DATABASES = {
|
||||
'default': CONFIG.get_db_setting()
|
||||
}
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'users.apps.UsersConfig',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
"drf_yasg", # swagger 接口
|
||||
'django_filters', # 条件过滤
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
JWT_AUTH = {
|
||||
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=60 * 60 * 2) # <-- 设置token有效时间
|
||||
}
|
||||
|
||||
ROOT_URLCONF = 'smartdoc.urls'
|
||||
# FORCE_SCRIPT_NAME
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': ['apps/static/ui'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
'DEFAULT_AUTO_SCHEMA_CLASS': 'common.config.swagger_conf.CustomSwaggerAutoSchema',
|
||||
"DEFAULT_MODEL_RENDERING": "example",
|
||||
'USE_SESSION_AUTH': False,
|
||||
'SECURITY_DEFINITIONS': {
|
||||
'Bearer': {
|
||||
'type': 'apiKey',
|
||||
'name': 'AUTHORIZATION',
|
||||
'in': 'header',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 缓存配置
|
||||
CACHES = {
|
||||
"default": {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
},
|
||||
# 存储用户信息
|
||||
'user_cache': {
|
||||
'BACKEND': 'common.cache.file_cache.FileCache',
|
||||
'LOCATION': os.path.join(PROJECT_DIR, 'data', 'cache', "user_cache") # 文件夹路径
|
||||
},
|
||||
# 存储用户Token
|
||||
"token_cache": {
|
||||
'BACKEND': 'common.cache.file_cache.FileCache',
|
||||
'LOCATION': os.path.join(PROJECT_DIR, 'data', 'cache', "token_cache") # 文件夹路径
|
||||
}
|
||||
}
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'EXCEPTION_HANDLER': 'common.handle.handle_exception.handle_exception',
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': ['common.auth.authenticate.AnonymousAuthentication']
|
||||
|
||||
}
|
||||
STATICFILES_DIRS = [(os.path.join(PROJECT_DIR, 'ui', 'dist'))]
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR.parent, 'static')
|
||||
|
||||
WSGI_APPLICATION = 'smartdoc.wsgi.application'
|
||||
|
||||
# 邮件配置
|
||||
EMAIL_ADDRESS = CONFIG.get('EMAIL_ADDRESS')
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_USE_TLS = CONFIG.get('EMAIL_USE_TLS') # 是否使用TLS安全传输协议(用于在两个通信应用程序之间提供保密性和数据完整性。)
|
||||
EMAIL_USE_SSL = CONFIG.get('EMAIL_USE_SSL') # 是否使用SSL加密,qq企业邮箱要求使用
|
||||
EMAIL_HOST = CONFIG.get('EMAIL_HOST') # 发送邮件的邮箱 的 SMTP服务器,这里用了163邮箱
|
||||
EMAIL_PORT = CONFIG.get('EMAIL_PORT') # 发件箱的SMTP服务器端口
|
||||
EMAIL_HOST_USER = CONFIG.get('EMAIL_HOST_USER') # 发送邮件的邮箱地址
|
||||
EMAIL_HOST_PASSWORD = CONFIG.get('EMAIL_HOST_PASSWORD') # 发送邮件的邮箱密码(这里使用的是授权码)
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
|
||||
from ..const import PROJECT_DIR, CONFIG
|
||||
|
||||
LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'logs')
|
||||
QA_BOT_LOG_FILE = os.path.join(LOG_DIR, 'smart_doc.log')
|
||||
DRF_EXCEPTION_LOG_FILE = os.path.join(LOG_DIR, 'drf_exception.log')
|
||||
UNEXPECTED_EXCEPTION_LOG_FILE = os.path.join(LOG_DIR, 'unexpected_exception.log')
|
||||
ANSIBLE_LOG_FILE = os.path.join(LOG_DIR, 'ansible.log')
|
||||
GUNICORN_LOG_FILE = os.path.join(LOG_DIR, 'gunicorn.log')
|
||||
LOG_LEVEL = CONFIG.LOG_LEVEL
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
|
||||
},
|
||||
'main': {
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S',
|
||||
'format': '%(asctime)s [%(module)s %(levelname)s] %(message)s',
|
||||
},
|
||||
'exception': {
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S',
|
||||
'format': '\n%(asctime)s [%(levelname)s] %(message)s',
|
||||
},
|
||||
'simple': {
|
||||
'format': '%(levelname)s %(message)s'
|
||||
},
|
||||
'syslog': {
|
||||
'format': 'jumpserver: %(message)s'
|
||||
},
|
||||
'msg': {
|
||||
'format': '%(message)s'
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'null': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.NullHandler',
|
||||
},
|
||||
'console': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'main'
|
||||
},
|
||||
'file': {
|
||||
'encoding': 'utf8',
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'maxBytes': 1024 * 1024 * 100,
|
||||
'backupCount': 7,
|
||||
'formatter': 'main',
|
||||
'filename': QA_BOT_LOG_FILE,
|
||||
},
|
||||
'ansible_logs': {
|
||||
'encoding': 'utf8',
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'formatter': 'main',
|
||||
'maxBytes': 1024 * 1024 * 100,
|
||||
'backupCount': 7,
|
||||
'filename': ANSIBLE_LOG_FILE,
|
||||
},
|
||||
'drf_exception': {
|
||||
'encoding': 'utf8',
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'formatter': 'exception',
|
||||
'maxBytes': 1024 * 1024 * 100,
|
||||
'backupCount': 7,
|
||||
'filename': DRF_EXCEPTION_LOG_FILE,
|
||||
},
|
||||
'unexpected_exception': {
|
||||
'encoding': 'utf8',
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'formatter': 'exception',
|
||||
'maxBytes': 1024 * 1024 * 100,
|
||||
'backupCount': 7,
|
||||
'filename': UNEXPECTED_EXCEPTION_LOG_FILE,
|
||||
},
|
||||
'syslog': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.NullHandler',
|
||||
'formatter': 'syslog'
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['null'],
|
||||
'propagate': False,
|
||||
'level': LOG_LEVEL,
|
||||
},
|
||||
'django.request': {
|
||||
'handlers': ['console', 'file', 'syslog'],
|
||||
'level': LOG_LEVEL,
|
||||
'propagate': False,
|
||||
},
|
||||
'django.server': {
|
||||
'handlers': ['console', 'file', 'syslog'],
|
||||
'level': LOG_LEVEL,
|
||||
'propagate': False,
|
||||
},
|
||||
'jumpserver': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': LOG_LEVEL,
|
||||
},
|
||||
'drf_exception': {
|
||||
'handlers': ['console', 'drf_exception'],
|
||||
'level': LOG_LEVEL,
|
||||
},
|
||||
'unexpected_exception': {
|
||||
'handlers': ['unexpected_exception'],
|
||||
'level': LOG_LEVEL,
|
||||
},
|
||||
'ops.ansible_api': {
|
||||
'handlers': ['console', 'ansible_logs'],
|
||||
'level': LOG_LEVEL,
|
||||
},
|
||||
'django_auth_ldap': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': "INFO",
|
||||
},
|
||||
'syslog': {
|
||||
'handlers': ['syslog'],
|
||||
'level': 'INFO'
|
||||
},
|
||||
'azure': {
|
||||
'handlers': ['null'],
|
||||
'level': 'ERROR'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SYSLOG_ENABLE = CONFIG.SYSLOG_ENABLE
|
||||
|
||||
if not os.path.isdir(LOG_DIR):
|
||||
os.makedirs(LOG_DIR, mode=0o755)
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
"""
|
||||
URL configuration for apps project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
import os
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.urls import path, re_path, include
|
||||
from django.views import static
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
from rest_framework import permissions, status
|
||||
|
||||
from common.auth import AnonymousAuthentication
|
||||
from common.response.result import Result
|
||||
from smartdoc import settings
|
||||
from smartdoc.conf import PROJECT_DIR
|
||||
|
||||
schema_view = get_schema_view(
|
||||
|
||||
openapi.Info(
|
||||
title="Python API",
|
||||
default_version='v1',
|
||||
description="智能客服平台",
|
||||
),
|
||||
public=True,
|
||||
permission_classes=[permissions.AllowAny],
|
||||
authentication_classes=[AnonymousAuthentication]
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("api/", include("users.urls")),
|
||||
# 暴露静态主要是swagger资源
|
||||
re_path(r'^static/(?P<path>.*)$', static.serve, {'document_root': settings.STATIC_ROOT}, name='static'),
|
||||
# 暴露ui静态资源
|
||||
re_path(r'^ui/(?P<path>.*)$', static.serve, {'document_root': os.path.join(settings.STATIC_ROOT, "ui")}, name='ui'),
|
||||
]
|
||||
|
||||
urlpatterns += [
|
||||
re_path(r'^doc(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0),
|
||||
name='schema-json'), # 导出
|
||||
path('doc/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
||||
]
|
||||
|
||||
|
||||
def page_not_found(request, exception):
|
||||
if request.path.startswith("/api/"):
|
||||
return Result(response_status=status.HTTP_404_NOT_FOUND, code=404, message="找不到接口")
|
||||
else:
|
||||
file = open(os.path.join(PROJECT_DIR, 'apps', "static", 'ui', 'index.html'), "r", encoding='utf-8')
|
||||
content = file.read()
|
||||
file.close()
|
||||
return HttpResponse(content, status=200)
|
||||
|
||||
|
||||
handler404 = page_not_found
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for apps project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartdoc.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'users'
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: __init__.py
|
||||
@date:2023/9/4 10:08
|
||||
@desc:
|
||||
"""
|
||||
from .user import *
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: users.py
|
||||
@date:2023/9/4 10:09
|
||||
@desc:
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
from django.db import models
|
||||
|
||||
__all__ = ["User", "password_encrypt"]
|
||||
|
||||
|
||||
def password_encrypt(raw_password):
|
||||
"""
|
||||
密码 md5加密
|
||||
:param raw_password: 密码
|
||||
:return: 加密后密码
|
||||
"""
|
||||
md5 = hashlib.md5() # 2,实例化md5() 方法
|
||||
md5.update(raw_password.encode()) # 3,对字符串的字节类型加密
|
||||
result = md5.hexdigest() # 4,加密
|
||||
return result
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
email = models.EmailField(unique=True, verbose_name="邮箱")
|
||||
username = models.CharField(max_length=150, unique=True, verbose_name="用户名")
|
||||
password = models.CharField(max_length=150, verbose_name="密码")
|
||||
role = models.CharField(max_length=150, verbose_name="角色")
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
USERNAME_FIELD = 'username'
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
class Meta:
|
||||
db_table = "user"
|
||||
|
||||
def set_password(self, raw_password):
|
||||
self.password = password_encrypt(raw_password)
|
||||
self._password = raw_password
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: user_serializers.py
|
||||
@date:2023/9/5 16:32
|
||||
@desc:
|
||||
"""
|
||||
import datetime
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
|
||||
from django.core import validators, signing, cache
|
||||
from django.core.mail import send_mail
|
||||
from django.db.models import Q
|
||||
from drf_yasg import openapi
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.constants.exception_code_constants import ExceptionCodeConstants
|
||||
from common.constants.permission_constants import RoleConstants
|
||||
from common.exception.app_exception import AppApiException
|
||||
from common.mixins.api_mixin import ApiMixin
|
||||
from common.response.result import get_api_response
|
||||
from common.util.lock import lock
|
||||
from smartdoc.conf import PROJECT_DIR
|
||||
from smartdoc.settings import EMAIL_ADDRESS
|
||||
from users.models.user import User, password_encrypt
|
||||
|
||||
user_cache = cache.caches['user_cache']
|
||||
|
||||
|
||||
class LoginSerializer(ApiMixin, serializers.Serializer):
|
||||
username = serializers.CharField(required=True,
|
||||
validators=[
|
||||
validators.MaxLengthValidator(limit_value=20,
|
||||
message=ExceptionCodeConstants.USERNAME_ERROR.value.message),
|
||||
validators.MinLengthValidator(limit_value=6,
|
||||
message=ExceptionCodeConstants.USERNAME_ERROR.value.message)
|
||||
])
|
||||
|
||||
password = serializers.CharField(required=True)
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
"""
|
||||
校验参数
|
||||
:param raise_exception: 是否抛出异常 只能是True
|
||||
:return: 用户信息
|
||||
"""
|
||||
super().is_valid(raise_exception=True)
|
||||
username = self.data.get("username")
|
||||
password = password_encrypt(self.data.get("password"))
|
||||
user = self.Meta.model.objects.filter(Q(username=username,
|
||||
password=password) | Q(email=username,
|
||||
password=password)).first()
|
||||
if user is None:
|
||||
raise ExceptionCodeConstants.INCORRECT_USERNAME_AND_PASSWORD.value.to_app_api_exception()
|
||||
return user
|
||||
|
||||
def get_user_token(self):
|
||||
"""
|
||||
获取用户Token
|
||||
:return: 用户Token(认证信息)
|
||||
"""
|
||||
user = self.is_valid()
|
||||
token = signing.dumps({'username': user.username, 'id': user.id, 'email': user.email})
|
||||
return token
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
def get_request_body_api(self):
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['username', 'password'],
|
||||
properties={
|
||||
'username': openapi.Schema(type=openapi.TYPE_STRING, title="用户名", description="用户名"),
|
||||
'password': openapi.Schema(type=openapi.TYPE_STRING, title="密码", description="密码")
|
||||
}
|
||||
)
|
||||
|
||||
def get_response_body_api(self):
|
||||
return get_api_response(openapi.Schema(
|
||||
type=openapi.TYPE_STRING,
|
||||
title="token",
|
||||
default="xxxx",
|
||||
description="认证token"
|
||||
), "token value")
|
||||
|
||||
|
||||
class RegisterSerializer(ApiMixin, serializers.Serializer):
|
||||
"""
|
||||
注册请求对象
|
||||
"""
|
||||
email = serializers.EmailField(
|
||||
validators=[validators.EmailValidator(message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message,
|
||||
code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code)])
|
||||
|
||||
username = serializers.CharField(required=True,
|
||||
validators=[
|
||||
validators.MaxLengthValidator(limit_value=20,
|
||||
message=ExceptionCodeConstants.USERNAME_ERROR.value.message),
|
||||
validators.MinLengthValidator(limit_value=6,
|
||||
message=ExceptionCodeConstants.USERNAME_ERROR.value.message)
|
||||
])
|
||||
password = serializers.CharField(required=True)
|
||||
|
||||
re_password = serializers.CharField(required=True)
|
||||
|
||||
code = serializers.CharField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
@lock(lock_key=lambda this, raise_exception: (
|
||||
this.initial_data.get("email") + ":register"
|
||||
|
||||
))
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
if self.data.get('password') != self.data.get('re_password'):
|
||||
raise ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.to_app_api_exception()
|
||||
username = self.data.get("username")
|
||||
email = self.data.get("email")
|
||||
code = self.data.get("code")
|
||||
code_cache_key = email + ":register"
|
||||
cache_code = user_cache.get(code_cache_key)
|
||||
if code != cache_code:
|
||||
raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception()
|
||||
u = User.objects.filter(Q(username=username) | Q(email=email)).first()
|
||||
if u is not None:
|
||||
if u.email == email:
|
||||
raise ExceptionCodeConstants.EMAIL_IS_EXIST.value.to_app_api_exception()
|
||||
if u.username == username:
|
||||
raise ExceptionCodeConstants.USERNAME_IS_EXIST.value.to_app_api_exception()
|
||||
|
||||
return True
|
||||
|
||||
def save(self, **kwargs):
|
||||
m = User(
|
||||
**{'email': self.data.get("email"), 'username': self.data.get("username"),
|
||||
'role': RoleConstants.USER.name})
|
||||
m.set_password(self.data.get("password"))
|
||||
# 插入用户
|
||||
m.save()
|
||||
email = self.data.get("email")
|
||||
code_cache_key = email + ":register"
|
||||
# 删除验证码缓存
|
||||
user_cache.delete(code_cache_key)
|
||||
|
||||
def get_request_body_api(self):
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['username', 'email', 'password', 're_password', 'code'],
|
||||
properties={
|
||||
'username': openapi.Schema(type=openapi.TYPE_STRING, title="用户名", description="用户名"),
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING, title="邮箱", description="邮箱地址"),
|
||||
'password': openapi.Schema(type=openapi.TYPE_STRING, title="密码", description="密码"),
|
||||
're_password': openapi.Schema(type=openapi.TYPE_STRING, title="确认密码", description="确认密码"),
|
||||
'code': openapi.Schema(type=openapi.TYPE_STRING, title="验证码", description="验证码")
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class CheckCodeSerializer(ApiMixin, serializers.Serializer):
|
||||
"""
|
||||
校验验证码
|
||||
"""
|
||||
email = serializers.EmailField(
|
||||
validators=[validators.EmailValidator(message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message,
|
||||
code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code)])
|
||||
code = serializers.CharField(required=True)
|
||||
|
||||
type = serializers.CharField(required=True, validators=[
|
||||
validators.RegexValidator(regex=re.compile("^register|reset_password$"),
|
||||
message="只支持register|reset_password", code=500)
|
||||
])
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid()
|
||||
value = user_cache.get(self.data.get("email") + ":" + self.data.get("type"))
|
||||
if value is None or value != self.data.get("code"):
|
||||
raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception()
|
||||
return True
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
def get_request_body_api(self):
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['email', 'code', 'type'],
|
||||
properties={
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING, title="邮箱", description="邮箱地址"),
|
||||
'code': openapi.Schema(type=openapi.TYPE_STRING, title="验证码", description="验证码"),
|
||||
'type': openapi.Schema(type=openapi.TYPE_STRING, title="类型", description="register|reset_password")
|
||||
}
|
||||
)
|
||||
|
||||
def get_response_body_api(self):
|
||||
return get_api_response(openapi.Schema(
|
||||
type=openapi.TYPE_BOOLEAN,
|
||||
title="是否成功",
|
||||
default=True,
|
||||
description="错误提示"), True)
|
||||
|
||||
|
||||
class RePasswordSerializer(ApiMixin, serializers.Serializer):
|
||||
email = serializers.EmailField(
|
||||
validators=[validators.EmailValidator(message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message,
|
||||
code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code)])
|
||||
|
||||
code = serializers.CharField(required=True)
|
||||
|
||||
password = serializers.CharField(required=True)
|
||||
|
||||
re_password = serializers.CharField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=True)
|
||||
email = self.data.get("email")
|
||||
cache_code = user_cache.get(email + ':reset_password')
|
||||
if self.data.get('password') != self.data.get('re_password'):
|
||||
raise AppApiException(ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.code,
|
||||
ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.message)
|
||||
if cache_code != self.data.get('code'):
|
||||
raise AppApiException(ExceptionCodeConstants.CODE_ERROR.value.code,
|
||||
ExceptionCodeConstants.CODE_ERROR.value.message)
|
||||
return True
|
||||
|
||||
def reset_password(self):
|
||||
"""
|
||||
修改密码
|
||||
:return: 是否成功
|
||||
"""
|
||||
if self.is_valid():
|
||||
email = self.data.get("email")
|
||||
self.Meta.model.objects.filter(email=email).update(
|
||||
password=password_encrypt(self.data.get('password')))
|
||||
code_cache_key = email + ":reset_password"
|
||||
# 删除验证码缓存
|
||||
user_cache.delete(code_cache_key)
|
||||
return True
|
||||
|
||||
def get_request_body_api(self):
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['email', 'code', "password", 're_password'],
|
||||
properties={
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING, title="邮箱", description="邮箱地址"),
|
||||
'code': openapi.Schema(type=openapi.TYPE_STRING, title="验证码", description="验证码"),
|
||||
'password': openapi.Schema(type=openapi.TYPE_STRING, title="密码", description="密码"),
|
||||
're_password': openapi.Schema(type=openapi.TYPE_STRING, title="确认密码", description="确认密码")
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SendEmailSerializer(ApiMixin, serializers.Serializer):
|
||||
email = serializers.EmailField(
|
||||
validators=[validators.EmailValidator(message=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.message,
|
||||
code=ExceptionCodeConstants.EMAIL_FORMAT_ERROR.value.code)])
|
||||
|
||||
type = serializers.CharField(required=True, validators=[
|
||||
validators.RegexValidator(regex=re.compile("^register|reset_password$"),
|
||||
message="只支持register|reset_password", code=500)
|
||||
])
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
def is_valid(self, *, raise_exception=False):
|
||||
super().is_valid(raise_exception=raise_exception)
|
||||
user_exists = self.Meta.model.objects.filter(email=self.data.get('email')).exists()
|
||||
if not user_exists and self.data.get('type') == 'reset_password':
|
||||
raise ExceptionCodeConstants.EMAIL_IS_NOT_EXIST.value.to_app_api_exception()
|
||||
elif user_exists and self.data.get('type') == 'register':
|
||||
raise ExceptionCodeConstants.EMAIL_IS_EXIST.value.to_app_api_exception()
|
||||
code_cache_key = self.data.get('email') + ":" + self.data.get("type")
|
||||
code_cache_key_lock = code_cache_key + "_lock"
|
||||
ttl = user_cache.ttl(code_cache_key_lock)
|
||||
if ttl is not None:
|
||||
raise AppApiException(500, f"{ttl.total_seconds()}秒内请勿重复发送邮件")
|
||||
return True
|
||||
|
||||
def send(self):
|
||||
"""
|
||||
发送邮件
|
||||
:return: 是否发送成功
|
||||
:exception 发送失败异常
|
||||
"""
|
||||
email = self.data.get("email")
|
||||
state = self.data.get("type")
|
||||
# 生成随机验证码
|
||||
code = "".join(list(map(lambda i: random.choice(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'
|
||||
]), range(6))))
|
||||
# 获取邮件模板
|
||||
file = open(os.path.join(PROJECT_DIR, "apps", "common", 'template', 'email_template.html'), "r",
|
||||
encoding='utf-8')
|
||||
content = file.read()
|
||||
file.close()
|
||||
code_cache_key = email + ":" + state
|
||||
code_cache_key_lock = code_cache_key + "_lock"
|
||||
# 设置缓存
|
||||
user_cache.set(code_cache_key_lock, code, timeout=datetime.timedelta(minutes=1))
|
||||
try:
|
||||
# 发送邮件
|
||||
send_mail(f'【智能客服{"用户注册" if state == "register" else "修改密码"}】',
|
||||
'',
|
||||
html_message=f'{content.replace("${code}", code)}',
|
||||
from_email=EMAIL_ADDRESS,
|
||||
recipient_list=[email], fail_silently=False)
|
||||
except Exception as e:
|
||||
user_cache.delete(code_cache_key_lock)
|
||||
raise AppApiException(500, f"{str(e)}邮件发送失败")
|
||||
user_cache.set(code_cache_key, code, timeout=datetime.timedelta(minutes=30))
|
||||
return True
|
||||
|
||||
def get_request_body_api(self):
|
||||
return openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['email', 'type'],
|
||||
properties={
|
||||
'email': openapi.Schema(type=openapi.TYPE_STRING, title="邮箱", description="邮箱地址"),
|
||||
'type': openapi.Schema(type=openapi.TYPE_STRING, title="类型", description="register|reset_password")
|
||||
}
|
||||
)
|
||||
|
||||
def get_response_body_api(self):
|
||||
return get_api_response(openapi.Schema(type=openapi.TYPE_STRING, default=True), True)
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["email", "id",
|
||||
"username", ]
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "user"
|
||||
urlpatterns = [
|
||||
path('user', views.User.as_view(), name="profile"),
|
||||
path('user/login', views.Login.as_view(), name='login'),
|
||||
path('user/logout', views.Logout.as_view(), name='logout'),
|
||||
path('user/register', views.Register.as_view(), name="register"),
|
||||
path("user/send_email", views.SendEmail.as_view(), name='send_email'),
|
||||
path("user/check_code", views.CheckCode.as_view(), name='check_code'),
|
||||
path("user/re_password", views.RePasswordView.as_view(), name='re_password'),
|
||||
path("user/current/send_email", views.SendEmailToCurrentUserView.as_view(), name="send_email_current"),
|
||||
path("user/current/reset_password", views.ResetCurrentUserPasswordView.as_view(), name="reset_password_current")
|
||||
]
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: smart-doc
|
||||
@Author:虎
|
||||
@file: __init__.py.py
|
||||
@date:2023/9/14 19:01
|
||||
@desc:
|
||||
"""
|
||||
from .user import *
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: qabot
|
||||
@Author:虎
|
||||
@file: user.py
|
||||
@date:2023/9/4 10:57
|
||||
@desc:
|
||||
"""
|
||||
from django.core import cache
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.decorators import permission_classes
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.views import Request
|
||||
|
||||
from common.auth.authenticate import TokenAuth
|
||||
from common.auth.authentication import has_permissions
|
||||
from common.constants.permission_constants import PermissionConstants, CompareConstants
|
||||
from common.response import result
|
||||
from smartdoc.settings import JWT_AUTH
|
||||
from users.models.user import User as UserModel
|
||||
from users.serializers.user_serializers import RegisterSerializer, LoginSerializer, UserSerializer, CheckCodeSerializer, \
|
||||
RePasswordSerializer, \
|
||||
SendEmailSerializer
|
||||
|
||||
user_cache = cache.caches['user_cache']
|
||||
token_cache = cache.caches['token_cache']
|
||||
|
||||
|
||||
class User(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['GET'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="获取当前用户信息",
|
||||
operation_id="获取当前用户信息")
|
||||
@has_permissions(PermissionConstants.USER_READ, compare=CompareConstants.AND)
|
||||
def get(self, request: Request):
|
||||
return result.success(UserSerializer(instance=UserModel.objects.get(id=request.user.id)).data)
|
||||
|
||||
|
||||
class ResetCurrentUserPasswordView(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="修改当前用户密码",
|
||||
operation_id="修改当前用户密码",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['email', 'code', "password", 're_password'],
|
||||
properties={
|
||||
'code': openapi.Schema(type=openapi.TYPE_STRING, title="验证码", description="验证码"),
|
||||
'password': openapi.Schema(type=openapi.TYPE_STRING, title="密码", description="密码"),
|
||||
're_password': openapi.Schema(type=openapi.TYPE_STRING, title="密码",
|
||||
description="密码")
|
||||
}
|
||||
),
|
||||
responses=RePasswordSerializer().get_response_body_api())
|
||||
def post(self, request: Request):
|
||||
data = {'email': request.user.email}
|
||||
data.update(request.data)
|
||||
serializer_obj = RePasswordSerializer(data=data)
|
||||
if serializer_obj.reset_password():
|
||||
token_cache.delete(request.META.get('HTTP_AUTHORIZATION', None
|
||||
))
|
||||
return result.success(True)
|
||||
return result.error("修改密码失败")
|
||||
|
||||
|
||||
class SendEmailToCurrentUserView(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@permission_classes((AllowAny,))
|
||||
@swagger_auto_schema(operation_summary="发送邮件到当前用户",
|
||||
operation_id="发送邮件到当前用户",
|
||||
responses=SendEmailSerializer().get_response_body_api())
|
||||
def post(self, request: Request):
|
||||
serializer_obj = SendEmailSerializer(data={'email': request.user.email, 'type': "reset_password"})
|
||||
if serializer_obj.is_valid(raise_exception=True):
|
||||
return result.success(serializer_obj.send())
|
||||
|
||||
|
||||
class Logout(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@permission_classes((AllowAny,))
|
||||
@swagger_auto_schema(operation_summary="登出",
|
||||
operation_id="登出",
|
||||
responses=SendEmailSerializer().get_response_body_api())
|
||||
def post(self, request: Request):
|
||||
token_cache.delete(request.META.get('HTTP_AUTHORIZATION', None
|
||||
))
|
||||
return result.success(True)
|
||||
|
||||
|
||||
class Login(APIView):
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="登录",
|
||||
operation_id="登录",
|
||||
request_body=LoginSerializer().get_request_body_api(),
|
||||
responses=LoginSerializer().get_response_body_api())
|
||||
def post(self, request: Request):
|
||||
login_request = LoginSerializer(data=request.data)
|
||||
# 校验请求参数
|
||||
user = login_request.is_valid(raise_exception=True)
|
||||
token = login_request.get_user_token()
|
||||
token_cache.set(token, user, timeout=JWT_AUTH['JWT_EXPIRATION_DELTA'])
|
||||
return result.success(token)
|
||||
|
||||
|
||||
class Register(APIView):
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@permission_classes((AllowAny,))
|
||||
@swagger_auto_schema(operation_summary="用户注册",
|
||||
operation_id="用户注册",
|
||||
request_body=RegisterSerializer().get_request_body_api(),
|
||||
responses=RegisterSerializer().get_response_body_api())
|
||||
def post(self, request: Request):
|
||||
serializer_obj = RegisterSerializer(data=request.data)
|
||||
if serializer_obj.is_valid(raise_exception=True):
|
||||
serializer_obj.save()
|
||||
return result.success("注册成功")
|
||||
|
||||
|
||||
class RePasswordView(APIView):
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@permission_classes((AllowAny,))
|
||||
@swagger_auto_schema(operation_summary="修改密码",
|
||||
operation_id="修改密码",
|
||||
request_body=RePasswordSerializer().get_request_body_api(),
|
||||
responses=RePasswordSerializer().get_response_body_api())
|
||||
def post(self, request: Request):
|
||||
serializer_obj = RePasswordSerializer(data=request.data)
|
||||
return result.success(serializer_obj.reset_password())
|
||||
|
||||
|
||||
class CheckCode(APIView):
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@permission_classes((AllowAny,))
|
||||
@swagger_auto_schema(operation_summary="校验验证码是否正确",
|
||||
operation_id="校验验证码是否正确",
|
||||
request_body=CheckCodeSerializer().get_request_body_api(),
|
||||
responses=CheckCodeSerializer().get_response_body_api())
|
||||
def post(self, request: Request):
|
||||
return result.success(CheckCodeSerializer(data=request.data).is_valid(raise_exception=True))
|
||||
|
||||
|
||||
class SendEmail(APIView):
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@swagger_auto_schema(operation_summary="发送邮件",
|
||||
operation_id="发送邮件",
|
||||
request_body=SendEmailSerializer().get_request_body_api(),
|
||||
responses=SendEmailSerializer().get_response_body_api())
|
||||
def post(self, request: Request):
|
||||
serializer_obj = SendEmailSerializer(data=request.data)
|
||||
if serializer_obj.is_valid(raise_exception=True):
|
||||
return result.success(serializer_obj.send())
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# 数据库配置
|
||||
EMAIL_ADDRESS:
|
||||
EMAIL_USE_TLS: False
|
||||
EMAIL_USE_SSL: True
|
||||
EMAIL_HOST: smtp.qq.com
|
||||
EMAIL_PORT: 465
|
||||
EMAIL_HOST_USER:
|
||||
EMAIL_HOST_PASSWORD:
|
||||
|
||||
# 数据库链接信息
|
||||
DB_NAME: smart-doc
|
||||
DB_HOST: localhost
|
||||
DB_PORT: 5432
|
||||
DB_USER: root
|
||||
DB_PASSWORD: xxx
|
||||
DB_ENGINE: django.db.backends.postgresql_psycopg2
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import django
|
||||
from django.core import management
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
APP_DIR = os.path.join(BASE_DIR, 'apps')
|
||||
|
||||
os.chdir(BASE_DIR)
|
||||
sys.path.insert(0, APP_DIR)
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "smartdoc.settings")
|
||||
django.setup()
|
||||
|
||||
|
||||
def collect_static():
|
||||
"""
|
||||
收集静态文件到指定目录
|
||||
本项目主要是将前段vue/dist的前段项目放到静态目录下面
|
||||
:return:
|
||||
"""
|
||||
logging.info("Collect static files")
|
||||
try:
|
||||
management.call_command('collectstatic', '--no-input', '-c', verbosity=0, interactive=False)
|
||||
logging.info("Collect static files done")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def perform_db_migrate():
|
||||
"""
|
||||
初始化数据库表
|
||||
"""
|
||||
logging.info("Check database structure change ...")
|
||||
logging.info("Migrate model change to database ...")
|
||||
try:
|
||||
management.call_command('migrate')
|
||||
except Exception as e:
|
||||
logging.error('Perform migrate failed, exit', exc_info=True)
|
||||
sys.exit(11)
|
||||
|
||||
|
||||
def start_services():
|
||||
management.call_command('runserver')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description="""
|
||||
qabot service control tools;
|
||||
|
||||
Example: \r\n
|
||||
|
||||
%(prog)s start all -d;
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
'action', type=str,
|
||||
choices=("start", "upgrade_db", "collect_static"),
|
||||
help="Action to run"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
action = args.action
|
||||
if action == "upgrade_db":
|
||||
perform_db_migrate()
|
||||
elif action == "collect_static":
|
||||
collect_static()
|
||||
else:
|
||||
start_services()
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,23 @@
|
|||
[tool.poetry]
|
||||
name = "smart-doc"
|
||||
version = "0.1.0"
|
||||
description = "智能客服系统"
|
||||
authors = ["shaohuzhang1 <shaohu.zhang@fit2cloud.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
django = "4.1.10"
|
||||
djangorestframework = "3.14.0"
|
||||
drf-yasg = "1.21.7"
|
||||
django-filter = "23.2"
|
||||
elasticsearch = "8.9.0"
|
||||
langchain = "0.0.274"
|
||||
psycopg2 = "2.9.7"
|
||||
jieba = "^0.42.1"
|
||||
diskcache = "^5.6.3"
|
||||
pillow = "9.5.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
},
|
||||
rules: {
|
||||
// 添加组件命名忽略规则
|
||||
"vue/multi-word-component-names": ["error",{
|
||||
"ignores": ["index","main"]//需要忽略的组件名
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# web
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
```sh
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
/// <reference types="vite/client" />
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"element-plus": "^2.3.7",
|
||||
"lodash": "^4.17.21",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.6",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@tsconfig/node18": "^18.2.0",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/node": "^18.17.5",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@vitejs/plugin-vue": "^4.3.1",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/test-utils": "^2.4.1",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-plugin-vue": "^9.16.1",
|
||||
"jsdom": "^22.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.0.0",
|
||||
"sass": "^1.66.1",
|
||||
"typescript": "~5.1.6",
|
||||
"vite": "^4.4.9",
|
||||
"vitest": "^0.34.2",
|
||||
"vue-tsc": "^1.8.8"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -0,0 +1,69 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
header {
|
||||
line-height: 1.5;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-block;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
nav a:first-of-type {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: left;
|
||||
margin-left: -1rem;
|
||||
font-size: 1rem;
|
||||
|
||||
padding: 1rem 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import { Result } from './../../request/Result'
|
||||
import { get, post } from '@/request/index'
|
||||
import type {
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
CheckCodeRequest,
|
||||
ResetPasswordRequest,
|
||||
User,
|
||||
ResetCurrentUserPasswordRequest
|
||||
} from './type'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param request 登录接口请求表单
|
||||
* @param loading 接口加载器
|
||||
* @returns 认证数据
|
||||
*/
|
||||
const login: (request: LoginRequest, loading?: Ref<boolean>) => Promise<Result<string>> = (
|
||||
request,
|
||||
loading
|
||||
) => {
|
||||
return post('/user/login', undefined, request, loading)
|
||||
}
|
||||
/**
|
||||
* 登出
|
||||
* @param loading 接口加载器
|
||||
* @returns
|
||||
*/
|
||||
const logout: (loading?: Ref<boolean>) => Promise<Result<boolean>> = (loading) => {
|
||||
return post('/user/logout', undefined, undefined, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册用户
|
||||
* @param request 注册请求对象
|
||||
* @param loading 接口加载器
|
||||
* @returns
|
||||
*/
|
||||
const register: (request: RegisterRequest, loading?: Ref<boolean>) => Promise<Result<string>> = (
|
||||
request,
|
||||
loading
|
||||
) => {
|
||||
return post('/user/register', undefined, request, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
* @param request 请求对象
|
||||
* @param loading 接口加载器
|
||||
* @returns
|
||||
*/
|
||||
const checkCode: (request: CheckCodeRequest, loading?: Ref<boolean>) => Promise<Result<boolean>> = (
|
||||
request,
|
||||
loading
|
||||
) => {
|
||||
return post('/user/check_code', undefined, request, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮件
|
||||
* @param email 邮件地址
|
||||
* @param loading 接口加载器
|
||||
* @returns
|
||||
*/
|
||||
const sendEmit: (
|
||||
email: string,
|
||||
type: 'register' | 'reset_password',
|
||||
loading?: Ref<boolean>
|
||||
) => Promise<Result<boolean>> = (email, type, loading) => {
|
||||
return post('/user/send_email', undefined, { email, type }, loading)
|
||||
}
|
||||
/**
|
||||
* 发送邮件到当前用户
|
||||
* @param loading 发送验证码到当前用户
|
||||
* @returns
|
||||
*/
|
||||
const sendEmailToCurrent: (loading?: Ref<boolean>) => Promise<Result<boolean>> = (loading) => {
|
||||
return post('/user/current/send_email', undefined, undefined, loading)
|
||||
}
|
||||
/**
|
||||
* 修改当前用户密码
|
||||
* @param request 请求对象
|
||||
* @param loading 加载器
|
||||
* @returns
|
||||
*/
|
||||
const resetCurrentUserPassword: (
|
||||
request: ResetCurrentUserPasswordRequest,
|
||||
loading?: Ref<boolean>
|
||||
) => Promise<Result<boolean>> = (request, loading) => {
|
||||
return post('/user/current/reset_password', undefined, request, loading)
|
||||
}
|
||||
/**
|
||||
* 获取用户基本信息
|
||||
* @param loading 接口加载器
|
||||
* @returns 用户基本信息
|
||||
*/
|
||||
const profile: (loading?: Ref<boolean>) => Promise<Result<User>> = (loading) => {
|
||||
return get('/user', undefined, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
* @param request 重置密码请求参数
|
||||
* @param loading 接口加载器
|
||||
* @returns
|
||||
*/
|
||||
const resetPassword: (
|
||||
request: ResetPasswordRequest,
|
||||
loading?: Ref<boolean>
|
||||
) => Promise<Result<boolean>> = (request, loading) => {
|
||||
return post('/user/re_password', undefined, request, loading)
|
||||
}
|
||||
|
||||
export default {
|
||||
login,
|
||||
register,
|
||||
sendEmit,
|
||||
checkCode,
|
||||
profile,
|
||||
resetPassword,
|
||||
sendEmailToCurrent,
|
||||
resetCurrentUserPassword,
|
||||
logout
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
interface User {
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
username: string
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
email: string
|
||||
}
|
||||
|
||||
interface LoginRequest {
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
username: string
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
password: string
|
||||
}
|
||||
|
||||
interface RegisterRequest {
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
username: string
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
password: string
|
||||
/**
|
||||
* 确定密码
|
||||
*/
|
||||
re_password: string
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
email: string
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
code: string
|
||||
}
|
||||
|
||||
interface CheckCodeRequest {
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
email: string
|
||||
/**
|
||||
*验证码
|
||||
*/
|
||||
code: string
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type: 'register' | 'reset_password'
|
||||
}
|
||||
|
||||
interface ResetCurrentUserPasswordRequest {
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
code: string
|
||||
/**
|
||||
*密码
|
||||
*/
|
||||
password: string
|
||||
/**
|
||||
* 确认密码
|
||||
*/
|
||||
re_password: string
|
||||
}
|
||||
|
||||
interface ResetPasswordRequest {
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
email: string
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
code: string
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
password: string
|
||||
/**
|
||||
* 确认密码
|
||||
*/
|
||||
re_password: string
|
||||
}
|
||||
|
||||
export type {
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
CheckCodeRequest,
|
||||
ResetPasswordRequest,
|
||||
User,
|
||||
ResetCurrentUserPasswordRequest
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 359 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 344 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<component :is="Object.keys(iconMap).includes(iconName) ? iconMap[iconName].iconReader() : iconMap['404'].iconReader()">
|
||||
</component>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { iconMap } from "@/components/icons/index"
|
||||
withDefaults(defineProps<{
|
||||
iconName?: string;
|
||||
}>(), {
|
||||
iconName: '404'
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import { h } from 'vue'
|
||||
export const iconMap: any = {
|
||||
'404': {
|
||||
iconReader: () => {
|
||||
return h('el-icon', { style: 'display:flex' }, [
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
viewBox: '0 0 1024 1024',
|
||||
version: '1.1',
|
||||
style: 'height:14px;width:14px',
|
||||
xmlns: 'http://www.w3.org/2000/svg'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M260.266667 789.333333c-21.333333 0-38.4-17.066667-38.4-38.4v-59.733333H38.4c-12.8 0-29.866667-8.533333-34.133333-21.333333-4.266667-17.066667-4.266667-29.866667 4.266666-42.666667l221.866667-294.4c8.533333-12.8 25.6-17.066667 42.666667-12.8 17.066667 4.266667 25.6 21.333333 25.6 38.4v256h34.133333c21.333333 0 38.4 17.066667 38.4 38.4s-17.066667 38.4-38.4 38.4H298.666667v59.733333c0 21.333333-17.066667 38.4-38.4 38.4z m-145.066667-179.2h106.666667V469.333333l-106.666667 140.8zM913.066667 742.4c-21.333333 0-38.4-17.066667-38.4-38.4v-59.733333h-183.466667c-12.8 0-29.866667-8.533333-34.133333-21.333334-8.533333-12.8-4.266667-29.866667 4.266666-38.4l221.866667-294.4c8.533333-12.8 25.6-17.066667 42.666667-12.8 17.066667 4.266667 25.6 21.333333 25.6 38.4v256h34.133333c21.333333 0 38.4 17.066667 38.4 38.4s-17.066667 38.4-38.4 38.4h-34.133333v59.733334c0 17.066667-17.066667 34.133333-38.4 34.133333zM768 567.466667h106.666667V426.666667L768 567.466667zM533.333333 597.333333c-46.933333 0-85.333333-25.6-119.466666-68.266666-29.866667-38.4-42.666667-93.866667-42.666667-145.066667 0-55.466667 17.066667-106.666667 42.666667-145.066667 29.866667-42.666667 72.533333-68.266667 119.466666-68.266666 46.933333 0 85.333333 25.6 119.466667 68.266666 29.866667 38.4 42.666667 93.866667 42.666667 145.066667 0 55.466667-17.066667 106.666667-42.666667 145.066667-34.133333 46.933333-76.8 68.266667-119.466667 68.266666z m0-362.666666c-55.466667 0-98.133333 68.266667-98.133333 149.333333s46.933333 149.333333 98.133333 149.333333c55.466667 0 98.133333-68.266667 98.133334-149.333333s-46.933333-149.333333-98.133334-149.333333z',
|
||||
fill: '#978CFF'
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M354.133333 691.2a162.133333 21.333333 0 1 0 324.266667 0 162.133333 21.333333 0 1 0-324.266667 0Z',
|
||||
fill: '#E3E5FC'
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M8.533333 832a162.133333 21.333333 0 1 0 324.266667 0 162.133333 21.333333 0 1 0-324.266667 0Z',
|
||||
fill: '#E3E5FC'
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M661.333333 797.866667a162.133333 21.333333 0 1 0 324.266667 0 162.133333 21.333333 0 1 0-324.266667 0Z',
|
||||
fill: '#E3E5FC'
|
||||
})
|
||||
]
|
||||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
home: {
|
||||
iconReader: () => {
|
||||
return h('el-icon', { style: 'display:flex' }, [
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
viewBox: '0 0 1024 1024',
|
||||
version: '1.1',
|
||||
style: 'height:14px;width:14px',
|
||||
xmlns: 'http://www.w3.org/2000/svg'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M362.666667 895.914667V639.850667c0-36.266667 33.109333-63.850667 72.533333-63.850667h153.6c39.253333 0 72.533333 27.648 72.533333 63.850667v256.064h59.904c61.269333 0 110.762667-47.957333 110.762667-106.730667V414.165333L557.162667 139.328a63.808 63.808 0 0 0-90.325334 0L192 414.165333v375.018667c0 58.88 49.386667 106.730667 110.762667 106.730667H362.666667z m42.666666 0h213.333334V639.850667c0-10.709333-12.586667-21.184-29.866667-21.184h-153.6c-17.408 0-29.866667 10.389333-29.866667 21.184v256.064z m469.333334-439.082667v332.352c0 82.645333-68.885333 149.397333-153.429334 149.397333H302.762667C218.133333 938.581333 149.333333 871.936 149.333333 789.184V456.832l-27.584 27.584a21.333333 21.333333 0 1 1-30.165333-30.165333L436.672 109.162667a106.474667 106.474667 0 0 1 150.656 0l345.088 345.088a21.333333 21.333333 0 0 1-30.165333 30.165333L874.666667 456.832z',
|
||||
fill: '#666666'
|
||||
})
|
||||
]
|
||||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
app: {
|
||||
iconReader: () => {
|
||||
return h('el-icon', { style: 'display:flex' }, [
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
viewBox: '0 0 1024 1024',
|
||||
version: '1.1',
|
||||
style: 'height:14px;width:14px',
|
||||
xmlns: 'http://www.w3.org/2000/svg'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M906.62890625 212.8203125C906.62890625 161.58007812 865.05664063 120.0078125 813.81640625 120.0078125H645.2421875C594.00195313 120.0078125 552.4296875 161.58007812 552.4296875 212.8203125v168.57421875c0 51.24023438 41.57226563 92.8125 92.8125 92.8125h168.57421875c51.24023438 0 92.8125-41.57226563 92.8125-92.8125V212.8203125z m-56.25 173.93554688c0 17.05078125-28.125 31.20117188-45.26367188 31.20117187H640.3203125c-17.05078125 0-30.76171875-14.0625-30.76171875-31.20117188V207.81054687c0-17.05078125 13.7109375-30.67382813 30.76171875-30.67382812h178.9453125c17.05078125 0 31.02539063 13.62304688 31.02539063 30.67382813v178.94531249z m56.25 251.45507812c0-51.24023438-41.57226563-92.8125-92.8125-92.8125H645.2421875C594.00195313 545.3984375 552.4296875 586.97070313 552.4296875 638.2109375v168.57421875c0 51.24023438 41.57226563 92.8125 92.8125 92.8125h168.57421875c51.24023438 0 92.8125-41.57226563 92.8125-92.8125V638.2109375z m-56.25 173.3203125c0 17.05078125-13.88671875 30.9375-30.9375 30.9375H640.49609375c-17.05078125 0-30.9375-13.88671875-30.9375-30.9375V632.5859375c0-17.05078125 13.88671875-30.9375 30.9375-30.9375h178.9453125c17.05078125 0 30.9375 13.88671875 30.9375 30.9375v178.9453125zM468.0546875 638.2109375c0-51.24023438-41.57226563-92.8125-92.8125-92.8125H206.66796875C155.42773437 545.3984375 113.85546875 586.97070313 113.85546875 638.2109375v168.57421875C113.85546875 858.02539063 155.42773437 899.59765625 206.66796875 899.59765625h168.57421875c51.24023438 0 92.8125-41.57226563 92.8125-92.8125V638.2109375z m-57.12890625 173.3203125c0 17.05078125-13.88671875 30.9375-30.9375 30.9375H201.04296875c-17.05078125 0-30.9375-13.88671875-30.9375-30.9375V632.5859375c0-17.05078125 13.88671875-30.9375 30.9375-30.9375h178.9453125c17.05078125 0 30.9375 13.88671875 30.9375 30.9375v178.9453125z m57.12890625-598.7109375C468.0546875 161.58007812 426.48242187 120.0078125 375.2421875 120.0078125H206.66796875C155.42773437 120.0078125 113.85546875 161.58007812 113.85546875 212.8203125v168.57421875C113.85546875 432.63476562 155.42773437 474.20703125 206.66796875 474.20703125h168.57421875c51.24023438 0 92.8125-41.57226563 92.8125-92.8125V212.8203125z m-57.12890625 174.19921875c0 17.05078125-13.88671875 30.9375-30.9375 30.9375H201.04296875c-17.05078125 0-30.9375-13.88671875-30.9375-30.9375V208.07421875c0-17.05078125 13.88671875-30.9375 30.9375-30.9375h178.9453125c17.05078125 0 30.9375 13.88671875 30.9375 30.9375v178.9453125z',
|
||||
fill: '#768696'
|
||||
})
|
||||
]
|
||||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
dataset: {
|
||||
iconReader: () => {
|
||||
return h('el-icon', { style: 'display:flex' }, [
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
viewBox: '0 0 1024 1024',
|
||||
version: '1.1',
|
||||
style: 'height:14px;width:14px',
|
||||
xmlns: 'http://www.w3.org/2000/svg'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M859.5 193H446.939c-1.851-53.25-45.747-96-99.439-96h-183C109.635 97 65 141.635 65 196.5v632c0 54.864 44.635 99.5 99.5 99.5h695c54.864 0 99.5-44.636 99.5-99.5v-536c0-54.865-44.636-99.5-99.5-99.5z m-695-33h183c20.126 0 36.5 16.374 36.5 36.5v28c0 17.397 14.103 31.5 31.5 31.5h444c20.126 0 36.5 16.374 36.5 36.5V321H128V196.5c0-20.126 16.374-36.5 36.5-36.5z m695 705h-695c-20.126 0-36.5-16.374-36.5-36.5V384h768v444.5c0 20.126-16.374 36.5-36.5 36.5z',
|
||||
fill: '#070102'
|
||||
})
|
||||
]
|
||||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
setting: {
|
||||
iconReader: () => {
|
||||
return h('el-icon', { style: 'display:flex' }, [
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
viewBox: '0 0 1024 1024',
|
||||
version: '1.1',
|
||||
style: 'height:14px;width:14px',
|
||||
xmlns: 'http://www.w3.org/2000/svg'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M512 328c-100.8 0-184 83.2-184 184S411.2 696 512 696 696 612.8 696 512 612.8 328 512 328z m0 320c-75.2 0-136-60.8-136-136s60.8-136 136-136 136 60.8 136 136-60.8 136-136 136z',
|
||||
fill: '#070102'
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M857.6 572.8c-20.8-12.8-33.6-35.2-33.6-60.8s12.8-46.4 33.6-60.8c14.4-9.6 20.8-27.2 16-44.8-8-27.2-19.2-52.8-32-76.8-8-14.4-25.6-24-43.2-19.2-24 4.8-48-1.6-65.6-19.2-17.6-17.6-24-41.6-19.2-65.6 3.2-16-4.8-33.6-19.2-43.2-24-14.4-51.2-24-76.8-32-16-4.8-35.2 1.6-44.8 16-12.8 20.8-35.2 33.6-60.8 33.6s-46.4-12.8-60.8-33.6c-9.6-14.4-27.2-20.8-44.8-16-27.2 8-52.8 19.2-76.8 32-14.4 8-24 25.6-19.2 43.2 4.8 24-1.6 49.6-19.2 65.6-17.6 17.6-41.6 24-65.6 19.2-16-3.2-33.6 4.8-43.2 19.2-14.4 24-24 51.2-32 76.8-4.8 16 1.6 35.2 16 44.8 20.8 12.8 33.6 35.2 33.6 60.8s-12.8 46.4-33.6 60.8c-14.4 9.6-20.8 27.2-16 44.8 8 27.2 19.2 52.8 32 76.8 8 14.4 25.6 22.4 43.2 19.2 24-4.8 49.6 1.6 65.6 19.2 17.6 17.6 24 41.6 19.2 65.6-3.2 16 4.8 33.6 19.2 43.2 24 14.4 51.2 24 76.8 32 16 4.8 35.2-1.6 44.8-16 12.8-20.8 35.2-33.6 60.8-33.6s46.4 12.8 60.8 33.6c8 11.2 20.8 17.6 33.6 17.6 3.2 0 8 0 11.2-1.6 27.2-8 52.8-19.2 76.8-32 14.4-8 24-25.6 19.2-43.2-4.8-24 1.6-49.6 19.2-65.6 17.6-17.6 41.6-24 65.6-19.2 16 3.2 33.6-4.8 43.2-19.2 14.4-24 24-51.2 32-76.8 4.8-17.6-1.6-35.2-16-44.8z m-56 92.8c-38.4-6.4-76.8 6.4-104 33.6-27.2 27.2-40 65.6-33.6 104-17.6 9.6-36.8 17.6-56 24-22.4-30.4-57.6-49.6-97.6-49.6-38.4 0-73.6 17.6-97.6 49.6-19.2-6.4-38.4-14.4-56-24 6.4-38.4-6.4-76.8-33.6-104-27.2-27.2-65.6-40-104-33.6-9.6-17.6-17.6-36.8-24-56 30.4-22.4 49.6-57.6 49.6-97.6 0-38.4-17.6-73.6-49.6-97.6 6.4-19.2 14.4-38.4 24-56 38.4 6.4 76.8-6.4 104-33.6 27.2-27.2 40-65.6 33.6-104 17.6-9.6 36.8-17.6 56-24 22.4 30.4 57.6 49.6 97.6 49.6 38.4 0 73.6-17.6 97.6-49.6 19.2 6.4 38.4 14.4 56 24-6.4 38.4 6.4 76.8 33.6 104 27.2 27.2 65.6 40 104 33.6 9.6 17.6 17.6 36.8 24 56-30.4 22.4-49.6 57.6-49.6 97.6 0 38.4 17.6 73.6 49.6 97.6-6.4 19.2-14.4 38.4-24 56z',
|
||||
fill: '#070102'
|
||||
})
|
||||
]
|
||||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
password: {
|
||||
iconReader: () => {
|
||||
return h('el-icon', { style: 'display:flex' }, [
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
viewBox: '0 0 1024 1024',
|
||||
version: '1.1',
|
||||
style: 'height:14px;width:14px',
|
||||
xmlns: 'http://www.w3.org/2000/svg'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M807.28626 393.9647l-59.057047 0 0-78.729086c0-130.193201-105.96438-236.099253-236.229213-236.099253S275.770787 185.042413 275.770787 315.235614l0 78.729086-59.057047 0c-32.616862 0-59.057047 26.425859-59.057047 59.025325L157.656693 885.838314c0 32.598442 26.441209 59.025325 59.057047 59.025325l590.57252 0c32.616862 0 59.057047-26.425859 59.057047-59.025325L866.343307 452.989001C866.343307 420.390559 839.903122 393.9647 807.28626 393.9647zM334.827835 315.235614c0-97.644901 79.473029-177.074951 177.172165-177.074951s177.172165 79.43005 177.172165 177.074951l0 78.729086L334.827835 393.9647 334.827835 315.235614zM807.28626 885.838314 216.71374 885.838314 216.71374 452.989001l590.57252 0L807.28626 885.838314z'
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M512 777.635963c16.302291 0 29.528524-13.219069 29.528524-29.512151L541.528524 590.723969c0-16.293081-13.226233-29.512151-29.528524-29.512151s-29.528524 13.219069-29.528524 29.512151l0 157.399843C482.471476 764.416893 495.697709 777.635963 512 777.635963z'
|
||||
})
|
||||
]
|
||||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
exit: {
|
||||
iconReader: () => {
|
||||
return h('el-icon', { style: 'display:flex' }, [
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
viewBox: '0 0 1024 1024',
|
||||
version: '1.1',
|
||||
style: 'height:14px;width:14px',
|
||||
xmlns: 'http://www.w3.org/2000/svg'
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M874.666667 855.744a19.093333 19.093333 0 0 1-19.136 18.922667H168.469333A19.2 19.2 0 0 1 149.333333 855.530667V168.469333A19.2 19.2 0 0 1 168.469333 149.333333h687.061334c10.581333 0 19.136 8.533333 19.136 18.922667V320h42.666666V168.256A61.717333 61.717333 0 0 0 855.530667 106.666667H168.469333A61.866667 61.866667 0 0 0 106.666667 168.469333v687.061334A61.866667 61.866667 0 0 0 168.469333 917.333333h687.061334A61.76 61.76 0 0 0 917.333333 855.744V704h-42.666666v151.744zM851.84 533.333333l-131.797333 131.754667a21.141333 21.141333 0 0 0 0.213333 29.973333 21.141333 21.141333 0 0 0 29.973333 0.192l165.589334-165.589333a20.821333 20.821333 0 0 0 6.122666-14.976 21.44 21.44 0 0 0-6.314666-14.997333l-168.533334-168.533334a21.141333 21.141333 0 0 0-29.952-0.213333 21.141333 21.141333 0 0 0 0.213334 29.973333L847.296 490.666667H469.333333v42.666666h382.506667z'
|
||||
})
|
||||
]
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div class="login-warp">
|
||||
<div class="login-container">
|
||||
<el-row class="container">
|
||||
<el-col :span="14" class="left-container">
|
||||
<div class="login-image"></div>
|
||||
</el-col>
|
||||
<el-col :span="10" class="right-container">
|
||||
<slot></slot>
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
</script>
|
||||
<style lang="scss" scope>
|
||||
.login-warp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.right-container {
|
||||
display: flex;
|
||||
margin-top: 20vh;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.left-container {
|
||||
.login-image {
|
||||
background-image: url('@/assets/login.png');
|
||||
background-size: 100% 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
<template >
|
||||
<el-dialog v-model="resetPasswordDialog" title="修改密码">
|
||||
<el-form class="reset-password-form" ref="resetPasswordFormRef" :model="resetPasswordForm" :rules="rules">
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input type="password" size="large" class="input-item" v-model="resetPasswordForm.password"
|
||||
placeholder="请输入密码">
|
||||
|
||||
<template #prepend>
|
||||
<el-button :icon="Lock" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="re_password">
|
||||
<el-input type="password" size="large" class="input-item" v-model="resetPasswordForm.re_password"
|
||||
placeholder="请输入确认密码">
|
||||
<template #prepend>
|
||||
<el-button :icon="Lock" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input size="large" class="input-item" :disabled="true" v-bind:modelValue="userStore.userInfo?.email"
|
||||
@change="() => { }" placeholder="请输入邮箱">
|
||||
|
||||
<template #prepend>
|
||||
<el-button :icon="UserFilled" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code">
|
||||
<el-input size="large" class="code-input" v-model="resetPasswordForm.code" placeholder="请输入验证码">
|
||||
<template #prepend>
|
||||
<el-button :icon="Key" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button size="large" class="send-email-button" @click="sendEmail" :loading="loading">获取验证码</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="resetPassword">
|
||||
修改密码
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import type { ResetCurrentUserPasswordRequest } from "@/api/user/type";
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from "element-plus"
|
||||
import UserApi from "@/api/user"
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { Lock, UserFilled, Key } from '@element-plus/icons-vue'
|
||||
import { useRouter } from "vue-router"
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore()
|
||||
|
||||
const resetPasswordDialog = ref<boolean>(false);
|
||||
|
||||
const resetPasswordForm = ref<ResetCurrentUserPasswordRequest>({
|
||||
code: "",
|
||||
password: "",
|
||||
re_password: ""
|
||||
});
|
||||
|
||||
const resetPasswordFormRef = ref<FormInstance>();
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
const rules = ref<FormRules<ResetCurrentUserPasswordRequest>>({
|
||||
|
||||
code: [
|
||||
{ required: true, message: '请输入验证码' }
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: "请输入密码",
|
||||
trigger: "blur",
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 30,
|
||||
message: "长度在 6 到 30 个字符",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
re_password: [{
|
||||
required: true,
|
||||
message: '请输入确认密码',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 30,
|
||||
message: "长度在 6 到 30 个字符",
|
||||
trigger: "blur",
|
||||
},
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (resetPasswordForm.value.password != resetPasswordForm.value.re_password) {
|
||||
callback(new Error('密码不一致'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}]
|
||||
|
||||
})
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
const sendEmail = () => {
|
||||
UserApi.sendEmailToCurrent(loading)
|
||||
.then(() => {
|
||||
ElMessage.success("发送验证码成功")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
resetPasswordForm.value = {
|
||||
code: "",
|
||||
password: "",
|
||||
re_password: ""
|
||||
}
|
||||
resetPasswordDialog.value = true
|
||||
}
|
||||
const resetPassword = () => {
|
||||
resetPasswordFormRef.value?.validate().then(() => {
|
||||
return UserApi.resetCurrentUserPassword(resetPasswordForm.value)
|
||||
}).then(() => {
|
||||
return userStore.logout()
|
||||
}).then(() => {
|
||||
router.push({ name: 'login' })
|
||||
})
|
||||
}
|
||||
const close = () => { resetPasswordDialog.value = false }
|
||||
|
||||
defineExpose({ open, close })
|
||||
</script>
|
||||
<style lang="scss" scope>
|
||||
.code-input {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.send-email-button {
|
||||
margin-left: 12px;
|
||||
width: 158px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<template >
|
||||
<el-dropdown trigger="click" size="small" type="primary">
|
||||
<el-avatar> {{ firstUserName }} </el-avatar>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openResetPassword">
|
||||
<AppIcon iconName="password"></AppIcon><span style="margin-left:5px">修改密码</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout">
|
||||
<AppIcon iconName="exit"></AppIcon><span style="margin-left:5px">退出</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<ResetPassword ref="resetPasswordRef"></ResetPassword>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useRouter } from "vue-router";
|
||||
import AppIcon from "@/components/icons/AppIcon.vue"
|
||||
import ResetPassword from "@/components/layout/top-bar/components/avatar/ResetPasssword.vue"
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const firstUserName = computed(() => {
|
||||
return userStore.userInfo?.username?.substring(0, 1)
|
||||
})
|
||||
const resetPasswordRef = ref<InstanceType<typeof ResetPassword>>();
|
||||
|
||||
const openResetPassword = () => {
|
||||
resetPasswordRef.value?.open()
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userStore.logout().then(() => {
|
||||
router.push({ name: "login" })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.el-avatar {
|
||||
--el-avatar-size: 30px;
|
||||
--el-avatar-bg-color: var(--app-base-action-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-dropdown-menu--small {
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div class="menu-item-container" :class="isActive ? 'active' : ''" @click="router.push({ name: menu.name })">
|
||||
<div class="icon">
|
||||
<AppIcon :iconName="menu.meta ? menu.meta.icon as string : '404'"></AppIcon>
|
||||
</div>
|
||||
<div class="title">{{ menu.meta?.title }} </div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute, type RouteRecordRaw } from 'vue-router'
|
||||
import { computed } from "vue";
|
||||
import AppIcon from "@/components/icons/AppIcon.vue"
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const props = defineProps<{
|
||||
menu: RouteRecordRaw
|
||||
}>()
|
||||
|
||||
const isActive = computed(() => {
|
||||
return route.name == props.menu.name && route.path == props.menu.path
|
||||
})
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.menu-item-container {
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--app-base-text-hover-bg-color);
|
||||
border-bottom: 3px solid var(--app-base-text-hover-color);
|
||||
height: calc(100% - 3px);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<template >
|
||||
<div class="top-menu-container">
|
||||
<MenuItem :menu="menu" v-for="(menu, index) in topMenuList" :key="index">
|
||||
</MenuItem>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { getChildRouteListByPathAndName } from "@/router/index"
|
||||
import MenuItem from "@/components/layout/top-bar/components/top-menu/MenuItem.vue"
|
||||
const topMenuList = computed(() => {
|
||||
return getChildRouteListByPathAndName("/", "home")
|
||||
})
|
||||
|
||||
</script>
|
||||
<style lang="scss" scope>
|
||||
.top-menu-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div class="top-bar-container">
|
||||
<div class="app-title-container">
|
||||
<div class="app-title-icon"></div>
|
||||
<div class="app-title-text">智能客服</div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<div class="app-top-menu-container">
|
||||
<TopMenu></TopMenu>
|
||||
</div>
|
||||
<div class="flex-auto"></div>
|
||||
<div class="avatar">
|
||||
<Avatar></Avatar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import TopMenu from "@/components/layout/top-bar/components/top-menu/index.vue"
|
||||
import Avatar from "@/components/layout/top-bar/components/avatar/index.vue"
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.top-bar-container {
|
||||
border-bottom: 1px solid rgba(229, 229, 229, 1);
|
||||
height: calc(100% - 1px);
|
||||
background-color: var(--app-header-background-color, #fff);
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
|
||||
.flex-auto {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 100%;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.app-title-container {
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
|
||||
.app-title-icon {
|
||||
background-image: url('@/assets/logo.png');
|
||||
background-size: 100% 100%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.app-title-text {
|
||||
color: var(--app-base-action-text-color);
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.line {
|
||||
height: 60%;
|
||||
width: 1px;
|
||||
margin-left: 20px;
|
||||
background-color: rgba(229, 229, 229, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import 'nprogress/nprogress.css'
|
||||
import '@/styles/index.scss'
|
||||
import ElementPlus from 'element-plus'
|
||||
import * as ElementPlusIcons from '@element-plus/icons-vue'
|
||||
import 'element-plus/dist/index.css'
|
||||
import { createApp } from 'vue'
|
||||
import { store } from '@/stores'
|
||||
import theme from '@/theme'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(store)
|
||||
const ElementPlusIconsVue: object = ElementPlusIcons
|
||||
// 将elementIcon放到全局
|
||||
app.config.globalProperties.$antIcons = ElementPlusIconsVue
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.use(theme)
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
export class Result<T> {
|
||||
message: string;
|
||||
code: number;
|
||||
data: T;
|
||||
constructor(message: string, code: number, data: T) {
|
||||
this.message = message;
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static success(data: any) {
|
||||
return new Result("请求成功", 200, data);
|
||||
}
|
||||
static error(message: string, code: number) {
|
||||
return new Result(message, code, null);
|
||||
}
|
||||
}
|
||||
|
||||
interface Page<T> {
|
||||
/**
|
||||
*分页数据
|
||||
*/
|
||||
records: Array<T>;
|
||||
/**
|
||||
*当前页
|
||||
*/
|
||||
current: number;
|
||||
/**
|
||||
* 每页展示size
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
*总数
|
||||
*/
|
||||
total: number;
|
||||
/**
|
||||
*是否有下一页
|
||||
*/
|
||||
hasNext: boolean;
|
||||
}
|
||||
export type { Page };
|
||||
export default Result;
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
import axios, { type AxiosRequestConfig } from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { NProgress } from 'nprogress'
|
||||
import type { Ref } from 'vue'
|
||||
import type { Result } from '@/request/Result'
|
||||
import { store } from '@/stores/index'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import router from '@/router'
|
||||
|
||||
import { ref, type WritableComputedRef } from 'vue'
|
||||
|
||||
const axiosConfig = {
|
||||
baseURL: '/api',
|
||||
withCredentials: false,
|
||||
timeout: 60000,
|
||||
headers: {}
|
||||
}
|
||||
|
||||
const instance = axios.create(axiosConfig)
|
||||
|
||||
// 设置请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
(config: AxiosRequestConfig) => {
|
||||
if (config.headers === undefined) {
|
||||
config.headers = {}
|
||||
}
|
||||
const userStore = useUserStore(store)
|
||||
const token = userStore.getToken()
|
||||
if (token) {
|
||||
config.headers['AUTHORIZATION'] = token
|
||||
}
|
||||
return config
|
||||
},
|
||||
(err: any) => {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
//设置响应拦截器
|
||||
instance.interceptors.response.use(
|
||||
(response: any) => {
|
||||
if (response.data) {
|
||||
if (response.data.code !== 200 && !(response.data instanceof Blob)) {
|
||||
ElMessage.error(response.data.message)
|
||||
}
|
||||
}
|
||||
if (response.headers['content-type'] === 'application/octet-stream') {
|
||||
return response
|
||||
}
|
||||
return response
|
||||
},
|
||||
(err: any) => {
|
||||
if (err.code === 'ECONNABORTED') {
|
||||
ElMessage.error(err.message)
|
||||
console.error(err)
|
||||
}
|
||||
if (err.response?.status === 401) {
|
||||
router.push({ name: 'login' })
|
||||
}
|
||||
|
||||
if (err.response?.status === 403) {
|
||||
ElMessage.error(
|
||||
err.response.data && err.response.data.message ? err.response.data.message : '没有权限访问'
|
||||
)
|
||||
}
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
export const request = instance
|
||||
|
||||
/* 简化请求方法,统一处理返回结果,并增加loading处理,这里以{success,data,message}格式的返回值为例,具体项目根据实际需求修改 */
|
||||
const promise: (
|
||||
request: Promise<any>,
|
||||
loading?: NProgress | Ref<boolean> | WritableComputedRef<boolean>
|
||||
) => Promise<Result<any>> = (request, loading = ref(false)) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if ((loading as NProgress).start) {
|
||||
;(loading as NProgress).start()
|
||||
} else {
|
||||
;(loading as Ref).value = true
|
||||
}
|
||||
request
|
||||
.then((response) => {
|
||||
// blob类型的返回状态是response.status
|
||||
if (
|
||||
response.data.code === 200 ||
|
||||
(response.data instanceof Blob && response.status === 200)
|
||||
) {
|
||||
resolve(response.data)
|
||||
} else {
|
||||
reject(response.data)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
.finally(() => {
|
||||
if ((loading as NProgress).start) {
|
||||
;(loading as NProgress).done()
|
||||
} else {
|
||||
;(loading as Ref).value = false
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送get请求 一般用来请求资源
|
||||
* @param url 资源url
|
||||
* @param params 参数
|
||||
* @param loading loading
|
||||
* @returns 异步promise对象
|
||||
*/
|
||||
export const get: (
|
||||
url: string,
|
||||
params?: unknown,
|
||||
loading?: NProgress | Ref<boolean>
|
||||
) => Promise<Result<any>> = (url: string, params: unknown, loading?: NProgress | Ref<boolean>) => {
|
||||
return promise(request({ url: url, method: 'get', params }), loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* faso post请求 一般用来添加资源
|
||||
* @param url 资源url
|
||||
* @param params 参数
|
||||
* @param data 添加数据
|
||||
* @param loading loading
|
||||
* @returns 异步promise对象
|
||||
*/
|
||||
export const post: (
|
||||
url: string,
|
||||
params?: unknown,
|
||||
data?: unknown,
|
||||
loading?: NProgress | Ref<boolean>
|
||||
) => Promise<Result<any> | any> = (url, params, data, loading) => {
|
||||
return promise(request({ url: url, method: 'post', data, params }), loading)
|
||||
}
|
||||
|
||||
/**|
|
||||
* 发送put请求 用于修改服务器资源
|
||||
* @param url 资源地址
|
||||
* @param params params参数地址
|
||||
* @param data 需要修改的数据
|
||||
* @param loading 进度条
|
||||
* @returns
|
||||
*/
|
||||
export const put: (
|
||||
url: string,
|
||||
params?: unknown,
|
||||
data?: unknown,
|
||||
loading?: NProgress | Ref<boolean>
|
||||
) => Promise<Result<any>> = (url, params, data, loading) => {
|
||||
return promise(request({ url: url, method: 'put', data, params }), loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param url 删除url
|
||||
* @param params params参数
|
||||
* @param loading 进度条
|
||||
* @returns
|
||||
*/
|
||||
export const del: (
|
||||
url: string,
|
||||
params?: unknown,
|
||||
data?: unknown,
|
||||
loading?: NProgress | Ref<boolean>
|
||||
) => Promise<Result<any>> = (url, params, data, loading) => {
|
||||
return promise(request({ url: url, method: 'delete', data, params }), loading)
|
||||
}
|
||||
|
||||
export const exportExcel: (
|
||||
fileName: string,
|
||||
url: string,
|
||||
params: any,
|
||||
loading?: NProgress | Ref<boolean>
|
||||
) => void = (fileName: string, url: string, params: any, loading?: NProgress | Ref<boolean>) => {
|
||||
promise(request({ url: url, method: 'get', params, responseType: 'blob' }), loading)
|
||||
.then((res: any) => {
|
||||
if (res) {
|
||||
const blob = new Blob([res], {
|
||||
type: 'application/vnd.ms-excel'
|
||||
})
|
||||
const link = document.createElement('a')
|
||||
link.href = window.URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
link.click()
|
||||
//释放内存
|
||||
window.URL.revokeObjectURL(link.href)
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 与服务器建立ws链接
|
||||
* @param url websocket路径
|
||||
* @returns 返回一个websocket实例
|
||||
*/
|
||||
export const socket = (url: string) => {
|
||||
let protocol = 'ws://'
|
||||
if (window.location.protocol === 'https:') {
|
||||
protocol = 'wss://'
|
||||
}
|
||||
let uri = protocol + window.location.host + url
|
||||
if (!import.meta.env.DEV) {
|
||||
uri = protocol + window.location.host + import.meta.env.VITE_BASE_PATH + url
|
||||
}
|
||||
return new WebSocket(uri)
|
||||
}
|
||||
export default instance
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/first',
|
||||
name: 'first',
|
||||
meta: { icon: 'app', title: '首页' },
|
||||
component: () => import('@/views/first/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/app',
|
||||
name: 'app',
|
||||
meta: { icon: 'app', title: '应用' },
|
||||
component: () => import('@/views/app/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/dataset',
|
||||
name: 'dataset',
|
||||
meta: { icon: 'dataset', title: '数据集' },
|
||||
component: () => import('@/views/dataset/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/setting',
|
||||
name: 'setting',
|
||||
meta: { icon: 'setting', title: '数据设置' },
|
||||
component: () => import('@/views/setting/index.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/login/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: () => import('@/views/register/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/forgot_password',
|
||||
name: 'forgot_password',
|
||||
component: () => import('@/views/forgot-password/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/reset_password/:code/:email',
|
||||
name: 'reset_password',
|
||||
component: () => import('@/views/reset-password/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)',
|
||||
name: '404',
|
||||
component: () => import('@/views/404/index.vue')
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
type NavigationGuardNext,
|
||||
type RouteLocationNormalized,
|
||||
type RouteRecordRaw
|
||||
} from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { store } from '@/stores'
|
||||
import { routes } from '@/router/data'
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: routes
|
||||
})
|
||||
|
||||
// 解决刷新获取用户信息问题
|
||||
let userStore: any = null
|
||||
|
||||
// 路由前置拦截器
|
||||
router.beforeEach(
|
||||
async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
if (to.name === '404') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
if (userStore === null) {
|
||||
userStore = useUserStore(store)
|
||||
}
|
||||
const notAuthRouteNameList = ['register', 'login', 'forgot_password', 'reset_password']
|
||||
|
||||
if (!notAuthRouteNameList.includes(to.name ? to.name.toString() : '')) {
|
||||
const token = userStore.getToken()
|
||||
if (!token) {
|
||||
next({
|
||||
path: '/login'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!userStore.userInfo) {
|
||||
userStore.profile()
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
)
|
||||
|
||||
export const getChildRouteListByPathAndName = (path: string, name: string) => {
|
||||
return getChildRouteList(routes, path, name)
|
||||
}
|
||||
|
||||
export const getChildRouteList: (
|
||||
routeList: Array<RouteRecordRaw>,
|
||||
path: string,
|
||||
name: string
|
||||
) => Array<RouteRecordRaw> = (routeList, path, name) => {
|
||||
for (let index = 0; index < routeList.length; index++) {
|
||||
const route = routeList[index]
|
||||
if (name === route.name && path === route.path) {
|
||||
return route.children || []
|
||||
}
|
||||
if (route.children && route.children.length > 0) {
|
||||
const result = getChildRouteList(route.children, path, name)
|
||||
if (result && result?.length > 0) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export default router
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import type { App } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
const store = createPinia();
|
||||
|
||||
export function setupStore(app: App<Element>) {
|
||||
app.use(store);
|
||||
}
|
||||
|
||||
export { store };
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import type { User } from '@/api/user/type'
|
||||
import UserApi from '@/api/user'
|
||||
import { ref } from 'vue'
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const userInfo = ref<User>()
|
||||
// 用户认证token
|
||||
const token = ref<string>()
|
||||
|
||||
const getToken = () => {
|
||||
if (token.value) {
|
||||
return token.value
|
||||
}
|
||||
return localStorage.getItem('token')
|
||||
}
|
||||
|
||||
const profile = () => {
|
||||
return UserApi.profile().then((ok) => {
|
||||
userInfo.value = ok.data
|
||||
return ok.data
|
||||
})
|
||||
}
|
||||
|
||||
const login = (username: string, password: string) => {
|
||||
return UserApi.login({ username, password }).then((ok) => {
|
||||
token.value = ok.data
|
||||
localStorage.setItem('token', ok.data)
|
||||
return profile()
|
||||
})
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
return UserApi.logout().then(() => {
|
||||
localStorage.removeItem('token')
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return { token, getToken, userInfo, profile, login, logout }
|
||||
})
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
html {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Helvetica, PingFang SC, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
height:100%;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// 滚动条整体部分
|
||||
::-webkit-scrollbar {
|
||||
width: 6px; // 纵向滚动条宽度
|
||||
height: 6px; // 横向滚动条高度
|
||||
}
|
||||
|
||||
// 滑块
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 5px;
|
||||
background-color: var(--ce-webkit-scrollbar-background-color, rgba(31, 35, 41, 0.3));
|
||||
}
|
||||
|
||||
// 轨道
|
||||
::-webkit-scrollbar-track {
|
||||
border-radius: 5px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// 创建表单
|
||||
.create-catalog-container {
|
||||
height: 100%;
|
||||
margin-top: -20px;
|
||||
.padding-top-30{
|
||||
padding-top:30px;
|
||||
}
|
||||
.padding-top-40{
|
||||
padding-top:40px;
|
||||
}
|
||||
// 表单外套
|
||||
.form-div{
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
min-width: 300px;
|
||||
form{
|
||||
.el-form-item {margin-bottom: 28px;}
|
||||
label{
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #1F2329;
|
||||
flex: none;
|
||||
order: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除按钮样式
|
||||
.delete-button-class{
|
||||
cursor: pointer;
|
||||
color: #646a73
|
||||
}
|
||||
|
||||
// 添加按钮样式
|
||||
.add-button-class{
|
||||
cursor: pointer;
|
||||
border: 0 solid;
|
||||
//width: 105px;
|
||||
height: 22px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
letter-spacing: -0.1px;
|
||||
color: #3370FF;
|
||||
.span-class{
|
||||
vertical-align:2px;
|
||||
color: #3370FF;
|
||||
padding-left: 5px
|
||||
}
|
||||
}
|
||||
|
||||
button{
|
||||
height: 32px;
|
||||
min-width: 80px
|
||||
}
|
||||
.save-btn{
|
||||
background-color: #3370FF;
|
||||
}
|
||||
.cancel-btn{
|
||||
|
||||
}
|
||||
|
||||
// 下方操作按钮区域
|
||||
.footer {
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
padding: 24px 0px 0px 0px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin: 0px -50px 0px;
|
||||
|
||||
.footer-form {
|
||||
min-width: 400px;
|
||||
}
|
||||
.footer-center {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
margin: 0px 80px 0px;
|
||||
text-align: right;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
padding-left: 15px;
|
||||
font-size: smaller;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义弹出框样式
|
||||
.custom-dialog{
|
||||
//标题样式
|
||||
.el-dialog__header{
|
||||
padding: 24px !important;
|
||||
}
|
||||
//关闭按钮样式
|
||||
.el-dialog__headerbtn .el-dialog__close{
|
||||
height: auto !important;
|
||||
color: #646A73 !important;
|
||||
font-size: x-large !important;
|
||||
}
|
||||
.el-dialog__headerbtn .el-dialog__close:hover{
|
||||
background: rgba(31, 35, 41, 0.1) !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
//内容间距
|
||||
.el-dialog__body{
|
||||
padding: 0px 24px 0px 24px;
|
||||
}
|
||||
.el-dialog__footer{
|
||||
padding-bottom: 29px !important;
|
||||
}
|
||||
//下方按钮
|
||||
.footer-btn{
|
||||
button{
|
||||
height: 32px;
|
||||
min-width: 80px
|
||||
}
|
||||
.save-btn{
|
||||
background-color: #3370FF;
|
||||
}
|
||||
.cancel-btn{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-radio-group.el-radio-group{
|
||||
border: 1px solid #BBBFC4;
|
||||
border-radius: 5px;
|
||||
height: 30px;
|
||||
label{
|
||||
border: 0px solid;
|
||||
padding: 2px 10px 2px 4px;
|
||||
}
|
||||
.el-radio-button__inner{
|
||||
padding: 4px;
|
||||
border: 0px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.el-radio-button{
|
||||
height: auto;
|
||||
}
|
||||
.el-radio-button is-active{
|
||||
height: auto;
|
||||
}
|
||||
.el-radio-button__original-radio:checked + .el-radio-button__inner{
|
||||
color: #3370FF;
|
||||
border: 0;
|
||||
box-shadow: 0 0 0 0;
|
||||
background: rgba(51, 112, 255, 0.1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
// 抽屉样式整体修改
|
||||
.el-drawer{
|
||||
|
||||
.el-drawer__header{
|
||||
padding: 0;
|
||||
margin: 0 24px;
|
||||
height: 56px;
|
||||
border-bottom: 1px solid #D5D6D8;
|
||||
.el-drawer__title {
|
||||
color: #1f2329;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
}
|
||||
.el-drawer__body{
|
||||
--el-drawer-padding-primary:24px
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.el-popper {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.el-form {
|
||||
--el-form-inline-content-width: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@use "./variables/index.scss";
|
||||
@use "./app.scss";
|
||||
@use "./element-plus.scss";
|
||||
@use "./drawer.scss";
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
@mixin flex-row($justify: flex-start, $align: stretch) {
|
||||
display: flex;
|
||||
@if $justify != flex-start {
|
||||
justify-content: $justify;
|
||||
}
|
||||
@if $align != stretch {
|
||||
align-items: $align;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin variant($color, $background-color, $border-color) {
|
||||
color: $color;
|
||||
background-color: $background-color;
|
||||
border-color: $border-color;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
:root{
|
||||
--app-base-text-color:rgba(31, 35, 41, 1);
|
||||
--app-base-text-font-size:14px;
|
||||
--app-base-text-hover-color:rgba(51, 112, 255, 1);
|
||||
--app-base-text-hover-bg-color:rgba(51, 112, 255, 0.1);
|
||||
--app-base-action-text-color:var(--app-base-text-hover-color );
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
:root{
|
||||
--app-header-height: 56px;
|
||||
--app-header-padding: 0 20px;
|
||||
--app-header-bg-color: #252b3c;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
@use "./header.scss";
|
||||
@use "./app.scss";
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import type { InferData } from "./type";
|
||||
const inferData: Array<InferData> = [
|
||||
{
|
||||
key: "primary",
|
||||
value: "#3370FF",
|
||||
},
|
||||
{ key: "success", value: "#67c23a" },
|
||||
{ key: "warning", value: "#e6a23c" },
|
||||
{ key: "danger", value: "#f56c6c" },
|
||||
{ key: "error", value: "#F54A45" },
|
||||
{ key: "info", value: "#909399" },
|
||||
];
|
||||
export default inferData;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import type { KeyValueData } from './type'
|
||||
const keyValueData: KeyValueData = {
|
||||
'--el-header-padding': '0px'
|
||||
}
|
||||
export default keyValueData
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
import type {
|
||||
ThemeSetting,
|
||||
InferData,
|
||||
KeyValueData,
|
||||
UpdateInferData,
|
||||
UpdateKeyValueData
|
||||
} from './type'
|
||||
import tinycolor from '@ctrl/tinycolor'
|
||||
// 引入默认推断数据
|
||||
import inferData from './defaultInferData'
|
||||
// 引入默认keyValue数据
|
||||
import keyValueData from './defaultKeyValueData'
|
||||
// 引入设置对象
|
||||
import setting from './setting'
|
||||
import type { App } from 'vue'
|
||||
declare global {
|
||||
interface ChildNode {
|
||||
innerText: string
|
||||
}
|
||||
}
|
||||
class Theme {
|
||||
/**
|
||||
* 主题设置
|
||||
*/
|
||||
themeSetting: ThemeSetting
|
||||
/**
|
||||
* 键值数据
|
||||
*/
|
||||
keyValue: KeyValueData
|
||||
/**
|
||||
* 外推数据
|
||||
*/
|
||||
inferData: Array<InferData>
|
||||
/**
|
||||
*是否是第一次初始化
|
||||
*/
|
||||
isFirstWriteStyle: boolean
|
||||
/**
|
||||
* 混色白
|
||||
*/
|
||||
colorWhite: string
|
||||
/**
|
||||
* 混色黑
|
||||
*/
|
||||
colorBlack: string
|
||||
|
||||
constructor(themeSetting: ThemeSetting, keyValue: KeyValueData, inferData: Array<InferData>) {
|
||||
this.themeSetting = themeSetting
|
||||
this.keyValue = keyValue
|
||||
this.inferData = inferData
|
||||
this.isFirstWriteStyle = true
|
||||
this.colorWhite = '#ffffff'
|
||||
this.colorBlack = '#000000'
|
||||
this.initDefaultTheme()
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接
|
||||
* @param setting 主题设置
|
||||
* @param names 需要拼接的所有值
|
||||
* @returns 拼接后的数据
|
||||
*/
|
||||
getVarName = (setting: ThemeSetting, ...names: Array<string>) => {
|
||||
return (
|
||||
setting.startDivision + setting.namespace + setting.division + names.join(setting.division)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换外推数据
|
||||
* @param setting 主题设置对象
|
||||
* @param inferData 外推数据
|
||||
* @returns
|
||||
*/
|
||||
mapInferMainStyle = (setting: ThemeSetting, inferData: InferData) => {
|
||||
const key: string = this.getVarName(
|
||||
setting,
|
||||
inferData.setting ? inferData.setting.type : setting.colorInferSetting.type,
|
||||
inferData.key
|
||||
)
|
||||
return {
|
||||
[key]: inferData.value,
|
||||
...this.mapInferDataStyle(setting, inferData)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 转换外推数据
|
||||
* @param setting 设置
|
||||
* @param inferDatas 外推数据
|
||||
*/
|
||||
mapInferData = (setting: ThemeSetting, inferDatas: Array<InferData>) => {
|
||||
return inferDatas
|
||||
.map((itemData) => {
|
||||
return this.mapInferMainStyle(setting, itemData)
|
||||
})
|
||||
.reduce((pre, next) => {
|
||||
return { ...pre, ...next }
|
||||
}, {})
|
||||
}
|
||||
/**
|
||||
* 转换外推数据
|
||||
* @param setting 主题设置对象
|
||||
* @param inferData 外推数据
|
||||
* @returns
|
||||
*/
|
||||
mapInferDataStyle = (setting: ThemeSetting, inferData: InferData) => {
|
||||
const inferSetting = inferData.setting ? inferData.setting : setting.colorInferSetting
|
||||
if (inferSetting.type === 'color') {
|
||||
return Object.keys(inferSetting)
|
||||
.map((key: string) => {
|
||||
if (key === 'light' || key === 'dark') {
|
||||
return inferSetting[key]
|
||||
.map((l: any) => {
|
||||
const varName = this.getVarName(
|
||||
setting,
|
||||
inferSetting.type,
|
||||
inferData.key,
|
||||
key,
|
||||
l.toString()
|
||||
)
|
||||
return {
|
||||
[varName]: tinycolor(inferData.value)
|
||||
.mix(key === 'light' ? this.colorWhite : this.colorBlack, l * 10)
|
||||
.toHexString()
|
||||
}
|
||||
})
|
||||
.reduce((pre: any, next: any) => {
|
||||
return { ...pre, ...next }
|
||||
}, {})
|
||||
}
|
||||
return {}
|
||||
})
|
||||
.reduce((pre, next) => {
|
||||
return { ...pre, ...next }
|
||||
}, {})
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param themeSetting 主题设置
|
||||
* @param keyValueData 键值数据
|
||||
* @returns 映射后的键值数据
|
||||
*/
|
||||
mapKeyValue = (themeSetting: ThemeSetting, keyValueData: KeyValueData) => {
|
||||
return Object.keys(keyValueData)
|
||||
.map((key: string) => {
|
||||
return {
|
||||
[this.updateKeyBySetting(key, themeSetting)]: keyValueData[key]
|
||||
}
|
||||
})
|
||||
.reduce((pre, next) => {
|
||||
return { ...pre, ...next }
|
||||
}, {})
|
||||
}
|
||||
/**
|
||||
* 根据配置文件修改Key
|
||||
* @param key key
|
||||
* @param themeSetting 主题设置
|
||||
* @returns
|
||||
*/
|
||||
updateKeyBySetting = (key: string, themeSetting: ThemeSetting) => {
|
||||
return key.startsWith(themeSetting.startDivision)
|
||||
? key
|
||||
: key.startsWith(themeSetting.namespace)
|
||||
? themeSetting.startDivision + key
|
||||
: key.startsWith(themeSetting.division)
|
||||
? themeSetting.startDivision + themeSetting.namespace
|
||||
: themeSetting.startDivision + themeSetting.namespace + themeSetting.division + key
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param setting 主题设置
|
||||
* @param keyValue 主题键值对数据
|
||||
* @param inferDatas 外推数据
|
||||
* @returns 合并后的键值对数据
|
||||
*/
|
||||
tokeyValueStyle = () => {
|
||||
return {
|
||||
...this.mapInferData(this.themeSetting, this.inferData),
|
||||
...this.mapKeyValue(this.themeSetting, this.keyValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将keyValue对象转换为S
|
||||
* @param keyValue
|
||||
* @returns
|
||||
*/
|
||||
toString = (keyValue: KeyValueData) => {
|
||||
const inner = Object.keys(keyValue)
|
||||
.map((key: string) => {
|
||||
return key + ':' + keyValue[key] + ';'
|
||||
})
|
||||
.join('')
|
||||
return `@charset "UTF-8";:root{${inner}}`
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elNewStyle 新的变量样式
|
||||
*/
|
||||
writeNewStyle = (elNewStyle: string) => {
|
||||
if (this.isFirstWriteStyle) {
|
||||
const style = document.createElement('style')
|
||||
style.innerText = elNewStyle
|
||||
document.head.appendChild(style)
|
||||
this.isFirstWriteStyle = false
|
||||
} else {
|
||||
if (document.head.lastChild) {
|
||||
document.head.lastChild.innerText = elNewStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改数据并且写入dom
|
||||
* @param updateInferData 平滑数据修改
|
||||
* @param updateKeyvalueData keyValue数据修改
|
||||
*/
|
||||
updateWrite = (updateInferData?: UpdateInferData, updateKeyvalueData?: UpdateKeyValueData) => {
|
||||
this.update(updateInferData, updateKeyvalueData)
|
||||
const newStyle = this.tokeyValueStyle()
|
||||
const newStyleString = this.toString(newStyle)
|
||||
this.writeNewStyle(newStyleString)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改数据
|
||||
* @param inferData
|
||||
* @param keyvalueData
|
||||
*/
|
||||
update = (updateInferData?: UpdateInferData, updateKeyvalueData?: UpdateKeyValueData) => {
|
||||
if (updateInferData) {
|
||||
this.updateInferData(updateInferData)
|
||||
}
|
||||
if (updateKeyvalueData) {
|
||||
this.updateOrCreateKeyValueData(updateKeyvalueData)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改外推数据 外推数据只能修改,不能新增
|
||||
* @param inferData
|
||||
*/
|
||||
updateInferData = (updateInferData: UpdateInferData) => {
|
||||
Object.keys(updateInferData).forEach((key) => {
|
||||
const findInfer = this.inferData.find((itemInfer) => {
|
||||
return itemInfer.key === key
|
||||
})
|
||||
if (findInfer) {
|
||||
findInfer.value = updateInferData[key]
|
||||
} else {
|
||||
this.inferData.push({ key, value: updateInferData[key] })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认主题
|
||||
*/
|
||||
initDefaultTheme = () => {
|
||||
this.updateWrite()
|
||||
}
|
||||
/**
|
||||
* 修改KeyValue数据
|
||||
* @param keyvalueData keyValue数据
|
||||
*/
|
||||
updateOrCreateKeyValueData = (updateKeyvalueData: UpdateKeyValueData) => {
|
||||
Object.keys(updateKeyvalueData).forEach((key) => {
|
||||
const newKey = this.updateKeyBySetting(key, this.themeSetting)
|
||||
this.keyValue[newKey] = updateKeyvalueData[newKey]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const install = (app: App) => {
|
||||
app.config.globalProperties.theme = new Theme(setting, keyValueData, inferData)
|
||||
}
|
||||
export default { install }
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import type { ThemeSetting } from "./type";
|
||||
const setting: ThemeSetting = {
|
||||
namespace: "el",
|
||||
division: "-",
|
||||
startDivision: "--",
|
||||
colorInferSetting: {
|
||||
light: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
dark: [2],
|
||||
type: "color",
|
||||
},
|
||||
};
|
||||
export default setting;
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
interface ThemeSetting {
|
||||
/**
|
||||
*element-ui Namespace
|
||||
*/
|
||||
namespace: string;
|
||||
/**
|
||||
* 数据分隔符
|
||||
*/
|
||||
division: string;
|
||||
/**
|
||||
* 前缀
|
||||
*/
|
||||
startDivision: string;
|
||||
/**
|
||||
* 颜色外推设置
|
||||
*/
|
||||
colorInferSetting: ColorInferSetting;
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色混和设置
|
||||
*/
|
||||
interface ColorInferSetting {
|
||||
/**
|
||||
* 与白色混
|
||||
*/
|
||||
light: Array<number>;
|
||||
/**
|
||||
* 与黑色混
|
||||
*/
|
||||
dark: Array<number>;
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑数据
|
||||
*/
|
||||
interface KeyValueData {
|
||||
[propName: string]: string;
|
||||
}
|
||||
type UpdateInferData = KeyValueData;
|
||||
|
||||
type UpdateKeyValueData = KeyValueData;
|
||||
/**
|
||||
*平滑数据
|
||||
*/
|
||||
interface InferData {
|
||||
/**
|
||||
* 设置
|
||||
*/
|
||||
setting?: ColorInferSetting | any;
|
||||
/**
|
||||
* 健
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* 值
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type {
|
||||
KeyValueData,
|
||||
InferData,
|
||||
ThemeSetting,
|
||||
UpdateInferData,
|
||||
UpdateKeyValueData,
|
||||
};
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<template >
|
||||
<el-row class="not-found-container">
|
||||
<el-col class="img" :span="12">
|
||||
|
||||
</el-col>
|
||||
<el-col class="message-container" :span="12">
|
||||
<div class="title">404</div>
|
||||
<div class="message">很抱歉,您要访问的页面地址有误,或者该页面不存在。</div>
|
||||
<div class="operate"><el-button type="primary" @click="router.push('/')">返回首页</el-button></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter()
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.not-found-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.img {
|
||||
background-image: url('@/assets/404.png');
|
||||
background-size: 100% 100%;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
color: var(--app-base-text-color);
|
||||
|
||||
.title {
|
||||
font-size: 50px;
|
||||
font-weight: 500;
|
||||
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 20px;
|
||||
margin: 50px 0 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<template >
|
||||
<div>
|
||||
app
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<template >
|
||||
<div>
|
||||
dataset
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<template >
|
||||
<div>
|
||||
first
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
<template>
|
||||
<LoiginLayout>
|
||||
<div class="register-form-container">
|
||||
<div class="register-form-title">
|
||||
<div class="title">
|
||||
<div class="logo"></div>
|
||||
<div>智能客服</div>
|
||||
</div>
|
||||
<div class="sub-title">忘记密码</div>
|
||||
</div>
|
||||
<el-form class="register-form" ref="resetPasswordFormRef" :model="CheckEmailForm" :rules="rules">
|
||||
|
||||
<el-form-item prop="email">
|
||||
<el-input size="large" class="input-item" v-model="CheckEmailForm.email" placeholder="请输入邮箱">
|
||||
|
||||
<template #prepend>
|
||||
<el-button :icon="UserFilled" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code">
|
||||
<el-input size="large" class="code-input" v-model="CheckEmailForm.code" placeholder="请输入验证码">
|
||||
|
||||
<template #prepend>
|
||||
<el-button :icon="Key" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button size="large" class="send-email-button" @click="sendEmail"
|
||||
:loading="loading">获取验证码</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" class="register-button" @click="checkCode">立即验证</el-button>
|
||||
<div class="operate-container">
|
||||
<span class="register" @click="router.push('login')">< 返回登陆</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</LoiginLayout>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { UserFilled, Key } from '@element-plus/icons-vue'
|
||||
import type {
|
||||
CheckCodeRequest
|
||||
} from "@/api/user/type"
|
||||
import LoiginLayout from "@/components/layout/login-layout/index.vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import UserApi from "@/api/user/index"
|
||||
import { ElMessage } from "element-plus"
|
||||
|
||||
const router = useRouter()
|
||||
const CheckEmailForm = ref<CheckCodeRequest>({
|
||||
email: "",
|
||||
code: "",
|
||||
type: 'reset_password'
|
||||
});
|
||||
|
||||
const resetPasswordFormRef = ref<FormInstance>()
|
||||
const rules = ref<FormRules<CheckCodeRequest>>({
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const emailRegExp = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/
|
||||
if ((!emailRegExp.test(value) && value != '')) {
|
||||
callback(new Error('请输入有效邮箱格式!'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入验证码' }
|
||||
]
|
||||
|
||||
})
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
const checkCode = () => {
|
||||
resetPasswordFormRef.value?.validate()
|
||||
.then(() => UserApi.checkCode(CheckEmailForm.value, loading))
|
||||
.then(() => router.push({ name: 'reset_password', params: CheckEmailForm.value }))
|
||||
}
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
const sendEmail = () => {
|
||||
resetPasswordFormRef.value?.validateField("email", (v: boolean) => {
|
||||
if (v) {
|
||||
UserApi.sendEmit(CheckEmailForm.value.email, "reset_password", loading)
|
||||
.then(() => {
|
||||
ElMessage.success("发送验证码成功")
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss" scope>
|
||||
.register-form-container {
|
||||
width: 420px;
|
||||
|
||||
.code-input {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.send-email-button {
|
||||
margin-left: 12px;
|
||||
width: 158px;
|
||||
}
|
||||
|
||||
.register-form-title {
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.logo {
|
||||
background-image: url('@/assets/logo.png');
|
||||
background-size: 100% 100%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
color: #101010;
|
||||
height: 60px;
|
||||
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
color: #101010;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.operate-container {
|
||||
margin-top: 12px;
|
||||
color: rgba(51, 112, 255, 1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.register {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.register-button {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<script setup lang="ts">
|
||||
import TopBar from "@/components/layout/top-bar/index.vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<el-container>
|
||||
<el-header>
|
||||
<TopBar></TopBar>
|
||||
</el-header>
|
||||
<el-main>
|
||||
<RouterView></RouterView>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.el-header {
|
||||
--el-header-padding: 0;
|
||||
--el-header-height: 56px;
|
||||
padding: var(--el-header-padding);
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
height: var(--el-header-height);
|
||||
}
|
||||
|
||||
.el-main {
|
||||
--el-main-padding: 0;
|
||||
width: 100vw;
|
||||
height: calc(100vh - var(--el-header-height, 60px));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
<template>
|
||||
<LoiginLayout v-loading="loading">
|
||||
<div class="login-form-container">
|
||||
<div class="login-form-title">
|
||||
<div class="title">
|
||||
<div class="logo"></div>
|
||||
<div>智能客服</div>
|
||||
</div>
|
||||
<div class="sub-title">欢迎使用智能客服管理平台</div>
|
||||
</div>
|
||||
<el-form class="login-form" :rules="rules" :model="loginForm" ref="loginFormRef">
|
||||
<el-form-item>
|
||||
<el-input size="large" class="input-item" v-model="loginForm.username" placeholder="请输入用户名">
|
||||
<template #prepend>
|
||||
<el-button :icon="UserFilled" />
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input type="password" size="large" class="input-item" v-model="loginForm.password"
|
||||
placeholder="请输入密码">
|
||||
|
||||
<template #prepend>
|
||||
<el-button :icon="Lock" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="operate-container">
|
||||
<span class="register" @click="router.push('register')">注册</span>
|
||||
<span class="forgot-password" @click="router.push('forgot_password')">忘记密码</span>
|
||||
</div>
|
||||
<el-button type="primary" class="login-button" @click="login">登录</el-button>
|
||||
</div>
|
||||
</LoiginLayout>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import type { LoginRequest } from "@/api/user/type"
|
||||
import { UserFilled, Lock } from '@element-plus/icons-vue'
|
||||
import LoiginLayout from "@/components/layout/login-layout/index.vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useUserStore } from "@/stores/user"
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter()
|
||||
const loginForm = ref<LoginRequest>({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
|
||||
const rules = ref<FormRules<LoginRequest>>({
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: "请输入用户名",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: "请输入密码",
|
||||
trigger: "blur",
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 30,
|
||||
message: "长度在 6 到 30 个字符",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
})
|
||||
const loginFormRef = ref<FormInstance>()
|
||||
|
||||
const login = () => {
|
||||
loginFormRef.value?.validate().then(() => {
|
||||
loading.value = true
|
||||
userStore.login(loginForm.value.username, loginForm.value.password)
|
||||
.then(() => { router.push({ name: 'home' }) })
|
||||
.finally(() => loading.value = false)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss" scope>
|
||||
.login-form-container {
|
||||
width: 420px;
|
||||
|
||||
|
||||
.login-form-title {
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.logo {
|
||||
background-image: url('@/assets/logo.png');
|
||||
background-size: 100% 100%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
color: #101010;
|
||||
height: 60px;
|
||||
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #101010;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
.operate-container {
|
||||
color: rgba(51, 112, 255, 1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.register {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
<template>
|
||||
<LoiginLayout>
|
||||
<div class="register-form-container">
|
||||
<div class="register-form-title">
|
||||
<div class="title">
|
||||
<div class="logo"></div>
|
||||
<div>智能客服</div>
|
||||
</div>
|
||||
<div class="sub-title">修改密码</div>
|
||||
</div>
|
||||
<el-form class="register-form" :model="registerForm" :rules="rules" ref="registerFormRef">
|
||||
<el-form-item prop="username">
|
||||
<el-input size="large" class="input-item" v-model="registerForm.username" placeholder="请输入用户名">
|
||||
<template #prepend>
|
||||
<el-button :icon="UserFilled" />
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input type="password" size="large" class="input-item" v-model="registerForm.password"
|
||||
placeholder="请输入密码">
|
||||
|
||||
<template #prepend>
|
||||
<el-button :icon="Lock" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="repassword">
|
||||
<el-input type="password" size="large" class="input-item" v-model="registerForm.re_password"
|
||||
placeholder="请输入确认密码">
|
||||
|
||||
<template #prepend>
|
||||
<el-button :icon="Lock" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="email">
|
||||
<el-input size="large" class="input-item" v-model="registerForm.email" placeholder="请输入邮箱">
|
||||
|
||||
<template #prepend>
|
||||
<el-button :icon="Message" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code">
|
||||
<el-input size="large" class="code-input" v-model="registerForm.code" placeholder="请输入验证码">
|
||||
|
||||
<template #prepend>
|
||||
<el-button :icon="Key" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button size="large" class="send-email-button" @click="sendEmail"
|
||||
:loading="sendEmailLoading">获取验证码</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" class="register-button" @click="register">注册</el-button>
|
||||
<div class="operate-container">
|
||||
<span class="register" @click="router.push('login')">< 返回登陆</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</LoiginLayout>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import type { RegisterRequest } from "@/api/user/type"
|
||||
import { UserFilled, Lock, Message, Key } from '@element-plus/icons-vue'
|
||||
import LoiginLayout from "@/components/layout/login-layout/index.vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import UserApi from "@/api/user/index"
|
||||
import { ElMessage } from "element-plus"
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const registerForm = ref<RegisterRequest>({
|
||||
username: '',
|
||||
password: '',
|
||||
re_password: '',
|
||||
email: '',
|
||||
code: ''
|
||||
});
|
||||
|
||||
const rules = ref<FormRules<RegisterRequest>>({
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: "请输入用户名",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: "请输入密码",
|
||||
trigger: "blur",
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 30,
|
||||
message: "长度在 6 到 30 个字符",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
re_password: [{
|
||||
required: true,
|
||||
message: '请输入确认密码',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 30,
|
||||
message: "长度在 6 到 30 个字符",
|
||||
trigger: "blur",
|
||||
},
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (registerForm.value.password != registerForm.value.re_password) {
|
||||
callback(new Error('密码不一致'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const emailRegExp = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/
|
||||
if ((!emailRegExp.test(value) && value != '')) {
|
||||
callback(new Error('请输入有效邮箱格式!'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入验证码' }
|
||||
]
|
||||
|
||||
})
|
||||
|
||||
const registerFormRef = ref<FormInstance>();
|
||||
const register = () => {
|
||||
registerFormRef.value?.validate().then(() => {
|
||||
return UserApi.register(registerForm.value)
|
||||
}).then(() => {
|
||||
router.push("login")
|
||||
})
|
||||
}
|
||||
const sendEmailLoading = ref<boolean>(false);
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
const sendEmail = () => {
|
||||
registerFormRef.value?.validateField("email", (v: boolean) => {
|
||||
if (v) {
|
||||
UserApi.sendEmit(registerForm.value.email, "register",sendEmailLoading)
|
||||
.then(() => {
|
||||
ElMessage.success("发送验证码成功")
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scope>
|
||||
.register-form-container {
|
||||
width: 420px;
|
||||
|
||||
.code-input {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.send-email-button {
|
||||
margin-left: 12px;
|
||||
width: 158px;
|
||||
}
|
||||
|
||||
.register-form-title {
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.logo {
|
||||
background-image: url('@/assets/logo.png');
|
||||
background-size: 100% 100%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
color: #101010;
|
||||
height: 60px;
|
||||
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
color: #101010;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.operate-container {
|
||||
margin-top: 12px;
|
||||
color: rgba(51, 112, 255, 1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.register {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.register-button {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
<template>
|
||||
<LoiginLayout>
|
||||
<div class="register-form-container">
|
||||
<div class="register-form-title">
|
||||
<div class="title">
|
||||
<div class="logo"></div>
|
||||
<div>智能客服</div>
|
||||
</div>
|
||||
<div class="sub-title">修改密码</div>
|
||||
</div>
|
||||
<el-form class="reset-password-form" ref="resetPasswordFormRef" :model="resetPasswordForm" :rules="rules">
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input type="password" size="large" class="input-item" v-model="resetPasswordForm.password"
|
||||
placeholder="请输入密码">
|
||||
|
||||
<template #prepend>
|
||||
<el-button :icon="Lock" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="re_password">
|
||||
<el-input type="password" size="large" class="input-item" v-model="resetPasswordForm.re_password"
|
||||
placeholder="请输入确认密码">
|
||||
<template #prepend>
|
||||
<el-button :icon="Lock" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" class="register-button" @click="resetPassword">确认修改</el-button>
|
||||
<div class="operate-container">
|
||||
<span class="register" @click="router.push('login')">< 返回登陆</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</LoiginLayout>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue"
|
||||
import type { ResetPasswordRequest } from "@/api/user/type"
|
||||
import { Lock } from '@element-plus/icons-vue'
|
||||
import LoiginLayout from "@/components/layout/login-layout/index.vue"
|
||||
import { useRouter, useRoute } from "vue-router"
|
||||
import { ElMessage } from "element-plus"
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import UserApi from "@/api/user/index"
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const resetPasswordForm = ref<ResetPasswordRequest>({
|
||||
password: '',
|
||||
re_password: '',
|
||||
email: '',
|
||||
code: ''
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const code = route.params.code;
|
||||
const email = route.params.email;
|
||||
if (code && email) {
|
||||
resetPasswordForm.value.code = code as string;
|
||||
resetPasswordForm.value.email = email as string;
|
||||
} else {
|
||||
router.push('forgot_password')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const rules = ref<FormRules<ResetPasswordRequest>>({
|
||||
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: "请输入密码",
|
||||
trigger: "blur",
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 30,
|
||||
message: "长度在 6 到 30 个字符",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
re_password: [{
|
||||
required: true,
|
||||
message: '请输入确认密码',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
max: 30,
|
||||
message: "长度在 6 到 30 个字符",
|
||||
trigger: "blur",
|
||||
},
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (resetPasswordForm.value.password != resetPasswordForm.value.re_password) {
|
||||
callback(new Error('密码不一致'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}],
|
||||
|
||||
})
|
||||
const resetPasswordFormRef = ref<FormInstance>();
|
||||
const loading = ref<boolean>(false);
|
||||
const resetPassword = () => {
|
||||
resetPasswordFormRef.value?.validate()
|
||||
.then(() => UserApi.resetPassword(resetPasswordForm.value, loading))
|
||||
.then(() => {
|
||||
ElMessage.success("修改密码成功")
|
||||
router.push({ name: 'login' })
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss" scope>
|
||||
.register-form-container {
|
||||
width: 420px;
|
||||
|
||||
.code-input {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.send-email-button {
|
||||
margin-left: 12px;
|
||||
width: 158px;
|
||||
}
|
||||
|
||||
.register-form-title {
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.logo {
|
||||
background-image: url('@/assets/logo.png');
|
||||
background-size: 100% 100%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
color: #101010;
|
||||
height: 60px;
|
||||
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
color: #101010;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.operate-container {
|
||||
margin-top: 12px;
|
||||
color: rgba(51, 112, 255, 1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.register {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.register-button {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<template >
|
||||
<div>
|
||||
setting
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
|
||||
"files": [],
|
||||
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue