!89 正式发布v2.1.0版本

Merge pull request !89 from dvadmin/v2.x
pull/90/head v2.1.0
dvadmin 2023-03-03 08:29:05 +00:00 committed by Gitee
commit 1310e206a5
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
22 changed files with 783 additions and 367 deletions

View File

@ -30,8 +30,6 @@ system_url.register(r'system_config', SystemConfigViewSet)
system_url.register(r'message_center',MessageCenterViewSet) system_url.register(r'message_center',MessageCenterViewSet)
urlpatterns = [ urlpatterns = [
path('user/export/', UserViewSet.as_view({'post': 'export_data', })),
path('user/import/', UserViewSet.as_view({'get': 'import_data', 'post': 'import_data'})),
path('system_config/save_content/', SystemConfigViewSet.as_view({'put': 'save_content'})), path('system_config/save_content/', SystemConfigViewSet.as_view({'put': 'save_content'})),
path('system_config/get_association_table/', SystemConfigViewSet.as_view({'get': 'get_association_table'})), path('system_config/get_association_table/', SystemConfigViewSet.as_view({'get': 'get_association_table'})),
path('system_config/get_table_data/<int:pk>/', SystemConfigViewSet.as_view({'get': 'get_table_data'})), path('system_config/get_table_data/<int:pk>/', SystemConfigViewSet.as_view({'get': 'get_table_data'})),

View File

@ -54,25 +54,39 @@ class LoginSerializer(TokenObtainPairSerializer):
登录的序列化器: 登录的序列化器:
重写djangorestframework-simplejwt的序列化器 重写djangorestframework-simplejwt的序列化器
""" """
captcha = serializers.CharField( captcha = serializers.CharField(
max_length=6, required=False, allow_null=True, allow_blank=True max_length=6, required=False, allow_null=True, allow_blank=True
) )
class Meta: class Meta:
model = Users model = Users
fields = "__all__" fields = "__all__"
read_only_fields = ["id"] read_only_fields = ["id"]
default_error_messages = {"no_active_account": _("账号/密码错误")} class LoginView(TokenObtainPairView):
"""
登录接口
"""
serializer_class = LoginSerializer
permission_classes = []
def post(self, request, *args, **kwargs):
# username可能携带的不止是用户名可能还是用户的其它唯一标识 手机号 邮箱
username = request.data.get('username',None)
if username is None:
return ErrorResponse(msg="账号不能为空")
password = request.data.get('password',None)
if password is None:
return ErrorResponse(msg="密码不能为空")
def validate(self, attrs):
captcha = self.initial_data.get("captcha", None)
if dispatch.get_system_config_values("base.captcha_state"): if dispatch.get_system_config_values("base.captcha_state"):
captcha = request.data.get('captcha', None)
captchaKey = request.data.get('captchaKey', None)
if captchaKey is None:
return ErrorResponse(msg="验证码不能为空")
if captcha is None: if captcha is None:
raise CustomValidationError("验证码不能为空") raise CustomValidationError("验证码不能为空")
self.image_code = CaptchaStore.objects.filter( self.image_code = CaptchaStore.objects.filter(
id=self.initial_data["captchaKey"] id=captchaKey
).first() ).first()
five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
if self.image_code and five_minute_ago > self.image_code.expiration: if self.image_code and five_minute_ago > self.image_code.expiration:
@ -87,34 +101,36 @@ class LoginSerializer(TokenObtainPairSerializer):
else: else:
self.image_code and self.image_code.delete() self.image_code and self.image_code.delete()
raise CustomValidationError("图片验证码错误") raise CustomValidationError("图片验证码错误")
data = super().validate(attrs) try:
data["name"] = self.user.name # 手动通过 user 签发 jwt-token
data["userId"] = self.user.id user = Users.objects.get(username=username)
data["avatar"] = self.user.avatar except:
dept = getattr(self.user, 'dept', None) return ErrorResponse(msg='该账号未注册')
# 获得用户后校验密码并签发token
if not user.check_password(password):
return ErrorResponse(msg='密码错误')
result = {
"name":user.name,
"userId":user.id,
"avatar":user.avatar,
}
dept = getattr(user, 'dept', None)
if dept: if dept:
data['dept_info'] = { result['dept_info'] = {
'dept_id': dept.id, 'dept_id': dept.id,
'dept_name': dept.name, 'dept_name': dept.name,
'dept_key': dept.key 'dept_key': dept.key
} }
role = getattr(self.user, 'role', None) role = getattr(user, 'role', None)
if role: if role:
data['role_info'] = role.values('id', 'name', 'key') result['role_info'] = role.values('id', 'name', 'key')
request = self.context.get("request") refresh = LoginSerializer.get_token(user)
request.user = self.user result["refresh"] = str(refresh)
result["access"] = str(refresh.access_token)
# 记录登录日志 # 记录登录日志
request.user = user
save_login_log(request=request) save_login_log(request=request)
return {"code": 2000, "msg": "请求成功", "data": data} return DetailResponse(data=result,msg="获取成功")
class LoginView(TokenObtainPairView):
"""
登录接口
"""
serializer_class = LoginSerializer
permission_classes = []
class LoginTokenSerializer(TokenObtainPairSerializer): class LoginTokenSerializer(TokenObtainPairSerializer):

View File

@ -34,3 +34,4 @@ class LoginLogViewSet(CustomModelViewSet):
queryset = LoginLog.objects.all() queryset = LoginLog.objects.all()
serializer_class = LoginLogSerializer serializer_class = LoginLogSerializer
extra_filter_backends = [] extra_filter_backends = []
ordering_fields = ['create_datetime']

View File

@ -185,11 +185,11 @@ class MenuViewSet(CustomModelViewSet):
parent = params.get('parent', None) parent = params.get('parent', None)
if params: if params:
if parent: if parent:
queryset = self.queryset.filter(status=1, parent=parent) queryset = self.queryset.filter(parent=parent)
else: else:
queryset = self.queryset.filter(status=1) queryset = self.queryset
else: else:
queryset = self.queryset.filter(status=1, parent__isnull=True) queryset = self.queryset.filter(parent__isnull=True)
queryset = self.filter_queryset(queryset) queryset = self.filter_queryset(queryset)
serializer = MenuSerializer(queryset, many=True, request=request) serializer = MenuSerializer(queryset, many=True, request=request)
data = serializer.data data = serializer.data

View File

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

View File

@ -219,10 +219,12 @@ class ExportUserProfileSerializer(CustomModelSerializer):
class UserProfileImportSerializer(CustomModelSerializer): class UserProfileImportSerializer(CustomModelSerializer):
password = serializers.CharField(required=True, max_length=50, error_messages={"required": "登录密码不能为空"})
def save(self, **kwargs): def save(self, **kwargs):
data = super().save(**kwargs) data = super().save(**kwargs)
password = hashlib.new( password = hashlib.new(
"md5", str(self.initial_data.get("password", "")).encode(encoding="UTF-8") "md5", str(self.initial_data.get("password", "admin123456")).encode(encoding="UTF-8")
).hexdigest() ).hexdigest()
data.set_password(password) data.set_password(password)
data.save() data.save()

View File

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

View File

@ -20,9 +20,9 @@
"china-division": "^2.4.0", "china-division": "^2.4.0",
"core-js": "^3.4.3", "core-js": "^3.4.3",
"cropperjs": "^1.5.6", "cropperjs": "^1.5.6",
"d2-crud-plus": "^2.13.1", "d2-crud-plus": "^2.17.9",
"d2-crud-x": "^2.13.1", "d2-crud-x": "^2.17.9",
"d2p-extends": "^2.13.1", "d2p-extends": "^2.17.9",
"dayjs": "^1.8.17", "dayjs": "^1.8.17",
"echarts": "^5.1.2", "echarts": "^5.1.2",
"el-phone-number-input": "^1.1.5", "el-phone-number-input": "^1.1.5",
@ -42,6 +42,7 @@
"sortablejs": "^1.10.1", "sortablejs": "^1.10.1",
"ua-parser-js": "^0.7.20", "ua-parser-js": "^0.7.20",
"vue": "2.7.14", "vue": "2.7.14",
"vue-grid-layout": "^2.4.0",
"vue-i18n": "^8.15.1", "vue-i18n": "^8.15.1",
"vue-infinite-scroll": "^2.0.2", "vue-infinite-scroll": "^2.0.2",
"vue-router": "^3.6.5", "vue-router": "^3.6.5",

View File

@ -118,7 +118,7 @@ export default {
that.$refs.upload.clearFiles() that.$refs.upload.clearFiles()
// //
return request({ return request({
url: util.baseURL() + that.api + 'import_data/', url: that.api + 'import_data/',
method: 'post', method: 'post',
data: { data: {
url: response.data.url url: response.data.url

View File

@ -62,7 +62,15 @@ Vue.use(d2CrudPlus, {
page: { // page接口返回的数据结构配置 page: { // page接口返回的数据结构配置
request: { request: {
current: 'page', current: 'page',
size: 'limit' size: 'limit',
orderAsc (query, value) {
const field = query.orderProp
if (value) {
query.ordering = field
} else {
query.ordering = `-${field}`
}
}
}, },
response: { response: {
current: 'page', // 当前页码 ret.data.current current: 'page', // 当前页码 ret.data.current

View File

@ -97,6 +97,12 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 授权后可以删除-->
<div class="dvadmin-auth">
<span>Powered by Django-Vue-Admin</span>
<el-divider direction="vertical"></el-divider>
<span>Copyright dvadmin团队</span>
</div>
</div> </div>
</template> </template>
@ -208,4 +214,24 @@ export default {
<style lang="scss"> <style lang="scss">
// //
@import "~@/assets/style/theme/register.scss"; @import "~@/assets/style/theme/register.scss";
//
.dvadmin-auth{
font-size: 0.8em;
position: absolute;
top: 50vh;
right: -9vw;
text-align: center;
color: #888888;
background-image: linear-gradient(to left, #d3d3d3, #989898, #888888, #363636,#888888,#989898,#d3d3d3);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
-webkit-background-size: 200% 100%;
animation: bgp 6s infinite linear;
transform: rotate(90deg);
}
@-webkit-keyframes bgp {
0% {background-position: 0 0; }
100% {background-position: -100% 0; }
}
</style> </style>

View File

@ -0,0 +1,32 @@
<template>
<el-card shadow="hover" header="关于项目" class="item-background">
<p>高性能 / 精致 / 优雅基于Vue2 + Element-UI 的中后台前端解决方案如果喜欢就点个星星支持一下</p>
<p>
<a href='https://liqianglog.gitee.io/django-vue-admin' target="_blank">
<img src='https://liqianglog.gitee.io/django-vue-admin/badge/star.svg?theme=dark' alt='star' style="vertical-align: middle">
</a>
</p>
</el-card>
</template>
<script>
export default {
title: '关于项目',
icon: 'el-icon-setting',
description: '点个星星支持一下',
height: 20,
width: 8,
minH: 20,
minW: 4,
isResizable: true,
data () {
return {}
}
}
</script>
<style scoped>
.item-background p {
color: #999;
}
</style>

View File

@ -0,0 +1,13 @@
import { markRaw } from 'vue'
const resultComps = {}
const requireComponent = require.context(
'./', // 在当前目录下查找
false, // 不遍历子文件夹
/\.vue$/ // 正则匹配 .vue结尾的文件
)
requireComponent.keys().forEach(fileName => {
const comp = requireComponent(fileName)
resultComps[fileName.replace(/^\.\/(.*)\.\w+$/, '$1')] = comp.default
})
export default markRaw(resultComps)

View File

@ -0,0 +1,58 @@
<template>
<el-card shadow="hover" header="时钟" class="item-background">
<div class="time">
<h2>{{ time }}</h2>
<p>{{ day }}</p>
</div>
</el-card>
</template>
<script>
import dayjs from 'dayjs'
export default {
title: '时钟',
icon: 'el-icon-alarm-clock',
description: '演示部件效果',
height: 17,
minH: 17,
width: 8,
minW: 4,
isResizable: true,
data () {
return {
time: '',
day: ''
}
},
mounted () {
this.showTime()
setInterval(() => {
this.showTime()
}, 1000)
},
methods: {
showTime () {
this.time = dayjs().format('hh:mm:ss')
this.day = dayjs().format('YYYY年MM月DD日')
}
}
}
</script>
<style scoped>
.item-background {
background: linear-gradient(to right, #8E54E9, #4776E6);
color: #fff;
}
.time h2 {
font-size: 40px;
}
.time p {
font-size: 14px;
margin-top: 10px;
opacity: 0.7;
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<el-card shadow="hover" header="版本信息">
<div style="height: 70px;text-align: center;">
<h2 style="margin-top: 5px;">Dvadmin</h2>
<p style="margin-top: 5px;">最新版本 {{ ver }}</p>
</div>
<div style="margin-top: 5px;">
<el-button type="primary" plain round @click="golog"></el-button>
<el-button type="primary" plain round @click="gogit">gitee</el-button>
</div>
</el-card>
</template>
<script>
export default {
title: '版本信息',
icon: 'el-icon-monitor',
description: '当前项目版本信息',
height: 22,
width: 8,
minH: 22,
minW: 4,
isResizable: true,
data () {
return {
ver: 'loading...'
}
},
mounted () {
this.getVer()
},
methods: {
async getVer () {
this.ver = 'v2.0.9'
},
golog () {
window.open('https://gitee.com/liqianglog/django-vue-admin/releases')
},
gogit () {
window.open('https://gitee.com/liqianglog/django-vue-admin')
}
}
}
</script>

View File

@ -0,0 +1,113 @@
<template>
<el-card shadow="hover" header="欢迎">
<div class="welcome">
<div class="logo">
<img src="/image/django-vue-admin.png">
<h2>欢迎体验 Dvadmin</h2>
</div>
<div class="tips">
<div class="tips-item">
<div class="tips-item-icon">
<i class="el-icon-menu"></i>
</div>
<div class="tips-item-message">这里是项目控制台你可以点击右上方的自定义按钮来添加移除或者移动部件</div>
</div>
<div class="tips-item">
<div class="tips-item-icon">
<i class="el-icon-star-on"></i>
</div>
<div class="tips-item-message">热爱Python和Vue,打造一个低代码开源平台并且持续着</div>
</div>
<div class="tips-item">
<div class="tips-item-icon">
<i class="el-icon-milk-tea"></i>
</div>
<div class="tips-item-message">项目目的让前端和后端工作更快乐</div>
</div>
</div>
<div class="actions">
<el-button type="primary" icon="el-icon-check" size="large" @click="godoc"></el-button>
</div>
</div>
</el-card>
</template>
<script>
export default {
title: '欢迎',
icon: 'el-icon-present',
description: '项目特色以及文档链接',
height: 50,
minH: 50,
width: 8,
minW: 4,
isResizable: true,
data () {
return {}
},
methods: {
godoc () {
window.open('https://www.django-vue-admin.com/')
}
}
}
</script>
<style scoped>
.welcome {
}
.welcome .logo {
text-align: center;
}
.welcome .logo img {
vertical-align: bottom;
width: 100px;
height: 100px;
margin-bottom: 20px;
}
.welcome .logo h2 {
font-size: 30px;
font-weight: normal;
display: flex;
align-items: center;
justify-content: center;
}
.tips {
margin-top: 20px;
padding: 0 40px;
}
.tips-item {
display: flex;
align-items: center;
justify-content: center;
padding: 7.5px 0;
}
.tips-item-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 18px;
margin-right: 20px;
color: var(--el-color-primary);
background: rgba(180, 180, 180, 0.1);
}
.tips-item-message {
flex: 1;
font-size: 14px;
}
.actions {
text-align: center;
margin: 40px 0 20px 0;
}
</style>

View File

@ -1,313 +1,382 @@
<template> <template>
<d2-container> <d2-container>
<div class="page-header"> <div class="component-header">
<div class="set-btn-class">
<el-avatar src="https://q1.qlogo.cn/g?b=qq&amp;nk=190848757&amp;s=640" class="user-avatar"> <el-button v-if="customizing" type="primary" icon="el-icon-check" round @click="save"></el-button>
<el-button v-else type="primary" icon="el-icon-edit" round @click="custom"></el-button>
</el-avatar> </div>
<div class="title"> <div v-if="customizing" class="all-component-class">
<h1>早安, DVAdmin, 开始您一天的工作吧</h1> <el-card style="margin-bottom: 20px">
<span> 今日晴20 - 32 </span> <div slot="header">
<i class="el-icon-circle-plus"></i>
<span>添加部件</span>
<el-button-group style="float: right">
<el-button type="primary" size="mini" @click="backDefaul()"></el-button>
<el-button type="danger" size="mini" @click="close()"></el-button>
</el-button-group>
</div>
<div class="widgets-list">
<div v-if="myCompsList.length<=0" class="widgets-list-nodata">
<el-empty description="没有部件啦" :image-size="60"></el-empty>
</div>
<div v-for="item in myCompsList" :key="item.title" class="widgets-list-item" @drag="onDrag($event,item)"
@dragend="onDragend($event,item)" draggable="true"
unselectable="on">
<el-card style="width: 300px">
<div slot="header">
<i :class="item.icon"></i>
<span> {{ item.title }}</span>
<el-button type="primary" style="float: right;" size="mini" @click="push(item)"></el-button>
</div>
<div class="item-info">
<p>{{ item.description }}</p>
</div>
</el-card>
</div>
</div>
</el-card>
</div> </div>
</div> </div>
<div class="widgets" ref="widgets">
<el-row :gutter="20"> <div :class="['widgets-wrapper',customizing?'widgets-wrapper-bg':'']">
<el-col :span="12"> <div v-if="nowCompsList.length<=0" class="no-widgets">
<el-card class="box-card"> <el-empty image="img/no-widgets.svg" description="没有部件啦" :image-size="280"></el-empty>
<div slot="header" class="clearfix">
<span>友情链接</span>
<el-button style="float: right; padding: 3px 0" type="text">
<el-link href="https://bbs.django-vue-admin.com" target="_blank" type="primary">更多</el-link>
</el-button>
</div>
<el-row>
<el-col :span="8" v-for="({name,imageUrl,slogan,link},index) in projects" :key="index">
<el-card shadow="hover" style="padding: 0">
<div class="project-detail">
<div>
<a :href="link" target="_blank">
<img :src="imageUrl" alt="">
<span v-text="name" class="name"></span>
</a>
</div>
<div v-text="slogan" class="slogan" :title="slogan"></div>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
</el-col>
<el-col :span="12">
<div class="grid-content bg-purple">
<el-card class="box-card" >
<div slot="header" class="clearfix">
<span>快捷导航</span>
</div>
<el-row>
<el-col :span="8" v-for="({name,icon,route,color},index) of navigators" :key="index" style="padding: 0">
<el-card shadow="hover">
<div style="display: flex;align-items: center;flex-direction: column;cursor: pointer" @click="()=>{gotoRoute(route)}">
<d2-icon-svg :name="icon" style="width: 25px;height: 25px;" :style="{fill:color}"/>
<div style="text-align: center;font-size: 12px;margin-top: 20px" v-text="name"></div>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
<el-card class="box-card" style="margin-top: 25px">
<div class="work">
<d2-icon-svg name="work" style="margin-left: 50%;transform: translateX(-50%);height: 216px"/>
</div>
</el-card>
</div> </div>
</el-col> <grid-layout
</el-row> ref="gridlayout"
:layout.sync="layout"
:col-num="24"
:row-height="1"
:is-draggable="customizing"
:vertical-compact="false"
:use-css-transforms="true"
:autoSize="true"
>
<grid-item v-for="(item, index) in layout"
:static="item.static"
:x="item.x"
:y="item.y"
:w="item.w"
:h="item.h"
:i="item.i"
:key="index"
:isResizable="customizing"
>
<div v-if="customizing" class="customize-overlay">
<el-button class="close" type="danger" plain icon="el-icon-close" size="small"
@click="remove(index)"></el-button>
<label>
<i :class="allComps[item.element].icon"></i>
{{ allComps[item.element].title }}</label>
</div>
<component :class="customizing?'set-component-bg':''" :is="allComps[item.element]"></component>
</grid-item>
</grid-layout>
</div>
</div>
</d2-container> </d2-container>
</template> </template>
<script> <script>
import { use } from 'echarts/core' import draggable from 'vuedraggable'
import { CanvasRenderer } from 'echarts/renderers' import allComps from './components'
import { PieChart } from 'echarts/charts' import util from '@/libs/util'
import { import VueGridLayout from 'vue-grid-layout'
TitleComponent, import XEUtils from 'xe-utils'
TooltipComponent, const mouseXY = { x: null, y: null }
LegendComponent const DragPos = { x: 0, y: 0, w: 1, h: 1, i: null }
} from 'echarts/components'
use([
CanvasRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent
])
export default { export default {
name: 'workbench', components: {
draggable,
GridLayout: VueGridLayout.GridLayout,
GridItem: VueGridLayout.GridItem
},
data () { data () {
return { return {
projects: [ customizing: false,
{ allComps: allComps,
name: '官方文档', selectLayout: [],
imageUrl: '/image/django-vue-admin.png', defaultGrid: {
slogan: 'Django-Vue-Admin 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。', // [24] [18,6] [8,8,8] [6,12,6]
link: 'http://django-vue-admin.com' layout: [12, 6, 6],
}, // com:views/home/components
{ copmsList: [
name: '官方论坛', ['welcome'],
imageUrl: '/image/django-vue-admin.png', ['about', 'ver'],
slogan: 'Django-Vue-Admin 官方论坛', ['time', 'progress']
link: 'http://bbs.django-vue-admin.com'
},
{
name: 'D2admin',
imageUrl: '/image/d2-pub.png',
slogan: 'D2Admin (opens new window)是一个完全 开源免费 的企业中后台产品前端集成方案,使用最新的前端技术栈,' +
'小于 60kb 的本地首屏 js 加载,已经做好大部分项目前期准备工作,并且带有大量示例代码,助力管理系统快速开发。',
link: 'https://d2.pub/zh'
},
{
name: 'SimpleUi',
imageUrl: '/image/simple-ui.png',
slogan: '一个基于Django Admin的现代化主题。',
link: 'https://simpleui.72wo.com/'
},
{
name: '若依',
imageUrl: '/image/ruoyi.png',
slogan: '基于SpringBoot、Shiro、Mybatis的权限后台管理系统。',
link: 'http://ruoyi.vip/'
},
{
name: 'Gin-Vue-Admin',
imageUrl: '/image/gin-vue-admin.png',
slogan: '使用gin+vue进行极速开发的全栈后台管理系统。',
link: 'https://www.gin-vue-admin.com/'
},
{
name: 'DCM',
imageUrl: '/image/django-comment-migrate.png',
slogan: '这是一个Django model注释迁移的app',
link: 'https://github.com/starryrbs/django-comment-migrate'
},
{
name: 'Jetbrains',
imageUrl: '/image/jetbrains.jpeg',
slogan: '我们构建我们的软件,让您可以享受构建自己的软件的乐趣',
link: 'https://www.jetbrains.com/'
},
{
name: 'Django',
imageUrl: '/image/django.png',
slogan: '有期限的完美主义者的网络框架。',
link: 'https://github.com/django/django'
}
],
navigators: [
{
name: '控制台',
icon: 'home',
route: {
name: 'index'
},
color: 'rgb(31, 218, 202);'
},
{
name: '部门管理',
icon: 'department',
route: {
name: 'dept'
},
color: 'rgb(225, 133, 37);'
},
{
name: '角色管理',
icon: 'role',
route: {
name: 'role'
},
color: 'rgb(191, 12, 44);'
},
{
name: '菜单管理',
icon: 'menu',
route: {
name: 'menu'
},
color: 'rgb(63, 178, 127);'
},
{
name: '用户管理',
icon: 'user',
route: {
name: 'user'
},
color: 'rgb(191, 12, 44);'
},
{
name: '日志管理',
icon: 'log',
route: {
name: 'operationLog'
},
color: 'rgb(0, 216, 255);'
}
],
chartData: {
columns: ['日期', '销售额'],
rows: [
{ 日期: '1月1日', 销售额: 123 },
{ 日期: '1月2日', 销售额: 1223 },
{ 日期: '1月3日', 销售额: 2123 },
{ 日期: '1月4日', 销售额: 4123 },
{ 日期: '1月5日', 销售额: 3123 },
{ 日期: '1月6日', 销售额: 7123 }
] ]
},
defaultLayout: [],
layout: [
// { x: 0, y: 0, w: 2, h: 2, i: '0', element: 'welcome' },
// { x: 2, y: 0, w: 2, h: 4, i: '1', element: 'about' },
// { x: 4, y: 0, w: 2, h: 5, i: '2', element: 'time' },
// { x: 6, y: 0, w: 2, h: 3, i: '3', element: 'progress' }
]
}
},
created () {
this.layout = JSON.parse(util.cookies.get('grid-layout') || JSON.stringify(this.defaultLayout))
},
mounted () {
document.addEventListener('dragover', function (e) {
mouseXY.x = e.clientX
mouseXY.y = e.clientY
}, false)
this.$emit('on-mounted')
},
computed: {
allCompsList () {
var allCompsList = []
for (var key in this.allComps) {
allCompsList.push({
key: key,
title: allComps[key].title,
icon: allComps[key].icon,
height: allComps[key].height,
width: allComps[key].width,
maxH: allComps[key].maxH || Infinity,
maxW: allComps[key].maxW || Infinity,
isResizable: allComps[key].isResizable || null,
description: allComps[key].description
})
} }
return allCompsList
},
myCompsList () {
return this.allCompsList
},
nowCompsList () {
return this.allCompsList
} }
}, },
methods: { methods: {
gotoRoute (route) { //
this.$router.push(route) custom () {
this.customizing = true
const oldWidth = this.$refs.widgets.offsetWidth
this.$nextTick(() => {
const scale = this.$refs.widgets.offsetWidth / oldWidth
this.$refs.widgets.style.setProperty('transform', `scale(${scale})`)
})
},
//
setLayout (layout) {
//
},
getLayoutElementNumber (elementName) {
// var index = 0
// this.layout.map(res => {
// if (elementName === res.element) {
// index += 1
// }
// })
// return index + 1
return elementName + this.layout.length
},
//
push (item) {
console.log(1, item)
this.layout.push({
x: 6,
y: 0,
w: item.width,
h: item.height,
isResizable: item.isResizable || null,
i: this.getLayoutElementNumber(item.key),
element: item.key
})
},
//
remove (index) {
this.layout.splice(index, 1)
},
//
save () {
console.log(this.layout)
this.customizing = false
this.$refs.widgets.style.removeProperty('transform')
util.cookies.set('grid-layout', this.layout)
},
//
backDefaul () {
this.customizing = false
this.$refs.widgets.style.removeProperty('transform')
this.layout = JSON.parse(JSON.stringify(this.defaultLayout))
util.cookies.remove('grid-layout')
},
//
close () {
this.customizing = false
this.$refs.widgets.style.removeProperty('transform')
},
//
onDrag (e, item) {
const { key, width, height } = item
const parentRect = this.$refs.widgets.getBoundingClientRect()
let mouseInGrid = false
if (((mouseXY.x > parentRect.left) && (mouseXY.x < parentRect.right)) && ((mouseXY.y > parentRect.top) && (mouseXY.y < parentRect.bottom))) {
mouseInGrid = true
}
const cloneLayout = XEUtils.clone(this.layout, true)
if (mouseInGrid === true && (cloneLayout.findIndex(item => item.i === this.getLayoutElementNumber(key)) === -1)) {
// this.layout.push({
// x: (this.layout.length * 2) % (this.colNum || 12),
// y: this.layout.length + (this.colNum || 12), // puts it at the bottom
// w: width,
// h: height,
// i: this.getLayoutElementNumber(key),
// element: key
// })
}
const index = this.layout.findIndex(item => item.i === this.getLayoutElementNumber(key))
if (index !== -1) {
try {
this.$refs.gridlayout.$children[this.layout.length].$refs.item.style.display = 'none'
} catch {
}
const el = this.$refs.gridlayout.$children[index]
el.dragging = { top: mouseXY.y - parentRect.top, left: mouseXY.x - parentRect.left }
const new_pos = el.calcXY(mouseXY.y - parentRect.top, mouseXY.x - parentRect.left)
if (mouseInGrid === true) {
this.$refs.gridlayout.dragEvent('dragstart', this.getLayoutElementNumber(key), new_pos.x, new_pos.y, 1, 1)
DragPos.i = String(index)
DragPos.x = this.layout[index].x
DragPos.y = this.layout[index].y
}
if (mouseInGrid === false) {
this.$refs.gridlayout.dragEvent('dragend', this.getLayoutElementNumber(key), new_pos.x, new_pos.y, 1, 1)
this.layout = this.layout.filter(obj => obj.i !== this.getLayoutElementNumber(key))
}
}
},
onDragend (e, item) {
const { key, width, height } = item
const parentRect = this.$refs.widgets.getBoundingClientRect()
let mouseInGrid = false
if (((mouseXY.x > parentRect.left) && (mouseXY.x < parentRect.right)) && ((mouseXY.y > parentRect.top) && (mouseXY.y < parentRect.bottom))) {
mouseInGrid = true
}
if (mouseInGrid === true) {
this.layout.push({
x: DragPos.x,
y: DragPos.y,
w: width,
h: height,
i: this.getLayoutElementNumber(key),
element: key
})
this.$refs.gridlayout.dragEvent('dragend', this.getLayoutElementNumber(key), DragPos.x, DragPos.y, 1, 1)
this.layout = this.layout.filter(obj => obj.i !== this.getLayoutElementNumber(key))
// UNCOMMENT below if you want to add a grid-item
/*
this.layout.push({
x: DragPos.x,
y: DragPos.y,
w: 1,
h: 1,
i: DragPos.i,
});
this.$refs.gridLayout.dragEvent('dragend', DragPos.i, DragPos.x,DragPos.y,1,1);
try {
this.$refs.gridLayout.$children[this.layout.length].$refs.item.style.display="block";
} catch {
}
*/
}
} }
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.component-header {
background-color: #FFFFFF;
position: sticky;
top: -20px;
z-index: 99;
$userAvatarLength: 72px; .set-btn-class {
float: right;
z-index: 99;
margin-bottom: 10px;
}
.page-header{ .all-component-class {
box-sizing: border-box; clear: right;
padding: 16px;
.user-avatar{
width: $userAvatarLength;
height: $userAvatarLength;
line-height: $userAvatarLength;
display: inline-block;
}
.title{ .widgets-list {
display: inline-block; display: flex;
padding: 0 0 0 15px; justify-content: space-between;
position: relative; overflow-x: scroll;
top: -5px; padding-bottom: 10px;
h1{ .widgets-list-item {
font-size: 1.125rem; margin-right: 10px;
font-weight: 500;
line-height: 1.75rem;
} }
span{
font-size: 14px; .widgets-list-item:last-child {
color: rgba(0,0,0,.45); margin-right: 0px;
} }
} }
}
}
} .widgets-wrapper-bg {
background: rgba(180, 180, 180, .2);
min-height: 500px;
}
.project-detail{ .widgets-wrapper .sortable-ghost {
color: rgba(0,0,0,.45); opacity: 0.5;
height: 65px; }
img {
width: 25px;
height: 25px;
}
.name{
margin-left: 1rem;
font-size: 1rem;
line-height: 2rem;
height: 2rem;
display: inline-block;
color: rgba(0,0,0,.85);
position: relative;
top: -5px;
}
.slogan{
font-size: 12px;
padding: 5px 0;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
}
.team{
font-size: 14px;
}
}
.activity{ .set-component-bg{
padding: 0; background:rgba(255, 255, 255, 0.5);
.activity-avatar{ border: 1px solid rgba(0,0,0,.5);
width: 40px; }
height: 40px;
line-height: 40px;
}
.activity-detail{
padding: 10px;
line-height: 15px;
font-size: 14px;
color: rgba(0,0,0,.85);
}
}
.chart {
height: 408px;
}
.el-divider--horizontal{ .customize-overlay {
margin: 4px 0; position: absolute;
background: 0 0; top: 0;
border-top: 1px solid #e8eaec; right: 0;
bottom: 5px;
left: 0;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.5);
cursor: move;
}
.customize-overlay label {
background: #409EFF;
color: #fff;
height: 40px;
padding: 0 30px;
border-radius: 40px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
cursor: move;
}
.customize-overlay label i {
margin-right: 15px;
font-size: 24px;
}
.customize-overlay .close {
position: absolute;
top: 15px;
right: 15px;
}
.customize-overlay .close:focus, .customize-overlay .close:hover {
background: #76B1F9;
}
}
.el-card, .el-message {
border-radius: 0;
}
</style> </style>

View File

@ -29,34 +29,35 @@ export const crudOptions = (vm) => {
iconLoaded: 'el-icon-loading' // 美化loading图标 iconLoaded: 'el-icon-loading' // 美化loading图标
} }
}, },
rowHandle: { // rowHandle: {
show: false, // show: false,
width: 140, // width: 140,
view: { // view: {
thin: true, // thin: true,
text: '', // text: '',
show: false, // show: false,
disabled () { // disabled () {
return !vm.hasPermissions('Retrieve') // return !vm.hasPermissions('Retrieve')
} // }
}, // },
edit: { // edit: {
thin: true, // thin: true,
text: '', // text: '',
show: false, // show: false,
disabled () { // disabled () {
return !vm.hasPermissions('Update') // return !vm.hasPermissions('Update')
} // }
}, // },
remove: { // remove: {
thin: true, // thin: true,
text: '', // text: '',
show: false, // show: false,
disabled () { // disabled () {
return !vm.hasPermissions('Delete') // return !vm.hasPermissions('Delete')
} // }
} // }
}, // },
rowHandle: false,
viewOptions: { viewOptions: {
componentType: 'form' componentType: 'form'
}, },

View File

@ -15,13 +15,7 @@ export const crudOptions = (vm) => {
return !vm.hasPermissions('Retrieve') return !vm.hasPermissions('Retrieve')
} }
}, },
edit: { edit: false,
thin: true,
text: '',
disabled () {
return !vm.hasPermissions('Update')
}
},
remove: { remove: {
thin: true, thin: true,
text: '', text: '',

View File

@ -4,12 +4,11 @@ export const crudOptions = (vm) => {
compact: true compact: true
}, },
options: { options: {
tableType: 'vxe-table', // tableType: 'vxe-table',
rowKey: true, // 必须设置true or false // rowKey: true, // 必须设置true or false
rowId: 'id', rowId: 'id',
height: '100%', // 表格高度100%, 使用toolbar必须设置 height: '100%', // 表格高度100%, 使用toolbar必须设置
highlightCurrentRow: false highlightCurrentRow: false,
}, },
rowHandle: { rowHandle: {
fixed: 'right', fixed: 'right',
@ -284,7 +283,8 @@ export const crudOptions = (vm) => {
title: '登录时间', title: '登录时间',
key: 'create_datetime', key: 'create_datetime',
width: 160, width: 160,
type: 'datetime' type: 'datetime',
sortable: true
} }
] ]
} }

View File

@ -19,9 +19,7 @@ export const crudOptions = (vm) => {
edit: { edit: {
thin: true, thin: true,
text: '', text: '',
show () { show: false,
return vm.tabActivted !== 'receive'
},
disabled () { disabled () {
return !vm.hasPermissions('Update') return !vm.hasPermissions('Update')
} }

View File

@ -9,8 +9,8 @@ export const crudOptions = (vm) => {
}, },
options: { options: {
height: '100%', height: '100%',
tableType: 'vxe-table', // tableType: 'vxe-table',
rowKey: true, //rowKey: true,
rowId: 'id' rowId: 'id'
}, },
selectionRow: { selectionRow: {
@ -149,6 +149,7 @@ export const crudOptions = (vm) => {
{ {
title: '姓名', title: '姓名',
key: 'name', key: 'name',
sortable: 'custom',
minWidth: 90, minWidth: 90,
search: { search: {
disabled: false disabled: false