Browse Source
- 导入Play和Square图标组件,替换原有的"开始"和"停止"文字 - 使用绿色主题的Play图标表示开始操作,红色主题的Square图标表示停止操作 - 添加悬停效果、背景色变化和圆角设计,提升按钮交互体验 - 为图标按钮添加title属性,提供操作说明提示 - 优化表格空间利用,使界面更加紧凑美观 - 保持原有功能逻辑不变,仅优化用户界面体验refactor/permission-config
6 changed files with 504 additions and 4 deletions
@ -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> |
||||
|
); |
||||
|
} |
||||
@ -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> |
||||
|
); |
||||
|
} |
||||
Loading…
Reference in new issue