|
|
@@ -0,0 +1,267 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import { Form, Input, Button, Tabs, Divider, Alert, Typography } from 'antd';
|
|
|
+import { SendOutlined, LoadingOutlined, FileExcelOutlined, FileOutlined } from '@ant-design/icons';
|
|
|
+import TemplateSelector from './TemplateSelector';
|
|
|
+import ApiTesterFileUpload from './ApiTesterFileUpload';
|
|
|
+
|
|
|
+const { TextArea } = Input;
|
|
|
+const { TabPane } = Tabs;
|
|
|
+const { Text } = Typography;
|
|
|
+
|
|
|
+interface ApiTestResult {
|
|
|
+ status: number;
|
|
|
+ statusText: string;
|
|
|
+ headers: Record<string, string>;
|
|
|
+ data: any;
|
|
|
+}
|
|
|
+
|
|
|
+export function ApiTester() {
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [response, setResponse] = useState<ApiTestResult | null>(null);
|
|
|
+ const [endpointUrl, setEndpointUrl] = useState('');
|
|
|
+ const [requestBody, setRequestBody] = useState<string>(JSON.stringify({
|
|
|
+ templateId: "",
|
|
|
+ input: ""
|
|
|
+ }, null, 2));
|
|
|
+ const [apiKey, setApiKey] = useState('excel2json-api-key');
|
|
|
+ const [selectedTemplateId, setSelectedTemplateId] = useState<number | undefined>(undefined);
|
|
|
+ const [uploadedInput, setUploadedInput] = useState('');
|
|
|
+
|
|
|
+ // 在组件挂载时获取当前网站的完整域名
|
|
|
+ useEffect(() => {
|
|
|
+ // 获取当前网站的完整URL前缀
|
|
|
+ const origin = window.location.origin;
|
|
|
+ // 设置完整的API端点URL
|
|
|
+ setEndpointUrl(`${origin}/api/v1/convert`);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 处理模板选择
|
|
|
+ const handleTemplateSelected = (templateId: string) => {
|
|
|
+ setSelectedTemplateId(templateId);
|
|
|
+ try {
|
|
|
+ const currentBody = JSON.parse(requestBody);
|
|
|
+ currentBody.templateId = templateId;
|
|
|
+ setRequestBody(JSON.stringify(currentBody, null, 2));
|
|
|
+ } catch (error) {
|
|
|
+ console.error('解析请求体失败', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理文件上传并生成Base64
|
|
|
+ const handleBase64Generated = (base64: string) => {
|
|
|
+ setUploadedInput(base64);
|
|
|
+ try {
|
|
|
+ const currentBody = JSON.parse(requestBody);
|
|
|
+ currentBody.input = base64;
|
|
|
+ setRequestBody(JSON.stringify(currentBody, null, 2));
|
|
|
+ } catch (error) {
|
|
|
+ console.error('解析请求体失败', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const testApi = async () => {
|
|
|
+ // 验证必填参数
|
|
|
+ try {
|
|
|
+ const body = JSON.parse(requestBody);
|
|
|
+ if (!body.templateId || !body.input) {
|
|
|
+ setResponse({
|
|
|
+ status: 400,
|
|
|
+ statusText: '请求参数错误',
|
|
|
+ headers: {},
|
|
|
+ data: {
|
|
|
+ success: false,
|
|
|
+ message: '请求失败:templateId 和 input 均为必填参数',
|
|
|
+ errors: {
|
|
|
+ templateId: !body.templateId ? '模板ID不能为空' : undefined,
|
|
|
+ input: !body.input ? 'Excel文件数据不能为空' : undefined
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ setResponse({
|
|
|
+ status: 400,
|
|
|
+ statusText: '请求体格式错误',
|
|
|
+ headers: {},
|
|
|
+ data: {
|
|
|
+ success: false,
|
|
|
+ message: 'JSON格式错误,请检查请求体格式'
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const requestOptions: RequestInit = {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'X-API-Key': apiKey
|
|
|
+ },
|
|
|
+ body: requestBody
|
|
|
+ };
|
|
|
+
|
|
|
+ const start = performance.now();
|
|
|
+ const response = await fetch(endpointUrl, requestOptions);
|
|
|
+ const end = performance.now();
|
|
|
+
|
|
|
+ const contentType = response.headers.get('Content-Type') || '';
|
|
|
+ let data;
|
|
|
+
|
|
|
+ if (contentType.includes('application/json')) {
|
|
|
+ data = await response.json();
|
|
|
+ } else {
|
|
|
+ data = await response.text();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取响应头
|
|
|
+ const headers: Record<string, string> = {};
|
|
|
+ response.headers.forEach((value, key) => {
|
|
|
+ headers[key] = value;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加响应时间
|
|
|
+ headers['X-Response-Time'] = `${(end - start).toFixed(2)}ms`;
|
|
|
+
|
|
|
+ setResponse({
|
|
|
+ status: response.status,
|
|
|
+ statusText: response.statusText,
|
|
|
+ headers,
|
|
|
+ data
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ setResponse({
|
|
|
+ status: 500,
|
|
|
+ statusText: 'Error',
|
|
|
+ headers: {},
|
|
|
+ data: { error: (error as Error).message }
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div>
|
|
|
+ <Alert
|
|
|
+ type="info"
|
|
|
+ message="API调用必须同时提供模板ID和Excel文件数据"
|
|
|
+ description="请确保已选择模板并上传了Excel文件或提供了有效的Excel文件URL"
|
|
|
+ showIcon
|
|
|
+ style={{ marginBottom: '16px' }}
|
|
|
+ />
|
|
|
+
|
|
|
+ <Form layout="vertical">
|
|
|
+ <Form.Item label="API端点">
|
|
|
+ <Input
|
|
|
+ value={endpointUrl}
|
|
|
+ onChange={e => setEndpointUrl(e.target.value)}
|
|
|
+ addonBefore="URL"
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item label="API密钥">
|
|
|
+ <Input
|
|
|
+ value={apiKey}
|
|
|
+ onChange={e => setApiKey(e.target.value)}
|
|
|
+ placeholder="请输入API密钥"
|
|
|
+ addonBefore="X-API-Key"
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
+ <Form.Item
|
|
|
+ label={<Text strong>选择模板 (必填)</Text>}
|
|
|
+ required
|
|
|
+ help="选择一个Excel解析模板"
|
|
|
+ >
|
|
|
+ <TemplateSelector
|
|
|
+ onTemplateSelected={handleTemplateSelected}
|
|
|
+ value={selectedTemplateId?.toString()}
|
|
|
+ required={true}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label={<Text strong>上传Excel文件 (必填)</Text>}
|
|
|
+ required
|
|
|
+ help="上传Excel文件或提供文件URL"
|
|
|
+ >
|
|
|
+ <ApiTesterFileUpload onBase64Generated={handleBase64Generated} />
|
|
|
+ </Form.Item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Divider dashed />
|
|
|
+
|
|
|
+ <Form.Item label="请求体 (JSON)">
|
|
|
+ <TextArea
|
|
|
+ value={requestBody}
|
|
|
+ onChange={e => setRequestBody(e.target.value)}
|
|
|
+ rows={8}
|
|
|
+ placeholder="输入JSON请求体"
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ onClick={testApi}
|
|
|
+ icon={loading ? <LoadingOutlined /> : <SendOutlined />}
|
|
|
+ loading={loading}
|
|
|
+ disabled={!selectedTemplateId || !uploadedInput}
|
|
|
+ >
|
|
|
+ 发送请求
|
|
|
+ </Button>
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+
|
|
|
+ {response && (
|
|
|
+ <div className="mt-4">
|
|
|
+ <div className="flex mb-2">
|
|
|
+ <div className={`px-3 py-1 rounded-md text-white font-bold ${
|
|
|
+ response.status >= 200 && response.status < 300 ? 'bg-green-500' : 'bg-red-500'
|
|
|
+ }`}>
|
|
|
+ {response.status} {response.statusText}
|
|
|
+ </div>
|
|
|
+ <div className="ml-4 px-3 py-1 bg-gray-100 rounded-md">
|
|
|
+ {response.headers['X-Response-Time']}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="mt-2 p-4 border rounded bg-gray-50">
|
|
|
+ <h3 className="text-lg font-medium mb-2">响应头</h3>
|
|
|
+ <div className="overflow-x-auto">
|
|
|
+ <table className="min-w-full bg-white">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th className="py-2 px-4 border-b border-gray-200 bg-gray-100 text-left">键</th>
|
|
|
+ <th className="py-2 px-4 border-b border-gray-200 bg-gray-100 text-left">值</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ {Object.entries(response.headers).map(([key, value]) => (
|
|
|
+ <tr key={key}>
|
|
|
+ <td className="py-2 px-4 border-b border-gray-200">{key}</td>
|
|
|
+ <td className="py-2 px-4 border-b border-gray-200">{value}</td>
|
|
|
+ </tr>
|
|
|
+ ))}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="mt-4 p-4 border rounded bg-gray-50">
|
|
|
+ <h3 className="text-lg font-medium mb-2">响应体</h3>
|
|
|
+ <pre className="bg-gray-800 text-white p-4 rounded overflow-x-auto">
|
|
|
+ {typeof response.data === 'object'
|
|
|
+ ? JSON.stringify(response.data, null, 2)
|
|
|
+ : response.data}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|