!95 正式发布v2.1.3版本

Merge pull request !95 from dvadmin/dev
pull/96/MERGE v2.1.3
dvadmin 2023-04-22 16:20:46 +00:00 committed by Gitee
commit 2877e12142
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
38 changed files with 2392 additions and 278 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -147,11 +147,11 @@ docker-compose up -d --build
## Demo screenshot✅
![image-01](https://foruda.gitee.com/images/1681118295661653899/12b2f755_5074988.jpeg)
![image-01](https://foruda.gitee.com/images/1682179942561449504/020863bb_5074988.jpeg)
![image-02](https://foruda.gitee.com/images/1681118320563350469/311f888b_5074988.jpeg)
![image-02](https://foruda.gitee.com/images/1682179701820334814/f20eb5e8_5074988.png)
![image-03](https://foruda.gitee.com/images/1681118339552860024/780a4e11_5074988.jpeg)
![image-03](https://foruda.gitee.com/images/1682179718209143602/e6b6a4b1_5074988.png)
![image-04](https://foruda.gitee.com/images/1681118349561624452/d917f8bc_5074988.jpeg)

View File

@ -172,11 +172,11 @@ docker-compose up -d --build
## 演示图✅
![image-01](https://foruda.gitee.com/images/1681118295661653899/12b2f755_5074988.jpeg)
![image-01](https://foruda.gitee.com/images/1682179942561449504/020863bb_5074988.jpeg)
![image-02](https://foruda.gitee.com/images/1681118320563350469/311f888b_5074988.jpeg)
![image-02](https://foruda.gitee.com/images/1682179701820334814/f20eb5e8_5074988.png)
![image-03](https://foruda.gitee.com/images/1681118339552860024/780a4e11_5074988.jpeg)
![image-03](https://foruda.gitee.com/images/1682179718209143602/e6b6a4b1_5074988.png)
![image-04](https://foruda.gitee.com/images/1681118349561624452/d917f8bc_5074988.jpeg)

View File

@ -3,6 +3,7 @@
from django.conf import settings
from django.db import connection
from django.core.cache import cache
from dvadmin.utils.validator import CustomValidationError
dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory') # redis
@ -159,7 +160,7 @@ def get_dictionary_config(schema_name=None):
init_dictionary_data = cache.get(f"init_dictionary")
if not init_dictionary_data:
refresh_dictionary()
return cache.get(f"init_dictionary") or {}
return cache.get(f"init_dictionary") or {}
if not settings.DICTIONARY_CONFIG:
refresh_dictionary()
if is_tenants_mode():
@ -243,6 +244,22 @@ def get_system_config_values(key, schema_name=None):
return system_config.get(key)
def get_system_config_values_to_dict(key, schema_name=None):
"""
获取系统配置数据并转换为字典 **仅限于数组类型系统配置
:param key: 对应系统配置的key值(字典编号)
:param schema_name: 对应系统配置的租户schema_name值
:return:
"""
values_dict = {}
config_values = get_system_config_values(key, schema_name)
if not isinstance(config_values, list):
raise CustomValidationError("该方式仅限于数组类型系统配置")
for ele in get_system_config_values(key, schema_name):
values_dict[ele.get('key')] = ele.get('value')
return values_dict
def get_system_config_label(key, name, schema_name=None):
"""
获取获取系统配置label值

View File

@ -255,21 +255,11 @@ LOGGING = {
"level": "INFO",
"propagate": False,
},
'celery': {
'handlers': ["console", "error", "file"],
'propagate': False,
'level': "INFO"
},
'django.db.backends': {
'handlers': ["console", "error", "file"],
'propagate': False,
'level': "INFO"
},
'django.request': {
'handlers': ["console", "error", "file"],
'propagate': False,
'level': "DEBUG"
},
"uvicorn.error": {
"level": "INFO",
"handlers": ["console", "error", "file"],
@ -418,6 +408,7 @@ PLUGINS_URL_PATTERNS = []
# from dvadmin_uniapp.settings import * # UniApp后端
# from dvadmin_ak_sk.settings import * # 秘钥管理管理
# from dvadmin_tenants.settings import * # 租户管理
# from dvadmin_cloud_storage.settings import * # 云存储
# ...
# ********** 一键导入插件配置结束 **********

View File

@ -42,7 +42,7 @@ LOGIN_NO_CAPTCHA_AUTH = True
# ================================================= #
# ****************** 其他 配置 ******************* #
# ================================================= #
ENVIRONMENT = "local" # 环境test 测试环境;prod线上环境;local本地环境
ALLOWED_HOSTS = ["*"]
# 系统配置存放位置redis/memory(默认)
DISPATCH_DB_TYPE = 'redis'

View File

@ -1,5 +1,6 @@
import hashlib
import os
from pathlib import PurePath, PureWindowsPath, PurePosixPath
from django.contrib.auth.models import AbstractUser
from django.db import models
@ -13,8 +14,11 @@ STATUS_CHOICES = (
)
class Users(CoreModel,AbstractUser):
username = models.CharField(max_length=150, unique=True, db_index=True, verbose_name="用户账号", help_text="用户账号")
class Users(CoreModel, AbstractUser):
username = models.CharField(max_length=150, unique=True, db_index=True, verbose_name="用户账号",
help_text="用户账号")
employee_no = models.CharField(max_length=150, unique=True, db_index=True, null=True, blank=True,
verbose_name="工号", help_text="工号")
email = models.EmailField(max_length=255, verbose_name="邮箱", null=True, blank=True, help_text="邮箱")
mobile = models.CharField(max_length=255, verbose_name="电话", null=True, blank=True, help_text="电话")
avatar = models.CharField(max_length=255, verbose_name="头像", null=True, blank=True, help_text="头像")
@ -34,8 +38,10 @@ class Users(CoreModel,AbstractUser):
user_type = models.IntegerField(
choices=USER_TYPE, default=0, verbose_name="用户类型", null=True, blank=True, help_text="用户类型"
)
post = models.ManyToManyField(to="Post",blank=True, verbose_name="关联岗位", db_constraint=False, help_text="关联岗位")
role = models.ManyToManyField(to="Role", blank=True,verbose_name="关联角色", db_constraint=False, help_text="关联角色")
post = models.ManyToManyField(to="Post", blank=True, verbose_name="关联岗位", db_constraint=False,
help_text="关联岗位")
role = models.ManyToManyField(to="Role", blank=True, verbose_name="关联角色", db_constraint=False,
help_text="关联角色")
dept = models.ForeignKey(
to="Dept",
verbose_name="所属部门",
@ -45,7 +51,8 @@ class Users(CoreModel,AbstractUser):
blank=True,
help_text="关联部门",
)
last_token = models.CharField(max_length=255,null=True,blank=True, verbose_name="最后一次登录Token", help_text="最后一次登录Token")
last_token = models.CharField(max_length=255, null=True, blank=True, verbose_name="最后一次登录Token",
help_text="最后一次登录Token")
def set_password(self, raw_password):
super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest())
@ -87,9 +94,11 @@ class Role(CoreModel):
(3, "全部数据权限"),
(4, "自定数据权限"),
)
data_range = models.IntegerField(default=0, choices=DATASCOPE_CHOICES, verbose_name="数据权限范围", help_text="数据权限范围")
data_range = models.IntegerField(default=0, choices=DATASCOPE_CHOICES, verbose_name="数据权限范围",
help_text="数据权限范围")
remark = models.TextField(verbose_name="备注", help_text="备注", null=True, blank=True)
dept = models.ManyToManyField(to="Dept", verbose_name="数据权限-关联部门", db_constraint=False, help_text="数据权限-关联部门")
dept = models.ManyToManyField(to="Dept", verbose_name="数据权限-关联部门", db_constraint=False,
help_text="数据权限-关联部门")
menu = models.ManyToManyField(to="Menu", verbose_name="关联菜单", db_constraint=False, help_text="关联菜单")
permission = models.ManyToManyField(
to="MenuButton", verbose_name="关联菜单的接口按钮", db_constraint=False, help_text="关联菜单的接口按钮"
@ -104,7 +113,8 @@ class Role(CoreModel):
class Dept(CoreModel):
name = models.CharField(max_length=64, verbose_name="部门名称", help_text="部门名称")
key = models.CharField(max_length=64, unique=True,null=True,blank=True, verbose_name="关联字符", help_text="关联字符")
key = models.CharField(max_length=64, unique=True, null=True, blank=True, verbose_name="关联字符",
help_text="关联字符")
sort = models.IntegerField(default=1, verbose_name="显示排序", help_text="显示排序")
owner = models.CharField(max_length=32, verbose_name="负责人", null=True, blank=True, help_text="负责人")
phone = models.CharField(max_length=32, verbose_name="联系电话", null=True, blank=True, help_text="联系电话")
@ -168,10 +178,12 @@ class Menu(CoreModel):
is_catalog = models.BooleanField(default=False, verbose_name="是否目录", help_text="是否目录")
web_path = models.CharField(max_length=128, verbose_name="路由地址", null=True, blank=True, help_text="路由地址")
component = models.CharField(max_length=128, verbose_name="组件地址", null=True, blank=True, help_text="组件地址")
component_name = models.CharField(max_length=50, verbose_name="组件名称", null=True, blank=True, help_text="组件名称")
component_name = models.CharField(max_length=50, verbose_name="组件名称", null=True, blank=True,
help_text="组件名称")
status = models.BooleanField(default=True, blank=True, verbose_name="菜单状态", help_text="菜单状态")
cache = models.BooleanField(default=False, blank=True, verbose_name="是否页面缓存", help_text="是否页面缓存")
visible = models.BooleanField(default=True, blank=True, verbose_name="侧边栏中是否显示", help_text="侧边栏中是否显示")
visible = models.BooleanField(default=True, blank=True, verbose_name="侧边栏中是否显示",
help_text="侧边栏中是否显示")
class Meta:
db_table = table_prefix + "system_menu"
@ -198,7 +210,8 @@ class MenuButton(CoreModel):
(2, "PUT"),
(3, "DELETE"),
)
method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True, help_text="接口请求方法")
method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True,
help_text="接口请求方法")
class Meta:
db_table = table_prefix + "system_menu_button"
@ -219,7 +232,8 @@ class Dictionary(CoreModel):
(7, "images"),
)
label = models.CharField(max_length=100, blank=True, null=True, verbose_name="字典名称", help_text="字典名称")
value = models.CharField(max_length=200, blank=True, null=True, verbose_name="字典编号", help_text="字典编号/实际值")
value = models.CharField(max_length=200, blank=True, null=True, verbose_name="字典编号",
help_text="字典编号/实际值")
parent = models.ForeignKey(
to="self",
related_name="sublist",
@ -232,7 +246,8 @@ class Dictionary(CoreModel):
)
type = models.IntegerField(choices=TYPE_LIST, default=0, verbose_name="数据值类型", help_text="数据值类型")
color = models.CharField(max_length=20, blank=True, null=True, verbose_name="颜色", help_text="颜色")
is_value = models.BooleanField(default=False, verbose_name="是否为value值", help_text="是否为value值,用来做具体值存放")
is_value = models.BooleanField(default=False, verbose_name="是否为value值",
help_text="是否为value值,用来做具体值存放")
status = models.BooleanField(default=True, verbose_name="状态", help_text="状态")
sort = models.IntegerField(default=1, verbose_name="显示排序", null=True, blank=True, help_text="显示排序")
remark = models.CharField(max_length=2000, blank=True, null=True, verbose_name="备注", help_text="备注")
@ -254,14 +269,20 @@ class Dictionary(CoreModel):
class OperationLog(CoreModel):
request_modular = models.CharField(max_length=64, verbose_name="请求模块", null=True, blank=True, help_text="请求模块")
request_path = models.CharField(max_length=400, verbose_name="请求地址", null=True, blank=True, help_text="请求地址")
request_modular = models.CharField(max_length=64, verbose_name="请求模块", null=True, blank=True,
help_text="请求模块")
request_path = models.CharField(max_length=400, verbose_name="请求地址", null=True, blank=True,
help_text="请求地址")
request_body = models.TextField(verbose_name="请求参数", null=True, blank=True, help_text="请求参数")
request_method = models.CharField(max_length=8, verbose_name="请求方式", null=True, blank=True, help_text="请求方式")
request_method = models.CharField(max_length=8, verbose_name="请求方式", null=True, blank=True,
help_text="请求方式")
request_msg = models.TextField(verbose_name="操作说明", null=True, blank=True, help_text="操作说明")
request_ip = models.CharField(max_length=32, verbose_name="请求ip地址", null=True, blank=True, help_text="请求ip地址")
request_browser = models.CharField(max_length=64, verbose_name="请求浏览器", null=True, blank=True, help_text="请求浏览器")
response_code = models.CharField(max_length=32, verbose_name="响应状态码", null=True, blank=True, help_text="响应状态码")
request_ip = models.CharField(max_length=32, verbose_name="请求ip地址", null=True, blank=True,
help_text="请求ip地址")
request_browser = models.CharField(max_length=64, verbose_name="请求浏览器", null=True, blank=True,
help_text="请求浏览器")
response_code = models.CharField(max_length=32, verbose_name="响应状态码", null=True, blank=True,
help_text="响应状态码")
request_os = models.CharField(max_length=64, verbose_name="操作系统", null=True, blank=True, help_text="操作系统")
json_result = models.TextField(verbose_name="返回信息", null=True, blank=True, help_text="返回信息")
status = models.BooleanField(default=False, verbose_name="响应状态", help_text="响应状态")
@ -276,12 +297,16 @@ class OperationLog(CoreModel):
def media_file_name(instance, filename):
h = instance.md5sum
basename, ext = os.path.splitext(filename)
return os.path.join("files", h[0:1], h[1:2], h + ext.lower())
return PurePosixPath("files", h[:1], h[1:2], h + ext.lower())
class FileList(CoreModel):
name = models.CharField(max_length=200, null=True, blank=True, verbose_name="名称", help_text="名称")
url = models.FileField(upload_to=media_file_name)
url = models.FileField(upload_to=media_file_name, null=True, blank=True,)
file_url = models.CharField(max_length=255, blank=True, verbose_name="文件地址", help_text="文件地址")
engine = models.CharField(max_length=100, default='local', blank=True, verbose_name="引擎", help_text="引擎")
mime_type = models.CharField(max_length=100, blank=True, verbose_name="Mime类型", help_text="Mime类型")
size = models.CharField(max_length=36, blank=True, verbose_name="文件大小", help_text="文件大小")
md5sum = models.CharField(max_length=36, blank=True, verbose_name="文件md5", help_text="文件md5")
def save(self, *args, **kwargs):
@ -290,6 +315,11 @@ class FileList(CoreModel):
for chunk in self.url.chunks():
md5.update(chunk)
self.md5sum = md5.hexdigest()
if not self.size:
self.size = self.url.size
if not self.file_url:
url = media_file_name(self,self.name)
self.file_url = f'media/{url}'
super(FileList, self).save(*args, **kwargs)
class Meta:
@ -302,7 +332,8 @@ class FileList(CoreModel):
class Area(CoreModel):
name = models.CharField(max_length=100, verbose_name="名称", help_text="名称")
code = models.CharField(max_length=20, verbose_name="地区编码", help_text="地区编码", unique=True, db_index=True)
level = models.BigIntegerField(verbose_name="地区层级(1省份 2城市 3区县 4乡级)", help_text="地区层级(1省份 2城市 3区县 4乡级)")
level = models.BigIntegerField(verbose_name="地区层级(1省份 2城市 3区县 4乡级)",
help_text="地区层级(1省份 2城市 3区县 4乡级)")
pinyin = models.CharField(max_length=255, verbose_name="拼音", help_text="拼音")
initials = models.CharField(max_length=20, verbose_name="首字母", help_text="首字母")
enable = models.BooleanField(default=True, verbose_name="是否启用", help_text="是否启用")
@ -335,8 +366,10 @@ class ApiWhiteList(CoreModel):
(2, "PUT"),
(3, "DELETE"),
)
method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True, help_text="接口请求方法")
enable_datasource = models.BooleanField(default=True, verbose_name="激活数据权限", help_text="激活数据权限", blank=True)
method = models.IntegerField(default=0, verbose_name="接口请求方法", null=True, blank=True,
help_text="接口请求方法")
enable_datasource = models.BooleanField(default=True, verbose_name="激活数据权限", help_text="激活数据权限",
blank=True)
class Meta:
db_table = table_prefix + "api_white_list"
@ -356,8 +389,8 @@ class SystemConfig(CoreModel):
help_text="父级",
)
title = models.CharField(max_length=50, verbose_name="标题", help_text="标题")
key = models.CharField(max_length=100, verbose_name="", help_text="", db_index=True)
value = models.JSONField(max_length=200, verbose_name="", help_text="", null=True, blank=True)
key = models.CharField(max_length=200, verbose_name="", help_text="", db_index=True)
value = models.JSONField(max_length=500, verbose_name="", help_text="", null=True, blank=True)
sort = models.IntegerField(default=0, verbose_name="排序", help_text="排序", blank=True)
status = models.BooleanField(default=True, verbose_name="启用状态", help_text="启用状态")
data_options = models.JSONField(verbose_name="数据options", help_text="数据options", null=True, blank=True)
@ -383,7 +416,7 @@ class SystemConfig(CoreModel):
choices=FORM_ITEM_TYPE_LIST, verbose_name="表单类型", help_text="表单类型", default=0, blank=True
)
rule = models.JSONField(null=True, blank=True, verbose_name="校验规则", help_text="校验规则")
placeholder = models.CharField(max_length=50, null=True, blank=True, verbose_name="提示信息", help_text="提示信息")
placeholder = models.CharField(max_length=100, null=True, blank=True, verbose_name="提示信息", help_text="提示信息")
setting = models.JSONField(null=True, blank=True, verbose_name="配置", help_text="配置")
class Meta:
@ -415,8 +448,16 @@ class SystemConfig(CoreModel):
class LoginLog(CoreModel):
LOGIN_TYPE_CHOICES = ((1, "普通登录"), (2, "微信扫码登录"), (3, "飞书扫码登录"), (4, "钉钉扫码登录"))
username = models.CharField(max_length=32, verbose_name="登录用户名", null=True, blank=True, help_text="登录用户名")
LOGIN_TYPE_CHOICES = (
(1, "普通登录"),
(2, "普通扫码登录"),
(3, "微信扫码登录"),
(4, "飞书扫码登录"),
(5, "钉钉扫码登录"),
(6, "短信登录")
)
username = models.CharField(max_length=150, verbose_name="登录用户名", null=True, blank=True,
help_text="登录用户名")
ip = models.CharField(max_length=32, verbose_name="登录ip", null=True, blank=True, help_text="登录ip")
agent = models.TextField(verbose_name="agent信息", null=True, blank=True, help_text="agent信息")
browser = models.CharField(max_length=200, verbose_name="浏览器名", null=True, blank=True, help_text="浏览器名")
@ -428,11 +469,13 @@ class LoginLog(CoreModel):
district = models.CharField(max_length=50, verbose_name="县区", null=True, blank=True, help_text="县区")
isp = models.CharField(max_length=50, verbose_name="运营商", null=True, blank=True, help_text="运营商")
area_code = models.CharField(max_length=50, verbose_name="区域代码", null=True, blank=True, help_text="区域代码")
country_english = models.CharField(max_length=50, verbose_name="英文全称", null=True, blank=True, help_text="英文全称")
country_english = models.CharField(max_length=50, verbose_name="英文全称", null=True, blank=True,
help_text="英文全称")
country_code = models.CharField(max_length=50, verbose_name="简称", null=True, blank=True, help_text="简称")
longitude = models.CharField(max_length=50, verbose_name="经度", null=True, blank=True, help_text="经度")
latitude = models.CharField(max_length=50, verbose_name="纬度", null=True, blank=True, help_text="纬度")
login_type = models.IntegerField(default=1, choices=LOGIN_TYPE_CHOICES, verbose_name="登录类型", help_text="登录类型")
login_type = models.IntegerField(default=1, choices=LOGIN_TYPE_CHOICES, verbose_name="登录类型",
help_text="登录类型")
class Meta:
db_table = table_prefix + "system_login_log"
@ -442,14 +485,16 @@ class LoginLog(CoreModel):
class MessageCenter(CoreModel):
title = models.CharField(max_length=100,verbose_name="标题",help_text="标题")
content = models.TextField(verbose_name="内容",help_text="内容")
target_type=models.IntegerField(default=0,verbose_name="目标类型",help_text="目标类型")
target_user = models.ManyToManyField(to=Users,related_name='user',through='MessageCenterTargetUser', through_fields=('messagecenter','users'),blank=True,verbose_name="目标用户",help_text="目标用户")
target_dept = models.ManyToManyField(to=Dept, blank=True, db_constraint=False,
verbose_name="目标部门", help_text="目标部门")
target_role = models.ManyToManyField(to=Role, blank=True, db_constraint=False,
verbose_name="目标角色", help_text="目标角色")
title = models.CharField(max_length=100, verbose_name="标题", help_text="标题")
content = models.TextField(verbose_name="内容", help_text="内容")
target_type = models.IntegerField(default=0, verbose_name="目标类型", help_text="目标类型")
target_user = models.ManyToManyField(to=Users, related_name='user', through='MessageCenterTargetUser',
through_fields=('messagecenter', 'users'), blank=True, verbose_name="目标用户",
help_text="目标用户")
target_dept = models.ManyToManyField(to=Dept, blank=True, db_constraint=False,
verbose_name="目标部门", help_text="目标部门")
target_role = models.ManyToManyField(to=Role, blank=True, db_constraint=False,
verbose_name="目标角色", help_text="目标角色")
class Meta:
db_table = table_prefix + "message_center"
@ -457,10 +502,13 @@ class MessageCenter(CoreModel):
verbose_name_plural = verbose_name
ordering = ("-create_datetime",)
class MessageCenterTargetUser(CoreModel):
users = models.ForeignKey(Users,related_name="target_user", on_delete=models.CASCADE,db_constraint=False,verbose_name="关联用户表",help_text="关联用户表")
messagecenter = models.ForeignKey(MessageCenter, on_delete=models.CASCADE,db_constraint=False,verbose_name="关联消息中心表",help_text="关联消息中心表")
is_read = models.BooleanField(default=False,blank=True,null=True,verbose_name="是否已读",help_text="是否已读")
users = models.ForeignKey(Users, related_name="target_user", on_delete=models.CASCADE, db_constraint=False,
verbose_name="关联用户表", help_text="关联用户表")
messagecenter = models.ForeignKey(MessageCenter, on_delete=models.CASCADE, db_constraint=False,
verbose_name="关联消息中心表", help_text="关联消息中心表")
is_read = models.BooleanField(default=False, blank=True, null=True, verbose_name="是否已读", help_text="是否已读")
class Meta:
db_table = table_prefix + "message_center_target_user"

View File

@ -4,6 +4,7 @@ from rest_framework import routers
from dvadmin.system.views.api_white_list import ApiWhiteListViewSet
from dvadmin.system.views.area import AreaViewSet
from dvadmin.system.views.clause import PrivacyView, TermsServiceView
from dvadmin.system.views.datav import DataVViewSet
from dvadmin.system.views.dept import DeptViewSet
from dvadmin.system.views.dictionary import DictionaryViewSet
from dvadmin.system.views.file_list import FileViewSet
@ -29,6 +30,7 @@ system_url.register(r'file', FileViewSet)
system_url.register(r'api_white_list', ApiWhiteListViewSet)
system_url.register(r'system_config', SystemConfigViewSet)
system_url.register(r'message_center', MessageCenterViewSet)
system_url.register(r'datav', DataVViewSet)
urlpatterns = [
path('system_config/save_content/', SystemConfigViewSet.as_view({'put': 'save_content'})),

View File

@ -0,0 +1,262 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2023/4/14 15:49
# @Author : harry
import datetime
import json
import re
import time
from django.db.models import Count, Sum, Q
from django.db.models.functions import TruncMonth, TruncDay
from django.utils import timezone
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import GenericViewSet
from conf.env import DATABASE_USER, DATABASE_NAME
from dvadmin.system.models import Users, LoginLog, FileList
from dvadmin.system.views.login_log import LoginLogSerializer
from dvadmin.utils.json_response import DetailResponse
from django.db import connection
from django.utils.timezone import now
from django.db.models import Count
from django.db.models.functions import TruncDate
from dvadmin.utils.string_util import format_bytes
def jx_timestamp():
cur_time = datetime.datetime.now()
a = datetime.datetime.strftime(cur_time, '%Y-%m-%d %H:%M:%S')
timeStamp = int(time.mktime(time.strptime(a, "%Y-%m-%d %H:%M:%S")))
timeArray = time.localtime(timeStamp)
otherStyleTime = time.strftime("%Y-%m-%d %H:%M:%S", timeArray)
return otherStyleTime
class DataVViewSet(GenericViewSet):
queryset = LoginLog.objects.all()
serializer_class = LoginLogSerializer
extra_filter_backends = []
ordering_fields = ['create_datetime']
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
def users_login_total(self, request):
"""
用户登录总数数据
:param request:
:return:
"""
login_total = LoginLog.objects.all().count()
return DetailResponse(data={"login_total": login_total}, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
def users_total(self, request):
"""
用户总数
:param request:
:return:
"""
users_total = Users.objects.all().count()
return DetailResponse(data={"users_total": users_total, }, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
def attachment_total(self, request):
"""
附件统计数据
:param request:
:return:
"""
count = FileList.objects.all().count()
data = FileList.objects.aggregate(sum_size=Sum('size'))
return DetailResponse(data={"count": count, "occupy_space": format_bytes(data.get('sum_size') or 0)}, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
def database_total(self, request):
"""
数据库统计数据
:param request:
:return:
"""
count = len(connection.introspection.table_names())
database_type = connection.settings_dict['ENGINE']
sql = None
if 'mysql' in database_type:
sql = "SELECT SUM(data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = DATABASE()"
elif 'postgres' in database_type or 'psqlextra' in database_type:
sql = """SELECT SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename))) AS size FROM pg_tables WHERE schemaname = current_schema();"""
elif 'oracle' in database_type:
sql = "SELECT SUM(bytes) AS size FROM user_segments"
elif 'microsoft' in database_type:
sql = "SELECT SUM(size) * 8 AS size FROM sys.database_files"
else:
space = 0
if sql:
with connection.cursor() as cursor:
try:
cursor.execute(sql)
result = cursor.fetchone()
space = result[0]
except Exception as e:
print(e)
space = '无权限'
return DetailResponse(data={"count": count, "space": format_bytes(space or 0)}, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
def registered_user(self, request):
"""
用户注册趋势
:param request:
:return:
"""
today = datetime.datetime.today()
seven_days_ago = today - datetime.timedelta(days=30)
users = Users.objects.filter(date_joined__gte=seven_days_ago).annotate(day=TruncDay('date_joined')).values(
'day').annotate(count=Count('id'))
result = []
for i in range(30):
date = (today - datetime.timedelta(days=i)).strftime('%Y-%m-%d')
count = 0
for user in users:
if user['day'] == date:
count = user['count']
break
result.append({'day': date, 'count': count})
# users_last_month = Users.objects.filter(date_joined__gte=last_month).annotate(day=TruncDate('date_joined')).values('day').annotate(count=Count('id'))
return DetailResponse(data={"registered_user_list": result}, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
def registered_user(self, request):
"""
用户注册趋势
:param request:
:return:
"""
day = 30
today = datetime.datetime.today()
seven_days_ago = today - datetime.timedelta(days=day)
users = Users.objects.filter(create_datetime__gte=seven_days_ago).annotate(
day=TruncDay('create_datetime')).values(
'day').annotate(count=Count('id')).order_by('-day')
result = []
data_dict = {ele.get('day').strftime('%Y-%m-%d'): ele.get('count') for ele in users}
for i in range(day):
date = (today - datetime.timedelta(days=i)).strftime('%Y-%m-%d')
result.append({'day': date, 'count': data_dict[date] if date in data_dict else 0})
result = sorted(result, key=lambda x: x['day'])
return DetailResponse(data={"registered_user_list": result}, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
def login_user(self, request):
"""
用户登录趋势
:param request:
:return:
"""
day = 30
today = datetime.datetime.today()
seven_days_ago = today - datetime.timedelta(days=day)
users = LoginLog.objects.filter(create_datetime__gte=seven_days_ago).annotate(
day=TruncDay('create_datetime')).values(
'day').annotate(count=Count('id')).order_by('-day')
result = []
data_dict = {ele.get('day').strftime('%Y-%m-%d'): ele.get('count') for ele in users}
for i in range(day):
date = (today - datetime.timedelta(days=i)).strftime('%Y-%m-%d')
result.append({'day': date, 'count': data_dict[date] if date in data_dict else 0})
result = sorted(result, key=lambda x: x['day'])
return DetailResponse(data={"login_user": result}, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
def users_active(self, request):
"""
用户新增活跃数据统计
:param request:
:return:
"""
today = datetime.date.today()
seven_days_ago = today - datetime.timedelta(days=6)
thirty_days_ago = today - datetime.timedelta(days=29)
today_users = Users.objects.filter(date_joined__date=today).count()
today_logins = Users.objects.filter(last_login__date=today).count()
three_days_users = Users.objects.filter(date_joined__gte=seven_days_ago).count()
seven_days_users = Users.objects.filter(date_joined__gte=thirty_days_ago).count()
seven_days_active = Users.objects.filter(last_login__gte=seven_days_ago).values('last_login').annotate(
count=Count('id', distinct=True)).count()
monthly_active = Users.objects.filter(last_login__gte=thirty_days_ago).values('last_login').annotate(
count=Count('id', distinct=True)).count()
data = {
'today_users': today_users,
'today_logins': today_logins,
'three_days': three_days_users,
'seven_days': seven_days_users,
'seven_days_active': seven_days_active,
'monthly_active': monthly_active
}
return DetailResponse(data=data, msg="获取成功")
@action(methods=["GET"], detail=False, permission_classes=[IsAuthenticated])
def login_region(self, request):
"""
登录用户区域分布
:param request:
:return:
"""
CHINA_PROVINCES = [
{'name': '北京', 'code': '110000'},
{'name': '天津', 'code': '120000'},
{'name': '河北', 'code': '130000'},
{'name': '山西', 'code': '140000'},
{'name': '内蒙古', 'code': '150000'},
{'name': '辽宁', 'code': '210000'},
{'name': '吉林', 'code': '220000'},
{'name': '黑龙江', 'code': '230000'},
{'name': '上海', 'code': '310000'},
{'name': '江苏', 'code': '320000'},
{'name': '浙江', 'code': '330000'},
{'name': '安徽', 'code': '340000'},
{'name': '福建', 'code': '350000'},
{'name': '江西', 'code': '360000'},
{'name': '山东', 'code': '370000'},
{'name': '河南', 'code': '410000'},
{'name': '湖北', 'code': '420000'},
{'name': '湖南', 'code': '430000'},
{'name': '广东', 'code': '440000'},
{'name': '广西', 'code': '450000'},
{'name': '海南', 'code': '460000'},
{'name': '重庆', 'code': '500000'},
{'name': '四川', 'code': '510000'},
{'name': '贵州', 'code': '520000'},
{'name': '云南', 'code': '530000'},
{'name': '西藏', 'code': '540000'},
{'name': '陕西', 'code': '610000'},
{'name': '甘肃', 'code': '620000'},
{'name': '青海', 'code': '630000'},
{'name': '宁夏', 'code': '640000'},
{'name': '新疆', 'code': '650000'},
{'name': '台湾', 'code': '710000'},
{'name': '香港', 'code': '810000'},
{'name': '澳门', 'code': '820000'},
{'name': '未知区域', 'code': '000000'},
]
provinces = [x['name'] for x in CHINA_PROVINCES]
day = 30
today = datetime.datetime.today()
seven_days_ago = today - datetime.timedelta(days=day)
province_data = LoginLog.objects.filter(create_datetime__gte=seven_days_ago).values('province').annotate(
count=Count('id')).order_by('-count')
province_dict = {p: 0 for p in provinces}
for ele in province_data:
if ele.get('province') in province_dict:
province_dict[ele.get('province')] += ele.get('count')
else:
province_dict['未知区域'] += ele.get('count')
data = [{'region': key, 'count': val} for key, val in province_dict.items()]
data = sorted(data, key=lambda x: x['count'], reverse=True)
return DetailResponse(data=data, msg="获取成功")

View File

@ -1,5 +1,9 @@
import hashlib
import mimetypes
from rest_framework import serializers
from application import dispatch
from dvadmin.system.models import FileList
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
@ -9,15 +13,49 @@ class FileSerializer(CustomModelSerializer):
url = serializers.SerializerMethodField(read_only=True)
def get_url(self, instance):
return 'media/' + str(instance.url)
# return 'media/' + str(instance.url)
return instance.file_url or (f'media/{str(instance.url)}')
class Meta:
model = FileList
fields = "__all__"
def create(self, validated_data):
validated_data['name'] = str(self.initial_data.get('file'))
validated_data['url'] = self.initial_data.get('file')
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine")
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup")
file = self.initial_data.get('file')
file_size = file.size
validated_data['name'] = file.name
validated_data['size'] = file_size
validated_data['md5sum'] = hashlib.md5().hexdigest()
validated_data['engine'] = file_engine
validated_data['mime_type'] = file.content_type
if file_backup:
validated_data['url'] = file
if file_engine =='oss':
from dvadmin_cloud_storage.views.aliyun import ali_oss_upload
file_path = ali_oss_upload(file)
if file_path:
validated_data['file_url'] = file_path
else:
raise ValueError("上传失败")
elif file_engine == 'cos':
from dvadmin_cloud_storage.views.tencent import tencent_cos_upload
file_path = tencent_cos_upload(file)
if file_path:
validated_data['file_url'] = file_path
else:
raise ValueError("上传失败")
else:
validated_data['url'] = file
# 审计字段
try:
request_user = self.request.user
validated_data['dept_belong_id'] = request_user.dept.id
validated_data['creator'] = request_user.id
validated_data['modifier'] = request_user.id
except:
pass
return super().create(validated_data)

View File

@ -70,6 +70,7 @@ class LoginSerializer(TokenObtainPairSerializer):
default_error_messages = {"no_active_account": _("账号/密码错误")}
def validate(self, attrs):
captcha = self.initial_data.get("captcha", None)
if dispatch.get_system_config_values("base.captcha_state"):
if captcha is None:

View File

@ -7,12 +7,15 @@
@Remark: 公共基础model类
"""
import uuid
from datetime import date, timedelta
from django.apps import apps
from django.db import models
from django.db import models, connection, ProgrammingError
from django.db.models import QuerySet
from application import settings
from application.dispatch import is_tenants_mode
table_prefix = settings.TABLE_PREFIX # 数据库表名前缀
@ -40,10 +43,17 @@ class SoftDeleteManager(models.Manager):
return SoftDeleteQuerySet(self.model, using=self._db).exclude(is_deleted=False)
return SoftDeleteQuerySet(self.model).exclude(is_deleted=True)
def get_by_natural_key(self,name):
def get_by_natural_key(self, name):
return SoftDeleteQuerySet(self.model).get(username=name)
def get_month_range(start_day, end_day):
months = (end_day.year - start_day.year) * 12 + end_day.month - start_day.month
month_range = ['%s-%s-01' % (start_day.year + mon // 12, str(mon % 12 + 1).zfill(2))
for mon in range(start_day.month - 1, start_day.month + months)]
return month_range
class SoftDeleteModel(models.Model):
"""
软删除模型
@ -73,10 +83,13 @@ class CoreModel(models.Model):
id = models.BigAutoField(primary_key=True, help_text="Id", verbose_name="Id")
description = models.CharField(max_length=255, verbose_name="描述", null=True, blank=True, help_text="描述")
creator = models.ForeignKey(to=settings.AUTH_USER_MODEL, related_query_name='creator_query', null=True,
verbose_name='创建人', help_text="创建人", on_delete=models.SET_NULL, db_constraint=False)
verbose_name='创建人', help_text="创建人", on_delete=models.SET_NULL,
db_constraint=False)
modifier = models.CharField(max_length=255, null=True, blank=True, help_text="修改人", verbose_name="修改人")
dept_belong_id = models.CharField(max_length=255, help_text="数据归属部门", null=True, blank=True, verbose_name="数据归属部门")
update_datetime = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间", verbose_name="修改时间")
dept_belong_id = models.CharField(max_length=255, help_text="数据归属部门", null=True, blank=True,
verbose_name="数据归属部门")
update_datetime = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间",
verbose_name="修改时间")
create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
verbose_name="创建时间")
@ -86,6 +99,113 @@ class CoreModel(models.Model):
verbose_name_plural = verbose_name
class AddPostgresPartitionedBase:
"""
pgsql表分表基类
"""
@classmethod
def add_hash_partition(cls, number=36):
"""
创建分区表
:return:
"""
if cls.PartitioningMeta.method != 'hash':
raise ProgrammingError("表分区错误,无法进行分区")
schema_editor = connection.schema_editor()
if is_tenants_mode():
schema_editor.sql_add_hash_partition = f'CREATE TABLE "{connection.tenant.schema_name}".%s PARTITION OF "{connection.tenant.schema_name}".%s FOR VALUES WITH (MODULUS %s, REMAINDER %s)'
for item in range(number):
try:
schema_editor.add_hash_partition(
model=cls,
name="_" + str(item),
modulus=number,
remainder=item,
)
except ProgrammingError as e:
print(f"{cls.__name__}分表失败:" + str(e).rstrip('\n'))
return
@classmethod
def add_range_day_partition(cls, day=7):
"""
按照创建时间""分表
:return:
"""
if cls.PartitioningMeta.method != 'range':
raise ProgrammingError("表分区错误,无法进行分区")
day_before = date.today().strftime("%Y-%m-%d")
schema_editor = connection.schema_editor()
if is_tenants_mode():
schema_editor.sql_add_range_partition = (
f'CREATE TABLE "{connection.tenant.schema_name}".%s PARTITION OF "{connection.tenant.schema_name}".%s FOR VALUES FROM (%s) TO (%s)'
)
for index in range(day):
try:
day_following = (date.today() + timedelta(days=index + 1)).strftime("%Y-%m-%d")
schema_editor.add_range_partition(
model=cls,
name=f"{day_before}_{day_following}",
from_values=day_before,
to_values=day_following,
)
day_before = day_following
except ProgrammingError as e:
print(f"{cls.__name__}分表失败:" + str(e).rstrip('\n'))
return
@classmethod
def add_range_month_partition(cls, start_date, end_date):
"""
按照创建时间""分表
:return:
"""
if cls.PartitioningMeta.method != 'range':
raise ProgrammingError("表分区错误,无法进行分区")
range_month_partition_list = get_month_range(start_date, end_date)
schema_editor = connection.schema_editor()
if is_tenants_mode():
schema_editor.sql_add_range_partition = (
f'CREATE TABLE "{connection.tenant.schema_name}".%s PARTITION OF "{connection.tenant.schema_name}".%s FOR VALUES FROM (%s) TO (%s)'
)
for index, ele in enumerate(range_month_partition_list):
if index == 0:
continue
try:
schema_editor.add_range_partition(
model=cls,
name=f"{range_month_partition_list[index - 1][:-3]}_{ele[:-3]}",
from_values=range_month_partition_list[index - 1],
to_values=ele,
)
except ProgrammingError as e:
print(f"{cls.__name__}分表失败:" + str(e).rstrip('\n'))
return
@classmethod
def add_list_partition(cls, unique_value):
"""
按照某个值进行分区
:param unique_value:
:return:
"""
if cls.PartitioningMeta.method != 'list':
raise ProgrammingError("表分区错误,无法进行分区")
schema_editor = connection.schema_editor()
if is_tenants_mode():
schema_editor.sql_add_list_partition = (
f'CREATE TABLE "{connection.tenant.schema_name}".%s PARTITION OF "{connection.tenant.schema_name}".%s FOR VALUES IN (%s)'
)
try:
schema_editor.add_list_partition(
model=cls,
name=f"_{unique_value}",
values=[unique_value],
)
except ProgrammingError as e:
print(f"{cls.__name__}分表失败:" + str(e).rstrip('\n'))
return
def get_all_models_objects(model_name=None):
@ -111,4 +231,4 @@ def get_all_models_objects(model_name=None):
settings.ALL_MODELS_OBJECTS.setdefault(item.__name__, {"table": table, "object": item})
if model_name:
return settings.ALL_MODELS_OBJECTS[model_name] or {}
return settings.ALL_MODELS_OBJECTS or {}
return settings.ALL_MODELS_OBJECTS or {}

View File

@ -202,7 +202,7 @@ def get_ip_analysis(ip):
return data
def save_login_log(request):
def save_login_log(request, login_type=1):
"""
保存登录日志
:return:
@ -214,6 +214,7 @@ def save_login_log(request):
analysis_data['agent'] = str(parse(request.META['HTTP_USER_AGENT']))
analysis_data['browser'] = get_browser(request)
analysis_data['os'] = get_os(request)
analysis_data['login_type'] = login_type
analysis_data['creator_id'] = request.user.id
analysis_data['dept_belong_id'] = getattr(request.user, 'dept_id', '')
LoginLog.objects.create(**analysis_data)

View File

@ -8,6 +8,7 @@
"""
import hashlib
import random
from decimal import Decimal
CHAR_SET = ("2", "3", "4", "5",
"6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H",
@ -40,3 +41,25 @@ def has_md5(str, salt='123456'):
md.update(str.encode())
res = md.hexdigest()
return res
def format_bytes(size, decimals=2):
"""
格式化字节大小
:param size:
:param decimals:
:return:
"""
if isinstance(size, (str)) and size.isnumeric():
size = int(size)
elif not isinstance(size, (int, float, Decimal)):
return size
if size == 0:
return "0 Bytes"
units = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
i = 0
while size >= 1024:
size /= 1024
i += 1
return f"{round(size, decimals)} {units[i]}"

View File

@ -8,8 +8,10 @@ django-comment-migrate==0.1.7
django-cors-headers==3.10.1
django-filter==22.1
django-ranged-response==0.2.0
django-redis==5.2.0
django-restql==0.15.3
django-simple-captcha==0.5.17
django-tenants==3.4.8
django-timezone-field==4.2.3
djangorestframework==3.14.0
djangorestframework-simplejwt==5.2.2

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "django-vue-admin",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}

View File

@ -1,6 +1,6 @@
{
"name": "django-vue-admin",
"version": "2.1.2",
"version": "2.1.3",
"scripts": {
"serve": "vue-cli-service serve --open",
"start": "npm run serve",
@ -23,7 +23,7 @@
"d2-crud-x": "^2.17.9",
"d2p-extends": "^2.17.9",
"dayjs": "^1.8.17",
"echarts": "^5.1.2",
"echarts": "^5.4.2",
"el-phone-number-input": "^1.1.5",
"element-ui": "^2.15.5",
"faker": "^4.1.0",
@ -40,7 +40,9 @@
"screenfull": "^5.0.2",
"sortablejs": "^1.10.1",
"ua-parser-js": "^0.7.20",
"viser-vue": "^2.4.8",
"vue": "2.7.14",
"vue-echarts": "^6.5.4",
"vue-grid-layout": "^2.4.0",
"vue-i18n": "^8.15.1",
"vue-infinite-scroll": "^2.0.2",

View File

@ -172,5 +172,22 @@ util.ArrayToTree = function (rootList, parentValue, parentName, list) {
}
return list
}
// 格式化字节大小
util.formatBytes = function (bytes, decimals = 2) {
if (isNaN(bytes)) {
return bytes
}
if (bytes === 0) {
return '0 Bytes'
}
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}
export default util

View File

@ -36,13 +36,15 @@ import md5 from 'js-md5'
import websocket from '@/api/websocket'
import util from '@/libs/util'
// 引入echarts
import * as echarts from 'echarts' // 注册echarts组件
// 核心插件
Vue.use(d2Admin)
Vue.use(VXETable)
Vue.prototype.$md5 = md5
Vue.prototype.$util = util
Vue.prototype.$websocket = websocket
Vue.prototype.$echarts = echarts
new Vue({
router,
store,

View File

@ -1,6 +1,6 @@
<template>
<el-card shadow="hover" header="关于项目" class="card-view" :style="{backgroundColor:randomColor()}">
<p>基于RBAC模型的权限控制的一整套基础开发平台前后端分离后端采用 django+django-rest-framework前端采用
<el-card shadow="hover" :header="config?.showHeader?.value ? '关于项目' : ''" class="card-view" :style="{backgroundColor:randomColor(),color: config?.fontColor?.value}">
<p :style="{color: config?.fontColor?.value}">基于RBAC模型的权限控制的一整套基础开发平台前后端分离后端采用 django+django-rest-framework前端采用
vue+ElementUI+d2-crud-plus如果喜欢就点个星星支持一下
<a href='https://gitee.com/liqianglog/django-vue-admin'>
<img src='https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark' alt='star'
@ -12,20 +12,32 @@
<script>
export default {
sort: 9,
title: '关于项目',
name: 'about',
icon: 'el-icon-setting',
description: '点个星星支持一下',
height: 20,
width: 8,
minH: 10,
minW: 2,
width: 16,
isResizable: true,
config: {
showHeader: {
label: '显示头部信息',
type: 'boot',
value: true,
placeholder: '颜色为空则随机变换颜色'
},
color: {
label: '背景颜色',
type: 'color',
value: '',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: '',
placeholder: '请选择字体颜色'
}
},
props: {
@ -43,7 +55,7 @@ export default {
if (this.config?.color?.value) {
return this.config.color.value
}
return this.color || this.$util.randomColor()
return this.$util.randomColor()
}
}
}
@ -51,11 +63,11 @@ export default {
<style scoped lang="scss">
.card-view {
color: #FFFFFF;
color: $color-primary;
p {
font-size: 0.7em;
color: #FFFFFF;
font-size: 0.8em;
color: $color-primary;
}
}
.el-card{

View File

@ -0,0 +1,149 @@
<template>
<el-card
class="card-view"
:style="{ backgroundColor: randomColor() }"
shadow="always"
>
<div :style="{color: config?.fontColor?.value}">
<div>
<div class="card-content-label">附件统计</div>
<i class="real-time">实时</i>
</div>
<div class="absolute-left">
<div class="card-content">
<div class="card-content-value">{{ count }}</div>
<div class="el-icon-document-copy">
附件数量
</div>
</div>
</div>
<div class="absolute-right">
<div class="card-content-time">
<div class="attachment-value">{{ occupy_space }}</div>
<div class="el-icon-s-flag">
附件大小
</div>
</div>
</div>
</div>
</el-card>
</template>
<script>
import { request } from '@/api/service'
export default {
sort: 3,
title: '附件统计',
name: 'attachmentTotal',
icon: 'el-icon-s-order',
description: '总附件数以及附件占用大小',
height: 14,
width: 16,
isResizable: true,
config: {
color: {
label: '背景颜色',
type: 'color',
value: '',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: '',
placeholder: '请选择字体颜色'
}
},
props: {
config: {
type: Object,
required: false
}
},
data () {
return {
count: '',
occupy_space: ''
}
},
methods: {
initGet () {
request({
url: '/api/system/datav/attachment_total/'
}).then((res) => {
this.count = res.data.count
this.occupy_space = this.$util.formatBytes(res.data.occupy_space)
})
},
//
randomColor () {
if (this.config?.color?.value) {
return this.config.color.value
}
return this.$util.randomColor()
}
},
mounted () {
this.initGet()
}
}
</script>
<style scoped lang="scss">
.card-view {
//border-radius: 10px;
color: $color-primary;
.card-content {
.card-content-label {
font-size: 0.8em;
}
.card-content-value {
margin-top: 5px;
font-size: 1.5em;
font-weight: bold;
}
}
.attachment-value {
margin-top: 5px;
font-size: 1.5em;
font-weight: bold;
}
.el-icon-document-copy {
font-size: 12px;
}
.el-icon-s-flag {
font-size: 12px;
}
}
.real-time {
background: rgb(53, 59, 86);
color: #ffffff;
font-size: 14px;
font-style: normal;
padding: 0 7px 0 7px;
border-radius: 4px;
position: absolute;
right: 20px;
top: 20px;
}
.el-card {
height: 100%;
}
.absolute-right {
position: absolute;
right: 30px;
}
.absolute-left {
position: absolute;
}
</style>

View File

@ -6,14 +6,13 @@
<script>
export default {
sort: 8,
title: '宣传图',
name: 'dashboardImg',
icon: 'el-icon-medal',
description: '用于展示各种图片宣传页',
height: 10,
width: 8,
minH: 10,
minW: 1,
maxW: 24,
maxH: 100,
width: 16,
isResizable: true,
config: {
src: {
@ -37,7 +36,6 @@ export default {
required: false
}
},
description: '用于展示各种图片宣传页',
data () {
return {
}

View File

@ -0,0 +1,146 @@
<template>
<el-card
class="card-view"
:style="{ backgroundColor: randomColor() }"
shadow="always"
>
<div :style="{color: config?.fontColor?.value}">
<div>
<div class="card-content-label">数据库统计</div>
<i class="real-time">实时</i>
</div>
<div class="absolute-left">
<div class="card-content">
<div class="card-content-value">{{ count }}</div>
<div class="el-icon-coin">
数据库数量
</div>
</div>
</div>
<div class="absolute-right">
<div class="card-content-time">
<div class="attachment-value">{{ space }}</div>
<div class="el-icon-s-flag">
占用空间
</div>
</div>
</div>
</div>
</el-card>
</template>
<script>
import { request } from '@/api/service'
export default {
sort: 4,
title: '数据库统计',
name: 'databaseTotal',
icon: 'el-icon-coin',
description: '数据库统计',
height: 14,
width: 16,
isResizable: true,
data () {
return {
count: '',
space: ''
}
},
config: {
color: {
label: '背景颜色',
type: 'color',
value: '',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: '',
placeholder: '请选择字体颜色'
}
},
props: {
config: {
type: Object,
required: false
}
},
methods: {
initGet () {
request({
url: '/api/system/datav/database_total/'
}).then((res) => {
this.count = res.data.count
this.space = this.$util.formatBytes(res.data.space)
})
},
randomColor () {
if (this.config?.color?.value) {
return this.config.color.value
}
return this.$util.randomColor()
}
},
mounted () {
this.initGet()
}
}
</script>
<style scoped lang="scss">
.card-view {
//border-radius: 10px;
color: $color-primary;
.card-content {
.card-content-label {
font-size: 0.8em;
}
.card-content-value {
margin-top: 5px;
font-size: 1.5em;
font-weight: bold;
}
}
.attachment-value {
margin-top: 5px;
font-size: 1.5em;
font-weight: bold;
}
.el-icon-coin {
font-size: 12px;
}
.el-icon-s-flag {
font-size: 12px;
}
}
.real-time {
background: rgb(53, 59, 86);
color: #ffffff;
font-size: 14px;
font-style: normal;
padding: 0 7px 0 7px;
border-radius: 4px;
position: absolute;
right: 20px;
top: 20px;
}
.el-card {
height: 100%;
}
.absolute-right{
position: absolute;
right: 30px;
}
.absolute-left{
position: absolute;
}
</style>

View File

@ -0,0 +1,205 @@
<template>
<el-card
class="card-view"
:style="{
backgroundColor: randomColor(),
}"
>
<div id="region" :style="{width: pxData.wpx+'px',height: pxData.hpx+'px'}"></div>
</el-card>
</template>
<script>
import { request } from '@/api/service'
export default {
sort: 7,
title: '登录区域分布',
name: 'loginRegion',
icon: 'el-icon-s-data',
description: '登录区域分布详情',
height: 28,
width: 20,
isResizable: true,
props: {
pxData: {
type: Object,
require: false,
default: () => ({
wpx: 0,
hpx: 0
})
}
},
watch: {
pxData: {
handler () {
// eslint-disable-next-line no-unused-expressions
this.myChart?.resize({ width: this.pxData.wpx, height: this.pxData.hpx })
},
immediate: true,
deep: true
}
},
data () {
this.myChart = null
return {
data: []
}
},
methods: {
initGet () {
request({
url: '/api/system/datav/login_region/'
}).then((res) => {
this.data = res.data
this.drawLine(this.data)
})
},
//
randomColor () {
const color = ['#fffff']
const ran = Math.floor(Math.random() * 4)
return color[ran]
},
drawLine () {
// domecharts
//
const xAxisData = this.data.map(item => item.region)
const seriesData = this.data.map(item => item.count)
const option = {
title: {
text: '登录区域分布',
textStyle: {
color: '#666666',
fontSize: 14,
fontWeight: '600'
},
left: 'left'
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
textStyle: {
color: '#666'
},
axisPointer: {
lineStyle: {
color: '#999',
type: 'dotted',
width: 1
}
},
formatter: params => {
const param = params[0]
return `<div style="padding: 8px;"><div style="color: #333;">${param.name}</div><div style="color: #FFA500;">${param.value} 次</div></div>`
}
},
legend: {
data: ['登录区域分布'],
textStyle: {
color: '#666',
fontSize: 12
}
},
grid: {
top: 40,
left: 40,
right: 65,
bottom: 75
},
xAxis: {
data: xAxisData,
boundaryGap: true,
axisLine: {
lineStyle: {
color: '#aaa',
width: 1
}
},
axisLabel: {
interval: '0',
maxInterval: 1,
rotate: 0,
formatter: function (value) {
return value.split('').join('\n')
},
textStyle: {
color: '#333',
fontSize: 10
}
}
},
yAxis: {
axisLine: {
lineStyle: {
color: '#aaa',
width: 1
}
},
axisLabel: {
textStyle: {
color: '#333',
fontSize: 12
}
},
splitLine: {
lineStyle: {
color: '#ddd',
type: 'dotted',
width: 1
}
}
},
series: [
{
name: '用户注册数',
type: 'bar',
data: seriesData,
barWidth: 16,
barGap: 0,
barCategoryGap: '20%',
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(0, 128, 255, 1)'
},
{
offset: 1,
color: 'rgba(0, 128, 255, 0.2)'
}
]
}
}
}
]
}
this.myChart.setOption(option)
}
},
mounted () {
this.myChart = this.$echarts.init(document.getElementById('region'))
this.initGet()
this.drawLine()
}
}
</script>
<style scoped lang="scss">
.card-view {
//border-radius: 10px;
color: $color-primary;
}
.el-card {
height: 100%;
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<el-card class="card-view" :style="{backgroundColor:randomColor(),color: config?.fontColor?.value}" shadow="always">
<div>
<el-row type="flex" justify="space-around" style="padding:10px">
<el-col :span="12">
<div class="card-content">
<div class="card-content-label">登录次数</div>
<div class="card-content-value">{{ loginTotal }}</div>
</div>
</el-col>
<el-col :span="6" :offset="6" style="text-align: right;">
<i class="el-icon-user-solid" size="48px"></i>
</el-col>
</el-row>
</div>
</el-card>
</template>
<script>
import { request } from '@/api/service'
export default {
sort: 2,
title: '登录总次数',
name: 'loginTotal',
icon: 'el-icon-user-solid',
description: '用户登录平台总次数',
height: 14,
width: 16,
isResizable: true,
config: {
color: {
label: '背景颜色',
type: 'color',
value: '',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: '',
placeholder: '请选择字体颜色'
}
},
props: {
config: {
type: Object,
required: false
}
},
data () {
return {
loginTotal: ''
}
},
methods: {
initGet () {
request({
url: '/api/system/datav/users_login_total/'
}).then((res) => {
this.loginTotal = res.data.login_total
})
},
//
randomColor () {
if (this.config?.color?.value) {
return this.config.color.value
}
return this.$util.randomColor()
}
},
mounted () {
this.initGet()
}
}
</script>
<style scoped lang="scss">
.card-view {
//border-radius: 10px;
color: $color-primary;
.card-content {
.card-content-label {
font-size: 1em;
}
.card-content-value {
margin-top: 10px;
font-size: 1.5em;
font-weight: bold;
}
}
}
.el-icon-user-solid {
font-size: 30px;
}
.el-card {
height: 100%;
}
</style>

View File

@ -0,0 +1,201 @@
<template>
<el-card
class="card-view"
:style="{
backgroundColor: randomColor(),
}"
>
<div id="main" :style="{width: pxData.wpx+'px',height: pxData.hpx+'px'}"></div>
</el-card>
</template>
<script>
import { request } from '@/api/service'
export default {
sort: 7,
title: '注册用户趋势',
name: 'registeredUser',
icon: 'el-icon-s-data',
description: '用户注册',
height: 28,
width: 20,
isResizable: true,
props: {
pxData: {
type: Object,
require: false,
default: () => ({
wpx: 0,
hpx: 0
})
}
},
watch: {
pxData: {
handler () {
// eslint-disable-next-line no-unused-expressions
this.myChart?.resize({ width: this.pxData.wpx, height: this.pxData.hpx })
},
immediate: true,
deep: true
}
},
data () {
this.myChart = null
return {
data: []
}
},
methods: {
initGet () {
request({
url: '/api/system/datav/registered_user/'
}).then((res) => {
this.data = res.data.registered_user_list
this.drawLine(this.data)
})
},
//
randomColor () {
const color = ['#fffff']
const ran = Math.floor(Math.random() * 4)
return color[ran]
},
drawLine () {
// domecharts
//
const xAxisData = this.data.map(item => item.day)
const seriesData = this.data.map(item => item.count)
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
textStyle: {
color: '#666'
},
axisPointer: {
lineStyle: {
color: '#999',
type: 'dotted',
width: 1
}
},
formatter: params => {
const param = params[0]
return `<div style="padding: 8px;"><div style="color: #333;">${param.name}</div><div style="color: #FFA500;">${param.value} 人</div></div>`
}
},
legend: {
data: ['用户注册数'],
textStyle: {
color: '#666',
fontSize: 12
}
},
grid: {
top: 40,
left: 40,
right: 65,
bottom: 60
},
xAxis: {
data: xAxisData,
boundaryGap: false,
axisLine: {
lineStyle: {
color: '#aaa',
width: 1
}
},
axisLabel: {
interval: 'auto',
maxInterval: 1,
rotate: 0,
textStyle: {
color: '#333',
fontSize: 12
}
}
},
yAxis: {
axisLine: {
lineStyle: {
color: '#aaa',
width: 1
}
},
axisLabel: {
textStyle: {
color: '#333',
fontSize: 12
}
},
splitLine: {
lineStyle: {
color: '#ddd',
type: 'dotted',
width: 1
}
}
},
series: [
{
name: '用户注册数',
type: 'line',
data: seriesData,
symbol: 'circle',
symbolSize: 6,
smooth: true,
lineStyle: {
color: 'rgba(38,204,164, 0.8)',
width: 2
},
itemStyle: {
color: 'rgba(98,206,178, 0.8)',
borderColor: 'rgba(38,204,164, 1)',
borderWidth: 1
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(140,189,250, 0.8)'
},
{
offset: 1,
color: 'rgba(0, 128, 255, 0)'
}
]
}
}
}
]
}
this.myChart.setOption(option)
}
},
mounted () {
this.myChart = this.$echarts.init(document.getElementById('main'))
this.initGet()
this.drawLine()
}
}
</script>
<style scoped lang="scss">
.card-view {
//border-radius: 10px;
color: $color-primary;
}
.el-card {
height: 100%;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<el-card shadow="hover" header="时钟" class="item-background">
<el-card shadow="hover" class="card-view" :style="{backgroundColor: randomColor(),color: config?.fontColor?.value}">
<div class="time">
<h2>{{ time }}</h2>
<p>{{ day }}</p>
@ -11,14 +11,34 @@
import dayjs from 'dayjs'
export default {
sort: 12,
title: '时钟',
name: 'myTime',
icon: 'el-icon-alarm-clock',
description: '演示部件效果',
height: 20,
minH: 10,
width: 8,
minW: 4,
height: 14,
width: 16,
isResizable: true,
config: {
color: {
label: '背景颜色',
type: 'color',
value: '',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: '',
placeholder: '请选择字体颜色'
}
},
props: {
config: {
type: Object,
required: false
}
},
data () {
return {
time: '',
@ -35,15 +55,21 @@ export default {
showTime () {
this.time = dayjs().format('HH:mm:ss')
this.day = dayjs().format('YYYY年MM月DD日')
},
//
randomColor () {
if (this.config?.color?.value) {
return this.config.color.value
}
return 'linear-gradient(to right, #8E54E9, #4776E6)'
}
}
}
</script>
<style scoped>
.item-background {
background: linear-gradient(to right, #8E54E9, #4776E6);
color: #fff;
<style scoped lang="scss">
.card-view {
color: $color-primary;
}
.time h2 {

View File

@ -0,0 +1,212 @@
<template>
<el-card
class="card-view"
:style="{
backgroundColor: randomColor(),
}"
>
<!-- shadow="always" -->
<div id="myChart" :style="{width: pxData.wpx+'px',height: pxData.hpx+'px'}"></div>
</el-card>
</template>
<script>
import { request } from '@/api/service'
export default {
sort: 6,
title: '用户登录趋势',
name: 'userLogin',
icon: 'el-icon-s-data',
description: '用户登陆',
height: 28,
width: 20,
isResizable: true,
props: {
pxData: {
type: Object,
require: false,
default: () => ({
wpx: 0,
hpx: 0
})
}
},
watch: {
pxData: {
handler () {
// eslint-disable-next-line no-unused-expressions
this.myChart?.resize({ width: this.pxData.wpx, height: this.pxData.hpx })
},
immediate: true,
deep: true
}
},
data () {
this.myChart = null
return {
data: [],
radio: '7'
}
},
methods: {
initGet () {
request({
url: '/api/system/datav/login_user/'
}).then((res) => {
this.data = res.data.login_user
this.drawLine(this.data)
})
},
//
randomColor () {
const color = ['#fffff']
const ran = Math.floor(Math.random() * 4)
return color[ran]
},
drawLine () {
// domecharts
//
const xAxisData = this.data.map(item => item.day)
const seriesData = this.data.map(item => item.count)
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
textStyle: {
color: '#666'
},
axisPointer: {
lineStyle: {
color: '#999',
type: 'dotted',
width: 1
}
},
formatter: params => {
const param = params[0]
return `<div style="padding: 8px;"><div style="color: #333;">${param.name}</div><div style="color: #FFA500;">${param.value} 次</div></div>`
}
},
legend: {
data: ['用户登陆数'],
textStyle: {
color: '#666',
fontSize: 12
}
},
grid: {
top: 40,
left: 40,
right: 65,
bottom: 60
},
xAxis: {
data: xAxisData,
boundaryGap: false,
axisLine: {
lineStyle: {
color: '#aaa',
width: 1
}
},
axisLabel: {
interval: 'auto',
maxInterval: 1,
rotate: 0,
textStyle: {
color: '#333',
fontSize: 12
}
}
},
yAxis: {
axisLine: {
lineStyle: {
color: '#aaa',
width: 1
}
},
axisLabel: {
textStyle: {
color: '#333',
fontSize: 12
}
},
splitLine: {
lineStyle: {
color: '#ddd',
type: 'dotted',
width: 1
}
}
},
series: [
{
name: '用户登陆数',
type: 'line',
data: seriesData,
symbol: 'circle',
smooth: true,
symbolSize: 6,
lineStyle: {
color: 'rgba(0, 128, 255, 0.8)',
width: 2
},
itemStyle: {
color: 'rgba(0, 128, 255, 0.8)',
borderColor: 'rgba(0, 128, 255, 1)',
borderWidth: 1
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(140,189,250, 0.8)'
},
{
offset: 1,
color: 'rgba(0, 128, 255, 0)'
}
]
}
}
}
]
}
this.myChart.setOption(option)
}
},
mounted () {
this.myChart = this.$echarts.init(document.getElementById('myChart'))
this.initGet()
this.drawLine()
}
}
</script>
<style scoped lang="scss">
.card-view {
//border-radius: 10px;
color: $color-primary;
}
.el-card{
height: 100%;
}
::v-deep .el-card__body {
width: 100%;
height: 100%;
}
.el-radio-button__inner {
border-radius: 20px;
}
</style>

View File

@ -0,0 +1,198 @@
<template>
<el-card class="card-view" shadow="always"
:style="{backgroundColor:randomColor(),color: config?.fontColor?.value}">
<div style="display:flex;flex: 1; flex-wrap: wrap;justify-content: space-between; margin-top: 10px;"
:style="{color: config?.fontColor?.value}">
<div style="flex: 1; min-width: 180px;max-width:180px;height: 80px; display: flex;">
<el-col :span="4" class="lightgreen-box">
<div class="underline">
<i class="el-icon-s-promotion"></i>
</div>
</el-col>
<el-col :span="20" class="orange-box">
<div class="enroll-time">
<div class="enroll-number"><h3>{{ data.today_users || 0 }}</h3>
</div>
<div class="enroll-text">今日注册
</div>
</div>
</el-col>
</div>
<div style="flex: 1;min-width: 180px;max-width:180px;height: 80px; display: flex;">
<el-col :span="4" class="lightgreen-box">
<div class="underline">
<i class="el-icon-chat-line-square"></i>
</div>
</el-col>
<el-col :span="20" class="orange-box">
<div class="enroll-time">
<div class="enroll-number"><h3>{{ data.today_logins || 0 }}</h3>
</div>
<div class="enroll-text">今日登录
</div>
</div>
</el-col>
</div>
<div style="flex: 1;min-width: 180px;max-width:180px;height: 80px; display: flex;">
<el-col :span="4" class="lightgreen-box">
<div class="underline">
<i class="el-icon-date"></i>
</div>
</el-col>
<el-col :span="20" class="orange-box">
<div class="enroll-time">
<div class="enroll-number"><h3>{{ data.three_days || 0 }}</h3>
</div>
<div class="enroll-text">三日新增
</div>
</div>
</el-col>
</div>
<div style="flex: 1;min-width: 180px;max-width:180px;height: 80px; display: flex;">
<el-col :span="4" class="lightgreen-box">
<div class="underline">
<i class="el-icon-user-solid"></i>
</div>
</el-col>
<el-col :span="20" class="orange-box">
<div class="enroll-time">
<div class="enroll-number"><h3>{{ data.seven_days || 0 }}</h3>
</div>
<div class="enroll-text">七日活跃
</div>
</div>
</el-col>
</div>
<div style="flex: 1;min-width: 180px;max-width:180px;height: 80px;display: flex;">
<el-col :span="4" class="lightgreen-box">
<div class="underline">
<i class="el-icon-folder-add"></i>
</div>
</el-col>
<el-col :span="20" class="orange-box">
<div class="enroll-time">
<div class="enroll-number"><h3>{{ data.seven_days_active || 0 }}</h3>
</div>
<div class="enroll-text">七日新增
</div>
</div>
</el-col>
</div>
<div style="flex: 1;min-width: 180px;max-width:180px;height: 80px; display: flex;">
<el-col :span="4" class="lightgreen-box">
<div class="underline">
<i class="el-icon-user"></i>
</div>
</el-col>
<el-col :span="20" class="orange-box">
<div class="enroll-time">
<div class="enroll-number"><h3>{{ data.monthly_active || 0 }}</h3>
</div>
<div class="enroll-text">月活跃
</div>
</div>
</el-col>
</div>
</div>
</el-card>
</template>
<script>
import { request } from '@/api/service'
export default {
sort: 5,
title: '用户新增统计',
name: 'usersActive',
icon: 'el-icon-user-solid',
description: '用户新增以及活跃统计数据',
height: 18,
width: 20,
isResizable: true,
config: {
color: {
label: '背景颜色',
type: 'color',
value: '',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: '',
placeholder: '请选择字体颜色'
}
},
props: {
config: {
type: Object,
required: false
}
},
data () {
return {
data: {}
}
},
mounted () {
this.initGet()
},
methods: {
initGet () {
request({
url: '/api/system/datav/users_active/'
}).then((res) => {
this.data = res.data
})
},
//
randomColor () {
const color = [
'#fffff'
]
const ran = Math.floor(Math.random() * 4)
return color[ran]
}
}
}
</script>
<style scoped lang="scss">
.card-view {
// border-radius: 10px;
color: $color-primary;
}
.enroll-number{
color: $color-primary;
}
h3 {
margin: 0;
}
.lightgreen-box {
border-bottom: 2px solid;
height: 60px;
margin-bottom: 10px;
}
.underline i {
font-size: 30px;
}
.orange-box {
height: 60px;
color: black;
border-bottom: 2px solid rgb(242, 242, 242);
}
.enroll-time {
margin-left: 10px;
}
.enroll-text {
color: rgb(138, 138, 138);
}
.el-card {
height: 100%;
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<el-card class="card-view" :style="{backgroundColor:randomColor(),color: config?.fontColor?.value}" shadow="always">
<div>
<el-row type="flex" justify="space-around" style="padding:10px">
<el-col :span="12">
<div class="card-content">
<div class="card-content-label">用户总数</div>
<div class="card-content-value">{{ usersTotal }}</div>
</div>
</el-col>
<el-col :span="6" :offset="6" style="text-align: right;">
<i class="el-icon-user-solid" size="48px"></i>
</el-col>
</el-row>
</div>
</el-card>
</template>
<script>
import { request } from '@/api/service'
export default {
sort: 1,
title: '用户总数',
icon: 'el-icon-user-solid',
description: '平台总会员数',
name: 'usersTotal',
height: 14,
width: 16,
isResizable: true,
config: {
color: {
label: '背景颜色',
type: 'color',
value: '',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: '',
placeholder: '请选择字体颜色'
}
},
props: {
config: {
type: Object,
required: false
}
},
data () {
return {
usersTotal: ''
}
},
methods: {
initGet () {
request({
url: '/api/system/datav/users_total/'
}).then((res) => {
this.usersTotal = res.data.users_total
})
},
//
randomColor () {
if (this.config?.color?.value) {
return this.config.color.value
}
return this.$util.randomColor()
}
},
mounted () {
this.initGet()
}
}
</script>
<style scoped lang="scss">
.card-view {
//border-radius: 10px;
color: $color-primary;
.card-content {
.card-content-label {
font-size: 1em;
}
.card-content-value {
margin-top: 10px;
font-size: 1.5em;
font-weight: bold;
}
}
}
.el-icon-user-solid {
font-size: 30px;
}
.el-card {
height: 100%;
}
</style>

View File

@ -1,6 +1,11 @@
<template>
<el-card shadow="hover" class="card-view" :style="{backgroundColor:randomColor()}" header="版本信息">
<div style="text-align: center;color: #FFFFFF">
<el-card
shadow="hover"
:header="config?.showHeader?.value ? '版本信息' : ''"
class="card-view"
:style="{backgroundColor:randomColor(),color: config?.fontColor?.value}"
>
<div style="text-align: center;">
<h2 style="margin-top: 5px;">{{ title }}</h2>
<p style="margin-top: 5px;">最新版本 {{ ver }}</p>
</div>
@ -11,14 +16,40 @@
import { mapState } from 'vuex'
export default {
sort: 10,
title: '版本信息',
name: 'ver',
icon: 'el-icon-monitor',
description: '当前项目版本信息',
height: 20,
minH: 10,
width: 8,
minW: 4,
height: 14,
width: 16,
isResizable: true,
config: {
showHeader: {
label: '显示头部信息',
type: 'boot',
value: true,
placeholder: '颜色为空则随机变换颜色'
},
color: {
label: '背景颜色',
type: 'color',
value: '',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: '',
placeholder: '请选择字体颜色'
}
},
props: {
config: {
type: Object,
required: false
}
},
data () {
return {
ver: 'loading...',
@ -38,33 +69,32 @@ export default {
this.ver = `v${process.env.VUE_APP_VERSION}` || 'v2.1.1'
this.title = this.siteName || process.env.VUE_APP_TITLE
},
golog () {
window.open('https://gitee.com/liqianglog/django-vue-admin/releases')
},
gogit () {
window.open('https://gitee.com/liqianglog/django-vue-admin')
},
//
randomColor () {
return this.color || this.$util.randomColor()
if (this.config?.color?.value) {
return this.config.color.value
}
return this.$util.randomColor()
}
}
}
</script>
<style scoped lang="scss">
.card-view{
color: #FFFFFF;
.card-view {
color: $color-primary;
//background: rgb(80,168,244);
//box-shadow: 1px 6px 8px 2px rgba(80,168,244,0.2);
.card-content{
.card-content {
//text-align: center;
}
}
::v-deep .el-card__body {
height: 110px;
}
.el-card{
.el-card {
height: 100%;
}
</style>

View File

@ -1,5 +1,9 @@
<template>
<el-card shadow="hover" header="欢迎" style="background: linear-gradient(150deg, #3b88ec 0%, #accaff 100%);color: #fff;">
<el-card
shadow="hover"
:header="config?.showHeader?.value ? '欢迎使用' : ''"
class="card-view"
:style="{background: randomColor(), color: config?.fontColor?.value}">
<div class="welcome">
<div class="logo">
<img src="/image/django-vue-admin.png">
@ -34,14 +38,40 @@
<script>
export default {
title: '欢迎',
sort: 11,
title: '欢迎使用',
name: 'welcome',
icon: 'el-icon-present',
description: '项目特色以及文档链接',
width: 8,
width: 16,
height: 45,
minH: 45,
minW: 1,
isResizable: true,
config: {
showHeader: {
label: '显示头部信息',
type: 'boot',
value: true,
placeholder: '颜色为空则随机变换颜色'
},
color: {
label: '背景颜色',
type: 'color',
value: '',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: '',
placeholder: '请选择字体颜色'
}
},
props: {
config: {
type: Object,
required: false
}
},
data () {
return {}
},
@ -54,13 +84,16 @@ export default {
if (this.config?.color?.value) {
return this.config.color.value
}
return this.color || this.$util.randomColor()
return 'linear-gradient(150deg, #3b88ec 0%, #accaff 100%)'
}
}
}
</script>
<style scoped>
<style scoped lang="scss">
.card-view {
color: $color-primary;
}
.welcome .logo {
text-align: center;
}

View File

@ -15,10 +15,11 @@
:key="index"
:rules="item.rules">
<el-input v-if="item.type==='input'" v-model="item.value" :placeholder="item.placeholder || '请输入'"></el-input>
<el-switch v-if="item.type==='boot'" v-model="item.value" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
<el-color-picker v-if="item.type==='color'" v-model="item.value" show-alpha :predefine="predefineColors"></el-color-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="deviceUpgradeDrawer = false">保存</el-button>
<el-button type="primary" @click="saveConfig"></el-button>
<el-button @click="deviceUpgradeDrawer = false">关闭</el-button>
</el-form-item>
</el-form>
@ -59,6 +60,10 @@ export default {
this.myComp = myComp
this.items = items
console.log(1112, this.myComp, this.items)
},
saveConfig () {
this.deviceUpgradeDrawer = false
this.$emit('saveConfig', this.myComp, this.items)
}
}

View File

@ -9,6 +9,10 @@
</el-button>
</div>
<div slot="operateButton">
<el-tooltip class="item" effect="dark" content="清空画布" placement="top">
<el-button v-if="customizing" type="danger" icon="el-icon-delete" circle size="mini"
@click="clickEmpty"></el-button>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="最小化" placement="top">
<el-button v-if="customizing" type="success" icon="el-icon-minus" circle size="mini"
@click="clickMinimize"></el-button>
@ -30,7 +34,8 @@
<span v-for="item in myCompsList" :key="item.title">
<el-tooltip class="item" effect="dark" :content="item.description" placement="top">
<div class="widgetsListItem" :style="{background: $util.randomBackground()}">
<span style="position: relative;right: 8px;float: right;top: -22px;cursor: pointer;" @click="push(item)">
<span style="position: relative;right: 8px;float: right;top: -22px;cursor: pointer;"
@click="push(item)">
<i class="el-icon-plus"></i>
</span>
<i :class="item.icon"></i> &nbsp;{{ item.title }}
@ -55,20 +60,17 @@
: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"
:minW="item.minW"
:minH="item.minH"
:maxW="item.maxW"
:maxH="item.maxH"
:key="index"
:isResizable="customizing"
<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"
@container-resized="containerResizedEvent"
>
<div v-if="customizing" class="customize-overlay">
<el-button v-if="item.config && Object.keys(item.config).length!==0" class="close" style="right: 60px;"
@ -79,21 +81,21 @@
<label>
<i :class="allComps[item.element].icon"></i>
{{ allComps[item.element].title }}</label>
<div style="color:#000;">{{ item.w }} x {{ item.h }}</div>
</div>
<component :class="customizing?'set-component-bg':''" :is="allComps[item.element]"
:config="item.config || {}"></component>
:config="item.config || {}" :width="item.w" :height="item.h" :pxData="pxData[item.i]"></component>
</grid-item>
</grid-layout>
</div>
</div>
<dashboard-config ref="dashboardConfig"></dashboard-config>
<dashboard-config ref="dashboardConfig" @saveConfig="saveConfig"></dashboard-config>
</d2-container>
</template>
<script>
import draggable from 'vuedraggable'
import allComps from './components'
import util from '@/libs/util'
import VueGridLayout from 'vue-grid-layout'
import SuspendedLibrary from '@/views/dashboard/workbench/suspendedLibrary'
import DashboardConfig from '@/views/dashboard/workbench/config'
@ -114,12 +116,18 @@ export default {
selectLayout: [],
defaultLayout: initData,
layout: [],
colNum: 24,
minimize: false
colNum: 48,
minimize: false,
pxData: {}
}
},
created () {
this.layout = JSON.parse(util.cookies.get('grid-layout') || JSON.stringify(this.defaultLayout))
async created () {
this.layout = await this.$store.dispatch('d2admin/db/get', {
dbName: 'sys',
path: 'grid-layout',
defaultValue: JSON.parse(JSON.stringify(this.defaultLayout)),
user: true
}, { root: true })
},
mounted () {
this.$emit('on-mounted')
@ -130,19 +138,19 @@ export default {
for (var key in this.allComps) {
allCompsList.push({
key: key,
sort: allComps[key].sort,
title: allComps[key].title,
icon: allComps[key].icon,
height: allComps[key].height,
width: allComps[key].width,
minH: allComps[key].minH || 1,
minW: allComps[key].minW || 1,
maxH: allComps[key].maxH || 100,
maxW: (allComps[key].maxW > this.colNum ? this.colNum : allComps[key].maxW) || Infinity,
config: allComps[key].config || {},
isResizable: allComps[key].isResizable || null,
description: allComps[key].description
})
}
allCompsList.sort(function (a, b) {
return (a.sort || 0) - (b.sort || 0)
})
return allCompsList
},
myCompsList () {
@ -168,17 +176,12 @@ export default {
},
//
push (item) {
console.log(1, item)
this.layout.push({
i: this.getLayoutElementNumber(item.key),
x: (this.layout.length * 2) % (this.colNum || 12),
y: this.layout.length + (this.colNum || 12),
w: item.width,
h: item.height,
minW: item.minW,
minH: item.minH,
maxW: item.maxW,
maxH: item.maxH,
config: item.config || {},
isResizable: item.isResizable || null,
element: item.key
@ -189,13 +192,22 @@ export default {
this.layout.splice(index, 1)
},
//
save () {
async save () {
console.log(this.layout)
this.customizing = false
this.minimize = false
this.$refs.suspendedLibrary.menu = false
this.$refs.widgets.style.removeProperty('transform')
util.cookies.set('grid-layout', this.layout)
var layout = JSON.parse(JSON.stringify(this.layout))
layout.map(val => {
delete val.pxData
})
await this.$store.dispatch('d2admin/db/set', {
dbName: 'sys',
path: 'grid-layout',
value: layout,
user: true
}, { root: true })
},
//
backDefault () {
@ -204,33 +216,69 @@ export default {
this.$refs.suspendedLibrary.menu = false
this.$refs.widgets.style.removeProperty('transform')
this.layout = JSON.parse(JSON.stringify(this.defaultLayout))
util.cookies.remove('grid-layout')
//
this.$store.dispatch('d2admin/db/set', {
dbName: 'sys',
path: 'grid-layout',
value: this.layout,
user: true
}, { root: true })
},
//
close () {
async close () {
this.customizing = false
this.minimize = false
this.$refs.suspendedLibrary.menu = false
this.$refs.widgets.style.removeProperty('transform')
this.layout = await this.$store.dispatch('d2admin/db/get', {
dbName: 'sys',
path: 'grid-layout',
defaultValue: JSON.stringify(this.defaultLayout),
user: true
}, { root: true })
},
//
clickEmpty () {
this.layout = []
},
//
saveConfig (myComp, items) {
this.layout.map(val => {
if (val.i === items.i) {
val.config = JSON.parse(JSON.stringify(items.config))
}
})
},
//
clickMinimize () {
this.minimize = !this.minimize
this.$refs.suspendedLibrary.menu = !this.$refs.suspendedLibrary.menu
},
//
//
clickConfig (itme) {
this.$refs.dashboardConfig.deviceUpgradeDrawer = true
this.$refs.dashboardConfig.initData(this.allComps[itme.element], itme)
this.$refs.dashboardConfig.initData(this.allComps[itme.element], JSON.parse(JSON.stringify(itme)))
this.minimize = false
},
//
containerResizedEvent: function (i, newH, newW, newHPx, newWPx) {
this.layout.map(val => {
if (val.i === i) {
this.$set(this.pxData, val.i, {
hpx: Number(newHPx),
wpx: Number(newWPx)
})
}
})
}
}
}
</script>
<style scoped lang="scss">
::v-deep .d2-container-full__body{
::v-deep .d2-container-full__body {
padding: 0!important;
}
.widgetsListItem {
width: 168px;
height: 75px;
@ -294,7 +342,7 @@ export default {
}
.set-component-bg {
background: rgba(255, 255, 255, 0.5);
//background: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(0, 0, 0, .5);
}

View File

@ -1,19 +1,134 @@
const log = [
{
i: 'dashboardImg1',
x: 9,
y: 21,
w: 8,
i: 'usersTotal0',
x: 0,
y: 0,
w: 12,
h: 12,
config: {
color: {
label: '背景颜色',
type: 'color',
value: 'rgba(255, 255, 255, 1)',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: null,
placeholder: '请选择字体颜色'
}
},
isResizable: true,
element: 'usersTotal',
moved: false
},
{
i: 'loginTotal1',
x: 12,
y: 0,
w: 12,
h: 12,
config: {
color: {
label: '背景颜色',
type: 'color',
value: 'rgba(255, 255, 255, 1)',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: null,
placeholder: '请选择字体颜色'
}
},
isResizable: true,
element: 'loginTotal',
moved: false
},
{
i: 'attachmentTotal2',
x: 24,
y: 0,
w: 12,
h: 12,
config: {
color: {
label: '背景颜色',
type: 'color',
value: 'rgba(255, 255, 255, 1)',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: null,
placeholder: '请选择字体颜色'
}
},
isResizable: true,
element: 'attachmentTotal',
moved: false
},
{
i: 'databaseTotal3',
x: 36,
y: 0,
w: 12,
h: 12,
config: {
color: {
label: '背景颜色',
type: 'color',
value: 'rgba(255, 255, 255, 1)',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: null,
placeholder: '请选择字体颜色'
}
},
isResizable: true,
element: 'databaseTotal',
moved: false
},
{
i: 'userLogin6',
x: 14,
y: 12,
w: 17,
h: 24,
minW: 1,
minH: 10,
maxW: 24,
maxH: 100,
config: {},
isResizable: true,
element: 'userLogin',
moved: false
},
{
i: 'registeredUser7',
x: 31,
y: 12,
w: 17,
h: 24,
config: {},
isResizable: true,
element: 'registeredUser',
moved: false
},
{
i: 'dashboardImg8',
x: 14,
y: 58,
w: 16,
h: 14,
config: {
src: {
label: '图片地址',
type: 'input',
value: '/image/card/tencent.jpg',
value: 'https://kfm-waiter.oss-cn-zhangjiakou.aliyuncs.com/dvadmin/img/chajianshichang.jpg',
placeholder: '请输入图片地址',
rules: [
{
required: true,
@ -24,6 +139,43 @@ const log = [
url: {
label: '跳转地址',
type: 'input',
placeholder: '请输入跳转地址',
value: 'https://bbs.django-vue-admin.com/plugMarket.html',
rules: [
{
required: true,
message: '不能为空'
}
]
}
},
isResizable: true,
element: 'dashboardImg',
moved: false
},
{
i: 'dashboardImg9',
x: 0,
y: 58,
w: 14,
h: 14,
config: {
src: {
label: '图片地址',
type: 'input',
value: '/image/card/tencent.jpg',
placeholder: '请输入图片地址',
rules: [
{
required: true,
message: '不能为空'
}
]
},
url: {
label: '跳转地址',
type: 'input',
placeholder: '请输入跳转地址',
value: 'https://cloud.tencent.com/act/cps/redirect?redirect=1060&cps_key=b302a514a6688aa30823fac954464e5d&from=console',
rules: [
{
@ -38,15 +190,11 @@ const log = [
moved: false
},
{
i: 'dashboardImg2',
x: 9,
y: 0,
w: 15,
h: 21,
minW: 1,
minH: 10,
maxW: 24,
maxH: 100,
i: 'dashboardImg10',
x: 30,
y: 58,
w: 18,
h: 14,
config: {
src: {
label: '图片地址',
@ -78,110 +226,68 @@ const log = [
moved: false
},
{
i: 'time3',
x: 9,
y: 45,
w: 8,
h: 19,
minW: 4,
minH: 10,
maxW: null,
maxH: 100,
config: {},
i: 'usersActive11',
x: 0,
y: 12,
w: 14,
h: 24,
config: {
color: {
label: '背景颜色',
type: 'color',
value: '',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: null,
placeholder: '请选择字体颜色'
}
},
isResizable: true,
element: 'time',
element: 'usersActive',
moved: false
},
{
i: 'ver4',
x: 17,
y: 45,
w: 7,
h: 19,
minW: 4,
minH: 10,
maxW: null,
maxH: 100,
config: {},
i: 'ver11',
x: 35,
y: 36,
w: 13,
h: 22,
config: {
showHeader: {
label: '显示头部信息',
type: 'boot',
value: true,
placeholder: '颜色为空则随机变换颜色'
},
color: {
label: '背景颜色',
type: 'color',
value: 'rgba(255, 255, 255, 1)',
placeholder: '颜色为空则随机变换颜色'
},
fontColor: {
label: '字体颜色',
type: 'color',
value: null,
placeholder: '请选择字体颜色'
}
},
isResizable: true,
element: 'ver',
moved: false
},
{
i: 'about5',
i: 'loginRegion12',
x: 0,
y: 45,
w: 9,
h: 19,
minW: 2,
minH: 10,
maxW: null,
maxH: 100,
config: {
color: {
label: '背景颜色',
type: 'color',
value: null,
placeholder: '颜色为空则随机变换颜色'
}
},
isResizable: true,
element: 'about',
moved: false
},
{
i: 'welcome5',
x: 0,
y: 0,
w: 9,
h: 45,
minW: 1,
minH: 45,
maxW: null,
maxH: 100,
y: 36,
w: 35,
h: 22,
config: {},
isResizable: true,
element: 'welcome',
moved: false
},
{
i: 'dashboardImg6',
x: 17,
y: 21,
w: 7,
h: 24,
minW: 1,
minH: 10,
maxW: 24,
maxH: 100,
config: {
src: {
label: '图片地址',
type: 'input',
value: 'https://kfm-waiter.oss-cn-zhangjiakou.aliyuncs.com/dvadmin/img/chajianshichang.jpg',
placeholder: '请输入图片地址',
rules: [
{
required: true,
message: '不能为空'
}
]
},
url: {
label: '跳转地址',
type: 'input',
placeholder: '请输入跳转地址',
value: 'https://bbs.django-vue-admin.com/plugMarket.html',
rules: [
{
required: true,
message: '不能为空'
}
]
}
},
isResizable: true,
element: 'dashboardImg',
element: 'loginRegion',
moved: false
}
]

View File

@ -331,6 +331,7 @@ export default {
const form = JSON.parse(JSON.stringify(this.form))
const keys = Object.keys(form)
const values = Object.values(form)
let submitForm = Object.assign([],this.formList)
for (const index in this.formList) {
const item = this.formList[index]
// eslint-disable-next-line camelcase
@ -347,7 +348,7 @@ export default {
child.parent = parentId
child.id = null
}
this.formList.push(child)
submitForm.push(child)
}
//
for (const arr of item.rule) {
@ -380,7 +381,7 @@ export default {
that.$refs.form.validate((valid) => {
if (valid) {
api.saveContent(this.options.id,
this.formList).then(res => {
submitForm).then(res => {
this.$message.success('保存成功')
this.refreshView()
})
@ -397,14 +398,13 @@ export default {
const tableLength = tableData.length
if (tableLength === 0) {
const { row } = $table.insert()
console.log(row)
} else {
const errMap = await $table.validate().catch(errMap => errMap)
if (errMap) {
this.$message.error('校验不通过!')
} else {
const { row } = $table.insert()
console.log(row)
}
}
},
@ -450,7 +450,7 @@ export default {
if (!uploadImgKey || uploadImgKey === '') {
that.form[imgKey] = []
}
// console.log(len)
const dict = {
name: name,
url: util.baseURL() + url

View File

@ -239,7 +239,13 @@ export const crudOptions = (vm) => {
disabled: false
},
dict: {
data: [{ label: '普通登录', value: 1 }, { label: '微信扫码登录', value: 2 }]
data: [
{ label: '普通登录', value: 1 },
{ label: '普通扫码登录', value: 2 },
{ label: '微信扫码登录', value: 3 },
{ label: '飞书扫码登录', value: 4 },
{ label: '钉钉扫码登录', value: 5 },
{ label: '短信登录', value: 6 }]
},
form: {
component: {

View File

@ -107,7 +107,8 @@ export default {
})
.then(() => {
//
this.$router.replace(this.$route.query.redirect || '/')
// this.$router.replace(this.$route.query.redirect || '/')
this.$router.replace('/')
})
.catch(() => {
this.getCaptcha()