17 Commits

Author SHA1 Message Date
liqiang
3a2739774a refactor(file): 调整文件列表视图的认证类配置 2025-12-07 00:23:15 +08:00
liqiang
012f3a2f63 refactor(file): 调整文件列表视图的认证类配置 2025-12-06 23:03:50 +08:00
ahui
64c5d505e7 Merge branch 'develop' of https://gitee.com/huge-dream/django-vue3-admin into develop 2025-12-01 10:41:43 +08:00
ahui
3914432d9f 修复权限过滤器DataLevelPermissionsSubFilter单独使用时没有经过数据权限过滤的问题 2025-12-01 10:28:51 +08:00
ahui
cc6ac68223 角色授权按钮更新 2025-12-01 10:24:45 +08:00
1638245306
05ee833fe5 chore(layout): 移除用户头像在线状态徽章
- 注释掉了用户头像的在线状态徽章组件
- 移除了相关的头像图片显示逻辑
-保留了用户名显示功能
2025-11-01 22:28:48 +08:00
1638245306
ff736aae93 feat(layout): 添加搜索功能图标并调整布局
- 在用户面包屑导航中添加搜索图标
- 调整图标点击事件处理- 移除重复的搜索图标代码
- 优化布局结构和样式
2025-11-01 22:15:55 +08:00
liyimin
c0a68f91ca 首页优化 2025-11-01 16:14:36 +08:00
liyimin
8d6abeb891 登陆界面美化 2025-10-31 23:26:27 +08:00
1638245306
163e5eb2db fix(role): 更新角色授权用户权限检查
- 将授权用户按钮的权限检查从 role:AuthorizedAdd 更改为 role:SetUserRole
-保持授权用户搜索权限检查不变- 确保只有具有适当权限的用户才能访问角色分配功能
2025-10-23 22:16:03 +08:00
1638245306
e786f60cdd feat(websocket): 实现 WebSocket 消息推送功能
- 配置 ASGI 支持 WebSocket 连接
- 新增 WebSocket 路由和消费者类 MegCenter
- 实现消息序列化和推送逻辑
- 前端集成 WebSocket 连接状态管理和重连机制
- 添加用户在线状态提示和未读消息提醒- 更新角色权限配置显示条件
- 扩展用户信息存储结构支持 WebSocket 状态字段
2025-10-19 16:03:59 +08:00
1638245306
abe2db3c82 feat(websocket): 实现 WebSocket 消息推送功能
- 配置 ASGI 支持 WebSocket 连接
- 新增 WebSocket 路由和消费者类 MegCenter
- 实现消息序列化和推送逻辑
- 前端集成 WebSocket 连接状态管理和重连机制
- 添加用户在线状态提示和未读消息提醒- 更新角色权限配置显示条件
- 扩展用户信息存储结构支持 WebSocket 状态字段
2025-10-19 16:03:16 +08:00
ahui
fa734dd479 管理部门数据级过滤器优化 2025-08-22 18:03:51 +08:00
ahui
6e9b94aed2 用户的管理部门权限功能 2025-08-14 10:35:51 +08:00
ahui
2a9f5be895 页面优化 2025-08-13 10:29:52 +08:00
ahui
2ea34bfbd5 工具函数更新 2025-08-07 14:10:07 +08:00
ahui
edbd6862a2 用户表增加当前角色字段 2025-08-07 14:09:48 +08:00
29 changed files with 1139 additions and 539 deletions

View File

@@ -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
)
)
),
})

View File

@@ -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

View 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}
}
)

View 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 是该路由的消费者
]

View File

@@ -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 = "部门表"

View File

@@ -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):

View File

@@ -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])

View File

@@ -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="获取成功")

View File

@@ -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

View File

@@ -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 = {}

View File

@@ -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优化速度非常快"""

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -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 || '&nbsp;' }}</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 || '&nbsp;' }}</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>

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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) {

View File

@@ -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;

View File

@@ -23,6 +23,7 @@ export interface UserInfosState {
}
export interface UserInfosStates {
userInfos: UserInfosState;
isSocketOpen: boolean
}
// 路由缓存列表

View File

@@ -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,
// 是否开启分栏菜单鼠标悬停预加载(预览菜单)

View File

@@ -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/',

View File

@@ -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 {

View File

@@ -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;">&nbsp;{{ v.create_datetime }}&nbsp;&nbsp;</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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
}
})
},

View File

@@ -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,