10 changed files with 873 additions and 6 deletions
@ -0,0 +1,135 @@ |
|||||
|
import React from 'react'; |
||||
|
import { Button } from '@/components/ui/button'; |
||||
|
import { Input } from '@/components/ui/input'; |
||||
|
import { Label } from '@/components/ui/label'; |
||||
|
import { Textarea } from '@/components/ui/textarea'; |
||||
|
import { Checkbox } from '@/components/ui/checkbox'; |
||||
|
import { CreateTestCaseSetRequest, UpdateTestCaseSetRequest, TestCaseSet } from '@/services/testCaseSetService'; |
||||
|
|
||||
|
interface TestCaseSetFormProps { |
||||
|
onSubmit: (data: CreateTestCaseSetRequest | UpdateTestCaseSetRequest) => void; |
||||
|
initialData?: Partial<TestCaseSet>; |
||||
|
isEdit?: boolean; |
||||
|
isSubmitting?: boolean; |
||||
|
} |
||||
|
|
||||
|
export default function TestCaseSetForm({ onSubmit, initialData, isEdit = false, isSubmitting = false }: TestCaseSetFormProps) { |
||||
|
const [formData, setFormData] = React.useState<CreateTestCaseSetRequest>({ |
||||
|
code: initialData?.code || '', |
||||
|
name: initialData?.name || '', |
||||
|
version: initialData?.version || '', |
||||
|
description: initialData?.description || '', |
||||
|
versionUpdateInfo: initialData?.versionUpdateInfo || '', |
||||
|
remarks: initialData?.remarks || '', |
||||
|
isDisabled: initialData?.isDisabled ?? false |
||||
|
}); |
||||
|
|
||||
|
const handleSubmit = (e: React.FormEvent) => { |
||||
|
e.preventDefault(); |
||||
|
if (isSubmitting) return; // 防止重复提交
|
||||
|
|
||||
|
if (isEdit && initialData?.testCaseSetId) { |
||||
|
// 编辑模式,需要包含testCaseSetId
|
||||
|
const updateData: UpdateTestCaseSetRequest = { |
||||
|
...formData, |
||||
|
testCaseSetId: initialData.testCaseSetId |
||||
|
}; |
||||
|
onSubmit(updateData); |
||||
|
} else { |
||||
|
// 创建模式
|
||||
|
onSubmit(formData); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<form onSubmit={handleSubmit} className="space-y-4"> |
||||
|
<div className="space-y-2"> |
||||
|
<Label htmlFor="code">用例集编码</Label> |
||||
|
<Input |
||||
|
id="code" |
||||
|
value={formData.code} |
||||
|
onChange={e => setFormData({ ...formData, code: e.target.value })} |
||||
|
placeholder="请输入用例集编码" |
||||
|
required |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-2"> |
||||
|
<Label htmlFor="name">用例集名称</Label> |
||||
|
<Input |
||||
|
id="name" |
||||
|
value={formData.name} |
||||
|
onChange={e => setFormData({ ...formData, name: e.target.value })} |
||||
|
placeholder="请输入用例集名称" |
||||
|
required |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-2"> |
||||
|
<Label htmlFor="version">版本</Label> |
||||
|
<Input |
||||
|
id="version" |
||||
|
value={formData.version} |
||||
|
onChange={e => setFormData({ ...formData, version: e.target.value })} |
||||
|
placeholder="请输入版本号" |
||||
|
required |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-2"> |
||||
|
<Label htmlFor="description">描述</Label> |
||||
|
<Textarea |
||||
|
id="description" |
||||
|
value={formData.description} |
||||
|
onChange={e => setFormData({ ...formData, description: e.target.value })} |
||||
|
placeholder="请输入用例集描述" |
||||
|
rows={3} |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-2"> |
||||
|
<Label htmlFor="versionUpdateInfo">版本更新信息</Label> |
||||
|
<Textarea |
||||
|
id="versionUpdateInfo" |
||||
|
value={formData.versionUpdateInfo} |
||||
|
onChange={e => setFormData({ ...formData, versionUpdateInfo: e.target.value })} |
||||
|
placeholder="请输入版本更新信息" |
||||
|
rows={2} |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="space-y-2"> |
||||
|
<Label htmlFor="remarks">备注</Label> |
||||
|
<Textarea |
||||
|
id="remarks" |
||||
|
value={formData.remarks} |
||||
|
onChange={e => setFormData({ ...formData, remarks: e.target.value })} |
||||
|
placeholder="请输入备注信息" |
||||
|
rows={2} |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<div className="flex items-center space-x-2"> |
||||
|
<Checkbox |
||||
|
id="isDisabled" |
||||
|
checked={formData.isDisabled} |
||||
|
onCheckedChange={(checked) => |
||||
|
setFormData({ ...formData, isDisabled: checked as boolean }) |
||||
|
} |
||||
|
disabled={isSubmitting} |
||||
|
/> |
||||
|
<Label htmlFor="isDisabled">禁用用例集</Label> |
||||
|
</div> |
||||
|
|
||||
|
<Button type="submit" className="w-full" disabled={isSubmitting}> |
||||
|
{isSubmitting ? '提交中...' : (isEdit ? '更新用例集' : '创建用例集')} |
||||
|
</Button> |
||||
|
</form> |
||||
|
); |
||||
|
} |
@ -0,0 +1,187 @@ |
|||||
|
import React from 'react'; |
||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; |
||||
|
import { TestCaseSet } from '@/services/testCaseSetService'; |
||||
|
import { Badge } from '@/components/ui/badge'; |
||||
|
|
||||
|
interface TestCaseSetTableProps { |
||||
|
testCaseSets: TestCaseSet[]; |
||||
|
loading: boolean; |
||||
|
onEdit: (testCaseSet: TestCaseSet) => void; |
||||
|
onDelete: (testCaseSet: TestCaseSet) => 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 StatusBadge: React.FC<{ isDisabled: boolean }> = ({ isDisabled }) => { |
||||
|
return ( |
||||
|
<Badge className={!isDisabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-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 TestCaseSetTable({ |
||||
|
testCaseSets, |
||||
|
loading, |
||||
|
onEdit, |
||||
|
onDelete, |
||||
|
page, |
||||
|
pageSize, |
||||
|
total, |
||||
|
onPageChange, |
||||
|
hideCard = false, |
||||
|
density = 'default', |
||||
|
columns = [] |
||||
|
}: TestCaseSetTableProps) { |
||||
|
const densityClasses = { |
||||
|
relaxed: 'py-3', |
||||
|
default: 'py-2', |
||||
|
compact: 'py-1', |
||||
|
}; |
||||
|
|
||||
|
const visibleColumns = columns.filter(col => col.visible); |
||||
|
|
||||
|
const renderCell = (testCaseSet: TestCaseSet, columnKey: string) => { |
||||
|
switch (columnKey) { |
||||
|
case 'code': |
||||
|
return ( |
||||
|
<div className="max-w-xs truncate font-mono text-sm" title={testCaseSet.code}> |
||||
|
{testCaseSet.code} |
||||
|
</div> |
||||
|
); |
||||
|
case 'name': |
||||
|
return ( |
||||
|
<div className="max-w-xs truncate" title={testCaseSet.name}> |
||||
|
{testCaseSet.name} |
||||
|
</div> |
||||
|
); |
||||
|
case 'version': |
||||
|
return ( |
||||
|
<div className="max-w-xs truncate font-mono text-sm" title={testCaseSet.version}> |
||||
|
{testCaseSet.version} |
||||
|
</div> |
||||
|
); |
||||
|
case 'description': |
||||
|
return ( |
||||
|
<div className="max-w-xs truncate text-gray-600" title={testCaseSet.description || ''}> |
||||
|
{testCaseSet.description || '-'} |
||||
|
</div> |
||||
|
); |
||||
|
case 'versionUpdateInfo': |
||||
|
return ( |
||||
|
<div className="max-w-xs truncate text-gray-600" title={testCaseSet.versionUpdateInfo || ''}> |
||||
|
{testCaseSet.versionUpdateInfo || '-'} |
||||
|
</div> |
||||
|
); |
||||
|
case 'remarks': |
||||
|
return ( |
||||
|
<div className="max-w-xs truncate text-gray-600" title={testCaseSet.remarks || ''}> |
||||
|
{testCaseSet.remarks || '-'} |
||||
|
</div> |
||||
|
); |
||||
|
case 'isDisabled': |
||||
|
return <StatusBadge isDisabled={testCaseSet.isDisabled} />; |
||||
|
case 'createdAt': |
||||
|
return <DateDisplay date={testCaseSet.createdAt} />; |
||||
|
case 'createdBy': |
||||
|
return ( |
||||
|
<span className="text-sm"> |
||||
|
{testCaseSet.createdBy || '-'} |
||||
|
</span> |
||||
|
); |
||||
|
case 'actions': |
||||
|
return ( |
||||
|
<div className="flex justify-end gap-4"> |
||||
|
<span |
||||
|
className="cursor-pointer text-blue-600 hover:underline select-none" |
||||
|
onClick={() => onEdit(testCaseSet)} |
||||
|
> |
||||
|
修改 |
||||
|
</span> |
||||
|
<span |
||||
|
className="cursor-pointer text-red-500 hover:underline select-none" |
||||
|
onClick={() => onDelete(testCaseSet)} |
||||
|
> |
||||
|
删除 |
||||
|
</span> |
||||
|
</div> |
||||
|
); |
||||
|
default: |
||||
|
return null; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const totalPages = Math.ceil(total / pageSize); |
||||
|
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> |
||||
|
) : testCaseSets.length === 0 ? ( |
||||
|
<TableRow key="empty" className={rowClass}> |
||||
|
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}> |
||||
|
暂无数据 |
||||
|
</TableCell> |
||||
|
</TableRow> |
||||
|
) : ( |
||||
|
testCaseSets.map((testCaseSet) => ( |
||||
|
<TableRow key={testCaseSet.testCaseSetId} className={rowClass}> |
||||
|
{visibleColumns.map((column) => ( |
||||
|
<TableCell key={column.key} className={`text-foreground text-center ${cellPadding}`}> |
||||
|
{renderCell(testCaseSet, column.key)} |
||||
|
</TableCell> |
||||
|
))} |
||||
|
</TableRow> |
||||
|
)) |
||||
|
)} |
||||
|
</TableBody> |
||||
|
</Table> |
||||
|
</Wrapper> |
||||
|
); |
||||
|
} |
@ -0,0 +1,367 @@ |
|||||
|
import React, { useEffect, useState } from 'react'; |
||||
|
import { testCaseSetService, TestCaseSet, GetTestCaseSetsRequest, CreateTestCaseSetRequest, UpdateTestCaseSetRequest } from '@/services/testCaseSetService'; |
||||
|
import TestCaseSetTable from './TestCaseSetTable'; |
||||
|
import TestCaseSetForm from './TestCaseSetForm'; |
||||
|
import { Input } from '@/components/ui/input'; |
||||
|
import PaginationBar from '@/components/ui/PaginationBar'; |
||||
|
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar'; |
||||
|
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'; |
||||
|
import { Button } from '@/components/ui/button'; |
||||
|
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; |
||||
|
import { useToast } from '@/components/ui/use-toast'; |
||||
|
|
||||
|
const defaultColumns = [ |
||||
|
{ key: 'code', title: '用例集编码', visible: true }, |
||||
|
{ key: 'name', title: '用例集名称', visible: true }, |
||||
|
{ key: 'version', title: '版本', visible: true }, |
||||
|
{ key: 'description', title: '描述', visible: true }, |
||||
|
{ key: 'versionUpdateInfo', title: '版本更新信息', visible: true }, |
||||
|
{ key: 'remarks', title: '备注', visible: true }, |
||||
|
{ key: 'isDisabled', title: '状态', visible: true }, |
||||
|
{ key: 'createdAt', title: '创建时间', visible: true }, |
||||
|
{ key: 'createdBy', title: '创建人', visible: true }, |
||||
|
{ key: 'actions', title: '操作', visible: true } |
||||
|
]; |
||||
|
|
||||
|
// 字段类型声明
|
||||
|
type SearchField = |
||||
|
| { key: string; label: string; type: 'input'; placeholder: string } |
||||
|
| { key: string; label: string; type: 'select'; options: { value: string; label: string }[] }; |
||||
|
|
||||
|
// 第一行字段(收起时只显示这3个)
|
||||
|
const firstRowFields: SearchField[] = [ |
||||
|
{ key: 'searchTerm', label: '搜索关键词', type: 'input', placeholder: '请输入用例集编码或用例集名称' }, |
||||
|
{ key: 'isEnabled', label: '状态', type: 'select', options: [ |
||||
|
{ value: '', label: '请选择' }, |
||||
|
{ value: 'true', label: '启用' }, |
||||
|
{ value: 'false', label: '禁用' }, |
||||
|
] }, |
||||
|
]; |
||||
|
|
||||
|
// 高级字段(展开时才显示)
|
||||
|
const advancedFields: SearchField[] = [ |
||||
|
{ key: 'version', label: '版本', type: 'input', placeholder: '请输入版本号' }, |
||||
|
{ key: 'pageSize', label: '每页数量', type: 'select', options: [ |
||||
|
{ value: '10', label: '10条/页' }, |
||||
|
{ value: '20', label: '20条/页' }, |
||||
|
{ value: '50', label: '50条/页' }, |
||||
|
{ value: '100', label: '100条/页' }, |
||||
|
] }, |
||||
|
]; |
||||
|
|
||||
|
export default function TestCaseSetsView() { |
||||
|
const [testCaseSets, setTestCaseSets] = useState<TestCaseSet[]>([]); |
||||
|
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 [showAdvanced, setShowAdvanced] = useState(false); |
||||
|
|
||||
|
// 搜索参数
|
||||
|
const [searchTerm, setSearchTerm] = useState(''); |
||||
|
const [isEnabled, setIsEnabled] = useState<boolean | undefined>(undefined); |
||||
|
const [version, setVersion] = useState(''); |
||||
|
|
||||
|
// 表单对话框状态
|
||||
|
const [open, setOpen] = useState(false); |
||||
|
const [editOpen, setEditOpen] = useState(false); |
||||
|
const [selectedTestCaseSet, setSelectedTestCaseSet] = useState<TestCaseSet | null>(null); |
||||
|
|
||||
|
// 提交状态
|
||||
|
const [isSubmitting, setIsSubmitting] = useState(false); |
||||
|
|
||||
|
// Toast 提示
|
||||
|
const { toast } = useToast(); |
||||
|
|
||||
|
const fetchTestCaseSets = async (params: Partial<GetTestCaseSetsRequest> = {}) => { |
||||
|
setLoading(true); |
||||
|
const queryParams: GetTestCaseSetsRequest = { |
||||
|
pageNumber, |
||||
|
pageSize, |
||||
|
searchTerm, |
||||
|
isEnabled, |
||||
|
version, |
||||
|
...params |
||||
|
}; |
||||
|
|
||||
|
const result = await testCaseSetService.getTestCaseSets(queryParams); |
||||
|
if (result.isSuccess && result.data) { |
||||
|
setTestCaseSets(result.data.items || []); |
||||
|
setTotal(result.data.totalCount || 0); |
||||
|
} |
||||
|
setLoading(false); |
||||
|
}; |
||||
|
|
||||
|
useEffect(() => { |
||||
|
fetchTestCaseSets(); |
||||
|
// eslint-disable-next-line
|
||||
|
}, [pageNumber, pageSize]); |
||||
|
|
||||
|
const handleEdit = (testCaseSet: TestCaseSet) => { |
||||
|
setSelectedTestCaseSet(testCaseSet); |
||||
|
setEditOpen(true); |
||||
|
}; |
||||
|
|
||||
|
const handleDelete = async (testCaseSet: TestCaseSet) => { |
||||
|
if (confirm(`确定要删除用例集 "${testCaseSet.name}" 吗?`)) { |
||||
|
try { |
||||
|
const result = await testCaseSetService.deleteTestCaseSet(testCaseSet.testCaseSetId); |
||||
|
if (result.isSuccess) { |
||||
|
toast({ |
||||
|
title: "删除成功", |
||||
|
description: `用例集 "${testCaseSet.name}" 删除成功`, |
||||
|
}); |
||||
|
fetchTestCaseSets(); |
||||
|
} else { |
||||
|
const errorMessage = result.errorMessages?.join(', ') || "删除用例集时发生错误"; |
||||
|
console.error('删除用例集失败:', errorMessage); |
||||
|
toast({ |
||||
|
title: "删除失败", |
||||
|
description: errorMessage, |
||||
|
variant: "destructive", |
||||
|
}); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('删除用例集异常:', error); |
||||
|
toast({ |
||||
|
title: "删除失败", |
||||
|
description: "网络错误,请稍后重试", |
||||
|
variant: "destructive", |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleCreate = async (data: CreateTestCaseSetRequest) => { |
||||
|
if (isSubmitting) return; // 防止重复提交
|
||||
|
|
||||
|
console.log('开始创建用例集:', data); |
||||
|
setIsSubmitting(true); |
||||
|
try { |
||||
|
const result = await testCaseSetService.createTestCaseSet(data); |
||||
|
console.log('创建用例集结果:', result); |
||||
|
|
||||
|
if (result.isSuccess) { |
||||
|
toast({ |
||||
|
title: "创建成功", |
||||
|
description: `用例集 "${data.name}" 创建成功`, |
||||
|
}); |
||||
|
setOpen(false); |
||||
|
fetchTestCaseSets(); |
||||
|
} else { |
||||
|
const errorMessage = result.errorMessages?.join(', ') || "创建用例集时发生错误"; |
||||
|
console.error('创建用例集失败:', errorMessage, result); |
||||
|
toast({ |
||||
|
title: "创建失败", |
||||
|
description: errorMessage, |
||||
|
variant: "destructive", |
||||
|
}); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('创建用例集异常:', error); |
||||
|
toast({ |
||||
|
title: "创建失败", |
||||
|
description: "网络错误,请稍后重试", |
||||
|
variant: "destructive", |
||||
|
}); |
||||
|
} finally { |
||||
|
setIsSubmitting(false); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleUpdate = async (data: UpdateTestCaseSetRequest) => { |
||||
|
if (!selectedTestCaseSet || isSubmitting) return; // 防止重复提交
|
||||
|
|
||||
|
setIsSubmitting(true); |
||||
|
try { |
||||
|
const result = await testCaseSetService.updateTestCaseSet(selectedTestCaseSet.testCaseSetId, data); |
||||
|
if (result.isSuccess) { |
||||
|
toast({ |
||||
|
title: "更新成功", |
||||
|
description: `用例集 "${data.name}" 更新成功`, |
||||
|
}); |
||||
|
setEditOpen(false); |
||||
|
setSelectedTestCaseSet(null); |
||||
|
fetchTestCaseSets(); |
||||
|
} else { |
||||
|
const errorMessage = result.errorMessages?.join(', ') || "更新用例集时发生错误"; |
||||
|
console.error('更新用例集失败:', errorMessage); |
||||
|
toast({ |
||||
|
title: "更新失败", |
||||
|
description: errorMessage, |
||||
|
variant: "destructive", |
||||
|
}); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('更新用例集异常:', error); |
||||
|
toast({ |
||||
|
title: "更新失败", |
||||
|
description: "网络错误,请稍后重试", |
||||
|
variant: "destructive", |
||||
|
}); |
||||
|
} finally { |
||||
|
setIsSubmitting(false); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// 查询按钮
|
||||
|
const handleQuery = () => { |
||||
|
setPageNumber(1); |
||||
|
fetchTestCaseSets({ pageNumber: 1 }); |
||||
|
}; |
||||
|
|
||||
|
// 重置按钮
|
||||
|
const handleReset = () => { |
||||
|
setSearchTerm(''); |
||||
|
setIsEnabled(undefined); |
||||
|
setVersion(''); |
||||
|
setPageNumber(1); |
||||
|
fetchTestCaseSets({ |
||||
|
searchTerm: '', |
||||
|
isEnabled: undefined, |
||||
|
version: '', |
||||
|
pageNumber: 1 |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
// 每页条数选择
|
||||
|
const handlePageSizeChange = (size: number) => { |
||||
|
setPageSize(size); |
||||
|
setPageNumber(1); |
||||
|
}; |
||||
|
|
||||
|
const totalPages = Math.ceil(total / pageSize); |
||||
|
|
||||
|
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-white p-4 rounded-md border mb-2"> |
||||
|
<form |
||||
|
className={`grid gap-x-8 gap-y-4 items-center ${showAdvanced ? 'md:grid-cols-4' : 'md:grid-cols-3'} grid-cols-1`} |
||||
|
onSubmit={e => { |
||||
|
e.preventDefault(); |
||||
|
handleQuery(); |
||||
|
}} |
||||
|
> |
||||
|
{(showAdvanced ? [...firstRowFields, ...advancedFields] : firstRowFields).map(field => ( |
||||
|
<div className="flex flex-row items-center min-w-[200px] flex-1" key={field.key}> |
||||
|
<label |
||||
|
className="mr-2 text-sm font-medium text-foreground whitespace-nowrap text-right" |
||||
|
style={{ width: 80, minWidth: 80 }} |
||||
|
> |
||||
|
{field.label}: |
||||
|
</label> |
||||
|
{field.type === 'input' && ( |
||||
|
<Input |
||||
|
className="input flex-1" |
||||
|
placeholder={field.placeholder} |
||||
|
value={field.key === 'searchTerm' ? searchTerm : |
||||
|
field.key === 'version' ? version : ''} |
||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
||||
|
if (field.key === 'searchTerm') setSearchTerm(e.target.value); |
||||
|
if (field.key === 'version') setVersion(e.target.value); |
||||
|
}} |
||||
|
/> |
||||
|
)} |
||||
|
{field.type === 'select' && ( |
||||
|
<select |
||||
|
className="input h-10 rounded border border-border bg-background px-3 text-sm flex-1" |
||||
|
value={field.key === 'isEnabled' ? (isEnabled === undefined ? '' : isEnabled.toString()) : |
||||
|
field.key === 'pageSize' ? pageSize.toString() : ''} |
||||
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => { |
||||
|
if (field.key === 'isEnabled') { |
||||
|
const value = e.target.value; |
||||
|
setIsEnabled(value === '' ? undefined : value === 'true'); |
||||
|
} else if (field.key === 'pageSize') { |
||||
|
setPageSize(parseInt(e.target.value)); |
||||
|
} |
||||
|
}} |
||||
|
> |
||||
|
{field.options.map(opt => ( |
||||
|
<option value={opt.value} key={opt.value}>{opt.label}</option> |
||||
|
))} |
||||
|
</select> |
||||
|
)} |
||||
|
</div> |
||||
|
))} |
||||
|
{/* 按钮组直接作为表单项之一,紧跟在最后一个表单项后面 */} |
||||
|
<div className="flex flex-row items-center min-w-[200px] flex-1 justify-end gap-2"> |
||||
|
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={handleReset}>重置</button> |
||||
|
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700">查询</button> |
||||
|
<button type="button" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" onClick={() => setShowAdvanced(v => !v)}> |
||||
|
{showAdvanced ? ( |
||||
|
<> |
||||
|
收起 <ChevronUpIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
||||
|
</> |
||||
|
) : ( |
||||
|
<> |
||||
|
展开 <ChevronDownIcon className="inline-block ml-1 w-4 h-4 align-middle" /> |
||||
|
</> |
||||
|
)} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
{/* 表格整体卡片区域,包括工具栏、表格、分页 */} |
||||
|
<div className="rounded-md border bg-background p-4"> |
||||
|
{/* 顶部操作栏:添加用例集+工具栏 */} |
||||
|
<div className="flex items-center justify-between mb-2"> |
||||
|
<Dialog open={open} onOpenChange={setOpen}> |
||||
|
<DialogTrigger asChild> |
||||
|
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">+ 添加用例集</Button> |
||||
|
</DialogTrigger> |
||||
|
<DialogContent className="bg-background"> |
||||
|
<TestCaseSetForm onSubmit={handleCreate} isSubmitting={isSubmitting} /> |
||||
|
</DialogContent> |
||||
|
</Dialog> |
||||
|
<TableToolbar |
||||
|
onRefresh={() => fetchTestCaseSets()} |
||||
|
onDensityChange={setDensity} |
||||
|
onColumnsChange={setColumns} |
||||
|
onColumnsReset={() => setColumns(defaultColumns)} |
||||
|
columns={columns} |
||||
|
density={density} |
||||
|
/> |
||||
|
</div> |
||||
|
{/* 表格区域 */} |
||||
|
<TestCaseSetTable |
||||
|
testCaseSets={testCaseSets} |
||||
|
loading={loading} |
||||
|
onEdit={handleEdit} |
||||
|
onDelete={handleDelete} |
||||
|
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> |
||||
|
|
||||
|
{/* 编辑用例集对话框 */} |
||||
|
<Dialog open={editOpen} onOpenChange={setEditOpen}> |
||||
|
<DialogContent className="bg-background"> |
||||
|
<TestCaseSetForm |
||||
|
onSubmit={(data) => handleUpdate(data as UpdateTestCaseSetRequest)} |
||||
|
initialData={selectedTestCaseSet || undefined} |
||||
|
isEdit={true} |
||||
|
isSubmitting={isSubmitting} |
||||
|
/> |
||||
|
</DialogContent> |
||||
|
</Dialog> |
||||
|
</main> |
||||
|
); |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
export { default as TestCaseSetsView } from './TestCaseSetsView'; |
||||
|
export { default as TestCaseSetTable } from './TestCaseSetTable'; |
||||
|
export { default as TestCaseSetForm } from './TestCaseSetForm'; |
@ -0,0 +1,142 @@ |
|||||
|
import { httpClient } from '@/lib/http-client'; |
||||
|
import { OperationResult } from '@/types/auth'; |
||||
|
import { API_PATHS } from '@/constants/api'; |
||||
|
|
||||
|
// 用例集接口定义
|
||||
|
export interface TestCaseSet { |
||||
|
testCaseSetId: string; |
||||
|
code: string; |
||||
|
name: string; |
||||
|
description?: string; |
||||
|
version: string; |
||||
|
versionUpdateInfo?: string; |
||||
|
isDisabled: boolean; |
||||
|
remarks?: string; |
||||
|
createdAt: string; |
||||
|
updatedAt?: string; |
||||
|
createdBy: string; |
||||
|
updatedBy?: string; |
||||
|
} |
||||
|
|
||||
|
// 获取用例集列表请求接口
|
||||
|
export interface GetTestCaseSetsRequest { |
||||
|
pageNumber?: number; |
||||
|
pageSize?: number; |
||||
|
searchTerm?: string; |
||||
|
isEnabled?: boolean; |
||||
|
version?: string; |
||||
|
} |
||||
|
|
||||
|
// 获取用例集列表响应接口
|
||||
|
export interface GetTestCaseSetsResponse { |
||||
|
totalCount: number; |
||||
|
pageNumber: number; |
||||
|
pageSize: number; |
||||
|
totalPages: number; |
||||
|
hasPreviousPage: boolean; |
||||
|
hasNextPage: boolean; |
||||
|
items: TestCaseSet[]; |
||||
|
} |
||||
|
|
||||
|
// 创建用例集请求接口
|
||||
|
export interface CreateTestCaseSetRequest { |
||||
|
code: string; |
||||
|
name: string; |
||||
|
version: string; |
||||
|
description?: string; |
||||
|
versionUpdateInfo?: string; |
||||
|
isDisabled?: boolean; |
||||
|
remarks?: string; |
||||
|
} |
||||
|
|
||||
|
// 创建用例集响应接口
|
||||
|
export interface CreateTestCaseSetResponse { |
||||
|
testCaseSetId: string; |
||||
|
code: string; |
||||
|
name: string; |
||||
|
description?: string; |
||||
|
version: string; |
||||
|
versionUpdateInfo?: string; |
||||
|
isDisabled: boolean; |
||||
|
remarks?: string; |
||||
|
createdAt: string; |
||||
|
createdBy: string; |
||||
|
} |
||||
|
|
||||
|
// 更新用例集请求接口
|
||||
|
export interface UpdateTestCaseSetRequest { |
||||
|
testCaseSetId: string; |
||||
|
code: string; |
||||
|
name: string; |
||||
|
version: string; |
||||
|
description?: string; |
||||
|
versionUpdateInfo?: string; |
||||
|
isDisabled?: boolean; |
||||
|
remarks?: string; |
||||
|
} |
||||
|
|
||||
|
// 更新用例集响应接口
|
||||
|
export interface UpdateTestCaseSetResponse { |
||||
|
testCaseSetId: string; |
||||
|
code: string; |
||||
|
name: string; |
||||
|
description?: string; |
||||
|
version: string; |
||||
|
versionUpdateInfo?: string; |
||||
|
isDisabled: boolean; |
||||
|
remarks?: string; |
||||
|
createdAt: string; |
||||
|
updatedAt?: string; |
||||
|
createdBy: string; |
||||
|
updatedBy?: string; |
||||
|
} |
||||
|
|
||||
|
class TestCaseSetService { |
||||
|
private readonly baseUrl = API_PATHS.TEST_CASE_SETS; |
||||
|
|
||||
|
// 获取用例集列表
|
||||
|
async getTestCaseSets(params: GetTestCaseSetsRequest = {}): Promise<OperationResult<GetTestCaseSetsResponse>> { |
||||
|
const queryParams = new URLSearchParams(); |
||||
|
|
||||
|
if (params.pageNumber) queryParams.append('PageNumber', params.pageNumber.toString()); |
||||
|
if (params.pageSize) queryParams.append('PageSize', params.pageSize.toString()); |
||||
|
if (params.searchTerm) queryParams.append('SearchTerm', params.searchTerm); |
||||
|
if (params.isEnabled !== undefined) queryParams.append('IsEnabled', params.isEnabled.toString()); |
||||
|
if (params.version) queryParams.append('Version', params.version); |
||||
|
|
||||
|
const url = `${this.baseUrl}?${queryParams.toString()}`; |
||||
|
return httpClient.get<GetTestCaseSetsResponse>(url); |
||||
|
} |
||||
|
|
||||
|
// 根据ID获取用例集详情
|
||||
|
async getTestCaseSetById(id: string): Promise<OperationResult<TestCaseSet>> { |
||||
|
return httpClient.get<TestCaseSet>(`${this.baseUrl}/${id}`); |
||||
|
} |
||||
|
|
||||
|
// 创建用例集
|
||||
|
async createTestCaseSet(data: CreateTestCaseSetRequest): Promise<OperationResult<CreateTestCaseSetResponse>> { |
||||
|
return httpClient.post<CreateTestCaseSetResponse>(this.baseUrl, data); |
||||
|
} |
||||
|
|
||||
|
// 更新用例集
|
||||
|
async updateTestCaseSet(id: string, data: UpdateTestCaseSetRequest): Promise<OperationResult<UpdateTestCaseSetResponse>> { |
||||
|
return httpClient.put<UpdateTestCaseSetResponse>(`${this.baseUrl}/${id}`, data); |
||||
|
} |
||||
|
|
||||
|
// 删除用例集
|
||||
|
async deleteTestCaseSet(id: string): Promise<OperationResult<boolean>> { |
||||
|
return httpClient.delete<boolean>(`${this.baseUrl}/${id}`); |
||||
|
} |
||||
|
|
||||
|
// 启用用例集
|
||||
|
async enableTestCaseSet(id: string): Promise<OperationResult<boolean>> { |
||||
|
return httpClient.post<boolean>(`${this.baseUrl}/${id}/enable`); |
||||
|
} |
||||
|
|
||||
|
// 禁用用例集
|
||||
|
async disableTestCaseSet(id: string): Promise<OperationResult<boolean>> { |
||||
|
return httpClient.post<boolean>(`${this.baseUrl}/${id}/disable`); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const testCaseSetService = new TestCaseSetService(); |
Loading…
Reference in new issue