Ver código fonte

加 api test

yourname 5 meses atrás
pai
commit
a13a74f370

+ 267 - 0
src/client/member/components/ApiTester.tsx

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

+ 66 - 0
src/client/member/components/ApiTesterFileUpload.tsx

@@ -0,0 +1,66 @@
+import React, { useState } from 'react';
+import { Upload, Button, message } from 'antd';
+import { UploadOutlined } from '@ant-design/icons';
+import type { UploadFile } from 'antd';
+
+interface ApiTesterFileUploadProps {
+  onBase64Generated: (base64: string) => void;
+}
+
+const ApiTesterFileUpload: React.FC<ApiTesterFileUploadProps> = ({ onBase64Generated }) => {
+  const [fileList, setFileList] = useState<UploadFile[]>([]);
+  const [loading, setLoading] = useState(false);
+
+  const convertToBase64 = (file: File): Promise<string> => {
+    return new Promise((resolve, reject) => {
+      const reader = new FileReader();
+      reader.readAsDataURL(file);
+      reader.onload = () => resolve(reader.result as string);
+      reader.onerror = error => reject(error);
+    });
+  };
+
+  const uploadProps = {
+    beforeUpload: async (file: File) => {
+      const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || // xlsx
+                     file.type === 'application/vnd.ms-excel' || // xls
+                     /\.(xlsx|xls)$/.test(file.name.toLowerCase()); // 通过文件扩展名判断
+      
+      if (!isExcel) {
+        message.error('只能上传 Excel 格式的文件(.xls 或 .xlsx)!');
+        return false;
+      }
+
+      setLoading(true);
+      try {
+        const base64 = await convertToBase64(file);
+        onBase64Generated(base64);
+        message.success(`${file.name} 已转换为 Base64 格式`);
+      } catch (error) {
+        message.error('文件转换失败');
+        console.error(error);
+      } finally {
+        setLoading(false);
+      }
+      
+      return false;
+    },
+    fileList,
+    onChange: ({ fileList }: { fileList: UploadFile[] }) => {
+      setFileList(fileList);
+    },
+    accept: '.xlsx,.xls',
+    maxCount: 1,
+    showUploadList: true
+  };
+
+  return (
+    <Upload {...uploadProps}>
+      <Button icon={<UploadOutlined />} loading={loading}>
+        选择 Excel 文件并转换为 Base64
+      </Button>
+    </Upload>
+  );
+};
+
+export default ApiTesterFileUpload; 

+ 6 - 10
src/client/member/components/TemplateSelector.tsx

@@ -2,6 +2,7 @@ import React from 'react';
 import { Select, Spin, message } from 'antd';
 import type { Template } from '@/share/exceltypes';
 import { useQuery } from '@tanstack/react-query';
+import { excelTemplateClient } from '@/client/api';
 
 interface TemplateSelectorProps {
   onTemplateSelected: (templateId: string) => void;
@@ -21,24 +22,19 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
   disabled = false,
   required = true
 }) => {
-  const { requestWithToken } = useApiRequest();
   
   // 使用 React Query 获取模板数据
   const { data, isLoading, isError } = useQuery({
     queryKey: ['templates'],
     queryFn: async () => {
       try {
-        const result = await requestWithToken({
-          url: '/v1/member/templates',
-          method: 'GET'
-        });
-        
-        if (result.success && Array.isArray(result.data)) {
-          return result.data as Template[];
-        } else {
+        const res = await excelTemplateClient.$get({ query: { page:1, pageSize: 100 }});
+        if (res.status !== 200) {
           message.error('获取模板列表失败');
           return [];
         }
+        const templates = await res.json();
+        return templates.data as Template[];
       } catch (error) {
         console.error('获取模板列表错误:', error);
         message.error('获取模板列表失败');
@@ -65,7 +61,7 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
       >
         {data?.map(template => (
           <Select.Option key={template.id} value={String(template.id)}>
-            {template.template_name}
+            {template.templateName}
           </Select.Option>
         ))}
       </Select>

+ 73 - 0
src/client/member/pages/ApiPlayground.tsx

@@ -0,0 +1,73 @@
+import { Card, Typography, Alert, Button } from 'antd';
+import { ApiTester } from '../components/ApiTester';
+import { FileExcelOutlined } from '@ant-design/icons';
+import { Link } from 'react-router-dom';
+const { Title, Paragraph, Text } = Typography;
+
+function ApiPlayground() {
+  return (
+    <div className="p-3 h-full space-y-4">
+        <Card title="Excel转JSON API 说明" className="shadow-sm">
+          <Typography>
+            <div className="flex justify-between items-center mb-4">
+              <Title level={4}>API基本信息</Title>
+              <Link to="/member/templates">
+                <Button type="primary" icon={<FileExcelOutlined />}>
+                  管理Excel模板
+                </Button>
+              </Link>
+            </div>
+            <Paragraph>
+              <ul>
+                <li><Text strong>接口地址:</Text> /api/v1/convert</li>
+                <li><Text strong>请求方法:</Text> POST</li>
+                <li><Text strong>认证方式:</Text> API密钥 (X-API-Key 请求头)</li>
+                <li><Text strong>内容类型:</Text> application/json</li>
+              </ul>
+            </Paragraph>
+
+            <Title level={4}>使用方法</Title>
+            <Paragraph>
+              API调用需要同时提供以下两个<Text mark>必填</Text>参数:
+            </Paragraph>
+            <Paragraph>
+              <ul>
+                <li><Text strong>模板ID (templateId):</Text> 必须选择一个已创建的Excel模板</li>
+                <li><Text strong>Excel数据 (input):</Text> 提供Excel文件的Base64编码或URL</li>
+              </ul>
+            </Paragraph>
+            <Paragraph>
+              使用步骤:
+            </Paragraph>
+            <Paragraph>
+              <ol>
+                <li>从下拉列表中选择一个Excel模板</li>
+                <li>上传Excel文件或提供Excel文件URL作为input参数</li>
+                <li>点击"发送请求"按钮测试API</li>
+              </ol>
+            </Paragraph>
+
+            <Alert
+              type="warning"
+              message="注意事项"
+              description={
+                <div>
+                  <p><Text strong>templateId</Text>和<Text strong>input</Text>参数均为<Text mark>必填</Text>,缺少任何一项将导致请求失败。</p>
+                  <p>请确保上传的Excel文件结构与所选模板匹配,否则可能导致解析结果不正确。</p>
+                  <p>如果您需要处理新的Excel格式,请先<Link to="/member/template/add">创建一个模板</Link>以便使用。</p>
+                </div>
+              }
+            />
+          </Typography>
+        </Card>
+
+        <Card title="API调试" className="shadow-sm">
+          <ApiTester />
+        </Card>
+      {/* <Space direction="vertical" size="large" style={{ width: '100%' }}>
+      </Space> */}
+    </div>
+  );
+} 
+
+export default ApiPlayground