40 Commits
1.0.0 ... 1.0.1

Author SHA1 Message Date
H0nGzA1
967e4e76c2 Merge remote-tracking branch 'origin/master' 2023-04-14 23:08:56 +08:00
H0nGzA1
f7b93c349e fix: 🐛 部门删除bug 2023-04-14 23:04:30 +08:00
H0nGzA1
72f1d2eae7 feat: 用户管理细节优化 2023-04-14 23:04:30 +08:00
H0nGzA1
592bc947a0 feat: 账号锁定功能 2023-04-14 23:04:30 +08:00
H0nGzA1
953fcc3437 fix: 🐛 修复添加用户bug 2023-04-14 23:04:30 +08:00
H0nGzA1
13aa6dbb99 fix: 🐛 头像更新问题 2023-04-14 23:04:30 +08:00
猿小天
460ae171dd fix(文件上传): 🐛 文件上传 2023-04-14 23:04:30 +08:00
H0nGzA1
0d556bfb2b fix: 🐛 头像上传,文件上传问题 2023-04-14 23:04:29 +08:00
猿小天
61ae6e8eb3 refactor(所有页面): ♻️ 所有页面的按钮权限配置 2023-04-14 23:04:29 +08:00
猿小天
175b151f7f refactor(所有页面): ♻️ 所有页面的表格列宽优化 2023-04-14 23:04:29 +08:00
H0nGzA1
a50f73d466 refactor: ♻️ 重构部门管理前端 2023-04-14 23:04:29 +08:00
猿小天
eea9d320af fix(登录): 登录优化 2023-04-14 23:04:29 +08:00
猿小天
15c4808bbb refactor(登录页面): ♻️ 登录优化 2023-04-14 23:04:29 +08:00
H0nGzA1
d2d6ba3460 refactor: ♻️ 重构菜单管理前端 2023-04-14 23:04:29 +08:00
猿小天
4f0295acb1 refactor(用户管理): ♻️ 用户管理优化 2023-04-14 23:04:29 +08:00
H0nGzA1
255c405e59 feat: 前端自动注册插件功能 2023-04-14 23:04:29 +08:00
H0nGzA1
96ad956efd refactor: ♻️ 更新优化精简fast-crud结构 2023-04-14 23:04:29 +08:00
H0nGzA1
b586b46016 feat: 更新fast-crud版本以及依赖 2023-04-14 23:04:29 +08:00
H0nGzA1
875146e588 feat: 所有菜单页面状态改为可编辑开关 2023-04-14 23:04:29 +08:00
H0nGzA1
1ee709b9eb feat: 所有菜单页面样式优化~ 2023-04-14 23:04:29 +08:00
H0nGzA1
f89a5228cd refactor: ♻️ 菜单管理重构完成 2023-04-14 23:04:29 +08:00
H0nGzA1
795f621637 feat: 精简化初始化菜单json文件 2023-04-14 23:04:29 +08:00
H0nGzA1
c5d7a70f46 feat: 登录页面,自动刷新验证码 2023-04-14 23:04:29 +08:00
H0nGzA1
8710b047b1 feat: 登录页面回车登录功能 2023-04-14 23:04:28 +08:00
H0nGzA1
300b6c6bb8 refactor: ♻️ 菜单管理重构完成 2023-04-14 23:04:28 +08:00
raymond
296640cb2a feat: 支持通过python manage.py createsuperuser创建管理员用户 2023-04-14 23:04:28 +08:00
H0nGzA1
24861fda42 refactor: ♻️ 用户管理重构 2023-04-14 23:04:28 +08:00
H0nGzA1
0e3fac37e9 feat: 🚀 更新nginx配置文件 2023-04-14 23:04:28 +08:00
H0nGzA1
93d8d94049 feat: 🔧 tailwind css 配置 2023-04-14 23:04:28 +08:00
victory
9d4f007d48 feat:添加对url中排序参数的解析 2023-04-14 23:04:28 +08:00
victory
4d17a7d9df fix:优化角色管理用户管理和接口白名单的页面显示 2023-04-14 23:04:28 +08:00
李强
c126c704a4 新功能: 删除gunicorn.pid 2023-03-31 10:31:38 +08:00
李强
662c314518 Merge remote-tracking branch 'origin/master' 2023-03-31 10:31:03 +08:00
李强
1715fcb4d8 新功能: 完善日志信息 2023-03-31 10:30:39 +08:00
H0nGzA1
a304a49107 feat: 引入tailwind css样式 2023-03-23 18:08:41 +08:00
H0nGzA1
fc1ab98b2b build: ⬆️ 更新fast-crud版本 2023-03-23 12:44:09 +08:00
H0nGzA1
92f57f8608 build: ⬆️ 更新fast-crud版本 2023-03-22 21:08:43 +08:00
H0nGzA1
5ab2aaa066 fix(修复登录界面验证码关闭issue): 🐛 https://gitee.com/huge-dream/django-vue3-admin/issues/I6OS75 2023-03-22 19:56:54 +08:00
H0nGzA1
b5f50bdf30 fix(修复登录界面验证码关闭issue): 🐛 https://gitee.com/huge-dream/django-vue3-admin/issues/I6OS75 2023-03-21 18:01:42 +08:00
H0nGzA1
256c6e4ab9 fix(修复issue): 🐛 https://gitee.com/huge-dream/django-vue3-admin/issues/I6OS1E 2023-03-21 13:43:24 +08:00
80 changed files with 5639 additions and 10286 deletions

View File

@@ -2,7 +2,7 @@
[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.7.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark)](https://gitee.com/liqianglog/django-vue-admin)
[preview](https://demo.django-vue-admin.com) | [Official website document](https://www.django-vue-admin.com) | [qq group](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [community](https://bbs.django-vue-admin.com) | [plugins market](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
[preview](https://demo.dvadmin.com) | [Official website document](https://www.django-vue-admin.com) | [qq group](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [community](https://bbs.django-vue-admin.com) | [plugins market](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
💡 **「About」**

View File

@@ -2,7 +2,7 @@
[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.7.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark)](https://gitee.com/liqianglog/django-vue-admin)
[预 览](https://demo.django-vue-admin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
[预 览](https://demo.dvadmin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)

1
backend/.gitignore vendored
View File

@@ -97,3 +97,4 @@ db.sqlite3
media/
__pypackages__/
package-lock.json
gunicorn.pid

View File

@@ -261,6 +261,7 @@ LOGGING = {
REST_FRAMEWORK = {
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.MultiPartParser',
),
"DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", # 日期时间格式配置
"DATE_FORMAT": "%Y-%m-%d",

View File

@@ -6,8 +6,8 @@
"is_link": false,
"is_catalog": true,
"web_path": "/system",
"component": "layout/routerView/parent",
"component_name": "menu",
"component": "",
"component_name": "",
"status": true,
"cache": false,
"visible": true,
@@ -25,7 +25,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"parent": 41,
"children": [],
"menu_button": [
{
@@ -72,7 +72,7 @@
"status": true,
"cache": false,
"visible": false,
"parent": 19,
"parent": 41,
"children": [],
"menu_button": [
{
@@ -113,7 +113,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"parent": 41,
"children": [],
"menu_button": [
{
@@ -160,7 +160,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"parent": 41,
"children": [],
"menu_button": [
{
@@ -213,7 +213,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"parent": 41,
"children": [],
"menu_button": [
{
@@ -284,7 +284,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"parent": 41,
"children": [],
"menu_button": [
{
@@ -331,7 +331,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 19,
"parent": 41,
"children": [],
"menu_button": [
{
@@ -376,8 +376,8 @@
"is_link": false,
"is_catalog": true,
"web_path": "/generalConfig",
"component": "layout/routerView/parent",
"component_name": "config",
"component": "",
"component_name": "",
"status": true,
"cache": false,
"visible": true,
@@ -395,7 +395,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 27,
"parent": 49,
"children": [],
"menu_button": [
{
@@ -442,7 +442,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 27,
"parent": 49,
"children": [],
"menu_button": [
{
@@ -489,7 +489,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 27,
"parent": 49,
"children": [],
"menu_button": [
{
@@ -536,7 +536,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 27,
"parent": 49,
"children": [],
"menu_button": [
{
@@ -575,8 +575,8 @@
"is_link": false,
"is_catalog": true,
"web_path": "/log",
"component": "layout/routerView/parent",
"component_name": "log",
"component": "",
"component_name": "",
"status": true,
"cache": false,
"visible": true,
@@ -594,7 +594,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 32,
"parent": 54,
"children": [],
"menu_button": [
{
@@ -623,7 +623,7 @@
"status": true,
"cache": false,
"visible": true,
"parent": 32,
"parent": 54,
"children": [],
"menu_button": [
{

View File

@@ -1,9 +1,9 @@
import hashlib
import os
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import AbstractUser, UserManager
from django.db import models
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from application import dispatch
from dvadmin.utils.models import CoreModel, table_prefix
@@ -13,6 +13,34 @@ STATUS_CHOICES = (
)
class Role(CoreModel):
name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
status = models.BooleanField(default=True, verbose_name="角色状态", help_text="角色状态")
admin = models.BooleanField(default=False, verbose_name="是否为admin", help_text="是否为admin")
class Meta:
db_table = table_prefix + "system_role"
verbose_name = "角色表"
verbose_name_plural = verbose_name
ordering = ("sort",)
class CustomUserManager(UserManager):
def create_superuser(self, username, email=None, password=None, **extra_fields):
user = super(CustomUserManager, self).create_superuser(username, email, password, **extra_fields)
user.set_password(password)
try:
user.role.add(Role.objects.get(name="管理员"))
user.save(using=self._db)
return user
except ObjectDoesNotExist:
user.delete()
raise ValidationError("角色`管理员`不存在, 创建失败, 请先执行python manage.py init")
class Users(CoreModel, AbstractUser):
username = models.CharField(max_length=150, unique=True, db_index=True, verbose_name="用户账号",
help_text="用户账号")
@@ -48,6 +76,7 @@ class Users(CoreModel, AbstractUser):
blank=True,
help_text="关联部门",
)
objects = CustomUserManager()
def set_password(self, raw_password):
super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest())
@@ -76,20 +105,6 @@ class Post(CoreModel):
ordering = ("sort",)
class Role(CoreModel):
name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称")
key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符")
sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序")
status = models.BooleanField(default=True, verbose_name="角色状态", help_text="角色状态")
admin = models.BooleanField(default=False, verbose_name="是否为admin", help_text="是否为admin")
class Meta:
db_table = table_prefix + "system_role"
verbose_name = "角色表"
verbose_name_plural = verbose_name
ordering = ("sort",)
class Dept(CoreModel):
name = models.CharField(max_length=64, verbose_name="部门名称", help_text="部门名称")
key = models.CharField(max_length=64, unique=True, null=True, blank=True, verbose_name="关联字符",
@@ -339,7 +354,7 @@ class OperationLog(CoreModel):
def media_file_name(instance, filename):
h = instance.md5sum
basename, ext = os.path.splitext(filename)
return os.path.join("files", h[0:1], h[1:2], h + ext.lower())
return os.path.join("files", h[:1], h[1:2], h + ext.lower())
class FileList(CoreModel):

View File

@@ -123,6 +123,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

View File

@@ -1,5 +1,4 @@
from rest_framework import serializers
from dvadmin.system.models import FileList
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@@ -9,7 +8,7 @@ class FileSerializer(CustomModelSerializer):
url = serializers.SerializerMethodField(read_only=True)
def get_url(self, instance):
return 'media/' + str(instance.url)
return f'media/{str(instance.url)}'
class Meta:
model = FileList

View File

@@ -1,11 +1,9 @@
import base64
import hashlib
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 make_password, check_password
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from drf_yasg import openapi
@@ -14,9 +12,7 @@ from rest_framework import serializers
from rest_framework.views import APIView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
from django.conf import settings
from application import dispatch
from dvadmin.system.models import Users
from dvadmin.utils.json_response import ErrorResponse, DetailResponse
@@ -58,6 +54,7 @@ class LoginSerializer(TokenObtainPairSerializer):
captcha = serializers.CharField(
max_length=6, required=False, allow_null=True, allow_blank=True
)
class Meta:
model = Users
fields = "__all__"
@@ -79,13 +76,18 @@ class LoginSerializer(TokenObtainPairSerializer):
raise CustomValidationError("验证码过期")
else:
if self.image_code and (
self.image_code.response == captcha
or self.image_code.challenge == captcha
self.image_code.response == captcha
or self.image_code.challenge == captcha
):
self.image_code and self.image_code.delete()
else:
self.image_code and self.image_code.delete()
raise CustomValidationError("图片验证码错误")
user = Users.objects.get(username=attrs['username'])
if not user.is_active:
raise CustomValidationError("账号被锁定")
data = super().validate(attrs)
data["name"] = self.user.name
data["userId"] = self.user.id
@@ -107,6 +109,7 @@ class LoginSerializer(TokenObtainPairSerializer):
save_login_log(request=request)
return {"code": 2000, "msg": "请求成功", "data": data}
class LoginView(TokenObtainPairView):
"""
登录接口

View File

@@ -53,9 +53,6 @@ class MenuCreateSerializer(CustomModelSerializer):
read_only_fields = ["id"]
class WebRouterSerializer(CustomModelSerializer):
"""
前端菜单路由的简单序列化器
@@ -63,11 +60,11 @@ class WebRouterSerializer(CustomModelSerializer):
path = serializers.CharField(source="web_path")
title = serializers.CharField(source="name")
class Meta:
model = Menu
fields = ('id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component',
'component_name', 'cache', 'visible')
fields = (
'id', 'parent', 'icon', 'sort', 'path', 'name', 'title', 'is_link', 'is_catalog', 'web_path', 'component',
'component_name', 'cache', 'visible', 'status')
read_only_fields = ["id"]
@@ -86,6 +83,7 @@ class MenuViewSet(CustomModelViewSet):
update_serializer_class = MenuCreateSerializer
search_fields = ['name', 'status']
filter_fields = ['parent', 'name', 'status', 'is_link', 'visible', 'cache', 'is_catalog']
# extra_filter_class = []
@action(methods=['GET'], detail=False, permission_classes=[])
@@ -101,12 +99,36 @@ class MenuViewSet(CustomModelViewSet):
data = serializer.data
return SuccessResponse(data=data, total=len(data), msg="获取成功")
def list(self,request):
@action(methods=['GET'], detail=False, permission_classes=[])
def get_all_menu(self, request):
"""用于菜单管理获取所有的菜单"""
user = request.user
queryset = self.queryset.all()
if not user.is_superuser:
role_list = user.role.values_list('id', flat=True)
menu_list = RoleMenuPermission.objects.filter(role__in=role_list).values_list('menu_id')
queryset = Menu.objects.filter(id__in=menu_list)
serializer = WebRouterSerializer(queryset, many=True, request=request)
data = serializer.data
return SuccessResponse(data=data, total=len(data), msg="获取成功")
@action(methods=['POST'], detail=False, permission_classes=[])
def drag_menu(self, request):
"""前端拖拽菜单之后重写parent"""
menu_id = request.data['menu_id']
parent_id = request.data['parent_id']
menu = Menu.objects.get(id=menu_id)
menu.parent_id = parent_id
menu.save()
return SuccessResponse(data=[], msg="拖拽菜单成功")
def list(self, request):
"""懒加载"""
request.query_params._mutable = True
params = request.query_params
parent = params.get('parent', None)
page = params.get('page',None)
page = params.get('page', None)
limit = params.get('limit', None)
if page:
del params['page']
@@ -114,11 +136,11 @@ class MenuViewSet(CustomModelViewSet):
del params['limit']
if params:
if parent:
queryset = self.queryset.filter(status=1, parent=parent)
queryset = self.queryset.filter(parent=parent)
else:
queryset = self.queryset.filter(status=1)
queryset = self.queryset.filter()
else:
queryset = self.queryset.filter(status=1, parent__isnull=True)
queryset = self.queryset.filter(parent__isnull=True)
queryset = self.filter_queryset(queryset)
serializer = MenuSerializer(queryset, many=True, request=request)
data = serializer.data

View File

@@ -85,7 +85,7 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
def get_is_read(self, instance):
user_id = self.request.user.id
message_center_id = instance.id
queryset = MessageCenterTargetUser.objects.filter(messagecenter__id=message_center_id,users_id=user_id).first()
queryset = MessageCenterTargetUser.objects.filter(messagecenter__id=message_center_id, users_id=user_id).first()
if queryset:
return queryset.is_read
return False
@@ -95,21 +95,22 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
fields = "__all__"
read_only_fields = ["id"]
def websocket_push(user_id, message):
"""
主动推送消息
"""
username = "user_"+str(user_id)
print(103,message)
username = "user_" + str(user_id)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
username,
{
"type": "push.message",
"json": message
}
username,
{
"type": "push.message",
"json": message
}
)
class MessageCenterCreateSerializer(CustomModelSerializer):
"""
消息中心-新增-序列化器
@@ -122,10 +123,10 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
# 在保存之前,根据目标类型,把目标用户查询出来并保存
users = initial_data.get('target_user', [])
if target_type in [1]: # 按角色
target_role = initial_data.get('target_role',[])
target_role = initial_data.get('target_role', [])
users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True)
if target_type in [2]: # 按部门
target_dept = initial_data.get('target_dept',[])
target_dept = initial_data.get('target_dept', [])
users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True)
if target_type in [3]: # 系统通知
users = Users.objects.values_list('id', flat=True)
@@ -141,7 +142,7 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
for user in users:
unread_count = MessageCenterTargetUser.objects.filter(users__id=user, is_read=False).count()
websocket_push(user, message={"sender": 'system', "contentType": 'SYSTEM',
"content": '您有一条新消息~', "unread": unread_count})
"content": '您有一条新消息~', "unread": unread_count})
return data
class Meta:
@@ -184,7 +185,7 @@ class MessageCenterViewSet(CustomModelViewSet):
# 主动推送消息
unread_count = MessageCenterTargetUser.objects.filter(users__id=user_id, is_read=False).count()
websocket_push(user_id, message={"sender": 'system', "contentType": 'TEXT',
"content": '您查看了一条消息~', "unread": unread_count})
"content": '您查看了一条消息~', "unread": unread_count})
return DetailResponse(data=serializer.data, msg="获取成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
@@ -195,7 +196,6 @@ class MessageCenterViewSet(CustomModelViewSet):
self_user_id = self.request.user.id
# queryset = MessageCenterTargetUser.objects.filter(users__id=self_user_id).order_by('-create_datetime')
queryset = MessageCenter.objects.filter(target_user__id=self_user_id)
print(queryset)
# queryset = self.filter_queryset(queryset)
page = self.paginate_queryset(queryset)
if page is not None:

View File

@@ -61,10 +61,10 @@ class SystemConfigChinldernSerializer(CustomModelSerializer):
"""
系统配置子级-序列化器
"""
chinldern = serializers.SerializerMethodField()
children = serializers.SerializerMethodField()
form_item_type_label = serializers.CharField(source='get_form_item_type_display', read_only=True)
def get_chinldern(self, instance):
def get_children(self, instance):
queryset = SystemConfig.objects.filter(parent=instance)
serializer = SystemConfigSerializer(queryset, many=True)
return serializer.data

View File

@@ -41,6 +41,7 @@ class UserSerializer(CustomModelSerializer):
exclude = ["password"]
extra_kwargs = {
"post": {"required": False},
"mobile": {"required": False},
}
def get_dept_name_all(self, instance):
@@ -60,9 +61,6 @@ class UserSerializer(CustomModelSerializer):
return serializer.data
class UserCreateSerializer(CustomModelSerializer):
"""
用户新增-序列化器
@@ -82,10 +80,10 @@ class UserCreateSerializer(CustomModelSerializer):
"""
对密码进行验证
"""
password = self.initial_data.get("password")
if password:
return make_password(value)
return value
md5 = hashlib.md5()
md5.update(value.encode('utf-8'))
md5_password = md5.hexdigest()
return make_password(md5_password)
def save(self, **kwargs):
data = super().save(**kwargs)
@@ -100,6 +98,7 @@ class UserCreateSerializer(CustomModelSerializer):
read_only_fields = ["id"]
extra_kwargs = {
"post": {"required": False},
"mobile": {"required": False},
}
@@ -114,14 +113,15 @@ class UserUpdateSerializer(CustomModelSerializer):
CustomUniqueValidator(queryset=Users.objects.all(), message="账号必须唯一")
],
)
# password = serializers.CharField(required=False, allow_blank=True)
mobile = serializers.CharField(
max_length=50,
validators=[
CustomUniqueValidator(queryset=Users.objects.all(), message="手机号必须唯一")
],
allow_blank=True
)
# mobile = serializers.CharField(
# max_length=50,
# validators=[
# CustomUniqueValidator(queryset=Users.objects.all(), message="手机号必须唯一")
# ],
# allow_blank=True
# )
def save(self, **kwargs):
data = super().save(**kwargs)
@@ -136,6 +136,7 @@ class UserUpdateSerializer(CustomModelSerializer):
fields = "__all__"
extra_kwargs = {
"post": {"required": False, "read_only": True},
"mobile": {"required": False},
}
@@ -159,6 +160,7 @@ class UserInfoUpdateSerializer(CustomModelSerializer):
fields = ['email', 'mobile', 'avatar', 'name', 'gender']
extra_kwargs = {
"post": {"required": False, "read_only": True},
"mobile": {"required": False},
}
@@ -293,6 +295,11 @@ class UserViewSet(CustomModelViewSet):
'dept_id': dept.id,
'dept_name': dept.name
}
else:
result['dept_info'] = {
'dept_id': None,
'dept_name': "暂无部门"
}
role = getattr(user, 'role', None)
if role:
result['role_info'] = role.values('id', 'name', 'key')

View File

@@ -38,12 +38,16 @@ def CustomExceptionHandler(ex, context):
# 调用默认的异常处理函数
response = exception_handler(ex, context)
if isinstance(ex, AuthenticationFailed):
code = 401
code_type = response.data.get('detail').code
if code_type == 'no_active_account':
code=400
# 如果是身份验证错误
if response and response.data.get('detail') == "Given token not valid for any token type":
code = 401
msg = ex.detail
elif response and response.data.get('detail') == "Token is blacklisted":
# token在黑名单
return ErrorResponse(status=HTTP_401_UNAUTHORIZED)
msg = ex.detail
else:
code = 401
msg = ex.detail
elif isinstance(ex,Http404):
code = 400
msg = "接口地址不正确"

View File

@@ -1,28 +1,37 @@
import logging
import os.path
from logging import LogRecord
import sys
from django.core.servers.basehttp import WSGIRequestHandler
from django.db import connection
from loguru import logger
from logging.handlers import RotatingFileHandler
# 1.🎖先声明一个类继承logging.Handler(制作一件品如的衣服)
from loguru._defaults import LOGURU_FORMAT
from application.dispatch import is_tenants_mode
class InterceptTimedRotatingFileHandler(RotatingFileHandler):
class InterceptTimedRotatingFileHandler(RotatingFileHandler, logging.Filter):
"""
自定义反射时间回滚日志记录器
缺少命名空间
"""
def __init__(self, filename, when='d', interval=1, backupCount=5, encoding="utf-8", delay=False, utc=False,
maxBytes=1024 * 1024 * 100, atTime=None, logging_levels="all"):
maxBytes=1024 * 1024 * 100, atTime=None, logging_levels="all", format=None):
super(InterceptTimedRotatingFileHandler, self).__init__(filename)
filename = os.path.abspath(filename)
# 定义默认格式
if not format:
format = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <green>{extra[ip]}:{extra[port]}</green> | <level>{level: <8}</level>| <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>"
when = when.lower()
# 2.🎖️需要本地用不同的文件名做为不同日志的筛选器
logger.configure(
handlers=[
dict(sink=sys.stderr, format=format),
],
)
self.logger_ = logger.bind(sime=filename, ip="-", port="-", username="张三")
self.filename = filename
key_map = {
@@ -74,7 +83,7 @@ class InterceptTimedRotatingFileHandler(RotatingFileHandler):
# self.logger_.remove(file_key[filename_fmt_key])
self.logger_.add(
filename_fmt,
# format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <green>{extra[ip]}:{extra[port]}</green> | <level>{level: <8}</level>| <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
format=format,
retention=retention,
encoding=encoding,
level=self.level,
@@ -100,13 +109,17 @@ class InterceptTimedRotatingFileHandler(RotatingFileHandler):
# 设置自定义属性
port = "-"
ip = "-"
locals_self = frame.f_locals.get('self', None)
details = frame.f_locals.get('details', None)
msg = self.format(record)
if locals_self and hasattr(locals_self, 'client_address'):
ip, port = locals_self.client_address
# - 127.0.0.1:56525 -
msg = f"{ip}:{port} - {msg}"
bind = {}
if details and details.get('client'):
ip, port = details.get('client').split(':')
if is_tenants_mode():
bind["schema_name"] = connection.tenant.schema_name
bind["domain_url"] = getattr(connection.tenant, 'domain_url', None)
bind["ip"] = ip
bind["port"] = port
self.logger_ \
.opt(depth=depth, exception=record.exc_info, colors=True) \
.bind(ip=ip, port=port) \
.bind(**bind) \
.log(level, msg)

View File

@@ -1 +0,0 @@
7

View File

@@ -28,6 +28,6 @@ server {
proxy_send_timeout 600s;
real_ip_header X-Forwarded-For;
rewrite ^/api/(.*)$ /$1 break; #重写
proxy_pass http://177.8.0.12:8000/; # 设置代理服务器的协议和地址
proxy_pass http://178.10.0.12:8000/; # 设置代理服务器的协议和地址
}
}

6818
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,14 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.0.10",
"@fast-crud/fast-crud": "^1.9.0",
"@fast-crud/fast-extends": "^1.9.0",
"@fast-crud/ui-element": "^1.9.0",
"@fast-crud/fast-crud": "^1.11.10",
"@fast-crud/fast-extends": "^1.11.10",
"@fast-crud/ui-element": "^1.11.10",
"@fast-crud/ui-interface": "^1.11.9",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@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",
@@ -32,15 +34,18 @@
"nprogress": "^0.2.0",
"pinia": "^2.0.28",
"pinia-plugin-persist": "^1.0.0",
"postcss": "^8.4.21",
"print-js": "^1.6.0",
"qrcodejs2-fixes": "^0.0.2",
"qs": "^6.11.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.0",
"splitpanes": "^3.1.5",
"tailwindcss": "^3.2.7",
"ts-md5": "^1.3.1",
"vue": "^3.2.45",
"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",

6
web/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,196 @@
<template>
<div class="user-info-head" @click="editCropper()">
<el-avatar :size="100" :src="options.img" />
<el-dialog :title="title" v-model="dialogVisiable" width="600px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row>
<el-col class="flex justify-center">
<vue-cropper
ref="cropper"
:img="options.img"
:info="true"
:autoCrop="options.autoCrop"
:autoCropWidth="options.autoCropWidth"
:autoCropHeight="options.autoCropHeight"
:fixedBox="options.fixedBox"
:outputType="options.outputType"
@realTime="realTime"
:centerBox="true"
v-if="visible"
class="cropper"
/>
</el-col>
</el-row>
<br />
<el-row class="flex justify-center">
<el-col :lg="2" :md="2">
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
<el-button type="success">
选择
<el-icon class="el-icon--right"><Plus /></el-icon>
</el-button>
</el-upload>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button icon="RefreshRight" @click="rotateRight()"></el-button>
</el-col>
<el-col :lg="{ span: 2, offset: 2 }" :md="2">
<el-button type="primary" @click="uploadImg()">更新头像</el-button>
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script setup>
import 'vue-cropper/dist/index.css';
import { VueCropper } from 'vue-cropper';
import { useUserInfo } from '/@/stores/userInfo';
import { getCurrentInstance, nextTick, reactive, ref, computed, onMounted, defineExpose } from 'vue';
import { base64ToFile } from '/@/utils/tools';
const userStore = useUserInfo();
const { proxy } = getCurrentInstance();
const open = ref(false);
const visible = ref(false);
const title = ref('修改头像');
const emit = defineEmits(['uploadImg']);
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
required: true,
},
});
const dialogVisiable = computed({
get() {
return props.modelValue;
},
set(newVal) {
emit('update:modelValue', newVal);
},
});
//图片裁剪数据
const options = reactive({
img: userStore.userInfos.avatar, // 裁剪图片的地址
fileName: '',
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
autoCropHeight: 200, // 默认生成截图框高度
fixedBox: true, // 固定截图框大小 不允许改变
outputType: 'png', // 默认生成截图为PNG格式
});
/** 编辑头像 */
function editCropper() {
dialogVisiable.value = true;
}
/** 打开弹出层结束时的回调 */
function modalOpened() {
nextTick(() => {
visible.value = true;
});
}
/** 覆盖默认上传行为 */
function requestUpload() {}
/** 向左旋转 */
function rotateLeft() {
proxy.$refs.cropper.rotateLeft();
}
/** 向右旋转 */
function rotateRight() {
proxy.$refs.cropper.rotateRight();
}
/** 图片缩放 */
function changeScale(num) {
num = num || 1;
proxy.$refs.cropper.changeScale(num);
}
/** 上传预处理 */
function beforeUpload(file) {
if (file.type.indexOf('image/') == -1) {
proxy.$modal.msgError('文件格式错误,请上传图片类型,如JPGPNG后缀的文件。');
} else {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
options.img = reader.result;
options.fileName = file.name;
};
}
}
/** 上传图片 */
function uploadImg() {
// 获取截图的 base64 数据
proxy.$refs.cropper.getCropData((data) => {
let img = new Image();
img.src = data;
img.onload = async () => {
let _data = compress(img);
const imgFile = base64ToFile(_data, options.fileName);
emit('uploadImg', imgFile);
};
});
}
// 压缩图片
function compress(img) {
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
// let initSize = img.src.length;
let width = img.width;
let height = img.height;
canvas.width = width;
canvas.height = height;
// 铺底色
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
// 进行压缩
let ndata = canvas.toDataURL('image/jpeg', 0.8);
return ndata;
}
/** 关闭窗口 */
function closeDialog() {
options.visible = false;
options.img = userStore.userInfos.avatar;
}
const updateAvatar = (img) => {
options.img = img;
};
defineExpose({
updateAvatar,
});
</script>
<style lang="scss" scoped>
.user-info-head {
position: relative;
display: inline-block;
height: 120px;
}
.user-info-head:hover:after {
content: '修改头像';
position: absolute;
text-align: center;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #000000;
font-size: 20px;
font-style: normal;
cursor: pointer;
line-height: 110px;
}
.cropper {
height: 400px;
width: 400px;
}
</style>

View File

@@ -9,8 +9,8 @@ export default {
two4: 'Links',
},
account: {
accountPlaceholder1: 'The user name admin or not is common',
accountPlaceholder2: 'Password: 123456',
accountPlaceholder1: 'Please enter your login account',
accountPlaceholder2: 'Please enter your login password',
accountPlaceholder3: 'Please enter the verification code',
accountBtnText: 'Sign in',
},

View File

@@ -9,8 +9,8 @@ export default {
two4: '友情链接',
},
account: {
accountPlaceholder1: '用户名 admin 或不输均为 common',
accountPlaceholder2: '密码123456',
accountPlaceholder1: '请输入登录账号',
accountPlaceholder2: '请输入登录密码',
accountPlaceholder3: '请输入验证码',
accountBtnText: '登 录',
},

View File

@@ -9,8 +9,8 @@ export default {
two4: '友情連結',
},
account: {
accountPlaceholder1: '用戶名admin或不輸均為common',
accountPlaceholder2: '密碼123456',
accountPlaceholder1: '請輸入登入賬號',
accountPlaceholder2: '請輸入登入密碼',
accountPlaceholder3: '請輸入驗證碼',
accountBtnText: '登入',
},

View File

@@ -39,7 +39,7 @@
<div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference>
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread===0">
<el-badge :value="messageCenter.unread" :hidden="messageCenter.unread === 0">
<el-icon :title="$t('message.user.title4')">
<ele-Bell />
</el-icon>
@@ -59,7 +59,7 @@
</div>
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link">
<img :src="userInfos.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
<img :src="userInfos.avatar" class="layout-navbars-breadcrumb-user-link-photo mr5" />
{{ userInfos.userName === '' ? 'common' : userInfos.userName }}
<el-icon class="el-icon--right">
<ele-ArrowDown />
@@ -68,7 +68,7 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item>
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item>
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
</el-dropdown-menu>
@@ -208,8 +208,8 @@ onMounted(() => {
});
//消息中心的未读数量
import {messageCenterStore} from "/@/stores/messageCenter";
const messageCenter = messageCenterStore()
import { messageCenterStore } from '/@/stores/messageCenter';
const messageCenter = messageCenterStore();
</script>
<style scoped lang="scss">

View File

@@ -1,9 +1,10 @@
import {createApp} from 'vue';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import {directive} from '/@/utils/directive';
import {i18n} from '/@/i18n/index';
import { directive } from '/@/utils/directive';
import { i18n } from '/@/i18n/index';
import other from '/@/utils/other';
import '/@/assets/style/tailwind.css'; // 先引入tailwind css, 以免element-plus冲突
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import '/@/theme/index.scss';
@@ -15,7 +16,7 @@ import fastCrud from './settings.ts';
import pinia from './stores';
import permission from '/@/plugin/permission/index';
// @ts-ignore
import eIconPicker, { iconList,analyzingIconForIconfont } from 'e-icon-picker';
import eIconPicker, { iconList, analyzingIconForIconfont } from 'e-icon-picker';
import 'e-icon-picker/icon/default-icon/symbol.js'; //基本彩色图标库
import 'e-icon-picker/index.css'; // 基本样式,包含基本图标
import 'font-awesome/css/font-awesome.min.css';
@@ -24,6 +25,9 @@ import fontAwesome470 from 'e-icon-picker/icon/fontawesome/font-awesome.v4.7.0.j
import eIconList from 'e-icon-picker/icon/default-icon/eIconList.js';
import iconfont from '/@/assets/iconfont/iconfont.json'; //引入json文件
import '/@/assets/iconfont/iconfont.css'; //引入css
// 自动注册插件
import { scanAndInstallPlugins } from '/@/views/plugins/index';
let forIconfont = analyzingIconForIconfont(iconfont); //解析class
iconList.addIcon(forIconfont.list); // 添加iconfont dvadmin3的icon
iconList.addIcon(elementPlus); // 添加element plus的图标
@@ -31,10 +35,12 @@ iconList.addIcon(fontAwesome470); // 添加fontAwesome 470版本的图标
let app = createApp(App);
scanAndInstallPlugins(app);
app.use(eIconPicker, {
addIconList: eIconList, //全局添加图标
removeIconList: [], //全局删除图标
zIndex: 3100, //选择器弹层的最低层,全局配置
addIconList: eIconList, //全局添加图标
removeIconList: [], //全局删除图标
zIndex: 3100, //选择器弹层的最低层,全局配置
});
pinia.use(piniaPersist);
@@ -42,6 +48,6 @@ directive(app);
other.elSvg(app);
app.use(permission);
app.use(pinia).use(router).use(ElementPlus, {i18n: i18n.global.t}).use(i18n).use(VueGridLayout).use(fastCrud).mount('#app');
app.use(pinia).use(router).use(ElementPlus, { i18n: i18n.global.t }).use(i18n).use(VueGridLayout).use(fastCrud).mount('#app');
app.config.globalProperties.mittBus = mitt();

View File

@@ -11,7 +11,7 @@ import { useRoutesList } from '/@/stores/routesList';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { useMenuApi } from '/@/api/menu/index';
import { handleMenu } from '../utils/menu';
import {BtnPermissionStore} from "/@/plugin/permission/store.permission";
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
const menuApi = useMenuApi();

View File

@@ -7,8 +7,8 @@ import { setLogger } from '@fast-crud/fast-crud';
import ui from '@fast-crud/ui-element';
import { request } from '/@/utils/service';
//扩展包
import {FsExtendsEditor} from "@fast-crud/fast-extends";
import "@fast-crud/fast-extends/dist/style.css";
import { FsExtendsEditor } from '@fast-crud/fast-extends';
import '@fast-crud/fast-extends/dist/style.css';
export default {
async install(app: any, options: any) {
// 先安装ui
@@ -18,7 +18,7 @@ export default {
//i18n, //i18n配置可选默认使用中文具体用法请看demo里的 src/i18n/index.js 文件
// 此处配置公共的dictRequest字典请求
async dictRequest({ dict }: any) {
return await request({ url: dict.url,params:dict.params || {} }); //根据dict的url异步返回一个字典数组
return await request({ url: dict.url, params: dict.params || {} }); //根据dict的url异步返回一个字典数组
},
//公共crud配置
commonOptions() {
@@ -28,6 +28,9 @@ export default {
//你项目后台接口大概率与fast-crud所需要的返回结构不一致所以需要配置此项
//请参考文档http://fast-crud.docmirror.cn/api/crud-options/request.html
transformQuery: ({ page, form, sort }: any) => {
if (sort.asc !== undefined) {
form['ordering'] = `${sort.asc ? '' : '-'}${sort.prop}`;
}
//转换为你pageRequest所需要的请求参数结构
return { page: page.currentPage, limit: page.pageSize, ...form };
},
@@ -55,11 +58,11 @@ export default {
},
});
//富文本
app.use(FsExtendsEditor,{
wangEditor:{
width:300,
}
})
app.use(FsExtendsEditor, {
wangEditor: {
width: 300,
},
});
setLogger({ level: 'error' });
// 设置自动染色
const dictComponentList = ['dict-cascader', 'dict-checkbox', 'dict-radio', 'dict-select', 'dict-switch', 'dict-tree'];

View File

@@ -5,11 +5,17 @@
// 用户信息
export interface UserInfosState {
authBtnList: string[];
photo: string;
roles: string[];
time: number;
avatar: string;
userName: string;
name: string;
email: string;
mobile: string;
gender: string;
dept_info: {
dept_id: number;
dept_name: string;
};
role_info: any[];
}
export interface UserInfosStates {
userInfos: UserInfosState;
@@ -92,3 +98,6 @@ export interface ThemeConfigStates {
export interface DictionaryStates {
data: any;
}
export interface ConfigStates {
systemConfig: any;
}

View File

@@ -0,0 +1,28 @@
import { defineStore } from 'pinia';
import { ConfigStates } from './interface';
import { request } from '../utils/service';
export const urlPrefix = '/api/init/settings/';
/**
* 系统配置数据
* @methods getSystemConfig 获取系统配置数据
*/
export const SystemConfigStore = defineStore('SystemConfig', {
state: (): ConfigStates => ({
systemConfig: {},
}),
actions: {
async getSystemConfigs() {
request({
url: urlPrefix,
method: 'get',
}).then((ret: { data: [] }) => {
// 转换数据格式并保存到pinia
this.systemConfig = JSON.parse(JSON.stringify(ret.data));
});
},
},
persist: {
enabled: true,
},
});

View File

@@ -1,9 +1,7 @@
import { defineStore } from 'pinia';
import Cookies from 'js-cookie';
import { UserInfosStates } from './interface';
import { Session } from '/@/utils/storage';
import { request } from '../utils/service';
/**
* 用户信息
* @methods setUserInfos 设置用户信息
@@ -11,14 +9,37 @@ import { request } from '../utils/service';
export const useUserInfo = defineStore('userInfo', {
state: (): UserInfosStates => ({
userInfos: {
avatar: '',
userName: '',
photo: '',
time: 0,
roles: [],
authBtnList: [],
name: '',
email: '',
mobile: '',
gender: '',
dept_info: {
dept_id: 0,
dept_name: '',
},
role_info: [
{
id: 0,
name: '',
},
],
},
}),
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;
Session.set('userInfo', this.userInfos);
},
async setUserInfos() {
// 存储用户信息到浏览器缓存
if (Session.get('userInfo')) {
@@ -26,17 +47,21 @@ export const useUserInfo = defineStore('userInfo', {
} else {
let userInfos: any = await this.getApiUserInfo();
this.userInfos.userName = userInfos.data.name;
this.userInfos.photo = userInfos.data.avatar || 'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500'
this.userInfos.time = new Date().getTime()
this.userInfos.roles = ['admin']
Session.set('userInfo', this.userInfos)
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;
Session.set('userInfo', this.userInfos);
}
},
async getApiUserInfo() {
return request({
url: '/api/system/user/user_info/',
method: 'get',
})
}
});
},
},
});

View File

@@ -62,15 +62,15 @@ function createService() {
// 有 code 代表这是一个后端接口 可以进行进一步的判断
switch (code) {
case 400:
Local.clear();
Session.clear();
// Local.clear();
// Session.clear();
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
window.location.reload();
// window.location.reload();
break;
case 401:
Local.clear();
Session.clear();
dataAxios.msg = '登录授权过期,请重新登录';
dataAxios.msg = '登录认证失败,请重新登录';
ElMessageBox.alert(dataAxios.msg, '提示', {
confirmButtonText: 'OK',
callback: (action: Action) => {
@@ -100,7 +100,15 @@ function createService() {
error.message = '请求错误';
break;
case 401:
error.message = '未授权,请登录';
Local.clear();
Session.clear();
error.message = '登录授权过期,请重新登录';
ElMessageBox.alert(error.message, '提示', {
confirmButtonText: 'OK',
callback: (action: Action) => {
window.location.reload();
},
})
break;
case 403:
error.message = '拒绝访问';

View File

@@ -3,16 +3,16 @@
* @param {String} jsonString 需要解析的 json 字符串
* @param {String} defaultValue 默认值
*/
import { uiContext } from "@fast-crud/fast-crud";
import { uiContext } from '@fast-crud/fast-crud';
export function parse(jsonString = "{}", defaultValue = {}) {
let result = defaultValue;
try {
result = JSON.parse(jsonString);
} catch (error) {
console.log(error);
}
return result;
export function parse(jsonString = '{}', defaultValue = {}) {
let result = defaultValue;
try {
result = JSON.parse(jsonString);
} catch (error) {
console.log(error);
}
return result;
}
/**
@@ -21,8 +21,8 @@ export function parse(jsonString = "{}", defaultValue = {}) {
* @param {String} msg 状态信息
* @param {Number} code 状态码
*/
export function response(data = {}, msg = "", code = 0) {
return [200, { code, msg, data }];
export function response(data = {}, msg = '', code = 0) {
return [200, { code, msg, data }];
}
/**
@@ -30,8 +30,8 @@ export function response(data = {}, msg = "", code = 0) {
* @param {Any} data 返回值
* @param {String} msg 状态信息
*/
export function responseSuccess(data = {}, msg = "成功") {
return response(data, msg);
export function responseSuccess(data = {}, msg = '成功') {
return response(data, msg);
}
/**
@@ -40,30 +40,62 @@ export function responseSuccess(data = {}, msg = "成功") {
* @param {String} msg 状态信息
* @param {Number} code 状态码
*/
export function responseError(data = {}, msg = "请求失败", code = 500) {
return response(data, msg, code);
export function responseError(data = {}, msg = '请求失败', code = 500) {
return response(data, msg, code);
}
/**
* @description 记录和显示错误
* @param {Error} error 错误对象
*/
export function errorLog(error:any,notification=true) {
// 打印到控制台
console.error(error);
// 显示提示
if(notification){
uiContext.get().notification.error({ message: error.message });
}
export function errorLog(error: any, notification = true) {
// 打印到控制台
console.error(error);
// 显示提示
if (notification) {
uiContext.get().notification.error({ message: error.message });
}
}
/**
* @description 创建一个错误
* @param {String} msg 错误信息
*/
export function errorCreate(msg:any,notification=true) {
const error = new Error(msg);
errorLog(error,notification);
// throw error;
export function errorCreate(msg: any, notification = true) {
const error = new Error(msg);
errorLog(error, notification);
// throw error;
}
/**
* @description base64转file
* @param {String} base64 base64字符串
* @param {String} fileName 文件名
*/
export function base64ToFile(base64: any, fileName: string) {
// 将base64按照 , 进行分割 将前缀 与后续内容分隔开
let data = base64.split(',');
// 利用正则表达式 从前缀中获取图片的类型信息image/png、image/jpeg、image/webp等
let type = data[0].match(/:(.*?);/)[1];
// 从图片的类型信息中 获取具体的文件格式后缀png、jpeg、webp
let suffix = type.split('/')[1];
// 使用atob()对base64数据进行解码 结果是一个文件数据流 以字符串的格式输出
const bstr = window.atob(data[1]);
// 获取解码结果字符串的长度
let n = bstr.length;
// 根据解码结果字符串的长度创建一个等长的整形数字数组
// 但在创建时 所有元素初始值都为 0
const u8arr = new Uint8Array(n);
// 将整形数组的每个元素填充为解码结果字符串对应位置字符的UTF-16 编码单元
while (n--) {
// charCodeAt():获取给定索引处字符对应的 UTF-16 代码单元
u8arr[n] = bstr.charCodeAt(n);
}
// 利用构造函数创建File文件对象
// new File(bits, name, options)
const file = new File([u8arr], `${fileName}.${suffix}`, {
type: type,
});
// 将File文件对象返回给方法的调用者
return file;
}

View File

@@ -30,7 +30,7 @@ const websocket: socket = {
}
const token = Session.get('token')
if(!token){
message.warning('websocket认证失败')
// message.warning('websocket认证失败')
return null
}
const wsUrl = `${getWsBaseURL()}ws/${token}/`

View File

@@ -0,0 +1,9 @@
/*
*
* 后端API接口响应数据
*/
interface APIResponseData {
code?: number;
data: [];
msg?: string;
}

View File

@@ -0,0 +1,15 @@
import { defineAsyncComponent, AsyncComponentLoader } from 'vue';
// 扫描插件目录并注册插件
export const scanAndInstallPlugins = (app: any) => {
const components = import.meta.glob('./**/*.vue');
const pluginNames = new Set();
// 遍历对象并注册异步组件
for (const [key, value] of Object.entries(components)) {
const name = key.slice(key.lastIndexOf('/') + 1, key.lastIndexOf('.'));
app.component(name, defineAsyncComponent(value as AsyncComponentLoader));
const pluginsName = key.match(/\/([^\/]*)\//)?.[1];
pluginNames.add(pluginsName);
}
console.log('已发现插件:', Array.from(pluginNames));
};

View File

@@ -1,41 +1,41 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/area/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -1,13 +1,10 @@
import * as api from './api';
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { dictionary } from '/@/utils/dictionary';
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
import { successMessage } from '/@/utils/message';
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
@@ -26,8 +23,8 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
* @param row
* @returns {Promise<unknown>}
*/
const loadContentMethod = (tree: any, treeNode: any, resolve: any) => {
api.GetList({ pcode: tree.code }).then((res: any) => {
const loadContentMethod = (tree: any, treeNode: any, resolve: Function) => {
pageRequest({ pcode: tree.code }).then((res: APIResponseData) => {
resolve(res.data);
});
};
@@ -40,6 +37,24 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
},
pagination: {
show: false,
},
@@ -90,8 +105,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
show: true,
},
treeNode: true,
width: 160,
type: 'input',
column:{
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
@@ -108,6 +125,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
show: true,
},
type: 'input',
column:{
minWidth: 90,
},
form: {
rules: [
// 表单校验规则
@@ -124,6 +144,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: true,
},
type: 'input',
column:{
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
@@ -140,6 +163,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: true,
},
type: 'input',
column:{
minWidth: 100,
},
form: {
disabled: false,
rules: [
@@ -153,6 +179,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
initials: {
title: '首字母',
column:{
minWidth: 100,
},
form: {
rules: [
// 表单校验规则
@@ -169,14 +198,26 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
search: {
show: true,
},
width: 90,
type: 'dict-radio',
column: {
minWidth:90,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: #409eff; --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'),
}),
form: {
value: true,
},
},
},
},

View File

@@ -6,18 +6,10 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
// 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 });
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {

View File

@@ -1,9 +1,9 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import XEUtils from 'xe-utils';
export const apiPrefix = '/api/system/system_config/';
export function GetList(query: PageQuery) {
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
@@ -52,10 +52,10 @@ export function GetAssociationTable() {
});
}
export function saveContent (data:any) {
export function saveContent(data: any) {
return request({
url: apiPrefix + 'save_content/',
method: 'put',
data: data
})
data: data,
});
}

View File

@@ -1,41 +1,49 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/dept/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
export function DelObj(obj: DelReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'delete',
data: obj,
});
}
export function lazyLoadDept(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}

View File

@@ -1,15 +1,11 @@
import * as api from './api';
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { verifyPhone } from '/@/utils/toolsValidate';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
import { successMessage } from '/@/utils/message';
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsTypes {
const { getFormRef, getFormData } = crudExpose;
const pageRequest = async (query: PageQuery) => {
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
@@ -64,14 +60,26 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
rowHandle: {
fiexd: 'right',
width: 310,
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
addChildren: {
text: '添加子级',
type: 'warning',
type: 'text',
click(context) {
const rowId = context.row.id;
crudExpose.openAdd({ row: { parent: rowId } });
crudExpose!.openAdd({ row: { parent: rowId } });
},
},
},
@@ -192,6 +200,7 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
{
type: 'email',
message: '请输入正确的邮箱地址',
// @ts-ignore
trigger: ['blur', 'change'],
},
],
@@ -217,16 +226,24 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: false,
},
type: 'dict-radio',
column: {
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: #409eff; --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'),
}),
form: {
value: true,
component: {
span: 12,
placeholder: '请选择状态',
},
},
},
},
},

View File

@@ -1,26 +1,272 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<el-row class="mx-2">
<el-col :span="4" class="p-1">
<el-card :body-style="{ height: '100%' }">
<p class="font-mono font-black text-center text-xl pb-5">部门列表</p>
<el-input v-model="filterText" :placeholder="placeholder" />
<el-tree
ref="treeRef"
class="font-mono font-bold leading-6 text-7xl"
:data="data"
:props="treeProps"
:filter-node-method="filterNode"
:load="loadNode"
@node-drop="nodeDrop"
lazy
icon="ArrowRightBold"
:indent="12"
draggable
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span v-if="data.status" class="text-center font-black text-xl"><SvgIcon :name="node.data.icon" />&nbsp;{{ node.label }}</span>
<span v-else class="text-center font-black text-xl text-red-700"><SvgIcon :name="node.data.icon" />&nbsp;{{ node.label }}</span>
</template>
</el-tree>
</el-card>
</el-col>
<el-col :span="20" class="p-1">
<el-card :body-style="{ height: '100%' }">
<el-form ref="formRef" :rules="rules" :model="form" label-width="120px" label-position="right">
<el-divider>
<strong>部门配置</strong>
</el-divider>
<el-row>
<el-col :span="10">
<el-form-item label="部门ID" prop="id"> <el-input v-model="form.id" disabled /> </el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="父级部门ID" prop="parent"> <el-input v-model="form.parent" /> </el-form-item>
</el-col>
<el-col :span="10">
<el-form-item required label="部门名称" prop="name"> <el-input v-model="form.name" /> </el-form-item>
</el-col>
<el-col :span="10">
<el-form-item required label="部门标识" prop="key"> <el-input v-model="form.key" /> </el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="负责人" prop="owner"> <el-input v-model="form.owner" /> </el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="联系电话" prop="phone"> <el-input v-model="form.phone" /> </el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="邮箱" prop="email"> <el-input v-model="form.email" /> </el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" controls-position="right" />
</el-form-item>
</el-col>
<el-col class="center">
<el-divider>
<el-button @click="saveMenu()" type="primary" round>保存</el-button>
<el-button @click="newMenu()" type="success" round>新建</el-button>
<el-button @click="addChildMenu()" type="info" round>添加子级</el-button>
<el-button @click="addSameLevelMenu()" type="warning" round>添加同级</el-button>
<el-button @click="deleteMenu()" type="danger" round>删除部门</el-button>
</el-divider>
</el-col>
</el-row>
</el-form>
</el-card>
</el-col>
</el-row>
</fs-page>
</template>
<script lang="ts" setup>
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 });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
// 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
import * as api from './api';
import { ElForm, ElTree, FormRules } from 'element-plus';
import { ref, onMounted, reactive, toRaw, watch } from 'vue';
import XEUtils from 'xe-utils';
import { errorMessage, successMessage } from '../../../utils/message';
interface Tree {
id: number;
name: string;
status: boolean;
children?: Tree[];
}
interface APIResponseData {
code?: number;
data: [];
msg?: string;
}
interface Form<T> {
[key: string]: T;
}
const placeholder = ref('请输入部门名称');
const filterText = ref('');
const treeRef = ref<InstanceType<typeof ElTree>>();
const treeProps = {
children: 'children',
label: 'name',
// isLeaf: (data: Tree[], node: Node) => {
// // @ts-ignore
// if (node.data.is_catalog) {
// return false;
// } else {
// return true;
// }
// },
};
const validateWebPath = (rule: string, value: string, callback: Function) => {
let pattern = /^\/.*?/;
if (!pattern.test(value)) {
callback(new Error('请输入正确的地址'));
} else {
callback();
}
};
watch(filterText, (val) => {
treeRef.value!.filter(val);
});
const filterNode = (value: string, data: Tree) => {
if (!value) return true;
return toRaw(data).name.indexOf(value) !== -1;
};
// 懒加载
const loadNode = (node: Node, resolve: (data: Tree[]) => void) => {
// @ts-ignore
if (node.level !== 0) {
// @ts-ignore
api.lazyLoadDept({ parent: node.data.id }).then((res: APIResponseData) => {
resolve(res.data);
});
}
};
const nodeDrop = (draggingNode: Node, dropNode: Node, dropType: string, event: any) => {
// @ts-ignore
if (!dropNode.isLeaf) {
// @ts-ignore
api.dragMenu({ menu_id: draggingNode.data.id, parent_id: dropNode.data.id }).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
}
};
let data = ref([]);
let isAddNewMenu = ref(false); // 判断当前是新增部门,还是更新保存当前部门
let form: Form<any> = reactive({
id: '',
parent: '',
name: '',
owner: '',
phone: '',
email: '',
sort: '',
});
const formRef = ref<InstanceType<typeof ElForm>>();
const rules = reactive<FormRules>({
// @ts-ignore
parent: [{ required: true, message: '父级部门名称必填', trigger: 'blur' }],
name: [{ required: true, message: '部门名称必填', trigger: 'blur' }],
key: [{ required: true, message: '部门标识必填', trigger: 'blur' }],
});
const getData = () => {
api.GetList({}).then((ret: APIResponseData) => {
const responseData = ret.data;
const result = XEUtils.toArrayTree(responseData, {
parentKey: 'parent',
children: 'children',
strict: true,
});
data.value = result;
});
};
const saveMenu = () => {
formRef.value?.validate((valid, fields) => {
if (valid) {
if (!isAddNewMenu.value) {
// 保存部门
form.component == '' ? (form.is_catalog = true) : (form.is_catalog = false);
api.UpdateObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
} else {
// 新增部门
form.component == '' ? (form.is_catalog = true) : (form.is_catalog = false);
api.AddObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
}
} else {
errorMessage('请填写检查表单');
}
});
};
const newMenu = () => {
formRef.value?.resetFields();
isAddNewMenu.value = true;
};
const addChildMenu = () => {
let parentId = form.id;
formRef.value?.resetFields();
form.parent = parentId;
isAddNewMenu.value = true;
};
const addSameLevelMenu = () => {
let parentId = form.parent;
formRef.value?.resetFields();
form.parent = parentId;
isAddNewMenu.value = true;
};
const deleteMenu = () => {
api.DelObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
};
const handleNodeClick = (data: any, node: any, prop: any) => {
Object.keys(toRaw(data)).forEach((key: string) => {
form[key] = data[key];
});
delete form.component_name;
form.id = data.id;
isAddNewMenu.value = false;
};
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
getData();
});
</script>
<style lang="scss" scoped>
.el-row {
height: 100%;
.el-col {
height: 100%;
}
}
.el-card {
height: 100%;
}
</style>

View File

@@ -1,14 +1,11 @@
import * as api from './api';
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { dictionary } from '/@/utils/dictionary';
import {nextTick, ref} from 'vue';
import { inject, nextTick, ref } from 'vue';
import { successMessage } from '/@/utils/message';
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
export const createCrudOptions = function ({ crudExpose, subDictRef }: { crudExpose: CrudExpose; subDictRef: any }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
@@ -21,6 +18,10 @@ export const createCrudOptions = function ({ crudExpose, subDictRef }: { crudExp
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
//权限判定
const hasPermissions = inject('$hasPermissions');
return {
crudOptions: {
request: {
@@ -30,25 +31,38 @@ export const createCrudOptions = function ({ crudExpose, subDictRef }: { crudExp
delRequest,
},
rowHandle: {
width: 360,
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show: hasPermissions('dictionary:Update'),
},
remove: {
iconRight: 'Delete',
type: 'text',
show: hasPermissions('dictionary:Delete'),
},
custom: {
text: '字典配置',
type: 'success',
type: 'text',
show: hasPermissions('dictionary:Update'),
tooltip: {
placement: 'top',
content: '字典配置',
},
//@ts-ignore
click: (context: any) => {
const {row} = context
subDictRef.value.drawer = true;
nextTick(()=>{
subDictRef.value.setSearchFormData({ form: { parent: row.id } });
subDictRef.value.doRefresh();
})
click: (ctx: any) => {
const { row } = ctx;
context!.subDictRef.value.drawer = true;
nextTick(() => {
context!.subDictRef.value.setSearchFormData({ form: { parent: row.id } });
context!.subDictRef.value.doRefresh();
});
},
},
},
@@ -65,7 +79,7 @@ export const createCrudOptions = function ({ crudExpose, subDictRef }: { crudExp
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose.crudBinding.value.pagination;
let pagination = crudExpose!.crudBinding.value.pagination;
// @ts-ignore
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
@@ -105,6 +119,9 @@ export const createCrudOptions = function ({ crudExpose, subDictRef }: { crudExp
},
},
type: 'input',
column: {
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
@@ -129,6 +146,9 @@ export const createCrudOptions = function ({ crudExpose, subDictRef }: { crudExp
},
},
type: 'input',
column: {
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
@@ -149,34 +169,36 @@ export const createCrudOptions = function ({ crudExpose, subDictRef }: { crudExp
},
status: {
title: '状态',
width: 90,
search: {
show: true,
},
type: 'dict-radio',
column: {
minWidth: 90,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: #409eff; --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'),
}),
component: {
props: {
options: [],
},
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '状态必填项' },
],
value: true,
component: {
placeholder: '请选择状态',
},
},
},
sort: {
title: '排序',
width: 90,
type: 'number',
column: {
minWidth: 80,
},
form: {
value: 1,
},

View File

@@ -6,24 +6,13 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, watch } from 'vue';
import type { Ref } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { ref, onMounted, defineAsyncComponent } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import subDict from './subDict/index.vue';
//字典配置ref
const subDict = defineAsyncComponent(() => import('./subDict/index.vue'));
const subDictRef = ref();
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose, subDictRef });
// 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { subDictRef } });
// 页面打开后获取列表数据
onMounted(() => {

View File

@@ -1,41 +1,41 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/dictionary/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -1,14 +1,9 @@
import * as api from './api';
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { dictionary } from '/@/utils/dictionary';
import {watch} from "vue";
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
@@ -30,6 +25,24 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
},
columns: {
_index: {
title: '序号',
@@ -42,7 +55,7 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose.crudBinding.value.pagination;
let pagination = crudExpose!.crudBinding.value.pagination;
// @ts-ignore
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},

View File

@@ -1,5 +1,5 @@
<template>
<el-drawer size="70%" v-model="drawer" direction="rtl" destroy-on-close :before-close="handleClose">
<el-drawer size="70%" v-model="drawer" direction="rtl" destroy-on-close :before-close="handleClose">
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
@@ -7,27 +7,17 @@
</template>
<script lang="ts" setup>
import {ref, onMounted, defineProps, computed, watch} from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { ref, onMounted, defineAsyncComponent } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { ElMessageBox } from 'element-plus';
// 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 });
//抽屉是否显示
const drawer = ref(false);
//抽屉关闭确认
const handleClose = (done: () => void) => {
ElMessageBox.confirm('您确定要关闭?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@@ -40,11 +30,13 @@ const handleClose = (done: () => void) => {
// catch error
});
};
const {setSearchFormData,doRefresh} = crudExpose
defineExpose({ drawer,setSearchFormData,doRefresh });
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
const { setSearchFormData, doRefresh } = crudExpose;
defineExpose({ drawer, setSearchFormData, doRefresh });
// 页面打开后获取列表数据
onMounted(() => {
// console.log(48,currentRow)
crudExpose.doRefresh();
});
</script>

View File

@@ -1,41 +1,41 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/file/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
data: query,
});
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
data: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -1,13 +1,8 @@
import * as api from './api';
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
import { UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
@@ -35,6 +30,24 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
},
columns: {
_index: {
title: '序号',
@@ -47,8 +60,8 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
let pagination = crudExpose!.crudBinding.value.pagination;
return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1;
},
},
},
@@ -81,6 +94,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
show: true,
},
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入文件名称',
@@ -93,13 +109,18 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
search: {
disabled: true,
},
column:{
minWidth: 200,
},
},
md5sum: {
title: '文件MD5',
width: 200,
search: {
disabled: true,
},
column:{
minWidth: 120,
},
form: {
disabled: false,
},

View File

@@ -6,18 +6,10 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
// 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 });
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {

View File

@@ -1,41 +1,41 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/login_log/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
data: query,
});
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
data: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -1,13 +1,8 @@
import * as api from './api';
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
import { UserPageQuery, AddReq, DelReq, EditReq, CreateCrudOptionsProps, CreateCrudOptionsRet, dict } from '@fast-crud/fast-crud';
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
@@ -36,7 +31,12 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
},
rowHandle: {
fixed:'right',
width: 100,
buttons: {
view: {
type: 'text',
},
edit: {
show: false,
},
@@ -54,6 +54,12 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose!.crudBinding.value.pagination;
return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1;
},
},
},
search: {
@@ -84,8 +90,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
search: {
disabled: false,
},
width: 140,
type: 'input',
column:{
minWidth: 120,
},
form: {
disabled: true,
component: {
@@ -98,8 +106,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
search: {
disabled: false,
},
width: 130,
type: 'input',
column:{
minWidth: 120,
},
form: {
disabled: true,
component: {
@@ -113,8 +123,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: true,
},
disabled: true,
width: 180,
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入运营商',
@@ -123,8 +135,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
continent: {
title: '大州',
width: 80,
type: 'input',
column:{
minWidth: 90,
},
form: {
disabled: true,
component: {
@@ -135,8 +149,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
country: {
title: '国家',
width: 80,
type: 'input',
column:{
minWidth: 90,
},
form: {
component: {
placeholder: '请输入国家',
@@ -146,8 +162,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
province: {
title: '省份',
width: 80,
type: 'input',
column:{
minWidth: 80,
},
form: {
component: {
placeholder: '请输入省份',
@@ -157,8 +175,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
city: {
title: '城市',
width: 80,
type: 'input',
column:{
minWidth: 80,
},
form: {
component: {
placeholder: '请输入城市',
@@ -169,8 +189,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
district: {
title: '县区',
key: '',
width: 80,
type: 'input',
column:{
minWidth: 80,
},
form: {
component: {
placeholder: '请输入县区',
@@ -181,6 +203,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
area_code: {
title: '区域代码',
type: 'input',
column:{
minWidth: 90,
},
form: {
component: {
placeholder: '请输入区域代码',
@@ -190,8 +215,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
country_english: {
title: '英文全称',
width: 120,
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入英文全称',
@@ -202,6 +229,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
country_code: {
title: '简称',
type: 'input',
column:{
minWidth: 100,
},
form: {
component: {
placeholder: '请输入简称',
@@ -213,6 +243,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
title: '经度',
type: 'input',
disabled: true,
column:{
minWidth: 100,
},
form: {
component: {
placeholder: '请输入经度',
@@ -224,6 +257,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
title: '纬度',
type: 'input',
disabled: true,
column:{
minWidth: 100,
},
form: {
component: {
placeholder: '请输入纬度',
@@ -233,26 +269,31 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
login_type: {
title: '登录类型',
type: 'select',
type: 'dict-select',
search: {
disabled: false,
},
dict: {
dict: dict({
data: [
{ label: '普通登录', value: 1 },
{ label: '微信扫码登录', value: 2 },
],
}),
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请选择登录类型',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
os: {
title: '操作系统',
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入操作系统',
@@ -262,6 +303,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
browser: {
title: '浏览器名',
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入浏览器名',
@@ -272,6 +316,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
title: 'agent信息',
disabled: true,
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入agent信息',

View File

@@ -6,18 +6,10 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
// 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 });
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {

View File

@@ -1,41 +1,41 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/operation_log/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -1,13 +1,8 @@
import * as api from './api';
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
import { UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
@@ -36,7 +31,12 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
},
rowHandle: {
fixed:'right',
width: 100,
buttons: {
view: {
type: 'text',
},
edit: {
show: false,
},
@@ -57,8 +57,8 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
let pagination = crudExpose!.crudBinding.value.pagination;
return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1;
},
},
},
@@ -91,6 +91,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: false,
},
type: 'input',
column:{
minWidth: 100,
},
form: {
disabled: true,
component: {
@@ -104,6 +107,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: false,
},
type: 'input',
column:{
minWidth: 200,
},
form: {
disabled: true,
component: {
@@ -142,6 +148,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
search: {
disabled: false,
},
column:{
minWidth: 100,
},
form: {
disabled: true,
component: {
@@ -165,6 +174,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: false,
},
type: 'input',
column:{
minWidth: 100,
},
form: {
disabled: true,
component: {
@@ -176,6 +188,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
request_browser: {
title: '请求浏览器',
type: 'input',
column:{
minWidth: 120,
},
form: {
disabled: true,
},
@@ -187,6 +202,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: true,
},
type: 'input',
column:{
minWidth: 100,
},
form: {
disabled: true,
},
@@ -199,6 +217,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: true,
},
type: 'input',
column:{
minWidth: 120,
},
form: {
disabled: true,
},
@@ -210,6 +231,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
disabled: true,
},
type: 'input',
column:{
minWidth: 150,
},
form: {
disabled: true,
},
@@ -217,6 +241,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
creator_name: {
title: '操作人',
column:{
minWidth: 100,
},
form: {
disabled: true,
},

View File

@@ -6,18 +6,10 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
// 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 });
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {

View File

@@ -27,7 +27,7 @@
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation3">
<el-form-item class="login-animation3" v-if="isShowCaptcha">
<el-col :span="15">
<el-input
type="text"
@@ -46,7 +46,6 @@
<el-col :span="8">
<el-button class="login-content-captcha">
<el-image :src="ruleForm.captchaImgBase" @click="refreshCaptcha" />
<!-- TODO 完成点击刷新验证码 -->
</el-button>
</el-col>
</el-form-item>
@@ -59,7 +58,7 @@
</template>
<script lang="ts">
import { toRefs, reactive, defineComponent, computed, onMounted } from 'vue';
import { toRefs, reactive, defineComponent, computed, onMounted, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
@@ -74,7 +73,8 @@ import { NextLoading } from '/@/utils/loading';
import * as loginApi from '/@/views/system/login/api';
import { useUserInfo } from '/@/stores/userInfo';
import { DictionaryStore } from '/@/stores/dictionary';
import {BtnPermissionStore} from "/@/plugin/permission/store.permission";
import { SystemConfigStore } from '/@/stores/systemConfig';
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
import { Md5 } from 'ts-md5';
export default defineComponent({
@@ -89,8 +89,8 @@ export default defineComponent({
const state = reactive({
isShowPassword: false,
ruleForm: {
username: 'superadmin',
password: 'admin123456',
username: '',
password: '',
captcha: '',
captchaKey: '',
captchaImgBase: '',
@@ -103,6 +103,10 @@ export default defineComponent({
const currentTime = computed(() => {
return formatAxis(new Date());
});
// 是否关闭验证码
const isShowCaptcha = computed(() => {
return SystemConfigStore().systemConfig['base.captcha_state'];
});
const getCaptcha = async () => {
loginApi.getCaptcha().then((ret: any) => {
@@ -117,31 +121,43 @@ export default defineComponent({
});
};
const loginClick = async () => {
loginApi.login({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password) }).then((ret: any) => {
Session.set('token', ret.data.access);
Cookies.set('username', ret.data.name);
if (!themeConfig.value.isRequestRoutes) {
// 前端控制路由2、请注意执行顺序
initFrontEndControlRoutes();
loginSuccess();
} else {
// 模拟后端控制路由isRequestRoutes 为 true则开启后端控制路由
// 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
initBackEndControlRoutes();
// 执行完 initBackEndControlRoutes,再执行 signInSuccess
loginSuccess();
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);
if (!themeConfig.value.isRequestRoutes) {
// 前端控制路由2、请注意执行顺序
initFrontEndControlRoutes();
loginSuccess();
} else {
// 模拟后端控制路由isRequestRoutes 为 true则开启后端控制路由
// 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
initBackEndControlRoutes();
// 执行完 initBackEndControlRoutes再执行 signInSuccess
loginSuccess();
}
} else if (res.code === 4000) {
// 登录错误之后,刷新验证码
refreshCaptcha();
}
});
};
const getUserInfo = () => {
useUserInfo().setUserInfos();
};
const enterClickLogin = (e: any) => {
if (e.keyCode == 13 || e.keyCode == 100) {
loginClick();
}
};
// 登录成功后的跳转
const loginSuccess = () => {
//登录成功获取用户信息,获取系统字典数据
getUserInfo();
//获取所有字典
DictionaryStore().getSystemDictionarys();
// 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页
@@ -164,12 +180,19 @@ export default defineComponent({
};
onMounted(() => {
getCaptcha();
//获取系统配置
SystemConfigStore().getSystemConfigs();
window.addEventListener('keyup', enterClickLogin, false);
});
onUnmounted(() => {
window.removeEventListener('keyup', enterClickLogin, false);
});
return {
refreshCaptcha,
loginClick,
loginSuccess,
isShowCaptcha,
...toRefs(state),
};
},

View File

@@ -1,17 +1,18 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/menu/';
export function GetList(query: PageQuery) {
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id+'/',
url: apiPrefix + id + '/',
method: 'get',
});
}
@@ -32,10 +33,33 @@ export function UpdateObj(obj: EditReq) {
});
}
export function DelObj(id: DelReq) {
export function DelObj(obj: DelReq) {
return request({
url: apiPrefix + id + '/',
url: apiPrefix + obj.id + '/',
method: 'delete',
data: { id },
});
}
export function GetAllMenu(query: UserPageQuery) {
return request({
url: apiPrefix + 'get_all_menu/',
method: 'get',
params: query,
});
}
export function lazyLoadMenu(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function dragMenu(obj: AddReq) {
return request({
url: apiPrefix + 'drag_menu/',
method: 'post',
data: obj,
});
}

View File

@@ -0,0 +1,201 @@
import {
CrudOptions,
AddReq,
DelReq,
EditReq,
dict,
CrudExpose,
CreateCrudOptionsRet,
CreateCrudOptionsProps,
UserPageQuery,
} from '@fast-crud/fast-crud';
import _ from 'lodash-es';
import * as api from './api';
import { request } from '/@/utils/service';
//此处为crudOptions配置
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
console.log(context!.selectOptions);
return await api.GetList({ menu: context!.selectOptions.value.id } as any);
};
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);
};
return {
crudOptions: {
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
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, //禁止在列设置中选择
},
},
search: {
title: '关键词',
column: { show: false },
type: 'text',
search: { show: true },
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
id: {
title: 'ID',
type: 'text',
column: { show: false },
search: { show: false },
form: { show: false },
},
name: {
title: '权限名称',
type: 'text',
search: { show: true },
column: {
minWidth: 120,
sortable: true,
},
form: {
rules: [{ required: true, message: '权限名称必填' }],
component: {
placeholder: '输入权限名称搜索',
props: {
clearable: true,
allowCreate: true,
filterable: true,
},
},
helper: {
render(h) {
return <el-alert title="手动输入" type="warning" description="页面中按钮的名称或者自定义一个名称" />;
},
},
},
},
value: {
title: '权限值',
type: 'text',
search: { show: false },
column: {
width: 120,
sortable: true,
},
form: {
rules: [{ required: true, message: '权限标识必填' }],
placeholder: '输入权限标识',
helper: {
render(h) {
return <el-alert title="唯一值" type="warning" description="用于判断前端按钮权限或接口权限" />;
},
},
},
},
method: {
title: '请求方式',
search: { show: false },
type: 'dict-select',
column: {
width: 120,
sortable: true,
},
dict: dict({
data: [
{ label: 'GET', value: 0 },
{ label: 'POST', value: 1, color: 'success' },
{ label: 'PUT', value: 2, color: 'warning' },
{ label: 'DELETE', value: 3, color: 'danger' },
],
}),
form: {
rules: [{ required: true, message: '必填项' }],
},
},
api: {
title: '接口地址',
search: { show: false },
type: 'dict-select',
dict: dict({
getData() {
return request({ url: '/swagger.json' }).then((res: any) => {
const ret = Object.keys(res.paths);
const data = [];
for (const item of ret) {
const obj: any = {};
obj.label = item;
obj.value = item;
data.push(obj);
}
return data;
});
},
}),
column: {
minWidth: 200,
sortable: true,
},
form: {
rules: [{ required: true, message: '必填项' }],
component: {
props: {
allowCreate: true,
filterable: true,
clearable: true,
},
},
helper: {
render(h) {
return <el-alert title="请正确填写,以免请求时被拦截。匹配单例使用正则,例如:/api/xx/.*?/" type="warning" />;
},
},
},
},
},
},
};
};

View File

@@ -1,186 +0,0 @@
import {CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose} from '@fast-crud/fast-crud';
import _ from 'lodash-es';
import * as api from "./api";
import {dictionary} from '/@/utils/dictionary';
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
import {request} from '/@/utils/service';
//此处为crudOptions配置
export const createCrudOptions = function ({
crudExpose,
selectOptions
}: { crudExpose: CrudExpose, selectOptions: any }): CreateCrudOptionsTypes {
const pageRequest = async (query: any) => {
return await api.GetList({ menu: selectOptions.value.id});
};
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);
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
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, //禁止在列设置中选择
},
},
search: {
title: '关键词',
column: {show: false},
type: 'text',
search: {show: true},
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
id: {
title: 'ID',
type: 'text',
column: {show: false},
search: {show: false},
form: {show: false},
},
name: {
title: '权限名称',
type: 'text',
search: {show: true},
column: {
minWidth: 120,
sortable: true,
},
form: {
rules: [{required: true, message: '权限名称必填'}],
component: {
placeholder: '输入权限名称搜索',
props: {
clearable: true,
allowCreate: true,
filterable: true,
}
},
helper: {
render (h) {
return (< el-alert title="手动输入" type="warning" description="页面中按钮的名称或者自定义一个名称"/>
)
}
}
},
},
value: {
title: '权限值',
type: 'text',
search: {show: false},
column: {
width: 120,
sortable: true,
},
form: {
rules: [{required: true, message: '权限标识必填'}],
placeholder: '输入权限标识',
helper: {
render (h) {
return (< el-alert title="唯一值" type="warning" description="用于判断前端按钮权限或接口权限"/>
)
}
}
},
},
method: {
title: '请求方式',
search: {show: false},
type: 'dict-select',
column: {
width: 120,
sortable: true,
},
dict: dict({
data: [
{label: 'GET', value: 0},
{label: 'POST', value: 1, color: 'success'},
{label: 'PUT', value: 2, color: 'warning'},
{label: 'DELETE', value: 3, color: 'danger'}
]
}),
form:{
rules: [{required: true, message: '必填项'}],
}
},
api: {
title: '接口地址',
search: {show: false},
type: 'dict-select',
dict: dict({
getData() {
return request({url: '/swagger.json'}).then((res: any) => {
const ret = Object.keys(res.paths)
const data = []
for (const item of ret) {
const obj: any = {}
obj.label = item
obj.value = item
data.push(obj)
}
return data
})
}
}),
column: {
minWidth: 200,
sortable: true,
},
form: {
rules: [{required: true, message: '必填项'}],
component:{
props:{
allowCreate: true,
filterable: true,
clearable: true
}
},
helper: {
render (h) {
return (< el-alert title="请正确填写,以免请求时被拦截。匹配单例使用正则,例如:/api/xx/.*?/" type="warning" />
)
}
}
},
}
},
},
};
};

View File

@@ -1,69 +1,26 @@
<template>
<el-drawer
size="70%"
v-model="drawer"
direction="rtl"
destroy-on-close
:before-close="handleClose"
>
<template #header>
<div>当前菜单:
<el-tag>{{ selectOptions.name }}</el-tag>
</div>
</template>
<div>
<fs-page style="margin-top: 60px;">
<fs-crud ref="crudRef" v-bind="crudBinding">
</fs-crud>
</fs-page>
</div>
</el-drawer>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</template>
<script lang="ts" setup>
import {ref, onMounted} from 'vue';
import {useExpose, useCrud} from '@fast-crud/fast-crud';
import {createCrudOptions} from './curd';
import {ElMessageBox} from "element-plus";
import * as api from './api'
// 弹窗是否显示
const drawer = ref(false)
import { ref, defineProps, watch } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
// 当前选择的菜单信息
const selectOptions:any = ref({name:null})
let selectOptions: any = ref({ name: null });
const props = defineProps<{
selectMenu: object;
}>();
//抽屉关闭确认
const handleClose = (done: () => void) => {
ElMessageBox.confirm('您确定要关闭?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
done()
})
.catch(() => {
// catch error
})
}
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const {crudExpose} = useExpose({crudRef, crudBinding});
// 你的crud配置
const {crudOptions} = createCrudOptions({crudExpose, selectOptions});
// 初始化crud配置
const {resetCrudOptions} = useCrud({crudExpose, crudOptions});
// 你可以调用此方法重新初始化crud配置
// resetCrudOptions(options)
const initGet = ()=>{
api.GetList({menu:selectOptions.value.id}).then((res:any)=>{
const {data} = res
crudExpose.crudBinding.value.data=data
})
}
watch(props.selectMenu, (val: any) => {
if (!val.is_catalog) {
selectOptions.value = val;
doRefresh();
}
});
defineExpose({drawer, selectOptions,initGet})
const { crudRef, crudBinding, crudExpose, context } = useFs({ createCrudOptions, context: { selectOptions } });
const { doRefresh } = crudExpose;
defineExpose({ selectOptions });
</script>

View File

@@ -1,37 +1,8 @@
import * as api from './api';
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import { dictionary } from '/@/utils/dictionary';
import { eIconPicker, eIcon } from 'e-icon-picker';
import { useCompute } from '@fast-crud/fast-crud';
import { inject,computed } from 'vue';
import { apiPrefix as menuPrefix } from './api';
import XEUtils from 'xe-utils';
import { request } from '/@/utils/service';
const { compute } = useCompute();
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from '@fast-crud/fast-crud';
export const createCrudOptions = function ({ crudExpose, menuButtonRef }: { crudExpose: CrudExpose; menuButtonRef: any }): CreateCrudOptionsTypes {
const hasPermissions: any = inject('$hasPermissions');
//验证路由地址
const validateWebPath = (rule: string, value: string, callback: Function) => {
const isLink = JSON.parse(crudExpose.getFormData().is_link);
let pattern = /^\/.*?/;
if (isLink) {
pattern = /^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+/g;
} else {
pattern = /^\/.*?/;
}
// console.log('isLink', isLink, 'pattern', pattern, pattern.test(value));
if (!pattern.test(value)) {
callback(new Error('请正确的地址'));
} else {
callback();
}
};
const pageRequest = async (query: PageQuery) => {
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
@@ -41,21 +12,11 @@ export const createCrudOptions = function ({ crudExpose, menuButtonRef }: { crud
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: any) => {
api.GetList({ parent: tree.id }).then((res: any) => {
resolve(res.data);
});
};
return {
crudOptions: {
request: {
@@ -64,404 +25,7 @@ export const createCrudOptions = function ({ crudExpose, menuButtonRef }: { crud
editRequest,
delRequest,
},
pagination: {
show: false,
},
table: {
rowKey: 'id',
lazy: true,
load: loadContentMethod,
treeProps: { children: 'children', hasChildren: 'hasChild' },
},
actionbar: {
buttons: {
add: {
show: hasPermissions('menu:Create'),
},
},
},
rowHandle: {
fixed: "right",
width: 310,
buttons: {
custom: {
text: '按钮配置',
type: 'warning',
tooltip: {
placement: 'top',
content: '按钮配置',
},
show: compute(({ row }: any) => {
if (row.is_catalog) {
return false;
}
return true;
}),
click: (context: any): void => {
const { row } = context;
menuButtonRef.value.drawer = true;
menuButtonRef.value.selectOptions = row;
menuButtonRef.value.initGet();
},
},
addChildren:{
text: "添加子级",
type:"warning",
click(context){
const rowId =context.row.id
crudExpose.openAdd({ row: { parent: rowId } })
}
}
}
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '80px',
columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination: any = crudExpose.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
search: {
title: '关键词',
column: {
show: false,
},
search: {
show: true,
component: {
props: {
clearable: true,
},
placeholder: '请输入关键词',
},
},
form: {
show: false,
component: {
props: {
clearable: true,
},
},
},
},
name: {
title: '菜单名称',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
type: 'input',
form: {
rules: [
// 表单校验规则
{ required: true, message: '菜单名称必填项' },
],
component: {
props: {
clearable: true,
},
placeholder: '请输入菜单名称',
},
},
column: {
width: 180,
},
},
icon: {
title: '图标',
form: {
component: {
name: eIconPicker,
vModel: 'modelValue',
},
},
column: {
component: {
name: eIcon,
vModel: 'modelValue',
style: 'font-size:18px',
},
},
},
sort: {
title: '排序',
column: {
width: 60,
},
type: 'number',
form: {
value: 1,
component: {
placeholder: '请输入排序',
},
},
},
is_catalog: {
title: '是否目录',
column: {
width: 100,
},
type: 'dict-switch',
dict: dict({
data: dictionary('button_whether_bool'),
}),
form: {
component: {
placeholder: '请选择是否目录',
},
// valueChange(key, value, form, { getColumn, mode, component, immediate, getComponent }) {
// if (!value) {
// form.web_path = undefined
// form.component = undefined
// form.component_name = undefined
// form.cache = false
// form.is_link = false
// }
// }
},
},
is_link: {
title: '外链接',
type: 'dict-switch',
dict: dict({
data: dictionary('button_whether_bool'),
}),
form: {
value: false,
component: {
/* show(context) {
const { form } = context
return !form.is_catalog
}, */
placeholder: '请选择是否外链接',
},
/* valueChange(key, value, form, { getColumn, mode, component, immediate, getComponent }) {
form.web_path = undefined
form.component = undefined
form.component_name = undefined
if (value) {
getColumn('web_path').title = '外链接地址'
getColumn('web_path').component.placeholder = '请输入外链接地址'
getColumn('web_path').helper = {
render(h) {
return (< el-alert title="外链接地址,请以https|http|ftp|rtsp|mms开头" type="warning" />
)
}
}
} else {
getColumn('web_path').title = '路由地址'
getColumn('web_path').component.placeholder = '请输入路由地址'
getColumn('web_path').helper = {
render(h) {
return (< el-alert title="浏览器中url的地址,请以/开头" type="warning" />
)
}
}
}
} */
},
},
web_path: {
title: '路由地址',
width: 150,
column: {
show: false,
},
form: {
rules: [
{ required: true, message: '请输入正确的路由地址' },
{ validator: validateWebPath, trigger: 'change' },
],
component: {
show(context: any) {
const { form } = context;
return !form.is_catalog;
},
props: {
clearable: true,
},
placeholder: '请输入路由地址',
},
helper: {
render(h) {
return <el-alert title="浏览器中url的地址,请以/开头" type="warning" />;
},
},
},
},
component: {
title: '组件地址',
type: 'dict-select',
show: false,
dict: dict({
getData: () => {
const files: any = import.meta.globEager('@views/**/*.vue');
let result: Array<any> = [];
Object.keys(files).forEach((key: string) => {
result.push({
label: key.replace(/(\.\/|\.vue)/g, ''),
value: key.replace(/(\.\/|\.vue)/g, ''),
});
});
return result;
},
}),
column: {
show: false,
},
form: {
rules: [{ required: true, message: '请选择组件地址' }],
component: {
props: {
clearable: true,
filterable: true, // 可过滤选择项
},
placeholder: '请输入组件地址',
},
helper: {
render(h) {
return <el-alert title="src/views下的文件夹地址" type="warning" />;
},
},
},
},
component_name: {
title: '组件名称',
column: {
width: 140,
},
form: {
rules: [{ required: true, message: '请输入组件名称' }],
component: {
show(context: any) {
const { form } = context;
return !form.is_catalog && !form.is_link;
},
props: {
clearable: true,
},
placeholder: '请输入组件名称',
},
helper: {
render(h) {
return <el-alert title="xx.vue文件中的name" type="warning" />;
},
},
},
},
menuPermission: {
title: '拥有权限',
type: 'dict-select',
form: {
show: false,
component: {
elProps: {
// el-select的配置项https://element.eleme.cn/#/zh-CN/component/select
filterable: true,
multiple: true,
clearable: true,
},
},
},
column: {
width: 260,
},
},
cache: {
title: '缓存',
column: {
width: 60,
},
search: {
show: true,
},
type: 'dict-switch',
dict: dict({
data: dictionary('button_whether_bool'),
}),
form: {
value: false,
component: {
// show(context) {
// const { form } = context
// return !form.is_catalog
// },
placeholder: '请选择是否缓存',
},
helper: {
render(h) {
return <el-alert title="是否开启页面缓存,需要组件名称和xx.vue页面的name一致" type="warning" />;
},
},
},
},
visible: {
title: '侧边可见',
column: {
width: 100,
},
search: {
show: true,
},
type: 'dict-switch',
dict: dict({
data: dictionary('button_whether_bool'),
}),
form: {
value: true,
component: {
placeholder: '请选择侧边可见',
},
rules: [
// 表单校验规则
{ required: true, message: '侧边可见必填项' },
],
helper: {
render(h) {
return <el-alert title="是否显示在侧边菜单中" type="warning" />;
},
},
},
},
status: {
title: '状态',
sortable: true,
search: {
show: true,
},
column: {
width: 80,
},
type: 'dict-switch',
dict: dict({
data: dictionary('button_status_bool'),
}),
form: {
value: true,
component: {
placeholder: '请选择状态',
},
rules: [
// 表单校验规则
{ required: true, message: '状态必填项' },
],
},
},
},
columns: {},
},
};
};
}

View File

@@ -1,30 +1,384 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<MenuButton ref="menuButtonRef"></MenuButton>
</fs-page>
<el-row class="mx-2">
<el-col :span="4" class="p-1">
<el-card :body-style="{ height: '100%' }">
<p class="font-mono font-black text-center text-xl pb-5">
菜单列表
<el-tooltip effect="dark" :content="content" placement="right">
<el-icon> <QuestionFilled /> </el-icon>
</el-tooltip>
</p>
<el-input v-model="filterText" :placeholder="placeholder" />
<el-tree
ref="treeRef"
class="font-mono font-bold leading-6 text-7xl"
:data="data"
:props="treeProps"
:filter-node-method="filterNode"
:load="loadNode"
:allow-drag="allowDrag"
:allow-drop="allowDrop"
@node-drop="nodeDrop"
lazy
icon="ArrowRightBold"
:indent="12"
draggable
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span v-if="data.status" class="text-center font-black text-xl"><SvgIcon :name="node.data.icon" />&nbsp;{{ node.label }}</span>
<span v-else class="text-center font-black text-xl text-red-700"><SvgIcon :name="node.data.icon" />&nbsp;{{ node.label }}</span>
</template>
</el-tree>
</el-card>
</el-col>
<el-col :span="20" class="p-1">
<el-card :body-style="{ height: '100%' }">
<el-form ref="formRef" :rules="rules" :model="form" label-width="80px" label-position="right">
<el-alert :title="content" type="success" effect="dark" :closable="false" center />
<el-divider>
<strong>菜单配置</strong>
</el-divider>
<el-row>
<el-col :span="10">
<el-form-item label="菜单ID" prop="id"> <el-input v-model="form.id" disabled /> </el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="父级ID" prop="parent"> <el-input v-model="form.parent" /> </el-form-item>
</el-col>
<el-col :span="10">
<el-form-item required label="菜单名称" prop="name"> <el-input v-model="form.name" /> </el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="组件地址" prop="component">
<el-autocomplete
class="w-full"
v-model="form.component"
:fetch-suggestions="querySearch"
:trigger-on-focus="false"
clearable
debounce="100"
placeholder="输入组件地址"
/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item required label="Url" prop="web_path"> <el-input v-model="form.web_path" /> </el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="20">
<el-form-item label="图标" prop="icon">
<IconSelector clearable v-model="form.icon" />
</el-form-item>
</el-col>
<el-col class="center">
<el-divider>
<el-button @click="saveMenu()" type="primary" round>保存</el-button>
<el-button @click="newMenu()" type="success" round>新建</el-button>
<el-button @click="addChildMenu()" type="info" round>添加子级</el-button>
<el-button @click="addSameLevelMenu()" type="warning" round>添加同级</el-button>
<el-button @click="deleteMenu()" type="danger" round>删除菜单</el-button>
</el-divider>
</el-col>
</el-row>
<el-row>
<el-col class="h-full">
<el-card :body-style="{ height: '100%' }" class="mt-10"><menuButton :select-menu="form" /></el-card>
</el-col>
</el-row>
</el-form>
</el-card>
</el-col>
</el-row>
</fs-page>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import MenuButton from './components/menuButton/index.vue'
const menuButtonRef = ref()
defineExpose(menuButtonRef);
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose,menuButtonRef });
// 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
import * as api from './api';
import * as menuButoonApi from './components/menuButton/api';
import { ElForm, ElTree, FormRules } from 'element-plus';
import { ref, onMounted, watch, reactive, toRaw, defineAsyncComponent, nextTick, shallowRef } from 'vue';
import XEUtils from 'xe-utils';
import { errorMessage, successMessage } from '../../../utils/message';
interface Tree {
id: number;
name: string;
status: boolean;
children?: Tree[];
}
interface APIResponseData {
code?: number;
data: [];
msg?: string;
}
interface Form<T> {
[key: string]: T;
}
interface ComponentFileItem {
value: string;
label: string;
}
// 引入组件
const menuButton = defineAsyncComponent(() => import('./components/menuButton/index.vue'));
const IconSelector = defineAsyncComponent(() => import('/@/components/iconSelector/index.vue'));
const SvgIcon = defineAsyncComponent(() => import('/@/components/svgIcon/index.vue'));
const placeholder = ref('请输入菜单名称');
const filterText = ref('');
const treeRef = ref<InstanceType<typeof ElTree>>();
const treeProps = {
children: 'children',
label: 'name',
icon: 'icon',
isLeaf: (data: Tree[], node: Node) => {
// @ts-ignore
if (node.data.is_catalog) {
return false;
} else {
return true;
}
},
};
const validateWebPath = (rule: string, value: string, callback: Function) => {
let pattern = /^\/.*?/;
if (!pattern.test(value)) {
callback(new Error('请输入正确的地址'));
} else {
callback();
}
};
watch(filterText, (val) => {
treeRef.value!.filter(val);
});
const filterNode = (value: string, data: Tree) => {
if (!value) return true;
return toRaw(data).name.indexOf(value) !== -1;
};
// 懒加载
const loadNode = (node: Node, resolve: (data: Tree[]) => void) => {
// @ts-ignore
if (node.level !== 0) {
// @ts-ignore
api.lazyLoadMenu({ parent: node.data.id }).then((res: APIResponseData) => {
resolve(res.data);
});
}
};
// 判断是否可以拖动
const allowDrag = (node: Node) => {
// @ts-ignore
if (node.data.is_catalog) {
return false;
} else {
return true;
}
};
// 判断是否可以被放置
const allowDrop = (draggingNode: Node, dropNode: Node, type: string) => {
// @ts-ignore
if (!dropNode.isLeaf) {
return true;
}
};
const nodeDrop = (draggingNode: Node, dropNode: Node, dropType: string, event: any) => {
// @ts-ignore
if (!dropNode.isLeaf) {
// @ts-ignore
api.dragMenu({ menu_id: draggingNode.data.id, parent_id: dropNode.data.id }).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
}
};
let data = ref([]);
let isAddNewMenu = ref(false); // 判断当前是新增菜单,还是更新保存当前菜单
const permissionDrawerVisible = ref(false);
const content = `
1.红色菜单代表状态禁用;
2.添加菜单,如果是目录,组件地址为空即可;
3.添加根节点菜单父级ID为空即可;
4.支持拖拽菜单;
`;
let form: Form<any> = reactive({
id: '',
parent: '',
name: '',
component: '',
web_path: '',
sort: '',
status: '',
is_catalog: false,
permission: '',
icon: '',
});
let menuPermissonList = ref([]);
const formRef = ref<InstanceType<typeof ElForm>>();
const querySearch = (queryString: string, cb: any) => {
const files: any = import.meta.glob('@views/**/*.vue');
let fileLists: Array<any> = [];
Object.keys(files).forEach((queryString: string) => {
fileLists.push({
label: queryString.replace(/(\.\/|\.vue)/g, ''),
value: queryString.replace(/(\.\/|\.vue)/g, ''),
});
});
const results = queryString ? fileLists.filter(createFilter(queryString)) : fileLists;
// 统一去掉/src/views/前缀
results.forEach((val) => {
val.label = val.label.replace('/src/views/', '');
val.value = val.value.replace('/src/views/', '');
});
cb(results);
};
const createFilter = (queryString: string) => {
return (file: ComponentFileItem) => {
return file.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1;
};
};
const rules = reactive<FormRules>({
// @ts-ignore
web_path: [{ validator: validateWebPath, trigger: 'blur' }],
});
const getData = () => {
api.GetList({}).then((ret: APIResponseData) => {
const responseData = ret.data;
const result = XEUtils.toArrayTree(responseData, {
parentKey: 'parent',
children: 'children',
strict: true,
});
data.value = result;
});
};
const getPermissions = (menu: object) => {
menuButoonApi.GetList(menu).then((res: APIResponseData) => {
menuPermissonList.value = res.data;
});
};
const saveMenu = () => {
formRef.value?.validate((valid, fields) => {
if (valid) {
if (!isAddNewMenu.value) {
// 保存菜单
form.component == '' ? (form.is_catalog = true) : (form.is_catalog = false);
api.UpdateObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
} else {
// 新增菜单
form.component == '' ? (form.is_catalog = true) : (form.is_catalog = false);
api.AddObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
}
} else {
errorMessage('请填写检查表单');
}
});
};
const newMenu = () => {
formRef.value?.resetFields();
isAddNewMenu.value = true;
};
const addChildMenu = () => {
let parentId = form.id;
formRef.value?.resetFields();
form.parent = parentId;
isAddNewMenu.value = true;
};
const addSameLevelMenu = () => {
let parentId = form.parent;
formRef.value?.resetFields();
form.parent = parentId;
isAddNewMenu.value = true;
};
const deleteMenu = () => {
api.DelObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
};
const handleNodeClick = (data: any, node: any, prop: any) => {
Object.keys(toRaw(data)).forEach((key: string) => {
form[key] = data[key];
});
delete form.component_name;
form.id = data.id;
isAddNewMenu.value = false;
// 点击tree node时加载对应的权限菜单
getPermissions({ menu: form.id });
};
const addPermission = () => {
!form.is_catalog ? (permissionDrawerVisible.value = true) : errorMessage('目录没有菜单权限');
};
const drawerClose = () => {
permissionDrawerVisible.value = false;
};
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
getData();
});
</script>
<style lang="scss" scoped>
.el-row {
height: 100%;
.el-col {
height: 100%;
}
}
.el-card {
height: 100%;
}
</style>

View File

@@ -1,331 +1,363 @@
import * as api from "./api";
import {dict, useCompute, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions,} from "@fast-crud/fast-crud";
import tableSelector from "/@/components/tableSelector/index.vue"
import {shallowRef, computed, ref} from "vue";
import manyToMany from "/@/components/manyToMany/index.vue"
import * as api from './api';
import { dict, useCompute, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import tableSelector from '/@/components/tableSelector/index.vue';
import {shallowRef, computed, ref, inject} from 'vue';
import manyToMany from '/@/components/manyToMany/index.vue';
const {compute} = useCompute()
const { compute } = useCompute();
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
crudOptions: CrudOptions;
}
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);
};
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 viewRequest = async ({ row }: { row: any }) => {
return await api.GetObj(row.id);
};
const viewRequest = async ({row}: { row: any }) => {
return await api.GetObj(row.id);
}
const IsReadFunc = computed(() => {
return tabActivted.value === 'receive';
});
const IsReadFunc = computed(() => {
return tabActivted.value === 'receive'
})
//权限判定
const hasPermissions = inject("$hasPermissions")
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
rowHandle: {
buttons: {
edit: {
show: false
},
view: {
click({index,row}) {
crudExpose.openView({index,row})
if(tabActivted.value === 'receive'){
viewRequest({row})
crudExpose.doRefresh()
}
}
}
}
},
columns: {
id: {
title: 'id',
form: {
show: false,
}
},
title: {
title: '标题',
search: {
show: true,
},
type: ["text", "colspan"],
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
component: {span: 24, placeholder: '请输入标题'}
}
},
is_read: {
title: '是否已读',
type: 'dict-select',
column: {
show: IsReadFunc,
},
dict: dict({
data: [
{label: '已读', value: true, color: 'success'},
{label: '未读', value: false, color: 'danger'}
]
}),
form: {
show: false
}
},
target_type: {
title: '目标类型',
type: ['dict-radio', 'colspan'],
width: 120,
dict: dict({
data: [{value: 0, label: '按用户'}, {value: 1, label: '按角色'}, {
value: 2,
label: '按部门'
}, {value: 3, label: '通知公告'}]
}),
form: {
component: {
optionName: "el-radio-button"
},
rules: [
{
required: true,
message: '必选项',
trigger: ['blur', 'change']
}
]
}
},
target_user: {
title: '目标用户',
search: {
disabled: true
},
width: 130,
form: {
component: {
name: shallowRef(tableSelector),
vModel: "modelValue",
displayLabel: compute(({row}) => {
if (row) {
return row.user_info;
}
return null
}),
tableConfig: {
url: '/api/system/user/',
label: 'name',
value: 'id',
isMultiple: true,
columns: [{
prop: 'name',
label: '用户名称',
width: 120
}, {
prop: 'phone',
label: '用户电话',
width: 120
}]
}
},
show: compute(({form}) => {
return form.target_type === 0
}),
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
]
},
column: {
show: false,
component: {
name: shallowRef(manyToMany),
vModel: "modelValue",
bindValue: compute(({row}) => {
return row.user_info
}),
displayLabel: 'name'
}
}
},
target_role: {
title: '目标角色',
search: {
disabled: true
},
width: 130,
form: {
component: {
name: shallowRef(tableSelector),
vModel: "modelValue",
displayLabel: compute(({row}) => {
if (row) {
return row.role_info;
}
return null
}),
tableConfig: {
url: '/api/system/role/',
label: 'name',
value: 'id',
isMultiple: true,
columns: [{
prop: 'name',
label: '角色名称'
},
{
prop: 'key',
label: '权限标识'
}]
}
},
show: compute(({form}) => {
return form.target_type === 1
}),
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
]
},
column: {
show: false,
component: {
name: shallowRef(manyToMany),
vModel: "modelValue",
bindValue: compute(({row}) => {
return row.role_info
}),
displayLabel: 'name'
}
}
},
target_dept: {
title: '目标部门',
search: {
disabled: true
},
width: 130,
type: 'table-selector',
form: {
component: {
name: shallowRef(tableSelector),
vModel: "modelValue",
displayLabel: compute(({form}) => {
return form.target_dept_name;
}),
tableConfig: {
url: '/api/system/dept/all_dept/',
label: 'name',
value: 'id',
isTree: true,
isMultiple: true,
columns: [{
prop: 'name',
label: '部门名称'
},
{
prop: 'status_label',
label: '状态'
},
{
prop: 'parent_name',
label: '父级部门'
}]
}
},
show: compute(({form}) => {
return form.target_type === 2
}),
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
]
},
column: {
show: false,
component: {
name: shallowRef(manyToMany),
vModel: "modelValue",
bindValue: compute(({row}) => {
return row.dept_info
}),
displayLabel: 'name'
}
}
},
content: {
title: '内容',
column: {
width: 300,
show: false
},
type: ["editor-wang5", "colspan"],
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
component: {
disabled: true,
id: "1", // 当同一个页面有多个editor时需要配置不同的id
editorConfig: {
// 是否只读
readOnly: compute((context) => {
const {mode} = context
if (mode === 'add') {
return false
}
return true;
}),
},
uploader: {
type: "form",
buildUrl(res: any) {
return res.url;
}
}
}
}
}
}
}
}
}
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
fixed:'right',
width:150,
buttons: {
edit: {
show: false,
},
view: {
text:"查看",
type:'text',
iconRight:'View',
show:hasPermissions("messageCenter:Search"),
click({ index, row }) {
crudExpose.openView({ index, row });
if (tabActivted.value === 'receive') {
viewRequest({ row });
crudExpose.doRefresh();
}
},
},
remove: {
iconRight: 'Delete',
type: 'text',
show:hasPermissions('messageCenter:Delete')
},
},
},
columns: {
id: {
title: 'id',
form: {
show: false,
},
},
title: {
title: '标题',
search: {
show: true,
},
type: ['text', 'colspan'],
column:{
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: { span: 24, placeholder: '请输入标题' },
},
},
is_read: {
title: '是否已读',
type: 'dict-select',
column: {
show: IsReadFunc.value,
},
dict: dict({
data: [
{ label: '已读', value: true, color: 'success' },
{ label: '未读', value: false, color: 'danger' },
],
}),
form: {
show: false,
},
},
target_type: {
title: '目标类型',
type: ['dict-radio', 'colspan'],
column:{
minWidth: 120,
},
dict: dict({
data: [
{ value: 0, label: '按用户' },
{ value: 1, label: '按角色' },
{
value: 2,
label: '按部门',
},
{ value: 3, label: '通知公告' },
],
}),
form: {
component: {
optionName: 'el-radio-button',
},
rules: [
{
required: true,
message: '必选项',
// @ts-ignore
trigger: ['blur', 'change'],
},
],
},
},
target_user: {
title: '目标用户',
search: {
disabled: true,
},
form: {
component: {
name: shallowRef(tableSelector),
vModel: 'modelValue',
displayLabel: compute(({ row }) => {
if (row) {
return row.user_info;
}
return null;
}),
tableConfig: {
url: '/api/system/user/',
label: 'name',
value: 'id',
isMultiple: true,
columns: [
{
prop: 'name',
label: '用户名称',
width: 120,
},
{
prop: 'phone',
label: '用户电话',
width: 120,
},
],
},
},
show: compute(({ form }) => {
return form.target_type === 0;
}),
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
},
column: {
show: false,
component: {
name: shallowRef(manyToMany),
vModel: 'modelValue',
bindValue: compute(({ row }) => {
return row.user_info;
}),
displayLabel: 'name',
},
},
},
target_role: {
title: '目标角色',
search: {
disabled: true,
},
width: 130,
form: {
component: {
name: shallowRef(tableSelector),
vModel: 'modelValue',
displayLabel: compute(({ row }) => {
if (row) {
return row.role_info;
}
return null;
}),
tableConfig: {
url: '/api/system/role/',
label: 'name',
value: 'id',
isMultiple: true,
columns: [
{
prop: 'name',
label: '角色名称',
},
{
prop: 'key',
label: '权限标识',
},
],
},
},
show: compute(({ form }) => {
return form.target_type === 1;
}),
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
},
column: {
show: false,
component: {
name: shallowRef(manyToMany),
vModel: 'modelValue',
bindValue: compute(({ row }) => {
return row.role_info;
}),
displayLabel: 'name',
},
},
},
target_dept: {
title: '目标部门',
search: {
disabled: true,
},
width: 130,
type: 'table-selector',
form: {
component: {
name: shallowRef(tableSelector),
vModel: 'modelValue',
displayLabel: compute(({ form }) => {
return form.target_dept_name;
}),
tableConfig: {
url: '/api/system/dept/all_dept/',
label: 'name',
value: 'id',
isTree: true,
isMultiple: true,
columns: [
{
prop: 'name',
label: '部门名称',
},
{
prop: 'status_label',
label: '状态',
},
{
prop: 'parent_name',
label: '父级部门',
},
],
},
},
show: compute(({ form }) => {
return form.target_type === 2;
}),
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
},
column: {
show: false,
component: {
name: shallowRef(manyToMany),
vModel: 'modelValue',
bindValue: compute(({ row }) => {
return row.dept_info;
}),
displayLabel: 'name',
},
},
},
content: {
title: '内容',
column: {
width: 300,
show: false,
},
type: ['editor-wang5', 'colspan'],
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
disabled: true,
id: '1', // 当同一个页面有多个editor时需要配置不同的id
editorConfig: {
// 是否只读
readOnly: compute((context) => {
const { mode } = context;
if (mode === 'add') {
return false;
}
return true;
}),
},
uploader: {
type: 'form',
buildUrl(res: any) {
return res.url;
},
},
},
},
},
},
},
};
};

View File

@@ -1,40 +1,38 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import {apiPrefix} from "/@/views/system/messageCenter/api";
import { apiPrefix } from '/@/views/system/messageCenter/api';
export function GetUserInfo(query: PageQuery) {
return request({
url: '/api/system/user/user_info/',
method: 'get',
params: query
});
return request({
url: '/api/system/user/user_info/',
method: 'get',
params: query,
});
}
/**
* 更新用户信息
* @param data
*/
export function updateUserInfo(data: AddReq) {
return request({
url: '/api/system/user/update_user_info/',
method: 'put',
data: data
})
return request({
url: '/api/system/user/update_user_info/',
method: 'put',
data: data,
});
}
/**
* 获取自己接收的消息
* @param query
* @returns {*}
* @constructor
*/
export function GetSelfReceive (query:PageQuery) {
return request({
url: '/api/system/message_center/get_self_receive/',
method: 'get',
params: query
})
export function GetSelfReceive(query: PageQuery) {
return request({
url: '/api/system/message_center/get_self_receive/',
method: 'get',
params: query,
});
}
/***
@@ -42,9 +40,24 @@ export function GetSelfReceive (query:PageQuery) {
* @param data
*/
export function UpdatePassword(data: EditReq) {
return request({
url: '/api/system/user/change_password/',
method: 'put',
data: data
})
return request({
url: '/api/system/user/change_password/',
method: 'put',
data: data,
});
}
/***
* 上传头像
* @param data
*/
export function uploadAvatar(data: AddReq) {
return request({
url: 'api/system/file/',
method: 'post',
data: data,
headers: {
'Content-Type': 'multipart/form-data',
},
});
}

View File

@@ -1,524 +1,522 @@
<template>
<div class="personal layout-pd">
<el-row>
<!-- 个人信息 -->
<el-col :xs="24" :sm="16">
<el-card shadow="hover" header="个人信息">
<div class="personal-user">
<div class="personal-user-left">
<el-upload class="h100 personal-user-left-upload" :action="uploadAvatar.action" :headers="uploadAvatar.headers" multiple :limit="1">
<img v-if="state.personalForm.avatar" :src="state.personalForm.avatar" />
<img v-else src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" />
</el-upload>
</div>
<div class="personal-user-right">
<el-row>
<el-col :span="24" class="personal-title mb18">{{ currentTime }}{{state.personalForm.username}}生活变的再糟糕也不妨碍我变得更好 </el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="8" class="personal-item mb6">
<div class="personal-item-label">昵称</div>
<div class="personal-item-value">{{state.personalForm.name}}</div>
</el-col>
<el-col :xs="24" :sm="16" class="personal-item mb6">
<div class="personal-item-label">部门</div>
<div class="personal-item-value">
<el-tag >{{state.personalForm.dept_info.dept_name}}</el-tag>
</div>
</el-col>
</el-row>
</el-col>
<el-col :span="24">
<el-row>
<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>
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</div>
</el-card>
</el-col>
<div class="personal layout-pd">
<el-row>
<!-- 个人信息 -->
<el-col :xs="24" :sm="16">
<el-card shadow="hover" header="个人信息">
<div class="personal-user">
<div class="personal-user-left">
<avatarSelector v-model="selectImgVisible" @uploadImg="uploadImg" ref="avatarSelectorRef"></avatarSelector>
</div>
<div class="personal-user-right">
<el-row>
<el-col :span="24" class="personal-title mb18"
>{{ currentTime }}{{ state.personalForm.username }}生活变的再糟糕也不妨碍我变得更好
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="8" class="personal-item mb6">
<div class="personal-item-label">昵称</div>
<div class="personal-item-value">{{ state.personalForm.name }}</div>
</el-col>
<el-col :xs="24" :sm="16" class="personal-item mb6">
<div class="personal-item-label">部门</div>
<div class="personal-item-value">
<el-tag>{{ state.personalForm.dept_info.dept_name }}</el-tag>
</div>
</el-col>
</el-row>
</el-col>
<el-col :span="24">
<el-row>
<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>
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</div>
</el-card>
</el-col>
<!-- 消息通知 -->
<el-col :xs="24" :sm="8" class="pl15 personal-info">
<el-card shadow="hover">
<template #header>
<span>消息通知</span>
<span class="personal-info-more" @click="msgMore">更多</span>
</template>
<div class="personal-info-box">
<ul class="personal-info-ul">
<li v-for="(v, k) in state.newsInfoList" :key="k" class="personal-info-li">
<div class="personal-info-li-title">
[{{v.creator_name}},{{v.create_datetime}}] {{v.title}}
</div>
</li>
</ul>
</div>
</el-card>
</el-col>
<!-- 消息通知 -->
<el-col :xs="24" :sm="8" class="pl15 personal-info">
<el-card shadow="hover">
<template #header>
<span>消息通知</span>
<span class="personal-info-more" @click="msgMore">更多</span>
</template>
<div class="personal-info-box">
<ul class="personal-info-ul">
<li v-for="(v, k) in state.newsInfoList" :key="k" class="personal-info-li">
<div class="personal-info-li-title">[{{ v.creator_name }},{{ v.create_datetime }}] {{ v.title }}</div>
</li>
</ul>
</div>
</el-card>
</el-col>
<!-- 更新信息 -->
<el-col :span="24">
<el-card shadow="hover" class="mt15 personal-edit" header="更新信息">
<div class="personal-edit-title">基本信息</div>
<el-form :model="state.personalForm" ref="userInfoFormRef" :rules="rules" size="default" label-width="50px" class="mt35 mb35">
<el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="昵称" prop="name">
<el-input v-model="state.personalForm.name" placeholder="请输入昵称" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="邮箱">
<el-input v-model="state.personalForm.email" placeholder="请输入邮箱" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="手机" prop="mobile">
<el-input v-model="state.personalForm.mobile" placeholder="请输入手机" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="性别">
<el-select v-model="state.personalForm.gender" placeholder="请选择性别" clearable class="w100">
<el-option label="男" :value="1"></el-option>
<el-option label="女" :value="0"></el-option>
<el-option label="保密" :value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-form-item>
<el-button type="primary" @click="submitForm">
<el-icon>
<ele-Position />
</el-icon>
更新个人信息
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="personal-edit-title mb15">账号安全</div>
<div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">账户密码</div>
<div class="personal-edit-safe-item-left-value">当前密码强度</div>
</div>
<div class="personal-edit-safe-item-right">
<el-button text type="primary" @click="passwordFormShow=true">立即修改</el-button>
</div>
</div>
</div>
<div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">密保手机</div>
<div class="personal-edit-safe-item-left-value">已绑定手机{{state.personalForm.mobile}}</div>
</div>
<div class="personal-edit-safe-item-right">
<!-- <el-button text type="primary">立即修改</el-button>-->
</div>
</div>
</div>
<!-- 更新信息 -->
<el-col :span="24">
<el-card shadow="hover" class="mt15 personal-edit" header="更新信息">
<div class="personal-edit-title">基本信息</div>
<el-form :model="state.personalForm" ref="userInfoFormRef" :rules="rules" size="default" label-width="50px" class="mt35 mb35">
<el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="昵称" prop="name">
<el-input v-model="state.personalForm.name" placeholder="请输入昵称" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="邮箱">
<el-input v-model="state.personalForm.email" placeholder="请输入邮箱" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="手机" prop="mobile">
<el-input v-model="state.personalForm.mobile" placeholder="请输入手机" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="性别">
<el-select v-model="state.personalForm.gender" placeholder="请选择性别" clearable class="w100">
<el-option label="男" :value="1"></el-option>
<el-option label="女" :value="0"></el-option>
<el-option label="保密" :value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-form-item>
<el-button type="primary" @click="submitForm">
<el-icon>
<ele-Position />
</el-icon>
更新个人信息
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="personal-edit-title mb15">账号安全</div>
<div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">账户密码</div>
<div class="personal-edit-safe-item-left-value">当前密码强度</div>
</div>
<div class="personal-edit-safe-item-right">
<el-button text type="primary" @click="passwordFormShow = true">立即修改</el-button>
</div>
</div>
</div>
<div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">密保手机</div>
<div class="personal-edit-safe-item-left-value">已绑定手机{{ state.personalForm.mobile }}</div>
</div>
<div class="personal-edit-safe-item-right">
<!-- <el-button text type="primary">立即修改</el-button>-->
</div>
</div>
</div>
<div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">绑定邮箱</div>
<div class="personal-edit-safe-item-left-value">已绑定邮箱{{state.personalForm.email}}</div>
</div>
<div class="personal-edit-safe-item-right">
<!-- <el-button text type="primary">立即设置</el-button>-->
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 密码修改-->
<el-dialog v-model="passwordFormShow" title="密码修改">
<el-form
ref="userPasswordFormRef"
:model="userPasswordInfo"
required-asterisk
label-width="100px"
label-position="left"
:rules="passwordRules"
center
>
<el-form-item label="原密码" required prop="oldPassword">
<el-input
v-model="userPasswordInfo.oldPassword"
placeholder="请输入原始密码"
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>
</el-form-item>
<el-form-item required prop="newPassword2" label="确认密码">
<el-input
type="password"
v-model="userPasswordInfo.newPassword2"
placeholder="请再次输入新密码"
show-password
clearable
></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="settingPassword">
<i class="fa fa-check"></i>提交
</el-button>
</span>
</template>
</el-dialog>
</div>
<div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">绑定邮箱</div>
<div class="personal-edit-safe-item-left-value">已绑定邮箱{{ state.personalForm.email }}</div>
</div>
<div class="personal-edit-safe-item-right">
<!-- <el-button text type="primary">立即设置</el-button>-->
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 密码修改-->
<el-dialog v-model="passwordFormShow" title="密码修改">
<el-form
ref="userPasswordFormRef"
:model="userPasswordInfo"
required-asterisk
label-width="100px"
label-position="left"
:rules="passwordRules"
center
>
<el-form-item label="原密码" required prop="oldPassword">
<el-input v-model="userPasswordInfo.oldPassword" placeholder="请输入原始密码" 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>
</el-form-item>
<el-form-item required prop="newPassword2" label="确认密码">
<el-input type="password" v-model="userPasswordInfo.newPassword2" placeholder="请再次输入新密码" show-password clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="settingPassword"> <i class="fa fa-check"></i>提交 </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="personal">
import { reactive, computed,onMounted,ref } from 'vue';
import { reactive, computed, onMounted, ref, defineAsyncComponent } from 'vue';
import { formatAxis } from '/@/utils/formatTime';
import * as api from './api'
import {ElMessage } from "element-plus";
import {getBaseURL} from "/@/utils/baseUrl";
import * as api from './api';
import { ElMessage } from 'element-plus';
import { getBaseURL } from '/@/utils/baseUrl';
import { Session } from '/@/utils/storage';
import { useRouter } from 'vue-router';
import { useUserInfo } from '/@/stores/userInfo';
import { successMessage } from '/@/utils/message';
// 头像裁剪组件
const avatarSelector = defineAsyncComponent(() => import('/@/components/avatarSelector/index.vue'));
const avatarSelectorRef = ref(null);
// 当前时间提示语
const currentTime = computed(() => {
return formatAxis(new Date());
return formatAxis(new Date());
});
const userInfoFormRef = ref()
const userInfoFormRef = ref();
const rules = reactive({
name: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
mobile: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确手机号' }]
})
// 定义变量内容
const uploadAvatar = reactive({
action:getBaseURL() + 'api/system/file/',
headers: {
Authorization: 'JWT ' + Session.get('token')
},
})
name: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
mobile: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确手机号' }],
});
let selectImgVisible = ref(false);
const state = reactive<PersonalState>({
newsInfoList:[],
personalForm: {
avatar:'',
username:'',
name: '',
email: '',
mobile: '',
gender:'',
dept_info:{
dept_id:0,
dept_name:''
},
role_info:[{
id:0,
name:''
}]
},
newsInfoList: [],
personalForm: {
avatar: '',
username: '',
name: '',
email: '',
mobile: '',
gender: '',
dept_info: {
dept_id: 0,
dept_name: '',
},
role_info: [
{
id: 0,
name: '',
},
],
},
});
/**
* 跳转消息中心
*/
import {useRouter } from "vue-router";
import {Session} from "/@/utils/storage";
const route = useRouter()
const msgMore=()=>{
route.push({path:'/messageCenter'})
}
const route = useRouter();
const msgMore = () => {
route.push({ path: '/messageCenter' });
};
/**
* 获取用户个人信息
*/
const getUserInfo = function (){
api.GetUserInfo({}).then((res:any)=>{
const {data} = res
state.personalForm.avatar = data.avatar || '';
state.personalForm.username = data.username || '';
state.personalForm.name = data.name || '';
state.personalForm.email = data.email || '';
state.personalForm.mobile = data.mobile || '';
state.personalForm.gender = data.gender || '';
state.personalForm.dept_info.dept_name = data.dept_info.dept_name || '';
state.personalForm.role_info = data.role_info || [];
})
}
const getUserInfo = function () {
api.GetUserInfo({}).then((res: any) => {
const { data } = res;
state.personalForm.avatar = data.avatar || '';
state.personalForm.username = data.username || '';
state.personalForm.name = data.name || '';
state.personalForm.email = data.email || '';
state.personalForm.mobile = data.mobile || '';
state.personalForm.gender = data.gender;
state.personalForm.dept_info.dept_name = data.dept_info.dept_name || '';
state.personalForm.role_info = data.role_info || [];
});
};
/**
* 更新用户信息
* @param formEl
*/
const submitForm = async () => {
if (!userInfoFormRef.value) return
await userInfoFormRef.value.validate((valid, fields) => {
if (valid) {
api.updateUserInfo(state.personalForm).then((res:any)=>{
ElMessage.success('更新成功')
getUserInfo()
})
} else {
ElMessage.error('表单验证失败,请检查~')
}
})
}
if (!userInfoFormRef.value) return;
await userInfoFormRef.value.validate((valid, fields) => {
if (valid) {
api.updateUserInfo(state.personalForm).then((res: any) => {
ElMessage.success('更新成功');
getUserInfo();
});
} else {
ElMessage.error('表单验证失败,请检查~');
}
});
};
/**
* 获取消息通知
*/
const getMsg = () => {
api.GetSelfReceive({}).then((res:any)=>{
const {data} = res
state.newsInfoList = data || [];
})
}
onMounted(()=>{
getUserInfo();
getMsg();
})
api.GetSelfReceive({}).then((res: any) => {
const { data } = res;
state.newsInfoList = data || [];
});
};
onMounted(() => {
getUserInfo();
getMsg();
});
/**************************密码修改部分************************/
const passwordFormShow = ref(false)
const userPasswordFormRef = ref()
const userPasswordInfo=reactive({
oldPassword: '',
newPassword: '',
newPassword2: ''
})
const passwordFormShow = ref(false);
const userPasswordFormRef = ref();
const userPasswordInfo = reactive({
oldPassword: '',
newPassword: '',
newPassword2: '',
});
const validatePass = (rule, value, callback) => {
const pwdRegex = new RegExp('(?=.*[0-9])(?=.*[a-zA-Z]).{8,30}')
if (value === '') {
callback(new Error('请输入密码'))
} else if (value === userPasswordInfo.oldPassword) {
callback(new Error('原密码与新密码一致'))
} else if (!pwdRegex.test(value)) {
callback(new Error('您的密码复杂度太低(密码中必须包含字母、数字)'))
} else {
if (userPasswordInfo.newPassword2 !== '') {
userPasswordFormRef.value.validateField('newPassword2')
}
callback()
}
}
const pwdRegex = new RegExp('(?=.*[0-9])(?=.*[a-zA-Z]).{8,30}');
if (value === '') {
callback(new Error('请输入密码'));
} else if (value === userPasswordInfo.oldPassword) {
callback(new Error('原密码与新密码一致'));
} else if (!pwdRegex.test(value)) {
callback(new Error('您的密码复杂度太低(密码中必须包含字母、数字)'));
} else {
if (userPasswordInfo.newPassword2 !== '') {
userPasswordFormRef.value.validateField('newPassword2');
}
callback();
}
};
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== userPasswordInfo.newPassword) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== userPasswordInfo.newPassword) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
const passwordRules=reactive({
oldPassword: [
{
required: true,
message: '请输入原密码',
trigger: 'blur'
}
],
newPassword: [{ validator: validatePass, trigger: 'blur' }],
newPassword2: [{ validator: validatePass2, trigger: 'blur' }]
})
const passwordRules = reactive({
oldPassword: [
{
required: true,
message: '请输入原密码',
trigger: 'blur',
},
],
newPassword: [{ validator: validatePass, trigger: 'blur' }],
newPassword2: [{ validator: validatePass2, trigger: 'blur' }],
});
/**
* 重新设置密码
*/
const settingPassword= ()=>{
userPasswordFormRef.value.validate((valid) => {
if (valid) {
api.UpdatePassword(userPasswordInfo).then((res:any)=>{
ElMessage.success('密码修改成功')
})
} else {
// 校验失败
// 登录表单校验失败
ElMessage.error('表单校验失败,请检查')
}
})
}
const settingPassword = () => {
userPasswordFormRef.value.validate((valid) => {
if (valid) {
api.UpdatePassword(userPasswordInfo).then((res: any) => {
ElMessage.success('密码修改成功');
});
} else {
// 校验失败
// 登录表单校验失败
ElMessage.error('表单校验失败,请检查');
}
});
};
const uploadImg = (data: any) => {
let formdata = new FormData();
formdata.append('file', data);
api.uploadAvatar(formdata).then((res: any) => {
if (res.code === 2000) {
selectImgVisible.value = false;
state.personalForm.avatar = getBaseURL() + res.data.url;
api.updateUserInfo(state.personalForm).then((res: any) => {
successMessage('更新成功');
getUserInfo();
useUserInfo().updateUserInfos();
// @ts-ignore
avatarSelectorRef.value.updateAvatar(state.personalForm.avatar);
});
}
});
};
</script>
<style scoped lang="scss">
@import '/@/theme/mixins/index.scss';
.personal {
.personal-user {
height: 130px;
display: flex;
align-items: center;
.personal-user-left {
width: 100px;
height: 130px;
border-radius: 3px;
:deep(.el-upload) {
height: 100%;
}
.personal-user-left-upload {
img {
width: 100%;
height: 100%;
border-radius: 3px;
}
&:hover {
img {
animation: logoAnimation 0.3s ease-in-out;
}
}
}
}
.personal-user-right {
flex: 1;
padding: 0 15px;
.personal-title {
font-size: 18px;
@include text-ellipsis(1);
}
.personal-item {
display: flex;
align-items: center;
font-size: 13px;
.personal-item-label {
color: var(--el-text-color-secondary);
@include text-ellipsis(1);
}
.personal-item-value {
@include text-ellipsis(1);
}
}
}
}
.personal-info {
.personal-info-more {
float: right;
color: var(--el-text-color-secondary);
font-size: 13px;
&:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
.personal-info-box {
height: 130px;
overflow: hidden;
.personal-info-ul {
list-style: none;
.personal-info-li {
font-size: 13px;
padding-bottom: 10px;
.personal-info-li-title {
display: inline-block;
@include text-ellipsis(1);
color: var(--el-text-color-secondary);
text-decoration: none;
}
& a:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
}
}
}
.personal-recommend-row {
.personal-recommend-col {
.personal-recommend {
position: relative;
height: 100px;
border-radius: 3px;
overflow: hidden;
cursor: pointer;
&:hover {
i {
right: 0px !important;
bottom: 0px !important;
transition: all ease 0.3s;
}
}
i {
position: absolute;
right: -10px;
bottom: -10px;
font-size: 70px;
transform: rotate(-30deg);
transition: all ease 0.3s;
}
.personal-recommend-auto {
padding: 15px;
position: absolute;
left: 0;
top: 5%;
color: var(--next-color-white);
.personal-recommend-msg {
font-size: 12px;
margin-top: 10px;
}
}
}
}
}
.personal-edit {
.personal-edit-title {
position: relative;
padding-left: 10px;
color: var(--el-text-color-regular);
&::after {
content: '';
width: 2px;
height: 10px;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
background: var(--el-color-primary);
}
}
.personal-edit-safe-box {
border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
padding: 15px 0;
.personal-edit-safe-item {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.personal-edit-safe-item-left {
flex: 1;
overflow: hidden;
.personal-edit-safe-item-left-label {
color: var(--el-text-color-regular);
margin-bottom: 5px;
}
.personal-edit-safe-item-left-value {
color: var(--el-text-color-secondary);
@include text-ellipsis(1);
margin-right: 15px;
}
}
}
&:last-of-type {
padding-bottom: 0;
border-bottom: none;
}
}
}
.personal-user {
height: 130px;
display: flex;
align-items: center;
.personal-user-left {
width: 100px;
height: 130px;
border-radius: 3px;
:deep(.el-upload) {
height: 100%;
}
.personal-user-left-upload {
img {
width: 100%;
height: 100%;
border-radius: 3px;
}
&:hover {
img {
animation: logoAnimation 0.3s ease-in-out;
}
}
}
}
.personal-user-right {
flex: 1;
padding: 0 15px;
.personal-title {
font-size: 18px;
@include text-ellipsis(1);
}
.personal-item {
display: flex;
align-items: center;
font-size: 13px;
.personal-item-label {
color: var(--el-text-color-secondary);
@include text-ellipsis(1);
}
.personal-item-value {
@include text-ellipsis(1);
}
}
}
}
.personal-info {
.personal-info-more {
float: right;
color: var(--el-text-color-secondary);
font-size: 13px;
&:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
.personal-info-box {
height: 130px;
overflow: hidden;
.personal-info-ul {
list-style: none;
.personal-info-li {
font-size: 13px;
padding-bottom: 10px;
.personal-info-li-title {
display: inline-block;
@include text-ellipsis(1);
color: var(--el-text-color-secondary);
text-decoration: none;
}
& a:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
}
}
}
.personal-recommend-row {
.personal-recommend-col {
.personal-recommend {
position: relative;
height: 100px;
border-radius: 3px;
overflow: hidden;
cursor: pointer;
&:hover {
i {
right: 0px !important;
bottom: 0px !important;
transition: all ease 0.3s;
}
}
i {
position: absolute;
right: -10px;
bottom: -10px;
font-size: 70px;
transform: rotate(-30deg);
transition: all ease 0.3s;
}
.personal-recommend-auto {
padding: 15px;
position: absolute;
left: 0;
top: 5%;
color: var(--next-color-white);
.personal-recommend-msg {
font-size: 12px;
margin-top: 10px;
}
}
}
}
}
.personal-edit {
.personal-edit-title {
position: relative;
padding-left: 10px;
color: var(--el-text-color-regular);
&::after {
content: '';
width: 2px;
height: 10px;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
background: var(--el-color-primary);
}
}
.personal-edit-safe-box {
border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
padding: 15px 0;
.personal-edit-safe-item {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.personal-edit-safe-item-left {
flex: 1;
overflow: hidden;
.personal-edit-safe-item-left-label {
color: var(--el-text-color-regular);
margin-bottom: 5px;
}
.personal-edit-safe-item-left-value {
color: var(--el-text-color-secondary);
@include text-ellipsis(1);
margin-right: 15px;
}
}
}
&:last-of-type {
padding-bottom: 0;
border-bottom: none;
}
}
}
}
</style>

View File

@@ -0,0 +1,236 @@
import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from '@fast-crud/fast-crud';
import _ from 'lodash-es';
import * as api from './api';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '../../../utils/message';
import {inject} from "vue";
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
//此处为crudOptions配置
export const createCrudOptions = function ({ crudExpose, rolePermission }: { crudExpose: CrudExpose; rolePermission: any }): 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);
};
//权限判定
const hasPermissions = inject("$hasPermissions")
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show:hasPermissions('role:Update')
},
remove: {
iconRight: 'Delete',
type: 'text',
show:hasPermissions('role:Delete')
},
custom: {
text: '权限配置',
type: 'text',
show:hasPermissions('role:Update'),
tooltip: {
placement: 'top',
content: '权限配置',
},
click: (context: any): void => {
const { row } = context;
// eslint-disable-next-line no-mixed-spaces-and-tabs
rolePermission.value.drawer = true;
rolePermission.value.editedRoleInfo = row;
rolePermission.value.initGet();
},
},
},
},
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, //禁止在列设置中选择
},
},
id: {
title: 'ID',
type: 'text',
column: { show: false },
search: { show: false },
form: { show: false },
},
name: {
title: '角色名称',
type: 'text',
search: { show: true },
column: {
minWidth: 120,
sortable: 'custom',
},
form: {
rules: [{ required: true, message: '角色名称必填' }],
component: {
placeholder: '请输入角色名称',
},
},
},
key: {
title: '权限标识',
type: 'text',
search: { show: false },
column: {
width: 120,
sortable: 'custom',
},
form: {
rules: [{ required: true, message: '权限标识必填' }],
placeholder: '输入权限标识',
},
},
sort: {
title: '排序',
search: { show: false },
type: 'number',
column: {
width: 90,
sortable: 'custom',
},
form: {
rules: [{ required: true, message: '排序必填' }],
value: 1,
},
},
admin: {
title: '是否管理员',
search: { show: false },
type: 'dict-radio',
dict: dict({
data: [
{
label: '是',
value: true,
color: 'success',
},
{
label: '否',
value: false,
color: 'danger',
},
],
}),
column: {
width: 130,
sortable: 'custom',
},
form: {
rules: [{ required: true, message: '是否管理员必填' }],
value: false,
},
},
status: {
title: '状态',
search: { show: true },
type: 'dict-radio',
column: {
width:100,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: #409eff; --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'),
}),
},
update_datetime: {
title: '更新时间',
type: 'text',
search: { show: false },
column: {
width: 170,
sortable: 'custom',
},
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
create_datetime: {
title: '创建时间',
type: 'text',
search: { show: false },
column: {
sortable: 'custom',
width: 170,
},
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
// description: {
// title: '备注',
// type: 'textarea',
// search: {show: false},
// form: {
// component: {
// maxlength: 200,
// placeholder: '输入备注',
// },
// },
// },
},
},
};
};

View File

@@ -1,237 +0,0 @@
import {CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose} from '@fast-crud/fast-crud';
import _ from 'lodash-es';
import * as api from "./api";
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
//此处为crudOptions配置
export const createCrudOptions = function ({crudExpose,rolePermission}: {crudExpose: CrudExpose,rolePermission:any}): 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);
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: "right",
width:310,
buttons: {
custom: {
text: "权限配置",
type:'warning',
tooltip: {
placement: "top",
content: "删除"
},
click: (context:any):void => {
const {row} = context
// eslint-disable-next-line no-mixed-spaces-and-tabs
rolePermission.value.drawer=true
rolePermission.value.editedRoleInfo = row
rolePermission.value.initGet()
}
}
},
},
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, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose.crudBinding.value.pagination;
// @ts-ignore
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
search: {
title: '关键词',
column: {show: false},
type: 'text',
search: {show: true},
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
id: {
title: 'ID',
type: 'text',
column: {show: false},
search: {show: false},
form: {show: false},
},
name: {
title: '角色名称',
type: 'text',
search: {show: true},
column: {
minWidth: 120,
sortable: true,
},
form: {
rules: [{required: true, message: '角色名称必填'}],
component: {
placeholder: '输入角色名称搜索',
},
},
},
key: {
title: '权限标识',
type: 'text',
search: {show: false},
column: {
width: 120,
sortable: true,
},
form: {
rules: [{required: true, message: '权限标识必填'}],
placeholder: '输入权限标识',
},
},
sort: {
title: '排序',
search: {show: false},
type: 'number',
column: {
width: 90,
sortable: true,
},
form: {
value: 1,
},
},
admin: {
title: '是否管理员',
search: {show: false},
type: 'dict-radio',
dict: dict({
data: [
{
label: '是',
value: true,
color: 'success',
},
{
label: '否',
value: false,
color: 'danger',
},
],
}),
column: {
width: 130,
sortable: true,
},
form: {
value: false,
},
},
status: {
title: '状态',
search: {show: true},
type: 'dict-radio',
dict: dict({
data: [
{
label: '启用',
value: true,
color: 'success',
},
{
label: '禁用',
value: false,
color: 'danger',
},
],
}),
column: {
width: 90,
sortable: true,
},
form: {
value: true,
},
},
update_datetime: {
title: '更新时间',
type: 'text',
search: {show: false},
column: {
width: 170,
sortable: true,
},
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
create_datetime: {
title: '创建时间',
type: 'text',
search: {show: false},
column: {
sortable: true,
width: 170,
},
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
description: {
title: '备注',
type: 'textarea',
search: {show: false},
form: {
component: {
maxlength: 200,
placeholder: '输入备注',
},
},
},
},
},
};
};

View File

@@ -11,11 +11,11 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useExpose, useCrud,dict } from '@fast-crud/fast-crud';
import { createCrudOptions } from './curd';
import { useExpose, useCrud, dict } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import RolePermission from '/@/views/system/rolePermission/index.vue';
import * as api from './api'
import _ from "lodash-es";
import * as api from './api';
import _ from 'lodash-es';
const rolePermission = ref();
defineExpose(rolePermission);
// crud组件的ref

View File

@@ -7,18 +7,24 @@
:before-close="handleClose"
>
<template #header>
<div >
<el-tag size="large" type="primary">当前角色:{{ editedRoleInfo.name }}</el-tag>
<div>
<el-tag size="large" type="primary">当前角色:{{ editedRoleInfo.name }}</el-tag>
</div>
</template>
</template>
<div style="padding: 1em">
<el-row :gutter="10">
<el-col :xs="24" :sm="24" :md="8" :lg="6" :xl="6">
<el-alert title="针对角色的页面菜单进行授权" description="点击菜单项,可对菜单下的按钮/接口授权" type="warning" />
<el-card header="菜单页面授权">
<template #header>
<div class="card-header">
<span>菜单页面</span>
<el-tooltip effect="dark" content="点击菜单项,可对菜单下的按钮/接口授权" placement="right">
<div>
<span>菜单页面</span>
<el-icon>
<QuestionFilled/>
</el-icon>
</div>
</el-tooltip>
<el-button size="mini" type="primary" @click="onSaveAuth">保存菜单授权</el-button>
</div>
</template>
@@ -35,11 +41,18 @@
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="16" :lg="18" :xl="18">
<el-alert title="对页面菜单下按钮授权" description="新增或删除对菜单下的按钮/接口授权" type="warning" />
<!-- <el-alert title="对页面菜单下按钮授权" description="新增或删除对菜单下的按钮/接口授权" type="warning" />-->
<el-card v-if="isBtnPermissionShow">
<template #header>
<div class="card-header">
<span>按钮/接口授权</span>
<el-tooltip effect="dark" content="新增或删除对菜单下的按钮/接口授权" placement="right">
<div>
<span>按钮/接口授权</span>
<el-icon>
<QuestionFilled/>
</el-icon>
</div>
</el-tooltip>
</div>
</template>
<div>
@@ -49,12 +62,12 @@
<el-table size="small" :data="buttonPermissionData" border style="width: 100%">
<el-table-column prop="menu_button" label="权限名称" width="100">
<template #default="scope">
<div>{{formatMenuBtn(scope.row.menu_button)}}</div>
<div>{{ formatMenuBtn(scope.row.menu_button) }}</div>
</template>
</el-table-column>
<el-table-column prop="data_range" label="权限范围" width="140">
<template #default="scope">
<div>{{formatDataRange(scope.row.data_range)}}</div>
<div>{{ formatDataRange(scope.row.data_range) }}</div>
</template>
</el-table-column>
<el-table-column prop="dept" label="权限涉及部门"/>
@@ -139,8 +152,8 @@
import {ref, defineExpose, reactive, toRefs} from 'vue'
import {ElMessageBox} from 'element-plus'
import * as api from './api'
import type {FormRules,FormInstance} from 'element-plus'
import { ElMessage } from 'element-plus'
import type {FormRules, FormInstance} from 'element-plus'
import {ElMessage} from 'element-plus'
import XEUtils from 'xe-utils'
//抽屉是否显示
const drawer = ref(false)
@@ -190,7 +203,7 @@ let buttonOptions = ref<[]>()
let editedMenuInfo = ref()
//菜单节点点击事件
const menuNodeClick = (node: any, obj: any) => {
isBtnPermissionShow.value = !node.is_catalog
isBtnPermissionShow.value = !node.is_catalog
if (!node.is_catalog) {
buttonOptions.value = []
editedMenuInfo.value = node
@@ -198,7 +211,7 @@ const menuNodeClick = (node: any, obj: any) => {
const {data} = res
buttonOptions.value = data
})
api.getObj({menu: node.id,role:editedRoleInfo.value.id}).then((res:any)=>{
api.getObj({menu: node.id, role: editedRoleInfo.value.id}).then((res: any) => {
const {data} = res
buttonPermissionData.value = data
})
@@ -211,7 +224,7 @@ const menuTree = ref()
//是否显示新增表单
const dialogFormVisible = ref(false)
//部门树
const deptTree=ref()
const deptTree = ref()
//自定义部门数据
const deptOptions = ref()
//选中的部门数据
@@ -220,7 +233,7 @@ const deptCheckedKeys = []
const buttonForm = reactive({
menu_button: null,
role: null,
menu:null,
menu: null,
data_range: null,
dept: []
})
@@ -256,20 +269,20 @@ const onChangeButton = (val: any) => {
})
//获取权限部门值
api.GetDataScopeDept({menu_button: val}).then((res: any) => {
deptOptions.value = XEUtils.toArrayTree(res.data,{ parentKey: 'parent', strict: false })
deptOptions.value = XEUtils.toArrayTree(res.data, {parentKey: 'parent', strict: false})
})
}
//过滤按钮名称
const formatMenuBtn = (val:any)=>{
let obj:any = buttonOptions.value?.find((item:any)=>{
return item.id===val
const formatMenuBtn = (val: any) => {
let obj: any = buttonOptions.value?.find((item: any) => {
return item.id === val
})
return obj?obj.name:null
return obj ? obj.name : null
}
//过滤权限范围
const formatDataRange = (val:any)=>{
let obj:any = [
const formatDataRange = (val: any) => {
let obj: any = [
{
"value": 0,
"label": '仅本人数据权限'
@@ -290,17 +303,17 @@ const formatDataRange = (val:any)=>{
"value": 4,
"label": '自定义数据权限'
}
].find((item:any)=>{
return item.value===val
].find((item: any) => {
return item.value === val
})
return obj?obj.label:null
return obj ? obj.label : null
}
//保存按钮表单
const onSaveButtonForm = async () => {
const {id:roleId} = editedRoleInfo.value
const {id:menuId} = editedMenuInfo.value
const form:any = Object.assign({},buttonForm)
const {id: roleId} = editedRoleInfo.value
const {id: menuId} = editedMenuInfo.value
const form: any = Object.assign({}, buttonForm)
form.role = roleId
form.menu = menuId
//选中的部门
@@ -309,7 +322,7 @@ const onSaveButtonForm = async () => {
if (!buttonFormRef.value) return
await buttonFormRef.value.validate((valid, fields) => {
if (valid) {
api.CreatePermission(form).then((res:any)=>{
api.CreatePermission(form).then((res: any) => {
buttonPermissionData.value.push(form)
dialogFormVisible.value = false
ElMessage({
@@ -320,7 +333,7 @@ const onSaveButtonForm = async () => {
} else {
ElMessage({
type: 'error',
title:'提交错误',
title: '提交错误',
message: 'F12控制台看详情',
})
console.log('提交错误', fields)
@@ -329,8 +342,8 @@ const onSaveButtonForm = async () => {
}
//删除按钮权限
const onDeleteBtn = (scope:any)=>{
const {row,$index} = scope
const onDeleteBtn = (scope: any) => {
const {row, $index} = scope
ElMessageBox.confirm(
'您是否要删除数据?',
'温馨提示',
@@ -339,15 +352,15 @@ const onDeleteBtn = (scope:any)=>{
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
api.DeletePermission({id:row.id}).then(res=>{
buttonPermissionData.value.splice($index,1)
ElMessage({
type: 'success',
message: res.msg,
})
})
).then(() => {
api.DeletePermission({id: row.id}).then(res => {
buttonPermissionData.value.splice($index, 1)
ElMessage({
type: 'success',
message: res.msg,
})
})
})
.catch(() => {
ElMessage({
type: 'info',
@@ -373,12 +386,12 @@ const onSaveAuth = () => {
//合并的菜单数据
const menuIdList = [...checkedList, ...halfCheckedList]
// console.log(menuIdList)
const { id:roleId } = editedRoleInfo.value
const {id: roleId} = editedRoleInfo.value
const data = {
role:roleId,
menu:menuIdList
role: roleId,
menu: menuIdList
}
api.SaveMenuPermission(data).then((res:any)=>{
api.SaveMenuPermission(data).then((res: any) => {
ElMessage({
message: res.msg,
type: 'success',

View File

@@ -2,6 +2,15 @@ import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
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 GetList(query: PageQuery) {
return request({
url: apiPrefix,

View File

@@ -1,320 +1,341 @@
import * as api from "./api";
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, } from "@fast-crud/fast-crud";
import { request } from "/@/utils/service";
import { dictionary } from "/@/utils/dictionary";
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
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 { successMessage } from '/@/utils/message';
import { inject } from 'vue';
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
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);
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
rowHandle: {
//固定右侧
fixed: "right",
width: 310,
buttons: {
orderExample: {
show:false,
text: "重置密码",
click: () => {
//console.log("reset password")
}
}
},
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);
};
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
search: {
title: '关键词',
column: {
show: false
},
search: {
show: true,
component: {
props: {
clearable: true
},
placeholder: '请输入关键词'
}
},
form: {
show: false,
component: {
props: {
clearable: true
}
}
},
},
username: {
title: '账号',
search: {
show: true,
},
minWidth: 100,
type: 'input',
form: {
rules: [ // 表单校验规则
{
required: true,
message: '账号必填项'
}
],
component: {
placeholder: '请输入账号'
},
}
},
password: {
title: '密码',
type: 'input',
column: {
show: false
},
editForm: {
show: false
},
form: {
rules: [ // 表单校验规则
{
required: true,
message: '密码必填项'
}
],
component: {
span: 12,
showPassword: true,
placeholder: '请输入密码'
},
// value: vm.systemConfig('base.default_password'),
},
/* valueResolve(row, key) {
//权限判定
const hasPermissions = inject('$hasPermissions');
return {
crudOptions: {
table: {
remove: {
confirmMessage: '是否删除该用户?',
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 140,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show: hasPermissions('user:Update'),
},
remove: {
iconRight: 'Delete',
type: 'text',
show: hasPermissions('user:Delete'),
},
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
username: {
title: '账号',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '账号必填项',
},
],
component: {
placeholder: '请输入账号',
},
},
},
password: {
title: '密码',
type: 'input',
column: {
show: false,
},
editForm: {
show: false,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '密码必填项',
},
],
component: {
span: 12,
showPassword: true,
placeholder: '请输入密码',
},
// value: vm.systemConfig('base.default_password'),
},
/* valueResolve(row, key) {
if (row.password) {
row.password = vm.$md5(row.password)
}
} */
},
name: {
title: '姓名',
search: {
show: true,
},
type: 'input',
form: {
rules: [ // 表单校验规则
{
required: true,
message: '姓名必填项'
}
],
component: {
span: 12,
placeholder: '请输入姓名'
},
}
},
dept: {
title: '部门',
search: {
disabled: true
},
type: 'dict-tree',
dict: dict({
isTree: true,
url: '/api/system/dept/all_dept/',
value: 'id',
label: 'name',
getData: async ({ url }: { url: string }) => {
return request({
url: url,
}).then((ret: any) => {
return ret.data
})
}
}),
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
component: {
filterable: true,
placeholder: '请选择',
props: {
props: {
value: "id",
label: "name",
}
}
},
},
},
role: {
title: '角色',
search: {
disabled: true
},
type: 'dict-select',
dict: dict({
url: '/api/system/role/',
value: 'id',
label: 'name',
isTree: true,
getData: async ({ url }: { url: string }) => {
return request({
url: url,
params: {
page: 1,
limit: 10
}
}).then((ret: any) => {
return ret.data
})
}
}),
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
component: {
multiple: true,
filterable: true,
placeholder: '请选择角色'
},
}
},
mobile: {
title: '手机号码',
search: {
show: true,
},
type: 'input',
form: {
rules: [
{
max: 20,
message: '请输入正确的手机号码',
trigger: 'blur'
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号码'
}
],
component: {
placeholder: '请输入手机号码'
}
}
},
email: {
title: '邮箱',
column:{
width:260
},
form: {
rules: [
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: ['blur', 'change']
}
],
component: {
placeholder: '请输入邮箱'
}
}
},
gender: {
title: '性别',
type: 'dict-radio',
dict: dict({
data: dictionary('gender')
}),
form: {
value: 1,
component: {
span: 12
}
},
component: { props: { color: 'auto' } } // 自动染色
},
user_type: {
title: '用户类型',
search: {
show: true,
},
type: 'dict-select',
dict: dict({
data: dictionary('user_type')
}),
form: {
show: false,
value: 0,
component: {
span: 12
}
}
},
is_active: {
title: '状态',
search: {
show: true,
},
type: 'dict-radio',
dict: dict({
data: dictionary('button_status_bool')
}),
form: {
value: true,
component: {
span: 12
}
}
},
avatar: {
title: '头像',
type: 'avatar-cropper',
form:{
show:false
}
}
}
}
};
}
},
name: {
title: '姓名',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '姓名必填项',
},
],
component: {
span: 12,
placeholder: '请输入姓名',
},
},
},
dept: {
title: '部门',
search: {
disabled: true,
},
type: 'dict-tree',
dict: dict({
isTree: true,
url: '/api/system/dept/all_dept/',
value: 'id',
label: 'name',
getData: async ({ url }: { url: string }) => {
return request({
url: url,
}).then((ret: any) => {
return ret.data;
});
},
}),
column: {
minWidth: 150, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
filterable: true,
placeholder: '请选择',
props: {
props: {
value: 'id',
label: 'name',
},
},
},
},
},
role: {
title: '角色',
search: {
disabled: true,
},
type: 'dict-select',
dict: dict({
url: '/api/system/role/',
value: 'id',
label: 'name',
isTree: true,
getData: async ({ url }: { url: string }) => {
return request({
url: url,
params: {
page: 1,
limit: 10,
},
}).then((ret: any) => {
return ret.data;
});
},
}),
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
multiple: true,
filterable: true,
placeholder: '请选择角色',
},
},
},
mobile: {
title: '手机号码',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 120, //最小列宽
},
form: {
rules: [
{
max: 20,
message: '请输入正确的手机号码',
trigger: 'blur',
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号码',
},
],
component: {
placeholder: '请输入手机号码',
},
},
},
email: {
title: '邮箱',
column: {
width: 260,
},
form: {
rules: [
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: ['blur', 'change'],
},
],
component: {
placeholder: '请输入邮箱',
},
},
},
gender: {
title: '性别',
type: 'dict-select',
dict: dict({
data: dictionary('gender'),
}),
form: {
value: 1,
component: {
span: 12,
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
user_type: {
title: '用户类型',
search: {
show: true,
},
type: 'dict-select',
dict: dict({
data: dictionary('user_type'),
}),
column: {
minWidth: 100, //最小列宽
},
form: {
show: false,
value: 0,
component: {
span: 12,
},
},
},
is_active: {
title: '锁定',
search: {
show: true,
},
type: 'dict-radio',
column: {
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: #409eff; --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'),
}),
},
avatar: {
title: '头像',
type: 'avatar-cropper',
form: {
show: false,
},
},
},
},
};
};

View File

@@ -1,13 +1,115 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<el-row class="mx-2">
<el-col xs="24" :sm="8" :md="6" :lg="4" :xl="4" class="p-1">
<el-card :body-style="{ height: '100%' }">
<p class="font-mono font-black text-center text-xl pb-5">
部门列表
<el-tooltip effect="dark" :content="content" placement="right">
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
</p>
<el-input v-model="filterText" :placeholder="placeholder" />
<el-tree
ref="treeRef"
class="font-mono font-bold leading-6 text-7xl"
:data="data"
:props="treeProps"
:filter-node-method="filterNode"
icon="ArrowRightBold"
:indent="12"
@node-click="onTreeNodeClick"
>
<template #default="{ node, data }">
<span class="text-center font-black text-xl">{{ node.label }}</span>
</template>
</el-tree>
</el-card>
</el-col>
<el-col xs="24" :sm="16" :md="18" :lg="20" :xl="20" class="p-1">
<el-card :body-style="{ height: '100%' }">
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
</el-card>
</el-col>
</el-row>
</fs-page>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import * as api from './api';
import { ElTree } from 'element-plus';
import { ref, onMounted, watch, toRaw, defineAsyncComponent } from 'vue';
import XEUtils from 'xe-utils';
import { errorMessage, successMessage } from '../../../utils/message';
import { GetDept } from './api';
import { dictionary } from '/@/utils/dictionary';
interface Tree {
id: number;
name: string;
status: boolean;
children?: Tree[];
}
interface APIResponseData {
code?: number;
data: [];
msg?: string;
}
// 引入组件
const placeholder = ref('请输入部门名称');
const filterText = ref('');
const treeRef = ref<InstanceType<typeof ElTree>>();
const treeProps = {
children: 'children',
label: 'name',
icon: 'icon',
};
watch(filterText, (val) => {
treeRef.value!.filter(val);
});
const filterNode = (value: string, data: Tree) => {
if (!value) return true;
return toRaw(data).name.indexOf(value) !== -1;
};
let data = ref([]);
const content = `
1.部门信息;
`;
const getData = () => {
api.GetDept({}).then((ret: APIResponseData) => {
const responseData = ret.data;
const result = XEUtils.toArrayTree(responseData, {
parentKey: 'parent',
children: 'children',
strict: true,
});
data.value = result;
});
};
//树形点击事件
const onTreeNodeClick = (node: any) => {
const { id } = node;
crudExpose.doSearch({ form: { dept: id } });
};
// 页面打开后获取列表数据
onMounted(() => {
getData();
});
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
@@ -24,3 +126,17 @@ onMounted(() => {
crudExpose.doRefresh();
});
</script>
<style lang="scss" scoped>
.el-row {
height: 100%;
.el-col {
height: 100%;
}
}
.el-card {
height: 100%;
}
</style>

View File

@@ -1,8 +1,8 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/api_white_list/';
export function GetList(query: PageQuery) {
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',

View File

@@ -1,13 +1,12 @@
import * as api from './api';
import { dict, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
import { successMessage } from '/@/utils/message';
import {inject} from "vue";
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
@@ -20,6 +19,10 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
//权限判定
const hasPermissions = inject("$hasPermissions")
return {
crudOptions: {
request: {
@@ -28,6 +31,34 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 150,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show:hasPermissions("api_white_list:Update")
},
remove: {
iconRight: 'Delete',
type: 'text',
show:hasPermissions("api_white_list:Delete")
},
},
},
form: {
col: { span: 24 },
labelWidth: '110px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
@@ -37,10 +68,11 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
//@ts-ignore
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination: any = crudExpose.crudBinding.value.pagination;
let pagination: any = crudExpose!.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
@@ -70,7 +102,7 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
method: {
title: '请求方式',
sortable: true,
sortable: 'custom',
search: {
disabled: false,
},
@@ -93,8 +125,15 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
label: 'DELETE',
value: 3,
},
{
label: 'PATCH',
value: 4,
},
],
}),
column:{
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
@@ -113,7 +152,7 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
},
url: {
title: '接口地址',
sortable: true,
sortable: 'custom',
search: {
disabled: true,
},
@@ -133,6 +172,9 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
});
},
}),
column:{
minWidth: 200,
},
form: {
rules: [
// 表单校验规则
@@ -155,9 +197,11 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
class: { yxtInput: true },
},
helper: {
render(h) {
return <el-alert title="请正确填写,以免请求时被拦截。匹配单例使用正则,例如:/api/xx/.*?/" type="warning" />;
position: 'label',
tooltip: {
placement: 'top-start',
},
text: '请正确填写,以免请求时被拦截。匹配单例使用正则,例如:/api/xx/.*?/',
},
},
},
@@ -166,17 +210,26 @@ export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExp
search: {
disabled: false,
},
width: 150,
type: 'dict-radio',
column: {
minWidth:120,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: #409eff; --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'),
}),
form: {
value: true,
component: {
span: 12,
},
},
},
},
},

View File

@@ -6,18 +6,10 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
// 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 });
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {

12
web/tailwind.config.js Normal file
View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './src/**/*.{vue,js}'],
theme: {
extend: {
height: {
'screen/2': '50vh',
},
},
},
plugins: [],
};

File diff suppressed because it is too large Load Diff