Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
1638245306
2025-02-09 23:17:35 +08:00
38 changed files with 1762 additions and 661 deletions

1
backend/.gitignore vendored
View File

@@ -98,5 +98,4 @@ media/
__pypackages__/ __pypackages__/
package-lock.json package-lock.json
gunicorn.pid gunicorn.pid
plugins/*
!plugins/__init__.py !plugins/__init__.py

View File

@@ -1,6 +1,8 @@
import functools import functools
import os import os
from celery.signals import task_postrun
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
from django.conf import settings from django.conf import settings
@@ -38,3 +40,12 @@ def retry_base_task_error():
return wrapper return wrapper
return wraps return wraps
@task_postrun.connect
def add_periodic_task_name(sender, task_id, task, args, kwargs, **extras):
periodic_task_name = kwargs.get('periodic_task_name')
if periodic_task_name:
from django_celery_results.models import TaskResult
# 更新 TaskResult 表中的 periodic_task_name 字段
TaskResult.objects.filter(task_id=task_id).update(periodic_task_name=periodic_task_name)

View File

@@ -404,7 +404,7 @@ PLUGINS_URL_PATTERNS = []
# ********** 一键导入插件配置开始 ********** # ********** 一键导入插件配置开始 **********
# 例如: # 例如:
# from dvadmin_upgrade_center.settings import * # 升级中心 # from dvadmin_upgrade_center.settings import * # 升级中心
# from dvadmin3_celery.settings import * # celery 异步任务 from dvadmin3_celery.settings import * # celery 异步任务
# from dvadmin_third.settings import * # 第三方用户管理 # from dvadmin_third.settings import * # 第三方用户管理
# from dvadmin_ak_sk.settings import * # 秘钥管理管理 # from dvadmin_ak_sk.settings import * # 秘钥管理管理
# from dvadmin_tenants.settings import * # 租户管理 # from dvadmin_tenants.settings import * # 租户管理

View File

@@ -49,7 +49,7 @@ urlpatterns = [
path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})), path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})),
# path('login_log/', LoginLogViewSet.as_view({'get': 'list'})), # path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
# path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})), # path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})), # path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
path('clause/privacy.html', PrivacyView.as_view()), path('clause/privacy.html', PrivacyView.as_view()),
path('clause/terms_service.html', TermsServiceView.as_view()), path('clause/terms_service.html', TermsServiceView.as_view()),
] ]

View File

@@ -10,16 +10,17 @@ from rest_framework import serializers
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Role, Menu, MenuButton, Dept from dvadmin.system.models import Role, Menu, MenuButton, Dept, Users
from dvadmin.system.views.dept import DeptSerializer from dvadmin.system.views.dept import DeptSerializer
from dvadmin.system.views.menu import MenuSerializer from dvadmin.system.views.menu import MenuSerializer
from dvadmin.system.views.menu_button import MenuButtonSerializer from dvadmin.system.views.menu_button import MenuButtonSerializer
from dvadmin.utils.crud_mixin import FastCrudMixin from dvadmin.utils.crud_mixin import FastCrudMixin
from dvadmin.utils.field_permission import FieldPermissionMixin from dvadmin.utils.field_permission import FieldPermissionMixin
from dvadmin.utils.json_response import SuccessResponse, DetailResponse from dvadmin.utils.json_response import SuccessResponse, DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator from dvadmin.utils.validator import CustomUniqueValidator
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.permission import CustomPermission
class RoleSerializer(CustomModelSerializer): class RoleSerializer(CustomModelSerializer):
@@ -107,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer):
fields = '__all__' fields = '__all__'
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin): class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
""" """
角色管理接口 角色管理接口
@@ -142,3 +142,62 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
role.users_set.add(*movedKeys) role.users_set.add(*movedKeys)
serializer = RoleSerializer(role) serializer = RoleSerializer(role)
return DetailResponse(data=serializer.data, msg="更新成功") return DetailResponse(data=serializer.data, msg="更新成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated, CustomPermission])
def get_role_users(self, request):
"""
获取角色已授权、未授权的用户
已授权的用户:1
未授权的用户:0
"""
role_id = request.query_params.get('role_id', None)
if not role_id:
return ErrorResponse(msg="请选择角色")
if request.query_params.get('authorized', 0) == "1":
queryset = Users.objects.filter(role__id=role_id).exclude(is_superuser=True)
else:
queryset = Users.objects.exclude(role__id=role_id).exclude(is_superuser=True)
if name := request.query_params.get('name', None):
queryset = queryset.filter(name__icontains=name)
if dept := request.query_params.get('dept', None):
queryset = queryset.filter(dept=dept)
page = self.paginate_queryset(queryset.values('id', 'name', 'dept__name'))
if page is not None:
return self.get_paginated_response(page)
return SuccessResponse(data=page)
@action(methods=['DELETE'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
def remove_role_user(self, request, pk):
"""
角色-删除用户
"""
user_id = request.data.get('user_id', None)
if not user_id:
return ErrorResponse(msg="请选择用户")
role = self.get_object()
role.users_set.remove(*user_id)
return SuccessResponse(msg="删除成功")
@action(methods=['POST'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
def add_role_users(self, request, pk):
"""
角色-添加用户
"""
users_id = request.data.get('users_id', None)
if not users_id:
return ErrorResponse(msg="请选择用户")
role = self.get_object()
role.users_set.add(*users_id)
return DetailResponse(msg="添加成功")

View File

@@ -231,9 +231,17 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
isCheck = data.get('isCheck', None) isCheck = data.get('isCheck', None)
roleId = data.get('roleId', None) roleId = data.get('roleId', None)
btnId = data.get('btnId', None) btnId = data.get('btnId', None)
data_range = data.get('data_range', None) or 0 # 默认仅本人权限
dept = data.get('dept', None) or [] # 默认空部门
if isCheck: if isCheck:
# 添加权限:创建关联记录 # 添加权限:创建关联记录
RoleMenuButtonPermission.objects.create(role_id=roleId, menu_button_id=btnId) instance = RoleMenuButtonPermission.objects.create(role_id=roleId,
menu_button_id=btnId,
data_range=data_range)
# 自定义部门权限
if data_range == 4 and dept:
instance.dept.set(dept)
else: else:
# 删除权限:移除关联记录 # 删除权限:移除关联记录
RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete() RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete()

View File

@@ -33,7 +33,7 @@ class CoreModelFilterBankend(BaseFilterBackend):
create_datetime_after = request.query_params.get('create_datetime_after', None) create_datetime_after = request.query_params.get('create_datetime_after', None)
create_datetime_before = request.query_params.get('create_datetime_before', None) create_datetime_before = request.query_params.get('create_datetime_before', None)
update_datetime_after = request.query_params.get('update_datetime_after', None) update_datetime_after = request.query_params.get('update_datetime_after', None)
update_datetime_before = request.query_params.get('update_datetime_after', None) update_datetime_before = request.query_params.get('update_datetime_before', None)
if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]): if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]):
create_filter = Q() create_filter = Q()
if create_datetime_after and create_datetime_before: if create_datetime_after and create_datetime_before:

View File

@@ -29,3 +29,4 @@ gunicorn==22.0.0
gevent==24.2.1 gevent==24.2.1
Pillow==10.4.0 Pillow==10.4.0
pyinstaller==6.9.0 pyinstaller==6.9.0
dvadmin3-celery==3.1.6

58
init.sh
View File

@@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
ENV_FILE=".env" ENV_FILE=".env"
HOST="177.10.0.13"
# 检查 .env 文件是否存在 # 检查 .env 文件是否存在
if [ -f "$ENV_FILE" ]; then if [ -f "$ENV_FILE" ]; then
echo "$ENV_FILE 文件已存在。" echo "$ENV_FILE 文件已存在。"
@@ -15,17 +16,60 @@ else
echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。" echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。"
awk 'BEGIN { cmd="cp -i ./backend/conf/env.example.py ./backend/conf/env.py "; print "n" |cmd; }' awk 'BEGIN { cmd="cp -i ./backend/conf/env.example.py ./backend/conf/env.py "; print "n" |cmd; }'
sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.13'|g" ./backend/conf/env.py sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '$HOST'|g" ./backend/conf/env.py
sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.15'|g" ./backend/conf/env.py sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.15'|g" ./backend/conf/env.py
sed -i "s|DATABASE_PASSWORD = 'DVADMIN3'|DATABASE_PASSWORD = '$MYSQL_PASSWORD'|g" ./backend/conf/env.py sed -i "s|DATABASE_PASSWORD = 'DVADMIN3'|DATABASE_PASSWORD = '$MYSQL_PASSWORD'|g" ./backend/conf/env.py
sed -i "s|REDIS_PASSWORD = 'DVADMIN3'|REDIS_PASSWORD = '$REDIS_PASSWORD'|g" ./backend/conf/env.py sed -i "s|REDIS_PASSWORD = 'DVADMIN3'|REDIS_PASSWORD = '$REDIS_PASSWORD'|g" ./backend/conf/env.py
echo "初始化密码创建成功" echo "初始化密码创建成功"
fi fi
echo "正在启动容器..."
docker-compose up -d docker-compose up -d
docker exec dvadmin3-django python manage.py makemigrations
docker exec dvadmin3-django python manage.py migrate if [ $? -ne 0 ]; then
docker exec dvadmin3-django python manage.py init echo "docker-compose up -d 执行失败!"
echo "欢迎使用dvadmin3项目" exit 1
echo "登录地址http://ip:8080" fi
echo "如访问不到,请检查防火墙配置"
MYSQL_PORT=3306
REDIS_PORT=6379
check_mysql() {
if nc -z "$HOST" "$MYSQL_PORT" >/dev/null 2>&1; then
echo "MySQL 服务正在运行在 $HOST:$MYSQL_PORT"
return 0
else
return 1
fi
}
check_redis() {
if nc -z "$HOST" "$REDIS_PORT" >/dev/null 2>&1; then
echo "Redis 服务正在运行在 $HOST:$REDIS_PORT"
return 0
else
return 1
fi
}
i=1
while [ $i -le 8 ]; do
if check_mysql || check_redis; then
echo "正在迁移数据..."
docker exec dvadmin3-django python3 manage.py makemigrations
docker exec dvadmin3-django python3 manage.py migrate
echo "正在初始化数据..."
docker exec dvadmin3-django python3 manage.py init
echo "欢迎使用dvadmin3项目"
echo "登录地址http://ip:8080"
echo "如访问不到,请检查防火墙配置"
exit 0
else
echo "$i 次尝试MySQL 或 REDIS服务未运行等待 2 秒后重试..."
sleep 2
fi
i=$((i+1))
done
echo "尝试 5 次后MySQL 或 REDIS服务仍未运行"
exit 1

View File

@@ -1,6 +1,6 @@
{ {
"name": "django-vue3-admin", "name": "django-vue3-admin",
"version": "3.0.4", "version": "3.1.0",
"description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus", "description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@@ -15,6 +15,7 @@
"@fast-crud/fast-extends": "^1.21.2", "@fast-crud/fast-extends": "^1.21.2",
"@fast-crud/ui-element": "^1.21.2", "@fast-crud/ui-element": "^1.21.2",
"@fast-crud/ui-interface": "^1.21.2", "@fast-crud/ui-interface": "^1.21.2",
"@great-dream/dvadmin3-celery-web": "^3.1.3",
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.7",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "^4.0.1",

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="user-info-head" @click="editCropper()"> <div class="user-info-head" @click="editCropper()">
<el-avatar :size="100" :src="options.img" /> <el-avatar :size="100" :src="getBaseURL(options.img)" />
<el-dialog :title="title" v-model="dialogVisiable" width="600px" append-to-body @opened="modalOpened" @close="closeDialog"> <el-dialog :title="title" v-model="dialogVisiable" width="600px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row> <el-row>
<el-col class="flex justify-center"> <el-col class="flex justify-center">
@@ -50,10 +50,11 @@ import { VueCropper } from 'vue-cropper';
import { useUserInfo } from '/@/stores/userInfo'; import { useUserInfo } from '/@/stores/userInfo';
import { getCurrentInstance, nextTick, reactive, ref, computed, onMounted, defineExpose } from 'vue'; import { getCurrentInstance, nextTick, reactive, ref, computed, onMounted, defineExpose } from 'vue';
import { base64ToFile } from '/@/utils/tools'; import { base64ToFile } from '/@/utils/tools';
import headerImage from "/@/assets/img/headerImage.png";
import {getBaseURL} from "/@/utils/baseUrl";
const userStore = useUserInfo(); const userStore = useUserInfo();
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const open = ref(false);
const visible = ref(false); const visible = ref(false);
const title = ref('修改头像'); const title = ref('修改头像');
const emit = defineEmits(['uploadImg']); const emit = defineEmits(['uploadImg']);
@@ -75,7 +76,7 @@ const dialogVisiable = computed({
//图片裁剪数据 //图片裁剪数据
const options = reactive({ const options = reactive({
img: userStore.userInfos.avatar, // 裁剪图片的地址 img: userStore.userInfos.avatar || headerImage, // 裁剪图片的地址
fileName: '', fileName: '',
autoCrop: true, // 是否默认生成截图框 autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度 autoCropWidth: 200, // 默认生成截图框宽度
@@ -165,6 +166,7 @@ const updateAvatar = (img) => {
defineExpose({ defineExpose({
updateAvatar, updateAvatar,
editCropper
}); });
</script> </script>
@@ -172,7 +174,6 @@ defineExpose({
.user-info-head { .user-info-head {
position: relative; position: relative;
display: inline-block; display: inline-block;
height: 120px;
} }
.user-info-head:hover:after { .user-info-head:hover:after {

View File

@@ -8,7 +8,27 @@
<el-option v-for="item, index in listAllData" :key="index" :value="String(item[props.valueKey])" <el-option v-for="item, index in listAllData" :key="index" :value="String(item[props.valueKey])"
:label="item.name" /> :label="item.name" />
</el-select> </el-select>
<div v-if="props.inputType === 'image'" style="position: relative;" class="form-display"
<div v-if="props.inputType === 'image' && props.multiple"
style="width: 100%; display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 4px;">
<el-image v-for="item, index in (data || [])" :src="item" :key="index" fit="scale-down" class="itemList"
:style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }" />
<div style="position: relative;" :style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
<div
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
<el-icon :size="24">
<Plus />
</el-icon>
</div>
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
<el-icon v-show="(!!data && !props.disabled) && !props.multiple" class="closeHover" :size="16"
@click="clear">
<Close />
</el-icon>
</div>
</div>
<div v-if="props.inputType === 'image' && !props.multiple" class="form-display" style="position: relative;"
@mouseenter="formDisplayEnter" @mouseleave="formDisplayLeave" @mouseenter="formDisplayEnter" @mouseleave="formDisplayLeave"
:style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }"> :style="{ width: props.inputSize + 'px', height: props.inputSize + 'px' }">
<el-image :src="data" fit="scale-down" :style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }"> <el-image :src="data" fit="scale-down" :style="{ width: props.inputSize + 'px', aspectRatio: '1 / 1' }">
@@ -24,10 +44,11 @@
</div> </div>
<div @click="selectVisiable = true && !props.disabled" class="addControllorHover" <div @click="selectVisiable = true && !props.disabled" class="addControllorHover"
:style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div> :style="{ cursor: props.disabled ? 'not-allowed' : 'pointer' }"></div>
<el-icon v-show="!!data && !props.disabled" class="closeHover" :size="16" @click="clear"> <el-icon v-show="(!!data && !props.disabled) && !props.multiple" class="closeHover" :size="16" @click="clear">
<Close /> <Close />
</el-icon> </el-icon>
</div> </div>
<div v-if="props.inputType === 'video'" class="form-display" @mouseenter="formDisplayEnter" <div v-if="props.inputType === 'video'" class="form-display" @mouseenter="formDisplayEnter"
@mouseleave="formDisplayLeave" @mouseleave="formDisplayLeave"
style="position: relative; display: flex; align-items: center; justify-items: center;" style="position: relative; display: flex; align-items: center; justify-items: center;"
@@ -46,6 +67,7 @@
<Close /> <Close />
</el-icon> </el-icon>
</div> </div>
<div v-if="props.inputType === 'audio'" class="form-display" @mouseenter="formDisplayEnter" <div v-if="props.inputType === 'audio'" class="form-display" @mouseenter="formDisplayEnter"
@mouseleave="formDisplayLeave" @mouseleave="formDisplayLeave"
style="position: relative; display: flex; align-items: center; justify-items: center;" style="position: relative; display: flex; align-items: center; justify-items: center;"
@@ -199,7 +221,7 @@ const props = defineProps({
tabsShow: { type: Number, default: SHOW.ALL }, tabsShow: { type: Number, default: SHOW.ALL },
// 是否可以多选,默认单选 // 是否可以多选,默认单选
// 该值为true时inputType必须是selector暂不支持其他type的多选 // 该值为true时inputType必须是selector或image暂不支持其他type的多选
multiple: { type: Boolean, default: false }, multiple: { type: Boolean, default: false },
// 是否可选该参数用于只上传和展示而不选择和绑定model的情况 // 是否可选该参数用于只上传和展示而不选择和绑定model的情况
@@ -274,6 +296,7 @@ const onItemClick = async (e: MouseEvent) => {
while (!target.dataset.id) target = target.parentElement as HTMLElement; while (!target.dataset.id) target = target.parentElement as HTMLElement;
let fileId = target.dataset.id; let fileId = target.dataset.id;
if (props.multiple) { if (props.multiple) {
if (!!!data.value) data.value = [];
if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; } if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; }
else { target.classList.add('active'); flat = 1; } else { target.classList.add('active'); flat = 1; }
if (data.value.length) { if (data.value.length) {
@@ -394,7 +417,8 @@ const onDataChange = (value: any) => {
defineExpose({ data, onDataChange, selectVisiable, clearState, clear }); defineExpose({ data, onDataChange, selectVisiable, clearState, clear });
onMounted(() => { onMounted(() => {
if (props.multiple && props.inputType !== 'selector')
if (props.multiple && !['selector', 'image'].includes(props.inputType))
throw new Error('FileSelector组件属性multiple为true时inputType必须为selector'); throw new Error('FileSelector组件属性multiple为true时inputType必须为selector');
listRequestAll(); listRequestAll();
console.log('fileselector tenentmdoe', isTenentMode); console.log('fileselector tenentmdoe', isTenentMode);
@@ -475,4 +499,9 @@ onMounted(() => {
top: 2px; top: 2px;
cursor: pointer; cursor: pointer;
} }
.itemList {
border: 1px solid #dcdfe6;
border-radius: 8px;
}
</style> </style>

View File

@@ -3,6 +3,7 @@
popper-class="popperClass" popper-class="popperClass"
class="tableSelector" class="tableSelector"
multiple multiple
:collapseTags="props.tableConfig.collapseTags"
@remove-tag="removeTag" @remove-tag="removeTag"
v-model="data" v-model="data"
placeholder="请选择" placeholder="请选择"
@@ -18,20 +19,22 @@
<el-table <el-table
ref="tableRef" ref="tableRef"
:data="tableData" :data="tableData"
size="mini" :size="props.tableConfig.size"
border border
row-key="id" row-key="id"
:lazy="props.tableConfig.lazy" :lazy="props.tableConfig.lazy"
:load="props.tableConfig.load" :load="props.tableConfig.load"
:tree-props="props.tableConfig.treeProps" :tree-props="props.tableConfig.treeProps"
style="width: 400px" style="width: 600px"
max-height="200" max-height="200"
height="200" height="200"
:highlight-current-row="!props.tableConfig.isMultiple" :highlight-current-row="!props.tableConfig.isMultiple"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@select="handleSelectionChange"
@selectAll="handleSelectionChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
> >
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" width="55" /> <el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" reserve-selection width="55" />
<el-table-column fixed type="index" label="#" width="50" /> <el-table-column fixed type="index" label="#" width="50" />
<el-table-column <el-table-column
:prop="item.prop" :prop="item.prop"
@@ -56,24 +59,32 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, reactive, ref, watch } from 'vue'; import {computed, defineProps, onMounted, reactive, ref, watch} from 'vue';
import XEUtils from 'xe-utils'; import XEUtils from 'xe-utils';
import { request } from '/@/utils/service'; import { request } from '/@/utils/service';
const props = defineProps({ const props = defineProps({
modelValue: {}, modelValue: {
type: Array || String || Number,
default: () => []
},
tableConfig: { tableConfig: {
url: null, type: Object,
label: null, //显示值 default:{
value: null, //数据值 url: null,
isTree: false, label: null, //显示值
lazy: true, value: null, //数据值
load: () => {}, isTree: false,
data: [], //默认数据 lazy: true,
isMultiple: false, //是否多选 size:'default',
treeProps: { children: 'children', hasChildren: 'hasChildren' }, load: () => {},
columns: [], //每一项对应的列表项 data: [], //默认数据
}, isMultiple: false, //是否多选
collapseTags:false,
treeProps: { children: 'children', hasChildren: 'hasChildren' },
columns: [], //每一项对应的列表项
},
},
displayLabel: {}, displayLabel: {},
} as any); } as any);
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
@@ -86,7 +97,7 @@ const multipleSelection = ref();
// 搜索值 // 搜索值
const search = ref(undefined); const search = ref(undefined);
//表格数据 //表格数据
const tableData = ref(); const tableData = ref([]);
// 分页的配置 // 分页的配置
const pageConfig = reactive({ const pageConfig = reactive({
page: 1, page: 1,
@@ -99,7 +110,6 @@ const pageConfig = reactive({
* @param val:Array * @param val:Array
*/ */
const handleSelectionChange = (val: any) => { const handleSelectionChange = (val: any) => {
multipleSelection.value = val;
const { tableConfig } = props; const { tableConfig } = props;
const result = val.map((item: any) => { const result = val.map((item: any) => {
return item[tableConfig.value]; return item[tableConfig.value];
@@ -117,7 +127,7 @@ const handleSelectionChange = (val: any) => {
const handleCurrentChange = (val: any) => { const handleCurrentChange = (val: any) => {
const { tableConfig } = props; const { tableConfig } = props;
if (!tableConfig.isMultiple && val) { if (!tableConfig.isMultiple && val) {
data.value = [val[tableConfig.label]]; // data.value = [val[tableConfig.label]];
emit('update:modelValue', val[tableConfig.value]); emit('update:modelValue', val[tableConfig.value]);
} }
}; };
@@ -150,6 +160,32 @@ const getDict = async () => {
} }
}; };
// 获取节点值
const getNodeValues = () => {
request({
url:props.tableConfig.valueUrl,
method:'post',
data:{ids:props.modelValue}
}).then(res=>{
if(res.data.length>0){
data.value = res.data.map((item:any)=>{
return item[props.tableConfig.label]
})
tableRef.value!.clearSelection()
res.data.forEach((row) => {
tableRef.value!.toggleRowSelection(
row,
true,
false
)
})
}
})
}
/** /**
* 下拉框展开/关闭 * 下拉框展开/关闭
* @param bool * @param bool
@@ -169,20 +205,12 @@ const handlePageChange = (page: any) => {
getDict(); getDict();
}; };
// 监听displayLabel的变化更新数据 onMounted(()=>{
watch( setTimeout(()=>{
() => { getNodeValues()
return props.displayLabel; },1000)
}, })
(value) => {
const { tableConfig } = props;
const result = value
? value.map((item: any) => { return item[tableConfig.label];})
: null;
data.value = result;
},
{ immediate: true }
);
</script> </script>
<style scoped> <style scoped>

View File

@@ -17,13 +17,15 @@ import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import mittBus from '/@/utils/mitt'; import mittBus from '/@/utils/mitt';
import { useRoute } from 'vue-router';
const route = useRoute();
// 引入组件 // 引入组件
const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue')); const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue')); const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue'));
// 定义变量内容 // 定义变量内容
const layoutAsideScrollbarRef = ref(); const layoutAsideScrollbarRef = ref();
const routesIndex = ref(0);
const stores = useRoutesList(); const stores = useRoutesList();
const storesThemeConfig = useThemeConfig(); const storesThemeConfig = useThemeConfig();
const storesTagsViewRoutes = useTagsViewRoutes(); const storesTagsViewRoutes = useTagsViewRoutes();
@@ -83,10 +85,36 @@ const closeLayoutAsideMobileMode = () => {
if (clientWidth < 1000) themeConfig.value.isCollapse = false; if (clientWidth < 1000) themeConfig.value.isCollapse = false;
document.body.setAttribute('class', ''); document.body.setAttribute('class', '');
}; };
const findFirstLevelIndex = (data, path) => {
for (let index = 0; index < data.length; index++) {
const item = data[index];
// 检查当前菜单项是否有子菜单,并查找是否在子菜单中找到路径
if (item.children && item.children.length > 0) {
// 检查子菜单中是否有匹配的路径
const childIndex = item.children.findIndex((child) => child.path === path);
if (childIndex !== -1) {
return index; // 返回当前一级菜单的索引
}
// 递归查找子菜单
const foundIndex = findFirstLevelIndex(item.children, path);
if (foundIndex !== null) {
return index; // 返回找到的索引
}
}
}
return null; // 找不到路径时返回 null
};
// 设置/过滤路由(非静态路由/是否显示在菜单中) // 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => { const setFilterRoutes = (path='') => {
if (themeConfig.value.layout === 'columns') return false; if (themeConfig.value.layout === 'columns') return false;
state.menuList = filterRoutesFun(routesList.value); let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
// 获取当前地址的索引,不用从参数选取
routesIndex.value = findFirstLevelIndex(routesList.value,path || route.path) || 0
state.menuList = filterRoutesFun(routesList.value[routesIndex.value].children || [routesList.value[routesIndex.value]]);
} else {
state.menuList = filterRoutesFun(routesList.value);
}
}; };
// 路由过滤递归函数 // 路由过滤递归函数
const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => { const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
@@ -122,7 +150,8 @@ onBeforeMount(() => {
let { layout, isClassicSplitMenu } = themeConfig.value; let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) { if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = []; state.menuList = [];
state.menuList = res.children; // state.menuList = res.children;
setFilterRoutes(res.path);
} }
}); });
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => { mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {

View File

@@ -102,6 +102,5 @@ onUnmounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
background: var(--next-bg-topBar); background: var(--next-bg-topBar);
border-bottom: 1px solid var(--next-border-color-light);
} }
</style> </style>

View File

@@ -37,7 +37,7 @@
<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i> <i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
</div> </div>
<div class="layout-navbars-breadcrumb-user-icon"> <div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false"> <el-popover placement="bottom" trigger="hover" transition="el-zoom-in-top" :width="300" :persistent="false">
<template #reference> <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')"> <el-icon :title="$t('message.user.title4')">
@@ -58,7 +58,7 @@
></i> ></i>
</div> </div>
<div> <div>
<span v-if="!isSocketOpen"> <span v-if="!isSocketOpen" class="online-status-span">
<el-popconfirm <el-popconfirm
width="250" width="250"
ref="onlinePopoverRef" ref="onlinePopoverRef"
@@ -71,7 +71,7 @@
> >
<template #reference> <template #reference>
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}"> <el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" /> <img :src="getBaseURL(userInfos.avatar) || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
</el-badge> </el-badge>
</template> </template>
</el-popconfirm> </el-popconfirm>
@@ -93,7 +93,7 @@
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item> <el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item>
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item> <el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item> <el-dropdown-item command="/versionUpgradeLog">更新日志</el-dropdown-item>
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item> <el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@@ -250,6 +250,7 @@ onMounted(() => {
//消息中心的未读数量 //消息中心的未读数量
import { messageCenterStore } from '/@/stores/messageCenter'; import { messageCenterStore } from '/@/stores/messageCenter';
import {getBaseURL} from "/@/utils/baseUrl";
const messageCenter = messageCenterStore(); const messageCenter = messageCenterStore();
</script> </script>

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="el-menu-horizontal-warp"> <div class="el-menu-horizontal-warp">
<el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef"> <!-- <el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">-->
<el-menu router :default-active="state.defaultActive" :ellipsis="false" background-color="transparent" mode="horizontal"> <el-menu :default-active="defaultActive" background-color="transparent" mode="horizontal">
<template v-for="val in menuLists"> <template v-for="(val,index) in menuLists">
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path"> <el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title> <template #title>
<SvgIcon :name="val.meta.icon" /> <SvgIcon :name="val.meta.icon" />
@@ -11,7 +11,7 @@
<SubItem :chil="val.children" /> <SubItem :chil="val.children" />
</el-sub-menu> </el-sub-menu>
<template v-else> <template v-else>
<el-menu-item :index="val.path" :key="val.path"> <el-menu-item :index="val.path" :key="val.path" style="--el-menu-active-color: #fff" @click="onToRouteClick(val,index)">
<template #title v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)"> <template #title v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" /> <SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }} {{ $t(val.meta.title) }}
@@ -26,22 +26,25 @@
</template> </template>
</template> </template>
</el-menu> </el-menu>
</el-scrollbar> <!-- </el-scrollbar>-->
</div> </div>
</template> </template>
<script setup lang="ts" name="navMenuHorizontal"> <script setup lang="ts" name="navMenuHorizontal">
import { defineAsyncComponent, reactive, computed, onMounted, nextTick, onBeforeMount, ref } from 'vue'; import { defineAsyncComponent, reactive, computed, onMounted, nextTick, onBeforeMount, ref } from 'vue';
import { useRoute, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router'; import {useRoute, onBeforeRouteUpdate, RouteRecordRaw, useRouter} from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useRoutesList } from '/@/stores/routesList'; import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other'; import other from '/@/utils/other';
import mittBus from '/@/utils/mitt'; import mittBus from '/@/utils/mitt';
const router = useRouter()
// 引入组件 // 引入组件
const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue')); const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
const state = reactive<AsideState>({
menuList: [],
clientWidth: 0
});
// 定义父组件传过来的值 // 定义父组件传过来的值
const props = defineProps({ const props = defineProps({
// 菜单列表 // 菜单列表
@@ -58,19 +61,33 @@ const storesThemeConfig = useThemeConfig();
const { routesList } = storeToRefs(stores); const { routesList } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig); const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute(); const route = useRoute();
const state = reactive({ const defaultActive = ref('')
defaultActive: '' as string | undefined,
});
// 获取父级菜单数据 // 获取父级菜单数据
const menuLists = computed(() => { const menuLists = computed(() => {
<RouteItems>props.menuList.shift()
return <RouteItems>props.menuList; return <RouteItems>props.menuList;
}); });
// 设置横向滚动条可以鼠标滚轮滚动 // 递归获取当前路由的顶级索引
const onElMenuHorizontalScroll = (e: WheelEventType) => { const findFirstLevelIndex = (data, path) => {
const eventDelta = e.wheelDelta || -e.deltaY * 40; for (let index = 0; index < data.length; index++) {
elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft + eventDelta / 4; const item = data[index];
// 检查当前菜单项是否有子菜单,并查找是否在子菜单中找到路径
if (item.children && item.children.length > 0) {
// 检查子菜单中是否有匹配的路径
const childIndex = item.children.findIndex((child) => child.path === path);
if (childIndex !== -1) {
return index; // 返回当前一级菜单的索引
}
// 递归查找子菜单
const foundIndex = findFirstLevelIndex(item.children, path);
if (foundIndex !== null) {
return index; // 返回找到的索引
}
}
}
return null; // 找不到路径时返回 null
}; };
// 初始化数据,页面刷新时,滚动条滚动到对应位置 // 初始化数据,页面刷新时,滚动条滚动到对应位置
const initElMenuOffsetLeft = () => { const initElMenuOffsetLeft = () => {
nextTick(() => { nextTick(() => {
@@ -107,17 +124,41 @@ const setSendClassicChildren = (path: string) => {
const setCurrentRouterHighlight = (currentRoute: RouteToFrom) => { const setCurrentRouterHighlight = (currentRoute: RouteToFrom) => {
const { path, meta } = currentRoute; const { path, meta } = currentRoute;
if (themeConfig.value.layout === 'classic') { if (themeConfig.value.layout === 'classic') {
state.defaultActive = `/${path?.split('/')[1]}`; let firstLevelIndex = (findFirstLevelIndex(routesList.value, route.path) || 0) - 1
defaultActive.value = firstLevelIndex < 0 ? defaultActive.value : menuLists.value[firstLevelIndex].path
} else { } else {
const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/'); const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/');
if (pathSplit.length >= 4 && meta?.isHide) state.defaultActive = pathSplit.splice(0, 3).join('/'); if (pathSplit.length >= 4 && meta?.isHide) defaultActive.value = pathSplit.splice(0, 3).join('/');
else state.defaultActive = path; else defaultActive.value = path;
} }
}; };
// 打开外部链接 // 打开外部链接
const onALinkClick = (val: RouteItem) => { const onALinkClick = (val: RouteItem) => {
other.handleOpenLink(val); other.handleOpenLink(val);
}; };
// 跳转页面
const onToRouteClick = (val: RouteItem,index) => {
// 跳转到子级页面
let children = val.children
if (children === undefined){
defaultActive.value = val.path
children = setSendClassicChildren(val.path).children
}
if (children.length >= 1){
if (children[0].is_catalog) {
onToRouteClick(children[0],index)
return
}
router.push(children[0].path)
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
mittBus.emit('setSendClassicChildren', children[0]);
}
} else {
router.push('/home')
}
};
// 页面加载前 // 页面加载前
onBeforeMount(() => { onBeforeMount(() => {
setCurrentRouterHighlight(route); setCurrentRouterHighlight(route);
@@ -126,16 +167,6 @@ onBeforeMount(() => {
onMounted(() => { onMounted(() => {
initElMenuOffsetLeft(); initElMenuOffsetLeft();
}); });
// 路由更新时
onBeforeRouteUpdate((to) => {
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
setCurrentRouterHighlight(to);
// 修复经典布局开启切割菜单时点击tagsView后左侧导航菜单数据不变的问题
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
mittBus.emit('setSendClassicChildren', setSendClassicChildren(to.path));
}
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -8,7 +8,21 @@
<sub-item :chil="val.children" /> <sub-item :chil="val.children" />
</el-sub-menu> </el-sub-menu>
<template v-else> <template v-else>
<el-menu-item :index="val.path" :key="val.path"> <a v-if="val.name==='templateCenter'" href="#/templateCenter" target="_blank">
<el-menu-item :key="val.path">
<template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.meta.title) }}</span>
</template>
<template v-else>
<a class="w100" @click.prevent="onALinkClick(val)">
<SvgIcon :name="val.meta.icon" />
{{ $t(val.meta.title) }}
</a>
</template>
</el-menu-item>
</a>
<el-menu-item v-else :index="val.path" :key="val.path">
<template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)"> <template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<SvgIcon :name="val.meta.icon" /> <SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.meta.title) }}</span> <span>{{ $t(val.meta.title) }}</span>

View File

@@ -23,6 +23,7 @@
<script setup lang="ts" name="layoutIframeView"> <script setup lang="ts" name="layoutIframeView">
import { computed, watch, ref, nextTick } from 'vue'; import { computed, watch, ref, nextTick } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import {cookie} from "xe-utils";
// 定义父组件传过来的值 // 定义父组件传过来的值
const props = defineProps({ const props = defineProps({
@@ -49,7 +50,15 @@ const route = useRoute();
// 处理 list 列表,当打开时,才进行加载 // 处理 list 列表,当打开时,才进行加载
const setIframeList = computed(() => { const setIframeList = computed(() => {
return (<RouteItems>props.list).filter((v: RouteItem) => v.meta?.isIframeOpen); return (<RouteItems>props.list).filter((v: RouteItem) => {
if (v.meta?.isIframeOpen) {
const isLink = v.meta?.isLink || '';
if (isLink.includes("{{token}}")) {
v.meta.isLink = isLink.replace("{{token}}", cookie.get('token'))
}
}
return v.meta?.isIframeOpen
});
}); });
// 获取 iframe 当前路由 path // 获取 iframe 当前路由 path
const getRoutePath = computed(() => { const getRoutePath = computed(() => {

View File

@@ -17,19 +17,31 @@
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { verifyUrl } from '/@/utils/toolsValidate'; import { verifyUrl } from '/@/utils/toolsValidate';
import {cookie} from "xe-utils";
// 定义变量内容 // 定义变量内容
const route = useRoute(); const route = useRoute();
const state = reactive<LinkViewState>({ const state = reactive<LinkViewState>({
title: '', title: '',
isLink: '', isLink: '',
query: null
}); });
// 立即前往 // 立即前往
const onGotoFullPage = () => { const onGotoFullPage = () => {
const { origin, pathname } = window.location; const { origin, pathname } = window.location;
if (state.isLink.includes("{{token}}")) {
state.isLink = state.isLink.replace("{{token}}", cookie.get('token'))
}
if (verifyUrl(<string>state.isLink)) window.open(state.isLink); if (verifyUrl(<string>state.isLink)) window.open(state.isLink);
else window.open(`${origin}${pathname}#${state.isLink}`); else {
function objectToUrlParams(obj: { [key: string]: string | number }): string {
return Object.keys(obj)
.map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]))
.join('&');
}
window.open(`${origin}${pathname}#${state.isLink}?${objectToUrlParams(state.query)}`)
};
}; };
// 监听路由的变化,设置内容 // 监听路由的变化,设置内容
watch( watch(
@@ -37,6 +49,7 @@ watch(
() => { () => {
state.title = <string>route.meta.title; state.title = <string>route.meta.title;
state.isLink = <string>route.meta.isLink; state.isLink = <string>route.meta.isLink;
state.query = <any>route.query;
}, },
{ {
immediate: true, immediate: true,

View File

@@ -21,13 +21,14 @@ const menuApi = useMenuApi();
const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}'); const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}'); const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
const greatDream: any = import.meta.glob('@great-dream/**/*.{vue,tsx}');
/** /**
* 获取目录下的 .vue、.tsx 全部文件 * 获取目录下的 .vue、.tsx 全部文件
* @method import.meta.glob * @method import.meta.glob
* @link 参考https://cn.vitejs.dev/guide/features.html#json * @link 参考https://cn.vitejs.dev/guide/features.html#json
*/ */
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules }); const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules }, { ...greatDream });
/** /**
* 后端控制路由:初始化方法,防止刷新时路由丢失 * 后端控制路由:初始化方法,防止刷新时路由丢失
@@ -198,7 +199,10 @@ export function dynamicImport(dynamicViewsModules: Record<string, Function>, com
const keys = Object.keys(dynamicViewsModules); const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => { const matchKeys = keys.filter((key) => {
const k = key.replace(/..\/views|../, ''); const k = key.replace(/..\/views|../, '');
return k.startsWith(`${component}`) || k.startsWith(`/${component}`); const k0 = k.replace("ode_modules/@great-dream/", '')
const k1 = k0.replace("/plugins", '')
const newComponent = component.replace("plugins/", "")
return k1.startsWith(`${newComponent}`) || k1.startsWith(`/${newComponent}`);
}); });
if (matchKeys?.length === 1) { if (matchKeys?.length === 1) {
const matchKey = matchKeys[0]; const matchKey = matchKeys[0];

View File

@@ -56,4 +56,5 @@ declare type ParentViewState<T = any> = {
declare type LinkViewState = { declare type LinkViewState = {
title: string; title: string;
isLink: string; isLink: string;
query: any;
}; };

View File

@@ -1,259 +1,286 @@
import {dict} from "@fast-crud/fast-crud"; import {dict} from '@fast-crud/fast-crud';
import {shallowRef} from 'vue' import {shallowRef} from 'vue';
import deptFormat from "/@/components/dept-format/index.vue"; import deptFormat from '/@/components/dept-format/index.vue';
export const commonCrudConfig = (options = { /** 1. 每个字段可选属性 */
create_datetime: { export interface CrudFieldOption {
form: false, form?: boolean;
table: false, table?: boolean;
search: false search?: boolean;
}, width?: number;
update_datetime: {
form: false,
table: false,
search: false
},
creator_name: {
form: false,
table: false,
search: false
},
modifier_name: {
form: false,
table: false,
search: false
},
dept_belong_id: {
form: false,
table: false,
search: false
},
description: {
form: false,
table: false,
search: false
},
}) => {
return {
dept_belong_id: {
title: '所属部门',
type: 'dict-tree',
search: {
show: options.dept_belong_id?.search || false
},
dict: dict({
url: '/api/system/dept/all_dept/',
isTree: true,
value: 'id',
label: 'name',
children: 'children',
}),
column: {
align: 'center',
width: 300,
show: options.dept_belong_id?.table || false,
component: {
name: shallowRef(deptFormat),
vModel: "modelValue",
}
},
form: {
show: options.dept_belong_id?.form || false,
component: {
multiple: false,
clearable: true,
props: {
checkStrictly: true,
props: {
// 为什么这里要写两层props
// 因为props属性名与fs的动态渲染的props命名冲突所以要多写一层
label: "name",
value: "id",
}
}
},
helper: "默认不填则为当前创建用户的部门ID"
}
},
description: {
title: '备注',
search: {
show: options.description?.search || false
},
type: 'textarea',
column: {
width: 100,
show: options.description?.table || false,
},
form: {
show: options.description?.form || false,
component: {
placeholder: '请输入内容',
showWordLimit: true,
maxlength: '200',
}
},
viewForm: {
show: true
}
},
modifier_name: {
title: '修改人',
search: {
show: options.modifier_name?.search || false
},
column: {
width: 100,
show: options.modifier_name?.table || false,
},
form: {
show: false,
},
viewForm: {
show: true
}
},
creator_name: {
title: '创建人',
search: {
show: options.creator_name?.search || false
},
column: {
width: 100,
show: options.creator_name?.table || false,
},
form: {
show: false,
},
viewForm: {
show: true
}
},
update_datetime: {
title: '更新时间',
type: 'datetime',
search: {
show: options.update_datetime?.search || false,
col: {span: 8},
component: {
type: 'datetimerange',
props: {
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [{
text: '最近一周',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近一个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近三个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
}
}]
}
}
},
valueResolve(context: any) {
const {key, value} = context
//value解析就是把组件的值转化为后台所需要的值
//在form表单点击保存按钮后提交到后台之前执行转化
if (value) {
context.form.update_datetime_after = value[0]
context.form.update_datetime_before = value[1]
}
// ↑↑↑↑↑ 注意这里是form不是row
}
},
column: {
width: 160,
show: options.update_datetime?.table || false,
},
form: {
show: false,
},
viewForm: {
show: true
}
},
create_datetime: {
title: '创建时间',
type: 'datetime',
search: {
show: options.create_datetime?.search || false,
col: {span: 8},
component: {
type: 'datetimerange',
props: {
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [{
text: '最近一周',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近一个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近三个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
}
}]
}
}
},
valueResolve(context: any) {
const {key, value} = context
//value解析就是把组件的值转化为后台所需要的值
//在form表单点击保存按钮后提交到后台之前执行转化
if (value) {
context.form.create_datetime_after = value[0]
context.form.create_datetime_before = value[1]
}
// ↑↑↑↑↑ 注意这里是form不是row
}
},
column: {
width: 160,
show: options.create_datetime?.table || false,
},
form: {
show: false
},
viewForm: {
show: true
}
}
}
} }
/** 2. 总配置接口 */
export interface CrudOptions {
create_datetime?: CrudFieldOption;
update_datetime?: CrudFieldOption;
creator_name?: CrudFieldOption;
modifier_name?: CrudFieldOption;
dept_belong_id?: CrudFieldOption;
description?: CrudFieldOption;
}
/** 3. 默认完整配置 */
const defaultOptions: Required<CrudOptions> = {
create_datetime: { form: false, table: false, search: false, width: 160 },
update_datetime: { form: false, table: false, search: false, width: 160 },
creator_name: { form: false, table: false, search: false, width: 100 },
modifier_name: { form: false, table: false, search: false, width: 100 },
dept_belong_id: { form: false, table: false, search: false, width: 300 },
description: { form: false, table: false, search: false, width: 100 },
};
/** 4. mergeOptions 函数 */
function mergeOptions(baseOptions: Required<CrudOptions>, userOptions: CrudOptions = {}): Required<CrudOptions> {
const result = { ...baseOptions };
for (const key in userOptions) {
if (Object.prototype.hasOwnProperty.call(userOptions, key)) {
const baseField = result[key as keyof CrudOptions];
const userField = userOptions[key as keyof CrudOptions];
if (baseField && userField) {
result[key as keyof CrudOptions] = { ...baseField, ...userField };
}
}
}
return result;
}
/**
* 最终暴露的 commonCrudConfig
* @param options 用户自定义配置(可传可不传,不传就用默认)
*/
export const commonCrudConfig = (options: CrudOptions = {}) => {
// ① 合并
const merged = mergeOptions(defaultOptions, options);
// ② 用 merged 中的值生成真正的 CRUD 配置
return {
dept_belong_id: {
title: '所属部门',
type: 'dict-tree',
search: {
show: merged.dept_belong_id.search,
},
dict: dict({
url: '/api/system/dept/all_dept/',
isTree: true,
value: 'id',
label: 'name',
children: 'children',
}),
column: {
align: 'center',
width: merged.dept_belong_id.width,
show: merged.dept_belong_id.table,
component: {
// fast-crud里自定义组件常用"component.is"
is: shallowRef(deptFormat),
vModel: 'modelValue',
},
},
form: {
show: merged.dept_belong_id.form,
component: {
multiple: false,
clearable: true,
props: {
checkStrictly: true,
props: {
label: 'name',
value: 'id',
},
},
},
helper: '默认不填则为当前创建用户的部门ID',
},
},
description: {
title: '备注',
search: {
show: merged.description.search,
},
type: 'textarea',
column: {
width: merged.description.width,
show: merged.description.table,
},
form: {
show: merged.description.form,
component: {
placeholder: '请输入内容',
showWordLimit: true,
maxlength: '200',
},
},
viewForm: {
show: true,
},
},
modifier_name: {
title: '修改人',
search: {
show: merged.modifier_name.search,
},
column: {
width: merged.modifier_name.width,
show: merged.modifier_name.table,
},
form: {
show: merged.modifier_name.form,
},
viewForm: {
show: true,
},
},
creator_name: {
title: '创建人',
search: {
show: merged.creator_name.search,
},
column: {
width: merged.creator_name.width,
show: merged.creator_name.table,
},
form: {
show: merged.creator_name.form,
},
viewForm: {
show: true,
},
},
update_datetime: {
title: '更新时间',
type: 'datetime',
search: {
show: merged.update_datetime.search,
col: { span: 8 },
component: {
type: 'datetimerange',
props: {
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [
{
text: '最近一周',
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
},
},
{
text: '最近一个月',
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
},
},
{
text: '最近三个月',
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
},
},
],
},
},
},
valueResolve(context: any) {
const { value } = context;
if (value) {
context.form.update_datetime_after = value[0];
context.form.update_datetime_before = value[1];
delete context.form.update_datetime;
}
},
},
column: {
width: merged.update_datetime.width,
show: merged.update_datetime.table,
},
form: {
show: merged.update_datetime.form,
},
viewForm: {
show: true,
},
},
create_datetime: {
title: '创建时间',
type: 'datetime',
search: {
show: merged.create_datetime.search,
col: { span: 8 },
component: {
type: 'datetimerange',
props: {
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
'value-format': 'YYYY-MM-DD HH:mm:ss',
'picker-options': {
shortcuts: [
{
text: '最近一周',
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
},
},
{
text: '最近一个月',
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
},
},
{
text: '最近三个月',
onClick(picker: any) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
},
},
],
},
},
},
valueResolve(context: any) {
const { value } = context;
if (value) {
context.form.create_datetime_after = value[0];
context.form.create_datetime_before = value[1];
delete context.form.create_datetime;
}
},
},
column: {
width: merged.create_datetime.width,
show: merged.create_datetime.table,
},
form: {
show: merged.create_datetime.form,
},
viewForm: {
show: true,
},
},
};
};

View File

@@ -1,55 +1,58 @@
import axios from "axios"; import axios from 'axios';
import * as process from "process"; import * as process from 'process';
import {Local, Session} from '/@/utils/storage'; import { Local, Session } from '/@/utils/storage';
import {ElNotification} from "element-plus"; import { ElNotification } from 'element-plus';
import fs from "fs"; import fs from 'fs';
// 是否显示升级提示信息框 // 是否显示升级提示信息框
const IS_SHOW_UPGRADE_SESSION_KEY = 'isShowUpgrade'; const IS_SHOW_UPGRADE_SESSION_KEY = 'isShowUpgrade';
const VERSION_KEY = 'DVADMIN3_VERSION' const VERSION_KEY = 'DVADMIN3_VERSION';
const VERSION_FILE_NAME = 'version-build' const VERSION_FILE_NAME = 'version-build';
export function showUpgrade () { const META_ENV = import.meta.env;
const isShowUpgrade = Session.get(IS_SHOW_UPGRADE_SESSION_KEY) ?? false
if (isShowUpgrade) { export function showUpgrade() {
Session.remove(IS_SHOW_UPGRADE_SESSION_KEY) const isShowUpgrade = Session.get(IS_SHOW_UPGRADE_SESSION_KEY) ?? false;
ElNotification({ if (isShowUpgrade) {
title: '新版本升级', Session.remove(IS_SHOW_UPGRADE_SESSION_KEY);
message: "检测到系统新版本,正在更新中!不用担心,更新很快的哦!", ElNotification({
type: 'success', title: '新版本升级',
duration: 5000, message: '检测到系统新版本,正在更新中!不用担心,更新很快的哦!',
}); type: 'success',
} duration: 5000,
});
}
} }
// 生产环境前端版本校验, // 生产环境前端版本校验,
export async function checkVersion(){ export async function checkVersion() {
if (process.env.NODE_ENV === 'development') { if (META_ENV.MODE === 'development') {
// 开发环境无需校验前端版本 // 开发环境无需校验前端版本
return return;
} }
// 获取线上版本号 t为时间戳防止缓存 // 获取线上版本号 t为时间戳防止缓存
await axios.get(`${import.meta.env.VITE_PUBLIC_PATH}${VERSION_FILE_NAME}?t=${new Date().getTime()}`).then(res => { await axios.get(`${META_ENV.VITE_PUBLIC_PATH}${VERSION_FILE_NAME}?t=${new Date().getTime()}`).then((res) => {
const {status, data} = res || {} const { status, data } = res || {};
if (status === 200) { if (status === 200) {
// 获取当前版本号 // 获取当前版本号
const localVersion = Local.get(VERSION_KEY) const localVersion = Local.get(VERSION_KEY);
// 将当前版本号持久缓存至本地 // 将当前版本号持久缓存至本地
Local.set(VERSION_KEY, data) Local.set(VERSION_KEY, data);
// 当用户本地存在版本号并且和线上版本号不一致时,进行页面刷新操作 // 当用户本地存在版本号并且和线上版本号不一致时,进行页面刷新操作
if (localVersion && localVersion !== data) { if (localVersion && localVersion !== data) {
// 本地缓存版本号和线上版本号不一致,弹出升级提示框 // 本地缓存版本号和线上版本号不一致,弹出升级提示框
// 此处无法直接使用消息框进行提醒,因为 window.location.reload()会导致消息框消失,将在loading页面判断是否需要显示升级提示框 // 此处无法直接使用消息框进行提醒,因为 window.location.reload()会导致消息框消失,将在loading页面判断是否需要显示升级提示框
Session.set(IS_SHOW_UPGRADE_SESSION_KEY, true) Session.set(IS_SHOW_UPGRADE_SESSION_KEY, true);
window.location.reload() window.location.reload();
}
} }
} });
})
} }
export function generateVersionFile (){ export function generateVersionFile() {
// 生成版本文件到public目录下version文件中 // 生成版本文件到public目录下version文件中
const version = `${process.env.npm_package_version}.${new Date().getTime()}`; const package_version = META_ENV?.npm_package_version ?? process.env?.npm_package_version;
fs.writeFileSync(`public/${VERSION_FILE_NAME}`, version);
const version = `${package_version}.${new Date().getTime()}`;
fs.writeFileSync(`public/${VERSION_FILE_NAME}`, version);
} }

View File

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

View File

@@ -1,253 +1,254 @@
import * as api from './api'; import * as api from './api';
import { import {
UserPageQuery, UserPageQuery,
AddReq, AddReq,
DelReq, DelReq,
EditReq, EditReq,
CrudExpose, CrudExpose,
CrudOptions, CrudOptions,
CreateCrudOptionsProps, CreateCrudOptionsProps,
CreateCrudOptionsRet, CreateCrudOptionsRet,
dict dict
} from '@fast-crud/fast-crud'; } from '@fast-crud/fast-crud';
import fileSelector from '/@/components/fileSelector/index.vue'; import fileSelector from '/@/components/fileSelector/index.vue';
import { shallowRef } from 'vue';
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => { const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query); return await api.GetList(query);
}; };
const editRequest = async ({ form, row }: EditReq) => { const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id; form.id = row.id;
return await api.UpdateObj(form); return await api.UpdateObj(form);
}; };
const delRequest = async ({ row }: DelReq) => { const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id); return await api.DelObj(row.id);
}; };
const addRequest = async ({ form }: AddReq) => { const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form); return await api.AddObj(form);
}; };
return { return {
crudOptions: { crudOptions: {
actionbar: { actionbar: {
buttons: { buttons: {
add: { add: {
show: true, show: true,
click: () => context.openAddHandle?.() click: () => context.openAddHandle?.()
}, },
}, },
}, },
request: { request: {
pageRequest, pageRequest,
addRequest, addRequest,
editRequest, editRequest,
delRequest, delRequest,
}, },
tabs: { tabs: {
show: true, show: true,
name: 'file_type', name: 'file_type',
type: '', type: '',
options: [ options: [
{ value: 0, label: '图片' }, { value: 0, label: '图片' },
{ value: 1, label: '视频' }, { value: 1, label: '视频' },
{ value: 2, label: '音频' }, { value: 2, label: '音频' },
{ value: 3, label: '其他' }, { value: 3, label: '其他' },
] ]
}, },
rowHandle: { rowHandle: {
//固定右侧 //固定右侧
fixed: 'right', fixed: 'right',
width: 200, width: 200,
show: false, show: false,
buttons: { buttons: {
view: { view: {
show: false, show: false,
}, },
edit: { edit: {
iconRight: 'Edit', iconRight: 'Edit',
type: 'text', type: 'text',
}, },
remove: { remove: {
iconRight: 'Delete', iconRight: 'Delete',
type: 'text', type: 'text',
}, },
}, },
}, },
columns: { columns: {
_index: { _index: {
title: '序号', title: '序号',
form: { show: false }, form: { show: false },
column: { column: {
//type: 'index', //type: 'index',
align: 'center', align: 'center',
width: '70px', width: '70px',
columnSetDisabled: true, //禁止在列设置中选择 columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => { formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加 //计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1; let index = context.index ?? 1;
let pagination = crudExpose!.crudBinding.value.pagination; let pagination = crudExpose!.crudBinding.value.pagination;
return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1; return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1;
}, },
}, },
}, },
search: { search: {
title: '关键词', title: '关键词',
column: { column: {
show: false, show: false,
}, },
search: { search: {
show: true, show: true,
component: { component: {
props: { props: {
clearable: true, clearable: true,
}, },
placeholder: '请输入关键词', placeholder: '请输入关键词',
}, },
}, },
form: { form: {
show: false, show: false,
component: { component: {
props: { props: {
clearable: true, clearable: true,
}, },
}, },
}, },
}, },
name: { name: {
title: '文件名称', title: '文件名称',
search: { search: {
show: true, show: true,
}, },
type: 'input', type: 'input',
column: { column: {
minWidth: 200, minWidth: 200,
}, },
form: { form: {
component: { component: {
placeholder: '请输入文件名称', placeholder: '请输入文件名称',
clearable: true clearable: true
}, },
}, },
}, },
preview: { preview: {
title: '预览', title: '预览',
column: { column: {
minWidth: 120, minWidth: 120,
align: 'center' align: 'center'
}, },
form: { form: {
show: false show: false
} }
}, },
url: { url: {
title: '文件地址', title: '文件地址',
type: 'file-uploader', type: 'file-uploader',
search: { search: {
disabled: true, disabled: true,
}, },
column: { column: {
minWidth: 360, minWidth: 360,
}, },
}, },
md5sum: { md5sum: {
title: '文件MD5', title: '文件MD5',
search: { search: {
disabled: true, disabled: true,
}, },
column: { column: {
minWidth: 300, minWidth: 300,
}, },
form: { form: {
disabled: false disabled: false
}, },
}, },
mime_type: { mime_type: {
title: '文件类型', title: '文件类型',
type: 'input', type: 'input',
form: { form: {
show: false, show: false,
}, },
column: { column: {
minWidth: 160 minWidth: 160
} }
}, },
file_type: { file_type: {
title: '文件类型', title: '文件类型',
type: 'dict-select', type: 'dict-select',
dict: dict({ dict: dict({
data: [ data: [
{ label: '图片', value: 0, color: 'success' }, { label: '图片', value: 0, color: 'success' },
{ label: '视频', value: 1, color: 'warning' }, { label: '视频', value: 1, color: 'warning' },
{ label: '音频', value: 2, color: 'danger' }, { label: '音频', value: 2, color: 'danger' },
{ label: '其他', value: 3, color: 'primary' }, { label: '其他', value: 3, color: 'primary' },
] ]
}), }),
column: { column: {
show: false show: false
}, },
search: { search: {
show: true show: true
}, },
form: { form: {
show: false, show: false,
component: { component: {
placeholder: '请选择文件类型' placeholder: '请选择文件类型'
} }
} }
}, },
size: { size: {
title: '文件大小', title: '文件大小',
column: { column: {
minWidth: 120 minWidth: 120
}, },
form: { form: {
show: false show: false
} }
}, },
upload_method: { upload_method: {
title: '上传方式', title: '上传方式',
type: 'dict-select', type: 'dict-select',
dict: dict({ dict: dict({
data: [ data: [
{ label: '默认上传', value: 0, color: 'primary' }, { label: '默认上传', value: 0, color: 'primary' },
{ label: '文件选择器上传', value: 1, color: 'warning' }, { label: '文件选择器上传', value: 1, color: 'warning' },
] ]
}), }),
column: { column: {
minWidth: 140 minWidth: 140
}, },
search: { search: {
show: true show: true
} }
}, },
create_datetime: { create_datetime: {
title: '创建时间', title: '创建时间',
column: { column: {
minWidth: 160 minWidth: 160
}, },
form: { form: {
show: false show: false
} }
}, },
// fileselectortest: { fileselectortest: {
// title: '文件选择器测试', title: '文件选择器测试',
// type: 'file-selector', type: 'file-selector',
// width: 200, column: {
// form: { minWidth: 200
// component: { },
// name: shallowRef(fileSelector), form: {
// vModel: 'modelValue', component: {
// tabsShow: 0b0100, name: fileSelector,
// itemSize: 100, vModel: 'modelValue',
// multiple: false, tabsShow: 0b1111,
// selectable: true, itemSize: 100,
// showInput: true, multiple: true,
// inputType: 'video', selectable: true,
// valueKey: 'url', showInput: true,
// } inputType: 'image',
// } valueKey: 'url',
// } }
}, }
}, }
}; },
},
};
}; };

View File

@@ -85,6 +85,7 @@
<el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" label="外链接" prop="link_url"> <el-form-item v-if="!menuFormData.is_catalog && menuFormData.is_link" label="外链接" prop="link_url">
<el-input v-model="menuFormData.link_url" placeholder="请输入外链接地址" /> <el-input v-model="menuFormData.link_url" placeholder="请输入外链接地址" />
<el-alert :title="`输入{{token}}可自动替换系统 token `" type="info" />
</el-form-item> </el-form-item>
<el-form-item v-if="!menuFormData.is_catalog" label="缓存"> <el-form-item v-if="!menuFormData.is_catalog" label="缓存">

View File

@@ -1,9 +1,39 @@
<template> <template>
<div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0"> <div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0">
<div class="menu-form-alert">配置操作功能接口权限配置数据权限点击小齿轮</div> <div class="menu-form-alert">
<div style="display:flex; align-items: center; white-space: nowrap; margin-bottom: 10px;">
<span>默认接口权限:</span>
<el-select
v-model="default_selectBtn.data_range"
@change="defaulthandlePermissionRangeChange"
placeholder="请选择"
style="margin-left: 5px; width: 250px; min-width: 250px;"
>
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-tree-select
v-show="default_selectBtn.data_range === 4"
node-key="id"
v-model="default_selectBtn.dept"
:props="defaultTreeProps"
:data="deptData"
@change="customhandlePermissionRangeChange(default_selectBtn.dept)"
placeholder="请选择自定义部门"
multiple
check-strictly
:render-after-expand="false"
show-checkbox
class="dialog-tree"
style="margin-left: 15px; width: AUTO; min-width: 250px; margin-top: 0;"
/>
</div>
<span>配置操作功能接口权限配置数据权限点击小齿轮</span>
</div>
<el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck" @change="handleCheckChange(btn)"> <el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck" @change="handleCheckChange(btn)">
<div class="btn-item"> <div class="btn-item">
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range)})` : btn.name }} {{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range, btn.dept)})` : btn.name }}
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)"> <span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)">
<el-icon> <el-icon>
<Setting /> <Setting />
@@ -48,10 +78,26 @@ import { RoleMenuBtnType } from '../types';
import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api'; import { getRoleToDeptAll, setRoleMenuBtn, setRoleMenuBtnDataRange } from './api';
import XEUtils from 'xe-utils'; import XEUtils from 'xe-utils';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { Local } from '/@/utils/storage';
const RoleDrawer = RoleDrawerStores(); // 角色-菜单 const RoleDrawer = RoleDrawerStores(); // 角色-菜单
const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单 const RoleMenuTree = RoleMenuTreeStores(); // 角色-菜单
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮 const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单-按钮
const dialogVisible = ref(false); const dialogVisible = ref(false);
// 默认按钮
const default_selectBtn = ref<RoleMenuBtnType>({
id: 0,
menu_btn_pre_id: 0,
/** 是否选中 */
isCheck: false,
/** 按钮名称 */
name: '',
/** 数据权限范围 */
data_range: Local.get('role_default_data_range'),
dept: Local.get('role_default_custom_dept'),
});
// 选中的按钮 // 选中的按钮
const selectBtn = ref<RoleMenuBtnType>({ const selectBtn = ref<RoleMenuBtnType>({
id: 0, id: 0,
@@ -83,6 +129,29 @@ const defaultTreeProps = {
value: 'id', value: 'id',
}; };
/**
* 默认数据权限下拉选择事件
* 保留数据到cache
*/
const defaulthandlePermissionRangeChange = async (val: number) => {
if (val < 4) {
// default_selectBtn.value.dept = [];
// Local.set('role_default_custom_dept', []);
}
default_selectBtn.value.data_range = val;
Local.set('role_default_data_range', val);
};
/**
* 默认部门下拉选择事件
* 保留数据到cache
*/
const customhandlePermissionRangeChange = async (dept: Array<number>) => {
default_selectBtn.value.dept = dept;
Local.set('role_default_custom_dept', dept);
};
/** /**
* 自定数据权限下拉选择事件 * 自定数据权限下拉选择事件
*/ */
@@ -95,12 +164,21 @@ const handlePermissionRangeChange = async (val: number) => {
* 格式化按钮数据范围 * 格式化按钮数据范围
*/ */
const formatDataRange = computed(() => { const formatDataRange = computed(() => {
return function (datarange: number) { return function (datarange: number, dept: Array<number>) {
const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => { const datarangeitem = XEUtils.find(dataPermissionRange.value, (item: any) => {
if (item.value === datarange) { if (item.value === datarange) {
return item.label; return item.label;
} }
}); });
// 数据权限与默认数据权限一致
if (datarange === default_selectBtn.value.data_range) {
// 判断选择的部门是否一致
if (datarange !== 4 || JSON.stringify(dept) === JSON.stringify(default_selectBtn.value.dept)) {
return "默认接口权限"
}
}
// datarange === 4 选择的部门不一致返回datarangeitem.label
return datarangeitem.label; return datarangeitem.label;
}; };
}); });
@@ -108,11 +186,14 @@ const formatDataRange = computed(() => {
* 勾选按钮 * 勾选按钮
*/ */
const handleCheckChange = async (btn: RoleMenuBtnType) => { const handleCheckChange = async (btn: RoleMenuBtnType) => {
selectBtn.value = default_selectBtn.value;
const put_data = { const put_data = {
isCheck: btn.isCheck, isCheck: btn.isCheck,
roleId: RoleDrawer.roleId, roleId: RoleDrawer.roleId,
menuId: RoleMenuTree.id, menuId: RoleMenuTree.id,
btnId: btn.id, btnId: btn.id,
data_range: default_selectBtn.value.data_range,
dept: default_selectBtn.value.dept,
}; };
const { data, msg } = await setRoleMenuBtn(put_data); const { data, msg } = await setRoleMenuBtn(put_data);
RoleMenuBtn.updateState(data); RoleMenuBtn.updateState(data);
@@ -168,9 +249,10 @@ onMounted(async () => {
background-color: var(--el-color-primary); background-color: var(--el-color-primary);
} }
} }
// .el-checkbox {
// width: 200px; .el-checkbox {
// } width: 20%;
}
.btn-item { .btn-item {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -0,0 +1,30 @@
import { request } from '/@/utils/service';
import { UserPageQuery} from '@fast-crud/fast-crud';
/**
* 当前角色查询未授权的用户
* @param role_id 角色id
* @param query 查询条件 需要有角色id
* @returns
*/
export function getRoleUsersUnauthorized(query: UserPageQuery) {
query["authorized"] = 0; // 未授权的用户
return request({
url: '/api/system/role/get_role_users/',
method: 'get',
params: query,
});
}
/**
* 当前用户角色添加用户
* @param role_id 角色id
* @param users_id 用户id数组
* @returns
*/
export function addRoleUsers(role_id: number, users_id: Array<Number>) {
return request({
url: `/api/system/role/${role_id}/add_role_users/`,
method: 'post',
data: {users_id: users_id},
});
}

View File

@@ -0,0 +1,184 @@
import {getRoleUsersUnauthorized} from './api';
import {
compute,
dict,
UserPageQuery,
AddReq,
DelReq,
EditReq,
CrudOptions,
CreateCrudOptionsProps,
CreateCrudOptionsRet
} from '@fast-crud/fast-crud';
import { ref , nextTick} from 'vue';
import XEUtils from 'xe-utils';
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await getRoleUsersUnauthorized(query);
};
const editRequest = async ({ form, row }: EditReq) => {
return undefined;
};
const delRequest = async ({ row }: DelReq) => {
return undefined;
};
const addRequest = async ({ form }: AddReq) => {
return undefined;
};
// 记录选中的行
const selectedRows = ref<any>([]);
const onSelectionChange = (changed: any) => {
const tableData = crudExpose.getTableData();
const unChanged = tableData.filter((row: any) => !changed.includes(row));
// 添加已选择的行
XEUtils.arrayEach(changed, (item: any) => {
const ids = XEUtils.pluck(selectedRows.value, 'id');
if (!ids.includes(item.id)) {
selectedRows.value = XEUtils.union(selectedRows.value, [item]);
}
});
// 剔除未选择的行
XEUtils.arrayEach(unChanged, (unItem: any) => {
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== unItem.id);
});
};
const toggleRowSelection = () => {
// 多选后,回显默认勾选
const tableRef = crudExpose.getBaseTableRef();
const tableData = crudExpose.getTableData();
const selected = XEUtils.filter(tableData, (item: any) => {
const ids = XEUtils.pluck(selectedRows.value, 'id');
return ids.includes(item.id);
});
nextTick(() => {
XEUtils.arrayEach(selected, (item) => {
tableRef.toggleRowSelection(item, true);
});
});
};
return {
selectedRows,
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
show: false,
buttons: {
add: {
show: false,
},
},
},
rowHandle: {
show: false,
//固定右侧
fixed: 'left',
width: 150,
buttons: {
view: {
show: false,
},
edit: {
show: false,
},
remove: {
show: false,
},
},
},
table: {
rowKey: "id",
onSelectionChange,
onRefreshed: () => toggleRowSelection(),
},
columns: {
$checked: {
title: "选择",
form: { show: false},
column: {
show: true,
type: "selection",
align: "center",
width: "55px",
columnSetDisabled: true, //禁止在列设置中选择
}
},
_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;
},
},
},
name: {
title: '用户名',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
type: 'text',
form: {
show: false,
},
},
dept: {
title: '部门',
show: true,
type: 'dict-tree',
column: {
name: 'text',
formatter({value,row,index}){
return row.dept__name
}
},
search: {
show: true,
disabled: true,
col:{span: 6},
component: {
multiple: false,
props: {
checkStrictly: true,
clearable: true,
filterable: true,
},
},
},
form: {
show: false
},
dict: dict({
isTree: true,
url: '/api/system/dept/all_dept/',
value: 'id',
label: 'name'
}),
},
},
},
};
};

View File

@@ -0,0 +1,91 @@
<template>
<el-dialog v-model="dialog" title="添加授权用户" direction="rtl" destroy-on-close :before-close="handleDialogClose">
<div style="height: 500px;" >
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-right>
<el-popover placement="top" :width="200" trigger="click">
<template #reference>
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
</template>
<el-table :data="selectedRows" size="small" :max-height="500">
<!-- <el-table-column width="100" property="id" label="id" /> -->
<el-table-column width="100" property="name" label="用户名" />
<el-table-column fixed="right" label="操作" min-width="50">
<template #default="scope">
<el-button text type="info" :icon="Close" @click="removeSelectedRows(scope.row)" circle />
</template>
</el-table-column>
</el-table>
</el-popover>
</template>
</fs-crud>
</div>
<template #footer>
<div>
<el-button type="primary" @click="handleDialogConfirm"> 确定</el-button>
<el-button @click="handleDialogClose"> 取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { successNotification } from '/@/utils/message';
import { addRoleUsers } from './api';
import { Close } from '@element-plus/icons-vue';
import XEUtils from 'xe-utils';
const props = defineProps({
refreshCallback: {
type: Function,
required: true,
},
});
//对话框是否显示
const dialog = ref(false);
// 父组件刷新回调函数
const parentRefreshCallbackFunc = props.refreshCallback;
//抽屉关闭确认
const handleDialogClose = () => {
dialog.value = false;
selectedRows.value = [];
};
const handleDialogConfirm = async () => {
if (selectedRows.value.length === 0) {
return;
}
await addRoleUsers(crudRef.value.getSearchFormData().role_id, XEUtils.pluck(selectedRows.value, 'id')).then(res => {
successNotification(res.msg);
})
parentRefreshCallbackFunc && parentRefreshCallbackFunc(); // 刷新父组件
handleDialogClose();
};
const { crudBinding, crudRef, crudExpose, selectedRows } = useFs({ createCrudOptions, context: {} });
const { setSearchFormData, doRefresh } = crudExpose;
// 选中行的条数
const selectedRowsCount = computed(() => {
return selectedRows.value.length;
});
const removeSelectedRows = (row: any) => {
const tableRef = crudExpose.getBaseTableRef();
const tableData = crudExpose.getTableData();
if (XEUtils.pluck(tableData, 'id').includes(row.id)) {
tableRef.toggleRowSelection(row, false);
} else {
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== row.id);
}
};
defineExpose({ dialog, setSearchFormData, doRefresh, parentRefreshCallbackFunc});
</script>

View File

@@ -0,0 +1,44 @@
import { request } from '/@/utils/service';
import { UserPageQuery} from '@fast-crud/fast-crud';
/**
* 当前角色查询授权的用户
* @param query 查询条件 需要有角色id
* @returns
*/
export function getRoleUsersAuthorized(query: UserPageQuery) {
query["authorized"] = 1; // 授权的用户
return request({
url: '/api/system/role/get_role_users/',
method: 'get',
params: query,
});
}
/**
* 当前角色删除授权的用户
* @param role_id 角色id
* @param user_id 用户id数组
* @returns
*/
export function removeRoleUser(role_id: number, user_id: Array<number>) {
return request({
url: `/api/system/role/${role_id}/remove_role_user/`,
method: 'delete',
data: {user_id: user_id},
});
}
/**
* 当前用户角色添加用户
* @param role_id 角色id
* @param data 用户id数组
* @returns
*/
export function addRoleUsers(role_id: number, data: Array<Number>) {
return request({
url: `/api/system/role/${role_id}/add_role_users/`,
method: 'post',
data: {users_id: data},
});
}

View File

@@ -0,0 +1,193 @@
import {getRoleUsersAuthorized, removeRoleUser} from './api';
import {
compute,
dict,
UserPageQuery,
AddReq,
DelReq,
EditReq,
CrudOptions,
CreateCrudOptionsProps,
CreateCrudOptionsRet
} from '@fast-crud/fast-crud';
import {auth} from "/@/utils/authFunction";
import { ref , nextTick} from 'vue';
import XEUtils from 'xe-utils';
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await getRoleUsersAuthorized(query);
};
const editRequest = async ({ form, row }: EditReq) => {
return undefined;
};
const delRequest = async ({ row }: DelReq) => {
return await removeRoleUser(crudExpose.crudRef.value.getSearchFormData().role_id, [row.id]);
};
const addRequest = async ({ form }: AddReq) => {
return undefined;
};
// 记录选中的行
const selectedRows = ref<any>([]);
const onSelectionChange = (changed: any) => {
const tableData = crudExpose.getTableData();
const unChanged = tableData.filter((row: any) => !changed.includes(row));
// 添加已选择的行
XEUtils.arrayEach(changed, (item: any) => {
const ids = XEUtils.pluck(selectedRows.value, 'id');
if (!ids.includes(item.id)) {
selectedRows.value = XEUtils.union(selectedRows.value, [item]);
}
});
// 剔除未选择的行
XEUtils.arrayEach(unChanged, (unItem: any) => {
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== unItem.id);
});
};
const toggleRowSelection = () => {
// 多选后,回显默认勾选
const tableRef = crudExpose.getBaseTableRef();
const tableData = crudExpose.getTableData();
const selected = XEUtils.filter(tableData, (item: any) => {
const ids = XEUtils.pluck(selectedRows.value, 'id');
return ids.includes(item.id);
});
nextTick(() => {
XEUtils.arrayEach(selected, (item) => {
tableRef.toggleRowSelection(item, true);
});
});
};
return {
selectedRows,
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: auth('role:AuthorizedAdd'),
click: (ctx: any) => {
context!.subUserRef.value.dialog = true;
nextTick(() => {
context!.subUserRef.value.setSearchFormData({ form: { role_id: crudExpose.crudRef.value.getSearchFormData().role_id } });
context!.subUserRef.value.doRefresh();
});
},
},
},
},
rowHandle: {
//固定右侧
fixed: 'left',
width: 120,
show: auth('role:AuthorizedDel'),
buttons: {
view: {
show: false,
},
edit: {
show: false,
},
remove: {
iconRight: 'Delete',
show: true,
},
},
},
table: {
rowKey: "id",
onSelectionChange,
onRefreshed: () => toggleRowSelection(),
},
columns: {
$checked: {
title: "选择",
form: { show: false},
column: {
show: auth('role:AuthorizedDel'),
type: "selection",
align: "center",
width: "55px",
columnSetDisabled: true, //禁止在列设置中选择
}
},
_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;
},
},
},
name: {
title: '用户名',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
type: 'text',
form: {
show: false,
},
},
dept: {
title: '部门',
show: true,
type: 'dict-tree',
column: {
name: 'text',
formatter({value,row,index}){
return row.dept__name
}
},
search: {
show: true,
disabled: true,
col:{span: 6},
component: {
multiple: false,
props: {
checkStrictly: true,
clearable: true,
filterable: true,
},
},
},
form: {
show: false
},
dict: dict({
isTree: true,
url: '/api/system/dept/all_dept/',
value: 'id',
label: 'name'
}),
},
},
},
};
};

View File

@@ -0,0 +1,98 @@
<template>
<el-drawer size="70%" v-model="RoleUserDrawer.drawerVisible" direction="rtl" destroy-on-close :before-close="handleClose">
<template #header>
<div>
当前授权角色
<el-tag>{{ RoleUserDrawer.role_name }}</el-tag>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-right>
<el-popover placement="top" :width="200" trigger="click">
<template #reference>
<el-button text :type="selectedRowsCount > 0 ? 'primary' : ''">已选中{{ selectedRowsCount }}条数据</el-button>
</template>
<el-table :data="selectedRows" size="small" :max-height="500" >
<!-- <el-table-column width="100" property="id" label="id" /> -->
<el-table-column width="100" property="name" label="用户名" />
<el-table-column fixed="right" label="操作" min-width="60">
<template #default="scope">
<el-button text type="info" :icon="Close" @click="removeSelectedRows(scope.row)" circle />
</template>
</el-table-column>
</el-table>
</el-popover>
</template>
<template #pagination-left>
<el-tooltip content="批量删除所选择的用户权限">
<el-button v-show="selectedRowsCount > 0 && auth('role:AuthorizedDel')" type="danger" @click="multipleDel" :icon="Delete">批量删除</el-button>
</el-tooltip>
</template>
</fs-crud>
<subUser ref="subUserRef" :refreshCallback="refreshData"> </subUser>
</el-drawer>
</template>
<script lang="ts" setup>
import {auth} from "/@/utils/authFunction";
import { ref, onMounted, defineAsyncComponent, computed } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { Close, Delete } from '@element-plus/icons-vue';
import XEUtils from 'xe-utils';
import {removeRoleUser} from "./api"
import { ElMessageBox } from 'element-plus';
import { errorMessage, successNotification } from '/@/utils/message';
import { RoleUserStores } from '../../stores/RoleUserStores';
const RoleUserDrawer = RoleUserStores(); // 授权用户抽屉参数
const subUser = defineAsyncComponent(() => import('../addUsers/index.vue'));
const subUserRef = ref();
const refreshData = () => {
crudExpose.doRefresh();
};
//抽屉是否显示
const drawer = ref(false);
//抽屉关闭确认
const handleClose = (done: () => void) => {
selectedRows.value = [];
done();
};
// 选中行的条数
const selectedRowsCount = computed(() => {
return selectedRows.value.length;
});
const removeSelectedRows = (row: any) => {
const tableRef = crudExpose.getBaseTableRef();
const tableData = crudExpose.getTableData();
if (XEUtils.pluck(tableData, 'id').includes(row.id)) {
tableRef.toggleRowSelection(row, false);
} else {
selectedRows.value = XEUtils.remove(selectedRows.value, (item: any) => item.id !== row.id);
}
};
const multipleDel = async () => {
if (selectedRows.value.length < 1) {
errorMessage("请先勾选用户");
return
}
await ElMessageBox.confirm(`确定要删除这 “${selectedRows.value.length}” 位用户的权限吗`, "确认");
const req = await removeRoleUser(crudRef.value.getSearchFormData().role_id, XEUtils.pluck(selectedRows.value, 'id'));
selectedRows.value = [];
successNotification(req.msg)
crudExpose.doRefresh()
}
const { crudBinding, crudRef, crudExpose, selectedRows } = useFs({ createCrudOptions, context: {subUserRef} });
const { setSearchFormData, doRefresh } = crudExpose;
defineExpose({ drawer, setSearchFormData, doRefresh });
</script>

View File

@@ -3,6 +3,7 @@ import * as api from './api';
import { dictionary } from '/@/utils/dictionary'; import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '../../../utils/message'; import { successMessage } from '../../../utils/message';
import { auth } from '/@/utils/authFunction'; import { auth } from '/@/utils/authFunction';
import { nextTick, computed } from 'vue';
/** /**
* *
@@ -46,7 +47,12 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
rowHandle: { rowHandle: {
//固定右侧 //固定右侧
fixed: 'right', fixed: 'right',
width: 320, width: computed(() => {
if (auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch')){
return 420;
}
return 320;
}),
buttons: { buttons: {
view: { view: {
show: true, show: true,
@@ -57,6 +63,19 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
remove: { remove: {
show: auth('role:Delete'), show: auth('role:Delete'),
}, },
assignment: {
type: 'primary',
text: '授权用户',
show: auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch'),
click: (ctx: any) => {
const { row } = ctx;
context!.RoleUserDrawer.handleDrawerOpen(row);
nextTick(() => {
context!.RoleUserRef.value.setSearchFormData({ form: { role_id: row.id } });
context!.RoleUserRef.value.doRefresh();
});
},
},
permission: { permission: {
type: 'primary', type: 'primary',
text: '权限配置', text: '权限配置',

View File

@@ -2,17 +2,22 @@
<fs-page> <fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud> <fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<PermissionDrawerCom /> <PermissionDrawerCom />
<RoleUser ref="RoleUserRef" />
</fs-page> </fs-page>
</template> </template>
<script lang="ts" setup name="role"> <script lang="ts" setup name="role">
import { defineAsyncComponent, onMounted } from 'vue'; import { defineAsyncComponent, onMounted, ref} from 'vue';
import { useFs } from '@fast-crud/fast-crud'; import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud'; import { createCrudOptions } from './crud';
import { RoleDrawerStores } from './stores/RoleDrawerStores'; import { RoleDrawerStores } from './stores/RoleDrawerStores';
import { RoleMenuBtnStores } from './stores/RoleMenuBtnStores'; import { RoleMenuBtnStores } from './stores/RoleMenuBtnStores';
import { RoleMenuFieldStores } from './stores/RoleMenuFieldStores'; import { RoleMenuFieldStores } from './stores/RoleMenuFieldStores';
import { RoleUsersStores } from './stores/RoleUsersStores'; import { RoleUsersStores } from './stores/RoleUsersStores';
import { RoleUserStores } from './stores/RoleUserStores';
const RoleUser = defineAsyncComponent(() => import('./components/searchUsers/index.vue'));
const RoleUserRef = ref();
const PermissionDrawerCom = defineAsyncComponent(() => import('./components/RoleDrawer.vue')); const PermissionDrawerCom = defineAsyncComponent(() => import('./components/RoleDrawer.vue'));
@@ -20,9 +25,11 @@ const RoleDrawer = RoleDrawerStores(); // 角色-抽屉
const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单 const RoleMenuBtn = RoleMenuBtnStores(); // 角色-菜单
const RoleMenuField = RoleMenuFieldStores();// 角色-菜单-字段 const RoleMenuField = RoleMenuFieldStores();// 角色-菜单-字段
const RoleUsers = RoleUsersStores();// 角色-用户 const RoleUsers = RoleUsersStores();// 角色-用户
const RoleUserDrawer = RoleUserStores(); // 授权用户抽屉参数
const { crudBinding, crudRef, crudExpose } = useFs({ const { crudBinding, crudRef, crudExpose } = useFs({
createCrudOptions, createCrudOptions,
context: { RoleDrawer, RoleMenuBtn, RoleMenuField }, context: { RoleDrawer, RoleMenuBtn, RoleMenuField, RoleUserDrawer, RoleUserRef },
}); });
// 页面打开后获取列表数据 // 页面打开后获取列表数据

View File

@@ -0,0 +1,29 @@
import { defineStore } from 'pinia';
/**
* 权限抽屉:角色-用户
*/
export const RoleUserStores = defineStore('RoleUserStores', {
state: (): any => ({
drawerVisible: false,
role_id: undefined,
role_name: undefined,
}),
actions: {
/**
* 打开权限修改抽屉
*/
handleDrawerOpen(row: any) {
this.drawerVisible = true;
this.role_name = row.name;
this.role_id = row.id;
},
/**
* 关闭权限修改抽屉
*/
handleDrawerClose() {
this.drawerVisible = false;
},
},
});

View File

@@ -11,6 +11,7 @@ const pathResolve = (dir: string) => {
const alias: Record<string, string> = { const alias: Record<string, string> = {
'/@': pathResolve('./src/'), '/@': pathResolve('./src/'),
'@great-dream': pathResolve('./node_modules/@great-dream/'),
'@views': pathResolve('./src/views'), '@views': pathResolve('./src/views'),
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js', 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
'@dvaformflow':pathResolve('./src/viwes/plugins/dvaadmin_form_flow/src/') '@dvaformflow':pathResolve('./src/viwes/plugins/dvaadmin_form_flow/src/')