Initial commit

This commit is contained in:
Your Name
2026-02-05 16:25:52 +08:00
commit d5ea866eb4
178 changed files with 32681 additions and 0 deletions

7239
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
frontend/package.json Normal file
View File

@@ -0,0 +1,60 @@
{
"name": "flexible-test-platform",
"version": "1.0.0",
"description": "柔性敏捷智能测试体系平台",
"main": "dist/main/main.js",
"scripts": {
"dev": "concurrently -k \"npm run dev:renderer\" \"npm run electron:dev\"",
"dev:main": "tsc -p src/main/tsconfig.json && tsc -p src/preload/tsconfig.json",
"dev:renderer": "vite",
"electron:dev": "npm run dev:main && wait-on http://localhost:5173 && cross-env NODE_ENV=development electron .",
"build": "npm run build:main && npm run build:renderer",
"build:main": "tsc -p src/main/tsconfig.json",
"build:renderer": "vite build",
"start": "npm run build:main && cross-env NODE_ENV=production electron .",
"preview": "vite preview"
},
"keywords": [
"electron",
"test-platform",
"ai-assistant"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.11.0",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"electron": "^28.1.0",
"electron-builder": "^24.9.1",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"wait-on": "^7.2.0"
},
"dependencies": {
"axios": "^1.6.5",
"mammoth": "^1.11.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1"
},
"build": {
"appId": "com.flextest.platform",
"productName": "柔性敏捷智能测试体系平台",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"package.json"
],
"win": {
"target": "nsis"
}
}
}

75
frontend/src/main/main.ts Normal file
View File

@@ -0,0 +1,75 @@
/**
* Electron 主进程入口
* 柔性敏捷智能测试体系平台
*/
import { app, BrowserWindow, ipcMain } from 'electron';
import * as path from 'path';
let mainWindow: BrowserWindow | null = null;
function createMainWindow(): void {
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1000,
minHeight: 700,
title: '柔性敏捷智能测试体系平台',
frame: false, // 无边框窗口,自定义标题栏
webPreferences: {
preload: path.join(__dirname, '../preload/preload.js'),
nodeIntegration: false,
contextIsolation: true,
},
});
// 开发模式加载 Vite 开发服务器
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
} else {
// 生产模式加载构建后的文件
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
}
mainWindow.on('closed', () => {
mainWindow = null;
});
}
// 窗口控制 IPC 处理
ipcMain.on('window-minimize', () => {
mainWindow?.minimize();
});
ipcMain.on('window-maximize', () => {
if (mainWindow?.isMaximized()) {
mainWindow?.unmaximize();
} else {
mainWindow?.maximize();
}
});
ipcMain.on('window-close', () => {
mainWindow?.close();
});
ipcMain.handle('window-is-maximized', () => {
return mainWindow?.isMaximized();
});
app.whenReady().then(() => {
createMainWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": [
"ES2022"
],
"outDir": "../../dist/main",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": [
"./**/*"
],
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,31 @@
/**
* Electron Preload 脚本
* 安全地暴露 API 给渲染进程
*/
import { contextBridge, ipcRenderer } from 'electron';
// 暴露给渲染进程的 API
contextBridge.exposeInMainWorld('electronAPI', {
// 窗口控制
minimize: () => ipcRenderer.send('window-minimize'),
maximize: () => ipcRenderer.send('window-maximize'),
close: () => ipcRenderer.send('window-close'),
isMaximized: () => ipcRenderer.invoke('window-is-maximized'),
// 平台信息
platform: process.platform,
});
// TypeScript 类型声明
declare global {
interface Window {
electronAPI: {
minimize: () => void;
maximize: () => void;
close: () => void;
isMaximized: () => Promise<boolean>;
platform: string;
};
}
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": [
"ES2022"
],
"outDir": "../../dist/preload",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": [
"./**/*"
],
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,19 @@
.app {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.app-body {
display: flex;
flex: 1;
overflow: hidden;
}
.workspace {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}

View File

@@ -0,0 +1,117 @@
import React, { useState, useCallback } from 'react';
import TitleBar from './components/TitleBar/TitleBar';
import Toolbar from './components/Toolbar/Toolbar';
import Sidebar from './components/Sidebar/Sidebar';
import MainPanel from './components/MainPanel/MainPanel';
import AIPanel from './components/AIPanel/AIPanel';
import './App.css';
import { IntentActionHandlers, IntentActionStatus } from './types/intentActions';
import { ExtractedStep } from './services/api';
// 功能模块类型定义
export type ModuleType = 'intent' | 'task' | 'simulation' | 'code' | 'data' | 'knowledge';
const App: React.FC = () => {
// 当前选中的功能模块
const [activeModule, setActiveModule] = useState<ModuleType>('intent');
const [intentActions, setIntentActions] = useState<IntentActionHandlers | null>(null);
const [intentActionStatus, setIntentActionStatus] = useState<IntentActionStatus>({
isSaving: false,
isGenerating: false,
isChecking: false,
});
// 从意图编制提取的步骤(共享给任务规划)
const [extractedSteps, setExtractedSteps] = useState<ExtractedStep[]>([]);
// 当前选中的知识库
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<string>('default');
// AI 面板是否最大化
const [aiPanelMaximized, setAiPanelMaximized] = useState(false);
// AI 面板高度
const [aiPanelHeight, setAiPanelHeight] = useState(250);
// 完成编制后跳转到任务规划
const handleIntentComplete = useCallback((steps: ExtractedStep[]) => {
setExtractedSteps(steps);
setActiveModule('task');
}, []);
const handleToolbarAction = useCallback((action: string) => {
if (activeModule === 'intent') {
switch (action) {
case '新建意图':
intentActions?.onNew?.();
return;
case '打开':
intentActions?.onOpen?.();
return;
case '保存':
intentActions?.onSave?.();
return;
case 'AI 生成':
intentActions?.onGenerate?.();
return;
case 'AI 检查':
intentActions?.onCheck?.();
return;
case '导出':
intentActions?.onExport?.();
return;
default:
return;
}
}
}, [activeModule, intentActions]);
return (
<div className="app">
{/* 标题栏 */}
<TitleBar />
{/* 二级功能快捷栏 */}
<Toolbar
activeModule={activeModule}
onActionClick={handleToolbarAction}
intentActionStatus={intentActionStatus}
selectedKnowledgeBase={selectedKnowledgeBase}
onKnowledgeBaseChange={setSelectedKnowledgeBase}
/>
{/* 主体区域 */}
<div className="app-body">
{/* 左侧功能面板 */}
<Sidebar
activeModule={activeModule}
onModuleChange={setActiveModule}
/>
{/* 工作区 */}
<div className="workspace">
{/* 主窗口 */}
<MainPanel
activeModule={activeModule}
aiPanelMaximized={aiPanelMaximized}
onRegisterIntentActions={setIntentActions}
onIntentStatusChange={setIntentActionStatus}
onIntentComplete={handleIntentComplete}
extractedSteps={extractedSteps}
/>
{/* AI 交互窗口 */}
<AIPanel
height={aiPanelHeight}
onHeightChange={setAiPanelHeight}
maximized={aiPanelMaximized}
onMaximizeChange={setAiPanelMaximized}
/>
</div>
</div>
</div>
);
};
export default App;

View File

@@ -0,0 +1,200 @@
.ai-panel {
background: var(--bg-darker);
border-top: 1px solid var(--border-color);
display: flex;
flex-direction: column;
position: relative;
transition: height 0.2s ease;
}
.ai-panel.maximized {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
}
.ai-panel.minimized {
height: 36px !important;
}
.resize-handle {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
cursor: ns-resize;
background: transparent;
z-index: 10;
}
.resize-handle:hover {
background: var(--primary-color);
}
.ai-panel-header {
height: 36px;
min-height: 36px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--spacing-md);
background: var(--bg-sidebar);
border-bottom: 1px solid var(--border-color);
}
.ai-panel-title {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
}
.ai-panel-controls {
display: flex;
gap: var(--spacing-xs);
}
.panel-control-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
border-radius: 4px;
transition: all 0.15s;
}
.panel-control-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
}
.ai-panel-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.ai-messages {
flex: 1;
overflow-y: auto;
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.ai-message {
display: flex;
gap: var(--spacing-sm);
align-items: flex-start;
}
.ai-message.user {
flex-direction: row-reverse;
}
.message-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--bg-sidebar);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.message-content {
max-width: 80%;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: 8px;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.ai-message.ai .message-content {
background: var(--bg-sidebar);
color: var(--text-primary);
}
.ai-message.user .message-content {
background: var(--primary-color);
color: white;
}
.message-content.loading {
display: flex;
gap: 4px;
padding: var(--spacing-md);
}
.loading-dot {
width: 8px;
height: 8px;
background: var(--text-muted);
border-radius: 50%;
animation: loadingBounce 1.4s infinite ease-in-out both;
}
.loading-dot:nth-child(1) {
animation-delay: -0.32s;
}
.loading-dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes loadingBounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
.ai-input-area {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-top: 1px solid var(--border-color);
}
.ai-input {
flex: 1;
resize: none;
min-height: 40px;
max-height: 100px;
}
.ai-send-btn {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--primary-color);
color: white;
border-radius: 4px;
font-weight: 500;
transition: all 0.15s;
align-self: flex-end;
}
.ai-send-btn:hover:not(:disabled) {
background: var(--primary-hover);
}
.ai-send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

View File

@@ -0,0 +1,200 @@
import React, { useState, useRef, useCallback } from 'react';
import './AIPanel.css';
interface AIPanelProps {
height: number;
onHeightChange: (height: number) => void;
maximized: boolean;
onMaximizeChange: (maximized: boolean) => void;
}
const AIPanel: React.FC<AIPanelProps> = ({
height,
onHeightChange,
maximized,
onMaximizeChange,
}) => {
const [isMinimized, setIsMinimized] = useState(false);
const [messages, setMessages] = useState<{ role: 'user' | 'ai'; content: string }[]>([
{ role: 'ai', content: '你好!我是 AI 助手,可以帮助你生成意图编制内容、检查内容完整性等。有什么需要帮助的吗?' },
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const panelRef = useRef<HTMLDivElement>(null);
const resizeRef = useRef<HTMLDivElement>(null);
// 拖拽调整大小
const handleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
const startY = e.clientY;
const startHeight = height;
const handleMouseMove = (e: MouseEvent) => {
const delta = startY - e.clientY;
const newHeight = Math.max(150, Math.min(600, startHeight + delta));
onHeightChange(newHeight);
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}, [height, onHeightChange]);
// 发送消息
const handleSend = async () => {
if (!input.trim() || isLoading) return;
const userMessage = input.trim();
setMessages(prev => [...prev, { role: 'user', content: userMessage }]);
setInput('');
setIsLoading(true);
try {
const response = await fetch('http://localhost:8080/api/ai/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: userMessage }),
});
const data = await response.json();
if (data.success) {
setMessages(prev => [...prev, { role: 'ai', content: data.content }]);
} else {
setMessages(prev => [...prev, { role: 'ai', content: `错误: ${data.error || '请求失败'}` }]);
}
} catch (error) {
setMessages(prev => [...prev, { role: 'ai', content: '网络错误,请确保后端服务已启动。' }]);
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
// 切换最小化
const toggleMinimize = () => {
if (maximized) {
onMaximizeChange(false);
}
setIsMinimized(!isMinimized);
};
// 切换最大化
const toggleMaximize = () => {
if (isMinimized) {
setIsMinimized(false);
}
onMaximizeChange(!maximized);
};
const panelHeight = maximized ? '100%' : isMinimized ? 36 : height;
return (
<div
ref={panelRef}
className={`ai-panel ${maximized ? 'maximized' : ''} ${isMinimized ? 'minimized' : ''}`}
style={{ height: panelHeight }}
>
{/* 拖拽条 */}
{!maximized && !isMinimized && (
<div
ref={resizeRef}
className="resize-handle"
onMouseDown={handleMouseDown}
/>
)}
{/* 标题栏 */}
<div className="ai-panel-header">
<span className="ai-panel-title">AI </span>
<div className="ai-panel-controls">
<button
className="panel-control-btn"
onClick={toggleMinimize}
title={isMinimized ? '展开' : '最小化'}
>
<svg viewBox="0 0 16 16" width="14" height="14">
{isMinimized ? (
<path d="M3 8h10M8 3v10" stroke="currentColor" strokeWidth="1.5" fill="none" />
) : (
<rect x="3" y="7" width="10" height="2" fill="currentColor" />
)}
</svg>
</button>
<button
className="panel-control-btn"
onClick={toggleMaximize}
title={maximized ? '还原' : '最大化'}
>
<svg viewBox="0 0 16 16" width="14" height="14">
{maximized ? (
<>
<rect x="3" y="5" width="8" height="8" stroke="currentColor" strokeWidth="1.5" fill="none" />
<path d="M5 5V3h8v8h-2" stroke="currentColor" strokeWidth="1.5" fill="none" />
</>
) : (
<rect x="3" y="3" width="10" height="10" stroke="currentColor" strokeWidth="1.5" fill="none" />
)}
</svg>
</button>
</div>
</div>
{/* 内容区 */}
{!isMinimized && (
<div className="ai-panel-content">
<div className="ai-messages">
{messages.map((msg, idx) => (
<div key={idx} className={`ai-message ${msg.role}`}>
<div className="message-avatar">
{msg.role === 'ai' ? '🤖' : '👤'}
</div>
<div className="message-content selectable">
{msg.content}
</div>
</div>
))}
{isLoading && (
<div className="ai-message ai">
<div className="message-avatar">🤖</div>
<div className="message-content loading">
<span className="loading-dot"></span>
<span className="loading-dot"></span>
<span className="loading-dot"></span>
</div>
</div>
)}
</div>
<div className="ai-input-area">
<textarea
className="ai-input selectable"
placeholder="输入消息,按 Enter 发送..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
rows={2}
/>
<button
className="ai-send-btn"
onClick={handleSend}
disabled={!input.trim() || isLoading}
>
</button>
</div>
</div>
)}
</div>
);
};
export default AIPanel;

View File

@@ -0,0 +1,454 @@
.intent-editor {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.editor-message {
padding: var(--spacing-sm) var(--spacing-md);
font-size: 12px;
text-align: center;
}
.editor-message.success {
background: rgba(82, 196, 26, 0.2);
color: var(--success-color);
}
.editor-message.error {
background: rgba(245, 34, 45, 0.2);
color: var(--error-color);
}
.editor-content {
flex: 1;
display: flex;
overflow: hidden;
}
.editor-main {
flex: 1;
display: flex;
flex-direction: column;
padding: var(--spacing-md);
gap: var(--spacing-md);
overflow: hidden;
}
.editor-title-row {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.editor-title {
font-size: 18px;
font-weight: 500;
padding: var(--spacing-sm);
background: transparent;
border: 1px solid transparent;
color: var(--text-primary);
flex: 1;
}
.editor-title:focus {
border-color: var(--border-focus);
background: var(--bg-darker);
}
.editor-title-actions {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-left: auto;
}
.complete-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
font-size: 13px;
font-weight: 500;
color: white;
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.complete-btn:hover {
background: linear-gradient(135deg, #73d13d 0%, #52c41a 100%);
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.4);
}
.complete-btn svg {
opacity: 0.9;
}
.complete-btn.loading {
background: linear-gradient(135deg, #666 0%, #555 100%);
cursor: wait;
}
.complete-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.loading-spinner {
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.view-mode-toggle {
display: flex;
background: var(--bg-darker);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 2px;
}
.toggle-btn {
padding: 4px 12px;
font-size: 12px;
color: var(--text-secondary);
background: transparent;
border: none;
border-radius: 3px;
cursor: pointer;
transition: all 0.15s;
}
.toggle-btn:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
}
.toggle-btn.active {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.1);
font-weight: 500;
}
.editor-body {
flex: 1;
display: flex;
overflow: hidden;
gap: var(--spacing-sm);
}
.editor-body.mode-edit .editor-textarea,
.editor-body.mode-preview .editor-markdown {
width: 100%;
}
.editor-body.mode-split .editor-textarea,
.editor-body.mode-split .editor-markdown {
width: 50%;
}
.editor-divider {
width: 1px;
background: var(--border-color);
margin: 0 4px;
}
.editor-textarea {
flex: 1;
resize: none;
padding: var(--spacing-md);
font-size: 14px;
line-height: 1.6;
font-family: 'Consolas', 'Monaco', monospace;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-darker);
color: var(--text-primary);
}
.editor-markdown {
flex: 1;
overflow: auto;
padding: var(--spacing-md);
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-darker);
font-size: 14px;
line-height: 1.6;
}
.editor-markdown h1,
.editor-markdown h2,
.editor-markdown h3,
.editor-markdown h4,
.editor-markdown h5,
.editor-markdown h6 {
margin: 0.6em 0 0.4em;
}
.editor-markdown p {
margin: 0.5em 0;
}
.editor-markdown ul,
.editor-markdown ol {
padding-left: 2em;
margin: 0.5em 0;
}
.editor-markdown li {
margin-bottom: 0.2em;
}
.editor-markdown blockquote {
border-left: 4px solid var(--border-color);
padding-left: 1em;
margin: 0.5em 0;
color: var(--text-secondary);
}
.editor-markdown img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 0.5em 0;
}
.editor-markdown table {
width: 100%;
border-collapse: collapse;
margin: 0.5em 0;
}
.editor-markdown th,
.editor-markdown td {
border: 1px solid var(--border-color);
padding: 6px 8px;
}
.editor-markdown pre {
background: rgba(0, 0, 0, 0.2);
padding: var(--spacing-sm);
border-radius: 4px;
overflow-x: auto;
margin: 0.5em 0;
border: 1px solid var(--border-color);
}
.editor-markdown pre code {
background: transparent;
padding: 0;
border-radius: 0;
color: inherit;
}
.editor-markdown code {
background: rgba(255, 255, 255, 0.08);
padding: 2px 4px;
border-radius: 4px;
}
/* AI 检查结果面板 */
.check-result-panel {
width: 320px;
background: var(--bg-sidebar);
border-left: 1px solid var(--border-color);
display: flex;
flex-direction: column;
}
.check-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--border-color);
}
.check-title {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
}
.check-header .close-btn {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: var(--text-muted);
border-radius: 4px;
}
.check-header .close-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
}
.check-body {
flex: 1;
overflow-y: auto;
padding: var(--spacing-md);
}
.check-status {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md);
border-radius: 8px;
margin-bottom: var(--spacing-md);
}
.check-status.passed {
background: rgba(82, 196, 26, 0.2);
}
.check-status.failed {
background: rgba(245, 34, 45, 0.2);
}
.status-icon {
font-size: 20px;
}
.check-status.passed .status-icon {
color: var(--success-color);
}
.check-status.failed .status-icon {
color: var(--error-color);
}
.status-text {
flex: 1;
font-weight: 500;
}
.status-score {
font-size: 12px;
color: var(--text-muted);
}
.check-section {
margin-bottom: var(--spacing-md);
}
.section-title {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.issue-list,
.suggestion-list {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.issue-list li,
.suggestion-list li {
font-size: 12px;
color: var(--text-secondary);
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--bg-darker);
border-radius: 4px;
line-height: 1.4;
}
.issue-list li::before {
content: '• ';
color: var(--error-color);
}
.suggestion-list li::before {
content: ' ';
}
/* 源码定位高亮 - 点击预览中的文字,定位到编辑器中对应位置 */
.editor-markdown [data-source-line] {
cursor: pointer;
position: relative;
transition: background-color 0.15s;
}
.editor-markdown [data-source-line]:hover {
background-color: rgba(24, 144, 255, 0.15);
box-shadow: inset 4px 0 0 0 var(--primary-color);
}
.editor-markdown [data-source-line]::after {
content: "点击定位";
position: absolute;
right: 4px;
top: 2px;
font-size: 10px;
color: var(--primary-color);
opacity: 0;
padding: 2px 6px;
background: rgba(24, 144, 255, 0.1);
border-radius: 3px;
pointer-events: none;
transition: opacity 0.2s;
}
.editor-markdown [data-source-line]:hover::after {
opacity: 1;
}
/* 可点击的预览区域样式 */
.clickable-preview {
cursor: pointer;
}
.clickable-preview h1,
.clickable-preview h2,
.clickable-preview h3,
.clickable-preview h4,
.clickable-preview h5,
.clickable-preview h6,
.clickable-preview p,
.clickable-preview li,
.clickable-preview blockquote,
.clickable-preview pre,
.clickable-preview td,
.clickable-preview th {
cursor: pointer;
transition: background-color 0.15s;
border-radius: 3px;
}
.clickable-preview h1:hover,
.clickable-preview h2:hover,
.clickable-preview h3:hover,
.clickable-preview h4:hover,
.clickable-preview h5:hover,
.clickable-preview h6:hover,
.clickable-preview p:hover,
.clickable-preview li:hover,
.clickable-preview blockquote:hover,
.clickable-preview pre:hover,
.clickable-preview td:hover,
.clickable-preview th:hover {
background-color: rgba(24, 144, 255, 0.15);
box-shadow: inset 4px 0 0 0 var(--primary-color);
}

View File

@@ -0,0 +1,465 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import './IntentEditor.css';
import { apiService, Intent, ExtractedStep } from '../../services/api';
import { IntentActionHandlers, IntentActionStatus } from '../../types/intentActions';
import * as mammoth from 'mammoth/mammoth.browser';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
interface IntentEditorProps {
onRegisterActions?: (handlers: IntentActionHandlers | null) => void;
onStatusChange?: (status: IntentActionStatus) => void;
onComplete?: (steps: ExtractedStep[]) => void;
}
const IntentEditor: React.FC<IntentEditorProps> = ({ onRegisterActions, onStatusChange, onComplete }) => {
const [title, setTitle] = useState('新建意图编制');
const [content, setContent] = useState('');
const [currentIntent, setCurrentIntent] = useState<Intent | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [isChecking, setIsChecking] = useState(false);
const [isExtracting, setIsExtracting] = useState(false);
const [checkResult, setCheckResult] = useState<any>(null);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'split'>('edit');
const fileInputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
onStatusChange?.({
isSaving,
isGenerating,
isChecking,
});
}, [onStatusChange, isSaving, isGenerating, isChecking]);
// 显示消息
const showMessage = (type: 'success' | 'error', text: string) => {
setMessage({ type, text });
setTimeout(() => setMessage(null), 3000);
};
// 保存意图
const handleSave = useCallback(async () => {
if (!title.trim()) {
showMessage('error', '请输入标题');
return;
}
setIsSaving(true);
try {
if (currentIntent) {
// 更新
const updated = await apiService.updateIntent(currentIntent.id, { title, content });
setCurrentIntent(updated);
showMessage('success', '保存成功');
} else {
// 创建
const created = await apiService.createIntent({ title, content });
setCurrentIntent(created);
showMessage('success', '创建成功');
}
} catch (error) {
showMessage('error', '保存失败');
} finally {
setIsSaving(false);
}
}, [title, content, currentIntent]);
// AI 生成内容
const handleGenerate = useCallback(async () => {
// 如果文本框有内容,则基于当前内容生成测试规划
if (content.trim()) {
setIsGenerating(true);
try {
const result = await apiService.generateTestPlan(content, title || '用户输入');
if (result.success) {
setContent(result.content);
showMessage('success', 'AI 测试规划生成完成');
} else {
showMessage('error', result.error || 'AI 生成失败');
}
} catch (error) {
showMessage('error', '网络错误,请确保后端服务已启动');
} finally {
setIsGenerating(false);
}
} else {
// 如果文本框为空,使用原有的意图编制生成
setIsGenerating(true);
try {
const result = await apiService.generateIntentContent({
test_type: '功能测试',
test_target: title || '待定义的测试目标',
});
if (result.success) {
setContent(result.content);
showMessage('success', 'AI 生成完成');
} else {
showMessage('error', result.error || 'AI 生成失败');
}
} catch (error) {
showMessage('error', '网络错误,请确保后端服务已启动');
} finally {
setIsGenerating(false);
}
}
}, [title, content]);
// AI 检查内容
const handleCheck = useCallback(async () => {
if (!content.trim()) {
showMessage('error', '请先输入或生成内容');
return;
}
setIsChecking(true);
setCheckResult(null);
try {
const result = await apiService.checkContent({
content,
intent_id: currentIntent?.id,
});
if (result.success) {
setCheckResult(result.result);
} else {
showMessage('error', result.error || 'AI 检查失败');
}
} catch (error) {
showMessage('error', '网络错误');
} finally {
setIsChecking(false);
}
}, [content, currentIntent]);
// 新建意图
const handleNew = useCallback(() => {
setTitle('新建意图编制');
setContent('');
setCurrentIntent(null);
setCheckResult(null);
}, []);
const handleOpen = useCallback(() => {
fileInputRef.current?.click();
}, []);
const extractTitleAndContent = useCallback((text: string, fallbackTitle: string) => {
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
if (lines.length === 0) {
return { parsedTitle: fallbackTitle, parsedContent: text };
}
const first = lines[0];
const parsedTitle = first.replace(/^#+\s*/, '') || fallbackTitle;
const parsedContent = lines.slice(1).join('\n').trim();
return { parsedTitle, parsedContent: parsedContent || text };
}, []);
const handleFileChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const fileName = file.name.replace(/\.[^/.]+$/, '');
const ext = `.${file.name.split('.').pop() || ''}`.toLowerCase();
try {
if (ext === '.docx') {
const buffer = await file.arrayBuffer();
const result = await mammoth.extractRawText({ arrayBuffer: buffer });
const fileContent = result.value || '';
const { parsedContent } = extractTitleAndContent(
fileContent,
fileName || '新建意图编制'
);
setTitle(fileName || '新建意图编制');
setContent(parsedContent);
setCurrentIntent(null);
setCheckResult(null);
showMessage('success', '文件已打开');
} else if (['.pdf', '.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff', '.webp'].includes(ext)) {
const parsed = await apiService.parseIntentFile(file);
setTitle(fileName || '新建意图编制');
setContent(parsed.content || '');
setCurrentIntent(null);
setCheckResult(null);
showMessage('success', '文件已解析并打开');
} else {
const reader = new FileReader();
reader.onload = () => {
const fileContent = typeof reader.result === 'string' ? reader.result : '';
const { parsedContent } = extractTitleAndContent(
fileContent,
fileName || '新建意图编制'
);
setTitle(fileName || '新建意图编制');
setContent(parsedContent);
setCurrentIntent(null);
setCheckResult(null);
showMessage('success', '文件已打开');
};
reader.onerror = () => {
showMessage('error', '文件读取失败');
};
reader.readAsText(file, 'utf-8');
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '文件解析失败';
showMessage('error', errorMessage);
} finally {
event.target.value = '';
}
}, []);
const handleExport = useCallback(() => {
if (!title.trim() && !content.trim()) {
showMessage('error', '没有可导出的内容');
return;
}
const exportContent = `# ${title || '未命名意图'}\n\n${content || ''}`;
const blob = new Blob([exportContent], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const safeTitle = (title || 'intent').replace(/[\\/:*?"<>|]/g, '_');
link.href = url;
link.download = `${safeTitle}.md`;
link.click();
URL.revokeObjectURL(url);
showMessage('success', '导出完成');
}, [title, content]);
const isMarkdownContent = useCallback((text: string) => {
return /(^|\n)#{1,6}\s+|\|.+\||```|\*\*|__|\[.+\]\(.+\)|^\s*[-*+]\s+/m.test(text);
}, []);
// 点击预览区域时,通过搜索文本内容来定位到源码
const handlePreviewClick = useCallback((event: React.MouseEvent) => {
if (!textareaRef.current || viewMode === 'preview') return;
const target = event.target as HTMLElement;
// 获取点击元素的纯文本内容
const clickedText = target.textContent?.trim();
if (!clickedText || clickedText.length < 2) return;
// 在源码中搜索这段文本
const searchText = clickedText.slice(0, Math.min(clickedText.length, 50)); // 取前50字符搜索
const index = content.indexOf(searchText);
if (index !== -1) {
const textarea = textareaRef.current;
const endIndex = index + searchText.length;
// 方法:按字符位置的比例来计算滚动位置
// 这比依赖行高更可靠
const totalLength = content.length;
const ratio = index / totalLength;
const maxScroll = textarea.scrollHeight - textarea.clientHeight;
// 目标位置,稍微往上偏移一点让选中内容在视口中间偏上
const targetScrollTop = Math.max(0, ratio * textarea.scrollHeight - textarea.clientHeight / 3);
// 设置滚动位置
textarea.scrollTop = Math.min(targetScrollTop, maxScroll);
// 使用 setTimeout 确保滚动完成后再设置选区和焦点
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(index, endIndex);
// 如果浏览器自动调整了滚动位置,我们再修正一次
const currentScroll = textarea.scrollTop;
const expectedScroll = Math.min(targetScrollTop, maxScroll);
if (Math.abs(currentScroll - expectedScroll) > 50) {
textarea.scrollTop = expectedScroll;
}
}, 10);
}
}, [content, viewMode]);
useEffect(() => {
onRegisterActions?.({
onNew: handleNew,
onOpen: handleOpen,
onSave: handleSave,
onGenerate: handleGenerate,
onCheck: handleCheck,
onExport: handleExport,
});
return () => {
onRegisterActions?.(null);
};
}, [onRegisterActions, handleNew, handleOpen, handleSave, handleGenerate, handleCheck, handleExport]);
return (
<div className="intent-editor">
<input
ref={fileInputRef}
type="file"
accept=".md,.txt,.markdown,.docx,.pdf,.png,.jpg,.jpeg,.bmp,.tif,.tiff,.webp"
onChange={handleFileChange}
style={{ display: 'none' }}
/>
{/* 消息提示 */}
{message && (
<div className={`editor-message ${message.type}`}>
{message.text}
</div>
)}
{/* 编辑区域 */}
<div className="editor-content">
<div className="editor-main">
<div className="editor-title-row">
<input
type="text"
className="editor-title selectable"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="请输入意图标题..."
/>
<div className="editor-title-actions">
{isMarkdownContent(content) && (
<div className="view-mode-toggle">
<button
className={`toggle-btn ${viewMode === 'edit' ? 'active' : ''}`}
onClick={() => setViewMode('edit')}
title="仅编辑"
>
</button>
<button
className={`toggle-btn ${viewMode === 'split' ? 'active' : ''}`}
onClick={() => setViewMode('split')}
title="分屏预览"
>
</button>
<button
className={`toggle-btn ${viewMode === 'preview' ? 'active' : ''}`}
onClick={() => setViewMode('preview')}
title="仅预览"
>
</button>
</div>
)}
<button
className={`complete-btn ${isExtracting ? 'loading' : ''}`}
onClick={async () => {
if (!content.trim()) {
showMessage('error', '请先输入或生成意图编制内容');
return;
}
setIsExtracting(true);
try {
const result = await apiService.extractSteps(content, title);
if (result.success && result.steps.length > 0) {
showMessage('success', `已提取 ${result.steps.length} 个测试步骤`);
onComplete?.(result.steps);
} else {
showMessage('error', result.error || '未能提取到测试步骤');
}
} catch (error) {
showMessage('error', '网络错误,请确保后端服务已启动');
} finally {
setIsExtracting(false);
}
}}
disabled={isExtracting}
title="完成编制并提交到下一流程"
>
{isExtracting ? (
<span className="loading-spinner" />
) : (
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
)}
{isExtracting ? '提取中...' : '完成编制'}
</button>
</div>
</div>
<div className={`editor-body mode-${viewMode}`}>
{(viewMode === 'edit' || viewMode === 'split') && (
<textarea
ref={textareaRef}
className="editor-textarea selectable"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="请输入意图编制内容,或使用 AI 生成..."
/>
)}
{viewMode === 'split' && <div className="editor-divider" />}
{(viewMode === 'preview' || viewMode === 'split') && (
<div
className="editor-markdown selectable clickable-preview"
onClick={handlePreviewClick}
title="点击文字可定位到左侧编辑器对应位置"
>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{content || '暂无预览内容'}
</ReactMarkdown>
</div>
)}
</div>
</div>
{/* AI 检查结果面板 */}
{checkResult && (
<div className="check-result-panel">
<div className="check-header">
<span className="check-title">AI </span>
<button className="close-btn" onClick={() => setCheckResult(null)}>
</button>
</div>
<div className="check-body">
<div className={`check-status ${checkResult.passed ? 'passed' : 'failed'}`}>
<span className="status-icon">{checkResult.passed ? '✓' : '✗'}</span>
<span className="status-text">
{checkResult.passed ? '检查通过' : '需要改进'}
</span>
<span className="status-score">: {checkResult.score}/100</span>
</div>
{checkResult.issues?.length > 0 && (
<div className="check-section">
<div className="section-title"></div>
<ul className="issue-list">
{checkResult.issues.map((issue: string, idx: number) => (
<li key={idx}>{issue}</li>
))}
</ul>
</div>
)}
{checkResult.suggestions?.length > 0 && (
<div className="check-section">
<div className="section-title"></div>
<ul className="suggestion-list">
{checkResult.suggestions.map((suggestion: string, idx: number) => (
<li key={idx}>{suggestion}</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
</div>
</div>
);
};
export default IntentEditor;

View File

@@ -0,0 +1,37 @@
.main-panel {
flex: 1;
background: var(--bg-panel);
overflow: hidden; /* Prevent double scrollbars, let children handle scroll */
display: flex;
flex-direction: column;
}
.main-panel.minimized {
flex: 0;
min-height: 0;
overflow: hidden;
}
.placeholder-panel {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-muted);
gap: var(--spacing-md);
}
.placeholder-icon {
opacity: 0.5;
}
.placeholder-panel h2 {
font-size: 20px;
font-weight: 500;
color: var(--text-secondary);
}
.placeholder-panel p {
font-size: 14px;
}

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { ModuleType } from '../../App';
import IntentEditor from '../IntentEditor/IntentEditor';
import TaskPlanner from '../TaskPlanner/TaskPlanner';
import './MainPanel.css';
import { IntentActionHandlers, IntentActionStatus } from '../../types/intentActions';
import { ExtractedStep } from '../../services/api';
interface MainPanelProps {
activeModule: ModuleType;
aiPanelMaximized: boolean;
onRegisterIntentActions?: (handlers: IntentActionHandlers | null) => void;
onIntentStatusChange?: (status: IntentActionStatus) => void;
onIntentComplete?: (steps: ExtractedStep[]) => void;
extractedSteps?: ExtractedStep[];
}
// 模块标题映射
const moduleTitles: Record<ModuleType, string> = {
intent: '意图编制',
task: '任务规划',
simulation: '逻辑仿真',
code: '代码生成',
data: '数据服务',
knowledge: '知识库',
};
// 占位组件
const PlaceholderPanel: React.FC<{ title: string }> = ({ title }) => (
<div className="placeholder-panel">
<div className="placeholder-icon">
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" strokeWidth="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="9" y1="21" x2="9" y2="9" />
</svg>
</div>
<h2>{title}</h2>
<p>...</p>
</div>
);
const MainPanel: React.FC<MainPanelProps> = ({
activeModule,
aiPanelMaximized,
onRegisterIntentActions,
onIntentStatusChange,
onIntentComplete,
extractedSteps = [],
}) => {
// 渲染占位组件仅当前激活且非intent/task时
const renderPlaceholder = () => {
if (activeModule === 'intent' || activeModule === 'task') {
return null;
}
return <PlaceholderPanel title={moduleTitles[activeModule]} />;
};
return (
<div className={`main-panel ${aiPanelMaximized ? 'minimized' : ''}`}>
{/* IntentEditor 始终挂载,通过 CSS 控制显示/隐藏以保持状态 */}
<div style={{ display: activeModule === 'intent' ? 'contents' : 'none' }}>
<IntentEditor
onRegisterActions={onRegisterIntentActions}
onStatusChange={onIntentStatusChange}
onComplete={onIntentComplete}
/>
</div>
{/* TaskPlanner 始终挂载 */}
<div style={{ display: activeModule === 'task' ? 'contents' : 'none' }}>
<TaskPlanner extractedSteps={extractedSteps} />
</div>
{/* 其他占位模块 */}
{renderPlaceholder()}
</div>
);
};
export default MainPanel;

View File

@@ -0,0 +1,65 @@
.sidebar {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
background: var(--bg-sidebar);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: var(--spacing-sm) 0;
}
.sidebar-item {
width: 100%;
padding: var(--spacing-md) var(--spacing-xs);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
color: var(--text-muted);
transition: all 0.15s;
position: relative;
}
.sidebar-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
background: transparent;
transition: background 0.15s;
}
.sidebar-item:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
}
.sidebar-item.active {
color: var(--primary-color);
}
.sidebar-item.active::before {
background: var(--primary-color);
}
.sidebar-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-icon svg {
width: 20px;
height: 20px;
}
.sidebar-label {
font-size: 10px;
text-align: center;
line-height: 1.2;
word-break: keep-all;
}

View File

@@ -0,0 +1,97 @@
import React from 'react';
import { ModuleType } from '../../App';
import './Sidebar.css';
interface SidebarProps {
activeModule: ModuleType;
onModuleChange: (module: ModuleType) => void;
}
// 功能模块配置
const modules: { id: ModuleType; label: string; icon: React.ReactNode }[] = [
{
id: 'intent',
label: '意图编制',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
),
},
{
id: 'task',
label: '任务规划',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M9 11l3 3L22 4" />
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
</svg>
),
},
{
id: 'simulation',
label: '逻辑仿真',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="23 4 23 10 17 10" />
<polyline points="1 20 1 14 7 14" />
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
</svg>
),
},
{
id: 'code',
label: '代码生成',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
),
},
{
id: 'data',
label: '数据服务',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<ellipse cx="12" cy="5" rx="9" ry="3" />
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
</svg>
),
},
{
id: 'knowledge',
label: '知识库',
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
</svg>
),
},
];
const Sidebar: React.FC<SidebarProps> = ({ activeModule, onModuleChange }) => {
return (
<div className="sidebar">
{modules.map((module) => (
<button
key={module.id}
className={`sidebar-item ${activeModule === module.id ? 'active' : ''}`}
onClick={() => onModuleChange(module.id)}
title={module.label}
>
<div className="sidebar-icon">{module.icon}</div>
<span className="sidebar-label">{module.label}</span>
</button>
))}
</div>
);
};
export default Sidebar;

View File

@@ -0,0 +1,510 @@
.task-planner {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #e0e0e0;
}
/* 消息提示 */
.planner-message {
padding: 8px 16px;
font-size: 12px;
text-align: center;
border-radius: 0;
}
.planner-message.success {
background: rgba(82, 196, 26, 0.2);
color: #52c41a;
}
.planner-message.error {
background: rgba(245, 34, 45, 0.2);
color: #f5222d;
}
/* 标题行 */
.planner-title-row {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 20px;
background-color: #1a1a1a;
border-bottom: 1px solid #2d2d2d;
}
.planner-title {
flex: 1;
font-size: 18px;
font-weight: 500;
padding: 8px 12px;
background: transparent;
border: 1px solid transparent;
color: #e0e0e0;
border-radius: 4px;
}
.planner-title:focus {
border-color: #2563eb;
background: #222;
outline: none;
}
.planner-title-row .complete-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
font-size: 13px;
font-weight: 500;
color: white;
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.planner-title-row .complete-btn:hover {
background: linear-gradient(135deg, #73d13d 0%, #52c41a 100%);
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.4);
}
.planner-title-row .complete-btn svg {
opacity: 0.9;
}
/* 主体区域 - 三栏布局 */
.planner-body {
display: flex;
flex: 1;
overflow: hidden;
}
/* 通用面板头部 */
.panel-header {
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
background-color: #252526;
border-bottom: 1px solid #333;
color: #e0e0e0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 空状态提示 */
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #666;
font-size: 14px;
}
/* ========== 左侧:步骤列表 ========== */
.planner-steps {
width: 220px;
min-width: 180px;
background-color: #181818;
border-right: 1px solid #2d2d2d;
display: flex;
flex-direction: column;
}
.steps-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.step-item {
display: flex;
flex-direction: column;
padding: 10px 12px;
margin-bottom: 4px;
cursor: pointer;
border-radius: 6px;
border: 1px solid transparent;
transition: all 0.2s ease;
}
.step-item:hover {
background-color: #2b2b2b;
border-color: #3d3d3d;
}
.step-item.active {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
border-color: #3b82f6;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3);
}
.step-number {
font-size: 12px;
font-weight: 600;
color: #888;
margin-bottom: 4px;
}
.step-item.active .step-number {
color: rgba(255, 255, 255, 0.8);
}
.step-name {
font-size: 13px;
color: #d4d4d4;
line-height: 1.4;
word-break: break-all;
}
.step-item.active .step-name {
color: #fff;
font-weight: 500;
}
/* ========== 中间:一级标题列表 ========== */
.planner-categories {
width: 240px;
min-width: 200px;
background-color: #1a1a1a;
border-right: 1px solid #2d2d2d;
display: flex;
flex-direction: column;
}
.categories-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.planner-categories .category-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
margin-bottom: 4px;
cursor: pointer;
border-radius: 6px;
border: 1px solid transparent;
transition: all 0.2s ease;
}
.planner-categories .category-item:hover {
background-color: #2b2b2b;
border-color: #3d3d3d;
}
.planner-categories .category-item.active {
background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%);
border-color: #14b8a6;
box-shadow: 0 2px 8px rgba(20, 184, 166, 0.3);
}
.category-title {
font-size: 14px;
color: #d4d4d4;
font-weight: 500;
}
.planner-categories .category-item.active .category-title {
color: #fff;
}
.category-count {
font-size: 12px;
color: #666;
background: #2d2d2d;
padding: 2px 8px;
border-radius: 10px;
}
.planner-categories .category-item.active .category-count {
background: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.9);
}
/* ========== 右侧:参数详情 ========== */
.planner-params {
flex: 1;
background-color: #1a1a1a;
display: flex;
flex-direction: column;
min-width: 0;
}
.params-content {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
/* 表头 */
.params-table-header {
display: flex;
background-color: #252526;
border-bottom: 1px solid #333;
padding: 10px 16px;
font-weight: 600;
font-size: 13px;
color: #cccccc;
flex-shrink: 0;
}
/* 参数行 */
.param-row {
display: flex;
border-bottom: 1px solid #2d2d2d;
padding: 10px 16px;
align-items: center;
font-size: 13px;
transition: background-color 0.1s;
flex-shrink: 0;
}
.param-row:last-child {
border-bottom: none;
}
.param-row:hover {
background-color: #252526;
}
/* 列样式 */
.planner-params .col-name {
flex: 1;
min-width: 120px;
padding-right: 12px;
color: #d4d4d4;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.planner-params .col-desc {
flex: 2;
padding-right: 12px;
color: #888;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.planner-params .col-value {
flex: 1.5;
min-width: 150px;
}
/* 输入框 */
.param-input {
width: 100%;
background-color: #2d2d2d;
border: 1px solid #3e3e42;
color: #d4d4d4;
padding: 6px 10px;
border-radius: 4px;
font-family: inherit;
font-size: 13px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.param-input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
}
.param-input::placeholder {
color: #666;
}
/* ========== 分组面板样式 ========== */
.group-panel {
border: 1px solid #333;
border-radius: 4px;
background-color: #1e1e1e;
overflow: hidden;
margin-bottom: 12px;
flex-shrink: 0;
}
.group-header {
background-color: #2d2d2d;
padding: 10px 15px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
display: flex;
align-items: center;
justify-content: space-between;
transition: background-color 0.2s;
user-select: none;
}
.group-header:hover {
background-color: #383838;
}
.group-header::after {
content: '▼';
font-size: 10px;
transition: transform 0.2s;
opacity: 0.7;
}
.group-panel.collapsed .group-header::after {
transform: rotate(-90deg);
}
.group-body {
border-top: 1px solid #333;
}
.group-panel.collapsed .group-body {
display: none;
}
/* 表头样式 */
.attr-table-header {
display: flex;
background-color: #252526;
border-bottom: 1px solid #333;
padding: 8px 15px;
font-weight: 600;
font-size: 12px;
color: #999;
}
.attr-row {
display: flex;
border-bottom: 1px solid #2d2d2d;
padding: 8px 15px;
align-items: center;
font-size: 13px;
transition: background-color 0.1s;
}
.attr-row:last-child {
border-bottom: none;
}
.attr-row:hover {
background-color: #252526;
}
/* 列样式 */
.col-name {
flex: 1;
min-width: 120px;
padding-right: 10px;
color: #d4d4d4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-desc {
flex: 1.5;
padding-right: 10px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-value {
flex: 1.5;
min-width: 150px;
}
.attr-input {
width: 100%;
background-color: #2d2d2d;
border: 1px solid #3e3e42;
color: #d4d4d4;
padding: 5px 8px;
border-radius: 3px;
font-family: inherit;
font-size: 13px;
transition: border-color 0.2s;
}
.attr-input:focus {
outline: none;
border-color: #2563eb;
}
.attr-input:disabled {
background-color: #252526;
color: #666;
cursor: not-allowed;
}
.attr-input::placeholder {
color: #555;
}
/* ========== 空状态垂直布局 ========== */
.empty-state-vertical {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #666;
font-size: 14px;
padding: 20px;
text-align: center;
}
.empty-state-vertical svg {
margin-bottom: 12px;
opacity: 0.5;
}
.empty-state-vertical .hint {
font-size: 12px;
color: #555;
margin-top: 8px;
}
/* ========== 步骤计数和已配置标记 ========== */
.step-count {
font-weight: normal;
color: #888;
margin-left: 6px;
}
.has-data-badge {
font-size: 11px;
color: #52c41a;
background: rgba(82, 196, 26, 0.15);
padding: 2px 8px;
border-radius: 10px;
}
.planner-categories .category-item.has-params {
border-left: 3px solid #52c41a;
}
.planner-categories .category-item.active.has-params {
border-left-color: rgba(255, 255, 255, 0.5);
}
.planner-categories .category-item.active .has-data-badge {
background: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.9);
}
/* params-content 内的边距 */
.params-content {
padding: 12px;
}

View File

@@ -0,0 +1,315 @@
import React, { useState, useEffect } from 'react';
import './TaskPlanner.css';
import { deviceParameters, DeviceCategory, AttributeGroup } from '../../data/deviceParameters';
import { ExtractedStep } from '../../services/api';
interface TaskPlannerProps {
extractedSteps?: ExtractedStep[];
}
const TaskPlanner: React.FC<TaskPlannerProps> = ({ extractedSteps = [] }) => {
// 任务标题
const [taskTitle, setTaskTitle] = useState('新建任务规划');
// 消息提示
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
// 当前选中的步骤
const [selectedStep, setSelectedStep] = useState<ExtractedStep | null>(null);
// 当前选中的设备分类
const [selectedDevice, setSelectedDevice] = useState<DeviceCategory | null>(
deviceParameters.length > 0 ? deviceParameters[0] : null
);
// 折叠状态
const [collapsedGroups, setCollapsedGroups] = useState<{ [key: string]: boolean }>({});
// 参数值状态步骤ID -> 设备类别 -> 参数名 -> 值)
const [paramValues, setParamValues] = useState<{
[stepId: number]: {
[deviceCategory: string]: {
[paramName: string]: string;
};
};
}>({});
// 过滤掉空值的参数对象
const filterEmptyParams = (
params: { [param: string]: string }
): { [param: string]: string } => {
const filtered: { [param: string]: string } = {};
for (const [key, value] of Object.entries(params)) {
if (value && typeof value === 'string' && value.trim().length > 0) {
filtered[key] = value;
}
}
return filtered;
};
// 深度合并两个设备参数对象(忽略空值和空对象)
const mergeDeviceParams = (
base: { [device: string]: { [param: string]: string } },
overlay: { [device: string]: { [param: string]: string } }
): { [device: string]: { [param: string]: string } } => {
const result = { ...base };
for (const deviceCategory of Object.keys(overlay)) {
const overlayParams = overlay[deviceCategory];
// 跳过空对象
if (!overlayParams || Object.keys(overlayParams).length === 0) {
continue;
}
// 过滤掉空值
const filteredOverlay = filterEmptyParams(overlayParams);
if (Object.keys(filteredOverlay).length === 0) {
continue;
}
// 合并非空参数
const baseParams = result[deviceCategory] || {};
result[deviceCategory] = {
...baseParams,
...filteredOverlay
};
}
return result;
};
// 当 extractedSteps 变化时,初始化参数值(带累积继承)
useEffect(() => {
if (extractedSteps.length > 0) {
// 初始化参数值,参数在步骤之间累积继承
const initialValues: typeof paramValues = {};
let accumulatedParams: { [device: string]: { [param: string]: string } } = {};
extractedSteps.forEach(step => {
// 当前步骤的参数 = 之前累积的参数 + 当前步骤新增/修改的参数
accumulatedParams = mergeDeviceParams(accumulatedParams, step.deviceParams);
// 深拷贝累积参数到当前步骤
initialValues[step.id] = JSON.parse(JSON.stringify(accumulatedParams));
});
setParamValues(initialValues);
// 选中第一个步骤
setSelectedStep(extractedSteps[0]);
} else {
setSelectedStep(null);
setParamValues({});
}
}, [extractedSteps]);
const showMessage = (type: 'success' | 'error', text: string) => {
setMessage({ type, text });
setTimeout(() => setMessage(null), 3000);
};
// 选择步骤
const handleStepSelect = (step: ExtractedStep) => {
setSelectedStep(step);
};
// 切换设备分类
const handleDeviceSelect = (device: DeviceCategory) => {
setSelectedDevice(device);
setCollapsedGroups({});
};
// 切换组折叠状态
const toggleGroup = (groupName: string) => {
setCollapsedGroups(prev => ({
...prev,
[groupName]: !prev[groupName]
}));
};
// 获取参数值
const getParamValue = (attrName: string): string => {
if (!selectedStep || !selectedDevice) return '';
const stepParams = paramValues[selectedStep.id];
if (!stepParams) return '';
const deviceParams = stepParams[selectedDevice.category];
if (!deviceParams) return '';
return deviceParams[attrName] || '';
};
// 更新参数值
const updateParamValue = (attrName: string, value: string) => {
if (!selectedStep || !selectedDevice) return;
setParamValues(prev => ({
...prev,
[selectedStep.id]: {
...prev[selectedStep.id],
[selectedDevice.category]: {
...(prev[selectedStep.id]?.[selectedDevice.category] || {}),
[attrName]: value
}
}
}));
};
// 检查当前步骤是否有该设备的非空参数
const hasDeviceParams = (deviceCategory: string): boolean => {
if (!selectedStep) return false;
const stepParams = paramValues[selectedStep.id];
if (!stepParams) return false;
const deviceParams = stepParams[deviceCategory];
if (!deviceParams || typeof deviceParams !== 'object') return false;
// 检查是否有任何非空值排除空字符串、null、undefined
const values = Object.values(deviceParams);
if (values.length === 0) return false;
return values.some(value => {
if (value === null || value === undefined) return false;
if (typeof value === 'string') return value.trim().length > 0;
return true;
});
};
return (
<div className="task-planner">
{/* 消息提示 */}
{message && (
<div className={`planner-message ${message.type}`}>
{message.text}
</div>
)}
{/* 标题行 */}
<div className="planner-title-row">
<input
type="text"
className="planner-title selectable"
value={taskTitle}
onChange={(e) => setTaskTitle(e.target.value)}
placeholder="请输入任务名称..."
/>
<button
className="complete-btn"
onClick={() => showMessage('success', '任务规划已完成')}
title="完成规划并提交到下一流程"
>
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
</button>
</div>
{/* 主体区域 - 三栏布局 */}
<div className="planner-body">
{/* 左侧:动态步骤列表 */}
<div className="planner-steps">
<div className="panel-header">
{extractedSteps.length > 0 && (
<span className="step-count">({extractedSteps.length})</span>
)}
</div>
<div className="steps-list">
{extractedSteps.length > 0 ? (
extractedSteps.map((step) => (
<div
key={step.id}
className={`step-item ${selectedStep?.id === step.id ? 'active' : ''}`}
onClick={() => handleStepSelect(step)}
>
<span className="step-number"> {step.id}</span>
<span className="step-name">{step.name}</span>
</div>
))
) : (
<div className="empty-state-vertical">
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2" />
<rect x="9" y="3" width="6" height="4" rx="1" />
<path d="M9 12h6M9 16h6" />
</svg>
<span></span>
<span className="hint"></span>
</div>
)}
</div>
</div>
{/* 中间:设备分类列表 */}
<div className="planner-categories">
<div className="panel-header">
{selectedStep ? `仪器配置` : '仪器配置'}
</div>
<div className="categories-list">
{deviceParameters.map((device) => (
<div
key={device.category}
className={`category-item ${selectedDevice?.category === device.category ? 'active' : ''} ${hasDeviceParams(device.category) ? 'has-params' : ''}`}
onClick={() => handleDeviceSelect(device)}
>
<span className="category-title">{device.category}</span>
{hasDeviceParams(device.category) && (
<span className="has-data-badge"></span>
)}
</div>
))}
</div>
</div>
{/* 右侧:参数详情 */}
<div className="planner-params">
{selectedDevice ? (
<>
<div className="panel-header">{selectedDevice.category}</div>
<div className="params-content">
{selectedDevice.groups.map((group) => {
const isCollapsed = collapsedGroups[group.name];
return (
<div key={group.name} className={`group-panel ${isCollapsed ? 'collapsed' : ''}`}>
<div
className="group-header"
onClick={() => toggleGroup(group.name)}
>
{group.name}
</div>
<div className="group-body">
{/* 表头 */}
<div className="attr-table-header">
<div className="col-name"></div>
<div className="col-desc"></div>
<div className="col-value"></div>
</div>
{/* 属性行 */}
{group.attributes.map((attr) => (
<div key={attr} className="attr-row">
<div className="col-name">{attr}</div>
<div className="col-desc">-</div>
<div className="col-value">
<input
type="text"
className="attr-input"
value={getParamValue(attr)}
onChange={(e) => updateParamValue(attr, e.target.value)}
placeholder="请输入..."
disabled={!selectedStep}
/>
</div>
</div>
))}
</div>
</div>
);
})}
</div>
</>
) : (
<>
<div className="panel-header"></div>
<div className="empty-state"></div>
</>
)}
</div>
</div>
</div>
);
};
export default TaskPlanner;

View File

@@ -0,0 +1,51 @@
.titlebar {
height: var(--titlebar-height);
background: var(--bg-darker);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--spacing-sm);
border-bottom: 1px solid var(--border-color);
}
.titlebar-left {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.logo {
display: flex;
align-items: center;
justify-content: center;
color: var(--primary-color);
}
.titlebar-title {
font-size: 12px;
color: var(--text-secondary);
}
.titlebar-controls {
display: flex;
align-items: center;
}
.control-btn {
width: 46px;
height: var(--titlebar-height);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: background-color 0.15s;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.control-btn.close-btn:hover {
background: var(--error-color);
color: white;
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import './TitleBar.css';
const TitleBar: React.FC = () => {
const handleMinimize = () => {
window.electronAPI?.minimize();
};
const handleMaximize = () => {
window.electronAPI?.maximize();
};
const handleClose = () => {
window.electronAPI?.close();
};
return (
<div className="titlebar draggable">
<div className="titlebar-left">
<div className="logo">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
<span className="titlebar-title"></span>
</div>
<div className="titlebar-controls no-drag">
<button className="control-btn" onClick={handleMinimize} title="最小化">
<svg viewBox="0 0 16 16" width="12" height="12">
<rect x="3" y="7" width="10" height="2" fill="currentColor" />
</svg>
</button>
<button className="control-btn" onClick={handleMaximize} title="最大化">
<svg viewBox="0 0 16 16" width="12" height="12">
<rect x="3" y="3" width="10" height="10" stroke="currentColor" strokeWidth="2" fill="none" />
</svg>
</button>
<button className="control-btn close-btn" onClick={handleClose} title="关闭">
<svg viewBox="0 0 16 16" width="12" height="12">
<path d="M3 3l10 10M13 3l-10 10" stroke="currentColor" strokeWidth="2" />
</svg>
</button>
</div>
</div>
);
};
export default TitleBar;

View File

@@ -0,0 +1,242 @@
.toolbar {
height: var(--toolbar-height);
background: var(--bg-sidebar);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--spacing-md);
border-bottom: 1px solid var(--border-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.toolbar-label {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
}
.toolbar-separator {
color: var(--border-color);
margin: 0 var(--spacing-xs);
}
.toolbar-action {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 12px;
color: var(--text-secondary);
border-radius: 4px;
transition: all 0.15s;
}
.toolbar-action:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
}
.toolbar-action.loading {
cursor: not-allowed;
opacity: 0.8;
}
.toolbar-action:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.toolbar-action.primary {
background: var(--primary-color);
color: white;
}
.toolbar-action.primary:hover {
background: var(--primary-hover);
}
.toolbar-action.ai {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.toolbar-action.ai:hover {
opacity: 0.9;
}
.toolbar-dropdown {
position: relative;
display: inline-flex;
}
.dropdown-caret {
margin-left: 4px;
font-size: 10px;
opacity: 0.8;
}
.toolbar-menu {
position: absolute;
top: calc(100% + 6px);
left: 0;
min-width: 120px;
background: var(--bg-darker);
border: 1px solid var(--border-color);
border-radius: 6px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.35);
z-index: 10;
padding: 4px;
}
.toolbar-menu-item {
width: 100%;
text-align: left;
padding: 6px 10px;
font-size: 12px;
color: var(--text-secondary);
border-radius: 4px;
}
.toolbar-menu-item:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--text-primary);
}
.toolbar-spinner {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.4);
border-top-color: rgba(255, 255, 255, 0.9);
animation: toolbar-spin 0.9s linear infinite;
}
@keyframes toolbar-spin {
to {
transform: rotate(360deg);
}
}
.toolbar-right {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.toolbar-status {
font-size: 12px;
color: var(--text-muted);
}
/* 知识库选择器 */
.toolbar-kb-selector {
position: relative;
}
.toolbar-kb-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-size: 12px;
color: var(--text-secondary);
background: var(--bg-darker);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
}
.toolbar-kb-btn:hover {
border-color: var(--primary-color);
color: var(--text-primary);
}
.toolbar-kb-btn svg {
opacity: 0.7;
}
.kb-name {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.toolbar-kb-menu {
position: absolute;
top: calc(100% + 6px);
right: 0;
min-width: 200px;
background: var(--bg-darker);
border: 1px solid var(--border-color);
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
z-index: 100;
overflow: hidden;
}
.kb-menu-header {
padding: 8px 12px;
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--border-color);
background: rgba(255, 255, 255, 0.02);
}
.toolbar-kb-item {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
padding: 10px 12px;
font-size: 12px;
color: var(--text-secondary);
text-align: left;
border: none;
background: transparent;
cursor: pointer;
transition: background 0.1s;
position: relative;
}
.toolbar-kb-item:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--text-primary);
}
.toolbar-kb-item.active {
background: rgba(24, 144, 255, 0.1);
color: var(--primary-color);
}
.kb-item-name {
font-weight: 500;
}
.kb-item-desc {
font-size: 11px;
color: var(--text-muted);
}
.toolbar-kb-item.active .kb-item-desc {
color: rgba(24, 144, 255, 0.7);
}
.kb-item-check {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--primary-color);
font-weight: bold;
}

View File

@@ -0,0 +1,257 @@
import React, { useEffect, useRef, useState } from 'react';
import { ModuleType } from '../../App';
import './Toolbar.css';
interface KnowledgeBase {
id: string;
name: string;
description?: string;
}
interface ToolbarProps {
activeModule: ModuleType;
onActionClick?: (action: string) => void;
intentActionStatus?: {
isSaving: boolean;
isGenerating: boolean;
isChecking: boolean;
};
selectedKnowledgeBase?: string;
onKnowledgeBaseChange?: (kbId: string) => void;
}
// 模块对应的快捷操作
const moduleActions: Record<ModuleType, { label: string; actions: string[] }> = {
intent: {
label: '意图编制',
actions: ['开始', 'AI 生成', 'AI 检查', '保存', '导出'],
},
task: {
label: '任务规划',
actions: ['新建任务', '分配资源', '查看进度', '导出报告'],
},
simulation: {
label: '逻辑仿真',
actions: ['新建仿真', '运行仿真', '查看结果', '参数配置'],
},
code: {
label: '代码生成',
actions: ['生成代码', '选择模板', '配置参数', '预览'],
},
data: {
label: '数据服务',
actions: ['数据查询', '数据导入', '数据导出', '数据统计'],
},
knowledge: {
label: '知识库',
actions: ['搜索知识', '添加知识', '知识分类', '知识图谱'],
},
};
// 模拟知识库列表(后续可从后端获取)
const knowledgeBases: KnowledgeBase[] = [
{ id: 'default', name: '默认知识库', description: '系统默认知识库' },
{ id: 'test-specs', name: '测试规范库', description: '测试标准和规范' },
{ id: 'device-params', name: '设备参数库', description: '仪器设备参数' },
{ id: 'historical', name: '历史案例库', description: '历史测试案例' },
];
const Toolbar: React.FC<ToolbarProps> = ({
activeModule,
onActionClick,
intentActionStatus,
selectedKnowledgeBase = 'default',
onKnowledgeBaseChange
}) => {
const currentModule = moduleActions[activeModule];
const [isStartMenuOpen, setIsStartMenuOpen] = useState(false);
const [isKbMenuOpen, setIsKbMenuOpen] = useState(false);
const startMenuRef = useRef<HTMLDivElement>(null);
const kbMenuRef = useRef<HTMLDivElement>(null);
const selectedKb = knowledgeBases.find(kb => kb.id === selectedKnowledgeBase) || knowledgeBases[0];
const isActionLoading = (action: string) => {
if (action === '保存') return intentActionStatus?.isSaving ?? false;
if (action === 'AI 生成') return intentActionStatus?.isGenerating ?? false;
if (action === 'AI 检查') return intentActionStatus?.isChecking ?? false;
return false;
};
const getActionLabel = (action: string) => {
if (action === '保存' && intentActionStatus?.isSaving) return '保存中...';
if (action === 'AI 生成' && intentActionStatus?.isGenerating) return 'AI 生成中...';
if (action === 'AI 检查' && intentActionStatus?.isChecking) return 'AI 检查中...';
return action;
};
const renderActionIcon = (action: string) => {
switch (action) {
case '新建意图':
case '开始':
case '新建任务':
case '新建仿真':
return (
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="12" y1="18" x2="12" y2="12" />
<line x1="9" y1="15" x2="15" y2="15" />
</svg>
);
case '保存':
return (
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
<polyline points="17 21 17 13 7 13 7 21" />
<polyline points="7 3 7 8 15 8" />
</svg>
);
case 'AI 生成':
return (
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 2L2 7l10 5 10-5-10-5z" />
<path d="M2 17l10 5 10-5" />
<path d="M2 12l10 5 10-5" />
</svg>
);
case 'AI 检查':
return (
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M9 11l3 3L22 4" />
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
</svg>
);
case '导出':
case '导出报告':
case '数据导出':
return (
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 3v12" />
<polyline points="8 11 12 15 16 11" />
<path d="M4 21h16" />
</svg>
);
default:
return null;
}
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (isStartMenuOpen && startMenuRef.current && !startMenuRef.current.contains(event.target as Node)) {
setIsStartMenuOpen(false);
}
if (isKbMenuOpen && kbMenuRef.current && !kbMenuRef.current.contains(event.target as Node)) {
setIsKbMenuOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isStartMenuOpen, isKbMenuOpen]);
const getActionClassName = (action: string) => {
if (action === '保存') {
return 'toolbar-action primary';
}
if (action === 'AI 生成' || action === 'AI 检查') {
return 'toolbar-action ai';
}
return 'toolbar-action';
};
return (
<div className="toolbar">
<div className="toolbar-left">
<span className="toolbar-label">{currentModule.label}</span>
<span className="toolbar-separator">|</span>
{currentModule.actions.map((action, index) => {
if (action === '开始') {
return (
<div key={index} className="toolbar-dropdown" ref={startMenuRef}>
<button
className="toolbar-action"
onClick={() => setIsStartMenuOpen((prev) => !prev)}
>
{renderActionIcon(action)}
<span className="dropdown-caret"></span>
</button>
{isStartMenuOpen && (
<div className="toolbar-menu">
<button
className="toolbar-menu-item"
onClick={() => {
setIsStartMenuOpen(false);
onActionClick?.('新建意图');
}}
>
</button>
<button
className="toolbar-menu-item"
onClick={() => {
setIsStartMenuOpen(false);
onActionClick?.('打开');
}}
>
</button>
</div>
)}
</div>
);
}
return (
<button
key={index}
className={`${getActionClassName(action)} ${isActionLoading(action) ? 'loading' : ''}`}
onClick={() => onActionClick?.(action)}
disabled={isActionLoading(action)}
>
{renderActionIcon(action)}
{isActionLoading(action) && <span className="toolbar-spinner" />}
{getActionLabel(action)}
</button>
);
})}
</div>
<div className="toolbar-right">
<div className="toolbar-kb-selector" ref={kbMenuRef}>
<button
className="toolbar-kb-btn"
onClick={() => setIsKbMenuOpen(prev => !prev)}
title={selectedKb.description}
>
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
</svg>
<span className="kb-name">{selectedKb.name}</span>
<span className="dropdown-caret"></span>
</button>
{isKbMenuOpen && (
<div className="toolbar-kb-menu">
<div className="kb-menu-header"></div>
{knowledgeBases.map(kb => (
<button
key={kb.id}
className={`toolbar-kb-item ${kb.id === selectedKnowledgeBase ? 'active' : ''}`}
onClick={() => {
onKnowledgeBaseChange?.(kb.id);
setIsKbMenuOpen(false);
}}
>
<span className="kb-item-name">{kb.name}</span>
{kb.description && <span className="kb-item-desc">{kb.description}</span>}
{kb.id === selectedKnowledgeBase && <span className="kb-item-check"></span>}
</button>
))}
</div>
)}
</div>
</div>
</div>
);
};
export default Toolbar;

View File

@@ -0,0 +1,358 @@
export interface AttributeGroup {
name: string;
attributes: string[];
}
export interface DeviceCategory {
category: string;
groups: AttributeGroup[];
}
export const deviceParameters: DeviceCategory[] = [
{
"category": "程控电源参数",
"groups": [
{
"name": "基本输出参数",
"attributes": [
"输出电压",
"输出电流",
"输出频率",
"输出相位"
]
},
{
"name": "保护功能参数",
"attributes": [
"过压保护OVP",
"过流保护OCP",
"过温保护 OTP"
]
},
{
"name": "工作模式参数",
"attributes": [
"恒压模式CV",
"恒流模式 CC",
"序列模式"
]
},
{
"name": "远程控制参数",
"attributes": [
"接口类型",
"编程指令"
]
},
{
"name": "三相电源特有参数",
"attributes": [
"接线模式",
"相序",
"单相控制"
]
}
]
},
{
"category": "功率探头参数",
"groups": [
{
"name": "测量基础参数",
"attributes": [
"频率范围",
"功率量程",
"测量单位"
]
},
{
"name": "校准与补偿参数",
"attributes": [
"归零校准",
"温度补偿",
"校准因子"
]
},
{
"name": "信号处理参数",
"attributes": [
"平均滤波器",
"平均次数",
"测量速度"
]
},
{
"name": "触发与测量模式",
"attributes": [
"触发源",
"测量模式"
]
},
{
"name": "物理与连接配置",
"attributes": [
"接口类型",
"探头类型"
]
}
]
},
{
"category": "频谱分析仪参数",
"groups": [
{
"name": "频率参数",
"attributes": [
"中心频率",
"扫宽Span",
"起始/终止频率"
]
},
{
"name": "电平参数",
"attributes": [
"参考电平",
"射频衰减器"
]
},
{
"name": "带宽参数",
"attributes": [
"分辨率带宽RBW",
"视频带宽VBW"
]
},
{
"name": "扫描与控制",
"attributes": [
"扫描时间",
"扫描模式",
"检波方式"
]
},
{
"name": "迹线与标记",
"attributes": [
"迹线模式",
"标记功能"
]
}
]
},
{
"category": "矢量网络分析仪参数",
"groups": [
{
"name": "频率参数",
"attributes": [
"扫描类型线性/对数/分段",
"起始终止频率或中心频率与扫宽",
"扫描点数"
]
},
{
"name": "激励与校频率参数功率参数",
"attributes": [
"输出功率电平",
"平均与平滑"
]
},
{
"name": "带宽与扫描参数",
"attributes": [
"中频带宽IFBW",
"扫描速度"
]
},
{
"name": "校准参数",
"attributes": [
"校准类型响应/全单端口/全双端口",
"校准件类型"
]
},
{
"name": "显示与数据参数",
"attributes": [
"测量参数S11/S21/S12/S22",
"显示格式LogMag/Smith/Phase等",
"标记Marker与限值线"
]
}
]
},
{
"category": "矢量信号分析仪参数",
"groups": [
{
"name": "频率参数",
"attributes": [
"中心频率",
"扫宽/频距"
]
},
{
"name": "幅度/电平参数",
"attributes": [
"参考电平",
"输入衰减",
"预放状态"
]
},
{
"name": "带宽与采样参数",
"attributes": [
"分辨率带宽RBW",
"测量带宽",
"采样点数"
]
},
{
"name": "解调与分析参数",
"attributes": [
"调制格式自动/手动",
"符号率",
"滤波器类型",
"分析类型EVM/星座图等"
]
},
{
"name": "触发与数据管理",
"attributes": [
"触发源与电平",
"数据记录与导出"
]
}
]
},
{
"category": "矢量信号源参数",
"groups": [
{
"name": "频率与电平",
"attributes": [
"载波频率",
"输出功率",
"ALC自动电平控制"
]
},
{
"name": "调制参数",
"attributes": [
"调制格式自动/手动",
"滤波器类型",
"符号率"
]
},
{
"name": "基带信号",
"attributes": [
"数据源",
"IQ调制器"
]
},
{
"name": "扫描与序列",
"attributes": [
"扫描模式",
"序列编辑"
]
},
{
"name": "系统与校准",
"attributes": [
"系统连接",
"校准"
]
}
]
},
{
"category": "示波器参数",
"groups": [
{
"name": "垂直系统",
"attributes": [
"垂直灵敏度Volts/Div",
"垂直位置",
"耦合方式AC/DC/GND"
]
},
{
"name": "水平系统",
"attributes": [
"水平时基Time/Div",
"水平位置",
"滚动模式"
]
},
{
"name": "触发系统",
"attributes": [
"触发源",
"触发电平",
"触发模式Auto/Normal/Single",
"触发类型达沿/脉宽/斜率"
]
},
{
"name": "采样与存储",
"attributes": [
"采样率",
"存储深度",
"采集模式"
]
},
{
"name": "探头与输入",
"attributes": [
"探头衰减比",
"输入阻抗"
]
}
]
},
{
"category": "信号源基础参数",
"groups": [
{
"name": "频率参数",
"attributes": [
"输出频率",
"频率偏移",
"相位"
]
},
{
"name": "电平/幅度参数",
"attributes": [
"输出功率/幅度",
"衰减器设置",
"ALC自动电平控制"
]
},
{
"name": "调制参数",
"attributes": [
"模拟调制AM/FM/PM",
"数字调制ASK/PSK/QAM",
"调制源内部/外部"
]
},
{
"name": "扫描与触发",
"attributes": [
"扫描模式",
"触发源"
]
},
{
"name": "波形与数字特性",
"attributes": [
"波形选择",
"码型与时钟"
]
}
]
}
];

View File

@@ -0,0 +1,385 @@
// 测试步骤数据结构定义
export interface StepParameter {
name: string; // 属性名
description: string; // 说明
value: string; // 属性值
}
export interface StepCategory {
title: string; // 二级标题(如:直流电源、信号源)
parameters: StepParameter[];
}
export interface TestStep {
id: number;
name: string; // 步骤名称
purpose: string; // 目的
categories: StepCategory[];
}
// 从意图编制提取的步骤(用于动态生成)
export interface ExtractedStep {
id: number;
name: string; // 步骤名称上电测试_电源初始化
purpose: string; // 步骤目的
// 按设备分类的参数key为设备类别如"程控电源参数"、"频谱分析仪参数"
deviceParams: {
[deviceCategory: string]: {
[paramName: string]: string; // 参数名 -> 参数值
};
};
}
// 提取步骤的API响应
export interface ExtractStepsResponse {
success: boolean;
steps: ExtractedStep[];
error?: string;
}
// 空的初始步骤列表
export const emptySteps: ExtractedStep[] = [];
// 示例测试步骤数据(基于用户提供的意图编制)
export const testSteps: TestStep[] = [
{
id: 1,
name: "上电测试_电源初始化",
purpose: "初始化直流电源并验证OCP/OVP保护逻辑",
categories: [
{
title: "直流电源",
parameters: [
{ name: "输出电压", description: "直流输出电压设置", value: "28V" },
{ name: "输出电流", description: "直流输出电流限制", value: "6A" },
{ name: "OCP阈值", description: "过流保护阈值", value: "6.1A" },
{ name: "OVP阈值", description: "过压保护阈值", value: "31V" },
],
},
{
title: "信号源",
parameters: [
{ name: "射频输出", description: "RF输出开关状态", value: "OFF" },
{ name: "远程控制", description: "远程控制模式", value: "ON" },
],
},
{
title: "频谱仪",
parameters: [
{ name: "远程控制", description: "远程控制模式", value: "ON" },
],
},
{
title: "开关矩阵",
parameters: [
{ name: "远程控制", description: "远程控制模式", value: "ON" },
{ name: "路径状态", description: "输出路径配置", value: "阻断所有路径" },
],
},
],
},
{
id: 2,
name: "镜频测试_SC频段_通道切换",
purpose: "验证SC频段信号通道的镜像抑制比",
categories: [
{
title: "信号源",
parameters: [
{ name: "RF Power", description: "射频功率输出", value: "-25dBm" },
{ name: "输出模式", description: "信号输出模式", value: "CW模式" },
{ name: "频率", description: "输出信号频率", value: "2.7GHz" },
],
},
{
title: "频谱仪",
parameters: [
{ name: "Span", description: "频率跨度", value: "500MHz" },
{ name: "中心频率", description: "频谱仪中心频率", value: "自动调整" },
{ name: "MAXH", description: "最大保持模式", value: "ON" },
],
},
{
title: "开关矩阵",
parameters: [
{ name: "路径选择", description: "信号通道选择", value: "SC边" },
],
},
],
},
{
id: 3,
name: "镜频测试_SC2.7GHz_P1测量",
purpose: "测量SC频段2.7GHz主频信号功率P1",
categories: [
{
title: "信号源",
parameters: [
{ name: "RF Power", description: "射频功率输出", value: "-25dBm" },
{ name: "频率", description: "输出信号频率", value: "2.7GHz" },
{ name: "RF输出", description: "射频输出开关", value: "ON" },
],
},
{
title: "频谱仪",
parameters: [
{ name: "中心频率", description: "频谱仪中心频率", value: "2.7GHz" },
{ name: "Span", description: "频率跨度", value: "500MHz" },
{ name: "MAXH", description: "最大保持模式", value: "ON" },
{ name: "Detector", description: "检测器类型", value: "Peak" },
{ name: "Sweeptime", description: "扫描时间", value: "1ms" },
],
},
{
title: "数据采集",
parameters: [
{ name: "读取项", description: "采集数据类型", value: "Max Peak功率(P1)" },
{ name: "计算逻辑", description: "数据处理公式", value: "P1=FreqSpan[0].PowerValue" },
],
},
],
},
{
id: 4,
name: "镜频测试_SC2.7GHz_P2测量",
purpose: "测量SC频段3.88GHz镜像频信号功率P2",
categories: [
{
title: "信号源",
parameters: [
{ name: "RF Power", description: "射频功率输出", value: "-25dBm" },
{ name: "频率", description: "输出信号频率(保持)", value: "2.7GHz" },
],
},
{
title: "频谱仪",
parameters: [
{ name: "中心频率", description: "频谱仪中心频率", value: "3.88GHz" },
{ name: "Span", description: "频率跨度", value: "500MHz" },
{ name: "MAXH", description: "最大保持模式", value: "ON" },
{ name: "Detector", description: "检测器类型", value: "Peak" },
{ name: "Sweeptime", description: "扫描时间", value: "1ms" },
],
},
{
title: "数据采集",
parameters: [
{ name: "读取项", description: "采集数据类型", value: "Max Peak功率(P2)" },
{ name: "计算逻辑", description: "数据处理公式", value: "P2=FreqSpan[1].PowerValue" },
{ name: "判定标准", description: "合格判定条件", value: "镜频抑制比(P1-P2) > 7dB" },
],
},
],
},
{
id: 5,
name: "镜频测试_SC6.2GHz_P1测量",
purpose: "测量SC频段6.2GHz主频信号功率P1",
categories: [
{
title: "信号源",
parameters: [
{ name: "RF Power", description: "射频功率输出", value: "-25dBm" },
{ name: "频率", description: "输出信号频率", value: "6.2GHz" },
],
},
{
title: "频谱仪",
parameters: [
{ name: "中心频率", description: "频谱仪中心频率", value: "6.2GHz" },
{ name: "Span", description: "频率跨度", value: "500MHz" },
{ name: "MAXH", description: "最大保持模式", value: "ON" },
],
},
{
title: "数据采集",
parameters: [
{ name: "读取项", description: "采集数据类型", value: "Max Peak功率(P1)" },
],
},
],
},
{
id: 6,
name: "镜频测试_SC6.2GHz_P2测量",
purpose: "测量SC频段5.88GHz镜像频信号功率P2",
categories: [
{
title: "频谱仪",
parameters: [
{ name: "中心频率", description: "频谱仪中心频率", value: "5.88GHz" },
{ name: "Span", description: "频率跨度", value: "500MHz" },
{ name: "MAXH", description: "最大保持模式", value: "ON" },
],
},
{
title: "数据采集",
parameters: [
{ name: "读取项", description: "采集数据类型", value: "Max Peak功率(P2)" },
{ name: "判断逻辑", description: "数据处理", value: "镜频抑制比=P1-P2" },
],
},
],
},
{
id: 7,
name: "镜频测试_X频段_通道切换",
purpose: "验证X频段信号通道的镜像抑制比",
categories: [
{
title: "信号源",
parameters: [
{ name: "RF Power", description: "射频功率输出", value: "-40dBm" },
],
},
{
title: "开关矩阵",
parameters: [
{ name: "路径选择", description: "信号通道选择", value: "X边" },
],
},
],
},
{
id: 8,
name: "镜频测试_X8GHz_P1测量",
purpose: "测量X频段8GHz主频信号功率P1",
categories: [
{
title: "信号源",
parameters: [
{ name: "RF Power", description: "射频功率输出", value: "-40dBm" },
{ name: "频率", description: "输出信号频率", value: "8GHz" },
],
},
{
title: "频谱仪",
parameters: [
{ name: "中心频率", description: "频谱仪中心频率", value: "8GHz" },
{ name: "Span", description: "频率跨度", value: "500MHz" },
{ name: "MAXH", description: "最大保持模式", value: "ON" },
],
},
{
title: "数据采集",
parameters: [
{ name: "读取项", description: "采集数据类型", value: "Max Peak功率(P1)" },
],
},
],
},
{
id: 9,
name: "镜频测试_X8GHz_P2测量",
purpose: "测量X频段11GHz镜像频信号功率P2",
categories: [
{
title: "信号源",
parameters: [
{ name: "RF Power", description: "射频功率输出", value: "-40dBm" },
{ name: "频率", description: "输出信号频率(保持)", value: "8GHz" },
],
},
{
title: "频谱仪",
parameters: [
{ name: "中心频率", description: "频谱仪中心频率", value: "11GHz" },
{ name: "Span", description: "频率跨度", value: "500MHz" },
{ name: "MAXH", description: "最大保持模式", value: "ON" },
],
},
{
title: "数据采集",
parameters: [
{ name: "读取项", description: "采集数据类型", value: "Max Peak功率(P2)" },
{ name: "判断逻辑", description: "数据处理", value: "镜频抑制比=P1-P2" },
],
},
],
},
{
id: 10,
name: "镜频测试_X12GHz_P1测量",
purpose: "测量X频段12GHz主频信号功率P1",
categories: [
{
title: "信号源",
parameters: [
{ name: "RF Power", description: "射频功率输出", value: "-40dBm" },
{ name: "频率", description: "输出信号频率", value: "12GHz" },
],
},
{
title: "频谱仪",
parameters: [
{ name: "中心频率", description: "频谱仪中心频率", value: "12GHz" },
{ name: "Span", description: "频率跨度", value: "500MHz" },
{ name: "MAXH", description: "最大保持模式", value: "ON" },
],
},
{
title: "数据采集",
parameters: [
{ name: "读取项", description: "采集数据类型", value: "Max Peak功率(P1)" },
],
},
],
},
{
id: 11,
name: "镜频测试_X12GHz_P2测量",
purpose: "测量X频段15GHz镜像频信号功率P2",
categories: [
{
title: "频谱仪",
parameters: [
{ name: "中心频率", description: "频谱仪中心频率", value: "15GHz" },
{ name: "Span", description: "频率跨度", value: "500MHz" },
{ name: "MAXH", description: "最大保持模式", value: "ON" },
],
},
{
title: "数据采集",
parameters: [
{ name: "读取项", description: "采集数据类型", value: "Max Peak功率(P2)" },
{ name: "判断逻辑", description: "数据处理", value: "镜频抑制比=P1-P2" },
],
},
],
},
{
id: 12,
name: "断电测试_仪器资源回收",
purpose: "断开所有设备供电并释放资源",
categories: [
{
title: "信号源",
parameters: [
{ name: "射频输出", description: "RF输出开关状态", value: "OFF" },
{ name: "远程控制", description: "远程控制模式", value: "OFF" },
],
},
{
title: "频谱仪",
parameters: [
{ name: "远程控制", description: "远程控制模式", value: "OFF" },
],
},
{
title: "开关矩阵",
parameters: [
{ name: "路径状态", description: "输出路径配置", value: "阻断所有路径" },
{ name: "远程控制", description: "远程控制模式", value: "OFF" },
],
},
{
title: "直流电源",
parameters: [
{ name: "输出状态", description: "电源输出开关", value: "关闭" },
{ name: "降压速率", description: "电压下降速度限制", value: "≤5V/s" },
],
},
],
},
];

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:*">
<title>柔性敏捷智能测试体系平台</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './styles/global.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,199 @@
/**
* API 服务层
* 与后端 FastAPI 通信
*/
const API_BASE = 'http://localhost:8080/api';
// 类型定义
export interface Intent {
id: number;
title: string;
content: string | null;
status: string;
created_at: string;
updated_at: string;
}
export interface IntentListResponse {
total: number;
items: Intent[];
}
export interface GenerateRequest {
prompt?: string;
test_type?: string;
test_target?: string;
additional_requirements?: string;
}
export interface GenerateResponse {
content: string;
success: boolean;
error?: string;
}
export interface CheckRequest {
content: string;
intent_id?: number;
requirements?: string[];
}
export interface CheckResult {
passed: boolean;
score: number;
issues: string[];
suggestions: string[];
}
export interface CheckResponse {
result: CheckResult;
record_id?: number;
success: boolean;
error?: string;
}
// 提取的步骤参数
export interface ExtractedStep {
id: number;
name: string;
purpose: string;
deviceParams: {
[deviceCategory: string]: {
[paramName: string]: string;
};
};
}
export interface ExtractStepsResponse {
success: boolean;
steps: ExtractedStep[];
error?: string;
}
export interface ParseFileResponse {
filename: string;
suffix: string;
title: string;
content: string;
raw_result?: unknown;
}
// API 方法
async function request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const response = await fetch(`${API_BASE}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
export const apiService = {
// 意图 CRUD
async listIntents(skip = 0, limit = 20): Promise<IntentListResponse> {
return request(`/intent?skip=${skip}&limit=${limit}`);
},
async getIntent(id: number): Promise<Intent> {
return request(`/intent/${id}`);
},
async createIntent(data: { title: string; content?: string }): Promise<Intent> {
return request('/intent', {
method: 'POST',
body: JSON.stringify(data),
});
},
async updateIntent(id: number, data: { title?: string; content?: string; status?: string }): Promise<Intent> {
return request(`/intent/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
},
async deleteIntent(id: number): Promise<void> {
await request(`/intent/${id}`, {
method: 'DELETE',
});
},
// AI 方法
async generateIntentContent(data: GenerateRequest): Promise<GenerateResponse> {
return request('/ai/generate-intent', {
method: 'POST',
body: JSON.stringify(data),
});
},
async generateTestPlan(requirementText: string, sourceName?: string): Promise<GenerateResponse> {
return request('/ai/generate-plan', {
method: 'POST',
body: JSON.stringify({
requirement_text: requirementText,
source_name: sourceName || '用户输入'
}),
});
},
async generateContent(prompt: string, context?: string): Promise<GenerateResponse> {
return request('/ai/generate', {
method: 'POST',
body: JSON.stringify({ prompt, context }),
});
},
async checkContent(data: CheckRequest): Promise<CheckResponse> {
return request('/ai/check', {
method: 'POST',
body: JSON.stringify(data),
});
},
// 从意图编制内容中提取测试步骤和参数
async extractSteps(content: string, title?: string): Promise<ExtractStepsResponse> {
return request('/ai/extract-steps', {
method: 'POST',
body: JSON.stringify({
content,
title: title || '意图编制'
}),
});
},
async parseIntentFile(file: File): Promise<ParseFileResponse> {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${API_BASE}/intent/parse-file`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
let detail = '';
try {
const data = await response.json();
detail = data?.detail ? `: ${data.detail}` : '';
} catch {
// ignore
}
throw new Error(`HTTP error! status: ${response.status}${detail}`);
}
return response.json();
},
};
export default apiService;

View File

@@ -0,0 +1,120 @@
/**
* 全局样式
* 柔性敏捷智能测试体系平台
*/
:root {
--primary-color: #1890ff;
--primary-hover: #40a9ff;
--bg-dark: #1e1e1e;
--bg-darker: #161616;
--bg-sidebar: #252526;
--bg-panel: #1e1e1e;
--text-primary: #ffffff;
--text-secondary: #cccccc;
--text-muted: #888888;
--border-color: #3c3c3c;
--border-focus: #007acc;
--success-color: #52c41a;
--warning-color: #faad14;
--error-color: #f5222d;
/* 间距 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* 尺寸 */
--titlebar-height: 32px;
--toolbar-height: 40px;
--sidebar-width: 60px;
--ai-panel-min-height: 150px;
--ai-panel-default-height: 250px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 13px;
color: var(--text-primary);
background-color: var(--bg-dark);
overflow: hidden;
user-select: none;
}
#root {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--text-muted);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
/* 文本可选择区域 */
.selectable {
user-select: text;
}
/* 按钮通用样式 */
button {
font-family: inherit;
font-size: inherit;
cursor: pointer;
border: none;
background: transparent;
color: inherit;
}
button:focus {
outline: none;
}
/* 输入框通用样式 */
input, textarea {
font-family: inherit;
font-size: inherit;
background: var(--bg-darker);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: var(--spacing-sm);
}
input:focus, textarea:focus {
outline: none;
border-color: var(--border-focus);
}
/* 拖拽区域 */
.draggable {
-webkit-app-region: drag;
}
.no-drag {
-webkit-app-region: no-drag;
}

View File

@@ -0,0 +1,14 @@
export interface IntentActionHandlers {
onNew?: () => void;
onOpen?: () => void;
onSave?: () => void;
onGenerate?: () => void;
onCheck?: () => void;
onExport?: () => void;
}
export interface IntentActionStatus {
isSaving: boolean;
isGenerating: boolean;
isChecking: boolean;
}

47
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,47 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": [
"src/renderer/*"
],
"@components/*": [
"src/renderer/components/*"
],
"@layouts/*": [
"src/renderer/layouts/*"
],
"@services/*": [
"src/renderer/services/*"
]
}
},
"include": [
"src/renderer/**/*",
"src/preload/**/*"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
}

24
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,24 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
base: './',
root: 'src/renderer',
build: {
outDir: '../../dist/renderer',
emptyOutDir: true,
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src/renderer'),
'@components': path.resolve(__dirname, 'src/renderer/components'),
'@layouts': path.resolve(__dirname, 'src/renderer/layouts'),
'@services': path.resolve(__dirname, 'src/renderer/services'),
},
},
server: {
port: 5173,
},
});