## 场景模板库(S4-1)
- scenarios/templates.py: 6 个内置模板
- 单轮问答基础 / 多轮对话 / 动态 LLM 生成 / 安全合规检测 / JSON 接口校验 / 加权评分
- 每个模板附带对应规则配置(含 v0.3 新规则)
- routers/scenarios.py: GET /api/scenarios/templates + GET /api/scenarios/templates/{id}
- api.ts: scenariosApi.listTemplates() / getTemplate()
- Scenarios.tsx: 「从模板新建」按钮 + 卡片式模板选择弹窗
- 选择后预填名称/描述/标签/cases JSON/llm_config,直接进入编辑 Drawer
## WebSocket 自动重连(S4-2)
- useRunSession.ts: connectWs() 函数 + 指数退避重连
- 异常断开(非 1000/clean)时自动重试,最多 5 次
- 延迟:1s → 2s → 4s → 8s → 16s(上限 30s)
- 超出重试次数后降级 REST 获取最终状态
- reconnectTimerRef 在组件卸载时清理,无内存泄漏
## PageWrapper 复用(S4-3)
- PageWrapper.tsx: 升级 inline 模式匹配全高页面的 padding 页头样式
- Home / Targets / Scenarios: 用 PageWrapper inline+fullHeight 替换重复内联页头
- Home.tsx: 去掉 unused `colors` import
Co-Authored-By: Claude <noreply@anthropic.com>
74 lines
2.4 KiB
TypeScript
74 lines
2.4 KiB
TypeScript
import type { ReactNode } from 'react'
|
||
import { colors } from '../tokens'
|
||
|
||
interface PageWrapperProps {
|
||
title: string
|
||
description?: string
|
||
extra?: ReactNode
|
||
children: ReactNode
|
||
/**
|
||
* 内联模式:title + 竖线 + description 横排,right-align extra。
|
||
* 搭配 fullHeight 使用,是全高页面的标准页头样式。
|
||
*/
|
||
inline?: boolean
|
||
/**
|
||
* 撑满父容器高度:header 固定,children 填充剩余空间。
|
||
* 适用于所有管理页面。
|
||
*/
|
||
fullHeight?: boolean
|
||
}
|
||
|
||
export default function PageWrapper({
|
||
title, description, extra, children, inline, fullHeight,
|
||
}: PageWrapperProps) {
|
||
// Inline mode: horizontal header with padding (used with fullHeight)
|
||
if (inline) {
|
||
return (
|
||
<div style={{ height: fullHeight ? '100%' : undefined, display: 'flex', flexDirection: 'column', overflow: fullHeight ? 'hidden' : undefined }}>
|
||
<div style={{
|
||
padding: '10px 16px 8px', flexShrink: 0,
|
||
display: 'flex', alignItems: 'center', gap: 12,
|
||
}}>
|
||
<h2 style={{ margin: 0, fontSize: 20, fontWeight: 600, color: colors.text }}>{title}</h2>
|
||
{description && (
|
||
<>
|
||
<span style={{ width: 1, height: 18, background: '#d9d9d9', display: 'inline-block' }} />
|
||
<span style={{ fontSize: 13, color: colors.textSecondary }}>{description}</span>
|
||
</>
|
||
)}
|
||
{extra && <div style={{ marginLeft: 'auto' }}>{extra}</div>}
|
||
</div>
|
||
{fullHeight
|
||
? <div style={{ flex: 1, minHeight: 0 }}>{children}</div>
|
||
: children}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Block mode: stacked header with marginBottom (legacy, scrollable pages)
|
||
const header = (
|
||
<div style={{
|
||
display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16,
|
||
}}>
|
||
<div>
|
||
<h2 style={{ margin: 0, fontSize: 20, fontWeight: 600, color: colors.text }}>{title}</h2>
|
||
{description && (
|
||
<p style={{ margin: '4px 0 0', fontSize: 13, color: colors.textSecondary }}>{description}</p>
|
||
)}
|
||
</div>
|
||
{extra && <div>{extra}</div>}
|
||
</div>
|
||
)
|
||
|
||
if (fullHeight) {
|
||
return (
|
||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||
{header}
|
||
<div style={{ flex: 1, minHeight: 0 }}>{children}</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return <div>{header}{children}</div>
|
||
}
|