312 lines
11 KiB
Python
312 lines
11 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Menu Schema - 菜单数据验证模式
|
|
字段名使用驼峰命名,与前端保持一致
|
|
"""
|
|
from datetime import datetime
|
|
from typing import Optional, List, Any, Dict
|
|
|
|
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
|
|
|
from app.base_schema import CSTDatetime
|
|
|
|
|
|
class MenuBase(BaseModel):
|
|
"""菜单基础Schema"""
|
|
# 应用关联
|
|
application_id: Optional[str] = Field(None, description="所属应用ID")
|
|
is_system: bool = Field(default=False, description="是否为系统菜单")
|
|
|
|
name: str = Field(..., description="菜单名称(路由名称)")
|
|
title: Optional[str] = Field(None, description="菜单标题(显示名称)")
|
|
authCode: Optional[str] = Field(None, description="后端权限标识")
|
|
path: str = Field(..., description="路由路径")
|
|
type: str = Field(default="catalog", description="菜单类型")
|
|
|
|
# 路由配置
|
|
component: Optional[str] = Field(None, description="组件路径")
|
|
redirect: Optional[str] = Field(None, description="重定向路径")
|
|
activePath: Optional[str] = Field(None, description="激活路径")
|
|
query: Optional[Dict[str, Any]] = Field(None, description="额外路由参数")
|
|
noBasicLayout: bool = Field(default=False, description="无需基础布局")
|
|
|
|
# 菜单展示
|
|
icon: Optional[str] = Field(None, description="菜单图标")
|
|
activeIcon: Optional[str] = Field(None, description="激活图标")
|
|
order: int = Field(default=0, description="菜单排序")
|
|
hideInMenu: bool = Field(default=False, description="在菜单中隐藏")
|
|
hideChildrenInMenu: bool = Field(default=False, description="在菜单中隐藏下级")
|
|
hideInBreadcrumb: bool = Field(default=False, description="在面包屑中隐藏")
|
|
|
|
# 标签页配置
|
|
hideInTab: bool = Field(default=False, description="在标签栏中隐藏")
|
|
affixTab: bool = Field(default=False, description="固定在标签栏")
|
|
affixTabOrder: Optional[int] = Field(None, description="标签栏固定顺序")
|
|
keepAlive: bool = Field(default=False, description="缓存页面")
|
|
maxNumOfOpenTab: Optional[int] = Field(None, description="最大打开标签数")
|
|
fullPathKey: bool = Field(default=True, description="路由完整路径作为key")
|
|
|
|
# 外部链接配置
|
|
link: Optional[str] = Field(None, description="外链URL")
|
|
iframeSrc: Optional[str] = Field(None, description="内嵌iframe URL")
|
|
openInNewWindow: bool = Field(default=False, description="在新窗口打开")
|
|
|
|
# 徽标配置
|
|
badge: Optional[str] = Field(None, description="徽标内容")
|
|
badgeType: Optional[str] = Field(None, description="徽标类型")
|
|
badgeVariants: Optional[str] = Field(None, description="徽标颜色")
|
|
|
|
@field_validator('name')
|
|
@classmethod
|
|
def validate_name(cls, v):
|
|
"""验证菜单名称格式"""
|
|
if not v:
|
|
raise ValueError('菜单名称不能为空')
|
|
if not v[0].isalpha():
|
|
raise ValueError('菜单名称必须以字母开头')
|
|
if not all(c.isalnum() or c in '_-' for c in v):
|
|
raise ValueError('菜单名称只能包含字母、数字、下划线和横线')
|
|
return v
|
|
|
|
@field_validator('path')
|
|
@classmethod
|
|
def validate_path(cls, v):
|
|
"""验证路由路径"""
|
|
if not v:
|
|
raise ValueError('路由路径不能为空')
|
|
return v
|
|
|
|
@field_validator('type')
|
|
@classmethod
|
|
def validate_type(cls, v):
|
|
"""验证菜单类型"""
|
|
valid_types = [
|
|
'catalog', 'menu', 'external', 'link', 'embedded',
|
|
'online_form', 'online_page', 'online_report', 'agent',
|
|
]
|
|
if v not in valid_types:
|
|
raise ValueError(f'菜单类型必须为 {", ".join(valid_types)} 之一')
|
|
return v
|
|
|
|
|
|
class MenuCreate(MenuBase):
|
|
"""菜单创建Schema"""
|
|
parent_id: Optional[str] = Field(None, description="父菜单ID")
|
|
|
|
|
|
class MenuUpdate(BaseModel):
|
|
"""菜单更新Schema - 所有字段可选"""
|
|
# 应用关联
|
|
application_id: Optional[str] = Field(None, description="所属应用ID")
|
|
is_system: Optional[bool] = Field(None, description="是否为系统菜单")
|
|
|
|
name: Optional[str] = Field(None, description="菜单名称")
|
|
title: Optional[str] = Field(None, description="菜单标题")
|
|
authCode: Optional[str] = Field(None, description="后端权限标识")
|
|
path: Optional[str] = Field(None, description="路由路径")
|
|
type: Optional[str] = Field(None, description="菜单类型")
|
|
parent_id: Optional[str] = Field(None, description="父菜单ID")
|
|
|
|
# 路由配置
|
|
component: Optional[str] = Field(None, description="组件路径")
|
|
redirect: Optional[str] = Field(None, description="重定向路径")
|
|
activePath: Optional[str] = Field(None, description="激活路径")
|
|
query: Optional[Dict[str, Any]] = Field(None, description="额外路由参数")
|
|
noBasicLayout: Optional[bool] = Field(None, description="无需基础布局")
|
|
|
|
# 菜单展示
|
|
icon: Optional[str] = Field(None, description="菜单图标")
|
|
activeIcon: Optional[str] = Field(None, description="激活图标")
|
|
order: Optional[int] = Field(None, description="菜单排序")
|
|
hideInMenu: Optional[bool] = Field(None, description="在菜单中隐藏")
|
|
hideChildrenInMenu: Optional[bool] = Field(None, description="在菜单中隐藏下级")
|
|
hideInBreadcrumb: Optional[bool] = Field(None, description="在面包屑中隐藏")
|
|
|
|
# 标签页配置
|
|
hideInTab: Optional[bool] = Field(None, description="在标签栏中隐藏")
|
|
affixTab: Optional[bool] = Field(None, description="固定在标签栏")
|
|
affixTabOrder: Optional[int] = Field(None, description="标签栏固定顺序")
|
|
keepAlive: Optional[bool] = Field(None, description="缓存页面")
|
|
maxNumOfOpenTab: Optional[int] = Field(None, description="最大打开标签数")
|
|
fullPathKey: Optional[bool] = Field(None, description="路由完整路径作为key")
|
|
|
|
# 外部链接配置
|
|
link: Optional[str] = Field(None, description="外链URL")
|
|
iframeSrc: Optional[str] = Field(None, description="内嵌iframe URL")
|
|
openInNewWindow: Optional[bool] = Field(None, description="在新窗口打开")
|
|
|
|
# 徽标配置
|
|
badge: Optional[str] = Field(None, description="徽标内容")
|
|
badgeType: Optional[str] = Field(None, description="徽标类型")
|
|
badgeVariants: Optional[str] = Field(None, description="徽标颜色")
|
|
|
|
@field_validator('name')
|
|
@classmethod
|
|
def validate_name(cls, v):
|
|
"""验证菜单名称格式"""
|
|
if v is not None:
|
|
if not v:
|
|
raise ValueError('菜单名称不能为空')
|
|
if not v[0].isalpha():
|
|
raise ValueError('菜单名称必须以字母开头')
|
|
if not all(c.isalnum() or c in '_-' for c in v):
|
|
raise ValueError('菜单名称只能包含字母、数字、下划线和横线')
|
|
return v
|
|
|
|
@field_validator('type')
|
|
@classmethod
|
|
def validate_type(cls, v):
|
|
"""验证菜单类型"""
|
|
if v is not None:
|
|
valid_types = [
|
|
'catalog', 'menu', 'external', 'link', 'embedded',
|
|
'online_form', 'online_page', 'online_report', 'agent',
|
|
]
|
|
if v not in valid_types:
|
|
raise ValueError(f'菜单类型必须为 {", ".join(valid_types)} 之一')
|
|
return v
|
|
|
|
|
|
class MenuResponse(BaseModel):
|
|
"""菜单响应Schema"""
|
|
id: str
|
|
parent_id: Optional[str] = None
|
|
application_id: Optional[str] = None
|
|
is_system: bool = False
|
|
name: str
|
|
title: Optional[str] = None
|
|
authCode: Optional[str] = None
|
|
path: str
|
|
type: str
|
|
|
|
# 路由配置
|
|
component: Optional[str] = None
|
|
redirect: Optional[str] = None
|
|
activePath: Optional[str] = None
|
|
query: Optional[Dict[str, Any]] = None
|
|
noBasicLayout: bool = False
|
|
|
|
# 菜单展示
|
|
icon: Optional[str] = None
|
|
activeIcon: Optional[str] = None
|
|
order: int = 0
|
|
hideInMenu: bool = False
|
|
hideChildrenInMenu: bool = False
|
|
hideInBreadcrumb: bool = False
|
|
|
|
# 标签页配置
|
|
hideInTab: bool = False
|
|
affixTab: bool = False
|
|
affixTabOrder: Optional[int] = None
|
|
keepAlive: bool = False
|
|
maxNumOfOpenTab: Optional[int] = None
|
|
fullPathKey: bool = True
|
|
|
|
# 外部链接配置
|
|
link: Optional[str] = None
|
|
iframeSrc: Optional[str] = None
|
|
openInNewWindow: bool = False
|
|
|
|
# 徽标配置
|
|
badge: Optional[str] = None
|
|
badgeType: Optional[str] = None
|
|
badgeVariants: Optional[str] = None
|
|
|
|
# 公共字段
|
|
sort: int = 0
|
|
is_deleted: bool = False
|
|
sys_create_datetime: Optional[CSTDatetime] = None
|
|
sys_update_datetime: Optional[CSTDatetime] = None
|
|
|
|
# 计算字段
|
|
level: Optional[int] = None
|
|
childCount: Optional[int] = None
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class MenuTreeNode(BaseModel):
|
|
"""菜单树节点"""
|
|
id: str
|
|
parent_id: Optional[str] = None
|
|
name: str
|
|
title: Optional[str] = None
|
|
path: str
|
|
type: str
|
|
icon: Optional[str] = None
|
|
order: int = 0
|
|
level: int = 0
|
|
childCount: int = 0
|
|
children: List['MenuTreeNode'] = []
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class MenuSimple(BaseModel):
|
|
"""菜单简单输出(用于选择器)"""
|
|
id: str
|
|
name: str
|
|
title: Optional[str] = None
|
|
path: str
|
|
type: str
|
|
parent_id: Optional[str] = None
|
|
level: int = 0
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class MenuRouteNode(BaseModel):
|
|
"""菜单路由输出(前端路由格式)"""
|
|
name: str
|
|
path: str
|
|
component: Optional[str] = None
|
|
redirect: Optional[str] = None
|
|
meta: Dict[str, Any] = {}
|
|
children: List['MenuRouteNode'] = []
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class MenuBatchDeleteRequest(BaseModel):
|
|
"""批量删除菜单请求"""
|
|
ids: List[str] = Field(..., description="要删除的菜单ID列表")
|
|
|
|
|
|
class MenuBatchDeleteResponse(BaseModel):
|
|
"""批量删除菜单响应"""
|
|
count: int = Field(..., description="删除的记录数")
|
|
failedIds: List[str] = Field(default=[], description="删除失败的ID列表")
|
|
|
|
|
|
class MenuPathResponse(BaseModel):
|
|
"""菜单路径响应"""
|
|
menuId: str
|
|
menuName: str
|
|
path: List[MenuSimple] = Field(..., description="从根到当前菜单的路径")
|
|
|
|
|
|
class MenuStatsResponse(BaseModel):
|
|
"""菜单统计响应"""
|
|
totalCount: int
|
|
typeStats: Dict[str, int]
|
|
maxLevel: int
|
|
|
|
|
|
class MenuMoveRequest(BaseModel):
|
|
"""移动菜单请求"""
|
|
menuId: str = Field(..., description="要移动的菜单ID")
|
|
newParentId: Optional[str] = Field(None, description="新父菜单ID,为空表示移动到根节点")
|
|
|
|
|
|
class MenuCheckNameRequest(BaseModel):
|
|
"""检查菜单名称请求"""
|
|
name: str = Field(..., description="菜单名称")
|
|
exclude_id: Optional[str] = Field(None, description="排除的菜单ID")
|
|
|
|
|
|
class MenuCheckPathRequest(BaseModel):
|
|
"""检查路由路径请求"""
|
|
path: str = Field(..., description="路由路径")
|
|
exclude_id: Optional[str] = Field(None, description="排除的菜单ID")
|