生产管理系统前端 - openapi - fetch生成器开发

This commit is contained in:
2025-10-27 21:53:18 +08:00
parent 5055e40de6
commit 42a4a9f566
29 changed files with 4264 additions and 1463 deletions

View File

@@ -1,14 +1,654 @@
import ApiExample from '@/components/examples/ApiExample';
'use client';
import { useState } from 'react';
import {
loginApiV1AuthLoginPost,
registerApiV1AuthRegisterPost,
getCurrentUserApiV1AuthMeGet,
getAllUsersApiV1AuthUsersGet,
logoutApiV1AuthLogoutPost,
rootGet,
healthCheckHealthGet,
type UserLogin,
type UserRegister,
type ApiResponse
} from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
export default function ApiExamplePage() {
const [loading, setLoading] = useState(false);
const [results, setResults] = useState<any[]>([]);
const [errors, setErrors] = useState<string[]>([]);
// 登录表单状态
const [loginData, setLoginData] = useState<UserLogin>({
username: '',
password: ''
});
// 注册表单状态
const [registerData, setRegisterData] = useState<UserRegister>({
username: '',
password: ''
});
// 添加结果到显示列表
const addResult = (type: string, input: any, output: any, error?: string) => {
const result = {
id: Date.now(),
type,
input,
output,
error,
timestamp: new Date().toLocaleTimeString()
};
setResults(prev => [result, ...prev]);
if (error) {
setErrors(prev => [error, ...prev]);
}
};
// 清空结果
const clearResults = () => {
setResults([]);
setErrors([]);
};
// 用户登录
const handleLogin = async () => {
setLoading(true);
try {
addResult('用户登录', '登录请求', '发送中...');
const response = await loginApiV1AuthLoginPost({
body: loginData
});
if (response.data) {
addResult('用户登录', loginData, response.data);
} else if (response.error) {
addResult('用户登录', loginData, null, JSON.stringify(response.error));
}
} catch (error: any) {
addResult('用户登录', loginData, null, error.message);
} finally {
setLoading(false);
}
};
// 用户注册
const handleRegister = async () => {
setLoading(true);
try {
addResult('用户注册', '注册请求', '发送中...');
const response = await registerApiV1AuthRegisterPost({
body: registerData
});
if (response.data) {
addResult('用户注册', registerData, response.data);
} else if (response.error) {
addResult('用户注册', registerData, null, JSON.stringify(response.error));
}
} catch (error: any) {
addResult('用户注册', registerData, null, error.message);
} finally {
setLoading(false);
}
};
// 获取当前用户信息
const handleGetCurrentUser = async () => {
setLoading(true);
try {
addResult('获取当前用户', 'GET /api/v1/auth/me', '发送中...');
const response = await getCurrentUserApiV1AuthMeGet({});
if (response.data) {
addResult('获取当前用户', '无参数', response.data);
} else if (response.error) {
addResult('获取当前用户', '无参数', null, JSON.stringify(response.error));
}
} catch (error: any) {
addResult('获取当前用户', '无参数', null, error.message);
} finally {
setLoading(false);
}
};
// 获取所有用户
const handleGetAllUsers = async () => {
setLoading(true);
try {
addResult('获取所有用户', 'GET /api/v1/auth/users', '发送中...');
const response = await getAllUsersApiV1AuthUsersGet({});
if (response.data) {
addResult('获取所有用户', '无参数', response.data);
} else if (response.error) {
addResult('获取所有用户', '无参数', null, JSON.stringify(response.error));
}
} catch (error: any) {
addResult('获取所有用户', '无参数', null, error.message);
} finally {
setLoading(false);
}
};
// 用户登出
const handleLogout = async () => {
setLoading(true);
try {
addResult('用户登出', 'POST /api/v1/auth/logout', '发送中...');
const response = await logoutApiV1AuthLogoutPost({});
if (response.data) {
addResult('用户登出', '无参数', response.data);
} else if (response.error) {
addResult('用户登出', '无参数', null, JSON.stringify(response.error));
}
} catch (error: any) {
addResult('用户登出', '无参数', null, error.message);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-background">
<ApiExample />
<div className="min-h-screen bg-background p-6">
<div className="max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">OpenAPI </h1>
<Button
variant="outline"
onClick={clearResults}
disabled={results.length === 0}
>
({results.length})
</Button>
</div>
<Tabs defaultValue="interactive" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="interactive"></TabsTrigger>
<TabsTrigger value="examples"></TabsTrigger>
</TabsList>
<TabsContent value="interactive" className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 左侧API 操作面板 */}
<Card>
<CardHeader>
<CardTitle>API </CardTitle>
<CardDescription>
使 @hey-api/openapi-ts
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="auth" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="auth"></TabsTrigger>
<TabsTrigger value="user"></TabsTrigger>
</TabsList>
<TabsContent value="auth" className="space-y-4">
{/* 登录表单 */}
<div className="space-y-2">
<h4 className="font-medium"></h4>
<Input
placeholder="用户名"
value={loginData.username}
onChange={(e) => setLoginData(prev => ({ ...prev, username: e.target.value }))}
/>
<Input
type="password"
placeholder="密码"
value={loginData.password}
onChange={(e) => setLoginData(prev => ({ ...prev, password: e.target.value }))}
/>
<Button
onClick={handleLogin}
disabled={loading || !loginData.username || !loginData.password}
className="w-full"
>
{loading ? '登录中...' : '登录'}
</Button>
</div>
{/* 注册表单 */}
<div className="space-y-2">
<h4 className="font-medium"></h4>
<Input
placeholder="用户名"
value={registerData.username}
onChange={(e) => setRegisterData(prev => ({ ...prev, username: e.target.value }))}
/>
<Input
type="password"
placeholder="密码"
value={registerData.password}
onChange={(e) => setRegisterData(prev => ({ ...prev, password: e.target.value }))}
/>
<Button
onClick={handleRegister}
disabled={loading || !registerData.username || !registerData.password}
variant="outline"
className="w-full"
>
{loading ? '注册中...' : '注册'}
</Button>
</div>
</TabsContent>
<TabsContent value="user" className="space-y-4">
<div className="grid grid-cols-1 gap-2">
<Button
onClick={handleGetCurrentUser}
disabled={loading}
variant="outline"
>
</Button>
<Button
onClick={handleGetAllUsers}
disabled={loading}
variant="outline"
>
</Button>
<Button
onClick={handleLogout}
disabled={loading}
variant="outline"
>
</Button>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* 右侧:结果显示面板 */}
<Card>
<CardHeader>
<CardTitle> & </CardTitle>
<CardDescription>
API
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4 max-h-[600px] overflow-y-auto">
{results.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
<p> API </p>
<p className="text-sm"> API </p>
</div>
) : (
results.map((result) => (
<div key={result.id} className="border rounded-lg p-4 space-y-2">
<div className="flex items-center justify-between">
<h4 className="font-medium">{result.type}</h4>
<div className="flex items-center gap-2">
<Badge variant="outline">{result.timestamp}</Badge>
{result.error ? (
<Badge variant="destructive"></Badge>
) : (
<Badge variant="default"></Badge>
)}
</div>
</div>
{/* 输入数据 */}
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground">:</p>
<pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
{JSON.stringify(result.input, null, 2)}
</pre>
</div>
{/* 输出数据 */}
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground">
{result.error ? '错误信息:' : '响应数据:'}
</p>
<pre className={`text-xs p-2 rounded overflow-x-auto ${
result.error ? 'bg-destructive/10 text-destructive' : 'bg-muted'
}`}>
{result.error || JSON.stringify(result.output, null, 2)}
</pre>
</div>
</div>
))
)}
</div>
</CardContent>
</Card>
</div>
{/* 错误提示 */}
{errors.length > 0 && (
<div className="mt-6">
<Alert>
<AlertDescription>
{errors.length}
</AlertDescription>
</Alert>
</div>
)}
</TabsContent>
<TabsContent value="examples" className="space-y-6">
<ApiExamplesPage />
</TabsContent>
</Tabs>
</div>
</div>
);
}
export const metadata = {
title: 'API 调用示例 - 智慧农业生产管理系统',
description: '测试和展示 OpenAPI 客户端的类型安全 API 调用',
};
// API示例页面组件
function ApiExamplesPage() {
const [examplesLoading, setExamplesLoading] = useState<Record<string, boolean>>({});
const [examplesResults, setExamplesResults] = useState<Record<string, any>>({});
// API示例配置 - 基于openapi.json
const apiExamples = [
{
id: 'login',
method: 'POST',
path: '/api/v1/auth/login',
title: '用户登录',
description: '用户登录接口',
exampleParams: {
username: 'admin',
password: 'admin123'
},
expectedOutput: {
success: true,
message: '登录成功',
data: { token: 'jwt_token_here' }
}
},
{
id: 'register',
method: 'POST',
path: '/api/v1/auth/register',
title: '用户注册',
description: '用户注册接口',
exampleParams: {
username: 'newuser',
password: 'newpassword'
},
expectedOutput: {
success: true,
message: '注册成功',
data: { user_id: 1, username: 'newuser' }
}
},
{
id: 'me',
method: 'GET',
path: '/api/v1/auth/me',
title: '获取当前用户信息',
description: '获取当前登录用户的信息',
exampleParams: null,
expectedOutput: {
success: true,
message: '获取用户信息成功',
data: { user_id: 1, username: 'admin' }
}
},
{
id: 'logout',
method: 'POST',
path: '/api/v1/auth/logout',
title: '用户登出',
description: '用户登出接口',
exampleParams: null,
expectedOutput: {
success: true,
message: '登出成功',
data: null
}
},
{
id: 'users',
method: 'GET',
path: '/api/v1/auth/users',
title: '获取所有用户列表',
description: '获取系统中所有用户的列表 (仅用于演示)',
exampleParams: null,
expectedOutput: {
success: true,
message: '获取用户列表成功',
data: [{ user_id: 1, username: 'admin' }]
}
},
{
id: 'root',
method: 'GET',
path: '/',
title: '根路径',
description: 'API根路径用于测试连接',
exampleParams: null,
expectedOutput: {}
},
{
id: 'health',
method: 'GET',
path: '/health',
title: '健康检查',
description: 'API健康检查接口',
exampleParams: null,
expectedOutput: { status: 'healthy' }
}
];
// 调用示例API
const callExampleApi = async (example: typeof apiExamples[0]) => {
setExamplesLoading(prev => ({ ...prev, [example.id]: true }));
try {
let response;
switch (example.id) {
case 'login':
response = await loginApiV1AuthLoginPost({
body: example.exampleParams
});
break;
case 'register':
response = await registerApiV1AuthRegisterPost({
body: example.exampleParams
});
break;
case 'me':
response = await getCurrentUserApiV1AuthMeGet({});
break;
case 'logout':
response = await logoutApiV1AuthLogoutPost({});
break;
case 'users':
response = await getAllUsersApiV1AuthUsersGet({});
break;
case 'root':
response = await rootGet({});
break;
case 'health':
response = await healthCheckHealthGet({});
break;
default:
throw new Error('Unknown API example');
}
setExamplesResults(prev => ({
...prev,
[example.id]: {
success: !response.error,
data: response.data,
error: response.error,
input: example.exampleParams,
timestamp: new Date().toLocaleTimeString()
}
}));
} catch (error: any) {
setExamplesResults(prev => ({
...prev,
[example.id]: {
success: false,
error: error.message,
input: example.exampleParams,
timestamp: new Date().toLocaleTimeString()
}
}));
} finally {
setExamplesLoading(prev => ({ ...prev, [example.id]: false }));
}
};
// 批量调用所有示例
const callAllExamples = async () => {
// 先调用登录接口获取token
await callExampleApi(apiExamples[0]);
// 然后调用其他接口
for (const example of apiExamples.slice(1)) {
await new Promise(resolve => setTimeout(resolve, 100)); // 间隔100ms
await callExampleApi(example);
}
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold"></h2>
<p className="text-muted-foreground">
OpenAPI规范自动生成的所有接口示例
</p>
</div>
<Button onClick={callAllExamples} disabled={Object.values(examplesLoading).some(v => v)}>
{Object.values(examplesLoading).some(v => v) ? '测试中...' : '批量测试所有接口'}
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{apiExamples.map((example) => (
<Card key={example.id} className="h-fit">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base">{example.title}</CardTitle>
<CardDescription className="text-xs">
{example.method} {example.path}
</CardDescription>
</div>
<Badge variant="outline">{example.method}</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">{example.description}</p>
{/* 示例参数 */}
<div className="space-y-2">
<h4 className="text-sm font-medium">:</h4>
{example.exampleParams ? (
<pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
{JSON.stringify(example.exampleParams, null, 2)}
</pre>
) : (
<p className="text-xs text-muted-foreground italic"></p>
)}
</div>
{/* 预期输出 */}
<div className="space-y-2">
<h4 className="text-sm font-medium">:</h4>
<pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
{JSON.stringify(example.expectedOutput, null, 2)}
</pre>
</div>
{/* 实际结果 */}
{examplesResults[example.id] && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium">
<span className="ml-2 text-xs text-muted-foreground">
({examplesResults[example.id].timestamp})
</span>
</h4>
<Badge
variant={examplesResults[example.id].success ? "default" : "destructive"}
>
{examplesResults[example.id].success ? '成功' : '失败'}
</Badge>
</div>
<pre className={`text-xs p-2 rounded overflow-x-auto ${
examplesResults[example.id].success ? 'bg-green-50 text-green-800 border-green-200' : 'bg-red-50 text-red-800 border-red-200'
}`}>
{examplesResults[example.id].error
? JSON.stringify(examplesResults[example.id].error, null, 2)
: JSON.stringify(examplesResults[example.id].data, null, 2)
}
</pre>
</div>
)}
<Button
onClick={() => callExampleApi(example)}
disabled={examplesLoading[example.id]}
className="w-full"
variant={examplesResults[example.id]?.success ? "outline" : "default"}
>
{examplesLoading[example.id] ? '测试中...' : '测试此接口'}
</Button>
</CardContent>
</Card>
))}
</div>
{/* 统计信息 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<p className="text-2xl font-bold text-green-600">
{Object.values(examplesResults).filter(r => r?.success).length}
</p>
<p className="text-sm text-muted-foreground"></p>
</div>
<div>
<p className="text-2xl font-bold text-red-600">
{Object.values(examplesResults).filter(r => r?.success === false).length}
</p>
<p className="text-sm text-muted-foreground"></p>
</div>
<div>
<p className="text-2xl font-bold text-blue-600">
{apiExamples.length}
</p>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}