Selaa lähdekoodia

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

microrain 5 kuukautta sitten
vanhempi
sitoutus
87f78b9038

+ 8 - 0
src/api/modules/apiHub.ts

@@ -33,5 +33,13 @@ export default {
     resetSecret: (id: number) => put('/client/reset-secret', { id }),
     getApis: (clientId: string) => get('/client/apis', { clientId }),
     save_apis: (clientId: string, apiKeys: string[]) => post('/client/save_apis', { clientId, apiKeys })
+  },
+  plugin: {
+    list: (params: object) => get('/plugin/list', params),
+    get: (id: number) => get(`/plugin/get`, { id }),
+    add: (data: object) => post('/plugin/add', data),
+    edit: (data: object) => put('/plugin/edit', data),
+    delete: (ids: number[]) => del('/plugin/delete', { ids }),
+    test: (data: object) => post('/plugin/test', data)
   }
 }

+ 551 - 0
src/views/apihub/component/PluginEditor.vue

@@ -0,0 +1,551 @@
+<template>
+  <div class="plugin-editor-wrapper" :style="{ height: height + 'px' }">
+    <div class="editor-container">
+      <div class="editor-toolbar" v-if="!readonly">
+        <el-tooltip content="格式化代码" placement="top" v-if="['JavaScript', 'LUA'].includes(language)">
+          <el-button type="primary" link @click="formatCode">
+            <el-icon><Operation /></el-icon>
+          </el-button>
+        </el-tooltip>
+
+        <el-dropdown @command="handleSnippetSelect" trigger="click" v-if="['JavaScript', 'LUA'].includes(language)">
+          <el-button type="primary" link>
+            <el-icon><Document /></el-icon>
+            <span>插入代码片段</span>
+          </el-button>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item v-for="(snippet, index) in snippets"
+                              :key="index"
+                              :command="snippet.code">
+                {{ snippet.name }}
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
+
+      <!-- 当Monaco编辑器加载失败时显示的备用文本区 -->
+      <el-input
+        v-if="useTextarea"
+        v-model="codeValue"
+        type="textarea"
+        :autosize="{ minRows: 10, maxRows: 20 }"
+        :readonly="readonly"
+        :placeholder="getPlaceholder()"
+        class="code-textarea"
+        @input="handleTextareaInput"
+      />
+
+      <!-- Monaco编辑器容器 -->
+      <div v-else ref="editorContainer" class="monaco-editor-container"></div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, onMounted, onBeforeUnmount, computed, nextTick, markRaw } from 'vue';
+import { ElMessage } from 'element-plus';
+import { Operation, Document } from '@element-plus/icons-vue';
+
+// 是否使用textarea作为降级方案
+const useTextarea = ref(false);
+// 延迟加载Monaco编辑器,避免直接导入可能带来的问题
+let monaco: any = null;
+
+// 在组件挂载后动态加载Monaco编辑器
+const loadMonaco = async () => {
+  try {
+    // 动态导入Monaco编辑器
+    monaco = await import('monaco-editor');
+    // 成功加载后的处理
+    nextTick(() => {
+      initMonacoEditor();
+    });
+  } catch (e) {
+    // 如果加载失败,使用textarea作为备选方案
+    useTextarea.value = true;
+    // 在控制台记录错误,但不提示给用户
+    // console.error('Monaco编辑器加载失败,使用备选文本编辑器', e);
+  }
+};
+
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: ''
+  },
+  height: {
+    type: Number,
+    default: 300
+  },
+  language: {
+    type: String,
+    default: 'JavaScript',
+    validator: (value: string) => ['JavaScript', 'LUA', 'Go'].includes(value)
+  },
+  readonly: {
+    type: Boolean,
+    default: false
+  }
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+// 编辑器相关引用
+const codeValue = ref(props.modelValue);
+const editorContainer = ref(null);
+const editorInstance = ref(null);
+const isFormatting = ref(false); // 格式化状态标记
+const isInserting = ref(false); // 插入片段状态标记
+
+// JavaScript代码片段
+const jsSnippets = [
+  {
+    name: '前置插件模板',
+    code: `// 前置插件模板
+// 参数: ctx - 上下文对象,包含请求信息
+// 返回: 修改后的上下文对象
+function beforePlugin(ctx) {
+  // 这里可以访问和修改ctx中的数据
+  // 例如: ctx.request.headers, ctx.request.params等
+  
+  console.log("执行前置插件逻辑");
+  
+  // 可以修改请求参数
+  if (ctx.request && ctx.request.params) {
+    ctx.request.params.addedByPlugin = true;
+  }
+  
+  // 返回修改后的上下文
+  return ctx;
+}`
+  },
+  {
+    name: '后置插件模板',
+    code: `// 后置插件模板
+// 参数: ctx - 上下文对象,包含请求和响应信息
+// 返回: 修改后的上下文对象
+function afterPlugin(ctx) {
+  // 这里可以访问和修改ctx中的数据
+  // 例如: ctx.response.data, ctx.response.headers等
+  
+  console.log("执行后置插件逻辑");
+  
+  // 可以修改响应数据
+  if (ctx.response && ctx.response.data) {
+    ctx.response.data.processedByPlugin = true;
+  }
+  
+  // 返回修改后的上下文
+  return ctx;
+}`
+  },
+  {
+    name: '数据转换',
+    code: `// 数据转换示例
+function transform(data) {
+  if (Array.isArray(data)) {
+    return data.map(item => ({
+      ...item,
+      transformed: true
+    }));
+  }
+  
+  return {
+    ...data,
+    transformed: true
+  };
+}`
+  },
+  {
+    name: '参数验证',
+    code: `// 参数验证示例
+function validateParams(params) {
+  if (!params.id) {
+    throw new Error("缺少必要参数: id");
+  }
+  
+  if (params.type && !["A", "B", "C"].includes(params.type)) {
+    throw new Error("type参数值无效,必须是A、B或C之一");
+  }
+  
+  return true;
+}`
+  }
+];
+
+// LUA代码片段
+const luaSnippets = [
+  {
+    name: '前置插件模板',
+    code: `-- 前置插件模板
+-- 参数: ctx - 上下文对象,包含请求信息
+-- 返回: 修改后的上下文对象
+function before_plugin(ctx)
+  -- 这里可以访问和修改ctx中的数据
+  -- 例如: ctx.request.headers, ctx.request.params等
+  
+  print("执行前置插件逻辑")
+  
+  -- 可以修改请求参数
+  if ctx.request and ctx.request.params then
+    ctx.request.params.added_by_plugin = true
+  end
+  
+  -- 返回修改后的上下文
+  return ctx
+end
+
+return before_plugin`
+  },
+  {
+    name: '后置插件模板',
+    code: `-- 后置插件模板
+-- 参数: ctx - 上下文对象,包含请求和响应信息
+-- 返回: 修改后的上下文对象
+function after_plugin(ctx)
+  -- 这里可以访问和修改ctx中的数据
+  -- 例如: ctx.response.data, ctx.response.headers等
+  
+  print("执行后置插件逻辑")
+  
+  -- 可以修改响应数据
+  if ctx.response and ctx.response.data then
+    ctx.response.data.processed_by_plugin = true
+  end
+  
+  -- 返回修改后的上下文
+  return ctx
+end
+
+return after_plugin`
+  },
+  {
+    name: '数据转换',
+    code: `-- 数据转换示例
+function transform(data)
+  if type(data) == "table" then
+    -- 判断是否是数组
+    if #data > 0 then
+      local result = {}
+      for i, item in ipairs(data) do
+        item.transformed = true
+        table.insert(result, item)
+      end
+      return result
+    else
+      -- 处理普通对象
+      data.transformed = true
+      return data
+    end
+  end
+  
+  return data
+end`
+  },
+  {
+    name: '参数验证',
+    code: `-- 参数验证示例
+function validate_params(params)
+  if not params.id then
+    error("缺少必要参数: id")
+  end
+  
+  if params.type then
+    local valid = false
+    for _, v in ipairs({"A", "B", "C"}) do
+      if params.type == v then
+        valid = true
+        break
+      end
+    end
+    
+    if not valid then
+      error("type参数值无效,必须是A、B或C之一")
+    end
+  end
+  
+  return true
+end`
+  }
+];
+
+// Go插件路径示例
+const goSnippets = [
+  {
+    name: 'Go插件路径示例',
+    code: 'plugins/my_plugin.so'
+  }
+];
+
+// 使用计算属性,根据当前语言动态返回相应的代码片段
+const snippets = computed(() => {
+  switch (props.language) {
+    case 'JavaScript':
+      return jsSnippets;
+    case 'LUA':
+      return luaSnippets;
+    case 'Go':
+      return goSnippets;
+    default:
+      return jsSnippets;
+  }
+});
+
+// 获取编辑器语言
+const getEditorLanguage = () => {
+  switch (props.language) {
+    case 'JavaScript':
+      return 'javascript';
+    case 'LUA':
+      return 'lua';
+    case 'Go':
+      // 对于Go路径,我们使用普通文本编辑器
+      return 'plaintext';
+    default:
+      return 'javascript';
+  }
+};
+
+// 监听值变化
+watch(() => props.modelValue, (newValue) => {
+  if (newValue !== codeValue.value) {
+    codeValue.value = newValue;
+    if (editorInstance.value) {
+      // 使用安全的方式更新编辑器内容
+      const model = editorInstance.value.getModel();
+      if (model) {
+        model.setValue(newValue);
+      }
+    }
+  }
+});
+
+// 监听语言变化
+watch(() => props.language, () => {
+  // 语言变化时重新初始化编辑器
+  nextTick(() => {
+    initMonacoEditor();
+  });
+});
+
+// 完全禁用Monaco编辑器的worker加载
+// 这种方式确保不会尝试加载任何worker脚本
+if (typeof window !== 'undefined') {
+  window.MonacoEnvironment = {
+    // 创建一个空的worker,不尝试加载任何脚本
+    getWorker: function() {
+      // 返回一个空的worker
+      return {
+        addEventListener: function() {},
+        removeEventListener: function() {},
+        postMessage: function() {},
+        terminate: function() {}
+      };
+    }
+  };
+}
+
+// 初始化Monaco编辑器
+const initMonacoEditor = () => {
+  if (!editorContainer.value || !monaco) return;
+
+  try {
+    // 清理现有实例
+    if (editorInstance.value) {
+      editorInstance.value.dispose();
+      editorInstance.value = null;
+    }
+
+    // 确保容器是空的
+    if (editorContainer.value.childNodes.length > 0) {
+      editorContainer.value.innerHTML = '';
+    }
+
+    // 禁用所有worker,防止加载失败
+    if (typeof window !== 'undefined' && !window.MonacoEnvironment) {
+      window.MonacoEnvironment = {
+        getWorkerUrl: function() { return ''; }
+      };
+    }
+    
+    // 创建编辑器实例
+    // 非常简化的编辑器配置,只保留最基本的功能
+    const editor = monaco.editor.create(editorContainer.value, {
+      value: codeValue.value || '',
+      language: getEditorLanguage(),
+      theme: 'vs',
+      automaticLayout: true,
+      minimap: { enabled: false },
+      readOnly: props.readonly
+    });
+
+    editorInstance.value = markRaw(editor);
+
+    // 使用节流处理内容变化
+    const handleContentChange = () => {
+      const currentEditor = editorInstance.value;
+      if (!currentEditor) return;
+      const value = currentEditor.getValue();
+      codeValue.value = value;
+      emit('update:modelValue', value);
+    };
+    const changeModelDisposable = editorInstance.value.onDidChangeModelContent(handleContentChange);
+
+    // 在组件销毁时清理事件监听器
+    onBeforeUnmount(() => {
+      if (changeModelDisposable) {
+        try {
+          changeModelDisposable.dispose();
+        } catch (e) {
+          // Error handling if needed
+        }
+      }
+      
+      // 确保也清理 editorInstance
+      if (editorInstance.value) {
+        try {
+          editorInstance.value.dispose();
+        } catch (e) {
+          // Error handling if needed
+        }
+        editorInstance.value = null;
+      }
+    });
+  } catch (e) {
+    // 初始化编辑器失败
+  }
+};
+
+// 格式化代码
+const formatCode = async () => {
+  if (isFormatting.value || !editorInstance.value) return;
+  
+  isFormatting.value = true;
+  try {
+    const currentValue = editorInstance.value.getValue();
+    if (!currentValue) {
+      isFormatting.value = false;
+      return;
+    }
+
+    // 使用Monaco编辑器的格式化功能
+    await editorInstance.value.getAction('editor.action.formatDocument')?.run();
+    
+    // 触发内容更新
+    codeValue.value = editorInstance.value.getValue();
+    emit('update:modelValue', codeValue.value);
+    ElMessage.success('格式化成功');
+  } catch (error) {
+    // 格式化失败
+    ElMessage.error('格式化失败');
+  } finally {
+    isFormatting.value = false;
+  }
+};
+
+// 处理代码片段选择 - 使用防抖动处理
+const handleSnippetSelect = (snippet: string) => {
+  if (isInserting.value || !editorInstance.value) return;
+  
+  isInserting.value = true;
+  try {
+    // 获取当前编辑器的位置
+    const position = editorInstance.value.getPosition();
+    
+    // 创建一个编辑操作
+    const edit = {
+      range: {
+        startLineNumber: position.lineNumber,
+        startColumn: position.column,
+        endLineNumber: position.lineNumber,
+        endColumn: position.column
+      },
+      text: snippet
+    };
+    
+    // 应用编辑操作
+    editorInstance.value.executeEdits('snippet-insert', [edit]);
+    
+    // 触发内容更新
+    codeValue.value = editorInstance.value.getValue();
+    emit('update:modelValue', codeValue.value);
+  } catch (error) {
+    // 插入代码片段失败
+  } finally {
+    isInserting.value = false;
+  }
+};
+
+// 添加备用文本编辑器的处理方法
+const handleTextareaInput = () => {
+  emit('update:modelValue', codeValue.value);
+};
+
+// 获取适合当前语言的占位符文本
+const getPlaceholder = () => {
+  switch (props.language) {
+    case 'JavaScript':
+      return '请输入JavaScript插件代码';
+    case 'LUA':
+      return '请输入LUA插件代码';
+    case 'Go':
+      return '请输入Go插件路径,如 plugins/my_plugin.so';
+    default:
+      return '请输入插件代码';
+  }
+};
+
+// 组件挂载后的生命周期钩子
+onMounted(() => {
+  // 延迟加载 Monaco 编辑器并初始化
+  setTimeout(() => {
+    loadMonaco();
+  }, 300);
+});
+
+// 组件销毁前
+onBeforeUnmount(() => {
+  // 清理编辑器实例
+  if (editorInstance.value) {
+    try {
+      editorInstance.value.dispose();
+      editorInstance.value = null;
+    } catch (e) {
+      // 忽略错误
+    }
+  }
+});
+</script>
+
+<style scoped>
+.plugin-editor-wrapper {
+  width: 100%;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+}
+
+.editor-container {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.editor-toolbar {
+  display: flex;
+  align-items: center;
+  padding: 4px 8px;
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #dcdfe6;
+}
+
+.monaco-editor-container {
+  flex: 1;
+  overflow: hidden;
+}
+</style>

+ 1 - 1
src/views/apihub/component/edit.vue

@@ -91,7 +91,7 @@
 
             <el-form-item label="访问权限" prop="private">
               <el-radio-group v-model="formData.private">
-                <el-radio :label="0">开API</el-radio>
+                <el-radio :label="0">开API</el-radio>
                 <el-radio :label="1">私有API</el-radio>
               </el-radio-group>
             </el-form-item>

+ 517 - 0
src/views/apihub/plugin.vue

@@ -0,0 +1,517 @@
+<template>
+  <div class="page">
+    <div class="plugin-container">
+      <el-card shadow="never">
+        <div class="plugin-header">
+          <h3>API插件管理</h3>
+        </div>
+        <el-form :model="params" inline ref="queryRef">
+          <el-form-item label="关键字" prop="keyWord">
+            <el-input v-model="params.keyWord" placeholder="输入插件名称或描述" clearable style="width: 170px" @keyup.enter.native="getList(1)" />
+          </el-form-item>
+          <el-form-item label="类型" prop="type">
+            <el-select v-model="params.type" placeholder="请选择类型" clearable style="width: 140px">
+              <el-option label="全部" value="" />
+              <!-- <el-option label="Go" value="Go" /> -->
+              <el-option label="JavaScript" value="JavaScript" />
+              <el-option label="LUA" value="LUA" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="分类" prop="category">
+            <el-select v-model="params.category" placeholder="请选择分类" clearable style="width: 140px">
+              <el-option label="全部" value="" />
+              <el-option label="前置插件" value="Before" />
+              <el-option label="后置插件" value="After" />
+            </el-select>
+          </el-form-item>
+          <!-- <el-form-item label="日期范围" prop="dateRange">
+            <el-date-picker v-model="params.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 240px" />
+          </el-form-item> -->
+          <el-form-item>
+            <el-button type="primary" class="ml10" @click="getList(1)">
+              <el-icon>
+                <ele-Search />
+              </el-icon>
+              查询
+            </el-button>
+            <el-button type="primary" @click="addOrEdit()" v-auth="'add'">
+              <el-icon>
+                <ele-Plus />
+              </el-icon>
+              新增插件
+            </el-button>
+          </el-form-item>
+        </el-form>
+        <el-table :data="tableData" style="width: 100%" v-loading="loading" row-key="id">
+          <el-table-column type="selection" width="40" align="center" />
+          <el-table-column prop="name" label="插件名称" min-width="140" show-overflow-tooltip></el-table-column>
+          <el-table-column prop="type" label="类型" width="120" align="center">
+            <template #default="scope">
+              <el-tag size="small" type="success" v-if="scope.row.type === 'Go'">Go</el-tag>
+              <el-tag size="small" type="primary" v-else-if="scope.row.type === 'JavaScript'">JavaScript</el-tag>
+              <el-tag size="small" type="warning" v-else-if="scope.row.type === 'LUA'">LUA</el-tag>
+              <span v-else>{{ scope.row.type }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="category" label="分类" width="120" align="center">
+            <template #default="scope">
+              <el-tag size="small" type="info" v-if="scope.row.category === 'Before'">前置插件</el-tag>
+              <el-tag size="small" type="info" v-else-if="scope.row.category === 'After'">后置插件</el-tag>
+              <span v-else>{{ scope.row.category }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="description" label="描述" show-overflow-tooltip></el-table-column>
+          <!-- <el-table-column prop="createdAt" label="创建时间" width="160" align="center"></el-table-column> -->
+          <el-table-column label="操作" width="250" align="center" fixed="right">
+            <template #default="scope">
+              <div class="flex-row">
+                <el-button size="small" text type="primary" @click="viewDetail(scope.row)" v-auth="'view'">查看</el-button>
+                <el-button size="small" text type="warning" @click="addOrEdit(scope.row)" v-auth="'edit'">编辑</el-button>
+                <el-button size="small" text type="success" @click="testPlugin(scope.row)" v-auth="'test'">测试</el-button>
+                <el-button size="small" text type="danger" @click="deletePlugin(scope.row)" v-auth="'delete'">删除</el-button>
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <pagination v-if="params.total" :total="params.total" v-model:page="params.pageNum" v-model:limit="params.pageSize" @pagination="getList()" />
+      </el-card>
+    </div>
+
+    <!-- 插件表单弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="formTitle" width="800px" destroy-on-close>
+      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+        <el-form-item label="插件名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入插件名称" />
+        </el-form-item>
+        <el-form-item label="插件类型" prop="type">
+          <el-select v-model="form.type" placeholder="请选择插件类型" style="width: 100%">
+            <el-option label="Go" value="Go" />
+            <el-option label="JavaScript" value="JavaScript" />
+            <el-option label="LUA" value="LUA" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="插件分类" prop="category">
+          <el-select v-model="form.category" placeholder="请选择插件分类" style="width: 100%">
+            <el-option label="前置插件" value="Before" />
+            <el-option label="后置插件" value="After" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="插件内容" prop="content">
+          <plugin-editor v-model="form.content" :height="300" :language="form.type" />
+          <div class="form-tip">
+            <template v-if="form.type === 'Go'">插件内容为Go插件的路径</template>
+            <template v-else>插件内容为脚本代码</template>
+          </div>
+        </el-form-item>
+        <el-form-item label="描述" prop="description">
+          <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入描述信息" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitForm" :loading="submitLoading">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 插件详情弹窗 -->
+    <el-dialog v-model="detailVisible" title="插件详情" width="800px" destroy-on-close>
+      <el-descriptions :column="1" border>
+        <el-descriptions-item label="插件名称">{{ detail.name }}</el-descriptions-item>
+        <el-descriptions-item label="插件类型">
+          <el-tag size="small" type="success" v-if="detail.type === 'Go'">Go</el-tag>
+          <el-tag size="small" type="primary" v-else-if="detail.type === 'JavaScript'">JavaScript</el-tag>
+          <el-tag size="small" type="warning" v-else-if="detail.type === 'LUA'">LUA</el-tag>
+          <span v-else>{{ detail.type }}</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="插件分类">
+          <el-tag size="small" type="info" v-if="detail.category === 'Before'">前置插件</el-tag>
+          <el-tag size="small" type="info" v-else-if="detail.category === 'After'">后置插件</el-tag>
+          <span v-else>{{ detail.category }}</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="插件内容">
+          <div class="code-preview">
+            <plugin-editor v-model="detail.content" :height="200" :language="detail.type" :readonly="true" />
+          </div>
+        </el-descriptions-item>
+        <el-descriptions-item label="描述">{{ detail.description || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ detail.createdAt }}</el-descriptions-item>
+        <el-descriptions-item label="更新时间">{{ detail.updatedAt }}</el-descriptions-item>
+      </el-descriptions>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="detailVisible = false">关闭</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 插件测试弹窗 -->
+    <el-dialog v-model="testVisible" title="插件测试" width="800px" 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格式" />
+          </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-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>
+        </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>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, onMounted } from "vue";
+import { ElMessageBox, ElMessage } from "element-plus";
+import api from "/@/api/modules/apiHub";
+import PluginEditor from "./component/PluginEditor.vue";
+
+// 定义插件接口类型
+interface PluginInfo {
+  id?: number;
+  name: string;
+  type: string;
+  category: string;
+  content: string;
+  description?: string;
+  createdAt?: string;
+  updatedAt?: string;
+}
+
+// 引用组件
+const queryRef = ref();
+const formRef = ref();
+const testFormRef = ref();
+
+// 查询参数
+const params = reactive({
+  keyWord: '',
+  type: '',
+  category: '',
+  dateRange: [],
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+});
+
+// 表格数据
+const tableData = ref<PluginInfo[]>([]);
+const loading = ref(false);
+
+// 获取插件列表
+const getList = async (pageNum?: number) => {
+  if (pageNum) {
+    params.pageNum = pageNum;
+  }
+  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;
+      // 数据格式不符合预期
+    }
+  } catch (error) {
+    // 获取插件列表失败
+    tableData.value = [];
+    params.total = 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 表单相关
+const dialogVisible = ref(false);
+const formTitle = ref('');
+const submitLoading = ref(false);
+const form = reactive<PluginInfo>({
+  name: '',
+  type: 'JavaScript',
+  category: 'Before',
+  content: '',
+  description: ''
+});
+
+// 表单验证规则
+const rules = {
+  name: [{ required: true, message: '请输入插件名称', trigger: 'blur' }],
+  type: [{ required: true, message: '请选择插件类型', trigger: 'change' }],
+  category: [{ required: true, message: '请选择插件分类', trigger: 'change' }],
+  content: [{ required: true, message: '请输入插件内容', trigger: 'blur' }]
+};
+
+// 重置查询表单
+const resetQuery = () => {
+  if (queryRef.value) {
+    queryRef.value.resetFields();
+  }
+  params.keyWord = '';
+  params.type = '';
+  params.category = '';
+  params.dateRange = [];
+  getList(1);
+};
+
+// 新增或编辑插件
+const addOrEdit = (row?: PluginInfo) => {
+  resetForm();
+  if (row && row.id) {
+    formTitle.value = '编辑插件';
+    // 获取详情
+    api.plugin.get(row.id).then(res => {
+      Object.assign(form, res.data);
+    });
+  } else {
+    formTitle.value = '新增插件';
+  }
+  dialogVisible.value = true;
+};
+
+// 重置表单
+const resetForm = () => {
+  if (formRef.value) {
+    formRef.value.resetFields();
+  }
+  form.id = undefined;
+  form.name = '';
+  form.type = 'JavaScript';
+  form.category = 'Before';
+  form.content = '';
+  form.description = '';
+};
+
+// 提交表单
+const submitForm = () => {
+  if (!formRef.value) return;
+  
+  formRef.value.validate(async (valid: boolean) => {
+    if (!valid) return;
+    
+    submitLoading.value = true;
+    try {
+      if (form.id) {
+        // 编辑
+        await api.plugin.edit(form);
+        ElMessage.success('编辑成功');
+      } else {
+        // 新增
+        await api.plugin.add(form);
+        ElMessage.success('新增成功');
+      }
+      dialogVisible.value = false;
+      getList();
+    } catch (error) {
+      // 保存插件失败
+    } finally {
+      submitLoading.value = false;
+    }
+  });
+};
+
+// 插件详情
+const detailVisible = ref(false);
+const detail = reactive<PluginInfo>({
+  name: '',
+  type: 'JavaScript',
+  category: 'Before',
+  content: '',
+  description: ''
+});
+
+// 查看插件详情
+const viewDetail = (row: PluginInfo) => {
+  api.plugin.get(row.id).then(res => {
+    Object.assign(detail, res.data);
+    detailVisible.value = true;
+  });
+};
+
+// 删除插件
+const deletePlugin = (row: PluginInfo) => {
+  ElMessageBox.confirm(`确定要删除插件 "${row.name}" 吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    try {
+      await api.plugin.delete([row.id]);
+      ElMessage.success('删除成功');
+      getList();
+    } catch (error) {
+      // 删除插件失败
+    }
+  }).catch(() => {});
+};
+
+// 插件测试相关
+const testVisible = ref(false);
+const testLoading = ref(false);
+const currentTestPluginId = ref<number>(0);
+const testForm = reactive({
+  context: '{}'
+});
+const testResult = reactive({
+  success: false,
+  message: '',
+  data: null
+});
+
+// 打开测试窗口
+const testPlugin = (row: PluginInfo) => {
+  currentTestPluginId.value = row.id;
+  testForm.context = '{}';
+  testResult.success = false;
+  testResult.message = '';
+  testResult.data = null;
+  testVisible.value = true;
+};
+
+// 执行测试
+const runTest = async () => {
+  if (!currentTestPluginId.value) return;
+  
+  testLoading.value = true;
+  try {
+    // 解析上下文数据
+    let contextData = {};
+    try {
+      contextData = JSON.parse(testForm.context);
+    } catch (e) {
+      ElMessage.error('上下文数据格式不正确,请输入有效的JSON');
+      testLoading.value = false;
+      return;
+    }
+    
+    // 发送测试请求
+    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;
+  } catch (error) {
+    // 测试插件失败
+    testResult.success = false;
+    testResult.message = '测试发生错误';
+    testResult.data = null;
+  } finally {
+    testLoading.value = false;
+  }
+};
+
+// 组件挂载后获取数据
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+.plugin-container {
+  width: 100%;
+}
+
+.plugin-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.plugin-header h3 {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 500;
+}
+
+.flex-row {
+  display: flex;
+  flex-wrap: nowrap;
+  justify-content: center;
+}
+
+.code-preview {
+  width: 100%;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+
+.test-container {
+  width: 100%;
+}
+
+.test-result {
+  margin-top: 20px;
+  padding: 16px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+}
+
+.result-title {
+  font-weight: bold;
+  margin-bottom: 10px;
+}
+
+.result-message {
+  margin-top: 10px;
+  padding: 8px;
+  background-color: #fff;
+  border-radius: 4px;
+}
+
+.result-data {
+  margin-top: 10px;
+}
+
+.result-label {
+  font-weight: bold;
+  margin-bottom: 5px;
+}
+
+.result-data pre {
+  background-color: #fff;
+  padding: 8px;
+  border-radius: 4px;
+  white-space: pre-wrap;
+  word-break: break-all;
+  max-height: 200px;
+  overflow-y: auto;
+}
+</style>