提交 50f65be1 authored 作者: 陈泽健's avatar 陈泽健

feat(menu): 菜单分类架构与步骤编辑功能

- 后端: Module/TestCase/Execution 新增 module_type/case_type 字段,database _ensure_columns 自动补齐旧库列;modules/cases/executions/reports 路由与 service 层支持按类型筛选,创建用例/执行时透传/填充 case_type

- 前端: router 改 /:type? 可选参数,App.vue 模块/用例/执行/报告改父子菜单;Modules/Cases/Execution/Reports 读取路由 type 筛选;新建 CaseStepEditor UI自动化步骤编辑器(12动作+8断言动态表单,增删/上下移/自动重排)

- 平台更名为"测试管理平台"(App.vue / index.html)

- 修复构建: CreateCaseRequest 类型对齐 snake_case、清理未使用导入、补装 terser
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 12b134b7
此差异已折叠。
......@@ -89,7 +89,8 @@ async def init_db() -> None:
"""
初始化数据库
创建所有表结构。在应用启动时调用。
创建所有表结构,并对已存在的表补齐新增字段(SQLite 兼容)。
在应用启动时调用。
Example:
>>> import asyncio
......@@ -97,9 +98,48 @@ async def init_db() -> None:
"""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# SQLite 对已存在的表不会自动加列,需手动补齐
await _ensure_columns(conn)
logger.info("数据库表结构初始化完成")
async def _ensure_columns(conn) -> None:
"""
补齐已存在表的新增字段(SQLite ALTER TABLE ADD COLUMN)
SQLite 的 create_all 只创建不存在的表,已存在的表不会加新列。
本方法检查并补充新增字段,保证旧库平滑升级。
Args:
conn: 异步数据库连接(AsyncConnection),内部通过 run_sync 执行同步检查
"""
from sqlalchemy import text, inspect
# 需要补齐的字段:[(表名, 列名, 列定义SQL)]
columns_to_add = [
("modules", "module_type", "VARCHAR(20) DEFAULT 'standard'"),
("test_cases", "case_type", "VARCHAR(20) DEFAULT 'ui'"),
("executions", "case_type", "VARCHAR(20) DEFAULT 'ui'"),
]
def _do_ensure(sync_conn) -> None:
"""同步执行:检查并补齐字段(在 run_sync 内调用,可使用 inspect)"""
inspector = inspect(sync_conn)
for table, column, col_def in columns_to_add:
try:
cols = [c["name"] for c in inspector.get_columns(table)]
if column not in cols:
sync_conn.execute(
text(f"ALTER TABLE {table} ADD COLUMN {column} {col_def}")
)
logger.info(f"补齐字段: {table}.{column}")
except Exception as e:
logger.warning(f"补齐字段失败 {table}.{column}: {e}")
# AsyncConnection 不支持直接 inspect,必须通过 run_sync 在同步上下文中执行
await conn.run_sync(_do_ensure)
async def drop_db() -> None:
"""
删除所有数据库表
......
......@@ -82,6 +82,11 @@ class Execution(Base):
)
environment: Mapped[str] = mapped_column(String(50), default="default", comment="执行环境")
config: Mapped[dict] = mapped_column(JSON, default=dict, comment="执行配置")
case_type: Mapped[str] = mapped_column(
String(20),
default="ui",
comment="用例类型: ui/api/security/deploy(用于按类型筛选执行记录)"
)
created_at: Mapped[datetime] = mapped_column(
DateTime,
default=datetime.utcnow,
......@@ -122,6 +127,7 @@ class Execution(Base):
"end_time": self.end_time.isoformat() if self.end_time else None,
"environment": self.environment,
"config": self.config or {},
"case_type": self.case_type,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
......
......@@ -46,6 +46,11 @@ class Module(Base):
description: Mapped[str] = mapped_column(Text, default="", comment="模块描述")
icon: Mapped[str] = mapped_column(String(50), default="folder", comment="模块图标")
order: Mapped[int] = mapped_column(Integer, default=0, comment="排序序号")
module_type: Mapped[str] = mapped_column(
String(20),
default="standard",
comment="模块类型: standard(标准模块)/custom(项目定制模块)"
)
created_at: Mapped[datetime] = mapped_column(
DateTime,
default=datetime.utcnow,
......@@ -82,6 +87,7 @@ class Module(Base):
"description": self.description,
"icon": self.icon,
"order": self.order,
"module_type": self.module_type,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
......@@ -72,6 +72,11 @@ class TestCase(Base):
tags: Mapped[list] = mapped_column(JSON, default=list, comment="标签列表")
steps: Mapped[list] = mapped_column(JSON, default=list, comment="测试步骤")
config: Mapped[dict] = mapped_column(JSON, default=dict, comment="执行配置")
case_type: Mapped[str] = mapped_column(
String(20),
default="ui",
comment="用例类型: ui/api/security/deploy/performance/functional"
)
# 新增字段:依赖管理和参数化支持
depends_on: Mapped[list] = mapped_column(JSON, default=list, comment="依赖用例ID列表")
......@@ -135,6 +140,7 @@ class TestCase(Base):
"tags": self.tags or [],
"steps": self.steps or [],
"config": self.config or {},
"case_type": self.case_type,
"depends_on": self.depends_on or [],
"parameters": self.parameters or {},
"condition": self.condition or {},
......
......@@ -30,6 +30,32 @@ logger = logging.getLogger(__name__)
router = APIRouter()
def _case_to_response(c) -> TestCaseResponse:
"""
统一构造用例响应对象(包含 case_type 等所有字段)
Args:
c: TestCase ORM 对象
Returns:
TestCaseResponse: 用例响应
"""
return TestCaseResponse(
id=c.id,
module_id=c.module_id,
name=c.name,
description=c.description,
status=c.status,
priority=c.priority,
tags=c.tags or [],
steps=c.steps or [],
config=c.config or {},
case_type=getattr(c, "case_type", None) or "ui",
created_at=c.created_at,
updated_at=c.updated_at,
)
def get_case_service(db: AsyncSession = Depends(get_db)) -> CaseService:
"""
获取用例服务实例(依赖注入)
......@@ -49,6 +75,7 @@ async def list_cases(
keyword: Optional[str] = Query(None, description="关键词搜索"),
status: Optional[str] = Query(None, description="状态筛选"),
priority: Optional[str] = Query(None, description="优先级筛选"),
case_type: Optional[str] = Query(None, description="用例类型筛选: ui/api/security/deploy"),
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=500, description="返回记录数上限"),
service: CaseService = Depends(get_case_service),
......@@ -56,13 +83,14 @@ async def list_cases(
"""
获取用例列表
支持分页查询和按模块/关键词/状态/优先级筛选。
支持分页查询和按模块/关键词/状态/优先级/类型筛选。
Args:
module_id (Optional[str]): 模块ID筛选
keyword (Optional[str]): 模糊搜索关键词
status (Optional[str]): 状态过滤
priority (Optional[str]): 优先级过滤
case_type (Optional[str]): 用例类型过滤 ui/api/security/deploy
skip (int): 跳过的记录数
limit (int): 返回记录数上限
service (CaseService): 用例服务实例
......@@ -76,26 +104,12 @@ async def list_cases(
keyword=keyword,
status=status,
priority=priority,
case_type=case_type,
skip=skip,
limit=limit,
)
items = [
TestCaseResponse(
id=c.id,
module_id=c.module_id,
name=c.name,
description=c.description,
status=c.status,
priority=c.priority,
tags=c.tags or [],
steps=c.steps or [],
config=c.config or {},
created_at=c.created_at,
updated_at=c.updated_at,
)
for c in cases
]
items = [_case_to_response(c) for c in cases]
return TestCaseListResponse(total=total, items=items)
......@@ -127,19 +141,7 @@ async def get_case(
if not case:
raise HTTPException(status_code=404, detail=f"用例不存在: {case_id}")
return TestCaseResponse(
id=case.id,
module_id=case.module_id,
name=case.name,
description=case.description,
status=case.status,
priority=case.priority,
tags=case.tags or [],
steps=case.steps or [],
config=case.config or {},
created_at=case.created_at,
updated_at=case.updated_at,
)
return _case_to_response(case)
except HTTPException:
raise
......@@ -168,19 +170,7 @@ async def create_case(
"""
try:
case = await service.create(data)
return TestCaseResponse(
id=case.id,
module_id=case.module_id,
name=case.name,
description=case.description,
status=case.status,
priority=case.priority,
tags=case.tags or [],
steps=case.steps or [],
config=case.config or {},
created_at=case.created_at,
updated_at=case.updated_at,
)
return _case_to_response(case)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
......@@ -216,19 +206,7 @@ async def update_case(
if not case:
raise HTTPException(status_code=404, detail=f"用例不存在: {case_id}")
return TestCaseResponse(
id=case.id,
module_id=case.module_id,
name=case.name,
description=case.description,
status=case.status,
priority=case.priority,
tags=case.tags or [],
steps=case.steps or [],
config=case.config or {},
created_at=case.created_at,
updated_at=case.updated_at,
)
return _case_to_response(case)
except HTTPException:
raise
......@@ -289,19 +267,7 @@ async def copy_case(
if not new_case:
raise HTTPException(status_code=404, detail=f"用例不存在: {case_id}")
return TestCaseResponse(
id=new_case.id,
module_id=new_case.module_id,
name=new_case.name,
description=new_case.description,
status=new_case.status,
priority=new_case.priority,
tags=new_case.tags or [],
steps=new_case.steps or [],
config=new_case.config or {},
created_at=new_case.created_at,
updated_at=new_case.updated_at,
)
return _case_to_response(new_case)
except HTTPException:
raise
......
......@@ -85,6 +85,7 @@ async def list_executions(
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(50, ge=1, le=200, description="返回记录数上限"),
status: Optional[str] = Query(None, description="状态筛选"),
case_type: Optional[str] = Query(None, description="用例类型筛选: ui/api/security/deploy/performance/functional"),
service: ExecutionService = Depends(get_execution_service),
):
"""
......@@ -94,6 +95,7 @@ async def list_executions(
skip (int): 跳过记录数
limit (int): 返回记录数上限
status (Optional[str]): 状态筛选
case_type (Optional[str]): 用例类型筛选
service (ExecutionService): 执行服务实例
Returns:
......@@ -101,7 +103,7 @@ async def list_executions(
"""
try:
executions, total = await service.list_executions(
skip=skip, limit=limit, status=status
skip=skip, limit=limit, status=status, case_type=case_type
)
return {
"total": total,
......
......@@ -47,23 +47,25 @@ def get_module_service(db: AsyncSession = Depends(get_db)) -> ModuleService:
async def list_modules(
skip: int = Query(0, ge=0, description="跳过的记录数"),
limit: int = Query(100, ge=1, le=500, description="返回记录数上限"),
module_type: Optional[str] = Query(None, description="模块类型筛选: standard/custom"),
service: ModuleService = Depends(get_module_service)
):
"""
获取模块列表
支持分页查询,按排序号升序返回。
支持分页查询,按排序号升序返回,可按模块类型筛选
Args:
skip (int): 跳过的记录数
limit (int): 返回记录数上限
module_type (Optional[str]): 模块类型筛选 standard/custom
service (ModuleService): 模块服务实例
Returns:
ModuleListResponse: 模块列表响应
"""
try:
modules, total = await service.list(skip=skip, limit=limit)
modules, total = await service.list(skip=skip, limit=limit, module_type=module_type)
# 转换为响应格式
items = [
......@@ -73,6 +75,7 @@ async def list_modules(
description=m.description,
icon=m.icon,
order=m.order,
module_type=getattr(m, 'module_type', 'standard') or 'standard',
created_at=m.created_at,
updated_at=m.updated_at,
case_count=getattr(m, 'case_count', 0)
......@@ -116,6 +119,7 @@ async def get_module(
description=module.description,
icon=module.icon,
order=module.order,
module_type=getattr(module, 'module_type', 'standard') or 'standard',
created_at=module.created_at,
updated_at=module.updated_at,
case_count=getattr(module, 'case_count', 0)
......@@ -154,6 +158,7 @@ async def create_module(
description=module.description,
icon=module.icon,
order=module.order,
module_type=getattr(module, 'module_type', 'standard') or 'standard',
created_at=module.created_at,
updated_at=module.updated_at,
case_count=0
......@@ -199,6 +204,7 @@ async def update_module(
description=module.description,
icon=module.icon,
order=module.order,
module_type=getattr(module, 'module_type', 'standard') or 'standard',
created_at=module.created_at,
updated_at=module.updated_at,
case_count=getattr(module, 'case_count', 0)
......
......@@ -130,6 +130,7 @@ async def download_report(
async def list_reports(
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(50, ge=1, le=200, description="返回记录数上限"),
case_type: Optional[str] = Query(None, description="用例类型筛选: ui/api/security/deploy/performance/functional"),
db: AsyncSession = Depends(get_db),
):
"""
......@@ -138,6 +139,7 @@ async def list_reports(
Args:
skip (int): 跳过记录数
limit (int): 返回记录数上限
case_type (Optional[str]): 用例类型筛选(按关联执行的 case_type)
db (AsyncSession): 数据库会话
Returns:
......@@ -145,9 +147,14 @@ async def list_reports(
"""
from sqlalchemy import func
# 构建筛选条件:已完成 + 可选按用例类型
conditions = [Execution.status == "completed"]
if case_type:
conditions.append(Execution.case_type == case_type)
query = (
select(Execution)
.where(Execution.status == "completed")
.where(*conditions)
.order_by(desc(Execution.created_at))
.offset(skip)
.limit(limit)
......@@ -155,9 +162,7 @@ async def list_reports(
result = await db.execute(query)
items = result.scalars().all()
count_query = select(func.count(Execution.id)).where(
Execution.status == "completed"
)
count_query = select(func.count(Execution.id)).where(*conditions)
result = await db.execute(count_query)
total = result.scalar() or 0
......
......@@ -30,6 +30,7 @@ class ModuleBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="模块名称")
description: str = Field(default="", max_length=500, description="模块描述")
icon: str = Field(default="folder", max_length=50, description="模块图标")
module_type: str = Field(default="standard", description="模块类型: standard/custom")
class ModuleCreate(ModuleBase):
......@@ -62,6 +63,7 @@ class ModuleUpdate(BaseModel):
description: Optional[str] = Field(None, max_length=500, description="模块描述")
icon: Optional[str] = Field(None, max_length=50, description="模块图标")
order: Optional[int] = Field(None, ge=0, description="排序序号")
module_type: Optional[str] = Field(None, description="模块类型: standard/custom")
class ModuleResponse(ModuleBase):
......
......@@ -67,6 +67,7 @@ class TestCaseBase(BaseModel):
default_factory=lambda: {"timeout": 30000, "retry": 0, "screenshot": True},
description="执行配置"
)
case_type: str = Field(default="ui", description="用例类型: ui/api/security/deploy")
class TestCaseCreate(TestCaseBase):
......@@ -107,6 +108,7 @@ class TestCaseUpdate(BaseModel):
tags: Optional[List[str]] = Field(None, description="标签列表")
steps: Optional[List[StepDefinition]] = Field(None, description="测试步骤")
config: Optional[Dict[str, Any]] = Field(None, description="执行配置")
case_type: Optional[str] = Field(None, description="用例类型: ui/api/security/deploy")
class TestCaseResponse(TestCaseBase):
......@@ -178,4 +180,5 @@ class TestCaseImport(BaseModel):
config: Dict[str, Any] = Field(
default_factory=lambda: {"timeout": 30000, "retry": 0, "screenshot": True},
description="执行配置"
)
\ No newline at end of file
)
case_type: str = Field(default="ui", description="用例类型: ui/api/security/deploy")
\ No newline at end of file
......@@ -68,19 +68,21 @@ class CaseService:
keyword: Optional[str] = None,
status: Optional[str] = None,
priority: Optional[str] = None,
case_type: Optional[str] = None,
skip: int = 0,
limit: int = 100,
) -> Tuple[List[TestCase], int]:
"""
获取用例列表(分页、筛选)
支持按模块、关键词、状态、优先级筛选。
支持按模块、关键词、状态、优先级、类型筛选。
Args:
module_id (Optional[str]): 模块ID筛选
keyword (Optional[str]): 关键词搜索(匹配名称和描述)
status (Optional[str]): 状态筛选 (active/disabled)
priority (Optional[str]): 优先级筛选 (high/medium/low)
case_type (Optional[str]): 用例类型筛选 (ui/api/security/deploy/performance/functional)
skip (int): 跳过的记录数,默认0
limit (int): 返回记录数上限,默认100
......@@ -91,6 +93,7 @@ class CaseService:
>>> cases, total = await service.list(
... module_id="module_001",
... keyword="登录",
... case_type="ui",
... skip=0, limit=20
... )
"""
......@@ -107,6 +110,8 @@ class CaseService:
conditions.append(TestCase.status == status)
if priority:
conditions.append(TestCase.priority == priority)
if case_type:
conditions.append(TestCase.case_type == case_type)
# 查询总数
count_query = select(func.count(TestCase.id))
......@@ -193,6 +198,7 @@ class CaseService:
tags=data.tags or [],
steps=[s.model_dump() for s in data.steps] if data.steps else [],
config=data.config or {"timeout": 30000, "retry": 0, "screenshot": True},
case_type=data.case_type or "ui",
)
self.db.add(test_case)
......@@ -289,6 +295,7 @@ class CaseService:
tags=source.tags.copy() if source.tags else [],
steps=source.steps.copy() if source.steps else [],
config=source.config.copy() if source.config else {},
case_type=source.case_type or "ui",
)
self.db.add(new_case)
......@@ -350,6 +357,7 @@ class CaseService:
tags=case_data.tags or [],
steps=[s.model_dump() for s in case_data.steps] if case_data.steps else [],
config=case_data.config or {"timeout": 30000, "retry": 0, "screenshot": True},
case_type=case_data.case_type or "ui",
)
self.db.add(test_case)
await self.db.flush()
......@@ -402,6 +410,7 @@ class CaseService:
"tags": c.tags or [],
"steps": c.steps or [],
"config": c.config or {},
"case_type": getattr(c, "case_type", None) or "ui",
}
for c in cases
]
\ No newline at end of file
......@@ -69,6 +69,7 @@ class ExecutionService:
skip: int = 0,
limit: int = 50,
status: Optional[str] = None,
case_type: Optional[str] = None,
) -> Tuple[List[Execution], int]:
"""
获取执行记录列表
......@@ -77,6 +78,7 @@ class ExecutionService:
skip (int): 跳过记录数
limit (int): 返回记录数上限
status (Optional[str]): 状态筛选
case_type (Optional[str]): 用例类型筛选 (ui/api/security/deploy/performance/functional)
Returns:
Tuple[List[Execution], int]: (执行记录列表, 总数)
......@@ -84,6 +86,8 @@ class ExecutionService:
conditions = []
if status:
conditions.append(Execution.status == status)
if case_type:
conditions.append(Execution.case_type == case_type)
count_query = select(func.count(Execution.id))
if conditions:
......@@ -145,6 +149,12 @@ class ExecutionService:
date_str = datetime.now().strftime("%Y%m%d-%H%M%S")
name = f"测试执行-{date_str}"
# 先查询用例,取首个用例的 case_type 作为执行记录的类型(用于按类型筛选)
cases_query = select(TestCase).where(TestCase.id.in_(case_ids))
cases_result = await self.db.execute(cases_query)
cases = list(cases_result.scalars().all())
first_case_type = cases[0].case_type if cases else "ui"
execution = Execution(
id=generate_id("exec"),
name=name,
......@@ -154,6 +164,7 @@ class ExecutionService:
status="pending",
environment=environment,
config=config or {},
case_type=first_case_type,
)
self.db.add(execution)
......@@ -161,10 +172,6 @@ class ExecutionService:
await self.db.refresh(execution)
# 创建用例结果记录
cases_query = select(TestCase).where(TestCase.id.in_(case_ids))
cases_result = await self.db.execute(cases_query)
cases = list(cases_result.scalars().all())
for case in cases:
case_result = CaseResult(
id=generate_id("result"),
......@@ -176,7 +183,7 @@ class ExecutionService:
self.db.add(case_result)
await self.db.flush()
logger.info(f"执行记录创建成功: id={execution.id}, name={name}, cases={len(case_ids)}")
logger.info(f"执行记录创建成功: id={execution.id}, name={name}, cases={len(case_ids)}, case_type={first_case_type}")
return execution
async def run_execution(
......
......@@ -59,7 +59,8 @@ class ModuleService:
self,
skip: int = 0,
limit: int = 100,
order_by: str = "order"
order_by: str = "order",
module_type: Optional[str] = None,
) -> Tuple[List[Module], int]:
"""
获取模块列表(分页)
......@@ -70,16 +71,24 @@ class ModuleService:
skip (int): 跳过的记录数,默认0
limit (int): 返回记录数上限,默认100
order_by (str): 排序字段,默认 "order"
module_type (Optional[str]): 模块类型筛选 (standard/custom)
Returns:
Tuple[List[Module], int]: (模块列表, 总数)
Example:
>>> modules, total = await service.list(skip=0, limit=20)
>>> modules, total = await service.list(skip=0, limit=20, module_type="standard")
>>> print(f"共 {total} 个模块")
"""
# 构建查询条件
conditions = []
if module_type:
conditions.append(Module.module_type == module_type)
# 查询总数
count_query = select(func.count(Module.id))
if conditions:
count_query = count_query.where(*conditions)
total_result = await self.db.execute(count_query)
total = total_result.scalar() or 0
......@@ -90,6 +99,8 @@ class ModuleService:
.offset(skip)
.limit(limit)
)
if conditions:
query = query.where(*conditions)
result = await self.db.execute(query)
modules = list(result.scalars().all())
......@@ -170,7 +181,8 @@ class ModuleService:
name=data.name.strip(),
description=data.description or "",
icon=data.icon or "folder",
order=max_order + 1
order=max_order + 1,
module_type=data.module_type or "standard",
)
self.db.add(module)
......
......@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>平台自动化测试系统</title>
<title>测试管理平台</title>
</head>
<body>
<div id="app"></div>
......
此差异已折叠。
......@@ -10,19 +10,20 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
},
"dependencies": {
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"pinia": "^2.1.7",
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.7",
"echarts": "^5.5.0",
"element-plus": "^2.5.6",
"@element-plus/icons-vue": "^2.3.1",
"echarts": "^5.5.0"
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"sass": "^1.72.0",
"terser": "^5.49.0",
"typescript": "^5.4.2",
"vite": "^5.1.6",
"vue-tsc": "^2.0.6",
"sass": "^1.72.0"
"vue-tsc": "^2.0.6"
}
}
\ No newline at end of file
}
......@@ -13,38 +13,76 @@
<el-aside width="220px" class="app-aside">
<div class="logo">
<el-icon :size="24"><Monitor /></el-icon>
<span>自动化测试平台</span>
<span>测试管理平台</span>
</div>
<el-menu
:default-active="currentRoute"
class="app-menu"
router
unique-opened
>
<el-menu-item index="/">
<el-icon><DataBoard /></el-icon>
<span>总览</span>
</el-menu-item>
<el-menu-item index="/modules">
<el-icon><Folder /></el-icon>
<span>模块管理</span>
</el-menu-item>
<el-menu-item index="/cases">
<el-icon><Document /></el-icon>
<span>用例管理</span>
</el-menu-item>
<!-- 模块管理(父菜单) -->
<el-sub-menu index="/modules">
<template #title>
<el-icon><Folder /></el-icon>
<span>模块管理</span>
</template>
<el-menu-item index="/modules/standard">标准模块</el-menu-item>
<el-menu-item index="/modules/custom">项目定制模块</el-menu-item>
</el-sub-menu>
<!-- 用例管理(父菜单) -->
<el-sub-menu index="/cases">
<template #title>
<el-icon><Document /></el-icon>
<span>用例管理</span>
</template>
<el-menu-item index="/cases/ui">UI自动化</el-menu-item>
<el-menu-item index="/cases/api">接口自动化</el-menu-item>
<el-menu-item index="/cases/security">安全自动化测试</el-menu-item>
<el-menu-item index="/cases/deploy">自动化部署测试</el-menu-item>
<el-menu-item index="/cases/performance">性能测试</el-menu-item>
<el-menu-item index="/cases/functional">功能测试</el-menu-item>
</el-sub-menu>
<el-menu-item index="/recorder">
<el-icon><VideoCamera /></el-icon>
<span>用例录制</span>
</el-menu-item>
<el-menu-item index="/execution">
<el-icon><VideoPlay /></el-icon>
<span>执行中心</span>
</el-menu-item>
<el-menu-item index="/reports">
<el-icon><DataLine /></el-icon>
<span>报告中心</span>
</el-menu-item>
<!-- 执行中心(父菜单,6 种类型) -->
<el-sub-menu index="/execution">
<template #title>
<el-icon><VideoPlay /></el-icon>
<span>执行中心</span>
</template>
<el-menu-item index="/execution/ui">UI自动化</el-menu-item>
<el-menu-item index="/execution/api">接口自动化</el-menu-item>
<el-menu-item index="/execution/security">安全自动化测试</el-menu-item>
<el-menu-item index="/execution/deploy">自动化部署测试</el-menu-item>
<el-menu-item index="/execution/performance">性能测试</el-menu-item>
<el-menu-item index="/execution/functional">功能测试</el-menu-item>
</el-sub-menu>
<!-- 报告中心(父菜单,5 种类型,无部署) -->
<el-sub-menu index="/reports">
<template #title>
<el-icon><DataLine /></el-icon>
<span>报告中心</span>
</template>
<el-menu-item index="/reports/ui">UI自动化测试</el-menu-item>
<el-menu-item index="/reports/api">接口测试</el-menu-item>
<el-menu-item index="/reports/security">安全测试</el-menu-item>
<el-menu-item index="/reports/performance">性能测试</el-menu-item>
<el-menu-item index="/reports/functional">功能测试</el-menu-item>
</el-sub-menu>
<el-menu-item index="/settings">
<el-icon><Setting /></el-icon>
<span>系统配置</span>
......@@ -110,12 +148,49 @@ const currentEnv = ref('test')
const route = useRoute()
// ==================== 计算属性 ====================
/** 当前路由 */
const currentRoute = computed(() => route.path)
/** 分类标签映射:用例管理/执行中心 */
const CASE_TYPE_LABELS: Record<string, string> = {
ui: 'UI自动化',
api: '接口自动化',
security: '安全自动化测试',
deploy: '自动化部署测试',
performance: '性能测试',
functional: '功能测试',
}
/** 分类标签映射:报告中心(不含部署) */
const REPORT_TYPE_LABELS: Record<string, string> = {
ui: 'UI自动化测试',
api: '接口测试',
security: '安全测试',
performance: '性能测试',
functional: '功能测试',
}
/** 模块类型标签映射 */
const MODULE_TYPE_LABELS: Record<string, string> = {
standard: '标准模块',
custom: '项目定制模块',
}
/** 当前激活菜单项(无 type 时回退到默认子项以保持高亮) */
const currentRoute = computed(() => {
const seg = route.path.split('/').filter(Boolean)
const root = '/' + (seg[0] || '')
// 无 type 参数时回退到默认子项,保证菜单高亮
if (seg.length === 1 && ['/modules', '/cases', '/execution', '/reports'].includes(root)) {
const defaultType = root === '/modules' ? 'standard' : 'ui'
return `${root}/${defaultType}`
}
return route.path
})
/** 页面标题 */
const pageTitle = computed(() => {
const titleMap: Record<string, string> = {
const seg = route.path.split('/').filter(Boolean)
const root = '/' + (seg[0] || '')
const baseMap: Record<string, string> = {
'/': '测试总览',
'/modules': '模块管理',
'/cases': '用例管理',
......@@ -124,7 +199,16 @@ const pageTitle = computed(() => {
'/reports': '报告中心',
'/settings': '系统配置'
}
return titleMap[route.path] || '自动化测试平台'
const base = baseMap[root] || '测试管理平台'
const type = seg[1]
if (!type) return base
let typeLabel = ''
if (root === '/modules') typeLabel = MODULE_TYPE_LABELS[type]
else if (root === '/reports') typeLabel = REPORT_TYPE_LABELS[type]
else if (root === '/cases' || root === '/execution') typeLabel = CASE_TYPE_LABELS[type]
return typeLabel ? `${base} - ${typeLabel}` : base
})
</script>
......@@ -170,6 +254,7 @@ html, body, #app {
border-right: none;
background: transparent;
// 顶级菜单项
.el-menu-item {
color: #bfcbd9;
......@@ -179,6 +264,38 @@ html, body, #app {
color: #409eff;
}
}
// 父菜单标题(el-sub-menu)
.el-sub-menu {
.el-sub-menu__title {
color: #bfcbd9;
&:hover {
background: #263445;
}
.el-sub-menu__icon-arrow {
color: #bfcbd9;
}
}
// 嵌套子菜单容器背景(比侧边栏略深,体现层级)
.el-menu {
background: #1f2d3d;
}
// 子菜单项
.el-menu-item {
color: #bfcbd9;
background: transparent;
&:hover,
&.is-active {
background: #263445;
color: #409eff;
}
}
}
}
}
......
......@@ -21,6 +21,7 @@ export const caseApi = {
keyword?: string
status?: string
priority?: string
case_type?: string
skip?: number
limit?: number
}): Promise<CaseListResponse> {
......
......@@ -19,6 +19,7 @@ export const executionApi = {
skip?: number
limit?: number
status?: string
case_type?: string
}): Promise<{ total: number; items: any[] }> {
const response = await request.get('/api/executions', { params })
return response.data
......
......@@ -15,10 +15,11 @@ import type { Module, ModuleListResponse, CreateModuleRequest, UpdateModuleReque
export const moduleApi = {
/**
* 获取模块列表
* @param module_type 模块类型筛选 standard/custom
*/
async list(skip = 0, limit = 100): Promise<ModuleListResponse> {
async list(skip = 0, limit = 100, module_type?: string): Promise<ModuleListResponse> {
const response = await request.get('/api/modules', {
params: { skip, limit }
params: { skip, limit, module_type }
})
return response.data
},
......
......@@ -15,7 +15,7 @@ export const reportApi = {
/**
* 获取报告列表(已完成的执行记录)
*/
async list(params?: { skip?: number; limit?: number }): Promise<{ total: number; items: any[] }> {
async list(params?: { skip?: number; limit?: number; case_type?: string }): Promise<{ total: number; items: any[] }> {
const response = await request.get('/api/reports/list', { params })
return response.data
},
......
此差异已折叠。
......@@ -18,13 +18,13 @@ const routes: RouteRecordRaw[] = [
meta: { title: '测试总览' }
},
{
path: '/modules',
path: '/modules/:type?',
name: 'Modules',
component: () => import('@/views/Modules.vue'),
meta: { title: '模块管理' }
},
{
path: '/cases',
path: '/cases/:type?',
name: 'Cases',
component: () => import('@/views/Cases.vue'),
meta: { title: '用例管理' }
......@@ -36,13 +36,13 @@ const routes: RouteRecordRaw[] = [
meta: { title: '用例录制' }
},
{
path: '/execution',
path: '/execution/:type?',
name: 'Execution',
component: () => import('@/views/Execution.vue'),
meta: { title: '执行中心' }
},
{
path: '/reports',
path: '/reports/:type?',
name: 'Reports',
component: () => import('@/views/Reports.vue'),
meta: { title: '报告中心' }
......@@ -70,7 +70,7 @@ router.beforeEach((to, _from, next) => {
// 设置页面标题
const title = to.meta?.title as string
if (title) {
document.title = `${title} - 平台自动化测试系统`
document.title = `${title} - 测试管理平台`
}
next()
})
......
......@@ -50,6 +50,8 @@ export interface TestCase {
steps: StepDefinition[]
/** 执行配置 */
config: Record<string, any>
/** 用例类型 ui/api/security/deploy/performance/functional */
caseType?: string
/** 创建时间 */
createdAt: string
/** 更新时间 */
......@@ -58,10 +60,12 @@ export interface TestCase {
/** 创建用例请求参数 */
export interface CreateCaseRequest {
/** 所属模块ID */
moduleId: string
/** 所属模块ID(后端 snake_case) */
module_id: string
/** 用例名称 */
name: string
/** 状态: active/disabled(create 时后端默认 active,update 时生效) */
status?: string
/** 用例描述 */
description?: string
/** 优先级 */
......@@ -72,6 +76,8 @@ export interface CreateCaseRequest {
steps?: StepDefinition[]
/** 配置 */
config?: Record<string, any>
/** 用例类型 ui/api/security/deploy/performance/functional */
case_type?: string
}
/** 用例列表响应 */
......
......@@ -18,6 +18,8 @@ export interface Module {
icon: string
/** 排序序号 */
order: number
/** 模块类型 standard/custom */
moduleType?: string
/** 用例数量(扩展字段) */
caseCount?: number
/** 通过率(扩展字段) */
......@@ -36,6 +38,8 @@ export interface CreateModuleRequest {
description?: string
/** 模块图标 */
icon?: string
/** 模块类型 standard/custom */
module_type?: string
}
/** 更新模块请求参数 */
......@@ -44,6 +48,7 @@ export interface UpdateModuleRequest {
description?: string
icon?: string
order?: number
module_type?: string
}
/** 模块列表响应 */
......
......@@ -6,7 +6,7 @@
* @date 2026-07-09
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axios, { AxiosInstance, AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
/**
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
......@@ -262,17 +262,12 @@ import { ElMessage } from 'element-plus'
import {
VideoCamera,
VideoPause,
VideoPlay,
Plus,
Top,
Bottom,
Delete,
FolderChecked,
} from '@element-plus/icons-vue'
import type { Module } from '@/types/module'
import { moduleApi } from '@/api/modules'
import { recorderApi } from '@/api/recorder'
import { caseApi } from '@/api/cases'
const router = useRouter()
......
此差异已折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论