Преглед изворни кода

feat:新增APIHUB模块插件管理功能

microrain пре 5 месеци
родитељ
комит
32d0012fe1
1 измењених фајлова са 345 додато и 44 уклоњено
  1. 345 44
      src/views/apihub/plugin.vue

+ 345 - 44
src/views/apihub/plugin.vue

@@ -147,32 +147,73 @@
     </el-dialog>
 
     <!-- 插件测试弹窗 -->
-    <el-dialog v-model="testVisible" title="插件测试" width="800px" destroy-on-close>
+    <el-dialog v-model="testVisible" :title="`插件测试 - ${currentTestPluginName}`" width="900px" destroy-on-close>
       <div class="test-container">
         <el-form :model="testForm" ref="testFormRef" label-width="100px">
           <el-form-item label="上下文数据" prop="context">
-            <el-input v-model="testForm.context" type="textarea" :rows="5" placeholder="请输入测试用的上下文数据,JSON格式" />
+            <div class="context-toolbar">
+              <el-button-group>
+                <el-tooltip content="格式化JSON" placement="top">
+                  <el-button type="primary" :icon="Document" @click="formatJson" plain size="small">格式化</el-button>
+                </el-tooltip>
+                <el-tooltip content="模板帮助你快速创建测试数据" placement="top">
+                  <el-dropdown @command="useTestTemplate" trigger="click">
+                    <el-button type="primary" plain size="small">
+                      使用模板 <el-icon class="el-icon--right"><arrow-down /></el-icon>
+                    </el-button>
+                    <template #dropdown>
+                      <el-dropdown-menu>
+                        <el-dropdown-item v-for="(temp, index) in testTemplates" :key="index" :command="temp.value">
+                          {{ temp.name }}
+                        </el-dropdown-item>
+                      </el-dropdown-menu>
+                    </template>
+                  </el-dropdown>
+                </el-tooltip>
+              </el-button-group>
+            </div>
+            <el-input 
+              v-model="testForm.context" 
+              type="textarea" 
+              :rows="8" 
+              placeholder="请输入测试用的上下文数据,JSON格式。例如:{ &quot;key&quot;: &quot;value&quot; }" 
+              class="json-textarea"
+              :class="{ 'textarea-error': testResult.error }" 
+            />
+            <div v-if="testResult.error" class="error-message">
+              <el-icon><warning /></el-icon> {{ testResult.error }}
+            </div>
           </el-form-item>
         </el-form>
+
         <div class="test-result" v-if="testResult.message">
-          <div class="result-title">测试结果:</div>
-          <el-alert
-            :title="testResult.success ? '测试成功' : '测试失败'"
-            :type="testResult.success ? 'success' : 'error'"
-            :closable="false"
-            show-icon
-          />
+          <div class="result-header">
+            <div class="result-title">测试结果</div>
+            <el-tag :type="testResult.success ? 'success' : 'danger'" effect="dark">
+              {{ testResult.success ? '测试成功' : '测试失败' }}
+            </el-tag>
+          </div>
+          
           <div class="result-message">{{ testResult.message }}</div>
+          
           <div class="result-data" v-if="testResult.data">
-            <div class="result-label">返回数据:</div>
-            <pre>{{ JSON.stringify(testResult.data, null, 2) }}</pre>
+            <div class="result-data-header">
+              <div class="result-label">返回数据</div>
+              <el-button type="primary" link size="small" @click="copyToClipboard(testResult.data)">
+                <el-icon><copy-document /></el-icon> 复制
+              </el-button>
+            </div>
+            <pre class="data-preview">{{ testResult.data }}</pre>
           </div>
         </div>
       </div>
       <template #footer>
         <span class="dialog-footer">
           <el-button @click="testVisible = false">关闭</el-button>
-          <el-button type="primary" @click="runTest" :loading="testLoading">执行测试</el-button>
+          <el-button type="primary" @click="runTest" :loading="testLoading">
+            <el-icon v-if="!testLoading"><video-play /></el-icon>
+            <span>执行测试</span>
+          </el-button>
         </span>
       </template>
     </el-dialog>
@@ -184,6 +225,7 @@ import { ref, reactive, onMounted } from "vue";
 import { ElMessageBox, ElMessage } from "element-plus";
 import api from "/@/api/modules/apiHub";
 import PluginEditor from "./component/PluginEditor.vue";
+import { ArrowDown, Document, Warning, VideoPlay, CopyDocument } from '@element-plus/icons-vue';
 
 // 定义插件接口类型
 interface PluginInfo {
@@ -225,20 +267,56 @@ const getList = async (pageNum?: number) => {
   loading.value = true;
   try {
     const res = await api.plugin.list(params);
-    // 处理返回的数据结构,兼容不同格式
-    if (res.data && Array.isArray(res.data.data)) {
-      // 标准格式:{ data: { data: [...], total: number } }
-      tableData.value = res.data.data;
-      params.total = res.data.total || 0;
-    } else if (res.data && Array.isArray(res.data)) {
-      // 简化格式:{ data: [...], total: number }
-      tableData.value = res.data;
-      params.total = res.total || 0;
-    } else {
-      // 兜底处理
-      tableData.value = [];
-      params.total = 0;
-      // 数据格式不符合预期
+    // 处理API响应数据
+    
+    // 尝试所有可能的数据结构
+    tableData.value = []; // 清空当前数据
+    
+    if (res) {
+      // 1. 检查 res.data
+      if (res.data) {
+        // 1.1 直接是数组
+        if (Array.isArray(res.data)) {
+          tableData.value = res.data;
+          params.total = res.total || res.data.length || 0;
+        }
+        // 1.2 有data属性
+        else if (res.data.data && Array.isArray(res.data.data)) {
+          tableData.value = res.data.data;
+          params.total = res.data.total || res.data.data.length || 0;
+        }
+        // 1.3 有Data属性(首字母大写)
+        else if (res.data.Data && Array.isArray(res.data.Data)) {
+          tableData.value = res.data.Data;
+          params.total = res.data.Total || res.data.Data.length || 0;
+        }
+      }
+      
+      // 2. 检查 res.Data(可能首字母大写)
+      else if (res.Data) {
+        if (Array.isArray(res.Data)) {
+          tableData.value = res.Data;
+          params.total = res.Total || res.Data.length || 0;
+        }
+      }
+    }
+    
+    // 检查表格数据中的属性名称是否首字母大写,如果是,则转换为小写
+    // 这是因为表格组件的prop属性期望的是小写属性名
+    if (tableData.value.length > 0) {
+      tableData.value = tableData.value.map(item => {
+        // 如果有首字母大写的属性,增加小写形式的副本
+        const newItem = {...item};
+        if (newItem.Id !== undefined) newItem.id = newItem.Id;
+        if (newItem.Name !== undefined) newItem.name = newItem.Name;
+        if (newItem.Type !== undefined) newItem.type = newItem.Type;
+        if (newItem.Category !== undefined) newItem.category = newItem.Category;
+        if (newItem.Content !== undefined) newItem.content = newItem.Content;
+        if (newItem.Description !== undefined) newItem.description = newItem.Description;
+        if (newItem.CreatedAt !== undefined) newItem.createdAt = newItem.CreatedAt;
+        if (newItem.UpdatedAt !== undefined) newItem.updatedAt = newItem.UpdatedAt;
+        return newItem;
+      });
     }
   } catch (error) {
     // 获取插件列表失败
@@ -288,7 +366,31 @@ const addOrEdit = (row?: PluginInfo) => {
     formTitle.value = '编辑插件';
     // 获取详情
     api.plugin.get(row.id).then(res => {
-      Object.assign(form, res.data);
+      // 处理API返回数据
+      let pluginData = null;
+      
+      // 检查各种可能的数据结构
+      if (res.data) {
+        pluginData = res.data;
+      } else if (res.Data) {
+        pluginData = res.Data;
+      } else if (res) {
+        pluginData = res;
+      }
+      
+      // 首字母大写转小写处理
+      if (pluginData) {
+        // 如果有首字母大写的属性,添加小写形式
+        if (pluginData.Id !== undefined) pluginData.id = pluginData.Id;
+        if (pluginData.Name !== undefined) pluginData.name = pluginData.Name;
+        if (pluginData.Type !== undefined) pluginData.type = pluginData.Type;
+        if (pluginData.Category !== undefined) pluginData.category = pluginData.Category;
+        if (pluginData.Content !== undefined) pluginData.content = pluginData.Content;
+        if (pluginData.Description !== undefined) pluginData.description = pluginData.Description;
+      }
+      
+      // 将处理后的数据赋值给表单
+      Object.assign(form, pluginData);
     });
   } else {
     formTitle.value = '新增插件';
@@ -350,7 +452,31 @@ const detail = reactive<PluginInfo>({
 // 查看插件详情
 const viewDetail = (row: PluginInfo) => {
   api.plugin.get(row.id).then(res => {
-    Object.assign(detail, res.data);
+    // 处理API返回数据
+    let pluginData = null;
+    
+    // 检查各种可能的数据结构
+    if (res.data) {
+      pluginData = res.data;
+    } else if (res.Data) {
+      pluginData = res.Data;
+    } else if (res) {
+      pluginData = res;
+    }
+    
+    // 首字母大写转小写处理
+    if (pluginData) {
+      // 如果有首字母大写的属性,添加小写形式
+      if (pluginData.Id !== undefined) pluginData.id = pluginData.Id;
+      if (pluginData.Name !== undefined) pluginData.name = pluginData.Name;
+      if (pluginData.Type !== undefined) pluginData.type = pluginData.Type;
+      if (pluginData.Category !== undefined) pluginData.category = pluginData.Category;
+      if (pluginData.Content !== undefined) pluginData.content = pluginData.Content;
+      if (pluginData.Description !== undefined) pluginData.description = pluginData.Description;
+    }
+    
+    // 将处理后的数据赋值给详情对象
+    Object.assign(detail, pluginData);
     detailVisible.value = true;
   });
 };
@@ -376,61 +502,150 @@ const deletePlugin = (row: PluginInfo) => {
 const testVisible = ref(false);
 const testLoading = ref(false);
 const currentTestPluginId = ref<number>(0);
+const currentTestPluginName = ref('');
 const testForm = reactive({
   context: '{}'
 });
 const testResult = reactive({
   success: false,
   message: '',
-  data: null
+  data: null,
+  error: ''
 });
 
+// 测试模板
+const testTemplates = [
+  { name: '空对象', value: '{}' },
+  { name: '简单示例', value: '{\n  "key": "value",\n  "number": 123,\n  "boolean": true\n}' },
+  { name: '请求参数示例', value: '{\n  "headers": {\n    "content-type": "application/json"\n  },\n  "body": {\n    "data": "example"\n  },\n  "query": {\n    "id": 1\n  }\n}' }
+];
+
 // 打开测试窗口
 const testPlugin = (row: PluginInfo) => {
   currentTestPluginId.value = row.id;
+  currentTestPluginName.value = row.name || `插件ID: ${row.id}`;
   testForm.context = '{}';
   testResult.success = false;
   testResult.message = '';
   testResult.data = null;
+  testResult.error = '';
   testVisible.value = true;
 };
 
+// 使用测试模板
+const useTestTemplate = (template: string) => {
+  testForm.context = template;
+};
+
+// 格式化JSON
+const formatJson = () => {
+  try {
+    const obj = JSON.parse(testForm.context);
+    testForm.context = JSON.stringify(obj, null, 2);
+    testResult.error = '';
+  } catch (e) {
+    testResult.error = `JSON格式错误: ${e instanceof Error ? e.message : String(e)}`;
+  }
+};
+
 // 执行测试
 const runTest = async () => {
   if (!currentTestPluginId.value) return;
   
+  // 先格式化JSON
+  try {
+    const parsedJson = JSON.parse(testForm.context);
+    testForm.context = JSON.stringify(parsedJson, null, 2);
+    testResult.error = '';
+  } catch (e) {
+    testResult.error = `JSON格式错误: ${e instanceof Error ? e.message : String(e)}`;
+    ElMessage.error(`JSON格式错误: ${e instanceof Error ? e.message : String(e)}`);
+    return;
+  }
+  
   testLoading.value = true;
+  testResult.success = false;
+  testResult.message = '';
+  testResult.data = null;
+  
   try {
-    // 解析上下文数据
-    let contextData = {};
-    try {
-      contextData = JSON.parse(testForm.context);
-    } catch (e) {
-      ElMessage.error('上下文数据格式不正确,请输入有效的JSON');
-      testLoading.value = false;
-      return;
-    }
-    
     // 发送测试请求
+    const contextData = JSON.parse(testForm.context);
     const res = await api.plugin.test({
       id: currentTestPluginId.value,
       context: contextData
     });
     
-    // 更新测试结果
-    testResult.success = res.data.success;
-    testResult.message = res.data.message;
-    testResult.data = res.data.data;
+    // 处理不同格式的响应
+    if (res.data) {
+      if (typeof res.data.success === 'boolean') {
+        // 标准格式的响应
+        testResult.success = res.data.success;
+        testResult.message = res.data.message || (
+          testResult.success ? '测试成功' : '测试失败'
+        );
+        testResult.data = res.data.data;
+      } else {
+        // 直接返回的数据
+        testResult.success = true;
+        testResult.message = '测试成功';
+        testResult.data = res.data;
+      }
+    } else if (res.success !== undefined) {
+      // 有success字段在根级别的响应
+      testResult.success = res.success;
+      testResult.message = res.message || (
+        testResult.success ? '测试成功' : '测试失败'
+      );
+      testResult.data = res.data;
+    } else {
+      // 其他格式
+      testResult.success = true;
+      testResult.message = '测试成功';
+      testResult.data = res;
+    }
+    
+    // 格式化响应数据显示
+    if (testResult.data && typeof testResult.data === 'object') {
+      testResult.data = JSON.stringify(testResult.data, null, 2);
+    }
   } catch (error) {
     // 测试插件失败
     testResult.success = false;
     testResult.message = '测试发生错误';
-    testResult.data = null;
+    testResult.data = error instanceof Error ? error.message : String(error);
   } finally {
     testLoading.value = false;
   }
 };
 
+// 复制到剪切板
+const copyToClipboard = (text: string) => {
+  try {
+    navigator.clipboard.writeText(text).then(() => {
+      ElMessage.success('已复制到剪切板');
+    }).catch(() => {
+      ElMessage.error('复制失败,请手动复制');
+    });
+  } catch (error) {
+    // 兼容不支持 Clipboard API 的浏览器
+    const textarea = document.createElement('textarea');
+    textarea.textContent = text;
+    textarea.style.position = 'fixed';
+    document.body.appendChild(textarea);
+    textarea.select();
+    
+    try {
+      document.execCommand('copy');
+      ElMessage.success('已复制到剪切板');
+    } catch (err) {
+      ElMessage.error('复制失败,请手动复制');
+    } finally {
+      document.body.removeChild(textarea);
+    }
+  }
+};
+
 // 组件挂载后获取数据
 onMounted(() => {
   getList();
@@ -467,6 +682,92 @@ onMounted(() => {
   border-radius: 4px;
 }
 
+/* 测试界面样式 */
+.test-container {
+  padding: 10px;
+}
+
+.context-toolbar {
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.json-textarea {
+  margin-bottom: 5px;
+  font-family: monospace;
+}
+
+.textarea-error {
+  border-color: #f56c6c;
+}
+
+.error-message {
+  color: #f56c6c;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  margin-top: 5px;
+}
+
+.error-message .el-icon {
+  margin-right: 5px;
+}
+
+.test-result {
+  margin-top: 20px;
+  padding: 15px;
+  background-color: #f8f8f8;
+  border-radius: 4px;
+  border: 1px solid #e6e6e6;
+}
+
+.result-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+}
+
+.result-title {
+  font-weight: bold;
+  font-size: 16px;
+}
+
+.result-message {
+  margin: 10px 0;
+  word-break: break-word;
+}
+
+.result-data {
+  margin-top: 15px;
+}
+
+.result-data-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.result-label {
+  font-weight: bold;
+  color: #606266;
+}
+
+.data-preview {
+  background-color: #f5f7fa;
+  padding: 10px;
+  border-radius: 4px;
+  margin: 0;
+  overflow: auto;
+  max-height: 300px;
+  font-family: monospace;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
 .form-tip {
   font-size: 12px;
   color: #909399;