feat(system_manage): 添加接入控制策略功能
- 新增接入控制策略模型和应用记录模型 - 实现策略的增删改查和应用功能 - 添加策略应用记录查询功能 - 在系统设置菜单中增加策略管理入口 - 实现前端策略管理页面和相关API接口 - 完成策略内容展示和时间范围格式化功能v3.2
parent
6c78f7372c
commit
83fbb3c600
|
|
@ -0,0 +1,61 @@
|
|||
# Generated by Django 5.2.8 on 2026-04-01 00:00 tanlianwang
|
||||
|
||||
import uuid_utils.compat
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system_manage', '0005_usermenusetting'),
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AccessControlPolicy',
|
||||
fields=[
|
||||
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
|
||||
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
|
||||
('name', models.CharField(max_length=128, unique=True, verbose_name='策略名称')),
|
||||
('description', models.TextField(blank=True, default='', verbose_name='策略描述')),
|
||||
('enabled', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('access_time_enabled', models.BooleanField(default=False, verbose_name='是否限制访问时段')),
|
||||
('access_start_time', models.TimeField(blank=True, null=True, verbose_name='访问开始时间')),
|
||||
('access_end_time', models.TimeField(blank=True, null=True, verbose_name='访问结束时间')),
|
||||
('allowed_weekdays', models.JSONField(default=list, verbose_name='允许访问星期')),
|
||||
('allowed_device_types', models.JSONField(default=list, verbose_name='允许接入设备类型')),
|
||||
('allowed_regions', models.JSONField(default=list, verbose_name='允许接入区域')),
|
||||
('effect_start_time', models.DateTimeField(blank=True, null=True, verbose_name='生效开始时间')),
|
||||
('effect_end_time', models.DateTimeField(blank=True, null=True, verbose_name='生效结束时间')),
|
||||
('application_count', models.PositiveIntegerField(default=0, verbose_name='应用次数')),
|
||||
('last_applied_time', models.DateTimeField(blank=True, null=True, verbose_name='最近应用时间')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'access_control_policy',
|
||||
'ordering': ['-update_time', '-create_time'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AccessControlPolicyApplicationRecord',
|
||||
fields=[
|
||||
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
|
||||
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
|
||||
('policy_name', models.CharField(max_length=128, verbose_name='策略名称快照')),
|
||||
('target_type', models.CharField(choices=[('DEVICE', '设备'), ('SERVICE', '服务')], max_length=32, verbose_name='应用对象类型')),
|
||||
('target_name', models.CharField(max_length=128, verbose_name='应用对象名称')),
|
||||
('target_identifier', models.CharField(blank=True, default='', max_length=128, verbose_name='应用对象标识')),
|
||||
('remark', models.TextField(blank=True, default='', verbose_name='备注')),
|
||||
('operator_name', models.CharField(blank=True, default='', max_length=128, verbose_name='操作人名称')),
|
||||
('operator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.user', verbose_name='操作人')),
|
||||
('policy', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='application_records', to='system_manage.accesscontrolpolicy', verbose_name='策略')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'access_control_policy_application_record',
|
||||
'ordering': ['-create_time'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -11,3 +11,4 @@ from .system_setting import *
|
|||
from .log_management import *
|
||||
from .chat_user import *
|
||||
from .user_menu_setting import *
|
||||
from .access_control_policy import *
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:TanLianWang
|
||||
@file: access_control_policy.py
|
||||
@date:2026/4/1
|
||||
@desc: 接入控制策略
|
||||
"""
|
||||
import uuid_utils.compat as uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
from common.mixins.app_model_mixin import AppModelMixin
|
||||
from users.models import User
|
||||
|
||||
|
||||
class AccessControlPolicy(AppModelMixin):
|
||||
"""
|
||||
接入控制策略
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name="策略名称")
|
||||
description = models.TextField(blank=True, default="", verbose_name="策略描述")
|
||||
enabled = models.BooleanField(default=True, verbose_name="是否启用")
|
||||
access_time_enabled = models.BooleanField(default=False, verbose_name="是否限制访问时段")
|
||||
access_start_time = models.TimeField(null=True, blank=True, verbose_name="访问开始时间")
|
||||
access_end_time = models.TimeField(null=True, blank=True, verbose_name="访问结束时间")
|
||||
allowed_weekdays = models.JSONField(default=list, verbose_name="允许访问星期")
|
||||
allowed_device_types = models.JSONField(default=list, verbose_name="允许接入设备类型")
|
||||
allowed_regions = models.JSONField(default=list, verbose_name="允许接入区域")
|
||||
effect_start_time = models.DateTimeField(null=True, blank=True, verbose_name="生效开始时间")
|
||||
effect_end_time = models.DateTimeField(null=True, blank=True, verbose_name="生效结束时间")
|
||||
application_count = models.PositiveIntegerField(default=0, verbose_name="应用次数")
|
||||
last_applied_time = models.DateTimeField(null=True, blank=True, verbose_name="最近应用时间")
|
||||
|
||||
class Meta:
|
||||
db_table = "access_control_policy"
|
||||
ordering = ['-update_time', '-create_time']
|
||||
|
||||
|
||||
class AccessControlPolicyApplicationRecord(AppModelMixin):
|
||||
"""
|
||||
接入控制策略应用记录
|
||||
"""
|
||||
|
||||
class TargetType(models.TextChoices):
|
||||
DEVICE = "DEVICE", "设备"
|
||||
SERVICE = "SERVICE", "服务"
|
||||
|
||||
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
|
||||
policy = models.ForeignKey(
|
||||
AccessControlPolicy,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='application_records',
|
||||
verbose_name="策略",
|
||||
)
|
||||
policy_name = models.CharField(max_length=128, verbose_name="策略名称快照")
|
||||
target_type = models.CharField(max_length=32, choices=TargetType.choices, verbose_name="应用对象类型")
|
||||
target_name = models.CharField(max_length=128, verbose_name="应用对象名称")
|
||||
target_identifier = models.CharField(max_length=128, blank=True, default="", verbose_name="应用对象标识")
|
||||
remark = models.TextField(blank=True, default="", verbose_name="备注")
|
||||
operator = models.ForeignKey(
|
||||
User,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name="操作人",
|
||||
)
|
||||
operator_name = models.CharField(max_length=128, blank=True, default="", verbose_name="操作人名称")
|
||||
|
||||
class Meta:
|
||||
db_table = "access_control_policy_application_record"
|
||||
ordering = ['-create_time']
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:TanLianWang
|
||||
@file: access_control_policy.py
|
||||
@date:2026/4/1
|
||||
@desc: 接入控制策略序列化
|
||||
"""
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.exception.app_exception import AppApiException
|
||||
from system_manage.models import AccessControlPolicy, AccessControlPolicyApplicationRecord
|
||||
|
||||
|
||||
def _normalize_string_list(value):
|
||||
normalized = []
|
||||
seen = set()
|
||||
for item in value or []:
|
||||
text = str(item).strip()
|
||||
if not text or text in seen:
|
||||
continue
|
||||
seen.add(text)
|
||||
normalized.append(text)
|
||||
return normalized
|
||||
|
||||
|
||||
class AccessControlPolicySerializer(serializers.Serializer):
|
||||
id = serializers.UUIDField(required=False)
|
||||
name = serializers.CharField(required=True, max_length=128)
|
||||
description = serializers.CharField(required=False, allow_blank=True, default='')
|
||||
enabled = serializers.BooleanField(required=False, default=True)
|
||||
access_time_enabled = serializers.BooleanField(required=False, default=False)
|
||||
access_start_time = serializers.TimeField(required=False, allow_null=True)
|
||||
access_end_time = serializers.TimeField(required=False, allow_null=True)
|
||||
allowed_weekdays = serializers.ListField(child=serializers.IntegerField(min_value=1, max_value=7), required=False)
|
||||
allowed_device_types = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
allowed_regions = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
effect_start_time = serializers.DateTimeField(required=False, allow_null=True)
|
||||
effect_end_time = serializers.DateTimeField(required=False, allow_null=True)
|
||||
application_count = serializers.IntegerField(required=False)
|
||||
last_applied_time = serializers.DateTimeField(required=False, allow_null=True)
|
||||
create_time = serializers.DateTimeField(required=False)
|
||||
update_time = serializers.DateTimeField(required=False)
|
||||
content = serializers.CharField(required=False)
|
||||
|
||||
@staticmethod
|
||||
def _build_content(data):
|
||||
segments = []
|
||||
if data.get('access_time_enabled') and data.get('access_start_time') and data.get('access_end_time'):
|
||||
weekdays = data.get('allowed_weekdays') or []
|
||||
weekday_text = '、'.join([f'周{["一", "二", "三", "四", "五", "六", "日"][int(item) - 1]}' for item in weekdays]) if weekdays else '每日'
|
||||
segments.append(f'{weekday_text} {data.get("access_start_time")} - {data.get("access_end_time")}')
|
||||
else:
|
||||
segments.append('不限访问时段')
|
||||
device_types = data.get('allowed_device_types') or []
|
||||
segments.append(f'设备类型:{"、".join(device_types) if device_types else "不限"}')
|
||||
regions = data.get('allowed_regions') or []
|
||||
segments.append(f'接入区域:{"、".join(regions) if regions else "不限"}')
|
||||
return ';'.join(segments)
|
||||
|
||||
@staticmethod
|
||||
def to_representation_from_instance(instance: AccessControlPolicy):
|
||||
data = {
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'enabled': instance.enabled,
|
||||
'access_time_enabled': instance.access_time_enabled,
|
||||
'access_start_time': instance.access_start_time.strftime('%H:%M:%S') if instance.access_start_time else None,
|
||||
'access_end_time': instance.access_end_time.strftime('%H:%M:%S') if instance.access_end_time else None,
|
||||
'allowed_weekdays': instance.allowed_weekdays or [],
|
||||
'allowed_device_types': instance.allowed_device_types or [],
|
||||
'allowed_regions': instance.allowed_regions or [],
|
||||
'effect_start_time': instance.effect_start_time.isoformat() if instance.effect_start_time else None,
|
||||
'effect_end_time': instance.effect_end_time.isoformat() if instance.effect_end_time else None,
|
||||
'application_count': instance.application_count,
|
||||
'last_applied_time': instance.last_applied_time.isoformat() if instance.last_applied_time else None,
|
||||
'create_time': instance.create_time.isoformat() if instance.create_time else None,
|
||||
'update_time': instance.update_time.isoformat() if instance.update_time else None,
|
||||
}
|
||||
data['content'] = AccessControlPolicySerializer._build_content(data)
|
||||
return data
|
||||
|
||||
def validate_name(self, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
raise AppApiException(500, '策略名称不能为空')
|
||||
queryset = AccessControlPolicy.objects.filter(name=value)
|
||||
if self.instance is not None:
|
||||
queryset = queryset.exclude(id=self.instance.id)
|
||||
if queryset.exists():
|
||||
raise AppApiException(500, '策略名称已存在')
|
||||
return value
|
||||
|
||||
def validate_allowed_weekdays(self, value):
|
||||
return sorted(set(value or []))
|
||||
|
||||
def validate_allowed_device_types(self, value):
|
||||
return _normalize_string_list(value)
|
||||
|
||||
def validate_allowed_regions(self, value):
|
||||
return _normalize_string_list(value)
|
||||
|
||||
def validate(self, attrs):
|
||||
access_time_enabled = attrs.get('access_time_enabled', getattr(self.instance, 'access_time_enabled', False))
|
||||
access_start_time = attrs.get('access_start_time', getattr(self.instance, 'access_start_time', None))
|
||||
access_end_time = attrs.get('access_end_time', getattr(self.instance, 'access_end_time', None))
|
||||
effect_start_time = attrs.get('effect_start_time', getattr(self.instance, 'effect_start_time', None))
|
||||
effect_end_time = attrs.get('effect_end_time', getattr(self.instance, 'effect_end_time', None))
|
||||
|
||||
if access_time_enabled and (access_start_time is None or access_end_time is None):
|
||||
raise AppApiException(500, '启用访问时段限制后,必须设置开始和结束时间')
|
||||
if access_start_time and access_end_time and access_start_time >= access_end_time:
|
||||
raise AppApiException(500, '访问结束时间必须晚于开始时间')
|
||||
if effect_start_time and effect_end_time and effect_start_time >= effect_end_time:
|
||||
raise AppApiException(500, '生效结束时间必须晚于开始时间')
|
||||
return attrs
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, **kwargs):
|
||||
self.is_valid(raise_exception=True)
|
||||
validated_data = self.validated_data
|
||||
if self.instance is None:
|
||||
instance = AccessControlPolicy.objects.create(**validated_data)
|
||||
else:
|
||||
instance = self.instance
|
||||
for key, value in validated_data.items():
|
||||
setattr(instance, key, value)
|
||||
instance.save()
|
||||
return self.to_representation_from_instance(instance)
|
||||
|
||||
@staticmethod
|
||||
def list(name='', enabled=None):
|
||||
queryset = AccessControlPolicy.objects.all()
|
||||
if name:
|
||||
queryset = queryset.filter(name__icontains=name.strip())
|
||||
if enabled in ['true', 'false']:
|
||||
queryset = queryset.filter(enabled=(enabled == 'true'))
|
||||
return [AccessControlPolicySerializer.to_representation_from_instance(item) for item in queryset]
|
||||
|
||||
@staticmethod
|
||||
def get_one(policy_id):
|
||||
instance = AccessControlPolicy.objects.filter(id=policy_id).first()
|
||||
if instance is None:
|
||||
raise AppApiException(500, '接入控制策略不存在')
|
||||
return instance
|
||||
|
||||
|
||||
class AccessControlPolicyApplySerializer(serializers.Serializer):
|
||||
target_type = serializers.ChoiceField(choices=AccessControlPolicyApplicationRecord.TargetType.choices)
|
||||
target_name = serializers.CharField(required=True, max_length=128)
|
||||
target_identifier = serializers.CharField(required=False, allow_blank=True, default='', max_length=128)
|
||||
remark = serializers.CharField(required=False, allow_blank=True, default='')
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, policy: AccessControlPolicy, operator):
|
||||
self.is_valid(raise_exception=True)
|
||||
validated_data = self.validated_data
|
||||
AccessControlPolicyApplicationRecord.objects.create(
|
||||
policy=policy,
|
||||
policy_name=policy.name,
|
||||
target_type=validated_data.get('target_type'),
|
||||
target_name=validated_data.get('target_name').strip(),
|
||||
target_identifier=validated_data.get('target_identifier', '').strip(),
|
||||
remark=validated_data.get('remark', '').strip(),
|
||||
operator=operator,
|
||||
operator_name=getattr(operator, 'nick_name', '') or getattr(operator, 'username', ''),
|
||||
)
|
||||
policy.application_count = (policy.application_count or 0) + 1
|
||||
policy.last_applied_time = timezone.now()
|
||||
policy.save(update_fields=['application_count', 'last_applied_time', 'update_time'])
|
||||
return {'policy_id': str(policy.id), 'target_name': validated_data.get('target_name').strip()}
|
||||
|
||||
|
||||
class AccessControlPolicyApplicationRecordSerializer(serializers.Serializer):
|
||||
id = serializers.UUIDField(required=False)
|
||||
policy_id = serializers.UUIDField(required=False, allow_null=True)
|
||||
policy_name = serializers.CharField(required=False)
|
||||
target_type = serializers.CharField(required=False)
|
||||
target_name = serializers.CharField(required=False)
|
||||
target_identifier = serializers.CharField(required=False)
|
||||
remark = serializers.CharField(required=False)
|
||||
operator_name = serializers.CharField(required=False)
|
||||
apply_time = serializers.DateTimeField(required=False)
|
||||
|
||||
@staticmethod
|
||||
def list(policy_name='', target_name=''):
|
||||
queryset = AccessControlPolicyApplicationRecord.objects.all()
|
||||
if policy_name:
|
||||
queryset = queryset.filter(policy_name__icontains=policy_name.strip())
|
||||
if target_name:
|
||||
queryset = queryset.filter(Q(target_name__icontains=target_name.strip()) | Q(target_identifier__icontains=target_name.strip()))
|
||||
records = []
|
||||
for item in queryset:
|
||||
records.append({
|
||||
'id': item.id,
|
||||
'policy_id': item.policy_id,
|
||||
'policy_name': item.policy_name,
|
||||
'target_type': item.target_type,
|
||||
'target_name': item.target_name,
|
||||
'target_identifier': item.target_identifier,
|
||||
'remark': item.remark,
|
||||
'operator_name': item.operator_name,
|
||||
'apply_time': item.create_time.isoformat() if item.create_time else None,
|
||||
})
|
||||
return records
|
||||
|
|
@ -12,6 +12,10 @@ urlpatterns = [
|
|||
path('email_setting', views.SystemSetting.Email.as_view()),
|
||||
path('menu_setting', views.MenuSettingView.as_view()),
|
||||
path('menu_setting/current', views.CurrentMenuSettingView.as_view()),
|
||||
path('access_control_policy', views.AccessControlPolicyView.as_view()),
|
||||
path('access_control_policy/application_record', views.AccessControlPolicyApplicationRecordView.as_view()),
|
||||
path('access_control_policy/<uuid:policy_id>', views.AccessControlPolicyDetailView.as_view()),
|
||||
path('access_control_policy/<uuid:policy_id>/apply', views.AccessControlPolicyApplyView.as_view()),
|
||||
path('profile', views.SystemProfile.as_view()),
|
||||
path('valid/<str:valid_type>/<int:valid_count>', views.Valid.as_view()),
|
||||
path('config', views.get_system_config)
|
||||
|
|
|
|||
|
|
@ -12,3 +12,4 @@ from .system_profile import *
|
|||
from .valid import *
|
||||
from .system_config import *
|
||||
from .menu_setting import *
|
||||
from .access_control_policy import *
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:TanLianWang
|
||||
@file: access_control_policy.py
|
||||
@date:2026/4/1
|
||||
@desc: 接入控制策略视图
|
||||
"""
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from common.auth import TokenAuth
|
||||
from common.auth.authentication import has_permissions
|
||||
from common.constants.permission_constants import PermissionConstants, RoleConstants
|
||||
from common.log.log import log
|
||||
from common.result import result
|
||||
from system_manage.serializers.access_control_policy import (
|
||||
AccessControlPolicySerializer,
|
||||
AccessControlPolicyApplySerializer,
|
||||
AccessControlPolicyApplicationRecordSerializer,
|
||||
)
|
||||
|
||||
|
||||
def get_access_control_policy_details(request):
|
||||
return {
|
||||
'path': request.path,
|
||||
'body': request.data,
|
||||
'query': request.query_params,
|
||||
}
|
||||
|
||||
|
||||
class AccessControlPolicyView(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@extend_schema(tags=['Access Control Policy'])
|
||||
@has_permissions(PermissionConstants.USER_READ, RoleConstants.ADMIN)
|
||||
def get(self, request: Request):
|
||||
return result.success(
|
||||
AccessControlPolicySerializer.list(
|
||||
name=request.query_params.get('name', ''),
|
||||
enabled=request.query_params.get('enabled'),
|
||||
)
|
||||
)
|
||||
|
||||
@extend_schema(tags=['Access Control Policy'])
|
||||
@log(menu='Policy management', operate='Create access control policy', get_details=get_access_control_policy_details)
|
||||
@has_permissions(PermissionConstants.USER_EDIT, RoleConstants.ADMIN)
|
||||
def post(self, request: Request):
|
||||
return result.success(AccessControlPolicySerializer(data=request.data).save())
|
||||
|
||||
|
||||
class AccessControlPolicyDetailView(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@extend_schema(tags=['Access Control Policy'])
|
||||
@has_permissions(PermissionConstants.USER_READ, RoleConstants.ADMIN)
|
||||
def get(self, request: Request, policy_id):
|
||||
return result.success(
|
||||
AccessControlPolicySerializer.to_representation_from_instance(
|
||||
AccessControlPolicySerializer.get_one(policy_id)
|
||||
)
|
||||
)
|
||||
|
||||
@extend_schema(tags=['Access Control Policy'])
|
||||
@log(menu='Policy management', operate='Update access control policy', get_details=get_access_control_policy_details)
|
||||
@has_permissions(PermissionConstants.USER_EDIT, RoleConstants.ADMIN)
|
||||
def put(self, request: Request, policy_id):
|
||||
instance = AccessControlPolicySerializer.get_one(policy_id)
|
||||
return result.success(AccessControlPolicySerializer(instance=instance, data=request.data).save())
|
||||
|
||||
@extend_schema(tags=['Access Control Policy'])
|
||||
@log(menu='Policy management', operate='Delete access control policy', get_details=get_access_control_policy_details)
|
||||
@has_permissions(PermissionConstants.USER_EDIT, RoleConstants.ADMIN)
|
||||
def delete(self, request: Request, policy_id):
|
||||
instance = AccessControlPolicySerializer.get_one(policy_id)
|
||||
instance.delete()
|
||||
return result.success(True)
|
||||
|
||||
|
||||
class AccessControlPolicyApplyView(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@extend_schema(tags=['Access Control Policy'])
|
||||
@log(menu='Policy management', operate='Apply access control policy', get_details=get_access_control_policy_details)
|
||||
@has_permissions(PermissionConstants.USER_EDIT, RoleConstants.ADMIN)
|
||||
def post(self, request: Request, policy_id):
|
||||
policy = AccessControlPolicySerializer.get_one(policy_id)
|
||||
serializer = AccessControlPolicyApplySerializer(data=request.data)
|
||||
return result.success(serializer.save(policy=policy, operator=request.user))
|
||||
|
||||
|
||||
class AccessControlPolicyApplicationRecordView(APIView):
|
||||
authentication_classes = [TokenAuth]
|
||||
|
||||
@extend_schema(tags=['Access Control Policy'])
|
||||
@has_permissions(PermissionConstants.USER_READ, RoleConstants.ADMIN)
|
||||
def get(self, request: Request):
|
||||
return result.success(
|
||||
AccessControlPolicyApplicationRecordSerializer.list(
|
||||
policy_name=request.query_params.get('policy_name', ''),
|
||||
target_name=request.query_params.get('target_name', ''),
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { Result } from '@/request/Result'
|
||||
import { del, get, post, put } from '@/request'
|
||||
import { type Ref } from 'vue'
|
||||
|
||||
const prefix = '/access_control_policy'
|
||||
|
||||
const getPolicyList = (params?: any, loading?: Ref<boolean>): Promise<Result<any>> => {
|
||||
return get(prefix, params, loading)
|
||||
}
|
||||
|
||||
const getPolicyDetail = (id: string, loading?: Ref<boolean>): Promise<Result<any>> => {
|
||||
return get(`${prefix}/${id}`, undefined, loading)
|
||||
}
|
||||
|
||||
const createPolicy = (data: any, loading?: Ref<boolean>): Promise<Result<any>> => {
|
||||
return post(prefix, data, undefined, loading)
|
||||
}
|
||||
|
||||
const updatePolicy = (id: string, data: any, loading?: Ref<boolean>): Promise<Result<any>> => {
|
||||
return put(`${prefix}/${id}`, data, undefined, loading)
|
||||
}
|
||||
|
||||
const deletePolicy = (id: string, loading?: Ref<boolean>): Promise<Result<any>> => {
|
||||
return del(`${prefix}/${id}`, undefined, undefined, loading)
|
||||
}
|
||||
|
||||
const applyPolicy = (id: string, data: any, loading?: Ref<boolean>): Promise<Result<any>> => {
|
||||
return post(`${prefix}/${id}/apply`, data, undefined, loading)
|
||||
}
|
||||
|
||||
const getApplicationRecordList = (params?: any, loading?: Ref<boolean>): Promise<Result<any>> => {
|
||||
return get(`${prefix}/application_record`, params, loading)
|
||||
}
|
||||
|
||||
export default {
|
||||
getPolicyList,
|
||||
getPolicyDetail,
|
||||
createPolicy,
|
||||
updatePolicy,
|
||||
deletePolicy,
|
||||
applyPolicy,
|
||||
getApplicationRecordList,
|
||||
}
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
<div>
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; padding: 10px 16px; border-radius: 8px; cursor: pointer;"
|
||||
:style="activeSystemMenu.startsWith('/system/email') ? { backgroundColor: '#e6f0ff', color: '#1890ff' } : {}"
|
||||
:style="activeSystemMenu.startsWith('/system/email') || activeSystemMenu.startsWith('/system/setting/') ? { backgroundColor: '#e6f0ff', color: '#1890ff' } : {}"
|
||||
@click="toggleSystemSettings"
|
||||
>
|
||||
<span>{{ menuText.systemSettings }}</span>
|
||||
|
|
@ -129,6 +129,13 @@
|
|||
>
|
||||
{{ menuText.menuManagement }}
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/system/setting/policy"
|
||||
style="display: block; padding: 8px 16px; border-radius: 8px; text-decoration: none; color: #666; font-size: 13px;"
|
||||
:style="activeSystemMenu === '/system/setting/policy' ? { backgroundColor: '#e6f0ff', color: '#1890ff' } : {}"
|
||||
>
|
||||
{{ menuText.policyManagement }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -164,6 +171,7 @@ const menuText = computed(() => {
|
|||
systemSettings: 'System Settings',
|
||||
emailSettings: 'Email Settings',
|
||||
menuManagement: 'Menu Management',
|
||||
policyManagement: 'Policy Management',
|
||||
}
|
||||
}
|
||||
if (locale.value === 'zh-Hant') {
|
||||
|
|
@ -178,6 +186,7 @@ const menuText = computed(() => {
|
|||
systemSettings: '系統設定',
|
||||
emailSettings: '郵件設定',
|
||||
menuManagement: '菜單管理',
|
||||
policyManagement: '策略管理',
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
|
@ -191,6 +200,7 @@ const menuText = computed(() => {
|
|||
systemSettings: '系统设置',
|
||||
emailSettings: '邮件设置',
|
||||
menuManagement: '菜单管理',
|
||||
policyManagement: '策略管理',
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -221,6 +231,9 @@ const activeSystemMenu = computed(() => {
|
|||
if (path.startsWith('/system/setting/menu') || path.startsWith('/admin/system/setting/menu')) {
|
||||
return path.replace('/admin', '')
|
||||
}
|
||||
if (path.startsWith('/system/setting/policy') || path.startsWith('/admin/system/setting/policy')) {
|
||||
return path.replace('/admin', '')
|
||||
}
|
||||
return '/system/user'
|
||||
})
|
||||
|
||||
|
|
@ -239,6 +252,6 @@ const toggleSystemSettings = () => {
|
|||
|
||||
onMounted(() => {
|
||||
resourceAuthExpanded.value = false
|
||||
systemSettingsExpanded.value = false
|
||||
systemSettingsExpanded.value = activeSystemMenu.value.startsWith('/system/email') || activeSystemMenu.value.startsWith('/system/setting/')
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -153,6 +153,53 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
policy: {
|
||||
title: 'Policy Management',
|
||||
configTab: 'Policy Config',
|
||||
recordTab: 'Application Records',
|
||||
createPolicy: 'Create Policy',
|
||||
editPolicy: 'Edit Policy',
|
||||
applyPolicy: 'Apply Policy',
|
||||
applySuccess: 'Policy applied successfully',
|
||||
selectedPolicy: 'Policy',
|
||||
policyName: 'Policy Name',
|
||||
policyNameRequired: 'Enter the policy name',
|
||||
policyContent: 'Policy Content',
|
||||
description: 'Description',
|
||||
effectiveTime: 'Effective Time',
|
||||
applicationCount: 'Applications',
|
||||
lastAppliedTime: 'Last Applied',
|
||||
accessTimeLimit: 'Access Time Limit',
|
||||
weekdays: 'Weekdays',
|
||||
accessPeriod: 'Access Period',
|
||||
startTime: 'Start Time',
|
||||
endTime: 'End Time',
|
||||
deviceTypes: 'Device Types',
|
||||
regions: 'Regions',
|
||||
searchPolicyPlaceholder: 'Search policy name',
|
||||
searchRecordPolicyPlaceholder: 'Search policy name',
|
||||
searchRecordTargetPlaceholder: 'Search target name or identifier',
|
||||
allStatus: 'All Statuses',
|
||||
deletePolicyTip: 'Delete policy "{name}"? It cannot be applied after deletion.',
|
||||
unlimited: 'Unlimited',
|
||||
targetType: 'Target Type',
|
||||
targetTypeRequired: 'Select a target type',
|
||||
targetName: 'Target',
|
||||
targetNameRequired: 'Enter the target name',
|
||||
targetIdentifier: 'Target Identifier',
|
||||
operator: 'Operator',
|
||||
applyTime: 'Apply Time',
|
||||
remark: 'Remark',
|
||||
device: 'Device',
|
||||
service: 'Service',
|
||||
monday: 'Mon',
|
||||
tuesday: 'Tue',
|
||||
wednesday: 'Wed',
|
||||
thursday: 'Thu',
|
||||
friday: 'Fri',
|
||||
saturday: 'Sat',
|
||||
sunday: 'Sun',
|
||||
},
|
||||
|
||||
resourceAuthorization: {
|
||||
title: 'Resource Authorization',
|
||||
|
|
|
|||
|
|
@ -155,6 +155,53 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
policy: {
|
||||
title: '策略管理',
|
||||
configTab: '策略配置',
|
||||
recordTab: '应用记录',
|
||||
createPolicy: '创建策略',
|
||||
editPolicy: '编辑策略',
|
||||
applyPolicy: '应用策略',
|
||||
applySuccess: '策略应用成功',
|
||||
selectedPolicy: '当前策略',
|
||||
policyName: '策略名称',
|
||||
policyNameRequired: '请输入策略名称',
|
||||
policyContent: '策略内容',
|
||||
description: '策略描述',
|
||||
effectiveTime: '生效时间',
|
||||
applicationCount: '应用次数',
|
||||
lastAppliedTime: '最近应用时间',
|
||||
accessTimeLimit: '访问时段限制',
|
||||
weekdays: '允许星期',
|
||||
accessPeriod: '访问时段',
|
||||
startTime: '开始时间',
|
||||
endTime: '结束时间',
|
||||
deviceTypes: '设备类型限制',
|
||||
regions: '区域限制',
|
||||
searchPolicyPlaceholder: '请输入策略名称',
|
||||
searchRecordPolicyPlaceholder: '请输入策略名称',
|
||||
searchRecordTargetPlaceholder: '请输入应用对象名称或标识',
|
||||
allStatus: '全部状态',
|
||||
deletePolicyTip: '确认删除策略“{name}”吗?删除后将无法继续应用该策略。',
|
||||
unlimited: '不限',
|
||||
targetType: '应用对象类型',
|
||||
targetTypeRequired: '请选择应用对象类型',
|
||||
targetName: '应用对象',
|
||||
targetNameRequired: '请输入应用对象名称',
|
||||
targetIdentifier: '对象标识',
|
||||
operator: '操作人员',
|
||||
applyTime: '应用时间',
|
||||
remark: '备注',
|
||||
device: '设备',
|
||||
service: '服务',
|
||||
monday: '周一',
|
||||
tuesday: '周二',
|
||||
wednesday: '周三',
|
||||
thursday: '周四',
|
||||
friday: '周五',
|
||||
saturday: '周六',
|
||||
sunday: '周日',
|
||||
},
|
||||
resourceAuthorization: {
|
||||
title: '资源授权',
|
||||
member: '成员',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export default {
|
||||
title: '系統設置',
|
||||
subTitle: '系統設置',
|
||||
test: '測試連線',
|
||||
testSuccess: '測試連線成功',
|
||||
testFailed: '測試連線失敗',
|
||||
|
|
@ -153,6 +154,53 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
policy: {
|
||||
title: '策略管理',
|
||||
configTab: '策略配置',
|
||||
recordTab: '應用記錄',
|
||||
createPolicy: '創建策略',
|
||||
editPolicy: '編輯策略',
|
||||
applyPolicy: '應用策略',
|
||||
applySuccess: '策略應用成功',
|
||||
selectedPolicy: '當前策略',
|
||||
policyName: '策略名稱',
|
||||
policyNameRequired: '請輸入策略名稱',
|
||||
policyContent: '策略內容',
|
||||
description: '策略描述',
|
||||
effectiveTime: '生效時間',
|
||||
applicationCount: '應用次數',
|
||||
lastAppliedTime: '最近應用時間',
|
||||
accessTimeLimit: '訪問時段限制',
|
||||
weekdays: '允許星期',
|
||||
accessPeriod: '訪問時段',
|
||||
startTime: '開始時間',
|
||||
endTime: '結束時間',
|
||||
deviceTypes: '設備類型限制',
|
||||
regions: '區域限制',
|
||||
searchPolicyPlaceholder: '請輸入策略名稱',
|
||||
searchRecordPolicyPlaceholder: '請輸入策略名稱',
|
||||
searchRecordTargetPlaceholder: '請輸入應用對象名稱或標識',
|
||||
allStatus: '全部狀態',
|
||||
deletePolicyTip: '確認刪除策略“{name}”嗎?刪除後將無法繼續應用該策略。',
|
||||
unlimited: '不限',
|
||||
targetType: '應用對象類型',
|
||||
targetTypeRequired: '請選擇應用對象類型',
|
||||
targetName: '應用對象',
|
||||
targetNameRequired: '請輸入應用對象名稱',
|
||||
targetIdentifier: '對象標識',
|
||||
operator: '操作人員',
|
||||
applyTime: '應用時間',
|
||||
remark: '備註',
|
||||
device: '設備',
|
||||
service: '服務',
|
||||
monday: '週一',
|
||||
tuesday: '週二',
|
||||
wednesday: '週三',
|
||||
thursday: '週四',
|
||||
friday: '週五',
|
||||
saturday: '週六',
|
||||
sunday: '週日',
|
||||
},
|
||||
|
||||
resourceAuthorization: {
|
||||
title: '資源授權',
|
||||
|
|
|
|||
|
|
@ -614,6 +614,21 @@ const systemRouter = {
|
|||
},
|
||||
component: () => import('@/views/system-setting/menu/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/system/setting/policy',
|
||||
name: 'SystemPolicyManagement',
|
||||
meta: {
|
||||
title: 'views.system.policy.title',
|
||||
activeMenu: '/system',
|
||||
parentPath: '/system',
|
||||
parentName: 'system',
|
||||
sameRoute: 'setting',
|
||||
permission: [
|
||||
new ComplexPermission([RoleConst.ADMIN], [PermissionConst.USER_READ], [], 'OR'),
|
||||
],
|
||||
},
|
||||
component: () => import('@/views/system-setting/policy/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/system/email',
|
||||
name: 'email',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,633 @@
|
|||
<template>
|
||||
<div class="policy-page p-16-24">
|
||||
<el-breadcrumb separator-icon="ArrowRight" class="mb-16">
|
||||
<el-breadcrumb-item>{{ t('views.system.subTitle') }}</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>
|
||||
<h5 class="ml-4 color-text-primary">{{ t('views.system.policy.title') }}</h5>
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
|
||||
<el-card shadow="never" class="policy-page__card">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane :label="t('views.system.policy.configTab')" name="policy">
|
||||
<div class="policy-page__toolbar mb-16">
|
||||
<div class="policy-page__filters">
|
||||
<el-input
|
||||
v-model="policyQuery.name"
|
||||
clearable
|
||||
:placeholder="t('views.system.policy.searchPolicyPlaceholder')"
|
||||
style="width: 240px"
|
||||
@keyup.enter="loadPolicies"
|
||||
/>
|
||||
<el-select v-model="policyQuery.enabled" clearable style="width: 180px">
|
||||
<el-option :label="t('views.system.policy.allStatus')" value="" />
|
||||
<el-option :label="t('common.status.enabled')" value="true" />
|
||||
<el-option :label="t('common.status.disabled')" value="false" />
|
||||
</el-select>
|
||||
<el-button @click="loadPolicies">{{ t('common.search') }}</el-button>
|
||||
<el-button @click="resetPolicyFilters">{{ t('common.clear') }}</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="openPolicyDialog()">
|
||||
{{ t('views.system.policy.createPolicy') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="policyList" border v-loading="policyLoading">
|
||||
<el-table-column prop="name" :label="t('views.system.policy.policyName')" min-width="180" />
|
||||
<el-table-column prop="content" :label="t('views.system.policy.policyContent')" min-width="360" show-overflow-tooltip />
|
||||
<el-table-column :label="t('views.system.policy.effectiveTime')" min-width="240">
|
||||
<template #default="{ row }">
|
||||
{{ formatEffectiveTime(row.effect_start_time, row.effect_end_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="application_count" :label="t('views.system.policy.applicationCount')" width="110" align="center" />
|
||||
<el-table-column :label="t('common.status.label')" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enabled ? 'success' : 'info'" effect="light">
|
||||
{{ row.enabled ? t('common.status.enabled') : t('common.status.disabled') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('views.system.policy.lastAppliedTime')" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.last_applied_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.operation')" fixed="right" width="220">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openApplyDialog(row)">
|
||||
{{ t('views.system.policy.applyPolicy') }}
|
||||
</el-button>
|
||||
<el-button link type="primary" @click="openPolicyDialog(row)">
|
||||
{{ t('common.edit') }}
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="removePolicy(row)">
|
||||
{{ t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="t('views.system.policy.recordTab')" name="record">
|
||||
<div class="policy-page__toolbar mb-16">
|
||||
<div class="policy-page__filters">
|
||||
<el-input
|
||||
v-model="recordQuery.policy_name"
|
||||
clearable
|
||||
:placeholder="t('views.system.policy.searchRecordPolicyPlaceholder')"
|
||||
style="width: 240px"
|
||||
@keyup.enter="loadRecords"
|
||||
/>
|
||||
<el-input
|
||||
v-model="recordQuery.target_name"
|
||||
clearable
|
||||
:placeholder="t('views.system.policy.searchRecordTargetPlaceholder')"
|
||||
style="width: 240px"
|
||||
@keyup.enter="loadRecords"
|
||||
/>
|
||||
<el-button @click="loadRecords">{{ t('common.search') }}</el-button>
|
||||
<el-button @click="resetRecordFilters">{{ t('common.clear') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="recordList" border v-loading="recordLoading">
|
||||
<el-table-column prop="policy_name" :label="t('views.system.policy.policyName')" min-width="180" />
|
||||
<el-table-column :label="t('views.system.policy.targetType')" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ targetTypeLabelMap[row.target_type] || row.target_type }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="target_name" :label="t('views.system.policy.targetName')" min-width="180" />
|
||||
<el-table-column prop="target_identifier" :label="t('views.system.policy.targetIdentifier')" min-width="160" />
|
||||
<el-table-column prop="operator_name" :label="t('views.system.policy.operator')" width="140" />
|
||||
<el-table-column :label="t('views.system.policy.applyTime')" min-width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.apply_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" :label="t('views.system.policy.remark')" min-width="220" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="policyDialogVisible"
|
||||
:title="editingPolicyId ? t('views.system.policy.editPolicy') : t('views.system.policy.createPolicy')"
|
||||
width="760px"
|
||||
class="policy-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="policyFormRef"
|
||||
:model="policyForm"
|
||||
:rules="policyRules"
|
||||
label-position="top"
|
||||
class="policy-dialog__form"
|
||||
>
|
||||
<div class="policy-dialog__grid">
|
||||
<div class="policy-dialog__col">
|
||||
<el-form-item :label="t('views.system.policy.policyName')" prop="name">
|
||||
<el-input v-model="policyForm.name" maxlength="128" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="policy-dialog__col">
|
||||
<el-form-item :label="t('common.status.label')" prop="enabled">
|
||||
<el-switch v-model="policyForm.enabled" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="policy-dialog__col policy-dialog__col--full">
|
||||
<el-form-item :label="t('views.system.policy.description')" prop="description">
|
||||
<el-input v-model="policyForm.description" type="textarea" :rows="3" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="policy-dialog__col">
|
||||
<el-form-item :label="t('views.system.policy.accessTimeLimit')" prop="access_time_enabled">
|
||||
<el-switch v-model="policyForm.access_time_enabled" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<template v-if="policyForm.access_time_enabled">
|
||||
<div class="policy-dialog__col">
|
||||
<el-form-item :label="t('views.system.policy.weekdays')" prop="allowed_weekdays">
|
||||
<el-checkbox-group v-model="policyForm.allowed_weekdays" class="policy-dialog__weekdays">
|
||||
<el-checkbox v-for="item in weekdayOptions" :key="item.value" :label="item.value">
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="policy-dialog__col policy-dialog__col--full">
|
||||
<el-form-item :label="t('views.system.policy.accessPeriod')">
|
||||
<div class="policy-dialog__range">
|
||||
<el-time-picker
|
||||
v-model="policyForm.access_start_time"
|
||||
value-format="HH:mm:ss"
|
||||
:placeholder="t('views.system.policy.startTime')"
|
||||
/>
|
||||
<span class="policy-page__separator">-</span>
|
||||
<el-time-picker
|
||||
v-model="policyForm.access_end_time"
|
||||
value-format="HH:mm:ss"
|
||||
:placeholder="t('views.system.policy.endTime')"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="policy-dialog__col policy-dialog__col--full">
|
||||
<el-form-item :label="t('views.system.policy.deviceTypes')" prop="allowed_device_types">
|
||||
<el-select
|
||||
v-model="policyForm.allowed_device_types"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
>
|
||||
<el-option v-for="item in deviceTypeOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="policy-dialog__col policy-dialog__col--full">
|
||||
<el-form-item :label="t('views.system.policy.regions')" prop="allowed_regions">
|
||||
<el-select
|
||||
v-model="policyForm.allowed_regions"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
>
|
||||
<el-option v-for="item in regionOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="policy-dialog__col policy-dialog__col--full">
|
||||
<el-form-item :label="t('views.system.policy.effectiveTime')">
|
||||
<div class="policy-dialog__range">
|
||||
<el-date-picker
|
||||
v-model="policyForm.effect_start_time"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
:placeholder="t('views.system.policy.startTime')"
|
||||
/>
|
||||
<span class="policy-page__separator">-</span>
|
||||
<el-date-picker
|
||||
v-model="policyForm.effect_end_time"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
:placeholder="t('views.system.policy.endTime')"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="policyDialogVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveLoading" @click="submitPolicy">
|
||||
{{ t('common.save') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="applyDialogVisible"
|
||||
:title="t('views.system.policy.applyPolicy')"
|
||||
width="560px"
|
||||
class="policy-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="applyFormRef"
|
||||
:model="applyForm"
|
||||
:rules="applyRules"
|
||||
label-position="top"
|
||||
class="policy-dialog__form"
|
||||
>
|
||||
<div class="policy-dialog__grid">
|
||||
<div class="policy-dialog__col policy-dialog__col--full">
|
||||
<el-form-item :label="t('views.system.policy.selectedPolicy')">
|
||||
<el-input :model-value="applyPolicyName" disabled />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="policy-dialog__col policy-dialog__col--full">
|
||||
<el-form-item :label="t('views.system.policy.targetType')" prop="target_type">
|
||||
<el-radio-group v-model="applyForm.target_type" class="policy-dialog__radio-group">
|
||||
<el-radio-button label="DEVICE">{{ t('views.system.policy.device') }}</el-radio-button>
|
||||
<el-radio-button label="SERVICE">{{ t('views.system.policy.service') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="policy-dialog__col policy-dialog__col--full">
|
||||
<el-form-item :label="t('views.system.policy.targetName')" prop="target_name">
|
||||
<el-input v-model="applyForm.target_name" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="policy-dialog__col policy-dialog__col--full">
|
||||
<el-form-item :label="t('views.system.policy.targetIdentifier')" prop="target_identifier">
|
||||
<el-input v-model="applyForm.target_identifier" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="policy-dialog__col policy-dialog__col--full">
|
||||
<el-form-item :label="t('views.system.policy.remark')" prop="remark">
|
||||
<el-input v-model="applyForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="applyDialogVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="applyLoading" @click="submitApply">
|
||||
{{ t('common.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import accessControlPolicyApi from '@/api/system-settings/access-control-policy'
|
||||
import { MsgConfirm, MsgSuccess } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const activeTab = ref('policy')
|
||||
const policyLoading = ref(false)
|
||||
const recordLoading = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const applyLoading = ref(false)
|
||||
const policyDialogVisible = ref(false)
|
||||
const applyDialogVisible = ref(false)
|
||||
const editingPolicyId = ref('')
|
||||
const applyingPolicyId = ref('')
|
||||
const applyPolicyName = ref('')
|
||||
|
||||
const policyFormRef = ref<FormInstance>()
|
||||
const applyFormRef = ref<FormInstance>()
|
||||
|
||||
const policyList = ref<any[]>([])
|
||||
const recordList = ref<any[]>([])
|
||||
|
||||
const policyQuery = reactive({
|
||||
name: '',
|
||||
enabled: '',
|
||||
})
|
||||
|
||||
const recordQuery = reactive({
|
||||
policy_name: '',
|
||||
target_name: '',
|
||||
})
|
||||
|
||||
const policyForm = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
enabled: true,
|
||||
access_time_enabled: false,
|
||||
access_start_time: '',
|
||||
access_end_time: '',
|
||||
allowed_weekdays: [] as number[],
|
||||
allowed_device_types: [] as string[],
|
||||
allowed_regions: [] as string[],
|
||||
effect_start_time: '',
|
||||
effect_end_time: '',
|
||||
})
|
||||
|
||||
const applyForm = reactive({
|
||||
target_type: 'DEVICE',
|
||||
target_name: '',
|
||||
target_identifier: '',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const weekdayOptions = [
|
||||
{ label: t('views.system.policy.monday'), value: 1 },
|
||||
{ label: t('views.system.policy.tuesday'), value: 2 },
|
||||
{ label: t('views.system.policy.wednesday'), value: 3 },
|
||||
{ label: t('views.system.policy.thursday'), value: 4 },
|
||||
{ label: t('views.system.policy.friday'), value: 5 },
|
||||
{ label: t('views.system.policy.saturday'), value: 6 },
|
||||
{ label: t('views.system.policy.sunday'), value: 7 },
|
||||
]
|
||||
|
||||
const deviceTypeOptions = ['摄像头', '门禁设备', '工控终端', '移动终端', '服务器']
|
||||
const regionOptions = ['北京', '上海', '广州', '深圳', '成都', '西安']
|
||||
|
||||
const targetTypeLabelMap: Record<string, string> = {
|
||||
DEVICE: t('views.system.policy.device'),
|
||||
SERVICE: t('views.system.policy.service'),
|
||||
}
|
||||
|
||||
const policyRules = computed<FormRules>(() => ({
|
||||
name: [{ required: true, message: t('views.system.policy.policyNameRequired'), trigger: 'blur' }],
|
||||
}))
|
||||
|
||||
const applyRules = computed<FormRules>(() => ({
|
||||
target_type: [{ required: true, message: t('views.system.policy.targetTypeRequired'), trigger: 'change' }],
|
||||
target_name: [{ required: true, message: t('views.system.policy.targetNameRequired'), trigger: 'blur' }],
|
||||
}))
|
||||
|
||||
function resetPolicyForm() {
|
||||
editingPolicyId.value = ''
|
||||
policyForm.name = ''
|
||||
policyForm.description = ''
|
||||
policyForm.enabled = true
|
||||
policyForm.access_time_enabled = false
|
||||
policyForm.access_start_time = ''
|
||||
policyForm.access_end_time = ''
|
||||
policyForm.allowed_weekdays = []
|
||||
policyForm.allowed_device_types = []
|
||||
policyForm.allowed_regions = []
|
||||
policyForm.effect_start_time = ''
|
||||
policyForm.effect_end_time = ''
|
||||
}
|
||||
|
||||
function resetApplyForm() {
|
||||
applyingPolicyId.value = ''
|
||||
applyPolicyName.value = ''
|
||||
applyForm.target_type = 'DEVICE'
|
||||
applyForm.target_name = ''
|
||||
applyForm.target_identifier = ''
|
||||
applyForm.remark = ''
|
||||
}
|
||||
|
||||
function normalizeDateTimeValue(value?: string | null) {
|
||||
if (!value) return ''
|
||||
return value.includes('.') ? value.split('.')[0] : value.replace(/Z$/, '')
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string | null) {
|
||||
if (!value) return '-'
|
||||
return normalizeDateTimeValue(value).replace('T', ' ')
|
||||
}
|
||||
|
||||
function formatEffectiveTime(start?: string | null, end?: string | null) {
|
||||
if (!start && !end) return t('views.system.policy.unlimited')
|
||||
return `${formatDateTime(start)} ~ ${formatDateTime(end)}`
|
||||
}
|
||||
|
||||
async function loadPolicies() {
|
||||
const response = await accessControlPolicyApi.getPolicyList(
|
||||
{ ...policyQuery, enabled: policyQuery.enabled || undefined },
|
||||
policyLoading,
|
||||
)
|
||||
policyList.value = response.data || []
|
||||
}
|
||||
|
||||
async function loadRecords() {
|
||||
const response = await accessControlPolicyApi.getApplicationRecordList(recordQuery, recordLoading)
|
||||
recordList.value = response.data || []
|
||||
}
|
||||
|
||||
function resetPolicyFilters() {
|
||||
policyQuery.name = ''
|
||||
policyQuery.enabled = ''
|
||||
loadPolicies()
|
||||
}
|
||||
|
||||
function resetRecordFilters() {
|
||||
recordQuery.policy_name = ''
|
||||
recordQuery.target_name = ''
|
||||
loadRecords()
|
||||
}
|
||||
|
||||
function openPolicyDialog(row?: any) {
|
||||
resetPolicyForm()
|
||||
policyDialogVisible.value = true
|
||||
if (!row) return
|
||||
editingPolicyId.value = row.id
|
||||
policyForm.name = row.name || ''
|
||||
policyForm.description = row.description || ''
|
||||
policyForm.enabled = row.enabled ?? true
|
||||
policyForm.access_time_enabled = row.access_time_enabled ?? false
|
||||
policyForm.access_start_time = row.access_start_time || ''
|
||||
policyForm.access_end_time = row.access_end_time || ''
|
||||
policyForm.allowed_weekdays = row.allowed_weekdays || []
|
||||
policyForm.allowed_device_types = row.allowed_device_types || []
|
||||
policyForm.allowed_regions = row.allowed_regions || []
|
||||
policyForm.effect_start_time = normalizeDateTimeValue(row.effect_start_time)
|
||||
policyForm.effect_end_time = normalizeDateTimeValue(row.effect_end_time)
|
||||
}
|
||||
|
||||
async function submitPolicy() {
|
||||
await policyFormRef.value?.validate()
|
||||
const payload = {
|
||||
...policyForm,
|
||||
access_start_time: policyForm.access_time_enabled ? policyForm.access_start_time || null : null,
|
||||
access_end_time: policyForm.access_time_enabled ? policyForm.access_end_time || null : null,
|
||||
allowed_weekdays: policyForm.access_time_enabled ? policyForm.allowed_weekdays : [],
|
||||
effect_start_time: policyForm.effect_start_time || null,
|
||||
effect_end_time: policyForm.effect_end_time || null,
|
||||
}
|
||||
if (editingPolicyId.value) {
|
||||
await accessControlPolicyApi.updatePolicy(editingPolicyId.value, payload, saveLoading)
|
||||
} else {
|
||||
await accessControlPolicyApi.createPolicy(payload, saveLoading)
|
||||
}
|
||||
MsgSuccess(t('common.saveSuccess'))
|
||||
policyDialogVisible.value = false
|
||||
loadPolicies()
|
||||
}
|
||||
|
||||
async function removePolicy(row: any) {
|
||||
try {
|
||||
await MsgConfirm(t('common.delete'), t('views.system.policy.deletePolicyTip', { name: row.name }))
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
await accessControlPolicyApi.deletePolicy(row.id)
|
||||
MsgSuccess(t('common.deleteSuccess'))
|
||||
loadPolicies()
|
||||
loadRecords()
|
||||
}
|
||||
|
||||
function openApplyDialog(row: any) {
|
||||
resetApplyForm()
|
||||
applyingPolicyId.value = row.id
|
||||
applyPolicyName.value = row.name
|
||||
applyDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitApply() {
|
||||
await applyFormRef.value?.validate()
|
||||
await accessControlPolicyApi.applyPolicy(applyingPolicyId.value, applyForm, applyLoading)
|
||||
MsgSuccess(t('views.system.policy.applySuccess'))
|
||||
applyDialogVisible.value = false
|
||||
activeTab.value = 'record'
|
||||
loadPolicies()
|
||||
loadRecords()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadPolicies(), loadRecords()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.policy-page {
|
||||
&__card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
margin: 0 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.policy-dialog) {
|
||||
.el-dialog__body {
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.policy-dialog {
|
||||
&__form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
padding-bottom: 8px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-textarea),
|
||||
:deep(.el-select),
|
||||
:deep(.el-date-editor.el-input),
|
||||
:deep(.el-date-editor.el-input__wrapper),
|
||||
:deep(.el-date-editor.el-range-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0 16px;
|
||||
}
|
||||
|
||||
&__col {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__col--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
&__range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-date-editor),
|
||||
:deep(.el-input),
|
||||
:deep(.el-select) {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__weekdays {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
}
|
||||
|
||||
&__radio-group {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.policy-dialog {
|
||||
&__grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
&__range {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
.policy-page__separator {
|
||||
margin: 8px 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue