commit 632d32c0c6bd0767fa0e78e38e19893399bfc5b4 Author: haotian <2421912570@qq.com> Date: Fri Jul 25 10:03:06 2025 +0800 original_project diff --git a/README.md b/README.md new file mode 100644 index 0000000..6016170 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +

django-ruoyi-admin

+

基于django+ 若依(Vue3版本)快速开发框架(v1.0.1)

+ +

+ 👉 个人网站:http://124.71.212.219:8028/ 👈 +

+ +

+ + + + + +

+ +### 平台介绍 + +若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。[若依框架地址]([Vue3](https://v3.cn.vuejs.org)), +但目前主流的若以框架是基于SpringBoot的java开发语言,企业开发前端常用的是若依框架,而后端没有一个属于python版本的后端,因此萌生了调试一套适合python程序员的快速开发框架,删除广告页面和日常开发者不必要的功能,只保留项目的最核心的配置模块,减少初始化的方式,做到入手既可以开发功能 + +python主流框架请查看不同的分支(django+mysql,flask+mongo,sanic-mongo) + + +### 技术栈介绍 + +* 前端技术栈: [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) +若依框架版本。 +* 后端技术栈: [django](https://www.djangoproject.com/) + [mysql](https://www.mysql.com/) + [redis](https://redis.io/) 实现。 + + + + + +
+ +### 框架能力 +* 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 +* 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。 +* 岗位管理:配置系统用户所属担任职务。 +* 菜单管理:配置系统菜单,操作权限,按钮权限标识等。 + +### 代码优化 + +* 后端:收集日常的常用方法,增加常用的企业微信机器人通知,数据加解密等常用方法 +* 后端:数据库配置增加nacos和本地config的配置的2种配置,多种配置方式 +* 后端:配置数据库后,项目启动直接初始化相关数据,不需要导入sql的方式,方便快捷开发进度 +* 前端:删除不必要的广告页面,和不必要的功能,比如日志,人员职位,登陆等相关信息,只保留用户,角色,目录,部门核心的模块功能 +* 前端:收集封装日常中大佬们封装的一些方法和看到的一些好的方法,实现工具箱似的开发,开箱即用 +* 前端:优化若依框架的部分ui和提取字段,如title,base_path等变为env配置字段和调试时候的vite代理等均有展示 + +### 前端部署 + +``` +# 本地部署 +# 安装依赖(设置镜像源) +yarn --registry=https://registry.npmmirror.com + +# 启动服务 +yarn run dev + +# 打包构建 +yarn build:prod + +# 前端访问地址 +http://localhost:80 + +# api地址修改: +vite.config.js文件 server.proxy.target指向后端地址 + +======================================================================= +# 线上docker部署 +# 前提确认宿主机含有node环境,(调试环境) node_version = v21.1.0, +# 安装了yarn 和vite环境 +# npm install -g yarn vite + +# 安装依赖 +yarn --registry=https://registry.npmmirror.com + +# 启动服务 +yarn run dev + +# 打包构建 +yarn build:prod + +# 前端访问地址 +http://localhost:80 + +# api地址修改: +.env.production文件 VITE_APP_BASE_API 属性 + +# 构建命令 +sh build.sh +``` + +### 后端部署 + +``` +本地部署 +# 进入目录 +cd admin-api + +# 安装依赖 +pip install -r requirements.txt + +# 配置数据库 +/config.py 或者 配置nacos地址 (配置mongo和redis的数据) + +# 启动服务 +python mange.py runserver 0.0.0.0:8000 + +======================================================================= +# docker部署(docker-compose) +docker-compose build && docker-compose up -d + +# 或者执行脚本构建(gunicorn部署,已内置调试) +sh build.sh + +内置一个超级管理员账号 +superAdmin/superAdmin +``` + +### 在线体验 + +演示地址:[http://124.71.212.219:8101/#/login](http://124.71.212.219:8101/#/login) + +账号密码:superAdmin/superAdmin + +如果能帮助到您,快速的构建您的项目,麻烦帮我star一下吧,谢谢咯,如果有问题的请加我微信 + +## 演示图 + +
+ + + + + + + + + + + + + +
\ No newline at end of file diff --git a/admin-api/.gitignore b/admin-api/.gitignore new file mode 100644 index 0000000..79a1148 --- /dev/null +++ b/admin-api/.gitignore @@ -0,0 +1,8 @@ +.idea +/chrome +/logs +/meta +/venv +/static +*.pyc +/excel \ No newline at end of file diff --git a/admin-api/Dockerfile b/admin-api/Dockerfile new file mode 100644 index 0000000..f4cc1ba --- /dev/null +++ b/admin-api/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.7 +WORKDIR /app +COPY . /app +EXPOSE 8085 + +# 安装依赖 +RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ +RUN python -m pip install --upgrade pip +RUN python -m pip install gunicorn +RUN python -m pip install greenlet +RUN python -m pip install eventlet +RUN python -m pip install gevent +RUN python -m pip install -r requirements.txt + +# 创建数据 +RUN python createDatabase.py +RUN python manage.py makemigrations +RUN python manage.py migrate +RUN python initDatabase.py + +# 修改时区 +RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime + +# gunicorn部署 需要部署nginx 存在跨域问题, 配置文件 +CMD ["gunicorn" ,"application.wsgi", "-c", "gunicorn.conf.py"] + +# CMD ["python" ,"manage.py", "runserver", "0.0.0.0:8085"] diff --git a/admin-api/application/__init__.py b/admin-api/application/__init__.py new file mode 100644 index 0000000..c45523b --- /dev/null +++ b/admin-api/application/__init__.py @@ -0,0 +1,2 @@ +import pymysql +pymysql.install_as_MySQLdb() \ No newline at end of file diff --git a/admin-api/application/asgi.py b/admin-api/application/asgi.py new file mode 100644 index 0000000..256c1bc --- /dev/null +++ b/admin-api/application/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for application 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/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') + +application = get_asgi_application() diff --git a/admin-api/application/settings.py b/admin-api/application/settings.py new file mode 100644 index 0000000..5130b36 --- /dev/null +++ b/admin-api/application/settings.py @@ -0,0 +1,110 @@ +""" +Django settings for application project. + +Generated by 'django-admin startproject' using Django 3.2.25. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +from pathlib import Path + +from config import config + +# 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/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-68!wtv#sj9)8*eu7gjr(*9m6v3veg#de!e2(51t)04m$%7abhj' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = config.DEBUG + +ALLOWED_HOSTS = ["*"] + +# Application definition + +INSTALLED_APPS = ['django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', + 'django.contrib.staticfiles', "django_apscheduler", 'corsheaders', "web.apps.WebConfig" + "web", ] + +MIDDLEWARE = ['django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'middleware.myMiddleware.AuthMiddleware', 'middleware.myMiddleware.LogMiddleware', ] + +ROOT_URLCONF = 'application.urls' + +TEMPLATES = [{'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { + 'context_processors': ['django.template.context_processors.debug', 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] + +WSGI_APPLICATION = 'application.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = {'default': {'ENGINE': config.MYSQL_ENGINE, 'HOST': config.MYSQL_HOST, # 数据库ip + 'PORT': config.MYSQL_PORT, # 数据库端口 + 'USER': config.MYSQL_USER, # 用户名 + 'PASSWORD': config.MYSQL_PASSWORD, # 密码 + 'NAME': config.MYSQL_DB, # 数据库名 + }} + +CACHES = {"default": {"BACKEND": "django_redis.cache.RedisCache", "LOCATION": f'redis://:{config.REDIS_PASSWORD or ""}@{config.REDIS_HOST}:6379', + "DECODE_RESPONSES": True, "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient", + "CONNECTION_POOL_KWARGS": {"max_connections": 100, 'decode_responses': True}, + "PASSWORD": config.REDIS_PASSWORD, }}} + +# Password validation +# https://docs.djangoproject.com/en/3.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/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# 允许所有域名跨域(优先选择) +CORS_ORIGIN_ALLOW_ALL = True + +# 跨域白名单请求方式 +CORS_ALLOW_METHODS = ('DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'VIEW',) + +# 跨域白名单请求头 +CORS_ALL_HEADERS = ('XMLHttpRequest', 'accept-encoding', 'authorization', 'content-type', 'origin', 'user-agent', 'x-csrftoken', 'x-requested-with') + +# 关闭浏览器退出登录 +SESSION_EXPIRE_AT_BROWSER_CLOSE = True + +# 解决浏览器不能设置cookie问题 +SESSION_COOKIE_SAMESITE = None +X_FRAME_OPTIONS = 'ALLOWALL' diff --git a/admin-api/application/urls.py b/admin-api/application/urls.py new file mode 100644 index 0000000..c768fed --- /dev/null +++ b/admin-api/application/urls.py @@ -0,0 +1,21 @@ +"""application URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.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')) +""" +from django.urls import path, include + +urlpatterns = [ + path('api/', include('web.urls')), + path('api/system/', include('web.system.urls')), +] diff --git a/admin-api/application/wsgi.py b/admin-api/application/wsgi.py new file mode 100644 index 0000000..89d4bec --- /dev/null +++ b/admin-api/application/wsgi.py @@ -0,0 +1,15 @@ +""" +WSGI config for application 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/3.2/howto/deployment/wsgi/ +""" + +import os +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') + +application = get_wsgi_application() diff --git a/admin-api/build.sh b/admin-api/build.sh new file mode 100644 index 0000000..4238d63 --- /dev/null +++ b/admin-api/build.sh @@ -0,0 +1,3 @@ +git pull +docker-compose build +docker-compose up -d \ No newline at end of file diff --git a/admin-api/config.py b/admin-api/config.py new file mode 100644 index 0000000..4dbc354 --- /dev/null +++ b/admin-api/config.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +""" +@Author:mengying +@file: config.py +@date:2023/12/18 9:35 +@email: 652044581@qq.com +@desc: +""" +import nacos +from addict import Dict +import json + + +class Localconfig: + """本地配置(二选一)""" + + # 项目的名称 + PROJECT_NAME = "django-ruoyi-admin" + + # 配置redis缓存地址 + REDIS_HOST = '124.71.212.219' + REDIS_PORT = 6379 + REDIS_DB = 0 + REDIS_PASSWORD = "Zds2Xacb0kbiSGcs" + + # 配置mongo数据库 + MYSQL_DB = "django_ruoyi_admin" # 注: 数据库名不能用-特殊字符 + MYSQL_HOST = "124.71.212.219" + MYSQL_PORT = 3306 + MYSQL_USER = "root" + MYSQL_PASSWORD = "tpZCI6IiJ9" + MYSQL_ENGINE = 'django.db.backends.mysql' + + # 加密随机串(hash-md5) + ENCRYPT_STRING = "c-QULHn+u=-BUSQ$" + + DEBUG = False + + @classmethod + def get_server_config(cls): + return {item: getattr(cls, item) for item in dir(cls)} + + +class NacosClient: + """nacos配置(二选一)""" + + def __init__(self, addr: str = None, namespace: str = None, data_id: str = None, group_id: str = None, + username: str = "nacos", password: str = "nacos"): + self.addr = addr or "120.46.187.114:8848" + self.namespace = namespace or "7d35aedc-ec57-48c8-8489-9974d19a2942" + self.data_id = data_id or "sigin" + self.group_id = group_id or "dev" + self.client = nacos.NacosClient(self.addr, namespace=self.namespace, username=username, password=password) + + def get_server_config(self): + print(self.client.get_config(self.data_id, self.group_id)) + return json.loads(self.client.get_config(self.data_id, self.group_id)) + + +# nacos的配置 +# config = Dict(NacosClient().get_server_config()) + +# 本地的配置 +config = Dict(Localconfig.get_server_config()) + + +if __name__ == '__main__': + print(config.MYSQL_DB) diff --git a/admin-api/createDatabase.py b/admin-api/createDatabase.py new file mode 100644 index 0000000..4456064 --- /dev/null +++ b/admin-api/createDatabase.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +""" +@Author :mengying +@Date :2024/5/31 17:31 +@Email : 652044581@qq.com +@Desc : 创建数据库 +""" +import pymysql + +from config import config + + +def create_database(): + """项目启动自动创建数据表""" + connection = pymysql.connect(host=config.MYSQL_HOST, port=config.MYSQL_PORT, user=config.MYSQL_USER, passwd=config.MYSQL_PASSWORD) + create_database_sql = f"CREATE DATABASE IF NOT EXISTS {config.MYSQL_DB} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + try: + # 创建游标对象 + with connection.cursor() as cursor: + # 执行SQL语句 + cursor.execute(create_database_sql) + # 提交事务 + connection.commit() + except pymysql.MySQLError as e: + print(f"Error: {e}") + finally: + # 关闭数据库连接 + connection.close() + + +# 初始化创建数据库 +create_database() diff --git a/admin-api/crontab/myScheduler.py b/admin-api/crontab/myScheduler.py new file mode 100644 index 0000000..c155f2f --- /dev/null +++ b/admin-api/crontab/myScheduler.py @@ -0,0 +1,20 @@ +from apscheduler.schedulers.background import BackgroundScheduler +from django_apscheduler.jobstores import DjangoJobStore, register_events, register_job +import uuid + +import datetime + +scheduler = BackgroundScheduler() +scheduler.add_jobstore(DjangoJobStore(), "default") + +run_date = datetime.datetime.now() + datetime.timedelta(seconds=20) + + +@register_job(scheduler, "date", id=uuid.uuid4().hex, run_date=run_date, replace_existing=True, + timezone='Asia/Shanghai') +def setUp_database_scheduler(): + print("Setting up database scheduler") + + +register_events(scheduler) +scheduler.start() diff --git a/admin-api/database/core.py b/admin-api/database/core.py new file mode 100644 index 0000000..e7205e8 --- /dev/null +++ b/admin-api/database/core.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +@Author :mengying +@Date :2024/5/30 15:39 +@Email : 652044581@qq.com +@Desc : 功能描述 +""" +from django.db import models + + +class CoreModel(models.Model): + """ + 核心标准抽象模型 + """ + id = models.BigAutoField(primary_key=True, help_text="Id", verbose_name="Id") + update_time = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间", verbose_name="修改时间") + create_time = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间", verbose_name="创建时间") + remark = models.CharField(max_length=64, blank=True, verbose_name="备注信息", help_text="备注信息") + createBy = models.CharField(max_length=64, blank=True, verbose_name="创建者", help_text="创建者") + objects = models.Manager() + + class Meta: + abstract = True + verbose_name = '核心模型' + verbose_name_plural = verbose_name diff --git a/admin-api/docker-compose.yaml b/admin-api/docker-compose.yaml new file mode 100644 index 0000000..b6403f1 --- /dev/null +++ b/admin-api/docker-compose.yaml @@ -0,0 +1,21 @@ +version: '3.0' + +services: + + django-vue-api-std: + + build: + context: . + dockerfile: Dockerfile + + image: django-vue-api-image-std + + container_name: django-vue-api-container-std + + volumes: + - /opt/django-vue-admin/logs:/app/logs + + ports: + - "8090:8085" + + restart: always \ No newline at end of file diff --git a/admin-api/gunicorn.conf.py b/admin-api/gunicorn.conf.py new file mode 100644 index 0000000..a83b460 --- /dev/null +++ b/admin-api/gunicorn.conf.py @@ -0,0 +1,42 @@ +# 多进程 + +"""gunicorn+gevent 的配置文件""" + +timeout = 60 * 2 + +# 预加载资源 +# preload_app = True +# 绑定 ip + 端口 +bind = "0.0.0.0:8085" +# 进程数 = cup数量 * 2 + 1 +workers = 4 # multiprocessing.cpu_count() * 2 + 1 + +# 线程数 = cup数量 * 2 +threads = 20 + +max_requests = 1000 + +# 等待队列最大长度,超过这个长度的链接将被拒绝连接 +backlog = 2048 + +# 工作模式--协程 +worker_class = "gevent" + +# 最大客户客户端并发数量,对使用线程和协程的worker的工作有影响 +# 服务器配置设置的值 1200:中小型项目 上万并发: 中大型 +# 服务器硬件:宽带+数据库+内存 +# 服务器的架构:集群 主从 +worker_connections = 1200 + +# 进程名称 +proc_name = 'w2p.pid' +# 进程pid记录文件 +pidfile = 'app_run.log' +# 日志等级 +loglevel = 'debug' +# 日志文件名 +logfile = 'debug.log' +# 访问记录 +accesslog = 'access.log' +# 访问记录格式 +access_log_format = '%(h)s %(t)s %(U)s %(q)s' diff --git a/admin-api/initDatabase.py b/admin-api/initDatabase.py new file mode 100644 index 0000000..d370d4d --- /dev/null +++ b/admin-api/initDatabase.py @@ -0,0 +1,386 @@ +# -*- coding: utf-8 -*- +""" +@Author :mengying +@Date :2024/5/31 17:17 +@Email : 652044581@qq.com +@Desc : 初始化数据 +""" +import os, django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') +django.setup() + +import logging +from utils.mySnowflake import Sf +from web.models import SystemInit +from web.models import SystemDept, SystemRole, SystemUser, SystemMenu, SystemUserRole +from django_redis import get_redis_connection +from utils.myEnum import SystemUserTypeEnum +from config import config +from utils.myEncrypt import HashCipher + +from redis import Redis + +RedisClient: Redis = get_redis_connection() +logger = logging.getLogger(__file__) + + +class DatabaseUtils(object): + REDIS_KEY = 'init_database' + + @classmethod + def init_database(cls): + """初始化数据库""" + + # 多节点初始化 + if not RedisClient.setnx(cls.REDIS_KEY, cls.REDIS_KEY): + return + + RedisClient.expire(cls.REDIS_KEY, 10) + hasInit: bool = cls.initFlag() + deptId = Sf.generate() + roleId = Sf.generate() + userId = Sf.generate() + + if not hasInit: + cls.initDept(deptId) + cls.initRole(roleId) + cls.initUser(userId) + cls.initUserRole(userId, roleId) + cls.initMenu() + + RedisClient.delete(cls.REDIS_KEY) + logger.info("init database successfully") + + @classmethod + def initDept(cls, deptId): + initDeptData = { + "createBy": "superAdmin", + "deptId": deptId, + "ancestors": "0", + "parentId": "0", + "deptName": "宇驰商贸有限公司", + "orderNum": 0, + "leader": "admin", + "phone": "17783098377", + "email": "652044581@qq.com", + "status": "0", + "delFlag": "0", + } + SystemDept(**initDeptData).save() + + @classmethod + def initRole(cls, roleId): + """初始化角色超级管理员""" + initRoleData = { + "createBy": "superAdmin", + "roleId": roleId, + "roleName": "超级管理员", + "roleAdmin": True, + "roleKey": "superAdmin", + "roleSort": "0", + "status": "0", + "delFlag": "0", + } + SystemRole(**initRoleData).save() + + @classmethod + def initUser(cls, userId): + initUserData = {"userId": userId, + "username": "superAdmin", + "nickName": "superAdmin", + "password": HashCipher.md5(config.ENCRYPT_STRING + "superAdmin"), + "phone": "17783098375", + "email": "652044581@qq.com", + "status": "0", + "userType": SystemUserTypeEnum.p1.value + } + SystemUser(**initUserData).save() + + @classmethod + def initUserRole(cls, userId, roleId): + """初始化用户角色关系""" + initUserRoleData = {"userId": userId, "roleId": roleId} + SystemUserRole(**initUserRoleData).save() + + @classmethod + def initMenu(cls, ): + """初始化前端菜单""" + menuParentId = Sf.generate() + menuUserId = Sf.generate() + menuRoleId = Sf.generate() + menuMenuId = Sf.generate() + menuDeptId = Sf.generate() + initMenuData = [ + { + "menuId": menuParentId, + "menuName": "系统管理", + "parentId": "0", + "orderNum": 3, + "path": "system", + "component": "", + "perms": "", + "query": "", + "menuType": "M", + "icon": "system", + }, + { + "menuId": menuUserId, + "menuName": "用户管理", + "parentId": menuParentId, + "orderNum": 1, + "path": "user", + "component": "system/user/index", + "perms": "system:user:list", + "query": "", + "menuType": "C", + "icon": "user", + }, + + { + "menuId": menuRoleId, + "menuName": "角色管理", + "parentId": menuParentId, + "orderNum": 2, + "path": "role", + "component": "system/role/index", + "perms": "system:role:list", + "query": "", + "menuType": "C", + "icon": "peoples", + }, + { + "menuId": menuMenuId, + "menuName": "菜单管理", + "parentId": menuParentId, + "orderNum": 3, + "path": "menu", + "component": "system/menu/index", + "perms": "system:menu:list", + "query": "", + "menuType": "C", + "icon": "tree-table", + }, + { + "menuId": menuDeptId, + "menuName": "部门管理", + "parentId": menuParentId, + "orderNum": 4, + "path": "dept", + "component": "system/dept/index", + "perms": "system:dept:list", + "query": "", + "menuType": "C", + "icon": "tree", + }, + { + "menuId": Sf.generate(), + "menuName": "用户查询", + "parentId": menuUserId, + "orderNum": 1, + "path": "", + "component": "", + "perms": "system:user:query", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "用户新增", + "parentId": menuUserId, + "orderNum": 2, + "path": "", + "component": "", + "perms": "system:user:add", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "用户修改", + "parentId": menuUserId, + "orderNum": 3, + "path": "", + "component": "", + "perms": "system:user:edit", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "用户删除", + "parentId": menuUserId, + "orderNum": 4, + "path": "", + "component": "", + "perms": "system:user:remove", + "query": "", + "menuType": "F", + "icon": "#", + }, + + { + "menuId": Sf.generate(), + "menuName": "角色查询", + "parentId": menuRoleId, + "orderNum": 1, + "path": "", + "component": "", + "perms": "system:role:query", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "角色新增", + "parentId": menuRoleId, + "orderNum": 2, + "path": "", + "component": "", + "perms": "system:role:add", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "角色修改", + "parentId": menuRoleId, + "orderNum": 3, + "path": "", + "component": "", + "perms": "system:role:edit", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "角色删除", + "parentId": menuRoleId, + "orderNum": 4, + "path": "", + "component": "", + "perms": "system:role:remove", + "query": "", + "menuType": "F", + "icon": "#", + }, + + { + "menuId": Sf.generate(), + "menuName": "菜单查询", + "parentId": menuMenuId, + "orderNum": 1, + "path": "", + "component": "", + "perms": "system:menu:query", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "菜单新增", + "parentId": menuMenuId, + "orderNum": 2, + "path": "", + "component": "", + "perms": "system:menu:add", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "菜单修改", + "parentId": menuMenuId, + "orderNum": 3, + "path": "", + "component": "", + "perms": "system:menu:edit", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "菜单删除", + "parentId": menuMenuId, + "orderNum": 4, + "path": "", + "component": "", + "perms": "system:menu:remove", + "query": "", + "menuType": "F", + "icon": "#", + }, + + { + "menuId": Sf.generate(), + "menuName": "部门查询", + "parentId": menuDeptId, + "orderNum": 1, + "path": "", + "component": "", + "perms": "system:dept:query", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "部门新增", + "parentId": menuDeptId, + "orderNum": 2, + "path": "", + "component": "", + "perms": "system:dept:add", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "部门修改", + "parentId": menuDeptId, + "orderNum": 3, + "path": "", + "component": "", + "perms": "system:dept:edit", + "query": "", + "menuType": "F", + "icon": "#", + }, + { + "menuId": Sf.generate(), + "menuName": "部门删除", + "parentId": menuDeptId, + "orderNum": 4, + "path": "", + "component": "", + "perms": "system:dept:remove", + "query": "", + "menuType": "F", + "icon": "#", + }, + ] + for item in initMenuData: + SystemMenu(**item).save() + + @classmethod + def initFlag(cls): + """防止多次初始化文件""" + init = SystemInit.objects.all() + if not init: + SystemInit(Init=True).save() + return False + else: + return True + + +DatabaseUtils.init_database() diff --git a/admin-api/manage.py b/admin-api/manage.py new file mode 100644 index 0000000..a773519 --- /dev/null +++ b/admin-api/manage.py @@ -0,0 +1,20 @@ +#!/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', 'application.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() diff --git a/admin-api/middleware/myAuthorization.py b/admin-api/middleware/myAuthorization.py new file mode 100644 index 0000000..d576617 --- /dev/null +++ b/admin-api/middleware/myAuthorization.py @@ -0,0 +1,50 @@ +# -*- coding: -*- +""" +@Author:mengying +@file: myAuthorization.py +@date:2023/6/14 16:46 +@email: 652044581@qq.com +@desc: 授权相关 +""" +import json + +from django_redis import get_redis_connection + + +class CacheKeys: + TOKEN_NAME = "Authorization" + + +class Authorization: + + @staticmethod + def get_user_info(request): + """从header中获取访问人员信息放在header中""" + RedisClient = get_redis_connection() + + token = request.headers.get(CacheKeys.TOKEN_NAME, '') + + if not token: + setattr(request.headers, "user_info", {}) + return None + + user = RedisClient.get(token) + RedisClient.expire(token, 30 * 60) + + if user: + user = json.loads(user) + user["token"] = token + else: + user = {} + + setattr(request.headers, "user_info", user) + + @staticmethod + def white_list_check(request): + """判断访问名路径是否是白名单""" + + white_list = ['/api/login', '/api/captchaImage', '/api/logout'] + if request.path in white_list: + return True + else: + return False diff --git a/admin-api/middleware/myMiddleware.py b/admin-api/middleware/myMiddleware.py new file mode 100644 index 0000000..3038700 --- /dev/null +++ b/admin-api/middleware/myMiddleware.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +""" +@Author :mengying +@Date :2024/5/30 14:16 +@Email : 652044581@qq.com +@Desc : 功能描述 +""" +import logging +import time +import traceback +import uuid + +from django.http.response import JsonResponse + +from middleware.myAuthorization import Authorization +from utils.myResFormat import ResultJson, ResultCode + +logger = logging.getLogger(__file__) + +from django.utils.deprecation import MiddlewareMixin + + +class AuthMiddleware(MiddlewareMixin): + """用户认证中间件""" + + def process_request(self, request): + # 获取用户信息 + Authorization.get_user_info(request) + + # 检查白名单 + if not Authorization.white_list_check(request): + user_info = getattr(request.headers, 'user_info', {}) + if not user_info: + return JsonResponse(ResultJson(ResultCode.TOKEN_ERROR).result) + + def process_response(self, request, response): # 基于请求响应 + return response + + def process_exception(self, request, exception): + + formatter = ["接口地址 : %s" % request.get_full_path(), "请求方式 : %s" % request.method, "请求参数 : body: %s" % request.body.decode(), + "用户信息 : user: %s" % str(getattr(request.headers, 'user_info', {})), "报错信息 : %s" % str(traceback.format_exc())] + error_message = "\n".join(formatter) + logger.error(error_message) + return JsonResponse(ResultJson(ResultCode.SERVER_ERROR, description=str(exception)).result) + + +class LogMiddleware(MiddlewareMixin): + """记录日志中间件""" + + def process_request(self, request): + uid = uuid.uuid4().hex + setattr(request.headers, "my-duration", time.time()) + setattr(request.headers, "uid", uid) + self.recordsRequest(uid, request.get_full_path(), request.method, request.body.decode()) + + def process_response(self, request, response): # 基于请求响应 + duration = time.time() - getattr(request.headers, "my-duration") + uid = getattr(request.headers, "uid") + self.recordsResponse(uid, request.get_full_path(), response.content.decode(), duration) + return response + + @classmethod + def white_path(cls, path): + request_white_path = [] + return path in request_white_path + + @classmethod + def recordsRequest(cls, uid, path, method, data): + request_info = '链路id: %s 接口: %s 请求方式: %s body参数: %s' % (uid, path, method, data) + if cls.white_path(path): + return + logger.info(str(request_info)) + + @classmethod + def recordsResponse(cls, uid, path, data, duration=None): + response_info = '链路id: %s 接口: %s 返回数据: %s 耗时: %s ' % (uid, path, data, duration) + if cls.white_path(path): + return + logger.info(str(response_info)) diff --git a/admin-api/requirements.txt b/admin-api/requirements.txt new file mode 100644 index 0000000..c80be78 Binary files /dev/null and b/admin-api/requirements.txt differ diff --git a/admin-api/utils/myDataUtils.py b/admin-api/utils/myDataUtils.py new file mode 100644 index 0000000..505fb12 --- /dev/null +++ b/admin-api/utils/myDataUtils.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +""" +======================================================================================================================== +@project : my-sanic +@file: myDataUtils +@Author: mengying +@email: 652044581@qq.com +@date: 2023/3/23 15:54 +@desc: 数据的处理类 +======================================================================================================================== +""" +import jmespath + + +class JsonLocator: + """ + json数据指定位置的快速查询 + 使用教程参见: https://blog.csdn.net/be5yond/article/details/118976017 + """ + + def __init__(self, json_data): + self.json_data = json_data + + def locate(self, query): + return jmespath.search(query, self.json_data) + + +class JsonEditor: + """json数据指定位置的快速修改""" + + def __init__(self, json_data): + self.json_data = json_data + + def edit(self, path, new_value): + keys = path.split('.') + data = self.json_data + for index, key in enumerate(keys): + if key.isdigit(): + keys[index] = int(key) + for key in keys[:-1]: + data = data[key] + data[keys[-1]] = new_value + + +class JsonModifier: + """json数据删除value的bool值是false的值, indent为True只控制第一层级,False删除json全部层级""" + + @staticmethod + def deleteBoolKey(json_data, indent=True): + if isinstance(json_data, dict): + for key, value in list(json_data.items()): + if not bool(value): + del json_data[key] + if indent: + return json_data + else: + JsonModifier.deleteBoolKey(value) + elif isinstance(json_data, list): + for item in json_data: + JsonModifier.deleteBoolKey(item) + return json_data + + +class JsonValidator: + """校验json数据是否包含列表字段""" + + def __init__(self, json_data, fields_list): + self.json_data = json_data + self.fields_list = fields_list + + def validate(self): + for field in self.fields_list: + if field not in self.json_data: + return False + return True + + +class JsonComparator: + """比较2个json数据是否包含""" + + def __init__(self, big_json, small_json): + self.big_json = big_json + self.small_json = small_json + + def compare(self): + for key, value in self.small_json.items(): + if key not in self.big_json: + return False + if isinstance(value, dict): + if not JsonComparator(self.big_json[key], value).compare(): + return False + elif self.big_json[key] != value: + return False + return True + + +class JsonDiff: + """比较2个json数据,返回json diff的差异数据""" + + def __init__(self, origin_json, diff_json): + self.origin_json = origin_json + self.diff_json = diff_json + self.container = {} + + def compare(self): + for key, value in self.origin_json.items(): + diff_value = self.diff_json.get(key) + if diff_value is not None and value != diff_value: + self.container[key] = value + return self.container + + +class JsonRemover: + """删除指定列表,json里面的key""" + + def __init__(self, json_data): + self.json_data = json_data + + def remove_fields(self, fields_to_remove): + json_keys = self.json_data.keys() + for field in fields_to_remove: + if field in json_keys: + self.json_data.pop(field, None) + return self.json_data + + +class JsonKeeper: + """只保留指定列表,json里面的key""" + + def __init__(self, json_data): + self.json_data = json_data + self.container = {} + + def keeper_fields(self, fields_to_keeper): + json_keys = self.json_data.keys() + for field in fields_to_keeper: + if field in json_keys: + self.container[field] = self.json_data[field] + return self.container + + +class JsonSwap: + """交换json的键值的位置""" + + def __init__(self, json_data): + self.json_data = json_data + + def swap(self): + return {v: k for k, v in self.json_data.items()} + + +class ListFilter: + """过滤数据结构 list[dict]中dict满足某种条件的数据""" + + def __init__(self, data, condition): + + self.data = data + self.condition = condition + self.keeper = [] + self.filter = [] + + def filter(self, reverse=False): + for item in self.data: + threshold = JsonComparator(big_json=item, small_json=self.condition).compare() + if not threshold: + self.keeper.append(threshold) + else: + self.filter.append(threshold) + return self.keeper if reverse else self.filter + + +class List2Dict: + """把list[dict]合并成dict, 注:如果dict里面存在相同字段会被覆盖,特定数据结构使用""" + + def __init__(self, data_list): + self.data_list = data_list + self.container = {} + + def merge(self): + for d in self.data_list: + self.container.update(d) + return self.container + + +class TreeBuilder: + """列表数据根据指定的parentId和字数的id组装成关系树""" + + def __init__(self, data): + self.data: list = data + self.tree: dict = {} + + def build(self, parentKey: str = "parentId", ownerKey: str = "id", topParent: str = None): + node_dict = {item[ownerKey]: item for item in self.data} + for item in self.data: + if item[parentKey] is topParent: + self.tree[item[ownerKey]] = item + else: + parent = node_dict.get(item[parentKey]) + if parent: + if 'children' not in parent: + parent['children'] = [] + parent['children'].append(item) + return self.tree + + +if __name__ == '__main__': + # Example 1 JsonLocator + json_data = { + "name": { + "first": "John", + "last": "Doe" + }, + "age": 25, + "address": { + "street": "123 Main St", + "city": "Anytown", + "state": "CA", + "zip": "12345" + } + } + + locator = JsonLocator(json_data) + print(locator.locate("name.first")) # Output: John + + # Example 1 JsonEditor + json_data = { + "name": "John", + "age": 30, + "cars": { + "car1": "Ford", + "car2": "BMW", + "car3": "Fiat" + } + } + editor = JsonEditor(json_data) + editor.edit('cars.car2', 'Mercedes') + print(json_data) + + # Example 2 JsonEditor + json_data = { + "name": "John", + "age": 30, + "cars": [ + {"name": "Ford", "models": ["Fiesta", "Focus", "Mustang"]}, + {"name": "BMW", "models": ["320", "X3", "X5"]}, + {"name": "Fiat", "models": ["500", "Panda"]} + ] + } + editor = JsonEditor(json_data) + editor.edit('cars.1.models.2', 'X6') + print(json_data) + + # Example 3 JsonEditor + json_data = { + "name": "John", + "age": 30, + "address": { + "street": "Main Street", + "city": "New York", + "state": "NY", + "zip": "10001" + } + } + editor = JsonEditor(json_data) + editor.edit('address.zip', '10002') + print(json_data) + + # Example 1 JsonModifier + json_data = { + "name": "John", + "age": 30, + "married": "", + "pets": [ + { + "animal": "1", + "name": "Fido", + "is_alive": True + }, + { + "animal": "cat", + "name": "Fluffy", + "is_alive": {} + } + ] + } + + json_data = JsonModifier.deleteBoolKey(json_data, indent=False) + print(json_data) + + data = [{'id': '1739096268195635200', 'parentId': '0', 'component': 'Layout', 'hidden': False, 'path': '/system', 'name': 'System', 'redirect': 'noRedirect', 'meta': {'icon': 'system', 'link': '', 'noCache': False, 'title': '系统管理'}}, {'id': '1739096268195635201', 'parentId': '1739096268195635200', 'component': '0', 'hidden': False, 'path': 'user', 'name': 'User', 'redirect': 'noRedirect', 'meta': {'icon': 'user', 'link': '', 'noCache': False, 'title': '用户管理'}}, {'id': '1739096268195635202', 'parentId': '1739096268195635200', 'component': '0', 'hidden': False, 'path': 'role', 'name': 'Role', 'redirect': 'noRedirect', 'meta': {'icon': 'peoples', 'link': '', 'noCache': False, 'title': '角色管理'}}, {'id': '1739096268195635203', 'parentId': '1739096268195635200', 'component': '0', 'hidden': False, 'path': 'menu', 'name': 'Menu', 'redirect': 'noRedirect', 'meta': {'icon': 'tree-table', 'link': '', 'noCache': False, 'title': '菜单管理'}}, {'id': '1739096268195635204', 'parentId': '1739096268195635200', 'component': '0', 'hidden': False, 'path': 'dept', 'name': 'Dept', 'redirect': 'noRedirect', 'meta': {'icon': 'tree', 'link': '', 'noCache': False, 'title': '部门管理'}}] + print(TreeBuilder(data).build(parentKey="parentId", ownerKey="id", topParent="0")) diff --git a/admin-api/utils/myEncrypt.py b/admin-api/utils/myEncrypt.py new file mode 100644 index 0000000..ae6fc1f --- /dev/null +++ b/admin-api/utils/myEncrypt.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +""" +======================================================================================================================== +@Author: 孟颖 +@email: 652044581@qq.com +@date: 2023/4/20 10:19 +@desc: 加密解密模块 +======================================================================================================================== +""" + +import base64 +import hashlib + + +class HashCipher: + + @staticmethod + def md5(message): + """ + Returns the MD5 hash of the message + """ + return hashlib.md5(message.encode()).hexdigest() + + @staticmethod + def sha1(message): + """ + Returns the SHA1 hash of the message + """ + return hashlib.sha1(message.encode()).hexdigest() + + @staticmethod + def sha256(message): + """ + Returns the SHA256 hash of the message + """ + return hashlib.sha256(message.encode()).hexdigest() + + @staticmethod + def sha512(message): + """ + Returns the SHA512 hash of the message + """ + return hashlib.sha512(message.encode()).hexdigest() + + +class Base64Cipher: + @staticmethod + def bytes_to_base64(bytes_data): + """ + Converts bytes to base64 string + """ + return base64.b64encode(bytes_data).decode('utf-8') + + @staticmethod + def base64_to_bytes(base64_string): + """ + Converts base64 string to bytes + """ + return base64.b64decode(base64_string.encode('utf-8')) diff --git a/admin-api/utils/myEnum.py b/admin-api/utils/myEnum.py new file mode 100644 index 0000000..017a920 --- /dev/null +++ b/admin-api/utils/myEnum.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" +======================================================================================================================== +@Author: 孟颖 +@email: 652044581@qq.com +@date: 2023/4/20 10:19 +@desc: 枚举模块自定义dict转换 +======================================================================================================================== +""" +from enum import Enum, unique + + +class EnumDict(Enum): + + @classmethod + def transform(cls): + res = dict() + for key, value in cls.__members__.items(): + res[key] = value.value + return res + + @classmethod + def reverse_transform(cls): + res = dict() + for key, value in cls.__members__.items(): + res[value.value] = key + return res + + @classmethod + def format_front(cls): + return [{"label": key, "value": value.value} for key, value in cls.__members__.items()] + + @classmethod + def get_value(cls, key): + enum_map = cls.transform() + return enum_map.get(key) + + +@unique +class SystemStatusEnum(EnumDict): + p0 = "0" # 0正常 + p1 = "1" # 1停用 + + +@unique +class SystemDelEnum(EnumDict): + p0 = "0" # 0存在 + p1 = "2" # 1删除 + + +@unique +class SystemUserTypeEnum(EnumDict): + p0 = "00" # 系统账号 + p1 = "01" # 管理员账号 diff --git a/admin-api/utils/myResFormat.py b/admin-api/utils/myResFormat.py new file mode 100644 index 0000000..0903836 --- /dev/null +++ b/admin-api/utils/myResFormat.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" +======================================================================================================================== +@file: myResFormat +@Author: mengying +@email: 652044581@qq.com +@date: 2023/3/3 12:14 +@desc: 返回的数据格式定义 +======================================================================================================================== +""" +from addict import Dict + + +class ResultCode: + """ + @param: code返回码 + @param: msg信息 + @param: description描述 + @desc: 返回码 + """ + SUCCESS = (200, '操作成功', None) + FAIL = (500, '操作失败', None) + TOKEN_ERROR = (401, 'token过期', None) + SERVER_ERROR = (500, '服务错误', None) + DEPT_ERROR = (500, '部门名称重复', None) + ROLE_ERROR = (500, '角色名称或权限字符重复', None) + USER_ERROR = (500, '用户名已存在', None) + MENU_ERROR = (500, '目录名称重复', None) + PASSWORD_ERROR = (500, '密码错误', None) + AUTH_CODE_EXP = (500, '验证码过期', None) + AUTH_CODE_ERROR = (500, '验证码错误', None) + PRE_AUTH_ERROR = (400, '权限不足', None) + USER_NOT_EXIST = (500, '用户不存在', None) + USER_NOT_ALLOW = (500, '用户被禁用,请联系系统管理员', None) + + +class ResultJson: + result = Dict() + + def __init__(self, ret=ResultCode.SUCCESS, data=None, description=None, postscript=None): + self.result.code = ret[0] + self.result.msg = ret[1] % postscript if postscript else ret[1] + self.result.data = data + self.result.description = description or ret[2] + + +if __name__ == '__main__': + print(ResultJson(ret=ResultCode.SUCCESS, data=None, description="成功").result) diff --git a/admin-api/utils/mySnowflake.py b/admin-api/utils/mySnowflake.py new file mode 100644 index 0000000..9b83141 --- /dev/null +++ b/admin-api/utils/mySnowflake.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +""" +======================================================================================================================== +@project : my-sanic +@file: myUUID +@Author: mengying +@email: 652044581@qq.com +@date: 2023/4/4 12:14 +@desc: 雪花算法生成分布式id,用户订单生成相关或者uuid使用 +======================================================================================================================== +""" + +import time +from threading import Lock + +# 64 位 id 的划分,通常机器位和数据位各为 5 位 +WORKER_ID_BITS = 5 # 机器位 +DATACENTER_ID_BITS = 5 # 数据位 +SEQUENCE_BITS = 12 # 循环位 + +# 最大取值计算,计算机中负数表示为他的补码 +MAX_WORKER_ID = -1 ^ (-1 << WORKER_ID_BITS) # 2**5 -1 =31 +MAX_DATACENTER_ID = -1 ^ (-1 << DATACENTER_ID_BITS) + +# 移位偏移计算 +WORKER_ID_SHIFT = SEQUENCE_BITS +DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS +TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS + +# X序号循环掩码 +SEQUENCE_MASK = -1 ^ (-1 << SEQUENCE_BITS) + +# Twitter 元年时间戳 +TWEPOCH = 1288834974657 + + +class SingletonMeta(type): + """元类单例""" + __instance = None + __lock = Lock() + + def __call__(cls, *args, **kwargs): + with cls.__lock: + new = kwargs.pop('new', None) + if new is True: + return super().__call__(*args, **kwargs) + if not cls.__instance: + cls.__instance = super().__call__(*args, **kwargs) + return cls.__instance + + +class SnowIdWorker(metaclass=SingletonMeta): + """不能实例多次,会生产一样的id""" + + def __init__(self, datacenter_id=1, worker_id=2, sequence=0): + """ + 初始化方法 + :param datacenter_id:数据id + :param worker_id:机器id + :param sequence:序列码 + """ + if worker_id > MAX_WORKER_ID or worker_id < 0: + raise ValueError('worker_id 值越界') + if datacenter_id > MAX_DATACENTER_ID or datacenter_id < 0: + raise ValueError('datacenter_id 值越界') + + self.worker_id = worker_id + self.datacenter_id = datacenter_id + self.sequence = sequence + + self.last_timestamp = -1 # 上次计算的时间戳 + + def _gen_timestamp(self): + """ + 生成整数时间戳。 + :return: + """ + return int(time.time() * 1000) + + def generate(self) -> str: + """ + 获取新的ID. + :return: + """ + # 获取当前时间戳 + timestamp = self._gen_timestamp() + + # 时钟回拨的情况 + if timestamp < self.last_timestamp: + raise Exception("clock is moving backwards") + + if timestamp == self.last_timestamp: + # 同一毫秒的处理。 + self.sequence = (self.sequence + 1) & SEQUENCE_MASK + if self.sequence == 0: + timestamp = self._til_next_millis(self.last_timestamp) + else: + self.sequence = 0 + + self.last_timestamp = timestamp + + new_id = (((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) | (self.datacenter_id << DATACENTER_ID_SHIFT) | ( + self.worker_id << WORKER_ID_SHIFT)) | self.sequence + return str(new_id) + + def _til_next_millis(self, last_timestamp): + """ + 等到下一毫秒。 + :param last_timestamp: + :return: + """ + timestamp = self._gen_timestamp() + while timestamp <= last_timestamp: + timestamp = self._gen_timestamp() + return timestamp + + +Sf = SnowIdWorker() + +if __name__ == '__main__': + for i in range(1000): + print(Sf.generate()) diff --git a/admin-api/utils/myTimeFormat.py b/admin-api/utils/myTimeFormat.py new file mode 100644 index 0000000..6e6c2f4 --- /dev/null +++ b/admin-api/utils/myTimeFormat.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" +@Author: 孟颖 +@email: 652044581@qq.com +@date: 2023/4/23 10:03 +@desc: 时间类,生成各类的时间和时间格式转换 +""" +import time +from datetime import datetime, timedelta + + +class MyTimeUtils: + dateTimeType = '%Y-%m-%d %H:%M:%S' + dateType = '%Y-%m-%d' + timeType = '%H:%M:%S' + fileTimeType = "%Y%m%d%H%M%S%f" + fileTimeShortType = "%Y%m%d%H%M%S" + + @classmethod + def TimeFormat(cls, now_time: datetime = datetime.now(), timeType: str = dateTimeType) -> str: + return now_time.strftime(timeType) + + @classmethod + def TimeFormatCh(cls, now_time: datetime = datetime.now()) -> str: + ch_time = now_time.strftime(cls.dateType) + return "%s年%s月%s日" % tuple(ch_time.split("-")) + + @classmethod + def TimestampFormat(cls, long: bool = False) -> int: + return int(time.time() * 1000) if long else int(time.time()) + + @classmethod + def TimeOffsetFormat(cls, start_time: datetime = datetime.now(), days: int = 0, hour: int = 0, minute: int = 0, second: int = 0, + timeType: str = dateTimeType) -> str: + offsetDateTime = start_time + timedelta(days=days, hours=hour, minutes=minute, seconds=second) + return cls.TimeFormat(offsetDateTime, timeType) + + +if __name__ == '__main__': + print(MyTimeUtils.TimeOffsetFormat(days=-1, timeType=MyTimeUtils.fileTimeType)) + print(MyTimeUtils.TimeFormatCh()) diff --git a/admin-api/web/__init__.py b/admin-api/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/admin-api/web/admin.py b/admin-api/web/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/admin-api/web/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/admin-api/web/apps.py b/admin-api/web/apps.py new file mode 100644 index 0000000..fa72239 --- /dev/null +++ b/admin-api/web/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class WebConfigweb(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'web' diff --git a/admin-api/web/captchaImage.py b/admin-api/web/captchaImage.py new file mode 100644 index 0000000..246ef40 --- /dev/null +++ b/admin-api/web/captchaImage.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +""" +@Author: 孟颖 +@email: 652044581@qq.com +@date: 2023/4/13 13:36 +@desc: 生成验证码 +""" +import pathlib +import random +from PIL import Image, ImageDraw, ImageFont +from io import BytesIO +import base64 +from pathlib import Path + + +class CaptchaImage: + + def __init__(self, width=130, height=35, string_count=4, font_size=30, noise_line=3, noise_point=30): + """ + :param width: 图片宽度 + :param height: 图片高度 + :param string_count: 验证码的数量 + :param font_size: 字体的大小 + :param noise_line: 噪声线的数量 + :param noise_point: 噪声点的数量 + """ + self.width = width + self.height = height + self.string_count = string_count + self.font_size = font_size + self.noise_line = noise_line + self.noise_point = noise_point + + # 生成验证码 + def generate(self, width=120, height=35): + image = self.background() + captcha_string, image = self.draw_string(image) + image: Image = self.noise(image) + image = image.resize((width, height), resample=Image.BICUBIC) + output_buffer = BytesIO() + image.save(output_buffer, format='png') + binary_data = output_buffer.getvalue() + image_base64 = self.io2base64(binary_data) + return captcha_string, image_base64 + + # io转base64 + def io2base64(self, content: bytes): + encode_data = base64.b64encode(content) + return str(encode_data, encoding='utf-8') + + # 生成背景 + def background(self): + background_color = (255, 255, 255) + image = Image.new('RGB', (self.width, self.height), background_color) + return image + + # 随机颜色 + def random_color(self): + c1 = random.randint(0, 200) + c2 = random.randint(0, 200) + c3 = random.randint(0, 200) + return c1, c2, c3 + + # 生成随机字符串 + def random_string(self): + random_num = str(random.randint(0, 9)) + random_low_alpha = chr(random.randint(97, 122)) + return random.choice([random_num, random_low_alpha]) + + # 合成文字 + def draw_string(self, image: Image): + draw = ImageDraw.Draw(image) + font_file = pathlib.Path.joinpath(Path(__file__).parent, "static/fontBold.ttf").as_posix() + font = ImageFont.truetype(font=font_file, size=self.font_size) + string_container = [] + for i in range(self.string_count): + random_char = self.random_string() + draw.text((10 + i * 30, -2), random_char, self.random_color(), font=font) + string_container.append(random_char) + captcha_string = "".join(string_container) + return captcha_string, image + + # 生成噪声 + def noise(self, image: Image): + + draw = ImageDraw.Draw(image) + # 噪声线 + for i in range(self.noise_line): + x1 = random.randint(0, self.width) + x2 = random.randint(0, self.width) + y1 = random.randint(0, self.height) + y2 = random.randint(0, self.height) + draw.line((x1, y1, x2, y2), fill=self.random_color()) + + # 噪声点 + for i in range(self.noise_point): + draw.point([random.randint(0, self.width), random.randint(0, self.height)], fill=self.random_color()) + x = random.randint(0, self.width) + y = random.randint(0, self.height) + draw.arc((x, y, x + 4, y + 4), 0, 90, fill=self.random_color()) + + return image + + +if __name__ == '__main__': + captcha = CaptchaImage() + + # 生成验证码图片和字符串 + captcha_string, image_base64 = captcha.generate(width=108, height=36) diff --git a/admin-api/web/migrations/0001_initial.py b/admin-api/web/migrations/0001_initial.py new file mode 100644 index 0000000..fd35f32 --- /dev/null +++ b/admin-api/web/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.25 on 2024-05-31 04:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='SystemDept', + fields=[ + ('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')), + ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), + ('parentId', models.CharField(default='', help_text='父id', max_length=64, verbose_name='父id')), + ('deptName', models.CharField(default='', help_text='部门名称', max_length=64, verbose_name='部门名称')), + ('deptId', models.CharField(default='', help_text='部门id', max_length=64, unique=True, verbose_name='部门id')), + ('orderNum', models.IntegerField(default=0, help_text='排序', verbose_name='排序')), + ('leader', models.CharField(default='', help_text='负责人', max_length=64, verbose_name='负责人')), + ('phone', models.CharField(default='', help_text='联系电话', max_length=64, verbose_name='联系电话')), + ('email', models.CharField(default='', help_text='邮箱', max_length=64, verbose_name='邮箱')), + ('status', models.CharField(default='', help_text='启用状态 (0正常 1停用)', max_length=64, verbose_name='启用状态')), + ('delFlag', models.CharField(default='', help_text='是否删除 (0存在 2删除)', max_length=64, verbose_name='是否删除')), + ('ancestors', models.CharField(default='', help_text='上级目录id', max_length=64, verbose_name='上级目录id')), + ('createBy', models.CharField(default='', help_text='创建者', max_length=64, verbose_name='创建者')), + ], + options={ + 'verbose_name': '部门管理', + 'db_table': 'system_dept', + }, + ), + ] diff --git a/admin-api/web/migrations/0002_auto_20240531_1456.py b/admin-api/web/migrations/0002_auto_20240531_1456.py new file mode 100644 index 0000000..d62a8f9 --- /dev/null +++ b/admin-api/web/migrations/0002_auto_20240531_1456.py @@ -0,0 +1,174 @@ +# Generated by Django 3.2.25 on 2024-05-31 06:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SystemInit', + fields=[ + ('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')), + ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), + ('remark', models.CharField(blank=True, help_text='备注信息', max_length=64, verbose_name='备注信息')), + ('createBy', models.CharField(blank=True, help_text='创建者', max_length=64, verbose_name='创建者')), + ('Init', models.BooleanField(blank=True, default=False, help_text='是否已经初始化数据库')), + ], + options={ + 'verbose_name': '核心模型', + 'verbose_name_plural': '核心模型', + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SystemMenu', + fields=[ + ('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')), + ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), + ('remark', models.CharField(blank=True, help_text='备注信息', max_length=64, verbose_name='备注信息')), + ('createBy', models.CharField(blank=True, help_text='创建者', max_length=64, verbose_name='创建者')), + ('menuId', models.CharField(default='', help_text='菜单ID', max_length=64, unique=True, verbose_name='菜单ID')), + ('menuName', models.CharField(default='', help_text='菜单名称', max_length=64, verbose_name='菜单名称')), + ('parentId', models.CharField(default='', help_text='父菜单ID', max_length=64, verbose_name='父菜单ID')), + ('orderNum', models.IntegerField(default=0, help_text='显示顺序', verbose_name='显示顺序')), + ('path', models.CharField(default='', help_text='路由地址', max_length=64, verbose_name='路由地址')), + ('component', models.CharField(default='', help_text='组件路径', max_length=64, verbose_name='组件路径')), + ('query', models.CharField(default='', help_text='路由参数', max_length=64, verbose_name='路由参数')), + ('isFrame', models.CharField(default='', help_text='是否为外链(0是 1否)', max_length=4, verbose_name='是否为外链(0是 1否)')), + ('isCache', models.CharField(default='', help_text='是否缓存(0缓存 1不缓存)', max_length=4, verbose_name='是否缓存(0缓存 1不缓存)')), + ('visible', models.CharField(default='', help_text='菜单状态(0显示 1隐藏)', max_length=4, verbose_name='菜单状态(0显示 1隐藏)')), + ('menuType', models.CharField(default='', help_text='菜单类型(M目录 C菜单 F按钮)', max_length=4, verbose_name='菜单类型(M目录 C菜单 F按钮)')), + ('status', models.CharField(default='', help_text='菜单状态(0正常 1停用)', max_length=4, verbose_name='菜单状态(0正常 1停用)')), + ('perms', models.CharField(default='', help_text='权限标识', max_length=32, verbose_name='权限标识')), + ('icon', models.CharField(default='', help_text='菜单图', max_length=32, verbose_name='菜单图')), + ], + options={ + 'verbose_name': '目录管理', + 'db_table': 'system_menu', + }, + ), + migrations.CreateModel( + name='SystemRole', + fields=[ + ('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')), + ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), + ('remark', models.CharField(blank=True, help_text='备注信息', max_length=64, verbose_name='备注信息')), + ('createBy', models.CharField(blank=True, help_text='创建者', max_length=64, verbose_name='创建者')), + ('roleId', models.CharField(default='', help_text='角色id', max_length=64, unique=True, verbose_name='角色id')), + ('roleName', models.CharField(default='', help_text='角色名称', max_length=64, verbose_name='角色名称')), + ('roleKey', models.CharField(default='', help_text='角色权限字符', max_length=64, verbose_name='角色权限字符')), + ('roleSort', models.IntegerField(default=0, help_text='角色排序', verbose_name='角色排序')), + ('roleAdmin', models.BooleanField(default=False, help_text='是否是超级管理员', max_length=64, verbose_name='是否是超级管理员')), + ('status', models.CharField(default='0', help_text='启用状态 (0正常 1停用)', max_length=4, verbose_name='启用状态 (0正常 1停用)')), + ('menuCheckStrictly', models.BooleanField(default=True, help_text='菜单树选择项是否关联显示', verbose_name='菜单树选择项是否关联显示')), + ('deptCheckStrictly', models.BooleanField(default=True, help_text='部门树选择项是否关联显示', verbose_name='部门树选择项是否关联显示')), + ('delFlag', models.CharField(default='0', help_text='是否删除 (0代表存在 2代表删除)', max_length=4, verbose_name='是否删除 (0代表存在 2代表删除)')), + ], + options={ + 'verbose_name': '角色管理', + 'db_table': 'system_role', + }, + ), + migrations.CreateModel( + name='SystemRoleMenu', + fields=[ + ('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')), + ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), + ('remark', models.CharField(blank=True, help_text='备注信息', max_length=64, verbose_name='备注信息')), + ('createBy', models.CharField(blank=True, help_text='创建者', max_length=64, verbose_name='创建者')), + ('menuId', models.CharField(default='', help_text='菜单id', max_length=64, verbose_name='菜单id')), + ('roleId', models.CharField(default='', help_text='角色id', max_length=64, verbose_name='角色id')), + ], + options={ + 'verbose_name': '角色与目录关系映射', + 'db_table': 'system_role_menu', + }, + ), + migrations.CreateModel( + name='SystemUser', + fields=[ + ('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')), + ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), + ('remark', models.CharField(blank=True, help_text='备注信息', max_length=64, verbose_name='备注信息')), + ('createBy', models.CharField(blank=True, help_text='创建者', max_length=64, verbose_name='创建者')), + ('userId', models.CharField(default='', help_text='用户id', max_length=64, unique=True, verbose_name='用户id')), + ('deptId', models.CharField(default='', help_text='部门id', max_length=64, verbose_name='部门id')), + ('username', models.CharField(default='', help_text='用户账号', max_length=64, verbose_name='用户账号')), + ('nickName', models.CharField(default='', help_text='用户昵称', max_length=64, verbose_name='用户昵称')), + ('userType', models.CharField(default=False, help_text='用户类型(00系统用户)', max_length=4, verbose_name='用户类型(00系统用户)')), + ('email', models.CharField(default='', help_text='邮箱', max_length=64, verbose_name='邮箱')), + ('phone', models.CharField(default='', help_text='手机号码', max_length=64, verbose_name='手机号码')), + ('avatar', models.CharField(default='', help_text='头像', max_length=64, verbose_name='头像')), + ('password', models.CharField(default='', help_text='密码(加密后)', max_length=64, verbose_name='密码(加密后)')), + ('status', models.CharField(default='0', help_text='帐号状态(0正常 1停用)', max_length=4, verbose_name='帐号状态(0正常 1停用)')), + ('delFlag', models.CharField(default='0', help_text='删除标志(0代表存在 2代表删除)', max_length=4, verbose_name='删除标志(0代表存在 2代表删除)')), + ('loginIp', models.CharField(default='', help_text='最后登陆ip', max_length=64, verbose_name='最后登陆ip')), + ('loginDate', models.CharField(default='', help_text='最后登陆日期', max_length=64, verbose_name='最后登陆日期')), + ], + options={ + 'verbose_name': '用户管理', + 'db_table': 'system_user', + }, + ), + migrations.CreateModel( + name='SystemUserRole', + fields=[ + ('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')), + ('create_time', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), + ('remark', models.CharField(blank=True, help_text='备注信息', max_length=64, verbose_name='备注信息')), + ('createBy', models.CharField(blank=True, help_text='创建者', max_length=64, verbose_name='创建者')), + ('userId', models.CharField(default='', help_text='用户id', max_length=64, verbose_name='菜单id')), + ('roleId', models.CharField(default='', help_text='角色id', max_length=64, verbose_name='角色id')), + ], + options={ + 'verbose_name': '角色与人员关系映射', + 'db_table': 'system_role_user', + }, + ), + migrations.AddField( + model_name='systemdept', + name='remark', + field=models.CharField(blank=True, help_text='备注信息', max_length=64, verbose_name='备注信息'), + ), + migrations.AlterField( + model_name='systemdept', + name='createBy', + field=models.CharField(blank=True, help_text='创建者', max_length=64, verbose_name='创建者'), + ), + migrations.AlterField( + model_name='systemdept', + name='delFlag', + field=models.CharField(default='0', help_text='是否删除 (0存在 2删除)', max_length=4, verbose_name='是否删除'), + ), + migrations.AlterField( + model_name='systemdept', + name='email', + field=models.CharField(default='', help_text='邮箱', max_length=32, verbose_name='邮箱'), + ), + migrations.AlterField( + model_name='systemdept', + name='leader', + field=models.CharField(default='', help_text='负责人', max_length=32, verbose_name='负责人'), + ), + migrations.AlterField( + model_name='systemdept', + name='phone', + field=models.CharField(default='', help_text='联系电话', max_length=32, verbose_name='联系电话'), + ), + migrations.AlterField( + model_name='systemdept', + name='status', + field=models.CharField(default='0', help_text='启用状态 (0正常 1停用)', max_length=4, verbose_name='启用状态'), + ), + ] diff --git a/admin-api/web/migrations/0003_auto_20240531_2104.py b/admin-api/web/migrations/0003_auto_20240531_2104.py new file mode 100644 index 0000000..5f7b09b --- /dev/null +++ b/admin-api/web/migrations/0003_auto_20240531_2104.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.25 on 2024-05-31 13:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0002_auto_20240531_1456'), + ] + + operations = [ + migrations.AlterModelOptions( + name='systeminit', + options={'verbose_name': '部门管理'}, + ), + migrations.AlterModelTable( + name='systeminit', + table='system_init', + ), + ] diff --git a/admin-api/web/migrations/0004_auto_20240614_1628.py b/admin-api/web/migrations/0004_auto_20240614_1628.py new file mode 100644 index 0000000..27cdf7b --- /dev/null +++ b/admin-api/web/migrations/0004_auto_20240614_1628.py @@ -0,0 +1,58 @@ +# Generated by Django 3.2.25 on 2024-06-14 08:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0003_auto_20240531_2104'), + ] + + operations = [ + migrations.AlterField( + model_name='systemmenu', + name='isCache', + field=models.CharField(default='0', help_text='是否缓存(0缓存 1不缓存)', max_length=4, verbose_name='是否缓存(0缓存 1不缓存)'), + ), + migrations.AlterField( + model_name='systemmenu', + name='isFrame', + field=models.CharField(default='1', help_text='是否为外链(0是 1否)', max_length=4, verbose_name='是否为外链(0是 1否)'), + ), + migrations.AlterField( + model_name='systemmenu', + name='status', + field=models.CharField(default='0', help_text='菜单状态(0正常 1停用)', max_length=4, verbose_name='菜单状态(0正常 1停用)'), + ), + migrations.AlterField( + model_name='systemmenu', + name='visible', + field=models.CharField(default='0', help_text='菜单状态(0显示 1隐藏)', max_length=4, verbose_name='菜单状态(0显示 1隐藏)'), + ), + migrations.AlterField( + model_name='systemuser', + name='userType', + field=models.CharField(default='00', help_text='用户类型(00系统用户)', max_length=4, verbose_name='用户类型(00系统用户)'), + ), + migrations.AddIndex( + model_name='systemdept', + index=models.Index(fields=['deptId'], name='SystemDept_deptId'), + ), + migrations.AddIndex( + model_name='systemmenu', + index=models.Index(fields=['menuId'], name='SystemMenu_menuId'), + ), + migrations.AddIndex( + model_name='systemrole', + index=models.Index(fields=['roleId'], name='SystemRole_roleId'), + ), + migrations.AddIndex( + model_name='systemrolemenu', + index=models.Index(fields=['roleId'], name='SystemRoleMenu_roleId'), + ), + migrations.AddIndex( + model_name='systemuser', + index=models.Index(fields=['userId'], name='SystemUser_userId'), + ), + ] diff --git a/admin-api/web/migrations/__init__.py b/admin-api/web/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/admin-api/web/models.py b/admin-api/web/models.py new file mode 100644 index 0000000..e6de983 --- /dev/null +++ b/admin-api/web/models.py @@ -0,0 +1,108 @@ +from django.db import models + +from database.core import CoreModel + + +class SystemInit(CoreModel): + Init = models.BooleanField(default=False, blank=True, help_text="是否已经初始化数据库") + + class Meta: + db_table = 'system_init' + verbose_name = '部门管理' + + +class SystemDept(CoreModel): + parentId = models.CharField(max_length=64, default="", verbose_name="父id", help_text="父id") + deptName = models.CharField(max_length=64, default="", verbose_name="部门名称", help_text="部门名称") + deptId = models.CharField(unique=True, max_length=64, default="", verbose_name="部门id", help_text="部门id") + orderNum = models.IntegerField(default=0, verbose_name="排序", help_text="排序") + leader = models.CharField(max_length=32, default="", verbose_name="负责人", help_text="负责人") + phone = models.CharField(max_length=32, default="", verbose_name="联系电话", help_text="联系电话") + email = models.CharField(max_length=32, default="", verbose_name="邮箱", help_text="邮箱") + status = models.CharField(max_length=4, default="0", verbose_name="启用状态", help_text="启用状态 (0正常 1停用)") + delFlag = models.CharField(max_length=4, default="0", verbose_name="是否删除", help_text="是否删除 (0存在 2删除)") + ancestors = models.CharField(max_length=64, default="", verbose_name="上级目录id", help_text="上级目录id") + + class Meta: + indexes = [models.Index(fields=['deptId'], name='SystemDept_deptId')] + db_table = 'system_dept' + verbose_name = '部门管理' + + +class SystemMenu(CoreModel): + menuId = models.CharField(unique=True, max_length=64, default="", verbose_name="菜单ID", help_text="菜单ID") + menuName = models.CharField(max_length=64, default="", verbose_name="菜单名称", help_text="菜单名称") + parentId = models.CharField(max_length=64, default="", verbose_name="父菜单ID", help_text="父菜单ID") + orderNum = models.IntegerField(default=0, verbose_name="显示顺序", help_text="显示顺序") + path = models.CharField(max_length=64, default="", verbose_name="路由地址", help_text="路由地址") + component = models.CharField(max_length=64, default="", verbose_name="组件路径", help_text="组件路径") + query = models.CharField(max_length=64, default="", verbose_name="路由参数", help_text="路由参数") + isFrame = models.CharField(max_length=4, default="1", verbose_name="是否为外链(0是 1否)", help_text="是否为外链(0是 1否)") + isCache = models.CharField(max_length=4, default="0", verbose_name="是否缓存(0缓存 1不缓存)", help_text="是否缓存(0缓存 1不缓存)") + visible = models.CharField(max_length=4, default="0", verbose_name="菜单状态(0显示 1隐藏)", help_text="菜单状态(0显示 1隐藏)") + menuType = models.CharField(max_length=4, default="", verbose_name="菜单类型(M目录 C菜单 F按钮)", help_text="菜单类型(M目录 C菜单 F按钮)") + status = models.CharField(max_length=4, default="0", verbose_name="菜单状态(0正常 1停用)", help_text="菜单状态(0正常 1停用)") + perms = models.CharField(max_length=32, default="", verbose_name="权限标识", help_text="权限标识") + icon = models.CharField(max_length=32, default="", verbose_name="菜单图", help_text="菜单图") + + class Meta: + db_table = 'system_menu' + verbose_name = '目录管理' + indexes = [models.Index(fields=['menuId'], name='SystemMenu_menuId')] + + +class SystemRole(CoreModel): + roleId = models.CharField(unique=True, max_length=64, default="", verbose_name="角色id", help_text="角色id") + roleName = models.CharField(max_length=64, default="", verbose_name="角色名称", help_text="角色名称") + roleKey = models.CharField(max_length=64, default="", verbose_name="角色权限字符", help_text="角色权限字符") + roleSort = models.IntegerField(default=0, verbose_name="角色排序", help_text="角色排序") + roleAdmin = models.BooleanField(max_length=64, default=False, verbose_name="是否是超级管理员", help_text="是否是超级管理员") + status = models.CharField(max_length=4, default="0", verbose_name="启用状态 (0正常 1停用)", help_text="启用状态 (0正常 1停用)") + menuCheckStrictly = models.BooleanField(default=True, verbose_name="菜单树选择项是否关联显示", help_text="菜单树选择项是否关联显示") + deptCheckStrictly = models.BooleanField(default=True, verbose_name="部门树选择项是否关联显示", help_text="部门树选择项是否关联显示") + delFlag = models.CharField(max_length=4, default="0", verbose_name="是否删除 (0代表存在 2代表删除)", help_text="是否删除 (0代表存在 2代表删除)") + + class Meta: + db_table = 'system_role' + verbose_name = '角色管理' + indexes = [models.Index(fields=['roleId'], name='SystemRole_roleId')] + + +class SystemUser(CoreModel): + userId = models.CharField(unique=True, max_length=64, default="", verbose_name="用户id", help_text="用户id") + deptId = models.CharField(max_length=64, default="", verbose_name="部门id", help_text="部门id") + username = models.CharField(max_length=64, default="", verbose_name="用户账号", help_text="用户账号") + nickName = models.CharField(max_length=64, default="", verbose_name="用户昵称", help_text="用户昵称") + userType = models.CharField(max_length=4, default="00", verbose_name="用户类型(00系统用户)", help_text="用户类型(00系统用户)") + email = models.CharField(max_length=64, default="", verbose_name="邮箱", help_text="邮箱") + phone = models.CharField(max_length=64, default="", verbose_name="手机号码", help_text="手机号码") + avatar = models.CharField(max_length=64, default="", verbose_name="头像", help_text="头像") + password = models.CharField(max_length=64, default="", verbose_name="密码(加密后)", help_text="密码(加密后)") + status = models.CharField(max_length=4, default="0", verbose_name="帐号状态(0正常 1停用)", help_text="帐号状态(0正常 1停用)") + delFlag = models.CharField(max_length=4, default="0", verbose_name="删除标志(0代表存在 2代表删除)", help_text="删除标志(0代表存在 2代表删除)") + loginIp = models.CharField(max_length=64, default="", verbose_name="最后登陆ip", help_text="最后登陆ip") + loginDate = models.CharField(max_length=64, default="", verbose_name="最后登陆日期", help_text="最后登陆日期") + + class Meta: + db_table = 'system_user' + verbose_name = '用户管理' + indexes = [models.Index(fields=['userId'], name='SystemUser_userId')] + + +class SystemRoleMenu(CoreModel): + menuId = models.CharField(max_length=64, default="", verbose_name="菜单id", help_text="菜单id") + roleId = models.CharField(max_length=64, default="", verbose_name="角色id", help_text="角色id") + + class Meta: + db_table = 'system_role_menu' + verbose_name = '角色与目录关系映射' + indexes = [models.Index(fields=['roleId'], name='SystemRoleMenu_roleId')] + + +class SystemUserRole(CoreModel): + userId = models.CharField(max_length=64, default="", verbose_name="菜单id", help_text="用户id") + roleId = models.CharField(max_length=64, default="", verbose_name="角色id", help_text="角色id") + + class Meta: + db_table = 'system_role_user' + verbose_name = '角色与人员关系映射' diff --git a/admin-api/web/paginator.py b/admin-api/web/paginator.py new file mode 100644 index 0000000..e2f6c3a --- /dev/null +++ b/admin-api/web/paginator.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" +@Author :mengying +@Date :2024/6/13 14:27 +@Email : 652044581@qq.com +@Desc : 分页器 +""" +from rest_framework.exceptions import NotFound +from rest_framework.pagination import PageNumberPagination + + +class StandardResultsSetPagination(PageNumberPagination): + # 默认每页显示的数据条数 + page_size = 10 + + # 获取URL参数中设置的每页显示数据条数 + page_size_query_param = 'pageSize' + + # 获取URL参数中传入的页码key + page_query_param = 'pageNum' + + # 最大支持的每页显示的数据条数 + max_page_size = 50 + + def paginate_queryset_data(self, queryset, request, view=None, serializer=None): + try: + queryset = super().paginate_queryset(queryset, request=request, view=view) + ser = serializer(queryset, many=True) + return ser.data + except NotFound as e: + return [] + + def paginate_queryset_count(self, queryset, request, view=None, serializer=None): + try: + res = {} + queryset = super().paginate_queryset(queryset, request=request, view=view) + ser = serializer(queryset, many=True) + res["data"] = ser.data + res["count"] = self.page.paginator.count + return res + except NotFound as e: + return {} diff --git a/admin-api/web/serializer.py b/admin-api/web/serializer.py new file mode 100644 index 0000000..41c058b --- /dev/null +++ b/admin-api/web/serializer.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" +@Author :mengying +@Date :2024/6/3 17:19 +@Email : 652044581@qq.com +@Desc : 功能描述 +""" +from rest_framework import serializers + +from web.models import SystemUser, SystemUserRole, SystemMenu, SystemRole, SystemDept + + +class SystemUserSerializer(serializers.ModelSerializer): + update_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) + create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) + + class Meta: + model = SystemUser + fields = '__all__' + + +class SystemDeptSerializer(serializers.ModelSerializer): + update_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) + create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) + + class Meta: + model = SystemDept + fields = '__all__' + + +class SystemRoleSerializer(serializers.ModelSerializer): + update_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) + create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) + + class Meta: + model = SystemRole + fields = '__all__' + + +class SystemMenuSerializer(serializers.ModelSerializer): + update_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) + create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) + + class Meta: + model = SystemMenu + fields = '__all__' + + +class SystemUserRoleSerializer(serializers.ModelSerializer): + update_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) + create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) + + class Meta: + model = SystemUserRole + fields = '__all__' diff --git a/admin-api/web/static/fontBold.ttf b/admin-api/web/static/fontBold.ttf new file mode 100644 index 0000000..02bb4a6 Binary files /dev/null and b/admin-api/web/static/fontBold.ttf differ diff --git a/admin-api/web/system/urls.py b/admin-api/web/system/urls.py new file mode 100644 index 0000000..9e6e5a1 --- /dev/null +++ b/admin-api/web/system/urls.py @@ -0,0 +1,48 @@ +from django.urls import path, re_path + +from web.system import views as systemView + +urlpatterns = [ + + # 部门相关 + path('dept/list', systemView.DeptListView.as_view(), name="部门列表"), + path('dept', systemView.DeptCreateView.as_view(), name="部门新增/修改"), + re_path('dept/(?P[0-9]+$)', systemView.DeptDetailView.as_view(), name="部门详情/删除"), + re_path('dept/list/exclude/(?P[0-9]+$)', systemView.DeptExcludeView.as_view(), name="部门排除查询"), + + # 角色相关 + path('role/list', systemView.RoleListView.as_view(), name="角色列表"), + path('role', systemView.RoleCreateView.as_view(), name="角色新增"), + path('role/changeStatus', systemView.RoleStatusView.as_view(), name="角色状态修改"), + re_path('role/(?P[0-9]+$)', systemView.RoleDetailView.as_view(), name="角色新增"), + + # 角色认证 + path('role/authUser/allocatedList', systemView.AuthUserAllocatedListView.as_view(), name="分配用户列表"), + path('role/authUser/cancel', systemView.AuthUserCancelView.as_view(), name="角色取消人员授权"), + path('role/authUser/cancelAll', systemView.AuthUserCancelAllView.as_view(), name="批量取消授权"), + path('role/authUser/unallocatedList', systemView.AuthUserUnallocatedListView.as_view(), name="未授权用户列表"), + path('role/authUser/selectAll', systemView.AuthUserSelectAllView.as_view(), name="批量绑定角色人员"), + + # 用户相关 + re_path('user/authRole/(?P[0-9]+$)', systemView.AuthRoleView.as_view(), name="用户角色绑定"), + path('user/authRole', systemView.AuthRoleView.as_view(), name="用户角色绑定修改"), + path('user/list', systemView.UserListView.as_view(), name="用户列表"), + path('user/deptTree', systemView.UserDeptTreeView.as_view(), name="用户列表"), + path('user/changeStatus', systemView.UserStatusView.as_view(), name="用户状态修改"), + path('user/', systemView.UserCreateView.as_view(), name="用户新增"), + re_path('user/(?P[0-9 ,]+$)', systemView.UserDetailView.as_view(), name="用户详情/删除"), + path('user/resetPwd', systemView.UserResetPwdView.as_view(), name="重置密码"), + + # 个人中心 + path('user/profile/updatePwd', systemView.UserUpdatePwdView.as_view(), name="修改密码"), + path('user/profile', systemView.UserProfileView.as_view(), name="个人中心"), + path('user/profile/avatar', systemView.UserProfileAvatarView.as_view(), name="修改头像"), + + # 目录相关 + path('menu/list', systemView.MenuListView.as_view(), name="目录列表"), + re_path('menu/(?P[0-9]+$)', systemView.MenuDetailView.as_view(), name="目录详情/删除"), + path('menu', systemView.MenuCreateView.as_view(), name="目录新增"), + path('menu/treeselect', systemView.MenuTreeSelectView.as_view(), name="树选择器"), + re_path('menu/roleMenuTreeselect/(?P[0-9]+$)', systemView.MenuRoleTreeSelectView.as_view(), name="已选择的树选择器"), + +] diff --git a/admin-api/web/system/views.py b/admin-api/web/system/views.py new file mode 100644 index 0000000..7440ec7 --- /dev/null +++ b/admin-api/web/system/views.py @@ -0,0 +1,776 @@ +import json +from functools import wraps + +from addict import Dict +from django.db.models import Q +from django.http.response import JsonResponse +from django_redis import get_redis_connection +from rest_framework.request import Request +from rest_framework.views import APIView + +from config import config +from utils.myDataUtils import TreeBuilder +from utils.myEncrypt import HashCipher +from utils.myEnum import SystemDelEnum, SystemStatusEnum, SystemUserTypeEnum +from utils.myResFormat import ResultJson, ResultCode +from utils.mySnowflake import Sf +from web.models import SystemDept, SystemUser, SystemRole, SystemUserRole, SystemMenu, SystemRoleMenu +from web.paginator import StandardResultsSetPagination +from web.serializer import SystemDeptSerializer, SystemUserSerializer, SystemRoleSerializer, SystemMenuSerializer + +RedisClient = get_redis_connection() + + +# 检验权限字符串的装饰器 +def permAuth(perKey): + def decorator(function): + @wraps(function) + def decorated_function(self, *args, **kwargs): + # 获取token中的用户权限 + user_info = {} + for item in args: + if isinstance(item, Request): + user_info = item.headers.user_info + user_permissions = user_info.get("permissions", []) + + # 判断是否有权限 + if "*:*:*" in user_permissions or perKey in user_permissions: + response = function(self, *args, **kwargs) + return response + else: + return JsonResponse(ResultJson(ret=ResultCode.PRE_AUTH_ERROR).result) + + return decorated_function + + return decorator + + +def response2json(response) -> dict: + return json.loads(response.content.decode()) + + +class DeptListView(APIView): + """【系统部门】列表接口""" + + @permAuth("system:dept:list") + def get(self, request, *args, **kwargs): + deptName: str = request.GET.get('deptName') + status: str = request.GET.get('status') + + q = Q() + q.connector = 'AND' + + if deptName: + q.children.append(('deptName', deptName)) + + if status: + q.children.append(('status', status)) + + q.children.append(('delFlag', SystemDelEnum.p0.value)) + + dept = SystemDept.objects.filter(q) + deptData = SystemDeptSerializer(instance=dept, many=True).data + return JsonResponse(ResultJson(ResultCode.SUCCESS, data=deptData).result) + + +class DeptCreateView(APIView): + """【系统部门】新增部门""" + + @permAuth("system:dept:add") + def post(self, request, *args, **kwargs): + + data: dict = request.data + + parentId = request.data.get('parentId') + + dept_obj = SystemDept.objects.filter(deptId=parentId) + if not dept_obj: + raise Exception("未找到系统部门父级id") + + dept_obj = dept_obj.first() + data["ancestors"] = dept_obj.ancestors + "," + dept_obj.deptId + data["deptId"] = Sf.generate() + + # 判断同一父级部门下不重名 + deptNameExist = SystemDept.objects.filter(deptName=data.get("deptName"), parentId=data.get("parentId")) + if deptNameExist: + return JsonResponse(ResultJson(ret=ResultCode.DEPT_ERROR).result) + + SystemDept.objects.create(**data) + return JsonResponse(ResultJson(ResultCode.SUCCESS, data=data).result) + + + @permAuth("system:dept:edit") + def put(self, request, *args, **kwargs): + data: dict = request.data + deptId = data.get('deptId') + + deptData = SystemDept.objects.get(deptId=deptId) + + # 判断原始目录的父级id是否变化 + if deptData.parentId == data.get('parentId'): + SystemDept.objects.filter(deptId=deptId).update(**data) + else: + # 处理本条数据的上级关联关系 + parentId = data.get('parentId') + parentData = SystemDept.objects.get(deptId=parentId) + repAncestors = parentData.ancestors + "," + parentId + data['ancestors'] = repAncestors + SystemDept.objects.filter(deptId=deptId).update(**data) + + # 处理下级关联的路径问题 + oldAncestors = deptData.ancestors + "," + deptData.deptId + SystemDeptDataList = SystemDept.objects.filter(ancestors__contains=oldAncestors, delFlag=SystemDelEnum.p0.value) + for item in SystemDeptDataList: + new_ancestors = item.ancestors + new_ancestors = str(new_ancestors).replace(oldAncestors, repAncestors + "," + deptData.deptId) + item.ancestors = new_ancestors + item.save() + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class DeptDetailView(APIView): + """【系统部门】新增部门""" + + @permAuth("system:dept:query") + def get(self, request, deptId, *args, **kwargs): + """【系统部门】部门详情""" + data = SystemDept.objects.get(deptId=deptId, delFlag=SystemDelEnum.p0.value) + ser = SystemDeptSerializer(instance=data) + return JsonResponse(ResultJson(ResultCode.SUCCESS, data=ser.data).result) + + @permAuth("system:dept:remove") + def delete(self, request, deptId, *args, **kwargs): + # 更新部门状态 + SystemDept.objects.filter(deptId=deptId).update(delFlag=SystemDelEnum.p1.value) + + # 更新人员绑定部门 + SystemUser.objects.filter(deptId=deptId).update(deptId="") + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class DeptExcludeView(APIView): + """【系统部门】排除查询""" + + @permAuth("system:dept:query") + def get(self, request, deptId, *args, **kwargs): + # 查询部门id不是指定id或者ancestors不包含该id,且状态未删除的数据 + userData = SystemDept.objects.filter(delFlag=SystemDelEnum.p0.value, status=SystemStatusEnum.p0.value).exclude(deptId=deptId, + ancestors__contains=deptId) + ser = SystemDeptSerializer(instance=userData, many=True) + return JsonResponse(ResultJson(ResultCode.SUCCESS, data=ser.data).result) + + +class RoleListView(APIView): + """【系统角色】列表接口""" + + def get(self, request, *args, **kwargs): + roleName: str = request.GET.get('roleName') + roleKey: str = request.GET.get('roleKey') + status: str = request.GET.get('status') + + q = Q() + q.connector = 'AND' + + if roleName: + q.children.append(('roleName', roleName)) + + if roleKey: + q.children.append(('roleKey', roleKey)) + + if status: + q.children.append(('status', status)) + + q.children.append(('roleAdmin', False)) + q.children.append(('delFlag', SystemDelEnum.p0.value)) + + roleData = SystemRole.objects.filter(q).order_by('roleSort') + + paginator = StandardResultsSetPagination() + role_list = paginator.paginate_queryset_count(roleData, self.request, view=self, serializer=SystemRoleSerializer) + + return JsonResponse(ResultJson(ResultCode.SUCCESS, data=role_list).result) + + +class RoleCreateView(APIView): + """【系统角色】新增接口""" + + def post(self, request, *args, **kwargs): + data: dict = request.data + roleKey = data.get("roleKey") + roleName = data.get("roleName") + menuIds = data.pop("menuIds", []) + roleId = Sf.generate() + data["roleId"] = roleId + + roleData = SystemRole.objects.filter(Q(roleKey=roleKey, delFlag=SystemDelEnum.p0.value) | Q(roleName=roleName, delFlag=SystemDelEnum.p0.value)) + + if roleData: + return JsonResponse(ResultJson(ret=ResultCode.ROLE_ERROR).result) + + # 删除原始的关系 + SystemRoleMenu.objects.filter(roleId=roleId).delete() + for menuId in menuIds: + SystemRoleMenu.objects.create(menuId=menuId, roleId=roleId) + + # 创建角色数据 + SystemRole.objects.create(**data) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + def put(self, request, *args, **kwargs): + data: dict = request.data + roleId = data.get("roleId") + menuIds = data.pop("menuIds", []) + roleAdmin = data.pop("roleAdmin", False) + + # 删除原始的关系 + SystemRoleMenu.objects.filter(roleId=roleId).delete() + for menuId in menuIds: + SystemRoleMenu.objects.create(menuId=menuId, roleId=roleId) + + # 更新角色数据 + SystemRole.objects.filter(roleId=roleId).update(**data) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class RoleStatusView(APIView): + """【系统角色】修改状态""" + + def put(self, request): + data: dict = request.data + roleId = data.get("roleId") + SystemRole.objects.filter(roleId=roleId).update(**data) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class RoleDetailView(APIView): + @permAuth("system:role:query") + def get(self, request, roleId: str): + """【系统角色】获取角色信息""" + + roleData = SystemRole.objects.get(roleId=roleId) + roleData = SystemRoleSerializer(instance=roleData).data + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=roleData).result) + + @permAuth("system:role:remove") + def delete(self, request, roleId: str): + """【系统角色】获取删除角色信息""" + + roleIds = roleId.split(',') + SystemRole.objects.filter(roleId__in=roleIds).update(delFlag=SystemDelEnum.p1.value) + + SystemUserRole.objects.filter(roleId__in=roleIds).delete() + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class AuthUserCancelView(APIView): + def put(self, request): + """【系统角色】取消角色人员授权""" + # 处理角色用户关系 + roleId: str = request.data.get('roleId') + userId: str = request.data.get('userId') + + # 判断是批量删除还是单个删除 + SystemUserRole.objects.filter(roleId=roleId, userId=userId).delete() + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class AuthUserAllocatedListView(APIView): + + def get(self, request): + """【系统角色】给角色分配人员""" + roleId: str = request.GET.get('roleId') + username: str = request.GET.get('username') + phone: str = request.GET.get('phone') + + UserRoleIds = SystemUserRole.objects.filter(roleId=roleId) + + q = Q() + q.connector = 'AND' + + if username: + q.children.append(('username', username)) + + if phone: + q.children.append(('phone', phone)) + + q.children.append(('userId__in', [item.userId for item in UserRoleIds])) + q.children.append(('delFlag', SystemDelEnum.p0.value)) + q.children.append(('userType', SystemUserTypeEnum.p0.value)) + + userData = SystemUser.objects.filter(q) + + paginator = StandardResultsSetPagination() + + # 组装人员的部门信息 + userData = paginator.paginate_queryset_count(userData, self.request, view=self, serializer=SystemUserSerializer) + for item in userData.get("data", []): + deptId = item.get('deptId') + if not deptId: + item["dept"] = {} + else: + deptData = SystemDept.objects.get(deptId=deptId) + item["dept"] = SystemDeptSerializer(instance=deptData).data + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=userData).result) + + +class AuthUserUnallocatedListView(APIView): + + def get(self, request): + """【系统角色】获取未授权用户列表""" + roleId: str = request.GET.get('roleId') + username: str = request.GET.get('username') + phone: str = request.GET.get('phone') + + UserRoleIds = SystemUserRole.objects.filter(roleId=roleId) + + q = Q() + q.connector = 'AND' + + if username: + q.children.append(('username', username)) + + if phone: + q.children.append(('phone', phone)) + + q.children.append(('delFlag', SystemDelEnum.p0.value)) + q.children.append(('userType', SystemUserTypeEnum.p0.value)) + + userData = SystemUser.objects.filter(q).exclude(userId__in=[item.userId for item in UserRoleIds]) + + paginator = StandardResultsSetPagination() + + # 组装人员的部门信息 + userData = paginator.paginate_queryset_count(userData, self.request, view=self, serializer=SystemUserSerializer) + for item in userData.get("data", []): + deptId = item.get('deptId') + if not deptId: + item["dept"] = {} + else: + deptData = SystemDept.objects.get(deptId=deptId) + item["dept"] = SystemDeptSerializer(instance=deptData).data + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=userData).result) + + +class AuthUserSelectAllView(APIView): + def put(self, request): + """【系统角色】批量绑定角色人员""" + roleId: str = request.data.get('roleId') + userIds: str = request.data.get('userIds') + userIdList = userIds.split(',') + + # 批量插入用户关系 + for userId in userIdList: + SystemUserRole.objects.create(**dict(userId=userId, roleId=roleId)) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class AuthUserCancelAllView(APIView): + def put(self, request): + """【系统角色】批量取消角色人员授权""" + # 处理角色用户关系 + roleId: str = request.data.get('roleId') + userIds: str = request.data.get('userIds') + userIdList = userIds.split(',') + + # 判断是批量删除还是单个删除 + SystemUserRole.objects.filter(roleId=roleId, userId__in=userIdList).delete() + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class UserListView(APIView): + """【系统用户】列表接口""" + + @permAuth("system:user:list") + def get(self, request): + + deptId: str = request.GET.get('deptId') + username: str = request.GET.get('username') + phone: str = request.GET.get('phone') + status: str = request.GET.get('status') + + q = Q() + q.connector = 'AND' + + if deptId: + deptData = SystemDept.objects.filter(ancestors__contains=deptId) + deptIds = [item.deptId for item in deptData] + deptIds.append(deptId) + q.children.append(('deptId__in', deptIds)) + + if username: + q.children.append(('username', username)) + + if phone: + q.children.append(('phone', phone)) + + if status: + q.children.append(('status', status)) + + q.children.append(('delFlag', SystemDelEnum.p0.value)) + q.children.append(('userType', SystemUserTypeEnum.p0.value)) + + userData = SystemUser.objects.filter(q) + + # 分页 + paginator = StandardResultsSetPagination() + userData = paginator.paginate_queryset_count(userData, self.request, view=self, serializer=SystemUserSerializer) + + # 组装人员的部门信息 + for item in userData.get("data", []): + deptId = item.get('deptId') + if not deptId: + item["dept"] = {} + else: + deptData = SystemDept.objects.get(deptId=deptId) + item["dept"] = SystemDeptSerializer(instance=deptData).data + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=userData).result) + + +class UserDeptTreeView(APIView): + """【系统用户】部门的结构树""" + + @classmethod + def deptFormat(cls, SystemDeptList): + container = [] + for item in SystemDeptList: + deptItem = {"id": item.get("deptId"), "parentId": item.get("parentId"), "label": item.get("deptName"), } + container.append(deptItem) + return container + + def get(self, request): + # 查询全部数据 + deptData = SystemDept.objects.filter(delFlag=SystemDelEnum.p0.value, status=SystemStatusEnum.p0.value) + + # 组装前端所需要的数据结构 + SystemDeptList = SystemDeptSerializer(instance=deptData, many=True) + DeptList = self.deptFormat(SystemDeptList.data) + + # 组装成树的结构 + TreeData = TreeBuilder(DeptList).build(parentKey="parentId", ownerKey="id", topParent="0") + TreeDataList = list(TreeData.values()) + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=TreeDataList).result) + + +class UserStatusView(APIView): + """【系统用户】修改用户状态""" + + @permAuth("*:*:*") + def put(self, request): + data: dict = request.data + userId = data.get("userId") + SystemUser.objects.filter(userId=userId).update(**data) + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class UserCreateView(APIView): + """【系统用户】新增接口""" + + @permAuth("*:*:*") + def post(self, request): + data: dict = request.data + username = data.get("username") + password = data.get('password', "") + + userId = Sf.generate() + + # 判断用户名是否重复 + userExist = SystemUser.objects.filter(username=username, delFlag=SystemDelEnum.p0.value) + if userExist: + return JsonResponse(ResultJson(ret=ResultCode.USER_ERROR).result) + + # 处理角色用户关系 + roleIds = data.pop("roleIds", []) + for roleId in roleIds: + SystemUserRole.objects.create(**dict(roleId=roleId, userId=userId)) + + # 用户表插入数据 + user_info = request.headers.user_info + username = user_info.get('user', {}).get('username') + data["userId"] = userId + data["createBy"] = username + data["password"] = HashCipher.md5(config.ENCRYPT_STRING + password) + SystemUser.objects.create(**data) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + @permAuth("*:*:*") + def put(self, request): + """【系统用户】修改信息""" + # 处理角色用户关系 + data: dict = request.data + + userId = data.get('userId') + roleIds = data.pop("roleIds", []) + + # 删除以前的角色人员关系,在增加新的关系 + SystemUserRole.objects.filter(userId=userId).delete() + for roleId in roleIds: + SystemUserRole.objects.create(**dict(roleId=roleId, userId=userId)) + + SystemUser.objects.filter(userId=userId).update(**data) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + @permAuth("*:*:*") + def get(self, request): + """【系统用户】获取现有的角色列表""" + res = Dict() + roleData = SystemRole.objects.filter(status=SystemStatusEnum.p0.value, delFlag=SystemDelEnum.p0.value, roleAdmin=False) + res.roles = SystemRoleSerializer(instance=roleData, many=True).data + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=res).result) + + +class UserDetailView(APIView): + @permAuth("system:user:query") + def get(self, request, userId: str): + """【系统用户】获取用户信息""" + res = Dict() + + # 找到用户绑定的角色id + userData = SystemUser.objects.get(userId=userId) + + userRoleData = SystemUserRole.objects.filter(userId=userId) + roleIds = [item.roleId for item in userRoleData] + roleData = SystemRole.objects.filter(status=SystemStatusEnum.p0.value, roleAdmin=False, delFlag=SystemDelEnum.p0.value) + + # 组装数据 + res.user = SystemUserSerializer(instance=userData).data + res.roleIds = roleIds + res.roles = SystemRoleSerializer(instance=roleData, many=True).data + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=res).result) + + @permAuth("system:user:remove") + def delete(self, request, userId: str): + """【系统用户】获取删除用户信息""" + + # 分割路径的userId,变为list + userIds = userId.split(',') + SystemUser.objects.filter(userId__in=userIds).update(delFlag=SystemDelEnum.p1.value) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class UserProfileView(APIView): + + def get(self, request): + """【个人中心】获取信息""" + user_info = request.headers.user_info + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=user_info).result) + + def put(self, request): + """【个人中心】修改""" + data: dict = request.data + user_info = request.headers.user_info + userId = user_info.get('user', {}).get('userId') + SystemUser.objects.filter(userId=userId).update(**data) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class UserProfileAvatarView(APIView): + + def post(self, request): + """【个人中心】修改头像""" + avatar = request.Files.get("avatarfile") + user_info = request.headers.user_info + userId = user_info.get('user', {}).get('userId') + + # TODO: 上传头像文件流,生成一个url,更新用户信息的头像字段(没有obs, 功能未作) + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=user_info).result) + + +class UserResetPwdView(APIView): + @permAuth("*:*:*") + def put(self, request): + """【系统用户】重置密码/ 需要管理员权限""" + data: dict = request.data + password = data.get('password') + userId = data.get('userId') + + # 修改密码 + password_md5 = HashCipher.md5(config.ENCRYPT_STRING + password) + + SystemUser.objects.filter(userId=userId).update(password=password_md5) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class UserUpdatePwdView(APIView): + def put(self, request): + """【系统用户】更新密码""" + oldPassword = request.GET.get('oldPassword') + newPassword = request.GET.get('newPassword') + + # 判断旧密码是否正确 + user_info = request.headers.user_info + userId = user_info.get('user', {}).get('userId', "") + + # 对密码进行加密 + old_encrypt_password = HashCipher.md5(config.ENCRYPT_STRING + str(oldPassword)) + new_encrypt_password = HashCipher.md5(config.ENCRYPT_STRING + str(newPassword)) + + userData = SystemUser.objects.filter(password=old_encrypt_password, userId=userId) + + if not userData: + return JsonResponse(ResultJson(ret=ResultCode.PASSWORD_ERROR).result) + else: + SystemUser.objects.filter(userId=userId).update(password=new_encrypt_password) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class AuthRoleView(APIView): + + def get(self, request, userId: str): + """【系统用户】查询角色映射关系""" + # 查询用户信息 + userData = SystemUser.objects.get(userId=userId) + + # 查询用户角色映射关系 + userRoleList = SystemUserRole.objects.filter(userId=userId) + + # 查询角色信息 + roleIds = [item.roleId for item in userRoleList] + roleData = SystemRole.objects.filter(roleId__in=roleIds) + + # 序列化 + userData = SystemUserSerializer(instance=userData).data + roleData = SystemRoleSerializer(instance=roleData, many=True).data + + # 组装返回数据 + resData = dict(user=userData, roles=roleData) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=resData).result) + + def put(self, request): + """【系统用户】修改角色映射关系""" + userId = request.GET.get("userId") + roleIds = request.GET.get("roleIds") + roleIds = roleIds.split(",") + + # 删除以前的角色人员关系,在增加新的关系 + SystemUserRole.objects.filter(userId=userId).delete() + for roleId in roleIds: + SystemUserRole.objects.create(**dict(roleId=roleId, userId=userId)) + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=None).result) + + +class MenuDetailView(APIView): + @permAuth("system:menu:query") + def get(self, request, menuId: str): + """【系统用户】查询""" + menuData = SystemMenu.objects.get(menuId=menuId) + menuData = SystemMenuSerializer(instance=menuData).data + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=menuData).result) + + @permAuth("system:menu:remove") + def delete(self, request, menuId: str): + """【系统用户】删除""" + + # 删除目录信息 + SystemMenu.objects.filter(menuId=menuId).delete() + + # 删除角色目录映射 + SystemRoleMenu.objects.filter(menuId=menuId).delete() + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class MenuListView(APIView): + + @permAuth("system:menu:list") + def get(self, request): + """【系统目录】列表""" + menuName: str = request.GET.get('menuName') + status: str = request.GET.get('status') + + q = Q() + q.connector = 'AND' + + if menuName: + q.children.append(('menuName', menuName)) + + if status: + q.children.append(('status', status)) + + q.children.append(('visible', SystemDelEnum.p0.value)) + + # 查询数据列表 + menuData = SystemMenu.objects.filter(q).order_by('orderNum') + menuData = SystemMenuSerializer(instance=menuData, many=True).data + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=menuData).result) + + +class MenuCreateView(APIView): + + @permAuth("system:menu:add") + def post(self, request): + """【系统目录】新增""" + data: dict = request.data + parentId = data.get("parentId") + menuName = data.get("menuName") + # 生成父级数据 + data["menuId"] = Sf.generate() + + menuExist = SystemMenu.objects.filter(parentId=parentId, menuName=menuName) + + if menuExist: + return JsonResponse(ResultJson(ret=ResultCode.MENU_ERROR).result) + + SystemMenu.objects.create(**data) + + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=data).result) + + @permAuth("system:menu:edit") + def put(self, request): + """【系统目录】修改""" + data: dict = request.data + menuId = data.get('menuId') + SystemMenu.objects.filter(menuId=menuId).update(**data) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS).result) + + +class MenuTreeSelectView(APIView): + """【系统目录】树选择""" + + @classmethod + def MenuFormat(cls, MenuData): + container = [] + for item in MenuData: + deptItem = {"id": item.get("menuId"), "parentId": item.get("parentId"), "label": item.get("menuName"), } + container.append(deptItem) + return container + + def get(self, request): + menuData = SystemMenu.objects.filter(status=SystemStatusEnum.p0.value) + menuData = SystemMenuSerializer(instance=menuData, many=True).data + + # 组装前端所需要的数据结构 + MenuList = self.MenuFormat(menuData) + + # 组装成树的结构 + TreeData = TreeBuilder(MenuList).build(parentKey="parentId", ownerKey="id", topParent="0") + TreeDataList = list(TreeData.values()) + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=TreeDataList).result) + + +class MenuRoleTreeSelectView(APIView): + """【系统目录】已经选择树选择器""" + + def get(self, request, roleId: str): + res = Dict() + + # 查询树的列表 + TreeDataListResponse = MenuTreeSelectView().get(request) + TreeDataList = response2json(TreeDataListResponse).get("data", []) + + # 查询选中数据的 + res.menus = TreeDataList + roleMenuData = SystemRoleMenu.objects.filter(roleId=roleId) + + res.checkedKeys = [item.menuId for item in roleMenuData] + return JsonResponse(ResultJson(ret=ResultCode.SUCCESS, data=res.to_dict()).result) diff --git a/admin-api/web/tests.py b/admin-api/web/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/admin-api/web/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/admin-api/web/urls.py b/admin-api/web/urls.py new file mode 100644 index 0000000..46883d8 --- /dev/null +++ b/admin-api/web/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from web import views as webView + +urlpatterns = [ + path('captchaImage', webView.CaptchaImageView.as_view(), name="获取验证码"), + path('login', webView.LoginView.as_view(), name="登录"), + path('logout', webView.LogoutView.as_view(), name="退出登录"), + path('getInfo', webView.GetInfoView.as_view(), name="获取登陆信息"), + path('getRouters', webView.GetRoutersView.as_view(), name="获取用户权限路由"), +] diff --git a/admin-api/web/views.py b/admin-api/web/views.py new file mode 100644 index 0000000..c1d2203 --- /dev/null +++ b/admin-api/web/views.py @@ -0,0 +1,199 @@ +import json +import uuid + +from addict import Dict +from django.http.response import JsonResponse +from django_redis import get_redis_connection +from rest_framework.views import APIView + +from config import config +from utils.myDataUtils import TreeBuilder +from utils.myEncrypt import HashCipher +from utils.myEnum import SystemDelEnum, SystemStatusEnum +from utils.myResFormat import ResultJson, ResultCode +from utils.myTimeFormat import MyTimeUtils +from web.captchaImage import CaptchaImage +from web.models import SystemUser, SystemUserRole, SystemRole, SystemRoleMenu, SystemMenu +from web.serializer import SystemUserSerializer, SystemMenuSerializer, SystemRoleSerializer + +RedisClient = get_redis_connection() + + +class CaptchaImageView(APIView): + + def get(self, request, *args, **kwargs): + """【系统用户】获取验证码""" + captcha = CaptchaImage() + + # 生成验证码图片和字符串 + captcha_string, image_base64 = captcha.generate(width=120, height=40) + + res = Dict() + uid = uuid.uuid4().hex + res.img = image_base64 + res.uuid = uid + res.captchaEnabled = True + + RedisClient.set(uid, captcha_string, ex=5 * 60) + + return JsonResponse(ResultJson(ResultCode.SUCCESS, data=res.to_dict()).result) + + +class LoginView(APIView): + "登录" + + def post(self, request, *args, **kwargs): + + username = request.data.get("username", "") + password = request.data.get("password", "") + + # 如果有验证码的情况,校验方式 + uid = request.data.get("uuid", "") + code = request.data.get("code", "") + + # 密码加密 + password = HashCipher.md5(config.ENCRYPT_STRING + str(password)) + + if uid: + authCode = RedisClient.get(uid) + if not authCode: + return JsonResponse(ResultJson(ResultCode.AUTH_CODE_EXP).result) + else: + if str(authCode).strip() != str(code).strip(): + return JsonResponse(ResultJson(ResultCode.AUTH_CODE_ERROR).result) + + Users = SystemUser.objects.filter(username=username, password=password) + if not Users: + return JsonResponse(ResultJson(ResultCode.PASSWORD_ERROR).result) + + User = Users.first() + user_data = SystemUserSerializer(instance=User).data + + # 用户是否被删除 + if User.delFlag == SystemDelEnum.p1.value: + return JsonResponse(ResultJson(ResultCode.USER_NOT_EXIST).result) + + # 用户是否被禁用 + if User.status == SystemStatusEnum.p1.value: + return JsonResponse(ResultJson(ResultCode.USER_NOT_ALLOW).result) + + # 更新用户的登陆信息 + User.loginIp = request.META.get("REMOTE_ADDR", "") + User.loginDate = MyTimeUtils.TimeFormat() + User.save() + + # 获取角色信息 + token = uuid.uuid4().hex + userInfo = Dict() + + # 找到用户id和角色的映射 + UserRoleMap = SystemUserRole.objects.filter(userId=User.userId) + + RoleDataList = SystemRole.objects.filter(roleId__in=[roleItem.roleId for roleItem in UserRoleMap], delFlag=SystemDelEnum.p0.value, + status=SystemStatusEnum.p0.value) + + userInfo.user = user_data + userInfo.roles = [role.roleKey for role in RoleDataList] + user_data["role"] = SystemRoleSerializer(instance=RoleDataList, many=True).data + + if any([role.roleAdmin for role in RoleDataList]): + userInfo.permissions = ["*:*:*"] + else: + + roleIds = list(set([item.roleId for item in RoleDataList])) + + # 对目录权限进行去重 + RoleMenus = SystemRoleMenu.objects.filter(roleId__in=roleIds) + + menuIds = list(set([item.menuId for item in RoleMenus])) + + Menus = SystemMenu.objects.filter(menuId__in=menuIds, status=SystemStatusEnum.p0.value) + + userInfo.permissions = [item.perms for item in Menus] + + RedisClient.set(token, json.dumps(userInfo.to_dict()), ex=60 * 60) + + return JsonResponse(ResultJson(ResultCode.SUCCESS, data=token).result) + + +class LogoutView(APIView): + "退出登录" + + def post(self, request, *args, **kwargs): + token = request.headers.user_info.get("token") + + # token不存在的情况 + if not token: + return JsonResponse(ResultJson(ResultCode.SUCCESS).result) + + RedisClient.delete(token) + return JsonResponse(ResultJson(ResultCode.SUCCESS).result) + + +class GetInfoView(APIView): + "获取登陆信息" + + def get(self, request, *args, **kwargs): + user_info = request.headers.user_info + return JsonResponse(ResultJson(ResultCode.SUCCESS, data=user_info).result) + + +class GetRoutersView(APIView): + "获取用户权限路由" + + def routerFormat(self, MenuData): + # 目录数据组装前端的路由格式 + container = [] + for item in MenuData: + # 组装目录 + if item.get('menuType') == 'M': + + # 判断是否外链接 + if item.get('isFrame') == "0": + menuItem = {"id": item.get('menuId'), "parentId": item.get('parentId'), "component": "Layout", "hidden": bool(int(item.get('visible'))), + "path": item.get('path'), "name": item.get('path'), + "meta": {"icon": item.get('icon'), "link": item.get('path'), "noCache": bool(int(item.get('isCache'))), + "title": item.get('menuName'), } + + } + else: + menuItem = {"id": item.get('menuId'), "parentId": item.get('parentId'), "component": "Layout", "hidden": bool(int(item.get('visible'))), + "path": "/" + item.get('path'), "name": str(item.get('path')).capitalize(), "redirect": "noRedirect", + "meta": {"icon": item.get('icon'), "link": "", "noCache": bool(int(item.get('isCache'))), "title": item.get('menuName'), }} + container.append(menuItem) + + # 处理组件形式 + elif item.get('menuType') == 'C': + menuItem = {"id": item.get('menuId'), "parentId": item.get('parentId'), "component": item.get('component'), + "hidden": bool(int(item.get('visible'))), "path": item.get('path'), "name": str(item.get('path')).capitalize(), + "redirect": "noRedirect", + "meta": {"icon": item.get('icon'), "link": "", "noCache": bool(int(item.get('isCache'))), "title": item.get('menuName'), }} + container.append(menuItem) + + # 基础数据组装后,组装成树结构 + treeData = TreeBuilder(container).build(parentKey="parentId", ownerKey="id", topParent="0") + return list(treeData.values()) if treeData else [] + + def get(self, request, *args, **kwargs): + + user_info = request.headers.user_info + + # 获取用户的角色信息 + roleData = user_info.get("user", {}).get("role", []) + roleAdminList = [roleItem.get("roleAdmin") for roleItem in roleData] + + if any(roleAdminList): + Menus = SystemMenu.objects.all() + else: + roleIds = list(set([item.get("roleId") for item in roleData])) + + # 对目录权限进行去重 + RoleMenus = SystemRoleMenu.objects.filter(roleId__in=roleIds) + + menuIds = list(set([item.menuId for item in RoleMenus])) + + Menus = SystemMenu.objects.filter(menuId__in=menuIds, status=SystemStatusEnum.p0.value) + + MenuData = SystemMenuSerializer(instance=Menus, many=True).data + routerInfo = self.routerFormat(MenuData) + return JsonResponse(ResultJson(ResultCode.SUCCESS, data=routerInfo).result) diff --git a/admin-ui/.env.development b/admin-ui/.env.development new file mode 100644 index 0000000..98713c4 --- /dev/null +++ b/admin-ui/.env.development @@ -0,0 +1,9 @@ +# 页面标题 +VITE_APP_TITLE = 若依后台管理系统 + +# 开发环境配置 +VITE_APP_ENV = 'development' + +# 若依管理系统/开发环境 +VITE_APP_BASE_API = '/dev-api' + diff --git a/admin-ui/.env.production b/admin-ui/.env.production new file mode 100644 index 0000000..394212d --- /dev/null +++ b/admin-ui/.env.production @@ -0,0 +1,11 @@ +# 页面标题 +VITE_APP_TITLE = '若依管理系统' + +# 生产环境配置 +VITE_APP_ENV = 'production' + +# 若依管理系统/生产环境 +VITE_APP_BASE_API = 'http://124.71.212.219:8090/api' + +# 是否在打包时开启压缩,支持 gzip 和 brotli +VITE_BUILD_COMPRESS = gzip \ No newline at end of file diff --git a/admin-ui/.env.staging b/admin-ui/.env.staging new file mode 100644 index 0000000..12a8601 --- /dev/null +++ b/admin-ui/.env.staging @@ -0,0 +1,11 @@ +# 页面标题 +VITE_APP_TITLE = 若依后台管理 + +# 生产环境配置 +VITE_APP_ENV = 'staging' + +# 若依管理系统/生产环境 +VITE_APP_BASE_API = '/stage-api' + +# 是否在打包时开启压缩,支持 gzip 和 brotli +VITE_BUILD_COMPRESS = gzip \ No newline at end of file diff --git a/admin-ui/.gitignore b/admin-ui/.gitignore new file mode 100644 index 0000000..78a752d --- /dev/null +++ b/admin-ui/.gitignore @@ -0,0 +1,23 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +**/*.log + +tests/**/coverage/ +tests/e2e/reports +selenium-debug.log + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.local + +package-lock.json +yarn.lock diff --git a/admin-ui/Dockerfile b/admin-ui/Dockerfile new file mode 100644 index 0000000..5cc144e --- /dev/null +++ b/admin-ui/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:latest +WORKDIR /app +COPY ./dist /usr/share/nginx/html +EXPOSE 80 \ No newline at end of file diff --git a/admin-ui/LICENSE b/admin-ui/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/admin-ui/build.sh b/admin-ui/build.sh new file mode 100644 index 0000000..cae0d55 --- /dev/null +++ b/admin-ui/build.sh @@ -0,0 +1,4 @@ +git pull +yarn run build:prod +docker-compose build +docker-compose up -d \ No newline at end of file diff --git a/admin-ui/docker-compose.yaml b/admin-ui/docker-compose.yaml new file mode 100644 index 0000000..2941fba --- /dev/null +++ b/admin-ui/docker-compose.yaml @@ -0,0 +1,18 @@ +version: '3.0' + +services: + + django-vue-ui-std: + + build: + context: . + dockerfile: Dockerfile + + image: django-vue-ui-image-std + + container_name: django-vue-ui-container-std + + ports: + - "8101:80" + + restart: always \ No newline at end of file diff --git a/admin-ui/html/ie.html b/admin-ui/html/ie.html new file mode 100644 index 0000000..052ffcd --- /dev/null +++ b/admin-ui/html/ie.html @@ -0,0 +1,46 @@ + + + + + + 请升级您的浏览器 + + + + + + +

请升级您的浏览器,以便我们更好的为您提供服务!

+

您正在使用 Internet Explorer 的早期版本(IE11以下版本或使用该内核的浏览器)。这意味着在升级浏览器前,您将无法访问此网站。

+
+

请注意:微软公司对Windows XP 及 Internet Explorer 早期版本的支持已经结束

+

自 2016 年 1 月 12 日起,Microsoft 不再为 IE 11 以下版本提供相应支持和更新。没有关键的浏览器安全更新,您的电脑可能易受有害病毒、间谍软件和其他恶意软件的攻击,它们可以窃取或损害您的业务数据和信息。请参阅 微软对 Internet Explorer 早期版本的支持将于 2016 年 1 月 12 日结束的说明

+
+

您可以选择更先进的浏览器

+

推荐使用以下浏览器的最新版本。如果您的电脑已有以下浏览器的最新版本则直接使用该浏览器访问即可。

+ +
+ + \ No newline at end of file diff --git a/admin-ui/index.html b/admin-ui/index.html new file mode 100644 index 0000000..d755a92 --- /dev/null +++ b/admin-ui/index.html @@ -0,0 +1,216 @@ + + + + + + + + + + 后台管理系统 + + + + + +
+
+
+
+
+
系统加载中,请稍后.......
+
+
+ + + + \ No newline at end of file diff --git a/admin-ui/package.json b/admin-ui/package.json new file mode 100644 index 0000000..ad83ed1 --- /dev/null +++ b/admin-ui/package.json @@ -0,0 +1,45 @@ +{ + "name": "ruoyi", + "version": "3.8.7", + "description": "若依管理系统", + "author": "若依", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build:prod": "vite build --mode production", + "build:stage": "vite build --mode staging", + "preview": "vite preview" + }, + "repository": { + "type": "git", + "url": "https://gitee.com/y_project/RuoYi-Vue.git" + }, + "dependencies": { + "@element-plus/icons-vue": "2.3.1", + "@vueup/vue-quill": "1.2.0", + "@vueuse/core": "10.6.1", + "axios": "0.27.2", + "echarts": "5.4.3", + "element-plus": "2.4.3", + "file-saver": "2.0.5", + "fuse.js": "6.6.2", + "js-cookie": "3.0.5", + "jsencrypt": "3.3.2", + "nprogress": "0.2.0", + "pinia": "2.1.7", + "vue": "3.3.9", + "vue-cropper": "1.1.1", + "vue-router": "4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "4.5.0", + "@vue/compiler-sfc": "3.3.9", + "sass": "1.69.5", + "unplugin-auto-import": "0.17.1", + "vite": "5.0.4", + "vite-plugin-compression": "0.5.1", + "vite-plugin-svg-icons": "2.0.1", + "unplugin-vue-setup-extend-plus": "1.0.0" + } +} diff --git a/admin-ui/public/favicon.ico b/admin-ui/public/favicon.ico new file mode 100644 index 0000000..e263760 Binary files /dev/null and b/admin-ui/public/favicon.ico differ diff --git a/admin-ui/src/App.vue b/admin-ui/src/App.vue new file mode 100644 index 0000000..31839f2 --- /dev/null +++ b/admin-ui/src/App.vue @@ -0,0 +1,15 @@ + + + diff --git a/admin-ui/src/api/login.js b/admin-ui/src/api/login.js new file mode 100644 index 0000000..f5825a1 --- /dev/null +++ b/admin-ui/src/api/login.js @@ -0,0 +1,51 @@ +import request from '@/utils/request' + +// 登录方法 +export function login(username, password, code, uuid) { + const data = { + username, + password, + code, + uuid + } + return request({ + url: '/login', + + method: 'post', + data: data + }) +} + +// 注册方法 +export function register(data) { + return request({ + url: '/register', + method: 'post', + data: data + }) +} + +// 获取用户详细信息 +export function getInfo() { + return request({ + url: '/getInfo', + method: 'get' + }) +} + +// 退出方法 +export function logout() { + return request({ + url: '/logout', + method: 'post' + }) +} + +// 获取验证码 +export function getCodeImg() { + return request({ + url: '/captchaImage', + method: 'get', + timeout: 20000 + }) +} \ No newline at end of file diff --git a/admin-ui/src/api/menu.js b/admin-ui/src/api/menu.js new file mode 100644 index 0000000..faef101 --- /dev/null +++ b/admin-ui/src/api/menu.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 获取路由 +export const getRouters = () => { + return request({ + url: '/getRouters', + method: 'get' + }) +} \ No newline at end of file diff --git a/admin-ui/src/api/monitor/cache.js b/admin-ui/src/api/monitor/cache.js new file mode 100644 index 0000000..72c5f6a --- /dev/null +++ b/admin-ui/src/api/monitor/cache.js @@ -0,0 +1,57 @@ +import request from '@/utils/request' + +// 查询缓存详细 +export function getCache() { + return request({ + url: '/monitor/cache', + method: 'get' + }) +} + +// 查询缓存名称列表 +export function listCacheName() { + return request({ + url: '/monitor/cache/getNames', + method: 'get' + }) +} + +// 查询缓存键名列表 +export function listCacheKey(cacheName) { + return request({ + url: '/monitor/cache/getKeys/' + cacheName, + method: 'get' + }) +} + +// 查询缓存内容 +export function getCacheValue(cacheName, cacheKey) { + return request({ + url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey, + method: 'get' + }) +} + +// 清理指定名称缓存 +export function clearCacheName(cacheName) { + return request({ + url: '/monitor/cache/clearCacheName/' + cacheName, + method: 'delete' + }) +} + +// 清理指定键名缓存 +export function clearCacheKey(cacheKey) { + return request({ + url: '/monitor/cache/clearCacheKey/' + cacheKey, + method: 'delete' + }) +} + +// 清理全部缓存 +export function clearCacheAll() { + return request({ + url: '/monitor/cache/clearCacheAll', + method: 'delete' + }) +} diff --git a/admin-ui/src/api/monitor/job.js b/admin-ui/src/api/monitor/job.js new file mode 100644 index 0000000..3815569 --- /dev/null +++ b/admin-ui/src/api/monitor/job.js @@ -0,0 +1,71 @@ +import request from '@/utils/request' + +// 查询定时任务调度列表 +export function listJob(query) { + return request({ + url: '/monitor/job/list', + method: 'get', + params: query + }) +} + +// 查询定时任务调度详细 +export function getJob(jobId) { + return request({ + url: '/monitor/job/' + jobId, + method: 'get' + }) +} + +// 新增定时任务调度 +export function addJob(data) { + return request({ + url: '/monitor/job', + method: 'post', + data: data + }) +} + +// 修改定时任务调度 +export function updateJob(data) { + return request({ + url: '/monitor/job', + method: 'put', + data: data + }) +} + +// 删除定时任务调度 +export function delJob(jobId) { + return request({ + url: '/monitor/job/' + jobId, + method: 'delete' + }) +} + +// 任务状态修改 +export function changeJobStatus(jobId, status) { + const data = { + jobId, + status + } + return request({ + url: '/monitor/job/changeStatus', + method: 'put', + data: data + }) +} + + +// 定时任务立即执行一次 +export function runJob(jobId, jobGroup) { + const data = { + jobId, + jobGroup + } + return request({ + url: '/monitor/job/run', + method: 'put', + data: data + }) +} \ No newline at end of file diff --git a/admin-ui/src/api/monitor/jobLog.js b/admin-ui/src/api/monitor/jobLog.js new file mode 100644 index 0000000..6e0be61 --- /dev/null +++ b/admin-ui/src/api/monitor/jobLog.js @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// 查询调度日志列表 +export function listJobLog(query) { + return request({ + url: '/monitor/jobLog/list', + method: 'get', + params: query + }) +} + +// 删除调度日志 +export function delJobLog(jobLogId) { + return request({ + url: '/monitor/jobLog/' + jobLogId, + method: 'delete' + }) +} + +// 清空调度日志 +export function cleanJobLog() { + return request({ + url: '/monitor/jobLog/clean', + method: 'delete' + }) +} diff --git a/admin-ui/src/api/monitor/logininfor.js b/admin-ui/src/api/monitor/logininfor.js new file mode 100644 index 0000000..4d112b7 --- /dev/null +++ b/admin-ui/src/api/monitor/logininfor.js @@ -0,0 +1,34 @@ +import request from '@/utils/request' + +// 查询登录日志列表 +export function list(query) { + return request({ + url: '/monitor/logininfor/list', + method: 'get', + params: query + }) +} + +// 删除登录日志 +export function delLogininfor(infoId) { + return request({ + url: '/monitor/logininfor/' + infoId, + method: 'delete' + }) +} + +// 解锁用户登录状态 +export function unlockLogininfor(userName) { + return request({ + url: '/monitor/logininfor/unlock/' + userName, + method: 'get' + }) +} + +// 清空登录日志 +export function cleanLogininfor() { + return request({ + url: '/monitor/logininfor/clean', + method: 'delete' + }) +} diff --git a/admin-ui/src/api/monitor/online.js b/admin-ui/src/api/monitor/online.js new file mode 100644 index 0000000..bd22137 --- /dev/null +++ b/admin-ui/src/api/monitor/online.js @@ -0,0 +1,18 @@ +import request from '@/utils/request' + +// 查询在线用户列表 +export function list(query) { + return request({ + url: '/monitor/online/list', + method: 'get', + params: query + }) +} + +// 强退用户 +export function forceLogout(tokenId) { + return request({ + url: '/monitor/online/' + tokenId, + method: 'delete' + }) +} diff --git a/admin-ui/src/api/monitor/operlog.js b/admin-ui/src/api/monitor/operlog.js new file mode 100644 index 0000000..a04bca8 --- /dev/null +++ b/admin-ui/src/api/monitor/operlog.js @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// 查询操作日志列表 +export function list(query) { + return request({ + url: '/monitor/operlog/list', + method: 'get', + params: query + }) +} + +// 删除操作日志 +export function delOperlog(operId) { + return request({ + url: '/monitor/operlog/' + operId, + method: 'delete' + }) +} + +// 清空操作日志 +export function cleanOperlog() { + return request({ + url: '/monitor/operlog/clean', + method: 'delete' + }) +} diff --git a/admin-ui/src/api/monitor/server.js b/admin-ui/src/api/monitor/server.js new file mode 100644 index 0000000..e1f9ca2 --- /dev/null +++ b/admin-ui/src/api/monitor/server.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +// 获取服务信息 +export function getServer() { + return request({ + url: '/monitor/server', + method: 'get' + }) +} \ No newline at end of file diff --git a/admin-ui/src/api/system/config.js b/admin-ui/src/api/system/config.js new file mode 100644 index 0000000..a404d82 --- /dev/null +++ b/admin-ui/src/api/system/config.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询参数列表 +export function listConfig(query) { + return request({ + url: '/system/config/list', + method: 'get', + params: query + }) +} + +// 查询参数详细 +export function getConfig(configId) { + return request({ + url: '/system/config/' + configId, + method: 'get' + }) +} + +// 根据参数键名查询参数值 +export function getConfigKey(configKey) { + return request({ + url: '/system/config/configKey/' + configKey, + method: 'get' + }) +} + +// 新增参数配置 +export function addConfig(data) { + return request({ + url: '/system/config', + method: 'post', + data: data + }) +} + +// 修改参数配置 +export function updateConfig(data) { + return request({ + url: '/system/config', + method: 'put', + data: data + }) +} + +// 删除参数配置 +export function delConfig(configId) { + return request({ + url: '/system/config/' + configId, + method: 'delete' + }) +} + +// 刷新参数缓存 +export function refreshCache() { + return request({ + url: '/system/config/refreshCache', + method: 'delete' + }) +} diff --git a/admin-ui/src/api/system/dept.js b/admin-ui/src/api/system/dept.js new file mode 100644 index 0000000..fc943cd --- /dev/null +++ b/admin-ui/src/api/system/dept.js @@ -0,0 +1,52 @@ +import request from '@/utils/request' + +// 查询部门列表 +export function listDept(query) { + return request({ + url: '/system/dept/list', + method: 'get', + params: query + }) +} + +// 查询部门列表(排除节点) +export function listDeptExcludeChild(deptId) { + return request({ + url: '/system/dept/list/exclude/' + deptId, + method: 'get' + }) +} + +// 查询部门详细 +export function getDept(deptId) { + return request({ + url: '/system/dept/' + deptId, + method: 'get' + }) +} + +// 新增部门 +export function addDept(data) { + return request({ + url: '/system/dept', + method: 'post', + data: data + }) +} + +// 修改部门 +export function updateDept(data) { + return request({ + url: '/system/dept', + method: 'put', + data: data + }) +} + +// 删除部门 +export function delDept(deptId) { + return request({ + url: '/system/dept/' + deptId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/admin-ui/src/api/system/dict/data.js b/admin-ui/src/api/system/dict/data.js new file mode 100644 index 0000000..6c9eb79 --- /dev/null +++ b/admin-ui/src/api/system/dict/data.js @@ -0,0 +1,52 @@ +import request from '@/utils/request' + +// 查询字典数据列表 +export function listData(query) { + return request({ + url: '/system/dict/data/list', + method: 'get', + params: query + }) +} + +// 查询字典数据详细 +export function getData(dictCode) { + return request({ + url: '/system/dict/data/' + dictCode, + method: 'get' + }) +} + +// 根据字典类型查询字典数据信息 +export function getDicts(dictType) { + return request({ + url: '/system/dict/data/type/' + dictType, + method: 'get' + }) +} + +// 新增字典数据 +export function addData(data) { + return request({ + url: '/system/dict/data', + method: 'post', + data: data + }) +} + +// 修改字典数据 +export function updateData(data) { + return request({ + url: '/system/dict/data', + method: 'put', + data: data + }) +} + +// 删除字典数据 +export function delData(dictCode) { + return request({ + url: '/system/dict/data/' + dictCode, + method: 'delete' + }) +} diff --git a/admin-ui/src/api/system/dict/type.js b/admin-ui/src/api/system/dict/type.js new file mode 100644 index 0000000..a0254ba --- /dev/null +++ b/admin-ui/src/api/system/dict/type.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询字典类型列表 +export function listType(query) { + return request({ + url: '/system/dict/type/list', + method: 'get', + params: query + }) +} + +// 查询字典类型详细 +export function getType(dictId) { + return request({ + url: '/system/dict/type/' + dictId, + method: 'get' + }) +} + +// 新增字典类型 +export function addType(data) { + return request({ + url: '/system/dict/type', + method: 'post', + data: data + }) +} + +// 修改字典类型 +export function updateType(data) { + return request({ + url: '/system/dict/type', + method: 'put', + data: data + }) +} + +// 删除字典类型 +export function delType(dictId) { + return request({ + url: '/system/dict/type/' + dictId, + method: 'delete' + }) +} + +// 刷新字典缓存 +export function refreshCache() { + return request({ + url: '/system/dict/type/refreshCache', + method: 'delete' + }) +} + +// 获取字典选择框列表 +export function optionselect() { + return request({ + url: '/system/dict/type/optionselect', + method: 'get' + }) +} diff --git a/admin-ui/src/api/system/menu.js b/admin-ui/src/api/system/menu.js new file mode 100644 index 0000000..f6415c6 --- /dev/null +++ b/admin-ui/src/api/system/menu.js @@ -0,0 +1,60 @@ +import request from '@/utils/request' + +// 查询菜单列表 +export function listMenu(query) { + return request({ + url: '/system/menu/list', + method: 'get', + params: query + }) +} + +// 查询菜单详细 +export function getMenu(menuId) { + return request({ + url: '/system/menu/' + menuId, + method: 'get' + }) +} + +// 查询菜单下拉树结构 +export function treeselect() { + return request({ + url: '/system/menu/treeselect', + method: 'get' + }) +} + +// 根据角色ID查询菜单下拉树结构 +export function roleMenuTreeselect(roleId) { + return request({ + url: '/system/menu/roleMenuTreeselect/' + roleId, + method: 'get' + }) +} + +// 新增菜单 +export function addMenu(data) { + return request({ + url: '/system/menu', + method: 'post', + data: data + }) +} + +// 修改菜单 +export function updateMenu(data) { + return request({ + url: '/system/menu', + method: 'put', + data: data + }) +} + +// 删除菜单 +export function delMenu(menuId) { + return request({ + url: '/system/menu/' + menuId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/admin-ui/src/api/system/notice.js b/admin-ui/src/api/system/notice.js new file mode 100644 index 0000000..c274ea5 --- /dev/null +++ b/admin-ui/src/api/system/notice.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询公告列表 +export function listNotice(query) { + return request({ + url: '/system/notice/list', + method: 'get', + params: query + }) +} + +// 查询公告详细 +export function getNotice(noticeId) { + return request({ + url: '/system/notice/' + noticeId, + method: 'get' + }) +} + +// 新增公告 +export function addNotice(data) { + return request({ + url: '/system/notice', + method: 'post', + data: data + }) +} + +// 修改公告 +export function updateNotice(data) { + return request({ + url: '/system/notice', + method: 'put', + data: data + }) +} + +// 删除公告 +export function delNotice(noticeId) { + return request({ + url: '/system/notice/' + noticeId, + method: 'delete' + }) +} \ No newline at end of file diff --git a/admin-ui/src/api/system/post.js b/admin-ui/src/api/system/post.js new file mode 100644 index 0000000..1a8e9ca --- /dev/null +++ b/admin-ui/src/api/system/post.js @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询岗位列表 +export function listPost(query) { + return request({ + url: '/system/post/list', + method: 'get', + params: query + }) +} + +// 查询岗位详细 +export function getPost(postId) { + return request({ + url: '/system/post/' + postId, + method: 'get' + }) +} + +// 新增岗位 +export function addPost(data) { + return request({ + url: '/system/post', + method: 'post', + data: data + }) +} + +// 修改岗位 +export function updatePost(data) { + return request({ + url: '/system/post', + method: 'put', + data: data + }) +} + +// 删除岗位 +export function delPost(postId) { + return request({ + url: '/system/post/' + postId, + method: 'delete' + }) +} diff --git a/admin-ui/src/api/system/role.js b/admin-ui/src/api/system/role.js new file mode 100644 index 0000000..f13e6f4 --- /dev/null +++ b/admin-ui/src/api/system/role.js @@ -0,0 +1,119 @@ +import request from '@/utils/request' + +// 查询角色列表 +export function listRole(query) { + return request({ + url: '/system/role/list', + method: 'get', + params: query + }) +} + +// 查询角色详细 +export function getRole(roleId) { + return request({ + url: '/system/role/' + roleId, + method: 'get' + }) +} + +// 新增角色 +export function addRole(data) { + return request({ + url: '/system/role', + method: 'post', + data: data + }) +} + +// 修改角色 +export function updateRole(data) { + return request({ + url: '/system/role', + method: 'put', + data: data + }) +} + +// 角色数据权限 +export function dataScope(data) { + return request({ + url: '/system/role/dataScope', + method: 'put', + data: data + }) +} + +// 角色状态修改 +export function changeRoleStatus(roleId, status) { + const data = { + roleId, + status + } + return request({ + url: '/system/role/changeStatus', + method: 'put', + data: data + }) +} + +// 删除角色 +export function delRole(roleId) { + return request({ + url: '/system/role/' + roleId, + method: 'delete' + }) +} + +// 查询角色已授权用户列表 +export function allocatedUserList(query) { + return request({ + url: '/system/role/authUser/allocatedList', + method: 'get', + params: query + }) +} + +// 查询角色未授权用户列表 +export function unallocatedUserList(query) { + return request({ + url: '/system/role/authUser/unallocatedList', + method: 'get', + params: query + }) +} + +// 取消用户授权角色 +export function authUserCancel(data) { + return request({ + url: '/system/role/authUser/cancel', + method: 'put', + data: data + }) +} + +// 批量取消用户授权角色 +export function authUserCancelAll(data) { + return request({ + url: '/system/role/authUser/cancelAll', + method: 'put', + params: data + }) +} + +// 授权用户选择 +export function authUserSelectAll(data) { + return request({ + url: '/system/role/authUser/selectAll', + method: 'put', + params: data + }) +} + +// 根据角色ID查询部门树结构 +export function deptTreeSelect(roleId) { + return request({ + url: '/system/role/deptTree/' + roleId, + method: 'get' + }) +} diff --git a/admin-ui/src/api/system/user.js b/admin-ui/src/api/system/user.js new file mode 100644 index 0000000..147343e --- /dev/null +++ b/admin-ui/src/api/system/user.js @@ -0,0 +1,135 @@ +import request from '@/utils/request' +import { parseStrEmpty } from "@/utils/ruoyi"; + +// 查询用户列表 +export function listUser(query) { + return request({ + url: '/system/user/list', + method: 'get', + params: query + }) +} + +// 查询用户详细 +export function getUser(userId) { + return request({ + url: '/system/user/' + parseStrEmpty(userId), + method: 'get' + }) +} + +// 新增用户 +export function addUser(data) { + return request({ + url: '/system/user/', + method: 'post', + data: data + }) +} + +// 修改用户 +export function updateUser(data) { + return request({ + url: '/system/user/', + method: 'put', + data: data + }) +} + +// 删除用户 +export function delUser(userId) { + return request({ + url: '/system/user/' + userId, + method: 'delete' + }) +} + +// 用户密码重置 +export function resetUserPwd(userId, password) { + const data = { + userId, + password + } + return request({ + url: '/system/user/resetPwd', + method: 'put', + data: data + }) +} + +// 用户状态修改 +export function changeUserStatus(userId, status) { + const data = { + userId, + status + } + return request({ + url: '/system/user/changeStatus', + method: 'put', + data: data + }) +} + +// 查询用户个人信息 +export function getUserProfile() { + return request({ + url: '/system/user/profile', + method: 'get' + }) +} + +// 修改用户个人信息 +export function updateUserProfile(data) { + return request({ + url: '/system/user/profile', + method: 'put', + data: data + }) +} + +// 用户密码重置 +export function updateUserPwd(oldPassword, newPassword) { + const data = { + oldPassword, + newPassword + } + return request({ + url: '/system/user/profile/updatePwd', + method: 'put', + params: data + }) +} + +// 用户头像上传 +export function uploadAvatar(data) { + return request({ + url: '/system/user/profile/avatar', + method: 'post', + data: data + }) +} + +// 查询授权角色 +export function getAuthRole(userId) { + return request({ + url: '/system/user/authRole/' + userId, + method: 'get' + }) +} + +// 保存授权角色 +export function updateAuthRole(data) { + return request({ + url: '/system/user/authRole', + method: 'put', + params: data + }) +} + +// 查询部门下拉树结构 +export function deptTreeSelect() { + return request({ + url: '/system/user/deptTree', + method: 'get' + }) +} diff --git a/admin-ui/src/api/tool/gen.js b/admin-ui/src/api/tool/gen.js new file mode 100644 index 0000000..4506927 --- /dev/null +++ b/admin-ui/src/api/tool/gen.js @@ -0,0 +1,76 @@ +import request from '@/utils/request' + +// 查询生成表数据 +export function listTable(query) { + return request({ + url: '/tool/gen/list', + method: 'get', + params: query + }) +} +// 查询db数据库列表 +export function listDbTable(query) { + return request({ + url: '/tool/gen/db/list', + method: 'get', + params: query + }) +} + +// 查询表详细信息 +export function getGenTable(tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'get' + }) +} + +// 修改代码生成信息 +export function updateGenTable(data) { + return request({ + url: '/tool/gen', + method: 'put', + data: data + }) +} + +// 导入表 +export function importTable(data) { + return request({ + url: '/tool/gen/importTable', + method: 'post', + params: data + }) +} + +// 预览生成代码 +export function previewTable(tableId) { + return request({ + url: '/tool/gen/preview/' + tableId, + method: 'get' + }) +} + +// 删除表数据 +export function delTable(tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'delete' + }) +} + +// 生成代码(自定义路径) +export function genCode(tableName) { + return request({ + url: '/tool/gen/genCode/' + tableName, + method: 'get' + }) +} + +// 同步数据库 +export function synchDb(tableName) { + return request({ + url: '/tool/gen/synchDb/' + tableName, + method: 'get' + }) +} diff --git a/admin-ui/src/assets/icons/svg/404.svg b/admin-ui/src/assets/icons/svg/404.svg new file mode 100644 index 0000000..6df5019 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/bug.svg b/admin-ui/src/assets/icons/svg/bug.svg new file mode 100644 index 0000000..05a150d --- /dev/null +++ b/admin-ui/src/assets/icons/svg/bug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/build.svg b/admin-ui/src/assets/icons/svg/build.svg new file mode 100644 index 0000000..97c4688 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/build.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/button.svg b/admin-ui/src/assets/icons/svg/button.svg new file mode 100644 index 0000000..904fddc --- /dev/null +++ b/admin-ui/src/assets/icons/svg/button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/cascader.svg b/admin-ui/src/assets/icons/svg/cascader.svg new file mode 100644 index 0000000..e256024 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/cascader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/chart.svg b/admin-ui/src/assets/icons/svg/chart.svg new file mode 100644 index 0000000..27728fb --- /dev/null +++ b/admin-ui/src/assets/icons/svg/chart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/checkbox.svg b/admin-ui/src/assets/icons/svg/checkbox.svg new file mode 100644 index 0000000..013fd3a --- /dev/null +++ b/admin-ui/src/assets/icons/svg/checkbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/clipboard.svg b/admin-ui/src/assets/icons/svg/clipboard.svg new file mode 100644 index 0000000..90923ff --- /dev/null +++ b/admin-ui/src/assets/icons/svg/clipboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/code.svg b/admin-ui/src/assets/icons/svg/code.svg new file mode 100644 index 0000000..5f9c5ab --- /dev/null +++ b/admin-ui/src/assets/icons/svg/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/color.svg b/admin-ui/src/assets/icons/svg/color.svg new file mode 100644 index 0000000..44a81aa --- /dev/null +++ b/admin-ui/src/assets/icons/svg/color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/component.svg b/admin-ui/src/assets/icons/svg/component.svg new file mode 100644 index 0000000..29c3458 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/component.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/dashboard.svg b/admin-ui/src/assets/icons/svg/dashboard.svg new file mode 100644 index 0000000..5317d37 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/date-range.svg b/admin-ui/src/assets/icons/svg/date-range.svg new file mode 100644 index 0000000..fda571e --- /dev/null +++ b/admin-ui/src/assets/icons/svg/date-range.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/date.svg b/admin-ui/src/assets/icons/svg/date.svg new file mode 100644 index 0000000..52dc73e --- /dev/null +++ b/admin-ui/src/assets/icons/svg/date.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/dict.svg b/admin-ui/src/assets/icons/svg/dict.svg new file mode 100644 index 0000000..4849377 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/dict.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/documentation.svg b/admin-ui/src/assets/icons/svg/documentation.svg new file mode 100644 index 0000000..7043122 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/documentation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/download.svg b/admin-ui/src/assets/icons/svg/download.svg new file mode 100644 index 0000000..c896951 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/drag.svg b/admin-ui/src/assets/icons/svg/drag.svg new file mode 100644 index 0000000..4185d3c --- /dev/null +++ b/admin-ui/src/assets/icons/svg/drag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/druid.svg b/admin-ui/src/assets/icons/svg/druid.svg new file mode 100644 index 0000000..a2b4b4e --- /dev/null +++ b/admin-ui/src/assets/icons/svg/druid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/edit.svg b/admin-ui/src/assets/icons/svg/edit.svg new file mode 100644 index 0000000..d26101f --- /dev/null +++ b/admin-ui/src/assets/icons/svg/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/education.svg b/admin-ui/src/assets/icons/svg/education.svg new file mode 100644 index 0000000..7bfb01d --- /dev/null +++ b/admin-ui/src/assets/icons/svg/education.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/email.svg b/admin-ui/src/assets/icons/svg/email.svg new file mode 100644 index 0000000..74d25e2 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/example.svg b/admin-ui/src/assets/icons/svg/example.svg new file mode 100644 index 0000000..46f42b5 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/excel.svg b/admin-ui/src/assets/icons/svg/excel.svg new file mode 100644 index 0000000..74d97b8 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/excel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/exit-fullscreen.svg b/admin-ui/src/assets/icons/svg/exit-fullscreen.svg new file mode 100644 index 0000000..485c128 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/exit-fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/eye-open.svg b/admin-ui/src/assets/icons/svg/eye-open.svg new file mode 100644 index 0000000..88dcc98 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/eye-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/eye.svg b/admin-ui/src/assets/icons/svg/eye.svg new file mode 100644 index 0000000..16ed2d8 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/form.svg b/admin-ui/src/assets/icons/svg/form.svg new file mode 100644 index 0000000..dcbaa18 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/form.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/fullscreen.svg b/admin-ui/src/assets/icons/svg/fullscreen.svg new file mode 100644 index 0000000..0e86b6f --- /dev/null +++ b/admin-ui/src/assets/icons/svg/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/github.svg b/admin-ui/src/assets/icons/svg/github.svg new file mode 100644 index 0000000..db0a0d4 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/guide.svg b/admin-ui/src/assets/icons/svg/guide.svg new file mode 100644 index 0000000..b271001 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/guide.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/icon.svg b/admin-ui/src/assets/icons/svg/icon.svg new file mode 100644 index 0000000..82be8ee --- /dev/null +++ b/admin-ui/src/assets/icons/svg/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/input.svg b/admin-ui/src/assets/icons/svg/input.svg new file mode 100644 index 0000000..ab91381 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/input.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/international.svg b/admin-ui/src/assets/icons/svg/international.svg new file mode 100644 index 0000000..e9b56ee --- /dev/null +++ b/admin-ui/src/assets/icons/svg/international.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/job.svg b/admin-ui/src/assets/icons/svg/job.svg new file mode 100644 index 0000000..2a93a25 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/job.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/language.svg b/admin-ui/src/assets/icons/svg/language.svg new file mode 100644 index 0000000..0082b57 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/link.svg b/admin-ui/src/assets/icons/svg/link.svg new file mode 100644 index 0000000..48197ba --- /dev/null +++ b/admin-ui/src/assets/icons/svg/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/list.svg b/admin-ui/src/assets/icons/svg/list.svg new file mode 100644 index 0000000..20259ed --- /dev/null +++ b/admin-ui/src/assets/icons/svg/list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/lock.svg b/admin-ui/src/assets/icons/svg/lock.svg new file mode 100644 index 0000000..74fee54 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/log.svg b/admin-ui/src/assets/icons/svg/log.svg new file mode 100644 index 0000000..d879d33 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/log.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/logininfor.svg b/admin-ui/src/assets/icons/svg/logininfor.svg new file mode 100644 index 0000000..267f844 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/logininfor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/message.svg b/admin-ui/src/assets/icons/svg/message.svg new file mode 100644 index 0000000..14ca817 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/money.svg b/admin-ui/src/assets/icons/svg/money.svg new file mode 100644 index 0000000..c1580de --- /dev/null +++ b/admin-ui/src/assets/icons/svg/money.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/monitor.svg b/admin-ui/src/assets/icons/svg/monitor.svg new file mode 100644 index 0000000..bc308cb --- /dev/null +++ b/admin-ui/src/assets/icons/svg/monitor.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/nested.svg b/admin-ui/src/assets/icons/svg/nested.svg new file mode 100644 index 0000000..06713a8 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/nested.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/number.svg b/admin-ui/src/assets/icons/svg/number.svg new file mode 100644 index 0000000..ad5ce9a --- /dev/null +++ b/admin-ui/src/assets/icons/svg/number.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/online.svg b/admin-ui/src/assets/icons/svg/online.svg new file mode 100644 index 0000000..330a202 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/online.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/password.svg b/admin-ui/src/assets/icons/svg/password.svg new file mode 100644 index 0000000..6c64def --- /dev/null +++ b/admin-ui/src/assets/icons/svg/password.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/pdf.svg b/admin-ui/src/assets/icons/svg/pdf.svg new file mode 100644 index 0000000..957aa0c --- /dev/null +++ b/admin-ui/src/assets/icons/svg/pdf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/people.svg b/admin-ui/src/assets/icons/svg/people.svg new file mode 100644 index 0000000..2bd54ae --- /dev/null +++ b/admin-ui/src/assets/icons/svg/people.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/peoples.svg b/admin-ui/src/assets/icons/svg/peoples.svg new file mode 100644 index 0000000..aab852e --- /dev/null +++ b/admin-ui/src/assets/icons/svg/peoples.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/phone.svg b/admin-ui/src/assets/icons/svg/phone.svg new file mode 100644 index 0000000..ab8e8c4 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/post.svg b/admin-ui/src/assets/icons/svg/post.svg new file mode 100644 index 0000000..2922c61 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/post.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/qq.svg b/admin-ui/src/assets/icons/svg/qq.svg new file mode 100644 index 0000000..ee13d4e --- /dev/null +++ b/admin-ui/src/assets/icons/svg/qq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/question.svg b/admin-ui/src/assets/icons/svg/question.svg new file mode 100644 index 0000000..cf75bd4 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/question.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/radio.svg b/admin-ui/src/assets/icons/svg/radio.svg new file mode 100644 index 0000000..0cde345 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/radio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/rate.svg b/admin-ui/src/assets/icons/svg/rate.svg new file mode 100644 index 0000000..aa3b14d --- /dev/null +++ b/admin-ui/src/assets/icons/svg/rate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/redis-list.svg b/admin-ui/src/assets/icons/svg/redis-list.svg new file mode 100644 index 0000000..98a15b2 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/redis-list.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/redis.svg b/admin-ui/src/assets/icons/svg/redis.svg new file mode 100644 index 0000000..2f1d62d --- /dev/null +++ b/admin-ui/src/assets/icons/svg/redis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/row.svg b/admin-ui/src/assets/icons/svg/row.svg new file mode 100644 index 0000000..0780992 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/row.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/search.svg b/admin-ui/src/assets/icons/svg/search.svg new file mode 100644 index 0000000..84233dd --- /dev/null +++ b/admin-ui/src/assets/icons/svg/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/select.svg b/admin-ui/src/assets/icons/svg/select.svg new file mode 100644 index 0000000..d628382 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/server.svg b/admin-ui/src/assets/icons/svg/server.svg new file mode 100644 index 0000000..eb287e3 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/shopping.svg b/admin-ui/src/assets/icons/svg/shopping.svg new file mode 100644 index 0000000..87513e7 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/shopping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/size.svg b/admin-ui/src/assets/icons/svg/size.svg new file mode 100644 index 0000000..ddb25b8 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/size.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/skill.svg b/admin-ui/src/assets/icons/svg/skill.svg new file mode 100644 index 0000000..a3b7312 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/skill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/slider.svg b/admin-ui/src/assets/icons/svg/slider.svg new file mode 100644 index 0000000..fbe4f39 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/slider.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/star.svg b/admin-ui/src/assets/icons/svg/star.svg new file mode 100644 index 0000000..6cf86e6 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/swagger.svg b/admin-ui/src/assets/icons/svg/swagger.svg new file mode 100644 index 0000000..05d4e7b --- /dev/null +++ b/admin-ui/src/assets/icons/svg/swagger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/switch.svg b/admin-ui/src/assets/icons/svg/switch.svg new file mode 100644 index 0000000..0ba61e3 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/switch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/system.svg b/admin-ui/src/assets/icons/svg/system.svg new file mode 100644 index 0000000..5992593 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/system.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/tab.svg b/admin-ui/src/assets/icons/svg/tab.svg new file mode 100644 index 0000000..b4b48e4 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/table.svg b/admin-ui/src/assets/icons/svg/table.svg new file mode 100644 index 0000000..0e3dc9d --- /dev/null +++ b/admin-ui/src/assets/icons/svg/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/textarea.svg b/admin-ui/src/assets/icons/svg/textarea.svg new file mode 100644 index 0000000..2709f29 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/textarea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/theme.svg b/admin-ui/src/assets/icons/svg/theme.svg new file mode 100644 index 0000000..5982a2f --- /dev/null +++ b/admin-ui/src/assets/icons/svg/theme.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/time-range.svg b/admin-ui/src/assets/icons/svg/time-range.svg new file mode 100644 index 0000000..13c1202 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/time-range.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/time.svg b/admin-ui/src/assets/icons/svg/time.svg new file mode 100644 index 0000000..b376e32 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/time.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/tool.svg b/admin-ui/src/assets/icons/svg/tool.svg new file mode 100644 index 0000000..48e0e35 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/tool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/tree-table.svg b/admin-ui/src/assets/icons/svg/tree-table.svg new file mode 100644 index 0000000..8aafdb8 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/tree-table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/tree.svg b/admin-ui/src/assets/icons/svg/tree.svg new file mode 100644 index 0000000..dd4b7dd --- /dev/null +++ b/admin-ui/src/assets/icons/svg/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/upload.svg b/admin-ui/src/assets/icons/svg/upload.svg new file mode 100644 index 0000000..bae49c0 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/user.svg b/admin-ui/src/assets/icons/svg/user.svg new file mode 100644 index 0000000..0ba0716 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/validCode.svg b/admin-ui/src/assets/icons/svg/validCode.svg new file mode 100644 index 0000000..cfb1021 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/validCode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/wechat.svg b/admin-ui/src/assets/icons/svg/wechat.svg new file mode 100644 index 0000000..c586e55 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/wechat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/icons/svg/zip.svg b/admin-ui/src/assets/icons/svg/zip.svg new file mode 100644 index 0000000..f806fc4 --- /dev/null +++ b/admin-ui/src/assets/icons/svg/zip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-ui/src/assets/images/dark.svg b/admin-ui/src/assets/images/dark.svg new file mode 100644 index 0000000..f646bd7 --- /dev/null +++ b/admin-ui/src/assets/images/dark.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin-ui/src/assets/images/light.svg b/admin-ui/src/assets/images/light.svg new file mode 100644 index 0000000..ab7cc08 --- /dev/null +++ b/admin-ui/src/assets/images/light.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin-ui/src/assets/styles/btn.scss b/admin-ui/src/assets/styles/btn.scss new file mode 100644 index 0000000..3590d8d --- /dev/null +++ b/admin-ui/src/assets/styles/btn.scss @@ -0,0 +1,99 @@ +@import './variables.module.scss'; + +@mixin colorBtn($color) { + background: $color; + + &:hover { + color: $color; + + &:before, + &:after { + background: $color; + } + } +} + +.blue-btn { + @include colorBtn($blue) +} + +.light-blue-btn { + @include colorBtn($light-blue) +} + +.red-btn { + @include colorBtn($red) +} + +.pink-btn { + @include colorBtn($pink) +} + +.green-btn { + @include colorBtn($green) +} + +.tiffany-btn { + @include colorBtn($tiffany) +} + +.yellow-btn { + @include colorBtn($yellow) +} + +.pan-btn { + font-size: 14px; + color: #fff; + padding: 14px 36px; + border-radius: 8px; + border: none; + outline: none; + transition: 600ms ease all; + position: relative; + display: inline-block; + + &:hover { + background: #fff; + + &:before, + &:after { + width: 100%; + transition: 600ms ease all; + } + } + + &:before, + &:after { + content: ''; + position: absolute; + top: 0; + right: 0; + height: 2px; + width: 0; + transition: 400ms ease all; + } + + &::after { + right: inherit; + top: inherit; + left: 0; + bottom: 0; + } +} + +.custom-button { + display: inline-block; + line-height: 1; + white-space: nowrap; + cursor: pointer; + background: #fff; + color: #fff; + -webkit-appearance: none; + text-align: center; + box-sizing: border-box; + outline: 0; + margin: 0; + padding: 10px 15px; + font-size: 14px; + border-radius: 4px; +} diff --git a/admin-ui/src/assets/styles/element-ui.scss b/admin-ui/src/assets/styles/element-ui.scss new file mode 100644 index 0000000..0f175f2 --- /dev/null +++ b/admin-ui/src/assets/styles/element-ui.scss @@ -0,0 +1,96 @@ +// cover some element-ui styles + +.el-breadcrumb__inner, +.el-breadcrumb__inner a { + font-weight: 400 !important; +} + +.el-upload { + input[type="file"] { + display: none !important; + } +} + +.el-upload__input { + display: none; +} + +.cell { + .el-tag { + margin-right: 0px; + } +} + +.small-padding { + .cell { + padding-left: 5px; + padding-right: 5px; + } +} + +.fixed-width { + .el-button--mini { + padding: 7px 10px; + width: 60px; + } +} + +.status-col { + .cell { + padding: 0 10px; + text-align: center; + + .el-tag { + margin-right: 0px; + } + } +} + +// to fixed https://github.com/ElemeFE/element/issues/2461 +.el-dialog { + transform: none; + left: 0; + position: relative; + margin: 0 auto; +} + +// refine element ui upload +.upload-container { + .el-upload { + width: 100%; + + .el-upload-dragger { + width: 100%; + height: 200px; + } + } +} + +// dropdown +.el-dropdown-menu { + a { + display: block + } +} + +// fix date-picker ui bug in filter-item +.el-range-editor.el-input__inner { + display: inline-flex !important; +} + +// to fix el-date-picker css style +.el-range-separator { + box-sizing: content-box; +} + +.el-menu--collapse + > div + > .el-submenu + > .el-submenu__title + .el-submenu__icon-arrow { + display: none; +} + +.el-dropdown .el-dropdown-link{ + color: var(--el-color-primary) !important; +} \ No newline at end of file diff --git a/admin-ui/src/assets/styles/index.scss b/admin-ui/src/assets/styles/index.scss new file mode 100644 index 0000000..2b8dca5 --- /dev/null +++ b/admin-ui/src/assets/styles/index.scss @@ -0,0 +1,184 @@ +@import './variables.module.scss'; +@import './mixin.scss'; +@import './transition.scss'; +@import './element-ui.scss'; +@import './sidebar.scss'; +@import './btn.scss'; +@import './ruoyi.scss'; + +body { + height: 100%; + margin: 0; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; +} + +label { + font-weight: 700; +} + +html { + height: 100%; + box-sizing: border-box; +} + +#app { + height: 100%; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +.no-padding { + padding: 0px !important; +} + +.padding-content { + padding: 4px 0; +} + +a:focus, +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +div:focus { + outline: none; +} + +.fr { + float: right; +} + +.fl { + float: left; +} + +.pr-5 { + padding-right: 5px; +} + +.pl-5 { + padding-left: 5px; +} + +.block { + display: block; +} + +.pointer { + cursor: pointer; +} + +.inlineBlock { + display: block; +} + +.clearfix { + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +} + +aside { + background: #eef1f6; + padding: 8px 24px; + margin-bottom: 20px; + border-radius: 2px; + display: block; + line-height: 32px; + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + color: #2c3e50; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + a { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } + } +} + +//main-container全局样式 +.app-container { + padding: 20px; +} + +.components-container { + margin: 30px 50px; + position: relative; +} + +.pagination-container { + margin-top: 30px; +} + +.text-center { + text-align: center +} + +.sub-navbar { + height: 50px; + line-height: 50px; + position: relative; + width: 100%; + text-align: right; + padding-right: 20px; + transition: 600ms ease position; + background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%); + + .subtitle { + font-size: 20px; + color: #fff; + } + + &.draft { + background: #d0d0d0; + } + + &.deleted { + background: #d0d0d0; + } +} + +.link-type, +.link-type:focus { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } +} + +.filter-container { + padding-bottom: 10px; + + .filter-item { + display: inline-block; + vertical-align: middle; + margin-bottom: 10px; + } +} diff --git a/admin-ui/src/assets/styles/mixin.scss b/admin-ui/src/assets/styles/mixin.scss new file mode 100644 index 0000000..06fa061 --- /dev/null +++ b/admin-ui/src/assets/styles/mixin.scss @@ -0,0 +1,66 @@ +@mixin clearfix { + &:after { + content: ""; + display: table; + clear: both; + } +} + +@mixin scrollBar { + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } +} + +@mixin relative { + position: relative; + width: 100%; + height: 100%; +} + +@mixin pct($pct) { + width: #{$pct}; + position: relative; + margin: 0 auto; +} + +@mixin triangle($width, $height, $color, $direction) { + $width: $width/2; + $color-border-style: $height solid $color; + $transparent-border-style: $width solid transparent; + height: 0; + width: 0; + + @if $direction==up { + border-bottom: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==right { + border-left: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } + + @else if $direction==down { + border-top: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==left { + border-right: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } +} diff --git a/admin-ui/src/assets/styles/ruoyi.scss b/admin-ui/src/assets/styles/ruoyi.scss new file mode 100644 index 0000000..155fdb7 --- /dev/null +++ b/admin-ui/src/assets/styles/ruoyi.scss @@ -0,0 +1,281 @@ + /** + * 通用css样式布局处理 + * Copyright (c) 2019 ruoyi + */ + + /** 基础通用 **/ +.pt5 { + padding-top: 5px; +} +.pr5 { + padding-right: 5px; +} +.pb5 { + padding-bottom: 5px; +} +.mt5 { + margin-top: 5px; +} +.mr5 { + margin-right: 5px; +} +.mb5 { + margin-bottom: 5px; +} +.mb8 { + margin-bottom: 8px; +} +.ml5 { + margin-left: 5px; +} +.mt10 { + margin-top: 10px; +} +.mr10 { + margin-right: 10px; +} +.mb10 { + margin-bottom: 10px; +} +.ml10 { + margin-left: 10px; +} +.mt20 { + margin-top: 20px; +} +.mr20 { + margin-right: 20px; +} +.mb20 { + margin-bottom: 20px; +} +.ml20 { + margin-left: 20px; +} + +.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} + +.el-form .el-form-item__label { + font-weight: 700; +} +.el-dialog:not(.is-fullscreen) { + margin-top: 6vh !important; +} + +.el-dialog.scrollbar .el-dialog__body { + overflow: auto; + overflow-x: hidden; + max-height: 70vh; + padding: 10px 20px 0; +} + +.el-table { + .el-table__header-wrapper, .el-table__fixed-header-wrapper { + th { + word-break: break-word; + background-color: #f8f8f9 !important; + color: #515a6e; + height: 40px !important; + font-size: 13px; + } + } + .el-table__body-wrapper { + .el-button [class*="el-icon-"] + span { + margin-left: 1px; + } + } +} + +/** 表单布局 **/ +.form-header { + font-size:15px; + color:#6379bb; + border-bottom:1px solid #ddd; + margin:8px 10px 25px 10px; + padding-bottom:5px +} + +/** 表格布局 **/ +.pagination-container { + position: relative; + height: 25px; + margin-bottom: 10px; + margin-top: 15px; + padding: 10px 20px !important; +} + +.el-dialog .pagination-container { + position: static !important; +} + +/* tree border */ +.tree-border { + margin-top: 5px; + border: 1px solid #e5e6e7; + background: #FFFFFF none; + border-radius:4px; + width: 100%; +} + +.pagination-container .el-pagination { + right: 0; + position: absolute; +} + +@media ( max-width : 768px) { + .pagination-container .el-pagination > .el-pagination__jump { + display: none !important; + } + .pagination-container .el-pagination > .el-pagination__sizes { + display: none !important; + } +} + +.el-table .fixed-width .el-button--small { + padding-left: 0; + padding-right: 0; + width: inherit; +} + +/** 表格更多操作下拉样式 */ +.el-table .el-dropdown-link { + cursor: pointer; + color: #409EFF; + margin-left: 10px; +} + +.el-table .el-dropdown, .el-icon-arrow-down { + font-size: 12px; +} + +.el-tree-node__content > .el-checkbox { + margin-right: 8px; +} + +.list-group-striped > .list-group-item { + border-left: 0; + border-right: 0; + border-radius: 0; + padding-left: 0; + padding-right: 0; +} + +.list-group { + padding-left: 0px; + list-style: none; +} + +.list-group-item { + border-bottom: 1px solid #e7eaec; + border-top: 1px solid #e7eaec; + margin-bottom: -1px; + padding: 11px 0px; + font-size: 13px; +} + +.pull-right { + float: right !important; +} + +.el-card__header { + padding: 14px 15px 7px !important; + min-height: 40px; +} + +.el-card__body { + padding: 15px 20px 20px 20px !important; +} + +.card-box { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 10px; +} + +/* button color */ +.el-button--cyan.is-active, +.el-button--cyan:active { + background: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +.el-button--cyan:focus, +.el-button--cyan:hover { + background: #48D1CC; + border-color: #48D1CC; + color: #FFFFFF; +} + +.el-button--cyan { + background-color: #20B2AA; + border-color: #20B2AA; + color: #FFFFFF; +} + +/* text color */ +.text-navy { + color: #1ab394; +} + +.text-primary { + color: inherit; +} + +.text-success { + color: #1c84c6; +} + +.text-info { + color: #23c6c8; +} + +.text-warning { + color: #f8ac59; +} + +.text-danger { + color: #ed5565; +} + +.text-muted { + color: #888888; +} + +/* image */ +.img-circle { + border-radius: 50%; +} + +.img-lg { + width: 120px; + height: 120px; +} + +.avatar-upload-preview { + position: absolute; + top: 50%; + transform: translate(50%, -50%); + width: 200px; + height: 200px; + border-radius: 50%; + box-shadow: 0 0 4px #ccc; + overflow: hidden; +} + +/* 拖拽列样式 */ +.sortable-ghost{ + opacity: .8; + color: #fff!important; + background: #42b983!important; +} + +/* 表格右侧工具栏样式 */ +.top-right-btn { + margin-left: auto; +} diff --git a/admin-ui/src/assets/styles/sidebar.scss b/admin-ui/src/assets/styles/sidebar.scss new file mode 100644 index 0000000..8b3c472 --- /dev/null +++ b/admin-ui/src/assets/styles/sidebar.scss @@ -0,0 +1,238 @@ +#app { + + .main-container { + height: 100%; + transition: margin-left .28s; + margin-left: $base-sidebar-width; + position: relative; + } + + .sidebarHide { + margin-left: 0!important; + } + + .sidebar-container { + -webkit-transition: width .28s; + transition: width 0.28s; + width: $base-sidebar-width !important; + background-color: $base-menu-background; + height: 100%; + position: fixed; + font-size: 0px; + top: 0; + bottom: 0; + left: 0; + z-index: 1001; + overflow: hidden; + -webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35); + box-shadow: 2px 0 6px rgba(0,21,41,.35); + + // reset element-ui css + .horizontal-collapse-transition { + transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; + } + + .scrollbar-wrapper { + overflow-x: hidden !important; + } + + .el-scrollbar__bar.is-vertical { + right: 0px; + } + + .el-scrollbar { + height: 100%; + } + + &.has-logo { + .el-scrollbar { + height: calc(100% - 50px); + } + } + + .is-horizontal { + display: none; + } + + a { + display: inline-block; + width: 100%; + overflow: hidden; + } + + .svg-icon { + margin-right: 16px; + } + + .el-menu { + border: none; + height: 100%; + width: 100% !important; + } + + .el-menu-item, .menu-title { + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + } + + .el-menu-item .el-menu-tooltip__trigger { + display: inline-block !important; + } + + // menu hover + .sub-menu-title-noDropdown, + .el-sub-menu__title { + &:hover { + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + & .theme-dark .is-active > .el-sub-menu__title { + color: $base-menu-color-active !important; + } + + & .nest-menu .el-sub-menu>.el-sub-menu__title, + & .el-sub-menu .el-menu-item { + min-width: $base-sidebar-width !important; + + &:hover { + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title, + & .theme-dark .el-sub-menu .el-menu-item { + background-color: $base-sub-menu-background !important; + + &:hover { + background-color: $base-sub-menu-hover !important; + } + } + } + + .hideSidebar { + .sidebar-container { + width: 54px !important; + } + + .main-container { + margin-left: 54px; + } + + .sub-menu-title-noDropdown { + padding: 0 !important; + position: relative; + + .el-tooltip { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + } + } + + .el-sub-menu { + overflow: hidden; + + &>.el-sub-menu__title { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + + } + } + + .el-menu--collapse { + .el-sub-menu { + &>.el-sub-menu__title { + &>span { + height: 0; + width: 0; + overflow: hidden; + visibility: hidden; + display: inline-block; + } + &>i { + height: 0; + width: 0; + overflow: hidden; + visibility: hidden; + display: inline-block; + } + } + } + } + } + + .el-menu--collapse .el-menu .el-sub-menu { + min-width: $base-sidebar-width !important; + } + + // mobile responsive + .mobile { + .main-container { + margin-left: 0px; + } + + .sidebar-container { + transition: transform .28s; + width: $base-sidebar-width !important; + } + + &.hideSidebar { + .sidebar-container { + pointer-events: none; + transition-duration: 0.3s; + transform: translate3d(-$base-sidebar-width, 0, 0); + } + } + } + + .withoutAnimation { + + .main-container, + .sidebar-container { + transition: none; + } + } +} + +// when menu collapsed +.el-menu--vertical { + &>.el-menu { + .svg-icon { + margin-right: 16px; + } + } + + .nest-menu .el-sub-menu>.el-sub-menu__title, + .el-menu-item { + &:hover { + // you can use $sub-menuHover + background-color: rgba(0, 0, 0, 0.06) !important; + } + } + + // the scroll bar appears when the sub-menu is too long + >.el-menu--popup { + max-height: 100vh; + overflow-y: auto; + + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } + } +} diff --git a/admin-ui/src/assets/styles/transition.scss b/admin-ui/src/assets/styles/transition.scss new file mode 100644 index 0000000..073f8c6 --- /dev/null +++ b/admin-ui/src/assets/styles/transition.scss @@ -0,0 +1,49 @@ +// global transition css + +/* fade */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.28s; +} + +.fade-enter, +.fade-leave-active { + opacity: 0; +} + +/* fade-transform */ +.fade-transform--move, +.fade-transform-leave-active, +.fade-transform-enter-active { + transition: all .5s; +} + +.fade-transform-enter { + opacity: 0; + transform: translateX(-30px); +} + +.fade-transform-leave-to { + opacity: 0; + transform: translateX(30px); +} + +/* breadcrumb transition */ +.breadcrumb-enter-active, +.breadcrumb-leave-active { + transition: all .5s; +} + +.breadcrumb-enter, +.breadcrumb-leave-active { + opacity: 0; + transform: translateX(20px); +} + +.breadcrumb-move { + transition: all .5s; +} + +.breadcrumb-leave-active { + position: absolute; +} diff --git a/admin-ui/src/assets/styles/variables.module.scss b/admin-ui/src/assets/styles/variables.module.scss new file mode 100644 index 0000000..3dbfaa7 --- /dev/null +++ b/admin-ui/src/assets/styles/variables.module.scss @@ -0,0 +1,65 @@ +// base color +$blue: #324157; +$light-blue: #3A71A8; +$red: #C03639; +$pink: #E65D6E; +$green: #30B08F; +$tiffany: #4AB7BD; +$yellow: #FEC171; +$panGreen: #30B08F; + +// 默认菜单主题风格 +$base-menu-color: #bfcbd9; +$base-menu-color-active: #f4f4f5; +$base-menu-background: #304156; +$base-logo-title-color: #ffffff; + +$base-menu-light-color: rgba(0, 0, 0, 0.7); +$base-menu-light-background: #ffffff; +$base-logo-light-title-color: #001529; + +$base-sub-menu-background: #1f2d3d; +$base-sub-menu-hover: #001528; + +// 自定义暗色菜单风格 +/** +$base-menu-color:hsla(0,0%,100%,.65); +$base-menu-color-active:#fff; +$base-menu-background:#001529; +$base-logo-title-color: #ffffff; + +$base-menu-light-color:rgba(0,0,0,.70); +$base-menu-light-background:#ffffff; +$base-logo-light-title-color: #001529; + +$base-sub-menu-background:#000c17; +$base-sub-menu-hover:#001528; +*/ + +$--color-primary: #409EFF; +$--color-success: #67C23A; +$--color-warning: #E6A23C; +$--color-danger: #F56C6C; +$--color-info: #909399; + +$base-sidebar-width: 200px; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + menuColor: $base-menu-color; + menuLightColor: $base-menu-light-color; + menuColorActive: $base-menu-color-active; + menuBackground: $base-menu-background; + menuLightBackground: $base-menu-light-background; + subMenuBackground: $base-sub-menu-background; + subMenuHover: $base-sub-menu-hover; + sideBarWidth: $base-sidebar-width; + logoTitleColor: $base-logo-title-color; + logoLightTitleColor: $base-logo-light-title-color; + primaryColor: $--color-primary; + successColor: $--color-success; + dangerColor: $--color-danger; + infoColor: $--color-info; + warningColor: $--color-warning; +} diff --git a/admin-ui/src/components/Breadcrumb/index.vue b/admin-ui/src/components/Breadcrumb/index.vue new file mode 100644 index 0000000..489cba1 --- /dev/null +++ b/admin-ui/src/components/Breadcrumb/index.vue @@ -0,0 +1,66 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/Crontab/day.vue b/admin-ui/src/components/Crontab/day.vue new file mode 100644 index 0000000..25c4f79 --- /dev/null +++ b/admin-ui/src/components/Crontab/day.vue @@ -0,0 +1,174 @@ + + + + \ No newline at end of file diff --git a/admin-ui/src/components/Crontab/hour.vue b/admin-ui/src/components/Crontab/hour.vue new file mode 100644 index 0000000..9f052ad --- /dev/null +++ b/admin-ui/src/components/Crontab/hour.vue @@ -0,0 +1,127 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/Crontab/index.vue b/admin-ui/src/components/Crontab/index.vue new file mode 100644 index 0000000..910c9b3 --- /dev/null +++ b/admin-ui/src/components/Crontab/index.vue @@ -0,0 +1,310 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/Crontab/min.vue b/admin-ui/src/components/Crontab/min.vue new file mode 100644 index 0000000..5d80cd2 --- /dev/null +++ b/admin-ui/src/components/Crontab/min.vue @@ -0,0 +1,126 @@ + + + + \ No newline at end of file diff --git a/admin-ui/src/components/Crontab/month.vue b/admin-ui/src/components/Crontab/month.vue new file mode 100644 index 0000000..657d3f2 --- /dev/null +++ b/admin-ui/src/components/Crontab/month.vue @@ -0,0 +1,141 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/Crontab/result.vue b/admin-ui/src/components/Crontab/result.vue new file mode 100644 index 0000000..5a812ee --- /dev/null +++ b/admin-ui/src/components/Crontab/result.vue @@ -0,0 +1,540 @@ + + + \ No newline at end of file diff --git a/admin-ui/src/components/Crontab/second.vue b/admin-ui/src/components/Crontab/second.vue new file mode 100644 index 0000000..a7e5798 --- /dev/null +++ b/admin-ui/src/components/Crontab/second.vue @@ -0,0 +1,128 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/Crontab/week.vue b/admin-ui/src/components/Crontab/week.vue new file mode 100644 index 0000000..105a3be --- /dev/null +++ b/admin-ui/src/components/Crontab/week.vue @@ -0,0 +1,197 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/Crontab/year.vue b/admin-ui/src/components/Crontab/year.vue new file mode 100644 index 0000000..b26bdae --- /dev/null +++ b/admin-ui/src/components/Crontab/year.vue @@ -0,0 +1,149 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/DictTag/index.vue b/admin-ui/src/components/DictTag/index.vue new file mode 100644 index 0000000..7d0888c --- /dev/null +++ b/admin-ui/src/components/DictTag/index.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/admin-ui/src/components/Editor/index.vue b/admin-ui/src/components/Editor/index.vue new file mode 100644 index 0000000..0a696f2 --- /dev/null +++ b/admin-ui/src/components/Editor/index.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/admin-ui/src/components/FileUpload/index.vue b/admin-ui/src/components/FileUpload/index.vue new file mode 100644 index 0000000..2af9672 --- /dev/null +++ b/admin-ui/src/components/FileUpload/index.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/admin-ui/src/components/Hamburger/index.vue b/admin-ui/src/components/Hamburger/index.vue new file mode 100644 index 0000000..18c201e --- /dev/null +++ b/admin-ui/src/components/Hamburger/index.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/admin-ui/src/components/HeaderSearch/index.vue b/admin-ui/src/components/HeaderSearch/index.vue new file mode 100644 index 0000000..6ef6d2c --- /dev/null +++ b/admin-ui/src/components/HeaderSearch/index.vue @@ -0,0 +1,187 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/IconSelect/index.vue b/admin-ui/src/components/IconSelect/index.vue new file mode 100644 index 0000000..517a4af --- /dev/null +++ b/admin-ui/src/components/IconSelect/index.vue @@ -0,0 +1,111 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/IconSelect/requireIcons.js b/admin-ui/src/components/IconSelect/requireIcons.js new file mode 100644 index 0000000..ac22fd7 --- /dev/null +++ b/admin-ui/src/components/IconSelect/requireIcons.js @@ -0,0 +1,8 @@ +let icons = [] +const modules = import.meta.glob('./../../assets/icons/svg/*.svg'); +for (const path in modules) { + const p = path.split('assets/icons/svg/')[1].split('.svg')[0]; + icons.push(p); +} + +export default icons \ No newline at end of file diff --git a/admin-ui/src/components/ImagePreview/index.vue b/admin-ui/src/components/ImagePreview/index.vue new file mode 100644 index 0000000..7e3d2b6 --- /dev/null +++ b/admin-ui/src/components/ImagePreview/index.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/admin-ui/src/components/ImageUpload/index.vue b/admin-ui/src/components/ImageUpload/index.vue new file mode 100644 index 0000000..55dafb8 --- /dev/null +++ b/admin-ui/src/components/ImageUpload/index.vue @@ -0,0 +1,213 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/Pagination/index.vue b/admin-ui/src/components/Pagination/index.vue new file mode 100644 index 0000000..38de953 --- /dev/null +++ b/admin-ui/src/components/Pagination/index.vue @@ -0,0 +1,105 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/ParentView/index.vue b/admin-ui/src/components/ParentView/index.vue new file mode 100644 index 0000000..7bf6148 --- /dev/null +++ b/admin-ui/src/components/ParentView/index.vue @@ -0,0 +1,3 @@ + diff --git a/admin-ui/src/components/RightToolbar/index.vue b/admin-ui/src/components/RightToolbar/index.vue new file mode 100644 index 0000000..5a53dd4 --- /dev/null +++ b/admin-ui/src/components/RightToolbar/index.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/admin-ui/src/components/SvgIcon/index.vue b/admin-ui/src/components/SvgIcon/index.vue new file mode 100644 index 0000000..8c101f6 --- /dev/null +++ b/admin-ui/src/components/SvgIcon/index.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/admin-ui/src/components/SvgIcon/svgicon.js b/admin-ui/src/components/SvgIcon/svgicon.js new file mode 100644 index 0000000..4431719 --- /dev/null +++ b/admin-ui/src/components/SvgIcon/svgicon.js @@ -0,0 +1,10 @@ +import * as components from '@element-plus/icons-vue' + +export default { + install: (app) => { + for (const key in components) { + const componentConfig = components[key]; + app.component(componentConfig.name, componentConfig); + } + }, +}; diff --git a/admin-ui/src/components/TopNav/index.vue b/admin-ui/src/components/TopNav/index.vue new file mode 100644 index 0000000..52b40ea --- /dev/null +++ b/admin-ui/src/components/TopNav/index.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/admin-ui/src/components/TreeSelect/index.vue b/admin-ui/src/components/TreeSelect/index.vue new file mode 100644 index 0000000..4ff0e76 --- /dev/null +++ b/admin-ui/src/components/TreeSelect/index.vue @@ -0,0 +1,156 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/components/iFrame/index.vue b/admin-ui/src/components/iFrame/index.vue new file mode 100644 index 0000000..091b1a2 --- /dev/null +++ b/admin-ui/src/components/iFrame/index.vue @@ -0,0 +1,31 @@ + + + diff --git a/admin-ui/src/layout/components/Navbar.vue b/admin-ui/src/layout/components/Navbar.vue new file mode 100644 index 0000000..3f11f67 --- /dev/null +++ b/admin-ui/src/layout/components/Navbar.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/admin-ui/src/layout/components/Settings/index.vue b/admin-ui/src/layout/components/Settings/index.vue new file mode 100644 index 0000000..d0a0e88 --- /dev/null +++ b/admin-ui/src/layout/components/Settings/index.vue @@ -0,0 +1,205 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/layout/components/Sidebar/Link.vue b/admin-ui/src/layout/components/Sidebar/Link.vue new file mode 100644 index 0000000..8011431 --- /dev/null +++ b/admin-ui/src/layout/components/Sidebar/Link.vue @@ -0,0 +1,40 @@ + + + diff --git a/admin-ui/src/layout/components/Sidebar/Logo.vue b/admin-ui/src/layout/components/Sidebar/Logo.vue new file mode 100644 index 0000000..67582d0 --- /dev/null +++ b/admin-ui/src/layout/components/Sidebar/Logo.vue @@ -0,0 +1,81 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/layout/components/Sidebar/SidebarItem.vue b/admin-ui/src/layout/components/Sidebar/SidebarItem.vue new file mode 100644 index 0000000..3a85e7e --- /dev/null +++ b/admin-ui/src/layout/components/Sidebar/SidebarItem.vue @@ -0,0 +1,102 @@ + + + diff --git a/admin-ui/src/layout/components/Sidebar/index.vue b/admin-ui/src/layout/components/Sidebar/index.vue new file mode 100644 index 0000000..9b14dfc --- /dev/null +++ b/admin-ui/src/layout/components/Sidebar/index.vue @@ -0,0 +1,54 @@ + + + diff --git a/admin-ui/src/layout/components/TagsView/ScrollPane.vue b/admin-ui/src/layout/components/TagsView/ScrollPane.vue new file mode 100644 index 0000000..516b5d2 --- /dev/null +++ b/admin-ui/src/layout/components/TagsView/ScrollPane.vue @@ -0,0 +1,105 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/layout/components/TagsView/index.vue b/admin-ui/src/layout/components/TagsView/index.vue new file mode 100644 index 0000000..c1b7e3f --- /dev/null +++ b/admin-ui/src/layout/components/TagsView/index.vue @@ -0,0 +1,338 @@ + + + + + + + \ No newline at end of file diff --git a/admin-ui/src/layout/components/index.js b/admin-ui/src/layout/components/index.js new file mode 100644 index 0000000..fd57731 --- /dev/null +++ b/admin-ui/src/layout/components/index.js @@ -0,0 +1,4 @@ +export { default as AppMain } from './AppMain' +export { default as Navbar } from './Navbar' +export { default as Settings } from './Settings' +export { default as TagsView } from './TagsView/index.vue' diff --git a/admin-ui/src/layout/index.vue b/admin-ui/src/layout/index.vue new file mode 100644 index 0000000..3ddb165 --- /dev/null +++ b/admin-ui/src/layout/index.vue @@ -0,0 +1,111 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/main.js b/admin-ui/src/main.js new file mode 100644 index 0000000..04b6801 --- /dev/null +++ b/admin-ui/src/main.js @@ -0,0 +1,84 @@ +import { createApp } from 'vue' + +import Cookies from 'js-cookie' + +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import locale from 'element-plus/es/locale/lang/zh-cn' + +import '@/assets/styles/index.scss' // global css + +import App from './App' +import store from './store' +import router from './router' +import directive from './directive' // directive + +// 注册指令 +import plugins from './plugins' // plugins +import { download } from '@/utils/request' + +// svg图标 +import 'virtual:svg-icons-register' +import SvgIcon from '@/components/SvgIcon' +import elementIcons from '@/components/SvgIcon/svgicon' + +import './permission' // permission control + +import { useDict } from '@/utils/dict' +import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi' + +// 分页组件 +import Pagination from '@/components/Pagination' +// 自定义表格工具组件 +import RightToolbar from '@/components/RightToolbar' +// 富文本组件 +import Editor from "@/components/Editor" +// 文件上传组件 +import FileUpload from "@/components/FileUpload" +// 图片上传组件 +import ImageUpload from "@/components/ImageUpload" +// 图片预览组件 +import ImagePreview from "@/components/ImagePreview" +// 自定义树选择组件 +import TreeSelect from '@/components/TreeSelect' +// 字典标签组件 +import DictTag from '@/components/DictTag' + +const app = createApp(App) + +// 全局方法挂载 +app.config.globalProperties.useDict = useDict +app.config.globalProperties.download = download +app.config.globalProperties.parseTime = parseTime +app.config.globalProperties.resetForm = resetForm +app.config.globalProperties.handleTree = handleTree +app.config.globalProperties.addDateRange = addDateRange +app.config.globalProperties.selectDictLabel = selectDictLabel +app.config.globalProperties.selectDictLabels = selectDictLabels + +// 全局组件挂载 +app.component('DictTag', DictTag) +app.component('Pagination', Pagination) +app.component('TreeSelect', TreeSelect) +app.component('FileUpload', FileUpload) +app.component('ImageUpload', ImageUpload) +app.component('ImagePreview', ImagePreview) +app.component('RightToolbar', RightToolbar) +app.component('Editor', Editor) + +app.use(router) +app.use(store) +app.use(plugins) +app.use(elementIcons) +app.component('svg-icon', SvgIcon) + +directive(app) + +// 使用element-plus 并且设置全局的大小 +app.use(ElementPlus, { + locale: locale, + // 支持 large、default、small + size: Cookies.get('size') || 'default' +}) + +app.mount('#app') diff --git a/admin-ui/src/permission.js b/admin-ui/src/permission.js new file mode 100644 index 0000000..e7390f1 --- /dev/null +++ b/admin-ui/src/permission.js @@ -0,0 +1,79 @@ +import router from "./router"; +import { ElMessage } from "element-plus"; +import NProgress from "nprogress"; +import "nprogress/nprogress.css"; +import { getToken } from "@/utils/auth"; +import { isHttp } from "@/utils/validate"; +import { isRelogin } from "@/utils/request"; +import useUserStore from "@/store/modules/user"; +import useSettingsStore from "@/store/modules/settings"; +import usePermissionStore from "@/store/modules/permission"; + +NProgress.configure({ showSpinner: false }); + +const whiteList = ["/login", "/register"]; + +router.beforeEach((to, from, next) => { + NProgress.start(); + if (getToken()) { + to.meta.title && useSettingsStore().setTitle(to.meta.title); + /* has token*/ + if (to.path === "/login") { + next({ path: "/" }); + NProgress.done(); + } else if (whiteList.indexOf(to.path) !== -1) { + next(); + } else { + if (useUserStore().roles.length === 0) { + isRelogin.show = true; + // 判断当前用户是否已拉取完user_info信息 + useUserStore() + .getInfo() + .then(() => { + isRelogin.show = false; + usePermissionStore() + .generateRoutes() + .then((accessRoutes) => { + // 根据roles权限生成可访问的路由表 + accessRoutes.forEach((route) => { + if (!isHttp(route.path)) { + router.addRoute(route); // 动态添加可访问路由表 + } + }); + + if (to.meta.noCache) { + // 如果目标路由不需要缓存,强制重新加载 + const { path, query } = to; + window.location.href = `/${path}?${query}`; + } else { + next({ ...to, replace: true }); // hack方法 确保addRoutes已完成 + } + }); + }) + .catch((err) => { + useUserStore() + .logOut() + .then(() => { + ElMessage.error(err); + next({ path: "/" }); + }); + }); + } else { + next(); + } + } + } else { + // 没有token + if (whiteList.indexOf(to.path) !== -1) { + // 在免登录白名单,直接进入 + next(); + } else { + next(`/login?redirect=${to.fullPath}`); // 否则全部重定向到登录页 + NProgress.done(); + } + } +}); + +router.afterEach(() => { + NProgress.done(); +}); diff --git a/admin-ui/src/plugins/auth.js b/admin-ui/src/plugins/auth.js new file mode 100644 index 0000000..5e8c28d --- /dev/null +++ b/admin-ui/src/plugins/auth.js @@ -0,0 +1,60 @@ +import useUserStore from '@/store/modules/user' + +function authPermission(permission) { + const all_permission = "*:*:*"; + const permissions = useUserStore().permissions + if (permission && permission.length > 0) { + return permissions.some(v => { + return all_permission === v || v === permission + }) + } else { + return false + } +} + +function authRole(role) { + const super_admin = "admin"; + const roles = useUserStore().roles + if (role && role.length > 0) { + return roles.some(v => { + return super_admin === v || v === role + }) + } else { + return false + } +} + +export default { + // 验证用户是否具备某权限 + hasPermi(permission) { + return authPermission(permission); + }, + // 验证用户是否含有指定权限,只需包含其中一个 + hasPermiOr(permissions) { + return permissions.some(item => { + return authPermission(item) + }) + }, + // 验证用户是否含有指定权限,必须全部拥有 + hasPermiAnd(permissions) { + return permissions.every(item => { + return authPermission(item) + }) + }, + // 验证用户是否具备某角色 + hasRole(role) { + return authRole(role); + }, + // 验证用户是否含有指定角色,只需包含其中一个 + hasRoleOr(roles) { + return roles.some(item => { + return authRole(item) + }) + }, + // 验证用户是否含有指定角色,必须全部拥有 + hasRoleAnd(roles) { + return roles.every(item => { + return authRole(item) + }) + } +} diff --git a/admin-ui/src/plugins/cache.js b/admin-ui/src/plugins/cache.js new file mode 100644 index 0000000..6b5c00b --- /dev/null +++ b/admin-ui/src/plugins/cache.js @@ -0,0 +1,77 @@ +const sessionCache = { + set (key, value) { + if (!sessionStorage) { + return + } + if (key != null && value != null) { + sessionStorage.setItem(key, value) + } + }, + get (key) { + if (!sessionStorage) { + return null + } + if (key == null) { + return null + } + return sessionStorage.getItem(key) + }, + setJSON (key, jsonValue) { + if (jsonValue != null) { + this.set(key, JSON.stringify(jsonValue)) + } + }, + getJSON (key) { + const value = this.get(key) + if (value != null) { + return JSON.parse(value) + } + }, + remove (key) { + sessionStorage.removeItem(key); + } +} +const localCache = { + set (key, value) { + if (!localStorage) { + return + } + if (key != null && value != null) { + localStorage.setItem(key, value) + } + }, + get (key) { + if (!localStorage) { + return null + } + if (key == null) { + return null + } + return localStorage.getItem(key) + }, + setJSON (key, jsonValue) { + if (jsonValue != null) { + this.set(key, JSON.stringify(jsonValue)) + } + }, + getJSON (key) { + const value = this.get(key) + if (value != null) { + return JSON.parse(value) + } + }, + remove (key) { + localStorage.removeItem(key); + } +} + +export default { + /** + * 会话级缓存 + */ + session: sessionCache, + /** + * 本地缓存 + */ + local: localCache +} diff --git a/admin-ui/src/plugins/download.js b/admin-ui/src/plugins/download.js new file mode 100644 index 0000000..1a89efd --- /dev/null +++ b/admin-ui/src/plugins/download.js @@ -0,0 +1,79 @@ +import axios from 'axios' +import { ElLoading, ElMessage } from 'element-plus' +import { saveAs } from 'file-saver' +import { getToken } from '@/utils/auth' +import errorCode from '@/utils/errorCode' +import { blobValidate } from '@/utils/ruoyi' + +const baseURL = import.meta.env.VITE_APP_BASE_API +let downloadLoadingInstance; + +export default { + name(name, isDelete = true) { + var url = baseURL + "/common/download?fileName=" + encodeURIComponent(name) + "&delete=" + isDelete + axios({ + method: 'get', + url: url, + responseType: 'blob', + headers: { 'Authorization': 'Bearer ' + getToken() } + }).then((res) => { + const isBlob = blobValidate(res.data); + if (isBlob) { + const blob = new Blob([res.data]) + this.saveAs(blob, decodeURIComponent(res.headers['download-filename'])) + } else { + this.printErrMsg(res.data); + } + }) + }, + resource(resource) { + var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource); + axios({ + method: 'get', + url: url, + responseType: 'blob', + headers: { 'Authorization': 'Bearer ' + getToken() } + }).then((res) => { + const isBlob = blobValidate(res.data); + if (isBlob) { + const blob = new Blob([res.data]) + this.saveAs(blob, decodeURIComponent(res.headers['download-filename'])) + } else { + this.printErrMsg(res.data); + } + }) + }, + zip(url, name) { + var url = baseURL + url + downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", }) + axios({ + method: 'get', + url: url, + responseType: 'blob', + headers: { 'Authorization': 'Bearer ' + getToken() } + }).then((res) => { + const isBlob = blobValidate(res.data); + if (isBlob) { + const blob = new Blob([res.data], { type: 'application/zip' }) + this.saveAs(blob, name) + } else { + this.printErrMsg(res.data); + } + downloadLoadingInstance.close(); + }).catch((r) => { + console.error(r) + ElMessage.error('下载文件出现错误,请联系管理员!') + downloadLoadingInstance.close(); + }) + }, + saveAs(text, name, opts) { + saveAs(text, name, opts); + }, + async printErrMsg(data) { + const resText = await data.text(); + const rspObj = JSON.parse(resText); + const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] + ElMessage.error(errMsg); + } +} + diff --git a/admin-ui/src/plugins/index.js b/admin-ui/src/plugins/index.js new file mode 100644 index 0000000..47d1b41 --- /dev/null +++ b/admin-ui/src/plugins/index.js @@ -0,0 +1,18 @@ +import tab from './tab' +import auth from './auth' +import cache from './cache' +import modal from './modal' +import download from './download' + +export default function installPlugins(app){ + // 页签操作 + app.config.globalProperties.$tab = tab + // 认证对象 + app.config.globalProperties.$auth = auth + // 缓存对象 + app.config.globalProperties.$cache = cache + // 模态框对象 + app.config.globalProperties.$modal = modal + // 下载文件 + app.config.globalProperties.$download = download +} diff --git a/admin-ui/src/plugins/modal.js b/admin-ui/src/plugins/modal.js new file mode 100644 index 0000000..b59e14d --- /dev/null +++ b/admin-ui/src/plugins/modal.js @@ -0,0 +1,82 @@ +import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus' + +let loadingInstance; + +export default { + // 消息提示 + msg(content) { + ElMessage.info(content) + }, + // 错误消息 + msgError(content) { + ElMessage.error(content) + }, + // 成功消息 + msgSuccess(content) { + ElMessage.success(content) + }, + // 警告消息 + msgWarning(content) { + ElMessage.warning(content) + }, + // 弹出提示 + alert(content) { + ElMessageBox.alert(content, "系统提示") + }, + // 错误提示 + alertError(content) { + ElMessageBox.alert(content, "系统提示", { type: 'error' }) + }, + // 成功提示 + alertSuccess(content) { + ElMessageBox.alert(content, "系统提示", { type: 'success' }) + }, + // 警告提示 + alertWarning(content) { + ElMessageBox.alert(content, "系统提示", { type: 'warning' }) + }, + // 通知提示 + notify(content) { + ElNotification.info(content) + }, + // 错误通知 + notifyError(content) { + ElNotification.error(content); + }, + // 成功通知 + notifySuccess(content) { + ElNotification.success(content) + }, + // 警告通知 + notifyWarning(content) { + ElNotification.warning(content) + }, + // 确认窗体 + confirm(content) { + return ElMessageBox.confirm(content, "系统提示", { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: "warning", + }) + }, + // 提交内容 + prompt(content) { + return ElMessageBox.prompt(content, "系统提示", { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: "warning", + }) + }, + // 打开遮罩层 + loading(content) { + loadingInstance = ElLoading.service({ + lock: true, + text: content, + background: "rgba(0, 0, 0, 0.7)", + }) + }, + // 关闭遮罩层 + closeLoading() { + loadingInstance.close(); + } +} diff --git a/admin-ui/src/plugins/tab.js b/admin-ui/src/plugins/tab.js new file mode 100644 index 0000000..7b51cf5 --- /dev/null +++ b/admin-ui/src/plugins/tab.js @@ -0,0 +1,69 @@ +import useTagsViewStore from '@/store/modules/tagsView' +import router from '@/router' + +export default { + // 刷新当前tab页签 + refreshPage(obj) { + const { path, query, matched } = router.currentRoute.value; + if (obj === undefined) { + matched.forEach((m) => { + if (m.components && m.components.default && m.components.default.name) { + if (!['Layout', 'ParentView'].includes(m.components.default.name)) { + obj = { name: m.components.default.name, path: path, query: query }; + } + } + }); + } + return useTagsViewStore().delCachedView(obj).then(() => { + const { path, query } = obj + router.replace({ + path: '/redirect' + path, + query: query + }) + }) + }, + // 关闭当前tab页签,打开新页签 + closeOpenPage(obj) { + useTagsViewStore().delView(router.currentRoute.value); + if (obj !== undefined) { + return router.push(obj); + } + }, + // 关闭指定tab页签 + closePage(obj) { + if (obj === undefined) { + return useTagsViewStore().delView(router.currentRoute.value).then(({ visitedViews }) => { + const latestView = visitedViews.slice(-1)[0] + if (latestView) { + return router.push(latestView.fullPath) + } + return router.push('/'); + }); + } + return useTagsViewStore().delView(obj); + }, + // 关闭所有tab页签 + closeAllPage() { + return useTagsViewStore().delAllViews(); + }, + // 关闭左侧tab页签 + closeLeftPage(obj) { + return useTagsViewStore().delLeftTags(obj || router.currentRoute.value); + }, + // 关闭右侧tab页签 + closeRightPage(obj) { + return useTagsViewStore().delRightTags(obj || router.currentRoute.value); + }, + // 关闭其他tab页签 + closeOtherPage(obj) { + return useTagsViewStore().delOthersViews(obj || router.currentRoute.value); + }, + // 打开tab页签 + openPage(url) { + return router.push(url); + }, + // 修改tab页签 + updatePage(obj) { + return useTagsViewStore().updateVisitedView(obj); + } +} diff --git a/admin-ui/src/router/index.js b/admin-ui/src/router/index.js new file mode 100644 index 0000000..fda14cd --- /dev/null +++ b/admin-ui/src/router/index.js @@ -0,0 +1,134 @@ +import { createWebHashHistory, createRouter } from 'vue-router' +/* Layout */ +import Layout from '@/layout' + +/** + * Note: 路由配置项 + * + * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1 + * alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + * // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面 + * // 若你想不管路由下面的 children 声明的个数都显示你的根路由 + * // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由 + * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + * name:'router-name' // 设定路由的名字,一定要填写不然使用时会出现各种问题 + * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数 + * roles: ['admin', 'common'] // 访问路由的角色权限 + * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限 + * meta : { + noCache: true // 如果设置为true,则不会被 缓存(默认 false) + title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字 + icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg + breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示 + activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。 + } + */ + +// 公共路由 +export const constantRoutes = [ + { + path: '/redirect', + component: Layout, + hidden: true, + children: [ + { + path: '/redirect/:path(.*)', + component: () => import('@/views/redirect/index.vue') + } + ] + }, + { + path: '/login', + component: () => import('@/views/login'), + hidden: true + }, + { + path: '/register', + component: () => import('@/views/register'), + hidden: true + }, + { + path: "/:pathMatch(.*)*", + component: () => import('@/views/error/404'), + hidden: true + }, + { + path: '/401', + component: () => import('@/views/error/401'), + hidden: true + }, + { + path: '', + component: Layout, + redirect: '/index', + children: [ + { + path: '/index', + component: () => import('@/views/index'), + name: 'Index', + meta: { title: '首页', icon: 'dashboard', affix: true } + } + ] + }, + { + path: '/user', + component: Layout, + hidden: true, + redirect: 'noredirect', + children: [ + { + path: 'profile', + component: () => import('@/views/system/user/profile/index'), + name: 'Profile', + meta: { title: '个人中心', icon: 'user' } + } + ] + } +] + +// 动态路由,基于用户权限动态去加载 +export const dynamicRoutes = [ + { + path: '/system/user-auth', + component: Layout, + hidden: true, + permissions: ['system:user:edit'], + children: [ + { + path: 'role/:userId(\\d+)', + component: () => import('@/views/system/user/authRole'), + name: 'AuthRole', + meta: { title: '分配角色', activeMenu: '/system/user' } + } + ] + }, + { + path: '/system/role-auth', + component: Layout, + hidden: true, + permissions: ['system:role:edit'], + children: [ + { + path: 'user/:roleId(\\d+)', + component: () => import('@/views/system/role/authUser'), + name: 'AuthUser', + meta: { title: '分配用户', activeMenu: '/system/role' } + } + ] + }, + +] + +const router = createRouter({ + history: createWebHashHistory(), + routes: constantRoutes, + scrollBehavior(to, from, savedPosition) { + if (savedPosition) { + return savedPosition + } else { + return { top: 0 } + } + }, +}); + +export default router; diff --git a/admin-ui/src/settings.js b/admin-ui/src/settings.js new file mode 100644 index 0000000..08a0108 --- /dev/null +++ b/admin-ui/src/settings.js @@ -0,0 +1,47 @@ +export default { + /** + * 网页标题 + */ + title: import.meta.env.VITE_APP_TITLE, + /** + * 侧边栏主题 深色主题theme-dark,浅色主题theme-light + */ + sideTheme: 'theme-dark', + /** + * 是否系统布局配置 + */ + showSettings: true, + + /** + * 是否显示顶部导航 + */ + topNav: false, + + /** + * 是否显示 tagsView + */ + tagsView: true, + + /** + * 是否固定头部 + */ + fixedHeader: false, + + /** + * 是否显示logo + */ + sidebarLogo: true, + + /** + * 是否显示动态标题 + */ + dynamicTitle: false, + + /** + * @type {string | array} 'production' | ['production', 'development'] + * @description Need show err logs component. + * The default is only used in the production env + * If you want to also use it in dev, you can pass ['production', 'development'] + */ + errorLog: 'production' +} diff --git a/admin-ui/src/store/index.js b/admin-ui/src/store/index.js new file mode 100644 index 0000000..f10f389 --- /dev/null +++ b/admin-ui/src/store/index.js @@ -0,0 +1,3 @@ +const store = createPinia() + +export default store \ No newline at end of file diff --git a/admin-ui/src/store/modules/app.js b/admin-ui/src/store/modules/app.js new file mode 100644 index 0000000..0b57159 --- /dev/null +++ b/admin-ui/src/store/modules/app.js @@ -0,0 +1,46 @@ +import Cookies from 'js-cookie' + +const useAppStore = defineStore( + 'app', + { + state: () => ({ + sidebar: { + opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, + withoutAnimation: false, + hide: false + }, + device: 'desktop', + size: Cookies.get('size') || 'default' + }), + actions: { + toggleSideBar(withoutAnimation) { + if (this.sidebar.hide) { + return false; + } + this.sidebar.opened = !this.sidebar.opened + this.sidebar.withoutAnimation = withoutAnimation + if (this.sidebar.opened) { + Cookies.set('sidebarStatus', 1) + } else { + Cookies.set('sidebarStatus', 0) + } + }, + closeSideBar({ withoutAnimation }) { + Cookies.set('sidebarStatus', 0) + this.sidebar.opened = false + this.sidebar.withoutAnimation = withoutAnimation + }, + toggleDevice(device) { + this.device = device + }, + setSize(size) { + this.size = size; + Cookies.set('size', size) + }, + toggleSideBarHide(status) { + this.sidebar.hide = status + } + } + }) + +export default useAppStore diff --git a/admin-ui/src/store/modules/dict.js b/admin-ui/src/store/modules/dict.js new file mode 100644 index 0000000..27fc308 --- /dev/null +++ b/admin-ui/src/store/modules/dict.js @@ -0,0 +1,57 @@ +const useDictStore = defineStore( + 'dict', + { + state: () => ({ + dict: new Array() + }), + actions: { + // 获取字典 + getDict(_key) { + if (_key == null && _key == "") { + return null; + } + try { + for (let i = 0; i < this.dict.length; i++) { + if (this.dict[i].key == _key) { + return this.dict[i].value; + } + } + } catch (e) { + return null; + } + }, + // 设置字典 + setDict(_key, value) { + if (_key !== null && _key !== "") { + this.dict.push({ + key: _key, + value: value + }); + } + }, + // 删除字典 + removeDict(_key) { + var bln = false; + try { + for (let i = 0; i < this.dict.length; i++) { + if (this.dict[i].key == _key) { + this.dict.splice(i, 1); + return true; + } + } + } catch (e) { + bln = false; + } + return bln; + }, + // 清空字典 + cleanDict() { + this.dict = new Array(); + }, + // 初始字典 + initDict() { + } + } + }) + +export default useDictStore diff --git a/admin-ui/src/store/modules/permission.js b/admin-ui/src/store/modules/permission.js new file mode 100644 index 0000000..958fe63 --- /dev/null +++ b/admin-ui/src/store/modules/permission.js @@ -0,0 +1,142 @@ +import auth from '@/plugins/auth' +import router, { constantRoutes, dynamicRoutes } from '@/router' +import { getRouters } from '@/api/menu' +import Layout from '@/layout/index' +import ParentView from '@/components/ParentView' +import InnerLink from '@/layout/components/InnerLink' + +// 匹配views里面所有的.vue文件 +const modules = import.meta.glob('./../../views/**/*.vue') + +const usePermissionStore = defineStore( + 'permission', + { + state: () => ({ + routes: [], + addRoutes: [], + defaultRoutes: [], + topbarRouters: [], + sidebarRouters: [] + }), + actions: { + setRoutes(routes) { + this.addRoutes = routes + this.routes = constantRoutes.concat(routes) + }, + setDefaultRoutes(routes) { + this.defaultRoutes = constantRoutes.concat(routes) + }, + setTopbarRoutes(routes) { + this.topbarRouters = routes + }, + setSidebarRouters(routes) { + this.sidebarRouters = routes + }, + generateRoutes(roles) { + return new Promise(resolve => { + // 向后端请求路由数据 + getRouters().then(res => { + const sdata = JSON.parse(JSON.stringify(res.data)) + const rdata = JSON.parse(JSON.stringify(res.data)) + const defaultData = JSON.parse(JSON.stringify(res.data)) + const sidebarRoutes = filterAsyncRouter(sdata) + const rewriteRoutes = filterAsyncRouter(rdata, false, true) + const defaultRoutes = filterAsyncRouter(defaultData) + const asyncRoutes = filterDynamicRoutes(dynamicRoutes) + asyncRoutes.forEach(route => { router.addRoute(route) }) + this.setRoutes(rewriteRoutes) + this.setSidebarRouters(constantRoutes.concat(sidebarRoutes)) + this.setDefaultRoutes(sidebarRoutes) + this.setTopbarRoutes(defaultRoutes) + resolve(rewriteRoutes) + }) + }) + } + } + }) + +// 遍历后台传来的路由字符串,转换为组件对象 +function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) { + return asyncRouterMap.filter(route => { + if (type && route.children) { + route.children = filterChildren(route.children) + } + if (route.component) { + // Layout ParentView 组件特殊处理 + if (route.component === 'Layout') { + route.component = Layout + } else if (route.component === 'ParentView') { + route.component = ParentView + } else if (route.component === 'InnerLink') { + route.component = InnerLink + } else { + route.component = loadView(route.component) + } + } + if (route.children != null && route.children && route.children.length) { + route.children = filterAsyncRouter(route.children, route, type) + } else { + delete route['children'] + delete route['redirect'] + } + return true + }) +} + +function filterChildren(childrenMap, lastRouter = false) { + var children = [] + childrenMap.forEach((el, index) => { + if (el.children && el.children.length) { + if (el.component === 'ParentView' && !lastRouter) { + el.children.forEach(c => { + c.path = el.path + '/' + c.path + if (c.children && c.children.length) { + children = children.concat(filterChildren(c.children, c)) + return + } + children.push(c) + }) + return + } + } + if (lastRouter) { + el.path = lastRouter.path + '/' + el.path + if (el.children && el.children.length) { + children = children.concat(filterChildren(el.children, el)) + return + } + } + children = children.concat(el) + }) + return children +} + +// 动态路由遍历,验证是否具备权限 +export function filterDynamicRoutes(routes) { + const res = [] + routes.forEach(route => { + if (route.permissions) { + if (auth.hasPermiOr(route.permissions)) { + res.push(route) + } + } else if (route.roles) { + if (auth.hasRoleOr(route.roles)) { + res.push(route) + } + } + }) + return res +} + +export const loadView = (view) => { + let res; + for (const path in modules) { + const dir = path.split('views/')[1].split('.vue')[0]; + if (dir === view) { + res = () => modules[path](); + } + } + return res; +} + +export default usePermissionStore diff --git a/admin-ui/src/store/modules/settings.js b/admin-ui/src/store/modules/settings.js new file mode 100644 index 0000000..22b7336 --- /dev/null +++ b/admin-ui/src/store/modules/settings.js @@ -0,0 +1,38 @@ +import defaultSettings from '@/settings' +import { useDynamicTitle } from '@/utils/dynamicTitle' + +const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle } = defaultSettings + +const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || '' + +const useSettingsStore = defineStore( + 'settings', + { + state: () => ({ + title: '', + theme: storageSetting.theme || '#409EFF', + sideTheme: storageSetting.sideTheme || sideTheme, + showSettings: showSettings, + topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav, + tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView, + fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader, + sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo, + dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle + }), + actions: { + // 修改布局设置 + changeSetting(data) { + const { key, value } = data + if (this.hasOwnProperty(key)) { + this[key] = value + } + }, + // 设置网页标题 + setTitle(title) { + this.title = title + useDynamicTitle(); + } + } + }) + +export default useSettingsStore diff --git a/admin-ui/src/store/modules/tagsView.js b/admin-ui/src/store/modules/tagsView.js new file mode 100644 index 0000000..9d07f33 --- /dev/null +++ b/admin-ui/src/store/modules/tagsView.js @@ -0,0 +1,182 @@ +const useTagsViewStore = defineStore( + 'tags-view', + { + state: () => ({ + visitedViews: [], + cachedViews: [], + iframeViews: [] + }), + actions: { + addView(view) { + this.addVisitedView(view) + this.addCachedView(view) + }, + addIframeView(view) { + if (this.iframeViews.some(v => v.path === view.path)) return + this.iframeViews.push( + Object.assign({}, view, { + title: view.meta.title || 'no-name' + }) + ) + }, + addVisitedView(view) { + if (this.visitedViews.some(v => v.path === view.path)) return + this.visitedViews.push( + Object.assign({}, view, { + title: view.meta.title || 'no-name' + }) + ) + }, + addCachedView(view) { + if (this.cachedViews.includes(view.name)) return + if (!view.meta.noCache) { + this.cachedViews.push(view.name) + } + }, + delView(view) { + return new Promise(resolve => { + this.delVisitedView(view) + this.delCachedView(view) + resolve({ + visitedViews: [...this.visitedViews], + cachedViews: [...this.cachedViews] + }) + }) + }, + delVisitedView(view) { + return new Promise(resolve => { + for (const [i, v] of this.visitedViews.entries()) { + if (v.path === view.path) { + this.visitedViews.splice(i, 1) + break + } + } + this.iframeViews = this.iframeViews.filter(item => item.path !== view.path) + resolve([...this.visitedViews]) + }) + }, + delIframeView(view) { + return new Promise(resolve => { + this.iframeViews = this.iframeViews.filter(item => item.path !== view.path) + resolve([...this.iframeViews]) + }) + }, + delCachedView(view) { + return new Promise(resolve => { + const index = this.cachedViews.indexOf(view.name) + index > -1 && this.cachedViews.splice(index, 1) + resolve([...this.cachedViews]) + }) + }, + delOthersViews(view) { + return new Promise(resolve => { + this.delOthersVisitedViews(view) + this.delOthersCachedViews(view) + resolve({ + visitedViews: [...this.visitedViews], + cachedViews: [...this.cachedViews] + }) + }) + }, + delOthersVisitedViews(view) { + return new Promise(resolve => { + this.visitedViews = this.visitedViews.filter(v => { + return v.meta.affix || v.path === view.path + }) + this.iframeViews = this.iframeViews.filter(item => item.path === view.path) + resolve([...this.visitedViews]) + }) + }, + delOthersCachedViews(view) { + return new Promise(resolve => { + const index = this.cachedViews.indexOf(view.name) + if (index > -1) { + this.cachedViews = this.cachedViews.slice(index, index + 1) + } else { + this.cachedViews = [] + } + resolve([...this.cachedViews]) + }) + }, + delAllViews(view) { + return new Promise(resolve => { + this.delAllVisitedViews(view) + this.delAllCachedViews(view) + resolve({ + visitedViews: [...this.visitedViews], + cachedViews: [...this.cachedViews] + }) + }) + }, + delAllVisitedViews(view) { + return new Promise(resolve => { + const affixTags = this.visitedViews.filter(tag => tag.meta.affix) + this.visitedViews = affixTags + this.iframeViews = [] + resolve([...this.visitedViews]) + }) + }, + delAllCachedViews(view) { + return new Promise(resolve => { + this.cachedViews = [] + resolve([...this.cachedViews]) + }) + }, + updateVisitedView(view) { + for (let v of this.visitedViews) { + if (v.path === view.path) { + v = Object.assign(v, view) + break + } + } + }, + delRightTags(view) { + return new Promise(resolve => { + const index = this.visitedViews.findIndex(v => v.path === view.path) + if (index === -1) { + return + } + this.visitedViews = this.visitedViews.filter((item, idx) => { + if (idx <= index || (item.meta && item.meta.affix)) { + return true + } + const i = this.cachedViews.indexOf(item.name) + if (i > -1) { + this.cachedViews.splice(i, 1) + } + if(item.meta.link) { + const fi = this.iframeViews.findIndex(v => v.path === item.path) + this.iframeViews.splice(fi, 1) + } + return false + }) + resolve([...this.visitedViews]) + }) + }, + delLeftTags(view) { + return new Promise(resolve => { + const index = this.visitedViews.findIndex(v => v.path === view.path) + if (index === -1) { + return + } + this.visitedViews = this.visitedViews.filter((item, idx) => { + if (idx >= index || (item.meta && item.meta.affix)) { + return true + } + const i = this.cachedViews.indexOf(item.name) + if (i > -1) { + this.cachedViews.splice(i, 1) + } + if(item.meta.link) { + const fi = this.iframeViews.findIndex(v => v.path === item.path) + this.iframeViews.splice(fi, 1) + } + return false + }) + resolve([...this.visitedViews]) + }) + } + } + }) + +export default useTagsViewStore diff --git a/admin-ui/src/store/modules/user.js b/admin-ui/src/store/modules/user.js new file mode 100644 index 0000000..5c26f4e --- /dev/null +++ b/admin-ui/src/store/modules/user.js @@ -0,0 +1,72 @@ +import { login, logout, getInfo } from '@/api/login' +import { getToken, setToken, removeToken } from '@/utils/auth' +import defAva from '@/assets/images/profile.jpg' + +const useUserStore = defineStore( + 'user', + { + state: () => ({ + token: getToken(), + id: '', + name: '', + avatar: '', + roles: [], + permissions: [] + }), + actions: { + // 登录 + login(userInfo) { + const username = userInfo.username.trim() + const password = userInfo.password + const code = userInfo.code + const uuid = userInfo.uuid + return new Promise((resolve, reject) => { + login(username, password, code, uuid).then(res => { + setToken(res.data) + this.token = res.data + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + // 获取用户信息 + getInfo() { + return new Promise((resolve, reject) => { + getInfo().then(res => { + const user = res.data.user + const avatar = (user.avatar == "" || user.avatar == null) ? defAva : import.meta.env.VITE_APP_BASE_API + user.avatar; + + if (res.data.roles && res.data.roles.length > 0) { // 验证返回的roles是否是一个非空数组 + this.roles = res.data.roles + this.permissions = res.data.permissions + } else { + this.roles = ['ROLE_DEFAULT'] + } + this.id = user.userId + this.name = user.userName + this.avatar = avatar + resolve(res) + }).catch(error => { + reject(error) + }) + }) + }, + // 退出系统 + logOut() { + return new Promise((resolve, reject) => { + logout(this.token).then(() => { + this.token = '' + this.roles = [] + this.permissions = [] + removeToken() + resolve() + }).catch(error => { + reject(error) + }) + }) + } + } + }) + +export default useUserStore diff --git a/admin-ui/src/utils/auth.js b/admin-ui/src/utils/auth.js new file mode 100644 index 0000000..08a43d6 --- /dev/null +++ b/admin-ui/src/utils/auth.js @@ -0,0 +1,15 @@ +import Cookies from 'js-cookie' + +const TokenKey = 'Admin-Token' + +export function getToken() { + return Cookies.get(TokenKey) +} + +export function setToken(token) { + return Cookies.set(TokenKey, token) +} + +export function removeToken() { + return Cookies.remove(TokenKey) +} diff --git a/admin-ui/src/utils/dict.js b/admin-ui/src/utils/dict.js new file mode 100644 index 0000000..9648f14 --- /dev/null +++ b/admin-ui/src/utils/dict.js @@ -0,0 +1,24 @@ +import useDictStore from '@/store/modules/dict' +import { getDicts } from '@/api/system/dict/data' + +/** + * 获取字典数据 + */ +export function useDict(...args) { + const res = ref({}); + return (() => { + args.forEach((dictType, index) => { + res.value[dictType] = []; + const dicts = useDictStore().getDict(dictType); + if (dicts) { + res.value[dictType] = dicts; + } else { + getDicts(dictType).then(resp => { + res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass })) + useDictStore().setDict(dictType, res.value[dictType]); + }) + } + }) + return toRefs(res.value); + })() +} \ No newline at end of file diff --git a/admin-ui/src/utils/dynamicTitle.js b/admin-ui/src/utils/dynamicTitle.js new file mode 100644 index 0000000..64404b2 --- /dev/null +++ b/admin-ui/src/utils/dynamicTitle.js @@ -0,0 +1,15 @@ +import store from '@/store' +import defaultSettings from '@/settings' +import useSettingsStore from '@/store/modules/settings' + +/** + * 动态修改标题 + */ +export function useDynamicTitle() { + const settingsStore = useSettingsStore(); + if (settingsStore.dynamicTitle) { + document.title = settingsStore.title + ' - ' + defaultSettings.title; + } else { + document.title = defaultSettings.title; + } +} \ No newline at end of file diff --git a/admin-ui/src/utils/errorCode.js b/admin-ui/src/utils/errorCode.js new file mode 100644 index 0000000..d2111ee --- /dev/null +++ b/admin-ui/src/utils/errorCode.js @@ -0,0 +1,6 @@ +export default { + '401': '认证失败,无法访问系统资源', + '403': '当前操作没有权限', + '404': '访问资源不存在', + 'default': '系统未知错误,请反馈给管理员' +} diff --git a/admin-ui/src/utils/index.js b/admin-ui/src/utils/index.js new file mode 100644 index 0000000..4e65504 --- /dev/null +++ b/admin-ui/src/utils/index.js @@ -0,0 +1,390 @@ +import { parseTime } from './ruoyi' + +/** + * 表格时间格式化 + */ +export function formatDate(cellValue) { + if (cellValue == null || cellValue == "") return ""; + var date = new Date(cellValue) + var year = date.getFullYear() + var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1 + var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() + var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() + var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() + var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() + return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds +} + +/** + * @param {number} time + * @param {string} option + * @returns {string} + */ +export function formatTime(time, option) { + if (('' + time).length === 10) { + time = parseInt(time) * 1000 + } else { + time = +time + } + const d = new Date(time) + const now = Date.now() + + const diff = (now - d) / 1000 + + if (diff < 30) { + return '刚刚' + } else if (diff < 3600) { + // less 1 hour + return Math.ceil(diff / 60) + '分钟前' + } else if (diff < 3600 * 24) { + return Math.ceil(diff / 3600) + '小时前' + } else if (diff < 3600 * 24 * 2) { + return '1天前' + } + if (option) { + return parseTime(time, option) + } else { + return ( + d.getMonth() + + 1 + + '月' + + d.getDate() + + '日' + + d.getHours() + + '时' + + d.getMinutes() + + '分' + ) + } +} + +/** + * @param {string} url + * @returns {Object} + */ +export function getQueryObject(url) { + url = url == null ? window.location.href : url + const search = url.substring(url.lastIndexOf('?') + 1) + const obj = {} + const reg = /([^?&=]+)=([^?&=]*)/g + search.replace(reg, (rs, $1, $2) => { + const name = decodeURIComponent($1) + let val = decodeURIComponent($2) + val = String(val) + obj[name] = val + return rs + }) + return obj +} + +/** + * @param {string} input value + * @returns {number} output value + */ +export function byteLength(str) { + // returns the byte length of an utf8 string + let s = str.length + for (var i = str.length - 1; i >= 0; i--) { + const code = str.charCodeAt(i) + if (code > 0x7f && code <= 0x7ff) s++ + else if (code > 0x7ff && code <= 0xffff) s += 2 + if (code >= 0xDC00 && code <= 0xDFFF) i-- + } + return s +} + +/** + * @param {Array} actual + * @returns {Array} + */ +export function cleanArray(actual) { + const newArray = [] + for (let i = 0; i < actual.length; i++) { + if (actual[i]) { + newArray.push(actual[i]) + } + } + return newArray +} + +/** + * @param {Object} json + * @returns {Array} + */ +export function param(json) { + if (!json) return '' + return cleanArray( + Object.keys(json).map(key => { + if (json[key] === undefined) return '' + return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]) + }) + ).join('&') +} + +/** + * @param {string} url + * @returns {Object} + */ +export function param2Obj(url) { + const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') + if (!search) { + return {} + } + const obj = {} + const searchArr = search.split('&') + searchArr.forEach(v => { + const index = v.indexOf('=') + if (index !== -1) { + const name = v.substring(0, index) + const val = v.substring(index + 1, v.length) + obj[name] = val + } + }) + return obj +} + +/** + * @param {string} val + * @returns {string} + */ +export function html2Text(val) { + const div = document.createElement('div') + div.innerHTML = val + return div.textContent || div.innerText +} + +/** + * Merges two objects, giving the last one precedence + * @param {Object} target + * @param {(Object|Array)} source + * @returns {Object} + */ +export function objectMerge(target, source) { + if (typeof target !== 'object') { + target = {} + } + if (Array.isArray(source)) { + return source.slice() + } + Object.keys(source).forEach(property => { + const sourceProperty = source[property] + if (typeof sourceProperty === 'object') { + target[property] = objectMerge(target[property], sourceProperty) + } else { + target[property] = sourceProperty + } + }) + return target +} + +/** + * @param {HTMLElement} element + * @param {string} className + */ +export function toggleClass(element, className) { + if (!element || !className) { + return + } + let classString = element.className + const nameIndex = classString.indexOf(className) + if (nameIndex === -1) { + classString += '' + className + } else { + classString = + classString.substr(0, nameIndex) + + classString.substr(nameIndex + className.length) + } + element.className = classString +} + +/** + * @param {string} type + * @returns {Date} + */ +export function getTime(type) { + if (type === 'start') { + return new Date().getTime() - 3600 * 1000 * 24 * 90 + } else { + return new Date(new Date().toDateString()) + } +} + +/** + * @param {Function} func + * @param {number} wait + * @param {boolean} immediate + * @return {*} + */ +export function debounce(func, wait, immediate) { + let timeout, args, context, timestamp, result + + const later = function() { + // 据上一次触发时间间隔 + const last = +new Date() - timestamp + + // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait + if (last < wait && last > 0) { + timeout = setTimeout(later, wait - last) + } else { + timeout = null + // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用 + if (!immediate) { + result = func.apply(context, args) + if (!timeout) context = args = null + } + } + } + + return function(...args) { + context = this + timestamp = +new Date() + const callNow = immediate && !timeout + // 如果延时不存在,重新设定延时 + if (!timeout) timeout = setTimeout(later, wait) + if (callNow) { + result = func.apply(context, args) + context = args = null + } + + return result + } +} + +/** + * This is just a simple version of deep copy + * Has a lot of edge cases bug + * If you want to use a perfect deep copy, use lodash's _.cloneDeep + * @param {Object} source + * @returns {Object} + */ +export function deepClone(source) { + if (!source && typeof source !== 'object') { + throw new Error('error arguments', 'deepClone') + } + const targetObj = source.constructor === Array ? [] : {} + Object.keys(source).forEach(keys => { + if (source[keys] && typeof source[keys] === 'object') { + targetObj[keys] = deepClone(source[keys]) + } else { + targetObj[keys] = source[keys] + } + }) + return targetObj +} + +/** + * @param {Array} arr + * @returns {Array} + */ +export function uniqueArr(arr) { + return Array.from(new Set(arr)) +} + +/** + * @returns {string} + */ +export function createUniqueString() { + const timestamp = +new Date() + '' + const randomNum = parseInt((1 + Math.random()) * 65536) + '' + return (+(randomNum + timestamp)).toString(32) +} + +/** + * Check if an element has a class + * @param {HTMLElement} elm + * @param {string} cls + * @returns {boolean} + */ +export function hasClass(ele, cls) { + return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')) +} + +/** + * Add class to element + * @param {HTMLElement} elm + * @param {string} cls + */ +export function addClass(ele, cls) { + if (!hasClass(ele, cls)) ele.className += ' ' + cls +} + +/** + * Remove class from element + * @param {HTMLElement} elm + * @param {string} cls + */ +export function removeClass(ele, cls) { + if (hasClass(ele, cls)) { + const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)') + ele.className = ele.className.replace(reg, ' ') + } +} + +export function makeMap(str, expectsLowerCase) { + const map = Object.create(null) + const list = str.split(',') + for (let i = 0; i < list.length; i++) { + map[list[i]] = true + } + return expectsLowerCase + ? val => map[val.toLowerCase()] + : val => map[val] +} + +export const exportDefault = 'export default ' + +export const beautifierConf = { + html: { + indent_size: '2', + indent_char: ' ', + max_preserve_newlines: '-1', + preserve_newlines: false, + keep_array_indentation: false, + break_chained_methods: false, + indent_scripts: 'separate', + brace_style: 'end-expand', + space_before_conditional: true, + unescape_strings: false, + jslint_happy: false, + end_with_newline: true, + wrap_line_length: '110', + indent_inner_html: true, + comma_first: false, + e4x: true, + indent_empty_lines: true + }, + js: { + indent_size: '2', + indent_char: ' ', + max_preserve_newlines: '-1', + preserve_newlines: false, + keep_array_indentation: false, + break_chained_methods: false, + indent_scripts: 'normal', + brace_style: 'end-expand', + space_before_conditional: true, + unescape_strings: false, + jslint_happy: true, + end_with_newline: true, + wrap_line_length: '110', + indent_inner_html: true, + comma_first: false, + e4x: true, + indent_empty_lines: true + } +} + +// 首字母大小 +export function titleCase(str) { + return str.replace(/( |^)[a-z]/g, L => L.toUpperCase()) +} + +// 下划转驼峰 +export function camelCase(str) { + return str.replace(/_[a-z]/g, str1 => str1.substr(-1).toUpperCase()) +} + +export function isNumberStr(str) { + return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str) +} + diff --git a/admin-ui/src/utils/jsencrypt.js b/admin-ui/src/utils/jsencrypt.js new file mode 100644 index 0000000..78d9523 --- /dev/null +++ b/admin-ui/src/utils/jsencrypt.js @@ -0,0 +1,30 @@ +import JSEncrypt from 'jsencrypt/bin/jsencrypt.min' + +// 密钥对生成 http://web.chacuo.net/netrsakeypair + +const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' + +const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' + + '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' + + 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' + + 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' + + 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' + + 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' + + 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' + + 'UP8iWi1Qw0Y=' + +// 加密 +export function encrypt(txt) { + const encryptor = new JSEncrypt() + encryptor.setPublicKey(publicKey) // 设置公钥 + return encryptor.encrypt(txt) // 对数据进行加密 +} + +// 解密 +export function decrypt(txt) { + const encryptor = new JSEncrypt() + encryptor.setPrivateKey(privateKey) // 设置私钥 + return encryptor.decrypt(txt) // 对数据进行解密 +} + diff --git a/admin-ui/src/utils/permission.js b/admin-ui/src/utils/permission.js new file mode 100644 index 0000000..93fee87 --- /dev/null +++ b/admin-ui/src/utils/permission.js @@ -0,0 +1,51 @@ +import useUserStore from '@/store/modules/user' + +/** + * 字符权限校验 + * @param {Array} value 校验值 + * @returns {Boolean} + */ +export function checkPermi(value) { + if (value && value instanceof Array && value.length > 0) { + const permissions = useUserStore().permissions + const permissionDatas = value + const all_permission = "*:*:*"; + + const hasPermission = permissions.some(permission => { + return all_permission === permission || permissionDatas.includes(permission) + }) + + if (!hasPermission) { + return false + } + return true + } else { + console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`) + return false + } +} + +/** + * 角色权限校验 + * @param {Array} value 校验值 + * @returns {Boolean} + */ +export function checkRole(value) { + if (value && value instanceof Array && value.length > 0) { + const roles = useUserStore().roles + const permissionRoles = value + const super_admin = "admin"; + + const hasRole = roles.some(role => { + return super_admin === role || permissionRoles.includes(role) + }) + + if (!hasRole) { + return false + } + return true + } else { + console.error(`need roles! Like checkRole="['admin','editor']"`) + return false + } +} \ No newline at end of file diff --git a/admin-ui/src/utils/request.js b/admin-ui/src/utils/request.js new file mode 100644 index 0000000..3bdd541 --- /dev/null +++ b/admin-ui/src/utils/request.js @@ -0,0 +1,152 @@ +import axios from 'axios' +import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus' +import { getToken } from '@/utils/auth' +import errorCode from '@/utils/errorCode' +import { tansParams, blobValidate } from '@/utils/ruoyi' +import cache from '@/plugins/cache' +import { saveAs } from 'file-saver' +import useUserStore from '@/store/modules/user' +import router from '@/router' + +let downloadLoadingInstance; +// 是否显示重新登录 +export let isRelogin = { show: false }; + +axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' +// 创建axios实例 +const service = axios.create({ + // axios中请求配置有baseURL选项,表示请求URL公共部分 + baseURL: import.meta.env.VITE_APP_BASE_API, + // 超时 + timeout: 10000 +}) + +// request拦截器 +service.interceptors.request.use(config => { + + // 是否需要防止数据重复提交 + const isRepeatSubmit = (config.headers || {}).repeatSubmit === false + if (getToken()) { + config.headers['Authorization'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 + } + // get请求映射params参数 + if (config.method === 'get' && config.params) { + let url = config.url + '?' + tansParams(config.params); + url = url.slice(0, -1); + config.params = {}; + config.url = url; + } + if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { + const requestObj = { + url: config.url, + data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, + time: new Date().getTime() + } + const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小 + const limitSize = 5 * 1024 * 1024; // 限制存放数据5M + if (requestSize >= limitSize) { + console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。') + return config; + } + const sessionObj = cache.session.getJSON('sessionObj') + if (sessionObj === undefined || sessionObj === null || sessionObj === '') { + cache.session.setJSON('sessionObj', requestObj) + } else { + const s_url = sessionObj.url; // 请求地址 + const s_data = sessionObj.data; // 请求数据 + const s_time = sessionObj.time; // 请求时间 + const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交 + if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { + const message = '数据正在处理,请勿重复提交'; + console.warn(`[${s_url}]: ` + message) + return Promise.reject(new Error(message)) + } else { + cache.session.setJSON('sessionObj', requestObj) + } + } + } + return config +}, error => { + console.log(error) + Promise.reject(error) +}) + +// 响应拦截器 +service.interceptors.response.use(res => { + // 未设置状态码则默认成功状态 + const code = res.data.code || 200; + // 获取错误信息 + const msg = errorCode[code] || res.data.msg || errorCode['default'] + // 二进制数据则直接返回 + if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') { + return res.data + } + if (code === 401) { + if (!isRelogin.show) { + isRelogin.show = true; + ElMessageBox.confirm('登录状态已过期!', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { + isRelogin.show = false; + useUserStore().logOut().then(() => { + router.push({path:"/login"}); + }) + }).catch(() => { + isRelogin.show = false; + }); + } + return Promise.reject('登陆已过期,请重新登录') + } else if (code === 500) { + ElMessage({ message: msg, type: 'error' }) + return Promise.reject(new Error(msg)) + } else if (code === 601) { + ElMessage({ message: msg, type: 'warning' }) + return Promise.reject(new Error(msg)) + } else if (code !== 200) { + ElNotification.error({ title: msg }) + return Promise.reject('error') + } else { + return Promise.resolve(res.data) + } + }, + error => { + console.log('err' + error) + let { message } = error; + if (message == "Network Error") { + message = "后端接口连接异常"; + } else if (message.includes("timeout")) { + message = "系统接口请求超时"; + } else if (message.includes("Request failed with status code")) { + message = "系统接口" + message.substr(message.length - 3) + "异常"; + } + ElMessage({ message: message, type: 'error', duration: 5 * 1000 }) + return Promise.reject(error) + } +) + +// 通用下载方法 +export function download(url, params, filename, config) { + downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", }) + return service.post(url, params, { + transformRequest: [(params) => { return tansParams(params) }], + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + responseType: 'blob', + ...config + }).then(async (data) => { + const isBlob = blobValidate(data); + if (isBlob) { + const blob = new Blob([data]) + saveAs(blob, filename) + } else { + const resText = await data.text(); + const rspObj = JSON.parse(resText); + const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] + ElMessage.error(errMsg); + } + downloadLoadingInstance.close(); + }).catch((r) => { + console.error(r) + ElMessage.error('下载文件出现错误,请联系管理员!') + downloadLoadingInstance.close(); + }) +} + +export default service diff --git a/admin-ui/src/utils/ruoyi.js b/admin-ui/src/utils/ruoyi.js new file mode 100644 index 0000000..4efca08 --- /dev/null +++ b/admin-ui/src/utils/ruoyi.js @@ -0,0 +1,246 @@ + + +/** + * 通用js方法封装处理 + * Copyright (c) 2019 ruoyi + */ + +// 日期格式化 +export function parseTime(time, pattern) { + if (arguments.length === 0 || !time) { + return null + } + const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { + time = parseInt(time) + } else if (typeof time === 'string') { + time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), ''); + } + if ((typeof time === 'number') && (time.toString().length === 10)) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return time_str +} + +// 表单重置 +export function resetForm(refName) { + if (this.$refs[refName]) { + this.$refs[refName].resetFields(); + } +} + +// 添加日期范围 +export function addDateRange(params, dateRange, propName) { + let search = params; + search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {}; + dateRange = Array.isArray(dateRange) ? dateRange : []; + if (typeof (propName) === 'undefined') { + search.params['beginTime'] = dateRange[0]; + search.params['endTime'] = dateRange[1]; + } else { + search.params['begin' + propName] = dateRange[0]; + search.params['end' + propName] = dateRange[1]; + } + return search; +} + +// 回显数据字典 +export function selectDictLabel(datas, value) { + if (value === undefined) { + return ""; + } + var actions = []; + Object.keys(datas).some((key) => { + if (datas[key].value == ('' + value)) { + actions.push(datas[key].label); + return true; + } + }) + if (actions.length === 0) { + actions.push(value); + } + return actions.join(''); +} + +// 回显数据字典(字符串数组) +export function selectDictLabels(datas, value, separator) { + if (value === undefined || value.length ===0) { + return ""; + } + if (Array.isArray(value)) { + value = value.join(","); + } + var actions = []; + var currentSeparator = undefined === separator ? "," : separator; + var temp = value.split(currentSeparator); + Object.keys(value.split(currentSeparator)).some((val) => { + var match = false; + Object.keys(datas).some((key) => { + if (datas[key].value == ('' + temp[val])) { + actions.push(datas[key].label + currentSeparator); + match = true; + } + }) + if (!match) { + actions.push(temp[val] + currentSeparator); + } + }) + return actions.join('').substring(0, actions.join('').length - 1); +} + +// 字符串格式化(%s ) +export function sprintf(str) { + var args = arguments, flag = true, i = 1; + str = str.replace(/%s/g, function () { + var arg = args[i++]; + if (typeof arg === 'undefined') { + flag = false; + return ''; + } + return arg; + }); + return flag ? str : ''; +} + +// 转换字符串,undefined,null等转化为"" +export function parseStrEmpty(str) { + if (!str || str == "undefined" || str == "null") { + return ""; + } + return str; +} + +// 数据合并 +export function mergeRecursive(source, target) { + for (var p in target) { + try { + if (target[p].constructor == Object) { + source[p] = mergeRecursive(source[p], target[p]); + } else { + source[p] = target[p]; + } + } catch (e) { + source[p] = target[p]; + } + } + return source; +}; + +/** + * 构造树型结构数据 + * @param {*} data 数据源 + * @param {*} id id字段 默认 'id' + * @param {*} parentId 父节点字段 默认 'parentId' + * @param {*} children 孩子节点字段 默认 'children' + */ +export function handleTree(data, id, parentId, children) { + let config = { + id: id || 'id', + parentId: parentId || 'parentId', + childrenList: children || 'children' + }; + + var childrenListMap = {}; + var nodeIds = {}; + var tree = []; + + for (let d of data) { + let parentId = d[config.parentId]; + if (childrenListMap[parentId] == null) { + childrenListMap[parentId] = []; + } + nodeIds[d[config.id]] = d; + childrenListMap[parentId].push(d); + } + + for (let d of data) { + let parentId = d[config.parentId]; + if (nodeIds[parentId] == null) { + tree.push(d); + } + } + + for (let t of tree) { + adaptToChildrenList(t); + } + + function adaptToChildrenList(o) { + if (childrenListMap[o[config.id]] !== null) { + o[config.childrenList] = childrenListMap[o[config.id]]; + } + if (o[config.childrenList]) { + for (let c of o[config.childrenList]) { + adaptToChildrenList(c); + } + } + } + return tree; +} + +/** +* 参数处理 +* @param {*} params 参数 +*/ +export function tansParams(params) { + let result = '' + for (const propName of Object.keys(params)) { + const value = params[propName]; + var part = encodeURIComponent(propName) + "="; + if (value !== null && value !== "" && typeof (value) !== "undefined") { + if (typeof value === 'object') { + for (const key of Object.keys(value)) { + if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') { + let params = propName + '[' + key + ']'; + var subPart = encodeURIComponent(params) + "="; + result += subPart + encodeURIComponent(value[key]) + "&"; + } + } + } else { + result += part + encodeURIComponent(value) + "&"; + } + } + } + return result +} + + +// 返回项目路径 +export function getNormalPath(p) { + if (p.length === 0 || !p || p == 'undefined') { + return p + }; + let res = p.replace('//', '/') + if (res[res.length - 1] === '/') { + return res.slice(0, res.length - 1) + } + return res; +} + +// 验证是否为blob格式 +export function blobValidate(data) { + return data.type !== 'application/json' +} diff --git a/admin-ui/src/utils/scroll-to.js b/admin-ui/src/utils/scroll-to.js new file mode 100644 index 0000000..c5d8e04 --- /dev/null +++ b/admin-ui/src/utils/scroll-to.js @@ -0,0 +1,58 @@ +Math.easeInOutQuad = function(t, b, c, d) { + t /= d / 2 + if (t < 1) { + return c / 2 * t * t + b + } + t-- + return -c / 2 * (t * (t - 2) - 1) + b +} + +// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts +var requestAnimFrame = (function() { + return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } +})() + +/** + * Because it's so fucking difficult to detect the scrolling element, just move them all + * @param {number} amount + */ +function move(amount) { + document.documentElement.scrollTop = amount + document.body.parentNode.scrollTop = amount + document.body.scrollTop = amount +} + +function position() { + return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop +} + +/** + * @param {number} to + * @param {number} duration + * @param {Function} callback + */ +export function scrollTo(to, duration, callback) { + const start = position() + const change = to - start + const increment = 20 + let currentTime = 0 + duration = (typeof (duration) === 'undefined') ? 500 : duration + var animateScroll = function() { + // increment the time + currentTime += increment + // find the value with the quadratic in-out easing function + var val = Math.easeInOutQuad(currentTime, start, change, duration) + // move the document.body + move(val) + // do the animation unless its over + if (currentTime < duration) { + requestAnimFrame(animateScroll) + } else { + if (callback && typeof (callback) === 'function') { + // the animation is done so lets callback + callback() + } + } + } + animateScroll() +} diff --git a/admin-ui/src/utils/theme.js b/admin-ui/src/utils/theme.js new file mode 100644 index 0000000..f4badc6 --- /dev/null +++ b/admin-ui/src/utils/theme.js @@ -0,0 +1,49 @@ +// 处理主题样式 +export function handleThemeStyle(theme) { + document.documentElement.style.setProperty('--el-color-primary', theme) + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(theme, i / 10)}`) + } + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(theme, i / 10)}`) + } +} + +// hex颜色转rgb颜色 +export function hexToRgb(str) { + str = str.replace('#', '') + let hexs = str.match(/../g) + for (let i = 0; i < 3; i++) { + hexs[i] = parseInt(hexs[i], 16) + } + return hexs +} + +// rgb颜色转Hex颜色 +export function rgbToHex(r, g, b) { + let hexs = [r.toString(16), g.toString(16), b.toString(16)] + for (let i = 0; i < 3; i++) { + if (hexs[i].length == 1) { + hexs[i] = `0${hexs[i]}` + } + } + return `#${hexs.join('')}` +} + +// 变浅颜色值 +export function getLightColor(color, level) { + let rgb = hexToRgb(color) + for (let i = 0; i < 3; i++) { + rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]) + } + return rgbToHex(rgb[0], rgb[1], rgb[2]) +} + +// 变深颜色值 +export function getDarkColor(color, level) { + let rgb = hexToRgb(color) + for (let i = 0; i < 3; i++) { + rgb[i] = Math.floor(rgb[i] * (1 - level)) + } + return rgbToHex(rgb[0], rgb[1], rgb[2]) +} diff --git a/admin-ui/src/utils/validate.js b/admin-ui/src/utils/validate.js new file mode 100644 index 0000000..702add4 --- /dev/null +++ b/admin-ui/src/utils/validate.js @@ -0,0 +1,93 @@ +/** + * 判断url是否是http或https + * @param {string} path + * @returns {Boolean} + */ + export function isHttp(url) { + return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1 +} + +/** + * 判断path是否为外链 + * @param {string} path + * @returns {Boolean} + */ + export function isExternal(path) { + return /^(https?:|mailto:|tel:)/.test(path) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUsername(str) { + const valid_map = ['admin', 'editor'] + return valid_map.indexOf(str.trim()) >= 0 +} + +/** + * @param {string} url + * @returns {Boolean} + */ +export function validURL(url) { + const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ + return reg.test(url) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validLowerCase(str) { + const reg = /^[a-z]+$/ + return reg.test(str) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUpperCase(str) { + const reg = /^[A-Z]+$/ + return reg.test(str) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validAlphabets(str) { + const reg = /^[A-Za-z]+$/ + return reg.test(str) +} + +/** + * @param {string} email + * @returns {Boolean} + */ +export function validEmail(email) { + const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + return reg.test(email) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function isString(str) { + if (typeof str === 'string' || str instanceof String) { + return true + } + return false +} + +/** + * @param {Array} arg + * @returns {Boolean} + */ +export function isArray(arg) { + if (typeof Array.isArray === 'undefined') { + return Object.prototype.toString.call(arg) === '[object Array]' + } + return Array.isArray(arg) +} diff --git a/admin-ui/src/views/error/401.vue b/admin-ui/src/views/error/401.vue new file mode 100644 index 0000000..1ba3792 --- /dev/null +++ b/admin-ui/src/views/error/401.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/admin-ui/src/views/error/404.vue b/admin-ui/src/views/error/404.vue new file mode 100644 index 0000000..f205303 --- /dev/null +++ b/admin-ui/src/views/error/404.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/admin-ui/src/views/index.vue b/admin-ui/src/views/index.vue new file mode 100644 index 0000000..a98948c --- /dev/null +++ b/admin-ui/src/views/index.vue @@ -0,0 +1,23 @@ + + + + + + diff --git a/admin-ui/src/views/login.vue b/admin-ui/src/views/login.vue new file mode 100644 index 0000000..df9fc9c --- /dev/null +++ b/admin-ui/src/views/login.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/admin-ui/src/views/redirect/index.vue b/admin-ui/src/views/redirect/index.vue new file mode 100644 index 0000000..a469960 --- /dev/null +++ b/admin-ui/src/views/redirect/index.vue @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/admin-ui/src/views/register.vue b/admin-ui/src/views/register.vue new file mode 100644 index 0000000..d9e3bc2 --- /dev/null +++ b/admin-ui/src/views/register.vue @@ -0,0 +1,216 @@ + + + + + diff --git a/admin-ui/src/views/system/dept/index.vue b/admin-ui/src/views/system/dept/index.vue new file mode 100644 index 0000000..eb101e7 --- /dev/null +++ b/admin-ui/src/views/system/dept/index.vue @@ -0,0 +1,381 @@ + + + diff --git a/admin-ui/src/views/system/menu/index.vue b/admin-ui/src/views/system/menu/index.vue new file mode 100644 index 0000000..2eff120 --- /dev/null +++ b/admin-ui/src/views/system/menu/index.vue @@ -0,0 +1,462 @@ + + + diff --git a/admin-ui/src/views/system/role/authUser.vue b/admin-ui/src/views/system/role/authUser.vue new file mode 100644 index 0000000..98337d7 --- /dev/null +++ b/admin-ui/src/views/system/role/authUser.vue @@ -0,0 +1,171 @@ + + + + diff --git a/admin-ui/src/views/system/role/index.vue b/admin-ui/src/views/system/role/index.vue new file mode 100644 index 0000000..83ad99e --- /dev/null +++ b/admin-ui/src/views/system/role/index.vue @@ -0,0 +1,630 @@ + + + diff --git a/admin-ui/src/views/system/role/selectUser.vue b/admin-ui/src/views/system/role/selectUser.vue new file mode 100644 index 0000000..d77aefe --- /dev/null +++ b/admin-ui/src/views/system/role/selectUser.vue @@ -0,0 +1,138 @@ + + + diff --git a/admin-ui/src/views/system/user/authRole.vue b/admin-ui/src/views/system/user/authRole.vue new file mode 100644 index 0000000..8188ecb --- /dev/null +++ b/admin-ui/src/views/system/user/authRole.vue @@ -0,0 +1,112 @@ + + + diff --git a/admin-ui/src/views/system/user/index.vue b/admin-ui/src/views/system/user/index.vue new file mode 100644 index 0000000..25f6018 --- /dev/null +++ b/admin-ui/src/views/system/user/index.vue @@ -0,0 +1,482 @@ + + + diff --git a/admin-ui/src/views/system/user/profile/index.vue b/admin-ui/src/views/system/user/profile/index.vue new file mode 100644 index 0000000..6c03ff7 --- /dev/null +++ b/admin-ui/src/views/system/user/profile/index.vue @@ -0,0 +1,80 @@ + + + diff --git a/admin-ui/src/views/system/user/profile/resetPwd.vue b/admin-ui/src/views/system/user/profile/resetPwd.vue new file mode 100644 index 0000000..dec2d79 --- /dev/null +++ b/admin-ui/src/views/system/user/profile/resetPwd.vue @@ -0,0 +1,57 @@ + + + diff --git a/admin-ui/src/views/system/user/profile/userAvatar.vue b/admin-ui/src/views/system/user/profile/userAvatar.vue new file mode 100644 index 0000000..3b39636 --- /dev/null +++ b/admin-ui/src/views/system/user/profile/userAvatar.vue @@ -0,0 +1,171 @@ + + + + + \ No newline at end of file diff --git a/admin-ui/src/views/system/user/profile/userInfo.vue b/admin-ui/src/views/system/user/profile/userInfo.vue new file mode 100644 index 0000000..47cd36c --- /dev/null +++ b/admin-ui/src/views/system/user/profile/userInfo.vue @@ -0,0 +1,62 @@ + + + diff --git a/admin-ui/vite.config.js b/admin-ui/vite.config.js new file mode 100644 index 0000000..3c75f68 --- /dev/null +++ b/admin-ui/vite.config.js @@ -0,0 +1,64 @@ +import { defineConfig, loadEnv } from 'vite' +import path from 'path' +import createVitePlugins from './vite/plugins' + +// https://vitejs.dev/config/ +export default defineConfig(({ mode, command }) => { + const env = loadEnv(mode, process.cwd()) + const { VITE_APP_ENV } = env + return { + // 部署生产环境和开发环境下的URL。 + // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上 + // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。 + base: VITE_APP_ENV === 'production' ? '/' : '/', + plugins: createVitePlugins(env, command === 'build'), + resolve: { + // https://cn.vitejs.dev/config/#resolve-alias + alias: { + // 设置路径 + '~': path.resolve(__dirname, './'), + // 设置别名 + '@': path.resolve(__dirname, './src') + }, + // https://cn.vitejs.dev/config/#resolve-extensions + extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'] + }, + // vite 相关配置 + server: { + port: 80, + host: true, + open: true, + proxy: { + '/dev-api': { + // target: 'https://vue.ruoyi.vip/prod-api/', + target: 'http://124.71.212.219:8090/api', + changeOrigin: true, + rewrite: (p) => p.replace(/^\/dev-api/, ''), + bypass(req, res, options) { + const proxyURL = options.target + options.rewrite(req.url); + console.log('proxyURL', proxyURL); + req.headers['x-req-proxyURL'] = proxyURL; // 设置未生效 + res.setHeader('x-req-proxyURL', proxyURL); // 设置响应头可以看到 + }, + } + } + }, + //fix:error:stdin>:7356:1: warning: "@charset" must be the first rule in the file + css: { + postcss: { + plugins: [ + { + postcssPlugin: 'internal:charset-removal', + AtRule: { + charset: (atRule) => { + if (atRule.name === 'charset') { + atRule.remove(); + } + } + } + } + ] + } + } + } +}) diff --git a/admin-ui/vite/plugins/auto-import.js b/admin-ui/vite/plugins/auto-import.js new file mode 100644 index 0000000..a5d3576 --- /dev/null +++ b/admin-ui/vite/plugins/auto-import.js @@ -0,0 +1,12 @@ +import autoImport from 'unplugin-auto-import/vite' + +export default function createAutoImport() { + return autoImport({ + imports: [ + 'vue', + 'vue-router', + 'pinia' + ], + dts: false + }) +} diff --git a/admin-ui/vite/plugins/compression.js b/admin-ui/vite/plugins/compression.js new file mode 100644 index 0000000..e90aaec --- /dev/null +++ b/admin-ui/vite/plugins/compression.js @@ -0,0 +1,28 @@ +import compression from 'vite-plugin-compression' + +export default function createCompression(env) { + const { VITE_BUILD_COMPRESS } = env + const plugin = [] + if (VITE_BUILD_COMPRESS) { + const compressList = VITE_BUILD_COMPRESS.split(',') + if (compressList.includes('gzip')) { + // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件 + plugin.push( + compression({ + ext: '.gz', + deleteOriginFile: false + }) + ) + } + if (compressList.includes('brotli')) { + plugin.push( + compression({ + ext: '.br', + algorithm: 'brotliCompress', + deleteOriginFile: false + }) + ) + } + } + return plugin +} diff --git a/admin-ui/vite/plugins/index.js b/admin-ui/vite/plugins/index.js new file mode 100644 index 0000000..10e17c3 --- /dev/null +++ b/admin-ui/vite/plugins/index.js @@ -0,0 +1,15 @@ +import vue from '@vitejs/plugin-vue' + +import createAutoImport from './auto-import' +import createSvgIcon from './svg-icon' +import createCompression from './compression' +import createSetupExtend from './setup-extend' + +export default function createVitePlugins(viteEnv, isBuild = false) { + const vitePlugins = [vue()] + vitePlugins.push(createAutoImport()) + vitePlugins.push(createSetupExtend()) + vitePlugins.push(createSvgIcon(isBuild)) + isBuild && vitePlugins.push(...createCompression(viteEnv)) + return vitePlugins +} diff --git a/admin-ui/vite/plugins/setup-extend.js b/admin-ui/vite/plugins/setup-extend.js new file mode 100644 index 0000000..ed8342e --- /dev/null +++ b/admin-ui/vite/plugins/setup-extend.js @@ -0,0 +1,5 @@ +import setupExtend from 'unplugin-vue-setup-extend-plus/vite' + +export default function createSetupExtend() { + return setupExtend({}) +} diff --git a/admin-ui/vite/plugins/svg-icon.js b/admin-ui/vite/plugins/svg-icon.js new file mode 100644 index 0000000..30a4140 --- /dev/null +++ b/admin-ui/vite/plugins/svg-icon.js @@ -0,0 +1,10 @@ +import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' +import path from 'path' + +export default function createSvgIcon(isBuild) { + return createSvgIconsPlugin({ + iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')], + symbolId: 'icon-[dir]-[name]', + svgoOptions: isBuild + }) +}