Merge remote-tracking branch 'origin/master'

This commit is contained in:
李强
2023-03-31 10:31:03 +08:00
312 changed files with 21647 additions and 25563 deletions

View File

@@ -1,36 +1,168 @@
# django-vue3-admin # Django-Vue3-Admin
#### Description [![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.7.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark)](https://gitee.com/liqianglog/django-vue-admin)
django-vue3-admin
#### Software Architecture [preview](https://demo.dvadmin.com) | [Official website document](https://www.django-vue-admin.com) | [qq group](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [community](https://bbs.django-vue-admin.com) | [plugins market](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
Software architecture description
#### Installation 💡 **「About」**
1. xxxx We are a group of young people who love Code. In this hot era, we hope to calm down and bring some of our colors and colors through code.
2. xxxx
3. xxxx
#### Instructions Because of love, so embrace the future
1. xxxx ## framework introduction
2. xxxx
3. xxxx
#### Contribution 💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) Is a set of all open source rapid development platform, no reservation for individuals and enterprises free use.
1. Fork the repository * 🧑🤝🧑Front-end adoption Vue3+TS+pinia+fastcrud。
2. Create Feat_xxx branch * 👭The backend uses the Python language Django framework as well as the powerful[Django REST Framework](https://pypi.org/project/djangorestframework)。
3. Commit your code * 👫Permission authentication use[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt)Supports the multi-terminal authentication system.
4. Create Pull Request * 👬Support loading dynamic permission menu, multi - way easy permission control.
* 💏 Special thanks: [vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/).
* 💡 💏 Special thanks:[jetbrains](https://www.jetbrains.com/) To provide a free IntelliJ IDEA license for this open source project.
## Online experience
#### Gitee Feature 👩‍👧‍👦👩‍👧‍👦 demo address:[http://demo.django-vue-admin.com](http://demo.django-vue-admin.com)
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md * demo accountsuperadmin
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) * demo passwordadmin123456
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) 👩👦👦docs:[https://django-vue-admin.com](https://django-vue-admin.com)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
## communication
* Communication community:[click here](https://bbs.django-vue-admin.com)👩‍👦‍👦
* plugins market:[click here](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦
## source code url:
gitee(Main push)[https://gitee.com/liqianglog/django-vue-admin](https://gitee.com/liqianglog/django-vue-admin)👩‍👦‍👦
github[https://github.com/liqianglog/django-vue-admin](https://github.com/liqianglog/django-vue-admin)👩‍👦‍👦
## core function
1. 👨‍⚕️ Menu management: Configure the system menu, operation permissions, button permissions, back-end interface permissions, etc.
2. 🧑‍⚕️ Department management: Configure the system organization (company, department, role).
3. 👩‍⚕️ Role management: role menu permission allocation, data permission allocation, set roles according to the department for data range permission division.
4. 🧑‍🎓 Rights Specifies the rights of the authorization role.
5. 👨‍🎓 User management: The user is the system operator, this function mainly completes the system user configuration.
6. 👬 Interface whitelist: specifies the interface that does not need permission verification.
7. 🧑‍🔧 Dictionary management: Maintenance of some fixed data frequently used in the system.
8. 🧑‍🔧 Regional management: to manage provinces, cities, counties and regions.
9. 📁 Attachment management: Unified management of all files and pictures on the platform.
10. 🗓 operation logs: log and query the system normal operation; Log and query system exception information.
11.🔌 [plugins market] (<https://bbs.django-vue-admin.com/plugMarket.html>) : based on the Django framework - Vue - Admin application and plug-in development.
## plugins market 🔌
* Celery Asynchronous task[dvadmin-celery](https://gitee.com/huge-dream/dvadmin-celery)
* Upgrade center backend[dvadmin-upgrade-center](https://gitee.com/huge-dream/dvadmin-upgrade-center)
* Upgrade center front[dvadmin-upgrade-center-web](https://gitee.com/huge-dream/dvadmin-upgrade-center-web)
## before start project you need:
~~~
Python >= 3.8.0
nodejs >= 14.0
Mysql >= 5.7.0 (Optional. The default database is sqlite3. 8.0 is recommended)
Redis(Optional, the latest edition)
~~~
## frontend♝
```bash
# clone code
git clone https://gitee.com/liqianglog/django-vue-admin.git
# enter code dir
cd web
# install dependence
npm install --registry=https://registry.npm.taobao.org
# Start service
npm run dev
# Visit http://localhost:8080 in your browser
# Parameters such as boot port can be configured in the #.env.development file
# Build the production environment
# npm run build
```
## backend💈
~~~bash
1. enter code dir cd backend
2. copy ./conf/env.example.py to ./conf dirrename as env.py
3. in env.py configure database information
mysql database recommended version: 8.0
mysql database character set: utf8mb4
4. install pip dependence
pip3 install -r requirements.txt
5. Execute the migration command:
python3 manage.py makemigrations
python3 manage.py migrate
6. Initialization data
python3 manage.py init
7. Initialize provincial, municipal and county data:
python3 manage.py init_area
8. start backend
python3 manage.py runserver 0.0.0.0:8000
or daphne :
daphne -b 0.0.0.0 -p 8000 application.asgi:application
~~~
### visit backend swagger
* visit url[http://localhost:8080](http://localhost:8080) (The default address is this one. If you want to change it, follow the configuration file)
* account`superadmin` password`admin123456`
### docker-compose
~~~shell
docker-compose up -d
# Initialize backend data (first execution only)
docker exec -ti dvadmin-django bash
python manage.py makemigrations
python manage.py migrate
python manage.py init_area
python manage.py init
exit
frontend urlhttp://127.0.0.1:8080
backend urlhttp://127.0.0.1:8080/api
# Change 127.0.0.1 to your own public ip address on the server
account`superadmin` password`admin123456`
# docker-compose stop
docker-compose down
# docker-compose restart
docker-compose restart
# docker-compose on start build
docker-compose up -d --build
~~~
## Demo screenshot✅
![image-01](https://images.gitee.com/uploads/images/2022/0530/234137_b58c8f98_5074988.png)
![image-02](https://images.gitee.com/uploads/images/2022/0530/234240_39834603_5074988.png)
![image-03](https://images.gitee.com/uploads/images/2022/0530/234339_35e728a0_5074988.png)
![image-04](https://images.gitee.com/uploads/images/2022/0530/234426_957036b0_5074988.png)
![image-05](https://images.gitee.com/uploads/images/2022/0530/234458_898be492_5074988.png)
![image-06](https://images.gitee.com/uploads/images/2022/0530/234521_35b40076_5074988.png)
![image-07](https://images.gitee.com/uploads/images/2022/0530/234615_c2325639_5074988.png)
![image-08](https://images.gitee.com/uploads/images/2022/0530/234639_1ed6cc93_5074988.png)
![image-09](https://images.gitee.com/uploads/images/2022/0530/234815_cea2c53f_5074988.png)
![image-10](https://images.gitee.com/uploads/images/2022/0530/234840_5f3e5f53_5074988.png)

View File

@@ -1,8 +1,8 @@
# Django-Vue-Admin # Django-Vue3-Admin
[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.7.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark)](https://gitee.com/liqianglog/django-vue-admin) [![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.7.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark)](https://gitee.com/liqianglog/django-vue-admin)
[预 览](https://demo.django-vue-admin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin) [预 览](https://demo.dvadmin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
@@ -14,15 +14,15 @@
## 平台简介 ## 平台简介
💡 [django-vue-admin](https://gitee.com/dvadmin/django-vue-admin) 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。 💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
* 🧑‍🤝‍🧑前端采用[D2Admin](https://github.com/d2-projects/d2-admin) 、[Vue](https://cn.vuejs.org/)、[ElementUI](https://element.eleme.cn/) * 🧑‍🤝‍🧑前端采用 Vue3+TS+pinia+fastcrud(感谢[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/))
* 👭后端采用 Python 语言 Django 框架以及强大的 [Django REST Framework](https://pypi.org/project/djangorestframework)。 * 👭后端采用 Python 语言 Django 框架以及强大的 [Django REST Framework](https://pypi.org/project/djangorestframework)。
* 👫权限认证使用[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt),支持多终端认证系统。 * 👫权限认证使用[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt),支持多终端认证系统。
* 👬支持加载动态权限菜单,多方式轻松权限控制。 * 👬支持加载动态权限菜单,多方式轻松权限控制。
* 💏特别鸣谢:[D2Admin](https://github.com/d2-projects/d2-admin) 、[Vue-Element-Admin](https://github.com/PanJiaChen/vue-element-admin)。 * 💏特别鸣谢:[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/)。
* 💡 特别感谢[jetbrains](https://www.jetbrains.com/) 为本开源项目提供免费的 IntelliJ IDEA 授权。 * 💡 特别感谢[jetbrains](https://www.jetbrains.com/) 为本开源项目提供免费的 IntelliJ IDEA 授权。

View File

@@ -8,21 +8,25 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
""" """
import os import os
from django.core.asgi import get_asgi_application
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
http_application = get_asgi_application() http_application = get_asgi_application()
from application.routing import websocket_urlpatterns from application.routing import websocket_urlpatterns
application = ProtocolTypeRouter({ application = ProtocolTypeRouter({
"http":http_application, "http": http_application,
'websocket': AuthMiddlewareStack( 'websocket': AllowedHostsOriginValidator(
URLRouter( AuthMiddlewareStack(
websocket_urlpatterns #指明路由文件是devops/routing.py URLRouter(
websocket_urlpatterns # 指明路由文件是devops/routing.py
)
) )
), ),
}) })

View File

@@ -4,10 +4,10 @@ Django settings for application project.
Generated by 'django-admin startproject' using Django 3.2.3. Generated by 'django-admin startproject' using Django 3.2.3.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/ https://docs.djangoproject.com/en/4.1/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/ https://docs.djangoproject.com/en/4.1/ref/settings/
""" """
import os import os
@@ -259,6 +259,9 @@ LOGGING = {
# ================================================= # # ================================================= #
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
),
"DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", # 日期时间格式配置 "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", # 日期时间格式配置
"DATE_FORMAT": "%Y-%m-%d", "DATE_FORMAT": "%Y-%m-%d",
"DEFAULT_FILTER_BACKENDS": ( "DEFAULT_FILTER_BACKENDS": (
@@ -274,9 +277,6 @@ REST_FRAMEWORK = {
), ),
"DEFAULT_PERMISSION_CLASSES": [ "DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated", # 只有经过身份认证确定用户身份才能访问 "rest_framework.permissions.IsAuthenticated", # 只有经过身份认证确定用户身份才能访问
# 'rest_framework.permissions.IsAdminUser', # is_staff=True才能访问 —— 管理员(员工)权限
# 'rest_framework.permissions.AllowAny', # 允许所有
# 'rest_framework.permissions.IsAuthenticatedOrReadOnly', # 有身份 或者 只读访问(self.list,self.retrieve)
], ],
"EXCEPTION_HANDLER": "dvadmin.utils.exception.CustomExceptionHandler", # 自定义的异常处理 "EXCEPTION_HANDLER": "dvadmin.utils.exception.CustomExceptionHandler", # 自定义的异常处理
} }

View File

@@ -0,0 +1,345 @@
# -*- coding: utf-8 -*-
import os
from rest_framework import serializers
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
import django
django.setup()
from dvadmin.system.models import Role, Dept, Users, Menu, MenuButton, ApiWhiteList, Dictionary, SystemConfig, \
RoleMenuPermission, RoleMenuButtonPermission
from dvadmin.utils.serializers import CustomModelSerializer
class UsersInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
def save(self, **kwargs):
instance = super().save(**kwargs)
role_key = self.initial_data.get('role_key', [])
role_ids = Role.objects.filter(key__in=role_key).values_list('id', flat=True)
instance.role.set(role_ids)
dept_key = self.initial_data.get('dept_key', None)
dept_id = Dept.objects.filter(key=dept_key).first()
instance.dept = dept_id
instance.save()
return instance
class Meta:
model = Users
fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type',
'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id',
'password', 'last_login', 'is_superuser']
read_only_fields = ['id']
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class MenuButtonInitSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化器
"""
class Meta:
model = MenuButton
fields = ['id', 'name', 'value', 'api', 'method', 'menu']
read_only_fields = ["id"]
class MenuInitSerializer(CustomModelSerializer):
"""
递归深度获取数信息(用于生成初始化json文件)
"""
name = serializers.CharField(required=False)
children = serializers.SerializerMethodField()
menu_button = serializers.SerializerMethodField()
def get_children(self, obj: Menu):
data = []
instance = Menu.objects.filter(parent_id=obj.id)
if instance:
serializer = MenuInitSerializer(instance=instance, many=True)
data = serializer.data
return data
def get_menu_button(self, obj: Menu):
data = []
instance = obj.menuPermission.order_by('method')
if instance:
data = list(instance.values('name', 'value', 'api', 'method'))
return data
def save(self, **kwargs):
instance = super().save(**kwargs)
children = self.initial_data.get('children')
menu_button = self.initial_data.get('menu_button')
# 菜单表
if children:
for menu_data in children:
menu_data['parent'] = instance.id
filter_data = {
"name": menu_data['name'],
"web_path": menu_data['web_path'],
"component": menu_data['component'],
"component_name": menu_data['component_name'],
}
instance_obj = Menu.objects.filter(**filter_data).first()
if instance_obj and not self.initial_data.get('reset'):
continue
serializer = MenuInitSerializer(instance_obj, data=menu_data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
# 菜单按钮
if menu_button:
for menu_button_data in menu_button:
menu_button_data['menu'] = instance.id
filter_data = {
"menu": menu_button_data['menu'],
"value": menu_button_data['value']
}
instance_obj = MenuButton.objects.filter(**filter_data).first()
serializer = MenuButtonInitSerializer(instance_obj, data=menu_button_data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
return instance
class Meta:
model = Menu
fields = ['name', 'icon', 'sort', 'is_link', 'is_catalog', 'web_path', 'component', 'component_name', 'status',
'cache', 'visible', 'parent', 'children', 'menu_button', 'creator', 'dept_belong_id']
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
read_only_fields = ['id', 'children']
class RoleInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
class Meta:
model = Role
fields = ['name', 'key', 'sort', 'status', 'admin',
'creator', 'dept_belong_id']
read_only_fields = ["id"]
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class RoleMenuInitSerializer(CustomModelSerializer):
"""
初始化角色菜单(用于生成初始化json文件)
"""
role_key = serializers.CharField(max_length=100,required=True)
menu_component_name = serializers.CharField(max_length=100,required=True)
def create(self, validated_data):
init_data = self.initial_data
validated_data.pop('menu_component_name')
validated_data.pop('role_key')
role_id = Role.objects.filter(key=init_data['role_key']).first()
menu_id = Menu.objects.filter(component_name=init_data['menu_component_name']).first()
validated_data['role'] = role_id
validated_data['menu'] = menu_id
return super().create(validated_data)
class Meta:
model = RoleMenuPermission
fields = ['role_key','menu_component_name','creator', 'dept_belong_id']
read_only_fields = ["id"]
extra_kwargs = {
'role': {'required': False},
'menu': {'required': False},
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class RoleMenuButtonInitSerializer(CustomModelSerializer):
"""
初始化角色菜单按钮(用于生成初始化json文件)
"""
role_key = serializers.CharField(max_length=100,required=True)
menu_button_value = serializers.CharField(max_length=100,required=True)
data_range = serializers.CharField(max_length=100, required=False)
def create(self, validated_data):
init_data = self.initial_data
validated_data.pop('menu_button_value')
validated_data.pop('role_key')
role_id = Role.objects.filter(key=init_data['role_key']).first()
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button_value']).first()
validated_data['role'] = role_id
validated_data['menu_button'] = menu_button_id
instance = super().create(validated_data)
instance.dept.set([])
return instance
class Meta:
model = RoleMenuButtonPermission
fields = ['role_key','menu_button_value','data_range','dept','creator', 'dept_belong_id']
read_only_fields = ["id"]
extra_kwargs = {
'role': {'required': False},
'menu': {'required': False},
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class ApiWhiteListInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
class Meta:
model = ApiWhiteList
fields = ['url', 'method', 'enable_datasource', 'creator', 'dept_belong_id']
read_only_fields = ["id"]
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class DeptInitSerializer(CustomModelSerializer):
"""
递归深度获取数信息(用于生成初始化json文件)
"""
children = serializers.SerializerMethodField()
def get_children(self, obj: Dept):
data = []
instance = Dept.objects.filter(parent_id=obj.id)
if instance:
serializer = DeptInitSerializer(instance=instance, many=True)
data = serializer.data
return data
def save(self, **kwargs):
instance = super().save(**kwargs)
children = self.initial_data.get('children')
if children:
for menu_data in children:
menu_data['parent'] = instance.id
filter_data = {
"name": menu_data['name'],
"parent": menu_data['parent'],
"key": menu_data['key']
}
instance_obj = Dept.objects.filter(**filter_data).first()
if instance_obj and not self.initial_data.get('reset'):
continue
serializer = DeptInitSerializer(instance_obj, data=menu_data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
return instance
class Meta:
model = Dept
fields = ['name', 'sort', 'owner', 'phone', 'email', 'status', 'parent', 'creator', 'dept_belong_id',
'children', 'key']
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
read_only_fields = ['id', 'children']
class DictionaryInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
children = serializers.SerializerMethodField()
def get_children(self, obj: Dictionary):
data = []
instance = Dictionary.objects.filter(parent_id=obj.id)
if instance:
serializer = DictionaryInitSerializer(instance=instance, many=True)
data = serializer.data
return data
def save(self, **kwargs):
instance = super().save(**kwargs)
children = self.initial_data.get('children')
# 菜单表
if children:
for data in children:
data['parent'] = instance.id
filter_data = {
"value": data['value'],
"parent": data['parent']
}
instance_obj = Dictionary.objects.filter(**filter_data).first()
if instance_obj and not self.initial_data.get('reset'):
continue
serializer = DictionaryInitSerializer(instance_obj, data=data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
return instance
class Meta:
model = Dictionary
fields = ['label', 'value', 'parent', 'type', 'color', 'is_value', 'status', 'sort', 'remark', 'creator',
'dept_belong_id', 'children']
read_only_fields = ["id"]
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class SystemConfigInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
children = serializers.SerializerMethodField()
def get_children(self, obj: SystemConfig):
data = []
instance = SystemConfig.objects.filter(parent_id=obj.id)
if instance:
serializer = SystemConfigInitSerializer(instance=instance, many=True)
data = serializer.data
return data
def save(self, **kwargs):
instance = super().save(**kwargs)
children = self.initial_data.get('children')
# 菜单表
if children:
for data in children:
data['parent'] = instance.id
filter_data = {
"key": data['key'],
"parent": data['parent']
}
instance_obj = SystemConfig.objects.filter(**filter_data).first()
if instance_obj and not self.initial_data.get('reset'):
continue
serializer = SystemConfigInitSerializer(instance_obj, data=data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
return instance
class Meta:
model = SystemConfig
fields = ['parent', 'title', 'key', 'value', 'sort', 'status', 'data_options', 'form_item_type', 'rule',
'placeholder', 'setting', 'creator', 'dept_belong_id', 'children']
read_only_fields = ["id"]
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}

View File

@@ -15,7 +15,7 @@
"value": "true", "value": "true",
"parent": 1, "parent": 1,
"type": 6, "type": 6,
"color": null, "color": "success",
"is_value": true, "is_value": true,
"status": true, "status": true,
"sort": 1, "sort": 1,
@@ -27,7 +27,7 @@
"value": "false", "value": "false",
"parent": 1, "parent": 1,
"type": 6, "type": 6,
"color": null, "color": "danger",
"is_value": true, "is_value": true,
"status": true, "status": true,
"sort": 2, "sort": 2,
@@ -50,7 +50,7 @@
{ {
"label": "新增", "label": "新增",
"value": "Create", "value": "Create",
"parent": 66, "parent": 4,
"type": 0, "type": 0,
"color": "success", "color": "success",
"is_value": true, "is_value": true,
@@ -62,7 +62,7 @@
{ {
"label": "编辑", "label": "编辑",
"value": "Update", "value": "Update",
"parent": 66, "parent": 4,
"type": 0, "type": 0,
"color": "primary", "color": "primary",
"is_value": true, "is_value": true,
@@ -74,7 +74,7 @@
{ {
"label": "删除", "label": "删除",
"value": "Delete", "value": "Delete",
"parent": 66, "parent": 4,
"type": 0, "type": 0,
"color": "danger", "color": "danger",
"is_value": true, "is_value": true,
@@ -86,7 +86,7 @@
{ {
"label": "详情", "label": "详情",
"value": "Retrieve", "value": "Retrieve",
"parent": 66, "parent": 4,
"type": 0, "type": 0,
"color": "info", "color": "info",
"is_value": true, "is_value": true,
@@ -98,7 +98,7 @@
{ {
"label": "查询", "label": "查询",
"value": "Search", "value": "Search",
"parent": 66, "parent": 4,
"type": 0, "type": 0,
"color": "warning", "color": "warning",
"is_value": true, "is_value": true,
@@ -110,7 +110,7 @@
{ {
"label": "保存", "label": "保存",
"value": "Save", "value": "Save",
"parent": 66, "parent": 4,
"type": 0, "type": 0,
"color": "success", "color": "success",
"is_value": true, "is_value": true,
@@ -122,7 +122,7 @@
{ {
"label": "导入", "label": "导入",
"value": "Import", "value": "Import",
"parent": 66, "parent": 4,
"type": 0, "type": 0,
"color": "primary", "color": "primary",
"is_value": true, "is_value": true,
@@ -134,7 +134,7 @@
{ {
"label": "导出", "label": "导出",
"value": "Export", "value": "Export",
"parent": 66, "parent": 4,
"type": 0, "type": 0,
"color": "warning", "color": "warning",
"is_value": true, "is_value": true,
@@ -159,9 +159,9 @@
{ {
"label": "启用", "label": "启用",
"value": "1", "value": "1",
"parent": 7, "parent": 13,
"type": 1, "type": 1,
"color": null, "color": "success",
"is_value": true, "is_value": true,
"status": true, "status": true,
"sort": 1, "sort": 1,
@@ -171,9 +171,9 @@
{ {
"label": "禁用", "label": "禁用",
"value": "0", "value": "0",
"parent": 7, "parent": 13,
"type": 1, "type": 1,
"color": null, "color": "danger",
"is_value": true, "is_value": true,
"status": true, "status": true,
"sort": 2, "sort": 2,
@@ -196,9 +196,9 @@
{ {
"label": "是", "label": "是",
"value": "true", "value": "true",
"parent": 5, "parent": 16,
"type": 6, "type": 6,
"color": null, "color": "success",
"is_value": true, "is_value": true,
"status": true, "status": true,
"sort": 1, "sort": 1,
@@ -208,9 +208,9 @@
{ {
"label": "否", "label": "否",
"value": "false", "value": "false",
"parent": 5, "parent": 16,
"type": 6, "type": 6,
"color": null, "color": "danger",
"is_value": true, "is_value": true,
"status": true, "status": true,
"sort": 2, "sort": 2,
@@ -233,9 +233,9 @@
{ {
"label": "是", "label": "是",
"value": "1", "value": "1",
"parent": 10, "parent": 19,
"type": 1, "type": 1,
"color": null, "color": "success",
"is_value": true, "is_value": true,
"status": true, "status": true,
"sort": 1, "sort": 1,
@@ -245,9 +245,9 @@
{ {
"label": "否", "label": "否",
"value": "2", "value": "2",
"parent": 10, "parent": 19,
"type": 1, "type": 1,
"color": null, "color": "danger",
"is_value": true, "is_value": true,
"status": true, "status": true,
"sort": 2, "sort": 2,
@@ -270,7 +270,7 @@
{ {
"label": "后台用户", "label": "后台用户",
"value": "0", "value": "0",
"parent": 15, "parent": 22,
"type": 1, "type": 1,
"color": null, "color": null,
"is_value": true, "is_value": true,
@@ -282,7 +282,7 @@
{ {
"label": "前台用户", "label": "前台用户",
"value": "1", "value": "1",
"parent": 15, "parent": 22,
"type": 1, "type": 1,
"color": null, "color": null,
"is_value": true, "is_value": true,
@@ -307,7 +307,7 @@
{ {
"label": "text", "label": "text",
"value": "0", "value": "0",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": null, "color": null,
"is_value": true, "is_value": true,
@@ -319,7 +319,7 @@
{ {
"label": "textarea", "label": "textarea",
"value": "3", "value": "3",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": "", "color": "",
"is_value": true, "is_value": true,
@@ -331,7 +331,7 @@
{ {
"label": "number", "label": "number",
"value": "10", "value": "10",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": "", "color": "",
"is_value": true, "is_value": true,
@@ -343,7 +343,7 @@
{ {
"label": "datetime", "label": "datetime",
"value": "1", "value": "1",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": null, "color": null,
"is_value": true, "is_value": true,
@@ -355,7 +355,7 @@
{ {
"label": "date", "label": "date",
"value": "2", "value": "2",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": null, "color": null,
"is_value": true, "is_value": true,
@@ -367,7 +367,7 @@
{ {
"label": "time", "label": "time",
"value": "15", "value": "15",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": "", "color": "",
"is_value": true, "is_value": true,
@@ -379,7 +379,7 @@
{ {
"label": "select", "label": "select",
"value": "4", "value": "4",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": null, "color": null,
"is_value": true, "is_value": true,
@@ -391,7 +391,7 @@
{ {
"label": "checkbox", "label": "checkbox",
"value": "5", "value": "5",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": null, "color": null,
"is_value": true, "is_value": true,
@@ -403,7 +403,7 @@
{ {
"label": "radio", "label": "radio",
"value": "6", "value": "6",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": null, "color": null,
"is_value": true, "is_value": true,
@@ -415,7 +415,7 @@
{ {
"label": "switch", "label": "switch",
"value": "9", "value": "9",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": "", "color": "",
"is_value": true, "is_value": true,
@@ -427,7 +427,7 @@
{ {
"label": "文件附件", "label": "文件附件",
"value": "8", "value": "8",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": "", "color": "",
"is_value": true, "is_value": true,
@@ -439,7 +439,7 @@
{ {
"label": "图片(单张)", "label": "图片(单张)",
"value": "7", "value": "7",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": "", "color": "",
"is_value": true, "is_value": true,
@@ -451,7 +451,7 @@
{ {
"label": "图片(多张)", "label": "图片(多张)",
"value": "12", "value": "12",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": "", "color": "",
"is_value": true, "is_value": true,
@@ -463,7 +463,7 @@
{ {
"label": "数组", "label": "数组",
"value": "11", "value": "11",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": "", "color": "",
"is_value": true, "is_value": true,
@@ -475,7 +475,7 @@
{ {
"label": "关联表", "label": "关联表",
"value": "13", "value": "13",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": "", "color": "",
"is_value": true, "is_value": true,
@@ -487,7 +487,7 @@
{ {
"label": "关联表(多选)", "label": "关联表(多选)",
"value": "14", "value": "14",
"parent": 49, "parent": 25,
"type": 1, "type": 1,
"color": "", "color": "",
"is_value": true, "is_value": true,
@@ -512,7 +512,7 @@
{ {
"label": "未知", "label": "未知",
"value": "0", "value": "0",
"parent": 18, "parent": 42,
"type": 1, "type": 1,
"color": null, "color": null,
"is_value": true, "is_value": true,
@@ -524,7 +524,7 @@
{ {
"label": "男", "label": "男",
"value": "1", "value": "1",
"parent": 18, "parent": 42,
"type": 1, "type": 1,
"color": null, "color": null,
"is_value": true, "is_value": true,
@@ -536,7 +536,7 @@
{ {
"label": "女", "label": "女",
"value": "2", "value": "2",
"parent": 18, "parent": 42,
"type": 1, "type": 1,
"color": null, "color": null,
"is_value": true, "is_value": true,
@@ -547,4 +547,4 @@
} }
] ]
} }
] ]

View File

@@ -1,13 +1,13 @@
[ [
{ {
"name": "系统管理", "name": "系统管理",
"icon": "cog", "icon": "iconfont icon-xitongshezhi",
"sort": 1, "sort": 1,
"is_link": false, "is_link": false,
"is_catalog": true, "is_catalog": true,
"web_path": "", "web_path": "/system",
"component": "", "component": "layout/routerView/parent",
"component_name": "", "component_name": "menu",
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
@@ -15,46 +15,46 @@
"children": [ "children": [
{ {
"name": "菜单管理", "name": "菜单管理",
"icon": "navicon", "icon": "iconfont icon-caidan",
"sort": 1, "sort": 1,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
"web_path": "/menu", "web_path": "/menu",
"component": "system/menu", "component": "system/menu/index",
"component_name": "menu", "component_name": "menu",
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 277, "parent": 19,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "menu:Search",
"api": "/api/system/menu/", "api": "/api/system/menu/",
"method": 0 "method": 0
}, },
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "menu:Retrieve",
"api": "/api/system/menu/{id}/", "api": "/api/system/menu/{id}/",
"method": 0 "method": 0
}, },
{ {
"name": "新增", "name": "新增",
"value": "Create", "value": "menu:Create",
"api": "/api/system/menu/", "api": "/api/system/menu/",
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "编辑",
"value": "Update", "value": "menu:Update",
"api": "/api/system/menu/{id}/", "api": "/api/system/menu/{id}/",
"method": 2 "method": 2
}, },
{ {
"name": "删除", "name": "删除",
"value": "Delete", "value": "menu:Delete",
"api": "/api/system/menu/{id}/", "api": "/api/system/menu/{id}/",
"method": 3 "method": 3
} }
@@ -72,30 +72,30 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": false, "visible": false,
"parent": 277, "parent": 19,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "menu_button:Search",
"api": "/api/system/menu_button/", "api": "/api/system/menu_button/",
"method": 0 "method": 0
}, },
{ {
"name": "新增", "name": "新增",
"value": "Create", "value": "menu_button:Create",
"api": "/api/system/menu_button/", "api": "/api/system/menu_button/",
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "编辑",
"value": "Update", "value": "menu_button:Update",
"api": "/api/system/menu_button/{id}/", "api": "/api/system/menu_button/{id}/",
"method": 2 "method": 2
}, },
{ {
"name": "删除", "name": "删除",
"value": "Delete", "value": "menu_button:Delete",
"api": "/api/system/menu_button/{id}/", "api": "/api/system/menu_button/{id}/",
"method": 3 "method": 3
} }
@@ -103,7 +103,7 @@
}, },
{ {
"name": "部门管理", "name": "部门管理",
"icon": "bank", "icon": "ele-OfficeBuilding",
"sort": 3, "sort": 3,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
@@ -113,36 +113,36 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 277, "parent": 19,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "dept:Search",
"api": "/api/system/dept/", "api": "/api/system/dept/",
"method": 0 "method": 0
}, },
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "dept:Retrieve",
"api": "/api/system/dept/{id}/", "api": "/api/system/dept/{id}/",
"method": 0 "method": 0
}, },
{ {
"name": "新增", "name": "新增",
"value": "Create", "value": "dept:Create",
"api": "/api/system/dept/", "api": "/api/system/dept/",
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "编辑",
"value": "Update", "value": "dept:Update",
"api": "/api/system/dept/{id}/", "api": "/api/system/dept/{id}/",
"method": 2 "method": 2
}, },
{ {
"name": "删除", "name": "删除",
"value": "Delete", "value": "dept:Delete",
"api": "/api/system/dept/{id}/", "api": "/api/system/dept/{id}/",
"method": 3 "method": 3
} }
@@ -150,7 +150,7 @@
}, },
{ {
"name": "角色管理", "name": "角色管理",
"icon": "address-book", "icon": "ele-ColdDrink",
"sort": 4, "sort": 4,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
@@ -160,42 +160,42 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 277, "parent": 19,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "role:Search",
"api": "/api/system/role/", "api": "/api/system/role/",
"method": 0 "method": 0
}, },
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "role:Retrieve",
"api": "/api/system/role/{id}/", "api": "/api/system/role/{id}/",
"method": 0 "method": 0
}, },
{ {
"name": "新增", "name": "新增",
"value": "Create", "value": "role:Create",
"api": "/api/system/role/", "api": "/api/system/role/",
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "编辑",
"value": "Update", "value": "role:Update",
"api": "/api/system/role/{id}/", "api": "/api/system/role/{id}/",
"method": 2 "method": 2
}, },
{ {
"name": "保存", "name": "保存",
"value": "Save", "value": "role:Save",
"api": "/api/system/role/{id}/", "api": "/api/system/role/{id}/",
"method": 2 "method": 2
}, },
{ {
"name": "删除", "name": "删除",
"value": "Delete", "value": "role:Delete",
"api": "/api/system/role/{id}/", "api": "/api/system/role/{id}/",
"method": 3 "method": 3
} }
@@ -203,7 +203,7 @@
}, },
{ {
"name": "用户管理", "name": "用户管理",
"icon": "users", "icon": "iconfont icon-icon-",
"sort": 6, "sort": 6,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
@@ -213,60 +213,60 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 277, "parent": 19,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "user:Search",
"api": "/api/system/user/", "api": "/api/system/user/",
"method": 0 "method": 0
}, },
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "user:Retrieve",
"api": "/api/system/user/{id}/", "api": "/api/system/user/{id}/",
"method": 0 "method": 0
}, },
{ {
"name": "新增", "name": "新增",
"value": "Create", "value": "user:Create",
"api": "/api/system/user/", "api": "/api/system/user/",
"method": 1 "method": 1
}, },
{ {
"name": "导出", "name": "导出",
"value": "Export", "value": "user:Export",
"api": "/api/system/user/export/", "api": "/api/system/user/export/",
"method": 1 "method": 1
}, },
{ {
"name": "导入", "name": "导入",
"value": "Import", "value": "user:Import",
"api": "/api/system/user/import/", "api": "/api/system/user/import/",
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "编辑",
"value": "Update", "value": "user:Update",
"api": "/api/system/user/{id}/", "api": "/api/system/user/{id}/",
"method": 2 "method": 2
}, },
{ {
"name": "重设密码", "name": "重设密码",
"value": "ResetPassword", "value": "user:ResetPassword",
"api": "/api/system/user/{id}/reset_password/", "api": "/api/system/user/{id}/reset_password/",
"method": 2 "method": 2
}, },
{ {
"name": "重置密码", "name": "重置密码",
"value": "DefaultPassword", "value": "user:DefaultPassword",
"api": "/api/system/user/{id}/reset_to_default_password/", "api": "/api/system/user/{id}/reset_to_default_password/",
"method": 2 "method": 2
}, },
{ {
"name": "删除", "name": "删除",
"value": "Delete", "value": "user:Delete",
"api": "/api/system/user/{id}/", "api": "/api/system/user/{id}/",
"method": 3 "method": 3
} }
@@ -274,7 +274,7 @@
}, },
{ {
"name": "消息中心", "name": "消息中心",
"icon": "bullhorn", "icon": "iconfont icon-xiaoxizhongxin",
"sort": 7, "sort": 7,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
@@ -284,36 +284,36 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 277, "parent": 19,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "messageCenter:Search",
"api": "/api/system/message_center/", "api": "/api/system/message_center/",
"method": 0 "method": 0
}, },
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "messageCenter:Retrieve",
"api": "/api/system/message_center/{id}/", "api": "/api/system/message_center/{id}/",
"method": 0 "method": 0
}, },
{ {
"name": "新增", "name": "新增",
"value": "Create", "value": "messageCenter:Create",
"api": "/api/system/message_center/", "api": "/api/system/message_center/",
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "编辑",
"value": "Update", "value": "messageCenter:Update",
"api": "/api/system/message_center/{id}/", "api": "/api/system/message_center/{id}/",
"method": 2 "method": 2
}, },
{ {
"name": "删除", "name": "删除",
"value": "Delete", "value": "messageCenter:Delete",
"api": "/api/system/menu/{id}/", "api": "/api/system/menu/{id}/",
"method": 3 "method": 3
} }
@@ -321,7 +321,7 @@
}, },
{ {
"name": "接口白名单", "name": "接口白名单",
"icon": "compass", "icon": "ele-SetUp",
"sort": 8, "sort": 8,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
@@ -331,36 +331,36 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 277, "parent": 19,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "api_white_list:Search",
"api": "/api/system/api_white_list/", "api": "/api/system/api_white_list/",
"method": 0 "method": 0
}, },
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "api_white_list:Retrieve",
"api": "/api/system/api_white_list/{id}/", "api": "/api/system/api_white_list/{id}/",
"method": 0 "method": 0
}, },
{ {
"name": "新增", "name": "新增",
"value": "Create", "value": "api_white_list:Create",
"api": "/api/system/api_white_list/", "api": "/api/system/api_white_list/",
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "编辑",
"value": "Update", "value": "api_white_list:Update",
"api": "/api/system/api_white_list/{id}/", "api": "/api/system/api_white_list/{id}/",
"method": 2 "method": 2
}, },
{ {
"name": "删除", "name": "删除",
"value": "Delete", "value": "api_white_list:Delete",
"api": "/api/system/api_white_list/{id}/", "api": "/api/system/api_white_list/{id}/",
"method": 3 "method": 3
} }
@@ -371,13 +371,13 @@
}, },
{ {
"name": "常规配置", "name": "常规配置",
"icon": "cogs", "icon": "iconfont icon-configure",
"sort": 2, "sort": 2,
"is_link": false, "is_link": false,
"is_catalog": true, "is_catalog": true,
"web_path": "", "web_path": "/generalConfig",
"component": "", "component": "layout/routerView/parent",
"component_name": "", "component_name": "config",
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
@@ -385,7 +385,7 @@
"children": [ "children": [
{ {
"name": "系统配置", "name": "系统配置",
"icon": "desktop", "icon": "iconfont icon-system",
"sort": 0, "sort": 0,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
@@ -395,36 +395,36 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 285, "parent": 27,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "system_config:Search",
"api": "/api/system/system_config/", "api": "/api/system/system_config/",
"method": 0 "method": 0
}, },
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "system_config:Retrieve",
"api": "/api/system/system_config/{id}/", "api": "/api/system/system_config/{id}/",
"method": 0 "method": 0
}, },
{ {
"name": "新增", "name": "新增",
"value": "Create", "value": "system_config:Create",
"api": "/api/system/system_config/", "api": "/api/system/system_config/",
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "编辑",
"value": "Update", "value": "system_config:Update",
"api": "/api/system/system_config/{id}/", "api": "/api/system/system_config/{id}/",
"method": 2 "method": 2
}, },
{ {
"name": "删除", "name": "删除",
"value": "Delete", "value": "system_config:Delete",
"api": "/api/system/system_config/{id}/", "api": "/api/system/system_config/{id}/",
"method": 3 "method": 3
} }
@@ -432,7 +432,7 @@
}, },
{ {
"name": "字典管理", "name": "字典管理",
"icon": "book", "icon": "iconfont icon-dict",
"sort": 1, "sort": 1,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
@@ -442,36 +442,36 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 285, "parent": 27,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "dictionary:Search",
"api": "/api/system/dictionary/", "api": "/api/system/dictionary/",
"method": 0 "method": 0
}, },
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "dictionary:Retrieve",
"api": "/api/system/dictionary/{id}/", "api": "/api/system/dictionary/{id}/",
"method": 0 "method": 0
}, },
{ {
"name": "新增", "name": "新增",
"value": "Create", "value": "dictionary:Create",
"api": "/api/system/dictionary/", "api": "/api/system/dictionary/",
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "编辑",
"value": "Update", "value": "dictionary:Update",
"api": "/api/system/dictionary/{id}/", "api": "/api/system/dictionary/{id}/",
"method": 2 "method": 2
}, },
{ {
"name": "删除", "name": "删除",
"value": "Delete", "value": "dictionary:Delete",
"api": "/api/system/dictionary/{id}/", "api": "/api/system/dictionary/{id}/",
"method": 3 "method": 3
} }
@@ -479,7 +479,7 @@
}, },
{ {
"name": "地区管理", "name": "地区管理",
"icon": "map", "icon": "iconfont icon-Area",
"sort": 2, "sort": 2,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
@@ -489,36 +489,36 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 285, "parent": 27,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "area:Search",
"api": "/api/system/area/", "api": "/api/system/area/",
"method": 0 "method": 0
}, },
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "area:Retrieve",
"api": "/api/system/area/{id}/", "api": "/api/system/area/{id}/",
"method": 0 "method": 0
}, },
{ {
"name": "新增", "name": "新增",
"value": "Create", "value": "area:Create",
"api": "/api/system/area/", "api": "/api/system/area/",
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "编辑",
"value": "Update", "value": "area:Update",
"api": "/api/system/area/{id}/", "api": "/api/system/area/{id}/",
"method": 2 "method": 2
}, },
{ {
"name": "删除", "name": "删除",
"value": "Delete", "value": "area:Delete",
"api": "/api/system/area/{id}/", "api": "/api/system/area/{id}/",
"method": 3 "method": 3
} }
@@ -526,7 +526,7 @@
}, },
{ {
"name": "附件管理", "name": "附件管理",
"icon": "file-text-o", "icon": "iconfont icon-file",
"sort": 3, "sort": 3,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
@@ -536,30 +536,30 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 285, "parent": 27,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "file:Retrieve",
"api": "/api/system/file/{id}/", "api": "/api/system/file/{id}/",
"method": 0 "method": 0
}, },
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "file:Search",
"api": "/api/system/file/", "api": "/api/system/file/",
"method": 0 "method": 0
}, },
{ {
"name": "编辑", "name": "编辑",
"value": "Update", "value": "file:Update",
"api": "/api/system/file/{id}/", "api": "/api/system/file/{id}/",
"method": 1 "method": 1
}, },
{ {
"name": "删除", "name": "删除",
"value": "Delete", "value": "file:Delete",
"api": "/api/system/file/{id}/", "api": "/api/system/file/{id}/",
"method": 3 "method": 3
} }
@@ -570,13 +570,13 @@
}, },
{ {
"name": "日志管理", "name": "日志管理",
"icon": "book", "icon": "iconfont icon-rizhi",
"sort": 3, "sort": 3,
"is_link": false, "is_link": false,
"is_catalog": true, "is_catalog": true,
"web_path": "", "web_path": "/log",
"component": "", "component": "layout/routerView/parent",
"component_name": "", "component_name": "log",
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
@@ -584,7 +584,7 @@
"children": [ "children": [
{ {
"name": "登录日志", "name": "登录日志",
"icon": "file-text", "icon": "iconfont icon-guanlidenglurizhi",
"sort": 1, "sort": 1,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
@@ -594,18 +594,18 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 290, "parent": 32,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "login_log:Search",
"api": "/api/system/login_log/", "api": "/api/system/login_log/",
"method": 0 "method": 0
}, },
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "login_log:Retrieve",
"api": "/api/system/login_log/{id}/", "api": "/api/system/login_log/{id}/",
"method": 0 "method": 0
} }
@@ -613,7 +613,7 @@
}, },
{ {
"name": "操作日志", "name": "操作日志",
"icon": "file-code-o", "icon": "iconfont icon-caozuorizhi",
"sort": 2, "sort": 2,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
@@ -623,56 +623,24 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 290, "parent": 32,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
"name": "详情", "name": "详情",
"value": "Retrieve", "value": "operation_log:Retrieve",
"api": "/api/system/operation_log/{id}/", "api": "/api/system/operation_log/{id}/",
"method": 0 "method": 0
}, },
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "operation_log:Search",
"api": "/api/system/operation_log/", "api": "/api/system/operation_log/",
"method": 0 "method": 0
} }
] ]
},
{
"name": "前端错误日志",
"icon": "bug",
"sort": 4,
"is_link": false,
"is_catalog": false,
"web_path": "/frontendLog",
"component": "system/log/frontendLog/index",
"component_name": "frontendLog",
"status": true,
"cache": false,
"visible": true,
"parent": 290,
"children": [],
"menu_button": []
} }
], ],
"menu_button": [] "menu_button": []
},
{
"name": "DVAdmin官网",
"icon": "external-link",
"sort": 4,
"is_link": true,
"is_catalog": false,
"web_path": "https://django-vue-admin.com",
"component": "",
"component_name": "",
"status": true,
"cache": false,
"visible": true,
"parent": null,
"children": [],
"menu_button": []
} }
] ]

View File

@@ -5,7 +5,6 @@
"sort": 1, "sort": 1,
"status": true, "status": true,
"admin": true, "admin": true,
"data_range": 3,
"remark": null "remark": null
}, },
{ {
@@ -14,7 +13,6 @@
"sort": 2, "sort": 2,
"status": true, "status": true,
"admin": true, "admin": true,
"data_range": 3,
"remark": null "remark": null
} }
] ]

View File

@@ -0,0 +1,12 @@
[
{
"role_key": "admin",
"menu_button_value": "menu:Search",
"data_range": 0
},
{
"role_key": "public",
"menu_button_value":"menu:Search",
"data_range": 0
}
]

View File

@@ -0,0 +1,10 @@
[
{
"role_key": "admin",
"menu_component_name": "menu"
},
{
"role_key": "public",
"menu_component_name": "menu"
}
]

View File

@@ -3,17 +3,14 @@ import os
import django import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "application.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "application.settings")
django.setup() django.setup()
from dvadmin.system.views.user import UsersInitSerializer
from dvadmin.system.views.menu import MenuInitSerializer
from dvadmin.utils.core_initialize import CoreInitialize from dvadmin.utils.core_initialize import CoreInitialize
from dvadmin.system.views.role import RoleInitSerializer from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \
from dvadmin.system.views.api_white_list import ApiWhiteListInitSerializer MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \
from dvadmin.system.views.dept import DeptInitSerializer RoleMenuInitSerializer, RoleMenuButtonInitSerializer
from dvadmin.system.views.dictionary import DictionaryInitSerializer
from dvadmin.system.views.system_config import SystemConfigInitSerializer
class Initialize(CoreInitialize): class Initialize(CoreInitialize):
@@ -42,6 +39,19 @@ class Initialize(CoreInitialize):
""" """
self.init_base(MenuInitSerializer, unique_fields=['name', 'web_path', 'component', 'component_name']) self.init_base(MenuInitSerializer, unique_fields=['name', 'web_path', 'component', 'component_name'])
def init_role_menu(self):
"""
初始化角色菜单信息
"""
self.init_base(RoleMenuInitSerializer, unique_fields=['role', 'menu'])
def init_role_menu_button(self):
"""
初始化角色菜单按钮信息
"""
self.init_base(RoleMenuButtonInitSerializer, unique_fields=['role', 'menu_button'])
def init_api_white_list(self): def init_api_white_list(self):
""" """
初始API白名单 初始API白名单
@@ -65,6 +75,8 @@ class Initialize(CoreInitialize):
self.init_role() self.init_role()
self.init_users() self.init_users()
self.init_menu() self.init_menu()
self.init_role_menu()
self.init_role_menu_button()
self.init_api_white_list() self.init_api_white_list()
self.init_dictionary() self.init_dictionary()
self.init_system_config() self.init_system_config()

View File

@@ -5,20 +5,15 @@ import os
import django import django
from django.db.models import QuerySet from django.db.models import QuerySet
from dvadmin.system.views.system_config import SystemConfigInitSerializer
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
django.setup() django.setup()
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from application.settings import BASE_DIR from application.settings import BASE_DIR
from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig
from dvadmin.system.views.api_white_list import ApiWhiteListInitSerializer from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \
from dvadmin.system.views.dept import DeptInitSerializer MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \
from dvadmin.system.views.dictionary import DictionaryInitSerializer RoleMenuInitSerializer, RoleMenuButtonInitSerializer
from dvadmin.system.views.menu import MenuInitSerializer
from dvadmin.system.views.role import RoleInitSerializer
from dvadmin.system.views.user import UsersInitSerializer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -13,8 +13,9 @@ STATUS_CHOICES = (
) )
class Users(CoreModel,AbstractUser): class Users(CoreModel, AbstractUser):
username = models.CharField(max_length=150, unique=True, db_index=True, verbose_name="用户账号", help_text="用户账号") username = models.CharField(max_length=150, unique=True, db_index=True, verbose_name="用户账号",
help_text="用户账号")
email = models.EmailField(max_length=255, verbose_name="邮箱", null=True, blank=True, help_text="邮箱") email = models.EmailField(max_length=255, verbose_name="邮箱", null=True, blank=True, help_text="邮箱")
mobile = models.CharField(max_length=255, verbose_name="电话", null=True, blank=True, help_text="电话") mobile = models.CharField(max_length=255, verbose_name="电话", null=True, blank=True, help_text="电话")
avatar = models.CharField(max_length=255, verbose_name="头像", null=True, blank=True, help_text="头像") avatar = models.CharField(max_length=255, verbose_name="头像", null=True, blank=True, help_text="头像")
@@ -34,8 +35,10 @@ class Users(CoreModel,AbstractUser):
user_type = models.IntegerField( user_type = models.IntegerField(
choices=USER_TYPE, default=0, verbose_name="用户类型", null=True, blank=True, help_text="用户类型" choices=USER_TYPE, default=0, verbose_name="用户类型", null=True, blank=True, help_text="用户类型"
) )
post = models.ManyToManyField(to="Post",blank=True, verbose_name="关联岗位", db_constraint=False, help_text="关联岗位") post = models.ManyToManyField(to="Post", blank=True, verbose_name="关联岗位", db_constraint=False,
role = models.ManyToManyField(to="Role", blank=True,verbose_name="关联角色", db_constraint=False, help_text="关联角色") help_text="关联岗位")
role = models.ManyToManyField(to="Role", blank=True, verbose_name="关联角色", db_constraint=False,
help_text="关联角色")
dept = models.ForeignKey( dept = models.ForeignKey(
to="Dept", to="Dept",
verbose_name="所属部门", verbose_name="所属部门",
@@ -79,20 +82,6 @@ class Role(CoreModel):
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序") sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
status = models.BooleanField(default=True, verbose_name="角色状态", help_text="角色状态") status = models.BooleanField(default=True, verbose_name="角色状态", help_text="角色状态")
admin = models.BooleanField(default=False, verbose_name="是否为admin", help_text="是否为admin") admin = models.BooleanField(default=False, verbose_name="是否为admin", help_text="是否为admin")
DATASCOPE_CHOICES = (
(0, "仅本人数据权限"),
(1, "本部门及以下数据权限"),
(2, "本部门数据权限"),
(3, "全部数据权限"),
(4, "自定数据权限"),
)
data_range = models.IntegerField(default=0, choices=DATASCOPE_CHOICES, verbose_name="数据权限范围", help_text="数据权限范围")
remark = models.TextField(verbose_name="备注", help_text="备注", null=True, blank=True)
dept = models.ManyToManyField(to="Dept", verbose_name="数据权限-关联部门", db_constraint=False, help_text="数据权限-关联部门")
menu = models.ManyToManyField(to="Menu", verbose_name="关联菜单", db_constraint=False, help_text="关联菜单")
permission = models.ManyToManyField(
to="MenuButton", verbose_name="关联菜单的接口按钮", db_constraint=False, help_text="关联菜单的接口按钮"
)
class Meta: class Meta:
db_table = table_prefix + "system_role" db_table = table_prefix + "system_role"
@@ -103,7 +92,8 @@ class Role(CoreModel):
class Dept(CoreModel): class Dept(CoreModel):
name = models.CharField(max_length=64, verbose_name="部门名称", help_text="部门名称") name = models.CharField(max_length=64, verbose_name="部门名称", help_text="部门名称")
key = models.CharField(max_length=64, unique=True,null=True,blank=True, verbose_name="关联字符", help_text="关联字符") key = models.CharField(max_length=64, unique=True, null=True, blank=True, verbose_name="关联字符",
help_text="关联字符")
sort = models.IntegerField(default=1, verbose_name="显示排序", help_text="显示排序") sort = models.IntegerField(default=1, verbose_name="显示排序", help_text="显示排序")
owner = models.CharField(max_length=32, verbose_name="负责人", null=True, blank=True, help_text="负责人") owner = models.CharField(max_length=32, verbose_name="负责人", null=True, blank=True, help_text="负责人")
phone = models.CharField(max_length=32, verbose_name="联系电话", null=True, blank=True, help_text="联系电话") phone = models.CharField(max_length=32, verbose_name="联系电话", null=True, blank=True, help_text="联系电话")
@@ -121,7 +111,7 @@ class Dept(CoreModel):
) )
@classmethod @classmethod
def recursion_dept_info(cls, dept_id: int, dept_all_list=None, dept_list=None): def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None):
""" """
递归获取部门的所有下级部门 递归获取部门的所有下级部门
:param dept_id: 需要获取的id :param dept_id: 需要获取的id
@@ -136,7 +126,7 @@ class Dept(CoreModel):
for ele in dept_all_list: for ele in dept_all_list:
if ele.get("parent") == dept_id: if ele.get("parent") == dept_id:
dept_list.append(ele.get("id")) dept_list.append(ele.get("id"))
cls.recursion_dept_info(ele.get("id"), dept_all_list, dept_list) cls.recursion_all_dept(ele.get("id"), dept_all_list, dept_list)
return list(set(dept_list)) return list(set(dept_list))
class Meta: class Meta:
@@ -167,10 +157,12 @@ class Menu(CoreModel):
is_catalog = models.BooleanField(default=False, verbose_name="是否目录", help_text="是否目录") is_catalog = models.BooleanField(default=False, verbose_name="是否目录", help_text="是否目录")
web_path = models.CharField(max_length=128, verbose_name="路由地址", null=True, blank=True, help_text="路由地址") web_path = models.CharField(max_length=128, verbose_name="路由地址", null=True, blank=True, help_text="路由地址")
component = models.CharField(max_length=128, verbose_name="组件地址", null=True, blank=True, help_text="组件地址") component = models.CharField(max_length=128, verbose_name="组件地址", null=True, blank=True, help_text="组件地址")
component_name = models.CharField(max_length=50, verbose_name="组件名称", null=True, blank=True, help_text="组件名称") component_name = models.CharField(max_length=50, verbose_name="组件名称", null=True, blank=True,
help_text="组件名称")
status = models.BooleanField(default=True, blank=True, verbose_name="菜单状态", help_text="菜单状态") status = models.BooleanField(default=True, blank=True, verbose_name="菜单状态", help_text="菜单状态")
cache = models.BooleanField(default=False, blank=True, verbose_name="是否页面缓存", help_text="是否页面缓存") cache = models.BooleanField(default=False, blank=True, verbose_name="是否页面缓存", help_text="是否页面缓存")
visible = models.BooleanField(default=True, blank=True, verbose_name="侧边栏中是否显示", help_text="侧边栏中是否显示") visible = models.BooleanField(default=True, blank=True, verbose_name="侧边栏中是否显示",
help_text="侧边栏中是否显示")
class Meta: class Meta:
db_table = table_prefix + "system_menu" db_table = table_prefix + "system_menu"
@@ -189,7 +181,7 @@ class MenuButton(CoreModel):
help_text="关联菜单", help_text="关联菜单",
) )
name = models.CharField(max_length=64, verbose_name="名称", help_text="名称") name = models.CharField(max_length=64, verbose_name="名称", help_text="名称")
value = models.CharField(max_length=64, verbose_name="权限值", help_text="权限值") value = models.CharField(unique=True, max_length=64, verbose_name="权限值", help_text="权限值")
api = models.CharField(max_length=200, verbose_name="接口地址", help_text="接口地址") api = models.CharField(max_length=200, verbose_name="接口地址", help_text="接口地址")
METHOD_CHOICES = ( METHOD_CHOICES = (
(0, "GET"), (0, "GET"),
@@ -197,7 +189,8 @@ class MenuButton(CoreModel):
(2, "PUT"), (2, "PUT"),
(3, "DELETE"), (3, "DELETE"),
) )
method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True, help_text="接口请求方法") method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True,
help_text="接口请求方法")
class Meta: class Meta:
db_table = table_prefix + "system_menu_button" db_table = table_prefix + "system_menu_button"
@@ -206,6 +199,69 @@ class MenuButton(CoreModel):
ordering = ("-name",) ordering = ("-name",)
class RoleMenuPermission(CoreModel):
role = models.ForeignKey(
to="Role",
db_constraint=False,
related_name="role_menu",
on_delete=models.CASCADE,
verbose_name="关联角色",
help_text="关联角色",
)
menu = models.ForeignKey(
to="Menu",
db_constraint=False,
related_name="role_menu",
on_delete=models.CASCADE,
verbose_name="关联菜单",
help_text="关联菜单",
)
class Meta:
db_table = table_prefix + "role_menu_permission"
verbose_name = "角色菜单权限表"
verbose_name_plural = verbose_name
ordering = ("-create_datetime",)
class RoleMenuButtonPermission(CoreModel):
role = models.ForeignKey(
to="Role",
db_constraint=False,
related_name="role_menu_button",
on_delete=models.CASCADE,
verbose_name="关联角色",
help_text="关联角色",
)
menu_button = models.ForeignKey(
to="MenuButton",
db_constraint=False,
related_name="menu_button_permission",
on_delete=models.CASCADE,
verbose_name="关联菜单按钮",
help_text="关联菜单按钮",
null=True,
blank=True
)
DATASCOPE_CHOICES = (
(0, "仅本人数据权限"),
(1, "本部门及以下数据权限"),
(2, "本部门数据权限"),
(3, "全部数据权限"),
(4, "自定数据权限"),
)
data_range = models.IntegerField(default=0, choices=DATASCOPE_CHOICES, verbose_name="数据权限范围",
help_text="数据权限范围")
dept = models.ManyToManyField(to="Dept", blank=True, verbose_name="数据权限-关联部门", db_constraint=False,
help_text="数据权限-关联部门")
class Meta:
db_table = table_prefix + "role_menu_button_permission"
verbose_name = "角色按钮权限表"
verbose_name_plural = verbose_name
ordering = ("-create_datetime",)
class Dictionary(CoreModel): class Dictionary(CoreModel):
TYPE_LIST = ( TYPE_LIST = (
(0, "text"), (0, "text"),
@@ -218,7 +274,8 @@ class Dictionary(CoreModel):
(7, "images"), (7, "images"),
) )
label = models.CharField(max_length=100, blank=True, null=True, verbose_name="字典名称", help_text="字典名称") label = models.CharField(max_length=100, blank=True, null=True, verbose_name="字典名称", help_text="字典名称")
value = models.CharField(max_length=200, blank=True, null=True, verbose_name="字典编号", help_text="字典编号/实际值") value = models.CharField(max_length=200, blank=True, null=True, verbose_name="字典编号",
help_text="字典编号/实际值")
parent = models.ForeignKey( parent = models.ForeignKey(
to="self", to="self",
related_name="sublist", related_name="sublist",
@@ -231,7 +288,8 @@ class Dictionary(CoreModel):
) )
type = models.IntegerField(choices=TYPE_LIST, default=0, verbose_name="数据值类型", help_text="数据值类型") type = models.IntegerField(choices=TYPE_LIST, default=0, verbose_name="数据值类型", help_text="数据值类型")
color = models.CharField(max_length=20, blank=True, null=True, verbose_name="颜色", help_text="颜色") color = models.CharField(max_length=20, blank=True, null=True, verbose_name="颜色", help_text="颜色")
is_value = models.BooleanField(default=False, verbose_name="是否为value值", help_text="是否为value值,用来做具体值存放") is_value = models.BooleanField(default=False, verbose_name="是否为value值",
help_text="是否为value值,用来做具体值存放")
status = models.BooleanField(default=True, verbose_name="状态", help_text="状态") status = models.BooleanField(default=True, verbose_name="状态", help_text="状态")
sort = models.IntegerField(default=1, verbose_name="显示排序", null=True, blank=True, help_text="显示排序") sort = models.IntegerField(default=1, verbose_name="显示排序", null=True, blank=True, help_text="显示排序")
remark = models.CharField(max_length=2000, blank=True, null=True, verbose_name="备注", help_text="备注") remark = models.CharField(max_length=2000, blank=True, null=True, verbose_name="备注", help_text="备注")
@@ -253,14 +311,20 @@ class Dictionary(CoreModel):
class OperationLog(CoreModel): class OperationLog(CoreModel):
request_modular = models.CharField(max_length=64, verbose_name="请求模块", null=True, blank=True, help_text="请求模块") request_modular = models.CharField(max_length=64, verbose_name="请求模块", null=True, blank=True,
request_path = models.CharField(max_length=400, verbose_name="请求地址", null=True, blank=True, help_text="请求地址") help_text="请求模块")
request_path = models.CharField(max_length=400, verbose_name="请求地址", null=True, blank=True,
help_text="请求地址")
request_body = models.TextField(verbose_name="请求参数", null=True, blank=True, help_text="请求参数") request_body = models.TextField(verbose_name="请求参数", null=True, blank=True, help_text="请求参数")
request_method = models.CharField(max_length=8, verbose_name="请求方式", null=True, blank=True, help_text="请求方式") request_method = models.CharField(max_length=8, verbose_name="请求方式", null=True, blank=True,
help_text="请求方式")
request_msg = models.TextField(verbose_name="操作说明", null=True, blank=True, help_text="操作说明") request_msg = models.TextField(verbose_name="操作说明", null=True, blank=True, help_text="操作说明")
request_ip = models.CharField(max_length=32, verbose_name="请求ip地址", null=True, blank=True, help_text="请求ip地址") request_ip = models.CharField(max_length=32, verbose_name="请求ip地址", null=True, blank=True,
request_browser = models.CharField(max_length=64, verbose_name="请求浏览器", null=True, blank=True, help_text="请求浏览器") help_text="请求ip地址")
response_code = models.CharField(max_length=32, verbose_name="响应状态码", null=True, blank=True, help_text="响应状态码") request_browser = models.CharField(max_length=64, verbose_name="请求浏览器", null=True, blank=True,
help_text="请求浏览器")
response_code = models.CharField(max_length=32, verbose_name="响应状态码", null=True, blank=True,
help_text="响应状态码")
request_os = models.CharField(max_length=64, verbose_name="操作系统", null=True, blank=True, help_text="操作系统") request_os = models.CharField(max_length=64, verbose_name="操作系统", null=True, blank=True, help_text="操作系统")
json_result = models.TextField(verbose_name="返回信息", null=True, blank=True, help_text="返回信息") json_result = models.TextField(verbose_name="返回信息", null=True, blank=True, help_text="返回信息")
status = models.BooleanField(default=False, verbose_name="响应状态", help_text="响应状态") status = models.BooleanField(default=False, verbose_name="响应状态", help_text="响应状态")
@@ -301,7 +365,8 @@ class FileList(CoreModel):
class Area(CoreModel): class Area(CoreModel):
name = models.CharField(max_length=100, verbose_name="名称", help_text="名称") name = models.CharField(max_length=100, verbose_name="名称", help_text="名称")
code = models.CharField(max_length=20, verbose_name="地区编码", help_text="地区编码", unique=True, db_index=True) code = models.CharField(max_length=20, verbose_name="地区编码", help_text="地区编码", unique=True, db_index=True)
level = models.BigIntegerField(verbose_name="地区层级(1省份 2城市 3区县 4乡级)", help_text="地区层级(1省份 2城市 3区县 4乡级)") level = models.BigIntegerField(verbose_name="地区层级(1省份 2城市 3区县 4乡级)",
help_text="地区层级(1省份 2城市 3区县 4乡级)")
pinyin = models.CharField(max_length=255, verbose_name="拼音", help_text="拼音") pinyin = models.CharField(max_length=255, verbose_name="拼音", help_text="拼音")
initials = models.CharField(max_length=20, verbose_name="首字母", help_text="首字母") initials = models.CharField(max_length=20, verbose_name="首字母", help_text="首字母")
enable = models.BooleanField(default=True, verbose_name="是否启用", help_text="是否启用") enable = models.BooleanField(default=True, verbose_name="是否启用", help_text="是否启用")
@@ -334,8 +399,10 @@ class ApiWhiteList(CoreModel):
(2, "PUT"), (2, "PUT"),
(3, "DELETE"), (3, "DELETE"),
) )
method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True, help_text="接口请求方法") method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True,
enable_datasource = models.BooleanField(default=True, verbose_name="激活数据权限", help_text="激活数据权限", blank=True) help_text="接口请求方法")
enable_datasource = models.BooleanField(default=True, verbose_name="激活数据权限", help_text="激活数据权限",
blank=True)
class Meta: class Meta:
db_table = table_prefix + "api_white_list" db_table = table_prefix + "api_white_list"
@@ -419,11 +486,13 @@ class LoginLog(CoreModel):
district = models.CharField(max_length=50, verbose_name="县区", null=True, blank=True, help_text="县区") district = models.CharField(max_length=50, verbose_name="县区", null=True, blank=True, help_text="县区")
isp = models.CharField(max_length=50, verbose_name="运营商", null=True, blank=True, help_text="运营商") isp = models.CharField(max_length=50, verbose_name="运营商", null=True, blank=True, help_text="运营商")
area_code = models.CharField(max_length=50, verbose_name="区域代码", null=True, blank=True, help_text="区域代码") area_code = models.CharField(max_length=50, verbose_name="区域代码", null=True, blank=True, help_text="区域代码")
country_english = models.CharField(max_length=50, verbose_name="英文全称", null=True, blank=True, help_text="英文全称") country_english = models.CharField(max_length=50, verbose_name="英文全称", null=True, blank=True,
help_text="英文全称")
country_code = models.CharField(max_length=50, verbose_name="简称", null=True, blank=True, help_text="简称") country_code = models.CharField(max_length=50, verbose_name="简称", null=True, blank=True, help_text="简称")
longitude = models.CharField(max_length=50, verbose_name="经度", null=True, blank=True, help_text="经度") longitude = models.CharField(max_length=50, verbose_name="经度", null=True, blank=True, help_text="经度")
latitude = models.CharField(max_length=50, verbose_name="纬度", null=True, blank=True, help_text="纬度") latitude = models.CharField(max_length=50, verbose_name="纬度", null=True, blank=True, help_text="纬度")
login_type = models.IntegerField(default=1, choices=LOGIN_TYPE_CHOICES, verbose_name="登录类型", help_text="登录类型") login_type = models.IntegerField(default=1, choices=LOGIN_TYPE_CHOICES, verbose_name="登录类型",
help_text="登录类型")
class Meta: class Meta:
db_table = table_prefix + "system_login_log" db_table = table_prefix + "system_login_log"
@@ -433,14 +502,16 @@ class LoginLog(CoreModel):
class MessageCenter(CoreModel): class MessageCenter(CoreModel):
title = models.CharField(max_length=100,verbose_name="标题",help_text="标题") title = models.CharField(max_length=100, verbose_name="标题", help_text="标题")
content = models.TextField(verbose_name="内容",help_text="内容") content = models.TextField(verbose_name="内容", help_text="内容")
target_type=models.IntegerField(default=0,verbose_name="目标类型",help_text="目标类型") target_type = models.IntegerField(default=0, verbose_name="目标类型", help_text="目标类型")
target_user = models.ManyToManyField(to=Users,related_name='user',through='MessageCenterTargetUser', through_fields=('messagecenter','users'),blank=True,verbose_name="目标用户",help_text="目标用户") target_user = models.ManyToManyField(to=Users, related_name='user', through='MessageCenterTargetUser',
target_dept = models.ManyToManyField(to=Dept, blank=True, db_constraint=False, through_fields=('messagecenter', 'users'), blank=True, verbose_name="目标用户",
verbose_name="目标部门", help_text="目标部门") help_text="目标用户")
target_role = models.ManyToManyField(to=Role, blank=True, db_constraint=False, target_dept = models.ManyToManyField(to=Dept, blank=True, db_constraint=False,
verbose_name="目标角色", help_text="目标角色") verbose_name="目标部门", help_text="目标部门")
target_role = models.ManyToManyField(to=Role, blank=True, db_constraint=False,
verbose_name="目标角色", help_text="目标角色")
class Meta: class Meta:
db_table = table_prefix + "message_center" db_table = table_prefix + "message_center"
@@ -448,12 +519,15 @@ class MessageCenter(CoreModel):
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
ordering = ("-create_datetime",) ordering = ("-create_datetime",)
class MessageCenterTargetUser(CoreModel): class MessageCenterTargetUser(CoreModel):
users = models.ForeignKey(Users,related_name="target_user", on_delete=models.CASCADE,db_constraint=False,verbose_name="关联用户表",help_text="关联用户表") users = models.ForeignKey(Users, related_name="target_user", on_delete=models.CASCADE, db_constraint=False,
messagecenter = models.ForeignKey(MessageCenter, on_delete=models.CASCADE,db_constraint=False,verbose_name="关联消息中心",help_text="关联消息中心") verbose_name="关联用户", help_text="关联用户")
is_read = models.BooleanField(default=False,blank=True,null=True,verbose_name="是否已读",help_text="是否已读") messagecenter = models.ForeignKey(MessageCenter, on_delete=models.CASCADE, db_constraint=False,
verbose_name="关联消息中心表", help_text="关联消息中心表")
is_read = models.BooleanField(default=False, blank=True, null=True, verbose_name="是否已读", help_text="是否已读")
class Meta: class Meta:
db_table = table_prefix + "message_center_target_user" db_table = table_prefix + "message_center_target_user"
verbose_name = "消息中心目标用户表" verbose_name = "消息中心目标用户表"
verbose_name_plural = verbose_name verbose_name_plural = verbose_name

View File

@@ -0,0 +1 @@
from django.test import TestCase

View File

@@ -12,6 +12,8 @@ from dvadmin.system.views.menu_button import MenuButtonViewSet
from dvadmin.system.views.message_center import MessageCenterViewSet from dvadmin.system.views.message_center import MessageCenterViewSet
from dvadmin.system.views.operation_log import OperationLogViewSet from dvadmin.system.views.operation_log import OperationLogViewSet
from dvadmin.system.views.role import RoleViewSet from dvadmin.system.views.role import RoleViewSet
from dvadmin.system.views.role_menu import RoleMenuPermissionViewSet
from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermissionViewSet
from dvadmin.system.views.system_config import SystemConfigViewSet from dvadmin.system.views.system_config import SystemConfigViewSet
from dvadmin.system.views.user import UserViewSet from dvadmin.system.views.user import UserViewSet
@@ -28,6 +30,11 @@ system_url.register(r'file', FileViewSet)
system_url.register(r'api_white_list', ApiWhiteListViewSet) system_url.register(r'api_white_list', ApiWhiteListViewSet)
system_url.register(r'system_config', SystemConfigViewSet) system_url.register(r'system_config', SystemConfigViewSet)
system_url.register(r'message_center',MessageCenterViewSet) system_url.register(r'message_center',MessageCenterViewSet)
system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionViewSet)
system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet)
urlpatterns = [ urlpatterns = [
path('user/export/', UserViewSet.as_view({'post': 'export_data', })), path('user/export/', UserViewSet.as_view({'post': 'export_data', })),

View File

@@ -22,19 +22,7 @@ class ApiWhiteListSerializer(CustomModelSerializer):
read_only_fields = ["id"] read_only_fields = ["id"]
class ApiWhiteListInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
class Meta:
model = ApiWhiteList
fields = ['url', 'method', 'enable_datasource', 'creator', 'dept_belong_id']
read_only_fields = ["id"]
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class ApiWhiteListViewSet(CustomModelViewSet): class ApiWhiteListViewSet(CustomModelViewSet):

View File

@@ -13,10 +13,14 @@ class AreaSerializer(CustomModelSerializer):
地区-序列化器 地区-序列化器
""" """
pcode_count = serializers.SerializerMethodField(read_only=True) pcode_count = serializers.SerializerMethodField(read_only=True)
hasChild = serializers.SerializerMethodField()
def get_pcode_count(self, instance: Area): def get_pcode_count(self, instance: Area):
return Area.objects.filter(pcode=instance).count() return Area.objects.filter(pcode=instance).count()
def get_hasChild(self, instance):
hasChild = Area.objects.filter(pcode=instance.code)
if hasChild:
return True
return False
class Meta: class Meta:
model = Area model = Area
fields = "__all__" fields = "__all__"
@@ -44,4 +48,24 @@ class AreaViewSet(CustomModelViewSet):
""" """
queryset = Area.objects.all() queryset = Area.objects.all()
serializer_class = AreaSerializer serializer_class = AreaSerializer
extra_filter_backends = [] extra_filter_class = []
def get_queryset(self):
self.request.query_params._mutable = True
params = self.request.query_params
pcode = params.get('pcode', None)
page = params.get('page', None)
limit = params.get('limit', None)
if page:
del params['page']
if limit:
del params['limit']
if params:
if pcode:
queryset = self.queryset.filter(enable=True, pcode=pcode)
else:
queryset = self.queryset.filter(enable=True)
else:
queryset = self.queryset.filter(enable=True, pcode__isnull=True)
return queryset

View File

@@ -7,6 +7,7 @@
""" """
from rest_framework import serializers from rest_framework import serializers
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Dept from dvadmin.system.models import Dept
from dvadmin.utils.json_response import DetailResponse, SuccessResponse from dvadmin.utils.json_response import DetailResponse, SuccessResponse
@@ -55,49 +56,7 @@ class DeptImportSerializer(CustomModelSerializer):
read_only_fields = ["id"] read_only_fields = ["id"]
class DeptInitSerializer(CustomModelSerializer):
"""
递归深度获取数信息(用于生成初始化json文件)
"""
children = serializers.SerializerMethodField()
def get_children(self, obj: Dept):
data = []
instance = Dept.objects.filter(parent_id=obj.id)
if instance:
serializer = DeptInitSerializer(instance=instance, many=True)
data = serializer.data
return data
def save(self, **kwargs):
instance = super().save(**kwargs)
children = self.initial_data.get('children')
if children:
for menu_data in children:
menu_data['parent'] = instance.id
filter_data = {
"name": menu_data['name'],
"parent": menu_data['parent'],
"key": menu_data['key']
}
instance_obj = Dept.objects.filter(**filter_data).first()
if instance_obj and not self.initial_data.get('reset'):
continue
serializer = DeptInitSerializer(instance_obj, data=menu_data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
return instance
class Meta:
model = Dept
fields = ['name', 'sort', 'owner', 'phone', 'email', 'status', 'parent', 'creator', 'dept_belong_id',
'children', 'key']
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
read_only_fields = ['id', 'children']
class DeptCreateUpdateSerializer(CustomModelSerializer): class DeptCreateUpdateSerializer(CustomModelSerializer):
@@ -134,7 +93,7 @@ class DeptViewSet(CustomModelViewSet):
update_serializer_class = DeptCreateUpdateSerializer update_serializer_class = DeptCreateUpdateSerializer
filter_fields = ['name', 'id', 'parent'] filter_fields = ['name', 'id', 'parent']
search_fields = [] search_fields = []
# extra_filter_backends = [] # extra_filter_class = []
import_serializer_class = DeptImportSerializer import_serializer_class = DeptImportSerializer
import_field_dict = { import_field_dict = {
"name": "部门名称", "name": "部门名称",
@@ -143,8 +102,15 @@ class DeptViewSet(CustomModelViewSet):
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
# 如果懒加载,则只返回父级 # 如果懒加载,则只返回父级
request.query_params._mutable = True
params = request.query_params params = request.query_params
parent = params.get('parent', None) parent = params.get('parent', None)
page = params.get('page', None)
limit = params.get('limit', None)
if page:
del params['page']
if limit:
del params['limit']
if params: if params:
if parent: if parent:
queryset = self.queryset.filter(status=True, parent=parent) queryset = self.queryset.filter(status=True, parent=parent)
@@ -182,7 +148,7 @@ class DeptViewSet(CustomModelViewSet):
return DetailResponse(data=queryset, msg="获取成功") return DetailResponse(data=queryset, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[AnonymousUserPermission]) @action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated],extra_filter_class=[])
def all_dept(self, request, *args, **kwargs): def all_dept(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())
data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent') data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent')

View File

@@ -27,48 +27,7 @@ class DictionarySerializer(CustomModelSerializer):
read_only_fields = ["id"] read_only_fields = ["id"]
class DictionaryInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
children = serializers.SerializerMethodField()
def get_children(self, obj: Dictionary):
data = []
instance = Dictionary.objects.filter(parent_id=obj.id)
if instance:
serializer = DictionaryInitSerializer(instance=instance, many=True)
data = serializer.data
return data
def save(self, **kwargs):
instance = super().save(**kwargs)
children = self.initial_data.get('children')
# 菜单表
if children:
for data in children:
data['parent'] = instance.id
filter_data = {
"value": data['value'],
"parent": data['parent']
}
instance_obj = Dictionary.objects.filter(**filter_data).first()
if instance_obj and not self.initial_data.get('reset'):
continue
serializer = DictionaryInitSerializer(instance_obj, data=data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
return instance
class Meta:
model = Dictionary
fields = ['label', 'value', 'parent', 'type', 'color', 'is_value', 'status', 'sort', 'remark', 'creator',
'dept_belong_id', 'children']
read_only_fields = ["id"]
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class DictionaryCreateUpdateSerializer(CustomModelSerializer): class DictionaryCreateUpdateSerializer(CustomModelSerializer):
@@ -92,9 +51,21 @@ class DictionaryViewSet(CustomModelViewSet):
""" """
queryset = Dictionary.objects.all() queryset = Dictionary.objects.all()
serializer_class = DictionarySerializer serializer_class = DictionarySerializer
extra_filter_backends = [] extra_filter_class = []
search_fields = ['label'] search_fields = ['label']
def get_queryset(self):
params = self.request.query_params
parent = params.get('parent', None)
if params:
if parent:
queryset = self.queryset.filter(status=1, parent=parent)
else:
queryset = self.queryset.filter(status=1, parent__isnull=True)
else:
queryset = self.queryset.filter(status=1, parent__isnull=True)
return queryset
class InitDictionaryViewSet(APIView): class InitDictionaryViewSet(APIView):
""" """

View File

@@ -5,6 +5,7 @@ from datetime import datetime, timedelta
from captcha.views import CaptchaStore, captcha_image from captcha.views import CaptchaStore, captcha_image
from django.contrib import auth from django.contrib import auth
from django.contrib.auth import login from django.contrib.auth import login
from django.contrib.auth.hashers import make_password, check_password
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi from drf_yasg import openapi
@@ -54,11 +55,9 @@ class LoginSerializer(TokenObtainPairSerializer):
登录的序列化器: 登录的序列化器:
重写djangorestframework-simplejwt的序列化器 重写djangorestframework-simplejwt的序列化器
""" """
captcha = serializers.CharField( captcha = serializers.CharField(
max_length=6, required=False, allow_null=True, allow_blank=True max_length=6, required=False, allow_null=True, allow_blank=True
) )
class Meta: class Meta:
model = Users model = Users
fields = "__all__" fields = "__all__"
@@ -91,12 +90,13 @@ class LoginSerializer(TokenObtainPairSerializer):
data["name"] = self.user.name data["name"] = self.user.name
data["userId"] = self.user.id data["userId"] = self.user.id
data["avatar"] = self.user.avatar data["avatar"] = self.user.avatar
data['user_type'] = self.user.user_type
dept = getattr(self.user, 'dept', None) dept = getattr(self.user, 'dept', None)
if dept: if dept:
data['dept_info'] = { data['dept_info'] = {
'dept_id': dept.id, 'dept_id': dept.id,
'dept_name': dept.name, 'dept_name': dept.name,
'dept_key': dept.key
} }
role = getattr(self.user, 'role', None) role = getattr(self.user, 'role', None)
if role: if role:
@@ -107,15 +107,78 @@ class LoginSerializer(TokenObtainPairSerializer):
save_login_log(request=request) save_login_log(request=request)
return {"code": 2000, "msg": "请求成功", "data": data} return {"code": 2000, "msg": "请求成功", "data": data}
class LoginView(TokenObtainPairView): class LoginView(TokenObtainPairView):
""" """
登录接口 登录接口
""" """
serializer_class = LoginSerializer serializer_class = LoginSerializer
permission_classes = [] permission_classes = []
# def post(self, request, *args, **kwargs):
# # username可能携带的不止是用户名可能还是用户的其它唯一标识 手机号 邮箱
# username = request.data.get('username',None)
# if username is None:
# return ErrorResponse(msg="参数错误")
# password = request.data.get('password',None)
# if password is None:
# return ErrorResponse(msg="参数错误")
# captcha = request.data.get('captcha',None)
# if captcha is None:
# return ErrorResponse(msg="参数错误")
# captchaKey = request.data.get('captchaKey',None)
# if captchaKey is None:
# return ErrorResponse(msg="参数错误")
# if dispatch.get_system_config_values("base.captcha_state"):
# if captcha is None:
# raise CustomValidationError("验证码不能为空")
# self.image_code = CaptchaStore.objects.filter(
# id=captchaKey
# ).first()
# five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
# if self.image_code and five_minute_ago > self.image_code.expiration:
# self.image_code and self.image_code.delete()
# raise CustomValidationError("验证码过期")
# else:
# if self.image_code and (
# self.image_code.response == captcha
# or self.image_code.challenge == captcha
# ):
# self.image_code and self.image_code.delete()
# else:
# self.image_code and self.image_code.delete()
# raise CustomValidationError("图片验证码错误")
# try:
# # 手动通过 user 签发 jwt-token
# user = Users.objects.get(username=username)
# except:
# return DetailResponse(msg='该账号未注册')
# # 获得用户后校验密码并签发token
# print(make_password(password),user.password)
# if check_password(make_password(password),user.password):
# return DetailResponse(msg='密码错误')
# result = {
# "name":user.name,
# "userId":user.id,
# "avatar":user.avatar,
# }
# dept = getattr(user, 'dept', None)
# if dept:
# result['dept_info'] = {
# 'dept_id': dept.id,
# 'dept_name': dept.name,
# 'dept_key': dept.key
# }
# role = getattr(user, 'role', None)
# if role:
# result['role_info'] = role.values('id', 'name', 'key')
# refresh = LoginSerializer.get_token(user)
# result["refresh"] = str(refresh)
# result["access"] = str(refresh.access_token)
# # 记录登录日志
# request.user = user
# save_login_log(request=request)
# return DetailResponse(data=result,msg="获取成功")
class LoginTokenSerializer(TokenObtainPairSerializer): class LoginTokenSerializer(TokenObtainPairSerializer):
""" """

View File

@@ -33,4 +33,4 @@ class LoginLogViewSet(CustomModelViewSet):
""" """
queryset = LoginLog.objects.all() queryset = LoginLog.objects.all()
serializer_class = LoginLogSerializer serializer_class = LoginLogSerializer
extra_filter_backends = [] extra_filter_class = []

View File

@@ -9,8 +9,7 @@
from rest_framework import serializers from rest_framework import serializers
from rest_framework.decorators import action from rest_framework.decorators import action
from dvadmin.system.models import Menu, MenuButton from dvadmin.system.models import Menu, MenuButton, RoleMenuPermission
from dvadmin.system.views.menu_button import MenuButtonInitSerializer
from dvadmin.utils.json_response import SuccessResponse from dvadmin.utils.json_response import SuccessResponse
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
@@ -54,72 +53,7 @@ class MenuCreateSerializer(CustomModelSerializer):
read_only_fields = ["id"] read_only_fields = ["id"]
class MenuInitSerializer(CustomModelSerializer):
"""
递归深度获取数信息(用于生成初始化json文件)
"""
name = serializers.CharField(required=False)
children = serializers.SerializerMethodField()
menu_button = serializers.SerializerMethodField()
def get_children(self, obj: Menu):
data = []
instance = Menu.objects.filter(parent_id=obj.id)
if instance:
serializer = MenuInitSerializer(instance=instance, many=True)
data = serializer.data
return data
def get_menu_button(self, obj: Menu):
data = []
instance = obj.menuPermission.order_by('method')
if instance:
data = list(instance.values('name', 'value', 'api', 'method'))
return data
def save(self, **kwargs):
instance = super().save(**kwargs)
children = self.initial_data.get('children')
menu_button = self.initial_data.get('menu_button')
# 菜单表
if children:
for menu_data in children:
menu_data['parent'] = instance.id
filter_data = {
"name": menu_data['name'],
"web_path": menu_data['web_path'],
"component": menu_data['component'],
"component_name": menu_data['component_name'],
}
instance_obj = Menu.objects.filter(**filter_data).first()
if instance_obj and not self.initial_data.get('reset'):
continue
serializer = MenuInitSerializer(instance_obj, data=menu_data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
# 菜单按钮
if menu_button:
for menu_button_data in menu_button:
menu_button_data['menu'] = instance.id
filter_data = {
"menu": menu_button_data['menu'],
"value": menu_button_data['value']
}
instance_obj = MenuButton.objects.filter(**filter_data).first()
serializer = MenuButtonInitSerializer(instance_obj, data=menu_button_data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
return instance
class Meta:
model = Menu
fields = ['name', 'icon', 'sort', 'is_link', 'is_catalog', 'web_path', 'component', 'component_name', 'status',
'cache', 'visible', 'parent', 'children', 'menu_button', 'creator', 'dept_belong_id']
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
read_only_fields = ['id', 'children']
class WebRouterSerializer(CustomModelSerializer): class WebRouterSerializer(CustomModelSerializer):
@@ -128,25 +62,12 @@ class WebRouterSerializer(CustomModelSerializer):
""" """
path = serializers.CharField(source="web_path") path = serializers.CharField(source="web_path")
title = serializers.CharField(source="name") title = serializers.CharField(source="name")
menuPermission = serializers.SerializerMethodField(read_only=True)
def get_menuPermission(self, instance):
# 判断是否是超级管理员
if self.request.user.is_superuser:
return instance.menuPermission.values_list('value', flat=True)
else:
# 根据当前角色获取权限按钮id集合
permissionIds = self.request.user.role.values_list('permission', flat=True)
queryset = instance.menuPermission.filter(id__in=permissionIds, menu=instance.id).values_list('value', flat=True)
if queryset:
return queryset
else:
return None
class Meta: class Meta:
model = Menu model = Menu
fields = ('id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component', fields = ('id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component',
'component_name', 'cache', 'visible', 'menuPermission') 'component_name', 'cache', 'visible')
read_only_fields = ["id"] read_only_fields = ["id"]
@@ -165,7 +86,7 @@ class MenuViewSet(CustomModelViewSet):
update_serializer_class = MenuCreateSerializer update_serializer_class = MenuCreateSerializer
search_fields = ['name', 'status'] search_fields = ['name', 'status']
filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'is_catalog'] filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'is_catalog']
# extra_filter_backends = [] # extra_filter_class = []
@action(methods=['GET'], detail=False, permission_classes=[]) @action(methods=['GET'], detail=False, permission_classes=[])
def web_router(self, request): def web_router(self, request):
@@ -173,16 +94,24 @@ class MenuViewSet(CustomModelViewSet):
user = request.user user = request.user
queryset = self.queryset.filter(status=1) queryset = self.queryset.filter(status=1)
if not user.is_superuser: if not user.is_superuser:
menuIds = user.role.values_list('menu__id', flat=True) role_list = user.role.values_list('id', flat=True)
queryset = Menu.objects.filter(id__in=menuIds, status=1) menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id')
queryset = Menu.objects.filter(id__in=menu_list)
serializer = WebRouterSerializer(queryset, many=True, request=request) serializer = WebRouterSerializer(queryset, many=True, request=request)
data = serializer.data data = serializer.data
return SuccessResponse(data=data, total=len(data), msg="获取成功") return SuccessResponse(data=data, total=len(data), msg="获取成功")
def list(self,request): def list(self,request):
"""懒加载""" """懒加载"""
request.query_params._mutable = True
params = request.query_params params = request.query_params
parent = params.get('parent', None) parent = params.get('parent', None)
page = params.get('page',None)
limit = params.get('limit', None)
if page:
del params['page']
if limit:
del params['limit']
if params: if params:
if parent: if parent:
queryset = self.queryset.filter(status=1, parent=parent) queryset = self.queryset.filter(status=1, parent=parent)

View File

@@ -6,7 +6,12 @@
@Created on: 2021/6/3 003 0:30 @Created on: 2021/6/3 003 0:30
@Remark: 菜单按钮管理 @Remark: 菜单按钮管理
""" """
from dvadmin.system.models import MenuButton from django.db.models import F
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import MenuButton, RoleMenuButtonPermission
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
@@ -22,15 +27,7 @@ class MenuButtonSerializer(CustomModelSerializer):
read_only_fields = ["id"] read_only_fields = ["id"]
class MenuButtonInitSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化器
"""
class Meta:
model = MenuButton
fields = ['id', 'name', 'value', 'api', 'method', 'menu']
read_only_fields = ["id"]
class MenuButtonCreateUpdateSerializer(CustomModelSerializer): class MenuButtonCreateUpdateSerializer(CustomModelSerializer):
""" """
@@ -56,4 +53,20 @@ class MenuButtonViewSet(CustomModelViewSet):
serializer_class = MenuButtonSerializer serializer_class = MenuButtonSerializer
create_serializer_class = MenuButtonCreateUpdateSerializer create_serializer_class = MenuButtonCreateUpdateSerializer
update_serializer_class = MenuButtonCreateUpdateSerializer update_serializer_class = MenuButtonCreateUpdateSerializer
extra_filter_backends = [] extra_filter_class = []
@action(methods=['get'],detail=False,permission_classes=[IsAuthenticated])
def menu_button_all_permission(self,request):
"""
获取所有的按钮权限
:param request:
:return:
"""
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
queryset = MenuButton.objects.values_list('value',flat=True)
else:
role_id = request.user.role.values_list('id', flat=True)
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_id).values_list('menu_button__value',flat=True).distinct()
return DetailResponse(data=queryset)

View File

@@ -86,7 +86,9 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
user_id = self.request.user.id user_id = self.request.user.id
message_center_id = instance.id message_center_id = instance.id
queryset = MessageCenterTargetUser.objects.filter(messagecenter__id=message_center_id,users_id=user_id).first() queryset = MessageCenterTargetUser.objects.filter(messagecenter__id=message_center_id,users_id=user_id).first()
return queryset.is_read if queryset:
return queryset.is_read
return False
class Meta: class Meta:
model = MessageCenter model = MessageCenter
@@ -121,12 +123,12 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
users = initial_data.get('target_user', []) users = initial_data.get('target_user', [])
if target_type in [1]: # 按角色 if target_type in [1]: # 按角色
target_role = initial_data.get('target_role',[]) target_role = initial_data.get('target_role',[])
users = Users.objects.exclude(is_deleted=True).filter(role__id__in=target_role).values_list('id', flat=True) users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True)
if target_type in [2]: # 按部门 if target_type in [2]: # 按部门
target_dept = initial_data.get('target_dept',[]) target_dept = initial_data.get('target_dept',[])
users = Users.objects.exclude(is_deleted=True).filter(dept__id__in=target_dept).values_list('id', flat=True) users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True)
if target_type in [3]: # 系统通知 if target_type in [3]: # 系统通知
users = Users.objects.exclude(is_deleted=True).values_list('id', flat=True) users = Users.objects.values_list('id', flat=True)
targetuser_data = [] targetuser_data = []
for user in users: for user in users:
targetuser_data.append({ targetuser_data.append({
@@ -211,6 +213,6 @@ class MessageCenterViewSet(CustomModelViewSet):
queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('create_datetime').last() queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('create_datetime').last()
data = None data = None
if queryset: if queryset:
serializer = MessageCenterTargetUserListSerializer(queryset, many=False, request=request) serializer = MessageCenterTargetUserListSerializer(queryset.messagecenter, many=False, request=request)
data = serializer.data data = serializer.data
return DetailResponse(data=data, msg="获取成功") return DetailResponse(data=data, msg="获取成功")

View File

@@ -14,6 +14,7 @@ from dvadmin.system.models import Role, Menu, MenuButton, Dept
from dvadmin.system.views.dept import DeptSerializer from dvadmin.system.views.dept import DeptSerializer
from dvadmin.system.views.menu import MenuSerializer from dvadmin.system.views.menu import MenuSerializer
from dvadmin.system.views.menu_button import MenuButtonSerializer from dvadmin.system.views.menu_button import MenuButtonSerializer
from dvadmin.utils.crud_mixin import FastCrudMixin
from dvadmin.utils.json_response import SuccessResponse, DetailResponse from dvadmin.utils.json_response import SuccessResponse, DetailResponse
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator from dvadmin.utils.validator import CustomUniqueValidator
@@ -31,20 +32,7 @@ class RoleSerializer(CustomModelSerializer):
read_only_fields = ["id"] read_only_fields = ["id"]
class RoleInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
class Meta:
model = Role
fields = ['name', 'key', 'sort', 'status', 'admin', 'data_range', 'remark',
'creator', 'dept_belong_id']
read_only_fields = ["id"]
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class RoleCreateUpdateSerializer(CustomModelSerializer): class RoleCreateUpdateSerializer(CustomModelSerializer):
@@ -66,9 +54,6 @@ class RoleCreateUpdateSerializer(CustomModelSerializer):
if not is_superuser: if not is_superuser:
self.validated_data.pop('admin') self.validated_data.pop('admin')
data = super().save(**kwargs) data = super().save(**kwargs)
data.dept.set(self.initial_data.get('dept', []))
data.menu.set(self.initial_data.get('menu', []))
data.permission.set(self.initial_data.get('permission', []))
return data return data
class Meta: class Meta:
@@ -97,7 +82,7 @@ class MenuPermissonSerializer(CustomModelSerializer):
fields = ['id', 'parent', 'name', 'menuPermission'] fields = ['id', 'parent', 'name', 'menuPermission']
class RoleViewSet(CustomModelViewSet): class RoleViewSet(CustomModelViewSet,FastCrudMixin):
""" """
角色管理接口 角色管理接口
list:查询 list:查询
@@ -111,107 +96,3 @@ class RoleViewSet(CustomModelViewSet):
create_serializer_class = RoleCreateUpdateSerializer create_serializer_class = RoleCreateUpdateSerializer
update_serializer_class = RoleCreateUpdateSerializer update_serializer_class = RoleCreateUpdateSerializer
search_fields = ['name', 'key'] search_fields = ['name', 'key']
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def role_get_menu(self, request):
"""根据当前用户的角色返回角色拥有的菜单"""
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin',flat=True)
if is_superuser or True in is_admin:
queryset = Menu.objects.filter(status=1).all()
else:
menu_id_list = request.user.role.values_list('menu',flat=True)
queryset = Menu.objects.filter(id__in=menu_id_list)
# queryset = self.filter_queryset(queryset)
serializer = MenuPermissonSerializer(queryset, many=True,request=request)
return DetailResponse(data=serializer.data)
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def data_scope(self, request):
is_superuser = request.user.is_superuser
role_queryset = Role.objects.filter(users__id=request.user.id).values_list('data_range', flat=True)
if is_superuser:
data = [
{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
},
{
"value": 3,
"label": '全部数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}
]
else:
data = []
data_range_list = list(set(role_queryset))
for item in data_range_list:
if item == 0:
data = [{
"value": 0,
"label": '仅本人数据权限'
}]
elif item == 1:
data = [{
"value": 0,
"label": '仅本人数据权限'
}, {
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 2:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 3:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 3,
"label": '全部数据权限'
}, ]
elif item == 4:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}]
else:
data = []
return DetailResponse(data=data)
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def data_scope_dept(self,request):
"""根据当前角色获取部门信息"""
is_superuser = request.user.is_superuser
if is_superuser:
queryset = Dept.objects.values('id','name','parent')
else:
dept_list = request.user.role.values_list('dept',flat=True)
queryset = Dept.objects.filter(id__in=dept_list).values('id','name','parent')
return DetailResponse(data=queryset)

View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
from django.db.models import F
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import RoleMenuPermission, Menu, MenuButton
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
class RoleMenuPermissionSerializer(CustomModelSerializer):
"""
菜单按钮-序列化器
"""
class Meta:
model = RoleMenuPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleMenuPermissionInitSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化器
"""
class Meta:
model = RoleMenuPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleMenuPermissionCreateUpdateSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化器
"""
class Meta:
model = RoleMenuPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleMenuPermissionViewSet(CustomModelViewSet):
"""
菜单按钮接口
list:查询
create:新增
update:修改
retrieve:单例
destroy:删除
"""
queryset = RoleMenuPermission.objects.all()
serializer_class = RoleMenuPermissionSerializer
create_serializer_class = RoleMenuPermissionCreateUpdateSerializer
update_serializer_class = RoleMenuPermissionCreateUpdateSerializer
extra_filter_class = []
@action(methods=['post'],detail=False)
def save_auth(self,request):
"""
保存页面菜单授权
:param request:
:return:
"""
body = request.data
role_id = body.get('role',None)
if role_id is None:
return ErrorResponse(msg="未获取到角色参数")
menu_list = body.get('menu',None)
if menu_list is None:
return ErrorResponse(msg="未获取到菜单参数")
data = [{"role":role_id,"menu":item} for item in menu_list]
serializer = RoleMenuPermissionSerializer(data=data,many=True,request=request)
if serializer.is_valid(raise_exception=True):
serializer.save()
return DetailResponse(msg="保存成功",data=serializer.data)

View File

@@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
"""
@author: 猿小天
@contact: QQ:1638245306
@Created on: 2021/6/3 003 0:30
@Remark: 菜单按钮管理
"""
from django.db.models import F
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
"""
菜单按钮-序列化器
"""
class Meta:
model = RoleMenuButtonPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleMenuButtonPermissionInitSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化器
"""
class Meta:
model = RoleMenuButtonPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
"""
初始化菜单按钮-序列化器
"""
class Meta:
model = RoleMenuButtonPermission
fields = "__all__"
read_only_fields = ["id"]
class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
"""
菜单按钮接口
list:查询
create:新增
update:修改
retrieve:单例
destroy:删除
"""
queryset = RoleMenuButtonPermission.objects.all()
serializer_class = RoleMenuButtonPermissionSerializer
create_serializer_class = RoleMenuButtonPermissionCreateUpdateSerializer
update_serializer_class = RoleMenuButtonPermissionCreateUpdateSerializer
extra_filter_class = []
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def role_get_menu(self, request):
"""根据当前用户的角色返回角色拥有的菜单"""
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
queryset = Menu.objects.filter(status=1).values('id','name','parent','is_catalog')
else:
role_id = request.user.role.values_list('id',flat=True)
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_id).values(id=F('menu__id'),name=F('menu__name'),parent=F('menu__parent'),is_catalog=F('menu__is_catalog'))
return DetailResponse(data=queryset)
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def role_menu_get_button(self,request):
"""
当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用
:param request:
:return:
"""
params = request.query_params
if params:
menu_id = params.get('menu',None)
if menu_id:
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
queryset = MenuButton.objects.filter(menu=menu_id).values('id', 'name')
else:
role_list = request.user.role.values_list('id',flat=True)
queryset = RoleMenuButtonPermission.objects.filter(role_in=role_list,menu_button__menu=menu_id).values(
id=F('menu_button__id'),
name=F('menu_button__name')
)
return DetailResponse(data=queryset)
return ErrorResponse(msg="参数错误")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
def data_scope(self, request):
"""
获取数据权限范围:角色授权页面使用
:param request:
:return:
"""
is_superuser = request.user.is_superuser
if is_superuser:
data = [
{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
},
{
"value": 3,
"label": '全部数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}
]
return DetailResponse(data=data)
else:
data = []
role_id = request.user.role.id
params = request.query_params
if params:
menu_button_id = params.get('menu_button', None)
if menu_button_id:
role_queryset = RoleMenuButtonPermission.objects.filter(role=role_id,menu_button=menu_button_id).values_list('data_range',flat=True)
data_range_list = list(set(role_queryset))
for item in data_range_list:
if item == 0:
data = [{
"value": 0,
"label": '仅本人数据权限'
}]
elif item == 1:
data = [{
"value": 0,
"label": '仅本人数据权限'
}, {
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 2:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
}]
elif item == 3:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 3,
"label": '全部数据权限'
}, ]
elif item == 4:
data = [{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}]
else:
data = []
return DetailResponse(data=data)
return ErrorResponse(msg="参数错误")
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def role_to_dept_all(self, request):
"""
当前用户角色下所能授权的部门:角色授权页面使用
:param request:
:return:
"""
params = request.query_params
is_superuser = request.user.is_superuser
is_admin = request.user.role.values_list('admin', flat=True)
if is_superuser or True in is_admin:
queryset = Dept.objects.values('id','name','parent')
return DetailResponse(data=queryset)
else:
if params:
menu_button = params.get('menu_button')
if menu_button is None:
return ErrorResponse(msg="参数错误")
role_list = request.user.role.values_list('id', flat=True)
queryset = RoleMenuButtonPermission.objects.filter(role_in=role_list,menu_button=None).values(
id=F('dept__id'),
name=F('dept__name'),
parent=F('dept__parent')
)
return DetailResponse(data=queryset)
else:
return ErrorResponse(msg="参数错误")
@action(methods=['get'],detail=False,permission_classes=[IsAuthenticated])
def menu_to_button(self,request):
"""
根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用
:param request:
:return:
"""
params = request.query_params
if params:
menu_id = params.get('menu',None)
if menu_id is None:
return ErrorResponse(msg="未获取到参数")
role_id = params.get('role', None)
if role_id is None:
return ErrorResponse(msg="未获取到参数")
queryset = RoleMenuButtonPermission.objects.filter(role=role_id,menu_button__menu=menu_id).values(
'id',
'data_range',
'menu_button'
)
return DetailResponse(data=queryset)
return ErrorResponse(msg="未获取到参数")

View File

@@ -43,48 +43,6 @@ class SystemConfigCreateSerializer(CustomModelSerializer):
return value return value
class SystemConfigInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
children = serializers.SerializerMethodField()
def get_children(self, obj: SystemConfig):
data = []
instance = SystemConfig.objects.filter(parent_id=obj.id)
if instance:
serializer = SystemConfigInitSerializer(instance=instance, many=True)
data = serializer.data
return data
def save(self, **kwargs):
instance = super().save(**kwargs)
children = self.initial_data.get('children')
# 菜单表
if children:
for data in children:
data['parent'] = instance.id
filter_data = {
"key": data['key'],
"parent": data['parent']
}
instance_obj = SystemConfig.objects.filter(**filter_data).first()
if instance_obj and not self.initial_data.get('reset'):
continue
serializer = SystemConfigInitSerializer(instance_obj, data=data, request=self.request)
serializer.is_valid(raise_exception=True)
serializer.save()
return instance
class Meta:
model = SystemConfig
fields = ['parent', 'title', 'key', 'value', 'sort', 'status', 'data_options', 'form_item_type', 'rule',
'placeholder', 'setting', 'creator', 'dept_belong_id', 'children']
read_only_fields = ["id"]
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class SystemConfigSerializer(CustomModelSerializer): class SystemConfigSerializer(CustomModelSerializer):
@@ -103,10 +61,10 @@ class SystemConfigChinldernSerializer(CustomModelSerializer):
""" """
系统配置子级-序列化器 系统配置子级-序列化器
""" """
chinldern = serializers.SerializerMethodField() children = serializers.SerializerMethodField()
form_item_type_label = serializers.CharField(source='get_form_item_type_display', read_only=True) form_item_type_label = serializers.CharField(source='get_form_item_type_display', read_only=True)
def get_chinldern(self, instance): def get_children(self, instance):
queryset = SystemConfig.objects.filter(parent=instance) queryset = SystemConfig.objects.filter(parent=instance)
serializer = SystemConfigSerializer(queryset, many=True) serializer = SystemConfigSerializer(queryset, many=True)
return serializer.data return serializer.data

View File

@@ -60,32 +60,7 @@ class UserSerializer(CustomModelSerializer):
return serializer.data return serializer.data
class UsersInitSerializer(CustomModelSerializer):
"""
初始化获取数信息(用于生成初始化json文件)
"""
def save(self, **kwargs):
instance = super().save(**kwargs)
role_key = self.initial_data.get('role_key', [])
role_ids = Role.objects.filter(key__in=role_key).values_list('id', flat=True)
instance.role.set(role_ids)
dept_key = self.initial_data.get('dept_key', None)
dept_id = Dept.objects.filter(key=dept_key).first()
instance.dept = dept_id
instance.save()
return instance
class Meta:
model = Users
fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type',
'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id',
'password', 'last_login', 'is_superuser']
read_only_fields = ['id']
extra_kwargs = {
'creator': {'write_only': True},
'dept_belong_id': {'write_only': True}
}
class UserCreateSerializer(CustomModelSerializer): class UserCreateSerializer(CustomModelSerializer):
@@ -253,16 +228,7 @@ class UserViewSet(CustomModelViewSet):
serializer_class = UserSerializer serializer_class = UserSerializer
create_serializer_class = UserCreateSerializer create_serializer_class = UserCreateSerializer
update_serializer_class = UserUpdateSerializer update_serializer_class = UserUpdateSerializer
# filter_fields = ["name", "username", "gender", "is_active", "dept", "user_type"] filter_fields = ["name", "username", "gender", "is_active", "dept", "user_type"]
filter_fields = {
"name": ["exact"],
"mobile": ["exact"],
"username": ["exact"],
"gender": ["icontains"],
"is_active": ["icontains"],
"dept": ["exact"],
"user_type": ["exact"],
}
search_fields = ["username", "name", "gender", "dept__name", "role__name"] search_fields = ["username", "name", "gender", "dept__name", "role__name"]
# 导出 # 导出
export_field_label = { export_field_label = {
@@ -355,6 +321,7 @@ class UserViewSet(CustomModelViewSet):
if not check_password: if not check_password:
check_password = request.user.check_password(hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest()) check_password = request.user.check_password(hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest())
if check_password: if check_password:
new_pwd = hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()
request.user.password = make_password(new_pwd) request.user.password = make_password(new_pwd)
request.user.save() request.user.save()
return DetailResponse(data=None, msg="修改成功") return DetailResponse(data=None, msg="修改成功")

View File

@@ -33,6 +33,7 @@ class CoreInitialize:
path_file = os.path.join(apps.get_app_config(self.app.split('.')[-1]).path, 'fixtures', path_file = os.path.join(apps.get_app_config(self.app.split('.')[-1]).path, 'fixtures',
f'init_{Serializer.Meta.model._meta.model_name}.json') f'init_{Serializer.Meta.model._meta.model_name}.json')
if not os.path.isfile(path_file): if not os.path.isfile(path_file):
print("文件不存在,跳过初始化")
return return
with open(path_file,encoding="utf-8") as f: with open(path_file,encoding="utf-8") as f:
for data in json.load(f): for data in json.load(f):

View File

@@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
from dvadmin.utils.json_response import DetailResponse
class FastCrudMixin:
"""
定义快速CRUD数据操作的通用方法
"""
# 需要CRUD的字段
crud_fields = None
# 排除CRUD的字段
exclude_fields = None
# 自定义CRUD的JSON
custom_crud_json = None
# 需要修改的CRUD键值对
crud_update_key_value = None
# 将Django的字段类型处理为JS类型
def __handle_type(self, type):
if type in ['BigAutoField', 'CharField']:
return "input"
if type == 'DateTimeField':
return "datetime"
if type == 'DateField':
return "date"
if type == 'IntegerField':
return "number"
if type == 'BooleanField':
return "dict-switch"
# 获取字段属性信息
def __get_field_attribute(self):
result = []
queryset = self.get_queryset()
__name = ""
__verbose_name = ""
__type = "text"
# 判断指定CRUD字段
if self.crud_fields and type(self.crud_fields == list):
for item in self.crud_fields:
try:
field = queryset.model._meta.get_field(item)
field_type = field.get_internal_type()
__name = field.name
# 判断类型是否为外键类型,外键类型需要特殊方式获取verbose_name
if field_type in ['ForeignKey', 'OneToOneField', 'ManyToManyField']:
continue
# try:
# verbose_name = Users._meta.get_field(str(field.name)).verbose_name
# except:
# pass
else:
__verbose_name = field.verbose_name
__type = self.__handle_type(field_type)
except:
continue
result.append({"key": __name, "title": __verbose_name, "type": __type})
else:
# 获取model的所有字段及属性
model_fields = queryset.model._meta.get_fields()
# 遍历所有字段属性
for field in model_fields:
field_type = field.get_internal_type()
__name = field.name
# 判断需要排除的CRUD字段
if self.exclude_fields and type(self.exclude_fields == list):
if __name in self.exclude_fields:
continue
# 判断类型是否为外键类型,外键类型需要特殊方式获取verbose_name
if field_type in ['ForeignKey', 'OneToOneField', 'ManyToManyField']:
continue
# try:
# verbose_name = Users._meta.get_field(str(field.name)).verbose_name
# except:
# pass
else:
__verbose_name = field.verbose_name
__type = self.__handle_type(field_type)
result.append({"key": __name, "title": __verbose_name, "type": __type})
return result
#获取key
def __find_key(self,dct: dict,
target_key: str,
level: int = -1,
index: int = -1) -> tuple:
"""Find a key within a nested dictionary and return its level and index."""
for k, v in dct.items():
level += 1
index += 1
if k == target_key:
return level, index
elif isinstance(v, list):
for i, dct_ in enumerate(v):
if isinstance(dct_, dict):
result = self.__find_key(dct_, target_key)
if result is not None:
return result
else:
continue
elif isinstance(v, str) or isinstance(v, int) or isinstance(v, float):
continue
# 修改字典中key的value
def __update_nested_dict(self,nested_dict: dict,
target_key: str,
new_value) -> dict:
"""Update a nested dictionary with a new value."""
split_target_key = target_key.split('.')
if len(split_target_key) > 1:
new_dict = nested_dict[split_target_key[0]]
for item in split_target_key[1:-1]:
new_dict = new_dict[item]
self.__update_nested_dict(new_dict, split_target_key[-1], new_value)
else:
nested_dict[target_key] = new_value
return nested_dict
# 处理crud,返回columns
def __handle_crud(self):
result = self.__get_field_attribute()
columns = dict()
for item in result:
key = item.get('key')
title = item.get('title')
type = item.get('type')
columns[key] = {
"title": title,
"key": key,
"type": type
}
# 对自定义的crud配置合并
if self.custom_crud_json and isinstance(self.custom_crud_json,dict):
columns = columns | self.custom_crud_json
# 对curd进行修改配置
if self.crud_update_key_value and isinstance(self.crud_update_key_value,dict):
for key, value in self.crud_update_key_value.items():
columns = self.__update_nested_dict(columns,key,value)
return columns
@action(methods=['get'], detail=False,permission_classes=[AllowAny])
def init_crud(self, request):
self.permission_classes = [AllowAny]
columns = self.__handle_crud()
expose = "({expose,dict})=>{"
ret = "return {"
res = "}}"
data = f"""{expose}
{ret}
columns:{columns}
{res}
"""
return DetailResponse(data=data)

View File

@@ -11,14 +11,19 @@ import traceback
from django.db.models import ProtectedError from django.db.models import ProtectedError
from django.http import Http404 from django.http import Http404
from rest_framework.exceptions import APIException as DRFAPIException, AuthenticationFailed from rest_framework.exceptions import APIException as DRFAPIException, AuthenticationFailed, NotAuthenticated
from rest_framework.views import set_rollback from rest_framework.status import HTTP_401_UNAUTHORIZED
from rest_framework.views import set_rollback, exception_handler
from dvadmin.utils.json_response import ErrorResponse from dvadmin.utils.json_response import ErrorResponse
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CustomAuthenticationFailed(NotAuthenticated):
# 设置 status_code 属性为 400
status_code = 400
def CustomExceptionHandler(ex, context): def CustomExceptionHandler(ex, context):
""" """
统一异常拦截处理 统一异常拦截处理
@@ -30,9 +35,14 @@ def CustomExceptionHandler(ex, context):
""" """
msg = '' msg = ''
code = 4000 code = 4000
# 调用默认的异常处理函数
response = exception_handler(ex, context)
if isinstance(ex, AuthenticationFailed): if isinstance(ex, AuthenticationFailed):
code = 401 code = 401
code_type = response.data.get('detail').code
if code_type == 'no_active_account':
code=400
return ErrorResponse(status=HTTP_401_UNAUTHORIZED)
msg = ex.detail msg = ex.detail
elif isinstance(ex,Http404): elif isinstance(ex,Http404):
code = 400 code = 400

View File

@@ -20,7 +20,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from django_filters.utils import get_model_field from django_filters.utils import get_model_field
from rest_framework.filters import BaseFilterBackend from rest_framework.filters import BaseFilterBackend
from dvadmin.system.models import Dept, ApiWhiteList from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission
def get_dept(dept_id: int, dept_all_list=None, dept_list=None): def get_dept(dept_id: int, dept_all_list=None, dept_list=None):
@@ -105,11 +105,21 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
# (2, "本部门数据权限"), # (2, "本部门数据权限"),
# (3, "全部数据权限"), # (3, "全部数据权限"),
# (4, "自定数据权限") # (4, "自定数据权限")
role_list = request.user.role.filter(status=1).values("admin", "data_range") replace_str = re.compile('\d')
re_api = replace_str.sub('{id}', api)
role_id_list = request.user.role.values_list('id', flat=True)
role_permission_list=RoleMenuButtonPermission.objects.filter(
role__in=role_id_list,
role__status=1,
menu_button__api=re_api,
menu_button__method=method).values(
'data_range',
role_admin=F('role__admin')
)
dataScope_list = [] # 权限范围列表 dataScope_list = [] # 权限范围列表
for ele in role_list: for ele in role_permission_list:
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据 # 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
if 3 == ele.get("data_range") or ele.get("admin") == True: if 3 == ele.get("data_range") or ele.get("role_admin") == True:
return queryset return queryset
dataScope_list.append(ele.get("data_range")) dataScope_list.append(ele.get("data_range"))
dataScope_list = list(set(dataScope_list)) dataScope_list = list(set(dataScope_list))

View File

@@ -20,12 +20,10 @@ class SuccessResponse(Response):
content_type=None,page=1,limit=1,total=1): content_type=None,page=1,limit=1,total=1):
std_data = { std_data = {
"code": 2000, "code": 2000,
"data": { "page": page,
"page": page, "limit": limit,
"limit": limit, "total": total,
"total": total, "data": data,
"data": data
},
"msg": msg "msg": msg
} }
super().__init__(std_data, status, template_name, headers, exception, content_type) super().__init__(std_data, status, template_name, headers, exception, content_type)

View File

@@ -10,7 +10,7 @@
from collections import OrderedDict from collections import OrderedDict
from django.core import paginator from django.core import paginator
from django.core.paginator import Paginator as DjangoPaginator from django.core.paginator import Paginator as DjangoPaginator, InvalidPage
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response from rest_framework.response import Response
@@ -21,23 +21,65 @@ class CustomPagination(PageNumberPagination):
max_page_size = 999 max_page_size = 999
django_paginator_class = DjangoPaginator django_paginator_class = DjangoPaginator
def paginate_queryset(self, queryset, request, view=None):
"""
Paginate a queryset if required, either returning a
page object, or `None` if pagination is not configured for this view.
"""
empty = True
page_size = self.get_page_size(request)
if not page_size:
return None
paginator = self.django_paginator_class(queryset, page_size)
page_number = request.query_params.get(self.page_query_param, 1)
if page_number in self.last_page_strings:
page_number = paginator.num_pages
try:
self.page = paginator.page(page_number)
except InvalidPage as exc:
# msg = self.invalid_page_message.format(
# page_number=page_number, message=str(exc)
# )
# raise NotFound(msg)
empty = False
pass
if paginator.num_pages > 1 and self.template is not None:
# The browsable API should display pagination controls.
self.display_page_controls = True
self.request = request
if not empty:
self.page = []
return list(self.page)
def get_paginated_response(self, data): def get_paginated_response(self, data):
code = 2000 code = 2000
msg = 'success' msg = 'success'
res = { page =int(self.get_page_number(self.request, paginator)) or 1
"page": int(self.get_page_number(self.request, paginator)) or 1, total=self.page.paginator.count if self.page else 0
"total": self.page.paginator.count, limit= int(self.get_page_size(self.request)) or 10
"limit": int(self.get_page_size(self.request)) or 10, is_next= self.page.has_next()
"data": data is_previous= self.page.has_previous()
} data=data
if not data: if not data:
code = 2000 code = 2000
msg = "暂无数据" msg = "暂无数据"
res['data'] = [] data = []
return Response(OrderedDict([ return Response(OrderedDict([
('code', code), ('code', code),
('msg', msg), ('msg', msg),
# ('total',self.page.paginator.count), ('page', page),
('data', res), ('limit', limit),
('total',total),
('is_next',is_next),
('is_previous', is_previous),
('data', data)
])) ]))

View File

@@ -12,7 +12,7 @@ from django.contrib.auth.models import AnonymousUser
from django.db.models import F from django.db.models import F
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission
from dvadmin.system.models import ApiWhiteList from dvadmin.system.models import ApiWhiteList, RoleMenuButtonPermission
def ValidationApi(reqApi, validApi): def ValidationApi(reqApi, validApi):
@@ -81,7 +81,8 @@ class CustomPermission(BasePermission):
# ********# # ********#
if not hasattr(request.user, "role"): if not hasattr(request.user, "role"):
return False return False
userApiList = request.user.role.values('permission__api', 'permission__method') # 获取当前用户的角色拥有的所有接口 role_id_list = request.user.role.values_list('id',flat=True)
userApiList = RoleMenuButtonPermission.objects.filter(role__in=role_id_list).values(permission__api=F('menu_button__api'), permission__method=F('menu_button__method')) # 获取当前用户的角色拥有的所有接口
ApiList = [ ApiList = [
str(item.get('permission__api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str( str(item.get('permission__api').replace('{id}', '([a-zA-Z0-9-]+)')) + ":" + str(
item.get('permission__method')) + '$' for item in userApiList if item.get('permission__api')] item.get('permission__method')) + '$' for item in userApiList if item.get('permission__api')]

View File

@@ -20,7 +20,8 @@ from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailRe
from dvadmin.utils.permission import CustomPermission from dvadmin.utils.permission import CustomPermission
from django_restql.mixins import QueryArgumentsMixin from django_restql.mixins import QueryArgumentsMixin
class CustomModelViewSet(ModelViewSet,ImportSerializerMixin,ExportSerializerMixin,QueryArgumentsMixin):
class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMixin, QueryArgumentsMixin):
""" """
自定义的ModelViewSet: 自定义的ModelViewSet:
统一标准的返回格式;新增,查询,修改可使用不同序列化器 统一标准的返回格式;新增,查询,修改可使用不同序列化器
@@ -36,13 +37,13 @@ class CustomModelViewSet(ModelViewSet,ImportSerializerMixin,ExportSerializerMixi
update_serializer_class = None update_serializer_class = None
filter_fields = '__all__' filter_fields = '__all__'
search_fields = () search_fields = ()
extra_filter_backends = [DataLevelPermissionsFilter] extra_filter_class = [DataLevelPermissionsFilter]
permission_classes = [CustomPermission] permission_classes = [CustomPermission]
import_field_dict = {} import_field_dict = {}
export_field_label = {} export_field_label = {}
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
for backend in set(set(self.filter_backends) | set(self.extra_filter_backends or [])): for backend in set(set(self.filter_backends) | set(self.extra_filter_class or [])):
queryset = backend().filter_queryset(self.request, queryset, self) queryset = backend().filter_queryset(self.request, queryset, self)
return queryset return queryset
@@ -51,7 +52,6 @@ class CustomModelViewSet(ModelViewSet,ImportSerializerMixin,ExportSerializerMixi
return self.values_queryset return self.values_queryset
return super().get_queryset() return super().get_queryset()
def get_serializer_class(self): def get_serializer_class(self):
action_serializer_name = f"{self.action}_serializer_class" action_serializer_name = f"{self.action}_serializer_class"
action_serializer_class = getattr(self, action_serializer_name, None) action_serializer_class = getattr(self, action_serializer_name, None)
@@ -107,17 +107,17 @@ class CustomModelViewSet(ModelViewSet,ImportSerializerMixin,ExportSerializerMixi
instance.delete() instance.delete()
return DetailResponse(data=[], msg="删除成功") return DetailResponse(data=[], msg="删除成功")
keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.TYPE_STRING)
keys = openapi.Schema(description='主键列表',type=openapi.TYPE_ARRAY,items=openapi.TYPE_STRING)
@swagger_auto_schema(request_body=openapi.Schema( @swagger_auto_schema(request_body=openapi.Schema(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
required=['keys'], required=['keys'],
properties={'keys': keys} properties={'keys': keys}
), operation_summary='批量删除') ), operation_summary='批量删除')
@action(methods=['delete'],detail=False) @action(methods=['delete'], detail=False)
def multiple_delete(self,request,*args,**kwargs): def multiple_delete(self, request, *args, **kwargs):
request_data = request.data request_data = request.data
keys = request_data.get('keys',None) keys = request_data.get('keys', None)
if keys: if keys:
self.get_queryset().filter(id__in=keys).delete() self.get_queryset().filter(id__in=keys).delete()
return SuccessResponse(data=[], msg="删除成功") return SuccessResponse(data=[], msg="删除成功")

1
backend/gunicorn.pid Normal file
View File

@@ -0,0 +1 @@
7

View File

@@ -1,7 +1,8 @@
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:latest FROM node:16.19-alpine
WORKDIR /web/ WORKDIR /web/
COPY web/. . COPY web/. .
RUN cnpm run build RUN yarn install
RUN yarn build
FROM nginx:alpine FROM nginx:alpine
COPY ./docker_env/nginx/my.conf /etc/nginx/conf.d/my.conf COPY ./docker_env/nginx/my.conf /etc/nginx/conf.d/my.conf

View File

@@ -1,4 +1,4 @@
FROM node:14-alpine FROM node:16.19-alpine
WORKDIR / WORKDIR /
COPY ./web/package.json . COPY ./web/package.json .
RUN npm install -g cnpm --registry=https://registry.npm.taobao.org && cnpm install --registry=https://registry.npm.taobao.org RUN npm install -g yarn

View File

@@ -5,4 +5,4 @@ VITE_PORT = 8080
VITE_OPEN = false VITE_OPEN = false
# public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可 # public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可
VITE_PUBLIC_PATH = /vue-next-admin-preview/ VITE_PUBLIC_PATH = /

View File

@@ -2,4 +2,7 @@
ENV = 'development' ENV = 'development'
# 本地环境接口地址 # 本地环境接口地址
VITE_API_URL = 'http://127.0.0.1:8000' VITE_API_URL = 'http://127.0.0.1:8000/'
# 是否启用按钮权限
VITE_PM_ENABLED = true

View File

@@ -2,4 +2,7 @@
ENV = 'production' ENV = 'production'
# 线上环境接口地址 # 线上环境接口地址
VITE_API_URL = 'https://lyt-top.gitee.io/vue-next-admin-preview/' VITE_API_URL = ''
# 是否启用按钮权限
VITE_PM_ENABLED = true

View File

@@ -13,9 +13,18 @@ module.exports = {
}, },
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'], extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
plugins: ['vue', '@typescript-eslint'], plugins: ['vue', '@typescript-eslint'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.vue'],
rules: {
'no-undef': 'off',
},
},
],
rules: { rules: {
// http://eslint.cn/docs/rules/ // http://eslint.cn/docs/rules/
// https://eslint.vuejs.org/rules/ // https://eslint.vuejs.org/rules/
// https://typescript-eslint.io/rules/no-unused-vars/
'@typescript-eslint/ban-ts-ignore': 'off', '@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
@@ -26,6 +35,9 @@ module.exports = {
'@typescript-eslint/ban-types': 'off', '@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/no-unused-vars': ['off'],
'vue/custom-event-name-casing': 'off', 'vue/custom-event-name-casing': 'off',
'vue/attributes-order': 'off', 'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off', 'vue/one-component-per-file': 'off',
@@ -55,9 +67,11 @@ module.exports = {
'generator-star-spacing': 'off', 'generator-star-spacing': 'off',
'no-unreachable': 'off', 'no-unreachable': 'off',
'no-multiple-template-root': 'off', 'no-multiple-template-root': 'off',
'no-unused-vars': 'error', 'no-unused-vars': 'warn',
'vue/no-unused-vars': "off",
'no-v-model-argument': 'off', 'no-v-model-argument': 'off',
'no-case-declarations': 'off', 'no-case-declarations': 'off',
'no-console': 'error', 'no-console': 'error',
'no-redeclare': 'off',
}, },
}; };

View File

@@ -1,367 +0,0 @@
# <a href="https://gitee.com/lyt-top/vue-next-admin" target="_blank">vue-next-admin 更新日志</a>
🎉🎉🔥 `vue-next-admin` 基于 vue3.x 、Typescript、vite、Element plus 等适配手机、平板、pc 的后台开源免费模板库vue2.x 请切换 vue-prev-admin 分支)
## 2.2.0
`2022.07.10`
⚡⚡⚡ [/sec/stores/userInfo.ts](https://gitee.com/lyt-top/vue-next-admin/blob/master/src/stores/userInfo.ts) 下添加了 `getApiUserInfo` 接口模拟数据 `setTimeout` 为 3 秒
- 🌟 更新 依赖更新最新版本
- 🐞 修复 [主界面重新授权按钮点击卡死不跳转登录界面#I5C3JS](https://gitee.com/lyt-top/vue-next-admin/issues/I5C3JS),感谢[@Hero-Typ](https://gitee.com/tian_yu_peng)
- 🐞 修复 编译警告[#I5CVSB](https://gitee.com/lyt-top/vue-next-admin/issues/I5CVSB),全局替换成 `:deep(attr)`,感谢[@Linvas](https://gitee.com/linvas)。参考文档:[vue3 sfc-style](https://v3.cn.vuejs.org/api/sfc-style.html#style-scoped)。`node_modules\print-js\dist\print.js``print-js` 作者适配或去除 `package.json` 中的 `"print-js": "^1.6.0"`
- 🐞 修复 [vue-next-admin-template-js 版本前端控制路由userInfo.js 请求用户信息接口报错,加载不到路由 可以写个定时器模拟一下接口 一样的报错#I5F1HP](https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP),感谢[@白开水](https://gitee.com/libin951223)
## 2.1.1
`2022.05.27`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 深色模式下,`<el-button text></el-button>` 时,`:active` 样式
- 🎯 优化 [页面缓存在刷新之后失效 #I58U75](https://gitee.com/lyt-top/vue-next-admin/issues/I58U75)),感谢[@ls0428](https://gitee.com/ls0428)
- 🎯 优化 [SvgIcon 对下载的 Svg 图像设置颜色无效 #I59ND0](https://gitee.com/lyt-top/vue-next-admin/issues/I59ND0)),感谢[@elus_z](https://gitee.com/elus_z)
- 🎯 优化 `/src/utils/toolsValidate.ts` 工具类
- 🐞 修复 [布局切换TagsView 显示的 tab 会多一个出来 #I58WGM](https://gitee.com/lyt-top/vue-next-admin/issues/I58WGM),感谢[@lg_boy](https://gitee.com/lg_boy)
- 🐞 修复 [如果设置顶部面包屑导航开启图标 isBreadcrumbIcon=true 后,样式有点问题 如果不开启就是正常的 #I58VB8](https://gitee.com/lyt-top/vue-next-admin/issues/I58VB8)
- 🐞 修复 地址栏路由地址输入错误时,返回首页后,再次输入路由地址错误时,不跳转 404 问题
- 🐞 修复 [2.1.0 版本的图标选择组件多次点击后功能失效 #I590TH](https://gitee.com/lyt-top/vue-next-admin/issues/I590TH),感谢[@quber](https://gitee.com/quber)
## 2.1.0
`2022.04.18`
⚡⚡⚡ 此版本为破环性更新,优化内容如下:(谨慎更新!谨慎更新!!谨慎更新!!!)。因为 `vuex` 替换成 `pinia`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 部分界面图片不显示问题(更换 gitee 在线图片地址源)
- 🎯 优化 各界面方法引入与逻辑之间添加一行空行,方便区分内容
- 🎯 优化 图标选择器 [#I4YAHB](https://gitee.com/lyt-top/vue-next-admin/issues/I4YAHB),感谢[@真有你的](https://gitee.com/sunliusen)
- 🎯 优化 图标选择器 icon type 类型为 all 时,类型 ali、ele、awe 回显问题
- 🎯 优化 去掉开发环境 i18n 控制台警告,页面代码:[i18n/index.ts](https://gitee.com/lyt-top/vue-next-admin/blob/master/src/i18n/index.ts)
- 🎯 优化 `NextLoading.start()` 方法,防止第一次进入界面时出现短暂空白
- 🎯 优化 地址栏有参数退出登录,再次登录不跳之前界面问题 `src/layout/navBars/breadcrumb/user.vue`
- 🎯 优化 `SvgIcon` 组件,防止 `开启 Tagsview 图标` 时,`tagsView 右键菜单关闭` 报错问题,工作流不可连线、全屏时关闭按钮消失问题
- 🎯 优化 [如果 url 中有中文等特殊字符,第一次切换该 tab 时 keep-alive 失效#I55JS7](https://gitee.com/lyt-top/vue-next-admin/issues/I55JS7),感谢[yuyong1566](https://gitee.com/yuyong1566)
- 🎯 优化 [wangEditor](https://www.wangeditor.com/) 更新到 v5[vue3 版本线上示例中 wangeditor 富文本编辑器 demo 实例,无法换行#I5565B](https://gitee.com/lyt-top/vue-next-admin/issues/I5565B),感谢@[jenchih](https://gitee.com/jenchih)
- 🎯 优化 [在关闭 tagview 时,高度刷新时会会变化,出现滚动条](https://gitee.com/lyt-top/vue-next-admin/issues/I55FHM),感谢[张松](https://gitee.com/zs310071113)
- 🎯 优化 [路由参数](https://lyt-top.gitee.io/vue-next-admin-preview/#/params/common)演示
- 🎉 新增 [vuex](https://vuex.vuejs.org/) 替换成 [pinia](https://pinia.vuejs.org/getting-started.html)
- 🎉 新增 tagsView 支持自定义 tagsView 名称(文章详情时有用),前往体验:[路由参数/普通路由](https://lyt-top.gitee.io/vue-next-admin-preview/#/params/common)。新增 tagsView 支持自定义名称国际化,感谢[@q7but](https://gitee.com/q7but)、[!22 add 添加自定义 tagVIewName 拓展,支持国际化](https://gitee.com/lyt-top/vue-next-admin/pulls/22/files)、感谢[@tony_tong_xin](https://gitee.com/tony_tong_xin)
- 🐞 修复 适配 `"element-plus": "^2.1.9"2.2.0` 版本
- 🐞 修复 [导航栏横向布局后,一级菜单显示问题#I4Z3M3](https://gitee.com/lyt-top/vue-next-admin/issues/I4Z3M3)
- 🐞 修复 横向布局三级及以上导航菜单高亮、导航高度不统一问题
- 🐞 修复 分栏模式下,选中的菜单是 primary 样式,鼠标移入字也变成 primary 色了,感谢群友@孤夜-流殇
- 🐞 修复 [vuex 里面改了颜色 但是不生效 #I4WFMA](https://gitee.com/lyt-top/vue-next-admin/issues/I4WFMA)
- 🐞 修复 全局主题 primary 清空颜色后报错,[#I4X0LG](https://gitee.com/lyt-top/vue-next-admin/issues/I4X0LG),感谢[面向 BUG 编程](https://gitee.com/fhtfy)
- 🐞 修复 [.eslintrc.js 文件 rules 标签名错误 #I53IPK](https://gitee.com/lyt-top/vue-next-admin/issues/I53IPK),感谢[yuyong1566](https://gitee.com/yuyong1566)
- 🐞 修复 `开启 Tagsview 图标` 时,`tagsView 右键菜单关闭` 报错问题
- 🐞 修复 `router.push` 路径找不到时报错问题,`404、401 界面` 已移入到 `main` 主布局里(之前全屏)
- 🐞 修复 [全局修改组件大小失效了](https://gitee.com/lyt-top/vue-next-admin/issues/I551RP),感谢[lg_boy](https://gitee.com/lg_boy)
- 🐞 修复 [修改一下配置时,需要每次都清理 `window.localStorage` 浏览器永久缓存,配置才会生效,问题解决#I567R1](https://gitee.com/lyt-top/vue-next-admin/issues/I567R1),感谢[@lanbao123](https://gitee.com/lanbao123)
- 🐞 修复 [标记为需要缓存的 tab 页后,再次从左侧菜单打开,还是显示被缓存的页面内容#I4UY3G](https://gitee.com/lyt-top/vue-next-admin/issues/I4UY3G),感谢@axcc1234、特别感谢群友@华仔
- 🌈 重构 路由(`/src/router/index.ts`)解决 No match found for location with path "xxx"(前端控制,后端控制未解决) 问题
## 2.0.2
`2022.03.04`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 Alert 提示添加边框
- 🎯 优化 功能 / 数字滚动 演示界面
- 🐞 修复 全局主题按钮颜色 :active 问题
- 🐞 修复 Dropdown 下拉菜单样式问题
- 🐞 修复 SvgIcon 图标组件动态切换时报警告问题,[SvgIcon 改变 name 时可能导致图像不显示](https://gitee.com/lyt-top/vue-next-admin/issues/I4VGE0),感谢@axcc1234
## 2.0.1
`2022.02.25`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 svgIcon 图标组件
- 🎯 优化 vite.config.ts 打包,感谢群友@YourObjec
- 🐞 修复 tagViews 开启图标不显示问题(风格 5感谢群友@坏人
- 🐞 修复 [Element Plus 1.2.0-beta.6 以后的版本 el-table 在移动端无法左右滑动](https://gitee.com/lyt-top/vue-next-admin/issues/I4UPTP),感谢@YGDada
## 2.0.0
`2022.02.21`
⚡⚡⚡ 此版本为破环性更新,优化内容如下:(谨慎更新!谨慎更新!!谨慎更新!!!)。演示界面建议直接覆盖文件。如需使用之前版本,请前往[gitee 发行版](https://gitee.com/lyt-top/vue-next-admin/releases) 进行对应版本下载。基础版会基于 `master` 分支进行修改
- 🌟 更新 依赖更新最新版本
- 🌟 更新 登录页、首页
- 💔 移除 vue-web-screen-shot
- 💔 移除 城市多级联动,完整 json 数据请去 [vue-next-admin-images/menu](https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu) 仓库查看
- 💔 移除 功能/echartsTree 树图
- 💔 移除 其它设置/Tagsview 风格 2、Tagsview 风格 3
- 💔 移除 功能/验证器
- 🚧 调整 src/api 编写方式
- 🚧 调整 自定义封装公用组件演示,更好的维护
- 🎉 新增 Volar 支持vs code 配置参考 [Vue Language Features (Volar)](https://lyt-top.gitee.io/vue-next-admin-doc-preview/home/vscode/)
- 🎉 新增 `SvgIcon` 支持本地 svg 图标使用
- 🎉 新增 表单表格验证演示
- 🎯 优化 全局主题(移除 success、info、warning、danger
- 🎯 优化 工作流(开源)
- 🎯 优化 element plus svg 图标,`elementXXX` 改成 `ele-XXX`
- 🌈 重构 深色模式
- 🌹 合并 [处理 parent 的 h100 由于外层有 min-height 导致失效的问题](https://gitee.com/lyt-top/vue-next-admin/pulls/20),感谢@MaxNull@21030442-mao
- 🐞 修复 element plus 升级 `^1.3.0-beta.5` 后 组件 size 大小问题(大改:涉及布局、演示界面)
- 🐞 修复 vs code 使用 Vue Language Features (Volar) 插件 代码报红问题(可以把公用的 ts 类型定义封装起来公用)
## 1.2.2
`2021.12.21`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 iframes 滚动条问题
- 🎯 优化 部署后每次都要强制刷新清浏览器缓存问题
- 🎉 新增 工具类百分比验证演示
- 🐞 修复 [tag-view 标签右键会超出浏览器 #I4KN78](https://gitee.com/lyt-top/vue-next-admin/issues/I4KN78)
## 1.2.1
`2021.12.12`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 cropper 裁剪时卡顿问题 [#I4M2VQ](https://gitee.com/lyt-top/vue-next-admin/issues/I4M2VQ)
- 🎯 优化 Wangeditor 富文本编辑器的问题 [#I4LPC1](https://gitee.com/lyt-top/vue-next-admin/issues/I4LPC1)、[#I4LM7I](https://gitee.com/lyt-top/vue-next-admin/issues/I4LM7I)
- 🐞 修复 浏览器标题问题
- 🐞 修复 element plus svg 图标引入
- 🐞 修复 工作流不可以拖线连接问题
## 1.2.0
`2021.11.28`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 深色模式
- 🎯 优化 `/@/utils` 文件夹,合并删除单一内容
- 🎯 优化 系统设置:菜单管理(新增、修改)、角色管理(新增菜单权限)、用户管理、部门管理、字典管理
- 🎯 优化 登录界面逻辑、权限管理逻辑
- 🎯 优化 同步 [vue-next-admin-images](https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu) 后端控制菜单模拟数据
- 🎉 新增 适配 Font Icon 向 SVG Icon 迁移(改动大,"element-plus": "^1.2.0-beta.4" 谨慎更新)
- 🐞 修复 热更新问题,感谢@甜蜜蜜
- 🐞 修复 页面/element 字体图标演示
- 🐞 修复 功能/图标选择器演示,新增高级功能 [issues #I4GJXQ](https://gitee.com/lyt-top/vue-next-admin/issues/I4GJXQ)
## 1.1.2
`2021.10.17`
- 🌟 更新 依赖更新最新版本
- 🐞 修复 开启全屏时,刷新界面被还原成未全屏的状态
- 🎯 优化 tagsView 右键菜单关闭逻辑
- 🎯 优化 wangeditor 富文本编辑器(增加双向绑定)
- 🎉 新增 工作流(暂不开源)
- 🎉 新增 基础版 ts不带国际化切换 `vue-next-admin-template` 分支
## 1.1.1
`2021.09.25`
- 🌟 更新 依赖更新最新版本(`"element-plus": "^1.1.0-beta.13"` 版本运行错误,`^1.1.0-beta.16`修复横向菜单卡死问题)
- 🐞 修复 Dialog 弹窗位置错误、Drawer 抽屉内边距、el-menu 菜单收起时背景色问题
- 🎯 优化 锁屏界面自动锁屏(s/秒)必须设置至少 1 秒
- 🎉 新增 分栏布局,鼠标移入当前项时,显示当前项菜单内容
- 🎉 新增 工作流(未完成)
## 1.1.0
`2021.09.10`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 小屏模式下登录页二维码遮挡标题问题
- 🎉 新增 图片验证器
- 🎉 新增 动态复杂表单
- 🎉 新增 工作流(未完成)
- 🎉 新增 深色主题(伪深色,样式变动大,谨慎更新)
## 1.0.18
`2021.08.29`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 权限组件去掉顶级 div`/src/components/auth`
- 🎉 新增 布局配置添加恢复默认按钮
- 🐞 修复 升级 <a href="https://element-plus.gitee.io/#/zh-CN/component/changelog" target="_blank">element plus 1.1.0-beta.7</a>后项目无法启动、el-menu 菜单
- 🐞 修复 表格固定列时的层级、设置了相对定位时,遮挡左侧导航菜单问题
## 1.0.17
`2021.08.22`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 去除设置布局切换重置主题样式initSetLayoutChange切换布局需手动设置样式设置的样式自动同步各布局
- 🎯 优化 Dropdown 下拉菜单用户账号靠边时换行问题
- 🎯 优化 左侧导航菜单,共用菜单树,防止 `布局配置` 设置 `菜单 / 顶栏` 时,样式丢失等问题
- 🐞 修复 固定 header 后没有回到顶部的 bug拉取项目后运行不起来的 bug。<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/14" target="_blank">!14</a>,感谢<a href="https://gitee.com/wjs0509" target="_blank">@wjs0509</a>
- 🐞 修复 tagView 右键全屏后,浏览器窗口大小发生任何变化都会导致左边菜单显示出来,并且可点击打开对应页面。<a href="https://gitee.com/lyt-top/vue-next-admin/issues/I46E6T" target="_blank">I46E6T</a>
- 🐞 修复 默认设置 `菜单 / 顶栏` 样式不生效问题(/@/src/store/modules/themeConfig.ts
## 1.0.16
`2021.08.14`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 菜单高亮(详情且详情设置了 meta.isHide 时,顶级菜单高亮),感谢群友@YourObject
- 🎯 优化 详情路径写法:如父级(/pages/filtering那么详情为/pages/filtering/details?id=1。这样写可实现详情时父级菜单高亮否则写成/pages/filteringDetails?id=1顶级菜单将不会高亮。可参考`页面/过滤筛选组件`,点击当前图片进行测试
- 🎯 优化 tagsView 右键菜单全屏时,打开的界面高度问题
- 🎯 优化 图表批量 resize 问题
- 🐞 修复 菜单收起时设置全局主题primary 且有二级菜单时),文字高亮颜色不对
- 🐞 修复 国际化 <a href="https://gitee.com/lyt-top/vue-next-admin/issues/I43NPE" target="_blank">#I43NPE</a>。可参考:`页面/过滤筛选组件`,点击顶部语言切换,进行底部分页国际化查看
## 1.0.15
`2021.08.06`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 tagsView 右键菜单点击时的字段名id 已修改成 contextMenuClickId与路由中返回的 id 名冲突问题,感谢群友@伯牙已遇钟子期
- 🎉 新增 多个 form 表单验证界面演示
## 1.0.14
`2021.07.29`
- 🌟 更新 依赖更新最新版本vue、vuex、vue-router,出现问题,请手动降级。版本查看:<a href="https://www.npmjs.com/" target="_blank">vnpm</a>
- 🎯 优化 数据可视化图表演示加载卡顿问题、优化有图表的演示界面
- 🎯 优化 路由参数演示界面
- 🎯 优化 tagsView 操作演示界面由于存在相同路由多标签必须要传全部参数值query 或者 params
- 🎉 新增 开启 TagsView 共用开启时多个路由菜单共用一个详情组件参数为后点击的覆盖前面点击的tagsView 中只会出现一个(不支持同时出现多个 tagsView 标签))。关闭时:(多个路由菜单共用一个详情组件,参数不同,会同时出现多个 tagsView 标签)
- 🐞 修复 tagsView 共用(单标签)时,右键菜单功能点击,参数不对的问题(第 2n+个参数未覆盖第一个参数值)
- 🐞 修复 多 tagsView 标签(参数不同)、单个 tagsView 标签公用(参数不同)所带来的刷新功能、横向自动滚动等问题
- 🐞 修复 处理全屏若干问题,<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/12" target="_blank">pr!12</a>,感谢群友@另一个前端
## 1.0.13
`2021.07.25`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 数据可视化演示界面(/visualizingDemo1、/visualizingDemo2
- 🎉 新增 登录页扫码登录
## 1.0.12
`2021.07.16`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 数据可视化演示空界面(待完善)
- 🎯 优化 tagsView 动态路由xxx/:id/:name时的右键菜单刷新、关闭其它时参数丢失问题2021.07.15 优化)
- 🐞 修复 路由带参数时,复制路径到登录页,跳转后参数消失的问题
- 🐞 修复 设置多个外链,点击后,页面内容停留在上一个内容(内容未改变)、国际化处理、打开新窗口 sessionStorage 共享等
## 1.0.11
`2021.07.14`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 路由参数、图片懒加载界面演示
- ⚠️ 警告 Form 表单 `binding value must be a string or number`,解决:加上 `label-position="top"` 不报警告(等待官方修复)
- 🎯 优化 锁屏界面动画效果、首页图表显示
- 🎯 优化 tagsView 右键菜单 `关闭` 功能逻辑
- 🐞 修复 开启 TagsView 拖拽报错及小于 `1000px` 时自动设置禁止拖拽(<a href="https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI" target="_blank">#I3ZRRI</a>
- 🐞 修复 `iframe 内嵌、外链` 高度问题,使用 computed 进行计算
- 🐞 修复 默认布局开启 `侧边栏 Logo` 与关闭 `菜单水平折叠`,切换到横向布局时,菜单看不见的问题
- 🐞 修复 切换不同布局时,再去开启 `经典布局分割菜单` 功能不生效问题
- 🐞 修复 浏览器窗口标题中/英文切换不实时生效的问题
- 🐞 修复 切换布局时,某些功能不可以使用。部分界面不需要取消事件监听(proxy.mittBus.off('xxx'))
- 🐞 修复 动态路由带参数router-link 跳转问题(<a href="hhttps://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G" target="_blank">#I3YX6G</a>
- 🐞 修复 横向菜单有二级菜单时,点击子级菜单不高亮问题
- 🐞 修复 功能 tagsView 操作演示不生效
## 1.0.10
`2021.07.07`
- 🌟 更新 依赖更新最新版本(字体图标无问题)
- 🎯 优化 内嵌 iframe、外链解决 tagsView 刷新问题
## 1.0.9
`2021.07.02`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 图标选择器设置宽度、v-model 等问题
- 🎯 优化 滚动通知栏在手机上的体验
- 🎯 优化 系统管理/新增菜单(编辑菜单),使用 `图标选择器` 进行模拟
- 🎯 优化 字体图标(自动载入) 逻辑
- 🐞 修复 screenfull 全屏时,按键盘 esc 键图标不改变问题,感谢群友@伯牙已遇钟子期
## 1.0.8
`2021.06.29`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 表单中英文切换演示
- 🎯 优化 登录页查看密码 icon 图标
- 🎯 优化 图标选择器
- 🎯 优化 拖动指令
- 🐞 修复 form 表单在页面小于 576px 时的排版问题
## 1.0.7
`2021.06.24`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 拖动指令及其演示界面
- 🎯 优化 锁屏界面,解锁提示
- 🎯 优化 登录页在手机上显示的效果
## 1.0.6
`2021.06.23`
- 🎯 优化 去掉内嵌 iframe 内边距padding
- 🎯 优化 城市多级联动组件
- 🎯 优化 Tree 树形控件改成表格组件
- 🐞 修复 Cascader 级联选择器高度问题
## 1.0.5
`2021.06.22`
- 🌟 更新 vite 降级为@vite2.3.7,降级方法 `cnpm install vite@2.3.7`,防止 element plus 字体图标消失
- 🐞 修复 开启后端控制路由isRequestRoutes = true内嵌 iframe、外链不可使用的问题
## 1.0.4
`2021.06.19`
- 🌟 更新 依赖更新最新版本("vite": "^2.3.7")热更新无问题
- 🎉 新增 深克隆工具,方便开发,感谢<a href="https://gitee.com/kangert" target="_blank">@kangert</a>(<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/6" target="_blank">#6</a>)
- 🎯 优化 vuex 模块自动导入。感谢<a href="https://gitee.com/kangert" target="_blank">@kangert</a>(<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/4" target="_blank">#4</a>),感谢群友@web 小学生-第五君
- 🎯 优化 类型定义提高编码体验修复不能将类型“string | undefined”分配给类型“string”的问题。感谢<a href="https://gitee.com/kangert" target="_blank">@kangert</a>(<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/5" target="_blank">#5</a>)
- 🎯 优化 `layout` 文件夹移动到与 `views` 文件夹同级(改动较大,`/@/views/layout` 变成 `/@/layout`
- 🎯 优化 页面有 `console.log``eslint` 不生效问题
- 🎯 优化 页面、ts 中 `any` 类型问题(改动较大)
- 🎯 优化 登录页在手机上显示的效果
- 🎯 优化 多行注释信息,鼠标放到方法名即可查看,更加直观的知道方法参数等。引入方法时需去掉以 `.ts` 结尾的后缀(改动较大)
- 🎯 优化 移除 `utils/storage.ts` 下的旧写法(改动较大)
- 🎯 优化 拆分 `router` 下内容,路由、前端、后端控制分开写,方便理解
- 🐞 修复 鼠标移入顶部用户信息栏 `开/关全屏` 文字反向问题
- 🐞 修复 热更新时NextLoading界面 loading 不消失问题 `window.nextLoading === undefined`
- 🐞 修复 vuex 中不可以使用 `/@/api/xxx` 下的接口调用问题
## 1.0.3
`2021.06.02`
- ❄️ 删除 G6 思维导图界面
- 🌟 更新 手动更新 vue、vue-router、vuex 到最近最多人使用的版本,出现不可预测的问题请降低版本。版本查看:<a href="https://www.npmjs.com/package/vue" target="_blank">vue 版本查看</a>
- 🐞 修复 开启后端控制路由 `isRequestRoutes` 在非首页刷新页面后,回到首页的问题,感谢群友@伯牙已遇钟子期
## 1.0.2
`2021.06.01`
- 🌟 更新 依赖更新最新版本
- 🐞 修复 菜单搜索中文不可以搜索的问题,感谢群友@逍遥天意
## 1.0.1
`2021.05.31`
- 🎉 新增 更新日志文件 `CHANGELOG.md`,以后每次更新都会在这里显示对应内容
- 🌟 更新 依赖更新最新版本
- 🐞 修复 分栏、经典布局路由设置 `meta.isHide``true` 时报错问题,感谢群友@29@芭芭拉
- 🐞 修复 经典布局点击 `tagsView` 左侧菜单数据不变问题

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2021 lyt-Top Copyright (c) 2023 DVAdmin,hugedream
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,43 +1,8 @@
<div align="center"> <div align="center">django-vue3-admin:web </div>
<img src="https://img-blog.csdnimg.cn/9efd5420327a46b7bd6d93524a97229d.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbHl0LXRvcA==,size_14,color_FFFFFF,t_70,g_se,x_16">
<p align="center">
<a href="https://v3.vuejs.org/" target="_blank">
<img src="https://img.shields.io/badge/vue.js-vue3.x-green" alt="vue">
</a>
<a href="https://element-plus.gitee.io/#/zh-CN/component/changelog" target="_blank">
<img src="https://img.shields.io/badge/element--plus-%3E1.0.0-blue" alt="element plus">
</a>
<a href="https://www.tslang.cn/" target="_blank">
<img src="https://img.shields.io/badge/typescript-%3E4.0.0-blue" alt="typescript">
</a>
<a href="https://vitejs.dev/" target="_blank">
<img src="https://img.shields.io/badge/vite-%3E2.0.0-yellow" alt="vite">
</a>
<a href="https://gitee.com/lyt-top/vue-next-admin/blob/master/LICENSE" target="_blank">
<img src="https://img.shields.io/badge/license-MIT-success" alt="license">
</a>
</p>
<p>&nbsp;</p>
</div>
#### 🌈 介绍 #### 🌈 介绍
基于 vue3.x + CompositionAPI + typescript + vite + element plus + vue-router-next + next.vuex适配手机、平板、pc 的后台开源免费模板,希望减少工作量,帮助大家实现快速开发。 django-vue3-admin基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!
#### ⛱️ 线上预览
- vue3.x 版本预览vue-next-admin<a href="https://lyt-top.gitee.io/vue-next-admin-preview/#/login" target="_blank">https://lyt-top.gitee.io/vue-next-admin-preview/#/login</a>
- vue2.x 版本预览vue-prev-admin<a href="https://lyt-top.gitee.io/vue-prev-admin-preview/#/login" target="_blank">https://lyt-top.gitee.io/vue-prev-admin-preview/#/login</a>
#### 💒 代码仓库
- vue3.x 版本 <a href="https://gitee.com/lyt-top/vue-next-admin" target="_blank">https://gitee.com/lyt-top/vue-next-admin</a>
- vue2.x 版本 <a href="https://gitee.com/lyt-top/vue-next-admin/tree/vue-prev-admin" target="_blank">https://gitee.com/lyt-top/vue-next-admin/tree/vue-prev-admin</a>
#### 🚧 安装 cnpm、yarn
- 复制代码(桌面 cmd 运行) `npm install -g cnpm --registry=https://registry.npm.taobao.org`
- 复制代码(桌面 cmd 运行) `npm install -g yarn`
#### 🏭 环境支持 #### 🏭 环境支持
@@ -49,97 +14,19 @@
#### ⚡ 使用说明 #### ⚡ 使用说明
建议使用 cnpm因为 yarn 有时会报错。<a href="http://nodejs.cn/" target="_blank">node 版本 > 12xx.xx.x</a> 建议使用 yarnyarn 是一个类似于npm的包管理器 <a href="http://nodejs.cn/" target="_blank">node 版本 > 16</a>
```bash ```bash
# 克隆项目
git clone https://gitee.com/lyt-top/vue-next-admin.git
# 进入项目 # 进入项目
cd vue-next-admin cd django-vue-admin/web
# 安装依赖 # 安装依赖
cnpm install yarn install
# 运行项目 # 运行项目
cnpm run dev yarn dev
# 打包发布 # 打包发布
cnpm run build yarn build
``` ```
#### 💯 学习交流加 QQ 群
- 若加群了没同意一般秒过那就是群满了500 人群请换一个群试试。群会定期清理半年6 个月)未发言的群友,资源有限,请谅解。建议勿加多群,可能会误伤!
- 查看开发文档:<a href="https://lyt-top.gitee.io/vue-next-admin-doc-preview" target="_blank">vue-next-admin-doc</a>
- 群号码:
1 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=RdUY97Vx0T0vZ_1OOu-X1yFNkWgDwbjC&jump_from=webapi">665452019</a>
2 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=zVfy3gNy7pNWVK3kMduDzwU369PZg2fw&jump_from=webapi">766356862</a>
3 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=02EWb5P2JkP-8iwzaDadgFdxA0HSHPpn&jump_from=webapi">795345435</a>
4 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=0gTFO04WwkeZZ6R4lju6gucbeXHK-wNd&jump_from=webapi">736626228</a>
<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=RdUY97Vx0T0vZ_1OOu-X1yFNkWgDwbjC&jump_from=webapi">
<img src="https://img-blog.csdnimg.cn/35e00f12a3fe4820892ec630ca72f15f.png" width="220" height="220" alt="vue-next-admin 讨论群1" title="vue-next-admin 讨论群1"/>
</a>
<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=zVfy3gNy7pNWVK3kMduDzwU369PZg2fw&jump_from=webapi">
<img src="https://img-blog.csdnimg.cn/5f1b548abd9f434eb41edde31d1c1fa9.png" width="220" height="220" alt="vue-next-admin 讨论群2" title="vue-next-admin 讨论群2"/>
</a>
<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=02EWb5P2JkP-8iwzaDadgFdxA0HSHPpn&jump_from=webapi">
<img src="https://img-blog.csdnimg.cn/70c8a012dd304246bddeac2184c4ab3a.png" width="220" height="220" alt="vue-next-admin 讨论群3" title="vue-next-admin 讨论群3"/>
</a>
<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=0gTFO04WwkeZZ6R4lju6gucbeXHK-wNd&jump_from=webapi">
<img src="https://img-blog.csdnimg.cn/e5c9704eed1342bc9d9e74b37203402d.png" width="220" height="220" alt="vue-next-admin 讨论群4" title="vue-next-admin 讨论群4"/>
</a>
#### 💒 集成后端
- <a target="_blank" href="https://github.com/PandaGoAdmin/PandaX">@熊猫 PandaGoAdmin</a>
- <a target="_blank" href="https://toscode.gitee.com/GionConnection/gopro_free">@甜蜜蜜 GoPro 平台</a>
- <a target="_blank" href="https://gitee.com/GionConnection/niupi-free">@甜蜜蜜 NiuPi 平台</a>
- <a target="_blank" href="https://gitee.com/tiger1103/gfast/tree/os-v3/">@游子 GFast-V3</a>
- <a target="_blank" href="https://gitee.com/diygw/diygw-ui-php/">@diygw.com gw-ui-php</a>
- <a target="_blank" href="https://gitee.com/zsvg/vboot-net">@zsvg vboot-net</a>
- <a target="_blank" href="https://gitee.com/zsvg/vboot-java">@zsvg vboot-java</a>
- <a target="_blank" href="https://gitee.com/wonderful-code/buildadmin">@青红造了个白 buildadmin</a>
- <a target="_blank" href="https://github.com/xiaodingding/iotfast">@Goodwell iotfast(一个开源的物联网平台)</a>
#### ❤️ 鸣谢列表
- <a href="https://github.com/vuejs/vue" target="_blank">vue</a>
- <a href="https://github.com/vuejs/vue-next" target="_blank">vue-next</a>
- <a href="https://github.com/ElemeFE/element" target="_blank">element-ui</a>
- <a href="https://github.com/element-plus/element-plus" target="_blank">element-plus</a>
- <a href="https://github.com/vuejs/vue-router-next" target="_blank">vue-router-next</a>
- <a href="https://github.com/vuejs/vuex" target="_blank">vuex</a>
- <a href="https://github.com/apache/echarts" target="_blank">echarts</a>
- <a href="https://github.com/axios/axios" target="_blank">axios</a>
- <a href="https://github.com/zenorocha/clipboard.js" target="_blank">clipboard</a>
- <a href="https://github.com/inorganik/countUp.js" target="_blank">countUp</a>
- <a href="https://github.com/developit/mitt" target="_blank">mitt</a>
- <a href="https://github.com/rstacruz/nprogress" target="_blank">nprogress</a>
- <a href="https://github.com/sindresorhus/screenfull.js" target="_blank">screenfull</a>
- <a href="https://github.com/SortableJS/Sortable" target="_blank">sortablejs</a>
- <a href="https://github.com/sass/sass" target="_blank">sass</a>
- <a href="https://github.com/microsoft/TypeScript" target="_blank">typescript</a>
- <a href="https://github.com/vitejs/vite" target="_blank">vite</a>
- <a href="https://github.com/wangeditor-team/wangEditor" target="_blank">wangeditor</a>
- <a href="https://github.com/fengyuanchen/cropperjs" target="_blank">cropperjs</a>
- <a href="https://github.com/davidshimjs/qrcodejs" target="_blank">qrcodejs</a>
- <a href="https://github.com/crabbly/Print.js" target="_blank">print-js</a>
- <a href="https://github.com/jbaysolutions/vue-grid-layout" target="_blank">vue-grid-layout</a>
- <a href="https://github.com/antoniandre/splitpanes" target="_blank">splitpanes</a>
- <a href="https://github.com/jsplumb/jsplumb" target="_blank">jsplumb</a>
#### 💕 特别感谢
特别感谢老哥们的建议、指导与帮忙。谢谢!
- <a href="https://gitee.com/click33/sa-plus" target="_blank">@省长</a>
- <a href="https://gitee.com/jskz/Jskz-SpringCloud" target="_blank">@唐参</a>
- <a href="https://gitee.com/chuange" target="_blank">@川歌</a>
- @华仔
#### 💌 支持作者
如果觉得框架不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/lyt-Top/vue-next-admin">Github</a> 或者
<a target="_blank" href="https://gitee.com/lyt-top/vue-next-admin">Gitee</a> 帮我点个 ⭐ Star这将是对我极大的鼓励与支持。

View File

@@ -6,23 +6,23 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
name="keywords" name="keywords"
content="vue-next-adminvue-prev-adminvue-admin-wonderful后台管理系统一站式平台模板希望可以帮你完成快速开发。vue2.xvue2.0vue2vue3vue3.xvue3.0CompositionAPItypescriptelement pluselementplusadminwonderfulwonderful-nextvue-next-adminvitevite-admin快速高效后台模板后台系统管理系统" content="django-vue3-admin"
/> />
<meta <meta
name="description" name="description"
content="vue-next-admin基于 vue3 + CompositionAPI + typescript + vite + element plus适配手机、平板、pc 的后台开源免费管理系统模板vue-prev-admin基于 vue2 + element ui适配手机、平板、pc 的后台开源免费管理系统模板" content="django-vue3-admin基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!"
/> />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<title>vue-next-admin</title> <title>django-vue3-admin</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="text/javascript"> <script type="text/javascript">
var _hmt = _hmt || []; // let _hmt = _hmt || [];
(function () { (function () {
var hm = document.createElement('script'); let hm = document.createElement('script');
hm.src = 'https://hm.baidu.com/hm.js?d9c8b87d10717013641458b300c552e4'; hm.src = 'https://hm.baidu.com/hm.js?d9c8b87d10717013641458b300c552e4';
var s = document.getElementsByTagName('script')[0]; let s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s); s.parentNode.insertBefore(hm, s);
})(); })();
</script> </script>

4748
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +1,89 @@
{ {
"name": "vue-next-admin", "name": "django-vue3-admin",
"version": "2.2.0", "version": "1.0.0",
"description": "vue3 vite next admin template", "description": "django-vue3-admin基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!",
"author": "lyt_20201208", "license": "MIT",
"license": "MIT", "scripts": {
"scripts": { "dev": "vite --force",
"dev": "vite --force", "build": "vite build",
"build": "vite build", "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" },
}, "dependencies": {
"dependencies": { "@element-plus/icons-vue": "^2.0.10",
"@element-plus/icons-vue": "^2.0.6", "@fast-crud/fast-crud": "^1.9.0",
"@fast-crud/fast-crud": "^1.4.2", "@fast-crud/fast-extends": "^1.9.0",
"@fast-crud/ui-element": "^1.4.2", "@fast-crud/ui-element": "^1.9.0",
"@vitejs/plugin-vue-jsx": "^2.1.0", "@vitejs/plugin-vue-jsx": "^3.0.0",
"@wangeditor/editor": "^5.1.11", "@wangeditor/editor": "^5.1.23",
"axios": "^0.27.2", "@wangeditor/editor-for-vue": "^5.1.12",
"countup.js": "^2.3.2", "axios": "^1.2.1",
"cropperjs": "^1.5.12", "countup.js": "^2.3.2",
"echarts": "^5.3.3", "cropperjs": "^1.5.13",
"echarts-gl": "^2.0.9", "e-icon-picker": "^2.1.1",
"echarts-wordcloud": "^2.0.0", "echarts": "^5.4.1",
"element-plus": "^2.2.9", "echarts-gl": "^2.0.9",
"js-cookie": "^3.0.1", "echarts-wordcloud": "^2.1.0",
"jsplumb": "^2.15.6", "element-plus": "^2.2.26",
"lodash-es": "^4.17.21", "font-awesome": "^4.7.0",
"mitt": "^3.0.0", "js-cookie": "^3.0.1",
"nprogress": "^0.2.0", "js-table2excel": "^1.0.3",
"pinia": "^2.0.16", "jsplumb": "^2.15.6",
"print-js": "^1.6.0", "mitt": "^3.0.0",
"qrcodejs2-fixes": "^0.0.2", "nprogress": "^0.2.0",
"screenfull": "^6.0.2", "pinia": "^2.0.28",
"sortablejs": "^1.15.0", "pinia-plugin-persist": "^1.0.0",
"splitpanes": "^3.1.1", "print-js": "^1.6.0",
"vue": "^3.2.37", "qrcodejs2-fixes": "^0.0.2",
"vue-clipboard3": "^2.0.0", "qs": "^6.11.0",
"vue-grid-layout": "^3.0.0-beta1", "screenfull": "^6.0.2",
"vue-i18n": "^9.1.10", "sortablejs": "^1.15.0",
"vue-router": "^4.1.2" "splitpanes": "^3.1.5",
}, "ts-md5": "^1.3.1",
"devDependencies": { "vue": "^3.2.45",
"@types/node": "^18.0.6", "vue-clipboard3": "^2.0.0",
"@types/nprogress": "^0.2.0", "vue-grid-layout": "^3.0.0-beta1",
"@types/sortablejs": "^1.13.0", "vue-i18n": "^9.2.2",
"@typescript-eslint/eslint-plugin": "^5.30.7", "vue-router": "^4.1.6",
"@typescript-eslint/parser": "^5.30.7", "vxe-table": "^4.3.10",
"@vitejs/plugin-vue": "^2.3.3", "xe-utils": "^3.5.7"
"@vue/compiler-sfc": "^3.2.37", },
"dotenv": "^16.0.1", "devDependencies": {
"eslint": "^8.20.0", "@types/node": "^18.11.13",
"eslint-plugin-vue": "^9.2.0", "@types/nprogress": "^0.2.0",
"prettier": "^2.7.1", "@types/sortablejs": "^1.15.0",
"sass": "^1.53.0", "@typescript-eslint/eslint-plugin": "^5.46.0",
"sass-loader": "^13.0.2", "@typescript-eslint/parser": "^5.46.0",
"typescript": "^4.7.4", "@vitejs/plugin-vue": "^4.0.0",
"vite": "^3.0.0", "@vue/compiler-sfc": "^3.2.45",
"vue-eslint-parser": "^9.0.3" "eslint": "^8.29.0",
}, "eslint-plugin-vue": "^9.8.0",
"browserslist": [ "prettier": "^2.8.1",
"> 1%", "sass": "^1.56.2",
"last 2 versions", "typescript": "^4.9.4",
"not dead" "vite": "^4.0.0",
], "vite-plugin-vue-setup-extend": "^0.4.0",
"bugs": { "vue-eslint-parser": "^9.1.0"
"url": "https://gitee.com/lyt-top/vue-next-admin/issues" },
}, "browserslist": [
"engines": { "> 1%",
"node": ">=12.0.0", "last 2 versions",
"npm": ">= 6.0.0" "not dead"
}, ],
"keywords": [ "engines": {
"vue", "node": ">=16.0.0",
"vue3", "npm": ">= 7.0.0"
"vuejs/vue-next", },
"element-ui", "keywords": [
"element-plus", "vue",
"vue-next-admin", "vue3",
"next-admin" "element-ui",
], "element-plus",
"repository": { "django-vue3-admin",
"type": "git", "django",
"url": "https://gitee.com/lyt-top/vue-next-admin.git" "django-restframework"
} ],
"repository": {
"type": "git",
"url": "https://gitee.com/huge-dream/django-vue3-admin.git"
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,96 +1,120 @@
<template> <template>
<el-config-provider :size="getGlobalComponentSize" :locale="i18nLocale"> <el-config-provider :size="getGlobalComponentSize" :locale="getGlobalI18n">
<router-view v-show="themeConfig.lockScreenTime > 1" /> <router-view v-show="themeConfig.lockScreenTime > 1" />
<LockScreen v-if="themeConfig.isLockScreen" /> <LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime > 1" /> <Setings ref="setingsRef" v-show="themeConfig.lockScreenTime > 1" />
<CloseFull v-if="!themeConfig.isLockScreen" /> <CloseFull v-if="!themeConfig.isLockScreen" />
<Upgrade v-if="getVersion" />
</el-config-provider> </el-config-provider>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="app">
import { computed, ref, getCurrentInstance, onBeforeMount, onMounted, onUnmounted, nextTick, defineComponent, watch, reactive, toRefs } from 'vue'; import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch,onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other'; import other from '/@/utils/other';
import { Local, Session } from '/@/utils/storage'; import { Local, Session } from '/@/utils/storage';
import mittBus from '/@/utils/mitt';
import setIntroduction from '/@/utils/setIconfont'; import setIntroduction from '/@/utils/setIconfont';
import LockScreen from '/@/layout/lockScreen/index.vue';
import Setings from '/@/layout/navBars/breadcrumb/setings.vue';
import CloseFull from '/@/layout/navBars/breadcrumb/closeFull.vue';
export default defineComponent({ // 引入组件
name: 'app', const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
components: { LockScreen, Setings, CloseFull }, const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue'));
setup() { const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue'));
const { proxy } = <any>getCurrentInstance(); const Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.vue'));
const setingsRef = ref();
const route = useRoute(); // 定义变量内容
const stores = useTagsViewRoutes(); const { messages, locale } = useI18n();
const storesThemeConfig = useThemeConfig(); const setingsRef = ref();
const { themeConfig } = storeToRefs(storesThemeConfig); const route = useRoute();
const state = reactive({ const stores = useTagsViewRoutes();
i18nLocale: null, const storesThemeConfig = useThemeConfig();
}); const { themeConfig } = storeToRefs(storesThemeConfig);
// 获取全局组件大小 import websocket from "/@/utils/websocket";
const getGlobalComponentSize = computed(() => { import {ElNotification} from "element-plus";
return other.globalComponentSize(); // 获取版本号
}); const getVersion = computed(() => {
// 布局配置弹窗打开 let isVersion = false;
const openSetingsDrawer = () => { if (route.path !== '/login') {
setingsRef.value.openDrawer(); // @ts-ignore
}; if ((Local.get('version') && Local.get('version') !== __VERSION__) || !Local.get('version')) isVersion = true;
// 设置初始化,防止刷新时恢复默认 }
onBeforeMount(() => { return isVersion;
// 设置批量第三方 icon 图标
setIntroduction.cssCdn();
// 设置批量第三方 js
setIntroduction.jsCdn();
});
// 页面加载时
onMounted(() => {
nextTick(() => {
// 监听布局配置弹窗点击打开
proxy.mittBus.on('openSetingsDrawer', () => {
openSetingsDrawer();
});
// 设置 i18nApp.vue 中的 el-config-provider
proxy.mittBus.on('getI18nConfig', (locale: string) => {
(state.i18nLocale as string | null) = locale;
});
// 获取缓存中的布局配置
if (Local.get('themeConfig')) {
storesThemeConfig.setThemeConfig(Local.get('themeConfig'));
document.documentElement.style.cssText = Local.get('themeConfigStyle');
}
// 获取缓存中的全屏配置
if (Session.get('isTagsViewCurrenFull')) {
stores.setCurrenFullscreen(Session.get('isTagsViewCurrenFull'));
}
});
});
// 页面销毁时,关闭监听布局配置/i18n监听
onUnmounted(() => {
proxy.mittBus.off('openSetingsDrawer', () => {});
proxy.mittBus.off('getI18nConfig', () => {});
});
// 监听路由的变化,设置网站标题
watch(
() => route.path,
() => {
other.useTitle();
},
{
deep: true,
}
);
return {
themeConfig,
setingsRef,
getGlobalComponentSize,
...toRefs(state),
};
},
}); });
// 获取全局组件大小
const getGlobalComponentSize = computed(() => {
return other.globalComponentSize();
});
// 获取全局 i18n
const getGlobalI18n = computed(() => {
return messages.value[locale.value];
});
// 设置初始化,防止刷新时恢复默认
onBeforeMount(() => {
// 设置批量第三方 icon 图标
setIntroduction.cssCdn();
// 设置批量第三方 js
setIntroduction.jsCdn();
//websockt 模块
websocket.init(wsReceive)
});
// 页面加载时
onMounted(() => {
nextTick(() => {
// 监听布局配'置弹窗点击打开
mittBus.on('openSetingsDrawer', () => {
setingsRef.value.openDrawer();
});
// 获取缓存中的布局配置
if (Local.get('themeConfig')) {
storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') });
document.documentElement.style.cssText = Local.get('themeConfigStyle');
}
// 获取缓存中的全屏配置
if (Session.get('isTagsViewCurrenFull')) {
stores.setCurrenFullscreen(Session.get('isTagsViewCurrenFull'));
}
});
});
// 页面销毁时,关闭监听布局配置/i18n监听
onUnmounted(() => {
mittBus.off('openSetingsDrawer', () => {});
});
// 监听路由的变化,设置网站标题
watch(
() => route.path,
() => {
other.useTitle();
},
{
deep: true,
}
);
// websocket相关代码
import {messageCenterStore} from "/@/stores/messageCenter";
const wsReceive = (message: any) => {
const data = JSON.parse(message.data)
const { unread } = data
const messageCenter = messageCenterStore()
messageCenter.setUnread(unread);
if (data.contentType === 'SYSTEM') {
ElNotification({
title: '系统消息',
message: data.content,
type: 'success',
position: 'bottom-right',
duration: 5000
})
}
}
onBeforeUnmount(() => {
// 关闭连接
websocket.close()
})
</script> </script>

View File

@@ -1,4 +1,4 @@
import request from '/@/utils/request'; import { request } from "/@/utils/service";
/** /**
* 后端控制菜单模拟json路径在 https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu * 后端控制菜单模拟json路径在 https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu
@@ -8,6 +8,13 @@ import request from '/@/utils/request';
*/ */
export function useMenuApi() { export function useMenuApi() {
return { return {
getSystemMenu: (params?: object) => {
return request({
url: '/api/system/menu/web_router/',
method: 'get',
params,
});
},
getMenuAdmin: (params?: object) => { getMenuAdmin: (params?: object) => {
return request({ return request({
url: '/gitee/lyt-top/vue-next-admin-images/raw/master/menu/adminMenu.json', url: '/gitee/lyt-top/vue-next-admin-images/raw/master/menu/adminMenu.json',

View File

@@ -0,0 +1,55 @@
@font-face {
font-family: "iconfont"; /* Project id 3882322 */
src: url('iconfont.woff2?t=1676892176164') format('woff2'),
url('iconfont.woff?t=1676892176164') format('woff'),
url('iconfont.ttf?t=1676892176164') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-xiaoxizhongxin:before {
content: "\e665";
}
.icon-xitongshezhi:before {
content: "\e7ba";
}
.icon-caozuorizhi:before {
content: "\e611";
}
.icon-guanlidenglurizhi:before {
content: "\ea45";
}
.icon-rizhi:before {
content: "\e60c";
}
.icon-system:before {
content: "\e684";
}
.icon-Area:before {
content: "\eaa2";
}
.icon-file:before {
content: "\e671";
}
.icon-dict:before {
content: "\e626";
}
.icon-configure:before {
content: "\e733";
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,79 @@
{
"id": "3882322",
"name": "dvadmin3",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "dvadmin3前端项目icon",
"glyphs": [
{
"icon_id": "630252",
"name": "消息中心",
"font_class": "xiaoxizhongxin",
"unicode": "e665",
"unicode_decimal": 58981
},
{
"icon_id": "10465939",
"name": "系统设置",
"font_class": "xitongshezhi",
"unicode": "e7ba",
"unicode_decimal": 59322
},
{
"icon_id": "13952258",
"name": "操作日志",
"font_class": "caozuorizhi",
"unicode": "e611",
"unicode_decimal": 58897
},
{
"icon_id": "26876046",
"name": "管理登录日志",
"font_class": "guanlidenglurizhi",
"unicode": "ea45",
"unicode_decimal": 59973
},
{
"icon_id": "4520330",
"name": "日志",
"font_class": "rizhi",
"unicode": "e60c",
"unicode_decimal": 58892
},
{
"icon_id": "11686866",
"name": "system",
"font_class": "system",
"unicode": "e684",
"unicode_decimal": 59012
},
{
"icon_id": "12004865",
"name": "Area",
"font_class": "Area",
"unicode": "eaa2",
"unicode_decimal": 60066
},
{
"icon_id": "15838480",
"name": "file",
"font_class": "file",
"unicode": "e671",
"unicode_decimal": 58993
},
{
"icon_id": "16880980",
"name": "dict",
"font_class": "dict",
"unicode": "e626",
"unicode_decimal": 58918
},
{
"icon_id": "32434794",
"name": "configure",
"font_class": "configure",
"unicode": "e733",
"unicode_decimal": 59187
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 212 KiB

View File

@@ -2,29 +2,25 @@
<slot v-if="getUserAuthBtnList" /> <slot v-if="getUserAuthBtnList" />
</template> </template>
<script lang="ts"> <script setup lang="ts" name="auth">
import { computed, defineComponent } from 'vue'; import { computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useUserInfo } from '/@/stores/userInfo'; import { useUserInfo } from '/@/stores/userInfo';
export default defineComponent({ // 定义父组件传过来的值
name: 'auth', const props = defineProps({
props: { value: {
value: { type: String,
type: String, default: () => '',
default: () => '',
},
},
setup(props) {
const stores = useUserInfo();
const { userInfos } = storeToRefs(stores);
// 获取 vuex 中的用户权限
const getUserAuthBtnList = computed(() => {
return userInfos.value.authBtnList.some((v: string) => v === props.value);
});
return {
getUserAuthBtnList,
};
}, },
}); });
// 定义变量内容
const stores = useUserInfo();
const { userInfos } = storeToRefs(stores);
// 获取 pinia 中的用户权限
const getUserAuthBtnList = computed(() => {
return userInfos.value.authBtnList.some((v: string) => v === props.value);
});
</script> </script>

View File

@@ -2,30 +2,26 @@
<slot v-if="getUserAuthBtnList" /> <slot v-if="getUserAuthBtnList" />
</template> </template>
<script lang="ts"> <script setup lang="ts" name="authAll">
import { computed, defineComponent } from 'vue'; import { computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useUserInfo } from '/@/stores/userInfo'; import { useUserInfo } from '/@/stores/userInfo';
import { judementSameArr } from '/@/utils/arrayOperation'; import { judementSameArr } from '/@/utils/arrayOperation';
export default defineComponent({ // 定义父组件传过来的值
name: 'authAll', const props = defineProps({
props: { value: {
value: { type: Array,
type: Array, default: () => [],
default: () => [],
},
},
setup(props) {
const stores = useUserInfo();
const { userInfos } = storeToRefs(stores);
// 获取 pinia 中的用户权限
const getUserAuthBtnList = computed(() => {
return judementSameArr(props.value, userInfos.value.authBtnList);
});
return {
getUserAuthBtnList,
};
}, },
}); });
// 定义变量内容
const stores = useUserInfo();
const { userInfos } = storeToRefs(stores);
// 获取 pinia 中的用户权限
const getUserAuthBtnList = computed(() => {
return judementSameArr(props.value, userInfos.value.authBtnList);
});
</script> </script>

View File

@@ -2,35 +2,31 @@
<slot v-if="getUserAuthBtnList" /> <slot v-if="getUserAuthBtnList" />
</template> </template>
<script lang="ts"> <script setup lang="ts" name="auths">
import { computed, defineComponent } from 'vue'; import { computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useUserInfo } from '/@/stores/userInfo'; import { useUserInfo } from '/@/stores/userInfo';
export default defineComponent({ // 定义父组件传过来的值
name: 'auths', const props = defineProps({
props: { value: {
value: { type: Array,
type: Array, default: () => [],
default: () => [],
},
},
setup(props) {
const stores = useUserInfo();
const { userInfos } = storeToRefs(stores);
// 获取 vuex 中的用户权限
const getUserAuthBtnList = computed(() => {
let flag = false;
userInfos.value.authBtnList.map((val: string) => {
props.value.map((v) => {
if (val === v) flag = true;
});
});
return flag;
});
return {
getUserAuthBtnList,
};
}, },
}); });
// 定义变量内容
const stores = useUserInfo();
const { userInfos } = storeToRefs(stores);
// 获取 pinia 中的用户权限
const getUserAuthBtnList = computed(() => {
let flag = false;
userInfos.value.authBtnList.map((val: string) => {
props.value.map((v) => {
if (val === v) flag = true;
});
});
return flag;
});
</script> </script>

View File

@@ -1,21 +1,21 @@
<template> <template>
<div> <div>
<el-dialog title="更换头像" v-model="isShowDialog" width="769px"> <el-dialog title="更换头像" v-model="state.isShowDialog" width="769px">
<div class="cropper-warp"> <div class="cropper-warp">
<div class="cropper-warp-left"> <div class="cropper-warp-left">
<img :src="cropperImg" class="cropper-warp-left-img" /> <img :src="state.cropperImg" class="cropper-warp-left-img" />
</div> </div>
<div class="cropper-warp-right"> <div class="cropper-warp-right">
<div class="cropper-warp-right-title">预览</div> <div class="cropper-warp-right-title">预览</div>
<div class="cropper-warp-right-item"> <div class="cropper-warp-right-item">
<div class="cropper-warp-right-value"> <div class="cropper-warp-right-value">
<img :src="cropperImgBase64" class="cropper-warp-right-value-img" /> <img :src="state.cropperImgBase64" class="cropper-warp-right-value-img" />
</div> </div>
<div class="cropper-warp-right-label">100 x 100</div> <div class="cropper-warp-right-label">100 x 100</div>
</div> </div>
<div class="cropper-warp-right-item"> <div class="cropper-warp-right-item">
<div class="cropper-warp-right-value"> <div class="cropper-warp-right-value">
<img :src="cropperImgBase64" class="cropper-warp-right-value-img cropper-size" /> <img :src="state.cropperImgBase64" class="cropper-warp-right-value-img cropper-size" />
</div> </div>
<div class="cropper-warp-right-label">50 x 50</div> <div class="cropper-warp-right-label">50 x 50</div>
</div> </div>
@@ -31,66 +31,60 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="cropper">
import { reactive, toRefs, nextTick, defineComponent } from 'vue'; import { reactive, nextTick } from 'vue';
import Cropper from 'cropperjs'; import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.css'; import 'cropperjs/dist/cropper.css';
export default defineComponent({ // 定义变量内容
name: 'cropperIndex', const state = reactive({
setup() { isShowDialog: false,
const state = reactive({ cropperImg: '',
isShowDialog: false, cropperImgBase64: '',
cropperImg: '', cropper: '' as RefType,
cropperImgBase64: '', });
cropper: null,
}); // 打开弹窗
// 打开弹窗 const openDialog = (imgs: string) => {
const openDialog = (imgs: any) => { state.cropperImg = imgs;
state.cropperImg = imgs; state.isShowDialog = true;
state.isShowDialog = true; nextTick(() => {
nextTick(() => { initCropper();
initCropper(); });
}); };
}; // 关闭弹窗
// 关闭弹窗 const closeDialog = () => {
const closeDialog = () => { state.isShowDialog = false;
state.isShowDialog = false; };
}; // 取消
// 取消 const onCancel = () => {
const onCancel = () => { closeDialog();
closeDialog(); };
}; // 更换
// 更换 const onSubmit = () => {
const onSubmit = () => { // state.cropperImgBase64 = state.cropper.getCroppedCanvas().toDataURL('image/jpeg');
// state.cropperImgBase64 = state.cropper.getCroppedCanvas().toDataURL('image/jpeg'); };
}; // 初始化cropperjs图片裁剪
// 初始化cropperjs图片裁剪 const initCropper = () => {
const initCropper = () => { const letImg = <HTMLImageElement>document.querySelector('.cropper-warp-left-img');
const letImg: any = document.querySelector('.cropper-warp-left-img'); state.cropper = new Cropper(letImg, {
(<any>state.cropper) = new Cropper(letImg, { viewMode: 1,
viewMode: 1, dragMode: 'none',
dragMode: 'none', initialAspectRatio: 1,
initialAspectRatio: 1, aspectRatio: 1,
aspectRatio: 1, preview: '.before',
preview: '.before', background: false,
background: false, autoCropArea: 0.6,
autoCropArea: 0.6, zoomOnWheel: false,
zoomOnWheel: false, crop: () => {
crop: () => { state.cropperImgBase64 = state.cropper.getCroppedCanvas().toDataURL('image/jpeg');
state.cropperImgBase64 = (<any>state.cropper).getCroppedCanvas().toDataURL('image/jpeg'); },
}, });
}); };
};
return { // 暴露变量
openDialog, defineExpose({
closeDialog, openDialog,
onCancel,
onSubmit,
initCropper,
...toRefs(state),
};
},
}); });
</script> </script>

View File

@@ -1,115 +1,101 @@
<template> <template>
<div class="editor-container"> <div class="editor-container">
<div ref="editorToolbar"></div> <Toolbar :editor="editorRef" :mode="mode" />
<div ref="editorContent" :style="{ height }"></div> <Editor
:mode="mode"
:defaultConfig="state.editorConfig"
:style="{ height }"
v-model="state.editorVal"
@onCreated="handleCreated"
@onChange="handleChange"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="wngEditor">
import { toRefs, reactive, onMounted, watch, defineComponent } from 'vue'; // https://www.wangeditor.com/v5/for-frame.html#vue3
import { createEditor, createToolbar, IEditorConfig, IToolbarConfig, IDomEditor } from '@wangeditor/editor';
import '@wangeditor/editor/dist/css/style.css'; import '@wangeditor/editor/dist/css/style.css';
import { toolbarKeys } from './toolbar'; import { reactive, shallowRef, watch, onBeforeUnmount } from 'vue';
import { IDomEditor } from '@wangeditor/editor';
import { Toolbar, Editor } from '@wangeditor/editor-for-vue';
// 定义接口来定义对象的类型 // 定义父组件传过来的值
interface WangeditorState { const props = defineProps({
editorToolbar: HTMLDivElement | null; // 是否禁用
editorContent: HTMLDivElement | null; disable: {
editor: any; type: Boolean,
} default: () => false,
export default defineComponent({
name: 'wngEditor',
props: {
// 节点 id
id: {
type: String,
default: () => 'wangeditor',
},
// 是否禁用
isDisable: {
type: Boolean,
default: () => false,
},
// 内容框默认 placeholder
placeholder: {
type: String,
default: () => '请输入内容',
},
// 双向绑定:双向绑定值,字段名为固定,改了之后将不生效
// 参考https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
modelValue: String,
// https://www.wangeditor.com/v5/getting-started.html#mode-%E6%A8%A1%E5%BC%8F
// 模式,可选 <default|simple>,默认 default
mode: {
type: String,
default: () => 'default',
},
// 高度
height: {
type: String,
default: () => '310px',
},
}, },
setup(props, { emit }) { // 内容框默认 placeholder
const state = reactive<WangeditorState>({ placeholder: {
editorToolbar: null, type: String,
editor: null, default: () => '请输入内容...',
editorContent: null,
});
// 富文本配置
const wangeditorConfig = () => {
const editorConfig: Partial<IEditorConfig> = { MENU_CONF: {} };
props.isDisable ? (editorConfig.readOnly = true) : (editorConfig.readOnly = false);
editorConfig.placeholder = props.placeholder;
editorConfig.onChange = (editor: IDomEditor) => {
// console.log('content', editor.children);
// console.log('html', editor.getHtml());
emit('update:modelValue', editor.getHtml());
};
(<any>editorConfig).MENU_CONF['uploadImage'] = {
base64LimitSize: 10 * 1024 * 1024,
};
return editorConfig;
};
//
const toolbarConfig = () => {
const toolbarConfig: Partial<IToolbarConfig> = {};
toolbarConfig.toolbarKeys = toolbarKeys;
return toolbarConfig;
};
// 初始化富文本
// https://www.wangeditor.com/
const initWangeditor = () => {
state.editor = createEditor({
html: props.modelValue,
selector: state.editorContent!,
config: wangeditorConfig(),
mode: props.mode,
});
createToolbar({
editor: state.editor,
selector: state.editorToolbar!,
mode: props.mode,
config: toolbarConfig(),
});
};
// 页面加载时
onMounted(() => {
initWangeditor();
});
// 监听双向绑定值的改变
// https://gitee.com/lyt-top/vue-next-admin/issues/I4LM7I
watch(
() => props.modelValue,
(value) => {
state.editor.clear();
state.editor.dangerouslyInsertHtml(value);
}
);
return {
...toRefs(state),
};
}, },
// https://www.wangeditor.com/v5/getting-started.html#mode-%E6%A8%A1%E5%BC%8F
// 模式,可选 <default|simple>,默认 default
mode: {
type: String,
default: () => 'default',
},
// 高度
height: {
type: String,
default: () => '310px',
},
// 双向绑定,用于获取 editor.getHtml()
getHtml: String,
// 双向绑定,用于获取 editor.getText()
getText: String,
}); });
// 定义子组件向父组件传值/事件
const emit = defineEmits(['update:getHtml', 'update:getText']);
// 定义变量内容
const editorRef = shallowRef();
const state = reactive({
editorConfig: {
placeholder: props.placeholder,
},
editorVal: props.getHtml,
});
// 编辑器回调函数
const handleCreated = (editor: IDomEditor) => {
editorRef.value = editor;
};
// 编辑器内容改变时
const handleChange = (editor: IDomEditor) => {
emit('update:getHtml', editor.getHtml());
emit('update:getText', editor.getText());
};
// 页面销毁时
onBeforeUnmount(() => {
const editor = editorRef.value;
if (editor == null) return;
editor.destroy();
});
// 监听是否禁用改变
// https://gitee.com/lyt-top/vue-next-admin/issues/I4LM7I
watch(
() => props.disable,
(bool) => {
const editor = editorRef.value;
if (editor == null) return;
bool ? editor.disable() : editor.enable();
},
{
deep: true,
}
);
// 监听双向绑定值改变,用于回显
watch(
() => props.getHtml,
(val) => {
state.editorVal = val;
},
{
deep: true,
}
);
</script> </script>

View File

@@ -0,0 +1,41 @@
<template>
<!-- 你的自定义受控组件-->
<div>
<el-tag :type="randomType">{{ data }}</el-tag>
</div>
</template>
<script lang="ts" setup>
import {watch, ref} from "vue";
const props = defineProps({
modelValue: String || Object,
displayLabel: {
type:String,
default: ""
}
})
// template上使用data
const data = ref()
watch(() => {
return props.modelValue
}, // 监听modelValue的变化
(value) => {
if (typeof value === "string") {
data.value = value
} else if (typeof value === "object") {
const {displayLabel} = props
data.value = value ? value[displayLabel] : null
} else {
data.value = null
}
}, // 当modelValue值触发后同步修改data.value的值
{immediate: true} // 立即触发一次给data赋值初始值
)
const tagType = ['success', 'info', 'warning', 'danger']
const randomType = (): string => {
return tagType[Math.floor(Math.random() * tagType.length)];
}
</script>

View File

@@ -1,252 +1,241 @@
<template> <template>
<div class="icon-selector w100 h100"> <div class="icon-selector w100 h100">
<el-input
v-model="state.fontIconSearch"
:placeholder="state.fontIconPlaceholder"
:clearable="clearable"
:disabled="disabled"
:size="size"
ref="inputWidthRef"
@clear="onClearFontIcon"
@focus="onIconFocus"
@blur="onIconBlur"
>
<template #prepend>
<SvgIcon
:name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix"
class="font14"
v-if="state.fontIconPrefix === '' ? prepend?.indexOf('ele-') > -1 : state.fontIconPrefix?.indexOf('ele-') > -1"
/>
<i v-else :class="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="font14"></i>
</template>
</el-input>
<el-popover <el-popover
placement="bottom" placement="bottom"
:width="fontIconWidth" :width="state.fontIconWidth"
trigger="click"
transition="el-zoom-in-top" transition="el-zoom-in-top"
popper-class="icon-selector-popper" popper-class="icon-selector-popper"
@show="onPopoverShow" trigger="click"
:virtual-ref="inputWidthRef"
virtual-triggering
> >
<template #reference>
<el-input
v-model="fontIconSearch"
:placeholder="fontIconPlaceholder"
:clearable="clearable"
:disabled="disabled"
:size="size"
ref="inputWidthRef"
@clear="onClearFontIcon"
@focus="onIconFocus"
@blur="onIconBlur"
>
<template #prepend>
<SvgIcon
:name="fontIconPrefix === '' ? prepend : fontIconPrefix"
class="font14"
v-if="fontIconPrefix === '' ? prepend?.indexOf('ele-') > -1 : fontIconPrefix?.indexOf('ele-') > -1"
/>
<i v-else :class="fontIconPrefix === '' ? prepend : fontIconPrefix" class="font14"></i>
</template>
</el-input>
</template>
<template #default> <template #default>
<div class="icon-selector-warp"> <div class="icon-selector-warp">
<div class="icon-selector-warp-title flex"> <div class="icon-selector-warp-title">{{ title }}</div>
<div class="flex-auto">{{ title }}</div> <el-tabs v-model="state.fontIconTabActive" @tab-click="onIconClick">
<div class="icon-selector-warp-title-tab" v-if="type === 'all'"> <el-tab-pane lazy label="ali" name="ali">
<span :class="{ 'span-active': fontIconType === 'ali' }" @click="onIconChange('ali')" class="ml10" title="iconfont 图标">ali</span> <IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
<span :class="{ 'span-active': fontIconType === 'ele' }" @click="onIconChange('ele')" class="ml10" title="elementPlus 图标">ele</span> </el-tab-pane>
<span :class="{ 'span-active': fontIconType === 'awe' }" @click="onIconChange('awe')" class="ml10" title="fontawesome 图标">awe</span> <el-tab-pane lazy label="ele" name="ele">
</div> <IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
</div> </el-tab-pane>
<div class="icon-selector-warp-row"> <el-tab-pane lazy label="awe" name="awe">
<el-scrollbar ref="selectorScrollbarRef"> <IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
<el-row :gutter="10" v-if="fontIconSheetsFilterList.length > 0"> </el-tab-pane>
<el-col :xs="6" :sm="4" :md="4" :lg="4" :xl="4" @click="onColClick(v)" v-for="(v, k) in fontIconSheetsFilterList" :key="k"> </el-tabs>
<div class="icon-selector-warp-item" :class="{ 'icon-selector-active': fontIconPrefix === v }">
<div class="flex-margin">
<div class="icon-selector-warp-item-value">
<SvgIcon :name="v" />
</div>
</div>
</div>
</el-col>
</el-row>
<el-empty :image-size="100" v-if="fontIconSheetsFilterList.length <= 0" :description="emptyDescription"></el-empty>
</el-scrollbar>
</div>
</div> </div>
</template> </template>
</el-popover> </el-popover>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="iconSelector">
import { ref, toRefs, reactive, onMounted, nextTick, computed, watch, defineComponent } from 'vue'; import { defineAsyncComponent, ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
import type { TabsPaneContext } from 'element-plus';
import initIconfont from '/@/utils/getStyleSheets'; import initIconfont from '/@/utils/getStyleSheets';
import '/@/theme/iconSelector.scss';
export default defineComponent({ // 定义父组件传过来的值
name: 'iconSelector', const props = defineProps({
emits: ['update:modelValue', 'get', 'clear'], // 输入框前置内容
props: { prepend: {
// 输入框前置内容 type: String,
prepend: { default: () => 'ele-Pointer',
type: String,
default: () => 'ele-Pointer',
},
// 输入框占位文本
placeholder: {
type: String,
default: () => '请输入内容搜索图标或者选择图标',
},
// 输入框占位文本
size: {
type: String,
default: () => 'default',
},
// 弹窗标题
title: {
type: String,
default: () => '请选择图标',
},
// icon 图标类型
type: {
type: String,
default: () => 'ele',
},
// 禁用
disabled: {
type: Boolean,
default: () => false,
},
// 是否可清空
clearable: {
type: Boolean,
default: () => true,
},
// 自定义空状态描述文字
emptyDescription: {
type: String,
default: () => '无相关图标',
},
// 双向绑定值,默认为 modelValue
// 参考https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
// 参考https://v3.cn.vuejs.org/guide/component-custom-events.html#%E5%A4%9A%E4%B8%AA-v-model-%E7%BB%91%E5%AE%9A
modelValue: String,
}, },
setup(props, { emit }) { // 输入框占位文本
const inputWidthRef = ref(); placeholder: {
const selectorScrollbarRef = ref(); type: String,
const state = reactive({ default: () => '请输入内容搜索图标或者选择图标',
fontIconPrefix: '', },
fontIconWidth: 0, // 输入框占位文本
fontIconSearch: '', size: {
fontIconTabsIndex: 0, type: String,
fontIconSheetsList: [], default: () => 'default',
fontIconPlaceholder: '', },
fontIconType: 'ali', // 弹窗标题
fontIconShow: true, title: {
}); type: String,
// 处理 input 获取焦点时modelValue 有值时,改变 input 的 placeholder 值 default: () => '请选择图标',
const onIconFocus = () => { },
if (!props.modelValue) return false; // 禁用
state.fontIconSearch = ''; disabled: {
state.fontIconPlaceholder = props.modelValue; type: Boolean,
}; default: () => false,
// 处理 input 失去焦点时,为空将清空 input 值,为点击选中图标时,将取原先值 },
const onIconBlur = () => { // 是否可清空
setTimeout(() => { clearable: {
const icon = state.fontIconSheetsList.filter((icon: string) => icon === state.fontIconSearch); type: Boolean,
if (icon.length <= 0) state.fontIconSearch = ''; default: () => true,
}, 300); },
}; // 自定义空状态描述文字
// 处理 icon 双向绑定数值回显 emptyDescription: {
const initModeValueEcho = () => { type: String,
if (props.modelValue === '') return ((<string | undefined>state.fontIconPlaceholder) = props.placeholder); default: () => '无相关图标',
(<string | undefined>state.fontIconPlaceholder) = props.modelValue; },
(<string | undefined>state.fontIconPrefix) = props.modelValue; // 双向绑定值,默认为 modelValue
}; // 参考https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
// 处理 icon type 类型为 all 时,类型 ali、ele、awe 回显问题 // 参考https://v3.cn.vuejs.org/guide/component-custom-events.html#%E5%A4%9A%E4%B8%AA-v-model-%E7%BB%91%E5%AE%9A
const initFontIconTypeEcho = () => { modelValue: String,
if ((<any>props.modelValue)?.indexOf('iconfont') > -1) onIconChange('ali'); });
else if ((<any>props.modelValue)?.indexOf('ele-') > -1) onIconChange('ele');
else if ((<any>props.modelValue)?.indexOf('fa') > -1) onIconChange('awe');
else onIconChange('ali');
};
// 图标搜索及图标数据显示
const fontIconSheetsFilterList = computed(() => {
if (!state.fontIconSearch) return state.fontIconSheetsList;
let search = state.fontIconSearch.trim().toLowerCase();
return state.fontIconSheetsList.filter((item: any) => {
if (item.toLowerCase().indexOf(search) !== -1) return item;
});
});
// 获取 input 的宽度
const getInputWidth = () => {
nextTick(() => {
state.fontIconWidth = inputWidthRef.value.$el.offsetWidth;
});
};
// 监听页面宽度改变
const initResize = () => {
window.addEventListener('resize', () => {
getInputWidth();
});
};
// 初始化数据
const initFontIconData = async (type: string) => {
state.fontIconSheetsList = [];
if (type === 'ali') {
await initIconfont.ali().then((res: any) => {
// 阿里字体图标使用 `iconfont xxx`
state.fontIconSheetsList = res.map((i: string) => `iconfont ${i}`);
});
} else if (type === 'ele') {
await initIconfont.ele().then((res: any) => {
state.fontIconSheetsList = res;
});
} else if (type === 'awe') {
await initIconfont.awe().then((res: any) => {
// fontawesome字体图标使用 `fa xxx`
state.fontIconSheetsList = res.map((i: string) => `fa ${i}`);
});
}
// 初始化 input 的 placeholder
// 参考单项数据流https://cn.vuejs.org/v2/guide/components-props.html?#%E5%8D%95%E5%90%91%E6%95%B0%E6%8D%AE%E6%B5%81
state.fontIconPlaceholder = props.placeholder;
// 初始化双向绑定回显
initModeValueEcho();
};
// 图标点击切换
const onIconChange = (type: string) => {
state.fontIconType = type;
initFontIconData(type);
};
// 获取当前点击的 icon 图标
const onColClick = (v: any) => {
state.fontIconPlaceholder = v;
state.fontIconPrefix = v;
emit('get', state.fontIconPrefix);
emit('update:modelValue', state.fontIconPrefix);
};
// 清空当前点击的 icon 图标
const onClearFontIcon = () => {
state.fontIconPrefix = '';
emit('clear', state.fontIconPrefix);
emit('update:modelValue', state.fontIconPrefix);
};
// 监听 Popover 打开,用于双向绑定值回显
const onPopoverShow = () => {
initModeValueEcho();
initFontIconTypeEcho();
};
// 页面加载时
onMounted(() => {
initModeValueEcho();
initResize();
getInputWidth();
});
// 监听双向绑定 modelValue 的变化 // 定义子组件向父组件传值/事件
watch( const emit = defineEmits(['update:modelValue', 'get', 'clear']);
() => props.modelValue,
() => { // 引入组件
initModeValueEcho(); const IconList = defineAsyncComponent(() => import('/@/components/iconSelector/list.vue'));
}
); // 定义变量内容
return { const inputWidthRef = ref();
inputWidthRef, const state = reactive({
selectorScrollbarRef, fontIconPrefix: '',
fontIconSheetsFilterList, fontIconWidth: 0,
onColClick, fontIconSearch: '',
onIconChange, fontIconPlaceholder: '',
onClearFontIcon, fontIconTabActive: 'ali',
onIconFocus, fontIconList: {
onIconBlur, ali: [],
onPopoverShow, ele: [],
...toRefs(state), awe: [],
};
}, },
}); });
// 处理 input 获取焦点时modelValue 有值时,改变 input 的 placeholder 值
const onIconFocus = () => {
if (!props.modelValue) return false;
state.fontIconSearch = '';
state.fontIconPlaceholder = props.modelValue;
};
// 处理 input 失去焦点时,为空将清空 input 值,为点击选中图标时,将取原先值
const onIconBlur = () => {
const list = fontIconTabNameList();
setTimeout(() => {
const icon = list.filter((icon: string) => icon === state.fontIconSearch);
if (icon.length <= 0) state.fontIconSearch = '';
}, 300);
};
// 图标搜索及图标数据显示
const fontIconSheetsFilterList = computed(() => {
const list = fontIconTabNameList();
if (!state.fontIconSearch) return list;
let search = state.fontIconSearch.trim().toLowerCase();
return list.filter((item: string) => {
if (item.toLowerCase().indexOf(search) !== -1) return item;
});
});
// 根据 tab name 类型设置图标
const fontIconTabNameList = () => {
let iconList: any = [];
if (state.fontIconTabActive === 'ali') iconList = state.fontIconList.ali;
else if (state.fontIconTabActive === 'ele') iconList = state.fontIconList.ele;
else if (state.fontIconTabActive === 'awe') iconList = state.fontIconList.awe;
return iconList;
};
// 处理 icon 双向绑定数值回显
const initModeValueEcho = () => {
if (props.modelValue === '') return ((<string | undefined>state.fontIconPlaceholder) = props.placeholder);
(<string | undefined>state.fontIconPlaceholder) = props.modelValue;
(<string | undefined>state.fontIconPrefix) = props.modelValue;
};
// 处理 icon 类型用于回显时tab 高亮与初始化数据
const initFontIconName = () => {
let name = 'ali';
if (props.modelValue!.indexOf('iconfont') > -1) name = 'ali';
else if (props.modelValue!.indexOf('ele-') > -1) name = 'ele';
else if (props.modelValue!.indexOf('fa') > -1) name = 'awe';
// 初始化 tab 高亮回显
state.fontIconTabActive = name;
return name;
};
// 初始化数据
const initFontIconData = async (name: string) => {
if (name === 'ali') {
// 阿里字体图标使用 `iconfont xxx`
if (state.fontIconList.ali.length > 0) return;
await initIconfont.ali().then((res: any) => {
state.fontIconList.ali = res.map((i: string) => `iconfont ${i}`);
});
} else if (name === 'ele') {
// element plus 图标
if (state.fontIconList.ele.length > 0) return;
await initIconfont.ele().then((res: any) => {
state.fontIconList.ele = res;
});
} else if (name === 'awe') {
// fontawesome字体图标使用 `fa xxx`
if (state.fontIconList.awe.length > 0) return;
await initIconfont.awe().then((res: any) => {
state.fontIconList.awe = res.map((i: string) => `fa ${i}`);
});
}
// 初始化 input 的 placeholder
// 参考单项数据流https://cn.vuejs.org/v2/guide/components-props.html?#%E5%8D%95%E5%90%91%E6%95%B0%E6%8D%AE%E6%B5%81
state.fontIconPlaceholder = props.placeholder;
// 初始化双向绑定回显
initModeValueEcho();
};
// 图标点击切换
const onIconClick = (pane: TabsPaneContext) => {
initFontIconData(pane.paneName as string);
inputWidthRef.value.focus();
};
// 获取当前点击的 icon 图标
const onColClick = (v: string) => {
state.fontIconPlaceholder = v;
state.fontIconPrefix = v;
emit('get', state.fontIconPrefix);
emit('update:modelValue', state.fontIconPrefix);
inputWidthRef.value.focus();
};
// 清空当前点击的 icon 图标
const onClearFontIcon = () => {
state.fontIconPrefix = '';
emit('clear', state.fontIconPrefix);
emit('update:modelValue', state.fontIconPrefix);
};
// 获取 input 的宽度
const getInputWidth = () => {
nextTick(() => {
state.fontIconWidth = inputWidthRef.value.$el.offsetWidth;
});
};
// 监听页面宽度改变
const initResize = () => {
window.addEventListener('resize', () => {
getInputWidth();
});
};
// 页面加载时
onMounted(() => {
initFontIconData(initFontIconName());
initResize();
getInputWidth();
});
// 监听双向绑定 modelValue 的变化
watch(
() => props.modelValue,
() => {
initModeValueEcho();
initFontIconName();
}
);
</script> </script>

View File

@@ -0,0 +1,84 @@
<template>
<div class="icon-selector-warp-row">
<el-scrollbar ref="selectorScrollbarRef">
<el-row :gutter="10" v-if="props.list.length > 0">
<el-col :xs="6" :sm="4" :md="4" :lg="4" :xl="4" v-for="(v, k) in list" :key="k" @click="onColClick(v)">
<div class="icon-selector-warp-item" :class="{ 'icon-selector-active': prefix === v }">
<SvgIcon :name="v" />
</div>
</el-col>
</el-row>
<el-empty :image-size="100" v-if="list.length <= 0" :description="empty"></el-empty>
</el-scrollbar>
</div>
</template>
<script setup lang="ts" name="iconSelectorList">
// 定义父组件传过来的值
const props = defineProps({
// 图标列表数据
list: {
type: Array,
default: () => [],
},
// 自定义空状态描述文字
empty: {
type: String,
default: () => '无相关图标',
},
// 高亮当前选中图标
prefix: {
type: String,
default: () => '',
},
});
// 定义子组件向父组件传值/事件
const emit = defineEmits(['get-icon']);
// 当前 icon 图标点击时
const onColClick = (v: unknown | string) => {
emit('get-icon', v);
};
</script>
<style scoped lang="scss">
.icon-selector-warp-row {
height: 230px;
overflow: hidden;
.el-row {
padding: 15px;
}
.el-scrollbar__bar.is-horizontal {
display: none;
}
.icon-selector-warp-item {
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--el-border-color);
border-radius: 5px;
margin-bottom: 10px;
height: 30px;
i {
font-size: 20px;
color: var(--el-text-color-regular);
}
&:hover {
cursor: pointer;
background-color: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary-light-5);
i {
color: var(--el-color-primary);
}
}
}
.icon-selector-active {
background-color: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary-light-5);
i {
color: var(--el-color-primary);
}
}
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<!-- 你的自定义受控组件-->
<div>
<el-tag class="many-to-many-tag" :type="randomType" v-for="(item,index) in data" :key="index">{{item}}</el-tag>
</div>
</template>
<script lang="ts" setup>
import {watch, ref} from "vue";
const props = defineProps({
modelValue: Array,
bindValue: Array,
displayLabel: {
type:String,
default: ""
}
})
// template上使用data
const data = ref()
watch(() => {
return props.bindValue
}, // 监听modelValue的变化
(value) => {
const {displayLabel} = props
const result = value ? value.map((item: any) => {
return item[displayLabel]
}) : null
data.value = result
}, // 当modelValue值触发后同步修改data.value的值
{immediate: true} // 立即触发一次给data赋值初始值
)
const tagType = ['success', 'info', 'warning', 'danger']
const randomType = (): string => {
return tagType[Math.floor(Math.random() * tagType.length)];
}
</script>
<style scoped>
.many-to-many-tag{
margin-right: 5px;
}
.many-to-many-tag:last-child {
margin-right: 0;
}
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="notice-bar" :style="{ background, height: `${height}px` }" v-show="!isMode"> <div class="notice-bar" :style="{ background, height: `${height}px` }" v-show="!state.isMode">
<div class="notice-bar-warp" :style="{ color, fontSize: `${size}px` }"> <div class="notice-bar-warp" :style="{ color, fontSize: `${size}px` }">
<i v-if="leftIcon" class="notice-bar-warp-left-icon" :class="leftIcon"></i> <i v-if="leftIcon" class="notice-bar-warp-left-icon" :class="leftIcon"></i>
<div class="notice-bar-warp-text-box" ref="noticeBarWarpRef"> <div class="notice-bar-warp-text-box" ref="noticeBarWarpRef">
@@ -11,139 +11,135 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="noticeBar">
import { toRefs, reactive, defineComponent, ref, onMounted, nextTick } from 'vue'; import { reactive, ref, onMounted, nextTick } from 'vue';
export default defineComponent({ // 定义父组件传过来的值
name: 'noticeBar', const props = defineProps({
props: { // 通知栏模式,可选值为 closeable link
// 通知栏模式,可选值为 closeable link mode: {
mode: { type: String,
type: String, default: () => '',
default: () => '',
},
// 通知文本内容
text: {
type: String,
default: () => '',
},
// 通知文本颜色
color: {
type: String,
default: () => 'var(--el-color-warning)',
},
// 通知背景色
background: {
type: String,
default: () => 'var(--el-color-warning-light-9)',
},
// 字体大小单位px
size: {
type: [Number, String],
default: () => 14,
},
// 通知栏高度单位px
height: {
type: Number,
default: () => 40,
},
// 动画延迟时间 (s)
delay: {
type: Number,
default: () => 1,
},
// 滚动速率 (px/s)
speed: {
type: Number,
default: () => 100,
},
// 是否开启垂直滚动
scrollable: {
type: Boolean,
default: () => false,
},
// 自定义左侧图标
leftIcon: {
type: String,
default: () => '',
},
// 自定义右侧图标
rightIcon: {
type: String,
default: () => '',
},
}, },
setup(props, { emit }) { // 通知文本内容
const noticeBarWarpRef = ref(); text: {
const noticeBarTextRef = ref(); type: String,
const state = reactive({ default: () => '',
order: 1,
oneTime: 0,
twoTime: 0,
warpOWidth: 0,
textOWidth: 0,
isMode: false,
});
// 初始化 animation 各项参数
const initAnimation = () => {
nextTick(() => {
state.warpOWidth = noticeBarWarpRef.value.offsetWidth;
state.textOWidth = noticeBarTextRef.value.offsetWidth;
document.styleSheets[0].insertRule(`@keyframes oneAnimation {0% {left: 0px;} 100% {left: -${state.textOWidth}px;}}`);
document.styleSheets[0].insertRule(`@keyframes twoAnimation {0% {left: ${state.warpOWidth}px;} 100% {left: -${state.textOWidth}px;}}`);
computeAnimationTime();
setTimeout(() => {
changeAnimation();
}, props.delay * 1000);
});
};
// 计算 animation 滚动时长
const computeAnimationTime = () => {
state.oneTime = state.textOWidth / props.speed;
state.twoTime = (state.textOWidth + state.warpOWidth) / props.speed;
};
// 改变 animation 动画调用
const changeAnimation = () => {
if (state.order === 1) {
noticeBarTextRef.value.style.cssText = `animation: oneAnimation ${state.oneTime}s linear; opactity: 1;}`;
state.order = 2;
} else {
noticeBarTextRef.value.style.cssText = `animation: twoAnimation ${state.twoTime}s linear infinite; opacity: 1;`;
}
};
// 监听 animation 动画的结束
const listenerAnimationend = () => {
noticeBarTextRef.value.addEventListener(
'animationend',
() => {
changeAnimation();
},
false
);
};
// 右侧 icon 图标点击
const onRightIconClick = () => {
if (!props.mode) return false;
if (props.mode === 'closeable') {
state.isMode = true;
emit('close');
} else if (props.mode === 'link') {
emit('link');
}
};
// 页面加载时
onMounted(() => {
if (props.scrollable) return false;
initAnimation();
listenerAnimationend();
});
return {
noticeBarWarpRef,
noticeBarTextRef,
onRightIconClick,
...toRefs(state),
};
}, },
// 通知文本颜色
color: {
type: String,
default: () => 'var(--el-color-warning)',
},
// 通知背景色
background: {
type: String,
default: () => 'var(--el-color-warning-light-9)',
},
// 字体大小单位px
size: {
type: [Number, String],
default: () => 14,
},
// 通知栏高度单位px
height: {
type: Number,
default: () => 40,
},
// 动画延迟时间 (s)
delay: {
type: Number,
default: () => 1,
},
// 滚动速率 (px/s)
speed: {
type: Number,
default: () => 100,
},
// 是否开启垂直滚动
scrollable: {
type: Boolean,
default: () => false,
},
// 自定义左侧图标
leftIcon: {
type: String,
default: () => '',
},
// 自定义右侧图标
rightIcon: {
type: String,
default: () => '',
},
});
// 定义子组件向父组件传值/事件
const emit = defineEmits(['close', 'link']);
// 定义变量内容
const noticeBarWarpRef = ref();
const noticeBarTextRef = ref();
const state = reactive({
order: 1,
oneTime: 0,
twoTime: 0,
warpOWidth: 0,
textOWidth: 0,
isMode: false,
});
// 初始化 animation 各项参数
const initAnimation = () => {
nextTick(() => {
state.warpOWidth = noticeBarWarpRef.value.offsetWidth;
state.textOWidth = noticeBarTextRef.value.offsetWidth;
document.styleSheets[0].insertRule(`@keyframes oneAnimation {0% {left: 0px;} 100% {left: -${state.textOWidth}px;}}`);
document.styleSheets[0].insertRule(`@keyframes twoAnimation {0% {left: ${state.warpOWidth}px;} 100% {left: -${state.textOWidth}px;}}`);
computeAnimationTime();
setTimeout(() => {
changeAnimation();
}, props.delay * 1000);
});
};
// 计算 animation 滚动时长
const computeAnimationTime = () => {
state.oneTime = state.textOWidth / props.speed;
state.twoTime = (state.textOWidth + state.warpOWidth) / props.speed;
};
// 改变 animation 动画调用
const changeAnimation = () => {
if (state.order === 1) {
noticeBarTextRef.value.style.cssText = `animation: oneAnimation ${state.oneTime}s linear; opactity: 1;}`;
state.order = 2;
} else {
noticeBarTextRef.value.style.cssText = `animation: twoAnimation ${state.twoTime}s linear infinite; opacity: 1;`;
}
};
// 监听 animation 动画的结束
const listenerAnimationend = () => {
noticeBarTextRef.value.addEventListener(
'animationend',
() => {
changeAnimation();
},
false
);
};
// 右侧 icon 图标点击
const onRightIconClick = () => {
if (!props.mode) return false;
if (props.mode === 'closeable') {
state.isMode = true;
emit('close');
} else if (props.mode === 'link') {
emit('link');
}
};
// 页面加载时
onMounted(() => {
if (props.scrollable) return false;
initAnimation();
listenerAnimationend();
}); });
</script> </script>

View File

@@ -8,66 +8,56 @@
<i v-else :class="getIconName" :style="setIconSvgStyle" /> <i v-else :class="getIconName" :style="setIconSvgStyle" />
</template> </template>
<script lang="ts"> <script setup lang="ts" name="svgIcon">
import { computed, defineComponent } from 'vue'; import { computed } from 'vue';
export default defineComponent({ // 定义父组件传过来的值
name: 'svgIcon', const props = defineProps({
props: { // svg 图标组件名字
// svg 图标组件名字 name: {
name: { type: String,
type: String,
},
// svg 大小
size: {
type: Number,
default: () => 14,
},
// svg 颜色
color: {
type: String,
},
}, },
setup(props) { // svg 大小
// 在线链接、本地引入地址前缀 size: {
const linesString = ['https', 'http', '/src', '/assets', import.meta.env.VITE_PUBLIC_PATH]; type: Number,
default: () => 14,
// 获取 icon 图标名称 },
const getIconName = computed(() => { // svg 颜色
return props?.name; color: {
}); type: String,
// 用于判断 element plus 自带 svg 图标的显示、隐藏
const isShowIconSvg = computed(() => {
return props?.name?.startsWith('ele-');
});
// 用于判断在线链接、本地引入等图标显示、隐藏
const isShowIconImg = computed(() => {
return linesString.find((str) => props.name?.startsWith(str));
});
// 设置图标样式
const setIconSvgStyle = computed(() => {
return `font-size: ${props.size}px;color: ${props.color};`;
});
// 设置图片样式
const setIconImgOutStyle = computed(() => {
return `width: ${props.size}px;height: ${props.size}px;display: inline-block;overflow: hidden;`;
});
// 设置图片样式
// https://gitee.com/lyt-top/vue-next-admin/issues/I59ND0
const setIconSvgInsStyle = computed(() => {
const filterStyle: string[] = [];
const compatibles: string[] = ['-webkit', '-ms', '-o', '-moz'];
compatibles.forEach((j) => filterStyle.push(`${j}-filter: drop-shadow(${props.color} 30px 0);`));
return `width: ${props.size}px;height: ${props.size}px;position: relative;left: -${props.size}px;${filterStyle.join('')}`;
});
return {
getIconName,
isShowIconSvg,
isShowIconImg,
setIconSvgStyle,
setIconImgOutStyle,
setIconSvgInsStyle,
};
}, },
}); });
// 在线链接、本地引入地址前缀
// https://gitee.com/lyt-top/vue-next-admin/issues/I62OVL
const linesString = ['https', 'http', '/src', '/assets', 'data:image', import.meta.env.VITE_PUBLIC_PATH];
// 获取 icon 图标名称
const getIconName = computed(() => {
return props?.name;
});
// 用于判断 element plus 自带 svg 图标的显示、隐藏
const isShowIconSvg = computed(() => {
return props?.name?.startsWith('ele-');
});
// 用于判断在线链接、本地引入等图标显示、隐藏
const isShowIconImg = computed(() => {
return linesString.find((str) => props.name?.startsWith(str));
});
// 设置图标样式
const setIconSvgStyle = computed(() => {
return `font-size: ${props.size}px;color: ${props.color};`;
});
// 设置图片样式
const setIconImgOutStyle = computed(() => {
return `width: ${props.size}px;height: ${props.size}px;display: inline-block;overflow: hidden;`;
});
// 设置图片样式
// https://gitee.com/lyt-top/vue-next-admin/issues/I59ND0
const setIconSvgInsStyle = computed(() => {
const filterStyle: string[] = [];
const compatibles: string[] = ['-webkit', '-ms', '-o', '-moz'];
compatibles.forEach((j) => filterStyle.push(`${j}-filter: drop-shadow(${props.color} 30px 0);`));
return `width: ${props.size}px;height: ${props.size}px;position: relative;left: -${props.size}px;${filterStyle.join('')}`;
});
</script> </script>

View File

@@ -0,0 +1,256 @@
<template>
<div class="table-container">
<el-table
:data="data"
:border="setBorder"
v-bind="$attrs"
row-key="id"
stripe
style="width: 100%"
v-loading="config.loading"
@selection-change="onSelectionChange"
>
<el-table-column type="selection" :reserve-selection="true" width="30" v-if="config.isSelection" />
<el-table-column type="index" label="序号" width="60" v-if="config.isSerialNo" />
<el-table-column
v-for="(item, index) in setHeader"
:key="index"
show-overflow-tooltip
:prop="item.key"
:width="item.colWidth"
:label="item.title"
>
<template v-slot="scope">
<template v-if="item.type === 'image'">
<img :src="scope.row[item.key]" :width="item.width" :height="item.height" />
</template>
<template v-else>
{{ scope.row[item.key] }}
</template>
</template>
</el-table-column>
<el-table-column label="操作" width="100" v-if="config.isOperate">
<template v-slot="scope">
<el-popconfirm title="确定删除吗?" @confirm="onDelRow(scope.row)">
<template #reference>
<el-button text type="primary">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无数据" />
</template>
</el-table>
<div class="table-footer mt15">
<el-pagination
v-model:current-page="state.page.pageNum"
v-model:page-size="state.page.pageSize"
:pager-count="5"
:page-sizes="[10, 20, 30]"
:total="config.total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="onHandleSizeChange"
@current-change="onHandleCurrentChange"
>
</el-pagination>
<div class="table-footer-tool">
<SvgIcon name="iconfont icon-yunxiazai_o" :size="22" title="导出" @click="onImportTable" />
<SvgIcon name="iconfont icon-shuaxin" :size="22" title="刷新" @click="onRefreshTable" />
<el-popover
placement="top-end"
trigger="click"
transition="el-zoom-in-top"
popper-class="table-tool-popper"
:width="300"
:persistent="false"
@show="onSetTable"
>
<template #reference>
<SvgIcon name="iconfont icon-quanjushezhi_o" :size="22" title="设置" />
</template>
<template #default>
<div class="tool-box">
<el-tooltip content="拖动进行排序" placement="top-start">
<SvgIcon name="fa fa-question-circle-o" :size="17" class="ml11" color="#909399" />
</el-tooltip>
<el-checkbox
v-model="state.checkListAll"
:indeterminate="state.checkListIndeterminate"
class="ml10 mr1"
label="列显示"
@change="onCheckAllChange"
/>
<el-checkbox v-model="getConfig.isSerialNo" class="ml12 mr1" label="序号" />
<el-checkbox v-model="getConfig.isSelection" class="ml12 mr1" label="多选" />
</div>
<el-scrollbar>
<div ref="toolSetRef" class="tool-sortable">
<div class="tool-sortable-item" v-for="v in header" :key="v.key" :data-key="v.key">
<i class="fa fa-arrows-alt handle cursor-pointer"></i>
<el-checkbox v-model="v.isCheck" size="default" class="ml12 mr8" :label="v.title" @change="onCheckChange" />
</div>
</div>
</el-scrollbar>
</template>
</el-popover>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="netxTable">
import { reactive, computed, nextTick, ref } from 'vue';
import { ElMessage } from 'element-plus';
import table2excel from 'js-table2excel';
import Sortable from 'sortablejs';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import '/@/theme/tableTool.scss';
// 定义父组件传过来的值
const props = defineProps({
// 列表内容
data: {
type: Array<EmptyObjectType>,
default: () => [],
},
// 表头内容
header: {
type: Array<EmptyObjectType>,
default: () => [],
},
// 配置项
config: {
type: Object,
default: () => {},
},
});
// 定义子组件向父组件传值/事件
const emit = defineEmits(['delRow', 'pageChange', 'sortHeader']);
// 定义变量内容
const toolSetRef = ref();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const state = reactive({
page: {
pageNum: 1,
pageSize: 10,
},
selectlist: [] as EmptyObjectType[],
checkListAll: true,
checkListIndeterminate: false,
});
// 设置边框显示/隐藏
const setBorder = computed(() => {
return props.config.isBorder ? true : false;
});
// 获取父组件 配置项(必传)
const getConfig = computed(() => {
return props.config;
});
// 设置 tool header 数据
const setHeader = computed(() => {
return props.header.filter((v) => v.isCheck);
});
// tool 列显示全选改变时
const onCheckAllChange = <T>(val: T) => {
if (val) props.header.forEach((v) => (v.isCheck = true));
else props.header.forEach((v) => (v.isCheck = false));
state.checkListIndeterminate = false;
};
// tool 列显示当前项改变时
const onCheckChange = () => {
const headers = props.header.filter((v) => v.isCheck).length;
state.checkListAll = headers === props.header.length;
state.checkListIndeterminate = headers > 0 && headers < props.header.length;
};
// 表格多选改变时,用于导出
const onSelectionChange = (val: EmptyObjectType[]) => {
state.selectlist = val;
};
// 删除当前项
const onDelRow = (row: EmptyObjectType) => {
emit('delRow', row);
};
// 分页改变
const onHandleSizeChange = (val: number) => {
state.page.pageSize = val;
emit('pageChange', state.page);
};
// 分页改变
const onHandleCurrentChange = (val: number) => {
state.page.pageNum = val;
emit('pageChange', state.page);
};
// 搜索时,分页还原成默认
const pageReset = () => {
state.page.pageNum = 1;
state.page.pageSize = 10;
emit('pageChange', state.page);
};
// 导出
const onImportTable = () => {
if (state.selectlist.length <= 0) return ElMessage.warning('请先选择要导出的数据');
table2excel(props.header, state.selectlist, `${themeConfig.value.globalTitle} ${new Date().toLocaleString()}`);
};
// 刷新
const onRefreshTable = () => {
emit('pageChange', state.page);
};
// 设置
const onSetTable = () => {
nextTick(() => {
const sortable = Sortable.create(toolSetRef.value, {
handle: '.handle',
dataIdAttr: 'data-key',
animation: 150,
onEnd: () => {
const headerList: EmptyObjectType[] = [];
sortable.toArray().forEach((val) => {
props.header.forEach((v) => {
if (v.key === val) headerList.push({ ...v });
});
});
emit('sortHeader', headerList);
},
});
});
};
// 暴露变量
defineExpose({
pageReset,
});
</script>
<style scoped lang="scss">
.table-container {
display: flex;
flex-direction: column;
.el-table {
flex: 1;
}
.table-footer {
display: flex;
.table-footer-tool {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
i {
margin-right: 10px;
cursor: pointer;
color: var(--el-text-color-regular);
&:last-of-type {
margin-right: 0;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,202 @@
<template>
<el-select popper-class="popperClass" class="tableSelector" :multiple="props.tableConfig.isMultiple"
@remove-tag="removeTag" v-model="data" placeholder="请选择" @visible-change="visibleChange">
<template #empty>
<div class="option">
<el-input style="margin-bottom: 10px" v-model="search" clearable placeholder="请输入关键词" @change="getDict"
@clear="getDict">
<template #append>
<el-button type="primary" icon="Search"/>
</template>
</el-input>
<el-table
ref="tableRef"
:data="tableData"
size="mini"
border
row-key="id"
style="width: 400px"
max-height="200"
height="200"
:highlight-current-row="!props.tableConfig.isMultiple"
@selection-change="handleSelectionChange"
@current-change="handleCurrentChange"
>
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" width="55"/>
<el-table-column fixed type="index" label="#" width="50"/>
<el-table-column :prop="item.prop" :label="item.label" :width="item.width"
v-for="(item,index) in props.tableConfig.columns" :key="index"/>
</el-table>
<el-pagination style="margin-top: 10px" background
v-model:current-page="pageConfig.page"
v-model:page-size="pageConfig.limit"
layout="prev, pager, next"
:total="pageConfig.total"
@current-change="handlePageChange"
/>
</div>
</template>
</el-select>
</template>
<script setup lang="ts">
import {defineProps, onMounted, reactive, ref, toRaw, watch} from 'vue'
import {dict} from '@fast-crud/fast-crud'
import XEUtils from 'xe-utils'
const props = defineProps({
modelValue: {},
tableConfig: {
url: null,
label: null, //显示值
value: null, //数据值
isTree: false,
data: [],//默认数据
isMultiple: false, //是否多选
columns: [], //每一项对应的列表项
},
displayLabel: {}
} as any)
const emit = defineEmits(['update:modelValue'])
// tableRef
const tableRef = ref()
// template上使用data
const data = ref()
// 多选值
const multipleSelection = ref()
watch(multipleSelection, // 监听multipleSelection的变化
(value) => {
const {tableConfig} = props
//是否多选
if (!tableConfig.isMultiple) {
data.value = value ? value[tableConfig.label] : null
} else {
const result = value ? value.map((item: any) => {
return item[tableConfig.label]
}) : null
data.value = result
}
}, // 当multipleSelection值触发后同步修改data.value的值
{immediate: true} // 立即触发一次给data赋值初始值
)
// 搜索值
const search = ref(undefined)
//表格数据
const tableData = ref()
// 分页的配置
const pageConfig = reactive({
page: 1,
limit: 10,
total: 0
})
/**
* 表格多选
* @param val:Array
*/
const handleSelectionChange = (val: any) => {
multipleSelection.value = val
const {tableConfig} = props
const result = val.map((item: any) => {
return item[tableConfig.value]
})
emit('update:modelValue', result)
}
/**
* 表格单选
* @param val:Object
*/
const handleCurrentChange = (val: any) => {
multipleSelection.value = val
const {tableConfig} = props
emit('update:modelValue', val[tableConfig.value])
}
/**
* 获取字典值
*/
const getDict = async () => {
const url = props.tableConfig.url
const params = {
page: pageConfig.page,
limit: pageConfig.limit,
search: search
}
const dicts = dict({url: url, params: params})
await dicts.reloadDict()
const dictData: any = dicts.data
const {data, page, limit, total} = dictData
pageConfig.page = page
pageConfig.limit = limit
pageConfig.total = total
if (props.tableConfig.data === undefined || props.tableConfig.data.length === 0) {
if (props.tableConfig.isTree) {
tableData.value = XEUtils.toArrayTree(data, {parentKey: 'parent', key: 'id', children: 'children'})
} else {
tableData.value = data
}
} else {
tableData.value = props.tableConfig.data
}
}
/**
* 下拉框展开/关闭
* @param bool
*/
const visibleChange = (bool: any) => {
if (bool) {
getDict()
}
}
/**
* 分页
* @param page
*/
const handlePageChange = (page: any) => {
pageConfig.page = page
getDict()
}
// 监听displayLabel的变化更新数据
watch(() => {
return props.displayLabel
}, (value) => {
const {tableConfig} = props
const result = value ? value.map((item: any) => {
return item[tableConfig.label]
}) : null
data.value = result
}, {immediate: true})
</script>
<style scoped>
.option {
height: auto;
line-height: 1;
padding: 5px;
background-color: #fff;
}
</style>
<style lang="scss">
.popperClass {
height: 320px;
}
.el-select-dropdown__wrap {
max-height: 310px !important;
}
.tableSelector {
.el-icon, .el-tag__close {
display: none;
}
}
</style>

View File

@@ -0,0 +1,40 @@
import type { App } from 'vue';
import { useUserInfo } from '/@/stores/userInfo';
import { judementSameArr } from '/@/utils/arrayOperation';
/**
* 用户权限指令
* @directive 单个权限验证v-auth="xxx"
* @directive 多个权限验证满足一个则显示v-auths="[xxx,xxx]"
* @directive 多个权限验证全部满足则显示v-auth-all="[xxx,xxx]"
*/
export function authDirective(app: App) {
// 单个权限验证v-auth="xxx"
app.directive('auth', {
mounted(el, binding) {
const stores = useUserInfo();
if (!stores.userInfos.authBtnList.some((v: string) => v === binding.value)) el.parentNode.removeChild(el);
},
});
// 多个权限验证满足一个则显示v-auths="[xxx,xxx]"
app.directive('auths', {
mounted(el, binding) {
let flag = false;
const stores = useUserInfo();
stores.userInfos.authBtnList.map((val: string) => {
binding.value.map((v: string) => {
if (val === v) flag = true;
});
});
if (!flag) el.parentNode.removeChild(el);
},
});
// 多个权限验证全部满足则显示v-auth-all="[xxx,xxx]"
app.directive('auth-all', {
mounted(el, binding) {
const stores = useUserInfo();
const flag = judementSameArr(binding.value, stores.userInfos.authBtnList);
if (!flag) el.parentNode.removeChild(el);
},
});
}

View File

@@ -0,0 +1,178 @@
import type { App } from 'vue';
/**
* 按钮波浪指令
* @directive 默认方式v-waves如 `<div v-waves></div>`
* @directive 参数方式v-waves=" |light|red|orange|purple|green|teal",如 `<div v-waves="'light'"></div>`
*/
export function wavesDirective(app: App) {
app.directive('waves', {
mounted(el, binding) {
el.classList.add('waves-effect');
binding.value && el.classList.add(`waves-${binding.value}`);
function setConvertStyle(obj: { [key: string]: unknown }) {
let style: string = '';
for (let i in obj) {
if (obj.hasOwnProperty(i)) style += `${i}:${obj[i]};`;
}
return style;
}
function onCurrentClick(e: { [key: string]: unknown }) {
let elDiv = document.createElement('div');
elDiv.classList.add('waves-ripple');
el.appendChild(elDiv);
let styles = {
left: `${e.layerX}px`,
top: `${e.layerY}px`,
opacity: 1,
transform: `scale(${(el.clientWidth / 100) * 10})`,
'transition-duration': `750ms`,
'transition-timing-function': `cubic-bezier(0.250, 0.460, 0.450, 0.940)`,
};
elDiv.setAttribute('style', setConvertStyle(styles));
setTimeout(() => {
elDiv.setAttribute(
'style',
setConvertStyle({
opacity: 0,
transform: styles.transform,
left: styles.left,
top: styles.top,
})
);
setTimeout(() => {
elDiv && el.removeChild(elDiv);
}, 750);
}, 450);
}
el.addEventListener('mousedown', onCurrentClick, false);
},
unmounted(el) {
el.addEventListener('mousedown', () => {});
},
});
}
/**
* 自定义拖动指令
* @description 使用方式v-drag="[dragDom,dragHeader]",如 `<div v-drag="['.drag-container .el-dialog', '.drag-container .el-dialog__header']"></div>`
* @description dragDom 要拖动的元素dragHeader 要拖动的 Header 位置
* @link 注意https://github.com/element-plus/element-plus/issues/522
* @lick 参考https://blog.csdn.net/weixin_46391323/article/details/105228020?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-10&spm=1001.2101.3001.4242
*/
export function dragDirective(app: App) {
app.directive('drag', {
mounted(el, binding) {
if (!binding.value) return false;
const dragDom = document.querySelector(binding.value[0]) as HTMLElement;
const dragHeader = document.querySelector(binding.value[1]) as HTMLElement;
dragHeader.onmouseover = () => (dragHeader.style.cursor = `move`);
function down(e: any, type: string) {
// 鼠标按下,计算当前元素距离可视区的距离
const disX = type === 'pc' ? e.clientX - dragHeader.offsetLeft : e.touches[0].clientX - dragHeader.offsetLeft;
const disY = type === 'pc' ? e.clientY - dragHeader.offsetTop : e.touches[0].clientY - dragHeader.offsetTop;
// body当前宽度
const screenWidth = document.body.clientWidth;
// 可见区域高度(应为body高度可某些环境下无法获取)
const screenHeight = document.documentElement.clientHeight;
// 对话框宽度
const dragDomWidth = dragDom.offsetWidth;
// 对话框高度
const dragDomheight = dragDom.offsetHeight;
const minDragDomLeft = dragDom.offsetLeft;
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
const minDragDomTop = dragDom.offsetTop;
const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
// 获取到的值带px 正则匹配替换
let styL: any = getComputedStyle(dragDom).left;
let styT: any = getComputedStyle(dragDom).top;
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (styL.includes('%')) {
styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100);
styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100);
} else {
styL = +styL.replace(/\px/g, '');
styT = +styT.replace(/\px/g, '');
}
return {
disX,
disY,
minDragDomLeft,
maxDragDomLeft,
minDragDomTop,
maxDragDomTop,
styL,
styT,
};
}
function move(e: any, type: string, obj: any) {
let { disX, disY, minDragDomLeft, maxDragDomLeft, minDragDomTop, maxDragDomTop, styL, styT } = obj;
// 通过事件委托,计算移动的距离
let left = type === 'pc' ? e.clientX - disX : e.touches[0].clientX - disX;
let top = type === 'pc' ? e.clientY - disY : e.touches[0].clientY - disY;
// 边界处理
if (-left > minDragDomLeft) {
left = -minDragDomLeft;
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft;
}
if (-top > minDragDomTop) {
top = -minDragDomTop;
} else if (top > maxDragDomTop) {
top = maxDragDomTop;
}
// 移动当前元素
dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`;
}
/**
* pc端
* onmousedown 鼠标按下触发事件
* onmousemove 鼠标按下时持续触发事件
* onmouseup 鼠标抬起触发事件
*/
dragHeader.onmousedown = (e) => {
const obj = down(e, 'pc');
document.onmousemove = (e) => {
move(e, 'pc', obj);
};
document.onmouseup = () => {
document.onmousemove = null;
document.onmouseup = null;
};
};
/**
* 移动端
* ontouchstart 当按下手指时触发ontouchstart
* ontouchmove 当移动手指时触发ontouchmove
* ontouchend 当移走手指时触发ontouchend
*/
dragHeader.ontouchstart = (e) => {
const obj = down(e, 'app');
document.ontouchmove = (e) => {
move(e, 'app', obj);
};
document.ontouchend = () => {
document.ontouchmove = null;
document.ontouchend = null;
};
};
},
});
}

View File

@@ -0,0 +1,18 @@
import type { App } from 'vue';
import { authDirective } from '/@/directive/authDirective';
import { wavesDirective, dragDirective } from '/@/directive/customDirective';
/**
* 导出指令方法v-xxx
* @methods authDirective 用户权限指令用法v-auth
* @methods wavesDirective 按钮波浪指令用法v-waves
* @methods dragDirective 自定义拖动指令用法v-drag
*/
export function directive(app: App) {
// 用户权限指令
authDirective(app);
// 按钮波浪指令
wavesDirective(app);
// 自定义拖动指令
dragDirective(app);
}

View File

@@ -2,53 +2,53 @@ import { createI18n } from 'vue-i18n';
import pinia from '/@/stores/index'; import pinia from '/@/stores/index';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import zhcnLocale from 'element-plus/lib/locale/lang/zh-cn';
import enLocale from 'element-plus/lib/locale/lang/en';
import zhtwLocale from 'element-plus/lib/locale/lang/zh-tw';
import nextZhcn from '/@/i18n/lang/zh-cn';
import nextEn from '/@/i18n/lang/en';
import nextZhtw from '/@/i18n/lang/zh-tw';
import pagesLoginZhcn from '/@/i18n/pages/login/zh-cn';
import pagesLoginEn from '/@/i18n/pages/login/en';
import pagesLoginZhtw from '/@/i18n/pages/login/zh-tw';
import pagesFormI18nZhcn from '/@/i18n/pages/formI18n/zh-cn';
import pagesFormI18nEn from '/@/i18n/pages/formI18n/en';
import pagesFormI18nZhtw from '/@/i18n/pages/formI18n/zh-tw';
// 定义语言国际化内容 // 定义语言国际化内容
/** /**
* 说明: * 说明:
* /src/i18n/lang 下的 ts 为框架的国际化内容 * 须在 pages 下新建文件夹(建议 `要国际化界面目录` 与 `i18n 目录` 相同,方便查找),
* /src/i18n/pages 下的 ts 为各界面的国际化内容 * 注意国际化定义的字段,不要与原有的定义字段相同。
* 1、/src/i18n/lang 下的 ts 为框架的国际化内容
* 2、/src/i18n/pages 下的 ts 为各界面的国际化内容
*/ */
const messages = {
[zhcnLocale.name]: { // element plus 自带国际化
...zhcnLocale, import enLocale from 'element-plus/lib/locale/lang/en';
message: { import zhcnLocale from 'element-plus/lib/locale/lang/zh-cn';
...nextZhcn, import zhtwLocale from 'element-plus/lib/locale/lang/zh-tw';
...pagesLoginZhcn,
...pagesFormI18nZhcn, // 定义变量内容
}, const messages = {};
}, const element = { en: enLocale, 'zh-cn': zhcnLocale, 'zh-tw': zhtwLocale };
[enLocale.name]: { const itemize = { en: [], 'zh-cn': [], 'zh-tw': [] };
...enLocale, const modules: Record<string, any> = import.meta.glob('./**/*.ts', { eager: true });
message: {
...nextEn, // 对自动引入的 modules 进行分类 en、zh-cn、zh-tw
...pagesLoginEn, // https://vitejs.cn/vite3-cn/guide/features.html#glob-import
...pagesFormI18nEn, for (const path in modules) {
}, const key = path.match(/(\S+)\/(\S+).ts/);
}, if (itemize[key![2]]) itemize[key![2]].push(modules[path].default);
[zhtwLocale.name]: { else itemize[key![2]] = modules[path];
...zhtwLocale, }
message: {
...nextZhtw, // 合并数组对象(非标准数组对象,数组中对象的每项 key、value 都不同)
...pagesLoginZhtw, function mergeArrObj<T>(list: T, key: string) {
...pagesFormI18nZhtw, let obj = {};
}, list[key].forEach((i: EmptyObjectType) => {
}, obj = Object.assign({}, obj, i);
}; });
return obj;
}
// 处理最终格式
for (const key in itemize) {
messages[key] = {
name: key,
el: element[key].el,
message: mergeArrObj(itemize, key),
};
}
// 读取 pinia 默认语言 // 读取 pinia 默认语言
const stores = useThemeConfig(pinia); const stores = useThemeConfig(pinia);
@@ -58,7 +58,6 @@ const { themeConfig } = storeToRefs(stores);
// https://vue-i18n.intlify.dev/guide/essentials/fallback.html#explicit-fallback-with-one-locale // https://vue-i18n.intlify.dev/guide/essentials/fallback.html#explicit-fallback-with-one-locale
export const i18n = createI18n({ export const i18n = createI18n({
legacy: false, legacy: false,
globalInjection: true,
silentTranslationWarn: true, silentTranslationWarn: true,
missingWarn: false, missingWarn: false,
silentFallbackWarn: true, silentFallbackWarn: true,

View File

@@ -14,65 +14,7 @@ export default {
limitsFrontEndBtn: 'FrontEndBtn', limitsFrontEndBtn: 'FrontEndBtn',
limitsBackEnd: 'BackEnd', limitsBackEnd: 'BackEnd',
limitsBackEndEndPage: 'BackEndEndPage', limitsBackEndEndPage: 'BackEndEndPage',
menu: 'menu',
menu1: 'menu1',
menu11: 'menu11',
menu12: 'menu12',
menu121: 'menu121',
menu122: 'menu122',
menu13: 'menu13',
menu2: 'menu2',
funIndex: 'function',
funTagsView: 'funTagsView',
funCountup: 'countup',
funWangEditor: 'wangEditor',
funCropper: 'cropper',
funQrcode: 'qrcode',
funEchartsMap: 'EchartsMap',
funPrintJs: 'PrintJs',
funClipboard: 'Copy cut',
funGridLayout: 'Drag layout',
funSplitpanes: 'Pane splitter',
funDragVerify: 'Validator',
pagesIndex: 'pages',
pagesFiltering: 'Filtering',
pagesFilteringDetails: 'FilteringDetails',
pagesFilteringDetails1: 'FilteringDetails1',
pagesIocnfont: 'iconfont icon',
pagesElement: 'element icon',
pagesAwesome: 'awesome icon',
pagesFormAdapt: 'FormAdapt',
pagesTableRules: 'pagesTableRules',
pagesFormI18n: 'FormI18n',
pagesFormRules: 'Multi form validation',
pagesDynamicForm: 'Dynamic complex form',
pagesWorkflow: 'Workflow',
pagesListAdapt: 'ListAdapt',
pagesWaterfall: 'Waterfall',
pagesSteps: 'Steps',
pagesPreview: 'Large preview',
pagesWaves: 'Wave effect',
pagesTree: 'tree alter table',
pagesDrag: 'Drag command',
pagesLazyImg: 'Image lazy loading',
makeIndex: 'makeIndex',
makeSelector: 'Icon selector',
makeNoticeBar: 'notification bar',
makeSvgDemo: 'Svgicon demo',
paramsIndex: 'Routing parameters',
paramsCommon: 'General routing',
paramsDynamic: 'Dynamic routing',
paramsCommonDetails: 'General routing details',
paramsDynamicDetails: 'Dynamic routing details',
chartIndex: 'chartIndex',
visualizingIndex: 'visualizingIndex',
visualizingLinkDemo1: 'visualizingLinkDemo1',
visualizingLinkDemo2: 'visualizingLinkDemo2',
personal: 'personal', personal: 'personal',
tools: 'tools',
layoutLinkView: 'LinkView',
layoutIfameView: 'IfameView',
demo1:'demo1'
}, },
staticRoutes: { staticRoutes: {
signIn: 'signIn', signIn: 'signIn',

View File

@@ -2,12 +2,22 @@
export default { export default {
router: { router: {
home: '首页', home: '首页',
system: '系统设置', system: '系统管理',
config: '常规配置',
log: '日志管理',
/* 常规配置 */
configSystem: '系统配置',
configDict: '字典管理',
configArea: '地区管理',
configFile: '附件管理',
/* 系统管理 */
systemMenu: '菜单管理', systemMenu: '菜单管理',
systemRole: '角色管理', systemRole: '角色管理',
systemUser: '用户管理', systemUser: '用户管理',
systemDept: '部门管理', systemDept: '部门管理',
systemDic: '字典管理', /* 日志管理 */
loginLog: '登录日志',
operationLog: '操作日志',
systemApiWhiteList: '接口白名单', systemApiWhiteList: '接口白名单',
limits: '权限管理', limits: '权限管理',
limitsFrontEnd: '前端控制', limitsFrontEnd: '前端控制',
@@ -15,65 +25,7 @@ export default {
limitsFrontEndBtn: '按钮权限', limitsFrontEndBtn: '按钮权限',
limitsBackEnd: '后端控制', limitsBackEnd: '后端控制',
limitsBackEndEndPage: '页面权限', limitsBackEndEndPage: '页面权限',
menu: '菜单嵌套',
menu1: '菜单1',
menu11: '菜单11',
menu12: '菜单12',
menu121: '菜单121',
menu122: '菜单122',
menu13: '菜单13',
menu2: '菜单2',
funIndex: '功能',
funTagsView: 'tagsView 操作',
funCountup: '数字滚动',
funWangEditor: 'Editor 编辑器',
funCropper: '图片裁剪',
funQrcode: '二维码生成',
funEchartsMap: '地理坐标/地图',
funPrintJs: '页面打印',
funClipboard: '复制剪切',
funGridLayout: '拖拽布局',
funSplitpanes: '窗格拆分器',
funDragVerify: '验证器',
pagesIndex: '页面',
pagesFiltering: '过滤筛选组件',
pagesFilteringDetails: '过滤筛选组件详情',
pagesFilteringDetails1: '过滤筛选组件详情111',
pagesIocnfont: 'ali 字体图标',
pagesElement: 'ele 字体图标',
pagesAwesome: 'awe 字体图标',
pagesFormAdapt: '表单自适应',
pagesTableRules: '表单表格验证',
pagesFormI18n: '表单国际化',
pagesFormRules: '多表单验证',
pagesDynamicForm: '动态复杂表单',
pagesWorkflow: '工作流',
pagesListAdapt: '列表自适应',
pagesWaterfall: '瀑布屏',
pagesSteps: '步骤条',
pagesPreview: '大图预览',
pagesWaves: '波浪效果',
pagesTree: '树形改表格',
pagesDrag: '拖动指令',
pagesLazyImg: '图片懒加载',
makeIndex: '组件封装',
makeSelector: '图标选择器',
makeNoticeBar: '滚动通知栏',
makeSvgDemo: 'svgIcon 演示',
paramsIndex: '路由参数',
paramsCommon: '普通路由',
paramsDynamic: '动态路由',
paramsCommonDetails: '普通路由详情',
paramsDynamicDetails: '动态路由详情',
chartIndex: '大数据图表',
visualizingIndex: '数据可视化',
visualizingLinkDemo1: '数据可视化演示1',
visualizingLinkDemo2: '数据可视化演示2',
personal: '个人中心', personal: '个人中心',
tools: '工具类集合',
layoutLinkView: '外链',
layoutIfameView: '内嵌 iframe',
demo1: 'demo1',
}, },
staticRoutes: { staticRoutes: {
signIn: '登录', signIn: '登录',

View File

@@ -59,6 +59,7 @@ export default {
makeSelector: '圖標選擇器', makeSelector: '圖標選擇器',
makeNoticeBar: '滾動通知欄', makeNoticeBar: '滾動通知欄',
makeSvgDemo: 'svgIcon 演示', makeSvgDemo: 'svgIcon 演示',
makeTableDemo: '表格封裝演示',
paramsIndex: '路由參數', paramsIndex: '路由參數',
paramsCommon: '普通路由', paramsCommon: '普通路由',
paramsDynamic: '動態路由', paramsDynamic: '動態路由',
@@ -71,8 +72,8 @@ export default {
personal: '個人中心', personal: '個人中心',
tools: '工具類集合', tools: '工具類集合',
layoutLinkView: '外鏈', layoutLinkView: '外鏈',
layoutIfameView: '内嵌 iframe', layoutIframeViewOne: '内嵌 iframe1',
demo1:'demo1' layoutIframeViewTwo: '内嵌 iframe2',
}, },
staticRoutes: { staticRoutes: {
signIn: '登入', signIn: '登入',
@@ -136,10 +137,12 @@ export default {
twoIsTopBarColorGradual: '頂欄背景漸變', twoIsTopBarColorGradual: '頂欄背景漸變',
twoMenuBar: '選單背景', twoMenuBar: '選單背景',
twoMenuBarColor: '選單默認字體顏色', twoMenuBarColor: '選單默認字體顏色',
twoMenuBarActiveColor: '選單高亮背景色',
twoIsMenuBarColorGradual: '選單背景漸變', twoIsMenuBarColorGradual: '選單背景漸變',
twoColumnsMenuBar: '分欄選單背景', twoColumnsMenuBar: '分欄選單背景',
twoColumnsMenuBarColor: '分欄選單默認字體顏色', twoColumnsMenuBarColor: '分欄選單默認字體顏色',
twoIsColumnsMenuBarColorGradual: '分欄選單背景漸變', twoIsColumnsMenuBarColorGradual: '分欄選單背景漸變',
twoIsColumnsMenuHoverPreload: '分欄選單滑鼠懸停預加載',
threeTitle: '介面設定', threeTitle: '介面設定',
threeIsCollapse: '選單水准折疊', threeIsCollapse: '選單水准折疊',
threeIsUniqueOpened: '選單手風琴', threeIsUniqueOpened: '選單手風琴',
@@ -178,4 +181,12 @@ export default {
copyTextSuccess: '複製成功!', copyTextSuccess: '複製成功!',
copyTextError: '複製失敗!', copyTextError: '複製失敗!',
}, },
upgrade: {
title: '新版本陞級',
msg: '新版本來啦,馬上更新嘗鮮吧! 不用擔心,更新很快的哦!',
desc: '提示:更新會還原默認配寘',
btnOne: '殘忍拒絕',
btnTwo: '馬上更新',
btnTwoLoading: '更新中',
},
}; };

View File

@@ -3,161 +3,152 @@
<el-aside class="layout-aside" :class="setCollapseStyle"> <el-aside class="layout-aside" :class="setCollapseStyle">
<Logo v-if="setShowLogo" /> <Logo v-if="setShowLogo" />
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef" @mouseenter="onAsideEnterLeave(true)" @mouseleave="onAsideEnterLeave(false)"> <el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef" @mouseenter="onAsideEnterLeave(true)" @mouseleave="onAsideEnterLeave(false)">
<Vertical :menuList="menuList" /> <Vertical :menuList="state.menuList" />
</el-scrollbar> </el-scrollbar>
</el-aside> </el-aside>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutAside">
import { toRefs, reactive, computed, watch, getCurrentInstance, onBeforeMount, defineComponent } from 'vue'; import { defineAsyncComponent, reactive, computed, watch, onBeforeMount, ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import pinia from '/@/stores/index'; import pinia from '/@/stores/index';
import { useRoutesList } from '/@/stores/routesList'; import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import Logo from '/@/layout/logo/index.vue'; import mittBus from '/@/utils/mitt';
import Vertical from '/@/layout/navMenu/vertical.vue';
export default defineComponent({ // 引入组件
name: 'layoutAside', const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
components: { Logo, Vertical }, const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue'));
setup() {
const { proxy } = <any>getCurrentInstance(); // 定义变量内容
const stores = useRoutesList(); const layoutAsideScrollbarRef = ref();
const storesThemeConfig = useThemeConfig(); const stores = useRoutesList();
const storesTagsViewRoutes = useTagsViewRoutes(); const storesThemeConfig = useThemeConfig();
const { routesList } = storeToRefs(stores); const storesTagsViewRoutes = useTagsViewRoutes();
const { themeConfig } = storeToRefs(storesThemeConfig); const { routesList } = storeToRefs(stores);
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes); const { themeConfig } = storeToRefs(storesThemeConfig);
const state = reactive({ const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
menuList: [], const state = reactive<AsideState>({
clientWidth: 0, menuList: [],
}); clientWidth: 0,
// 设置菜单展开/收起时的宽度
const setCollapseStyle = computed(() => {
const { layout, isCollapse, menuBar } = themeConfig.value;
const asideBrTheme = ['#FFFFFF', '#FFF', '#fff', '#ffffff'];
const asideBrColor = asideBrTheme.includes(menuBar) ? 'layout-el-aside-br-color' : '';
// 判断是否是手机端
if (state.clientWidth <= 1000) {
if (isCollapse) {
document.body.setAttribute('class', 'el-popup-parent--hidden');
const asideEle = document.querySelector('.layout-container') as HTMLElement;
const modeDivs = document.createElement('div');
modeDivs.setAttribute('class', 'layout-aside-mobile-mode');
asideEle.appendChild(modeDivs);
modeDivs.addEventListener('click', closeLayoutAsideMobileMode);
return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-open'];
} else {
// 关闭弹窗
closeLayoutAsideMobileMode();
return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-close'];
}
} else {
if (layout === 'columns') {
// 分栏布局,菜单收起时宽度给 1px
if (isCollapse) return [asideBrColor, 'layout-aside-pc-1'];
else return [asideBrColor, 'layout-aside-pc-220'];
} else {
// 其它布局给 64px
if (isCollapse) return [asideBrColor, 'layout-aside-pc-64'];
else return [asideBrColor, 'layout-aside-pc-220'];
}
}
});
// 关闭移动端蒙版
const closeLayoutAsideMobileMode = () => {
const el = document.querySelector('.layout-aside-mobile-mode');
el?.setAttribute('style', 'animation: error-img-two 0.3s');
setTimeout(() => {
el?.parentNode?.removeChild(el);
}, 300);
const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) themeConfig.value.isCollapse = false;
document.body.setAttribute('class', '');
};
// 设置显示/隐藏 logo
const setShowLogo = computed(() => {
let { layout, isShowLogo } = themeConfig.value;
return (isShowLogo && layout === 'defaults') || (isShowLogo && layout === 'columns');
});
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
if (themeConfig.value.layout === 'columns') return false;
(state.menuList as any) = filterRoutesFun(routesList.value);
};
// 路由过滤递归函数
const filterRoutesFun = (arr: Array<string>) => {
return arr
.filter((item: any) => !item.meta.isHide)
.map((item: any) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// 设置菜单导航是否固定(移动端)
const initMenuFixed = (clientWidth: number) => {
state.clientWidth = clientWidth;
};
// 鼠标移入、移出
const onAsideEnterLeave = (bool: Boolean) => {
let { layout } = themeConfig.value;
if (layout !== 'columns') return false;
if (!bool) proxy.mittBus.emit('restoreDefault');
stores.setColumnsMenuHover(bool);
};
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(themeConfig.value, (val) => {
if (val.isShowLogoChange !== val.isShowLogo) {
if (!proxy.$refs.layoutAsideScrollbarRef) return false;
proxy.$refs.layoutAsideScrollbarRef.update();
}
});
// 监听vuex值的变化动态赋值给菜单中
watch(
pinia.state,
(val) => {
let { layout, isClassicSplitMenu } = val.themeConfig.themeConfig;
if (layout === 'classic' && isClassicSplitMenu) return false;
setFilterRoutes();
},
{
deep: true,
}
);
// 页面加载前
onBeforeMount(() => {
initMenuFixed(document.body.clientWidth);
setFilterRoutes();
// 此界面不需要取消监听(proxy.mittBus.off('setSendColumnsChildren))
// 因为切换布局时有的监听需要使用,取消了监听,某些操作将不生效
proxy.mittBus.on('setSendColumnsChildren', (res: any) => {
state.menuList = res.children;
});
proxy.mittBus.on('setSendClassicChildren', (res: any) => {
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = [];
state.menuList = res.children;
}
});
proxy.mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
setFilterRoutes();
});
proxy.mittBus.on('layoutMobileResize', (res: any) => {
initMenuFixed(res.clientWidth);
closeLayoutAsideMobileMode();
});
});
return {
setCollapseStyle,
setShowLogo,
isTagsViewCurrenFull,
onAsideEnterLeave,
...toRefs(state),
};
},
}); });
// 设置菜单展开/收起时的宽度
const setCollapseStyle = computed(() => {
const { layout, isCollapse, menuBar } = themeConfig.value;
const asideBrTheme = ['#FFFFFF', '#FFF', '#fff', '#ffffff'];
const asideBrColor = asideBrTheme.includes(menuBar) ? 'layout-el-aside-br-color' : '';
// 判断是否是手机端
if (state.clientWidth <= 1000) {
if (isCollapse) {
document.body.setAttribute('class', 'el-popup-parent--hidden');
const asideEle = document.querySelector('.layout-container') as HTMLElement;
const modeDivs = document.createElement('div');
modeDivs.setAttribute('class', 'layout-aside-mobile-mode');
asideEle.appendChild(modeDivs);
modeDivs.addEventListener('click', closeLayoutAsideMobileMode);
return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-open'];
} else {
// 关闭弹窗
closeLayoutAsideMobileMode();
return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-close'];
}
} else {
if (layout === 'columns') {
// 分栏布局,菜单收起时宽度给 1px
if (isCollapse) return [asideBrColor, 'layout-aside-pc-1'];
else return [asideBrColor, 'layout-aside-pc-220'];
} else {
// 其它布局给 64px
if (isCollapse) return [asideBrColor, 'layout-aside-pc-64'];
else return [asideBrColor, 'layout-aside-pc-220'];
}
}
});
// 设置显示/隐藏 logo
const setShowLogo = computed(() => {
let { layout, isShowLogo } = themeConfig.value;
return (isShowLogo && layout === 'defaults') || (isShowLogo && layout === 'columns');
});
// 关闭移动端蒙版
const closeLayoutAsideMobileMode = () => {
const el = document.querySelector('.layout-aside-mobile-mode');
el?.setAttribute('style', 'animation: error-img-two 0.3s');
setTimeout(() => {
el?.parentNode?.removeChild(el);
}, 300);
const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) themeConfig.value.isCollapse = false;
document.body.setAttribute('class', '');
};
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
if (themeConfig.value.layout === 'columns') return false;
state.menuList = filterRoutesFun(routesList.value);
};
// 路由过滤递归函数
const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
return arr
.filter((item: T) => !item.meta?.isHide)
.map((item: T) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// 设置菜单导航是否固定(移动端)
const initMenuFixed = (clientWidth: number) => {
state.clientWidth = clientWidth;
};
// 鼠标移入、移出
const onAsideEnterLeave = (bool: Boolean) => {
let { layout } = themeConfig.value;
if (layout !== 'columns') return false;
if (!bool) mittBus.emit('restoreDefault');
stores.setColumnsMenuHover(bool);
};
// 页面加载前
onBeforeMount(() => {
initMenuFixed(document.body.clientWidth);
setFilterRoutes();
// 此界面不需要取消监听(mittBus.off('setSendColumnsChildren))
// 因为切换布局时有的监听需要使用,取消了监听,某些操作将不生效
mittBus.on('setSendColumnsChildren', (res: MittMenu) => {
state.menuList = res.children;
});
mittBus.on('setSendClassicChildren', (res: MittMenu) => {
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = [];
state.menuList = res.children;
}
});
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
setFilterRoutes();
});
mittBus.on('layoutMobileResize', (res: LayoutMobileResize) => {
initMenuFixed(res.clientWidth);
closeLayoutAsideMobileMode();
});
});
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(themeConfig.value, (val) => {
if (val.isShowLogoChange !== val.isShowLogo) {
if (layoutAsideScrollbarRef.value) layoutAsideScrollbarRef.value.update();
}
});
// 监听 pinia 值的变化,动态赋值给菜单中
watch(
pinia.state,
(val) => {
let { layout, isClassicSplitMenu } = val.themeConfig.themeConfig;
if (layout === 'classic' && isClassicSplitMenu) return false;
setFilterRoutes();
},
{
deep: true,
}
);
</script> </script>

View File

@@ -3,7 +3,7 @@
<el-scrollbar> <el-scrollbar>
<ul @mouseleave="onColumnsAsideMenuMouseleave()"> <ul @mouseleave="onColumnsAsideMenuMouseleave()">
<li <li
v-for="(v, k) in columnsAsideList" v-for="(v, k) in state.columnsAsideList"
:key="k" :key="k"
@click="onColumnsAsideMenuClick(v, k)" @click="onColumnsAsideMenuClick(v, k)"
@mouseenter="onColumnsAsideMenuMouseenter(v, k)" @mouseenter="onColumnsAsideMenuMouseenter(v, k)"
@@ -12,7 +12,7 @@
if (el) columnsAsideOffsetTopRefs[k] = el; if (el) columnsAsideOffsetTopRefs[k] = el;
} }
" "
:class="{ 'layout-columns-active': liIndex === k, 'layout-columns-hover': liHoverIndex === k }" :class="{ 'layout-columns-active': state.liIndex === k, 'layout-columns-hover': state.liHoverIndex === k }"
:title="$t(v.meta.title)" :title="$t(v.meta.title)"
> >
<div :class="themeConfig.columnsAsideLayout" v-if="!v.meta.isLink || (v.meta.isLink && v.meta.isIframe)"> <div :class="themeConfig.columnsAsideLayout" v-if="!v.meta.isLink || (v.meta.isLink && v.meta.isIframe)">
@@ -44,174 +44,151 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutColumnsAside">
import { reactive, toRefs, ref, onMounted, nextTick, getCurrentInstance, watch, onUnmounted, defineComponent } from 'vue'; import { reactive, ref, onMounted, nextTick, watch, onUnmounted } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router'; import { useRoute, useRouter, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import pinia from '/@/stores/index'; import pinia from '/@/stores/index';
import { useRoutesList } from '/@/stores/routesList'; import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import mittBus from '/@/utils/mitt';
// 定义接口来定义对象的类型 // 定义变量内容
interface ColumnsAsideState { const columnsAsideOffsetTopRefs = ref<RefType>([]);
columnsAsideList: any[]; const columnsAsideActiveRef = ref();
liIndex: number; const stores = useRoutesList();
liOldIndex: null | number; const storesThemeConfig = useThemeConfig();
liHoverIndex: null | number; const { routesList, isColumnsMenuHover, isColumnsNavHover } = storeToRefs(stores);
liOldPath: null | string; const { themeConfig } = storeToRefs(storesThemeConfig);
difference: number; const route = useRoute();
routeSplit: string[]; const router = useRouter();
} const state = reactive<ColumnsAsideState>({
columnsAsideList: [],
export default defineComponent({ liIndex: 0,
name: 'layoutColumnsAside', liOldIndex: null,
setup() { liHoverIndex: null,
const columnsAsideOffsetTopRefs: any = ref([]); liOldPath: null,
const columnsAsideActiveRef = ref(); difference: 0,
const { proxy } = <any>getCurrentInstance(); routeSplit: [],
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const { routesList, isColumnsMenuHover, isColumnsNavHover } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute();
const router = useRouter();
const state = reactive<ColumnsAsideState>({
columnsAsideList: [],
liIndex: 0,
liOldIndex: null,
liHoverIndex: null,
liOldPath: null,
difference: 0,
routeSplit: [],
});
// 设置菜单高亮位置移动
const setColumnsAsideMove = (k: number) => {
state.liIndex = k;
columnsAsideActiveRef.value.style.top = `${columnsAsideOffsetTopRefs.value[k].offsetTop + state.difference}px`;
};
// 菜单高亮点击事件
const onColumnsAsideMenuClick = (v: Object, k: number) => {
setColumnsAsideMove(k);
let { path, redirect } = v as any;
if (redirect) router.push(redirect);
else router.push(path);
};
// 鼠标移入时,显示当前的子级菜单
const onColumnsAsideMenuMouseenter = (v: RouteRecordRaw, k: number) => {
let { path } = v;
state.liOldPath = path;
state.liOldIndex = k;
state.liHoverIndex = k;
proxy.mittBus.emit('setSendColumnsChildren', setSendChildren(path));
stores.setColumnsMenuHover(false);
stores.setColumnsNavHover(true);
};
// 鼠标移走时,显示原来的子级菜单
const onColumnsAsideMenuMouseleave = async () => {
await stores.setColumnsNavHover(false);
// 添加延时器,防止拿到的 store.state.routesList 值不是最新的
setTimeout(() => {
if (!isColumnsMenuHover && !isColumnsNavHover) proxy.mittBus.emit('restoreDefault');
}, 100);
};
// 设置高亮动态位置
const onColumnsAsideDown = (k: number) => {
nextTick(() => {
setColumnsAsideMove(k);
});
};
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
state.columnsAsideList = filterRoutesFun(routesList.value);
const resData: any = setSendChildren(route.path);
if (Object.keys(resData).length <= 0) return false;
onColumnsAsideDown(resData.item[0].k);
proxy.mittBus.emit('setSendColumnsChildren', resData);
};
// 传送当前子级数据到菜单中
const setSendChildren = (path: string) => {
const currentPathSplit = path.split('/');
let currentData: any = {};
state.columnsAsideList.map((v: any, k: number) => {
if (v.path === `/${currentPathSplit[1]}`) {
v['k'] = k;
currentData['item'] = [{ ...v }];
currentData['children'] = [{ ...v }];
if (v.children) currentData['children'] = v.children;
}
});
return currentData;
};
// 路由过滤递归函数
const filterRoutesFun = (arr: Array<string>) => {
return arr
.filter((item: any) => !item.meta.isHide)
.map((item: any) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// tagsView 点击时,根据路由查找下标 columnsAsideList实现左侧菜单高亮
const setColumnsMenuHighlight = (path: string) => {
state.routeSplit = path.split('/');
state.routeSplit.shift();
const routeFirst = `/${state.routeSplit[0]}`;
const currentSplitRoute = state.columnsAsideList.find((v: any) => v.path === routeFirst);
if (!currentSplitRoute) return false;
// 延迟拿值,防止取不到
setTimeout(() => {
onColumnsAsideDown((<any>currentSplitRoute).k);
}, 0);
};
// 监听布局配置信息的变化,动态增加菜单高亮位置移动像素
watch(
pinia.state,
(val) => {
val.themeConfig.themeConfig.columnsAsideStyle === 'columnsRound' ? (state.difference = 3) : (state.difference = 0);
if (!val.routesList.isColumnsMenuHover && !val.routesList.isColumnsNavHover) {
state.liHoverIndex = null;
proxy.mittBus.emit('setSendColumnsChildren', setSendChildren(route.path));
} else {
state.liHoverIndex = state.liOldIndex;
if (!state.liOldPath) return false;
proxy.mittBus.emit('setSendColumnsChildren', setSendChildren(state.liOldPath));
}
},
{
deep: true,
}
);
// 页面加载时
onMounted(() => {
setFilterRoutes();
// 销毁变量,防止鼠标再次移入时,保留了上次的记录
proxy.mittBus.on('restoreDefault', () => {
state.liOldIndex = null;
state.liOldPath = null;
});
});
// 页面卸载时
onUnmounted(() => {
proxy.mittBus.off('restoreDefault', () => {});
});
// 路由更新时
onBeforeRouteUpdate((to) => {
setColumnsMenuHighlight(to.path);
proxy.mittBus.emit('setSendColumnsChildren', setSendChildren(to.path));
});
return {
themeConfig,
columnsAsideOffsetTopRefs,
columnsAsideActiveRef,
onColumnsAsideDown,
onColumnsAsideMenuClick,
onColumnsAsideMenuMouseenter,
onColumnsAsideMenuMouseleave,
...toRefs(state),
};
},
}); });
// 设置菜单高亮位置移动
const setColumnsAsideMove = (k: number) => {
state.liIndex = k;
columnsAsideActiveRef.value.style.top = `${columnsAsideOffsetTopRefs.value[k].offsetTop + state.difference}px`;
};
// 菜单高亮点击事件
const onColumnsAsideMenuClick = (v: RouteItem, k: number) => {
setColumnsAsideMove(k);
let { path, redirect } = v;
if (redirect) router.push(redirect);
else router.push(path);
};
// 鼠标移入时,显示当前的子级菜单
const onColumnsAsideMenuMouseenter = (v: RouteRecordRaw, k: number) => {
if (!themeConfig.value.isColumnsMenuHoverPreload) return false;
let { path } = v;
state.liOldPath = path;
state.liOldIndex = k;
state.liHoverIndex = k;
mittBus.emit('setSendColumnsChildren', setSendChildren(path));
stores.setColumnsMenuHover(false);
stores.setColumnsNavHover(true);
};
// 鼠标移走时,显示原来的子级菜单
const onColumnsAsideMenuMouseleave = async () => {
await stores.setColumnsNavHover(false);
// 添加延时器,防止拿到的 store.state.routesList 值不是最新的
setTimeout(() => {
if (!isColumnsMenuHover && !isColumnsNavHover) mittBus.emit('restoreDefault');
}, 100);
};
// 设置高亮动态位置
const onColumnsAsideDown = (k: number) => {
nextTick(() => {
setColumnsAsideMove(k);
});
};
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
state.columnsAsideList = filterRoutesFun(routesList.value);
const resData: MittMenu = setSendChildren(route.path);
if (Object.keys(resData).length <= 0) return false;
onColumnsAsideDown(resData.item?.k);
mittBus.emit('setSendColumnsChildren', resData);
};
// 传送当前子级数据到菜单中
const setSendChildren = (path: string) => {
const currentPathSplit = path.split('/');
let currentData: MittMenu = { children: [] };
state.columnsAsideList.map((v: RouteItem, k: number) => {
if (v.path === `/${currentPathSplit[1]}`) {
v['k'] = k;
currentData['item'] = { ...v };
currentData['children'] = [{ ...v }];
if (v.children) currentData['children'] = v.children;
}
});
return currentData;
};
// 路由过滤递归函数
const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
return arr
.filter((item: T) => !item.meta?.isHide)
.map((item: T) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// tagsView 点击时,根据路由查找下标 columnsAsideList实现左侧菜单高亮
const setColumnsMenuHighlight = (path: string) => {
state.routeSplit = path.split('/');
state.routeSplit.shift();
const routeFirst = `/${state.routeSplit[0]}`;
const currentSplitRoute = state.columnsAsideList.find((v: RouteItem) => v.path === routeFirst);
if (!currentSplitRoute) return false;
// 延迟拿值,防止取不到
setTimeout(() => {
onColumnsAsideDown(currentSplitRoute.k);
}, 0);
};
// 页面加载时
onMounted(() => {
setFilterRoutes();
// 销毁变量,防止鼠标再次移入时,保留了上次的记录
mittBus.on('restoreDefault', () => {
state.liOldIndex = null;
state.liOldPath = null;
});
});
// 页面卸载时
onUnmounted(() => {
mittBus.off('restoreDefault', () => {});
});
// 路由更新时
onBeforeRouteUpdate((to) => {
setColumnsMenuHighlight(to.path);
mittBus.emit('setSendColumnsChildren', setSendChildren(to.path));
});
// 监听布局配置信息的变化,动态增加菜单高亮位置移动像素
watch(
pinia.state,
(val) => {
val.themeConfig.themeConfig.columnsAsideStyle === 'columnsRound' ? (state.difference = 3) : (state.difference = 0);
if (!val.routesList.isColumnsMenuHover && !val.routesList.isColumnsNavHover) {
state.liHoverIndex = null;
mittBus.emit('setSendColumnsChildren', setSendChildren(route.path));
} else {
state.liHoverIndex = state.liOldIndex;
if (!state.liOldPath) return false;
mittBus.emit('setSendColumnsChildren', setSendChildren(state.liOldPath));
}
},
{
deep: true,
}
);
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -221,6 +198,16 @@ export default defineComponent({
background: var(--next-bg-columnsMenuBar); background: var(--next-bg-columnsMenuBar);
ul { ul {
position: relative; position: relative;
.layout-columns-active {
color: var(--next-bg-columnsMenuBarColor) !important;
transition: 0.3s ease-in-out;
}
.layout-columns-hover {
color: var(--el-color-primary);
a {
color: var(--el-color-primary);
}
}
li { li {
color: var(--next-bg-columnsMenuBarColor); color: var(--next-bg-columnsMenuBarColor);
width: 100%; width: 100%;
@@ -230,6 +217,9 @@ export default defineComponent({
cursor: pointer; cursor: pointer;
position: relative; position: relative;
z-index: 1; z-index: 1;
&:hover {
@extend .layout-columns-hover;
}
.columns-vertical { .columns-vertical {
margin: auto; margin: auto;
.columns-vertical-title { .columns-vertical-title {
@@ -257,16 +247,6 @@ export default defineComponent({
color: var(--next-bg-columnsMenuBarColor); color: var(--next-bg-columnsMenuBarColor);
} }
} }
.layout-columns-active {
color: var(--next-bg-columnsMenuBarColor) !important;
transition: 0.3s ease-in-out;
}
.layout-columns-hover {
color: var(--el-color-primary);
a {
color: var(--el-color-primary);
}
}
.columns-round { .columns-round {
background: var(--el-color-primary); background: var(--el-color-primary);
color: var(--el-color-white); color: var(--el-color-white);

View File

@@ -1,34 +1,18 @@
<template> <template>
<el-header class="layout-header" :height="setHeaderHeight" v-show="!isTagsViewCurrenFull"> <el-header class="layout-header" v-show="!isTagsViewCurrenFull">
<NavBarsIndex /> <NavBarsIndex />
</el-header> </el-header>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutHeader">
import { computed, defineComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import NavBarsIndex from '/@/layout/navBars/index.vue';
export default defineComponent({ // 引入组件
name: 'layoutHeader', const NavBarsIndex = defineAsyncComponent(() => import('/@/layout/navBars/index.vue'));
components: { NavBarsIndex },
setup() { // 定义变量内容
const storesTagsViewRoutes = useTagsViewRoutes(); const storesTagsViewRoutes = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig(); const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
const { themeConfig } = storeToRefs(storesThemeConfig);
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
// 设置 header 的高度
const setHeaderHeight = computed(() => {
let { isTagsview, layout } = themeConfig.value;
if (isTagsview && layout !== 'classic') return '84px';
else return '50px';
});
return {
setHeaderHeight,
isTagsViewCurrenFull,
};
},
});
</script> </script>

View File

@@ -1,101 +1,65 @@
<template> <template>
<el-main class="layout-main"> <el-main class="layout-main" :style="isFixedHeader ? `height: calc(100% - ${setMainHeight})` : `minHeight: calc(100% - ${setMainHeight})`">
<el-scrollbar <el-scrollbar
ref="layoutScrollbarRef" ref="layoutMainScrollbarRef"
:class="{ class="layout-main-scroll layout-backtop-header-fixed"
'layout-scrollbar': wrap-class="layout-main-scroll"
(!isClassicOrTransverse && !currentRouteMeta.isLink && !currentRouteMeta.isIframe) || view-class="layout-main-scroll"
(!isClassicOrTransverse && currentRouteMeta.isLink && !currentRouteMeta.isIframe),
}"
> >
<LayoutParentView <LayoutParentView />
:style="{ <LayoutFooter v-if="isFooter" />
padding: !isClassicOrTransverse || (currentRouteMeta.isLink && currentRouteMeta.isIframe) ? '0' : '15px',
transition: 'padding 0.3s ease-in-out',
}"
/>
<Footer v-if="themeConfig.isFooter" />
</el-scrollbar> </el-scrollbar>
<el-backtop :target="setBacktopClass" />
</el-main> </el-main>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutMain">
import { defineComponent, toRefs, reactive, getCurrentInstance, watch, onMounted, computed } from 'vue'; import { defineAsyncComponent, onMounted, computed, ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import { NextLoading } from '/@/utils/loading'; import { NextLoading } from '/@/utils/loading';
import LayoutParentView from '/@/layout/routerView/parent.vue';
import Footer from '/@/layout/footer/index.vue';
// 定义接口来定义对象的类型 // 引入组件
interface MainState { const LayoutParentView = defineAsyncComponent(() => import('/@/layout/routerView/parent.vue'));
headerHeight: string | number; const LayoutFooter = defineAsyncComponent(() => import('/@/layout/footer/index.vue'));
currentRouteMeta: any;
}
export default defineComponent({ // 定义变量内容
name: 'layoutMain', const layoutMainScrollbarRef = ref();
components: { LayoutParentView, Footer }, const route = useRoute();
setup() { const storesTagsViewRoutes = useTagsViewRoutes();
const { proxy } = <any>getCurrentInstance(); const storesThemeConfig = useThemeConfig();
const storesThemeConfig = useThemeConfig(); const { themeConfig } = storeToRefs(storesThemeConfig);
const { themeConfig } = storeToRefs(storesThemeConfig); const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
const route = useRoute();
const state = reactive<MainState>({ // 设置 footer 显示/隐藏
headerHeight: '', const isFooter = computed(() => {
currentRouteMeta: {}, return themeConfig.value.isFooter && !route.meta.isIframe;
}); });
// 判断布局 // 设置 header 固定
const isClassicOrTransverse = computed(() => { const isFixedHeader = computed(() => {
const { layout } = themeConfig.value; return themeConfig.value.isFixedHeader;
return layout === 'classic' || layout === 'transverse'; });
}); // 设置 Backtop 回到顶部
// 设置 main 的高度 const setBacktopClass = computed(() => {
const initHeaderHeight = () => { if (themeConfig.value.isFixedHeader) return `.layout-backtop-header-fixed .el-scrollbar__wrap`;
const bool = state.currentRouteMeta.isLink && state.currentRouteMeta.isIframe; else return `.layout-backtop .el-scrollbar__wrap`;
let { isTagsview } = themeConfig.value; });
if (isTagsview) return (state.headerHeight = bool ? `86px` : `115px`); // 设置主内容区的高度
else return (state.headerHeight = `80px`); const setMainHeight = computed(() => {
}; if (isTagsViewCurrenFull.value) return '0px';
// 初始化获取当前路由 meta用于设置 iframes padding const { isTagsview, layout } = themeConfig.value;
const initGetMeta = () => { if (isTagsview && layout !== 'classic') return '85px';
state.currentRouteMeta = route.meta; else return '51px';
}; });
// 页面加载前 // 页面加载前
onMounted(async () => { onMounted(() => {
await initGetMeta(); NextLoading.done(600);
initHeaderHeight(); });
NextLoading.done();
}); // 暴露变量
// 监听路由变化 defineExpose({
watch( layoutMainScrollbarRef,
() => route.path,
() => {
state.currentRouteMeta = route.meta;
const bool = state.currentRouteMeta.isLink && state.currentRouteMeta.isIframe;
state.headerHeight = bool ? `86px` : `115px`;
proxy.$refs.layoutScrollbarRef.update();
}
);
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(
themeConfig,
(val) => {
state.currentRouteMeta = route.meta;
const bool = state.currentRouteMeta.isLink && state.currentRouteMeta.isIframe;
state.headerHeight = val.isTagsview ? (bool ? `86px` : `115px`) : '51px';
proxy.$refs?.layoutScrollbarRef?.update();
},
{
deep: true,
}
);
return {
themeConfig,
isClassicOrTransverse,
...toRefs(state),
};
},
}); });
</script> </script>

View File

@@ -1,36 +1,14 @@
<template> <template>
<div class="layout-footer mt15" v-show="isDelayFooter"> <div class="layout-footer pb15">
<div class="layout-footer-warp"> <div class="layout-footer-warp">
<div>vue-next-adminMade by lyt with </div> <div> Powered by Django-Vue3-Admin </div>
<div class="mt5">深圳市 xxx 公司版权所有</div> <div class="mt5">Copyright DVAdmin团队</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutFooter">
import { toRefs, reactive, defineComponent } from 'vue'; // 此处需有内容(注释也得),否则缓存将失败
import { onBeforeRouteUpdate } from 'vue-router';
export default defineComponent({
name: 'layoutFooter',
setup() {
const state = reactive({
isDelayFooter: true,
});
// 路由改变时,等主界面动画加载完毕再显示 footer
onBeforeRouteUpdate(() => {
setTimeout(() => {
state.isDelayFooter = false;
setTimeout(() => {
state.isDelayFooter = true;
}, 800);
}, 0);
});
return {
...toRefs(state),
};
},
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -41,7 +19,7 @@ export default defineComponent({
margin: auto; margin: auto;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
text-align: center; text-align: center;
animation: error-num 1s ease-in-out; animation: error-num 0.3s ease;
} }
} }
</style> </style>

View File

@@ -1,54 +1,50 @@
<template> <template>
<component :is="themeConfig.layout" /> <component :is="layouts[themeConfig.layout]" />
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layout">
import { onBeforeMount, onUnmounted, getCurrentInstance, defineComponent, defineAsyncComponent } from 'vue'; import { onBeforeMount, onUnmounted, defineAsyncComponent } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import { Local } from '/@/utils/storage'; import { Local } from '/@/utils/storage';
import mittBus from '/@/utils/mitt';
export default defineComponent({ // 引入组件
name: 'layout', const layouts: any = {
components: { defaults: defineAsyncComponent(() => import('/@/layout/main/defaults.vue')),
defaults: defineAsyncComponent(() => import('/@/layout/main/defaults.vue')), classic: defineAsyncComponent(() => import('/@/layout/main/classic.vue')),
classic: defineAsyncComponent(() => import('/@/layout/main/classic.vue')), transverse: defineAsyncComponent(() => import('/@/layout/main/transverse.vue')),
transverse: defineAsyncComponent(() => import('/@/layout/main/transverse.vue')), columns: defineAsyncComponent(() => import('/@/layout/main/columns.vue')),
columns: defineAsyncComponent(() => import('/@/layout/main/columns.vue')), };
},
setup() { // 定义变量内容
const { proxy } = <any>getCurrentInstance(); const storesThemeConfig = useThemeConfig();
const storesThemeConfig = useThemeConfig(); const { themeConfig } = storeToRefs(storesThemeConfig);
const { themeConfig } = storeToRefs(storesThemeConfig);
// 窗口大小改变时(适配移动端) // 窗口大小改变时(适配移动端)
const onLayoutResize = () => { const onLayoutResize = () => {
if (!Local.get('oldLayout')) Local.set('oldLayout', themeConfig.value.layout); if (!Local.get('oldLayout')) Local.set('oldLayout', themeConfig.value.layout);
const clientWidth = document.body.clientWidth; const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) { if (clientWidth < 1000) {
themeConfig.value.isCollapse = false; themeConfig.value.isCollapse = false;
proxy.mittBus.emit('layoutMobileResize', { mittBus.emit('layoutMobileResize', {
layout: 'defaults', layout: 'defaults',
clientWidth, clientWidth,
});
} else {
proxy.mittBus.emit('layoutMobileResize', {
layout: Local.get('oldLayout') ? Local.get('oldLayout') : themeConfig.value.layout,
clientWidth,
});
}
};
// 页面加载前
onBeforeMount(() => {
onLayoutResize();
window.addEventListener('resize', onLayoutResize);
}); });
// 页面卸载时 } else {
onUnmounted(() => { mittBus.emit('layoutMobileResize', {
window.removeEventListener('resize', onLayoutResize); layout: Local.get('oldLayout') ? Local.get('oldLayout') : themeConfig.value.layout,
clientWidth,
}); });
return { }
themeConfig, };
}; // 页面加载前
}, onBeforeMount(() => {
onLayoutResize();
window.addEventListener('resize', onLayoutResize);
});
// 页面卸载时
onUnmounted(() => {
window.removeEventListener('resize', onLayoutResize);
}); });
</script> </script>

View File

@@ -1,23 +1,23 @@
<template> <template>
<div v-show="isShowLockScreen"> <div v-show="state.isShowLockScreen">
<div class="layout-lock-screen-mask"></div> <div class="layout-lock-screen-mask"></div>
<div class="layout-lock-screen-img" :class="{ 'layout-lock-screen-filter': isShowLoockLogin }"></div> <div class="layout-lock-screen-img" :class="{ 'layout-lock-screen-filter': state.isShowLoockLogin }"></div>
<div class="layout-lock-screen"> <div class="layout-lock-screen">
<div <div
class="layout-lock-screen-date" class="layout-lock-screen-date"
ref="layoutLockScreenDateRef" ref="layoutLockScreenDateRef"
@mousedown="onDown" @mousedown="onDownPc"
@mousemove="onMove" @mousemove="onMovePc"
@mouseup="onEnd" @mouseup="onEnd"
@touchstart.stop="onDown" @touchstart.stop="onDownApp"
@touchmove.stop="onMove" @touchmove.stop="onMoveApp"
@touchend.stop="onEnd" @touchend.stop="onEnd"
> >
<div class="layout-lock-screen-date-box"> <div class="layout-lock-screen-date-box">
<div class="layout-lock-screen-date-box-time"> <div class="layout-lock-screen-date-box-time">
{{ time.hm }}<span class="layout-lock-screen-date-box-minutes">{{ time.s }}</span> {{ state.time.hm }}<span class="layout-lock-screen-date-box-minutes">{{ state.time.s }}</span>
</div> </div>
<div class="layout-lock-screen-date-box-info">{{ time.mdq }}</div> <div class="layout-lock-screen-date-box-info">{{ state.time.mdq }}</div>
</div> </div>
<div class="layout-lock-screen-date-top"> <div class="layout-lock-screen-date-top">
<SvgIcon name="ele-Top" /> <SvgIcon name="ele-Top" />
@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<transition name="el-zoom-in-center"> <transition name="el-zoom-in-center">
<div v-show="isShowLoockLogin" class="layout-lock-screen-login"> <div v-show="state.isShowLoockLogin" class="layout-lock-screen-login">
<div class="layout-lock-screen-login-box"> <div class="layout-lock-screen-login-box">
<div class="layout-lock-screen-login-box-img"> <div class="layout-lock-screen-login-box-img">
<img src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" /> <img src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" />
@@ -35,7 +35,7 @@
<el-input <el-input
placeholder="请输入密码" placeholder="请输入密码"
ref="layoutLockScreenInputRef" ref="layoutLockScreenInputRef"
v-model="lockScreenPassword" v-model="state.lockScreenPassword"
@keyup.enter.native.stop="onLockScreenSubmit()" @keyup.enter.native.stop="onLockScreenSubmit()"
> >
<template #append> <template #append>
@@ -59,159 +59,139 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutLockScreen">
import { nextTick, onMounted, reactive, toRefs, ref, onUnmounted, getCurrentInstance, defineComponent } from 'vue'; import { nextTick, onMounted, reactive, ref, onUnmounted } from 'vue';
import { formatDate } from '/@/utils/formatTime'; import { formatDate } from '/@/utils/formatTime';
import { Local } from '/@/utils/storage'; import { Local } from '/@/utils/storage';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
// 定义接口来定义对象的类型 // 定义变量内容
interface LockScreenState { const layoutLockScreenDateRef = ref<HtmlType>();
transparency: number; const layoutLockScreenInputRef = ref();
downClientY: number; const storesThemeConfig = useThemeConfig();
moveDifference: number; const { themeConfig } = storeToRefs(storesThemeConfig);
isShowLoockLogin: boolean; const state = reactive({
isFlags: boolean; transparency: 1,
querySelectorEl: HTMLElement | string; downClientY: 0,
moveDifference: 0,
isShowLoockLogin: false,
isFlags: false,
querySelectorEl: '' as HtmlType,
time: { time: {
hm: string; hm: '',
s: string; s: '',
mdq: string; mdq: '',
};
setIntervalTime: number;
isShowLockScreen: boolean;
isShowLockScreenIntervalTime: number;
lockScreenPassword: string;
}
export default defineComponent({
name: 'layoutLockScreen',
setup() {
const { proxy } = <any>getCurrentInstance();
const layoutLockScreenInputRef = ref();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const state = reactive<LockScreenState>({
transparency: 1,
downClientY: 0,
moveDifference: 0,
isShowLoockLogin: false,
isFlags: false,
querySelectorEl: '',
time: {
hm: '',
s: '',
mdq: '',
},
setIntervalTime: 0,
isShowLockScreen: false,
isShowLockScreenIntervalTime: 0,
lockScreenPassword: '',
});
// 鼠标按下
const onDown = (down: any) => {
state.isFlags = true;
state.downClientY = down.touches ? down.touches[0].clientY : down.clientY;
};
// 鼠标移动
const onMove = (move: any) => {
if (state.isFlags) {
const el = <HTMLElement>state.querySelectorEl;
const opacitys = (state.transparency -= 1 / 200);
if (move.touches) {
state.moveDifference = move.touches[0].clientY - state.downClientY;
} else {
state.moveDifference = move.clientY - state.downClientY;
}
if (state.moveDifference >= 0) return false;
el.setAttribute('style', `top:${state.moveDifference}px;cursor:pointer;opacity:${opacitys};`);
if (state.moveDifference < -400) {
el.setAttribute('style', `top:${-el.clientHeight}px;cursor:pointer;transition:all 0.3s ease;`);
state.moveDifference = -el.clientHeight;
setTimeout(() => {
el && el.parentNode?.removeChild(el);
}, 300);
}
if (state.moveDifference === -el.clientHeight) {
state.isShowLoockLogin = true;
layoutLockScreenInputRef.value.focus();
}
}
};
// 鼠标松开
const onEnd = () => {
state.isFlags = false;
state.transparency = 1;
if (state.moveDifference >= -400) {
(<HTMLElement>state.querySelectorEl).setAttribute('style', `top:0px;opacity:1;transition:all 0.3s ease;`);
}
};
// 获取要拖拽的初始元素
const initGetElement = () => {
nextTick(() => {
state.querySelectorEl = proxy.$refs.layoutLockScreenDateRef;
});
};
// 时间初始化
const initTime = () => {
state.time.hm = formatDate(new Date(), 'HH:MM');
state.time.s = formatDate(new Date(), 'SS');
state.time.mdq = formatDate(new Date(), 'mm月dd日WWW');
};
// 时间初始化定时器
const initSetTime = () => {
initTime();
state.setIntervalTime = window.setInterval(() => {
initTime();
}, 1000);
};
// 锁屏时间定时器
const initLockScreen = () => {
if (themeConfig.value.isLockScreen) {
state.isShowLockScreenIntervalTime = window.setInterval(() => {
if (themeConfig.value.lockScreenTime <= 1) {
state.isShowLockScreen = true;
setLocalThemeConfig();
return false;
}
themeConfig.value.lockScreenTime--;
}, 1000);
} else {
clearInterval(state.isShowLockScreenIntervalTime);
}
};
// 存储布局配置
const setLocalThemeConfig = () => {
themeConfig.value.isDrawer = false;
Local.set('themeConfig', themeConfig.value);
};
// 密码输入点击事件
const onLockScreenSubmit = () => {
themeConfig.value.isLockScreen = false;
themeConfig.value.lockScreenTime = 30;
setLocalThemeConfig();
};
// 页面加载时
onMounted(() => {
initGetElement();
initSetTime();
initLockScreen();
});
// 页面卸载时
onUnmounted(() => {
window.clearInterval(state.setIntervalTime);
window.clearInterval(state.isShowLockScreenIntervalTime);
});
return {
layoutLockScreenInputRef,
onDown,
onMove,
onEnd,
onLockScreenSubmit,
...toRefs(state),
};
}, },
setIntervalTime: 0,
isShowLockScreen: false,
isShowLockScreenIntervalTime: 0,
lockScreenPassword: '',
});
// 鼠标按下 pc
const onDownPc = (down: MouseEvent) => {
state.isFlags = true;
state.downClientY = down.clientY;
};
// 鼠标按下 app
const onDownApp = (down: TouchEvent) => {
state.isFlags = true;
state.downClientY = down.touches[0].clientY;
};
// 鼠标移动 pc
const onMovePc = (move: MouseEvent) => {
state.moveDifference = move.clientY - state.downClientY;
onMove();
};
// 鼠标移动 app
const onMoveApp = (move: TouchEvent) => {
state.moveDifference = move.touches[0].clientY - state.downClientY;
onMove();
};
// 鼠标移动事件
const onMove = () => {
if (state.isFlags) {
const el = <HTMLElement>state.querySelectorEl;
const opacitys = (state.transparency -= 1 / 200);
if (state.moveDifference >= 0) return false;
el.setAttribute('style', `top:${state.moveDifference}px;cursor:pointer;opacity:${opacitys};`);
if (state.moveDifference < -400) {
el.setAttribute('style', `top:${-el.clientHeight}px;cursor:pointer;transition:all 0.3s ease;`);
state.moveDifference = -el.clientHeight;
setTimeout(() => {
el && el.parentNode?.removeChild(el);
}, 300);
}
if (state.moveDifference === -el.clientHeight) {
state.isShowLoockLogin = true;
layoutLockScreenInputRef.value.focus();
}
}
};
// 鼠标松开
const onEnd = () => {
state.isFlags = false;
state.transparency = 1;
if (state.moveDifference >= -400) {
(<HTMLElement>state.querySelectorEl).setAttribute('style', `top:0px;opacity:1;transition:all 0.3s ease;`);
}
};
// 获取要拖拽的初始元素
const initGetElement = () => {
nextTick(() => {
state.querySelectorEl = layoutLockScreenDateRef.value;
});
};
// 时间初始化
const initTime = () => {
state.time.hm = formatDate(new Date(), 'HH:MM');
state.time.s = formatDate(new Date(), 'SS');
state.time.mdq = formatDate(new Date(), 'mm月dd日WWW');
};
// 时间初始化定时器
const initSetTime = () => {
initTime();
state.setIntervalTime = window.setInterval(() => {
initTime();
}, 1000);
};
// 锁屏时间定时器
const initLockScreen = () => {
if (themeConfig.value.isLockScreen) {
state.isShowLockScreenIntervalTime = window.setInterval(() => {
if (themeConfig.value.lockScreenTime <= 1) {
state.isShowLockScreen = true;
setLocalThemeConfig();
return false;
}
themeConfig.value.lockScreenTime--;
}, 1000);
} else {
clearInterval(state.isShowLockScreenIntervalTime);
}
};
// 存储布局配置
const setLocalThemeConfig = () => {
themeConfig.value.isDrawer = false;
Local.set('themeConfig', themeConfig.value);
};
// 密码输入点击事件
const onLockScreenSubmit = () => {
themeConfig.value.isLockScreen = false;
themeConfig.value.lockScreenTime = 30;
setLocalThemeConfig();
};
// 页面加载时
onMounted(() => {
initGetElement();
initSetTime();
initLockScreen();
});
// 页面卸载时
onUnmounted(() => {
window.clearInterval(state.setIntervalTime);
window.clearInterval(state.isShowLockScreenIntervalTime);
}); });
</script> </script>

View File

@@ -1,43 +1,33 @@
<template> <template>
<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange"> <div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
<img :src="logoMini" class="layout-logo-medium-img" /> <img :src="logoMini" class="layout-logo-medium-img" />
<span>{{ themeConfig.globalTitle }}</span> <span style="font-size: x-large">{{ themeConfig.globalTitle }}</span>
</div> </div>
<div class="layout-logo-size" v-else @click="onThemeConfigChange"> <div class="layout-logo-size" v-else @click="onThemeConfigChange">
<img :src="logoMini" class="layout-logo-size-img" /> <img :src="logoMini" class="layout-logo-size-img" />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutLogo">
import { computed, defineComponent } from 'vue'; import { computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import logoMini from '/@/assets/logo-mini.svg'; import logoMini from '/@/assets/logo-mini.svg';
export default defineComponent({ // 定义变量内容
name: 'layoutLogo', const storesThemeConfig = useThemeConfig();
setup() { const { themeConfig } = storeToRefs(storesThemeConfig);
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig); // 设置 logo 的显示。classic 经典布局默认显示 logo
// 设置 logo 的显示。classic 经典布局默认显示 logo const setShowLogo = computed(() => {
const setShowLogo = computed(() => { let { isCollapse, layout } = themeConfig.value;
let { isCollapse, layout } = themeConfig.value; return !isCollapse || layout === 'classic' || document.body.clientWidth < 1000;
return !isCollapse || layout === 'classic' || document.body.clientWidth < 1000;
});
// logo 点击实现菜单展开/收起
const onThemeConfigChange = () => {
if (themeConfig.value.layout === 'transverse') return false;
themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
};
return {
logoMini,
setShowLogo,
themeConfig,
onThemeConfigChange,
};
},
}); });
// logo 点击实现菜单展开/收起
const onThemeConfigChange = () => {
if (themeConfig.value.layout === 'transverse') return false;
themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -62,7 +52,7 @@ export default defineComponent({
} }
} }
&-medium-img { &-medium-img {
width: 20px; width: 40px;
margin-right: 5px; margin-right: 5px;
} }
} }
@@ -73,7 +63,7 @@ export default defineComponent({
cursor: pointer; cursor: pointer;
animation: logoAnimation 0.3s ease-in-out; animation: logoAnimation 0.3s ease-in-out;
&-img { &-img {
width: 20px; width: 40px;
margin: auto; margin: auto;
} }
&:hover { &:hover {

View File

@@ -1,35 +1,71 @@
<template> <template>
<el-container class="layout-container flex-center"> <el-container class="layout-container flex-center">
<Header /> <LayoutHeader />
<el-container class="layout-mian-height-50"> <el-container class="layout-mian-height-50">
<Aside /> <LayoutAside />
<div class="flex-center layout-backtop"> <div class="flex-center layout-backtop">
<TagsView v-if="themeConfig.isTagsview" /> <LayoutTagsView v-if="isTagsview" />
<Main style="padding: 10px !important" /> <LayoutMain ref="layoutMainRef" />
</div> </div>
</el-container> </el-container>
<el-backtop target=".layout-backtop .el-main .el-scrollbar__wrap"></el-backtop>
</el-container> </el-container>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutClassic">
import { defineComponent } from 'vue'; import { defineAsyncComponent, computed, ref, watch, nextTick, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import Aside from '/@/layout/component/aside.vue';
import Header from '/@/layout/component/header.vue';
import Main from '/@/layout/component/main.vue';
import TagsView from '/@/layout/navBars/tagsView/tagsView.vue';
export default defineComponent({ // 引入组件
name: 'layoutClassic', const LayoutAside = defineAsyncComponent(() => import('/@/layout/component/aside.vue'));
components: { Aside, Header, Main, TagsView }, const LayoutHeader = defineAsyncComponent(() => import('/@/layout/component/header.vue'));
setup() { const LayoutMain = defineAsyncComponent(() => import('/@/layout/component/main.vue'));
const storesThemeConfig = useThemeConfig(); const LayoutTagsView = defineAsyncComponent(() => import('/@/layout/navBars/tagsView/tagsView.vue'));
const { themeConfig } = storeToRefs(storesThemeConfig);
return { // 定义变量内容
themeConfig, const layoutMainRef = ref<InstanceType<typeof LayoutMain>>();
}; const route = useRoute();
}, const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 判断是否显示 tasgview
const isTagsview = computed(() => {
return themeConfig.value.isTagsview;
}); });
// 重置滚动条高度,更新子级 scrollbar
const updateScrollbar = () => {
layoutMainRef.value?.layoutMainScrollbarRef.update();
};
// 重置滚动条高度,由于组件是异步引入的
const initScrollBarHeight = () => {
nextTick(() => {
setTimeout(() => {
updateScrollbar();
// '!' not null 断言操作符,不执行运行时检查
layoutMainRef.value!.layoutMainScrollbarRef.wrapRef.scrollTop = 0;
}, 500);
});
};
// 页面加载时
onMounted(() => {
initScrollBarHeight();
});
// 监听路由的变化,切换界面时,滚动条置顶
watch(
() => route.path,
() => {
initScrollBarHeight();
}
);
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(
themeConfig,
() => {
updateScrollbar();
},
{
deep: true,
}
);
</script> </script>

View File

@@ -1,41 +1,71 @@
<template> <template>
<el-container class="layout-container"> <el-container class="layout-container">
<ColumnsAside /> <ColumnsAside />
<div class="layout-columns-warp"> <el-container class="layout-columns-warp layout-container-view h100">
<Aside /> <LayoutAside />
<el-container class="flex-center layout-backtop" :class="{ 'layout-backtop': !isFixedHeader }"> <el-scrollbar ref="layoutScrollbarRef" class="layout-backtop">
<Header v-if="isFixedHeader" /> <LayoutHeader />
<el-scrollbar :class="{ 'layout-backtop': isFixedHeader }"> <LayoutMain ref="layoutMainRef" />
<Header v-if="!isFixedHeader" /> </el-scrollbar>
<Main style="padding: 10px !important;height: 100vh" /> </el-container>
</el-scrollbar>
</el-container>
</div>
<el-backtop target=".layout-backtop .el-scrollbar__wrap"></el-backtop>
</el-container> </el-container>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutColumns">
import { computed, defineComponent } from 'vue'; import { defineAsyncComponent, watch, onMounted, nextTick, ref } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import Aside from '/@/layout/component/aside.vue';
import Header from '/@/layout/component/header.vue';
import Main from '/@/layout/component/main.vue';
import ColumnsAside from '/@/layout/component/columnsAside.vue';
export default defineComponent({ // 引入组件
name: 'layoutColumns', const LayoutAside = defineAsyncComponent(() => import('/@/layout/component/aside.vue'));
components: { Aside, Header, Main, ColumnsAside }, const LayoutHeader = defineAsyncComponent(() => import('/@/layout/component/header.vue'));
setup() { const LayoutMain = defineAsyncComponent(() => import('/@/layout/component/main.vue'));
const storesThemeConfig = useThemeConfig(); const ColumnsAside = defineAsyncComponent(() => import('/@/layout/component/columnsAside.vue'));
const { themeConfig } = storeToRefs(storesThemeConfig);
const isFixedHeader = computed(() => { // 定义变量内容
return themeConfig.value.isFixedHeader; const layoutScrollbarRef = ref<RefType>('');
}); const layoutMainRef = ref<InstanceType<typeof LayoutMain>>();
return { const route = useRoute();
isFixedHeader, const storesThemeConfig = useThemeConfig();
}; const { themeConfig } = storeToRefs(storesThemeConfig);
},
// 重置滚动条高度
const updateScrollbar = () => {
// 更新父级 scrollbar
layoutScrollbarRef.value.update();
// 更新子级 scrollbar
layoutMainRef.value!.layoutMainScrollbarRef.update();
};
// 重置滚动条高度,由于组件是异步引入的
const initScrollBarHeight = () => {
nextTick(() => {
setTimeout(() => {
updateScrollbar();
layoutScrollbarRef.value.wrapRef.scrollTop = 0;
layoutMainRef.value!.layoutMainScrollbarRef.wrapRef.scrollTop = 0;
}, 500);
});
};
// 页面加载时
onMounted(() => {
initScrollBarHeight();
}); });
// 监听路由的变化,切换界面时,滚动条置顶
watch(
() => route.path,
() => {
initScrollBarHeight();
}
);
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(
themeConfig,
() => {
updateScrollbar();
},
{
deep: true,
}
);
</script> </script>

View File

@@ -1,47 +1,71 @@
<template> <template>
<el-container class="layout-container"> <el-container class="layout-container">
<Aside /> <LayoutAside />
<el-container class="flex-center" :class="{ 'layout-backtop': !isFixedHeader }"> <el-container class="layout-container-view h100">
<Header v-if="isFixedHeader" /> <el-scrollbar ref="layoutScrollbarRef" class="layout-backtop">
<el-scrollbar ref="layoutDefaultsScrollbarRef" :class="{ 'layout-backtop': isFixedHeader }"> <LayoutHeader />
<Header v-if="!isFixedHeader" /> <LayoutMain ref="layoutMainRef" />
<Main style="padding: 10px !important;height: 100vh" />
</el-scrollbar> </el-scrollbar>
</el-container> </el-container>
<el-backtop target=".layout-backtop .el-scrollbar__wrap"></el-backtop>
</el-container> </el-container>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutDefaults">
import { computed, getCurrentInstance, watch, defineComponent } from 'vue'; import { defineAsyncComponent, watch, onMounted, nextTick, ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import Aside from '/@/layout/component/aside.vue'; import { NextLoading } from '/@/utils/loading';
import Header from '/@/layout/component/header.vue';
import Main from '/@/layout/component/main.vue';
export default defineComponent({ // 引入组件
name: 'layoutDefaults', const LayoutAside = defineAsyncComponent(() => import('/@/layout/component/aside.vue'));
components: { Aside, Header, Main }, const LayoutHeader = defineAsyncComponent(() => import('/@/layout/component/header.vue'));
setup() { const LayoutMain = defineAsyncComponent(() => import('/@/layout/component/main.vue'));
const { proxy } = <any>getCurrentInstance();
const route = useRoute(); // 定义变量内容
const storesThemeConfig = useThemeConfig(); const layoutScrollbarRef = ref<RefType>('');
const { themeConfig } = storeToRefs(storesThemeConfig); const layoutMainRef = ref<InstanceType<typeof LayoutMain>>();
const isFixedHeader = computed(() => { const route = useRoute();
return themeConfig.value.isFixedHeader; const storesThemeConfig = useThemeConfig();
}); const { themeConfig } = storeToRefs(storesThemeConfig);
// 监听路由的变化
watch( // 重置滚动条高度
() => route.path, const updateScrollbar = () => {
() => { // 更新父级 scrollbar
proxy.$refs.layoutDefaultsScrollbarRef.wrap$.scrollTop = 0; layoutScrollbarRef.value.update();
} // 更新子级 scrollbar
); layoutMainRef.value!.layoutMainScrollbarRef.update();
return { };
isFixedHeader, // 重置滚动条高度,由于组件是异步引入的
}; const initScrollBarHeight = () => {
}, nextTick(() => {
setTimeout(() => {
updateScrollbar();
layoutScrollbarRef.value.wrapRef.scrollTop = 0;
layoutMainRef.value!.layoutMainScrollbarRef.wrapRef.scrollTop = 0;
}, 500);
});
};
// 页面加载时
onMounted(() => {
initScrollBarHeight();
NextLoading.done(600);
}); });
// 监听路由的变化,切换界面时,滚动条置顶
watch(
() => route.path,
() => {
initScrollBarHeight();
}
);
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(
themeConfig,
() => {
updateScrollbar();
},
{
deep: true,
}
);
</script> </script>

View File

@@ -1,17 +1,58 @@
<template> <template>
<el-container class="layout-container flex-center layout-backtop"> <el-container class="layout-container flex-center layout-backtop">
<Header /> <LayoutHeader />
<Main style="padding: 10px !important" /> <LayoutMain ref="layoutMainRef" />
<el-backtop target=".layout-backtop .el-main .el-scrollbar__wrap"></el-backtop>
</el-container> </el-container>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutTransverse">
import Header from '/@/layout/component/header.vue'; import { defineAsyncComponent, ref, watch, nextTick, onMounted } from 'vue';
import Main from '/@/layout/component/main.vue'; import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
export default { // 引入组件
name: 'layoutTransverse', const LayoutHeader = defineAsyncComponent(() => import('/@/layout/component/header.vue'));
components: { Header, Main }, const LayoutMain = defineAsyncComponent(() => import('/@/layout/component/main.vue'));
// 定义变量内容
const layoutMainRef = ref<InstanceType<typeof LayoutMain>>();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute();
// 重置滚动条高度,更新子级 scrollbar
const updateScrollbar = () => {
layoutMainRef.value!.layoutMainScrollbarRef.update();
}; };
// 重置滚动条高度,由于组件是异步引入的
const initScrollBarHeight = () => {
nextTick(() => {
setTimeout(() => {
updateScrollbar();
layoutMainRef.value!.layoutMainScrollbarRef.wrapRef.scrollTop = 0;
}, 500);
});
};
// 页面加载时
onMounted(() => {
initScrollBarHeight();
});
// 监听路由的变化,切换界面时,滚动条置顶
watch(
() => route.path,
() => {
initScrollBarHeight();
}
);
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(
themeConfig,
() => {
updateScrollbar();
},
{
deep: true,
}
);
</script> </script>

View File

@@ -8,8 +8,8 @@
/> />
<el-breadcrumb class="layout-navbars-breadcrumb-hide"> <el-breadcrumb class="layout-navbars-breadcrumb-hide">
<transition-group name="breadcrumb"> <transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(v, k) in breadcrumbList" :key="!v.meta.tagsViewName ? v.meta.title : v.meta.tagsViewName"> <el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="!v.meta.tagsViewName ? v.meta.title : v.meta.tagsViewName">
<span v-if="k === breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span"> <span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" /> <SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
<div v-if="!v.meta.tagsViewName">{{ $t(v.meta.title) }}</div> <div v-if="!v.meta.tagsViewName">{{ $t(v.meta.title) }}</div>
<div v-else>{{ v.meta.tagsViewName }}</div> <div v-else>{{ v.meta.tagsViewName }}</div>
@@ -23,8 +23,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutBreadcrumb">
import { toRefs, reactive, computed, onMounted, defineComponent } from 'vue'; import { reactive, computed, onMounted } from 'vue';
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'; import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
import { Local } from '/@/utils/storage'; import { Local } from '/@/utils/storage';
import other from '/@/utils/other'; import other from '/@/utils/other';
@@ -32,93 +32,76 @@ import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import { useRoutesList } from '/@/stores/routesList'; import { useRoutesList } from '/@/stores/routesList';
// 定义接口来定义对象的类型 // 定义变量内容
interface BreadcrumbState { const stores = useRoutesList();
breadcrumbList: Array<any>; const storesThemeConfig = useThemeConfig();
routeSplit: Array<string>; const { themeConfig } = storeToRefs(storesThemeConfig);
routeSplitFirst: string; const { routesList } = storeToRefs(stores);
routeSplitIndex: number; const route = useRoute();
} const router = useRouter();
const state = reactive<BreadcrumbState>({
breadcrumbList: [],
routeSplit: [],
routeSplitFirst: '',
routeSplitIndex: 1,
});
export default defineComponent({ // 动态设置经典、横向布局不显示
name: 'layoutBreadcrumb', const isShowBreadcrumb = computed(() => {
setup() { initRouteSplit(route.path);
const stores = useRoutesList(); const { layout, isBreadcrumb } = themeConfig.value;
const storesThemeConfig = useThemeConfig(); if (layout === 'classic' || layout === 'transverse') return false;
const { themeConfig } = storeToRefs(storesThemeConfig); else return isBreadcrumb ? true : false;
const { routesList } = storeToRefs(stores); });
const route = useRoute(); // 面包屑点击时
const router = useRouter(); const onBreadcrumbClick = (v: RouteItem) => {
const state = reactive<BreadcrumbState>({ const { redirect, path } = v;
breadcrumbList: [], if (redirect) router.push(redirect);
routeSplit: [], else router.push(path);
routeSplitFirst: '', };
routeSplitIndex: 1, // 展开/收起左侧菜单点击
const onThemeConfigChange = () => {
themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
setLocalThemeConfig();
};
// 存储布局配置
const setLocalThemeConfig = () => {
Local.remove('themeConfig');
Local.set('themeConfig', themeConfig.value);
};
// 处理面包屑数据
const getBreadcrumbList = (arr: RouteItems) => {
arr.forEach((item: RouteItem) => {
state.routeSplit.forEach((v: string, k: number, arrs: string[]) => {
if (state.routeSplitFirst === item.path) {
state.routeSplitFirst += `/${arrs[state.routeSplitIndex]}`;
state.breadcrumbList.push(item);
state.routeSplitIndex++;
if (item.children) getBreadcrumbList(item.children);
}
}); });
// 动态设置经典、横向布局不显示 });
const isShowBreadcrumb = computed(() => { };
initRouteSplit(route.path); // 当前路由字符串切割成数组,并删除第一项空内容
const { layout, isBreadcrumb } = themeConfig.value; const initRouteSplit = (path: string) => {
if (layout === 'classic' || layout === 'transverse') return false; if (!themeConfig.value.isBreadcrumb) return false;
else return isBreadcrumb ? true : false; state.breadcrumbList = [routesList.value[0]];
}); state.routeSplit = path.split('/');
// 面包屑点击时 state.routeSplit.shift();
const onBreadcrumbClick = (v: any) => { state.routeSplitFirst = `/${state.routeSplit[0]}`;
const { redirect, path } = v; state.routeSplitIndex = 1;
if (redirect) router.push(redirect); getBreadcrumbList(routesList.value);
else router.push(path); if (route.name === 'home' || (route.name === 'notFound' && state.breadcrumbList.length > 1)) state.breadcrumbList.shift();
}; if (state.breadcrumbList.length > 0)
// 展开/收起左侧菜单点击 state.breadcrumbList[state.breadcrumbList.length - 1].meta.tagsViewName = other.setTagsViewNameI18n(<RouteToFrom>route);
const onThemeConfigChange = () => { };
themeConfig.value.isCollapse = !themeConfig.value.isCollapse; // 页面加载时
setLocalThemeConfig(); onMounted(() => {
}; initRouteSplit(route.path);
// 存储布局配置 });
const setLocalThemeConfig = () => { // 路由更新时
Local.remove('themeConfig'); onBeforeRouteUpdate((to) => {
Local.set('themeConfig', themeConfig.value); initRouteSplit(to.path);
};
// 处理面包屑数据
const getBreadcrumbList = (arr: Array<string>) => {
arr.forEach((item: any) => {
state.routeSplit.forEach((v: any, k: number, arrs: any) => {
if (state.routeSplitFirst === item.path) {
state.routeSplitFirst += `/${arrs[state.routeSplitIndex]}`;
state.breadcrumbList.push(item);
state.routeSplitIndex++;
if (item.children) getBreadcrumbList(item.children);
}
});
});
};
// 当前路由字符串切割成数组,并删除第一项空内容
const initRouteSplit = (path: string) => {
if (!themeConfig.value.isBreadcrumb) return false;
state.breadcrumbList = [routesList.value[0]];
state.routeSplit = path.split('/');
state.routeSplit.shift();
state.routeSplitFirst = `/${state.routeSplit[0]}`;
state.routeSplitIndex = 1;
getBreadcrumbList(routesList.value);
if (route.name === 'home' || (route.name === 'notFound' && state.breadcrumbList.length > 1)) state.breadcrumbList.shift();
if (state.breadcrumbList.length > 0) state.breadcrumbList[state.breadcrumbList.length - 1].meta.tagsViewName = other.setTagsViewNameI18n(route);
};
// 页面加载时
onMounted(() => {
initRouteSplit(route.path);
});
// 路由更新时
onBeforeRouteUpdate((to) => {
initRouteSplit(to.path);
});
return {
onThemeConfigChange,
isShowBreadcrumb,
themeConfig,
onBreadcrumbClick,
...toRefs(state),
};
},
}); });
</script> </script>

View File

@@ -6,26 +6,18 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutCloseFull">
import { defineComponent } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
export default defineComponent({ // 定义变量内容
name: 'layoutCloseFull', const stores = useTagsViewRoutes();
setup() { const { isTagsViewCurrenFull } = storeToRefs(stores);
const stores = useTagsViewRoutes();
const { isTagsViewCurrenFull } = storeToRefs(stores); // 关闭当前全屏
// 关闭当前全屏 const onCloseFullscreen = () => {
const onCloseFullscreen = () => { stores.setCurrenFullscreen(false);
stores.setCurrenFullscreen(false); };
};
return {
isTagsViewCurrenFull,
onCloseFullscreen,
};
},
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -2,109 +2,97 @@
<div class="layout-navbars-breadcrumb-index"> <div class="layout-navbars-breadcrumb-index">
<Logo v-if="setIsShowLogo" /> <Logo v-if="setIsShowLogo" />
<Breadcrumb /> <Breadcrumb />
<Horizontal :menuList="menuList" v-if="isLayoutTransverse" /> <Horizontal :menuList="state.menuList" v-if="isLayoutTransverse" />
<User /> <User />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutBreadcrumbIndex">
import { computed, reactive, toRefs, onMounted, onUnmounted, getCurrentInstance, defineComponent } from 'vue'; import { defineAsyncComponent, computed, reactive, onMounted, onUnmounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useRoutesList } from '/@/stores/routesList'; import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import Breadcrumb from '/@/layout/navBars/breadcrumb/breadcrumb.vue'; import mittBus from '/@/utils/mitt';
import User from '/@/layout/navBars/breadcrumb/user.vue';
import Logo from '/@/layout/logo/index.vue';
import Horizontal from '/@/layout/navMenu/horizontal.vue';
// 定义接口来定义对象的类型 // 引入组件
interface IndexState { const Breadcrumb = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/breadcrumb.vue'));
menuList: object[]; const User = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/user.vue'));
} const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
const Horizontal = defineAsyncComponent(() => import('/@/layout/navMenu/horizontal.vue'));
export default defineComponent({ // 定义变量内容
name: 'layoutBreadcrumbIndex', const stores = useRoutesList();
components: { Breadcrumb, User, Logo, Horizontal }, const storesThemeConfig = useThemeConfig();
setup() { const { themeConfig } = storeToRefs(storesThemeConfig);
const { proxy } = <any>getCurrentInstance(); const { routesList } = storeToRefs(stores);
const stores = useRoutesList(); const route = useRoute();
const storesThemeConfig = useThemeConfig(); const state = reactive({
const { themeConfig } = storeToRefs(storesThemeConfig); menuList: [] as RouteItems,
const { routesList } = storeToRefs(stores); });
const route = useRoute();
const state = reactive<IndexState>({ // 设置 logo 显示/隐藏
menuList: [], const setIsShowLogo = computed(() => {
let { isShowLogo, layout } = themeConfig.value;
return (isShowLogo && layout === 'classic') || (isShowLogo && layout === 'transverse');
});
// 设置是否显示横向导航菜单
const isLayoutTransverse = computed(() => {
let { layout, isClassicSplitMenu } = themeConfig.value;
return layout === 'transverse' || (isClassicSplitMenu && layout === 'classic');
});
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = delClassicChildren(filterRoutesFun(routesList.value));
const resData = setSendClassicChildren(route.path);
mittBus.emit('setSendClassicChildren', resData);
} else {
state.menuList = filterRoutesFun(routesList.value);
}
};
// 设置了分割菜单时,删除底下 children
const delClassicChildren = <T extends ChilType>(arr: T[]): T[] => {
arr.map((v: T) => {
if (v.children) delete v.children;
});
return arr;
};
// 路由过滤递归函数
const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
return arr
.filter((item: T) => !item.meta?.isHide)
.map((item: T) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
}); });
// 设置 logo 显示/隐藏 };
const setIsShowLogo = computed(() => { // 传送当前子级数据到菜单中
let { isShowLogo, layout } = themeConfig.value; const setSendClassicChildren = (path: string) => {
return (isShowLogo && layout === 'classic') || (isShowLogo && layout === 'transverse'); const currentPathSplit = path.split('/');
}); let currentData: MittMenu = { children: [] };
// 设置是否显示横向导航菜单 filterRoutesFun(routesList.value).map((v: RouteItem, k: number) => {
const isLayoutTransverse = computed(() => { if (v.path === `/${currentPathSplit[1]}`) {
let { layout, isClassicSplitMenu } = themeConfig.value; v['k'] = k;
return layout === 'transverse' || (isClassicSplitMenu && layout === 'classic'); currentData['item'] = { ...v };
}); currentData['children'] = [{ ...v }];
// 设置/过滤路由(非静态路由/是否显示在菜单中) if (v.children) currentData['children'] = v.children;
const setFilterRoutes = () => { }
let { layout, isClassicSplitMenu } = themeConfig.value; });
if (layout === 'classic' && isClassicSplitMenu) { return currentData;
state.menuList = delClassicChildren(filterRoutesFun(routesList.value)); };
const resData = setSendClassicChildren(route.path); // 页面加载时
proxy.mittBus.emit('setSendClassicChildren', resData); onMounted(() => {
} else { setFilterRoutes();
state.menuList = filterRoutesFun(routesList.value); mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
} setFilterRoutes();
}; });
// 设置了分割菜单时,删除底下 children });
const delClassicChildren = (arr: Array<object>) => { // 页面卸载时
arr.map((v: any) => { onUnmounted(() => {
if (v.children) delete v.children; mittBus.off('getBreadcrumbIndexSetFilterRoutes', () => {});
});
return arr;
};
// 路由过滤递归函数
const filterRoutesFun = (arr: Array<string>) => {
return arr
.filter((item: any) => !item.meta.isHide)
.map((item: any) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// 传送当前子级数据到菜单中
const setSendClassicChildren = (path: string) => {
const currentPathSplit = path.split('/');
let currentData: any = {};
filterRoutesFun(routesList.value).map((v, k) => {
if (v.path === `/${currentPathSplit[1]}`) {
v['k'] = k;
currentData['item'] = [{ ...v }];
currentData['children'] = [{ ...v }];
if (v.children) currentData['children'] = v.children;
}
});
return currentData;
};
// 页面加载时
onMounted(() => {
setFilterRoutes();
proxy.mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
setFilterRoutes();
});
});
// 页面卸载时
onUnmounted(() => {
proxy.mittBus.off('getBreadcrumbIndexSetFilterRoutes', () => {});
});
return {
setIsShowLogo,
isLayoutTransverse,
...toRefs(state),
};
},
}); });
</script> </script>

View File

@@ -1,136 +1,122 @@
<template> <template>
<div class="layout-search-dialog"> <div class="layout-search-dialog">
<el-dialog v-model="isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false"> <el-dialog v-model="state.isShowSearch" destroy-on-close :show-close="false">
<el-autocomplete <template #footer>
v-model="menuQuery" <el-autocomplete
:fetch-suggestions="menuSearch" v-model="state.menuQuery"
:placeholder="$t('message.user.searchPlaceholder')" :fetch-suggestions="menuSearch"
ref="layoutMenuAutocompleteRef" :placeholder="$t('message.user.searchPlaceholder')"
@select="onHandleSelect" ref="layoutMenuAutocompleteRef"
@blur="onSearchBlur" @select="onHandleSelect"
> :fit-input-width="true"
<template #prefix> >
<el-icon class="el-input__icon"> <template #prefix>
<ele-Search /> <el-icon class="el-input__icon">
</el-icon> <ele-Search />
</template> </el-icon>
<template #default="{ item }"> </template>
<div> <template #default="{ item }">
<SvgIcon :name="item.meta.icon" class="mr5" /> <div>
{{ $t(item.meta.title) }} <SvgIcon :name="item.meta.icon" class="mr5" />
</div> {{ $t(item.meta.title) }}
</template> </div>
</el-autocomplete> </template>
</el-autocomplete>
</template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutBreadcrumbSearch">
import { reactive, toRefs, defineComponent, ref, nextTick } from 'vue'; import { reactive, ref, nextTick } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
// 定义接口来定义对象的类型 // 定义变量内容
interface SearchState { const storesTagsViewRoutes = useTagsViewRoutes();
isShowSearch: boolean; const { tagsViewRoutes } = storeToRefs(storesTagsViewRoutes);
menuQuery: string; const layoutMenuAutocompleteRef = ref();
tagsViewList: object[]; const { t } = useI18n();
} const router = useRouter();
interface Restaurant { const state = reactive<SearchState>({
path: string; isShowSearch: false,
meta: { menuQuery: '',
title: string; tagsViewList: [],
}; });
}
export default defineComponent({ // 搜索弹窗打开
name: 'layoutBreadcrumbSearch', const openSearch = () => {
setup() { state.menuQuery = '';
const storesTagsViewRoutes = useTagsViewRoutes(); state.isShowSearch = true;
const { tagsViewRoutes } = storeToRefs(storesTagsViewRoutes); initTageView();
const layoutMenuAutocompleteRef = ref(); nextTick(() => {
const { t } = useI18n(); setTimeout(() => {
const router = useRouter(); layoutMenuAutocompleteRef.value.focus();
const state = reactive<SearchState>({
isShowSearch: false,
menuQuery: '',
tagsViewList: [],
}); });
// 搜索弹窗打开 });
const openSearch = () => { };
state.menuQuery = ''; // 搜索弹窗关闭
state.isShowSearch = true; const closeSearch = () => {
initTageView(); state.isShowSearch = false;
nextTick(() => { };
setTimeout(() => { // 菜单搜索数据过滤
layoutMenuAutocompleteRef.value.focus(); const menuSearch = (queryString: string, cb: Function) => {
}); let results = queryString ? state.tagsViewList.filter(createFilter(queryString)) : state.tagsViewList;
}); cb(results);
}; };
// 搜索弹窗关闭 // 菜单搜索过滤
const closeSearch = () => { const createFilter = (queryString: string) => {
state.isShowSearch = false; return (restaurant: RouteItem) => {
}; return (
// 菜单搜索数据过滤 restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
const menuSearch = (queryString: string, cb: Function) => { restaurant.meta!.title!.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
let results = queryString ? state.tagsViewList.filter(createFilter(queryString)) : state.tagsViewList; t(restaurant.meta!.title!).indexOf(queryString.toLowerCase()) > -1
cb(results); );
}; };
// 菜单搜索过滤 };
const createFilter: any = (queryString: string) => { // 初始化菜单数据
return (restaurant: Restaurant) => { const initTageView = () => {
return ( if (state.tagsViewList.length > 0) return false;
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || tagsViewRoutes.value.map((v: RouteItem) => {
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || if (!v.meta?.isHide) state.tagsViewList.push({ ...v });
t(restaurant.meta.title).indexOf(queryString.toLowerCase()) > -1 });
); };
}; // 当前菜单选中时
}; const onHandleSelect = (item: RouteItem) => {
// 初始化菜单数据 let { path, redirect } = item;
const initTageView = () => { if (item.meta?.isLink && !item.meta?.isIframe) window.open(item.meta?.isLink);
if (state.tagsViewList.length > 0) return false; else if (redirect) router.push(redirect);
tagsViewRoutes.value.map((v: any) => { else router.push(path);
if (!v.meta.isHide) state.tagsViewList.push({ ...v }); closeSearch();
}); };
};
// 当前菜单选中时 // 暴露变量
const onHandleSelect = (item: any) => { defineExpose({
let { path, redirect } = item; openSearch,
if (item.meta.isLink && !item.meta.isIframe) window.open(item.meta.isLink);
else if (redirect) router.push(redirect);
else router.push(path);
closeSearch();
};
// input 失去焦点时
const onSearchBlur = () => {
closeSearch();
};
return {
layoutMenuAutocompleteRef,
openSearch,
closeSearch,
menuSearch,
onHandleSelect,
onSearchBlur,
...toRefs(state),
};
},
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.layout-search-dialog { .layout-search-dialog {
position: relative;
:deep(.el-dialog) { :deep(.el-dialog) {
box-shadow: unset !important; .el-dialog__header,
border-radius: 0 !important; .el-dialog__body {
background: rgba(0, 0, 0, 0.5); display: none;
}
.el-dialog__footer {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -53vh;
}
} }
:deep(.el-autocomplete) { :deep(.el-autocomplete) {
width: 560px; width: 560px;
position: absolute; position: absolute;
top: 100px; top: 150px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }

View File

@@ -59,6 +59,17 @@
<el-color-picker v-model="getThemeConfig.menuBarColor" size="default" @change="onBgColorPickerChange('menuBarColor')"> </el-color-picker> <el-color-picker v-model="getThemeConfig.menuBarColor" size="default" @change="onBgColorPickerChange('menuBarColor')"> </el-color-picker>
</div> </div>
</div> </div>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoMenuBarActiveColor') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker
v-model="getThemeConfig.menuBarActiveColor"
size="default"
show-alpha
@change="onBgColorPickerChange('menuBarActiveColor')"
/>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt14"> <div class="layout-breadcrumb-seting-bar-flex mt14">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoIsMenuBarColorGradual') }}</div> <div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoIsMenuBarColorGradual') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value"> <div class="layout-breadcrumb-seting-bar-flex-value">
@@ -105,6 +116,17 @@
></el-switch> ></el-switch>
</div> </div>
</div> </div>
<div class="layout-breadcrumb-seting-bar-flex mt14" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoIsColumnsMenuHoverPreload') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch
v-model="getThemeConfig.isColumnsMenuHoverPreload"
size="small"
@change="onColumnsMenuHoverPreloadChange"
:disabled="getThemeConfig.layout !== 'columns'"
></el-switch>
</div>
</div>
<!-- 界面设置 --> <!-- 界面设置 -->
<el-divider content-position="left">{{ $t('message.layout.threeTitle') }}</el-divider> <el-divider content-position="left">{{ $t('message.layout.threeTitle') }}</el-divider>
@@ -216,12 +238,12 @@
<el-switch v-model="getThemeConfig.isCacheTagsView" size="small" @change="setLocalThemeConfig"></el-switch> <el-switch v-model="getThemeConfig.isCacheTagsView" size="small" @change="setLocalThemeConfig"></el-switch>
</div> </div>
</div> </div>
<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: isMobile ? 0.5 : 1 }"> <div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: state.isMobile ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsSortableTagsView') }}</div> <div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsSortableTagsView') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value"> <div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch <el-switch
v-model="getThemeConfig.isSortableTagsView" v-model="getThemeConfig.isSortableTagsView"
:disabled="isMobile ? true : false" :disabled="state.isMobile ? true : false"
size="small" size="small"
@change="onSortableTagsViewChange" @change="onSortableTagsViewChange"
></el-switch> ></el-switch>
@@ -260,7 +282,7 @@
<div class="layout-breadcrumb-seting-bar-flex mt14"> <div class="layout-breadcrumb-seting-bar-flex mt14">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourWartermarkText') }}</div> <div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourWartermarkText') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value"> <div class="layout-breadcrumb-seting-bar-flex-value">
<el-input v-model="getThemeConfig.wartermarkText" size="default" style="width: 90px" @input="onWartermarkTextInput($event)"></el-input> <el-input v-model="getThemeConfig.wartermarkText" size="default" style="width: 90px" @input="onWartermarkTextInput"></el-input>
</div> </div>
</div> </div>
@@ -407,271 +429,257 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutBreadcrumbSeting">
import { nextTick, onUnmounted, onMounted, getCurrentInstance, defineComponent, computed, reactive, toRefs } from 'vue'; import { nextTick, onUnmounted, onMounted, computed, reactive } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import { getLightColor, getDarkColor } from '/@/utils/theme'; import { useChangeColor } from '/@/utils/theme';
import { verifyAndSpace } from '/@/utils/toolsValidate'; import { verifyAndSpace } from '/@/utils/toolsValidate';
import { Local } from '/@/utils/storage'; import { Local } from '/@/utils/storage';
import Watermark from '/@/utils/wartermark'; import Watermark from '/@/utils/wartermark';
import commonFunction from '/@/utils/commonFunction'; import commonFunction from '/@/utils/commonFunction';
import other from '/@/utils/other'; import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
export default defineComponent({ // 定义变量内容
name: 'layoutBreadcrumbSeting', const { locale } = useI18n();
setup() { const storesThemeConfig = useThemeConfig();
const { proxy } = <any>getCurrentInstance(); const { themeConfig } = storeToRefs(storesThemeConfig);
const storesThemeConfig = useThemeConfig(); const { copyText } = commonFunction();
const { themeConfig } = storeToRefs(storesThemeConfig); const { getLightColor, getDarkColor } = useChangeColor();
const { copyText } = commonFunction(); const state = reactive({
const state = reactive({ isMobile: false,
isMobile: false, });
});
// 获取布局配置信息 // 获取布局配置信息
const getThemeConfig = computed(() => { const getThemeConfig = computed(() => {
return themeConfig.value; return themeConfig.value;
}); });
// 1、全局主题 // 1、全局主题
const onColorPickerChange = () => { const onColorPickerChange = () => {
if (!getThemeConfig.value.primary) return ElMessage.warning('全局主题 primary 颜色值不能为空'); if (!getThemeConfig.value.primary) return ElMessage.warning('全局主题 primary 颜色值不能为空');
// 颜色加深 // 颜色加深
document.documentElement.style.setProperty('--el-color-primary-dark-2', `${getDarkColor(getThemeConfig.value.primary, 0.1)}`); document.documentElement.style.setProperty('--el-color-primary-dark-2', `${getDarkColor(getThemeConfig.value.primary, 0.1)}`);
document.documentElement.style.setProperty('--el-color-primary', getThemeConfig.value.primary); document.documentElement.style.setProperty('--el-color-primary', getThemeConfig.value.primary);
// 颜色变浅 // 颜色变浅
for (let i = 1; i <= 9; i++) { for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(getThemeConfig.value.primary, i / 10)}`); document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(getThemeConfig.value.primary, i / 10)}`);
} }
setDispatchThemeConfig(); setDispatchThemeConfig();
}; };
// 2、菜单 / 顶栏 // 2、菜单 / 顶栏
const onBgColorPickerChange = (bg: string) => { const onBgColorPickerChange = (bg: string) => {
document.documentElement.style.setProperty(`--next-bg-${bg}`, (<any>getThemeConfig.value)[bg]); document.documentElement.style.setProperty(`--next-bg-${bg}`, themeConfig.value[bg]);
if (bg === 'menuBar') { if (bg === 'menuBar') {
document.documentElement.style.setProperty(`--next-bg-menuBar-light-1`, <any>getLightColor(getThemeConfig.value.menuBar, 0.05)); document.documentElement.style.setProperty(`--next-bg-menuBar-light-1`, getLightColor(getThemeConfig.value.menuBar, 0.05));
} }
onTopBarGradualChange(); onTopBarGradualChange();
onMenuBarGradualChange(); onMenuBarGradualChange();
onColumnsMenuBarGradualChange(); onColumnsMenuBarGradualChange();
setDispatchThemeConfig(); setDispatchThemeConfig();
}; };
// 2、菜单 / 顶栏 --> 顶栏背景渐变 // 2、菜单 / 顶栏 --> 顶栏背景渐变
const onTopBarGradualChange = () => { const onTopBarGradualChange = () => {
setGraduaFun('.layout-navbars-breadcrumb-index', getThemeConfig.value.isTopBarColorGradual, getThemeConfig.value.topBar); setGraduaFun('.layout-navbars-breadcrumb-index', getThemeConfig.value.isTopBarColorGradual, getThemeConfig.value.topBar);
}; };
// 2、菜单 / 顶栏 --> 菜单背景渐变 // 2、菜单 / 顶栏 --> 菜单背景渐变
const onMenuBarGradualChange = () => { const onMenuBarGradualChange = () => {
setGraduaFun('.layout-container .el-aside', getThemeConfig.value.isMenuBarColorGradual, getThemeConfig.value.menuBar); setGraduaFun('.layout-container .el-aside', getThemeConfig.value.isMenuBarColorGradual, getThemeConfig.value.menuBar);
}; };
// 2、菜单 / 顶栏 --> 分栏菜单背景渐变 // 2、菜单 / 顶栏 --> 分栏菜单背景渐变
const onColumnsMenuBarGradualChange = () => { const onColumnsMenuBarGradualChange = () => {
setGraduaFun('.layout-container .layout-columns-aside', getThemeConfig.value.isColumnsMenuBarColorGradual, getThemeConfig.value.columnsMenuBar); setGraduaFun('.layout-container .layout-columns-aside', getThemeConfig.value.isColumnsMenuBarColorGradual, getThemeConfig.value.columnsMenuBar);
}; };
// 2、菜单 / 顶栏 --> 背景渐变函数 // 2、菜单 / 顶栏 --> 背景渐变函数
const setGraduaFun = (el: string, bool: boolean, color: string) => { const setGraduaFun = (el: string, bool: boolean, color: string) => {
setTimeout(() => { setTimeout(() => {
let els = document.querySelector(el); let els = document.querySelector(el);
if (!els) return false; if (!els) return false;
document.documentElement.style.setProperty('--el-menu-bg-color', document.documentElement.style.getPropertyValue('--next-bg-menuBar')); document.documentElement.style.setProperty('--el-menu-bg-color', document.documentElement.style.getPropertyValue('--next-bg-menuBar'));
if (bool) els.setAttribute('style', `background:linear-gradient(to bottom left , ${color}, ${getLightColor(color, 0.6)}) !important;`); if (bool) els.setAttribute('style', `background:linear-gradient(to bottom left , ${color}, ${getLightColor(color, 0.6)}) !important;`);
else els.setAttribute('style', ``); else els.setAttribute('style', ``);
setLocalThemeConfig(); setLocalThemeConfig();
}, 200); }, 200);
}; };
// 3、界面设置 --> 菜单水平折叠 // 2、分栏设置 ->
const onThemeConfigChange = () => { const onColumnsMenuHoverPreloadChange = () => {
setDispatchThemeConfig(); setLocalThemeConfig();
}; };
// 3、界面设置 --> 固定 Header // 3、界面设置 --> 菜单水平折叠
const onIsFixedHeaderChange = () => { const onThemeConfigChange = () => {
getThemeConfig.value.isFixedHeaderChange = getThemeConfig.value.isFixedHeader ? false : true; setDispatchThemeConfig();
setLocalThemeConfig(); };
}; // 3、界面设置 --> 固定 Header
// 3、界面设置 --> 经典布局分割菜单 const onIsFixedHeaderChange = () => {
const onClassicSplitMenuChange = () => { getThemeConfig.value.isFixedHeaderChange = getThemeConfig.value.isFixedHeader ? false : true;
getThemeConfig.value.isBreadcrumb = false; setLocalThemeConfig();
setLocalThemeConfig(); };
proxy.mittBus.emit('getBreadcrumbIndexSetFilterRoutes'); // 3、界面设置 --> 经典布局分割菜单
}; const onClassicSplitMenuChange = () => {
// 4、界面显示 --> 侧边栏 Logo getThemeConfig.value.isBreadcrumb = false;
const onIsShowLogoChange = () => { setLocalThemeConfig();
getThemeConfig.value.isShowLogoChange = getThemeConfig.value.isShowLogo ? false : true; mittBus.emit('getBreadcrumbIndexSetFilterRoutes');
setLocalThemeConfig(); };
}; // 4、界面显示 --> 侧边栏 Logo
// 4、界面显示 --> 面包屑 Breadcrumb const onIsShowLogoChange = () => {
const onIsBreadcrumbChange = () => { getThemeConfig.value.isShowLogoChange = getThemeConfig.value.isShowLogo ? false : true;
if (getThemeConfig.value.layout === 'classic') { setLocalThemeConfig();
getThemeConfig.value.isClassicSplitMenu = false; };
} // 4、界面显示 --> 面包屑 Breadcrumb
setLocalThemeConfig(); const onIsBreadcrumbChange = () => {
}; if (getThemeConfig.value.layout === 'classic') {
// 4、界面显示 --> 开启 TagsView 拖拽 getThemeConfig.value.isClassicSplitMenu = false;
const onSortableTagsViewChange = () => { }
proxy.mittBus.emit('openOrCloseSortable'); setLocalThemeConfig();
setLocalThemeConfig(); };
}; // 4、界面显示 --> 开启 TagsView 拖拽
// 4、界面显示 --> 开启 TagsView 共用 const onSortableTagsViewChange = () => {
const onShareTagsViewChange = () => { mittBus.emit('openOrCloseSortable');
proxy.mittBus.emit('openShareTagsView'); setLocalThemeConfig();
setLocalThemeConfig(); };
}; // 4、界面显示 --> 开启 TagsView 共用
// 4、界面显示 --> 灰色模式/色弱模式 const onShareTagsViewChange = () => {
const onAddFilterChange = (attr: string) => { mittBus.emit('openShareTagsView');
if (attr === 'grayscale') { setLocalThemeConfig();
if (getThemeConfig.value.isGrayscale) getThemeConfig.value.isInvert = false; };
} else { // 4、界面显示 --> 灰色模式/色弱模式
if (getThemeConfig.value.isInvert) getThemeConfig.value.isGrayscale = false; const onAddFilterChange = (attr: string) => {
} if (attr === 'grayscale') {
const cssAttr = if (getThemeConfig.value.isGrayscale) getThemeConfig.value.isInvert = false;
attr === 'grayscale' ? `grayscale(${getThemeConfig.value.isGrayscale ? 1 : 0})` : `invert(${getThemeConfig.value.isInvert ? '80%' : '0%'})`; } else {
const appEle: any = document.body; if (getThemeConfig.value.isInvert) getThemeConfig.value.isGrayscale = false;
appEle.setAttribute('style', `filter: ${cssAttr}`); }
setLocalThemeConfig(); const cssAttr =
}; attr === 'grayscale' ? `grayscale(${getThemeConfig.value.isGrayscale ? 1 : 0})` : `invert(${getThemeConfig.value.isInvert ? '80%' : '0%'})`;
// 4、界面显示 --> 深色模式 const appEle = document.body;
const onAddDarkChange = () => { appEle.setAttribute('style', `filter: ${cssAttr}`);
const body = document.documentElement as HTMLElement; setLocalThemeConfig();
if (getThemeConfig.value.isIsDark) body.setAttribute('data-theme', 'dark'); };
else body.setAttribute('data-theme', ''); // 4、界面显示 --> 深色模式
}; const onAddDarkChange = () => {
// 4、界面显示 --> 开启水印 const body = document.documentElement as HTMLElement;
const onWartermarkChange = () => { if (getThemeConfig.value.isIsDark) body.setAttribute('data-theme', 'dark');
getThemeConfig.value.isWartermark ? Watermark.set(getThemeConfig.value.wartermarkText) : Watermark.del(); else body.setAttribute('data-theme', '');
setLocalThemeConfig(); };
}; // 4、界面显示 --> 开启水印
// 4、界面显示 --> 水印文案 const onWartermarkChange = () => {
const onWartermarkTextInput = (val: any) => { getThemeConfig.value.isWartermark ? Watermark.set(getThemeConfig.value.wartermarkText) : Watermark.del();
getThemeConfig.value.wartermarkText = verifyAndSpace(val); setLocalThemeConfig();
if (getThemeConfig.value.wartermarkText === '') return false; };
if (getThemeConfig.value.isWartermark) Watermark.set(getThemeConfig.value.wartermarkText); // 4、界面显示 --> 水印文案
setLocalThemeConfig(); const onWartermarkTextInput = (val: string) => {
}; getThemeConfig.value.wartermarkText = verifyAndSpace(val);
// 5、布局切换 if (getThemeConfig.value.wartermarkText === '') return false;
const onSetLayout = (layout: string) => { if (getThemeConfig.value.isWartermark) Watermark.set(getThemeConfig.value.wartermarkText);
Local.set('oldLayout', layout); setLocalThemeConfig();
if (getThemeConfig.value.layout === layout) return false; };
if (layout === 'transverse') getThemeConfig.value.isCollapse = false; // 5、布局切换
getThemeConfig.value.layout = layout; const onSetLayout = (layout: string) => {
Local.set('oldLayout', layout);
if (getThemeConfig.value.layout === layout) return false;
if (layout === 'transverse') getThemeConfig.value.isCollapse = false;
getThemeConfig.value.layout = layout;
getThemeConfig.value.isDrawer = false;
initLayoutChangeFun();
};
// 设置布局切换函数
const initLayoutChangeFun = () => {
onBgColorPickerChange('menuBar');
onBgColorPickerChange('menuBarColor');
onBgColorPickerChange('menuBarActiveColor');
onBgColorPickerChange('topBar');
onBgColorPickerChange('topBarColor');
onBgColorPickerChange('columnsMenuBar');
onBgColorPickerChange('columnsMenuBarColor');
};
// 关闭弹窗时,初始化变量。变量用于处理 layoutScrollbarRef.value.update() 更新滚动条高度
const onDrawerClose = () => {
getThemeConfig.value.isFixedHeaderChange = false;
getThemeConfig.value.isShowLogoChange = false;
getThemeConfig.value.isDrawer = false;
setLocalThemeConfig();
};
// 布局配置弹窗打开
const openDrawer = () => {
getThemeConfig.value.isDrawer = true;
};
// 触发 store 布局配置更新
const setDispatchThemeConfig = () => {
setLocalThemeConfig();
setLocalThemeConfigStyle();
};
// 存储布局配置
const setLocalThemeConfig = () => {
Local.remove('themeConfig');
Local.set('themeConfig', getThemeConfig.value);
};
// 存储布局配置全局主题样式html根标签
const setLocalThemeConfigStyle = () => {
Local.set('themeConfigStyle', document.documentElement.style.cssText);
};
// 一键复制配置
const onCopyConfigClick = () => {
let copyThemeConfig = Local.get('themeConfig');
copyThemeConfig.isDrawer = false;
copyText(JSON.stringify(copyThemeConfig)).then(() => {
getThemeConfig.value.isDrawer = false;
});
};
// 一键恢复默认
const onResetConfigClick = () => {
Local.clear();
window.location.reload();
// @ts-ignore
Local.set('version', __VERSION__);
};
// 初始化菜单样式等
const initSetStyle = () => {
// 2、菜单 / 顶栏 --> 顶栏背景渐变
onTopBarGradualChange();
// 2、菜单 / 顶栏 --> 菜单背景渐变
onMenuBarGradualChange();
// 2、菜单 / 顶栏 --> 分栏菜单背景渐变
onColumnsMenuBarGradualChange();
};
onMounted(() => {
nextTick(() => {
// 判断当前布局是否不相同不相同则初始化当前布局的样式防止监听窗口大小改变时布局配置logo、菜单背景等部分布局失效问题
if (!Local.get('frequency')) initLayoutChangeFun();
Local.set('frequency', 1);
// 监听窗口大小改变,非默认布局,设置成默认布局(适配移动端)
mittBus.on('layoutMobileResize', (res: LayoutMobileResize) => {
getThemeConfig.value.layout = res.layout;
getThemeConfig.value.isDrawer = false; getThemeConfig.value.isDrawer = false;
initLayoutChangeFun(); initLayoutChangeFun();
}; state.isMobile = other.isMobile();
// 设置布局切换函数
const initLayoutChangeFun = () => {
onBgColorPickerChange('menuBar');
onBgColorPickerChange('menuBarColor');
onBgColorPickerChange('topBar');
onBgColorPickerChange('topBarColor');
onBgColorPickerChange('columnsMenuBar');
onBgColorPickerChange('columnsMenuBarColor');
};
// 关闭弹窗时,初始化变量。变量用于处理 proxy.$refs.layoutScrollbarRef.update()
const onDrawerClose = () => {
getThemeConfig.value.isFixedHeaderChange = false;
getThemeConfig.value.isShowLogoChange = false;
getThemeConfig.value.isDrawer = false;
setLocalThemeConfig();
};
// 布局配置弹窗打开
const openDrawer = () => {
getThemeConfig.value.isDrawer = true;
};
// 触发 store 布局配置更新
const setDispatchThemeConfig = () => {
setLocalThemeConfig();
setLocalThemeConfigStyle();
};
// 存储布局配置
const setLocalThemeConfig = () => {
Local.remove('themeConfig');
Local.set('themeConfig', getThemeConfig.value);
};
// 存储布局配置全局主题样式html根标签
const setLocalThemeConfigStyle = () => {
Local.set('themeConfigStyle', document.documentElement.style.cssText);
};
// 一键复制配置
const onCopyConfigClick = () => {
let copyThemeConfig = Local.get('themeConfig');
copyThemeConfig.isDrawer = false;
copyText(JSON.stringify(copyThemeConfig)).then(() => {
getThemeConfig.value.isDrawer = false;
});
};
// 一键恢复默认
const onResetConfigClick = () => {
Local.clear();
window.location.reload();
};
// 初始化菜单样式等
const initSetStyle = () => {
// 2、菜单 / 顶栏 --> 顶栏背景渐变
onTopBarGradualChange();
// 2、菜单 / 顶栏 --> 菜单背景渐变
onMenuBarGradualChange();
// 2、菜单 / 顶栏 --> 分栏菜单背景渐变
onColumnsMenuBarGradualChange();
};
onMounted(() => {
nextTick(() => {
// 判断当前布局是否不相同不相同则初始化当前布局的样式防止监听窗口大小改变时布局配置logo、菜单背景等部分布局失效问题
if (!Local.get('frequency')) initLayoutChangeFun();
Local.set('frequency', 1);
// 监听窗口大小改变,非默认布局,设置成默认布局(适配移动端)
proxy.mittBus.on('layoutMobileResize', (res: any) => {
getThemeConfig.value.layout = res.layout;
getThemeConfig.value.isDrawer = false;
initLayoutChangeFun();
state.isMobile = other.isMobile();
});
setTimeout(() => {
// 默认样式
onColorPickerChange();
// 灰色模式
if (getThemeConfig.value.isGrayscale) onAddFilterChange('grayscale');
// 色弱模式
if (getThemeConfig.value.isInvert) onAddFilterChange('invert');
// 深色模式
if (getThemeConfig.value.isIsDark) onAddDarkChange();
// 开启水印
onWartermarkChange();
// 语言国际化
if (Local.get('themeConfig')) proxy.$i18n.locale = Local.get('themeConfig').globalI18n;
// 初始化菜单样式等
initSetStyle();
}, 100);
});
}); });
onUnmounted(() => { setTimeout(() => {
proxy.mittBus.off('layoutMobileResize', () => {}); // 默认样式
}); onColorPickerChange();
return { // 灰色模式
openDrawer, if (getThemeConfig.value.isGrayscale) onAddFilterChange('grayscale');
onColorPickerChange, // 色弱模式
onBgColorPickerChange, if (getThemeConfig.value.isInvert) onAddFilterChange('invert');
onTopBarGradualChange, // 深色模式
onMenuBarGradualChange, if (getThemeConfig.value.isIsDark) onAddDarkChange();
onColumnsMenuBarGradualChange, // 开启水印
onThemeConfigChange, onWartermarkChange();
onIsFixedHeaderChange, // 语言国际化
onIsShowLogoChange, if (Local.get('themeConfig')) locale.value = Local.get('themeConfig').globalI18n;
getThemeConfig, // 初始化菜单样式等
onDrawerClose, initSetStyle();
onAddFilterChange, }, 100);
onAddDarkChange, });
onWartermarkChange, });
onWartermarkTextInput, onUnmounted(() => {
onSetLayout, mittBus.off('layoutMobileResize', () => {});
setLocalThemeConfig, });
onClassicSplitMenuChange,
onIsBreadcrumbChange, // 暴露变量
onSortableTagsViewChange, defineExpose({
onShareTagsViewChange, openDrawer,
onCopyConfigClick,
onResetConfigClick,
...toRefs(state),
};
},
}); });
</script> </script>

View File

@@ -6,21 +6,25 @@
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="large" :disabled="disabledSize === 'large'">{{ $t('message.user.dropdownLarge') }}</el-dropdown-item> <el-dropdown-item command="large" :disabled="state.disabledSize === 'large'">{{ $t('message.user.dropdownLarge') }}</el-dropdown-item>
<el-dropdown-item command="default" :disabled="disabledSize === 'default'">{{ $t('message.user.dropdownDefault') }}</el-dropdown-item> <el-dropdown-item command="default" :disabled="state.disabledSize === 'default'">{{ $t('message.user.dropdownDefault') }}</el-dropdown-item>
<el-dropdown-item command="small" :disabled="disabledSize === 'small'">{{ $t('message.user.dropdownSmall') }}</el-dropdown-item> <el-dropdown-item command="small" :disabled="state.disabledSize === 'small'">{{ $t('message.user.dropdownSmall') }}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onLanguageChange"> <el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onLanguageChange">
<div class="layout-navbars-breadcrumb-user-icon"> <div class="layout-navbars-breadcrumb-user-icon">
<i class="iconfont" :class="disabledI18n === 'en' ? 'icon-fuhao-yingwen' : 'icon-fuhao-zhongwen'" :title="$t('message.user.title1')"></i> <i
class="iconfont"
:class="state.disabledI18n === 'en' ? 'icon-fuhao-yingwen' : 'icon-fuhao-zhongwen'"
:title="$t('message.user.title1')"
></i>
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="zh-cn" :disabled="disabledI18n === 'zh-cn'">简体中文</el-dropdown-item> <el-dropdown-item command="zh-cn" :disabled="state.disabledI18n === 'zh-cn'">简体中文</el-dropdown-item>
<el-dropdown-item command="en" :disabled="disabledI18n === 'en'">English</el-dropdown-item> <el-dropdown-item command="en" :disabled="state.disabledI18n === 'en'">English</el-dropdown-item>
<el-dropdown-item command="zh-tw" :disabled="disabledI18n === 'zh-tw'">繁體中文</el-dropdown-item> <el-dropdown-item command="zh-tw" :disabled="state.disabledI18n === 'zh-tw'">繁體中文</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@@ -30,12 +34,12 @@
</el-icon> </el-icon>
</div> </div>
<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick"> <div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
<i class="icon-xitongshezhi iconfont" :title="$t('message.user.title3')"></i> <i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
</div> </div>
<div class="layout-navbars-breadcrumb-user-icon"> <div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false"> <el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference> <template #reference>
<el-badge :is-dot="true"> <el-badge :value="messageCenter.unread" :hidden="messageCenter.unread===0">
<el-icon :title="$t('message.user.title4')"> <el-icon :title="$t('message.user.title4')">
<ele-Bell /> <ele-Bell />
</el-icon> </el-icon>
@@ -49,8 +53,8 @@
<div class="layout-navbars-breadcrumb-user-icon mr10" @click="onScreenfullClick"> <div class="layout-navbars-breadcrumb-user-icon mr10" @click="onScreenfullClick">
<i <i
class="iconfont" class="iconfont"
:title="isScreenfull ? $t('message.user.title6') : $t('message.user.title5')" :title="state.isScreenfull ? $t('message.user.title6') : $t('message.user.title5')"
:class="!isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'" :class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"
></i> ></i>
</div> </div>
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick"> <el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
@@ -64,10 +68,8 @@
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item> <el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item>
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item> <el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item>
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item command="/404">{{ $t('message.user.dropdown3') }}</el-dropdown-item>
<el-dropdown-item command="/401">{{ $t('message.user.dropdown4') }}</el-dropdown-item>
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item> <el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@@ -76,8 +78,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts" name="layoutBreadcrumbUser">
import { ref, getCurrentInstance, computed, reactive, toRefs, onMounted, defineComponent } from 'vue'; import { defineAsyncComponent, ref, computed, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus'; import { ElMessageBox, ElMessage } from 'element-plus';
import screenfull from 'screenfull'; import screenfull from 'screenfull';
@@ -86,170 +88,128 @@ import { storeToRefs } from 'pinia';
import { useUserInfo } from '/@/stores/userInfo'; import { useUserInfo } from '/@/stores/userInfo';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other'; import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
import { Session, Local } from '/@/utils/storage'; import { Session, Local } from '/@/utils/storage';
import UserNews from '/@/layout/navBars/breadcrumb/userNews.vue';
import Search from '/@/layout/navBars/breadcrumb/search.vue';
export default defineComponent({ // 引入组件
name: 'layoutBreadcrumbUser', const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
components: { UserNews, Search }, const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
setup() {
const { t } = useI18n(); // 定义变量内容
const { proxy } = <any>getCurrentInstance(); const { locale, t } = useI18n();
const router = useRouter(); const router = useRouter();
const stores = useUserInfo(); const stores = useUserInfo();
const storesThemeConfig = useThemeConfig(); const storesThemeConfig = useThemeConfig();
const { userInfos } = storeToRefs(stores); const { userInfos } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig); const { themeConfig } = storeToRefs(storesThemeConfig);
const searchRef = ref(); const searchRef = ref();
const state = reactive({ const state = reactive({
isScreenfull: false, isScreenfull: false,
disabledI18n: 'zh-cn', disabledI18n: 'zh-cn',
disabledSize: 'large', disabledSize: 'large',
});
// 设置分割样式
const layoutUserFlexNum = computed(() => {
let num: string | number = '';
const { layout, isClassicSplitMenu } = themeConfig.value;
const layoutArr: string[] = ['defaults', 'columns'];
if (layoutArr.includes(layout) || (layout === 'classic' && !isClassicSplitMenu)) num = '1';
else num = '';
return num;
});
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {
ElMessage.warning('暂不不支持全屏');
return false;
}
screenfull.toggle();
screenfull.on('change', () => {
if (screenfull.isFullscreen) state.isScreenfull = true;
else state.isScreenfull = false;
});
};
// 布局配置 icon 点击时
const onLayoutSetingClick = () => {
proxy.mittBus.emit('openSetingsDrawer');
};
// 下拉菜单点击时
const onHandleCommandClick = (path: string) => {
if (path === 'logOut') {
ElMessageBox({
closeOnClickModal: false,
closeOnPressEscape: false,
title: t('message.user.logOutTitle'),
message: t('message.user.logOutMessage'),
showCancelButton: true,
confirmButtonText: t('message.user.logOutConfirm'),
cancelButtonText: t('message.user.logOutCancel'),
buttonSize: 'default',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
instance.confirmButtonText = t('message.user.logOutExit');
setTimeout(() => {
done();
setTimeout(() => {
instance.confirmButtonLoading = false;
}, 300);
}, 700);
} else {
done();
}
},
})
.then(async () => {
// 清除缓存/token等
Session.clear();
// 使用 reload 时,不需要调用 resetRoute() 重置路由
window.location.reload();
})
.catch(() => {});
} else if (path === 'wareHouse') {
window.open('https://gitee.com/lyt-top/vue-next-admin');
} else {
router.push(path);
}
};
// 菜单搜索点击
const onSearchClick = () => {
searchRef.value.openSearch();
};
// 组件大小改变
const onComponentSizeChange = (size: string) => {
Local.remove('themeConfig');
themeConfig.value.globalComponentSize = size;
Local.set('themeConfig', themeConfig.value);
initComponentSize();
window.location.reload();
};
// 语言切换
const onLanguageChange = (lang: string) => {
Local.remove('themeConfig');
themeConfig.value.globalI18n = lang;
Local.set('themeConfig', themeConfig.value);
proxy.$i18n.locale = lang;
initI18n();
other.useTitle();
};
// 设置 element plus 组件的国际化
const setI18nConfig = (locale: string) => {
proxy.$i18n={messages:{}}
proxy.mittBus.emit('getI18nConfig', proxy.$i18n.messages[locale]);
};
// 初始化言语国际化
const initI18n = () => {
switch (Local.get('themeConfig').globalI18n) {
case 'zh-cn':
state.disabledI18n = 'zh-cn';
setI18nConfig('zh-cn');
break;
case 'en':
state.disabledI18n = 'en';
setI18nConfig('en');
break;
case 'zh-tw':
state.disabledI18n = 'zh-tw';
setI18nConfig('zh-tw');
break;
}
};
// 初始化全局组件大小
const initComponentSize = () => {
switch (Local.get('themeConfig').globalComponentSize) {
case 'large':
state.disabledSize = 'large';
break;
case 'default':
state.disabledSize = 'default';
break;
case 'small':
state.disabledSize = 'small';
break;
}
};
// 页面加载时
onMounted(() => {
if (Local.get('themeConfig')) {
initI18n();
initComponentSize();
}
});
return {
userInfos,
onLayoutSetingClick,
onHandleCommandClick,
onScreenfullClick,
onSearchClick,
onComponentSizeChange,
onLanguageChange,
searchRef,
layoutUserFlexNum,
...toRefs(state),
};
},
}); });
// 设置分割样式
const layoutUserFlexNum = computed(() => {
let num: string | number = '';
const { layout, isClassicSplitMenu } = themeConfig.value;
const layoutArr: string[] = ['defaults', 'columns'];
if (layoutArr.includes(layout) || (layout === 'classic' && !isClassicSplitMenu)) num = '1';
else num = '';
return num;
});
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {
ElMessage.warning('暂不不支持全屏');
return false;
}
screenfull.toggle();
screenfull.on('change', () => {
if (screenfull.isFullscreen) state.isScreenfull = true;
else state.isScreenfull = false;
});
};
// 布局配置 icon 点击时
const onLayoutSetingClick = () => {
mittBus.emit('openSetingsDrawer');
};
// 下拉菜单点击时
const onHandleCommandClick = (path: string) => {
if (path === 'logOut') {
ElMessageBox({
closeOnClickModal: false,
closeOnPressEscape: false,
title: t('message.user.logOutTitle'),
message: t('message.user.logOutMessage'),
showCancelButton: true,
confirmButtonText: t('message.user.logOutConfirm'),
cancelButtonText: t('message.user.logOutCancel'),
buttonSize: 'default',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
instance.confirmButtonText = t('message.user.logOutExit');
setTimeout(() => {
done();
setTimeout(() => {
instance.confirmButtonLoading = false;
}, 300);
}, 700);
} else {
done();
}
},
})
.then(async () => {
// 清除缓存/token等
Session.clear();
// 使用 reload 时,不需要调用 resetRoute() 重置路由
window.location.reload();
})
.catch(() => {});
} else if (path === 'wareHouse') {
window.open('https://gitee.com/huge-dream/django-vue3-admin');
} else {
router.push(path);
}
};
// 菜单搜索点击
const onSearchClick = () => {
searchRef.value.openSearch();
};
// 组件大小改变
const onComponentSizeChange = (size: string) => {
Local.remove('themeConfig');
themeConfig.value.globalComponentSize = size;
Local.set('themeConfig', themeConfig.value);
initI18nOrSize('globalComponentSize', 'disabledSize');
window.location.reload();
};
// 语言切换
const onLanguageChange = (lang: string) => {
Local.remove('themeConfig');
themeConfig.value.globalI18n = lang;
Local.set('themeConfig', themeConfig.value);
locale.value = lang;
other.useTitle();
initI18nOrSize('globalI18n', 'disabledI18n');
};
// 初始化组件大小/i18n
const initI18nOrSize = (value: string, attr: string) => {
state[attr] = Local.get('themeConfig')[value];
};
// 页面加载时
onMounted(() => {
if (Local.get('themeConfig')) {
initI18nOrSize('globalComponentSize', 'disabledSize');
initI18nOrSize('globalI18n', 'disabledI18n');
}
});
//消息中心的未读数量
import {messageCenterStore} from "/@/stores/messageCenter";
const messageCenter = messageCenterStore()
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

Some files were not shown because too many files have changed in this diff Show More