Browse Source

feat: 用例集功能开发与权限集成,前端页面与服务完善

feature/x1-owen-debug
test 4 weeks ago
parent
commit
1cb806dfe3
  1. 1
      src/X1.WebUI/src/constants/api.ts
  2. 8
      src/X1.WebUI/src/constants/menuConfig.ts
  3. 26
      src/X1.WebUI/src/contexts/AuthContext.tsx
  4. 2
      src/X1.WebUI/src/pages/scenarios/ScenarioForm.tsx
  5. 135
      src/X1.WebUI/src/pages/testcasesets/TestCaseSetForm.tsx
  6. 187
      src/X1.WebUI/src/pages/testcasesets/TestCaseSetTable.tsx
  7. 367
      src/X1.WebUI/src/pages/testcasesets/TestCaseSetsView.tsx
  8. 3
      src/X1.WebUI/src/pages/testcasesets/index.ts
  9. 8
      src/X1.WebUI/src/routes/AppRouter.tsx
  10. 142
      src/X1.WebUI/src/services/testCaseSetService.ts

1
src/X1.WebUI/src/constants/api.ts

@ -31,6 +31,7 @@ export const API_PATHS = {
// 测试用例相关
TEST_CASES: '/test-cases',
TEST_CASE_SETS: '/test-case-sets',
TEST_STEPS: '/test-steps',
// 分析相关

8
src/X1.WebUI/src/constants/menuConfig.ts

@ -19,6 +19,9 @@ export type Permission =
| 'testcases.view'
| 'testcases.manage'
| 'testcases.create'
| 'testcasesets.view'
| 'testcasesets.manage'
| 'testcasesets.create'
| 'teststeps.view'
| 'teststeps.manage'
| 'teststeps.create'
@ -97,6 +100,11 @@ export const menuItems: MenuItem[] = [
href: '/dashboard/testcases/list',
permission: 'testcases.view',
},
{
title: '用例集',
href: '/dashboard/testcases/sets',
permission: 'testcasesets.view',
},
{
title: '创建用例',
href: '/dashboard/testcases/create',

26
src/X1.WebUI/src/contexts/AuthContext.tsx

@ -1,7 +1,7 @@
import { createContext, useContext, useReducer, ReactNode, useMemo, useEffect, useState } from 'react';
import { AuthState, AuthContextType, LoginRequest, User, RegisterRequest } from '@/types/auth';
import { AuthState, AuthContextType, LoginRequest, User as AuthUser, RegisterRequest } from '@/types/auth';
import { useSetRecoilState, useRecoilState } from 'recoil';
import { userState } from '@/stores/userStore';
import { userState, User as StoreUser } from '@/stores/userStore';
import { authService } from '@/services/authService';
import { useAuthSync } from '@/hooks/useAuthSync';
import { useAuthInit } from '@/hooks/useAuthInit';
@ -24,14 +24,14 @@ const initialState: AuthState = {
type AuthAction =
| { type: 'LOGIN_START' }
| { type: 'LOGIN_SUCCESS'; payload: { user: User; accessToken: string; refreshToken: string; rememberMe: boolean } }
| { type: 'LOGIN_SUCCESS'; payload: { user: AuthUser; accessToken: string; refreshToken: string; rememberMe: boolean } }
| { type: 'LOGIN_FAILURE'; payload: { error: string } }
| { type: 'REGISTER_START' }
| { type: 'REGISTER_SUCCESS' }
| { type: 'REGISTER_FAILURE'; payload: { error: string } }
| { type: 'LOGOUT' }
| { type: 'CLEAR_ERROR' }
| { type: 'SET_USER'; payload: { user: User; accessToken: string; refreshToken: string } }
| { type: 'SET_USER'; payload: { user: AuthUser; accessToken: string; refreshToken: string } }
| { type: 'SET_REMEMBER_ME'; payload: boolean };
// 获取默认权限
@ -49,6 +49,9 @@ const getDefaultPermissions = (userPermissions: Record<string, boolean> = {}) =>
'testcases.view',
'testcases.manage',
'testcases.create',
'testcasesets.view',
'testcasesets.manage',
'testcasesets.create',
'teststeps.view',
'teststeps.manage',
'teststeps.create',
@ -181,7 +184,20 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}, [state]);
// 使用自定义 hooks
useAuthSync(state.user, setGlobalUser);
useAuthSync(state.user, (user: AuthUser | null) => {
if (user) {
const storeUser: StoreUser = {
id: user.id,
username: user.userName,
email: user.email,
roles: [], // 从用户权限中提取角色信息
permissions: Object.keys(user.permissions || {})
};
setGlobalUser(storeUser);
} else {
setGlobalUser(null);
}
});
useAuthInit(dispatch);
const authActions = useMemo(() => ({

2
src/X1.WebUI/src/pages/scenarios/ScenarioForm.tsx

@ -26,7 +26,7 @@ export default function ScenarioForm({ onSubmit, initialData, isEdit = false, is
e.preventDefault();
if (isSubmitting) return; // 防止重复提交
if (isEdit && initialData) {
if (isEdit && initialData?.scenarioId) {
// 编辑模式,需要包含scenarioId
const updateData: UpdateScenarioRequest = {
...formData,

135
src/X1.WebUI/src/pages/testcasesets/TestCaseSetForm.tsx

@ -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>
);
}

187
src/X1.WebUI/src/pages/testcasesets/TestCaseSetTable.tsx

@ -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>
);
}

367
src/X1.WebUI/src/pages/testcasesets/TestCaseSetsView.tsx

@ -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>
);
}

3
src/X1.WebUI/src/pages/testcasesets/index.ts

@ -0,0 +1,3 @@
export { default as TestCaseSetsView } from './TestCaseSetsView';
export { default as TestCaseSetTable } from './TestCaseSetTable';
export { default as TestCaseSetForm } from './TestCaseSetForm';

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

@ -16,6 +16,7 @@ const UsersView = lazy(() => import('@/pages/users/UsersView'));
// 场景管理页面
const ScenariosView = lazy(() => import('@/pages/scenarios/ScenariosView'));
const TestCasesView = lazy(() => import('@/pages/testcases/TestCasesView'));
const TestCaseSetsView = lazy(() => import('@/pages/testcasesets/TestCaseSetsView'));
const TestStepsView = lazy(() => import('@/pages/teststeps/TestStepsView'));
// 任务管理页面
@ -116,6 +117,13 @@ export function AppRouter() {
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="sets" element={
<ProtectedRoute requiredPermission="testcasesets.view">
<AnimatedContainer>
<TestCaseSetsView />
</AnimatedContainer>
</ProtectedRoute>
} />
<Route path="steps" element={
<ProtectedRoute requiredPermission="teststeps.view">
<AnimatedContainer>

142
src/X1.WebUI/src/services/testCaseSetService.ts

@ -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…
Cancel
Save