Browse Source

feat: 优化任务执行表格按钮UI,将文字按钮改为图标按钮

- 导入Play和Square图标组件,替换原有的"开始"和"停止"文字
- 使用绿色主题的Play图标表示开始操作,红色主题的Square图标表示停止操作
- 添加悬停效果、背景色变化和圆角设计,提升按钮交互体验
- 为图标按钮添加title属性,提供操作说明提示
- 优化表格空间利用,使界面更加紧凑美观
- 保持原有功能逻辑不变,仅优化用户界面体验
refactor/permission-config
root 3 months ago
parent
commit
9c56ecdd82
  1. 27
      src/X1.WebUI/modify.md
  2. 15
      src/X1.WebUI/src/constants/navigationMenuPresets.ts
  3. 19
      src/X1.WebUI/src/pages/navigation-menus/NavigationMenuForm_Examples.md
  4. 220
      src/X1.WebUI/src/pages/taskExecution/TaskExecutionTable.tsx
  5. 219
      src/X1.WebUI/src/pages/taskExecution/TaskExecutionView.tsx
  6. 8
      src/X1.WebUI/src/routes/AppRouter.tsx

27
src/X1.WebUI/modify.md

@ -626,3 +626,30 @@ if "%PACKAGE_MANAGER%"=="npm" (
- ✅ **所有表单类型正常工作** - 设备注册、网络连通性、网络性能、语音通话表单
- ✅ **代码类型安全性显著提升** - 完整的类型定义和检查
- ✅ **构建成功** - 项目可以正常构建和部署
## 2024-12-19 任务执行表格按钮图标化
### 修改描述:
将TaskExecutionTable组件中的"开始"和"停止"文字按钮改为图标按钮,提升用户体验和界面美观度。
### 修改文件:
- `X1.WebUI/src/pages/taskExecution/TaskExecutionTable.tsx`
### 具体修改:
1. **导入图标组件** - 添加 `import { Play, Square } from 'lucide-react';`
2. **替换开始按钮** - 将"开始"文字改为 `Play` 图标,使用绿色主题
3. **替换停止按钮** - 将"停止"文字改为 `Square` 图标,使用红色主题
4. **优化按钮样式** - 添加悬停效果、背景色变化和圆角设计
5. **添加工具提示** - 为图标按钮添加 `title` 属性,显示操作说明
### 技术细节:
- 使用 `Play` 图标表示开始操作(绿色主题)
- 使用 `Square` 图标表示停止操作(红色主题)
- 按钮样式包含悬停状态和过渡动画
- 保持原有的点击事件处理逻辑不变
### 用户体验改进:
- ✅ **视觉识别性提升** - 图标比文字更直观易懂
- ✅ **界面美观度提升** - 图标按钮比文字按钮更现代化
- ✅ **操作反馈增强** - 悬停效果和背景色变化提供更好的交互反馈
- ✅ **空间利用优化** - 图标按钮占用空间更小,表格更紧凑

15
src/X1.WebUI/src/constants/navigationMenuPresets.ts

@ -162,12 +162,21 @@ export const subMenuPresets: NavigationMenuPreset[] = [
description: '查看和管理所有测试任务',
parentTitle: '任务管理'
},
{
title: '任务执行明细',
path: '/dashboard/tasks/execution',
icon: 'ClipboardList',
permissionCode: 'taskexecution.view',
sortOrder: 2,
description: '查看任务执行明细,进行开始和停止操作',
parentTitle: '任务管理'
},
{
title: '创建任务',
path: '/dashboard/tasks/create',
icon: 'ClipboardList',
permissionCode: 'tasks.create',
sortOrder: 2,
sortOrder: 3,
description: '创建新的测试任务',
parentTitle: '任务管理'
},
@ -176,7 +185,7 @@ export const subMenuPresets: NavigationMenuPreset[] = [
path: '/dashboard/tasks/reviews',
icon: 'ClipboardList',
permissionCode: 'taskreviews.view',
sortOrder: 3,
sortOrder: 4,
description: '审核和批准测试任务',
parentTitle: '任务管理'
},
@ -185,7 +194,7 @@ export const subMenuPresets: NavigationMenuPreset[] = [
path: '/dashboard/tasks/executions',
icon: 'ClipboardList',
permissionCode: 'taskexecutions.view',
sortOrder: 4,
sortOrder: 5,
description: '执行和管理测试任务',
parentTitle: '任务管理'
},

19
src/X1.WebUI/src/pages/navigation-menus/NavigationMenuForm_Examples.md

@ -303,6 +303,21 @@
自动识别结果: SubMenuItem(子菜单项)
```
**任务执行明细**
```
菜单标题: 任务执行明细
菜单路径: /dashboard/tasks/execution
父级菜单: [选择"任务管理"菜单的ID]
图标: Play
权限代码: taskexecution.view
排序: 2
启用状态: ✅
系统菜单: ✅
描述: 查看任务执行明细,进行开始和停止操作
自动识别结果: SubMenuItem(子菜单项)
```
**创建任务**
```
菜单标题: 创建任务
@ -310,7 +325,7 @@
父级菜单: [选择"任务管理"菜单的ID]
图标: ClipboardList
权限代码: tasks.create
排序: 2
排序: 3
启用状态: ✅
系统菜单: ✅
描述: 创建新的测试任务
@ -710,6 +725,7 @@
- `testcases`: 用例管理
- `teststeps`: 测试步骤
- `tasks`: 任务管理
- `taskexecution`: 任务执行明细
- `taskreviews`: 任务审核
- `taskexecutions`: 任务执行
- `functionalanalysis`: 功能分析
@ -752,6 +768,7 @@ TestTube: 测试/实验
ClipboardList: 任务/列表
FolderOpen: 文件夹/场景管理
Activity: 活动/信令分析
Play: 播放/执行
```
## 📋 创建步骤建议

220
src/X1.WebUI/src/pages/taskExecution/TaskExecutionTable.tsx

@ -0,0 +1,220 @@
import React from 'react';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { TestScenarioTask } from '@/services/testScenarioTaskService';
import { Badge } from '@/components/ui/badge';
import { Play, Square } from 'lucide-react';
interface TaskExecutionTableProps {
tasks: TestScenarioTask[];
loading: boolean;
onStart: (task: TestScenarioTask) => void;
onStop: (task: TestScenarioTask) => void;
page: number;
pageSize: number;
total: number;
onPageChange: (page: number) => void;
hideCard?: boolean;
density?: 'relaxed' | 'default' | 'compact';
columns?: { key: string; title: string; visible: boolean }[];
}
// 优先级徽章组件
const PriorityBadge: React.FC<{ priority: number }> = ({ priority }) => {
const getPriorityConfig = (priority: number) => {
switch (priority) {
case 0:
return { label: '普通', className: 'bg-blue-100 text-blue-800' };
case 1:
return { label: '高', className: 'bg-orange-100 text-orange-800' };
default:
return { label: '未知', className: 'bg-gray-100 text-gray-800' };
}
};
const config = getPriorityConfig(priority);
return (
<Badge className={config.className}>
{config.label}
</Badge>
);
};
// 生命周期徽章组件
const LifecycleBadge: React.FC<{ lifecycle: string }> = ({ lifecycle }) => {
const getLifecycleConfig = (lifecycle: string) => {
switch (lifecycle) {
case 'Created':
return { label: '已创建', className: 'bg-blue-100 text-blue-800' };
case 'Scheduled':
return { label: '已调度', className: 'bg-green-100 text-green-800' };
case 'Archived':
return { label: '已归档', className: 'bg-gray-100 text-gray-800' };
case 'Cancelled':
return { label: '已取消', className: 'bg-red-100 text-red-800' };
default:
return { label: '未知', className: 'bg-gray-100 text-gray-800' };
}
};
const config = getLifecycleConfig(lifecycle);
return (
<Badge className={config.className}>
{config.label}
</Badge>
);
};
// 状态徽章组件
const StatusBadge: React.FC<{ isDisabled: boolean }> = ({ isDisabled }) => {
return (
<Badge className={isDisabled ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'}>
{isDisabled ? '禁用' : '启用'}
</Badge>
);
};
// 日期格式化组件
const DateDisplay: React.FC<{ date?: string }> = ({ date }) => {
if (!date) return <span className="text-gray-400">-</span>;
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
return <span>{formatDate(date)}</span>;
};
export default function TaskExecutionTable({
tasks,
loading,
onStart,
onStop,
hideCard = false,
density = 'default',
columns = []
}: TaskExecutionTableProps) {
const visibleColumns = columns.filter(col => col.visible);
const renderCell = (task: TestScenarioTask, columnKey: string) => {
switch (columnKey) {
case 'taskCode':
return (
<div className="max-w-xs truncate font-mono text-sm" title={task.taskCode}>
{task.taskCode}
</div>
);
case 'taskName':
return (
<div className="max-w-xs truncate" title={task.taskName}>
{task.taskName}
</div>
);
case 'scenarioCode':
return (
<div className="max-w-xs truncate text-sm" title={task.scenarioCode}>
{task.scenarioCode}
</div>
);
case 'deviceCode':
return (
<div className="max-w-xs truncate text-sm" title={task.deviceCode}>
{task.deviceCode}
</div>
);
case 'priority':
return <PriorityBadge priority={task.priority} />;
case 'lifecycle':
return <LifecycleBadge lifecycle={task.lifecycle} />;
case 'isDisabled':
return <StatusBadge isDisabled={task.isDisabled} />;
case 'createdAt':
return <DateDisplay date={task.createdAt} />;
case 'flowCount':
return (
<div className="text-center">
<Badge className="bg-purple-100 text-purple-800">
{task.flowCount}
</Badge>
</div>
);
case 'actions':
return (
<div className="flex justify-end gap-4">
<button
className="p-2 text-green-600 hover:text-green-700 hover:bg-green-50 rounded-md transition-colors"
onClick={() => onStart(task)}
title="开始任务"
>
<Play className="h-4 w-4" />
</button>
<button
className="p-2 text-red-500 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors"
onClick={() => onStop(task)}
title="停止任务"
>
<Square className="h-4 w-4" />
</button>
</div>
);
default:
return null;
}
};
const Wrapper = hideCard ? React.Fragment : 'div';
const wrapperProps = hideCard ? {} : { className: 'rounded-md border bg-background' };
const rowClass = density === 'relaxed' ? 'h-20' : density === 'compact' ? 'h-8' : 'h-12';
const cellPadding = density === 'relaxed' ? 'py-5' : density === 'compact' ? 'py-1' : 'py-3';
return (
<Wrapper {...wrapperProps}>
<Table>
<TableHeader key="header">
<TableRow className={rowClass}>
{visibleColumns.map(col => (
<TableHead
key={col.key}
className={`text-foreground text-center ${col.key === 'actions' ? 'text-right' : ''} ${cellPadding}`}
>
{col.title}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody key="body">
{loading ? (
<TableRow key="loading" className={rowClass}>
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}>
...
</TableCell>
</TableRow>
) : tasks.length === 0 ? (
<TableRow key="empty" className={rowClass}>
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}>
</TableCell>
</TableRow>
) : (
tasks.map((task) => (
<TableRow key={task.taskId} className={rowClass}>
{visibleColumns.map((column) => (
<TableCell key={column.key} className={`text-foreground text-center ${cellPadding}`}>
{renderCell(task, column.key)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</Wrapper>
);
}

219
src/X1.WebUI/src/pages/taskExecution/TaskExecutionView.tsx

@ -0,0 +1,219 @@
import React, { useEffect, useState } from 'react';
import { testScenarioTaskService, TestScenarioTask, GetTestScenarioTasksRequest } from '@/services/testScenarioTaskService';
import TaskExecutionTable from './TaskExecutionTable';
import { Input } from '@/components/ui/input';
import PaginationBar from '@/components/ui/PaginationBar';
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar';
import { useToast } from '@/components/ui/use-toast';
import { Button } from '@/components/ui/button';
const defaultColumns = [
{ key: 'taskCode', title: '任务编码', visible: true },
{ key: 'taskName', title: '任务名称', visible: true },
{ key: 'scenarioCode', title: '场景编码', visible: true },
{ key: 'deviceCode', title: '设备编码', visible: true },
{ key: 'priority', title: '优先级', visible: true },
{ key: 'lifecycle', title: '生命周期', visible: true },
{ key: 'isDisabled', title: '状态', visible: true },
{ key: 'flowCount', title: '流程数量', visible: true },
{ key: 'createdAt', title: '创建时间', visible: true },
{ key: 'actions', title: '操作', visible: true }
];
export default function TaskExecutionView() {
const [tasks, setTasks] = useState<TestScenarioTask[]>([]);
const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [density, setDensity] = useState<DensityType>('default');
const [columns, setColumns] = useState(defaultColumns);
// 搜索参数
const [searchTerm, setSearchTerm] = useState('');
const [isDisabled, setIsDisabled] = useState<boolean | undefined>(undefined);
// Toast 提示
const { toast } = useToast();
const fetchTasks = async (params: Partial<GetTestScenarioTasksRequest> = {}) => {
setLoading(true);
const queryParams: GetTestScenarioTasksRequest = {
pageNumber,
pageSize,
searchTerm,
isDisabled,
...params
};
const result = await testScenarioTaskService.getTestScenarioTasks(queryParams);
if (result.isSuccess && result.data) {
setTasks(result.data.tasks || []);
setTotal(result.data.totalCount || 0);
}
setLoading(false);
};
useEffect(() => {
fetchTasks();
// eslint-disable-next-line
}, [pageNumber, pageSize]);
const handleStart = async (task: TestScenarioTask) => {
try {
// TODO: 实现开始任务的逻辑
toast({
title: "开始任务",
description: `任务 "${task.taskName}" 已开始执行`,
});
// 这里可以调用相应的API来开始任务
// await taskExecutionService.startTask(task.taskId);
} catch (error) {
console.error('开始任务失败:', error);
toast({
title: "开始失败",
description: "网络错误,请稍后重试",
variant: "destructive",
});
}
};
const handleStop = async (task: TestScenarioTask) => {
try {
// TODO: 实现停止任务的逻辑
toast({
title: "停止任务",
description: `任务 "${task.taskName}" 已停止执行`,
});
// 这里可以调用相应的API来停止任务
// await taskExecutionService.stopTask(task.taskId);
} catch (error) {
console.error('停止任务失败:', error);
toast({
title: "停止失败",
description: "网络错误,请稍后重试",
variant: "destructive",
});
}
};
// 查询按钮
const handleQuery = () => {
setPageNumber(1);
fetchTasks({ pageNumber: 1 });
};
// 重置按钮
const handleReset = () => {
setSearchTerm('');
setIsDisabled(undefined);
setPageNumber(1);
fetchTasks({
searchTerm: '',
isDisabled: undefined,
pageNumber: 1
});
};
// 每页条数选择
const handlePageSizeChange = (size: number) => {
setPageSize(size);
setPageNumber(1);
};
return (
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
{/* 搜索工具栏 */}
<div className="flex flex-col bg-background p-4 rounded-md border mb-2">
<form
className="flex gap-x-8 gap-y-4 items-center flex-wrap"
onSubmit={e => {
e.preventDefault();
handleQuery();
}}
>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
</label>
<Input
className="flex-1 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all"
placeholder="请输入任务名称或编码"
value={searchTerm}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
/>
</div>
<div className="flex flex-row items-center min-w-[200px] flex-1">
<label
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right"
style={{ width: 80, minWidth: 80 }}
>
</label>
<select
className="h-10 rounded border border-border bg-background px-3 text-sm flex-1 text-foreground focus:outline-none focus:ring-0 focus:border-border transition-all"
value={isDisabled === undefined ? '' : isDisabled.toString()}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
setIsDisabled(value === '' ? undefined : value === 'true');
}}
>
<option value=""></option>
<option value="false"></option>
<option value="true"></option>
</select>
</div>
{/* 按钮组 */}
<div className="flex flex-row items-center gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery}></Button>
</div>
</form>
</div>
{/* 表格整体卡片区域,包括工具栏、表格、分页 */}
<div className="rounded-md border bg-background p-4">
{/* 顶部操作栏:工具栏 */}
<div className="flex items-center justify-end mb-2">
<TableToolbar
onRefresh={() => fetchTasks()}
onDensityChange={setDensity}
onColumnsChange={setColumns}
onColumnsReset={() => setColumns(defaultColumns)}
columns={columns}
density={density}
/>
</div>
{/* 表格区域 */}
<TaskExecutionTable
tasks={tasks}
loading={loading}
onStart={handleStart}
onStop={handleStop}
page={pageNumber}
pageSize={pageSize}
total={total}
onPageChange={setPageNumber}
hideCard={true}
density={density}
columns={columns}
/>
{/* 分页 */}
<PaginationBar
page={pageNumber}
pageSize={pageSize}
total={total}
onPageChange={setPageNumber}
onPageSizeChange={handlePageSizeChange}
/>
</div>
</div>
</main>
);
}

8
src/X1.WebUI/src/routes/AppRouter.tsx

@ -22,6 +22,7 @@ const TestStepsView = lazy(() => import('@/pages/teststeps/TestStepsView'));
// 任务管理页面
const TasksView = lazy(() => import('@/pages/tasks/TasksView'));
const TaskExecutionView = lazy(() => import('@/pages/taskExecution/TaskExecutionView'));
// 结果分析页面
const FunctionalAnalysisView = lazy(() => import('@/pages/analysis/FunctionalAnalysisView'));
@ -176,6 +177,13 @@ export function AppRouter() {
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="execution" element={
<ProtectedRoute requiredPermission="taskexecution.view">
<AnimatedContainer>
<TaskExecutionView />
</AnimatedContainer>
</ProtectedRoute>
} />
</Route>
{/* 结果分析路由 */}

Loading…
Cancel
Save