#!/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")