Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a2739774a | ||
|
|
012f3a2f63 | ||
|
|
64c5d505e7 | ||
|
|
3914432d9f | ||
|
|
cc6ac68223 | ||
|
|
05ee833fe5 | ||
|
|
ff736aae93 | ||
|
|
c0a68f91ca | ||
|
|
8d6abeb891 | ||
|
|
163e5eb2db | ||
|
|
e786f60cdd | ||
|
|
abe2db3c82 | ||
|
|
fa734dd479 | ||
|
|
6e9b94aed2 | ||
|
|
2a9f5be895 | ||
|
|
2ea34bfbd5 | ||
|
|
edbd6862a2 |
@@ -8,14 +8,25 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
from channels.routing import ProtocolTypeRouter
|
||||
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
|
||||
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
||||
|
||||
http_application = get_asgi_application()
|
||||
from application.ws_routing import websocket_urlpatterns
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": http_application,
|
||||
'websocket': AllowedHostsOriginValidator(
|
||||
AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
websocket_urlpatterns # 指明路由文件是devops/routing.py
|
||||
)
|
||||
)
|
||||
),
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ Including another URLconf
|
||||
"""
|
||||
from django.conf.urls.static import static
|
||||
from django.urls import path, include, re_path
|
||||
from django.views.static import serve
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
from rest_framework import permissions
|
||||
|
||||
183
backend/application/websocketConfig.py
Normal file
183
backend/application/websocketConfig.py
Normal file
@@ -0,0 +1,183 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import urllib
|
||||
|
||||
from asgiref.sync import sync_to_async, async_to_sync
|
||||
from channels.db import database_sync_to_async
|
||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer, AsyncWebsocketConsumer
|
||||
import json
|
||||
|
||||
from channels.layers import get_channel_layer
|
||||
from jwt import InvalidSignatureError
|
||||
from rest_framework.request import Request
|
||||
|
||||
from application import settings
|
||||
from dvadmin.system.models import MessageCenter, Users, MessageCenterTargetUser
|
||||
from dvadmin.system.views.message_center import MessageCenterTargetUserSerializer
|
||||
from dvadmin.utils.serializers import CustomModelSerializer
|
||||
|
||||
send_dict = {}
|
||||
|
||||
|
||||
# 发送消息结构体
|
||||
def set_message(sender, msg_type, msg, unread=0):
|
||||
text = {
|
||||
'sender': sender,
|
||||
'contentType': msg_type,
|
||||
'content': msg,
|
||||
'unread': unread
|
||||
}
|
||||
return text
|
||||
|
||||
|
||||
# 异步获取消息中心的目标用户
|
||||
@database_sync_to_async
|
||||
def _get_message_center_instance(message_id):
|
||||
from dvadmin.system.models import MessageCenter
|
||||
_MessageCenter = MessageCenter.objects.filter(id=message_id).values_list('target_user', flat=True)
|
||||
if _MessageCenter:
|
||||
return _MessageCenter
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def _get_message_unread(user_id):
|
||||
"""获取用户的未读消息数量"""
|
||||
from dvadmin.system.models import MessageCenterTargetUser
|
||||
count = MessageCenterTargetUser.objects.filter(users=user_id, is_read=False).count()
|
||||
return count or 0
|
||||
|
||||
|
||||
def request_data(scope):
|
||||
query_string = scope.get('query_string', b'').decode('utf-8')
|
||||
qs = urllib.parse.parse_qs(query_string)
|
||||
return qs
|
||||
|
||||
|
||||
class DvadminWebSocket(AsyncJsonWebsocketConsumer):
|
||||
async def connect(self):
|
||||
try:
|
||||
import jwt
|
||||
self.service_uid = self.scope["url_route"]["kwargs"]["service_uid"]
|
||||
decoded_result = jwt.decode(self.service_uid, settings.SECRET_KEY, algorithms=["HS256"])
|
||||
if decoded_result:
|
||||
self.user_id = decoded_result.get('user_id')
|
||||
self.chat_group_name = "user_" + str(self.user_id)
|
||||
# 收到连接时候处理,
|
||||
await self.channel_layer.group_add(
|
||||
self.chat_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
await self.accept()
|
||||
# 主动推送消息
|
||||
unread_count = await _get_message_unread(self.user_id)
|
||||
if unread_count == 0:
|
||||
# 发送连接成功
|
||||
await self.send_json(set_message('system', 'SYSTEM', '您已上线'))
|
||||
else:
|
||||
await self.send_json(
|
||||
set_message('system', 'SYSTEM', "请查看您的未读消息~",
|
||||
unread=unread_count))
|
||||
except InvalidSignatureError:
|
||||
await self.disconnect(None)
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
# Leave room group
|
||||
await self.channel_layer.group_discard(self.chat_group_name, self.channel_name)
|
||||
print("连接关闭")
|
||||
try:
|
||||
await self.close(close_code)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class MegCenter(DvadminWebSocket):
|
||||
"""
|
||||
消息中心
|
||||
"""
|
||||
|
||||
async def receive(self, text_data):
|
||||
# 接受客户端的信息,你处理的函数
|
||||
text_data_json = json.loads(text_data)
|
||||
message_id = text_data_json.get('message_id', None)
|
||||
user_list = await _get_message_center_instance(message_id)
|
||||
for send_user in user_list:
|
||||
await self.channel_layer.group_send(
|
||||
"user_" + str(send_user),
|
||||
{'type': 'push.message', 'json': text_data_json}
|
||||
)
|
||||
|
||||
async def push_message(self, event):
|
||||
"""消息发送"""
|
||||
message = event['json']
|
||||
await self.send(text_data=json.dumps(message))
|
||||
|
||||
|
||||
class MessageCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
消息中心-新增-序列化器
|
||||
"""
|
||||
class Meta:
|
||||
model = MessageCenter
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
def websocket_push(user_id, message):
|
||||
username = "user_" + str(user_id)
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
username,
|
||||
{
|
||||
"type": "push.message",
|
||||
"json": message
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def create_message_push(title: str, content: str, target_type: int = 0, target_user: list = None, target_dept=None,
|
||||
target_role=None, message: dict = None, request=Request):
|
||||
if message is None:
|
||||
message = {"contentType": "INFO", "content": None}
|
||||
if target_role is None:
|
||||
target_role = []
|
||||
if target_dept is None:
|
||||
target_dept = []
|
||||
data = {
|
||||
"title": title,
|
||||
"content": content,
|
||||
"target_type": target_type,
|
||||
"target_user": target_user,
|
||||
"target_dept": target_dept,
|
||||
"target_role": target_role
|
||||
}
|
||||
message_center_instance = MessageCreateSerializer(data=data, request=request)
|
||||
message_center_instance.is_valid(raise_exception=True)
|
||||
message_center_instance.save()
|
||||
users = target_user or []
|
||||
if target_type in [1]: # 按角色
|
||||
users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True)
|
||||
if target_type in [2]: # 按部门
|
||||
users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True)
|
||||
if target_type in [3]: # 系统通知
|
||||
users = Users.objects.values_list('id', flat=True)
|
||||
targetuser_data = []
|
||||
for user in users:
|
||||
targetuser_data.append({
|
||||
"messagecenter": message_center_instance.instance.id,
|
||||
"users": user
|
||||
})
|
||||
targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=request)
|
||||
targetuser_instance.is_valid(raise_exception=True)
|
||||
targetuser_instance.save()
|
||||
for user in users:
|
||||
username = "user_" + str(user)
|
||||
unread_count = async_to_sync(_get_message_unread)(user)
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
username,
|
||||
{
|
||||
"type": "push.message",
|
||||
"json": {**message, 'unread': unread_count}
|
||||
}
|
||||
)
|
||||
7
backend/application/ws_routing.py
Normal file
7
backend/application/ws_routing.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.urls import path
|
||||
from application.websocketConfig import MegCenter
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path('ws/<str:service_uid>/', MegCenter.as_asgi()), # consumers.DvadminWebSocket 是该路由的消费者
|
||||
]
|
||||
@@ -63,6 +63,8 @@ class Users(CoreModel, AbstractUser):
|
||||
help_text="关联岗位")
|
||||
role = models.ManyToManyField(to="Role", blank=True, verbose_name="关联角色", db_constraint=False,
|
||||
help_text="关联角色")
|
||||
current_role = models.ForeignKey(to=Role, null=True, blank=True, db_constraint=False, on_delete=models.SET_NULL,
|
||||
verbose_name="当前登录角色", help_text="当前登录角色", related_name='current_role_set')
|
||||
dept = models.ForeignKey(
|
||||
to="Dept",
|
||||
verbose_name="所属部门",
|
||||
@@ -72,6 +74,14 @@ class Users(CoreModel, AbstractUser):
|
||||
blank=True,
|
||||
help_text="关联部门",
|
||||
)
|
||||
manage_dept = models.ManyToManyField(
|
||||
to="Dept",
|
||||
verbose_name="管理部门",
|
||||
db_constraint=False,
|
||||
blank=True,
|
||||
help_text="管理部门",
|
||||
related_name='manage_dept_set'
|
||||
)
|
||||
login_error_count = models.IntegerField(default=0, verbose_name="登录错误次数", help_text="登录错误次数")
|
||||
pwd_change_count = models.IntegerField(default=0,blank=True, verbose_name="密码修改次数", help_text="密码修改次数")
|
||||
objects = CustomUserManager()
|
||||
@@ -168,6 +178,23 @@ class Dept(CoreModel):
|
||||
cls.recursion_all_dept(ele.get("id"), dept_all_list, dept_list)
|
||||
return list(set(dept_list))
|
||||
|
||||
@classmethod
|
||||
def recursion_all_parent_dept(cls, dept_id: int, dept_list=None):
|
||||
"""
|
||||
递归获取部门的所有上级部门
|
||||
:param dept_id: 需要获取的id
|
||||
:param dept_list: 递归list
|
||||
:return:
|
||||
"""
|
||||
if dept_list is None:
|
||||
dept_list = [dept_id]
|
||||
current_dept = Dept.objects.filter(id=dept_id).values('parent').first()
|
||||
if current_dept and current_dept.get('parent'):
|
||||
parent_id = current_dept.get('parent')
|
||||
dept_list.append(parent_id)
|
||||
cls.recursion_all_parent_dept(parent_id, dept_list)
|
||||
return list(set(dept_list))
|
||||
|
||||
class Meta:
|
||||
db_table = table_prefix + "system_dept"
|
||||
verbose_name = "部门表"
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.conf import settings
|
||||
from django.db import connection
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from application import dispatch
|
||||
from dvadmin.system.models import FileList
|
||||
@@ -106,7 +107,7 @@ class FileViewSet(CustomModelViewSet):
|
||||
queryset = FileList.objects.all()
|
||||
serializer_class = FileSerializer
|
||||
filter_class = FileFilter
|
||||
permission_classes = []
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(methods=['GET'], detail=False)
|
||||
def get_all(self, request):
|
||||
|
||||
@@ -138,6 +138,19 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id"]
|
||||
|
||||
def websocket_push(user_id, message):
|
||||
"""
|
||||
主动推送消息
|
||||
"""
|
||||
username = "user_" + str(user_id)
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
username,
|
||||
{
|
||||
"type": "push.message",
|
||||
"json": message
|
||||
}
|
||||
)
|
||||
|
||||
class MessageCenterCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
@@ -167,6 +180,10 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
|
||||
targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request)
|
||||
targetuser_instance.is_valid(raise_exception=True)
|
||||
targetuser_instance.save()
|
||||
for user in users:
|
||||
unread_count = MessageCenterTargetUser.objects.filter(users__id=user, is_read=False).count()
|
||||
websocket_push(user, message={"sender": 'system', "contentType": 'SYSTEM',
|
||||
"content": '您有一条新消息~', "unread": unread_count})
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
@@ -206,6 +223,10 @@ class MessageCenterViewSet(CustomModelViewSet):
|
||||
queryset.save()
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
# 主动推送消息
|
||||
unread_count = MessageCenterTargetUser.objects.filter(users__id=user_id, is_read=False).count()
|
||||
websocket_push(user_id, message={"sender": 'system', "contentType": 'TEXT',
|
||||
"content": '您查看了一条消息~', "unread": unread_count})
|
||||
return DetailResponse(data=serializer.data, msg="获取成功")
|
||||
|
||||
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])
|
||||
|
||||
@@ -90,6 +90,8 @@ class UserCreateSerializer(CustomModelSerializer):
|
||||
data = super().save(**kwargs)
|
||||
data.dept_belong_id = data.dept_id
|
||||
data.save()
|
||||
if not self.validated_data.get('manage_dept', None):
|
||||
data.manage_dept.add(data.dept_id)
|
||||
data.post.set(self.initial_data.get("post", []))
|
||||
return data
|
||||
|
||||
@@ -127,6 +129,8 @@ class UserUpdateSerializer(CustomModelSerializer):
|
||||
data = super().save(**kwargs)
|
||||
data.dept_belong_id = data.dept_id
|
||||
data.save()
|
||||
if not self.validated_data.get('manage_dept', None):
|
||||
data.manage_dept.add(data.dept_id)
|
||||
data.post.set(self.initial_data.get("post", []))
|
||||
return data
|
||||
|
||||
@@ -426,12 +430,9 @@ class UserViewSet(CustomModelViewSet):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
else:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
# print(queryset.values('id','name','dept__id'))
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True, request=request)
|
||||
# print(serializer.data)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = self.get_serializer(queryset, many=True, request=request)
|
||||
|
||||
return SuccessResponse(data=serializer.data, msg="获取成功")
|
||||
|
||||
@@ -15,15 +15,16 @@ import six
|
||||
from django.db import models
|
||||
from django.db.models import Q, F
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django_filters import utils, FilterSet
|
||||
from django_filters.constants import ALL_FIELDS
|
||||
from django_filters.filters import CharFilter, DateTimeFromToRangeFilter
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters.utils import get_model_field
|
||||
from django_filters.utils import get_model_field, translate_validation, deprecate
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
from django_filters.conf import settings
|
||||
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission, MenuButton
|
||||
from dvadmin.utils.models import CoreModel
|
||||
|
||||
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission, MenuButton, Users
|
||||
from util.currency import recursion_down_fast
|
||||
|
||||
|
||||
class CoreModelFilterBankend(BaseFilterBackend):
|
||||
"""
|
||||
@@ -200,6 +201,86 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
|
||||
return queryset.filter(dept_belong_id__in=list(set(dept_list)))
|
||||
|
||||
|
||||
class DataLevelPermissionsSubFilter(DataLevelPermissionsFilter):
|
||||
"""数据级权限过滤的子过滤器,过滤管理部门字段manage_dept"""
|
||||
|
||||
def _extracted_from_filter_queryset_33(self, request:Request, queryset, api, method):
|
||||
u:Users = request.user
|
||||
if u.is_superuser:
|
||||
return queryset
|
||||
# (0, "仅本人数据权限"),
|
||||
# (1, "本部门及以下数据权限"),
|
||||
# (2, "本部门数据权限"),
|
||||
# (3, "全部数据权限"),
|
||||
# (4, "自定数据权限")
|
||||
re_api = api
|
||||
_pk = request.parser_context["kwargs"].get('pk')
|
||||
if _pk: # 判断是否是单例查询
|
||||
re_api = re.sub(_pk,'{id}', api)
|
||||
role_id_list = request.user.role.values_list('id', flat=True)
|
||||
menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True)
|
||||
role_permission_list = []
|
||||
if menu_button_ids:
|
||||
role_permission_list=RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_id_list,
|
||||
role__status=1,
|
||||
menu_button_id__in=menu_button_ids).values(
|
||||
'data_range'
|
||||
)
|
||||
dataScope_list = [] # 权限范围列表
|
||||
for ele in role_permission_list:
|
||||
# 判断用户是否为超级管理员角色/如果拥有[全部数据权限]则返回所有数据
|
||||
if ele.get("data_range") == 3:
|
||||
return queryset
|
||||
dataScope_list.append(ele.get("data_range"))
|
||||
dataScope_list = list(set(dataScope_list))
|
||||
# 4. 只为仅本人数据权限时只返回过滤本人数据,并且部门为自己本部门(考虑到用户会变部门,只能看当前用户所在的部门数据)
|
||||
if 0 in dataScope_list:
|
||||
return queryset.filter(
|
||||
creator=request.user, dept_belong_id=u.dept_id
|
||||
)
|
||||
dept_list = []
|
||||
# 5. 自定数据权限 获取部门,根据部门过滤
|
||||
for ele in dataScope_list:
|
||||
if ele == 1:
|
||||
dept_list.append(u.dept_id)
|
||||
dept_list.extend(
|
||||
get_dept(
|
||||
u.dept_id,
|
||||
)
|
||||
)
|
||||
elif ele == 2:
|
||||
dept_list.append(u.dept_id)
|
||||
elif ele == 4:
|
||||
dept_ids = RoleMenuButtonPermission.objects.filter(
|
||||
role__in=role_id_list,
|
||||
role__status=1,
|
||||
data_range=4).values_list(
|
||||
'dept__id',flat=True
|
||||
)
|
||||
dept_list.extend(
|
||||
dept_ids
|
||||
)
|
||||
# 自己部门 交 管理部门
|
||||
if u.manage_dept.exists(): # 兼容旧数据
|
||||
for dept in u.manage_dept.all():
|
||||
dept_list.extend(recursion_down_fast(dept, 'parent', 'id'))
|
||||
else:
|
||||
dept_list = recursion_down_fast(u.dept, 'parent', 'id')
|
||||
dept_list = set(recursion_down_fast(u.dept)) & set(dept_list)
|
||||
# 自己创建的数据要能看到
|
||||
# 应对归属a管b、c等情况,如果自己创建数据则是a,不显式指定自己的数据就查不到
|
||||
if queryset.model._meta.model_name == 'dept':
|
||||
return queryset.filter(Q(id__in=dept_list) | Q(creator=u))
|
||||
return queryset.filter(Q(dept_belong_id__in=dept_list) | Q(creator=u))
|
||||
|
||||
|
||||
class DataLevelPermissionMargeFilter(DataLevelPermissionsFilter):
|
||||
def _extracted_from_filter_queryset_33(self, request, queryset, api, method):
|
||||
queryset = super()._extracted_from_filter_queryset_33(request, queryset, api, method)
|
||||
return DataLevelPermissionsSubFilter._extracted_from_filter_queryset_33(self, request, queryset, api, method)
|
||||
|
||||
|
||||
class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
lookup_prefixes = {
|
||||
"^": "istartswith",
|
||||
@@ -240,14 +321,14 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
|
||||
# TODO: remove assertion in 2.1
|
||||
if filterset_class is None and hasattr(view, "filter_class"):
|
||||
utils.deprecate(
|
||||
deprecate(
|
||||
"`%s.filter_class` attribute should be renamed `filterset_class`." % view.__class__.__name__
|
||||
)
|
||||
filterset_class = getattr(view, "filter_class", None)
|
||||
|
||||
# TODO: remove assertion in 2.1
|
||||
if filterset_fields is None and hasattr(view, "filter_fields"):
|
||||
utils.deprecate(
|
||||
deprecate(
|
||||
"`%s.filter_fields` attribute should be renamed `filterset_fields`." % view.__class__.__name__
|
||||
)
|
||||
self.filter_fields = getattr(view, "filter_fields", None)
|
||||
@@ -427,5 +508,5 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
return queryset
|
||||
|
||||
if not filterset.is_valid() and self.raise_exception:
|
||||
raise utils.translate_validation(filterset.errors)
|
||||
raise translate_validation(filterset.errors)
|
||||
return filterset.qs
|
||||
|
||||
@@ -16,7 +16,7 @@ from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from dvadmin.utils.filters import DataLevelPermissionsFilter, CoreModelFilterBankend
|
||||
from dvadmin.utils.filters import CoreModelFilterBankend, DataLevelPermissionMargeFilter
|
||||
from dvadmin.utils.import_export_mixin import ExportSerializerMixin, ImportSerializerMixin
|
||||
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse
|
||||
from dvadmin.utils.permission import CustomPermission
|
||||
@@ -41,7 +41,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
|
||||
update_serializer_class = None
|
||||
filter_fields = '__all__'
|
||||
search_fields = ()
|
||||
extra_filter_class = [CoreModelFilterBankend,DataLevelPermissionsFilter]
|
||||
extra_filter_class = [CoreModelFilterBankend,DataLevelPermissionMargeFilter]
|
||||
permission_classes = [CustomPermission]
|
||||
import_field_dict = {}
|
||||
export_field_label = {}
|
||||
|
||||
@@ -56,7 +56,7 @@ def recursion_up_fast(instance: Model, parent='parent', key='id') -> list[int]:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(sql, [getattr(instance, key)])
|
||||
data = cursor.fetchall()
|
||||
return [getattr(instance, key), *[i[0] for i in data]]
|
||||
return [i[0] for i in data]
|
||||
|
||||
def recursion_up_joint(instance: Model, parent='parent', key='name', joint='/') -> str:
|
||||
"""向上递归instance所有父级并拼接需要的值,返回完整路径,使用sql优化,速度非常快"""
|
||||
|
||||
@@ -20,6 +20,7 @@ import other from '/@/utils/other';
|
||||
import { Local, Session } from '/@/utils/storage';
|
||||
import mittBus from '/@/utils/mitt';
|
||||
import setIntroduction from '/@/utils/setIconfont';
|
||||
import websocket from '/@/utils/websocket';
|
||||
|
||||
// 引入组件
|
||||
const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
|
||||
@@ -91,5 +92,63 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
mittBus.off('openSetingsDrawer', () => {});
|
||||
});
|
||||
// 监听路由的变化,设置网站标题
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
other.useTitle();
|
||||
other.useFavicon();
|
||||
if (!websocket.websocket) {
|
||||
//websockt 模块
|
||||
try {
|
||||
websocket.init(wsReceive)
|
||||
} catch (e) {
|
||||
console.log('websocket错误');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
// websocket相关代码
|
||||
import { messageCenterStore } from '/@/stores/messageCenter';
|
||||
const wsReceive = (message: any) => {
|
||||
const data = JSON.parse(message.data);
|
||||
const { unread } = data;
|
||||
const messageCenter = messageCenterStore();
|
||||
messageCenter.setUnread(unread);
|
||||
if (data.contentType === 'SYSTEM') {
|
||||
ElNotification({
|
||||
title: '系统消息',
|
||||
message: data.content,
|
||||
type: 'success',
|
||||
position: 'bottom-right',
|
||||
duration: 5000,
|
||||
});
|
||||
} else if (data.contentType === 'Content') {
|
||||
ElMessageBox.confirm(data.content, data.notificationTitle, {
|
||||
confirmButtonText: data.notificationButton,
|
||||
dangerouslyUseHTMLString: true,
|
||||
cancelButtonText: '关闭',
|
||||
type: 'info',
|
||||
closeOnClickModal: false,
|
||||
}).then(() => {
|
||||
ElMessageBox.close();
|
||||
const path = data.path;
|
||||
if (route.path === path) {
|
||||
core.bus.emit('onNewTask', { name: 'onNewTask' });
|
||||
} else {
|
||||
router.push({ path});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
};
|
||||
onBeforeUnmount(() => {
|
||||
// 关闭连接
|
||||
websocket.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
BIN
web/src/assets/home-bg.png
Normal file
BIN
web/src/assets/home-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
@@ -1,403 +1,402 @@
|
||||
<template>
|
||||
<div style="width: 100%; height: 100%;">
|
||||
<div class="selected-show" v-if="props.modelValue && props.selectable">
|
||||
<el-text>已选择:</el-text>
|
||||
<el-tag v-if="props.multiple" v-for="item in data" closable @close="handleTagClose(item)">
|
||||
{{ item.toLocaleDateString('en-CA') }}
|
||||
</el-tag>
|
||||
<el-tag v-else closable @close="handleTagClose(data)">{{ data?.toLocaleDateString('en-CA') }}</el-tag>
|
||||
<el-button v-if="props.modelValue" size="small" type="text" @click="clear">清空</el-button>
|
||||
<div style="width: 100%; height: 100%;">
|
||||
<div class="selected-show" v-if="props.modelValue && props.selectable">
|
||||
<el-text>已选择:</el-text>
|
||||
<el-tag v-if="props.multiple" v-for="item in data" closable @close="handleTagClose(item)">
|
||||
{{ item.toLocaleDateString('en-CA') }}
|
||||
</el-tag>
|
||||
<el-tag v-else closable @close="handleTagClose(data)">{{ data?.toLocaleDateString('en-CA') }}</el-tag>
|
||||
<el-button v-if="props.modelValue" size="small" type="text" @click="clear">清空</el-button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div>
|
||||
今天:<el-text size="large">{{ today.toLocaleDateString('en-CA') }}</el-text>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div>
|
||||
今天:<el-text size="large">{{ today.toLocaleDateString('en-CA') }}</el-text>
|
||||
</div>
|
||||
<!-- <div class="current-month">
|
||||
<!-- <div class="current-month">
|
||||
<el-tag size="large" type="primary">
|
||||
{{ currentCalendarDate.getFullYear() }}年{{ currentCalendarDate.getMonth() + 1 }}月
|
||||
</el-tag>
|
||||
</div> -->
|
||||
<div class="control-button" v-if="!(!!props.range && props.range[0] && props.range[1]) && props.showPageTurn">
|
||||
<el-button-group size="small" type="default" v-if="props.pageTurn">
|
||||
<el-popover trigger="click" width="160px">
|
||||
<template #reference>
|
||||
<el-button type="text" size="small">节假日设置</el-button>
|
||||
</template>
|
||||
<el-switch v-model="showHoliday" active-text="显示节日" inactive-text="关闭节日" inline-prompt />
|
||||
<el-checkbox v-model="showLunarHoliday" label="农历节日" />
|
||||
<el-checkbox v-model="showJieQi" label="节气" />
|
||||
<el-checkbox v-model="showDetailedHoliday" label="更多节日" />
|
||||
</el-popover>
|
||||
<el-button icon="DArrowLeft" @click="turnToPreY">上年</el-button>
|
||||
<el-button icon="ArrowLeft" @click="turnToPreM">上月</el-button>
|
||||
<el-button @click="turnToToday">今天</el-button>
|
||||
<el-button icon="ArrowRight" @click="turnToNextM">下月</el-button>
|
||||
<el-button icon="DArrowRight" @click="turnToNextY">下年</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider style="margin: 4px;" />
|
||||
<div class="calender">
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="calender-header" v-for="item, ind in ['日', '一', '二', '三', '四', '五', '六']" :key="ind">
|
||||
{{ item }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="week in calendarList">
|
||||
<td class="calender-td" v-for="item in week">
|
||||
<div class="calender-cell" :data-date="item.date.toLocaleDateString('en-CA')" :class="{
|
||||
'no-current-month': item.date.getMonth() !== currentCalendarDate.getMonth(),
|
||||
'today': item.date.toDateString() === today.toDateString(),
|
||||
'selected': item.selected,
|
||||
'disabled': item.disabled,
|
||||
}" @mouseenter="onCalenderCellHover" @mouseleave="onCalenderCellUnhover"
|
||||
@click="(e: MouseEvent) => item.disabled ? null : onCalenderCellClick(e)">
|
||||
<div class="calender-cell-header calender-cell-line">
|
||||
<span>{{ item.date.getDate() }}</span>
|
||||
<span v-if="item.selected">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 1024 1024">
|
||||
<path fill="currentColor"
|
||||
d="M77.248 415.04a64 64 0 0 1 90.496 0l226.304 226.304L846.528 188.8a64 64 0 1 1 90.56 90.496l-543.04 543.04-316.8-316.8a64 64 0 0 1 0-90.496z">
|
||||
</path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="calender-cell-body calender-cell-line">
|
||||
<slot name="cell-body" v-bind="item">
|
||||
</slot>
|
||||
</div>
|
||||
<div class="calender-cell-footer calender-cell-line">
|
||||
<span>{{ item.holiday || ' ' }}</span>
|
||||
<el-text v-if="item.date.toDateString() === today.toDateString()" type="danger">今天</el-text>
|
||||
</div>
|
||||
<!-- {{ item }} -->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="watermark" v-if="props.watermark" :style="watermarkPositionMap[props.watermarkPosition]">
|
||||
{{ (currentCalendarDate.toLocaleDateString('en-CA').split('-').slice(0, 2)).join('-') }}
|
||||
</div>
|
||||
<div class="control-button" v-if="!(!!props.range && props.range[0] && props.range[1]) && props.showPageTurn">
|
||||
<el-button-group size="small" type="default" v-if="props.pageTurn">
|
||||
<el-popover trigger="click" width="160px">
|
||||
<template #reference>
|
||||
<el-button type="text" size="small">节假日设置</el-button>
|
||||
</template>
|
||||
<el-switch v-model="showHoliday" active-text="显示节日" inactive-text="关闭节日" inline-prompt />
|
||||
<el-checkbox v-model="showLunarHoliday" label="农历节日" />
|
||||
<el-checkbox v-model="showJieQi" label="节气" />
|
||||
<el-checkbox v-model="showDetailedHoliday" label="更多节日" />
|
||||
</el-popover>
|
||||
<el-button icon="DArrowLeft" @click="turnToPreY">上年</el-button>
|
||||
<el-button icon="ArrowLeft" @click="turnToPreM">上月</el-button>
|
||||
<el-button @click="turnToToday">今天</el-button>
|
||||
<el-button icon="ArrowRight" @click="turnToNextM">下月</el-button>
|
||||
<el-button icon="DArrowRight" @click="turnToNextY">下年</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUi } from '@fast-crud/fast-crud';
|
||||
import { ref, defineProps, PropType, watch, computed, onMounted } from 'vue';
|
||||
import Holidays from 'date-holidays';
|
||||
import Lunar from 'lunar-javascript';
|
||||
const LUNAR = Lunar.Lunar; // 农历
|
||||
const SOLAR = Lunar.Solar; // 阳历
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {},
|
||||
// 日期多选
|
||||
multiple: { type: Boolean, default: false },
|
||||
// 日期范围
|
||||
range: { type: Object as PropType<[Date, Date]> },
|
||||
// 可以翻页
|
||||
pageTurn: { type: Boolean, default: true },
|
||||
// 跨页选择
|
||||
crossPage: { type: Boolean, default: false },
|
||||
// 显示年月水印和水印位置
|
||||
watermark: { type: Boolean, default: true },
|
||||
watermarkPosition: { type: Object as PropType<PositionType>, default: 'bottom-right' },
|
||||
// 显示翻页控件
|
||||
showPageTurn: { type: Boolean, default: true },
|
||||
// 是否可选
|
||||
selectable: { type: Boolean, default: true },
|
||||
// 验证日期是否有效
|
||||
validDate: { type: Object as PropType<ValidDateFunc>, default: () => ((d: Date) => true) }
|
||||
});
|
||||
type ValidDateFunc = (d: Date) => boolean;
|
||||
type PositionType = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center' | 'center-left' | 'center-right' | 'center-top';
|
||||
const today = new Date();
|
||||
const showHoliday = ref<boolean>(true); // 显示节日
|
||||
const showDetailedHoliday = ref<boolean>(false); // 显示详细的国际节日
|
||||
const showJieQi = ref<boolean>(true); // 显示节气
|
||||
const showLunarHoliday = ref<boolean>(true); // 显示农历节日
|
||||
const watermarkPositionMap: { [key: string]: any } = {
|
||||
'top-left': { top: '40px', left: 0, transformOrigin: '0 0' },
|
||||
'top-right': { top: '40px', right: 0, transformOrigin: '100% 0' },
|
||||
'bottom-left': { bottom: 0, left: 0, transformOrigin: '0 100%' },
|
||||
'bottom-right': { bottom: 0, right: 0, transformOrigin: '100% 100%' },
|
||||
'center': { top: '50%', left: '50%', transformOrigin: '50% 50%', transform: 'translate(-50%, -50%) scale(10)' },
|
||||
'center-left': { top: '50%', left: 0, transformOrigin: '0 50%' },
|
||||
'center-right': { top: '50%', right: 0, transformOrigin: '100% 50%' },
|
||||
'center-top': { top: 0, left: '50%', transformOrigin: '50% 0', transform: 'translate(-50%, 40px) scale(10)' },
|
||||
'center-bottom': { bottom: 0, left: '50%', transformOrigin: '50% 100%', transform: 'translate(-50%, 0) scale(10)' },
|
||||
};
|
||||
// 获取当月第一周的第一天(包括上个月)
|
||||
const calendarFirstDay = (current: Date = new Date()) => {
|
||||
let today = new Date(current); // 指定天
|
||||
let firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); // 月初天
|
||||
let weekOfFirstDay = firstDayOfMonth.getDay(); // 周几,0日
|
||||
if (weekOfFirstDay === 0) return new Date(firstDayOfMonth); // 是周日则直接返回
|
||||
let firstDayOfWeek = new Date(firstDayOfMonth);
|
||||
// 月初减去周几,不+1是因为从日历周日开始
|
||||
firstDayOfWeek.setDate(firstDayOfMonth.getDate() - weekOfFirstDay);
|
||||
return new Date(firstDayOfWeek);
|
||||
};
|
||||
// 获取当月最后一周的最后一天(包括下个月)
|
||||
const calendarLastDay = (current: Date = new Date()) => {
|
||||
let today = new Date(current); // 指定天
|
||||
let lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 1); // 月末天
|
||||
lastDayOfMonth.setDate(lastDayOfMonth.getDate() - 1);
|
||||
let weekOfFirstDay = lastDayOfMonth.getDay();
|
||||
if (weekOfFirstDay === 6) return new Date(lastDayOfMonth); // 是周六则直接返回
|
||||
let lastDayOfWeek = new Date(lastDayOfMonth);
|
||||
// 月末加剩下周几,要-1是因为日历到周六结束
|
||||
lastDayOfWeek.setDate(lastDayOfMonth.getDate() + (7 - weekOfFirstDay - 1));
|
||||
return new Date(lastDayOfWeek);
|
||||
};
|
||||
const generateDateList = (startDate: Date, endDate: Date): Date[] => { // 生成日期列表
|
||||
let dates = [];
|
||||
let s = new Date(startDate);
|
||||
let e = new Date(endDate);
|
||||
while (s <= e) {
|
||||
dates.push(new Date(s));
|
||||
s.setDate(s.getDate() + 1);
|
||||
}
|
||||
return dates;
|
||||
};
|
||||
// 日历当前页范围
|
||||
interface CalendarCell {
|
||||
date: Date;
|
||||
selected: boolean;
|
||||
disabled: boolean;
|
||||
currentMonth: boolean;
|
||||
holiday: string;
|
||||
};
|
||||
const currentCalendarDate = ref<Date>(new Date());
|
||||
const calendarList = computed(() => {
|
||||
let dates = (!!props.range && props.range[0] && props.range[1]) ?
|
||||
generateDateList(props.range[0], props.range[1]) :
|
||||
generateDateList(calendarFirstDay(currentCalendarDate.value), calendarLastDay(currentCalendarDate.value));
|
||||
let proce_dates: CalendarCell[] = dates.map((value) => {
|
||||
let solarDate = SOLAR.fromDate(value);
|
||||
let lunarDate = solarDate.getLunar();
|
||||
let solarHolidays: string[] = solarDate.getFestivals(); // 国历节日
|
||||
let lunarHolidays: string[] = lunarDate.getFestivals(); // 农历节日
|
||||
let jieQi: string = lunarDate.getJieQi(); // 节气
|
||||
// 农历节日、国际节日、节气三选一
|
||||
let holiday = showHoliday.value ? ((showLunarHoliday.value ? lunarHolidays[0] : '') || (showJieQi.value ? jieQi : '') ||
|
||||
(showDetailedHoliday.value ? solarHolidays[0] : yearHolidays.value[value.toLocaleDateString('en-CA')])) : ''; // yearHolidays国际的
|
||||
return {
|
||||
date: value,
|
||||
selected: props.multiple ?
|
||||
(data.value as Date[]).findIndex((v) => v.toLocaleDateString('en-CA') === value.toLocaleDateString('en-CA')) !== -1 :
|
||||
data.value?.toLocaleDateString('en-CA') === value.toLocaleDateString('en-CA'),
|
||||
disabled: !props.validDate(value),
|
||||
currentMonth: value.getMonth() === currentCalendarDate.value.getMonth(),
|
||||
// 农历节日、节气、法定日三选一
|
||||
holiday: holiday
|
||||
}
|
||||
});
|
||||
let res: CalendarCell[][] = [];
|
||||
for (let i = 0; i < 6; i++) res.push(proce_dates.slice(i * 7, (i + 1) * 7));
|
||||
return res;
|
||||
});
|
||||
// 控件
|
||||
const turnToPreM = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear(), currentCalendarDate.value.getMonth() - 1, 1);
|
||||
const turnToNextM = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear(), currentCalendarDate.value.getMonth() + 1, 1);
|
||||
const turnToPreY = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear() - 1, currentCalendarDate.value.getMonth(), 1);
|
||||
const turnToNextY = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear() + 1, currentCalendarDate.value.getMonth(), 1);
|
||||
const turnToToday = () => currentCalendarDate.value = new Date();
|
||||
// 如果禁止跨页则跨页时清空选择
|
||||
watch(
|
||||
() => currentCalendarDate.value,
|
||||
(v, ov) => props.crossPage ? {} :
|
||||
(v.toLocaleDateString('en-CA') === ov.toLocaleDateString('en-CA') ? {} : clear())
|
||||
);
|
||||
// 单元格事件
|
||||
const onCalenderCellHover = ({ target }: MouseEvent) => (target as HTMLElement).classList.add('onhover');
|
||||
const onCalenderCellUnhover = ({ target }: MouseEvent) => (target as HTMLElement).classList.remove('onhover');
|
||||
const onCalenderCellClick = (e: MouseEvent) => {
|
||||
if (!props.selectable) return;
|
||||
let strValue = (e.target as HTMLElement).dataset.date as string;
|
||||
if (strValue === undefined) return;
|
||||
let value = new Date(strValue);
|
||||
if (props.multiple) {
|
||||
let d = (data.value as Date[]).map((v) => v.toLocaleDateString('en-CA'));
|
||||
let ind = d.findIndex((v) => v === strValue);
|
||||
if (ind === -1) d.push(strValue);
|
||||
else d.splice(ind, 1);
|
||||
onDataChange(d);
|
||||
}
|
||||
// 这里阻止了点击取消选中,需要通过tag的x来取消
|
||||
else (data.value?.toLocaleDateString('en-CA') === strValue ? {} : onDataChange(value));
|
||||
};
|
||||
// 选择回显
|
||||
const handleTagClose = (d: Date) => {
|
||||
let strValue = d.toLocaleDateString('en-CA');
|
||||
if (props.multiple) {
|
||||
let d = (data.value as Date[]).map((v) => v.toLocaleDateString('en-CA'));
|
||||
d.splice(d.findIndex((v) => v === strValue), 1);
|
||||
onDataChange(d);
|
||||
}
|
||||
else onDataChange(null);
|
||||
};
|
||||
// 节假日
|
||||
const holidays = new Holidays('CN');
|
||||
const yearHolidays = computed(() => {
|
||||
let h = holidays.getHolidays(currentCalendarDate.value.getFullYear());
|
||||
let proce_h: { [key: string]: string } = {};
|
||||
let _h: string[] = [];
|
||||
for (let i of h) {
|
||||
let d = i.date.split(' ')[0];
|
||||
let hn = i.name.split(' ')[0];
|
||||
if (_h.includes(hn)) continue;
|
||||
proce_h[d] = hn;
|
||||
_h.push(hn);
|
||||
}
|
||||
return proce_h
|
||||
});
|
||||
|
||||
// fs-crud部分
|
||||
const data = ref<any>();
|
||||
const emit = defineEmits(['update:modelValue', 'onSave', 'onClose', 'onClosed']);
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val === undefined) data.value = props.multiple ? [] : null;
|
||||
else data.value = props.multiple ? (val as Date[]).map((v: Date) => new Date(v)) : val;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
const { ui } = useUi();
|
||||
const formValidator = ui.formItem.injectFormItemContext();
|
||||
const onDataChange = (value: any) => {
|
||||
emit('update:modelValue', value);
|
||||
formValidator.onChange();
|
||||
formValidator.onBlur();
|
||||
};
|
||||
const reset = () => { // 重置日历
|
||||
currentCalendarDate.value = new Date();
|
||||
onDataChange(props.multiple ? [] : null);
|
||||
};
|
||||
const clear = () => onDataChange(props.multiple ? [] : null); // 清空数据
|
||||
defineExpose({
|
||||
data,
|
||||
onDataChange,
|
||||
reset,
|
||||
clear,
|
||||
showHoliday,
|
||||
showDetailedHoliday,
|
||||
showJieQi,
|
||||
showLunarHoliday
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.selected-show {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
<el-divider style="margin: 4px;" />
|
||||
<div class="calender">
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="calender-header" v-for="item, ind in ['日', '一', '二', '三', '四', '五', '六']" :key="ind">
|
||||
{{ item }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="week in calendarList">
|
||||
<td class="calender-td" v-for="item in week">
|
||||
<div class="calender-cell" :data-date="item.date.toLocaleDateString('en-CA')" :class="{
|
||||
'no-current-month': item.date.getMonth() !== currentCalendarDate.getMonth(),
|
||||
'today': item.date.toDateString() === today.toDateString(),
|
||||
'selected': item.selected,
|
||||
'disabled': item.disabled,
|
||||
}" @mouseenter="onCalenderCellHover" @mouseleave="onCalenderCellUnhover"
|
||||
@click="(e: MouseEvent) => item.disabled ? null : onCalenderCellClick(e)">
|
||||
<div class="calender-cell-header calender-cell-line">
|
||||
<span>{{ item.date.getDate() }}</span>
|
||||
<span v-if="item.selected">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 1024 1024">
|
||||
<path fill="currentColor"
|
||||
d="M77.248 415.04a64 64 0 0 1 90.496 0l226.304 226.304L846.528 188.8a64 64 0 1 1 90.56 90.496l-543.04 543.04-316.8-316.8a64 64 0 0 1 0-90.496z">
|
||||
</path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="calender-cell-body calender-cell-line">
|
||||
<slot name="cell-body" v-bind="item">
|
||||
</slot>
|
||||
</div>
|
||||
<div class="calender-cell-footer calender-cell-line">
|
||||
<span>{{ item.holiday || ' ' }}</span>
|
||||
<el-text v-if="item.date.toDateString() === today.toDateString()" type="danger">今天</el-text>
|
||||
</div>
|
||||
<!-- {{ item }} -->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="watermark" v-if="props.watermark" :style="watermarkPositionMap[props.watermarkPosition]">
|
||||
{{ (currentCalendarDate.toLocaleDateString('en-CA').split('-').slice(0, 2)).join('-') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUi } from '@fast-crud/fast-crud';
|
||||
import { ref, defineProps, PropType, watch, computed, onMounted } from 'vue';
|
||||
import Holidays from 'date-holidays';
|
||||
import Lunar from 'lunar-javascript';
|
||||
const LUNAR = Lunar.Lunar; // 农历
|
||||
const SOLAR = Lunar.Solar; // 阳历
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {},
|
||||
// 日期多选
|
||||
multiple: { type: Boolean, default: false },
|
||||
// 日期范围
|
||||
range: { type: Object as PropType<[Date, Date]> },
|
||||
// 可以翻页
|
||||
pageTurn: { type: Boolean, default: true },
|
||||
// 跨页选择
|
||||
crossPage: { type: Boolean, default: false },
|
||||
// 显示年月水印和水印位置
|
||||
watermark: { type: Boolean, default: true },
|
||||
watermarkPosition: { type: Object as PropType<PositionType>, default: 'bottom-right' },
|
||||
// 显示翻页控件
|
||||
showPageTurn: { type: Boolean, default: true },
|
||||
// 是否可选
|
||||
selectable: { type: Boolean, default: true },
|
||||
// 验证日期是否有效
|
||||
validDate: { type: Object as PropType<ValidDateFunc>, default: () => ((d: Date) => true) }
|
||||
});
|
||||
type ValidDateFunc = (d: Date) => boolean;
|
||||
type PositionType = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center' | 'center-left' | 'center-right' | 'center-top';
|
||||
const today = new Date();
|
||||
const showHoliday = ref<boolean>(true); // 显示节日
|
||||
const showDetailedHoliday = ref<boolean>(false); // 显示详细的国际节日
|
||||
const showJieQi = ref<boolean>(true); // 显示节气
|
||||
const showLunarHoliday = ref<boolean>(true); // 显示农历节日
|
||||
const watermarkPositionMap: { [key: string]: any } = {
|
||||
'top-left': { top: '40px', left: 0, transformOrigin: '0 0' },
|
||||
'top-right': { top: '40px', right: 0, transformOrigin: '100% 0' },
|
||||
'bottom-left': { bottom: 0, left: 0, transformOrigin: '0 100%' },
|
||||
'bottom-right': { bottom: 0, right: 0, transformOrigin: '100% 100%' },
|
||||
'center': { top: '50%', left: '50%', transformOrigin: '50% 50%', transform: 'translate(-50%, -50%) scale(10)' },
|
||||
'center-left': { top: '50%', left: 0, transformOrigin: '0 50%' },
|
||||
'center-right': { top: '50%', right: 0, transformOrigin: '100% 50%' },
|
||||
'center-top': { top: 0, left: '50%', transformOrigin: '50% 0', transform: 'translate(-50%, 40px) scale(10)' },
|
||||
'center-bottom': { bottom: 0, left: '50%', transformOrigin: '50% 100%', transform: 'translate(-50%, 0) scale(10)' },
|
||||
};
|
||||
// 获取当月第一周的第一天(包括上个月)
|
||||
const calendarFirstDay = (current: Date = new Date()) => {
|
||||
let today = new Date(current); // 指定天
|
||||
let firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); // 月初天
|
||||
let weekOfFirstDay = firstDayOfMonth.getDay(); // 周几,0日
|
||||
if (weekOfFirstDay === 0) return new Date(firstDayOfMonth); // 是周日则直接返回
|
||||
let firstDayOfWeek = new Date(firstDayOfMonth);
|
||||
// 月初减去周几,不+1是因为从日历周日开始
|
||||
firstDayOfWeek.setDate(firstDayOfMonth.getDate() - weekOfFirstDay);
|
||||
return new Date(firstDayOfWeek);
|
||||
};
|
||||
// 获取当月最后一周的最后一天(包括下个月)
|
||||
const calendarLastDay = (current: Date = new Date()) => {
|
||||
let today = new Date(current); // 指定天
|
||||
let lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 1); // 月末天
|
||||
lastDayOfMonth.setDate(lastDayOfMonth.getDate() - 1);
|
||||
let weekOfFirstDay = lastDayOfMonth.getDay();
|
||||
if (weekOfFirstDay === 6) return new Date(lastDayOfMonth); // 是周六则直接返回
|
||||
let lastDayOfWeek = new Date(lastDayOfMonth);
|
||||
// 月末加剩下周几,要-1是因为日历到周六结束
|
||||
lastDayOfWeek.setDate(lastDayOfMonth.getDate() + (7 - weekOfFirstDay - 1));
|
||||
return new Date(lastDayOfWeek);
|
||||
};
|
||||
const generateDateList = (startDate: Date, endDate: Date): Date[] => { // 生成日期列表
|
||||
let dates = [];
|
||||
let s = new Date(startDate);
|
||||
let e = new Date(endDate);
|
||||
while (s <= e) {
|
||||
dates.push(new Date(s));
|
||||
s.setDate(s.getDate() + 1);
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
return dates;
|
||||
};
|
||||
// 日历当前页范围
|
||||
interface CalendarCell {
|
||||
date: Date;
|
||||
selected: boolean;
|
||||
disabled: boolean;
|
||||
currentMonth: boolean;
|
||||
holiday: string;
|
||||
};
|
||||
const currentCalendarDate = ref<Date>(new Date());
|
||||
const calendarList = computed(() => {
|
||||
let dates = (!!props.range && props.range[0] && props.range[1]) ?
|
||||
generateDateList(props.range[0], props.range[1]) :
|
||||
generateDateList(calendarFirstDay(currentCalendarDate.value), calendarLastDay(currentCalendarDate.value));
|
||||
let proce_dates: CalendarCell[] = dates.map((value) => {
|
||||
let solarDate = SOLAR.fromDate(value);
|
||||
let lunarDate = solarDate.getLunar();
|
||||
let solarHolidays: string[] = solarDate.getFestivals(); // 国历节日
|
||||
let lunarHolidays: string[] = lunarDate.getFestivals(); // 农历节日
|
||||
let jieQi: string = lunarDate.getJieQi(); // 节气
|
||||
// 农历节日、国际节日、节气三选一
|
||||
let holiday = showHoliday.value ? ((showLunarHoliday.value ? lunarHolidays[0] : '') || (showJieQi.value ? jieQi : '') ||
|
||||
(showDetailedHoliday.value ? solarHolidays[0] : yearHolidays.value[value.toLocaleDateString('en-CA')])) : ''; // yearHolidays国际的
|
||||
return {
|
||||
date: value,
|
||||
selected: props.multiple ?
|
||||
(data.value as Date[]).findIndex((v) => v.toLocaleDateString('en-CA') === value.toLocaleDateString('en-CA')) !== -1 :
|
||||
data.value?.toLocaleDateString('en-CA') === value.toLocaleDateString('en-CA'),
|
||||
disabled: !props.validDate(value),
|
||||
currentMonth: value.getMonth() === currentCalendarDate.value.getMonth(),
|
||||
// 农历节日、节气、法定日三选一
|
||||
holiday: holiday
|
||||
}
|
||||
});
|
||||
let res: CalendarCell[][] = [];
|
||||
for (let i = 0; i < 6; i++) res.push(proce_dates.slice(i * 7, (i + 1) * 7));
|
||||
return res;
|
||||
});
|
||||
// 控件
|
||||
const turnToPreM = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear(), currentCalendarDate.value.getMonth() - 1, 1);
|
||||
const turnToNextM = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear(), currentCalendarDate.value.getMonth() + 1, 1);
|
||||
const turnToPreY = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear() - 1, currentCalendarDate.value.getMonth(), 1);
|
||||
const turnToNextY = () => currentCalendarDate.value = new Date(currentCalendarDate.value.getFullYear() + 1, currentCalendarDate.value.getMonth(), 1);
|
||||
const turnToToday = () => currentCalendarDate.value = new Date();
|
||||
// 如果禁止跨页则跨页时清空选择
|
||||
watch(
|
||||
() => currentCalendarDate.value,
|
||||
(v, ov) => props.crossPage ? {} :
|
||||
(v.toLocaleDateString('en-CA') === ov.toLocaleDateString('en-CA') ? {} : clear())
|
||||
);
|
||||
// 单元格事件
|
||||
const onCalenderCellHover = ({ target }: MouseEvent) => (target as HTMLElement).classList.add('onhover');
|
||||
const onCalenderCellUnhover = ({ target }: MouseEvent) => (target as HTMLElement).classList.remove('onhover');
|
||||
const onCalenderCellClick = (e: MouseEvent) => {
|
||||
if (!props.selectable) return;
|
||||
let strValue = (e.target as HTMLElement).dataset.date as string;
|
||||
if (strValue === undefined) return;
|
||||
let value = new Date(strValue);
|
||||
if (props.multiple) {
|
||||
let d = (data.value as Date[]).map((v) => v.toLocaleDateString('en-CA'));
|
||||
let ind = d.findIndex((v) => v === strValue);
|
||||
if (ind === -1) d.push(strValue);
|
||||
else d.splice(ind, 1);
|
||||
onDataChange(d);
|
||||
}
|
||||
|
||||
.calender {
|
||||
// 这里阻止了点击取消选中,需要通过tag的x来取消
|
||||
else (data.value?.toLocaleDateString('en-CA') === strValue ? {} : onDataChange(value));
|
||||
};
|
||||
// 选择回显
|
||||
const handleTagClose = (d: Date) => {
|
||||
let strValue = d.toLocaleDateString('en-CA');
|
||||
if (props.multiple) {
|
||||
let d = (data.value as Date[]).map((v) => v.toLocaleDateString('en-CA'));
|
||||
d.splice(d.findIndex((v) => v === strValue), 1);
|
||||
onDataChange(d);
|
||||
}
|
||||
else onDataChange(null);
|
||||
};
|
||||
// 节假日
|
||||
const holidays = new Holidays('CN');
|
||||
const yearHolidays = computed(() => {
|
||||
let h = holidays.getHolidays(currentCalendarDate.value.getFullYear());
|
||||
let proce_h: { [key: string]: string } = {};
|
||||
let _h: string[] = [];
|
||||
for (let i of h) {
|
||||
let d = i.date.split(' ')[0];
|
||||
let hn = i.name.split(' ')[0];
|
||||
if (_h.includes(hn)) continue;
|
||||
proce_h[d] = hn;
|
||||
_h.push(hn);
|
||||
}
|
||||
return proce_h
|
||||
});
|
||||
|
||||
// fs-crud部分
|
||||
const data = ref<any>();
|
||||
const emit = defineEmits(['update:modelValue', 'onSave', 'onClose', 'onClosed']);
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val === undefined) data.value = props.multiple ? [] : null;
|
||||
else data.value = props.multiple ? (val as Date[]).map((v: Date) => new Date(v)) : val;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
const { ui } = useUi();
|
||||
const formValidator = ui.formItem.injectFormItemContext();
|
||||
const onDataChange = (value: any) => {
|
||||
emit('update:modelValue', value);
|
||||
formValidator.onChange();
|
||||
formValidator.onBlur();
|
||||
};
|
||||
const reset = () => { // 重置日历
|
||||
currentCalendarDate.value = new Date();
|
||||
onDataChange(props.multiple ? [] : null);
|
||||
};
|
||||
const clear = () => onDataChange(props.multiple ? [] : null); // 清空数据
|
||||
defineExpose({
|
||||
data,
|
||||
onDataChange,
|
||||
reset,
|
||||
clear,
|
||||
showHoliday,
|
||||
showDetailedHoliday,
|
||||
showJieQi,
|
||||
showLunarHoliday
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.selected-show {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.calender {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
|
||||
.watermark {
|
||||
position: absolute;
|
||||
font-size: 16px;
|
||||
transform: scale(10);
|
||||
color: #aaa;
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
table {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
|
||||
.watermark {
|
||||
position: absolute;
|
||||
font-size: 16px;
|
||||
transform: scale(10);
|
||||
color: #aaa;
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.calender-header {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.calender-td {
|
||||
border: 1px solid #eee;
|
||||
width: calc(100% / 7);
|
||||
}
|
||||
|
||||
.calender-cell {
|
||||
min-height: 96px;
|
||||
min-width: 100px;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
|
||||
&.today {
|
||||
color: var(--el-color-warning) !important;
|
||||
background-color: var(--el-color-warning-light-9) !important;
|
||||
}
|
||||
|
||||
table {
|
||||
position: relative;
|
||||
|
||||
&.onhover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.calender-header {
|
||||
padding: 16px 0;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: #bbb;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.calender-td {
|
||||
border: 1px solid #eee;
|
||||
width: calc(100% / 7);
|
||||
|
||||
&.no-current-month {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.calender-cell {
|
||||
min-height: 96px;
|
||||
min-width: 100px;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
|
||||
&.today {
|
||||
color: var(--el-color-warning) !important;
|
||||
background-color: var(--el-color-warning-light-9) !important;
|
||||
|
||||
&.selected {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.calender-cell-line {
|
||||
min-height: 0px;
|
||||
|
||||
&.calender-cell-header {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.onhover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
|
||||
&.calender-cell-body {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: #bbb;
|
||||
background: none;
|
||||
}
|
||||
|
||||
&.no-current-month {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.calender-cell-line {
|
||||
min-height: 0px;
|
||||
|
||||
&.calender-cell-header {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.calender-cell-body {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.calender-cell-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.calender-cell-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div style="display: inline-block">
|
||||
<el-button size="default" type="success" @click="handleImport()">
|
||||
<el-button type="success" @click="handleImport()">
|
||||
<slot>导入</slot>
|
||||
</el-button>
|
||||
<el-dialog :title="props.upload.title" v-model="uploadShow" width="400px" append-to-body>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
|
||||
<img :src="siteLogo" class="layout-logo-medium-img" />
|
||||
<span style="font-size: x-large">{{ getSystemConfig['login.site_title'] || themeConfig.globalTitle }}</span>
|
||||
<span style="font-size: x-large; ">{{ getSystemConfig['login.site_title'] || themeConfig.globalTitle }}</span>
|
||||
</div>
|
||||
<div class="layout-logo-size" v-else @click="onThemeConfigChange">
|
||||
<img :src="siteLogo" class="layout-logo-size-img" />
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div class="layout-navbars-breadcrumb-user pr15" :style="{ flex: layoutUserFlexNum }">
|
||||
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
|
||||
<el-icon :title="$t('message.user.title2')">
|
||||
<ele-Search />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onComponentSizeChange">
|
||||
<div class="layout-navbars-breadcrumb-user-icon">
|
||||
<i class="iconfont icon-ziti" :title="$t('message.user.title0')"></i>
|
||||
@@ -28,11 +33,7 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
|
||||
<el-icon :title="$t('message.user.title2')">
|
||||
<ele-Search />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
|
||||
<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
|
||||
</div>
|
||||
@@ -57,12 +58,32 @@
|
||||
:class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"
|
||||
></i>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="!isSocketOpen" class="online-status-span">
|
||||
<el-popconfirm
|
||||
width="250"
|
||||
ref="onlinePopoverRef"
|
||||
:confirm-button-text="$t('message.user.retry')"
|
||||
:icon="InfoFilled"
|
||||
trigger="hover"
|
||||
icon-color="#626AEF"
|
||||
:title="$t('message.user.onlinePrompt')"
|
||||
@confirm="onlineConfirmEvent"
|
||||
>
|
||||
<template #reference>
|
||||
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
|
||||
<img :src="getBaseURL(userInfos.avatar) || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
||||
</el-badge>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</span>
|
||||
</div>
|
||||
<div></div>
|
||||
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
|
||||
<span class="layout-navbars-breadcrumb-user-link">
|
||||
<el-badge is-dot class="item online-status">
|
||||
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
|
||||
</el-badge>
|
||||
<!-- <el-badge is-dot class="item online-status">-->
|
||||
<!-- <img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />-->
|
||||
<!-- </el-badge>-->
|
||||
{{ userInfos.username === '' ? 'common' : userInfos.username }}
|
||||
<el-icon class="el-icon--right">
|
||||
<ele-ArrowDown />
|
||||
@@ -95,6 +116,7 @@ import mittBus from '/@/utils/mitt';
|
||||
import { Session, Local } from '/@/utils/storage';
|
||||
import headerImage from '/@/assets/img/headerImage.png';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import websocket from '/@/utils/websocket';
|
||||
// 引入组件
|
||||
const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
|
||||
const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
|
||||
@@ -123,6 +145,21 @@ const layoutUserFlexNum = computed(() => {
|
||||
return num;
|
||||
});
|
||||
|
||||
// 定义变量内容
|
||||
const { isSocketOpen } = storeToRefs(useUserInfo());
|
||||
|
||||
// websocket状态
|
||||
const onlinePopoverRef = ref()
|
||||
const onlineConfirmEvent = () => {
|
||||
if (!isSocketOpen.value) {
|
||||
websocket.is_reonnect = true
|
||||
websocket.reconnect_current = 1
|
||||
websocket.reconnect()
|
||||
}
|
||||
// 手动隐藏弹出
|
||||
unref(onlinePopoverRef).popperRef?.delayHide?.()
|
||||
}
|
||||
|
||||
// 全屏点击时
|
||||
const onScreenfullClick = () => {
|
||||
if (!screenfull.isEnabled) {
|
||||
|
||||
@@ -591,10 +591,13 @@ watch(
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-navbars-tagsview {
|
||||
background-color: var(--el-color-white);
|
||||
border-bottom: 1px solid var(--next-border-color-light);
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
height: 45px;
|
||||
border-radius: 8px;
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
|
||||
:deep(.el-scrollbar__wrap) {
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
@@ -602,7 +605,7 @@ watch(
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 34px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--el-text-color-regular);
|
||||
@@ -610,7 +613,7 @@ watch(
|
||||
white-space: nowrap;
|
||||
padding: 0 15px;
|
||||
&-li {
|
||||
height: 26px;
|
||||
height: 30px;
|
||||
line-height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface UserInfosState {
|
||||
}
|
||||
export interface UserInfosStates {
|
||||
userInfos: UserInfosState;
|
||||
isSocketOpen: boolean
|
||||
}
|
||||
|
||||
// 路由缓存列表
|
||||
|
||||
@@ -18,7 +18,7 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
* 全局主题
|
||||
*/
|
||||
// 默认 primary 主题颜色
|
||||
primary: '#409eff',
|
||||
primary: "#193755",
|
||||
// 是否开启深色模式
|
||||
isIsDark: false,
|
||||
|
||||
@@ -26,9 +26,9 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
* 顶栏设置
|
||||
*/
|
||||
// 默认顶栏导航背景颜色
|
||||
topBar: '#ffffff',
|
||||
topBar: "#f8f8f8",
|
||||
// 默认顶栏导航字体颜色
|
||||
topBarColor: '#606266',
|
||||
topBarColor: "#000000",
|
||||
// 是否开启顶栏背景颜色渐变
|
||||
isTopBarColorGradual: false,
|
||||
|
||||
@@ -36,11 +36,11 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
* 菜单设置
|
||||
*/
|
||||
// 默认菜单导航背景颜色
|
||||
menuBar: '#334054',
|
||||
menuBar: "#f8f8f8",
|
||||
// 默认菜单导航字体颜色
|
||||
menuBarColor: '#eaeaea',
|
||||
menuBarColor: "#000000",
|
||||
// 默认菜单高亮背景色
|
||||
menuBarActiveColor: 'rgba(0, 0, 0, 0.2)',
|
||||
menuBarActiveColor: "rgba(0, 48, 255, 0.38)",
|
||||
// 是否开启菜单背景颜色渐变
|
||||
isMenuBarColorGradual: false,
|
||||
|
||||
@@ -48,9 +48,9 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
* 分栏设置
|
||||
*/
|
||||
// 默认分栏菜单背景颜色
|
||||
columnsMenuBar: '#334054',
|
||||
columnsMenuBar:"#334054",
|
||||
// 默认分栏菜单字体颜色
|
||||
columnsMenuBarColor: '#e6e6e6',
|
||||
columnsMenuBarColor: "#e6e6e6",
|
||||
// 是否开启分栏菜单背景颜色渐变
|
||||
isColumnsMenuBarColorGradual: false,
|
||||
// 是否开启分栏菜单鼠标悬停预加载(预览菜单)
|
||||
|
||||
@@ -32,6 +32,7 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
},
|
||||
],
|
||||
},
|
||||
isSocketOpen: false
|
||||
}),
|
||||
actions: {
|
||||
async setPwdChangeCount(count: number) {
|
||||
@@ -71,6 +72,9 @@ export const useUserInfo = defineStore('userInfo', {
|
||||
Session.set('userInfo', this.userInfos);
|
||||
}
|
||||
},
|
||||
async setWebSocketState(socketState: boolean) {
|
||||
this.isSocketOpen = socketState;
|
||||
},
|
||||
async getApiUserInfo() {
|
||||
return request({
|
||||
url: '/api/system/user/user_info/',
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
.el-form {
|
||||
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
|
||||
.el-form-item:last-of-type {
|
||||
margin-bottom: 22px !important;
|
||||
margin-bottom: 24px !important;
|
||||
}
|
||||
// 修复行内表单最后一个 el-form-item 位置下移问题
|
||||
&.el-form--inline {
|
||||
@@ -38,12 +38,12 @@
|
||||
}
|
||||
.el-form-item--default.el-form-item:last-of-type,
|
||||
.el-form-item--small.el-form-item:last-of-type {
|
||||
margin-bottom: 18px !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
}
|
||||
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
|
||||
.el-form-item .el-form-item__label .el-icon {
|
||||
margin-right: 0px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,12 +76,16 @@
|
||||
width: 220px;
|
||||
}
|
||||
.el-menu-item {
|
||||
height: 56px !important;
|
||||
line-height: 56px !important;
|
||||
height: 46px !important;
|
||||
line-height: 46px !important;
|
||||
border-radius:12px;
|
||||
}
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
height: 46px !important;
|
||||
line-height: 46px !important;
|
||||
color: var(--next-bg-menuBarColor);
|
||||
border-radius:12px;
|
||||
}
|
||||
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
|
||||
.el-menu--collapse {
|
||||
|
||||
@@ -1,54 +1,96 @@
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<el-row :gutter="15" class="home-card-one mb15">
|
||||
<el-col
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="6"
|
||||
:xl="6"
|
||||
v-for="(v, k) in homeOne"
|
||||
:key="k"
|
||||
:class="{ 'home-media home-media-lg': k > 1, 'home-media-sm': k === 1 }"
|
||||
<div class="home-container">
|
||||
<div style="margin: 15px; font-size: 16px; font-weight: 700">
|
||||
欢迎回来,{{ userInfo.userInfos.name }}
|
||||
<span style="font-size: 12px; color: grey"> 这里是您的工作台,请愉快的工作吧!</span>
|
||||
</div>
|
||||
<el-row>
|
||||
<el-col :span="16">
|
||||
<el-row :gutter="15" class="home-card-one mb15">
|
||||
<el-col
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="8"
|
||||
:xl="6"
|
||||
v-for="(v, k) in homeOne"
|
||||
:key="k"
|
||||
:class="{ 'home-media home-media-lg': k > 1, 'home-media-sm': k === 1 }"
|
||||
>
|
||||
<div class="home-card-item flex">
|
||||
<div class="home-card-item flex" style="padding: 0;">
|
||||
<div class="flex-margin flex w100" :class="` home-one-animation${k}`">
|
||||
<div class="flex-auto">
|
||||
<span class="font30">{{ v.num1 }}</span>
|
||||
<span class="ml5 font16" :style="{ color: v.color1 }">{{ v.num2 }}%</span>
|
||||
<div class="mt10">{{ v.num3 }}</div>
|
||||
<div class="home-card-item-icon flex" style="margin: 10px;" :style="{ background: `var(${v.color2})` }">
|
||||
<i class="flex-margin font24" :class="v.num4" :style="{ color: `var(${v.color3})` }"></i>
|
||||
</div>
|
||||
<div class="home-card-item-icon flex" :style="{ background: `var(${v.color2})` }">
|
||||
<i class="flex-margin font32" :class="v.num4" :style="{ color: `var(${v.color3})` }"></i>
|
||||
<div class="flex-auto">
|
||||
<span class="font24">{{ v.num1 }}</span>
|
||||
<span class="ml5 font14" :style="{ color: v.color1 }">{{ v.num2 }}%</span>
|
||||
<div class="mt10">{{ v.num3 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="15" class="home-card-two mb15">
|
||||
<el-col :xs="24" :sm="14" :md="14" :lg="16" :xl="16">
|
||||
<el-row :gutter="15" class="home-card-two mb15">
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
|
||||
<div class="home-card-item">
|
||||
<div style="height: 100%" ref="homeLineRef"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="15" class="home-card-three">
|
||||
<el-col :span="24" :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="home-media">
|
||||
<div class="home-card-item">
|
||||
<div style="height: 100%" ref="homeLineRef"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="10" :md="10" :lg="8" :xl="8" class="home-media">
|
||||
<div class="home-card-item">
|
||||
<div style="height: 100%" ref="homePieRef"></div>
|
||||
<div style="height: 100%" ref="homeBarRef"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="15" class="home-card-three">
|
||||
<el-col :xs="24" :sm="10" :md="10" :lg="8" :xl="8">
|
||||
<div class="home-card-item">
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div style="margin-left: 15px">
|
||||
<el-row :gutter="100" class="home-card-one mb15">
|
||||
<el-col :span="24">
|
||||
<div class="home-card-item flex" style="padding: 0; width:350px; height:200px">
|
||||
<img :src="HomeBg" style="padding: 0; margin: 0;">
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="100" class="home-card-one mb15">
|
||||
<el-col :span="24" >
|
||||
<div class="home-card-item" style=" width:350px; height: 100%; max-height: 320px">
|
||||
<div class="home-card-item-title">消息通知
|
||||
<button type="button" class="el-button" style=" float: right; border-color: transparent; margin-top: -2px;" @click="msgMore">
|
||||
<span>更多
|
||||
<i class="el-icon fs-icon fs-button-icon-right"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M512 160c320 0 512 352 512 352S832 864 512 864 0 512 0 512s192-352 512-352m0 64c-225.28 0-384.128 208.064-436.8 288 52.608 79.872 211.456 288 436.8 288 225.28 0 384.128-208.064 436.8-288-52.608-79.872-211.456-288-436.8-288m0 64a224 224 0 1 1 0 448 224 224 0 0 1 0-448m0 64a160.19 160.19 0 0 0-160 160c0 88.192 71.744 160 160 160s160-71.808 160-160-71.744-160-160-160"></path></svg></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="(v, k) in newsInfoList" :key="k" class="personal-info-li flex-margin flex w100" >
|
||||
<div class="home-card-item-icon flex" style="margin: 5px;" :style="{ background: `#f8f8f8` }">
|
||||
<i class="flex-margin font24" :class="`fa fa-commenting-o`" :style="{ color: `#5d8b22` }"></i>
|
||||
</div>
|
||||
<div class="flex-auto" style="margin-top: 10px">
|
||||
<span class="font14">[{{ v.creator_name }}]</span>
|
||||
<span style=" color: grey; float: right; font-style:italic;"> {{ v.create_datetime }} </span>
|
||||
<div class="text-container"> {{ v.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="100" class="home-card-one mb15">
|
||||
<el-col :span="24">
|
||||
<div class="home-card-item" style=" width:350px; height: 100%">
|
||||
<div class="home-card-item-title">快捷导航工具</div>
|
||||
<div class="home-monitor">
|
||||
<div class="flex-warp">
|
||||
<div class="flex-warp-item" v-for="(v, k) in homeThree" :key="k">
|
||||
<div class="flex-warp-item-box" :class="`home-animation${k}`">
|
||||
<div class="flex-margin">
|
||||
<i :class="v.icon" :style="{ color: v.iconColor }"></i>
|
||||
<span class="pl5">{{ v.label }}</span>
|
||||
<div class="mt10">{{ v.value }}</div>
|
||||
<div class="home-card-item-icon flex" style="margin: 20px;" :style="{ background: '#f8f8f8' }">
|
||||
<i class="flex-margin font24" :class="v.icon" :style="{ color: v.iconColor}"></i>
|
||||
</div>
|
||||
<span class="pl20" :style="{ fontSize: 'clamp(0.875rem, 2vw, 1rem)' }">{{ v.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,13 +98,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="14" :md="14" :lg="16" :xl="16" class="home-media">
|
||||
<div class="home-card-item">
|
||||
<div style="height: 100%" ref="homeBarRef"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -71,6 +111,7 @@ import * as echarts from 'echarts';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '/@/stores/themeConfig';
|
||||
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
|
||||
import HomeBg from '/@/assets/home-bg.png';
|
||||
|
||||
let global: any = {
|
||||
homeChartOne: null,
|
||||
@@ -79,9 +120,21 @@ let global: any = {
|
||||
dispose: [null, '', undefined],
|
||||
};
|
||||
|
||||
import { useUserInfo } from '/@/stores/userInfo';
|
||||
import {useRouter} from "vue-router";
|
||||
import * as api from "/@/views/system/personal/api";
|
||||
|
||||
// 定义消息类型
|
||||
interface NewsItem {
|
||||
creator_name: string;
|
||||
create_datetime: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'home',
|
||||
setup() {
|
||||
const userInfo = useUserInfo();
|
||||
const homeLineRef = ref();
|
||||
const homePieRef = ref();
|
||||
const homeBarRef = ref();
|
||||
@@ -89,7 +142,11 @@ export default defineComponent({
|
||||
const storesThemeConfig = useThemeConfig();
|
||||
const { themeConfig } = storeToRefs(storesThemeConfig);
|
||||
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
|
||||
const router = useRouter(); // 将router移到组件级别
|
||||
const defaultNewsItems: NewsItem[] = [];
|
||||
|
||||
const state = reactive({
|
||||
newsInfoList: [...defaultNewsItems] as NewsItem[],
|
||||
homeOne: [
|
||||
{
|
||||
num1: '125,12',
|
||||
@@ -109,15 +166,6 @@ export default defineComponent({
|
||||
color2: '--next-color-success-lighter',
|
||||
color3: '--el-color-success',
|
||||
},
|
||||
{
|
||||
num1: '125,65',
|
||||
num2: '+17.32',
|
||||
num3: '年度计划信息',
|
||||
num4: 'iconfont icon-zaosheng',
|
||||
color1: '#6690F9',
|
||||
color2: '--next-color-warning-lighter',
|
||||
color3: '--el-color-warning',
|
||||
},
|
||||
{
|
||||
num1: '520,43',
|
||||
num2: '-10.01',
|
||||
@@ -131,57 +179,48 @@ export default defineComponent({
|
||||
homeThree: [
|
||||
{
|
||||
icon: 'iconfont icon-yangan',
|
||||
label: '浅粉红',
|
||||
value: '2.1%OBS/M',
|
||||
iconColor: '#F72B3F',
|
||||
label: '用户管理',
|
||||
iconColor: 'gray',
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-wendu',
|
||||
label: '深红(猩红)',
|
||||
value: '30℃',
|
||||
iconColor: '#91BFF8',
|
||||
label: '部门管理',
|
||||
iconColor: 'gray',
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-shidu',
|
||||
label: '淡紫红',
|
||||
value: '57%RH',
|
||||
iconColor: '#88D565',
|
||||
label: '权限管理',
|
||||
iconColor: 'gray',
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-shidu',
|
||||
label: '弱紫罗兰红',
|
||||
value: '107w',
|
||||
iconColor: '#88D565',
|
||||
label: '日志管理',
|
||||
iconColor: 'gray',
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-zaosheng',
|
||||
label: '中紫罗兰红',
|
||||
value: '57DB',
|
||||
iconColor: '#FBD4A0',
|
||||
label: '菜单管理',
|
||||
iconColor: 'gray',
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-zaosheng',
|
||||
label: '紫罗兰',
|
||||
value: '57PV',
|
||||
iconColor: '#FBD4A0',
|
||||
label: '消息中心',
|
||||
iconColor: 'gray',
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-zaosheng',
|
||||
label: '暗紫罗兰',
|
||||
value: '517Cpd',
|
||||
iconColor: '#FBD4A0',
|
||||
label: '接口管理',
|
||||
iconColor: 'gray',
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-zaosheng',
|
||||
label: '幽灵白',
|
||||
value: '12kg',
|
||||
iconColor: '#FBD4A0',
|
||||
label: '下载中心',
|
||||
iconColor: 'gray',
|
||||
},
|
||||
{
|
||||
icon: 'iconfont icon-zaosheng',
|
||||
label: '海军蓝',
|
||||
value: '64fm',
|
||||
iconColor: '#FBD4A0',
|
||||
label: '系统管理',
|
||||
iconColor: 'gray',
|
||||
},
|
||||
],
|
||||
myCharts: [],
|
||||
@@ -506,6 +545,7 @@ export default defineComponent({
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
initEchartsResize();
|
||||
getMsg(); // 确保组件挂载时立即获取消息列表
|
||||
});
|
||||
// 由于页面缓存原因,keep-alive
|
||||
onActivated(() => {
|
||||
@@ -542,11 +582,47 @@ export default defineComponent({
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
// 获取消息列表
|
||||
const getMsg = (): void => {
|
||||
// 先重置为默认数据
|
||||
state.newsInfoList = [...defaultNewsItems];
|
||||
|
||||
// 尝试从API获取最新数据
|
||||
api.GetSelfReceive({}).then((res: any) => {
|
||||
const { data } = res || {};
|
||||
// 严格检查返回数据的有效性
|
||||
if (data && Array.isArray(data) && data.length > 0) {
|
||||
try {
|
||||
// 安全地进行类型转换并更新状态
|
||||
state.newsInfoList = data.map((item: any): NewsItem => ({
|
||||
creator_name: String(item.creator_name || '未知用户'),
|
||||
create_datetime: String(item.create_datetime || ''),
|
||||
title: String(item.title || '')
|
||||
}));
|
||||
} catch (typeError) {
|
||||
console.error('数据类型转换失败:', typeError);
|
||||
// 类型转换失败时保持默认数据
|
||||
}
|
||||
}
|
||||
}).catch((error: Error) => {
|
||||
console.error('获取消息列表失败,保持默认数据:', error);
|
||||
// 错误时保持已设置的默认数据
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转消息中心
|
||||
const msgMore = (): void => {
|
||||
router.push({ path: '/messageCenter' });
|
||||
};
|
||||
|
||||
return {
|
||||
homeLineRef,
|
||||
homePieRef,
|
||||
homeBarRef,
|
||||
...toRefs(state),
|
||||
homeLineRef,
|
||||
homePieRef,
|
||||
homeBarRef,
|
||||
userInfo,
|
||||
...toRefs(state),
|
||||
HomeBg,
|
||||
msgMore,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -554,6 +630,20 @@ export default defineComponent({
|
||||
|
||||
<style scoped lang="scss">
|
||||
$homeNavLengh: 8;
|
||||
.text-container {
|
||||
width: 420px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.text-container:hover {
|
||||
animation: scrollText 5s linear infinite;
|
||||
}
|
||||
@keyframes scrollText {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}
|
||||
|
||||
.home-container {
|
||||
overflow: hidden;
|
||||
.home-card-one,
|
||||
@@ -561,21 +651,21 @@ $homeNavLengh: 8;
|
||||
.home-card-three {
|
||||
.home-card-item {
|
||||
width: 100%;
|
||||
height: 130px;
|
||||
border-radius: 4px;
|
||||
height: 120px;
|
||||
transition: all ease 0.3s;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
background: var(--el-color-white);
|
||||
color: var(--el-text-color-primary);
|
||||
border: 1px solid var(--next-border-color-light);
|
||||
border-radius: 24px;
|
||||
&:hover {
|
||||
box-shadow: 0 2px 12px var(--next-color-dark-hover);
|
||||
transition: all ease 0.3s;
|
||||
}
|
||||
&-icon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
border-radius: 100%;
|
||||
flex-shrink: 1;
|
||||
i {
|
||||
@@ -583,13 +673,15 @@ $homeNavLengh: 8;
|
||||
}
|
||||
}
|
||||
&-title {
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.home-card-one {
|
||||
left:15px;
|
||||
right: 15px;
|
||||
@for $i from 0 through 3 {
|
||||
.home-one-animation#{$i} {
|
||||
opacity: 0;
|
||||
@@ -602,6 +694,9 @@ $homeNavLengh: 8;
|
||||
}
|
||||
.home-card-two,
|
||||
.home-card-three {
|
||||
position: relative;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
.home-card-item {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
@@ -626,7 +721,7 @@ $homeNavLengh: 8;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
@for $i from 0 through $homeNavLengh {
|
||||
@for $i from 0 through 3 {
|
||||
.home-animation#{$i} {
|
||||
opacity: 0;
|
||||
animation-name: error-num;
|
||||
@@ -641,3 +736,4 @@ $homeNavLengh: 8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -187,8 +187,8 @@ export default defineComponent({
|
||||
// 初始化登录成功时间问候语
|
||||
let currentTimeInfo = currentTime.value;
|
||||
// 登录成功,跳到转首页
|
||||
const pwd_change_count = userInfos.value.pwd_change_count
|
||||
if(pwd_change_count>0){
|
||||
const pwd_change_count = userInfos.value.pwd_change_count ?? 0
|
||||
if(pwd_change_count > 0){
|
||||
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
|
||||
if (route.query?.redirect) {
|
||||
router.push({
|
||||
@@ -234,16 +234,37 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 定义error-num动画的关键帧
|
||||
@keyframes error-num {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.login-content-form {
|
||||
margin-top: 20px;
|
||||
|
||||
// 为输入框添加圆角和设置字体大小
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
// 设置输入框文字大小
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 12px !important; // Element Plus large尺寸的默认字体大小
|
||||
}
|
||||
|
||||
@for $i from 1 through 4 {
|
||||
.login-animation#{$i} {
|
||||
opacity: 0;
|
||||
animation-name: error-num;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: calc($i/10) + s;
|
||||
animation-delay: #{$i/10}s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +274,7 @@ export default defineComponent({
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #909399;
|
||||
color: #909397;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,6 +283,7 @@ export default defineComponent({
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
letter-spacing: 5px;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.login-content-submit {
|
||||
@@ -269,6 +291,7 @@ export default defineComponent({
|
||||
letter-spacing: 2px;
|
||||
font-weight: 800;
|
||||
margin-top: 15px;
|
||||
border-radius:8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -239,6 +239,15 @@ export default defineComponent({
|
||||
.login-content-form {
|
||||
margin-top: 20px;
|
||||
|
||||
// 为输入框添加圆角和设置字体大小
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
// 设置输入框文字大小
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 12px !important; // Element Plus large尺寸的默认字体大小
|
||||
}
|
||||
|
||||
@for $i from 1 through 5 {
|
||||
.login-animation#{$i} {
|
||||
opacity: 0;
|
||||
@@ -264,6 +273,7 @@ export default defineComponent({
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
letter-spacing: 5px;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.login-content-submit {
|
||||
@@ -271,6 +281,7 @@ export default defineComponent({
|
||||
letter-spacing: 2px;
|
||||
font-weight: 800;
|
||||
margin-top: 15px;
|
||||
border-radius:8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
<img :src="siteLogo" />
|
||||
<div class="login-left-logo-text">
|
||||
<span>{{ getSystemConfig['login.site_title'] || getThemeConfig.globalViceTitle }}</span>
|
||||
<span class="login-left-logo-text-msg" style="margin-top: 5px;">{{
|
||||
getSystemConfig['login.site_name'] || getThemeConfig.globalViceTitleMsg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,7 +14,9 @@
|
||||
<!-- <span class="login-right-warp-two"></span>-->
|
||||
<div class="login-right-warp-mian">
|
||||
<div class="login-right-warp-main-title">
|
||||
{{userInfos.pwd_change_count===0?'初次登录修改密码':'欢迎登录'}}
|
||||
<span>{{getSystemConfig['login.site_name'] || getThemeConfig.globalViceTitleMsg }}</span>
|
||||
<br>
|
||||
<span>{{userInfos.pwd_change_count===0?'初次登录请修改密码':'欢迎登录'}}</span>
|
||||
</div>
|
||||
<div class="login-right-warp-main-form">
|
||||
<div v-if="!state.isScan">
|
||||
@@ -168,14 +168,11 @@ onMounted(() => {
|
||||
|
||||
span {
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
font-size: 24px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.login-left-logo-text-msg {
|
||||
font-size: 12px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,10 +277,10 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
|
||||
.login-right-warp-main-title {
|
||||
height: 130px;
|
||||
line-height: 130px;
|
||||
font-size: 32px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
letter-spacing: 3px;
|
||||
@@ -347,7 +344,7 @@ onMounted(() => {
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
//固定右侧
|
||||
fixed: 'right',
|
||||
width: computed(() => {
|
||||
if (auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch')){
|
||||
if (auth('role:AllAuthorizedUser') || auth('role:AllCanMenu')){
|
||||
return 420;
|
||||
}
|
||||
return 320;
|
||||
@@ -66,7 +66,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
assignment: {
|
||||
type: 'primary',
|
||||
text: '授权用户',
|
||||
show: auth('role:AuthorizedAdd') || auth('role:AuthorizedSearch'),
|
||||
show: auth('role:AllAuthorizedUser'),
|
||||
click: (ctx: any) => {
|
||||
const { row } = ctx;
|
||||
context!.RoleUserDrawer.handleDrawerOpen(row);
|
||||
@@ -79,7 +79,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp
|
||||
permission: {
|
||||
type: 'primary',
|
||||
text: '权限配置',
|
||||
show: auth('role:Permission'),
|
||||
show: auth('role:SetMenu'),
|
||||
click: (clickContext: any): void => {
|
||||
const { row } = clickContext;
|
||||
context.RoleDrawer.handleDrawerOpen(row);
|
||||
|
||||
@@ -91,7 +91,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
||||
rowHandle: {
|
||||
//固定右侧
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
width: 220,
|
||||
buttons: {
|
||||
view: {
|
||||
show: false,
|
||||
@@ -205,10 +205,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
||||
},
|
||||
},
|
||||
dept: {
|
||||
title: '部门',
|
||||
search: {
|
||||
disabled: true,
|
||||
},
|
||||
title: '所属部门',
|
||||
type: 'dict-tree',
|
||||
dict: dict({
|
||||
isTree: true,
|
||||
@@ -217,7 +214,7 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
||||
label: 'name'
|
||||
}),
|
||||
column: {
|
||||
minWidth: 200, //最小列宽
|
||||
minWidth: 300, //最小列宽
|
||||
formatter({ value, row, index }) {
|
||||
return row.dept_name_all
|
||||
}
|
||||
@@ -243,6 +240,39 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
||||
},
|
||||
},
|
||||
},
|
||||
manage_dept: {
|
||||
title: '管理部门',
|
||||
type: 'dict-tree',
|
||||
dict: dict({
|
||||
isTree: true,
|
||||
url: '/api/system/dept/all_dept/',
|
||||
value: 'id',
|
||||
label: 'name'
|
||||
}),
|
||||
column: {
|
||||
minWidth: 300
|
||||
},
|
||||
form: {
|
||||
value: [],
|
||||
component: {
|
||||
filterable: true,
|
||||
multiple: true,
|
||||
placeholder: '请选择',
|
||||
clearable: true,
|
||||
collapseTags: true,
|
||||
maxCollapseTags: 2,
|
||||
collapseTagsTooltip: true,
|
||||
props: {
|
||||
checkStrictly: true,
|
||||
props: {
|
||||
value: 'id',
|
||||
label: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
helper: '不选则默认为所属部门',
|
||||
},
|
||||
},
|
||||
role: {
|
||||
title: '角色',
|
||||
search: {
|
||||
@@ -378,6 +408,9 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
||||
dict: dict({
|
||||
data: dictionary('button_status_bool'),
|
||||
}),
|
||||
form: {
|
||||
value: true
|
||||
}
|
||||
},
|
||||
avatar: {
|
||||
title: '头像',
|
||||
@@ -392,8 +425,8 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
|
||||
},
|
||||
...commonCrudConfig({
|
||||
dept_belong_id: {
|
||||
form: true,
|
||||
table: true
|
||||
form: false,
|
||||
table: false
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import vue from '@vitejs/plugin-vue';
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig, loadEnv, ConfigEnv } from 'vite';
|
||||
import vueSetupExtend from 'vite-plugin-vue-setup-extend';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
// import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import { generateVersionFile } from "/@/utils/upgrade";
|
||||
|
||||
const pathResolve = (dir: string) => {
|
||||
@@ -22,7 +22,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
|
||||
// 当Vite构建时,生成版本文件
|
||||
generateVersionFile()
|
||||
return {
|
||||
plugins: [vue(), vueJsx(), vueSetupExtend()],
|
||||
plugins: [vue(), /* vueJsx(), */ vueSetupExtend()],
|
||||
root: process.cwd(),
|
||||
resolve: { alias },
|
||||
base: mode.command === 'serve' ? './' : env.VITE_PUBLIC_PATH,
|
||||
|
||||
Reference in New Issue
Block a user