Compare commits
184 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3271f00f87 | ||
|
|
1fe0a89338 | ||
|
|
58971a3781 | ||
|
|
502e1f4d27 | ||
|
|
2015db53ab | ||
|
|
73edafb95f | ||
|
|
e8f5edd9c3 | ||
|
|
2d69633660 | ||
|
|
b5c583ba7d | ||
|
|
21be91a894 | ||
|
|
a527d367d9 | ||
|
|
eeda1bea4a | ||
|
|
2a0de4f0d3 | ||
|
|
792a22e606 | ||
|
|
6726d0167e | ||
|
|
dddafa4826 | ||
|
|
db27235f61 | ||
|
|
282ab9a6a1 | ||
|
|
15c87ddd26 | ||
|
|
a36dcfa1e5 | ||
|
|
ba5c2ab490 | ||
|
|
584bf57344 | ||
|
|
0c38343aca | ||
|
|
d8f41919ea | ||
|
|
81d2fac8b6 | ||
|
|
69d36cf858 | ||
|
|
2701cf9352 | ||
|
|
84b5426932 | ||
|
|
36a8471dd0 | ||
|
|
1d02f3e138 | ||
|
|
da0f084b0c | ||
|
|
1993fdd509 | ||
|
|
9fb5e95a55 | ||
|
|
706986e187 | ||
|
|
e3291fe22b | ||
|
|
ec137c9f84 | ||
|
|
1158bbb790 | ||
|
|
e880af6f1e | ||
|
|
547cc30818 | ||
|
|
d0f562b6ed | ||
|
|
ad3a190e96 | ||
|
|
9cc071edcd | ||
|
|
4177f84a62 | ||
|
|
c1db1c21d0 | ||
|
|
422f86da22 | ||
|
|
dac7ea90ae | ||
|
|
8ff773af3b | ||
|
|
b6557de0ca | ||
|
|
4a68bf2f2b | ||
|
|
9c370169d3 | ||
|
|
f62f0b440d | ||
|
|
4a8f907c7f | ||
|
|
1301346772 | ||
|
|
012c5a9c9c | ||
|
|
683b5164aa | ||
|
|
3eeea14b97 | ||
|
|
7a9ef47a68 | ||
|
|
495976726e | ||
|
|
50bcc4346f | ||
|
|
2f4a6e6b1f | ||
|
|
836b645507 | ||
|
|
ed3e3f12e0 | ||
|
|
7280cbbb68 | ||
|
|
848e4a62af | ||
|
|
98413bf125 | ||
|
|
21d61794bc | ||
|
|
72e046fd6d | ||
|
|
4f85de3247 | ||
|
|
6ad048b86a | ||
|
|
b9976cc2dd | ||
|
|
7a453da303 | ||
|
|
421e89823a | ||
|
|
436a722ce8 | ||
|
|
3ea38a59b7 | ||
|
|
665d675deb | ||
|
|
46a1854e24 | ||
|
|
a3c4e02aa1 | ||
|
|
199a75293d | ||
|
|
3f58c1cb7a | ||
|
|
2c8b51f463 | ||
|
|
7a752b1803 | ||
|
|
69d23c6f69 | ||
|
|
652ad1355a | ||
|
|
c0bd8c42a7 | ||
|
|
3657878d25 | ||
|
|
0e5d343f9f | ||
|
|
95f7046e49 | ||
|
|
233774a981 | ||
|
|
47b0ee7fe7 | ||
|
|
b9c6ab6fcd | ||
|
|
a042a65555 | ||
|
|
4ca0edc104 | ||
|
|
9c7e8097db | ||
|
|
3b2fd573bc | ||
|
|
83a6e41015 | ||
|
|
f12dcb3f1f | ||
|
|
c11cf378cb | ||
|
|
0764383989 | ||
|
|
b6e05c997d | ||
|
|
2576de48ec | ||
|
|
787b7b61c9 | ||
|
|
5cf2eef7ad | ||
|
|
1981567c59 | ||
|
|
3979628281 | ||
|
|
9a8506448f | ||
|
|
e7b63a61ba | ||
|
|
e106324c70 | ||
|
|
a42ccd0e18 | ||
|
|
5fc1390598 | ||
|
|
1942f1af4e | ||
|
|
c8e235bed6 | ||
|
|
cf93402763 | ||
|
|
3dcef90bbe | ||
|
|
6f8bae8d5c | ||
|
|
888a6f1c63 | ||
|
|
6d587fc1e2 | ||
|
|
630ec1e774 | ||
|
|
8a17d6f82b | ||
|
|
326149195f | ||
|
|
71ca7370e2 | ||
|
|
0dcf8ae794 | ||
|
|
9c5fe4f483 | ||
|
|
c9ff9d0716 | ||
|
|
27c9eff716 | ||
|
|
de603df07c | ||
|
|
6793a09b8b | ||
|
|
f60f84964a | ||
|
|
bba4472009 | ||
|
|
438480b2f1 | ||
|
|
3129c33adc | ||
|
|
ac0aaefe0f | ||
|
|
ef43dc4900 | ||
|
|
03f467abfa | ||
|
|
68d31dc515 | ||
|
|
9215cfd105 | ||
|
|
9c765c67e1 | ||
|
|
2fd2c76bdb | ||
|
|
798f9e8a7f | ||
|
|
1879d0d2fd | ||
|
|
e1d9f555c8 | ||
|
|
d3c2bbbb5b | ||
|
|
453d1e3875 | ||
|
|
d03a40d04f | ||
|
|
f2f0e06cd6 | ||
|
|
ae74105b74 | ||
|
|
314d7d79c2 | ||
|
|
c2b0c3f25b | ||
|
|
4a26e1476a | ||
|
|
735f17c2d8 | ||
|
|
47bccac7f9 | ||
|
|
f7f35151ac | ||
|
|
b49b1f0cc6 | ||
|
|
11ec6fb150 | ||
|
|
591360b879 | ||
|
|
8c7e8aee9f | ||
|
|
8554bf18f4 | ||
|
|
0779cc1a84 | ||
|
|
1b8a502d66 | ||
|
|
087d478094 | ||
|
|
82d0b19bc2 | ||
|
|
5cb7ec500c | ||
|
|
7a21f44eab | ||
|
|
9383508a85 | ||
|
|
b6a4be25f2 | ||
|
|
7234d2b3e9 | ||
|
|
14640be036 | ||
|
|
af60e9a0fa | ||
|
|
259c51b23c | ||
|
|
5b60f60b70 | ||
|
|
38ad2db7a7 | ||
|
|
0529c2747a | ||
|
|
fe5e44913d | ||
|
|
d7edbde434 | ||
|
|
b6c013dad7 | ||
|
|
452bc0a63a | ||
|
|
275d380fe0 | ||
|
|
354d230c2a | ||
|
|
37a0167193 | ||
|
|
6f4f5a771e | ||
|
|
297c4df74f | ||
|
|
b1b49aa0db | ||
|
|
b1f9faca0f | ||
|
|
05f659bc87 | ||
|
|
218036c0ea |
@@ -114,7 +114,7 @@ cd web
|
||||
|
||||
# 安装依赖
|
||||
npm install yarn
|
||||
yarn install --registry=https://registry.npm.taobao.org
|
||||
yarn install --registry=https://registry.npmmirror.com
|
||||
|
||||
# 启动服务
|
||||
yarn build
|
||||
|
||||
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -98,4 +98,5 @@ media/
|
||||
__pypackages__/
|
||||
package-lock.json
|
||||
gunicorn.pid
|
||||
plugins/*
|
||||
!plugins/__init__.py
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
||||
@@ -15,7 +15,7 @@ else:
|
||||
from celery import Celery
|
||||
|
||||
app = Celery(f"application")
|
||||
app.config_from_object('django.conf:settings')
|
||||
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
|
||||
platforms.C_FORCE_ROOT = True
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.core.cache import cache
|
||||
from dvadmin.utils.validator import CustomValidationError
|
||||
|
||||
dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory') # redis
|
||||
|
||||
|
||||
def is_tenants_mode():
|
||||
@@ -68,6 +72,9 @@ def init_dictionary():
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
if dispatch_db_type == 'redis':
|
||||
cache.set(f"init_dictionary", _get_all_dictionary())
|
||||
return
|
||||
if is_tenants_mode():
|
||||
from django_tenants.utils import tenant_context, get_tenant_model
|
||||
|
||||
@@ -88,7 +95,9 @@ def init_system_config():
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
|
||||
if dispatch_db_type == 'redis':
|
||||
cache.set(f"init_system_config", _get_all_system_config())
|
||||
return
|
||||
if is_tenants_mode():
|
||||
from django_tenants.utils import tenant_context, get_tenant_model
|
||||
|
||||
@@ -107,6 +116,9 @@ def refresh_dictionary():
|
||||
刷新字典配置
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
cache.set(f"init_dictionary", _get_all_dictionary())
|
||||
return
|
||||
if is_tenants_mode():
|
||||
from django_tenants.utils import tenant_context, get_tenant_model
|
||||
|
||||
@@ -122,6 +134,9 @@ def refresh_system_config():
|
||||
刷新系统配置
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
cache.set(f"init_system_config", _get_all_system_config())
|
||||
return
|
||||
if is_tenants_mode():
|
||||
from django_tenants.utils import tenant_context, get_tenant_model
|
||||
|
||||
@@ -141,6 +156,11 @@ def get_dictionary_config(schema_name=None):
|
||||
:param schema_name: 对应字典配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
init_dictionary_data = cache.get(f"init_dictionary")
|
||||
if not init_dictionary_data:
|
||||
refresh_dictionary()
|
||||
return cache.get(f"init_dictionary") or {}
|
||||
if not settings.DICTIONARY_CONFIG:
|
||||
refresh_dictionary()
|
||||
if is_tenants_mode():
|
||||
@@ -157,6 +177,12 @@ def get_dictionary_values(key, schema_name=None):
|
||||
:param schema_name: 对应字典配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
dictionary_config = cache.get(f"init_dictionary")
|
||||
if not dictionary_config:
|
||||
refresh_dictionary()
|
||||
dictionary_config = cache.get(f"init_dictionary")
|
||||
return dictionary_config.get(key)
|
||||
dictionary_config = get_dictionary_config(schema_name)
|
||||
return dictionary_config.get(key)
|
||||
|
||||
@@ -169,8 +195,8 @@ def get_dictionary_label(key, name, schema_name=None):
|
||||
:param schema_name: 对应字典配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
children = get_dictionary_values(key, schema_name) or []
|
||||
for ele in children:
|
||||
res = get_dictionary_values(key, schema_name) or []
|
||||
for ele in res.get('children'):
|
||||
if ele.get("value") == str(name):
|
||||
return ele.get("label")
|
||||
return ""
|
||||
@@ -187,6 +213,11 @@ def get_system_config(schema_name=None):
|
||||
:param schema_name: 对应字典配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
init_dictionary_data = cache.get(f"init_system_config")
|
||||
if not init_dictionary_data:
|
||||
refresh_system_config()
|
||||
return cache.get(f"init_system_config") or {}
|
||||
if not settings.SYSTEM_CONFIG:
|
||||
refresh_system_config()
|
||||
if is_tenants_mode():
|
||||
@@ -203,10 +234,32 @@ def get_system_config_values(key, schema_name=None):
|
||||
:param schema_name: 对应系统配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
if dispatch_db_type == 'redis':
|
||||
system_config = cache.get(f"init_system_config")
|
||||
if not system_config:
|
||||
refresh_system_config()
|
||||
system_config = cache.get(f"init_system_config")
|
||||
return system_config.get(key)
|
||||
system_config = get_system_config(schema_name)
|
||||
return system_config.get(key)
|
||||
|
||||
|
||||
def get_system_config_values_to_dict(key, schema_name=None):
|
||||
"""
|
||||
获取系统配置数据并转换为字典 **仅限于数组类型系统配置
|
||||
:param key: 对应系统配置的key值(字典编号)
|
||||
:param schema_name: 对应系统配置的租户schema_name值
|
||||
:return:
|
||||
"""
|
||||
values_dict = {}
|
||||
config_values = get_system_config_values(key, schema_name)
|
||||
if not isinstance(config_values, list):
|
||||
raise CustomValidationError("该方式仅限于数组类型系统配置")
|
||||
for ele in get_system_config_values(key, schema_name):
|
||||
values_dict[ele.get('key')] = ele.get('value')
|
||||
return values_dict
|
||||
|
||||
|
||||
def get_system_config_label(key, name, schema_name=None):
|
||||
"""
|
||||
获取获取系统配置label值
|
||||
|
||||
@@ -404,7 +404,7 @@ PLUGINS_URL_PATTERNS = []
|
||||
# ********** 一键导入插件配置开始 **********
|
||||
# 例如:
|
||||
# from dvadmin_upgrade_center.settings import * # 升级中心
|
||||
# from dvadmin_celery.settings import * # celery 异步任务
|
||||
# from dvadmin3_celery.settings import * # celery 异步任务
|
||||
# from dvadmin_third.settings import * # 第三方用户管理
|
||||
# from dvadmin_ak_sk.settings import * # 秘钥管理管理
|
||||
# from dvadmin_tenants.settings import * # 租户管理
|
||||
|
||||
1
backend/dvadmin/__init__.py
Normal file
1
backend/dvadmin/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -167,19 +167,13 @@
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "查询所有",
|
||||
"name": "获取所有部门",
|
||||
"value": "dept:SearchAll",
|
||||
"api": "/api/system/dept/all_dept/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "懒加载查询所有",
|
||||
"value": "dept:LazySearchAll",
|
||||
"api": "/api/system/dept/dept_lazy_tree/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "头信息",
|
||||
"name": "部门顶部信息",
|
||||
"value": "dept:HeaderInfo",
|
||||
"api": "/api/system/dept/dept_info/",
|
||||
"method": 0
|
||||
@@ -678,6 +672,53 @@
|
||||
"model": "ApiWhiteList"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "下载中心",
|
||||
"icon": "ele-Download",
|
||||
"sort": 9,
|
||||
"is_link": false,
|
||||
"is_catalog": false,
|
||||
"web_path": "/downloadCenter",
|
||||
"component": "system/downloadCenter/index",
|
||||
"component_name": "downloadCenter",
|
||||
"status": true,
|
||||
"cache": false,
|
||||
"visible": true,
|
||||
"parent": 277,
|
||||
"children": [],
|
||||
"menu_button": [
|
||||
{
|
||||
"name": "查询",
|
||||
"value": "Search",
|
||||
"api": "/api/system/downloadCenter/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "详情",
|
||||
"value": "Retrieve",
|
||||
"api": "/api/system/downloadCenter/{id}/",
|
||||
"method": 0
|
||||
},
|
||||
{
|
||||
"name": "新增",
|
||||
"value": "Create",
|
||||
"api": "/api/system/downloadCenter/",
|
||||
"method": 1
|
||||
},
|
||||
{
|
||||
"name": "编辑",
|
||||
"value": "Update",
|
||||
"api": "/api/system/downloadCenter/{id}/",
|
||||
"method": 2
|
||||
},
|
||||
{
|
||||
"name": "删除",
|
||||
"value": "Delete",
|
||||
"api": "/api/system/downloadCenter/{id}/",
|
||||
"method": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"menu_button": [],
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import hashlib
|
||||
import os
|
||||
from time import time
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
from django.contrib.auth.models import AbstractUser, UserManager
|
||||
from django.db import models
|
||||
@@ -71,6 +73,7 @@ class Users(CoreModel, AbstractUser):
|
||||
help_text="关联部门",
|
||||
)
|
||||
login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数")
|
||||
pwd_change_count = models.IntegerField(default=0,blank=True, verbose_name="密码修改次数", help_text="密码修改次数")
|
||||
objects = CustomUserManager()
|
||||
|
||||
def set_password(self, raw_password):
|
||||
@@ -405,6 +408,18 @@ class FileList(CoreModel):
|
||||
mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型")
|
||||
size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小")
|
||||
md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5")
|
||||
UPLOAD_METHOD_CHOIDES = (
|
||||
(0, '默认上传'),
|
||||
(1, '文件选择器上传'),
|
||||
)
|
||||
upload_method = models.SmallIntegerField(default=0, blank=True, null=True, choices=UPLOAD_METHOD_CHOIDES, verbose_name='上传方式', help_text='上传方式')
|
||||
FILE_TYPE_CHOIDES = (
|
||||
(0, '图片'),
|
||||
(1, '视频'),
|
||||
(2, '音频'),
|
||||
(3, '其他'),
|
||||
)
|
||||
file_type = models.SmallIntegerField(default=3, choices=FILE_TYPE_CHOIDES, blank=True, null=True, verbose_name='文件类型', help_text='文件类型')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.md5sum: # file is new
|
||||
@@ -595,3 +610,41 @@ class MessageCenterTargetUser(CoreModel):
|
||||
db_table = table_prefix + "message_center_target_user"
|
||||
verbose_name = "消息中心目标用户表"
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
|
||||
def media_file_name_downloadcenter(instance:'DownloadCenter', filename):
|
||||
h = instance.md5sum
|
||||
basename, ext = os.path.splitext(filename)
|
||||
return PurePosixPath("files", "dlct", h[:1], h[1:2], basename + '-' + str(time()).replace('.', '') + ext.lower())
|
||||
|
||||
|
||||
class DownloadCenter(CoreModel):
|
||||
TASK_STATUS_CHOICES = [
|
||||
(0, '任务已创建'),
|
||||
(1, '任务进行中'),
|
||||
(2, '任务完成'),
|
||||
(3, '任务失败'),
|
||||
]
|
||||
task_name = models.CharField(max_length=255, verbose_name="任务名称", help_text="任务名称")
|
||||
task_status = models.SmallIntegerField(default=0, choices=TASK_STATUS_CHOICES, verbose_name='是否可下载', help_text='是否可下载')
|
||||
file_name = models.CharField(max_length=255, null=True, blank=True, verbose_name="文件名", help_text="文件名")
|
||||
url = models.FileField(upload_to=media_file_name_downloadcenter, null=True, blank=True)
|
||||
size = models.BigIntegerField(default=0, verbose_name="文件大小", help_text="文件大小")
|
||||
md5sum = models.CharField(max_length=36, null=True, blank=True, verbose_name="文件md5", help_text="文件md5")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.url:
|
||||
if not self.md5sum: # file is new
|
||||
md5 = hashlib.md5()
|
||||
for chunk in self.url.chunks():
|
||||
md5.update(chunk)
|
||||
self.md5sum = md5.hexdigest()
|
||||
if not self.size:
|
||||
self.size = self.url.size
|
||||
super(DownloadCenter, self).save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "download_center"
|
||||
verbose_name = "下载中心"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ("-create_datetime",)
|
||||
|
||||
12
backend/dvadmin/system/signals.py
Normal file
12
backend/dvadmin/system/signals.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.dispatch import Signal
|
||||
# 初始化信号
|
||||
pre_init_complete = Signal()
|
||||
detail_init_complete = Signal()
|
||||
post_init_complete = Signal()
|
||||
# 租户初始化信号
|
||||
pre_tenants_init_complete = Signal()
|
||||
detail_tenants_init_complete = Signal()
|
||||
post_tenants_init_complete = Signal()
|
||||
post_tenants_all_init_complete = Signal()
|
||||
# 租户创建完成信号
|
||||
tenants_create_complete = Signal()
|
||||
107
backend/dvadmin/system/tasks.py
Normal file
107
backend/dvadmin/system/tasks.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from hashlib import md5
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.worksheet.table import Table, TableStyleInfo
|
||||
from openpyxl.utils import get_column_letter
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from application.celery import app
|
||||
from dvadmin.system.models import DownloadCenter
|
||||
|
||||
def is_number(num):
|
||||
try:
|
||||
float(num)
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import unicodedata
|
||||
unicodedata.numeric(num)
|
||||
return True
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return False
|
||||
|
||||
def get_string_len(string):
|
||||
"""
|
||||
获取字符串最大长度
|
||||
:param string:
|
||||
:return:
|
||||
"""
|
||||
length = 4
|
||||
if string is None:
|
||||
return length
|
||||
if is_number(string):
|
||||
return length
|
||||
for char in string:
|
||||
length += 2.1 if ord(char) > 256 else 1
|
||||
return round(length, 1) if length <= 50 else 50
|
||||
|
||||
@app.task
|
||||
def async_export_data(data: list, filename: str, dcid: int, export_field_label: dict):
|
||||
instance = DownloadCenter.objects.get(pk=dcid)
|
||||
instance.task_status = 1
|
||||
instance.save()
|
||||
sleep(2)
|
||||
try:
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
header_data = ["序号", *export_field_label.values()]
|
||||
hidden_header = ["#", *export_field_label.keys()]
|
||||
df_len_max = [get_string_len(ele) for ele in header_data]
|
||||
row = get_column_letter(len(export_field_label) + 1)
|
||||
column = 1
|
||||
ws.append(header_data)
|
||||
for index, results in enumerate(data):
|
||||
results_list = []
|
||||
for h_index, h_item in enumerate(hidden_header):
|
||||
for key, val in results.items():
|
||||
if key == h_item:
|
||||
if val is None or val == "":
|
||||
results_list.append("")
|
||||
elif isinstance(val, datetime):
|
||||
val = val.strftime("%Y-%m-%d %H:%M:%S")
|
||||
results_list.append(val)
|
||||
else:
|
||||
results_list.append(val)
|
||||
# 计算最大列宽度
|
||||
result_column_width = get_string_len(val)
|
||||
if h_index != 0 and result_column_width > df_len_max[h_index]:
|
||||
df_len_max[h_index] = result_column_width
|
||||
ws.append([index + 1, *results_list])
|
||||
column += 1
|
||||
# 更新列宽
|
||||
for index, width in enumerate(df_len_max):
|
||||
ws.column_dimensions[get_column_letter(index + 1)].width = width
|
||||
tab = Table(displayName="Table", ref=f"A1:{row}{column}") # 名称管理器
|
||||
style = TableStyleInfo(
|
||||
name="TableStyleLight11",
|
||||
showFirstColumn=True,
|
||||
showLastColumn=True,
|
||||
showRowStripes=True,
|
||||
showColumnStripes=True,
|
||||
)
|
||||
tab.tableStyleInfo = style
|
||||
ws.add_table(tab)
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
stream.seek(0)
|
||||
s = md5()
|
||||
while True:
|
||||
chunk = stream.read(1024)
|
||||
if not chunk:
|
||||
break
|
||||
s.update(chunk)
|
||||
stream.seek(0)
|
||||
instance.md5sum = s.hexdigest()
|
||||
instance.file_name = filename
|
||||
instance.url.save(filename, ContentFile(stream.read()))
|
||||
instance.task_status = 2
|
||||
except Exception as e:
|
||||
instance.task_status = 3
|
||||
instance.description = str(e)[:250]
|
||||
instance.save()
|
||||
@@ -18,6 +18,7 @@ from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermi
|
||||
from dvadmin.system.views.system_config import SystemConfigViewSet
|
||||
from dvadmin.system.views.user import UserViewSet
|
||||
from dvadmin.system.views.menu_field import MenuFieldViewSet
|
||||
from dvadmin.system.views.download_center import DownloadCenterViewSet
|
||||
|
||||
system_url = routers.SimpleRouter()
|
||||
system_url.register(r'menu', MenuViewSet)
|
||||
@@ -35,6 +36,8 @@ system_url.register(r'message_center', MessageCenterViewSet)
|
||||
system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionViewSet)
|
||||
system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet)
|
||||
system_url.register(r'column', MenuFieldViewSet)
|
||||
system_url.register(r'login_log', LoginLogViewSet)
|
||||
system_url.register(r'download_center', DownloadCenterViewSet)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -44,8 +47,8 @@ urlpatterns = [
|
||||
path('system_config/get_association_table/', SystemConfigViewSet.as_view({'get': 'get_association_table'})),
|
||||
path('system_config/get_table_data/<int:pk>/', SystemConfigViewSet.as_view({'get': 'get_table_data'})),
|
||||
path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})),
|
||||
path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
|
||||
path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
|
||||
# path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
|
||||
# path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
|
||||
path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
|
||||
path('clause/privacy.html', PrivacyView.as_view()),
|
||||
path('clause/terms_service.html', TermsServiceView.as_view()),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pypinyin
|
||||
from django.db.models import Q
|
||||
from rest_framework import serializers
|
||||
|
||||
from dvadmin.system.models import Area
|
||||
from dvadmin.utils.field_permission import FieldPermissionMixin
|
||||
from dvadmin.utils.json_response import SuccessResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
@@ -14,13 +16,21 @@ class AreaSerializer(CustomModelSerializer):
|
||||
"""
|
||||
pcode_count = serializers.SerializerMethodField(read_only=True)
|
||||
hasChild = serializers.SerializerMethodField()
|
||||
pcode_info = serializers.SerializerMethodField()
|
||||
|
||||
def get_pcode_info(self, instance):
|
||||
pcode = Area.objects.filter(code=instance.pcode_id).values("name", "code")
|
||||
return pcode
|
||||
|
||||
def get_pcode_count(self, instance: Area):
|
||||
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:
|
||||
model = Area
|
||||
fields = "__all__"
|
||||
@@ -32,12 +42,24 @@ class AreaCreateUpdateSerializer(CustomModelSerializer):
|
||||
地区管理 创建/更新时的列化器
|
||||
"""
|
||||
|
||||
def to_internal_value(self, data):
|
||||
pinyin = ''.join([''.join(i) for i in pypinyin.pinyin(data["name"], style=pypinyin.NORMAL)])
|
||||
data["level"] = 1
|
||||
data["pinyin"] = pinyin
|
||||
data["initials"] = pinyin[0].upper() if pinyin else "#"
|
||||
pcode = data["pcode"] if 'pcode' in data else None
|
||||
if pcode:
|
||||
pcode = Area.objects.get(pk=pcode)
|
||||
data["pcode"] = pcode.code
|
||||
data["level"] = pcode.level + 1
|
||||
return super().to_internal_value(data)
|
||||
|
||||
class Meta:
|
||||
model = Area
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class AreaViewSet(CustomModelViewSet):
|
||||
class AreaViewSet(CustomModelViewSet, FieldPermissionMixin):
|
||||
"""
|
||||
地区管理接口
|
||||
list:查询
|
||||
@@ -48,21 +70,28 @@ class AreaViewSet(CustomModelViewSet):
|
||||
"""
|
||||
queryset = Area.objects.all()
|
||||
serializer_class = AreaSerializer
|
||||
create_serializer_class = AreaCreateUpdateSerializer
|
||||
update_serializer_class = AreaCreateUpdateSerializer
|
||||
extra_filter_class = []
|
||||
|
||||
def get_queryset(self):
|
||||
def list(self, request, *args, **kwargs):
|
||||
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 and pcode:
|
||||
queryset = self.queryset.filter(enable=True, pcode=pcode)
|
||||
else:
|
||||
known_params = {'page', 'limit', 'pcode'}
|
||||
# 使用集合操作检查是否有未知参数
|
||||
other_params_exist = any(param not in known_params for param in params)
|
||||
if other_params_exist:
|
||||
queryset = self.queryset.filter(enable=True)
|
||||
return queryset
|
||||
|
||||
else:
|
||||
pcode = params.get('pcode', None)
|
||||
params['limit'] = 999
|
||||
if params and pcode:
|
||||
queryset = self.queryset.filter(enable=True, pcode=pcode)
|
||||
else:
|
||||
queryset = self.queryset.filter(enable=True, level=1)
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True, request=request)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = self.get_serializer(queryset, many=True, request=request)
|
||||
return SuccessResponse(data=serializer.data, msg="获取成功")
|
||||
|
||||
@@ -10,6 +10,7 @@ from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import Dept, RoleMenuButtonPermission, Users
|
||||
from dvadmin.utils.filters import DataLevelPermissionsFilter
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse, ErrorResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
@@ -124,33 +125,7 @@ class DeptViewSet(CustomModelViewSet):
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[])
|
||||
def dept_lazy_tree(self, request, *args, **kwargs):
|
||||
parent = self.request.query_params.get('parent')
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
queryset = Dept.objects.values('id', 'name', 'parent')
|
||||
else:
|
||||
role_ids = request.user.role.values_list('id', flat=True)
|
||||
data_range = RoleMenuButtonPermission.objects.filter(role__in=role_ids).values_list('data_range', flat=True)
|
||||
user_dept_id = request.user.dept.id
|
||||
dept_list = [user_dept_id]
|
||||
data_range_list = list(set(data_range))
|
||||
for item in data_range_list:
|
||||
if item in [0, 2]:
|
||||
dept_list = [user_dept_id]
|
||||
elif item == 1:
|
||||
dept_list = Dept.recursion_all_dept(dept_id=user_dept_id)
|
||||
elif item == 3:
|
||||
dept_list = Dept.objects.values_list('id', flat=True)
|
||||
elif item == 4:
|
||||
dept_list = request.user.role.values_list('dept', flat=True)
|
||||
else:
|
||||
dept_list = []
|
||||
queryset = Dept.objects.filter(id__in=dept_list).values('id', 'name', 'parent')
|
||||
return DetailResponse(data=queryset, msg="获取成功")
|
||||
|
||||
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated], extra_filter_class=[])
|
||||
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
|
||||
def all_dept(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
data = queryset.filter(status=True).order_by('sort').values('name', 'id', 'parent')
|
||||
|
||||
49
backend/dvadmin/system/views/download_center.py
Normal file
49
backend/dvadmin/system/views/download_center.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from rest_framework import serializers
|
||||
from django.conf import settings
|
||||
from django_filters.rest_framework import FilterSet, CharFilter
|
||||
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
from dvadmin.system.models import DownloadCenter
|
||||
|
||||
|
||||
class DownloadCenterSerializer(CustomModelSerializer):
|
||||
url = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_url(self, instance):
|
||||
if self.request.query_params.get('prefix'):
|
||||
if settings.ENVIRONMENT in ['local']:
|
||||
prefix = 'http://127.0.0.1:8000'
|
||||
elif settings.ENVIRONMENT in ['test']:
|
||||
prefix = 'http://{host}/api'.format(host=self.request.get_host())
|
||||
else:
|
||||
prefix = 'https://{host}/api'.format(host=self.request.get_host())
|
||||
return (f'{prefix}/media/{str(instance.url)}')
|
||||
return f'media/{str(instance.url)}'
|
||||
|
||||
class Meta:
|
||||
model = DownloadCenter
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class DownloadCenterFilterSet(FilterSet):
|
||||
task_name = CharFilter(field_name='task_name', lookup_expr='icontains')
|
||||
file_name = CharFilter(field_name='file_name', lookup_expr='icontains')
|
||||
|
||||
class Meta:
|
||||
model = DownloadCenter
|
||||
fields = ['task_status', 'task_name', 'file_name']
|
||||
|
||||
|
||||
class DownloadCenterViewSet(CustomModelViewSet):
|
||||
queryset = DownloadCenter.objects.all()
|
||||
serializer_class = DownloadCenterSerializer
|
||||
filter_class = DownloadCenterFilterSet
|
||||
permission_classes = []
|
||||
extra_filter_class = []
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_superuser:
|
||||
return super().get_queryset()
|
||||
return super().get_queryset().filter(creator=self.request.user)
|
||||
@@ -1,12 +1,15 @@
|
||||
import hashlib
|
||||
import mimetypes
|
||||
|
||||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from application import dispatch
|
||||
from dvadmin.system.models import FileList
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -15,7 +18,16 @@ class FileSerializer(CustomModelSerializer):
|
||||
url = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_url(self, instance):
|
||||
# return 'media/' + str(instance.url)
|
||||
if self.request.query_params.get('prefix'):
|
||||
if settings.ENVIRONMENT in ['local']:
|
||||
prefix = 'http://127.0.0.1:8000'
|
||||
elif settings.ENVIRONMENT in ['test']:
|
||||
prefix = 'http://{host}/api'.format(host=self.request.get_host())
|
||||
else:
|
||||
prefix = 'https://{host}/api'.format(host=self.request.get_host())
|
||||
if instance.file_url:
|
||||
return instance.file_url if instance.file_url.startswith('http') else f"{prefix}/{instance.file_url}"
|
||||
return (f'{prefix}/media/{str(instance.url)}')
|
||||
return instance.file_url or (f'media/{str(instance.url)}')
|
||||
|
||||
class Meta:
|
||||
@@ -35,6 +47,8 @@ class FileSerializer(CustomModelSerializer):
|
||||
validated_data['md5sum'] = md5.hexdigest()
|
||||
validated_data['engine'] = file_engine
|
||||
validated_data['mime_type'] = file.content_type
|
||||
ft = {'image':0,'video':1,'audio':2}.get(file.content_type.split('/')[0], None)
|
||||
validated_data['file_type'] = 3 if ft is None else ft
|
||||
if file_backup:
|
||||
validated_data['url'] = file
|
||||
if file_engine == 'oss':
|
||||
@@ -64,6 +78,22 @@ class FileSerializer(CustomModelSerializer):
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class FileAllSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = FileList
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
class FileFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(field_name="name", lookup_expr="icontains", help_text="文件名")
|
||||
mime_type = django_filters.CharFilter(field_name="mime_type", lookup_expr="icontains", help_text="文件类型")
|
||||
|
||||
class Meta:
|
||||
model = FileList
|
||||
fields = ['name', 'mime_type', 'upload_method', 'file_type']
|
||||
|
||||
|
||||
class FileViewSet(CustomModelViewSet):
|
||||
"""
|
||||
文件管理接口
|
||||
@@ -75,5 +105,22 @@ class FileViewSet(CustomModelViewSet):
|
||||
"""
|
||||
queryset = FileList.objects.all()
|
||||
serializer_class = FileSerializer
|
||||
filter_fields = ['name', ]
|
||||
permission_classes = []
|
||||
filter_class = FileFilter
|
||||
permission_classes = []
|
||||
|
||||
@action(methods=['GET'], detail=False)
|
||||
def get_all(self, request):
|
||||
data1 = self.get_serializer(self.get_queryset(), many=True).data
|
||||
data2 = []
|
||||
if dispatch.is_tenants_mode():
|
||||
from django_tenants.utils import schema_context
|
||||
with schema_context('public'):
|
||||
data2 = self.get_serializer(FileList.objects.all(), many=True).data
|
||||
return DetailResponse(data=data2+data1)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if self.request.query_params.get('system', 'False') == 'True' and dispatch.is_tenants_mode():
|
||||
from django_tenants.utils import schema_context
|
||||
with schema_context('public'):
|
||||
return super().list(request, *args, **kwargs)
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@@ -4,11 +4,15 @@ from datetime import datetime, timedelta
|
||||
from captcha.views import CaptchaStore, captcha_image
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.hashers import check_password, make_password
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
@@ -83,22 +87,30 @@ class LoginSerializer(TokenObtainPairSerializer):
|
||||
else:
|
||||
self.image_code and self.image_code.delete()
|
||||
raise CustomValidationError("图片验证码错误")
|
||||
|
||||
user = Users.objects.get(username=attrs['username'])
|
||||
try:
|
||||
user = Users.objects.get(
|
||||
Q(username=attrs['username']) | Q(email=attrs['username']) | Q(mobile=attrs['username']))
|
||||
except Users.DoesNotExist:
|
||||
raise CustomValidationError("您登录的账号不存在")
|
||||
except Users.MultipleObjectsReturned:
|
||||
raise CustomValidationError("您登录的账号存在多个,请联系管理员检查登录账号唯一性")
|
||||
if not user.is_active:
|
||||
raise CustomValidationError("账号已被锁定,联系管理员解锁")
|
||||
try:
|
||||
# 必须重置用户名为username,否则使用邮箱手机号登录会提示密码错误
|
||||
attrs['username'] = user.username
|
||||
data = super().validate(attrs)
|
||||
data["username"] = self.user.username
|
||||
data["name"] = self.user.name
|
||||
data["userId"] = self.user.id
|
||||
data["avatar"] = self.user.avatar
|
||||
data['user_type'] = self.user.user_type
|
||||
data['pwd_change_count'] = self.user.pwd_change_count
|
||||
dept = getattr(self.user, 'dept', None)
|
||||
if dept:
|
||||
data['dept_info'] = {
|
||||
'dept_id': dept.id,
|
||||
'dept_name': dept.name,
|
||||
|
||||
}
|
||||
role = getattr(self.user, 'role', None)
|
||||
if role:
|
||||
@@ -114,6 +126,7 @@ class LoginSerializer(TokenObtainPairSerializer):
|
||||
user.login_error_count += 1
|
||||
if user.login_error_count >= 5:
|
||||
user.is_active = False
|
||||
user.save()
|
||||
raise CustomValidationError("账号已被锁定,联系管理员解锁")
|
||||
user.save()
|
||||
count = 5 - user.login_error_count
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@Remark: 按钮权限管理
|
||||
"""
|
||||
from dvadmin.system.models import LoginLog
|
||||
from dvadmin.utils.field_permission import FieldPermissionMixin
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
@@ -22,7 +23,7 @@ class LoginLogSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class LoginLogViewSet(CustomModelViewSet):
|
||||
class LoginLogViewSet(CustomModelViewSet, FieldPermissionMixin):
|
||||
"""
|
||||
登录日志接口
|
||||
list:查询
|
||||
@@ -33,4 +34,4 @@ class LoginLogViewSet(CustomModelViewSet):
|
||||
"""
|
||||
queryset = LoginLog.objects.all()
|
||||
serializer_class = LoginLogSerializer
|
||||
extra_filter_class = []
|
||||
# extra_filter_class = []
|
||||
|
||||
@@ -120,11 +120,11 @@ class MenuViewSet(CustomModelViewSet):
|
||||
"""用于前端获取当前角色的路由"""
|
||||
user = request.user
|
||||
if user.is_superuser:
|
||||
queryset = self.queryset.filter(status=1)
|
||||
queryset = self.queryset.filter(status=1).order_by("sort")
|
||||
else:
|
||||
role_list = user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id', flat=True)
|
||||
queryset = Menu.objects.filter(id__in=menu_list)
|
||||
queryset = Menu.objects.filter(id__in=menu_list).order_by("sort")
|
||||
serializer = WebRouterSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return SuccessResponse(data=data, total=len(data), msg="获取成功")
|
||||
|
||||
@@ -10,12 +10,14 @@ 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.system.models import MenuButton, RoleMenuButtonPermission, Menu
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
|
||||
|
||||
|
||||
class MenuButtonSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单按钮-序列化器
|
||||
@@ -92,17 +94,15 @@ class MenuButtonViewSet(CustomModelViewSet):
|
||||
"""
|
||||
menu_obj = Menu.objects.filter(id=request.data['menu']).first()
|
||||
result_list = [
|
||||
{'menu': menu_obj.id, 'name': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api{menu_obj.web_path}/',
|
||||
'method': 1},
|
||||
{'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api{menu_obj.web_path}/{{id}}/',
|
||||
'method': 3},
|
||||
{'menu': menu_obj.id, 'name': '修改', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api{menu_obj.web_path}/{{id}}/',
|
||||
'method': 2},
|
||||
{'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api{menu_obj.web_path}/',
|
||||
'method': 0},
|
||||
{'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api{menu_obj.web_path}/{{id}}/',
|
||||
'method': 0}]
|
||||
{'menu': menu_obj.id, 'name': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api/{menu_obj.component_name}/', 'method': 1},
|
||||
{'menu': menu_obj.id, 'name': '删除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 3},
|
||||
{'menu': menu_obj.id, 'name': '编辑', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 2},
|
||||
{'menu': menu_obj.id, 'name': '查询', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api/{menu_obj.component_name}/', 'method': 0},
|
||||
{'menu': menu_obj.id, 'name': '详情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 0},
|
||||
{'menu': menu_obj.id, 'name': '复制', 'value': f'{menu_obj.component_name}:Copy', 'api': f'/api/{menu_obj.component_name}/', 'method': 1},
|
||||
{'menu': menu_obj.id, 'name': '导入', 'value': f'{menu_obj.component_name}:Import', 'api': f'/api/{menu_obj.component_name}/import_data/', 'method': 1},
|
||||
{'menu': menu_obj.id, 'name': '导出', 'value': f'{menu_obj.component_name}:Export', 'api': f'/api{menu_obj.component_name}/export_data/', 'method': 1},]
|
||||
serializer = self.get_serializer(data=result_list, many=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return SuccessResponse(serializer.data, msg="批量创建成功")
|
||||
return SuccessResponse(serializer.data, msg="批量创建成功")
|
||||
@@ -36,6 +36,8 @@ class MessageCenterSerializer(CustomModelSerializer):
|
||||
return serializer.data
|
||||
|
||||
def get_user_info(self, instance, parsed_query):
|
||||
if instance.target_type in (1,2,3):
|
||||
return []
|
||||
users = instance.target_user.all()
|
||||
# You can do what ever you want in here
|
||||
# `parsed_query` param is passed to BookSerializer to allow further querying
|
||||
@@ -80,6 +82,9 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
|
||||
"""
|
||||
目标用户序列化器-序列化器
|
||||
"""
|
||||
role_info = DynamicSerializerMethodField()
|
||||
user_info = DynamicSerializerMethodField()
|
||||
dept_info = DynamicSerializerMethodField()
|
||||
is_read = serializers.SerializerMethodField()
|
||||
|
||||
def get_is_read(self, instance):
|
||||
@@ -90,6 +95,44 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
|
||||
return queryset.is_read
|
||||
return False
|
||||
|
||||
def get_role_info(self, instance, parsed_query):
|
||||
roles = instance.target_role.all()
|
||||
# You can do what ever you want in here
|
||||
# `parsed_query` param is passed to BookSerializer to allow further querying
|
||||
from dvadmin.system.views.role import RoleSerializer
|
||||
serializer = RoleSerializer(
|
||||
roles,
|
||||
many=True,
|
||||
parsed_query=parsed_query
|
||||
)
|
||||
return serializer.data
|
||||
|
||||
def get_user_info(self, instance, parsed_query):
|
||||
if instance.target_type in (1,2,3):
|
||||
return []
|
||||
users = instance.target_user.all()
|
||||
# You can do what ever you want in here
|
||||
# `parsed_query` param is passed to BookSerializer to allow further querying
|
||||
from dvadmin.system.views.user import UserSerializer
|
||||
serializer = UserSerializer(
|
||||
users,
|
||||
many=True,
|
||||
parsed_query=parsed_query
|
||||
)
|
||||
return serializer.data
|
||||
|
||||
def get_dept_info(self, instance, parsed_query):
|
||||
dept = instance.target_dept.all()
|
||||
# You can do what ever you want in here
|
||||
# `parsed_query` param is passed to BookSerializer to allow further querying
|
||||
from dvadmin.system.views.dept import DeptSerializer
|
||||
serializer = DeptSerializer(
|
||||
dept,
|
||||
many=True,
|
||||
parsed_query=parsed_query
|
||||
)
|
||||
return serializer.data
|
||||
|
||||
class Meta:
|
||||
model = MessageCenter
|
||||
fields = "__all__"
|
||||
|
||||
@@ -26,6 +26,12 @@ class RoleSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色-序列化器
|
||||
"""
|
||||
users = serializers.SerializerMethodField()
|
||||
|
||||
@staticmethod
|
||||
def get_users(instance):
|
||||
users = instance.users_set.exclude(id=1).values('id', 'name', 'dept__name')
|
||||
return users
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
@@ -116,3 +122,23 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
|
||||
create_serializer_class = RoleCreateUpdateSerializer
|
||||
update_serializer_class = RoleCreateUpdateSerializer
|
||||
search_fields = ['name', 'key']
|
||||
|
||||
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
|
||||
def set_role_users(self, request, pk):
|
||||
"""
|
||||
设置 角色-用户
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
data = request.data
|
||||
direction = data.get('direction')
|
||||
movedKeys = data.get('movedKeys')
|
||||
role = Role.objects.get(pk=pk)
|
||||
if direction == "left":
|
||||
# left : 移除用户权限
|
||||
role.users_set.remove(*movedKeys)
|
||||
else:
|
||||
# right : 添加用户权限
|
||||
role.users_set.add(*movedKeys)
|
||||
serializer = RoleSerializer(role)
|
||||
return DetailResponse(data=serializer.data, msg="更新成功")
|
||||
@@ -6,22 +6,20 @@
|
||||
@Created on: 2021/6/3 003 0:30
|
||||
@Remark: 菜单按钮管理
|
||||
"""
|
||||
from django.db.models import F, Subquery, OuterRef, Exists
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import RoleMenuButtonPermission, Menu, MenuButton, Dept, RoleMenuPermission, FieldPermission, \
|
||||
MenuField
|
||||
from dvadmin.system.views.menu import MenuSerializer
|
||||
from dvadmin.utils.json_response import DetailResponse, ErrorResponse
|
||||
from dvadmin.system.models import RoleMenuButtonPermission, Menu, Dept, MenuButton, RoleMenuPermission, \
|
||||
MenuField, FieldPermission
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
from dvadmin.utils.viewset import CustomModelViewSet
|
||||
|
||||
|
||||
class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单按钮-序列化器
|
||||
角色-菜单-按钮-权限 查询序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
@@ -32,7 +30,7 @@ class RoleMenuButtonPermissionSerializer(CustomModelSerializer):
|
||||
|
||||
class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
初始化菜单按钮-序列化器
|
||||
角色-菜单-按钮-权限 创建/修改序列化
|
||||
"""
|
||||
menu_button__name = serializers.CharField(source='menu_button.name', read_only=True)
|
||||
menu_button__value = serializers.CharField(source='menu_button.value', read_only=True)
|
||||
@@ -43,63 +41,99 @@ class RoleMenuButtonPermissionCreateUpdateSerializer(CustomModelSerializer):
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class RoleButtonPermissionSerializer(CustomModelSerializer):
|
||||
class RoleMenuSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色按钮权限
|
||||
角色-菜单 序列化
|
||||
"""
|
||||
isCheck = serializers.SerializerMethodField()
|
||||
data_range = serializers.SerializerMethodField()
|
||||
|
||||
def get_isCheck(self, instance):
|
||||
params = self.request.query_params
|
||||
data = self.request.data
|
||||
return RoleMenuPermission.objects.filter(
|
||||
menu_id=instance.id,
|
||||
role_id=params.get('roleId', data.get('roleId')),
|
||||
).exists()
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = ["id", "name", "parent", "is_catalog", "isCheck"]
|
||||
|
||||
|
||||
class RoleMenuButtonSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色-菜单-按钮 序列化
|
||||
"""
|
||||
isCheck = serializers.SerializerMethodField()
|
||||
data_range = serializers.SerializerMethodField()
|
||||
role_menu_btn_perm_id = serializers.SerializerMethodField()
|
||||
dept = serializers.SerializerMethodField()
|
||||
|
||||
def get_isCheck(self, instance):
|
||||
params = self.request.query_params
|
||||
data = self.request.data
|
||||
return RoleMenuButtonPermission.objects.filter(
|
||||
menu_button__id=instance['id'],
|
||||
role__id=params.get('role'),
|
||||
menu_button_id=instance.id,
|
||||
role_id=params.get('roleId', data.get('roleId')),
|
||||
).exists()
|
||||
|
||||
def get_data_range(self, instance):
|
||||
params = self.request.query_params
|
||||
obj = RoleMenuButtonPermission.objects.filter(
|
||||
menu_button__id=instance['id'],
|
||||
role__id=params.get('role'),
|
||||
).first()
|
||||
obj = self.get_role_menu_btn_prem(instance)
|
||||
if obj is None:
|
||||
return None
|
||||
return obj.data_range
|
||||
|
||||
def get_role_menu_btn_perm_id(self, instance):
|
||||
obj = self.get_role_menu_btn_prem(instance)
|
||||
if obj is None:
|
||||
return None
|
||||
return obj.id
|
||||
|
||||
def get_dept(self, instance):
|
||||
obj = self.get_role_menu_btn_prem(instance)
|
||||
if obj is None:
|
||||
return None
|
||||
return obj.dept.all().values_list('id', flat=True)
|
||||
|
||||
def get_role_menu_btn_prem(self, instance):
|
||||
params = self.request.query_params
|
||||
data = self.request.data
|
||||
obj = RoleMenuButtonPermission.objects.filter(
|
||||
menu_button_id=instance.id,
|
||||
role_id=params.get('roleId', data.get('roleId')),
|
||||
).first()
|
||||
return obj
|
||||
|
||||
class Meta:
|
||||
model = MenuButton
|
||||
fields = ['id', 'name', 'value', 'isCheck', 'data_range']
|
||||
|
||||
|
||||
class RoleFieldPermissionSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = FieldPermission
|
||||
fields = "__all__"
|
||||
fields = ['id', 'menu', 'name', 'isCheck', 'data_range', 'role_menu_btn_perm_id', 'dept']
|
||||
|
||||
|
||||
class RoleMenuFieldSerializer(CustomModelSerializer):
|
||||
"""
|
||||
角色-菜单-字段 序列化
|
||||
"""
|
||||
is_query = serializers.SerializerMethodField()
|
||||
is_create = serializers.SerializerMethodField()
|
||||
is_update = serializers.SerializerMethodField()
|
||||
|
||||
def get_is_query(self, instance):
|
||||
params = self.request.query_params
|
||||
queryset = instance.menu_field.filter(role=params.get('role')).first()
|
||||
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
|
||||
if queryset:
|
||||
return queryset.is_query
|
||||
return False
|
||||
|
||||
def get_is_create(self, instance):
|
||||
params = self.request.query_params
|
||||
queryset = instance.menu_field.filter(role=params.get('role')).first()
|
||||
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
|
||||
if queryset:
|
||||
return queryset.is_create
|
||||
return False
|
||||
|
||||
def get_is_update(self, instance):
|
||||
params = self.request.query_params
|
||||
queryset = instance.menu_field.filter(role=params.get('role')).first()
|
||||
queryset = instance.menu_field.filter(role=params.get('roleId')).first()
|
||||
if queryset:
|
||||
return queryset.is_update
|
||||
return False
|
||||
@@ -109,54 +143,6 @@ class RoleMenuFieldSerializer(CustomModelSerializer):
|
||||
fields = ['id', 'field_name', 'title', 'is_query', 'is_create', 'is_update']
|
||||
|
||||
|
||||
class RoleMenuSerializer(CustomModelSerializer):
|
||||
menus = serializers.SerializerMethodField()
|
||||
|
||||
def get_menus(self, instance):
|
||||
menu_list = Menu.objects.filter(parent=instance['id']).values('id', 'name')
|
||||
serializer = RoleMenuPermissionSerializer(menu_list, many=True, request=self.request)
|
||||
return serializer.data
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = ['id', 'name', 'menus']
|
||||
|
||||
|
||||
class RoleMenuPermissionSerializer(CustomModelSerializer):
|
||||
"""
|
||||
菜单和按钮权限
|
||||
"""
|
||||
# name = serializers.SerializerMethodField()
|
||||
isCheck = serializers.SerializerMethodField()
|
||||
btns = serializers.SerializerMethodField()
|
||||
columns = serializers.SerializerMethodField()
|
||||
|
||||
# def get_name(self, instance):
|
||||
# parent_list = Menu.get_all_parent(instance['id'])
|
||||
# names = [d["name"] for d in parent_list]
|
||||
# return "/".join(names)
|
||||
def get_isCheck(self, instance):
|
||||
params = self.request.query_params
|
||||
return RoleMenuPermission.objects.filter(
|
||||
menu__id=instance['id'],
|
||||
role__id=params.get('role'),
|
||||
).exists()
|
||||
|
||||
def get_btns(self, instance):
|
||||
btn_list = MenuButton.objects.filter(menu__id=instance['id']).values('id', 'name', 'value')
|
||||
serializer = RoleButtonPermissionSerializer(btn_list, many=True, request=self.request)
|
||||
return serializer.data
|
||||
|
||||
def get_columns(self, instance):
|
||||
col_list = MenuField.objects.filter(menu=instance['id'])
|
||||
serializer = RoleMenuFieldSerializer(col_list, many=True, request=self.request)
|
||||
return serializer.data
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = ['id', 'name', 'isCheck', 'btns', 'columns']
|
||||
|
||||
|
||||
class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
"""
|
||||
菜单按钮接口
|
||||
@@ -173,204 +159,102 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
extra_filter_class = []
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def get_role_premission(self, request):
|
||||
def get_role_menu(self, request):
|
||||
"""
|
||||
角色授权获取:
|
||||
:param request: role
|
||||
:return: menu,btns,columns
|
||||
获取 角色-菜单
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
menu_queryset = Menu.objects.all()
|
||||
serializer = RoleMenuSerializer(menu_queryset, many=True, request=request)
|
||||
return DetailResponse(data=serializer.data)
|
||||
|
||||
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def set_role_menu(self, request):
|
||||
"""
|
||||
设置 角色-菜单
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
data = request.data
|
||||
roleId = data.get('roleId')
|
||||
menuId = data.get('menuId')
|
||||
isCheck = data.get('isCheck')
|
||||
if isCheck:
|
||||
# 添加权限:创建关联记录
|
||||
instance = RoleMenuPermission.objects.create(role_id=roleId, menu_id=menuId)
|
||||
else:
|
||||
# 删除权限:移除关联记录
|
||||
RoleMenuPermission.objects.filter(role_id=roleId, menu_id=menuId).delete()
|
||||
menu_instance = Menu.objects.get(id=menuId)
|
||||
serializer = RoleMenuSerializer(menu_instance, request=request)
|
||||
return DetailResponse(data=serializer.data, msg="更新成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def get_role_menu_btn_field(self, request):
|
||||
"""
|
||||
获取 角色-菜单-按钮-列字段
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
role = params.get('role', None)
|
||||
if role is None:
|
||||
return ErrorResponse(msg="未获取到角色信息")
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
queryset = Menu.objects.filter(status=1, is_catalog=True).values('name', 'id').all()
|
||||
else:
|
||||
role_id = request.user.role.values_list('id', flat=True)
|
||||
menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id', flat=True)
|
||||
queryset = Menu.objects.filter(status=1, is_catalog=True, id__in=menu_list).values('name', 'id').all()
|
||||
serializer = RoleMenuSerializer(queryset, many=True, request=request)
|
||||
data = serializer.data
|
||||
return DetailResponse(data=data)
|
||||
# data = []
|
||||
# if is_superuser:
|
||||
# queryset = Menu.objects.filter(status=1, is_catalog=False).values('name', 'id').all()
|
||||
# else:
|
||||
# role_id = request.user.role.values_list('id', flat=True)
|
||||
# menu_list = RoleMenuPermission.objects.filter(role__in=role_id).values_list('id', flat=True)
|
||||
# queryset = Menu.objects.filter(status=1, is_catalog=False, id__in=menu_list).values('name', 'id')
|
||||
# for item in queryset:
|
||||
# parent_list = Menu.get_all_parent(item['id'])
|
||||
# names = [d["name"] for d in parent_list]
|
||||
# completeName = "/".join(names)
|
||||
# isCheck = RoleMenuPermission.objects.filter(
|
||||
# menu__id=item['id'],
|
||||
# role__id=role,
|
||||
# ).exists()
|
||||
# mbCheck = RoleMenuButtonPermission.objects.filter(
|
||||
# menu_button=OuterRef("pk"),
|
||||
# role__id=role,
|
||||
# )
|
||||
# btns = MenuButton.objects.filter(
|
||||
# menu__id=item['id'],
|
||||
# ).annotate(isCheck=Exists(mbCheck)).values('id', 'name', 'value', 'isCheck',
|
||||
# data_range=F('menu_button_permission__data_range'))
|
||||
# dicts = {
|
||||
# 'name': completeName,
|
||||
# 'id': item['id'],
|
||||
# 'isCheck': isCheck,
|
||||
# 'btns': btns,
|
||||
#
|
||||
# }
|
||||
# data.append(dicts)
|
||||
# return DetailResponse(data=data)
|
||||
menuId = params.get('menuId', None)
|
||||
menu_btn_queryset = MenuButton.objects.filter(menu_id=menuId)
|
||||
menu_btn_serializer = RoleMenuButtonSerializer(menu_btn_queryset, many=True, request=request)
|
||||
menu_field_queryset = MenuField.objects.filter(menu_id=menuId)
|
||||
menu_field_serializer = RoleMenuFieldSerializer(menu_field_queryset, many=True, request=request)
|
||||
return DetailResponse(data={'menu_btn': menu_btn_serializer.data, 'menu_field': menu_field_serializer.data})
|
||||
|
||||
@action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated])
|
||||
def set_role_premission(self, request, pk):
|
||||
def set_role_menu_field(self, request, pk):
|
||||
"""
|
||||
对角色的菜单和按钮及按钮范围授权:
|
||||
:param request:
|
||||
:param pk: role
|
||||
:return:
|
||||
设置 角色-菜单-列字段
|
||||
"""
|
||||
body = request.data
|
||||
RoleMenuPermission.objects.filter(role=pk).delete()
|
||||
RoleMenuButtonPermission.objects.filter(role=pk).delete()
|
||||
for item in body:
|
||||
for menu in item["menus"]:
|
||||
if menu.get('isCheck'):
|
||||
menu_parent = Menu.get_all_parent(menu.get('id'))
|
||||
role_menu_permission_list = []
|
||||
for d in menu_parent:
|
||||
role_menu_permission_list.append(RoleMenuPermission(role_id=pk, menu_id=d["id"]))
|
||||
RoleMenuPermission.objects.bulk_create(role_menu_permission_list)
|
||||
# RoleMenuPermission.objects.create(role_id=pk, menu_id=menu.get('id'))
|
||||
for btn in menu.get('btns'):
|
||||
if btn.get('isCheck'):
|
||||
data_range = btn.get('data_range', 0) or 0
|
||||
instance = RoleMenuButtonPermission.objects.create(role_id=pk, menu_button_id=btn.get('id'),
|
||||
data_range=data_range)
|
||||
instance.dept.set(btn.get('dept', []))
|
||||
for col in menu.get('columns'):
|
||||
FieldPermission.objects.update_or_create(role_id=pk, field_id=col.get('id'),
|
||||
defaults={
|
||||
'is_query': col.get('is_query'),
|
||||
'is_create': col.get('is_create'),
|
||||
'is_update': col.get('is_update')
|
||||
})
|
||||
return DetailResponse(msg="授权成功")
|
||||
data = request.data
|
||||
for col in data:
|
||||
FieldPermission.objects.update_or_create(
|
||||
role_id=pk, field_id=col.get('id'),
|
||||
defaults={
|
||||
'is_create': col.get('is_create'),
|
||||
'is_update': col.get('is_update'),
|
||||
'is_query': col.get('is_query'),
|
||||
})
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_menu_get_button(self, request):
|
||||
"""
|
||||
当前用户角色和菜单获取可下拉选项的按钮:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
if params := request.query_params:
|
||||
if menu_id := params.get('menu', None):
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
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(btn_id=F('menu_button__id'), name=F('menu_button__name'))
|
||||
return DetailResponse(data=queryset)
|
||||
return ErrorResponse(msg="参数错误")
|
||||
return DetailResponse(data=[], msg="更新成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def data_scope(self, request):
|
||||
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def set_role_menu_btn(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)
|
||||
data = request.data
|
||||
isCheck = data.get('isCheck', None)
|
||||
roleId = data.get('roleId', None)
|
||||
btnId = data.get('btnId', None)
|
||||
if isCheck:
|
||||
# 添加权限:创建关联记录
|
||||
RoleMenuButtonPermission.objects.create(role_id=roleId, menu_button_id=btnId)
|
||||
else:
|
||||
data = []
|
||||
role_list = request.user.role.values_list('id', flat=True)
|
||||
if params := request.query_params:
|
||||
if menu_button_id := params.get('menu_button', None):
|
||||
role_queryset = RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_list, menu_button__id=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="参数错误")
|
||||
# 删除权限:移除关联记录
|
||||
RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete()
|
||||
menu_btn_instance = MenuButton.objects.get(id=btnId)
|
||||
serializer = RoleMenuButtonSerializer(menu_btn_instance, request=request)
|
||||
return DetailResponse(data=serializer.data, msg="更新成功")
|
||||
|
||||
@action(methods=['PUT'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def set_role_menu_btn_data_range(self, request):
|
||||
"""
|
||||
设置 角色-菜单-按钮-权限
|
||||
"""
|
||||
data = request.data
|
||||
instance = RoleMenuButtonPermission.objects.get(id=data.get('role_menu_btn_perm_id'))
|
||||
instance.data_range = data.get('data_range')
|
||||
instance.dept.add(*data.get('dept'))
|
||||
if not data.get('dept'):
|
||||
instance.dept.clear()
|
||||
instance.save()
|
||||
serializer = RoleMenuButtonPermissionSerializer(instance, request=request)
|
||||
return DetailResponse(data=serializer.data, msg="更新成功")
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_to_dept_all(self, request):
|
||||
@@ -379,72 +263,20 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
queryset = Dept.objects.values('id', 'name', 'parent')
|
||||
else:
|
||||
if not params:
|
||||
return ErrorResponse(msg="参数错误")
|
||||
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(
|
||||
dept_id=F('dept__id'),
|
||||
name=F('dept__name'),
|
||||
parent=F('dept__parent')
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def menu_to_button(self, request):
|
||||
"""
|
||||
根据所选择菜单获取已配置的按钮/接口权限:角色授权页面使用
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
menu_id = params.get('menu', None)
|
||||
if menu_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
is_superuser = request.user.is_superuser
|
||||
if is_superuser:
|
||||
queryset = RoleMenuButtonPermission.objects.filter(menu_button__menu=menu_id).values(
|
||||
'id',
|
||||
'data_range',
|
||||
'menu_button',
|
||||
'menu_button__name',
|
||||
'menu_button__value'
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
else:
|
||||
if params:
|
||||
# 当前登录用户的角色
|
||||
role_list = request.user.role.values_list('id', flat=True)
|
||||
|
||||
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',
|
||||
'menu_button__name',
|
||||
'menu_button__value'
|
||||
)
|
||||
return DetailResponse(data=queryset)
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
menu_button_id = params.get('menu_button')
|
||||
# 当前登录用户角色可以分配的自定义部门权限
|
||||
dept_checked_disabled = RoleMenuButtonPermission.objects.filter(
|
||||
role_id__in=role_list, menu_button_id=menu_button_id
|
||||
).values_list('dept', flat=True)
|
||||
dept_list = Dept.objects.values('id', 'name', 'parent')
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def role_to_menu(self, request):
|
||||
"""
|
||||
获取角色对应的按钮权限
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
params = request.query_params
|
||||
role_id = params.get('role', None)
|
||||
if role_id is None:
|
||||
return ErrorResponse(msg="未获取到参数")
|
||||
queryset = RoleMenuPermission.objects.filter(role_id=role_id).values_list('menu_id', flat=True).distinct()
|
||||
|
||||
return DetailResponse(data=queryset)
|
||||
data = []
|
||||
for dept in dept_list:
|
||||
dept["disabled"] = False if is_superuser else dept["id"] not in dept_checked_disabled
|
||||
data.append(dept)
|
||||
return DetailResponse(data=data)
|
||||
|
||||
@@ -119,7 +119,6 @@ class UserUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
更改激活状态
|
||||
"""
|
||||
print(111, value)
|
||||
if value:
|
||||
self.initial_data["login_error_count"] = 0
|
||||
return value
|
||||
@@ -287,6 +286,7 @@ class UserViewSet(CustomModelViewSet):
|
||||
"dept": user.dept_id,
|
||||
"is_superuser": user.is_superuser,
|
||||
"role": user.role.values_list('id', flat=True),
|
||||
"pwd_change_count":user.pwd_change_count
|
||||
}
|
||||
if hasattr(connection, 'tenant'):
|
||||
result['tenant_id'] = connection.tenant and connection.tenant.id
|
||||
@@ -320,7 +320,6 @@ class UserViewSet(CustomModelViewSet):
|
||||
"""密码修改"""
|
||||
data = request.data
|
||||
old_pwd = data.get("oldPassword")
|
||||
print(old_pwd)
|
||||
new_pwd = data.get("newPassword")
|
||||
new_pwd2 = data.get("newPassword2")
|
||||
if old_pwd is None or new_pwd is None or new_pwd2 is None:
|
||||
@@ -331,13 +330,33 @@ class UserViewSet(CustomModelViewSet):
|
||||
if not verify_password:
|
||||
old_pwd_md5 = hashlib.md5(old_pwd.encode(encoding='UTF-8')).hexdigest()
|
||||
verify_password = check_password(str(old_pwd_md5), request.user.password)
|
||||
# 创建用户时、自定义密码无法修改问题
|
||||
if not verify_password:
|
||||
old_pwd_md5 = hashlib.md5(old_pwd_md5.encode(encoding='UTF-8')).hexdigest()
|
||||
verify_password = check_password(str(old_pwd_md5), request.user.password)
|
||||
if verify_password:
|
||||
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||
# request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||
request.user.password = make_password(new_pwd)
|
||||
request.user.pwd_change_count += 1
|
||||
request.user.save()
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
else:
|
||||
return ErrorResponse(msg="旧密码不正确")
|
||||
|
||||
@action(methods=["post"], detail=False, permission_classes=[IsAuthenticated])
|
||||
def login_change_password(self, request, *args, **kwargs):
|
||||
"""初次登录进行密码修改"""
|
||||
data = request.data
|
||||
new_pwd = data.get("password")
|
||||
new_pwd2 = data.get("password_regain")
|
||||
if new_pwd != new_pwd2:
|
||||
return ErrorResponse(msg="两次密码不匹配")
|
||||
else:
|
||||
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
|
||||
request.user.pwd_change_count += 1
|
||||
request.user.save()
|
||||
return DetailResponse(data=None, msg="修改成功")
|
||||
|
||||
@action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated])
|
||||
def reset_to_default_password(self, request,pk):
|
||||
"""恢复默认密码"""
|
||||
@@ -407,9 +426,12 @@ class UserViewSet(CustomModelViewSet):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
else:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
# print(queryset.values('id','name','dept__id'))
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True, request=request)
|
||||
# print(serializer.data)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = self.get_serializer(queryset, many=True, request=request)
|
||||
|
||||
return SuccessResponse(data=serializer.data, msg="获取成功")
|
||||
|
||||
@@ -5,34 +5,39 @@ from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from dvadmin.system.models import FieldPermission, MenuField
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.models import get_custom_app_models
|
||||
|
||||
|
||||
def merge_permission(data):
|
||||
"""
|
||||
合并权限
|
||||
"""
|
||||
result = {}
|
||||
for item in data:
|
||||
field_name = item.pop('field_name')
|
||||
if field_name not in result:
|
||||
result[field_name] = item
|
||||
else:
|
||||
for key, value in item.items():
|
||||
result[field_name][key] = result[field_name][key] or value
|
||||
return result
|
||||
|
||||
|
||||
class FieldPermissionMixin:
|
||||
@action(methods=['get'], detail=False,permission_classes=[IsAuthenticated])
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def field_permission(self, request):
|
||||
"""
|
||||
获取字段权限
|
||||
"""
|
||||
finded = False
|
||||
for model in get_custom_app_models():
|
||||
if model['object'] is self.serializer_class.Meta.model:
|
||||
finded = True
|
||||
break
|
||||
if finded:
|
||||
break
|
||||
if finded is False:
|
||||
return []
|
||||
model = self.serializer_class.Meta.model.__name__
|
||||
user = request.user
|
||||
if user.is_superuser==1:
|
||||
data = MenuField.objects.filter( model=model['model']).values('field_name')
|
||||
for item in data:
|
||||
item['is_create'] = True
|
||||
item['is_query'] = True
|
||||
item['is_update'] = True
|
||||
# 创建一个默认字典来存储最终的结果
|
||||
if user.is_superuser == 1:
|
||||
data = MenuField.objects.filter(model=model).values('field_name')
|
||||
result = {item['field_name']: {"is_create": True, "is_query": True, "is_update": True} for item in data}
|
||||
else:
|
||||
roles = request.user.role.values_list('id', flat=True)
|
||||
data= FieldPermission.objects.filter(
|
||||
field__model=model['model'],role__in=roles
|
||||
).values( 'is_create', 'is_query', 'is_update',field_name=F('field__field_name'))
|
||||
return DetailResponse(data=data)
|
||||
data = FieldPermission.objects.filter(
|
||||
field__model=model, role__in=roles
|
||||
).values('is_create', 'is_query', 'is_update', field_name=F('field__field_name'))
|
||||
result = merge_permission(data)
|
||||
return DetailResponse(data=result)
|
||||
|
||||
@@ -37,11 +37,11 @@ class CoreModelFilterBankend(BaseFilterBackend):
|
||||
if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]):
|
||||
create_filter = Q()
|
||||
if create_datetime_after and create_datetime_before:
|
||||
create_filter &= Q(create_datetime__gte=create_datetime_after) & Q(create_datetime__lte=create_datetime_before)
|
||||
create_filter &= Q(create_datetime__gte=create_datetime_after) & Q(create_datetime__lte=f'{create_datetime_before} 23:59:59')
|
||||
elif create_datetime_after:
|
||||
create_filter &= Q(create_datetime__gte=create_datetime_after)
|
||||
elif create_datetime_before:
|
||||
create_filter &= Q(create_datetime__lte=create_datetime_before)
|
||||
create_filter &= Q(create_datetime__lte=f'{create_datetime_before} 23:59:59')
|
||||
|
||||
# 更新时间范围过滤条件
|
||||
update_filter = Q()
|
||||
@@ -340,7 +340,7 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
from timezone_field import TimeZoneField
|
||||
|
||||
# 不进行 过滤的model 类
|
||||
if isinstance(field, (models.JSONField, TimeZoneField)):
|
||||
if isinstance(field, (models.JSONField, TimeZoneField, models.FileField)):
|
||||
continue
|
||||
# warn if the field doesn't exist.
|
||||
if field is None:
|
||||
|
||||
@@ -86,4 +86,5 @@ def import_to_data(file_url, field_data, m2m_fields=None):
|
||||
else:
|
||||
array[key] = cell_value
|
||||
tables.append(array)
|
||||
return tables
|
||||
data = [i for i in tables if len(i) != 0]
|
||||
return data
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.db import transaction
|
||||
@@ -11,8 +12,10 @@ from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
|
||||
from dvadmin.utils.import_export import import_to_data
|
||||
from dvadmin.utils.json_response import DetailResponse
|
||||
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
||||
from dvadmin.utils.request_util import get_verbose_name
|
||||
from dvadmin.system.tasks import async_export_data
|
||||
from dvadmin.system.models import DownloadCenter
|
||||
|
||||
|
||||
class ImportSerializerMixin:
|
||||
@@ -301,6 +304,16 @@ class ExportSerializerMixin:
|
||||
assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__
|
||||
assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__
|
||||
data = self.export_serializer_class(queryset, many=True, request=request).data
|
||||
try:
|
||||
async_export_data.delay(
|
||||
data,
|
||||
str(f"导出{get_verbose_name(queryset)}-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx"),
|
||||
DownloadCenter.objects.create(creator=request.user, task_name=f'{get_verbose_name(queryset)}数据导出任务', dept_belong_id=request.user.dept_id).pk,
|
||||
self.export_field_label
|
||||
)
|
||||
return SuccessResponse(msg="导入任务已创建,请前往‘下载中心’等待下载")
|
||||
except:
|
||||
pass
|
||||
# 导出excel 表
|
||||
response = HttpResponse(content_type="application/msexcel")
|
||||
response["Access-Control-Expose-Headers"] = f"Content-Disposition"
|
||||
|
||||
@@ -32,6 +32,14 @@ class ApiLoggingMiddleware(MiddlewareMixin):
|
||||
request.request_path = get_request_path(request)
|
||||
|
||||
def __handle_response(self, request, response):
|
||||
|
||||
# 判断有无log_id属性,使用All记录时,会出现此情况
|
||||
if request.request_data.get('log_id', None) is None:
|
||||
return
|
||||
|
||||
# 移除log_id,不记录此ID
|
||||
log_id = request.request_data.pop('log_id')
|
||||
|
||||
# request_data,request_ip由PermissionInterfaceMiddleware中间件中添加的属性
|
||||
body = getattr(request, 'request_data', {})
|
||||
# 请求含有password则用*替换掉(暂时先用于所有接口的password请求参数)
|
||||
@@ -60,7 +68,7 @@ class ApiLoggingMiddleware(MiddlewareMixin):
|
||||
'status': True if response.data.get('code') in [2000, ] else False,
|
||||
'json_result': {"code": response.data.get('code'), "msg": response.data.get('msg')},
|
||||
}
|
||||
operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=self.operation_log_id)
|
||||
operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=log_id)
|
||||
if not operation_log.request_modular and settings.API_MODEL_MAP.get(request.request_path, None):
|
||||
operation_log.request_modular = settings.API_MODEL_MAP[request.request_path]
|
||||
operation_log.save()
|
||||
@@ -71,7 +79,8 @@ class ApiLoggingMiddleware(MiddlewareMixin):
|
||||
if self.methods == 'ALL' or request.method in self.methods:
|
||||
log = OperationLog(request_modular=get_verbose_name(view_func.cls.queryset))
|
||||
log.save()
|
||||
self.operation_log_id = log.id
|
||||
# self.operation_log_id = log.id
|
||||
request.request_data['log_id'] = log.id
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
@Created on: 2021/5/31 031 22:08
|
||||
@Remark: 公共基础model类
|
||||
"""
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
from application import settings
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from rest_framework.request import Request
|
||||
|
||||
table_prefix = settings.TABLE_PREFIX # 数据库表名前缀
|
||||
|
||||
@@ -60,8 +61,24 @@ class SoftDeleteModel(models.Model):
|
||||
"""
|
||||
重写删除方法,直接开启软删除
|
||||
"""
|
||||
self.is_deleted = True
|
||||
self.save(using=using)
|
||||
if soft_delete:
|
||||
self.is_deleted = True
|
||||
self.save(using=using)
|
||||
# 级联软删除关联对象
|
||||
for related_object in self._meta.related_objects:
|
||||
related_model = getattr(self, related_object.get_accessor_name())
|
||||
# 处理一对多和多对多的关联对象
|
||||
if related_object.one_to_many or related_object.many_to_many:
|
||||
related_objects = related_model.all()
|
||||
elif related_object.one_to_one:
|
||||
related_objects = [related_model]
|
||||
else:
|
||||
continue
|
||||
|
||||
for obj in related_objects:
|
||||
obj.delete(soft_delete=True)
|
||||
else:
|
||||
super().delete(using=using, *args, **kwargs)
|
||||
|
||||
|
||||
class CoreModel(models.Model):
|
||||
@@ -87,6 +104,111 @@ class CoreModel(models.Model):
|
||||
verbose_name = '核心模型'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def get_request_user(self, request: Request):
|
||||
if getattr(request, "user", None):
|
||||
return request.user
|
||||
return None
|
||||
|
||||
def get_request_user_id(self, request: Request):
|
||||
if getattr(request, "user", None):
|
||||
return getattr(request.user, "id", None)
|
||||
return None
|
||||
|
||||
def get_request_user_name(self, request: Request):
|
||||
if getattr(request, "user", None):
|
||||
return getattr(request.user, "name", None)
|
||||
return None
|
||||
|
||||
def get_request_user_username(self, request: Request):
|
||||
if getattr(request, "user", None):
|
||||
return getattr(request.user, "username", None)
|
||||
return None
|
||||
|
||||
def common_insert_data(self, request: Request):
|
||||
data = {
|
||||
'create_datetime': datetime.now(),
|
||||
'creator': self.get_request_user(request)
|
||||
}
|
||||
return {**data, **self.common_update_data(request)}
|
||||
|
||||
def common_update_data(self, request: Request):
|
||||
return {
|
||||
'update_datetime': datetime.now(),
|
||||
'modifier': self.get_request_user_username(request)
|
||||
}
|
||||
|
||||
exclude_fields = [
|
||||
'_state',
|
||||
'pk',
|
||||
'id',
|
||||
'create_datetime',
|
||||
'update_datetime',
|
||||
'creator',
|
||||
'creator_id',
|
||||
'creator_pk',
|
||||
'creator_name',
|
||||
'modifier',
|
||||
'modifier_id',
|
||||
'modifier_pk',
|
||||
'modifier_name',
|
||||
'dept_belong_id',
|
||||
]
|
||||
|
||||
def get_exclude_fields(self):
|
||||
return self.exclude_fields
|
||||
|
||||
def get_all_fields(self):
|
||||
return self._meta.fields
|
||||
|
||||
def get_all_fields_names(self):
|
||||
return [field.name for field in self.get_all_fields()]
|
||||
|
||||
def get_need_fields_names(self):
|
||||
return [field.name for field in self.get_all_fields() if field.name not in self.exclude_fields]
|
||||
|
||||
def to_data(self):
|
||||
"""将模型转化为字典(去除不包含字段)(注意与to_dict_data区分)。
|
||||
"""
|
||||
res = {}
|
||||
for field in self.get_need_fields_names():
|
||||
field_value = getattr(self, field)
|
||||
res[field] = field_value.id if (issubclass(field_value.__class__, CoreModel)) else field_value
|
||||
return res
|
||||
|
||||
@property
|
||||
def DATA(self):
|
||||
return self.to_data()
|
||||
|
||||
def to_dict_data(self):
|
||||
"""需要导出的字段(去除不包含字段)(注意与to_data区分)
|
||||
"""
|
||||
return {field: getattr(self, field) for field in self.get_need_fields_names()}
|
||||
|
||||
@property
|
||||
def DICT_DATA(self):
|
||||
return self.to_dict_data()
|
||||
|
||||
def insert(self, request):
|
||||
"""插入模型
|
||||
"""
|
||||
assert self.pk is None, f'模型{self.__class__.__name__}还没有保存到数据中,不能手动指定ID'
|
||||
validated_data = {**self.common_insert_data(request), **self.DICT_DATA}
|
||||
return self.__class__._default_manager.create(**validated_data)
|
||||
|
||||
def update(self, request, update_data: dict[str, any] = None):
|
||||
"""更新模型
|
||||
"""
|
||||
assert isinstance(update_data, dict), 'update_data必须为字典'
|
||||
validated_data = {**self.common_insert_data(request), **update_data}
|
||||
for key, value in validated_data.items():
|
||||
# 不允许修改id,pk,uuid字段
|
||||
if key in ['id', 'pk', 'uuid']:
|
||||
continue
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
self.save()
|
||||
return self
|
||||
|
||||
|
||||
def get_all_models_objects(model_name=None):
|
||||
"""
|
||||
@@ -97,16 +219,9 @@ def get_all_models_objects(model_name=None):
|
||||
if not settings.ALL_MODELS_OBJECTS:
|
||||
all_models = apps.get_models()
|
||||
for item in list(all_models):
|
||||
table = {
|
||||
"tableName": item._meta.verbose_name,
|
||||
"table": item.__name__,
|
||||
"tableFields": []
|
||||
}
|
||||
table = {"tableName": item._meta.verbose_name, "table": item.__name__, "tableFields": []}
|
||||
for field in item._meta.fields:
|
||||
fields = {
|
||||
"title": field.verbose_name,
|
||||
"field": field.name
|
||||
}
|
||||
fields = {"title": field.verbose_name, "field": field.name}
|
||||
table['tableFields'].append(fields)
|
||||
settings.ALL_MODELS_OBJECTS.setdefault(item.__name__, {"table": table, "object": item})
|
||||
if model_name:
|
||||
@@ -117,25 +232,20 @@ def get_all_models_objects(model_name=None):
|
||||
def get_model_from_app(app_name):
|
||||
"""获取模型里的字段"""
|
||||
model_module = import_module(app_name + '.models')
|
||||
exclude_models = getattr(model_module, 'exclude_models', [])
|
||||
filter_model = [
|
||||
getattr(model_module, item) for item in dir(model_module)
|
||||
if item != 'CoreModel' and issubclass(getattr(model_module, item).__class__, models.base.ModelBase)
|
||||
value for key, value in model_module.__dict__.items()
|
||||
if key != 'CoreModel'
|
||||
and isinstance(value, type)
|
||||
and issubclass(value, models.Model)
|
||||
and key not in exclude_models
|
||||
]
|
||||
model_list = []
|
||||
for model in filter_model:
|
||||
if model.__name__ == 'AbstractUser':
|
||||
continue
|
||||
fields = [
|
||||
{'title': field.verbose_name, 'name': field.name, 'object': field}
|
||||
for field in model._meta.fields
|
||||
]
|
||||
model_list.append({
|
||||
'app': app_name,
|
||||
'verbose': model._meta.verbose_name,
|
||||
'model': model.__name__,
|
||||
'object': model,
|
||||
'fields': fields
|
||||
})
|
||||
fields = [{'title': field.verbose_name, 'name': field.name, 'object': field} for field in model._meta.fields]
|
||||
model_list.append({'app': app_name, 'verbose': model._meta.verbose_name, 'model': model.__name__, 'object': model, 'fields': fields})
|
||||
return model_list
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,35 @@ class AnonymousUserPermission(BasePermission):
|
||||
return True
|
||||
|
||||
|
||||
class SuperuserPermission(BasePermission):
|
||||
"""
|
||||
超级管理员权限类
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if isinstance(request.user, AnonymousUser):
|
||||
return False
|
||||
# 判断是否是超级管理员
|
||||
if request.user.is_superuser:
|
||||
return True
|
||||
|
||||
|
||||
class AdminPermission(BasePermission):
|
||||
"""
|
||||
普通管理员权限类
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if isinstance(request.user, AnonymousUser):
|
||||
return False
|
||||
# 判断是否是超级管理员
|
||||
is_superuser = request.user.is_superuser
|
||||
# 判断是否是管理员角色
|
||||
is_admin = request.user.role.values_list('admin', flat=True)
|
||||
if is_superuser or True in is_admin:
|
||||
return True
|
||||
|
||||
|
||||
def ReUUID(api):
|
||||
"""
|
||||
将接口的uuid替换掉
|
||||
@@ -81,8 +110,9 @@ class CustomPermission(BasePermission):
|
||||
# ********#
|
||||
if not hasattr(request.user, "role"):
|
||||
return False
|
||||
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')) # 获取当前用户的角色拥有的所有接口
|
||||
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 = [
|
||||
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')]
|
||||
|
||||
@@ -26,7 +26,6 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
|
||||
# 修改人的审计字段名称, 默认modifier, 继承使用时可自定义覆盖
|
||||
modifier_field_id = "modifier"
|
||||
modifier_name = serializers.SerializerMethodField(read_only=True)
|
||||
dept_belong_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
def get_modifier_name(self, instance):
|
||||
if not hasattr(instance, "modifier"):
|
||||
@@ -52,7 +51,7 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
|
||||
format="%Y-%m-%d %H:%M:%S", required=False, read_only=True
|
||||
)
|
||||
update_datetime = serializers.DateTimeField(
|
||||
format="%Y-%m-%d %H:%M:%S", required=False
|
||||
format="%Y-%m-%d %H:%M:%S", required=False, read_only=True
|
||||
)
|
||||
|
||||
def __init__(self, instance=None, data=empty, request=None, **kwargs):
|
||||
@@ -71,11 +70,11 @@ class CustomModelSerializer(DynamicFieldsMixin, ModelSerializer):
|
||||
validated_data[self.creator_field_id] = self.request.user
|
||||
|
||||
if (
|
||||
self.dept_belong_id_field_name in self.fields.fields
|
||||
and validated_data.get(self.dept_belong_id_field_name, None) is None
|
||||
self.dept_belong_id_field_name in self.fields.fields
|
||||
and validated_data.get(self.dept_belong_id_field_name, None) is None
|
||||
):
|
||||
validated_data[self.dept_belong_id_field_name] = getattr(
|
||||
self.request.user, "dept_id", None
|
||||
self.request.user, "dept_id", validated_data.get(self.dept_belong_id_field_name, None)
|
||||
)
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
@@ -9,5 +9,9 @@ from application.settings import LOGGING
|
||||
|
||||
if __name__ == '__main__':
|
||||
multiprocessing.freeze_support()
|
||||
uvicorn.run("application.asgi:application", reload=False, host="0.0.0.0", port=8000, workers=4,
|
||||
workers = 4
|
||||
if os.sys.platform.startswith('win'):
|
||||
# Windows操作系统
|
||||
workers = None
|
||||
uvicorn.run("application.asgi:application", reload=False, host="0.0.0.0", port=8000, workers=workers,
|
||||
log_config=LOGGING)
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
Django==4.2.7
|
||||
Django==4.2.14
|
||||
django-comment-migrate==0.1.7
|
||||
django-cors-headers==4.3.0
|
||||
django-filter==23.3
|
||||
django-cors-headers==4.4.0
|
||||
django-filter==24.2
|
||||
django-ranged-response==0.2.0
|
||||
djangorestframework==3.14.0
|
||||
django-restql==0.15.3
|
||||
django-simple-captcha==0.5.20
|
||||
django-timezone-field==6.0.1
|
||||
djangorestframework-simplejwt==5.3.0
|
||||
djangorestframework==3.15.2
|
||||
django-restql==0.15.4
|
||||
django-simple-captcha==0.6.0
|
||||
django-timezone-field==7.0
|
||||
djangorestframework-simplejwt==5.3.1
|
||||
drf-yasg==1.21.7
|
||||
mysqlclient==2.2.0
|
||||
pypinyin==0.49.0
|
||||
pypinyin==0.51.0
|
||||
ua-parser==0.18.0
|
||||
pyparsing==3.1.1
|
||||
openpyxl==3.1.2
|
||||
requests==2.31.0
|
||||
typing-extensions==4.8.0
|
||||
tzlocal==5.1
|
||||
channels==3.0.5
|
||||
channels-redis==4.1.0
|
||||
pyparsing==3.1.2
|
||||
openpyxl==3.1.5
|
||||
requests==2.32.3
|
||||
typing-extensions==4.12.2
|
||||
tzlocal==5.2
|
||||
channels==4.1.0
|
||||
channels-redis==4.2.0
|
||||
websockets==11.0.3
|
||||
user-agents==2.2.0
|
||||
six==1.16.0
|
||||
whitenoise==6.6.0
|
||||
whitenoise==6.7.0
|
||||
psycopg2==2.9.9
|
||||
uvicorn==0.23.2
|
||||
gunicorn==21.2.0
|
||||
gevent==23.9.1
|
||||
Pillow==10.1.0
|
||||
dvadmin-celery==1.0.5
|
||||
pyinstaller==6.8.0
|
||||
uvicorn==0.30.3
|
||||
gunicorn==22.0.0
|
||||
gevent==24.2.1
|
||||
Pillow==10.4.0
|
||||
pyinstaller==6.9.0
|
||||
BIN
backend/static/logo.icns
Normal file
BIN
backend/static/logo.icns
Normal file
Binary file not shown.
@@ -6,4 +6,4 @@ RUN awk 'BEGIN { cmd="cp -i ./conf/env.example.py ./conf/env.py "; print "n" |
|
||||
RUN sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.1'|g" ./conf/env.py
|
||||
RUN sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.1'|g" ./conf/env.py
|
||||
RUN python3 -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ -r requirements.txt
|
||||
CMD ["/backend/docker_start.sh"]
|
||||
CMD ["sh","docker_start.sh"]
|
||||
|
||||
@@ -7,6 +7,10 @@ server {
|
||||
index index.html index.htm;
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
# 禁止缓存html文件,避免前端页面不及时更新,需要用户手动刷新的情况
|
||||
if ($request_uri ~* "^/$|^/index.html|^/index.htm") {
|
||||
add_header Cache-Control "no-store";
|
||||
}
|
||||
}
|
||||
|
||||
location ~ ^/api/ {
|
||||
|
||||
@@ -11,6 +11,10 @@ server {
|
||||
real_ip_header X-Forwarded-For;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.php index.htm;
|
||||
# 禁止缓存html文件,避免前端页面不及时更新,需要用户手动刷新的情况
|
||||
if ($request_uri ~* "^/$|^/index.html|^/index.htm") {
|
||||
add_header Cache-Control "no-store";
|
||||
}
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
|
||||
2
web/.gitignore
vendored
2
web/.gitignore
vendored
@@ -21,3 +21,5 @@ pnpm-debug.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
# 构建版本文件,无需上传git
|
||||
public/version-build
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "django-vue3-admin",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.4",
|
||||
"description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台,权限粒度达到列级别,前后端分离,后端采用django + django-rest-framework,前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@@ -10,32 +10,32 @@
|
||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.0.10",
|
||||
"@fast-crud/fast-crud": "^1.20.1",
|
||||
"@fast-crud/fast-extends": "^1.20.1",
|
||||
"@fast-crud/ui-element": "^1.20.1",
|
||||
"@fast-crud/ui-interface": "^1.20.1",
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@fast-crud/fast-crud": "^1.21.2",
|
||||
"@fast-crud/fast-extends": "^1.21.2",
|
||||
"@fast-crud/ui-element": "^1.21.2",
|
||||
"@fast-crud/ui-interface": "^1.21.2",
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"axios": "^1.2.1",
|
||||
"countup.js": "^2.3.2",
|
||||
"cropperjs": "^1.5.13",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.7.4",
|
||||
"countup.js": "^2.8.0",
|
||||
"cropperjs": "^1.6.2",
|
||||
"e-icon-picker": "2.1.1",
|
||||
"echarts": "^5.4.1",
|
||||
"echarts": "^5.5.1",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"echarts-wordcloud": "^2.1.0",
|
||||
"element-plus": "^2.5.5",
|
||||
"element-plus": "^2.8.0",
|
||||
"element-tree-line": "^0.2.1",
|
||||
"font-awesome": "^4.7.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"js-table2excel": "^1.0.3",
|
||||
"js-cookie": "^3.0.5",
|
||||
"js-table2excel": "^1.1.2",
|
||||
"jsplumb": "^2.15.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.0",
|
||||
"mitt": "^3.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.28",
|
||||
"pinia-plugin-persist": "^1.0.0",
|
||||
@@ -49,31 +49,31 @@
|
||||
"tailwindcss": "^3.2.7",
|
||||
"ts-md5": "^1.3.1",
|
||||
"upgrade": "^1.1.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue": "^3.4.38",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-cropper": "^1.0.8",
|
||||
"vue-grid-layout": "^3.0.0-beta1",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.6",
|
||||
"vxe-table": "^4.4.1",
|
||||
"xe-utils": "^3.5.7"
|
||||
"vue-i18n": "^9.14.0",
|
||||
"vue-router": "^4.4.3",
|
||||
"vxe-table": "^4.6.18",
|
||||
"xe-utils": "^3.5.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.13",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.15.0",
|
||||
"@types/node": "^18.19.42",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.0",
|
||||
"@typescript-eslint/parser": "^5.46.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue/compiler-sfc": "^3.2.45",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-plugin-vue": "^9.8.0",
|
||||
"@vitejs/plugin-vue": "^5.1.2",
|
||||
"@vue/compiler-sfc": "^3.4.38",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"prettier": "^2.8.1",
|
||||
"sass": "^1.56.2",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.0.0",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vue-eslint-parser": "^9.1.0"
|
||||
"vue-eslint-parser": "^9.4.3"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
BIN
web/src/assets/login-bg.png
Normal file
BIN
web/src/assets/login-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 550 KiB |
84
web/src/components/fileSelector/fileItem.vue
Normal file
84
web/src/components/fileSelector/fileItem.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div ref="itemRef" class="file-item" :title="data.name" @mouseenter="isShow = true" @mouseleave="isShow = false">
|
||||
<div v-if="showTitle" class="file-name" :class="{ show: isShow }">{{ data.name }}</div>
|
||||
<component :is="FileTypes[data.file_type].tag" v-bind="FileTypes[data.file_type].attr" />
|
||||
<div v-if="props.showClose" class="file-del" :class="{ show: isShow }">
|
||||
<el-icon :size="24" color="white" @click.stop="delFileHandle" style="cursor: pointer;">
|
||||
<CircleClose style="mix-blend-mode: difference;" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { ref, defineProps, PropType, watch, onMounted, h } from 'vue';
|
||||
import { successNotification } from '/@/utils/message';
|
||||
import { getBaseURL } from '/@/utils/baseUrl';
|
||||
const props = defineProps({
|
||||
fileData: { type: Object as PropType<any>, required: true },
|
||||
api: { type: Object as PropType<any>, required: true },
|
||||
showTitle: { type: Boolean, default: true },
|
||||
showClose: { type: Boolean, default: true },
|
||||
});
|
||||
const _OtherFileComponent = defineComponent({ template: '<el-icon><Files /></el-icon>' });
|
||||
const FileTypes = [
|
||||
{ tag: 'img', attr: { src: getBaseURL(props.fileData.url), draggable: false } },
|
||||
{ tag: 'video', attr: { src: getBaseURL(props.fileData.url), controls: false, autoplay: true, muted: true, loop: true } },
|
||||
{ tag: 'audio', attr: { src: getBaseURL(props.fileData.url), controls: true, autoplay: false, muted: false, loop: false, volume: 0 } },
|
||||
{ tag: _OtherFileComponent, attr: { style: { fontSize: '2rem' } } },
|
||||
];
|
||||
const isShow = ref<boolean>(false);
|
||||
const itemRef = ref<HTMLDivElement>();
|
||||
const data = ref<any>(null);
|
||||
const delFileHandle = () => props.api.DelObj(props.fileData.id).then(() => {
|
||||
successNotification('删除成功');
|
||||
emit('onDelFile');
|
||||
});
|
||||
watch(props.fileData, (nVal) => data.value = nVal, { immediate: true, deep: true });
|
||||
const emit = defineEmits(['onDelFile']);
|
||||
defineExpose({});
|
||||
onMounted(() => { });
|
||||
</script>
|
||||
<style scoped>
|
||||
.file-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.file-item>* {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 4px 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.file-del {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.show {
|
||||
display: flex !important;
|
||||
}
|
||||
</style>
|
||||
478
web/src/components/fileSelector/index.vue
Normal file
478
web/src/components/fileSelector/index.vue
Normal file
@@ -0,0 +1,478 @@
|
||||
<template>
|
||||
<div style="width: 100%;" :class="props.class" :style="props.style">
|
||||
<slot name="input" v-bind="{}">
|
||||
<div v-if="props.showInput" style="width: 100%;" :class="props.inputClass" :style="props.inputStyle">
|
||||
<el-select v-if="props.inputType === 'selector'" v-model="data" suffix-icon="arrow-down" clearable
|
||||
:multiple="props.multiple" placeholder="请选择文件" @click="selectVisiable = true && !props.disabled"
|
||||
:disabled="props.disabled" @clear="selectedInit" @remove-tag="selectedInit">
|
||||
<el-option v-for="item, index in listAllData" :key="index" :value="String(item[props.valueKey])"
|
||||
:label="item.name" />
|
||||
</el-select>
|
||||
<div v-if="props.inputType === 'image'" style="position: relative;" class="form-display"
|
||||
@mouseenter="formDisplayEnter" @mouseleave="formDisplayLeave"
|
||||
:style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
|
||||
<el-image :src="data" fit="scale-down" :style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }">
|
||||
<template #error>
|
||||
<div></div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-show="!(!!data)"
|
||||
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
|
||||
<el-icon :size="24">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
|
||||
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
|
||||
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div v-if="props.inputType === 'video'" class="form-display" @mouseenter="formDisplayEnter"
|
||||
@mouseleave="formDisplayLeave"
|
||||
style="position: relative; display: flex; align-items: center; justify-items: center;"
|
||||
:style="{ width: props.inputSize * 2 + 'px', height: props.inputSize + 'px' }">
|
||||
<video :src="data" :controls="false" :autoplay="true" :muted="true" :loop="true"
|
||||
:style="{ maxWidth: props.inputSize * 2 + 'px', maxHeight: props.inputSize + 'px', margin: '0 auto' }"></video>
|
||||
<div v-show="!(!!data)"
|
||||
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
|
||||
<el-icon :size="24">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
|
||||
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
|
||||
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div v-if="props.inputType === 'audio'" class="form-display" @mouseenter="formDisplayEnter"
|
||||
@mouseleave="formDisplayLeave"
|
||||
style="position: relative; display: flex; align-items: center; justify-items: center;"
|
||||
:style="{ width: props.inputSize * 2 + 'px', height: props.inputSize + 'px' }">
|
||||
<audio :src="data" :controls="!!data" :autoplay="false" :muted="true" :loop="true"
|
||||
style="width: 100%; z-index: 1;"></audio>
|
||||
<div v-show="!(!!data)"
|
||||
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
|
||||
<el-icon :size="24">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
|
||||
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
|
||||
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
<el-dialog v-model="selectVisiable" :draggable="true" width="50%" :align-center="false" :append-to-body="true"
|
||||
@open="if (listData.length === 0) listRequest();" @close="onClose" @closed="onClosed" modal-class="_overlay">
|
||||
<template #header>
|
||||
<span class="el-dialog__title">文件选择</span>
|
||||
<el-divider style="margin: 0;" />
|
||||
</template>
|
||||
<div style="padding: 4px;">
|
||||
<div style="width: 100%; display: flex; justify-content: space-between; gap: 12px;">
|
||||
<el-tabs style="width: 100%;" v-model="tabsActived" :type="props.tabsType" :stretch="true"
|
||||
@tab-change="handleTabChange" v-if="!isSuperTenent">
|
||||
<el-tab-pane v-if="props.tabsShow & SHOW.IMAGE" :name="0" label="图片" />
|
||||
<el-tab-pane v-if="props.tabsShow & SHOW.VIDEO" :name="1" label="视频" />
|
||||
<el-tab-pane v-if="props.tabsShow & SHOW.AUDIO" :name="2" label="音频" />
|
||||
<el-tab-pane v-if="props.tabsShow & SHOW.OTHER" :name="3" label="其他" />
|
||||
</el-tabs>
|
||||
<el-tabs style="width: 100%;" v-model="tabsActived" :type="props.tabsType" :stretch="true"
|
||||
@tab-change="handleTabChange" v-if="isTenentMode">
|
||||
<el-tab-pane v-if="props.tabsShow & SHOW.IMAGE" :name="4" label="系统图片" />
|
||||
<el-tab-pane v-if="props.tabsShow & SHOW.VIDEO" :name="5" label="系统视频" />
|
||||
<el-tab-pane v-if="props.tabsShow & SHOW.AUDIO" :name="6" label="系统音频" />
|
||||
<el-tab-pane v-if="props.tabsShow & SHOW.OTHER" :name="7" label="系统其他" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-row justify="space-between" class="headerBar">
|
||||
<el-col :span="12">
|
||||
<slot name="actionbar-left">
|
||||
<el-input v-model="filterForm.name" :placeholder="`请输入${TypeLabel[tabsActived % 4]}名`"
|
||||
prefix-icon="search" clearable @change="listRequest" />
|
||||
<div>
|
||||
<el-tag v-if="props.multiple" type="primary" effect="light">
|
||||
一共选中 {{ data?.length || 0 }} 个文件
|
||||
</el-tag>
|
||||
</div>
|
||||
</slot>
|
||||
</el-col>
|
||||
<el-col :span="12" style="width: 100%; display: flex; gap: 12px; justify-content: flex-end;">
|
||||
<slot name="actionbar-right" v-bind="{}">
|
||||
<el-button type="default" circle icon="refresh" @click="listRequest" />
|
||||
<template v-if="tabsActived > 3 ? isSuperTenent : true">
|
||||
<el-upload ref="uploadRef" :action="getBaseURL() + 'api/system/file/'" :multiple="false" :drag="false"
|
||||
:data="{ upload_method: 1 }" :show-file-list="true" :accept="AcceptList[tabsActived % 4]"
|
||||
:on-success="() => { listRequest(); listRequestAll(); uploadRef.clearFiles(); }"
|
||||
v-if="props.showUploadButton">
|
||||
<el-button type="primary" icon="plus">上传{{ TypeLabel[tabsActived % 4] }}</el-button>
|
||||
</el-upload>
|
||||
<el-button type="info" icon="link" @click="netVisiable = true" v-if="props.showNetButton">
|
||||
网络{{ TypeLabel[tabsActived % 4] }}
|
||||
</el-button>
|
||||
</template>
|
||||
</slot>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div v-if="!listData.length">
|
||||
<slot name="empty">
|
||||
<el-empty description="无内容,请上传" style="width: 100%; height: calc(50vh); margin-top: 24px; padding: 4px;" />
|
||||
</slot>
|
||||
</div>
|
||||
<div ref="listContainerRef" class="listContainer" v-else>
|
||||
<div v-for="item, index in listData" :key="index" @click="onItemClick($event)" :data-id="item[props.valueKey]"
|
||||
:style="{ width: (props.itemSize || 100) + 'px', cursor: props.selectable ? 'pointer' : 'normal' }">
|
||||
<slot name="item" :data="item">
|
||||
<FileItem :fileData="item" :api="fileApi" :showClose="tabsActived < 4 || isSuperTenent"
|
||||
@onDelFile="listRequest(); listRequestAll();" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="listPaginator">
|
||||
<el-pagination background size="small" layout="total, sizes, prev, pager, next" :total="pageForm.total"
|
||||
v-model:page-size="pageForm.limit" :page-sizes="[10, 20, 30, 40, 50]" v-model:current-page="pageForm.page"
|
||||
:hide-on-single-page="false" @change="handlePageChange" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 只要在获取中,就最大程度阻止关闭dialog -->
|
||||
<el-dialog v-model="netVisiable" :draggable="false" width="50%" :align-center="false" :append-to-body="true"
|
||||
:title="'网络' + TypeLabel[tabsActived % 4] + '上传'" @closed="netUrl = ''" :close-on-click-modal="!netLoading"
|
||||
:close-on-press-escape="!netLoading" :show-close="!netLoading" modal-class="_overlay">
|
||||
<el-form-item :label="TypeLabel[tabsActived % 4] + '链接'">
|
||||
<el-input v-model="netUrl" placeholder="请输入网络连接" clearable @input="netChange">
|
||||
<template #prepend>
|
||||
<el-select v-model="netPrefix" style="width: 110px;">
|
||||
<el-option v-for="item, index in ['HTTP://', 'HTTPS://']" :key="index" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<template #footer>
|
||||
<el-button v-if="!netLoading" type="default" @click="netVisiable = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmNetUrl" :loading="netLoading">
|
||||
{{ netLoading ? '网络文件获取中...' : '确定' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<template #footer v-if="props.showInput">
|
||||
<el-button type="default" @click="onClose">取消</el-button>
|
||||
<el-button type="primary" @click="onSave">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUi, UserPageQuery, AddReq, EditReq, DelReq } from '@fast-crud/fast-crud';
|
||||
import { ref, reactive, defineProps, PropType, watch, onMounted, nextTick } from 'vue';
|
||||
import { getBaseURL } from '/@/utils/baseUrl';
|
||||
import { request } from '/@/utils/service';
|
||||
import { SHOW } from './types';
|
||||
import FileItem from './fileItem.vue';
|
||||
import { pluginsAll } from '/@/views/plugins/index';
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useUserInfo } from "/@/stores/userInfo";
|
||||
import { errorNotification, successNotification } from '/@/utils/message';
|
||||
|
||||
const userInfos = storeToRefs(useUserInfo()).userInfos;
|
||||
const isTenentMode = !!(pluginsAll && pluginsAll.length && pluginsAll.indexOf('dvadmin3-tenants-web') >= 0);
|
||||
const isSuperTenent = (userInfos.value as any).schema_name === 'public';
|
||||
|
||||
const TypeLabel = ['图片', '视频', '音频', '文件']
|
||||
const AcceptList = ['image/*', 'video/*', 'audio/*', ''];
|
||||
const props = defineProps({
|
||||
modelValue: {},
|
||||
class: { type: Object as PropType<String | Object>, default: '' },
|
||||
inputClass: { type: Object as PropType<String | Object>, default: '' },
|
||||
style: { type: Object as PropType<Object | string>, default: {} },
|
||||
inputStyle: { type: Object as PropType<Object | string>, default: {} },
|
||||
disabled: { type: Boolean, default: false },
|
||||
|
||||
tabsType: { type: Object as PropType<'' | 'card' | 'border-card'>, default: '' },
|
||||
itemSize: { type: Number, default: 100 },
|
||||
|
||||
// 1000图片 100视频 10音频 1 其他 控制tabs的显示
|
||||
tabsShow: { type: Number, default: SHOW.ALL },
|
||||
|
||||
// 是否可以多选,默认单选
|
||||
// 该值为true时inputType必须是selector(暂不支持其他type的多选)
|
||||
multiple: { type: Boolean, default: false },
|
||||
|
||||
// 是否可选,该参数用于只上传和展示而不选择和绑定model的情况
|
||||
selectable: { type: Boolean, default: true },
|
||||
|
||||
// 该参数用于控制是否显示表单item。若赋值为false,则不会显示表单item,也不会显示底部按钮
|
||||
// 如果不显示表单item,则无法触发dialog,需要父组件通过修改本组件暴露的 selectVisiable 状态来控制dialog
|
||||
showInput: { type: Boolean, default: true },
|
||||
|
||||
// 表单item类型,不为selector是需要设置valueKey,否则可能获取不到媒体数据
|
||||
inputType: { type: Object as PropType<'selector' | 'image' | 'video' | 'audio'>, default: 'selector' },
|
||||
// inputType不为selector时生效
|
||||
inputSize: { type: Number, default: 100 },
|
||||
|
||||
// v-model绑定的值是file数据的哪个key,默认是url
|
||||
valueKey: { type: String, default: 'url' },
|
||||
|
||||
showUploadButton: { type: Boolean, default: true },
|
||||
showNetButton: { type: Boolean, default: true },
|
||||
} as any);
|
||||
|
||||
const selectVisiable = ref<boolean>(false);
|
||||
const tabsActived = ref<number>([3, 2, 1, 0][((props.tabsShow & (props.tabsShow - 1)) === 0) ? Math.log2(props.tabsShow) : 3]);
|
||||
const fileApiPrefix = '/api/system/file/';
|
||||
const fileApi = {
|
||||
GetList: (query: UserPageQuery) => request({ url: fileApiPrefix, method: 'get', params: query }),
|
||||
AddObj: (obj: AddReq) => request({ url: fileApiPrefix, method: 'post', data: obj }),
|
||||
DelObj: (id: DelReq) => request({ url: fileApiPrefix + id + '/', method: 'delete', data: { id } }),
|
||||
GetAll: () => request({ url: fileApiPrefix + 'get_all/' }),
|
||||
};
|
||||
// 过滤表单
|
||||
const filterForm = reactive({ name: '' });
|
||||
// 分页表单
|
||||
const pageForm = reactive({ page: 1, limit: 10, total: 0 });
|
||||
// 展示的数据列表
|
||||
const listData = ref<any[]>([]);
|
||||
const listAllData = ref<any[]>([]);
|
||||
const listRequest = async () => {
|
||||
let res = await fileApi.GetList({
|
||||
page: pageForm.page,
|
||||
limit: pageForm.limit,
|
||||
file_type: isTenentMode ? tabsActived.value % 4 : tabsActived.value,
|
||||
system: tabsActived.value > 3,
|
||||
upload_method: 1,
|
||||
...filterForm
|
||||
});
|
||||
listData.value = [];
|
||||
await nextTick();
|
||||
listData.value = (res.data as any[]).map((item: any) => ({ ...item, url: getBaseURL(item.url) }));
|
||||
pageForm.total = res.total;
|
||||
pageForm.page = res.page;
|
||||
pageForm.limit = res.limit;
|
||||
selectedInit();
|
||||
};
|
||||
const formDisplayEnter = (e: MouseEvent) => (e.target as HTMLElement).style.setProperty('--fileselector-close-display', 'block');
|
||||
const formDisplayLeave = (e: MouseEvent) => (e.target as HTMLElement).style.setProperty('--fileselector-close-display', 'none');
|
||||
const listRequestAll = async () => {
|
||||
if (props.inputType !== 'selector') return;
|
||||
let res = await fileApi.GetAll();
|
||||
listAllData.value = res.data;
|
||||
};
|
||||
// tab改变时触发
|
||||
const handleTabChange = (name: string) => { pageForm.page = 1; listRequest(); };
|
||||
// 分页器改变时触发
|
||||
const handlePageChange = (currentPage: number, pageSize: number) => { pageForm.page = currentPage; pageForm.limit = pageSize; listRequest(); };
|
||||
// 选择的行为
|
||||
const listContainerRef = ref<any>();
|
||||
const onItemClick = async (e: MouseEvent) => {
|
||||
if (!props.selectable) return;
|
||||
let target = e.target as HTMLElement;
|
||||
let flat = 0; // -1删除 0不变 1添加
|
||||
while (!target.dataset.id) target = target.parentElement as HTMLElement;
|
||||
let fileId = target.dataset.id;
|
||||
if (props.multiple) {
|
||||
if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; }
|
||||
else { target.classList.add('active'); flat = 1; }
|
||||
if (data.value.length) {
|
||||
let _l = JSON.parse(JSON.stringify(data.value));
|
||||
if (flat === 1) _l.push(fileId);
|
||||
else _l.splice(_l.indexOf(fileId), 1);
|
||||
data.value = _l;
|
||||
} else data.value = [fileId];
|
||||
// 去重排序,<降序,>升序
|
||||
data.value = Array.from(new Set(data.value)).sort();
|
||||
} else {
|
||||
for (let i of listContainerRef.value?.children) (i as HTMLElement).classList.remove('active');
|
||||
target.classList.add('active');
|
||||
data.value = fileId;
|
||||
}
|
||||
// onDataChange(data.value);
|
||||
};
|
||||
// 每次列表刷新都得更新一下选择状态,因为所有标签页共享列表
|
||||
const selectedInit = async () => {
|
||||
if (!props.selectable) return;
|
||||
await nextTick(); // 不等待一次不会刷新
|
||||
for (let i of (listContainerRef.value?.children || [])) {
|
||||
i.classList.remove('active');
|
||||
let fid = (i as HTMLElement).dataset.id;
|
||||
if (props.multiple) { if (data.value?.includes(fid)) i.classList.add('active'); }
|
||||
else { if (fid === data.value) i.classList.add('active'); }
|
||||
}
|
||||
};
|
||||
const uploadRef = ref<any>();
|
||||
const onSave = () => {
|
||||
onDataChange(data.value);
|
||||
emit('onSave', data.value);
|
||||
selectVisiable.value = false;
|
||||
};
|
||||
const onClose = () => {
|
||||
data.value = props.modelValue;
|
||||
emit('onClose');
|
||||
selectVisiable.value = false;
|
||||
};
|
||||
const onClosed = () => {
|
||||
clearState();
|
||||
emit('onClosed');
|
||||
};
|
||||
// 清空状态
|
||||
const clearState = () => {
|
||||
filterForm.name = '';
|
||||
pageForm.page = 1;
|
||||
pageForm.limit = 10;
|
||||
pageForm.total = 0;
|
||||
listData.value = [];
|
||||
// all数据不能清,因为all只会在挂载的时候赋值一次
|
||||
// listAllData.value = [];
|
||||
};
|
||||
const clear = () => { data.value = null; onDataChange(null); }
|
||||
|
||||
|
||||
// 网络文件部分
|
||||
const netLoading = ref<boolean>(false);
|
||||
const netVisiable = ref<boolean>(false);
|
||||
const netUrl = ref<string>('');
|
||||
const netPrefix = ref<string>('HTTP://');
|
||||
const netChange = () => {
|
||||
let s = netUrl.value.trim();
|
||||
if (s.toUpperCase().startsWith('HTTP://') || s.toUpperCase().startsWith('HTTPS://')) s = s.split('://')[1];
|
||||
if (s.startsWith('/')) s = s.substring(1);
|
||||
netUrl.value = s;
|
||||
};
|
||||
const confirmNetUrl = () => {
|
||||
if (!netUrl.value) return;
|
||||
netLoading.value = true;
|
||||
let controller = new AbortController();
|
||||
let timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 10 * 1000);
|
||||
fetch(netPrefix.value + netUrl.value, { signal: controller.signal }).then(async (res: Response) => {
|
||||
clearTimeout(timeout);
|
||||
if (!res.ok) errorNotification(`网络${TypeLabel[tabsActived.value % 4]}获取失败!`);
|
||||
const _ = res.url.split('?')[0].split('/');
|
||||
let filename = _[_.length - 1];
|
||||
// let filetype = res.headers.get('content-type')?.split('/')[1] || '';
|
||||
let blob = await res.blob();
|
||||
let file = new File([blob], filename, { type: blob.type });
|
||||
let form = new FormData();
|
||||
form.append('file', file);
|
||||
form.append('upload_method', '1');
|
||||
fetch(getBaseURL() + 'api/system/file/', { method: 'post', body: form })
|
||||
.then(() => successNotification('网络文件上传成功!'))
|
||||
.then(() => { netVisiable.value = false; listRequest(); listRequestAll(); })
|
||||
.catch(() => errorNotification('网络文件上传失败!'))
|
||||
.then(() => netLoading.value = false);
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
clearTimeout(timeout);
|
||||
errorNotification(`网络${TypeLabel[tabsActived.value % 4]}获取失败!`);
|
||||
netLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// fs-crud部分
|
||||
const data = ref<any>(null);
|
||||
const emit = defineEmits(['update:modelValue', 'onSave', 'onClose', 'onClosed']);
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => data.value = props.multiple ? JSON.parse(JSON.stringify(val)) : val,
|
||||
{ immediate: true }
|
||||
);
|
||||
const { ui } = useUi();
|
||||
const formValidator = ui.formItem.injectFormItemContext();
|
||||
const onDataChange = (value: any) => {
|
||||
emit('update:modelValue', value);
|
||||
formValidator.onChange();
|
||||
formValidator.onBlur();
|
||||
};
|
||||
|
||||
defineExpose({ data, onDataChange, selectVisiable, clearState, clear });
|
||||
|
||||
onMounted(() => {
|
||||
if (props.multiple && props.inputType !== 'selector')
|
||||
throw new Error('FileSelector组件属性multiple为true时inputType必须为selector');
|
||||
listRequestAll();
|
||||
console.log('fileselector tenentmdoe', isTenentMode);
|
||||
console.log('fileselector supertenent', isSuperTenent);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-display {
|
||||
--fileselector-close-display: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
._overlay {
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
.headerBar>* {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__prepend) {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.listContainer {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-auto-rows: min-content;
|
||||
grid-gap: 36px;
|
||||
margin-top: 24px;
|
||||
padding: 8px;
|
||||
height: calc(50vh);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.listContainer>* {
|
||||
aspect-ratio: 1 / 1;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, .2);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.active {
|
||||
box-shadow: 0 0 8px var(--el-color-primary);
|
||||
}
|
||||
|
||||
.listPaginator {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-items: center;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.addControllorHover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.addControllorHover:hover {
|
||||
border-color: #c0c4cc;
|
||||
}
|
||||
|
||||
.closeHover {
|
||||
display: var(--fileselector-close-display);
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
7
web/src/components/fileSelector/types.ts
Normal file
7
web/src/components/fileSelector/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const SHOW = {
|
||||
IMAGE: 0b1000, // 图片
|
||||
VIDEO: 0b0100, // 视频
|
||||
AUDIO: 0b0010, // 音频
|
||||
OTHER: 0b0001, // 其他
|
||||
ALL: 0b1111, // 全部
|
||||
};
|
||||
@@ -1,203 +1,211 @@
|
||||
<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>
|
||||
<el-select
|
||||
popper-class="popperClass"
|
||||
class="tableSelector"
|
||||
multiple
|
||||
@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"
|
||||
:lazy="props.tableConfig.lazy"
|
||||
:load="props.tableConfig.load"
|
||||
:tree-props="props.tableConfig.treeProps"
|
||||
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'
|
||||
import {request} from '/@/utils/service'
|
||||
import { defineProps, reactive, ref, watch } from 'vue';
|
||||
import XEUtils from 'xe-utils';
|
||||
import { request } from '/@/utils/service';
|
||||
|
||||
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'])
|
||||
modelValue: {},
|
||||
tableConfig: {
|
||||
url: null,
|
||||
label: null, //显示值
|
||||
value: null, //数据值
|
||||
isTree: false,
|
||||
lazy: true,
|
||||
load: () => {},
|
||||
data: [], //默认数据
|
||||
isMultiple: false, //是否多选
|
||||
treeProps: { children: 'children', hasChildren: 'hasChildren' },
|
||||
columns: [], //每一项对应的列表项
|
||||
},
|
||||
displayLabel: {},
|
||||
} as any);
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
// tableRef
|
||||
const tableRef = ref()
|
||||
const tableRef = ref();
|
||||
// template上使用data
|
||||
const data = ref()
|
||||
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 multipleSelection = ref();
|
||||
// 搜索值
|
||||
const search = ref(undefined)
|
||||
const search = ref(undefined);
|
||||
//表格数据
|
||||
const tableData = ref()
|
||||
const tableData = ref();
|
||||
// 分页的配置
|
||||
const pageConfig = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
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)
|
||||
}
|
||||
multipleSelection.value = val;
|
||||
const { tableConfig } = props;
|
||||
const result = val.map((item: any) => {
|
||||
return item[tableConfig.value];
|
||||
});
|
||||
data.value = val.map((item: any) => {
|
||||
return item[tableConfig.label];
|
||||
});
|
||||
|
||||
emit('update:modelValue', result);
|
||||
};
|
||||
/**
|
||||
* 表格单选
|
||||
* @param val:Object
|
||||
*/
|
||||
const handleCurrentChange = (val: any) => {
|
||||
multipleSelection.value = val
|
||||
const {tableConfig} = props
|
||||
emit('update:modelValue', val[tableConfig.value])
|
||||
}
|
||||
const { tableConfig } = props;
|
||||
if (!tableConfig.isMultiple && val) {
|
||||
data.value = [val[tableConfig.label]];
|
||||
emit('update:modelValue', val[tableConfig.value]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取字典值
|
||||
*/
|
||||
const getDict = async () => {
|
||||
const url = props.tableConfig.url
|
||||
const params = {
|
||||
page: pageConfig.page,
|
||||
limit: pageConfig.limit,
|
||||
search: search.value
|
||||
}
|
||||
const {data, page, limit, total} = await request({
|
||||
url:url,
|
||||
params:params
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
const url = props.tableConfig.url;
|
||||
const params = {
|
||||
page: pageConfig.page,
|
||||
limit: pageConfig.limit,
|
||||
search: search.value,
|
||||
};
|
||||
const { data, page, limit, total } = await request({
|
||||
url: url,
|
||||
params: params,
|
||||
});
|
||||
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()
|
||||
}
|
||||
}
|
||||
if (bool) {
|
||||
getDict();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 分页
|
||||
* @param page
|
||||
*/
|
||||
const handlePageChange = (page: any) => {
|
||||
pageConfig.page = page
|
||||
getDict()
|
||||
}
|
||||
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})
|
||||
|
||||
|
||||
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;
|
||||
height: auto;
|
||||
line-height: 1;
|
||||
padding: 5px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.popperClass {
|
||||
height: 320px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.el-select-dropdown__wrap {
|
||||
max-height: 310px !important;
|
||||
max-height: 310px !important;
|
||||
}
|
||||
|
||||
.tableSelector {
|
||||
.el-icon, .el-tag__close {
|
||||
display: none;
|
||||
}
|
||||
.el-icon,
|
||||
.el-tag__close {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ export default {
|
||||
label: {
|
||||
one1: 'User name login',
|
||||
two2: 'Mobile number',
|
||||
changePwd: 'Change The Password',
|
||||
},
|
||||
link: {
|
||||
one3: 'Third party login',
|
||||
|
||||
@@ -3,15 +3,18 @@ export default {
|
||||
label: {
|
||||
one1: '账号密码登录',
|
||||
two2: '手机号登录',
|
||||
changePwd: '密码修改',
|
||||
},
|
||||
link: {
|
||||
one3: '第三方登录',
|
||||
two4: '友情链接',
|
||||
},
|
||||
account: {
|
||||
accountPlaceholder1: '请输入登录账号',
|
||||
accountPlaceholder1: '请输入登录账号/邮箱/手机号',
|
||||
accountPlaceholder2: '请输入登录密码',
|
||||
accountPlaceholder3: '请输入验证码',
|
||||
accountPlaceholder4:'请输入新密码',
|
||||
accountPlaceholder5:'请再次输入新密码',
|
||||
accountBtnText: '登 录',
|
||||
},
|
||||
mobile: {
|
||||
|
||||
@@ -3,6 +3,7 @@ export default {
|
||||
label: {
|
||||
one1: '用戶名登入',
|
||||
two2: '手機號登入',
|
||||
changePwd: '密码修改',
|
||||
},
|
||||
link: {
|
||||
one3: '協力廠商登入',
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function initBackEndControlRoutes() {
|
||||
if (!Session.get('token')) return false;
|
||||
// 触发初始化用户信息 pinia
|
||||
// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
|
||||
await useUserInfo().setUserInfos();
|
||||
await useUserInfo().getApiUserInfo();
|
||||
// 获取路由菜单数据
|
||||
const res = await getBackEndControlRoutes();
|
||||
// 无登录权限时,添加判断
|
||||
|
||||
@@ -13,6 +13,7 @@ import {initBackEndControlRoutes, setRouters} from '/@/router/backEnd';
|
||||
import {useFrontendMenuStore} from "/@/stores/frontendMenu";
|
||||
import {useTagsViewRoutes} from "/@/stores/tagsViewRoutes";
|
||||
import {toRaw} from "vue";
|
||||
import {checkVersion} from "/@/utils/upgrade";
|
||||
|
||||
/**
|
||||
* 1、前端控制路由时:isRequestRoutes 为 false,需要写 roles,需要走 setFilterRoute 方法。
|
||||
@@ -27,6 +28,8 @@ import {toRaw} from "vue";
|
||||
const storesThemeConfig = useThemeConfig(pinia);
|
||||
const {themeConfig} = storeToRefs(storesThemeConfig);
|
||||
const {isRequestRoutes} = themeConfig.value;
|
||||
import {useUserInfo} from "/@/stores/userInfo";
|
||||
const { userInfos } = storeToRefs(useUserInfo());
|
||||
|
||||
/**
|
||||
* 创建一个可以被 Vue 应用程序使用的路由实例
|
||||
@@ -93,8 +96,12 @@ export function formatTwoStageRoutes(arr: any) {
|
||||
return newArr;
|
||||
}
|
||||
|
||||
const frameOutRoutes = staticRoutes.map(item => item.path)
|
||||
|
||||
// 路由加载前
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新
|
||||
await checkVersion()
|
||||
NProgress.configure({showSpinner: false});
|
||||
if (to.meta.title) NProgress.start();
|
||||
const token = Session.get('token');
|
||||
@@ -106,11 +113,15 @@ router.beforeEach(async (to, from, next) => {
|
||||
next(`/login?redirect=${to.path}¶ms=${JSON.stringify(to.query ? to.query : to.params)}`);
|
||||
Session.clear();
|
||||
NProgress.done();
|
||||
} else if (token && to.path === '/login') {
|
||||
}else if (token && to.path === '/login' && userInfos.value.pwd_change_count===0 ) {
|
||||
next('/login');
|
||||
NProgress.done();
|
||||
} else if (token && to.path === '/login' && userInfos.value.pwd_change_count>0) {
|
||||
next('/home');
|
||||
NProgress.done();
|
||||
}else if(token && frameOutRoutes.includes(to.path) ){
|
||||
next()
|
||||
} else {
|
||||
|
||||
const storesRoutesList = useRoutesList(pinia);
|
||||
const {routesList} = storeToRefs(storesRoutesList);
|
||||
if (routesList.value.length === 0) {
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface UserInfosState {
|
||||
email: string;
|
||||
mobile: string;
|
||||
gender: string;
|
||||
pwd_change_count:null|number;
|
||||
dept_info: {
|
||||
dept_id: number;
|
||||
dept_name: string;
|
||||
|
||||
@@ -2,6 +2,9 @@ import { defineStore } from 'pinia';
|
||||
import { UserInfosStates } from './interface';
|
||||
import { Session } from '/@/utils/storage';
|
||||
import { request } from '../utils/service';
|
||||
import { getBaseURL } from '../utils/baseUrl';
|
||||
import headerImage from '/@/assets/img/headerImage.png';
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
* @methods setUserInfos 设置用户信息
|
||||
@@ -15,6 +18,7 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
email: '',
|
||||
mobile: '',
|
||||
gender: '',
|
||||
pwd_change_count:null,
|
||||
dept_info: {
|
||||
dept_id: 0,
|
||||
dept_name: '',
|
||||
@@ -29,16 +33,19 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
isSocketOpen: false
|
||||
}),
|
||||
actions: {
|
||||
async updateUserInfos() {
|
||||
let userInfos: any = await this.getApiUserInfo();
|
||||
this.userInfos.username = userInfos.data.name;
|
||||
this.userInfos.avatar = userInfos.data.avatar;
|
||||
this.userInfos.name = userInfos.data.name;
|
||||
this.userInfos.email = userInfos.data.email;
|
||||
this.userInfos.mobile = userInfos.data.mobile;
|
||||
this.userInfos.gender = userInfos.data.gender;
|
||||
this.userInfos.dept_info = userInfos.data.dept_info;
|
||||
this.userInfos.role_info = userInfos.data.role_info;
|
||||
async setPwdChangeCount(count: number) {
|
||||
this.userInfos.pwd_change_count = count;
|
||||
},
|
||||
async updateUserInfos(userInfos:any) {
|
||||
this.userInfos.username = userInfos.name;
|
||||
this.userInfos.avatar = userInfos.avatar;
|
||||
this.userInfos.name = userInfos.name;
|
||||
this.userInfos.email = userInfos.email;
|
||||
this.userInfos.mobile = userInfos.mobile;
|
||||
this.userInfos.gender = userInfos.gender;
|
||||
this.userInfos.dept_info = userInfos.dept_info;
|
||||
this.userInfos.role_info = userInfos.role_info;
|
||||
this.userInfos.pwd_change_count = userInfos.pwd_change_count;
|
||||
Session.set('userInfo', this.userInfos);
|
||||
},
|
||||
async setUserInfos() {
|
||||
@@ -55,6 +62,7 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
this.userInfos.gender = userInfos.data.gender;
|
||||
this.userInfos.dept_info = userInfos.data.dept_info;
|
||||
this.userInfos.role_info = userInfos.data.role_info;
|
||||
this.userInfos.pwd_change_count = userInfos.data.pwd_change_count;
|
||||
Session.set('userInfo', this.userInfos);
|
||||
}
|
||||
},
|
||||
@@ -65,7 +73,18 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
return request({
|
||||
url: '/api/system/user/user_info/',
|
||||
method: 'get',
|
||||
});
|
||||
}).then((res:any)=>{
|
||||
this.userInfos.username = res.data.name;
|
||||
this.userInfos.avatar = (res.data.avatar && getBaseURL(res.data.avatar)) || headerImage;
|
||||
this.userInfos.name = res.data.name;
|
||||
this.userInfos.email = res.data.email;
|
||||
this.userInfos.mobile = res.data.mobile;
|
||||
this.userInfos.gender = res.data.gender;
|
||||
this.userInfos.dept_info = res.data.dept_info;
|
||||
this.userInfos.role_info = res.data.role_info;
|
||||
this.userInfos.pwd_change_count = res.data.pwd_change_count;
|
||||
Session.set('userInfo', this.userInfos);
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import XEUtils from 'xe-utils';
|
||||
import {useColumnPermission} from '/@/stores/columnPermission';
|
||||
|
||||
type permissionType = 'is_create' | 'is_query' | 'is_update';
|
||||
@@ -22,41 +23,18 @@ export const handleColumnPermission = async (func: Function, crudOptions: any,ex
|
||||
}
|
||||
}
|
||||
const columns = crudOptions.columns;
|
||||
const excludeColumns = ['_index','id', 'create_datetime', 'update_datetime'].concat(excludeColumn)
|
||||
for (let col in columns) {
|
||||
if (excludeColumns.includes(col)) {
|
||||
continue
|
||||
}else{
|
||||
if (columns[col].column) {
|
||||
columns[col].column.show = false
|
||||
} else {
|
||||
columns[col]['column'] = {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
columns[col].addForm = {
|
||||
show: false
|
||||
}
|
||||
columns[col].editForm = {
|
||||
show: false
|
||||
const excludeColumns = ['checked','_index','id', 'create_datetime', 'update_datetime'].concat(excludeColumn)
|
||||
XEUtils.eachTree(columns, (item, key) => {
|
||||
if (!excludeColumns.includes(String(key)) && key in res.data) {
|
||||
// 如果列表不可见,则禁止在列设置中选择
|
||||
// 只有列表不可见,才修改列配置,这样才不影响默认的配置
|
||||
if (!res.data[key]['is_query']) {
|
||||
item.column.show = false;
|
||||
item.column.columnSetDisabled = true;
|
||||
}
|
||||
item.addForm = { show: res.data[key]['is_create'] };
|
||||
item.editForm = { show: res.data[key]['is_update'] };
|
||||
}
|
||||
|
||||
for (let item of res.data) {
|
||||
if (excludeColumns.includes(item.field_name)) {
|
||||
continue
|
||||
} else if(item.field_name === col) {
|
||||
columns[col].column.show = item['is_query']
|
||||
// 如果列表不可见,则禁止在列设置中选择
|
||||
if(!item['is_query'])columns[col].column.columnSetDisabled = true
|
||||
columns[col].addForm = {
|
||||
show: item['is_create']
|
||||
}
|
||||
columns[col].editForm = {
|
||||
show: item['is_update']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return crudOptions
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DictionaryStore } from '/@/stores/dictionary';
|
||||
/**
|
||||
* @method 获取指定name字典
|
||||
*/
|
||||
export const dictionary = (name: string,key:string|number|undefined) => {
|
||||
export const dictionary = (name: string,key?:string|number|undefined) => {
|
||||
const dict = DictionaryStore()
|
||||
const dictionary = toRaw(dict.data)
|
||||
if(key!=undefined){
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { nextTick } from 'vue';
|
||||
import '/@/theme/loading.scss';
|
||||
import { showUpgrade } from "/@/utils/upgrade";
|
||||
|
||||
|
||||
/**
|
||||
* 页面全局 Loading
|
||||
@@ -9,6 +11,8 @@ import '/@/theme/loading.scss';
|
||||
export const NextLoading = {
|
||||
// 创建 loading
|
||||
start: () => {
|
||||
// 显示升级提示
|
||||
showUpgrade()
|
||||
const bodys: Element = document.body;
|
||||
const div = <HTMLElement>document.createElement('div');
|
||||
div.setAttribute('class', 'loading-next');
|
||||
|
||||
@@ -10,6 +10,7 @@ import { errorLog, errorCreate } from './tools.ts';
|
||||
import { Local, Session } from '/@/utils/storage';
|
||||
import qs from 'qs';
|
||||
import { getBaseURL } from './baseUrl';
|
||||
import { successMessage } from './message.js';
|
||||
/**
|
||||
* @description 创建请求实例
|
||||
*/
|
||||
@@ -82,7 +83,7 @@ function createService() {
|
||||
ElMessageBox.alert(dataAxios.msg, '提示', {
|
||||
confirmButtonText: 'OK',
|
||||
callback: (action: Action) => {
|
||||
window.location.reload();
|
||||
// window.location.reload();
|
||||
},
|
||||
});
|
||||
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
|
||||
@@ -204,6 +205,8 @@ export const requestForMock = createRequestFunction(serviceForMock);
|
||||
* @param filename
|
||||
*/
|
||||
export const downloadFile = function ({ url, params, method, filename = '文件导出' }: any) {
|
||||
// return request({ url: url, method: method, params: params })
|
||||
// .then((res: any) => successMessage(res.msg));
|
||||
request({
|
||||
url: url,
|
||||
method: method,
|
||||
@@ -211,6 +214,9 @@ export const downloadFile = function ({ url, params, method, filename = '文件
|
||||
responseType: 'blob'
|
||||
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
|
||||
}).then((res: any) => {
|
||||
// console.log(res.headers['content-type']); // 根据content-type不同来判断是否异步下载
|
||||
// if (res.headers && res.headers['Content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载');
|
||||
if (res.headers['content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载');
|
||||
const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1])
|
||||
const fileName = xlsxName || `${filename}.xlsx`
|
||||
if (res) {
|
||||
|
||||
55
web/src/utils/upgrade.ts
Normal file
55
web/src/utils/upgrade.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import axios from "axios";
|
||||
import * as process from "process";
|
||||
import {Local, Session} from '/@/utils/storage';
|
||||
import {ElNotification} from "element-plus";
|
||||
import fs from "fs";
|
||||
|
||||
// 是否显示升级提示信息框
|
||||
const IS_SHOW_UPGRADE_SESSION_KEY = 'isShowUpgrade';
|
||||
const VERSION_KEY = 'DVADMIN3_VERSION'
|
||||
const VERSION_FILE_NAME = 'version-build'
|
||||
|
||||
export function showUpgrade () {
|
||||
const isShowUpgrade = Session.get(IS_SHOW_UPGRADE_SESSION_KEY) ?? false
|
||||
if (isShowUpgrade) {
|
||||
Session.remove(IS_SHOW_UPGRADE_SESSION_KEY)
|
||||
ElNotification({
|
||||
title: '新版本升级',
|
||||
message: "检测到系统新版本,正在更新中!不用担心,更新很快的哦!",
|
||||
type: 'success',
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 生产环境前端版本校验,
|
||||
export async function checkVersion(){
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 开发环境无需校验前端版本
|
||||
return
|
||||
}
|
||||
// 获取线上版本号 t为时间戳,防止缓存
|
||||
await axios.get(`${import.meta.env.VITE_PUBLIC_PATH}${VERSION_FILE_NAME}?t=${new Date().getTime()}`).then(res => {
|
||||
const {status, data} = res || {}
|
||||
if (status === 200) {
|
||||
// 获取当前版本号
|
||||
const localVersion = Local.get(VERSION_KEY)
|
||||
// 将当前版本号持久缓存至本地
|
||||
Local.set(VERSION_KEY, data)
|
||||
// 当用户本地存在版本号并且和线上版本号不一致时,进行页面刷新操作
|
||||
if (localVersion && localVersion !== data) {
|
||||
// 本地缓存版本号和线上版本号不一致,弹出升级提示框
|
||||
// 此处无法直接使用消息框进行提醒,因为 window.location.reload()会导致消息框消失,将在loading页面判断是否需要显示升级提示框
|
||||
Session.set(IS_SHOW_UPGRADE_SESSION_KEY, true)
|
||||
window.location.reload()
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function generateVersionFile (){
|
||||
// 生成版本文件到public目录下version文件中
|
||||
const version = `${process.env.npm_package_version}.${new Date().getTime()}`;
|
||||
fs.writeFileSync(`public/${VERSION_FILE_NAME}`, version);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import VFormRender from '@/components/form-render/index.vue'
|
||||
import ContainerItems from '@/components/form-render/container-item/index'
|
||||
|
||||
import {registerIcon} from '@/utils/el-icons'
|
||||
import 'virtual:svg-icons-register'
|
||||
import '@/iconfont/iconfont.css'
|
||||
|
||||
import { installI18n } from '@/utils/i18n'
|
||||
import { loadExtension } from '@/extension/extension-loader'
|
||||
|
||||
VFormRender.install = function (app) {
|
||||
installI18n(app)
|
||||
loadExtension(app)
|
||||
|
||||
app.use(ContainerItems)
|
||||
registerIcon(app)
|
||||
app.component(VFormRender.name, VFormRender)
|
||||
}
|
||||
|
||||
const components = [
|
||||
VFormRender
|
||||
]
|
||||
|
||||
const install = (app) => {
|
||||
installI18n(app)
|
||||
loadExtension(app)
|
||||
|
||||
app.use(ContainerItems)
|
||||
registerIcon(app)
|
||||
components.forEach(component => {
|
||||
app.component(component.name, component)
|
||||
})
|
||||
|
||||
window.axios = axios
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.Vue) { /* script<EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>ֵaxios<EFBFBD><EFBFBD><EFBFBD><EFBFBD> */
|
||||
//window.axios = axios
|
||||
}
|
||||
|
||||
export default {
|
||||
install,
|
||||
VFormRender
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import VFormDesigner from '@/components/form-designer/index.vue'
|
||||
import VFormRender from '@/components/form-render/index.vue'
|
||||
|
||||
import Draggable from '@/../lib/vuedraggable/dist/vuedraggable.umd.js'
|
||||
import {registerIcon} from '@/utils/el-icons'
|
||||
import 'virtual:svg-icons-register'
|
||||
import '@/iconfont/iconfont.css'
|
||||
|
||||
import ContainerWidgets from '@/components/form-designer/form-widget/container-widget/index'
|
||||
import ContainerItems from '@/components/form-render/container-item/index'
|
||||
|
||||
import { addDirective } from '@/utils/directive'
|
||||
import { installI18n } from '@/utils/i18n'
|
||||
import { loadExtension } from '@/extension/extension-loader'
|
||||
|
||||
|
||||
VFormDesigner.install = function (app) {
|
||||
addDirective(app)
|
||||
installI18n(app)
|
||||
loadExtension(app)
|
||||
|
||||
app.use(ContainerWidgets)
|
||||
app.use(ContainerItems)
|
||||
|
||||
registerIcon(app)
|
||||
app.component('draggable', Draggable)
|
||||
app.component(VFormDesigner.name, VFormDesigner)
|
||||
}
|
||||
|
||||
VFormRender.install = function (app) {
|
||||
installI18n(app)
|
||||
loadExtension(app)
|
||||
|
||||
app.use(ContainerItems)
|
||||
|
||||
registerIcon(app)
|
||||
app.component(VFormRender.name, VFormRender)
|
||||
}
|
||||
|
||||
const components = [
|
||||
VFormDesigner,
|
||||
VFormRender
|
||||
]
|
||||
|
||||
const install = (app) => {
|
||||
addDirective(app)
|
||||
installI18n(app)
|
||||
loadExtension(app)
|
||||
|
||||
app.use(ContainerWidgets)
|
||||
app.use(ContainerItems)
|
||||
|
||||
registerIcon(app)
|
||||
app.component('draggable', Draggable)
|
||||
|
||||
components.forEach(component => {
|
||||
app.component(component.name, component)
|
||||
})
|
||||
|
||||
window.axios = axios
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.Vue) { /* script<EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>ֵaxios<EFBFBD><EFBFBD><EFBFBD><EFBFBD> */
|
||||
//window.axios = axios
|
||||
}
|
||||
|
||||
export default {
|
||||
install,
|
||||
VFormDesigner,
|
||||
VFormRender
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
import DVAFormDesigner from './components/DVAFormDesigner.vue'
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>浽һ<E6B5BD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
const components = [
|
||||
DVAFormDesigner
|
||||
]
|
||||
|
||||
// <20><><EFBFBD><EFBFBD> install <20><><EFBFBD><EFBFBD>
|
||||
const install = function (Vue) {
|
||||
|
||||
if (install.installed) return
|
||||
install.installed = true
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD><D0B1><EFBFBD>ע<EFBFBD><D7A2>ȫ<EFBFBD><C8AB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
components.map(component => {
|
||||
Vue.component(component.name, component) //component.name <20>˴<EFBFBD>ʹ<EFBFBD>õ<EFBFBD><C3B5><EFBFBD><EFBFBD><EFBFBD>vue<75>ļ<EFBFBD><C4BC>е<EFBFBD> name <20><><EFBFBD><EFBFBD>
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.Vue) {
|
||||
install(window.Vue)
|
||||
}
|
||||
|
||||
export default {
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD>Ķ<EFBFBD><C4B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߱<EFBFBD>һ<EFBFBD><D2BB> install <20><><EFBFBD><EFBFBD>
|
||||
install,
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD>
|
||||
...components
|
||||
}
|
||||
@@ -39,3 +39,9 @@ export function DelObj(id: DelReq) {
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
export function GetPermission() {
|
||||
return request({
|
||||
url: apiPrefix + 'field_permission/',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,244 +1,202 @@
|
||||
import * as api from './api';
|
||||
import {
|
||||
dict,
|
||||
UserPageQuery,
|
||||
AddReq,
|
||||
DelReq,
|
||||
EditReq,
|
||||
compute,
|
||||
CreateCrudOptionsProps,
|
||||
CreateCrudOptionsRet
|
||||
} from '@fast-crud/fast-crud';
|
||||
import {dictionary} from '/@/utils/dictionary';
|
||||
import {successMessage} from '/@/utils/message';
|
||||
import {auth} from "/@/utils/authFunction";
|
||||
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
|
||||
import { dictionary } from '/@/utils/dictionary';
|
||||
import { successMessage } from '/@/utils/message';
|
||||
import { auth } from '/@/utils/authFunction';
|
||||
import tableSelector from '/@/components/tableSelector/index.vue';
|
||||
import { shallowRef } from 'vue';
|
||||
|
||||
export const createCrudOptions = function ({crudExpose}: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery) => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({form, row}: EditReq) => {
|
||||
form.id = row.id;
|
||||
return await api.UpdateObj(form);
|
||||
};
|
||||
const delRequest = async ({row}: DelReq) => {
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
const addRequest = async ({form}: AddReq) => {
|
||||
return await api.AddObj(form);
|
||||
};
|
||||
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery) => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }: EditReq) => {
|
||||
form.id = row.id;
|
||||
return await api.UpdateObj(form);
|
||||
};
|
||||
const delRequest = async ({ row }: DelReq) => {
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
const addRequest = async ({ form }: AddReq) => {
|
||||
return await api.AddObj(form);
|
||||
};
|
||||
|
||||
/**
|
||||
* 懒加载
|
||||
* @param row
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
const loadContentMethod = (tree: any, treeNode: any, resolve: Function) => {
|
||||
pageRequest({pcode: tree.code}).then((res: APIResponseData) => {
|
||||
resolve(res.data);
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 懒加载
|
||||
* @param row
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
const loadContentMethod = (tree: any, treeNode: any, resolve: Function) => {
|
||||
pageRequest({ pcode: tree.code }).then((res: APIResponseData) => {
|
||||
resolve(res.data);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
actionbar: {
|
||||
buttons: {
|
||||
add: {
|
||||
show: auth('area:Create'),
|
||||
}
|
||||
}
|
||||
},
|
||||
rowHandle: {
|
||||
//固定右侧
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
buttons: {
|
||||
view: {
|
||||
show: false,
|
||||
},
|
||||
edit: {
|
||||
iconRight: 'Edit',
|
||||
type: 'text',
|
||||
show: auth('area:Update')
|
||||
},
|
||||
remove: {
|
||||
iconRight: 'Delete',
|
||||
type: 'text',
|
||||
show: auth('area:Delete')
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
show: false,
|
||||
},
|
||||
table: {
|
||||
rowKey: 'id',
|
||||
lazy: true,
|
||||
load: loadContentMethod,
|
||||
treeProps: {children: 'children', hasChildren: 'hasChild'},
|
||||
},
|
||||
columns: {
|
||||
_index: {
|
||||
title: '序号',
|
||||
form: {show: false},
|
||||
column: {
|
||||
type: 'index',
|
||||
align: 'center',
|
||||
width: '70px',
|
||||
columnSetDisabled: true, //禁止在列设置中选择
|
||||
},
|
||||
},
|
||||
// pcode: {
|
||||
// title: '父级地区',
|
||||
// show: false,
|
||||
// search: {
|
||||
// show: true,
|
||||
// },
|
||||
// type: 'dict-tree',
|
||||
// form: {
|
||||
// component: {
|
||||
// showAllLevels: false, // 仅显示最后一级
|
||||
// props: {
|
||||
// elProps: {
|
||||
// clearable: true,
|
||||
// showAllLevels: false, // 仅显示最后一级
|
||||
// props: {
|
||||
// checkStrictly: true, // 可以不需要选到最后一级
|
||||
// emitPath: false,
|
||||
// clearable: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
name: {
|
||||
title: '名称',
|
||||
search: {
|
||||
show: true,
|
||||
},
|
||||
treeNode: true,
|
||||
type: 'input',
|
||||
column: {
|
||||
minWidth: 120,
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{required: true, message: '名称必填项'},
|
||||
],
|
||||
component: {
|
||||
placeholder: '请输入名称',
|
||||
},
|
||||
},
|
||||
},
|
||||
code: {
|
||||
title: '地区编码',
|
||||
search: {
|
||||
show: true,
|
||||
},
|
||||
type: 'input',
|
||||
column: {
|
||||
minWidth: 90,
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{required: true, message: '地区编码必填项'},
|
||||
],
|
||||
component: {
|
||||
placeholder: '请输入地区编码',
|
||||
},
|
||||
},
|
||||
},
|
||||
pinyin: {
|
||||
title: '拼音',
|
||||
search: {
|
||||
disabled: true,
|
||||
},
|
||||
type: 'input',
|
||||
column: {
|
||||
minWidth: 120,
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{required: true, message: '拼音必填项'},
|
||||
],
|
||||
component: {
|
||||
placeholder: '请输入拼音',
|
||||
},
|
||||
},
|
||||
},
|
||||
level: {
|
||||
title: '地区层级',
|
||||
search: {
|
||||
disabled: true,
|
||||
},
|
||||
type: 'input',
|
||||
column: {
|
||||
minWidth: 100,
|
||||
},
|
||||
form: {
|
||||
disabled: false,
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{required: true, message: '拼音必填项'},
|
||||
],
|
||||
component: {
|
||||
placeholder: '请输入拼音',
|
||||
},
|
||||
},
|
||||
},
|
||||
initials: {
|
||||
title: '首字母',
|
||||
column: {
|
||||
minWidth: 100,
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{required: true, message: '首字母必填项'},
|
||||
],
|
||||
|
||||
component: {
|
||||
placeholder: '请输入首字母',
|
||||
},
|
||||
},
|
||||
},
|
||||
enable: {
|
||||
title: '是否启用',
|
||||
search: {
|
||||
show: true,
|
||||
},
|
||||
type: 'dict-radio',
|
||||
column: {
|
||||
minWidth: 90,
|
||||
component: {
|
||||
name: 'fs-dict-switch',
|
||||
activeText: '',
|
||||
inactiveText: '',
|
||||
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
|
||||
onChange: compute((context) => {
|
||||
return () => {
|
||||
api.UpdateObj(context.row).then((res: APIResponseData) => {
|
||||
successMessage(res.msg as string);
|
||||
});
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
dict: dict({
|
||||
data: dictionary('button_status_bool'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
actionbar: {
|
||||
buttons: {
|
||||
add: {
|
||||
show: auth('area:Create'),
|
||||
},
|
||||
},
|
||||
},
|
||||
rowHandle: {
|
||||
//固定右侧
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
buttons: {
|
||||
view: {
|
||||
show: false,
|
||||
},
|
||||
edit: {
|
||||
iconRight: 'Edit',
|
||||
type: 'text',
|
||||
show: auth('area:Update'),
|
||||
},
|
||||
remove: {
|
||||
iconRight: 'Delete',
|
||||
type: 'text',
|
||||
show: auth('area:Delete'),
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
show: false,
|
||||
},
|
||||
table: {
|
||||
rowKey: 'id',
|
||||
lazy: true,
|
||||
load: loadContentMethod,
|
||||
treeProps: { children: 'children', hasChildren: 'hasChild' },
|
||||
},
|
||||
columns: {
|
||||
_index: {
|
||||
title: '序号',
|
||||
form: { show: false },
|
||||
column: {
|
||||
type: 'index',
|
||||
align: 'center',
|
||||
width: '70px',
|
||||
columnSetDisabled: true, //禁止在列设置中选择
|
||||
},
|
||||
},
|
||||
name: {
|
||||
title: '名称',
|
||||
search: {
|
||||
show: true,
|
||||
},
|
||||
treeNode: true,
|
||||
type: 'input',
|
||||
column: {
|
||||
minWidth: 120,
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{ required: true, message: '名称必填项' },
|
||||
],
|
||||
component: {
|
||||
placeholder: '请输入名称',
|
||||
},
|
||||
},
|
||||
},
|
||||
pcode: {
|
||||
title: '父级地区',
|
||||
search: {
|
||||
disabled: true,
|
||||
},
|
||||
width: 130,
|
||||
type: 'table-selector',
|
||||
form: {
|
||||
component: {
|
||||
name: shallowRef(tableSelector),
|
||||
vModel: 'modelValue',
|
||||
displayLabel: compute(({ row }) => {
|
||||
if (row) {
|
||||
return row.pcode_info;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
tableConfig: {
|
||||
url: '/api/system/area/',
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
isTree: true,
|
||||
isMultiple: false,
|
||||
lazy: true,
|
||||
load: loadContentMethod,
|
||||
treeProps: { children: 'children', hasChildren: 'hasChild' },
|
||||
columns: [
|
||||
{
|
||||
prop: 'name',
|
||||
label: '地区',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
prop: 'code',
|
||||
label: '地区编码',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
column: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
code: {
|
||||
title: '地区编码',
|
||||
search: {
|
||||
show: true,
|
||||
},
|
||||
type: 'input',
|
||||
column: {
|
||||
minWidth: 90,
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{ required: true, message: '地区编码必填项' },
|
||||
],
|
||||
component: {
|
||||
placeholder: '请输入地区编码',
|
||||
},
|
||||
},
|
||||
},
|
||||
enable: {
|
||||
title: '是否启用',
|
||||
search: {
|
||||
show: true,
|
||||
},
|
||||
type: 'dict-radio',
|
||||
column: {
|
||||
minWidth: 90,
|
||||
component: {
|
||||
name: 'fs-dict-switch',
|
||||
activeText: '',
|
||||
inactiveText: '',
|
||||
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
|
||||
onChange: compute((context) => {
|
||||
return () => {
|
||||
api.UpdateObj(context.row).then((res: APIResponseData) => {
|
||||
successMessage(res.msg as string);
|
||||
});
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
dict: dict({
|
||||
data: dictionary('button_status_bool'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,14 +5,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="areas">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import { createCrudOptions } from './crud';
|
||||
import { GetPermission } from './api';
|
||||
import { handleColumnPermission } from '/@/utils/columnPermission';
|
||||
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||
const { crudBinding, crudRef, crudExpose, crudOptions, resetCrudOptions } = useFs({ createCrudOptions });
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 设置列权限
|
||||
const newOptions = await handleColumnPermission(GetPermission, crudOptions);
|
||||
//重置crudBinding
|
||||
resetCrudOptions(newOptions);
|
||||
// 刷新
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -5,13 +5,13 @@ type GetListType = PageQuery & { show_all: string };
|
||||
|
||||
export const apiPrefix = '/api/system/user/';
|
||||
|
||||
export function GetDept(query: PageQuery) {
|
||||
return request({
|
||||
url: '/api/system/dept/dept_lazy_tree/',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
// export function GetDept(query: PageQuery) {
|
||||
// return request({
|
||||
// url: '/api/system/dept/dept_all/',
|
||||
// method: 'get',
|
||||
// params: query,
|
||||
// });
|
||||
// }
|
||||
|
||||
export function GetList(query: GetListType) {
|
||||
return request({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { request } from '/@/utils/service';
|
||||
import * as api from './api';
|
||||
import { dictionary } from '/@/utils/dictionary';
|
||||
import { successMessage } from '/@/utils/message';
|
||||
import {auth} from "/@/utils/authFunction";
|
||||
import { auth } from "/@/utils/authFunction";
|
||||
|
||||
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery) => {
|
||||
@@ -100,10 +100,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
placement: 'top',
|
||||
content: '重设密码',
|
||||
},
|
||||
click: (ctx: any) => {
|
||||
const { row } = ctx;
|
||||
context?.handleResetPwdOpen(row);
|
||||
},
|
||||
click: (ctx: any) => context?.handleResetPwdOpen(ctx.row),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -185,10 +182,10 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
// value: vm.systemConfig('base.default_password'),
|
||||
},
|
||||
/* valueResolve(row, key) {
|
||||
if (row.password) {
|
||||
row.password = vm.$md5(row.password)
|
||||
}
|
||||
} */
|
||||
if (row.password) {
|
||||
row.password = vm.$md5(row.password)
|
||||
}
|
||||
} */
|
||||
},
|
||||
name: {
|
||||
title: '姓名',
|
||||
@@ -220,7 +217,10 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
label: 'name',
|
||||
}),
|
||||
column: {
|
||||
minWidth: 150, //最小列宽
|
||||
minWidth: 200, //最小列宽
|
||||
formatter({ value, row, index }) {
|
||||
return row.dept_name_all
|
||||
}
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
@@ -259,7 +259,11 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
label: 'name',
|
||||
}),
|
||||
column: {
|
||||
minWidth: 100, //最小列宽
|
||||
minWidth: 200, //最小列宽
|
||||
// formatter({ value, row, index }) {
|
||||
// const values = row.role_info.map((item: any) => item.name);
|
||||
// return values.join(',')
|
||||
// }
|
||||
},
|
||||
form: {
|
||||
rules: [
|
||||
@@ -378,10 +382,14 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
},
|
||||
avatar: {
|
||||
title: '头像',
|
||||
type: 'avatar-cropper',
|
||||
type: 'avatar-uploader',
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 100,
|
||||
showOverflowTooltip: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,6 +42,15 @@
|
||||
<template #actionbar-right>
|
||||
<importExcel api="api/system/user/" v-auth="'user:Import'">导入 </importExcel>
|
||||
</template>
|
||||
<template #cell_avatar="scope">
|
||||
<div v-if="scope.row.avatar" style="display: flex; justify-content: center; align-items: center;">
|
||||
<el-image
|
||||
style="width: 50px; height: 50px; border-radius: 50%; aspect-ratio: 1 /1 ; "
|
||||
:src="getBaseURL(scope.row.avatar)"
|
||||
:preview-src-list="[getBaseURL(scope.row.avatar)]"
|
||||
:preview-teleported="true" />
|
||||
</div>
|
||||
</template>
|
||||
</fs-crud>
|
||||
|
||||
<el-dialog v-model="resetPwdVisible" title="重设密码" width="400px" draggable :before-close="handleResetPwdClose">
|
||||
@@ -69,6 +78,7 @@ import { ECharts, EChartsOption, init } from 'echarts';
|
||||
import { getDeptInfoById, resetPwd } from './api';
|
||||
import { warningNotification, successNotification } from '/@/utils/message';
|
||||
import { HeadDeptInfoType } from '../../types';
|
||||
import {getBaseURL} from '/@/utils/baseUrl';
|
||||
|
||||
let deptCountChart: ECharts;
|
||||
let deptSexChart: ECharts;
|
||||
@@ -277,7 +287,8 @@ const { resetCrudOptions } = useCrud({
|
||||
padding: 0 10px;
|
||||
border-radius: 8px 0 0 8px;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
color: var(--next-bg-topBarColor);
|
||||
background-color: var(--el-fill-color-blank);;
|
||||
}
|
||||
.dept-user-com-table {
|
||||
height: calc(100% - 200px);
|
||||
|
||||
@@ -133,7 +133,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.dept-left {
|
||||
background-color: #fff;
|
||||
background-color: var(--el-fill-color-blank);;
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@@ -1,309 +1,319 @@
|
||||
import * as api from './api';
|
||||
import {
|
||||
dict,
|
||||
UserPageQuery,
|
||||
AddReq,
|
||||
DelReq,
|
||||
EditReq,
|
||||
CrudOptions,
|
||||
CreateCrudOptionsProps,
|
||||
CreateCrudOptionsRet
|
||||
dict,
|
||||
UserPageQuery,
|
||||
AddReq,
|
||||
DelReq,
|
||||
EditReq,
|
||||
CrudOptions,
|
||||
CreateCrudOptionsProps,
|
||||
CreateCrudOptionsRet
|
||||
} from '@fast-crud/fast-crud';
|
||||
import {dictionary} from '/@/utils/dictionary';
|
||||
import { dictionary } from '/@/utils/dictionary';
|
||||
|
||||
export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery) => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({form, row}: EditReq) => {
|
||||
form.id = row.id;
|
||||
return await api.UpdateObj(form);
|
||||
};
|
||||
const delRequest = async ({row}: DelReq) => {
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
const addRequest = async ({form}: AddReq) => {
|
||||
const data = crudExpose!.getSearchFormData()
|
||||
const parent = data.parent
|
||||
form.parent = parent
|
||||
if (parent) {
|
||||
return await api.AddObj(form);
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery) => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }: EditReq) => {
|
||||
form.id = row.id;
|
||||
return await api.UpdateObj(form);
|
||||
};
|
||||
const delRequest = async ({ row }: DelReq) => {
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
const addRequest = async ({ form }: AddReq) => {
|
||||
const data = crudExpose!.getSearchFormData()
|
||||
const parent = data.parent
|
||||
form.parent = parent
|
||||
if (parent) {
|
||||
return await api.AddObj(form);
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
rowHandle: {
|
||||
//固定右侧
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
buttons: {
|
||||
view: {
|
||||
show: false,
|
||||
},
|
||||
edit: {
|
||||
iconRight: 'Edit',
|
||||
type: 'text',
|
||||
},
|
||||
remove: {
|
||||
iconRight: 'Delete',
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
_index: {
|
||||
title: '序号',
|
||||
form: {show: false},
|
||||
column: {
|
||||
//type: 'index',
|
||||
align: 'center',
|
||||
width: '70px',
|
||||
columnSetDisabled: true, //禁止在列设置中选择
|
||||
formatter: (context) => {
|
||||
//计算序号,你可以自定义计算规则,此处为翻页累加
|
||||
let index = context.index ?? 1;
|
||||
let pagination = crudExpose!.crudBinding.value.pagination;
|
||||
// @ts-ignore
|
||||
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
label: {
|
||||
title: '名称',
|
||||
search: {
|
||||
show: true,
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'input',
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{required: true, message: '名称必填项'},
|
||||
],
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
placeholder: '请输入名称',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: {
|
||||
title: '数据值类型',
|
||||
type: 'dict-select',
|
||||
search: {
|
||||
disabled: true,
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
show: false,
|
||||
dict: dict({
|
||||
data: [
|
||||
{label: 'text', value: 0},
|
||||
{label: 'number', value: 1},
|
||||
{label: 'date', value: 2},
|
||||
{label: 'datetime', value: 3},
|
||||
{label: 'time', value: 4},
|
||||
{label: 'file', value: 5},
|
||||
{label: 'boolean', value: 6},
|
||||
{label: 'images', value: 7},
|
||||
],
|
||||
}),
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{required: true, message: '数据值类型必填项'},
|
||||
],
|
||||
value: 0,
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
placeholder: '请选择数据值类型',
|
||||
},
|
||||
/* valueChange(key, value, form, { getColumn, mode, component, immediate, getComponent }) {
|
||||
const template = vm.getEditFormTemplate('value')
|
||||
// 选择框重新选择后,情况value值
|
||||
if (!immediate) {
|
||||
form.value = undefined
|
||||
}
|
||||
if (value === 0) {
|
||||
template.component.name = 'el-input'
|
||||
} else if (value === 1) {
|
||||
template.component.name = 'el-input-number'
|
||||
} else if (value === 2) {
|
||||
template.component.name = 'el-date-picker'
|
||||
template.component.props = {
|
||||
type: 'date',
|
||||
valueFormat: 'yyyy-MM-dd'
|
||||
}
|
||||
} else if (value === 3) {
|
||||
template.component.name = 'el-date-picker'
|
||||
template.component.props = {
|
||||
type: 'datetime',
|
||||
valueFormat: 'yyyy-MM-dd HH:mm:ss'
|
||||
}
|
||||
} else if (value === 4) {
|
||||
template.component.name = 'el-time-picker'
|
||||
template.component.props = {
|
||||
pickerOptions: {
|
||||
arrowControl: true
|
||||
},
|
||||
valueFormat: 'HH:mm:ss'
|
||||
}
|
||||
} else if (value === 5) {
|
||||
template.component.name = 'd2p-file-uploader'
|
||||
template.component.props = { elProps: { listType: 'text' } }
|
||||
} else if (value === 6) {
|
||||
template.component.name = 'dict-switch'
|
||||
template.component.value = true
|
||||
template.component.props = {
|
||||
dict: {
|
||||
data: [
|
||||
{ label: '是', value: 'true' },
|
||||
{ label: '否', value: 'false' }
|
||||
]
|
||||
}
|
||||
}
|
||||
} else if (value === 7) {
|
||||
template.component.name = 'd2p-cropper-uploader'
|
||||
template.component.props = { accept: '.png,.jpeg,.jpg,.ico,.bmp,.gif', cropper: { viewMode: 1 } }
|
||||
}
|
||||
}, */
|
||||
},
|
||||
},
|
||||
value: {
|
||||
title: '数据值',
|
||||
search: {
|
||||
show: true,
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
view: {
|
||||
component: {props: {height: 100, width: 100}},
|
||||
},
|
||||
/* // 提交时,处理数据
|
||||
valueResolve(row: any, col: any) {
|
||||
const value = row[col.key]
|
||||
const type = row.type
|
||||
if (type === 5 || type === 7) {
|
||||
if (value != null) {
|
||||
if (value.length >= 0) {
|
||||
if (value instanceof Array) {
|
||||
row[col.key] = value.toString()
|
||||
} else {
|
||||
row[col.key] = value
|
||||
}
|
||||
} else {
|
||||
row[col.key] = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
row[col.key] = value
|
||||
}
|
||||
},
|
||||
// 接收时,处理数据
|
||||
valueBuilder(row: any, col: any) {
|
||||
const value = row[col.key]
|
||||
const type = row.type
|
||||
if (type === 5 || type === 7) {
|
||||
if (value != null && value) {
|
||||
row[col.key] = value.split(',')
|
||||
}
|
||||
} else {
|
||||
row[col.key] = value
|
||||
}
|
||||
}, */
|
||||
type: 'input',
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{required: true, message: '数据值必填项'},
|
||||
],
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
placeholder: '请输入数据值',
|
||||
},
|
||||
},
|
||||
},
|
||||
status: {
|
||||
title: '状态',
|
||||
width: 80,
|
||||
search: {
|
||||
show: true
|
||||
},
|
||||
type: 'dict-radio',
|
||||
dict: dict({
|
||||
data: dictionary('button_status_bool'),
|
||||
}),
|
||||
form: {
|
||||
value: true,
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{required: true, message: '状态必填项'},
|
||||
],
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
title: '排序',
|
||||
width: 70,
|
||||
type: 'number',
|
||||
form: {
|
||||
value: 1,
|
||||
component: {},
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{required: true, message: '排序必填项'},
|
||||
],
|
||||
},
|
||||
},
|
||||
color: {
|
||||
title: '标签颜色',
|
||||
width: 90,
|
||||
search: {
|
||||
disabled: true,
|
||||
},
|
||||
type: 'dict-select',
|
||||
dict: dict({
|
||||
data: [
|
||||
{label: 'success', value: 'success', color: 'success'},
|
||||
{label: 'primary', value: 'primary', color: 'primary'},
|
||||
{label: 'info', value: 'info', color: 'info'},
|
||||
{label: 'danger', value: 'danger', color: 'danger'},
|
||||
{label: 'warning', value: 'warning', color: 'warning'},
|
||||
],
|
||||
}),
|
||||
form: {
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
return {
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
rowHandle: {
|
||||
//固定右侧
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
buttons: {
|
||||
view: {
|
||||
show: false,
|
||||
},
|
||||
edit: {
|
||||
iconRight: 'Edit',
|
||||
type: 'text',
|
||||
},
|
||||
remove: {
|
||||
iconRight: 'Delete',
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
columns: {
|
||||
_index: {
|
||||
title: '序号',
|
||||
form: { show: false },
|
||||
column: {
|
||||
//type: 'index',
|
||||
align: 'center',
|
||||
width: '70px',
|
||||
columnSetDisabled: true, //禁止在列设置中选择
|
||||
formatter: (context) => {
|
||||
//计算序号,你可以自定义计算规则,此处为翻页累加
|
||||
let index = context.index ?? 1;
|
||||
let pagination = crudExpose!.crudBinding.value.pagination;
|
||||
// @ts-ignore
|
||||
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
label: {
|
||||
title: '名称',
|
||||
search: {
|
||||
show: true,
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'input',
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{ required: true, message: '名称必填项' },
|
||||
],
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
placeholder: '请输入名称',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: {
|
||||
title: '数据值类型',
|
||||
type: 'dict-select',
|
||||
search: {
|
||||
disabled: true,
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
show: false,
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: 'text', value: 0 },
|
||||
{ label: 'number', value: 1 },
|
||||
{ label: 'date', value: 2 },
|
||||
{ label: 'datetime', value: 3 },
|
||||
{ label: 'time', value: 4 },
|
||||
{ label: 'file', value: 5 },
|
||||
{ label: 'boolean', value: 6 },
|
||||
{ label: 'images', value: 7 },
|
||||
],
|
||||
}),
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{ required: true, message: '数据值类型必填项' },
|
||||
],
|
||||
value: 0,
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
placeholder: '请选择数据值类型',
|
||||
},
|
||||
/* valueChange(key, value, form, { getColumn, mode, component, immediate, getComponent }) {
|
||||
const template = vm.getEditFormTemplate('value')
|
||||
// 选择框重新选择后,情况value值
|
||||
if (!immediate) {
|
||||
form.value = undefined
|
||||
}
|
||||
if (value === 0) {
|
||||
template.component.name = 'el-input'
|
||||
} else if (value === 1) {
|
||||
template.component.name = 'el-input-number'
|
||||
} else if (value === 2) {
|
||||
template.component.name = 'el-date-picker'
|
||||
template.component.props = {
|
||||
type: 'date',
|
||||
valueFormat: 'yyyy-MM-dd'
|
||||
}
|
||||
} else if (value === 3) {
|
||||
template.component.name = 'el-date-picker'
|
||||
template.component.props = {
|
||||
type: 'datetime',
|
||||
valueFormat: 'yyyy-MM-dd HH:mm:ss'
|
||||
}
|
||||
} else if (value === 4) {
|
||||
template.component.name = 'el-time-picker'
|
||||
template.component.props = {
|
||||
pickerOptions: {
|
||||
arrowControl: true
|
||||
},
|
||||
valueFormat: 'HH:mm:ss'
|
||||
}
|
||||
} else if (value === 5) {
|
||||
template.component.name = 'd2p-file-uploader'
|
||||
template.component.props = { elProps: { listType: 'text' } }
|
||||
} else if (value === 6) {
|
||||
template.component.name = 'dict-switch'
|
||||
template.component.value = true
|
||||
template.component.props = {
|
||||
dict: {
|
||||
data: [
|
||||
{ label: '是', value: 'true' },
|
||||
{ label: '否', value: 'false' }
|
||||
]
|
||||
}
|
||||
}
|
||||
} else if (value === 7) {
|
||||
template.component.name = 'd2p-cropper-uploader'
|
||||
template.component.props = { accept: '.png,.jpeg,.jpg,.ico,.bmp,.gif', cropper: { viewMode: 1 } }
|
||||
}
|
||||
}, */
|
||||
},
|
||||
},
|
||||
value: {
|
||||
title: '数据值',
|
||||
search: {
|
||||
show: true,
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
view: {
|
||||
component: { props: { height: 100, width: 100 } },
|
||||
},
|
||||
/* // 提交时,处理数据
|
||||
valueResolve(row: any, col: any) {
|
||||
const value = row[col.key]
|
||||
const type = row.type
|
||||
if (type === 5 || type === 7) {
|
||||
if (value != null) {
|
||||
if (value.length >= 0) {
|
||||
if (value instanceof Array) {
|
||||
row[col.key] = value.toString()
|
||||
} else {
|
||||
row[col.key] = value
|
||||
}
|
||||
} else {
|
||||
row[col.key] = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
row[col.key] = value
|
||||
}
|
||||
},
|
||||
// 接收时,处理数据
|
||||
valueBuilder(row: any, col: any) {
|
||||
const value = row[col.key]
|
||||
const type = row.type
|
||||
if (type === 5 || type === 7) {
|
||||
if (value != null && value) {
|
||||
row[col.key] = value.split(',')
|
||||
}
|
||||
} else {
|
||||
row[col.key] = value
|
||||
}
|
||||
}, */
|
||||
type: 'input',
|
||||
form: {
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{ required: true, message: '数据值必填项' },
|
||||
],
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
placeholder: '请输入数据值',
|
||||
},
|
||||
},
|
||||
},
|
||||
status: {
|
||||
title: '状态',
|
||||
width: 80,
|
||||
search: {
|
||||
show: true
|
||||
},
|
||||
type: 'dict-radio',
|
||||
dict: dict({
|
||||
data: dictionary('button_status_bool'),
|
||||
}),
|
||||
form: {
|
||||
value: true,
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{ required: true, message: '状态必填项' },
|
||||
],
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
title: '排序',
|
||||
width: 70,
|
||||
type: 'number',
|
||||
form: {
|
||||
value: 1,
|
||||
component: {},
|
||||
rules: [
|
||||
// 表单校验规则
|
||||
{ required: true, message: '排序必填项' },
|
||||
],
|
||||
},
|
||||
},
|
||||
color: {
|
||||
title: '标签颜色',
|
||||
width: 90,
|
||||
search: {
|
||||
disabled: true,
|
||||
},
|
||||
type: 'dict-select',
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: 'success', value: 'success', color: 'success' },
|
||||
{ label: 'primary', value: 'primary', color: 'primary' },
|
||||
{ label: 'info', value: 'info', color: 'info' },
|
||||
{ label: 'danger', value: 'danger', color: 'danger' },
|
||||
{ label: 'warning', value: 'warning', color: 'warning' },
|
||||
],
|
||||
}),
|
||||
form: {
|
||||
component: {
|
||||
props: {
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
is_value: {
|
||||
title: '是否值',
|
||||
column: {
|
||||
show: false
|
||||
},
|
||||
form: {
|
||||
show: false,
|
||||
value: true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
49
web/src/views/system/downloadCenter/api.ts
Normal file
49
web/src/views/system/downloadCenter/api.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { request } from '/@/utils/service';
|
||||
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
|
||||
|
||||
export const apiPrefix = '/api/system/download_center/';
|
||||
|
||||
export function GetPermission() {
|
||||
return request({
|
||||
url: apiPrefix + 'field_permission/',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
export function GetList(query: PageQuery) {
|
||||
return request({
|
||||
url: apiPrefix,
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
export function GetObj(id: InfoReq) {
|
||||
return request({
|
||||
url: apiPrefix + id,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
export function AddObj(obj: AddReq) {
|
||||
return request({
|
||||
url: apiPrefix,
|
||||
method: 'post',
|
||||
data: obj,
|
||||
});
|
||||
}
|
||||
|
||||
export function UpdateObj(obj: EditReq) {
|
||||
return request({
|
||||
url: apiPrefix + obj.id + '/',
|
||||
method: 'put',
|
||||
data: obj,
|
||||
});
|
||||
}
|
||||
|
||||
export function DelObj(id: DelReq) {
|
||||
return request({
|
||||
url: apiPrefix + id + '/',
|
||||
method: 'delete',
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
160
web/src/views/system/downloadCenter/crud.tsx
Normal file
160
web/src/views/system/downloadCenter/crud.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from '@fast-crud/fast-crud';
|
||||
import * as api from './api';
|
||||
import { dictionary } from '/@/utils/dictionary';
|
||||
import { successMessage } from '../../../utils/message';
|
||||
import { auth } from '/@/utils/authFunction'
|
||||
|
||||
interface CreateCrudOptionsTypes {
|
||||
output: any;
|
||||
crudOptions: CrudOptions;
|
||||
}
|
||||
|
||||
//此处为crudOptions配置
|
||||
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose; }): CreateCrudOptionsTypes {
|
||||
const pageRequest = async (query: any) => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }: EditReq) => {
|
||||
form.id = row.id;
|
||||
return await api.UpdateObj(form);
|
||||
};
|
||||
const delRequest = async ({ row }: DelReq) => {
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
const addRequest = async ({ form }: AddReq) => {
|
||||
return await api.AddObj(form);
|
||||
};
|
||||
|
||||
//权限判定
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
return {
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
pagination: {
|
||||
show: true
|
||||
},
|
||||
actionbar: {
|
||||
buttons: {
|
||||
add: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
},
|
||||
toolbar: {
|
||||
buttons: {
|
||||
export: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
},
|
||||
rowHandle: {
|
||||
//固定右侧
|
||||
fixed: 'right',
|
||||
width: 120,
|
||||
buttons: {
|
||||
view: {
|
||||
show: false
|
||||
},
|
||||
edit: {
|
||||
show: false
|
||||
},
|
||||
remove: {
|
||||
show: false
|
||||
},
|
||||
download: {
|
||||
show: compute(ctx => ctx.row.task_status === 2),
|
||||
text: '下载文件',
|
||||
type: 'warning',
|
||||
click: (ctx) => window.open(ctx.row.url, '_blank')
|
||||
}
|
||||
},
|
||||
},
|
||||
form: {
|
||||
col: { span: 24 },
|
||||
labelWidth: '100px',
|
||||
wrapper: {
|
||||
is: 'el-dialog',
|
||||
width: '600px',
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
_index: {
|
||||
title: '序号',
|
||||
form: { show: false },
|
||||
column: {
|
||||
type: 'index',
|
||||
align: 'center',
|
||||
width: '70px',
|
||||
columnSetDisabled: true, //禁止在列设置中选择
|
||||
},
|
||||
},
|
||||
task_name: {
|
||||
title: '任务名',
|
||||
type: 'text',
|
||||
column: {
|
||||
minWidth: 160,
|
||||
align: 'left'
|
||||
},
|
||||
search: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
file_name: {
|
||||
title: '文件名',
|
||||
type: 'text',
|
||||
column: {
|
||||
minWidth: 160,
|
||||
align: 'left'
|
||||
},
|
||||
search: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
size: {
|
||||
title: '文件大小(b)',
|
||||
type: 'number',
|
||||
column: {
|
||||
width: 100
|
||||
}
|
||||
},
|
||||
task_status: {
|
||||
title: '任务状态',
|
||||
type: 'dict-select',
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: '任务已创建', value: 0 },
|
||||
{ label: '任务进行中', value: 1 },
|
||||
{ label: '任务完成', value: 2 },
|
||||
{ label: '任务失败', value: 3 },
|
||||
]
|
||||
}),
|
||||
column: {
|
||||
width: 120
|
||||
},
|
||||
search: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
create_datetime: {
|
||||
title: '创建时间',
|
||||
column: {
|
||||
width: 160
|
||||
}
|
||||
},
|
||||
update_datetime: {
|
||||
title: '创建时间',
|
||||
column: {
|
||||
width: 160
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
42
web/src/views/system/downloadCenter/index.vue
Normal file
42
web/src/views/system/downloadCenter/index.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<fs-page>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<template #cell_url="scope">
|
||||
<el-tag size="small">{{ scope.row.url }}</el-tag>
|
||||
</template>
|
||||
</fs-crud>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="downloadCenter">
|
||||
import { ref, onMounted, inject, onBeforeUpdate } from 'vue';
|
||||
|
||||
import { GetPermission } from './api';
|
||||
import { useExpose, useCrud } from '@fast-crud/fast-crud';
|
||||
import { createCrudOptions } from './crud';
|
||||
import PermissionComNew from './components/PermissionComNew/index.vue';
|
||||
import _ from "lodash-es";
|
||||
import { handleColumnPermission } from "/@/utils/columnPermission";
|
||||
|
||||
// crud组件的ref
|
||||
const crudRef = ref();
|
||||
// crud 配置的ref
|
||||
const crudBinding = ref();
|
||||
|
||||
const { crudExpose } = useExpose({ crudRef, crudBinding });
|
||||
|
||||
// 你的crud配置
|
||||
const { crudOptions } = createCrudOptions({ crudExpose });
|
||||
|
||||
// 初始化crud配置
|
||||
const { resetCrudOptions } = useCrud({
|
||||
crudExpose,
|
||||
crudOptions,
|
||||
context: {},
|
||||
});
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(async () => {
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
</script>
|
||||
@@ -1,7 +1,19 @@
|
||||
import * as api from './api';
|
||||
import { UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
|
||||
import {
|
||||
UserPageQuery,
|
||||
AddReq,
|
||||
DelReq,
|
||||
EditReq,
|
||||
CrudExpose,
|
||||
CrudOptions,
|
||||
CreateCrudOptionsProps,
|
||||
CreateCrudOptionsRet,
|
||||
dict
|
||||
} from '@fast-crud/fast-crud';
|
||||
import fileSelector from '/@/components/fileSelector/index.vue';
|
||||
import { shallowRef } from 'vue';
|
||||
|
||||
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery) => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
@@ -20,7 +32,8 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
||||
actionbar: {
|
||||
buttons: {
|
||||
add: {
|
||||
show: false,
|
||||
show: true,
|
||||
click: () => context.openAddHandle?.()
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -30,11 +43,22 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
tabs: {
|
||||
show: true,
|
||||
name: 'file_type',
|
||||
type: '',
|
||||
options: [
|
||||
{ value: 0, label: '图片' },
|
||||
{ value: 1, label: '视频' },
|
||||
{ value: 2, label: '音频' },
|
||||
{ value: 3, label: '其他' },
|
||||
]
|
||||
},
|
||||
rowHandle: {
|
||||
//固定右侧
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
show:false,
|
||||
show: false,
|
||||
buttons: {
|
||||
view: {
|
||||
show: false,
|
||||
@@ -95,23 +119,34 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
||||
show: true,
|
||||
},
|
||||
type: 'input',
|
||||
column:{
|
||||
minWidth: 120,
|
||||
column: {
|
||||
minWidth: 200,
|
||||
},
|
||||
form: {
|
||||
component: {
|
||||
placeholder: '请输入文件名称',
|
||||
clearable: true
|
||||
},
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
title: '预览',
|
||||
column: {
|
||||
minWidth: 120,
|
||||
align: 'center'
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
url: {
|
||||
title: '文件地址',
|
||||
type: 'file-uploader',
|
||||
search: {
|
||||
disabled: true,
|
||||
},
|
||||
column:{
|
||||
minWidth: 200,
|
||||
column: {
|
||||
minWidth: 360,
|
||||
},
|
||||
},
|
||||
md5sum: {
|
||||
@@ -119,13 +154,99 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
||||
search: {
|
||||
disabled: true,
|
||||
},
|
||||
column:{
|
||||
minWidth: 120,
|
||||
column: {
|
||||
minWidth: 300,
|
||||
},
|
||||
form: {
|
||||
disabled: false,
|
||||
disabled: false
|
||||
},
|
||||
},
|
||||
mime_type: {
|
||||
title: '文件类型',
|
||||
type: 'input',
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
minWidth: 160
|
||||
}
|
||||
},
|
||||
file_type: {
|
||||
title: '文件类型',
|
||||
type: 'dict-select',
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: '图片', value: 0, color: 'success' },
|
||||
{ label: '视频', value: 1, color: 'warning' },
|
||||
{ label: '音频', value: 2, color: 'danger' },
|
||||
{ label: '其他', value: 3, color: 'primary' },
|
||||
]
|
||||
}),
|
||||
column: {
|
||||
show: false
|
||||
},
|
||||
search: {
|
||||
show: true
|
||||
},
|
||||
form: {
|
||||
show: false,
|
||||
component: {
|
||||
placeholder: '请选择文件类型'
|
||||
}
|
||||
}
|
||||
},
|
||||
size: {
|
||||
title: '文件大小',
|
||||
column: {
|
||||
minWidth: 120
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
upload_method: {
|
||||
title: '上传方式',
|
||||
type: 'dict-select',
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: '默认上传', value: 0, color: 'primary' },
|
||||
{ label: '文件选择器上传', value: 1, color: 'warning' },
|
||||
]
|
||||
}),
|
||||
column: {
|
||||
minWidth: 140
|
||||
},
|
||||
search: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
create_datetime: {
|
||||
title: '创建时间',
|
||||
column: {
|
||||
minWidth: 160
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
// fileselectortest: {
|
||||
// title: '文件选择器测试',
|
||||
// type: 'file-selector',
|
||||
// width: 200,
|
||||
// form: {
|
||||
// component: {
|
||||
// name: shallowRef(fileSelector),
|
||||
// vModel: 'modelValue',
|
||||
// tabsShow: 0b0100,
|
||||
// itemSize: 100,
|
||||
// multiple: false,
|
||||
// selectable: true,
|
||||
// showInput: true,
|
||||
// inputType: 'video',
|
||||
// valueKey: 'url',
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,13 +1,85 @@
|
||||
<template>
|
||||
<fs-page>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
||||
<FileSelector v-model="selected" :showInput="false" ref="fileSelectorRef" :tabsShow="SHOW.ALL" :itemSize="120"
|
||||
:multiple="false" :selectable="true" valueKey="url" inputType="image">
|
||||
<!-- <template #input="scope">
|
||||
input:{{ scope }}
|
||||
</template> -->
|
||||
<!-- <template #actionbar-left="scope">
|
||||
actionbar-left:{{ scope }}
|
||||
</template> -->
|
||||
<!-- <template #actionbar-right="scope">
|
||||
actionbar-right:{{ scope }}
|
||||
</template> -->
|
||||
<!-- <template #empty="scope">
|
||||
empty:{{ scope }}
|
||||
</template> -->
|
||||
<!-- <template #item="{ data }">
|
||||
{{ data }}
|
||||
</template> -->
|
||||
</FileSelector>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<template #actionbar-left="scope">
|
||||
<el-upload :action="getBaseURL() + 'api/system/file/'" :multiple="false"
|
||||
:on-success="() => crudExpose.doRefresh()" :drag="false" :show-file-list="false">
|
||||
<el-button type="primary" icon="plus">上传</el-button>
|
||||
</el-upload>
|
||||
</template>
|
||||
<template #cell_size="scope">
|
||||
<span>{{ scope.row.size ? getSizeDisplay(scope.row.size) : '0b' }}</span>
|
||||
</template>
|
||||
<template #cell_preview="scope">
|
||||
<div v-if="scope.row.file_type === 0">
|
||||
<el-image style="width: 100%; aspect-ratio: 1 /1 ;" :src="getBaseURL(scope.row.url)"
|
||||
:preview-src-list="[getBaseURL(scope.row.url)]" :preview-teleported="true" />
|
||||
</div>
|
||||
<div v-if="scope.row.file_type === 1" class="_preview"
|
||||
@click="openPreviewHandle(getBaseURL(scope.row.url), 'video')">
|
||||
<el-icon :size="60">
|
||||
<VideoCamera />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div v-if="scope.row.file_type === 2" class="_preview"
|
||||
@click="openPreviewHandle(getBaseURL(scope.row.url), 'video')">
|
||||
<el-icon :size="60">
|
||||
<Headset />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-icon v-if="scope.row.file_type === 3" :size="60">
|
||||
<Document />
|
||||
</el-icon>
|
||||
<div v-if="scope.row.file_type > 3">未知类型</div>
|
||||
</template>
|
||||
</fs-crud>
|
||||
<div class="preview" :class="{ show: openPreview }">
|
||||
<video v-show="videoPreviewSrc" :src="videoPreviewSrc" class="previewItem" :controls="true" :autoplay="true"
|
||||
:muted="true" :loop="false" ref="videoPreviewRef"></video>
|
||||
<audio v-show="audioPreviewSrc" :src="audioPreviewSrc" class="previewItem" :controls="true" :autoplay="false"
|
||||
:muted="true" :loop="false" ref="audioPreviewRef"></audio>
|
||||
<div class="closePreviewBtn">
|
||||
<el-icon :size="48" color="white" style="cursor: pointer;" @click="closePreview">
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
import { useExpose, useCrud } from '@fast-crud/fast-crud';
|
||||
import { createCrudOptions } from './crud';
|
||||
import { getBaseURL } from '/@/utils/baseUrl';
|
||||
import FileSelector from '/@/components/fileSelector/index.vue';
|
||||
import { SHOW } from '/@/components/fileSelector/types';
|
||||
|
||||
const fileSelectorRef = ref<any>(null);
|
||||
const getSizeDisplay = (n: number) => n < 1024 ? n + 'b' : (n < 1024 * 1024 ? (n / 1024).toFixed(2) + 'Kb' : (n / (1024 * 1024)).toFixed(2) + 'Mb');
|
||||
|
||||
const openAddHandle = async () => {
|
||||
fileSelectorRef.value.selectVisiable = true;
|
||||
await nextTick();
|
||||
};
|
||||
// crud组件的ref
|
||||
const crudRef = ref();
|
||||
// crud 配置的ref
|
||||
@@ -15,12 +87,81 @@ const crudBinding = ref();
|
||||
// 暴露的方法
|
||||
const { crudExpose } = useExpose({ crudRef, crudBinding });
|
||||
// 你的crud配置
|
||||
const { crudOptions } = createCrudOptions({ crudExpose });
|
||||
const { crudOptions } = createCrudOptions({ crudExpose, context: { openAddHandle } });
|
||||
// 初始化crud配置
|
||||
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
|
||||
|
||||
const selected = ref<any>([]);
|
||||
const openPreview = ref<boolean>(false);
|
||||
const videoPreviewSrc = ref<string>('');
|
||||
const audioPreviewSrc = ref<string>('');
|
||||
const videoPreviewRef = ref<HTMLVideoElement>();
|
||||
const audioPreviewRef = ref<HTMLAudioElement>();
|
||||
const openPreviewHandle = (src: string, type: string) => {
|
||||
openPreview.value = true;
|
||||
(videoPreviewRef.value as HTMLVideoElement).muted = true;
|
||||
(audioPreviewRef.value as HTMLAudioElement).muted = true;
|
||||
if (type === 'video') videoPreviewSrc.value = src;
|
||||
else audioPreviewSrc.value = src;
|
||||
window.addEventListener('keydown', onPreviewKeydown);
|
||||
};
|
||||
const closePreview = () => {
|
||||
openPreview.value = false;
|
||||
videoPreviewSrc.value = '';
|
||||
audioPreviewSrc.value = '';
|
||||
window.removeEventListener('keydown', onPreviewKeydown);
|
||||
};
|
||||
const onPreviewKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
openPreview.value = false;
|
||||
videoPreviewSrc.value = '';
|
||||
audioPreviewSrc.value = '';
|
||||
window.removeEventListener('keydown', onPreviewKeydown);
|
||||
};
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.preview {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.show {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.previewItem {
|
||||
width: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 50%;
|
||||
transform: translate(25%, -50%);
|
||||
}
|
||||
|
||||
.closePreviewBtn {
|
||||
width: 50%;
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
left: 50%;
|
||||
transform: translate(-75%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
._preview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -39,3 +39,10 @@ export function DelObj(id: DelReq) {
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export function GetPermission() {
|
||||
return request({
|
||||
url: apiPrefix + 'field_permission/',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,14 +5,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="loginLog">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import { createCrudOptions } from './crud';
|
||||
import { GetPermission } from './api';
|
||||
import { handleColumnPermission } from '/@/utils/columnPermission';
|
||||
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||
const { crudBinding, crudRef, crudExpose, crudOptions, resetCrudOptions } = useFs({ createCrudOptions });
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 设置列权限
|
||||
const newOptions = await handleColumnPermission(GetPermission, crudOptions);
|
||||
//重置crudBinding
|
||||
resetCrudOptions(newOptions);
|
||||
// 刷新
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -13,6 +13,15 @@ export function login(params: object) {
|
||||
data: params
|
||||
});
|
||||
}
|
||||
|
||||
export function loginChangePwd(data: object) {
|
||||
return request({
|
||||
url: '/api/system/user/login_change_password/',
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
export function getUserInfo() {
|
||||
return request({
|
||||
url: '/api/system/user/user_info/',
|
||||
|
||||
@@ -45,6 +45,12 @@
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 申请试用-->
|
||||
<div style="text-align: center" v-if="showApply()">
|
||||
<el-button class="login-content-apply" link type="primary" plain round @click="applyBtnClick">
|
||||
<span>申请试用</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -67,6 +73,7 @@ import { SystemConfigStore } from '/@/stores/systemConfig';
|
||||
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
|
||||
import { Md5 } from 'ts-md5';
|
||||
import { errorMessage } from '/@/utils/message';
|
||||
import {getBaseURL} from "/@/utils/baseUrl";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'loginAccount',
|
||||
@@ -125,7 +132,10 @@ export default defineComponent({
|
||||
state.ruleForm.captchaKey = ret.data.key;
|
||||
});
|
||||
};
|
||||
const refreshCaptcha = async () => {
|
||||
const applyBtnClick = async () => {
|
||||
window.open(getBaseURL('/api/system/apply_for_trial/'));
|
||||
};
|
||||
const refreshCaptcha = async () => {
|
||||
state.ruleForm.captcha=''
|
||||
loginApi.getCaptcha().then((ret: any) => {
|
||||
state.ruleForm.captchaImgBase = ret.data.image_base;
|
||||
@@ -138,8 +148,13 @@ export default defineComponent({
|
||||
if (valid) {
|
||||
loginApi.login({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password) }).then((res: any) => {
|
||||
if (res.code === 2000) {
|
||||
Session.set('token', res.data.access);
|
||||
Cookies.set('username', res.data.name);
|
||||
const {data} = res
|
||||
Cookies.set('username', res.data.username);
|
||||
Session.set('token', res.data.access);
|
||||
useUserInfo().setPwdChangeCount(data.pwd_change_count)
|
||||
if(data.pwd_change_count==0){
|
||||
return router.push('/login');
|
||||
}
|
||||
if (!themeConfig.value.isRequestRoutes) {
|
||||
// 前端控制路由,2、请注意执行顺序
|
||||
initFrontEndControlRoutes();
|
||||
@@ -162,35 +177,33 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
};
|
||||
const getUserInfo = () => {
|
||||
useUserInfo().setUserInfos();
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 登录成功后的跳转
|
||||
const loginSuccess = () => {
|
||||
//登录成功获取用户信息,获取系统字典数据
|
||||
getUserInfo();
|
||||
//获取所有字典
|
||||
DictionaryStore().getSystemDictionarys();
|
||||
|
||||
// 初始化登录成功时间问候语
|
||||
let currentTimeInfo = currentTime.value;
|
||||
// 登录成功,跳到转首页
|
||||
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
|
||||
if (route.query?.redirect) {
|
||||
router.push({
|
||||
path: <string>route.query?.redirect,
|
||||
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
|
||||
});
|
||||
} else {
|
||||
router.push('/');
|
||||
}
|
||||
// 登录成功提示
|
||||
// 关闭 loading
|
||||
state.loading.signIn = true;
|
||||
const signInText = t('message.signInText');
|
||||
ElMessage.success(`${currentTimeInfo},${signInText}`);
|
||||
const pwd_change_count = userInfos.value.pwd_change_count
|
||||
if(pwd_change_count>0){
|
||||
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
|
||||
if (route.query?.redirect) {
|
||||
router.push({
|
||||
path: <string>route.query?.redirect,
|
||||
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
|
||||
});
|
||||
} else {
|
||||
router.push('/');
|
||||
}
|
||||
// 登录成功提示
|
||||
// 关闭 loading
|
||||
state.loading.signIn = true;
|
||||
const signInText = t('message.signInText');
|
||||
ElMessage.success(`${currentTimeInfo},${signInText}`);
|
||||
}
|
||||
// 添加 loading,防止第一次进入界面时出现短暂空白
|
||||
NextLoading.start();
|
||||
};
|
||||
@@ -199,7 +212,10 @@ export default defineComponent({
|
||||
//获取系统配置
|
||||
SystemConfigStore().getSystemConfigs();
|
||||
});
|
||||
|
||||
// 是否显示申请试用按钮
|
||||
const showApply = () => {
|
||||
return window.location.href.indexOf('public') != -1
|
||||
}
|
||||
|
||||
return {
|
||||
refreshCaptcha,
|
||||
@@ -209,6 +225,8 @@ export default defineComponent({
|
||||
state,
|
||||
formRef,
|
||||
rules,
|
||||
applyBtnClick,
|
||||
showApply,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
@@ -249,7 +267,7 @@ export default defineComponent({
|
||||
.login-content-submit {
|
||||
width: 100%;
|
||||
letter-spacing: 2px;
|
||||
font-weight: 300;
|
||||
font-weight: 800;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
276
web/src/views/system/login/component/changePwd.vue
Normal file
276
web/src/views/system/login/component/changePwd.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<el-form ref="formRef" size="large" class="login-content-form" :model="state.ruleForm" :rules="rules"
|
||||
@keyup.enter="loginClick">
|
||||
<el-form-item class="login-animation1" prop="username">
|
||||
<el-input type="text" :placeholder="$t('message.account.accountPlaceholder1')" readonly
|
||||
v-model="ruleForm.username" clearable autocomplete="off">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon"><ele-User /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item class="login-animation2" prop="password">
|
||||
<el-input :type="isShowPassword ? 'text' : 'password'"
|
||||
:placeholder="$t('message.account.accountPlaceholder4')" v-model="ruleForm.password">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon"><ele-Unlock /></el-icon>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<i class="iconfont el-input__icon login-content-password"
|
||||
:class="isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
|
||||
@click="isShowPassword = !isShowPassword">
|
||||
</i>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item class="login-animation3" prop="password_regain">
|
||||
<el-input :type="isShowPassword ? 'text' : 'password'"
|
||||
:placeholder="$t('message.account.accountPlaceholder5')" v-model="ruleForm.password_regain">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon"><ele-Unlock /></el-icon>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<i class="iconfont el-input__icon login-content-password"
|
||||
:class="isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
|
||||
@click="isShowPassword = !isShowPassword">
|
||||
</i>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item class="login-animation4">
|
||||
<el-button type="primary" class="login-content-submit" round @click="loginClick" :loading="loading.signIn">
|
||||
<span>{{ $t('message.account.accountBtnText') }}</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 申请试用-->
|
||||
<div style="text-align: center" v-if="showApply()">
|
||||
<el-button class="login-content-apply" link type="primary" plain round @click="applyBtnClick">
|
||||
<span>申请试用</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { toRefs, reactive, defineComponent, computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage, FormInstance, FormRules } from 'element-plus';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Cookies from 'js-cookie';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '/@/stores/themeConfig';
|
||||
import { initFrontEndControlRoutes } from '/@/router/frontEnd';
|
||||
import { initBackEndControlRoutes } from '/@/router/backEnd';
|
||||
import { Session } from '/@/utils/storage';
|
||||
import { formatAxis } from '/@/utils/formatTime';
|
||||
import { NextLoading } from '/@/utils/loading';
|
||||
import * as loginApi from '/@/views/system/login/api';
|
||||
import { useUserInfo } from '/@/stores/userInfo';
|
||||
import { DictionaryStore } from '/@/stores/dictionary';
|
||||
import { SystemConfigStore } from '/@/stores/systemConfig';
|
||||
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
|
||||
import { Md5 } from 'ts-md5';
|
||||
import { errorMessage } from '/@/utils/message';
|
||||
import { getBaseURL } from "/@/utils/baseUrl";
|
||||
import { loginChangePwd } from "/@/views/system/login/api";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'changePwd',
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||
const { userInfos } = storeToRefs(useUserInfo());
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const state = reactive({
|
||||
isShowPassword: false,
|
||||
ruleForm: {
|
||||
username: '',
|
||||
password: '',
|
||||
password_regain: ''
|
||||
},
|
||||
loading: {
|
||||
signIn: false,
|
||||
},
|
||||
});
|
||||
|
||||
const validatePass = (rule, value, callback) => {
|
||||
const pwdRegex = new RegExp('(?=.*[0-9])(?=.*[a-zA-Z]).{8,30}');
|
||||
if (value === '') {
|
||||
callback(new Error('请输入密码'));
|
||||
} else if (!pwdRegex.test(value)) {
|
||||
callback(new Error('您的密码复杂度太低(密码中必须包含字母、数字)'));
|
||||
} else {
|
||||
if (state.ruleForm.password !== '') {
|
||||
formRef.value.validateField('password');
|
||||
}
|
||||
callback();
|
||||
}
|
||||
};
|
||||
const validatePass2 = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'));
|
||||
} else if (value !== state.ruleForm.password) {
|
||||
callback(new Error('两次输入密码不一致!'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请填写账号', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请填写密码',
|
||||
trigger: 'blur',
|
||||
},
|
||||
{
|
||||
validator: validatePass,
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
password_regain: [
|
||||
{
|
||||
required: true,
|
||||
message: '请填写密码',
|
||||
trigger: 'blur',
|
||||
},
|
||||
{
|
||||
validator: validatePass2,
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
})
|
||||
const formRef = ref();
|
||||
// 时间获取
|
||||
const currentTime = computed(() => {
|
||||
return formatAxis(new Date());
|
||||
});
|
||||
|
||||
const applyBtnClick = async () => {
|
||||
window.open(getBaseURL('/api/system/apply_for_trial/'));
|
||||
};
|
||||
|
||||
const loginClick = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate((valid: any) => {
|
||||
if (valid) {
|
||||
loginApi.loginChangePwd({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password), password_regain: Md5.hashStr(state.ruleForm.password_regain) }).then((res: any) => {
|
||||
if (res.code === 2000) {
|
||||
if (!themeConfig.value.isRequestRoutes) {
|
||||
// 前端控制路由,2、请注意执行顺序
|
||||
initFrontEndControlRoutes();
|
||||
loginSuccess();
|
||||
} else {
|
||||
// 模拟后端控制路由,isRequestRoutes 为 true,则开启后端控制路由
|
||||
// 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
|
||||
initBackEndControlRoutes();
|
||||
// 执行完 initBackEndControlRoutes,再执行 signInSuccess
|
||||
loginSuccess();
|
||||
}
|
||||
}
|
||||
}).catch((err: any) => {
|
||||
// 登录错误之后,刷新验证码
|
||||
errorMessage("登录失败")
|
||||
});
|
||||
} else {
|
||||
errorMessage("请填写登录信息")
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
|
||||
// 登录成功后的跳转
|
||||
const loginSuccess = () => {
|
||||
|
||||
//获取所有字典
|
||||
DictionaryStore().getSystemDictionarys();
|
||||
|
||||
// 初始化登录成功时间问候语
|
||||
let currentTimeInfo = currentTime.value;
|
||||
// 登录成功,跳到转首页
|
||||
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
|
||||
if (route.query?.redirect) {
|
||||
router.push({
|
||||
path: <string>route.query?.redirect,
|
||||
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
|
||||
});
|
||||
} else {
|
||||
router.push('/');
|
||||
}
|
||||
// 登录成功提示
|
||||
// 关闭 loading
|
||||
state.loading.signIn = true;
|
||||
const signInText = t('message.signInText');
|
||||
ElMessage.success(`${currentTimeInfo},${signInText}`);
|
||||
// 添加 loading,防止第一次进入界面时出现短暂空白
|
||||
NextLoading.start();
|
||||
};
|
||||
onMounted(() => {
|
||||
state.ruleForm.username = Cookies.get('username')
|
||||
//获取系统配置
|
||||
SystemConfigStore().getSystemConfigs();
|
||||
});
|
||||
// 是否显示申请试用按钮
|
||||
const showApply = () => {
|
||||
return window.location.href.indexOf('public') != -1
|
||||
}
|
||||
|
||||
return {
|
||||
loginClick,
|
||||
loginSuccess,
|
||||
state,
|
||||
formRef,
|
||||
rules,
|
||||
applyBtnClick,
|
||||
showApply,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login-content-form {
|
||||
margin-top: 20px;
|
||||
|
||||
@for $i from 1 through 5 {
|
||||
.login-animation#{$i} {
|
||||
opacity: 0;
|
||||
animation-name: error-num;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: calc($i/10) + s;
|
||||
}
|
||||
}
|
||||
|
||||
.login-content-password {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.login-content-captcha {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
letter-spacing: 5px;
|
||||
}
|
||||
|
||||
.login-content-submit {
|
||||
width: 100%;
|
||||
letter-spacing: 2px;
|
||||
font-weight: 800;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,51 +5,52 @@
|
||||
<img :src="siteLogo" />
|
||||
<div class="login-left-logo-text">
|
||||
<span>{{ getSystemConfig['login.site_title'] || getThemeConfig.globalViceTitle }}</span>
|
||||
<span class="login-left-logo-text-msg">{{
|
||||
<span class="login-left-logo-text-msg" style="margin-top: 5px;">{{
|
||||
getSystemConfig['login.site_name'] || getThemeConfig.globalViceTitleMsg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-left-img">
|
||||
<img :src="loginMain" />
|
||||
</div>
|
||||
<img :src="loginBg" class="login-left-waves" />
|
||||
</div>
|
||||
<div class="login-right flex z-10">
|
||||
<div class="login-right-warp flex-margin">
|
||||
<span class="login-right-warp-one"></span>
|
||||
<span class="login-right-warp-two"></span>
|
||||
<!-- <span class="login-right-warp-one"></span>-->
|
||||
<!-- <span class="login-right-warp-two"></span>-->
|
||||
<div class="login-right-warp-mian">
|
||||
<div class="login-right-warp-main-title">{{ getSystemConfig['login.site_title'] ||
|
||||
getThemeConfig.globalTitle }} 欢迎您!</div>
|
||||
<div class="login-right-warp-main-title">
|
||||
{{userInfos.pwd_change_count===0?'初次登录修改密码':'欢迎登录'}}
|
||||
</div>
|
||||
<div class="login-right-warp-main-form">
|
||||
<div v-if="!state.isScan">
|
||||
<el-tabs v-model="state.tabsActiveName">
|
||||
<el-tab-pane :label="$t('message.label.one1')" name="account">
|
||||
<el-tab-pane :label="$t('message.label.changePwd')" name="changePwd" v-if="userInfos.pwd_change_count===0">
|
||||
<ChangePwd />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('message.label.one1')" name="account" v-else>
|
||||
<Account />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- TODO 手机号码登录未接入,展示隐藏 -->
|
||||
<!-- <el-tab-pane :label="$t('message.label.two2')" name="mobile">
|
||||
<Mobile />
|
||||
</el-tab-pane> -->
|
||||
</el-tabs>
|
||||
</div>
|
||||
<Scan v-if="state.isScan" />
|
||||
<div class="login-content-main-sacn" @click="state.isScan = !state.isScan">
|
||||
<i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>
|
||||
<div class="login-content-main-sacn-delta"></div>
|
||||
</div>
|
||||
<!-- <Scan v-if="state.isScan" />-->
|
||||
<!-- <div class="login-content-main-sacn" @click="state.isScan = !state.isScan">-->
|
||||
<!-- <i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>-->
|
||||
<!-- <div class="login-content-main-sacn-delta"></div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-authorization z-10">
|
||||
<p>Copyright © {{ getSystemConfig['login.copyright'] || '2021-2024 django-vue-admin.com' }} 版权所有</p>
|
||||
<p class="la-other">
|
||||
<p>Copyright © {{ getSystemConfig['login.copyright'] || '2021-2024 北京信码新创科技有限公司' }} 版权所有</p>
|
||||
<p class="la-other" style="margin-top: 5px;">
|
||||
<a href="https://beian.miit.gov.cn" target="_blank">{{ getSystemConfig['login.keep_record'] ||
|
||||
'晋ICP备18005113号-3' }}</a>
|
||||
'京ICP备2021031018号' }}</a>
|
||||
|
|
||||
<a :href="getSystemConfig['login.help_url'] ? getSystemConfig['login.help_url'] : 'https://django-vue-admin.com'"
|
||||
<a :href="getSystemConfig['login.help_url'] ? getSystemConfig['login.help_url'] : '#'"
|
||||
target="_blank">帮助</a>
|
||||
|
|
||||
<a
|
||||
@@ -60,26 +61,29 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="siteBg">
|
||||
<img :src="siteBg" class="fixed inset-0 z-1 w-full h-full" />
|
||||
<div v-if="loginBg">
|
||||
<img :src="loginBg" class="loginBg fixed inset-0 z-1 w-full h-full" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="loginIndex">
|
||||
import { defineAsyncComponent, onMounted, reactive, computed } from 'vue';
|
||||
import {defineAsyncComponent, onMounted, reactive, computed, watch} from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '/@/stores/themeConfig';
|
||||
import { NextLoading } from '/@/utils/loading';
|
||||
import logoMini from '/@/assets/logo-mini.svg';
|
||||
import loginMain from '/@/assets/login-main.svg';
|
||||
import loginBg from '/@/assets/login-bg.svg';
|
||||
import loginBg from '/@/assets/login-bg.png';
|
||||
import { SystemConfigStore } from '/@/stores/systemConfig'
|
||||
import { getBaseURL } from "/@/utils/baseUrl";
|
||||
// 引入组件
|
||||
const Account = defineAsyncComponent(() => import('/@/views/system/login/component/account.vue'));
|
||||
const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue'));
|
||||
const Scan = defineAsyncComponent(() => import('/@/views/system/login/component/scan.vue'));
|
||||
const ChangePwd = defineAsyncComponent(() => import('/@/views/system/login/component/changePwd.vue'));
|
||||
import _ from "lodash-es";
|
||||
import {useUserInfo} from "/@/stores/userInfo";
|
||||
const { userInfos } = storeToRefs(useUserInfo());
|
||||
|
||||
// 定义变量内容
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
@@ -89,6 +93,16 @@ const state = reactive({
|
||||
isScan: false,
|
||||
});
|
||||
|
||||
|
||||
watch(()=>userInfos.value.pwd_change_count,(val)=>{
|
||||
if(val===0){
|
||||
state.tabsActiveName ='changePwd'
|
||||
}else{
|
||||
state.tabsActiveName ='account'
|
||||
}
|
||||
},{deep:true,immediate:true})
|
||||
|
||||
|
||||
// 获取布局配置信息
|
||||
const getThemeConfig = computed(() => {
|
||||
return themeConfig.value;
|
||||
@@ -187,13 +201,13 @@ onMounted(() => {
|
||||
width: 700px;
|
||||
|
||||
.login-right-warp {
|
||||
border: 1px solid var(--el-color-primary-light-3);
|
||||
//border: 1px solid var(--el-color-primary-light-3);
|
||||
border-radius: 3px;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: var(--el-color-white);
|
||||
//background-color: var(--el-color-white);
|
||||
|
||||
.login-right-warp-one,
|
||||
.login-right-warp-two {
|
||||
@@ -265,7 +279,8 @@ onMounted(() => {
|
||||
.login-right-warp-main-title {
|
||||
height: 130px;
|
||||
line-height: 130px;
|
||||
font-size: 27px;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
letter-spacing: 3px;
|
||||
animation: logoAnimation 0.3s ease;
|
||||
@@ -321,7 +336,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.login-authorization {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
@@ -48,3 +48,10 @@ export function BatchAdd(obj: AddReq) {
|
||||
});
|
||||
}
|
||||
|
||||
export function BatchDelete(keys: any) {
|
||||
return request({
|
||||
url: apiPrefix + 'multiple_delete/',
|
||||
method: 'delete',
|
||||
data: { keys },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import {auth} from '/@/utils/authFunction'
|
||||
import {request} from '/@/utils/service';
|
||||
import { successNotification } from '/@/utils/message';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import XEUtils from 'xe-utils';
|
||||
//此处为crudOptions配置
|
||||
export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const pageRequest = async () => {
|
||||
@@ -22,7 +24,42 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
|
||||
const addRequest = async ({form}: AddReq) => {
|
||||
return await api.AddObj({...form, ...{menu: context!.selectOptions.value.id}});
|
||||
};
|
||||
// 记录选中的行
|
||||
const selectedRows = ref<any>([]);
|
||||
|
||||
const onSelectionChange = (changed: any) => {
|
||||
const tableData = crudExpose.getTableData();
|
||||
const unChanged = tableData.filter((row: any) => !changed.includes(row));
|
||||
// 添加已选择的行
|
||||
XEUtils.arrayEach(changed, (item: any) => {
|
||||
const ids = XEUtils.pluck(selectedRows.value, 'id');
|
||||
if (!ids.includes(item.id)) {
|
||||
selectedRows.value = XEUtils.union(selectedRows.value, [item]);
|
||||
}
|
||||
});
|
||||
// 剔除未选择的行
|
||||
XEUtils.arrayEach(unChanged, (unItem: any) => {
|
||||
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== unItem.id);
|
||||
});
|
||||
};
|
||||
const toggleRowSelection = () => {
|
||||
// 多选后,回显默认勾选
|
||||
const tableRef = crudExpose.getBaseTableRef();
|
||||
const tableData = crudExpose.getTableData();
|
||||
const selected = XEUtils.filter(tableData, (item: any) => {
|
||||
const ids = XEUtils.pluck(selectedRows.value, 'id');
|
||||
return ids.includes(item.id);
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
XEUtils.arrayEach(selected, (item) => {
|
||||
tableRef.toggleRowSelection(item, true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
selectedRows,
|
||||
crudOptions: {
|
||||
pagination:{
|
||||
show:false
|
||||
@@ -84,6 +121,11 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
table: {
|
||||
rowKey: 'id', //设置你的主键id, 默认rowKey=id
|
||||
onSelectionChange,
|
||||
onRefreshed: () => toggleRowSelection(),
|
||||
},
|
||||
form: {
|
||||
col: {span: 24},
|
||||
labelWidth: '100px',
|
||||
@@ -93,6 +135,16 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
$checked: {
|
||||
title: '选择',
|
||||
form: { show: false },
|
||||
column: {
|
||||
type: 'selection',
|
||||
align: 'center',
|
||||
width: '70px',
|
||||
columnSetDisabled: true, //禁止在列设置中选择
|
||||
},
|
||||
},
|
||||
_index: {
|
||||
title: '序号',
|
||||
form: {show: false},
|
||||
|
||||
@@ -1,19 +1,72 @@
|
||||
<template>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<template #pagination-left>
|
||||
<el-tooltip content="批量删除">
|
||||
<el-button text type="danger" :disabled="selectedRowsCount === 0" :icon="Delete" circle @click="handleBatchDelete" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #pagination-right>
|
||||
<el-popover placement="top" :width="400" trigger="click">
|
||||
<template #reference>
|
||||
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
|
||||
</template>
|
||||
<el-table :data="selectedRows" size="small">
|
||||
<el-table-column width="150" property="id" label="id" />
|
||||
<el-table-column fixed="right" label="操作" min-width="60">
|
||||
<template #default="scope">
|
||||
<el-button text type="info" :icon="Close" @click="removeSelectedRows(scope.row)" circle />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-popover>
|
||||
</template>
|
||||
</fs-crud>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import { createCrudOptions } from './crud';
|
||||
import { MenuTreeItemType } from '../../types';
|
||||
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import XEUtils from 'xe-utils';
|
||||
import { BatchDelete } from './api';
|
||||
import { Close, Delete } from '@element-plus/icons-vue';
|
||||
// 当前选择的菜单信息
|
||||
let selectOptions: any = ref({ name: null });
|
||||
|
||||
const { crudRef, crudBinding, crudExpose, context } = useFs({ createCrudOptions, context: { selectOptions } });
|
||||
const { crudRef, crudBinding, crudExpose, context,selectedRows } = useFs({ createCrudOptions, context: { selectOptions } });
|
||||
const { doRefresh, setTableData } = crudExpose;
|
||||
|
||||
// 选中行的条数
|
||||
const selectedRowsCount = computed(() => {
|
||||
return selectedRows.value.length;
|
||||
});
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
await ElMessageBox.confirm(`确定要批量删除这${selectedRows.value.length}条记录吗`, '确认', {
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
closeOnClickModal: false,
|
||||
});
|
||||
await BatchDelete(XEUtils.pluck(selectedRows.value, 'id'));
|
||||
ElMessage.info('删除成功');
|
||||
selectedRows.value = [];
|
||||
await crudExpose.doRefresh();
|
||||
};
|
||||
|
||||
// 移除已选中的行
|
||||
const removeSelectedRows = (row: any) => {
|
||||
const tableRef = crudExpose.getBaseTableRef();
|
||||
const tableData = crudExpose.getTableData();
|
||||
if (XEUtils.pluck(tableData, 'id').includes(row.id)) {
|
||||
tableRef.toggleRowSelection(row, false);
|
||||
} else {
|
||||
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== row.id);
|
||||
}
|
||||
};
|
||||
const handleRefreshTable = (record: MenuTreeItemType) => {
|
||||
if (!record.is_catalog && record.id) {
|
||||
selectOptions.value = record;
|
||||
|
||||
@@ -42,6 +42,13 @@ export function DelObj(id: DelReq) {
|
||||
});
|
||||
}
|
||||
|
||||
export function BatchDelete(keys: any) {
|
||||
return request({
|
||||
url: apiPrefix + 'multiple_delete/',
|
||||
method: 'delete',
|
||||
data: { keys },
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取所有model
|
||||
*/
|
||||
|
||||
@@ -2,8 +2,9 @@ import * as api from './api';
|
||||
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
|
||||
import { request } from '/@/utils/service';
|
||||
import { dictionary } from '/@/utils/dictionary';
|
||||
import { inject } from 'vue';
|
||||
import { inject, nextTick, ref } from 'vue';
|
||||
import {auth} from "/@/utils/authFunction";
|
||||
import XEUtils from 'xe-utils';
|
||||
|
||||
|
||||
|
||||
@@ -27,8 +28,41 @@ export const createCrudOptions = function ({ crudExpose, props,modelDialog,selec
|
||||
form.menu = selectOptions.value.id;
|
||||
return await api.AddObj(form);
|
||||
};
|
||||
// 记录选中的行
|
||||
const selectedRows = ref<any>([]);
|
||||
|
||||
const onSelectionChange = (changed: any) => {
|
||||
const tableData = crudExpose.getTableData();
|
||||
const unChanged = tableData.filter((row: any) => !changed.includes(row));
|
||||
// 添加已选择的行
|
||||
XEUtils.arrayEach(changed, (item: any) => {
|
||||
const ids = XEUtils.pluck(selectedRows.value, 'id');
|
||||
if (!ids.includes(item.id)) {
|
||||
selectedRows.value = XEUtils.union(selectedRows.value, [item]);
|
||||
}
|
||||
});
|
||||
// 剔除未选择的行
|
||||
XEUtils.arrayEach(unChanged, (unItem: any) => {
|
||||
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== unItem.id);
|
||||
});
|
||||
};
|
||||
const toggleRowSelection = () => {
|
||||
// 多选后,回显默认勾选
|
||||
const tableRef = crudExpose.getBaseTableRef();
|
||||
const tableData = crudExpose.getTableData();
|
||||
const selected = XEUtils.filter(tableData, (item: any) => {
|
||||
const ids = XEUtils.pluck(selectedRows.value, 'id');
|
||||
return ids.includes(item.id);
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
XEUtils.arrayEach(selected, (item) => {
|
||||
tableRef.toggleRowSelection(item, true);
|
||||
});
|
||||
});
|
||||
};
|
||||
return {
|
||||
selectedRows,
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
@@ -77,7 +111,22 @@ export const createCrudOptions = function ({ crudExpose, props,modelDialog,selec
|
||||
width: '600px',
|
||||
},
|
||||
},
|
||||
table: {
|
||||
rowKey: 'id', //设置你的主键id, 默认rowKey=id
|
||||
onSelectionChange,
|
||||
onRefreshed: () => toggleRowSelection(),
|
||||
},
|
||||
columns: {
|
||||
$checked: {
|
||||
title: '选择',
|
||||
form: { show: false },
|
||||
column: {
|
||||
type: 'selection',
|
||||
align: 'center',
|
||||
width: '70px',
|
||||
columnSetDisabled: true, //禁止在列设置中选择
|
||||
},
|
||||
},
|
||||
_index: {
|
||||
title: '序号',
|
||||
form: { show: false },
|
||||
|
||||
@@ -1,137 +1,177 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog ref="modelRef" v-model="modelDialog" title="选择model">
|
||||
<div v-show="props.model">
|
||||
<el-tag>已选择:{{ props.model }}</el-tag>
|
||||
</div>
|
||||
<!-- 搜索输入框 -->
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索模型..."
|
||||
style="margin-bottom: 10px;"
|
||||
></el-input>
|
||||
<div class="model-card">
|
||||
<!--注释编号:django-vue3-admin-index483211: 对请求回来的allModelData进行computed计算,返加搜索框匹配到的内容-->
|
||||
<div v-for="(item,index) in filteredModelData" :value="item.key" :key="index">
|
||||
<el-text :type="modelCheckIndex===index?'primary':''" @click="onModelChecked(item,index)">
|
||||
{{ item.app + '--' + item.title + '(' + item.key + ')' }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="modelDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleAutomatch">
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<div style="height: 80vh">
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
</fs-crud>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-dialog ref="modelRef" v-model="modelDialog" title="选择model">
|
||||
<div v-show="props.model">
|
||||
<el-tag>已选择:{{ props.model }}</el-tag>
|
||||
</div>
|
||||
<!-- 搜索输入框 -->
|
||||
<el-input v-model="searchQuery" placeholder="搜索模型..." style="margin-bottom: 10px"></el-input>
|
||||
<div class="model-card">
|
||||
<!--注释编号:django-vue3-admin-index483211: 对请求回来的allModelData进行computed计算,返加搜索框匹配到的内容-->
|
||||
<div v-for="(item, index) in filteredModelData" :value="item.key" :key="index">
|
||||
<el-text :type="modelCheckIndex === index ? 'primary' : ''" @click="onModelChecked(item, index)">
|
||||
{{ item.app + '--' + item.title + '(' + item.key + ')' }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="modelDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleAutomatch"> 确定 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<div style="height: 72vh">
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<template #pagination-left>
|
||||
<el-tooltip content="批量删除">
|
||||
<el-button text type="danger" :disabled="selectedRowsCount === 0" :icon="Delete" circle @click="handleBatchDelete" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #pagination-right>
|
||||
<el-popover placement="top" :width="400" trigger="click">
|
||||
<template #reference>
|
||||
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
|
||||
</template>
|
||||
<el-table :data="selectedRows" size="small">
|
||||
<el-table-column width="150" property="id" label="id" />
|
||||
<el-table-column fixed="right" label="操作" min-width="60">
|
||||
<template #default="scope">
|
||||
<el-button text type="info" :icon="Close" @click="removeSelectedRows(scope.row)" circle />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-popover>
|
||||
</template>
|
||||
</fs-crud>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, onMounted, reactive, computed } from 'vue';
|
||||
import {useFs} from '@fast-crud/fast-crud';
|
||||
import {createCrudOptions} from './crud';
|
||||
import {getModelList} from './api'
|
||||
import {MenuTreeItemType} from "/@/views/system/menu/types";
|
||||
import {successMessage, successNotification, warningNotification} from '/@/utils/message';
|
||||
import {automatchColumnsData} from '/@/views/system/columns/components/ColumnsTableCom/api';
|
||||
import { ref, onMounted, reactive, computed } from 'vue';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import { createCrudOptions } from './crud';
|
||||
import { BatchDelete, getModelList } from './api';
|
||||
import { Close, Delete } from '@element-plus/icons-vue';
|
||||
import { MenuTreeItemType } from '/@/views/system/menu/types';
|
||||
import { successMessage, successNotification, warningNotification } from '/@/utils/message';
|
||||
import { automatchColumnsData } from '/@/views/system/columns/components/ColumnsTableCom/api';
|
||||
import XEUtils from 'xe-utils';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
// 当前选择的菜单信息
|
||||
let selectOptions: any = ref({name: null});
|
||||
let selectOptions: any = ref({ name: null });
|
||||
|
||||
const props = reactive({
|
||||
model: '',
|
||||
app: '',
|
||||
menu: ''
|
||||
})
|
||||
model: '',
|
||||
app: '',
|
||||
menu: '',
|
||||
});
|
||||
|
||||
//model弹窗
|
||||
const modelDialog = ref(false)
|
||||
const modelDialog = ref(false);
|
||||
// 获取所有model
|
||||
const allModelData = ref<any[]>([]);
|
||||
const modelCheckIndex = ref(null)
|
||||
const modelCheckIndex = ref(null);
|
||||
const onModelChecked = (row, index) => {
|
||||
modelCheckIndex.value = index
|
||||
props.model = row.key
|
||||
props.app = row.app
|
||||
}
|
||||
|
||||
modelCheckIndex.value = index;
|
||||
props.model = row.key;
|
||||
props.app = row.app;
|
||||
};
|
||||
|
||||
// 注释编号:django-vue3-admin-index083311:代码开始行
|
||||
// 功能说明:搭配搜索的处理,返回搜索结果
|
||||
const searchQuery = ref('');
|
||||
|
||||
const filteredModelData = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return allModelData.value;
|
||||
}
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return allModelData.value.filter(item =>
|
||||
item.app.toLowerCase().includes(query) ||
|
||||
item.title.toLowerCase().includes(query) ||
|
||||
item.key.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
if (!searchQuery.value) {
|
||||
return allModelData.value;
|
||||
}
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return allModelData.value.filter(
|
||||
(item) => item.app.toLowerCase().includes(query) || item.title.toLowerCase().includes(query) || item.key.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
// 注释编号:django-vue3-admin-index083311:代码结束行
|
||||
|
||||
|
||||
/**
|
||||
* 菜单选中时,加载表格数据
|
||||
* @param record
|
||||
*/
|
||||
const handleRefreshTable = (record: MenuTreeItemType) => {
|
||||
if (!record.is_catalog && record.id) {
|
||||
selectOptions.value = record;
|
||||
crudExpose.doRefresh();
|
||||
} else {
|
||||
//清空表格数据
|
||||
crudExpose.setTableData([]);
|
||||
}
|
||||
if (!record.is_catalog && record.id) {
|
||||
selectOptions.value = record;
|
||||
crudExpose.doRefresh();
|
||||
} else {
|
||||
//清空表格数据
|
||||
crudExpose.setTableData([]);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 自动匹配列
|
||||
*/
|
||||
const handleAutomatch = async () => {
|
||||
props.menu = selectOptions.value.id
|
||||
modelDialog.value = false
|
||||
if (props.menu && props.model) {
|
||||
const res = await automatchColumnsData(props);
|
||||
if (res?.code === 2000) {
|
||||
successNotification('匹配成功');
|
||||
}
|
||||
crudExpose.doSearch({form: {menu: props.menu, model: props.model}});
|
||||
}else {
|
||||
warningNotification('请选择角色和模型表!');
|
||||
}
|
||||
|
||||
props.menu = selectOptions.value.id;
|
||||
modelDialog.value = false;
|
||||
if (props.menu && props.model) {
|
||||
const res = await automatchColumnsData(props);
|
||||
if (res?.code === 2000) {
|
||||
successNotification('匹配成功');
|
||||
}
|
||||
crudExpose.doSearch({ form: { menu: props.menu, model: props.model } });
|
||||
} else {
|
||||
warningNotification('请选择角色和模型表!');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions, props, modelDialog, selectOptions,allModelData});
|
||||
onMounted(async () => {
|
||||
const res = await getModelList();
|
||||
allModelData.value = res.data;
|
||||
// 选中行的条数
|
||||
const selectedRowsCount = computed(() => {
|
||||
return selectedRows.value.length;
|
||||
});
|
||||
|
||||
defineExpose({selectOptions, handleRefreshTable});
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
await ElMessageBox.confirm(`确定要批量删除这${selectedRows.value.length}条记录吗`, '确认', {
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
closeOnClickModal: false,
|
||||
});
|
||||
await BatchDelete(XEUtils.pluck(selectedRows.value, 'id'));
|
||||
ElMessage.info('删除成功');
|
||||
selectedRows.value = [];
|
||||
await crudExpose.doRefresh();
|
||||
};
|
||||
|
||||
// 移除已选中的行
|
||||
const removeSelectedRows = (row: any) => {
|
||||
const tableRef = crudExpose.getBaseTableRef();
|
||||
const tableData = crudExpose.getTableData();
|
||||
if (XEUtils.pluck(tableData, 'id').includes(row.id)) {
|
||||
tableRef.toggleRowSelection(row, false);
|
||||
} else {
|
||||
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== row.id);
|
||||
}
|
||||
};
|
||||
|
||||
const { crudBinding, crudRef, crudExpose, selectedRows } = useFs({ createCrudOptions, props, modelDialog, selectOptions, allModelData });
|
||||
onMounted(async () => {
|
||||
const res = await getModelList();
|
||||
allModelData.value = res.data;
|
||||
});
|
||||
|
||||
defineExpose({ selectOptions, handleRefreshTable });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.model-card {
|
||||
margin-top: 10px;
|
||||
height: 30vh;
|
||||
overflow-y: scroll;
|
||||
margin-top: 10px;
|
||||
height: 30vh;
|
||||
overflow-y: scroll;
|
||||
|
||||
div {
|
||||
margin: 15px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
div {
|
||||
margin: 15px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,21 +10,12 @@
|
||||
<el-input v-model="menuFormData.name" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="父级菜单" prop="parent">
|
||||
<el-tree-select
|
||||
v-model="menuFormData.parent"
|
||||
:props="defaultTreeProps"
|
||||
:data="deptDefaultList"
|
||||
:cache-data="props.cacheData"
|
||||
lazy
|
||||
check-strictly
|
||||
clearable
|
||||
:load="handleTreeLoad"
|
||||
placeholder="请选择父级菜单"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<el-tree-select v-model="menuFormData.parent" :props="defaultTreeProps" :data="deptDefaultList"
|
||||
:cache-data="props.cacheData" lazy check-strictly clearable :load="handleTreeLoad"
|
||||
placeholder="请选择父级菜单" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="路由地址" prop="web_path">
|
||||
<el-form-item label="路由地址" prop="web_path">
|
||||
<el-input v-model="menuFormData.web_path" placeholder="请输入路由地址,请以/开头" />
|
||||
</el-form-item>
|
||||
|
||||
@@ -35,12 +26,14 @@
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item required label="状态">
|
||||
<el-switch v-model="menuFormData.status" width="60" inline-prompt active-text="启用" inactive-text="禁用" />
|
||||
<el-switch v-model="menuFormData.status" width="60" inline-prompt active-text="启用"
|
||||
inactive-text="禁用" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item v-if="menuFormData.status" required label="侧边显示">
|
||||
<el-switch v-model="menuFormData.visible" width="60" inline-prompt active-text="显示" inactive-text="隐藏" />
|
||||
<el-switch v-model="menuFormData.visible" width="60" inline-prompt active-text="显示"
|
||||
inactive-text="隐藏" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -48,46 +41,45 @@
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item required label="是否目录">
|
||||
<el-switch v-model="menuFormData.is_catalog" width="60" inline-prompt active-text="是" inactive-text="否" />
|
||||
<el-switch v-model="menuFormData.is_catalog" width="60" inline-prompt active-text="是"
|
||||
inactive-text="否" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item v-if="!menuFormData.is_catalog" required label="外链接">
|
||||
<el-switch v-model="menuFormData.is_link" width="60" inline-prompt active-text="是" inactive-text="否" />
|
||||
<el-switch v-model="menuFormData.is_link" width="60" inline-prompt active-text="是"
|
||||
inactive-text="否" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item required v-if="!menuFormData.is_catalog" label="是否固定">
|
||||
<el-switch v-model="menuFormData.is_affix" width="60" inline-prompt active-text="是"
|
||||
inactive-text="否" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" required label="是否内嵌">
|
||||
<el-switch v-model="menuFormData.is_iframe" width="60" inline-prompt active-text="是"
|
||||
inactive-text="否" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item required v-if="!menuFormData.is_catalog" label="是否固定">
|
||||
<el-switch v-model="menuFormData.is_affix" width="60" inline-prompt active-text="是" inactive-text="否" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" required label="是否内嵌">
|
||||
<el-switch v-model="menuFormData.is_iframe" width="60" inline-prompt active-text="是" inactive-text="否" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="menuFormData.description" maxlength="200" show-word-limit type="textarea" placeholder="请输入备注" />
|
||||
<el-input v-model="menuFormData.description" maxlength="200" show-word-limit type="textarea"
|
||||
placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<div style="min-height: 184px">
|
||||
<el-form-item v-if="!menuFormData.is_catalog && !menuFormData.is_link" label="组件地址" prop="component">
|
||||
<el-autocomplete
|
||||
class="w-full"
|
||||
v-model="menuFormData.component"
|
||||
:fetch-suggestions="querySearch"
|
||||
:trigger-on-focus="false"
|
||||
clearable
|
||||
:debounce="100"
|
||||
placeholder="输入组件地址"
|
||||
/>
|
||||
<el-autocomplete class="w-full" v-model="menuFormData.component" :fetch-suggestions="querySearch"
|
||||
:trigger-on-focus="false" clearable :debounce="100" placeholder="输入组件地址" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="!menuFormData.is_catalog && !menuFormData.is_link" label="组件名称" prop="component_name">
|
||||
<el-form-item v-if="!menuFormData.is_catalog && !menuFormData.is_link" label="组件名称"
|
||||
prop="component_name">
|
||||
<el-input v-model="menuFormData.component_name" placeholder="请输入组件名称" />
|
||||
</el-form-item>
|
||||
|
||||
@@ -96,7 +88,8 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="!menuFormData.is_catalog" label="缓存">
|
||||
<el-switch v-model="menuFormData.cache" width="60" inline-prompt active-text="启用" inactive-text="禁用" />
|
||||
<el-switch v-model="menuFormData.cache" width="60" inline-prompt active-text="启用"
|
||||
inactive-text="禁用" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
@@ -111,6 +104,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XEUtils from 'xe-utils';
|
||||
import { ref, onMounted, reactive } from 'vue';
|
||||
import { ElForm, FormRules } from 'element-plus';
|
||||
import IconSelector from '/@/components/iconSelector/index.vue';
|
||||
@@ -148,14 +142,14 @@ const validateWebPath = (rule: any, value: string, callback: Function) => {
|
||||
};
|
||||
|
||||
const validateLinkUrl = (rule: any, value: string, callback: Function) => {
|
||||
let pattern = /^\/.*?/;
|
||||
let patternUrl = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
|
||||
const reg = pattern.test(value) || patternUrl.test(value)
|
||||
if (reg) {
|
||||
callback();
|
||||
} else {
|
||||
callback(new Error('请输入正确的地址'));
|
||||
}
|
||||
let pattern = /^\/.*?/;
|
||||
let patternUrl = /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/;
|
||||
const reg = pattern.test(value) || patternUrl.test(value)
|
||||
if (reg) {
|
||||
callback();
|
||||
} else {
|
||||
callback(new Error('请输入正确的地址'));
|
||||
}
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
@@ -172,7 +166,7 @@ const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '菜单名称必填', trigger: 'blur' }],
|
||||
component: [{ required: true, message: '请输入组件地址', trigger: 'blur' }],
|
||||
component_name: [{ required: true, message: '请输入组件名称', trigger: 'blur' }],
|
||||
link_url: [{ required: true, message: '请输入外链接地址',validator:validateLinkUrl, trigger: 'blur' }],
|
||||
link_url: [{ required: true, message: '请输入外链接地址', validator: validateLinkUrl, trigger: 'blur' }],
|
||||
});
|
||||
|
||||
let deptDefaultList = ref<MenuTreeItemType[]>([]);
|
||||
@@ -189,9 +183,9 @@ let menuFormData = reactive<MenuFormDataType>({
|
||||
description: '',
|
||||
is_catalog: false,
|
||||
is_link: false,
|
||||
is_iframe: false,
|
||||
is_affix: false,
|
||||
link_url:''
|
||||
is_iframe: false,
|
||||
is_affix: false,
|
||||
link_url: ''
|
||||
});
|
||||
let menuBtnLoading = ref(false);
|
||||
|
||||
@@ -210,9 +204,9 @@ const setMenuFormData = () => {
|
||||
menuFormData.description = props.initFormData?.description || '';
|
||||
menuFormData.is_catalog = !!props.initFormData.is_catalog;
|
||||
menuFormData.is_link = !!props.initFormData.is_link;
|
||||
menuFormData.is_iframe =!!props.initFormData.is_iframe;
|
||||
menuFormData.is_affix =!!props.initFormData.is_affix;
|
||||
menuFormData.link_url =props.initFormData.link_url;
|
||||
menuFormData.is_iframe = !!props.initFormData.is_iframe;
|
||||
menuFormData.is_affix = !!props.initFormData.is_affix;
|
||||
menuFormData.link_url = props.initFormData.link_url;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -246,7 +240,7 @@ const createFilter = (queryString: string) => {
|
||||
const handleTreeLoad = (node: Node, resolve: Function) => {
|
||||
if (node.level !== 0) {
|
||||
lazyLoadMenu({ parent: node.data.id }).then((res: APIResponseData) => {
|
||||
resolve(res.data);
|
||||
resolve(XEUtils.filter(res.data, (i: MenuTreeItemType) => i.is_catalog));
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -278,9 +272,14 @@ const handleCancel = (type: string = '') => {
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
onMounted(async () => {
|
||||
props.treeData.map((item) => {
|
||||
deptDefaultList.value.push(item);
|
||||
if (item.is_catalog) {
|
||||
deptDefaultList.value.push(item);
|
||||
}
|
||||
});
|
||||
setMenuFormData();
|
||||
});
|
||||
@@ -290,6 +289,7 @@ onMounted(async () => {
|
||||
.menu-form-com {
|
||||
margin: 10px;
|
||||
overflow-y: auto;
|
||||
|
||||
.menu-form-alert {
|
||||
color: #fff;
|
||||
line-height: 24px;
|
||||
@@ -298,6 +298,7 @@ onMounted(async () => {
|
||||
border-radius: 4px;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.menu-form-btns {
|
||||
padding-bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
<el-col :span="18">
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane label="按钮权限配置" >
|
||||
<div style="height: 80vh">
|
||||
<div style="height: 72vh">
|
||||
<MenuButtonCom ref="menuButtonRef" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="列权限配置">
|
||||
<div style="height: 80vh">
|
||||
<div style="height: 72vh">
|
||||
<MenuFieldCom ref="menuFieldRef"></MenuFieldCom>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
@@ -138,7 +138,7 @@ onMounted(() => {
|
||||
.menu-box {
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
background-color: var(--el-fill-color-blank);;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,5 +65,5 @@ export interface MenuFormDataType {
|
||||
is_link: boolean;
|
||||
is_iframe:boolean;
|
||||
is_affix:boolean;
|
||||
link_url: string;
|
||||
link_url: string|undefined;
|
||||
}
|
||||
|
||||
@@ -1,46 +1,38 @@
|
||||
import * as api from './api';
|
||||
import {dict, useCompute, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions} from '@fast-crud/fast-crud';
|
||||
import { dict, useCompute, PageQuery, AddReq, DelReq, EditReq, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
|
||||
import tableSelector from '/@/components/tableSelector/index.vue';
|
||||
import {shallowRef, computed, ref, inject} from 'vue';
|
||||
import { shallowRef, computed } from 'vue';
|
||||
import manyToMany from '/@/components/manyToMany/index.vue';
|
||||
import {auth} from '/@/utils/authFunction'
|
||||
import {createCrudOptions as userCrudOptions } from "/@/views/system/user/crud";
|
||||
import {request} from '/@/utils/service'
|
||||
const {compute} = useCompute();
|
||||
import { auth } from '/@/utils/authFunction';
|
||||
const { compute } = useCompute();
|
||||
|
||||
interface CreateCrudOptionsTypes {
|
||||
crudOptions: CrudOptions;
|
||||
}
|
||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const { tabActivted } = context; //从context中获取tabActivted
|
||||
|
||||
export const createCrudOptions = function ({
|
||||
crudExpose,
|
||||
tabActivted
|
||||
}: { crudExpose: CrudExpose; tabActivted: any }): CreateCrudOptionsTypes {
|
||||
const pageRequest = async (query: PageQuery) => {
|
||||
if (tabActivted.value === 'receive') {
|
||||
return await api.GetSelfReceive(query);
|
||||
}
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({form, row}: EditReq) => {
|
||||
form.id = row.id;
|
||||
return await api.UpdateObj(form);
|
||||
};
|
||||
const delRequest = async ({row}: DelReq) => {
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
const addRequest = async ({form}: AddReq) => {
|
||||
return await api.AddObj(form);
|
||||
};
|
||||
const pageRequest = async (query: PageQuery) => {
|
||||
if (tabActivted.value === 'receive') {
|
||||
return await api.GetSelfReceive(query);
|
||||
}
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const editRequest = async ({ form, row }: EditReq) => {
|
||||
form.id = row.id;
|
||||
return await api.UpdateObj(form);
|
||||
};
|
||||
const delRequest = async ({ row }: DelReq) => {
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
const addRequest = async ({ form }: AddReq) => {
|
||||
return await api.AddObj(form);
|
||||
};
|
||||
|
||||
const viewRequest = async ({row}: { row: any }) => {
|
||||
return await api.GetObj(row.id);
|
||||
};
|
||||
|
||||
const IsReadFunc = computed(() => {
|
||||
return tabActivted.value === 'receive';
|
||||
});
|
||||
const viewRequest = async ({ row }: { row: any }) => {
|
||||
return await api.GetObj(row.id);
|
||||
};
|
||||
|
||||
const IsReadFunc = computed(() => {
|
||||
return tabActivted.value === 'receive';
|
||||
});
|
||||
|
||||
return {
|
||||
crudOptions: {
|
||||
@@ -50,27 +42,27 @@ export const createCrudOptions = function ({
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
actionbar:{
|
||||
buttons:{
|
||||
add:{
|
||||
show:computed(() =>{
|
||||
actionbar: {
|
||||
buttons: {
|
||||
add: {
|
||||
show: computed(() => {
|
||||
return tabActivted.value !== 'receive' && auth('messageCenter:Create');
|
||||
})
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
rowHandle: {
|
||||
fixed:'right',
|
||||
width:150,
|
||||
fixed: 'right',
|
||||
width: 150,
|
||||
buttons: {
|
||||
edit: {
|
||||
show: false,
|
||||
},
|
||||
view: {
|
||||
text:"查看",
|
||||
type:'text',
|
||||
iconRight:'View',
|
||||
show:auth("messageCenter:Search"),
|
||||
text: '查看',
|
||||
type: 'text',
|
||||
iconRight: 'View',
|
||||
show: auth('messageCenter:Search'),
|
||||
click({ index, row }) {
|
||||
crudExpose.openView({ index, row });
|
||||
if (tabActivted.value === 'receive') {
|
||||
@@ -82,7 +74,7 @@ export const createCrudOptions = function ({
|
||||
remove: {
|
||||
iconRight: 'Delete',
|
||||
type: 'text',
|
||||
show:auth('messageCenter:Delete')
|
||||
show: auth('messageCenter:Delete'),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -99,7 +91,7 @@ export const createCrudOptions = function ({
|
||||
show: true,
|
||||
},
|
||||
type: ['text', 'colspan'],
|
||||
column:{
|
||||
column: {
|
||||
minWidth: 120,
|
||||
},
|
||||
form: {
|
||||
@@ -132,7 +124,7 @@ export const createCrudOptions = function ({
|
||||
target_type: {
|
||||
title: '目标类型',
|
||||
type: ['dict-radio', 'colspan'],
|
||||
column:{
|
||||
column: {
|
||||
minWidth: 120,
|
||||
},
|
||||
dict: dict({
|
||||
@@ -285,7 +277,7 @@ export const createCrudOptions = function ({
|
||||
name: shallowRef(tableSelector),
|
||||
vModel: 'modelValue',
|
||||
displayLabel: compute(({ form }) => {
|
||||
return form.target_dept_name;
|
||||
return form.dept_info;
|
||||
}),
|
||||
tableConfig: {
|
||||
url: '/api/system/dept/all_dept/',
|
||||
@@ -297,7 +289,7 @@ export const createCrudOptions = function ({
|
||||
{
|
||||
prop: 'name',
|
||||
label: '部门名称',
|
||||
width: 150,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
prop: 'status_label',
|
||||
@@ -349,7 +341,7 @@ export const createCrudOptions = function ({
|
||||
},
|
||||
],
|
||||
component: {
|
||||
disabled: true,
|
||||
disabled: false,
|
||||
id: '1', // 当同一个页面有多个editor时,需要配置不同的id
|
||||
editorConfig: {
|
||||
// 是否只读
|
||||
@@ -373,4 +365,4 @@ export const createCrudOptions = function ({
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,43 +1,35 @@
|
||||
<template>
|
||||
<fs-page>
|
||||
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<template #header-middle>
|
||||
<el-tabs v-model="tabActivted" @tab-click="onTabClick">
|
||||
<el-tab-pane label="我的发布" name="send"></el-tab-pane>
|
||||
<el-tab-pane label="我的接收" name="receive"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</fs-crud>
|
||||
</fs-page>
|
||||
<fs-page>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding">
|
||||
<template #header-middle>
|
||||
<el-tabs v-model="tabActivted" @tab-click="onTabClick">
|
||||
<el-tab-pane label="我的发布" name="send"></el-tab-pane>
|
||||
<el-tab-pane label="我的接收" name="receive"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</fs-crud>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="messageCenter">
|
||||
import {ref, onMounted} from 'vue';
|
||||
import {useExpose, useCrud} from '@fast-crud/fast-crud';
|
||||
import {createCrudOptions} from './crud';
|
||||
// crud组件的ref
|
||||
const crudRef = ref();
|
||||
// crud 配置的ref
|
||||
const crudBinding = ref();
|
||||
// 暴露的方法
|
||||
const {crudExpose} = useExpose({crudRef, crudBinding});
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import createCrudOptions from './crud';
|
||||
|
||||
//tab选择
|
||||
const tabActivted = ref('send')
|
||||
const onTabClick= (tab:any)=> {
|
||||
const { paneName } = tab
|
||||
tabActivted.value = paneName
|
||||
crudExpose.doRefresh();
|
||||
}
|
||||
// 你的crud配置
|
||||
const {crudOptions} = createCrudOptions({crudExpose,tabActivted});
|
||||
const tabActivted = ref('send');
|
||||
const onTabClick = (tab: any) => {
|
||||
const { paneName } = tab;
|
||||
tabActivted.value = paneName;
|
||||
crudExpose.doRefresh();
|
||||
};
|
||||
|
||||
const context: any = { tabActivted }; //将 tabActivted 通过context传递给crud.tsx
|
||||
// 初始化crud配置
|
||||
const {resetCrudOptions} = useCrud({crudExpose, crudOptions});
|
||||
const { crudRef, crudBinding, crudExpose } = useFs({ createCrudOptions, context });
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
crudExpose.doRefresh();
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<el-col :xs="24" :sm="24" class="personal-item mb6">
|
||||
<div class="personal-item-label">角色:</div>
|
||||
<div class="personal-item-value">
|
||||
<el-tag v-for="(item, index) in state.personalForm.role_info" :key="index">{{ item.name }}</el-tag>
|
||||
<el-tag v-for="(item, index) in state.personalForm.role_info" :key="index" style="margin-right: 5px;">{{ item.name }}</el-tag>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -153,7 +153,7 @@
|
||||
center
|
||||
>
|
||||
<el-form-item label="原密码" required prop="oldPassword">
|
||||
<el-input v-model="userPasswordInfo.oldPassword" placeholder="请输入原始密码" clearable></el-input>
|
||||
<el-input type="password" v-model="userPasswordInfo.oldPassword" placeholder="请输入原始密码" show-password clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item required prop="newPassword" label="新密码">
|
||||
<el-input type="password" v-model="userPasswordInfo.newPassword" placeholder="请输入新密码" show-password clearable></el-input>
|
||||
@@ -354,7 +354,8 @@ const uploadImg = (data: any) => {
|
||||
api.uploadAvatar(formdata).then((res: any) => {
|
||||
if (res.code === 2000) {
|
||||
selectImgVisible.value = false;
|
||||
state.personalForm.avatar = getBaseURL() + res.data.url;
|
||||
// state.personalForm.avatar = getBaseURL() + res.data.url;
|
||||
state.personalForm.avatar = res.data.url;
|
||||
api.updateUserInfo(state.personalForm).then((res: any) => {
|
||||
successMessage('更新成功');
|
||||
getUserInfo();
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { request } from "/@/utils/service";
|
||||
|
||||
/**
|
||||
* 获取角色的授权列表
|
||||
* @param roleId
|
||||
* @param query
|
||||
*/
|
||||
export function getRolePremission(query:object) {
|
||||
return request({
|
||||
url: '/api/system/role_menu_button_permission/get_role_premission/',
|
||||
method: 'get',
|
||||
params:query
|
||||
})
|
||||
}
|
||||
|
||||
/***
|
||||
* 设置角色的权限
|
||||
* @param roleId
|
||||
* @param data
|
||||
*/
|
||||
export function setRolePremission(roleId:any,data:object) {
|
||||
return request({
|
||||
url: `/api/system/role_menu_button_permission/${roleId}/set_role_premission/`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getDataPermissionRange() {
|
||||
return request({
|
||||
url: '/api/system/role_menu_button_permission/data_scope/',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
export function getDataPermissionDept() {
|
||||
return request({
|
||||
url: '/api/system/role_menu_button_permission/role_to_dept_all/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getDataPermissionMenu() {
|
||||
return request({
|
||||
url: '/api/system/role_menu_button_permission/get_role_permissions/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置按钮的数据范围
|
||||
*/
|
||||
export function setBtnDatarange(roleId:number,data:object) {
|
||||
return request({
|
||||
url: `/api/system/role_menu_button_permission/${roleId}/set_btn_datarange/`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
<template>
|
||||
<el-drawer v-model="drawerVisible" title="权限配置" direction="rtl" size="60%" :close-on-click-modal="false"
|
||||
:before-close="handleDrawerClose" :destroy-on-close="true">
|
||||
<template #header>
|
||||
<el-row>
|
||||
<el-col :span="4">
|
||||
<div>当前授权角色:
|
||||
<el-tag>{{ props.roleName }}</el-tag>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div>
|
||||
<el-button size="small" type="primary" class="pc-save-btn" @click="handleSavePermission">保存菜单授权
|
||||
</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<div class="permission-com">
|
||||
<el-tabs>
|
||||
<el-tab-pane v-for="(item, mIndex) in menuData" :key="mIndex" :label="item.name">
|
||||
<el-tabs tab-position="left">
|
||||
<el-tab-pane v-for="(menu, mIndex) in item.menus" :key="mIndex" :label="menu.name" >
|
||||
<el-checkbox v-model="menu.isCheck">页面显示权限</el-checkbox>
|
||||
<div class="pc-collapse-main">
|
||||
<div class="pccm-item">
|
||||
<div class="menu-form-alert"> 配置操作功能接口权限,配置数据权限点击小齿轮 </div>
|
||||
<el-checkbox v-for="(btn, bIndex) in menu.btns" :key="bIndex" v-model="btn.isCheck"
|
||||
:label="btn.value">
|
||||
<div class="btn-item">
|
||||
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
|
||||
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(menu, btn.id)">
|
||||
<el-icon>
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="pccm-item" v-if="menu.columns && menu.columns.length > 0">
|
||||
<div class="menu-form-alert"> 配置数据列字段权限 </div>
|
||||
<ul class="columns-list">
|
||||
<li class="columns-head">
|
||||
<div class="width-txt">
|
||||
<span>字段</span>
|
||||
</div>
|
||||
<div v-for="(head, hIndex) in column.header" :key="hIndex" class="width-check">
|
||||
<el-checkbox :label="head.value" @change="handleColumnChange($event, menu, head.value)">
|
||||
<span>{{ head.label }}</span>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li v-for="(c_item, c_index) in menu.columns" :key="c_index" class="columns-item">
|
||||
<div class="width-txt">{{ c_item.title }}</div>
|
||||
<div v-for="(col, cIndex) in column.header" :key="cIndex" class="width-check">
|
||||
<el-checkbox v-model="c_item[col.value]" class="ci-checkout"></el-checkbox>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="数据权限配置" width="400px" :close-on-click-modal="false"
|
||||
:before-close="handleDialogClose">
|
||||
<div class="pc-dialog">
|
||||
<el-select v-model="dataPermission" @change="handlePermissionRangeChange" class="dialog-select"
|
||||
placeholder="请选择">
|
||||
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<el-tree-select v-show="dataPermission === 4" node-key="id" v-model="customDataPermission"
|
||||
:props="defaultTreeProps" :data="deptData" multiple check-strictly :render-after-expand="false"
|
||||
show-checkbox class="dialog-tree" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
|
||||
<el-button @click="handleDialogClose"> 取消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, defineProps, watch, computed, reactive } from 'vue';
|
||||
import XEUtils from 'xe-utils';
|
||||
import { errorNotification } from '/@/utils/message';
|
||||
import {
|
||||
getDataPermissionRange,
|
||||
getDataPermissionDept,
|
||||
getRolePremission,
|
||||
setRolePremission,
|
||||
setBtnDatarange
|
||||
} from './api';
|
||||
import { MenuDataType, MenusType, DataPermissionRangeType, CustomDataPermissionDeptType } from './types';
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
roleId: {
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
roleName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
drawerVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:drawerVisible'])
|
||||
|
||||
const drawerVisible = ref(false)
|
||||
watch(
|
||||
() => props.drawerVisible,
|
||||
(val) => {
|
||||
drawerVisible.value = val;
|
||||
getMenuBtnPermission()
|
||||
fetchData()
|
||||
}
|
||||
);
|
||||
const handleDrawerClose = () => {
|
||||
emit('update:drawerVisible', false);
|
||||
}
|
||||
|
||||
|
||||
const defaultTreeProps = {
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
};
|
||||
|
||||
let menuData = ref<MenuDataType[]>([]);
|
||||
let collapseCurrent = ref<number[]>([]);
|
||||
let menuCurrent = ref<Partial<MenuDataType>>({});
|
||||
let menuBtnCurrent = ref<number>(-1);
|
||||
let dialogVisible = ref(false);
|
||||
let dataPermissionRange = ref<DataPermissionRangeType[]>([]);
|
||||
const formatDataRange = computed(() => {
|
||||
return function (datarange: number) {
|
||||
const findItem = dataPermissionRange.value.find((i) => i.value === datarange);
|
||||
return findItem?.label || ''
|
||||
}
|
||||
})
|
||||
let deptData = ref<CustomDataPermissionDeptType[]>([]);
|
||||
let dataPermission = ref();
|
||||
let customDataPermission = ref([]);
|
||||
//获取菜单,按钮,权限
|
||||
const getMenuBtnPermission = async () => {
|
||||
const resMenu = await getRolePremission({ role: props.roleId })
|
||||
menuData.value = resMenu.data
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const resRange = await getDataPermissionRange();
|
||||
if (resRange?.code === 2000) {
|
||||
dataPermissionRange.value = resRange.data;
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollapseChange = (val: number) => {
|
||||
collapseCurrent.value = [val];
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置按钮数据权限
|
||||
* @param record 当前菜单
|
||||
* @param btnType 按钮类型
|
||||
*/
|
||||
const handleSettingClick = (record: MenusType, btnId: number) => {
|
||||
menuCurrent.value = record;
|
||||
menuBtnCurrent.value = btnId;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleColumnChange = (val: boolean, record: MenusType, btnType: string) => {
|
||||
for (const iterator of record.columns) {
|
||||
iterator[btnType] = val;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePermissionRangeChange = async (val: number) => {
|
||||
if (val === 4) {
|
||||
const res = await getDataPermissionDept();
|
||||
const data = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false });
|
||||
deptData.value = data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 数据权限设置确认
|
||||
*/
|
||||
const handleDialogConfirm = () => {
|
||||
if (dataPermission.value !== 0 && !dataPermission.value) {
|
||||
errorNotification('请选择');
|
||||
return;
|
||||
}
|
||||
|
||||
//if (dataPermission.value !== 4) {}
|
||||
for (const item of menuData.value) {
|
||||
for (const iterator of item.menus) {
|
||||
if (iterator.id === menuCurrent.value.id) {
|
||||
for (const btn of iterator.btns) {
|
||||
if (btn.id === menuBtnCurrent.value) {
|
||||
const findItem = dataPermissionRange.value.find((i) => i.value === dataPermission.value);
|
||||
btn.data_range = findItem?.value || 0;
|
||||
if (btn.data_range === 4) {
|
||||
btn.dept = customDataPermission.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
handleDialogClose();
|
||||
};
|
||||
const handleDialogClose = () => {
|
||||
dialogVisible.value = false;
|
||||
customDataPermission.value = [];
|
||||
dataPermission.value = null;
|
||||
};
|
||||
|
||||
//保存权限
|
||||
const handleSavePermission = () => {
|
||||
setRolePremission(props.roleId, menuData.value).then((res: any) => {
|
||||
ElMessage({
|
||||
message: res.msg,
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const column = reactive({
|
||||
header: [{ value: 'is_create', label: '新增可见' }, { value: 'is_update', label: '编辑可见' }, {
|
||||
value: 'is_query',
|
||||
label: '列表可见'
|
||||
}]
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.permission-com {
|
||||
margin: 15px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.pc-save-btn {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.pc-collapse-title {
|
||||
line-height: 32px;
|
||||
text-align: left;
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.pc-collapse-main {
|
||||
padding-top: 15px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.pccm-item {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.menu-form-alert {
|
||||
color: #fff;
|
||||
line-height: 24px;
|
||||
padding: 8px 16px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.btn-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.columns-list {
|
||||
.width-txt {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.width-check {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.width-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.columns-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
box-sizing: border-box;
|
||||
|
||||
span {
|
||||
font-weight: 900;
|
||||
}
|
||||
}
|
||||
|
||||
.columns-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
.ci-checkout {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pc-dialog {
|
||||
.dialog-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-tree {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.permission-com {
|
||||
.el-collapse {
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.el-collapse-item {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.el-collapse-item__header {
|
||||
height: auto;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
border-left: 1px solid #ebeef5;
|
||||
border-right: 1px solid #ebeef5;
|
||||
box-sizing: border-box;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.el-collapse-item__header.is-active {
|
||||
border-radius: 8px 8px 0 0;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
padding: 15px;
|
||||
border-left: 1px solid #ebeef5;
|
||||
border-right: 1px solid #ebeef5;
|
||||
border-top: 1px solid #ebeef5;
|
||||
border-radius: 0 0 8px 8px;
|
||||
background-color: #fafafa;
|
||||
box-sizing: border-box;
|
||||
|
||||
.el-collapse-item__content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,36 +0,0 @@
|
||||
export interface DataPermissionRangeType {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface CustomDataPermissionDeptType {
|
||||
id: number;
|
||||
name: string;
|
||||
patent: number;
|
||||
children: CustomDataPermissionDeptType[]
|
||||
}
|
||||
|
||||
export interface CustomDataPermissionMenuType {
|
||||
id: number;
|
||||
name: string;
|
||||
is_catalog: boolean;
|
||||
menuPermission: { id: number; name: string; value: string }[] | null;
|
||||
columns: { id: number; name: string; title: string }[] | null;
|
||||
children: CustomDataPermissionMenuType[]
|
||||
}
|
||||
|
||||
export interface MenusType{
|
||||
id: string;
|
||||
name: string;
|
||||
isCheck: boolean;
|
||||
radio: string;
|
||||
btns: { id:number,name: string; value: string; isCheck: boolean; data_range: number; dept:object; name:string }[];
|
||||
columns: { [key: string]: boolean | string; }[]
|
||||
}
|
||||
|
||||
export interface MenuDataType {
|
||||
id: string;
|
||||
name: string;
|
||||
menus:MenusType[];
|
||||
}
|
||||
|
||||
76
web/src/views/system/role/components/RoleDrawer.vue
Normal file
76
web/src/views/system/role/components/RoleDrawer.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="RoleDrawer.drawerVisible"
|
||||
title="权限配置"
|
||||
direction="rtl"
|
||||
size="80%"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="RoleDrawer.handleDrawerClose"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<template #header>
|
||||
<div>
|
||||
当前授权角色:
|
||||
<el-tag style="margin-right: 20px">{{ RoleDrawer.roleName }}</el-tag>
|
||||
授权人员:
|
||||
<el-button size="small" :icon="UserFilled" @click="handleUsers">{{ RoleDrawer.users.length }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<splitpanes class="default-theme" style="height: 100%">
|
||||
<pane min-size="20" size="22">
|
||||
<div class="pane-box">
|
||||
<MenuTreeCom />
|
||||
</div>
|
||||
</pane>
|
||||
<pane min-size="20">
|
||||
<div class="pane-box">
|
||||
<el-tabs v-model="activeName" class="demo-tabs">
|
||||
<el-tab-pane label="接口权限" name="first"><MenuBtnCom /></el-tab-pane>
|
||||
<el-tab-pane label="列字段权限" name="second"><MenuFieldCom /></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</pane>
|
||||
</splitpanes>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="授权用户" width="700px" :close-on-click-modal="false">
|
||||
<RoleUsersCom />
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
import 'splitpanes/dist/splitpanes.css';
|
||||
import { UserFilled } from '@element-plus/icons-vue';
|
||||
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import { RoleUsersStores } from '../stores/RoleUsersStores';
|
||||
|
||||
const MenuTreeCom = defineAsyncComponent(() => import('./RoleMenuTree.vue'));
|
||||
const MenuBtnCom = defineAsyncComponent(() => import('./RoleMenuBtn.vue'));
|
||||
const MenuFieldCom = defineAsyncComponent(() => import('./RoleMenuField.vue'));
|
||||
const RoleUsersCom = defineAsyncComponent(() => import('./RoleUsers.vue'));
|
||||
const RoleDrawer = RoleDrawerStores(); // 抽屉参数
|
||||
const RoleUsers = RoleUsersStores(); // 角色-用户
|
||||
const activeName = ref('first');
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
const handleUsers = () => {
|
||||
dialogVisible.value = true;
|
||||
RoleUsers.get_all_users(); // 获取所有用户
|
||||
RoleUsers.set_right_users(RoleDrawer.$state.users); // 设置已选中用户
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pane-box {
|
||||
width: 100vw; /* 视口宽度 */
|
||||
height: 100vh; /* 视口高度 */
|
||||
max-width: 100%; /* 确保不超过父元素的宽度 */
|
||||
max-height: 100%; /* 确保不超过父元素的高度 */
|
||||
overflow: auto; /* 当内容超出容器尺寸时显示滚动条 */
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
186
web/src/views/system/role/components/RoleMenuBtn.vue
Normal file
186
web/src/views/system/role/components/RoleMenuBtn.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0">
|
||||
<div class="menu-form-alert">配置操作功能接口权限,配置数据权限点击小齿轮</div>
|
||||
<el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck" @change="handleCheckChange(btn)">
|
||||
<div class="btn-item">
|
||||
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }}
|
||||
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)">
|
||||
<el-icon>
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<el-dialog v-model="dialogVisible" title="数据权限配置" width="400px" :close-on-click-modal="false" :before-close="handleDialogClose">
|
||||
<div class="pc-dialog">
|
||||
<el-select v-model="selectBtn.data_range" @change="handlePermissionRangeChange" placeholder="请选择">
|
||||
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<el-tree-select
|
||||
v-show="selectBtn.data_range === 4"
|
||||
node-key="id"
|
||||
v-model="selectBtn.dept"
|
||||
:props="defaultTreeProps"
|
||||
:data="deptData"
|
||||
multiple
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
show-checkbox
|
||||
class="dialog-tree"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
|
||||
<el-button @click="handleDialogClose"> 取消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
|
||||
import { RoleMenuBtnStores } from '../stores/RoleMenuBtnStores';
|
||||
import { RoleMenuTreeStores } from '../stores/RoleMenuTreeStores';
|
||||
import { RoleMenuBtnType } from '../types';
|
||||
import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api';
|
||||
import XEUtils from 'xe-utils';
|
||||
import { ElMessage } from 'element-plus';
|
||||
const RoleDrawer = RoleDrawerStores(); // 角色-菜单
|
||||
const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
|
||||
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
|
||||
const dialogVisible = ref(false);
|
||||
// 选中的按钮
|
||||
const selectBtn = ref<RoleMenuBtnType>({
|
||||
id: 0,
|
||||
menu_btn_pre_id: 0,
|
||||
/** 是否选中 */
|
||||
isCheck: false,
|
||||
/** 按钮名称 */
|
||||
name: '',
|
||||
/** 数据权限范围 */
|
||||
data_range: 0,
|
||||
dept: [],
|
||||
});
|
||||
/**
|
||||
* 数据权限范围
|
||||
*/
|
||||
const dataPermissionRange = ref([
|
||||
{ label: '仅本人数据权限', value: 0 },
|
||||
{ label: '本部门及以下数据权限', value: 1 },
|
||||
{ label: '本部门数据权限', value: 2 },
|
||||
{ label: '全部数据权限', value: 3 },
|
||||
{ label: '自定数据权限', value: 4 },
|
||||
]);
|
||||
/**
|
||||
* 自定义数据权限的部门树配置
|
||||
*/
|
||||
const defaultTreeProps = {
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定数据权限下拉选择事件
|
||||
*/
|
||||
const handlePermissionRangeChange = async (val: number) => {
|
||||
if (val < 4) {
|
||||
selectBtn.value.dept = [];
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 格式化按钮数据范围
|
||||
*/
|
||||
const formatDataRange = computed(() => {
|
||||
return function (datarange: number) {
|
||||
const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => {
|
||||
if (item.value === datarange) {
|
||||
return item.label;
|
||||
}
|
||||
});
|
||||
return datarangeitem.label;
|
||||
};
|
||||
});
|
||||
/**
|
||||
* 勾选按钮
|
||||
*/
|
||||
const handleCheckChange = async (btn: RoleMenuBtnType) => {
|
||||
const put_data = {
|
||||
isCheck: btn.isCheck,
|
||||
roleId: RoleDrawer.roleId,
|
||||
menuId: RoleMenuTree.id,
|
||||
btnId: btn.id,
|
||||
};
|
||||
const { data, msg } = await setRoleMenuBtn(put_data);
|
||||
RoleMenuBtn.updateState(data);
|
||||
ElMessage({ message: msg, type: 'success' });
|
||||
};
|
||||
|
||||
/**
|
||||
* 按钮-数据范围确定
|
||||
*/
|
||||
const handleDialogConfirm = async () => {
|
||||
const { data, msg } = await setRoleMenuBtnDataRange(selectBtn.value);
|
||||
selectBtn.value = data;
|
||||
dialogVisible.value = false;
|
||||
ElMessage({ message: msg, type: 'success' });
|
||||
};
|
||||
/**
|
||||
* 数据范围关闭
|
||||
*/
|
||||
const handleDialogClose = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 齿轮点击
|
||||
*/
|
||||
const handleSettingClick = async (btn: RoleMenuBtnType) => {
|
||||
selectBtn.value = btn;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 部门数据
|
||||
*
|
||||
*/
|
||||
const deptData = ref<number[]>([]);
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(async () => {
|
||||
const res = await getRoleToDeptAll({ role: RoleDrawer.roleId, menu_button: selectBtn.value.id });
|
||||
const depts = XEUtils.toArrayTree(res.data, { parentKey: 'parent', strict: false });
|
||||
deptData.value = depts;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pccm-item {
|
||||
margin-bottom: 10px;
|
||||
.menu-form-alert {
|
||||
color: #fff;
|
||||
line-height: 24px;
|
||||
padding: 8px 16px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
// .el-checkbox {
|
||||
// width: 200px;
|
||||
// }
|
||||
.btn-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center; /* 水平居中 */
|
||||
.el-icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
.dialog-tree {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
118
web/src/views/system/role/components/RoleMenuField.vue
Normal file
118
web/src/views/system/role/components/RoleMenuField.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="pccm-item" v-if="RoleMenuField.$state.length > 0">
|
||||
<div class="menu-form-alert">
|
||||
<el-button size="small" @click="handleSaveField">保存 </el-button>
|
||||
配置数据列字段权限
|
||||
</div>
|
||||
|
||||
<ul class="columns-list">
|
||||
<li class="columns-head">
|
||||
<div class="width-txt">
|
||||
<span>字段</span>
|
||||
</div>
|
||||
<div v-for="(head, hIndex) in RoleMenuFieldHeader.$state" :key="hIndex" class="width-check">
|
||||
<el-checkbox v-model="head.checked" @change="handleColumnChange($event, head.value, head.disabled)">
|
||||
<span>{{ head.label }}</span>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</li>
|
||||
<div class="columns-content">
|
||||
<li v-for="(c_item, c_index) in RoleMenuField.$state" :key="c_index" class="columns-item">
|
||||
<div class="width-txt">{{ c_item.title }}</div>
|
||||
<div v-for="(col, cIndex) in RoleMenuFieldHeader.$state" :key="cIndex" class="width-check">
|
||||
<el-checkbox v-model="c_item[col.value]" class="ci-checkout" :disabled="c_item[col.disabled]"></el-checkbox>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
|
||||
import { RoleMenuFieldStores, RoleMenuFieldHeaderStores } from '../stores/RoleMenuFieldStores';
|
||||
import { setRoleMenuField } from './api';
|
||||
const RoleMenuField = RoleMenuFieldStores();
|
||||
const RoleMenuFieldHeader = RoleMenuFieldHeaderStores();
|
||||
const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
|
||||
/** 全选 */
|
||||
const handleColumnChange = (val: boolean, btnType: string, disabledType: string) => {
|
||||
for (const iterator of RoleMenuField.$state) {
|
||||
iterator[btnType] = iterator[disabledType] ? iterator[btnType] : val;
|
||||
}
|
||||
};
|
||||
const handleSaveField = async () => {
|
||||
const res = await setRoleMenuField(RoleDrawer.$state.roleId, RoleMenuField.$state);
|
||||
ElMessage({ message: res.msg, type: 'success' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pccm-item {
|
||||
margin-bottom: 10px;
|
||||
.menu-form-alert {
|
||||
color: #fff;
|
||||
line-height: 24px;
|
||||
padding: 8px 16px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
.menu-form-btn {
|
||||
margin-left: 10px;
|
||||
height: 40px;
|
||||
padding: 8px 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.columns-list {
|
||||
.width-txt {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.width-check {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.width-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.columns-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
box-sizing: border-box;
|
||||
|
||||
span {
|
||||
font-weight: 900;
|
||||
}
|
||||
}
|
||||
.columns-content {
|
||||
max-height: calc(100vh - 240px); /* 视口高度 */
|
||||
overflow-y: auto; /* 当内容超出高度时显示垂直滚动条 */
|
||||
.columns-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
.ci-checkout {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
web/src/views/system/role/components/RoleMenuTree.vue
Normal file
81
web/src/views/system/role/components/RoleMenuTree.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="menuData"
|
||||
:props="defaultTreeProps"
|
||||
:default-checked-keys="menuDefaultCheckedKeys"
|
||||
@check-change="handleMenuChange"
|
||||
@node-click="handleMenuClick"
|
||||
node-key="id"
|
||||
check-strictly
|
||||
highlight-current
|
||||
show-checkbox
|
||||
default-expand-all
|
||||
>
|
||||
</el-tree>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
|
||||
import { RoleMenuTreeStores } from '../stores/RoleMenuTreeStores';
|
||||
import { RoleMenuBtnStores } from '../stores/RoleMenuBtnStores';
|
||||
import { RoleMenuFieldStores } from '../stores/RoleMenuFieldStores';
|
||||
import { RoleMenuFieldHeaderStores } from '../stores/RoleMenuFieldStores';
|
||||
import { getRoleMenu, getRoleMenuBtnField, setRoleMenu } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import XEUtils from 'xe-utils';
|
||||
import { RoleMenuTreeType } from '../types';
|
||||
|
||||
const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
|
||||
const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
|
||||
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
|
||||
const RoleMenuField = RoleMenuFieldStores(); // 角色-菜单-列字段
|
||||
const RoleMenuFieldHeader = RoleMenuFieldHeaderStores();// 角色-菜单-列字段
|
||||
const menuData = ref<RoleMenuTreeType[]>([]); // 菜单列表数据
|
||||
const menuDefaultCheckedKeys = ref<(number | string | undefined)[]>([]); // 默认选中的菜单列表
|
||||
// 菜单配置
|
||||
const defaultTreeProps = {
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
};
|
||||
|
||||
/**
|
||||
* 菜单复选框选中
|
||||
* @param node:当前节点的 Node 对象
|
||||
* @param checked:布尔值,表示当前节点是否被选中
|
||||
*/
|
||||
const handleMenuChange = (node: any, checked: boolean) => {
|
||||
setRoleMenu({ roleId: RoleDrawer.roleId, menuId: node.id, isCheck: checked }).then((res: any) => {
|
||||
ElMessage({ message: res.msg, type: 'success' });
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 菜单点击事件
|
||||
*/
|
||||
const handleMenuClick = async (selectNode: RoleMenuTreeType) => {
|
||||
if (!selectNode.is_catalog) {
|
||||
RoleMenuTree.setRoleMenuTree(selectNode); // 更新当前选中的菜单
|
||||
// 获取当前菜单的按钮列表
|
||||
const { data } = await getRoleMenuBtnField({
|
||||
roleId: RoleDrawer.roleId,
|
||||
menuId: selectNode.id,
|
||||
});
|
||||
RoleMenuBtn.setState(data.menu_btn); // 更新按钮列表
|
||||
RoleMenuField.setState(data.menu_field); // 更新列字段列表
|
||||
} else {
|
||||
RoleMenuBtn.setState([]); // 更新按钮列表
|
||||
RoleMenuField.setState([]); // 更新列字段列表
|
||||
}
|
||||
RoleMenuFieldHeader.$reset()
|
||||
};
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(async () => {
|
||||
menuData.value = await getRoleMenu({ roleId: RoleDrawer.roleId });
|
||||
menuDefaultCheckedKeys.value = XEUtils.toTreeArray(menuData.value)
|
||||
.filter((i) => i.isCheck)
|
||||
.map((i) => i.id);
|
||||
});
|
||||
</script>
|
||||
35
web/src/views/system/role/components/RoleUsers.vue
Normal file
35
web/src/views/system/role/components/RoleUsers.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<el-transfer
|
||||
v-model="RoleUsers.$state.right_users"
|
||||
filterable
|
||||
:titles="['未授权用户', '已授权用户']"
|
||||
:data="RoleUsers.$state.all_users"
|
||||
:props="{
|
||||
key: 'id',
|
||||
label: 'name',
|
||||
}"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { RoleDrawerStores } from '../stores/RoleDrawerStores';
|
||||
import { RoleUsersStores } from '../stores/RoleUsersStores';
|
||||
import { setRoleUsers } from './api';
|
||||
const RoleDrawer = RoleDrawerStores(); // 抽屉参数
|
||||
const RoleUsers = RoleUsersStores(); // 角色-用户
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value 当前右侧选中的用户
|
||||
* @param direction 移动的方向
|
||||
* @param movedKeys 移动的用户
|
||||
*/
|
||||
const handleChange = (value: number[] | string[], direction: 'left' | 'right', movedKeys: string[] | number[]) => {
|
||||
setRoleUsers(RoleDrawer.$state.roleId, { direction, movedKeys }).then((res:any) => {
|
||||
RoleDrawer.set_state(res.data)
|
||||
ElMessage({ message: res.msg, type: 'success' });
|
||||
});
|
||||
};
|
||||
</script>
|
||||
121
web/src/views/system/role/components/api.ts
Normal file
121
web/src/views/system/role/components/api.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { request } from '/@/utils/service';
|
||||
import XEUtils from 'xe-utils';
|
||||
/**
|
||||
* 获取 角色-菜单
|
||||
* @param query
|
||||
*/
|
||||
export function getRoleMenu(query: object) {
|
||||
return request({
|
||||
url: '/api/system/role_menu_button_permission/get_role_menu/',
|
||||
method: 'get',
|
||||
params: query,
|
||||
}).then((res: any) => {
|
||||
return XEUtils.toArrayTree(res.data, { key: 'id', parentKey: 'parent', children: 'children', strict: false });
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 设置 角色-菜单
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export function setRoleMenu(data: object) {
|
||||
return request({
|
||||
url: '/api/system/role_menu_button_permission/set_role_menu/',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取 角色-菜单-按钮-列字段
|
||||
* @param query
|
||||
*/
|
||||
export function getRoleMenuBtnField(query: object) {
|
||||
return request({
|
||||
url: '/api/system/role_menu_button_permission/get_role_menu_btn_field/',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 角色-菜单-按钮
|
||||
* @param data
|
||||
*/
|
||||
export function setRoleMenuBtn(data: object) {
|
||||
return request({
|
||||
url: '/api/system/role_menu_button_permission/set_role_menu_btn/',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 角色-菜单-列字段
|
||||
* @param data
|
||||
*/
|
||||
export function setRoleMenuField(roleId: string | number | undefined, data: object) {
|
||||
return request({
|
||||
url: `/api/system/role_menu_button_permission/${roleId}/set_role_menu_field/`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 角色-菜单-按钮-数据权限
|
||||
* @param query
|
||||
* @returns
|
||||
*/
|
||||
export function setRoleMenuBtnDataRange(data: object) {
|
||||
return request({
|
||||
url: '/api/system/role_menu_button_permission/set_role_menu_btn_data_range/',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户角色下所能授权的部门
|
||||
* @param query
|
||||
* @returns
|
||||
*/
|
||||
export function getRoleToDeptAll(query: object) {
|
||||
return request({
|
||||
url: '/api/system/role_menu_button_permission/role_to_dept_all/',
|
||||
method: 'get',
|
||||
params: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有用户
|
||||
* @param query
|
||||
* @returns
|
||||
*/
|
||||
export function getAllUsers() {
|
||||
return request({
|
||||
url: '/api/system/user/',
|
||||
method: 'get',
|
||||
params: { limit: 999 },
|
||||
}).then((res: any) => {
|
||||
return XEUtils.map(res.data, (item: any) => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置角色-用户
|
||||
* @param query
|
||||
* @returns
|
||||
*/
|
||||
export function setRoleUsers(roleId: string | number | undefined, data: object) {
|
||||
return request({
|
||||
url: `/api/system/role/${roleId}/set_role_users/`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user