!27 发布 v1.1.0

发布 v1.1.0 功能:
新增服务监控功能
新增操作日志功能
新增导入功能
新增celery定时任务
新增消息通知功能
新增后端接口文档
新增docker-compose部署
新增支持重写用户模型
数据权限完善
登录日志优化
后端代码架构优化
文件管理功能完善
修复创建用户密码问题
其他细节优化
pull/27/MERGE v1.1.0
李强 2021-05-05 22:19:22 +08:00 committed by Gitee
commit 44a8134c01
59 changed files with 1829 additions and 388 deletions

View File

@ -1,6 +1,6 @@
# Django-Vue-Admin
[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/pypi/v/django-simpleui.svg)](https://pypi.org/project/django-simpleui/#history) [![img](https://img.shields.io/badge/python-%3E=3.6.x-green.svg)](https://python.org/) ![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-2.2-blue)![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)
[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.6.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-2.2-blue)](https://docs.djangoproject.com/zh-hans/2.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark)](https://gitee.com/liqianglog/django-vue-admin)
@ -12,7 +12,7 @@ Django-Vue-Admin 是一套全部开源的快速开发平台,毫无保留给个
* 后端采用Python语言Django框架。
* 权限认证使用Jwt支持多终端认证系统。
* 支持加载动态权限菜单,多方式轻松权限控制。
* 特别鸣谢:<u>[Gin-Vue-Admin](https://www.gin-vue-admin.com/)</u>[RuoYi](https://gitee.com/y_project/RuoYi-Vue) [Vue-Element-Admin](https://github.com/PanJiaChen/vue-element-admin)[eladmin-web](https://gitee.com/elunez/eladmin-web?_from=gitee_search)
* 特别鸣谢:<u>[Gin-Vue-Admin](https://www.gin-vue-admin.com/)</u>[RuoYi](https://gitee.com/y_project/RuoYi-Vue) [Vue-Element-Admin](https://github.com/PanJiaChen/vue-element-admin)。
## QQ群
@ -47,12 +47,13 @@ github地址[https://github.com/liqianglog/django-vue-admin](https://github.c
13. 在线用户:当前系统中活跃用户状态监控、用户强退功能。
14. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
15. 在线构建器拖动表单元素生成相应的HTML代码。
16. 服务监控进行可视化的服务器监控CPU、内存、文件使用率等信息。
## 在线体验
演示地址:[https://demo.django-vue-admin.com/](https://demo.django-vue-admin.com/) 账号admin 密码123456
演示地址:[http://demo.django-vue-admin.com](http://demo.django-vue-admin.com) 账号admin 密码123456
文档地址:[https://django-vue-admin.com/](https://django-vue-admin.com/)
文档地址:[http://django-vue-admin.com](http://django-vue-admin.com)
## 前端
@ -92,9 +93,8 @@ npm run build:prod
2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py
3. 在 env.py 中配置数据库信息
mysql数据库版本建议:5.7以上
mysql数据库版本建议8.0
mysql数据库字符集utf8mb4
mysql数据库排序规则utf8mb4_0900_ai_ci
4. 安装依赖环境
pip3 install -r requirements.txt
@ -138,31 +138,40 @@ exit
<table>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/cd1f90be5f2684f4560c9519c0f2a232ee8.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/1cbcf0e6f257c7d3a063c0e3f2ff989e4b3.jpg"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/155624_fc01f49e_5074988.jpeg" height="200" width="400"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/162526_68e8c4c5_5074988.png" height="200" width="400"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/707825ad3f29de74a8d6d02fbd73ad631ea.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/46be40cc6f01aa300eed53a19b5012bf484.jpg"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/163049_0a16b3b8_5074988.png" height="200" width="400"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/163157_628941bc_5074988.png" height="200" width="400"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/4284796d4cea240d181b8f2201813dda710.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/3ecfac87a049f7fe36abbcaafb2c40d36cf.jpg"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/163444_73d4a6ae_5074988.png" height="200" width="400"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/163456_c4ddcaf6_5074988.png" height="200" width="400"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/71c2d48905221a09a728df4aff4160b8607.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/c14c1ee9a64a6a9c2c22f67d43198767dbe.jpg"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/163732_48cca279_5074988.png" height="200" width="400"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/163756_99176d5d_5074988.png" height="200" width="400"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/5e8c387724954459291aafd5eb52b456f53.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/644e78da53c2e92a95dfda4f76e6d117c4b.jpg"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/164149_b223657a_5074988.png" height="200" width="400"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/164226_58653572_5074988.png" height="200" width="400"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/fdea1d8bb8625c27bf964176a2c8ebc6945.jpg"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/509d2708cfd762b6e6339364cac1cc1970c.jpg"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/164259_e06fbfe9_5074988.png" height="200" width="400"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/164330_6406c28f_5074988.png" height="200" width="400"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-f1fd681cc9d295db74e85ad6d2fe4389454.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-c195234bbcd30be6927f037a6755e6ab69c.png"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/164359_add984a1_5074988.png" height="200" width="400"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/181144_9665dae5_5074988.png" height="200" width="400"/></td>
</tr>
<tr>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/181700_25edc19f_5074988.png" height="200" width="400"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/181715_9305b7e8_5074988.png" height="200" width="400"/></td>
</tr>
<tr>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/181732_953b05e4_5074988.png" height="200" width="400"/></td>
<td><img src="https://images.gitee.com/uploads/images/2021/0505/182122_73bddac6_5074988.png" height="200" width="400"/></td>
</tr>
</table>

View File

@ -8,6 +8,8 @@ version: "3"
services:
dvadmin-ui:
container_name: dvadmin-ui
ports:
- "8080:8080"
build:
context: ./
dockerfile: ./docker_env/vue-ui/Dockerfile

View File

@ -12,5 +12,6 @@ WORKDIR /dvadmin-backend
COPY ./dvadmin-backend/requirements.txt /
COPY ./dvadmin-backend/conf/env.example.py /dvadmin-backend/conf/env.py
RUN python3 -m pip install -i https://mirrors.aliyun.com/pypi/simple/ -r /requirements.txt
RUN python3 -m pip install -i https://mirrors.aliyun.com/pypi/simple/ uwsgi==2.0.19.1
CMD ["/dvadmin-backend/docker_start.sh"]
# ENTRYPOINT [ "uwsgi --ini /backend/azcrm/uwsgi.ini" ]

View File

@ -51,6 +51,7 @@ INSTALLED_APPS = [
'apps.vadmin.op_drf',
'apps.vadmin.system',
'apps.vadmin.celery',
'apps.vadmin.monitor',
]
MIDDLEWARE = [
@ -242,6 +243,8 @@ else:
# redis 缓存
REDIS_URL = f'redis://:{REDIS_PASSWORD if REDIS_PASSWORD else ""}@{os.getenv("REDIS_HOST") or REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}'
# 是否启用redis
if locals().get("REDIS_ENABLE", True):
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
@ -328,3 +331,4 @@ CELERYBEAT_SCHEDULER = 'django_celery_beat.schedulers.DatabaseScheduler' # Back
# ================================================= #
# 接口权限
INTERFACE_PERMISSION = locals().get("INTERFACE_PERMISSION", False)
DJANGO_CELERY_BEAT_TZ_AWARE = False

View File

@ -22,7 +22,7 @@ from django.urls import re_path, include
from django.views.static import serve
from rest_framework.views import APIView
from vadmin.utils.response import SuccessResponse
from apps.vadmin.utils.response import SuccessResponse
class CaptchaRefresh(APIView):

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MonitorConfig(AppConfig):
name = 'vadmin.monitor'
verbose_name = "系统监控"

View File

@ -0,0 +1,23 @@
import django_filters
from .models import Server, Monitor
class ServerFilter(django_filters.rest_framework.FilterSet):
"""
服务器信息 简单过滤器
"""
class Meta:
model = Server
fields = '__all__'
class MonitorFilter(django_filters.rest_framework.FilterSet):
"""
服务器监控信息 简单过滤器
"""
class Meta:
model = Monitor
fields = '__all__'

View File

@ -0,0 +1,3 @@
from ..models.monitor import Monitor
from ..models.server import Server
from ..models.sys_files import SysFiles

View File

@ -0,0 +1,19 @@
from django.db.models import CharField, ForeignKey, CASCADE
from ...op_drf.models import CoreModel
class Monitor(CoreModel):
cpu_num = CharField(max_length=8, verbose_name='CPU核数')
cpu_sys = CharField(max_length=8, verbose_name='CPU已使用率')
mem_num = CharField(max_length=32, verbose_name='内存总数(KB)')
mem_sys = CharField(max_length=32, verbose_name='内存已使用大小(KB)')
seconds = CharField(max_length=32, verbose_name='系统已运行时间')
server = ForeignKey(to='monitor.Server', on_delete=CASCADE, verbose_name="关联服务器信息", db_constraint=False)
class Meta:
verbose_name = '服务器监控信息'
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.server and self.server.name and self.server.ip}监控信息"

View File

@ -0,0 +1,20 @@
from django.db import models
from django.db.models import CharField
from apps.vadmin.op_drf.fields import UpdateDateTimeField, CreateDateTimeField
class Server(models.Model):
name = CharField(max_length=256, verbose_name='服务器名称', null=True, blank=True)
ip = CharField(max_length=32, verbose_name="ip地址")
os = CharField(max_length=32, verbose_name="操作系统")
remark = CharField(max_length=256, verbose_name="备注", null=True, blank=True)
update_datetime = UpdateDateTimeField() # 修改时间
create_datetime = CreateDateTimeField() # 创建时间
class Meta:
verbose_name = '服务器信息'
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.name and self.ip}"

View File

@ -0,0 +1,19 @@
from django.db.models import CharField, ForeignKey, CASCADE
from ...op_drf.models import CoreModel
class SysFiles(CoreModel):
dir_name = CharField(max_length=32, verbose_name='磁盘路径')
sys_type_name = CharField(max_length=400, verbose_name='系统文件类型')
type_name = CharField(max_length=32, verbose_name='盘符类型')
total = CharField(max_length=32, verbose_name='磁盘总大小(KB)')
disk_sys = CharField(max_length=32, verbose_name='已使用大小(KB)')
monitor = ForeignKey(to='monitor.Monitor', on_delete=CASCADE, verbose_name="关联服务器监控信息", db_constraint=False)
class Meta:
verbose_name = '系统磁盘'
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.creator and self.creator.name}"

View File

@ -0,0 +1,40 @@
from .models import Server, Monitor
from ..op_drf.serializers import CustomModelSerializer
# ================================================= #
# ************** 服务器信息 序列化器 ************** #
# ================================================= #
class ServerSerializer(CustomModelSerializer):
"""
服务器信息 简单序列化器
"""
class Meta:
model = Server
fields = ("id", "ip", "name", "os", "remark")
class UpdateServerSerializer(CustomModelSerializer):
"""
服务器信息 简单序列化器
"""
class Meta:
model = Server
fields = ("name", "remark")
# ================================================= #
# ************** 服务器监控信息 序列化器 ************** #
# ================================================= #
class MonitorSerializer(CustomModelSerializer):
"""
服务器监控信息 简单序列化器
"""
class Meta:
model = Monitor
fields = '__all__'

View File

@ -0,0 +1,89 @@
import datetime
import logging
import sys
import time
import psutil
from ..monitor.models import Server, Monitor, SysFiles
from ..op_drf.response import SuccessResponse
from ..system.models import ConfigSettings
from ..utils.decorators import BaseCeleryApp
logger = logging.getLogger(__name__)
from platform import platform
def getIP():
"""获取ipv4地址"""
dic = psutil.net_if_addrs()
ipv4_list = []
for adapter in dic:
snicList = dic[adapter]
for snic in snicList:
if snic.family.name == 'AF_INET':
ipv4 = snic.address
if ipv4 != '127.0.0.1':
ipv4_list.append(ipv4)
if len(ipv4_list) >= 1:
return ipv4_list[0]
else:
return None
@BaseCeleryApp(name='apps.vadmin.monitor.tasks.get_monitor_info', save_success_logs=False)
def get_monitor_info():
"""
定时获取系统监控信息
:return:
"""
# 获取服务器
ip = getIP()
if not ip:
logger.error("无法获取到IP")
return
server_obj, create = Server.objects.get_or_create(ip=ip)
if create:
server_obj.name = ip
terse = ('terse' in sys.argv or '--terse' in sys.argv)
aliased = (not 'nonaliased' in sys.argv and not '--nonaliased' in sys.argv)
server_obj.os = platform(aliased, terse)
server_obj.save()
# 获取CPU和内存信息
mem = psutil.virtual_memory()
monitor_obj = Monitor()
monitor_obj.cpu_num = psutil.cpu_count()
monitor_obj.cpu_sys = float(psutil.cpu_percent(0.1))
monitor_obj.mem_num = int(mem.total / 1024)
monitor_obj.mem_sys = int(mem.used / 1024)
monitor_obj.seconds = time.strftime("%d%H 小时 %M 分 %S 秒", time.gmtime(int(time.time()) - int(psutil.boot_time())))
monitor_obj.server = server_obj
monitor_obj.save()
# 保存磁盘信息
for ele in psutil.disk_partitions():
disk = psutil.disk_usage('/')
sys_files_obj = SysFiles()
sys_files_obj.dir_name = ele.mountpoint
sys_files_obj.sys_type_name = ele.opts
sys_files_obj.type_name = ele.fstype
sys_files_obj.total = disk.total
sys_files_obj.disk_sys = disk.used
sys_files_obj.monitor = monitor_obj
sys_files_obj.save()
return SuccessResponse(msg="")
@BaseCeleryApp(name='apps.vadmin.monitor.tasks.clean_surplus_monitor_info')
def clean_surplus_monitor_info():
"""
定时清理多余 系统监控信息
:return:
"""
config_settings_obj = ConfigSettings.objects.filter(configKey='sys.monitor.info.save_days').first()
Monitor.objects.filter(
update_datetime__lt=datetime.timedelta(days=int(config_settings_obj.configValue or 30))).delete()
logger.info(f"成功清空{config_settings_obj.configValue}天前数据")

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,16 @@
from django.urls import re_path
from rest_framework.routers import DefaultRouter
from .views import ServerModelViewSet, MonitorModelViewSet
router = DefaultRouter()
router.register(r'server', ServerModelViewSet)
router.register(r'monitor', MonitorModelViewSet)
urlpatterns = [
re_path('monitor/info/(?P<pk>.*)/', MonitorModelViewSet.as_view({'get': 'get_monitor_info'})),
re_path('monitor/rate/(?P<pk>.*)/', MonitorModelViewSet.as_view({'get': 'get_rate_info'})),
re_path('monitor/enabled/', MonitorModelViewSet.as_view({'get': 'enabled_monitor_info'})),
re_path('monitor/clean/', MonitorModelViewSet.as_view({'get': 'clean_all'})),
]
urlpatterns += router.urls

View File

@ -0,0 +1,184 @@
from django_celery_beat.models import PeriodicTask, IntervalSchedule, CrontabSchedule
from rest_framework.request import Request
from .filters import ServerFilter, MonitorFilter
from .models import Server, Monitor, SysFiles
from .serializers import ServerSerializer, MonitorSerializer, UpdateServerSerializer
from ..op_drf.response import SuccessResponse, ErrorResponse
from ..op_drf.viewsets import CustomModelViewSet
from ..permission.permissions import CommonPermission
from ..system.models import ConfigSettings
class ServerModelViewSet(CustomModelViewSet):
"""
服务器信息 模型的CRUD视图
"""
queryset = Server.objects.all()
serializer_class = ServerSerializer
update_serializer_class = UpdateServerSerializer
# extra_filter_backends = [DataLevelPermissionsFilter]
filter_class = ServerFilter
update_extra_permission_classes = (CommonPermission,)
destroy_extra_permission_classes = (CommonPermission,)
create_extra_permission_classes = (CommonPermission,)
ordering = '-create_datetime' # 默认排序
class MonitorModelViewSet(CustomModelViewSet):
"""
服务器监控信息 模型的CRUD视图
"""
queryset = Monitor.objects.all()
serializer_class = MonitorSerializer
# extra_filter_backends = [DataLevelPermissionsFilter]
filter_class = MonitorFilter
update_extra_permission_classes = (CommonPermission,)
destroy_extra_permission_classes = (CommonPermission,)
create_extra_permission_classes = (CommonPermission,)
ordering = '-create_datetime' # 默认排序
def get_rate_info(self, request: Request, *args, **kwargs):
"""
获取使用率 监控信息
:param request:
:param args:
:param kwargs:
:return:
"""
pk = kwargs.get("pk")
queryset = self.filter_queryset(self.get_queryset())
queryset = queryset.filter(server__id=pk).order_by("create_datetime")
# 间隔取值
queryset_count = queryset.count()
Interval_num = 1 if queryset_count < 200 else int(queryset_count / 100)
queryset = queryset.values('cpu_sys', 'mem_num', 'mem_sys', 'create_datetime')[::Interval_num][:100]
data = {
"cpu": [],
"memory": [],
"datetime": [],
}
for ele in queryset:
data["cpu"].append(round(float(ele.get('cpu_sys', 0)), 2))
data["datetime"].append(ele.get('create_datetime').strftime('%Y-%m-%d %H:%M:%S'))
data["memory"].append(round(float(ele.get('mem_num', 0)), 4) and round(float(ele.get('mem_sys', 0)) /
float(ele.get('mem_num', 0)) * 100,
2))
return SuccessResponse(data=data)
def get_monitor_info(self, request: Request, *args, **kwargs):
"""
最新的服务器状态信息
:param request:
:param args:
:param kwargs:
:return:
"""
pk = kwargs.get("pk")
instance = Monitor.objects.filter(server__id=pk).order_by("-create_datetime").first()
if not instance:
return ErrorResponse(msg="未找到服务器信息id")
serializer = self.get_serializer(instance)
data = serializer.data
return SuccessResponse(data={
"cpu": {
"total": int(data.get('cpu_num'), 0),
"used": "", # cpu核心 可不传如指cpu当前主频该值可以传
"rate": float(data.get('cpu_sys', 0)) / 100,
"unit": "核心", # 默认单位 核心
},
"memory": {
"total": int(int(data.get('mem_num', 0)) / 1024),
"used": int(int(data.get('mem_sys', 0)) / 1024),
"rate": int(data.get('mem_num', 0)) and round(int(data.get('mem_sys', 0)) /
int(data.get('mem_num', 0)), 4),
"unit": "MB", # 默认单位 MB
},
"disk": [{
"dir_name": ele.get('dir_name'),
"total": int(int(ele.get('total', 0)) / 1024 / 1024 / 1024),
"used": int(int(ele.get('disk_sys', 0)) / 1024 / 1024 / 1024),
"rate": int(ele.get('total', 0)) and round(int(ele.get('disk_sys', 0)) / int(ele.get('total', 0)),
4),
"unit": "GB", # 默认单位 GB
} for ele in SysFiles.objects.filter(monitor=instance).values('dir_name', 'total', 'disk_sys')]
})
def enabled_monitor_info(self, request: Request, *args, **kwargs):
"""
更新和获取监控信息
:param request:
:param args:
:param kwargs:
:return:
"""
enabled = request.query_params.get('enabled', None)
save_days = request.query_params.get('save_days', None)
interval = request.query_params.get('interval', None)
# 定时获取系统监控信息
periodictask_obj = PeriodicTask.objects.filter(task='apps.vadmin.monitor.tasks.get_monitor_info').first()
if not periodictask_obj:
intervalschedule_obj, _ = IntervalSchedule.objects.get_or_create(every=5, period="seconds")
periodictask_obj = PeriodicTask()
periodictask_obj.task = "apps.vadmin.monitor.tasks.get_monitor_info"
periodictask_obj.name = "定时获取系统监控信息"
periodictask_obj.interval = intervalschedule_obj
periodictask_obj.enabled = False
periodictask_obj.save()
# 定时清理多余 系统监控信息
clean_task_obj = PeriodicTask.objects.filter(
task='apps.vadmin.monitor.tasks.clean_surplus_monitor_info').first()
if not clean_task_obj:
crontab_obj, _ = CrontabSchedule.objects.get_or_create(day_of_month="*", day_of_week="*", hour="1",
minute="0", month_of_year="*")
clean_task_obj = PeriodicTask()
clean_task_obj.task = "apps.vadmin.monitor.tasks.clean_surplus_monitor_info"
clean_task_obj.name = "定时清理多余-系统监控信息"
clean_task_obj.crontab = crontab_obj
clean_task_obj.enabled = True
clean_task_obj.save()
# 配置添加
config_obj = ConfigSettings.objects.filter(configKey='sys.monitor.info.save_days').first()
if not config_obj:
config_obj = ConfigSettings()
config_obj.configKey = "sys.monitor.info.save_days"
config_obj.configName = "定时清理多余系统监控信息"
config_obj.configValue = "30"
config_obj.configType = "Y"
config_obj.status = "1"
config_obj.remark = "定时清理多余-系统监控信息默认30天"
config_obj.save()
if enabled:
# 更新监控状态
periodictask_obj.enabled = True if enabled == "1" else False
periodictask_obj.save()
# 更新 定时清理多余 系统监控信息 状态
clean_task_obj.enabled = True if enabled == "1" else False
clean_task_obj.save()
# 更新保留天数
if save_days and config_obj:
config_obj.configValue = save_days
config_obj.save()
# 更新监控获取频率
if interval:
periodictask_obj.interval.every = interval
periodictask_obj.interval.save()
return SuccessResponse(data={
"enabled": periodictask_obj.enabled,
"interval": periodictask_obj.interval.every,
"save_days": config_obj.configValue if config_obj else "30",
})
def clean_all(self, request: Request, *args, **kwargs):
"""
清空监控信息
:param request:
:param args:
:param kwargs:
:return:
"""
self.get_queryset().delete()
return SuccessResponse(msg="清空成功")

View File

@ -315,8 +315,9 @@ class ImportSerializerMixin:
# 导出模板
if request.method == 'GET':
# 示例数据
queryset = self.filter_queryset(self.get_queryset())
return SuccessResponse(
export_excel_save_model(request, self.import_field_data.values(), [], '导入用户数据模板.xls'))
export_excel_save_model(request, self.import_field_data.values(), [], f'导入{get_verbose_name(queryset)}模板.xls'))
updateSupport = request.data.get('updateSupport')
# 从excel中组织对应的数据结构然后使用序列化器保存
data = excel_to_data(request.data.get('file_url'), self.import_field_data)

View File

@ -1,3 +1,4 @@
from django.conf import settings
from django.db import models
from django.db.models import SET_NULL
@ -24,7 +25,7 @@ class CoreModel(models.Model):
增加审计字段, 覆盖字段时, 字段名称请勿修改, 必须统一审计字段名称
"""
description = DescriptionField() # 描述
creator = models.ForeignKey(to='permission.UserProfile', related_query_name='creator_query', null=True,
creator = models.ForeignKey(to=settings.AUTH_USER_MODEL, related_query_name='creator_query', null=True,
verbose_name='创建者', on_delete=SET_NULL, db_constraint=False) # 创建者
modifier = ModifierCharField() # 修改者
dept_belong_id = models.CharField(max_length=64, verbose_name="数据归属部门", null=True, blank=True)

View File

@ -1,7 +1,10 @@
from django.utils.functional import cached_property
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer
from rest_framework.fields import empty
from rest_framework.request import Request
from rest_framework.serializers import ModelSerializer
from rest_framework.utils.serializer_helpers import BindingDict
class CustomModelSerializer(ModelSerializer):
@ -30,6 +33,8 @@ class CustomModelSerializer(ModelSerializer):
return super().save(**kwargs)
def create(self, validated_data):
if self.context.get('request'):
self.request = self.context.get('request')
if self.request:
username = self.get_request_username()
if self.modifier_field_name in self.fields.fields:
@ -51,11 +56,11 @@ class CustomModelSerializer(ModelSerializer):
return getattr(self.request.user, 'username', None)
return None
@property
@cached_property
def fields(self):
fields = super().fields
fields = BindingDict(self)
for key, value in self.get_fields().items():
fields[key] = value
if not hasattr(self, '_context'):
return fields

View File

@ -0,0 +1,28 @@
"""
重写校验器返回字段
"""
from rest_framework.validators import UniqueValidator, qs_exists
from vadmin.utils.exceptions import APIException
class CustomUniqueValidator(UniqueValidator):
"""
继承,重写必填字段的验证器结果,防止字段暴露
"""
def __call__(self, value, serializer_field):
# Determine the underlying model field name. This may not be the
# same as the serializer field name if `source=<>` is set.
field_name = serializer_field.source_attrs[-1]
# Determine the existing instance, if this is an update operation.
instance = getattr(serializer_field.parent, 'instance', None)
queryset = self.queryset
queryset = self.filter_queryset(value, queryset, field_name)
queryset = self.exclude_current_instance(queryset, instance)
if qs_exists(queryset):
raise APIException(message=self.message)
def __repr__(self):
return super().__repr__()

View File

@ -1,8 +1,11 @@
import django_filters
from django.contrib.auth import get_user_model
from ..permission.models import Menu, Dept, Post, Role, UserProfile
from ..permission.models import Menu, Dept, Post, Role
from ..utils.model_util import get_dept
UserProfile = get_user_model()
class MenuFilter(django_filters.rest_framework.FilterSet):
"""

View File

@ -1,6 +1,7 @@
import logging
import os
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import connection
@ -75,6 +76,7 @@ class Command(BaseCommand):
parser.add_argument('-N', nargs='*')
def handle(self, *args, **options):
user_name = "_".join(settings.AUTH_USER_MODEL.lower().split("."))
init_dict = {
'system_dictdata': [os.path.join('system', 'system_dictdata.sql'), '字典管理', 'system_dictdata'],
'system_dictdetails': [os.path.join('system', 'system_dictdetails.sql'), '字典详情', 'system_dictdetails'],
@ -86,8 +88,7 @@ class Command(BaseCommand):
'permission_role': [os.path.join('permission', 'permission_role.sql'), '角色管理',
','.join(['permission_role', 'permission_role_dept', 'permission_role_menu'])],
'permission_userprofile': [os.path.join('permission', 'permission_userprofile.sql'), '用户管理', ','.join(
['permission_userprofile_groups', 'permission_userprofile', 'permission_userprofile_role',
'permission_userprofile_post'])]
[f'{user_name}_groups', f'{user_name}', f'{user_name}_role', f'{user_name}_post'])]
}
init_name = options.get('init_name')
is_yes = None

View File

@ -11,7 +11,7 @@ class Dept(CoreModel):
phone = CharField(max_length=32, verbose_name="联系电话", null=True, blank=True)
email = CharField(max_length=32, verbose_name="邮箱", null=True, blank=True)
status = CharField(max_length=8, verbose_name="部门状态", null=True, blank=True)
parentId = ForeignKey(to='Dept', on_delete=CASCADE, default=False, verbose_name="上级部门",
parentId = ForeignKey(to='permission.Dept', on_delete=CASCADE, default=False, verbose_name="上级部门",
db_constraint=False, null=True, blank=True)
class Meta:

View File

@ -18,8 +18,8 @@ class Role(CoreModel):
admin = BooleanField(default=False, verbose_name="是否为admin")
dataScope = CharField(max_length=8,default='1', choices=DATASCOPE_CHOICES, verbose_name="权限范围",)
remark = TextField(verbose_name="备注", help_text="备注", null=True, blank=True)
dept = ManyToManyField(to='Dept', verbose_name='数据权限-关联部门', db_constraint=False)
menu = ManyToManyField(to='Menu', verbose_name='关联菜单权限', db_constraint=False)
dept = ManyToManyField(to='permission.Dept', verbose_name='数据权限-关联部门', db_constraint=False)
menu = ManyToManyField(to='permission.Menu', verbose_name='关联菜单权限', db_constraint=False)
class Meta:
verbose_name = '角色管理'

View File

@ -1,5 +1,6 @@
from uuid import uuid4
from django.conf import settings
from django.contrib.auth.models import UserManager, AbstractUser
from django.core.cache import cache
from django.db.models import IntegerField, ForeignKey, CharField, TextField, ManyToManyField, CASCADE
@ -22,9 +23,9 @@ class UserProfile(AbstractUser, CoreModel):
gender = CharField(max_length=8, verbose_name="性别", null=True, blank=True)
remark = TextField(verbose_name="备注", null=True)
user_type = IntegerField(default=0, verbose_name="用户类型")
post = ManyToManyField(to='Post', verbose_name='关联岗位', db_constraint=False)
role = ManyToManyField(to='Role', verbose_name='关联角色', db_constraint=False)
dept = ForeignKey(to='Dept', verbose_name='归属部门', on_delete=CASCADE, db_constraint=False, null=True, blank=True)
post = ManyToManyField(to='permission.Post', verbose_name='关联岗位', db_constraint=False)
role = ManyToManyField(to='permission.Role', verbose_name='关联角色', db_constraint=False)
dept = ForeignKey(to='permission.Dept', verbose_name='归属部门', on_delete=CASCADE, db_constraint=False, null=True, blank=True)
@property
def get_user_interface_dict(self):
@ -51,6 +52,7 @@ class UserProfile(AbstractUser, CoreModel):
return cache.delete(f'permission_interface_dict_{self.username}')
class Meta:
abstract = settings.AUTH_USER_MODEL != 'permission.UserProfile'
verbose_name = '用户管理'
verbose_name_plural = verbose_name

View File

@ -1,10 +1,13 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from ..op_drf.serializers import CustomModelSerializer
from ..permission.models import Menu, Dept, Post, Role, UserProfile
from ..op_drf.validator import CustomUniqueValidator
from ..permission.models import Menu, Dept, Post, Role
from ..system.models import MessagePush
UserProfile = get_user_model()
# ================================================= #
# ************** 菜单管理 序列化器 ************** #
@ -257,7 +260,8 @@ class UserProfileCreateUpdateSerializer(CustomModelSerializer):
post = PostSerializer(many=True, read_only=True)
role = RoleSerializer(many=True, read_only=True)
username = serializers.CharField(required=True, max_length=150,
validators=[UniqueValidator(queryset=UserProfile.objects.all(), message="用戶已存在")],
validators=[
CustomUniqueValidator(queryset=UserProfile.objects.all(), message="用戶已存在")],
error_messages={
"blank": "请输入用户名称",
"required": "用户名称不能为空",

View File

@ -1,4 +1,4 @@
from django.contrib.auth import authenticate
from django.contrib.auth import authenticate, get_user_model
from rest_framework.request import Request
from rest_framework.views import APIView
@ -6,7 +6,7 @@ from .permissions import CommonPermission, DeptDestroyPermission
from ..op_drf.filters import DataLevelPermissionsFilter
from ..op_drf.viewsets import CustomModelViewSet
from ..permission.filters import MenuFilter, DeptFilter, PostFilter, RoleFilter, UserProfileFilter
from ..permission.models import Role, Menu, Dept, Post, UserProfile
from ..permission.models import Role, Menu, Dept, Post
from ..permission.serializers import UserProfileSerializer, MenuSerializer, RoleSerializer, \
MenuCreateUpdateSerializer, DeptSerializer, DeptCreateUpdateSerializer, PostSerializer, PostCreateUpdateSerializer, \
RoleCreateUpdateSerializer, DeptTreeSerializer, MenuTreeSerializer, UserProfileCreateUpdateSerializer, \
@ -15,6 +15,8 @@ from ..permission.serializers import UserProfileSerializer, MenuSerializer, Role
from ..system.models import DictDetails
from ..utils.response import SuccessResponse, ErrorResponse
UserProfile = get_user_model()
class GetUserProfileView(APIView):
"""

View File

@ -1,5 +1,7 @@
import os
from django.conf import settings
def getSql(filename):
"""
@ -11,4 +13,8 @@ def getSql(filename):
pwd = os.path.join(abspath, 'scripts', filename)
with open(pwd, 'rb') as fp:
content = fp.read().decode('utf8')
if filename == "permission/permission_userprofile.sql":
user_name = "_".join(settings.AUTH_USER_MODEL.lower().split("."))
content = content.replace("permission_userprofile", user_name). \
replace("userprofile", settings.AUTH_USER_MODEL.lower().split(".")[-1])
return [ele for ele in content.split('\n') if not ele.startswith('--') and ele.strip(' ')]

View File

@ -121,3 +121,9 @@ INSERT INTO `permission_menu` (id, description, modifier, update_datetime, creat
INSERT INTO `permission_menu` (id, description, modifier, update_datetime, create_datetime, menuType, icon, name, orderNum, isFrame, web_path, component_path, interface_path, interface_method, perms, status, visible, isCache, creator_id, parentId_id, dept_belong_id) VALUES (88, '', 'admin', '2021-03-21 23:33:30.888568', '2021-03-21 23:33:30.888593', '2', NULL, '登录日志清空', 4, '1', NULL, NULL, '/admin/system/logininfor/clean/', 'DELETE', 'admin:system:logininfor:clean:delete', '1', '1', '1', 1, 62, '1');
INSERT INTO `permission_menu` (id, description, modifier, update_datetime, create_datetime, menuType, icon, name, orderNum, isFrame, web_path, component_path, interface_path, interface_method, perms, status, visible, isCache, creator_id, parentId_id, dept_belong_id) VALUES (90, '', 'admin', '2021-03-26 00:44:00.756139', '2021-03-26 00:43:14.390228', '2', NULL, '定时日志批量删除', 3, '1', NULL, NULL, '/admin/system/celery_log/{id}/', 'DELETE', 'admin:system:celery_log:{id}:delete', '1', '1', '1', 1, 79, '8');
INSERT INTO `permission_menu` (id, description, modifier, update_datetime, create_datetime, menuType, icon, name, orderNum, isFrame, web_path, component_path, interface_path, interface_method, perms, status, visible, isCache, creator_id, parentId_id, dept_belong_id) VALUES (91, '', 'admin', '2021-03-26 00:44:36.135658', '2021-03-26 00:44:36.135679', '2', NULL, '定时日志清空', 4, '1', NULL, NULL, '/admin/system/celery_log/clean/', 'DELETE', 'admin:system:celery_log:clean:delete', '1', '1', '1', 1, 79, '8');
INSERT INTO `permission_menu` (id, description, modifier, update_datetime, create_datetime, menuType, icon, name, orderNum, isFrame, web_path, component_path, interface_path, interface_method, perms, status, visible, isCache, creator_id, parentId_id, dept_belong_id) VALUES (92, '', 'admin', '2021-04-27 23:49:59.036636', '2021-04-27 23:44:54.512207', '1', 'server', '服务监控', 3, '1', '/monitor/server', 'vadmin/monitor/server/index', NULL, 'GET', NULL, '1', '1', '1', 1, 66, '1');
INSERT INTO `permission_menu` (id, description, modifier, update_datetime, create_datetime, menuType, icon, name, orderNum, isFrame, web_path, component_path, interface_path, interface_method, perms, status, visible, isCache, creator_id, parentId_id, dept_belong_id) VALUES (93, '', 'admin', '2021-04-27 23:57:46.633022', '2021-04-27 23:49:28.569029', '2', NULL, '服务监控查询', 1, '1', NULL, NULL, '/admin/monitor/server/', 'GET', 'admin:monitor:server:get', '1', '1', '1', 1, 92, '1');
INSERT INTO `permission_menu` (id, description, modifier, update_datetime, create_datetime, menuType, icon, name, orderNum, isFrame, web_path, component_path, interface_path, interface_method, perms, status, visible, isCache, creator_id, parentId_id, dept_belong_id) VALUES (94, '', 'admin', '2021-04-27 23:58:44.705142', '2021-04-27 23:58:13.384483', '2', NULL, '修改服务器信息', 2, '1', NULL, NULL, '/admin/monitor/server/{id}/', 'PUT', 'admin:monitor:server:{id}:put', '1', '1', '1', 1, 92, '1');
INSERT INTO `permission_menu` (id, description, modifier, update_datetime, create_datetime, menuType, icon, name, orderNum, isFrame, web_path, component_path, interface_path, interface_method, perms, status, visible, isCache, creator_id, parentId_id, dept_belong_id) VALUES (95, '', 'admin', '2021-04-27 23:59:29.530633', '2021-04-27 23:59:07.744938', '2', NULL, '修改监控信息', 3, '1', NULL, NULL, '/admin/monitor/monitor/enabled/', 'GET', 'admin:monitor:monitor:enabled:get', '1', '1', '1', 1, 92, '1');
INSERT INTO `permission_menu` (id, description, modifier, update_datetime, create_datetime, menuType, icon, name, orderNum, isFrame, web_path, component_path, interface_path, interface_method, perms, status, visible, isCache, creator_id, parentId_id, dept_belong_id) VALUES (96, '', 'admin', '2021-04-28 00:01:15.071889', '2021-04-27 23:59:48.612905', '2', NULL, '清空监控记录', 4, '1', NULL, NULL, '/admin/monitor/monitor/clean/', 'GET', 'admin:monitor:monitor:clean:get', '1', '1', '1', 1, 92, '1');
INSERT INTO `permission_menu` (id, description, modifier, update_datetime, create_datetime, menuType, icon, name, orderNum, isFrame, web_path, component_path, interface_path, interface_method, perms, status, visible, isCache, creator_id, parentId_id, dept_belong_id) VALUES (97, '', 'admin', '2021-05-02 19:12:06.813143', '2021-05-02 18:58:06.260280', '0', 'dashboard', '首页', 0, '1', '/index', 'Layout/index', NULL, 'GET', NULL, '1', '1', '1', 1, NULL, '1');

View File

@ -9,7 +9,7 @@ class DictDetails(CoreModel):
is_default = BooleanField(verbose_name="是否默认", default=False)
status = CharField(max_length=2, verbose_name="字典状态")
sort = CharField(max_length=256, verbose_name="字典排序")
dict_data = ForeignKey(to='DictData', on_delete=CASCADE, verbose_name="关联字典", db_constraint=False)
dict_data = ForeignKey(to='system.DictData', on_delete=CASCADE, verbose_name="关联字典", db_constraint=False)
remark = CharField(max_length=256, verbose_name="备注", null=True, blank=True)
@classmethod

View File

@ -1,9 +1,9 @@
from django.conf import settings
from django.db import models
from django.db.models import *
from ...op_drf.fields import UpdateDateTimeField, CreateDateTimeField
from ...op_drf.models import CoreModel
from ...permission.models import UserProfile
"""
消息通知模型
@ -17,7 +17,7 @@ class MessagePush(CoreModel):
is_reviewed = BooleanField(default=True, verbose_name="是否审核")
status = CharField(max_length=8, verbose_name="通知状态")
to_path = CharField(max_length=256, verbose_name="跳转路径", null=True, blank=True, )
user = ManyToManyField(to="permission.UserProfile",
user = ManyToManyField(to=settings.AUTH_USER_MODEL,
related_name="user", related_query_name="user_query", through='MessagePushUser',
through_fields=('message_push', 'user'))
@ -34,7 +34,7 @@ class MessagePushUser(models.Model):
related_name="messagepushuser_message_push",
verbose_name='消息通知', help_text='消息通知')
user = ForeignKey(UserProfile, on_delete=CASCADE, db_constraint=False,
user = ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=CASCADE, db_constraint=False,
related_name="messagepushuser_user",
verbose_name='用户', help_text='用户')
is_read = BooleanField(default=False, verbose_name="是否已读")

View File

@ -302,4 +302,4 @@ class ExportCeleryLogSerializer(CustomModelSerializer):
class Meta:
model = CeleryLog
fields = ('name', 'kwargs', 'seconds', 'state', 'result', 'creator_name')
fields = ('name', 'kwargs', 'seconds', 'status', 'result', 'creator_name')

View File

@ -54,5 +54,6 @@ urlpatterns = [
re_path(r'^permission/', include('apps.vadmin.permission.urls')),
re_path(r'^system/', include('apps.vadmin.system.urls')),
re_path(r'^celery/', include('apps.vadmin.celery.urls')),
re_path(r'^monitor/', include('apps.vadmin.monitor.urls')),
]

View File

@ -27,7 +27,13 @@ def get_cache(alias=None):
logger = logging.getLogger(__name__)
def BaseCeleryApp(name):
def BaseCeleryApp(name, save_success_logs=True):
"""
celery 保存日志基础类
:param name: celery任务名字
:param save_success_logs: 是否保存成功的日志(适用于频率高的celery任务成功不需要保存日志则传False)
:return:
"""
def wraps(func):
@app.task(name=name)
@functools.wraps(func)
@ -40,6 +46,8 @@ def BaseCeleryApp(name):
res = None
try:
res = func(*args, **kwargs)
if not save_success_logs:
return res
obj.result = str(res)
obj.status = True
except Exception as exc:

View File

@ -18,6 +18,9 @@ DATABASE_NAME = "django-vue-admin"
# ================================================= #
# ************** redis 数据库配置 ************** #
# ================================================= #
# 是否启用Redis缓存
# 注不使用redis则无法使用celery
REDIS_ENABLE = True
REDIS_DB = 1
REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379

View File

@ -2,7 +2,6 @@
cd /dvadmin-backend
cp -rf ./conf/env.example.py ./conf/env.py
python3 -m pip install -i https://mirrors.aliyun.com/pypi/simple/ uwsgi==2.0.19.1
python ./manage.py makemigrations
python ./manage.py migrate
#python ./manage.py initialization

View File

@ -1,6 +1,6 @@
{
"name": "ruoyi",
"version": "3.3.0",
"name": "dvadmin",
"version": "1.1.0",
"description": "dvAdmin管理系统",
"author": "dvAdmin",
"license": "MIT",
@ -41,14 +41,16 @@
"axios": "0.21.0",
"clipboard": "2.0.6",
"core-js": "3.8.1",
"echarts": "4.9.0",
"echarts": "^4.9.0",
"element-ui": "2.15.0",
"eslint-loader": "^4.0.2",
"file-saver": "2.0.4",
"fuse.js": "6.4.3",
"highlight.js": "9.18.5",
"js-beautify": "1.13.0",
"js-cookie": "2.2.1",
"jsencrypt": "3.0.0-rc.1",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"nprogress": "0.2.0",
"quill": "1.3.7",
@ -58,6 +60,7 @@
"vue-count-to": "1.0.13",
"vue-cropper": "0.5.5",
"vue-router": "3.4.9",
"vue-types": "^2.0.3",
"vuedraggable": "2.24.3",
"vuex": "3.6.0"
},
@ -65,6 +68,7 @@
"@vue/cli-plugin-babel": "4.4.6",
"@vue/cli-plugin-eslint": "4.4.6",
"@vue/cli-service": "4.4.6",
"@vue/composition-api": "^1.0.0-rc.6",
"babel-eslint": "10.1.0",
"chalk": "4.1.0",
"connect": "3.6.6",

View File

@ -3,7 +3,7 @@ import request from '@/utils/request'
// 获取路由
export const getRouters = () => {
return request({
url: '/admin/getRouters',
url: '/admin/getRouters/',
method: 'get'
})
}

View File

@ -3,7 +3,7 @@ import request from '@/utils/request'
// 查询在线用户列表
export function list(query) {
return request({
url: '/monitor/online/list',
url: '/admin/monitor/online/list',
method: 'get',
params: query
})
@ -12,7 +12,7 @@ export function list(query) {
// 强退用户
export function forceLogout(tokenId) {
return request({
url: '/monitor/online/' + tokenId,
url: '/admin/monitor/online/' + tokenId,
method: 'delete'
})
}

View File

@ -1,9 +1,65 @@
import request from '@/utils/request'
// 查询服务器详细
export function getServer() {
// 查询服务器信息详细
export function getServerList(params) {
return request({
url: '/monitor/server',
url: 'admin/monitor/server/',
params,
method: 'get'
})
}
// 修改服务器信息
export function updateServerInfo(id, data) {
let {name, remark} = data;
return request({
url: `admin/monitor/server/${id}/`,
data: {
name,
remark
},
method: 'PUT'
})
}
// 获取监控配置信息
export function getMonitorStatusInfo() {
return request({
url: 'admin/monitor/monitor/enabled/',
method: 'get'
})
}
// 更新监控配置信息
export function updateMonitorStatusInfo(params) {
return request({
url: 'admin/monitor/monitor/enabled/',
params,
method: 'get'
})
}
// 清空记录
export function cleanMonitorLog() {
return request({
url: 'admin/monitor/monitor/clean/',
method: 'delete'
})
}
// 获取监控记录
export function getMonitorLogs(id, params) {
return request({
url: `admin/monitor/monitor/rate/${id}/`,
params,
method: 'get'
})
}
// 获取服务器最新监控日志信息
export function getServerLatestLog(id) {
return request({
url: `admin/monitor/monitor/info/${id}/`,
method: 'get'
})
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1619535267426" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="541" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M512 1024C230.4 1024 0 793.6 0 512S230.4 0 512 0s512 230.4 512 512-230.4 512-512 512z m259.2-569.6H480c-12.8 0-25.6 12.8-25.6 25.6v64c0 12.8 12.8 25.6 25.6 25.6h176c12.8 0 25.6 12.8 25.6 25.6v12.8c0 41.6-35.2 76.8-76.8 76.8h-240c-12.8 0-25.6-12.8-25.6-25.6V416c0-41.6 35.2-76.8 76.8-76.8h355.2c12.8 0 25.6-12.8 25.6-25.6v-64c0-12.8-12.8-25.6-25.6-25.6H416c-105.6 0-188.8 86.4-188.8 188.8V768c0 12.8 12.8 25.6 25.6 25.6h374.4c92.8 0 169.6-76.8 169.6-169.6v-144c0-12.8-12.8-25.6-25.6-25.6z" fill="#888888" p-id="542"></path></svg>

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

BIN
dvadmin-ui/src/assets/images/login-background.jpg Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

After

Width:  |  Height:  |  Size: 154 KiB

View File

@ -35,7 +35,7 @@ export default {
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{ path: '/index', meta: { title: '首页' }}].concat(matched)
// matched = [{ path: '/index', meta: { title: '' }}].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)

View File

@ -9,7 +9,7 @@ export default {
name: 'RuoYiDoc',
data() {
return {
url: 'http://doc.ruoyi.vip/ruoyi-vue'
url: 'http://django-vue-admin.com'
}
},
methods: {

View File

@ -1,6 +1,6 @@
<template>
<div>
<svg-icon icon-class="github" @click="goto"/>
<svg-icon icon-class="gitee" @click="goto"/>
</div>
</template>
@ -9,7 +9,7 @@ export default {
name: 'RuoYiGit',
data() {
return {
url: 'https://gitee.com/y_project/RuoYi-Vue'
url: 'https://gitee.com/liqianglog/django-vue-admin'
}
},
methods: {

View File

@ -56,14 +56,14 @@ export const constantRoutes = [
{
path: '',
component: Layout,
redirect: 'index',
redirect: '/index',
children: [
{
path: 'index',
component: (resolve) => require(['@/views/index'], resolve),
name: '首页',
meta: { title: '首页', icon: 'dashboard', noCache: true, affix: true }
}
// {
// path: 'index',
// component: (resolve) => require(['@/views/index'], resolve),
// name: '首页',
// meta: { title: '首页', icon: 'dashboard', noCache: true, affix: true }
// }
]
},
{

View File

@ -3,6 +3,7 @@ import { getRouters } from '@/api/vadmin/menu'
import Layout from '@/layout/index'
import ParentView from '@/components/ParentView';
import { handleTree } from "@/utils/ruoyi";
import de from 'element-ui/src/locale/lang/de'
const permission = {
state: {
routes: [],
@ -51,13 +52,32 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
route.component = Layout
} else if (route.component === 'ParentView') {
route.component = ParentView
} else if (typeof route.component === "string" && route.component === 'Layout/index' && !route.children) { // 首页定制
route.meta.affix = true
route.children = [
{
path: 'index',
component: 'index',
name: route.name,
meta: route.meta
}
]
route.path = ''
route.name = ''
route.redirect = '/index'
route.alwaysShow = false
route.component = Layout
} else {
route.component = loadView(route.component)
}
}
if (route.children != null && route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, route, type)
if (route.children.length === 1 && route.children[0].path === 'index') {
route.alwaysShow = false
} else {
route.alwaysShow = true
}
} else {
delete route['children']
delete route['redirect']

View File

@ -0,0 +1,102 @@
<template>
<div class="dashboard-editor-container">
<panel-group @handleSetLineChartData="handleSetLineChartData"/>
<el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
<line-chart :chart-data="lineChartData"/>
</el-row>
<el-row :gutter="32">
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<raddar-chart/>
</div>
</el-col>
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<pie-chart/>
</div>
</el-col>
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<bar-chart/>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import PanelGroup from './dashboard/PanelGroup'
import LineChart from './dashboard/LineChart'
import RaddarChart from './dashboard/RaddarChart'
import PieChart from './dashboard/PieChart'
import BarChart from './dashboard/BarChart'
const lineChartData = {
newVisitis: {
expectedData: [100, 120, 161, 134, 105, 160, 165],
actualData: [120, 82, 91, 154, 162, 140, 145]
},
messages: {
expectedData: [200, 192, 120, 144, 160, 130, 140],
actualData: [180, 160, 151, 106, 145, 150, 130]
},
purchases: {
expectedData: [80, 100, 121, 104, 105, 90, 100],
actualData: [120, 90, 100, 138, 142, 130, 130]
},
shoppings: {
expectedData: [130, 140, 141, 142, 145, 150, 160],
actualData: [120, 82, 91, 154, 162, 140, 130]
}
}
export default {
name: 'DashboardAdmin',
components: {
PanelGroup,
LineChart,
RaddarChart,
PieChart,
BarChart,
},
data() {
return {
lineChartData: lineChartData.newVisitis
}
},
methods: {
handleSetLineChartData(type) {
this.lineChartData = lineChartData[type]
}
}
}
</script>
<style lang="scss" scoped>
.dashboard-editor-container {
padding: 32px;
background-color: rgb(240, 242, 245);
position: relative;
.github-corner {
position: absolute;
top: 0px;
border: 0;
right: 0;
}
.chart-wrapper {
background: #fff;
padding: 16px 16px 0;
margin-bottom: 32px;
}
}
@media (max-width: 1024px) {
.chart-wrapper {
padding: 8px;
}
}
</style>

View File

@ -1,41 +1,51 @@
<template>
<div class="app-container home">
<el-row :gutter="20">
<!-- <el-col :sm="24" :lg="24">-->
<!-- <blockquote class="text-warning" style="font-size: 14px">-->
<!-- 领取阿里云通用云产品1888优惠券-->
<!-- <br />-->
<!-- <el-link-->
<!-- href="https://www.aliyun.com/minisite/goods?source=5176.11533457&userCode=jpef8a71"-->
<!-- type="primary"-->
<!-- target="_blank"-->
<!-- >https://www.aliyun.com/minisite/goods?source=5176.11533457&userCode=jpef8a71</el-link-->
<!-- >-->
<!-- <br />-->
<!-- 领取腾讯云通用云产品2860优惠券-->
<!-- <br />-->
<!-- <el-link-->
<!-- href="https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=198c8df2ed259157187173bc7f4f32fd&from=console"-->
<!-- type="primary"-->
<!-- target="_blank"-->
<!-- >https://cloud.tencent.com/redirect.php?redirect=1025&cps_key=198c8df2ed259157187173bc7f4f32fd&from=console</el-link-->
<!-- >-->
<!-- <br />-->
<!-- 阿里云服务器折扣区-->
<!-- <el-link href="https://www.aliyun.com/minisite/goods?source=5176.11533457&userCode=jpef8a71" type="primary" target="_blank"-->
<!-- >>点我进入</el-link-->
<!-- >-->
<!-- &nbsp;&nbsp;&nbsp; 腾讯云服务器秒杀区-->
<!-- <el-link href="http://txy.ruoyi.vip" type="primary" target="_blank"-->
<!-- >>点我进入</el-link-->
<!-- ><br />-->
<!-- <h4 class="text-danger">-->
<!-- 云产品通用红包可叠加官网常规优惠使用(仅限新用户)-->
<!-- </h4>-->
<!-- </blockquote>-->
<el-col :sm="24" :lg="24">
<blockquote class="text-warning" style="font-size: 14px">
领取阿里云通用云产品1888优惠券精选云服务器 ECS 1核2G 87.12/
<br/>
<el-link
href="https://www.aliyun.com/activity/new?source=5176.11533457&userCode=jpef8a71"
type="primary"
target="_blank"
>https://www.aliyun.com/activity/new?source=5176.11533457&userCode=jpef8a71
</el-link
>
<br/>
领取腾讯云通用云产品2860优惠券半价购买满200减100满500减250
<br/>
<el-link
href="https://curl.qcloud.com/yWalJsQY"
type="primary"
target="_blank"
>https://curl.qcloud.com/yWalJsQY
</el-link
>
<br/>
阿里云服务器折扣区
<el-link href="https://www.aliyun.com/minisite/goods?source=5176.11533457&userCode=jpef8a71" type="primary"
target="_blank"
>>点我进入
</el-link
>
&nbsp;&nbsp;&nbsp; 腾讯云服务器秒杀区
<el-link href="https://curl.qcloud.com/cRu8Ljf8" type="primary" target="_blank"
>>点我进入
</el-link
>
<br/>
<h4 class="text-danger">
云产品通用红包可叠加官网常规优惠使用(仅限新用户)
<br>
通过推广链接购买服务器者可免费提供搭建环境服务一次
</h4>
<h4 class="text-danger">
<!-- <hr />-->
<!-- </el-col>-->
</h4>
</blockquote>
<hr/>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :sm="24" :lg="12" style="padding-left: 20px">
@ -58,14 +68,16 @@
icon="el-icon-cloudy"
plain
@click="goTarget('https://gitee.com/liqianglog/django-vue-admin')"
>访问码云</el-button
>访问码云
</el-button
>
<el-button
size="mini"
icon="el-icon-s-home"
plain
@click="goTarget('https://django-vue-admin.com')"
>访问主页</el-button
@click="goTarget('http://django-vue-admin.com')"
>访问主页
</el-button
>
</p>
</el-col>
@ -83,6 +95,7 @@
<li>Python</li>
<li>Django</li>
<li>django-redis</li>
<li>django-celery-beat</li>
<li>django-rest-framework</li>
<li>django-rest-framework-jwt</li>
<li>...</li>
@ -112,18 +125,28 @@
</div>
<div class="body">
<p>
<i class="el-icon-s-promotion"></i> 官网<el-link
href="https://django-vue-admin.com"
<i class="el-icon-s-promotion"></i> 官网
<el-link
href="http://django-vue-admin.com"
target="_blank"
>https://django-vue-admin.com</el-link
>http://django-vue-admin.com
</el-link
>
</p>
<p>
<i class="el-icon-user-solid"></i> QQ群
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=E2fte0FJlSr56-thAmabGcV3Lv6vLsp9&jump_from=webapi" target="_blank"
<i class="el-icon-user-solid"></i> QQ群
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=E2fte0FJlSr56-thAmabGcV3Lv6vLsp9&jump_from=webapi"
target="_blank"
> 812482043</a
>
</p>
<p>
<img
src="@/assets/images/qq.jpg"
alt="donate"
width="25%"
/>
</p>
</div>
</el-card>
</el-col>
@ -133,6 +156,28 @@
<span>更新日志</span>
</div>
<el-collapse accordion>
<el-collapse-item title="">
<template slot="title">
v1.1.0 - 2021-05-05&nbsp;&nbsp;&nbsp;
<el-badge value="new" class="item" style="padding-top: 10px;padding-left: 10px;"/>
</template>
<ol>
<li>新增服务监控功能</li>
<li>新增操作日志功能</li>
<li>新增导入功能</li>
<li>新增celery定时任务</li>
<li>新增消息通知功能</li>
<li>新增后端接口文档</li>
<li>新增docker-compose部署</li>
<li>新增支持重写用户模型</li>
<li>数据权限完善</li>
<li>登录日志优化</li>
<li>后端代码架构优化</li>
<li>文件管理功能完善</li>
<li>修复创建用户密码问题</li>
<li>其他细节优化</li>
</ol>
</el-collapse-item>
<el-collapse-item title="v1.0.0 - 2021-03-01">
<ol>
<li>dvAdmin前后端分离系统正式发布</li>
@ -147,14 +192,14 @@
<span>捐赠支持</span>
</div>
<div class="body">
<!-- <img-->
<!-- src="https://oscimg.oschina.net/oscnet/up-d6695f82666e5018f715c41cb7ee60d3b73.png"-->
<!-- alt="donate"-->
<!-- width="100%"-->
<!-- />-->
<span style="display: inline-block; height: 30px; line-height: 30px"
>加群交流就是最好的支持~</span
>
<img
src="@/assets/images/collection_code.jpg"
alt="donate"
width="100%"
/>
<!-- <span style="display: inline-block; height: 30px; line-height: 30px"-->
<!-- >加群交流就是最好的支持~</span-->
<!-- >-->
</div>
</el-card>
</el-col>
@ -168,7 +213,7 @@ export default {
data() {
return {
//
version: "1.0.0",
version: "1.1.0",
};
},
methods: {
@ -187,12 +232,14 @@ export default {
font-size: 17.5px;
border-left: 5px solid #eee;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #eee;
}
.col-item {
margin-bottom: 20px;
}

View File

@ -48,7 +48,10 @@
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2018-2021 ruoyi.vip All Rights Reserved.</span>
<span>Copyright © 2018-2021 django-vue-admin.com All Rights Reserved.</span> |
<a href="https://beian.miit.gov.cn/#/Integrated/index"
target="_blank"
> 晋ICP备18005113号-3</a>
</div>
</div>
</template>

View File

@ -0,0 +1,216 @@
<template>
<div class="instrument-board">
<div v-if="showTopTitle && haveMultipleData" class="instrument-board-title">
<el-select :value="topTitle"
@change="chooseDisplayInstrumentBoardData"
>
<el-option
v-for="(item,index) in instrumentBoardData"
:key="index"
:label="item.name || item['dir_name']"
:value="index"
>
</el-option>
</el-select>
</div>
<div v-else-if="showTopTitle" class="instrument-board-title">
{{ topTitle }}
</div>
<div :id="ringGraphId" class="instrument-board-body"></div>
<div v-if="showSubTitle"
class="instrument-board-subtitle"
:title="subTitle.title"
>{{ subTitle.content }}
</div>
</div>
</template>
<script>
import VueTypes from 'vue-types'
// ,
const echarts = require('echarts/lib/echarts')
require('echarts/lib/chart/gauge')
//
const NORMAL_COLOR = {
color: '#28BCFE',
itemColor: ['#25bfff', '#5284de', '#2a95f9']
}
const WARNING_COLOR = {
color: '#e6a23c',
itemColor: ['#e6a23c', '#cc8b1d', '#ffaf18']
}
const DANGER_COLOR = {
color: '#F56C6C',
itemColor: ['#fd666d', '#cf1717', '#b31212']
}
export default {
name: 'InstrumentBoard',
props: {
// key
ringGraphKey: VueTypes.string.isRequired,
//
showTopTitle: VueTypes.bool.def(false),
//
showSubTitle: VueTypes.bool.def(false),
// top title
topTitleKeyToNameMapping: VueTypes.object.def({
cpu: 'CPU使用率',
memory: '内存使用率'
}),
instrumentBoardData: VueTypes.any.isRequired
},
data() {
return {
//
currentInstrumentBoardData: {}
}
},
computed: {
//
haveMultipleData() {
return this.instrumentBoardData instanceof Array && this.instrumentBoardData.length > 0
},
// 使
ringRate() {
let ringRate = this.currentInstrumentBoardData.rate
ringRate = ringRate < 1 ? ringRate * 100 : ringRate
return parseFloat(ringRate.toFixed(4))
},
// id
ringGraphId() {
return `${this.ringGraphKey}UsingRate`
},
//
topTitle() {
return this.currentInstrumentBoardData['dir_name'] || this.topTitleKeyToNameMapping[this.ringGraphKey] || this.ringGraphKey
},
//
subTitle() {
let used = this.currentInstrumentBoardData['used'] ? this.currentInstrumentBoardData['used'] + '/' : ''
let total = this.currentInstrumentBoardData['total'] ? this.currentInstrumentBoardData['total'] : ''
let unit = this.currentInstrumentBoardData['unit'] ? ` (${this.currentInstrumentBoardData['unit']})` : ''
let content = `${used}${total}${unit} `
let title = (this.currentInstrumentBoardData['used'] ? '已用/' : '') + '总量(单位)'
return { content, title }
},
// 使
usingRateStyle() {
return {
fontSize: 18,
...this.getCircleColor(this.ringRate)
}
}
},
mounted() {
if (this.haveMultipleData) {
this.currentInstrumentBoardData = this.instrumentBoardData[0]
} else {
this.currentInstrumentBoardData = this.instrumentBoardData
}
this.drawBar()
},
methods: {
drawBar() {
let currentRate = [this.ringRate]
// domecharts
let RingGraph = echarts.init(document.getElementById(this.ringGraphId))
let option = {
title: {
text: currentRate + '%',
textStyle: this.usingRateStyle,
itemGap: 10,
left: 'center',
top: '45%'
},
angleAxis: {
max: 100,
clockwise: true, //
// 线
show: false
},
radiusAxis: {
type: 'category',
show: true,
axisLabel: {
show: false
},
axisLine: {
show: false
},
axisTick: {
show: false
}
},
polar: {
center: ['50%', '50%'], //
radius: '100%' //
},
series: [{
type: 'bar',
data: currentRate,
showBackground: true,
backgroundStyle: {
color: '#BDEBFF' //
},
coordinateSystem: 'polar',
roundCap: true,
barWidth: 15,
itemStyle: {
normal: {
opacity: 1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: this.usingRateStyle.itemColor[0] || '#25BFFF'
}, {
offset: 1,
color: this.usingRateStyle.itemColor[1] || '#5284DE'
}]),
shadowBlur: 1,
shadowColor: this.usingRateStyle.itemColor[2] || '#2A95F9'
}
}
}]
}
//
RingGraph.setOption(option)
},
// -
getCircleColor(usingRate) {
if (usingRate < 60) {
return NORMAL_COLOR
} else if (usingRate > 60 && usingRate < 80) {
return WARNING_COLOR
} else if (usingRate > 80) {
return DANGER_COLOR
}
return NORMAL_COLOR
},
chooseDisplayInstrumentBoardData(index) {
this.currentInstrumentBoardData = this.instrumentBoardData[index]
this.drawBar()
}
}
}
</script>
<style scoped>
.instrument-board-title {
font-weight: bolder;
text-align: center;
}
.instrument-board-body {
min-height: 200px;
min-width: 200px;
}
.instrument-board-subtitle {
text-align: center;
}
</style>

View File

@ -0,0 +1,195 @@
<template>
<div>
<div class="line-chart-title">
<div class="line-chart-name">{{ chartTitle }}</div>
<div class="line-chart-time-radio">
<el-radio-group v-model="timeLimit" @change="changeTimeLimit">
<el-radio-button
v-for="item in Object.keys(TIME_LIMIT_SETTING)"
:label="TIME_LIMIT_SETTING[item].name"
:key="TIME_LIMIT_SETTING[item].key"
></el-radio-button>
</el-radio-group>
</div>
</div>
<div :id="lineChartId" class="line-chart-body"></div>
</div>
</template>
<script>
import VueTypes from 'vue-types'
// ,
import echarts from 'echarts'
import moment from 'moment'
import { getMonitorLogs } from '@/api/vadmin/monitor/server'
const MONTH = moment().month()
const YEAR = moment().year()
const TODAY = moment().format('YYYY-MM-DD')
const YESTERDAY = moment().subtract(1, 'days').format('YYYY-MM-DD')
const LAST_SEVEN_DAYS = moment().subtract(7, 'days').format('YYYY-MM-DD')
const LAST_THIRTY_DAYS = moment().subtract(30, 'days').format('YYYY-MM-DD')
//
const TIME_LIMIT_SETTING = {
'yesterday': {
key: 'yesterday',
name: '昨天',
timeRange: [
`${YESTERDAY} 00:00:00`,
`${YESTERDAY} 23:59:59`
]
},
'today': {
key: 'today',
name: '今天',
timeRange: [
`${TODAY} 00:00:00`,
`${TODAY} 23:59:59`
]
},
'latestWeek': {
key: 'latestWeek',
name: '最近7天',
timeRange: [
`${LAST_SEVEN_DAYS} 00:00:00`,
`${TODAY} 23:59:59`
]
},
'latestMonth': {
key: 'latestMonth',
name: '最近30天',
timeRange: [
`${LAST_THIRTY_DAYS} 00:00:00`,
`${TODAY} 23:59:59`
]
}
}
//
const DEFAULT_TIME = '今天'
export default {
name: 'LineChart',
props: {
serverInfo: VueTypes.object.isRequired,
lineChartKey: VueTypes.string.isRequired,
chartTitle: VueTypes.string.isRequired,
chartData: VueTypes.array.isRequired,
chartTime: VueTypes.array.isRequired,
},
data() {
return {
TIME_LIMIT_SETTING,
timeLimit: DEFAULT_TIME,
lineChartId: this.lineChartKey + 'Chart',
lineChartData: this.chartData,
lineChartTime: this.chartTime
}
},
mounted() {
this.drawBar()
},
computed: {
timeLimitNames() {
return Object.keys(TIME_LIMIT_SETTING).map(item => {
return TIME_LIMIT_SETTING[item].name
})
},
currentDateIndex() {
return this.timeLimitNames.indexOf(this.timeLimit)
},
currentTimeLimitSetting() {
return TIME_LIMIT_SETTING[Object.keys(TIME_LIMIT_SETTING)[this.currentDateIndex]]
}
},
methods: {
drawBar() {
// domecharts
let barGraph = echarts.init(document.getElementById(this.lineChartId))
//
barGraph.setOption({
tooltip : {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985',
}
}
},
legend: {
left: 'center',
data: [],
bottom: 0
},
xAxis: {
type: 'category',
name: 'x',
splitLine: { show: false },
data:this.lineChartTime,
offset:20
},
grid: {
left: '1%',
right: '2%',
bottom: '8%',
containLabel: true
},
yAxis: {
type: 'value',
name: '使用率',
axisLabel: {
show: true,
interval: 'auto',
formatter: '{value}%'
},
},
series: [
{
name: '使用率',
type: 'line',
data: this.lineChartData,
}
],
})
},
changeTimeLimit(value) {
this.timeLimit = value
this.getCurrentServerMonitorLogs()
},
getCurrentServerMonitorLogs() {
getMonitorLogs(this.serverInfo.id, {as: JSON.stringify({create_datetime__range: this.currentTimeLimitSetting.timeRange})}).then(results => {
this.lineChartData = results.data[this.lineChartKey]
this.lineChartTime = results.data["datetime"]
this.drawBar()
}).catch(error => {
this.$message.warning(error.msg || `获取${this.chartTitle}数据失败!`)
})
}
}
}
</script>
<style scoped>
.line-chart-name {
font-weight: bolder;
width: 20%;
min-width: 30px;
text-align: left;
display: inline-block;
}
.line-chart-time-radio {
font-weight: bolder;
width: 80%;
min-width: 200px;
text-align: right;
display: inline-block;
}
.line-chart-body {
min-height: 300px;
min-width: 300px;
}
</style>

View File

@ -1,210 +1,500 @@
<template>
<div class="app-container">
<el-row>
<el-col :span="12" class="card-box">
<el-card>
<div slot="header"><span>CPU</span></div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<table cellspacing="0" style="width: 100%;">
<thead>
<tr>
<th class="is-leaf"><div class="cell">属性</div></th>
<th class="is-leaf"><div class="cell"></div></th>
</tr>
</thead>
<tbody>
<tr>
<td><div class="cell">核心数</div></td>
<td><div class="cell" v-if="server.cpu">{{ server.cpu.cpuNum }}</div></td>
</tr>
<tr>
<td><div class="cell">用户使用率</div></td>
<td><div class="cell" v-if="server.cpu">{{ server.cpu.used }}%</div></td>
</tr>
<tr>
<td><div class="cell">系统使用率</div></td>
<td><div class="cell" v-if="server.cpu">{{ server.cpu.sys }}%</div></td>
</tr>
<tr>
<td><div class="cell">当前空闲率</div></td>
<td><div class="cell" v-if="server.cpu">{{ server.cpu.free }}%</div></td>
</tr>
</tbody>
</table>
<!-- 监控控制 -->
<div class="server-monitor-control">
<!-- 监控启用开关 -->
<div class="control-server-monitor same-block">
开启监控
<el-switch
v-model="isOpeningMonitor"
active-color="#13ce66"
inactive-color="#ff4949"
title="控制所有监控项"
@change="changeMonitorStatus"
>
</el-switch>
</div>
</el-card>
</el-col>
<!-- 更新频率设置 -->
<div class="monitor-update-interval same-block">
监控频率
<el-input-number v-model="monitorUpdateInterval"
label=""
class="monitor-update-interval-blank"
controls-position="right"
:min="minMonitorUpdateInterval"
@input="handleIntervalChange"
<el-col :span="12" class="card-box">
<el-card>
<div slot="header"><span>内存</span></div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<table cellspacing="0" style="width: 100%;">
<thead>
<tr>
<th class="is-leaf"><div class="cell">属性</div></th>
<th class="is-leaf"><div class="cell">内存</div></th>
<th class="is-leaf"><div class="cell">JVM</div></th>
</tr>
</thead>
<tbody>
<tr>
<td><div class="cell">总内存</div></td>
<td><div class="cell" v-if="server.mem">{{ server.mem.total }}G</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.total }}M</div></td>
</tr>
<tr>
<td><div class="cell">已用内存</div></td>
<td><div class="cell" v-if="server.mem">{{ server.mem.used}}G</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.used}}M</div></td>
</tr>
<tr>
<td><div class="cell">剩余内存</div></td>
<td><div class="cell" v-if="server.mem">{{ server.mem.free }}G</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.free }}M</div></td>
</tr>
<tr>
<td><div class="cell">使用率</div></td>
<td><div class="cell" v-if="server.mem" :class="{'text-danger': server.mem.usage > 80}">{{ server.mem.usage }}%</div></td>
<td><div class="cell" v-if="server.jvm" :class="{'text-danger': server.jvm.usage > 80}">{{ server.jvm.usage }}%</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
></el-input-number>
<el-select v-model="intervalType"
class="monitor-update-interval-unit"
@change="selectIntervalType"
>
<el-option
v-for="item in Object.keys(INTERVAL_ID_TO_TYPE_MAPPING)"
:key="INTERVAL_ID_TO_TYPE_MAPPING[item].type"
:label="INTERVAL_ID_TO_TYPE_MAPPING[item].name"
:value="INTERVAL_ID_TO_TYPE_MAPPING[item].name">
</el-option>
</el-select>
<el-col :span="24" class="card-box">
<el-card>
<div slot="header">
<span>服务器信息</span>
</div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<table cellspacing="0" style="width: 100%;">
<tbody>
<tr>
<td><div class="cell">服务器名称</div></td>
<td><div class="cell" v-if="server.sys">{{ server.sys.computerName }}</div></td>
<td><div class="cell">操作系统</div></td>
<td><div class="cell" v-if="server.sys">{{ server.sys.osName }}</div></td>
</tr>
<tr>
<td><div class="cell">服务器IP</div></td>
<td><div class="cell" v-if="server.sys">{{ server.sys.computerIp }}</div></td>
<td><div class="cell">系统架构</div></td>
<td><div class="cell" v-if="server.sys">{{ server.sys.osArch }}</div></td>
</tr>
</tbody>
</table>
<!-- 监控日志保存时间 -->
<div class="monitor-log-save-time same-block">
保存天数
<el-input v-model="monitorLogSavingDays" class=" same-block" style="width: 120px;"></el-input>
<el-button type="primary"
class="same-block"
title="只有提交更改才会生效"
@click="updateMonitorStatusSettingsInfo"
>更改
</el-button>
</div>
<!-- 清空记录 -->
<div class="clean-monitor-log same-block">
<el-button class="same-block"
type="warning"
title="清空所有监控记录"
@click="cleanMonitorLogsInfo"
>清空记录
</el-button>
</div>
</div>
</el-card>
</el-col>
<el-col :span="24" class="card-box">
<el-card>
<div slot="header">
<span>Java虚拟机信息</span>
<div class="server-monitor-top">
<!-- 左侧服务器信息 -->
<el-card class="box-card server-information">
<div slot="header" class="clearfix">
<div class="server-info-item">服务器</div>
<el-select filterable
:value="currentServerName"
class="server-info-item"
placeholder="请选择服务器"
@change="chooseServerInfo"
>
<el-option
v-for="(item,index) in allServerInfo"
:key="item.id"
:label="item.name || item.ip"
:value="index"
>
</el-option>
</el-select>
<el-button type="primary"
class="server-info-item"
title="只有提交更改才会生效"
@click="updateServerInfo"
>更改
</el-button>
</div>
<div class="server-info-detail">
<div v-for="(key,index) in currentServerInfoKeys" :key="index" class="server-info-detail-line text item">
<div class="server-info-detail-item">
<div style="width: 30%;display: inline-block;">{{ SERVER_KEY_TO_NAME_MAPPING[key] }}:</div>
<div v-if="CHANGEABLE_SERVER_FIELDS.indexOf(key) > -1" style="display: inline-block;">
<el-input style="display: inline-block; width: 90%;" v-model="currentServer[key]"></el-input>
</div>
<div v-else style="display: inline-block; "> {{ currentServer[key] }}</div>
</div>
</div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<table cellspacing="0" style="width: 100%;">
<tbody>
<tr>
<td><div class="cell">Java名称</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.name }}</div></td>
<td><div class="cell">Java版本</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.version }}</div></td>
</tr>
<tr>
<td><div class="cell">启动时间</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.startTime }}</div></td>
<td><div class="cell">运行时长</div></td>
<td><div class="cell" v-if="server.jvm">{{ server.jvm.runTime }}</div></td>
</tr>
<tr>
<td colspan="1"><div class="cell">安装路径</div></td>
<td colspan="3"><div class="cell" v-if="server.jvm">{{ server.jvm.home }}</div></td>
</tr>
<tr>
<td colspan="1"><div class="cell">项目路径</div></td>
<td colspan="3"><div class="cell" v-if="server.sys">{{ server.sys.userDir }}</div></td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
<el-col :span="24" class="card-box">
<el-card>
<div slot="header">
<span>磁盘状态</span>
</div>
<div class="el-table el-table--enable-row-hover el-table--medium">
<table cellspacing="0" style="width: 100%;">
<thead>
<tr>
<th class="is-leaf"><div class="cell">盘符路径</div></th>
<th class="is-leaf"><div class="cell">文件系统</div></th>
<th class="is-leaf"><div class="cell">盘符类型</div></th>
<th class="is-leaf"><div class="cell">总大小</div></th>
<th class="is-leaf"><div class="cell">可用大小</div></th>
<th class="is-leaf"><div class="cell">已用大小</div></th>
<th class="is-leaf"><div class="cell">已用百分比</div></th>
</tr>
</thead>
<tbody v-if="server.sysFiles">
<tr v-for="sysFile in server.sysFiles">
<td><div class="cell">{{ sysFile.dirName }}</div></td>
<td><div class="cell">{{ sysFile.sysTypeName }}</div></td>
<td><div class="cell">{{ sysFile.typeName }}</div></td>
<td><div class="cell">{{ sysFile.total }}</div></td>
<td><div class="cell">{{ sysFile.free }}</div></td>
<td><div class="cell">{{ sysFile.used }}</div></td>
<td><div class="cell" :class="{'text-danger': sysFile.usage > 80}">{{ sysFile.usage }}%</div></td>
</tr>
</tbody>
</table>
</div>
<!-- 右侧仪表盘 -->
<el-card class="box-card information-instrument-panel"
v-for="(key, index) of Object.keys(instrumentBoardData)"
:key="`${index}-${key}`">
<instrument-board
:show-top-title="true"
:show-sub-title="true"
:ring-graph-key="key"
:instrument-board-data="instrumentBoardData[key]"
:top-title-key-to-name-mapping="INSTRUMENT_BOARD_KEY_TO_NAME_MAPPING"
></instrument-board>
</el-card>
</el-col>
</el-row>
</div>
<!-- 下方折线图 -->
<div class="server-monitor-bottom">
<!-- 折线图 -->
<el-card class="box-card server-monitor-line-chart" v-for="(key, index) in Object.keys(lineChartData).slice(0,2)"
:key="`${index}-${key}`">
<line-chart :line-chart-key="key"
:server-info="currentServer"
:chart-title="CHART_KEY_NAME_MAPPING[key]"
:chart-data="lineChartData[key]"
:chart-time="lineChartData['datetime']"
></line-chart>
</el-card>
</div>
</div>
</template>
<script>
import { getServer } from "@/api/vadmin/monitor/server";
import {
cleanMonitorLog,
getMonitorLogs,
getMonitorStatusInfo,
getServerLatestLog,
getServerList,
updateMonitorStatusInfo,
updateServerInfo
} from '@/api/vadmin/monitor/server'
import InstrumentBoard from '@/views/vadmin/monitor/server/components/InstrumentBoard'
import LineChart from '@/views/vadmin/monitor/server/components/LineChart'
import moment from 'moment'
const debounce = require('lodash/debounce')
// key -> name
const SERVER_KEY_TO_NAME_MAPPING = {
ip: '服务器IP',
name: '服务器名称',
os: '操作系统',
remark: '备注'
}
//
const INTERVAL_ID_TO_TYPE_MAPPING = {
0: {
type: 0,
name: '秒',
key: 'seconds',
second: 1
},
1: {
type: 1,
name: '分钟',
key: 'minutes',
second: 60
},
2: {
type: 2,
name: '小时',
key: 'hours',
second: 60 * 60
},
3: {
type: 3,
name: '天',
key: 'days',
second: 24 * 60 * 60
}
}
const defaultUpdateInterval = INTERVAL_ID_TO_TYPE_MAPPING['0']
//
const CHART_KEY_NAME_MAPPING = {
cpu: 'CPU',
memory: '内存',
disk: '磁盘'
}
//
const INSTRUMENT_BOARD_KEY_TO_NAME_MAPPING = {
cpu: 'CPU使用率',
memory: '内存使用率'
}
//
const CHANGEABLE_SERVER_FIELDS = ['name', 'remark']
export default {
name: "Server",
name: 'Server',
components: {
InstrumentBoard,
LineChart
},
data() {
return {
SERVER_KEY_TO_NAME_MAPPING,
INTERVAL_ID_TO_TYPE_MAPPING,
CHART_KEY_NAME_MAPPING,
CHANGEABLE_SERVER_FIELDS,
INSTRUMENT_BOARD_KEY_TO_NAME_MAPPING,
timeRange: [
`${moment().format('YYYY-MM-DD')} 00:00:00`,
`${moment().format('YYYY-MM-DD')} 23:59:59`
],
//
loading: [],
//
server: []
};
//
allServerInfo: [],
//
currentServerName: '',
//
currentServer: {},
//
currentServerIndex: 0,
//
isOpeningMonitor: false,
//
monitorUpdateInterval: 60,
//
minMonitorUpdateInterval: 0,
//
intervalType: defaultUpdateInterval.name,
//
intervalTypeUnits: defaultUpdateInterval.second,
//
monitorLogSavingDays: 30,
// 线
lineChartData: {},
//
instrumentBoardData: {}
}
},
computed: {
currentServerInfoKeys() {
return Object.keys(this.currentServer).filter(key => {
if (SERVER_KEY_TO_NAME_MAPPING[key]) {
return { [key]: SERVER_KEY_TO_NAME_MAPPING[key] }
}
})
},
intervalNameToSecondMapping() {
let intervalNameToSecondMapping = {}
Object.values(INTERVAL_ID_TO_TYPE_MAPPING).forEach(item => {
intervalNameToSecondMapping[item.name] = item.second
})
return intervalNameToSecondMapping
},
monitorStatusInfo() {
return {
enabled: this.isOpeningMonitor ? 1 : 0,
save_days: this.monitorLogSavingDays,
interval: this.monitorUpdateInterval * this.intervalTypeUnits
}
}
},
watch: {
currentServer(newServerInfo) {
if (newServerInfo) {
//
this.getServerLatestLogInfo(newServerInfo.id)
//
this.getCurrentServerMonitorLogs()
}
}
},
created() {
this.getList();
this.openLoading();
this.openLoading()
//
this.getServerList(this.currentServerIndex)
//
this.getMonitorStatusSettingsInfo()
},
methods: {
/** 查询服务器信息 */
getList() {
getServer().then(response => {
this.server = response.data;
this.loading.close();
});
/** 查询所有服务器基础信息 */
getServerList(serverIndex) {
getServerList({ pageNum: 'all' }).then(response => {
this.allServerInfo = response.data
if (this.allServerInfo.length > 0) {
this.currentServer = this.allServerInfo[serverIndex || this.currentServerIndex]
this.currentServerName = this.currentServer.name || this.currentServer.ip
}
this.loading.close()
})
},
/**修改服务器信息*/
updateServerInfo() {
updateServerInfo(this.currentServer.id, this.currentServer).then(() => {
this.msgSuccess('修改服务器信息成功!')
}).catch(error => {
this.$message.error(error.msg || '提交修改服务器信息出错!')
}).finally(() => {
this.getServerList()
})
},
/** 获取服务器最新监控信息 */
getServerLatestLogInfo(serverId) {
getServerLatestLog(serverId).then(results => {
this.instrumentBoardData = results.data
}).catch(error => {
this.msgError(error.msg || '获取服务器最新监控信息错误!')
})
},
/** 获取监控日志信息 */
getCurrentServerMonitorLogs() {
getMonitorLogs(this.currentServer.id, { as: JSON.stringify( { 'create_datetime__range': this.timeRange })}).then(results => {
this.lineChartData = results.data
}).catch(error => {
this.msgError(error.msg || '获取监控日志信息出错误!')
})
},
/** 清除监控日志 */
cleanMonitorLogsInfo() {
this.$confirm('此操作将删除所有的监控记录,是否继续?', '提示', {
confirmButtonText: '确定删除',
cancelButtonText: '放弃'
}).then(() => {
cleanMonitorLog().then(results => {
this.msgSuccess('清除记录成功!')
}).catch(error => {
this.$message.warning(error.msg || '清除记录失败,请重试!')
})
}).catch(() => {
})
},
/** 获取监控配置信息 */
getMonitorStatusSettingsInfo() {
getMonitorStatusInfo().then(results => {
let { enabled, interval, save_days } = results.data
this.isOpeningMonitor = enabled
this.monitorLogSavingDays = parseInt(save_days)
this.formatInterval(parseInt(interval))
}).catch(error => {
this.msgError(error.msg || '获取服务器监控配置信息出错误!')
})
},
/** 更新监控配置信息 */
updateMonitorStatusSettingsInfo() {
updateMonitorStatusInfo(this.monitorStatusInfo).then(() => {
this.msgSuccess('更新配置成功!')
}).catch((error) => {
this.msgError(error.msg || '更新服务器监控配置信息出错误!')
})
},
//
openLoading() {
this.loading = this.$loading({
lock: true,
text: "拼命读取中",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)"
});
text: '拼命读取中',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
})
},
//
chooseServerInfo(index) {
this.currentServerIndex = index
this.currentServer = this.allServerInfo[index]
this.currentServerName = this.currentServer.name || this.currentServer.ip
},
//
handleIntervalChange: debounce(function(value) {
this.monitorUpdateInterval = value
}, 500),
//
selectIntervalType(value) {
this.intervalType = value
this.intervalTypeUnits = this.intervalNameToSecondMapping[value]
},
//
changeMonitorStatus(value) {
this.isOpeningMonitor = value
},
//
formatInterval(intervalTime) {
let biggerInterval = 0
for (let interval of Object.values(INTERVAL_ID_TO_TYPE_MAPPING)) {
if (interval.second > biggerInterval && interval.second < intervalTime) {
biggerInterval = interval.second
this.monitorUpdateInterval = intervalTime / interval.second
this.intervalType = interval.name
this.intervalTypeUnits = interval.second
}
}
}
}
}
};
</script>
<style scoped>
.el-button--medium {
margin: 2px;
padding: 10px 10px;
}
.server-monitor-top {
padding: 10px 10px;
text-align: justify-all;
overflow-x: scroll;
display: flex;
}
.server-monitor-bottom {
text-align: left;
overflow-x: scroll;
display: flex;
}
.server-information {
width: 20%;
min-width: 400px;
min-height: 300px;
display: inline-block;
}
.information-instrument-panel {
width: 20%;
display: inline-block;
min-height: 300px;
min-width: 300px;
margin: 0 10px;
}
.server-info-item {
display: inline-block;
margin: 0 5px;
}
.server-info-detail {
min-height: 200px;
}
.server-info-detail-line {
margin: 5px;
min-height: 20px;
}
.server-info-detail-item {
text-align: justify;
line-height: 40px;
margin: 4px 0;
overflow: auto;
}
.server-monitor-control {
width: 100%;
height: 60px;
line-height: 60px;
padding: 0 20px;
}
.monitor-update-interval {
margin: 0 20px;
}
.same-block {
display: inline-block;
}
.monitor-update-interval-blank {
width: 100px;
margin: 0 2px;
}
.monitor-update-interval-unit {
width: 80px;
margin: 0 2px;
}
.monitor-log-save-time {
width: 280px;
margin: 0 2px;
}
.clean-monitor-log {
}
.server-monitor-line-chart {
height: 400px;
width: 45%;
min-width: 500px;
margin: 10px;
display: inline-block;
overflow-x: scroll;
}
</style>

View File

@ -161,7 +161,7 @@
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="(role,index) in scope.row.role" :key="index">{{role.roleName}}</el-dropdown-item>
<el-dropdown-item v-for="role in scope.row.role">{{role.roleName}}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
@ -576,7 +576,7 @@
password: undefined,
mobile: undefined,
email: undefined,
gender: this.selectDictDefault(this.sexOptions),
gender: undefined,
is_active: false,
remark: undefined,
postIds: [],
@ -648,7 +648,6 @@
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.id != undefined) {
this.form.creator = undefined
updateUser(this.form).then(response => {
this.msgSuccess('修改成功')
this.open = false

View File

@ -117,7 +117,7 @@
<el-table-column label="字典名称" align="center" prop="dictName" :show-overflow-tooltip="true" />
<el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<router-link :to="'/dict/type/data/' + scope.row.id" class="link-type">
<router-link :to="hasPermi(['system:dict:type:get']) ?'/dict/type/data/' + scope.row.id :'#'" class="link-type">
<span>{{ scope.row.dictType }}</span>
</router-link>
</template>