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