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