Quellcode durchsuchen

Merge branch 'professional2' of http://git.mydig.net/Sagoo-Cloud/sagoo-admin-ui into professional2

vera_min vor 5 Monaten
Ursprung
Commit
154237591a

+ 1 - 0
package.json

@@ -57,6 +57,7 @@
     "semver": "7.6.2",
     "sortablejs": "1.14.0",
     "splitpanes": "3.1.1",
+    "sql-formatter": "^15.6.1",
     "uuid": "9.0.0",
     "vform3-builds": "3.0.8",
     "vue": "3.2.37",

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

@@ -23,5 +23,23 @@ export default {
     edit: (data: object) => put('/datasource/edit', data),
     delete: (ids: number[]) => del('/datasource/delete', { ids }),
     test: (data: object) => post('/datasource/test', data),
+  },
+  client: {
+    list: (params: object) => get('/client/list', params),
+    get: (id: number) => get(`/client/get`, { id }),
+    add: (data: object) => post('/client/add', data),
+    edit: (data: object) => put('/client/edit', data),
+    delete: (ids: number[]) => del('/client/delete', { ids }),
+    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)
   }
 }

+ 8 - 6
src/views/apihub/apilist.vue

@@ -55,16 +55,16 @@
           <div class="current-group" v-else>全部API</div>
         </div>
         <el-form :model="params" inline ref="queryRef">
-          <el-form-item label="API名称" prop="keyWord">
-            <el-input v-model="params.keyWord" placeholder="请输入API名称" clearable style="width: 150px" @keyup.enter.native="getList(1)" />
+          <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="dataSourceKey">
-            <el-select v-model="params.dataSourceKey" placeholder="请选择数据源" clearable style="width: 180px">
+            <el-select v-model="params.dataSourceKey" placeholder="请选择数据源" clearable style="width: 170px">
               <el-option v-for="item in dataSources" :key="item.dsKey" :label="item.name" :value="item.dsKey" />
             </el-select>
           </el-form-item>
           <el-form-item label="状态" prop="status">
-            <el-select v-model="params.status" placeholder="请选择状态" clearable style="width: 80px">
+            <el-select v-model="params.status" placeholder="请选择状态" clearable style="width: 90px">
               <el-option label="全部" value="" />
               <el-option label="草稿" value="Draft" />
               <el-option label="已发布" value="Published" />
@@ -115,8 +115,8 @@
 <!--              <span v-else>{{ scope.row.sqlType }}</span>-->
 <!--            </template>-->
 <!--          </el-table-column>-->
-          <el-table-column prop="version" label="版本" width="80" align="center"></el-table-column>
-          <el-table-column prop="description" label="描述" width="140" />
+<!--          <el-table-column prop="version" label="版本" width="80" align="center"></el-table-column>-->
+          <el-table-column prop="description" label="描述" width="140" show-overflow-tooltip/>
 
           <el-table-column prop="status" label="状态" width="100" align="center">
             <template #default="scope">
@@ -186,6 +186,7 @@ interface ApiDefinition {
   returnFormat: string;
   version: string;
   status: string;
+  private: string;
   plugins?: any[];
   description?: string;
   createdAt?: string;
@@ -220,6 +221,7 @@ const { params, tableData, getList, loading } = useSearch<ApiDefinition[]>(api.l
   keyWord: "",
   dataSourceKey: "",
   status: "",
+  private: "-1",
   dateRange: [],
   orderBy: "",
 });

+ 592 - 0
src/views/apihub/client.vue

@@ -0,0 +1,592 @@
+<template>
+  <div class="page">
+    <div class="client-container">
+      <el-card shadow="never">
+        <div class="client-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="status">
+            <el-select v-model="params.status" placeholder="请选择状态" clearable style="width: 120px">
+              <el-option label="全部" value="" />
+              <el-option label="激活" value="Active" />
+              <el-option label="未激活" value="Inactive" />
+            </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 @click="resetQuery()">-->
+<!--              <el-icon>-->
+<!--                <ele-Refresh />-->
+<!--              </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="clientId" label="客户端标识" min-width="140" show-overflow-tooltip></el-table-column>
+          <el-table-column prop="name" label="客户端名称" min-width="140" show-overflow-tooltip></el-table-column>
+          <el-table-column prop="remark" label="备注"  show-overflow-tooltip></el-table-column>
+          <el-table-column prop="status" label="状态" width="100" align="center">
+            <template #default="scope">
+              <el-tag size="small" type="success" v-if="scope.row.status === 'Active'">激活</el-tag>
+              <el-tag size="small" type="info" v-else-if="scope.row.status === 'Inactive'">未激活</el-tag>
+              <span v-else>{{ scope.row.status }}</span>
+            </template>
+          </el-table-column>
+<!--          <el-table-column prop="createdAt" label="创建时间" width="160" align="center"></el-table-column>-->
+          <el-table-column label="操作" width="300" 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="primary" @click="manageApiAuth(scope.row)" v-auth="'edit'">API权限</el-button>
+                <el-button size="small" text type="success" @click="resetSecret(scope.row)" v-auth="'reset_secret'">重置密钥</el-button>
+                <el-button size="small" text type="danger" @click="deleteClient(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="600px" destroy-on-close>
+      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+        <el-form-item label="客户端标识" prop="clientId">
+          <el-input v-model="form.clientId" placeholder="留空默认由系统自动生成" :disabled="!!form.id" />
+        </el-form-item>
+        <el-form-item label="客户端名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入客户端名称" />
+        </el-form-item>
+
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio label="Active">激活</el-radio>
+            <el-radio label="Inactive">未激活</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="IP白名单" prop="ipWhitelist">
+          <el-input v-model="form.ipWhitelist" placeholder="多个IP用逗号分隔" />
+          <div class="form-tip">多个IP用逗号分隔,留空表示不限制</div>
+        </el-form-item>
+        <el-form-item label="IP黑名单" prop="ipBlacklist">
+          <el-input v-model="form.ipBlacklist" placeholder="多个IP用逗号分隔" />
+          <div class="form-tip">多个IP用逗号分隔,留空表示不限制</div>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" 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>
+
+    <!-- API权限管理对话框 -->
+    <el-dialog
+      v-model="apiAuthVisible"
+      :title="`客户端 '${currentClient.name}' 的API权限管理`"
+      width="1100px"
+      append-to-body
+      destroy-on-close
+      class="api-permission-dialog"
+    >
+      <client-api-relation :client-id="currentClient.clientId" @update:apiKeys="handleApiKeysUpdate" />
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="apiAuthVisible = false">取消</el-button>
+          <el-button type="primary" @click="saveApiRelations" :loading="apiSaveLoading">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 客户端详情弹窗 -->
+    <el-dialog v-model="detailVisible" title="客户端详情" width="600px" destroy-on-close>
+      <el-descriptions :column="1" border>
+        <el-descriptions-item label="客户端标识">{{ detail.clientId }}</el-descriptions-item>
+        <el-descriptions-item label="客户端名称">{{ detail.name }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag size="small" type="success" v-if="detail.status === 'Active'">激活</el-tag>
+          <el-tag size="small" type="info" v-else-if="detail.status === 'Inactive'">未激活</el-tag>
+          <span v-else>{{ detail.status }}</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="允许访问的API">
+          <div v-if="detail.apiNames && detail.apiNames.length > 0">
+            <el-tag v-for="(api, index) in detail.apiNames" :key="index" size="small" class="api-tag">
+              {{ api }}
+            </el-tag>
+          </div>
+          <span v-else>-</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="IP白名单">{{ detail.ipWhitelist || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="IP黑名单">{{ detail.ipBlacklist || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="备注">{{ detail.remark || '-' }}</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="secretVisible" title="客户端密钥" width="600px" destroy-on-close>
+      <div class="secret-result">
+        <el-alert
+          title="请妥善保管以下密钥信息,它仅会显示一次!"
+          type="warning"
+          :closable="false"
+          show-icon
+        />
+        <div class="secret-info" v-if="secretResult.clientSecret">
+          <div class="secret-item">
+            <div class="secret-label">客户端密钥:</div>
+            <div class="secret-value">{{ secretResult.clientSecret }}</div>
+            <el-button size="small" type="primary" @click="copyText(secretResult.clientSecret)">复制</el-button>
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="secretVisible = false">关闭</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 ClientApiRelation from "./component/ClientApiRelation.vue";
+
+// 定义客户端接口类型
+interface ClientInfo {
+  id?: number;
+  clientId: string;
+  name: string;
+  apiIds?: number[];
+  apiNames?: string[];
+  status: string;
+  ipWhitelist?: string;
+  ipBlacklist?: string;
+  remark?: string;
+  createdAt?: string;
+  updatedAt?: string;
+}
+
+// 引用组件
+const queryRef = ref();
+const formRef = ref();
+
+// 表格数据和加载状态
+const loading = ref(false);
+const apiSaveLoading = ref(false);
+const selectedApiKeys = ref<string[]>([]);
+const apiAuthVisible = ref(false);
+const currentClient = ref<ClientInfo>({} as ClientInfo);
+const params = reactive({
+  keyWord: '',
+  status: '',
+  dateRange: [],
+  pageNum: 1,
+  pageSize: 20,
+  total: 0
+});
+const tableData = ref<ClientInfo[]>([]);
+
+// 获取列表数据
+const getList = async (pageNum?: number) => {
+  if (typeof pageNum === 'number') {
+    params.pageNum = pageNum;
+  }
+  loading.value = true;
+  try {
+    const res = await api.client.list(params);
+    // 根据request.ts中的响应拦截器处理,直接使用返回的数据
+    if (res && res.list) {
+      tableData.value = res.list || [];
+      params.total = res.total || 0;
+    } else {
+      tableData.value = [];
+      params.total = 0;
+    }
+  } catch (error) {
+    ElMessage.error('获取客户端列表失败');
+    tableData.value = [];
+    params.total = 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 表单相关
+const dialogVisible = ref(false);
+const formTitle = ref('');
+const submitLoading = ref(false);
+const form = reactive<ClientInfo>({
+  clientId: '',
+  name: '',
+  apiIds: [],
+  status: 'Active',
+  ipWhitelist: '',
+  ipBlacklist: '',
+  remark: ''
+});
+const rules = {
+  clientId: [{ required: false, message: '请输入客户端标识', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入客户端名称', trigger: 'blur' }],
+  status: [{ required: true, message: '请选择状态', trigger: 'change' }]
+};
+
+// 详情弹窗
+const detailVisible = ref(false);
+const detail = ref<ClientInfo>({
+  clientId: '',
+  name: '',
+  status: '',
+});
+
+// 密钥结果弹窗
+const secretVisible = ref(false);
+const secretResult = ref({
+  clientSecret: ''
+});
+
+// 初始化
+onMounted(() => {
+  getList(1);
+});
+
+// 重置查询表单
+const resetQuery = () => {
+  queryRef.value?.resetFields();
+  params.keyWord = '';
+  params.status = '';
+  params.dateRange = [];
+  getList(1);
+};
+
+// 新增或编辑客户端
+const addOrEdit = (row?: ClientInfo) => {
+  resetForm();
+  if (row && row.id) {
+    formTitle.value = '编辑客户端';
+    // 获取详细信息
+    api.client.get(row.id).then((res: any) => {
+      if (res) {
+        Object.assign(form, res);
+        dialogVisible.value = true;
+      }
+    });
+  } else {
+    formTitle.value = '新增客户端';
+    dialogVisible.value = true;
+  }
+};
+
+// 重置表单
+const resetForm = () => {
+  if (formRef.value) {
+    formRef.value.resetFields();
+  }
+  Object.assign(form, {
+    id: undefined,
+    clientId: '',
+    name: '',
+    status: 'Active',
+    ipWhitelist: '',
+    ipBlacklist: '',
+    remark: ''
+  });
+};
+
+// 提交表单
+const submitForm = () => {
+  formRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      submitLoading.value = true;
+      try {
+        const apiMethod = form.id ? api.client.edit : api.client.add;
+        const res = await apiMethod(form);
+        if (res) {
+          ElMessage.success(form.id ? '编辑成功' : '添加成功');
+          dialogVisible.value = false;
+          getList(1);
+
+          // 如果是新增,显示密钥信息
+          if (!form.id && res.clientSecret) {
+            secretResult.value = {
+              clientSecret: res.clientSecret
+            };
+            secretVisible.value = true;
+          }
+        }
+      } catch (error) {
+        ElMessage.error(form.id ? '编辑失败' : '添加失败');
+      } finally {
+        submitLoading.value = false;
+      }
+    }
+  });
+};
+
+// 查看客户端详情
+const viewDetail = (row: ClientInfo) => {
+  api.client.get(row.id as number).then((res: any) => {
+    if (res) {
+      detail.value = res;
+      detailVisible.value = true;
+    }
+  });
+};
+
+// 重置客户端密钥
+const resetSecret = async (row: ClientInfo) => {
+  try {
+    await ElMessageBox.confirm(`确定要重置客户端"${row.name}"的密钥吗?\n注意:重置后原密钥将失效!`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    });
+
+    const res = await api.client.resetSecret(row.id as number);
+    if (res) {
+      ElMessage.success('密钥重置成功');
+      secretResult.value = {
+        clientSecret: res.clientSecret
+      };
+      secretVisible.value = true;
+    }
+  } catch (error) {
+    // 用户取消操作
+  }
+};
+
+// 删除客户端
+const deleteClient = (row: ClientInfo) => {
+  ElMessageBox.confirm(`确定要删除客户端「${row.name}」吗?`, '警告', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      loading.value = true;
+      try {
+        await api.client.delete([row.id as number]);
+        ElMessage.success('删除成功');
+        getList();
+      } catch (error) {
+        ElMessage.error('删除失败');
+      } finally {
+        loading.value = false;
+      }
+    })
+    .catch(() => {
+      // 用户取消删除操作
+    });
+};
+
+// 复制文本
+const copyText = (text: string) => {
+  navigator.clipboard.writeText(text).then(() => {
+    ElMessage.success('复制成功');
+  }).catch(() => {
+    ElMessage.error('复制失败,请手动复制');
+  });
+};
+
+// 处理API Keys更新
+const handleApiKeysUpdate = (apiKeys: string[]) => {
+  selectedApiKeys.value = apiKeys;
+};
+
+// 打开API权限管理窗口
+const manageApiAuth = (row: ClientInfo) => {
+  currentClient.value = {...row};
+  apiAuthVisible.value = true;
+};
+
+// 保存API关联
+const saveApiRelations = async () => {
+  if (!currentClient.value.clientId) {
+    ElMessage.warning('客户端信息不完整');
+    return;
+  }
+
+  apiSaveLoading.value = true;
+  try {
+    await api.client.save_apis(currentClient.value.clientId, selectedApiKeys.value);
+    ElMessage.success('API权限关联保存成功');
+    apiAuthVisible.value = false;
+  } catch (error) {
+    ElMessage.error('API权限关联保存失败');
+  } finally {
+    apiSaveLoading.value = false;
+  }
+};
+</script>
+
+<style scoped>
+.client-container {
+  width: 100%;
+}
+
+.client-header {
+  margin-bottom: 20px;
+}
+
+.ml10 {
+  margin-left: 10px;
+}
+
+.flex-row {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.api-tag {
+  margin-right: 5px;
+  margin-bottom: 5px;
+}
+
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 5px;
+}
+
+.secret-result {
+  padding: 10px 0;
+}
+
+.secret-info {
+  margin-top: 20px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  padding: 15px;
+}
+/* 深色主题下的样式 */
+[data-theme='dark'] .secret-info {
+  margin-top: 20px;
+  background-color: #2f3030;
+  border-radius: 4px;
+  padding: 15px;
+  color: white;
+}
+
+.secret-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.secret-label {
+  width: 100px;
+  font-weight: bold;
+}
+
+.secret-value {
+  flex: 1;
+  word-break: break-all;
+  font-family: monospace;
+  background-color: #ebeef5;
+  padding: 5px 10px;
+  border-radius: 3px;
+  margin-right: 10px;
+}
+/* 深色主题下的样式 */
+[data-theme='dark'] .secret-value {
+  flex: 1;
+  word-break: break-all;
+  font-family: monospace;
+  background-color: #575656;
+  padding: 10px;
+  border-radius: 3px;
+  margin-right: 10px;
+}
+
+
+
+.client-detail-tabs {
+  margin-top: 20px;
+}
+
+.custom-upload {
+  margin-left: 10px;
+  color: #409eff;
+  cursor: pointer;
+}
+
+.dialog-header {
+  padding: 0;
+  margin: 0;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+  h4 {
+    margin: 0;
+    font-weight: 500;
+  }
+
+  .close-btn {
+    padding: 0;
+    margin: 0;
+    cursor: pointer;
+  }
+}
+
+/* API权限对话框样式优化 */
+:deep(.api-permission-dialog) {
+  .el-dialog__header {
+    background-color: var(--el-bg-color);
+    padding: 15px 20px;
+    margin-right: 0;
+    border-bottom: 1px solid var(--el-border-color-lighter);
+  }
+
+  .el-dialog__title {
+    font-weight: bold;
+    color: var(--el-text-color-primary);
+  }
+
+  .el-dialog__body {
+    background-color: var(--el-bg-color-page);
+    padding: 20px;
+  }
+
+  .el-dialog__footer {
+    background-color: var(--el-bg-color);
+    border-top: 1px solid var(--el-border-color-lighter);
+    padding: 10px 20px;
+  }
+}
+</style>

+ 632 - 0
src/views/apihub/component/ClientApiRelation.vue

@@ -0,0 +1,632 @@
+<template>
+  <div class="client-api-relation">
+    <div class="filter-container">
+      <el-row :gutter="20">
+        <el-col :span="6">
+          <el-input v-model="searchKey" placeholder="搜索API名称或路径" clearable @keyup.enter="handleSearch" />
+        </el-col>
+        <el-col :span="5">
+          <el-select v-model="selectedGroup" placeholder="API分组" clearable @change="handleSearch">
+            <el-option v-for="group in apiGroups" :key="group.id" :label="group.name" :value="group.id" />
+          </el-select>
+        </el-col>
+        <el-col :span="5">
+          <el-select v-model="selectedMethod" placeholder="请求方法" clearable @change="handleSearch">
+            <el-option v-for="method in methods" :key="method" :label="method" :value="method" />
+          </el-select>
+        </el-col>
+        <el-col :span="5">
+          <el-select v-model="selectedStatus" placeholder="状态" clearable @change="handleSearch">
+            <el-option label="已发布" value="Published" />
+            <el-option label="已弃用" value="Deprecated" />
+            <el-option label="未发布" value="Draft" />
+          </el-select>
+        </el-col>
+        <el-col :span="3">
+          <el-button type="primary" @click="handleSearch">
+            <el-icon><ele-Search /></el-icon>
+            搜索
+          </el-button>
+        </el-col>
+      </el-row>
+    </div>
+
+    <div class="relation-container">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <div class="api-list-container">
+            <div class="api-list-header">
+              <h4>可选API列表</h4>
+              <div class="api-list-actions">
+                <el-button type="primary" size="small" @click="addSelectedApis">添加选中</el-button>
+                <el-button size="small" @click="selectAllVisible">选择当前页</el-button>
+                <!-- <el-button size="small" @click="applyTemplate('readonly')">应用只读模板</el-button>
+                <el-button size="small" @click="applyTemplate('fullaccess')">应用完全访问</el-button> -->
+              </div>
+            </div>
+            <el-table 
+              ref="apiTable" 
+              :data="availableApis" 
+              style="width: 100%" 
+              height="450px"
+              v-loading="loading"
+              @selection-change="handleSelectionChange">
+              <el-table-column type="selection" width="55" />
+              <el-table-column prop="name" label="API名称" show-overflow-tooltip />
+              <el-table-column prop="method" label="请求方法" width="100">
+                <template #default="scope">
+                  <el-tag 
+                    :type="getMethodTagType(scope.row.method)" 
+                    size="small">
+                    {{ scope.row.method }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column prop="path" label="API路径" show-overflow-tooltip />
+              <el-table-column prop="status" label="状态" width="80">
+                <template #default="scope">
+                  <el-tag 
+                    :type="getStatusTagType(scope.row.status)" 
+                    size="small">
+                    {{ getStatusText(scope.row.status) }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="80" fixed="right">
+                <template #default="scope">
+                  <el-button 
+                    type="primary" 
+                    size="small" 
+                    @click="addApi(scope.row)"
+                    :disabled="isApiSelected(scope.row)">
+                    添加
+                  </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+            <div class="pagination-container">
+              <el-pagination
+                v-model:current-page="pageNum"
+                v-model:page-size="pageSize"
+                :page-sizes="[10, 20, 50, 100]"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="total"
+                @size-change="handleSizeChange"
+                @current-change="handleCurrentChange"
+              />
+            </div>
+          </div>
+        </el-col>
+        <el-col :span="12">
+          <div class="selected-api-container">
+            <div class="selected-api-header">
+              <h4>已授权API ({{ selectedApis.length }})</h4>
+              <el-button type="danger" size="small" @click="removeAllSelected">移除全部</el-button>
+            </div>
+            <el-table 
+              :data="selectedApis" 
+              style="width: 100%" 
+              height="500px">
+              <el-table-column prop="name" label="API名称" show-overflow-tooltip />
+              <el-table-column prop="method" label="请求方法" width="100">
+                <template #default="scope">
+                  <el-tag 
+                    :type="getMethodTagType(scope.row.method)" 
+                    size="small">
+                    {{ scope.row.method }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column prop="path" label="API路径" show-overflow-tooltip />
+              <el-table-column label="操作" width="80" fixed="right">
+                <template #default="scope">
+                  <el-button 
+                    type="danger" 
+                    size="small" 
+                    @click="removeApi(scope.row)">
+                    移除
+                  </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, watch, defineProps, defineEmits } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import api from '/@/api/modules/apiHub';
+
+// 定义API数据类型
+interface ApiInfo {
+  id: number;
+  key: string;
+  apiKey: string; // 添加apiKey字段
+  name: string;
+  method: string;
+  path: string;
+  status: string;
+  groupId?: number;
+  groupName?: string;
+}
+
+const props = defineProps({
+  clientId: {
+    type: String,
+    required: true
+  }
+});
+
+const emit = defineEmits(['update:apiKeys']);
+
+// 数据状态
+const loading = ref(false);
+const searchKey = ref('');
+const selectedGroup = ref('');
+const selectedMethod = ref('');
+const selectedStatus = ref('');
+const pageNum = ref(1);
+const pageSize = ref(20);
+const total = ref(0);
+
+// API 数据
+const availableApis = ref<ApiInfo[]>([]);
+const apiGroups = ref<{id: number, name: string}[]>([]);
+const selectedApis = ref<ApiInfo[]>([]);
+const selectedApiKeys = ref<string[]>([]);
+const apiTable = ref();
+const selectedRows = ref<ApiInfo[]>([]);
+
+// 预设模板
+const permissionTemplates = {
+  readonly: {
+    name: '只读访问',
+    filter: (api: ApiInfo) => api.method === 'GET'
+  },
+  fullaccess: {
+    name: '完全访问',
+    filter: (api: ApiInfo) => true
+  }
+};
+
+// 请求方法列表
+const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
+
+// 监听选中的API变化,通知父组件
+watch(selectedApiKeys, (newKeys) => {
+  emit('update:apiKeys', newKeys);
+});
+
+// 获取API分组
+const getApiGroups = async () => {
+  try {
+    const res = await api.group.tree();
+    if (res) {
+      apiGroups.value = res;
+    }
+  } catch (error) {
+    ElMessage.error('获取API分组失败');
+  }
+};
+
+// 获取API列表
+const getApiList = async () => {
+  loading.value = true;
+  try {
+    const params = {
+      pageNum: pageNum.value,
+      pageSize: pageSize.value,
+      keyWord: searchKey.value,
+      groupId: selectedGroup.value,
+      method: selectedMethod.value,
+      status: selectedStatus.value
+    };
+    
+    const res = await api.list(params);
+    if (res) {
+      availableApis.value = res.list.map((item: any) => {
+        // 使用APIKey作为唐一标识符
+        return {
+          id: item.id,
+          key: item.key || `${item.id}`,
+          apiKey: item.apiKey || item.key || `${item.id}`, // 使用apiKey或备选值
+          name: item.name,
+          method: item.method,
+          path: item.path,
+          status: item.status,
+          groupId: item.groupId,
+          groupName: item.groupName
+        };
+      });
+      total.value = res.total;
+    }
+  } catch (error) {
+    ElMessage.error('获取API列表失败');
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 获取客户端已关联的API
+const getClientApis = async () => {
+  if (!props.clientId) return;
+  
+  try {
+    const res = await api.client.getApis(props.clientId);
+    if (res && Array.isArray(res)) {
+      selectedApis.value = res;
+      // 使用apiKey字段或备选值
+      selectedApiKeys.value = res.map(item => item.apiKey || item.key || `${item.id}`);
+    }
+  } catch (error) {
+    ElMessage.error('获取客户端API失败');
+  }
+};
+
+// 处理表格选择变化
+const handleSelectionChange = (selection: ApiInfo[]) => {
+  selectedRows.value = selection;
+};
+
+// 选择当前页所有API
+const selectAllVisible = () => {
+  apiTable.value?.toggleAllSelection();
+};
+
+// 应用权限模板
+const applyTemplate = async (templateId: string) => {
+  const template = permissionTemplates[templateId];
+  if (!template) return;
+  
+  loading.value = true;
+  try {
+    // 获取所有API
+    const params = {
+      pageNum: 1,
+      pageSize: 1000,  // 获取较大数量的API
+    };
+    
+    const res = await api.list(params);
+    if (res && res.list) {
+      const filteredApis = res.list
+        .filter((api: any) => template.filter({
+          id: api.id,
+          key: api.key || `${api.id}`,
+          name: api.name,
+          method: api.method,
+          path: api.path,
+          status: api.status
+        }))
+        .map((api: any) => ({
+          id: api.id,
+          key: api.key || `${api.id}`,
+          name: api.name,
+          method: api.method,
+          path: api.path,
+          status: api.status,
+          groupId: api.groupId,
+          groupName: api.groupName
+        }));
+      
+      // 确认是否应用模板
+      await ElMessageBox.confirm(
+        `确定要应用"${template.name}"模板吗?这将会添加${filteredApis.length}个API到客户端权限。`,
+        '确认应用模板',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+      );
+      
+      // 合并已有的API和模板中的API
+      const existingApiIds = new Set(selectedApis.value.map(api => api.id));
+      const newApis = filteredApis.filter(api => !existingApiIds.has(api.id));
+      
+      selectedApis.value = [...selectedApis.value, ...newApis];
+      selectedApiKeys.value = selectedApis.value.map(api => api.apiKey);
+      
+      ElMessage.success(`成功应用"${template.name}"模板,添加了${newApis.length}个API`);
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error('应用模板失败');
+    }
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 批量添加选中的API
+const addSelectedApis = () => {
+  if (selectedRows.value.length === 0) {
+    ElMessage.warning('请先选择要添加的API');
+    return;
+  }
+  
+  // 过滤掉已经选择的API
+  const existingApiIds = new Set(selectedApis.value.map(api => api.id));
+  const newApis = selectedRows.value.filter(api => !existingApiIds.has(api.id));
+  
+  if (newApis.length === 0) {
+    ElMessage.warning('所选API已经全部添加');
+    return;
+  }
+  
+  selectedApis.value = [...selectedApis.value, ...newApis];
+  selectedApiKeys.value = selectedApis.value.map(api => api.apiKey);
+  ElMessage.success(`成功添加${newApis.length}个API`);
+  
+  // 清除表格选择
+  apiTable.value?.clearSelection();
+};
+
+// 添加单个API
+const addApi = (api: ApiInfo) => {
+  if (isApiSelected(api)) {
+    ElMessage.warning('该API已添加');
+    return;
+  }
+  
+  selectedApis.value.push(api);
+  selectedApiKeys.value = selectedApis.value.map(item => item.apiKey);
+  ElMessage.success('添加成功');
+};
+
+// 移除单个API
+const removeApi = (api: ApiInfo) => {
+  // 获取要移除的API唯一标识符
+  const apiIdentifier = api.apiKey || api.key || `${api.id}`;
+  
+  // 在已选API中查找匹配项
+  const index = selectedApis.value.findIndex(item => {
+    const itemIdentifier = item.apiKey || item.key || `${item.id}`;
+    return itemIdentifier === apiIdentifier;
+  });
+  
+  if (index !== -1) {
+    selectedApis.value.splice(index, 1);
+    selectedApiKeys.value = selectedApis.value.map(item => item.apiKey || item.key || `${item.id}`);
+    ElMessage.success('移除成功');
+  } else {
+    ElMessage.warning('未找到要移除的API');
+  }
+};
+
+// 移除所有已选API
+const removeAllSelected = async () => {
+  if (selectedApis.value.length === 0) {
+    ElMessage.warning('暂无已授权的API');
+    return;
+  }
+  
+  try {
+    await ElMessageBox.confirm('确定要移除所有已授权的API吗?', '警告', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    });
+    
+    selectedApis.value = [];
+    selectedApiKeys.value = [];
+    ElMessage.success('已移除所有API授权');
+  } catch (error) {
+    // 用户取消操作
+  }
+};
+
+// 检查API是否已选中
+const isApiSelected = (api: ApiInfo) => {
+  // 获取API的apiKey或备选标识符
+  const apiIdentifier = api.apiKey || api.key || `${api.id}`;
+  // 遍历已选中的API数组,检查是否已存在相同的apiKey
+  return selectedApis.value.some(selectedApi => {
+    const selectedApiIdentifier = selectedApi.apiKey || selectedApi.key || `${selectedApi.id}`;
+    return apiIdentifier === selectedApiIdentifier;
+  });
+};
+
+// 搜索API
+const handleSearch = () => {
+  pageNum.value = 1;
+  getApiList();
+};
+
+// 处理分页大小变化
+const handleSizeChange = (size: number) => {
+  pageSize.value = size;
+  getApiList();
+};
+
+// 处理页码变化
+const handleCurrentChange = (page: number) => {
+  pageNum.value = page;
+  getApiList();
+};
+
+// 获取请求方法对应的标签类型
+const getMethodTagType = (method: string) => {
+  const map: Record<string, string> = {
+    'GET': 'success',
+    'POST': 'primary',
+    'PUT': 'warning',
+    'DELETE': 'danger',
+    'PATCH': 'info'
+  };
+  
+  return map[method] || '';
+};
+
+// 获取状态对应的标签类型
+const getStatusTagType = (status: string) => {
+  const map: Record<string, string> = {
+    'Published': 'success',
+    'Deprecated': 'warning',
+    'Draft': 'info'
+  };
+  
+  return map[status] || '';
+};
+
+// 获取状态文本
+const getStatusText = (status: string) => {
+  const map: Record<string, string> = {
+    'Published': '已发布',
+    'Deprecated': '已弃用',
+    'Draft': '未发布'
+  };
+  
+  return map[status] || status;
+};
+
+// 初始化
+onMounted(() => {
+  getApiGroups();
+  getApiList();
+  getClientApis();
+});
+</script>
+
+<style scoped>
+.client-api-relation {
+  padding: 15px 0;
+}
+
+.filter-container {
+  margin-bottom: 20px;
+}
+
+.relation-container {
+  margin-top: 20px;
+}
+
+.api-list-header,
+.selected-api-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+  padding: 0 10px;
+}
+
+.api-list-header h4,
+.selected-api-header h4 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 500;
+  color: var(--el-text-color-primary);
+}
+
+.api-list-actions {
+  display: flex;
+  gap: 8px;
+}
+
+.api-list-container,
+.selected-api-container {
+  padding: 15px;
+  background-color: var(--el-bg-color-overlay);
+  border: 1px solid var(--el-border-color-light);
+  border-radius: 4px;
+  height: 600px;
+  display: flex;
+  flex-direction: column;
+  box-shadow: var(--el-box-shadow-light);
+}
+
+.pagination-container {
+  margin-top: 15px;
+  display: flex;
+  justify-content: center;
+}
+
+/* 优化深色主题下的表格样式 */
+:deep(.el-table) {
+  --el-table-border-color: var(--el-border-color-lighter);
+  --el-table-header-bg-color: var(--el-fill-color-dark);
+  --el-table-row-hover-bg-color: var(--el-fill-color-dark);
+  background-color: var(--el-bg-color-overlay);
+}
+
+:deep(.el-table th) {
+  background-color: var(--el-fill-color-dark);
+  color: var(--el-text-color-primary);
+  font-weight: bold;
+}
+
+:deep(.el-table--enable-row-hover .el-table__body tr:hover > td) {
+  background-color: var(--el-color-primary-light-9);
+}
+
+:deep(.el-table td) {
+  color: var(--el-text-color-regular);
+}
+
+:deep(.el-table__empty-text) {
+  color: var(--el-text-color-secondary);
+}
+
+/* 表格状态标识 */
+:deep(.el-tag) {
+  border: 1px solid transparent;
+}
+
+:deep(.el-tag--success) {
+  background-color: var(--el-color-success-light-9);
+  border-color: var(--el-color-success-light-7);
+  color: var(--el-color-success);
+}
+
+:deep(.el-tag--info) {
+  background-color: var(--el-color-info-light-9);
+  border-color: var(--el-color-info-light-7);
+  color: var(--el-color-info-dark-2);
+}
+
+/* 增强表格单元格边框 */
+:deep(.el-table--border .el-table__cell) {
+  border-right: 1px solid var(--el-border-color-lighter);
+}
+
+/* 边框分隔线 */
+.relation-container :deep(.el-col:first-child) {
+  position: relative;
+}
+
+.relation-container :deep(.el-col:first-child)::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  right: 0;
+  height: 100%;
+  width: 1px;
+  background-color: var(--el-border-color-lighter);
+}
+
+/* 搜索框样式优化 */
+.filter-container :deep(.el-input__inner) {
+  background-color: var(--el-fill-color-blank);
+  border-color: var(--el-border-color);
+}
+
+.filter-container :deep(.el-input__inner:hover) {
+  border-color: var(--el-color-primary-light-3);
+}
+
+.filter-container :deep(.el-input__inner:focus) {
+  border-color: var(--el-color-primary);
+}
+
+/* 按钮样式增强 */
+:deep(.el-button--primary) {
+  box-shadow: 0 2px 6px rgba(var(--el-color-primary-rgb), 0.4);
+}
+
+:deep(.el-button--danger) {
+  box-shadow: 0 2px 6px rgba(var(--el-color-danger-rgb), 0.4);
+}
+</style>

+ 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>

+ 370 - 0
src/views/apihub/component/SqlEditor.vue

@@ -0,0 +1,370 @@
+<template>
+  <div class="sql-editor-wrapper" :style="{ height: height + 'px' }">
+    <div class="editor-container">
+      <div class="editor-toolbar">
+        <el-tooltip content="格式化SQL" placement="top">
+          <el-button type="primary" link @click="formatSql">
+            <el-icon><Operation /></el-icon>
+          </el-button>
+        </el-tooltip>
+
+        <el-dropdown @command="handleSnippetSelect" trigger="click">
+          <el-button type="primary" link>
+            <el-icon><Document /></el-icon>
+            <span>插入SQL片段</span>
+          </el-button>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item v-for="(snippet, index) in sqlSnippets"
+                              :key="index"
+                              :command="snippet.code">
+                {{ snippet.name }}
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
+
+      <!-- Monaco编辑器容器 -->
+      <div ref="editorContainer" class="monaco-editor-container"></div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, onMounted, onBeforeUnmount, nextTick, markRaw } from 'vue';
+import { ElMessage } from 'element-plus';
+import { Operation, Document } from '@element-plus/icons-vue';
+import * as monaco from 'monaco-editor';
+import { format } from 'sql-formatter'; // Ensure this library is installed and imported correctly
+
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: ''
+  },
+  height: {
+    type: Number,
+    default: 300
+  }
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+// 编辑器相关引用
+const sqlValue = ref(props.modelValue);
+const editorContainer = ref(null);
+const editorInstance = ref(null);
+const isFormatting = ref(false); // 格式化状态标记
+const isInserting = ref(false); // 插入片段状态标记
+
+// SQL代码片段
+const sqlSnippets = [
+  {
+    name: '基本查询',
+    code: 'SELECT * FROM table_name WHERE condition;'
+  },
+  {
+    name: '分组查询',
+    code: 'SELECT column1, COUNT(*) FROM table_name GROUP BY column1 HAVING COUNT(*) > 1;'
+  },
+  {
+    name: '联表查询',
+    code: 'SELECT a.*, b.* FROM table1 a JOIN table2 b ON a.id = b.table1_id;'
+  },
+  {
+    name: '分页查询',
+    code: 'SELECT * FROM table_name LIMIT 10 OFFSET 0;'
+  },
+  {
+    name: '插入数据',
+    code: 'INSERT INTO table_name (column1, column2) VALUES (value1, value2);'
+  },
+  {
+    name: '更新数据',
+    code: 'UPDATE table_name SET column1 = value1 WHERE condition;'
+  },
+  {
+    name: '删除数据',
+    code: 'DELETE FROM table_name WHERE condition;'
+  }
+];
+
+// SQL关键字
+const sqlKeywords = [
+  "SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING",
+  "JOIN", "LEFT JOIN", "RIGHT JOIN", "INNER JOIN", "LIMIT", "OFFSET",
+  "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP", "TABLE",
+  "INTO", "VALUES", "SET", "AND", "OR", "NOT", "NULL", "IS", "AS",
+  "DISTINCT", "COUNT", "SUM", "AVG", "MAX", "MIN", "BETWEEN", "LIKE",
+  "IN", "EXISTS", "ALL", "ANY", "UNION", "CASE", "WHEN", "THEN", "ELSE", "END"
+];
+
+// 监听值变化
+watch(() => props.modelValue, (newValue) => {
+  if (newValue !== sqlValue.value) {
+    sqlValue.value = newValue;
+    if (editorInstance.value) {
+      // 使用安全的方式更新编辑器内容
+      const model = editorInstance.value.getModel();
+      if (model) {
+        model.setValue(newValue);
+      }
+    }
+  }
+});
+
+// 初始化Monaco编辑器
+const initMonacoEditor = () => {
+  if (!editorContainer.value) return;
+
+  try {
+    // 清理现有实例
+    if (editorInstance.value) {
+      editorInstance.value.dispose();
+      editorInstance.value = null;
+    }
+
+    // 确保容器是空的
+    if (editorContainer.value.childNodes.length > 0) {
+      editorContainer.value.innerHTML = '';
+    }
+
+    // 创建编辑器实例
+    const editor = monaco.editor.create(editorContainer.value, {
+      value: sqlValue.value || '',
+      language: 'sql',
+      theme: 'vs',
+      automaticLayout: true,
+      scrollBeyondLastLine: false,
+      minimap: { enabled: false },
+      fontSize: 14,
+      tabSize: 2,
+      lineNumbers: 'on',
+      scrollbar: {
+        useShadows: false,
+        verticalScrollbarSize: 10,
+        horizontalScrollbarSize: 10,
+        alwaysConsumeMouseWheel: false
+      },
+      // 禁用所有可能导致性能问题的功能
+      folding: false,
+      contextmenu: false,
+      quickSuggestions: false,
+      parameterHints: false,
+      autoClosingBrackets: false,
+      suggestOnTriggerCharacters: false,
+      wordBasedSuggestions: false,
+      links: false,
+      hover: false,
+      find: false,
+      colorDecorators: false,
+      lightbulb: { enabled: false },
+      formatOnType: false,
+      formatOnPaste: false,
+      selectionHighlight: false,
+      occurrencesHighlight: false,
+      codeLens: false,
+      renderControlCharacters: false,
+      renderIndentGuides: false,
+      renderLineHighlight: 'none',
+      renderWhitespace: 'none'
+    });
+
+    console.log('[DEBUG] initMonacoEditor: Creating Monaco editor instance.');
+    editorInstance.value = markRaw(editor);
+    console.log('[DEBUG] initMonacoEditor: Monaco editor instance assigned to ref after markRaw.');
+
+    // 使用节流处理内容变化
+    const handleContentChange = () => {
+      const currentEditor = editorInstance.value;
+      if (!currentEditor) return;
+      const value = currentEditor.getValue();
+      sqlValue.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();
+          editorInstance.value = null;
+        } catch (e) {
+          // Error handling if needed
+        }
+      }
+    });
+  } catch (error) {
+    // 静默处理错误或进行适当的错误上报
+  }
+};
+
+// 格式化SQL
+const formatSql = () => {
+  console.log('[DEBUG] formatSql START');
+  if (!editorInstance.value) {
+    console.error('[DEBUG] formatSql: Editor instance is NOT available.');
+    // ElMessage.error('编辑器实例未准备好,请稍后再试。');
+    return;
+  }
+  console.log('[DEBUG] formatSql: Editor instance is available.');
+
+  const currentContent = editorInstance.value.getValue();
+  console.log('[DEBUG] formatSql: Current content length:', currentContent.length);
+
+  if (!currentContent.trim()) {
+    console.log('[DEBUG] formatSql: Content is empty or whitespace, skipping format.');
+    // ElMessage.info('SQL内容为空,无需格式化。');
+    return;
+  }
+
+  // loading.value = true; // 假设有加载状态
+  // isFormatting.value = true;
+  console.log('[DEBUG] formatSql: Attempting to format SQL...');
+  try {
+    const formattedSql = format(currentContent, {
+      language: 'mysql', // 根据需要选择 'mysql', 'sql', 'n1ql', 'plsql', 'spark', 'tsql' 等
+      indent: '  ', // 两个空格缩进
+      uppercase: true, // 关键字大写
+      linesBetweenQueries: 1
+    });
+    console.log('[DEBUG] formatSql: SQL formatted successfully.');
+
+    editorInstance.value.setValue(formattedSql);
+    console.log('[DEBUG] formatSql: Formatted SQL set to editor.');
+    // ElMessage.success('SQL格式化成功!');
+  } catch (error) {
+    console.error('[DEBUG] formatSql: Error during formatting:', error);
+    // ElMessage.error('SQL格式化失败,请检查SQL语法或格式化库配置。详情请查看控制台。');
+  } finally {
+    // loading.value = false;
+    // isFormatting.value = false;
+    console.log('[DEBUG] formatSql END');
+  }
+};
+
+// 处理SQL片段选择 - 使用防抖动处理
+const handleSnippetSelect = (snippet: string) => {
+  console.log('[DEBUG] handleSnippetSelect START. Snippet:', snippet);
+  if (!editorInstance.value) {
+    console.error('[DEBUG] Editor instance is NOT available.');
+    return;
+  }
+  console.log('[DEBUG] Editor instance is available.');
+
+  try {
+    const model = editorInstance.value.getModel();
+    if (!model) {
+      console.error('[DEBUG] Editor model is NOT available.');
+      return;
+    }
+    console.log('[DEBUG] Editor model obtained.');
+
+    const currentPosition = editorInstance.value.getPosition();
+    console.log('[DEBUG] Current position:', currentPosition);
+
+    if (currentPosition) {
+      const range = new monaco.Range(
+        currentPosition.lineNumber,
+        currentPosition.column,
+        currentPosition.lineNumber,
+        currentPosition.column
+      );
+      const op = { range: range, text: snippet, forceMoveMarkers: true };
+      console.log('[DEBUG] Preparing to executeEdits. Op:', op);
+      editorInstance.value.executeEdits('snippet-inserter', [op]); // Monaco API call
+      console.log('[DEBUG] executeEdits completed.');
+    } else {
+      console.warn('[DEBUG] Cannot get current position. Appending to end.');
+      const currentValue = model.getValue();
+      const newContent = currentValue + (currentValue ? '\n' : '') + snippet;
+      model.setValue(newContent); // Monaco API call
+      console.log('[DEBUG] Appended to end using setValue.');
+    }
+  } catch (error) {
+    console.error('[DEBUG] CRITICAL ERROR in handleSnippetSelect:', error);
+  }
+  console.log('[DEBUG] handleSnippetSelect END.');
+};
+
+// 组件挂载后的生命周期钩子
+onMounted(() => {
+  // 延迟初始化编辑器,确保 DOM 已完全渲染
+  setTimeout(() => {
+    initMonacoEditor();
+  }, 300);
+});
+
+// 组件销毁前
+onBeforeUnmount(() => {
+  // 清理编辑器实例
+  if (editorInstance.value) {
+    try {
+      editorInstance.value.dispose();
+      editorInstance.value = null;
+    } catch (e) {
+      // Error handling if needed
+    }
+  }
+});
+</script>
+
+<style scoped>
+.sql-editor-wrapper {
+  display: flex;
+  height: 300px;
+  min-height: 200px;
+  width: 100%;
+  position: relative;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.editor-container {
+  flex: 1;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  width: 100%;
+}
+
+.editor-toolbar {
+  display: flex;
+  align-items: center;
+  padding: 5px 10px;
+  border-bottom: 1px solid #dcdfe6;
+  background-color: #f5f7fa;
+  z-index: 10;
+}
+
+.monaco-editor-container {
+  flex: 1;
+  overflow: hidden;
+  position: relative;
+  min-height: 200px;
+  width: 100%;
+}
+
+/* 深色模式样式 */
+.dark .sql-editor-wrapper {
+  border-color: #4c4d4f;
+}
+
+.dark .editor-toolbar {
+  background-color: #252526;
+  border-color: #4c4d4f;
+}
+</style>

+ 114 - 142
src/views/apihub/component/edit.vue

@@ -55,8 +55,8 @@
             </el-form-item>
             <el-form-item label="API路径" prop="path">
               <div class="path-input-container">
-                <div class="path-prefix">
-                  {{ originUrl }}
+                <div class="path-prefix2">
+                  {{ originUrl }}/apihub/
                 </div>
                 <el-input v-model="formData.path" placeholder="请输入API路径" class="path-input" />
               </div>
@@ -64,9 +64,9 @@
             <el-form-item label="所属分组" prop="groupKey">
               <el-cascader v-model="formData.groupKey" :options="groupOptions" :props="{ checkStrictly: true, emitPath: false, value: 'GroupKey', label: 'Name', children: 'Children' }" placeholder="请选择所属分组" clearable style="width: 100%" />
             </el-form-item>
-            <el-form-item label="版本" prop="version">
+            <!-- <el-form-item label="版本" prop="version">
               <el-input v-model="formData.version" placeholder="请输入版本号,如1.0" />
-            </el-form-item>
+            </el-form-item> -->
             <!-- <el-form-item label="状态" prop="status">
               <el-select v-model="formData.status" placeholder="请选择状态">
                 <el-option label="草稿" value="Draft"></el-option>
@@ -78,12 +78,30 @@
               <el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入API描述" />
             </el-form-item>
 
-            <div class="form-section-title">参数定义
+            <el-form-item label="ContentType" prop="contentType">
+              <el-select v-model="formData.contentType" placeholder="请选择ContentType" style="width: 100%;">
+                <el-option label="application/x-www-form-urlencoded" value="application/x-www-form-urlencoded"></el-option>
+                <el-option label="application/json" value="application/json"></el-option>
+              </el-select>
+            </el-form-item>
+
+            <el-form-item label="请求示例" prop="example" v-if="formData.contentType === 'application/json'">
+              <el-input v-model="formData.example" type="textarea" :rows="4" placeholder="请输入请求示例" />
+            </el-form-item>
+
+            <el-form-item label="访问权限" prop="private">
+              <el-radio-group v-model="formData.private">
+                <el-radio :label="0">开放API</el-radio>
+                <el-radio :label="1">私有API</el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <div class="form-section-title" v-if="formData.contentType === 'application/x-www-form-urlencoded'">参数定义
               <el-button type="primary" link @click="addParameter" style="margin-left: 10px;">
                 <el-icon><ele-Plus /></el-icon>添加参数
               </el-button>
             </div>
-            <el-form-item label-width="0">
+            <el-form-item label-width="0" v-if="formData.contentType === 'application/x-www-form-urlencoded'">
               <el-table :data="formData.parameters" style="width: 100%" border>
                 <el-table-column label="参数名" width="150">
                   <template #default="scope">
@@ -141,22 +159,11 @@
               </el-radio-group>
             </el-form-item>
             <el-form-item label="SQL内容" prop="sqlContent" class="sql-content-item">
-              <div class="monaco-editor-wrapper">
-                <div class="line-number-container">
-                  <div v-for="n in lineCount" :key="n" class="line-number">{{ n }}</div>
-                </div>
-                <div class="monaco-editor-container">
-                  <MonacoEditor
-                    v-model:value="formData.sqlContent"
-                    :options="monacoOptions"
-                    theme="vs-dark"
-                    language="sql"
-                    @change="onSqlContentChange"
-                    @editorDidMount="editorDidMount"
-                    @editorWillMount="editorWillMount"
-                  />
-                </div>
-              </div>
+              <SqlEditor
+                v-model="formData.sqlContent"
+                :height="300"
+                @update:modelValue="onSqlContentChange"
+              />
             </el-form-item>
             <el-form-item label="返回格式" prop="returnFormat">
               <el-select v-model="formData.returnFormat" placeholder="请选择返回格式">
@@ -229,6 +236,7 @@
 
 <script lang="ts" setup>
 import { ref, reactive, nextTick } from "vue";
+import SqlEditor from "./SqlEditor.vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import { ruleRequired } from "/@/utils/validator";
 import api from "/@/api/modules/apiHub";
@@ -239,7 +247,7 @@ import {
   FullScreen as EleFullScreen,
   ScaleToOriginal as EleScaleToOriginal
 } from '@element-plus/icons-vue';
-import MonacoEditor from "@guolao/vue-monaco-editor";
+// 使用自定义的 SqlEditor 组件替代 MonacoEditor
 
 const emit = defineEmits(["getList"]);
 
@@ -247,130 +255,35 @@ const showDialog = ref(false);
 const formRef = ref();
 const dataSources = ref<any[]>([]);
 const activeTab = ref('basic'); // 当前激活的Tab
-const isFullscreen = ref(false); // 是否全屏显示
-const lineCount = ref(1); // SQL编辑器行数
 const pluginParamsPlaceholder = '插件参数 (JSON格式,例如:{"key": "value"})';
 const originUrl: string = getOrigin("");
 
-// Monaco Editor 配置项
-const monacoOptions = {
-  automaticLayout: true,
-  scrollBeyondLastLine: false,
-  minimap: { enabled: false },
-  fontSize: 14,
-  tabSize: 2,
-  lineNumbers: 'off', // 关闭内置行号,因为我们自定义了行号显示
-  scrollbar: {
-    useShadows: false,
-    verticalScrollbarSize: 10,
-    horizontalScrollbarSize: 10,
-    alwaysConsumeMouseWheel: false
-  },
-  lineHeight: 20,
-  renderLineHighlight: 'line',
-  glyphMargin: false,
-  folding: true,
-  contextmenu: true,
-  cursorStyle: 'line',
-  fontFamily: 'Menlo, Monaco, Consolas, "Courier New", monospace',
-  // 自动提示相关配置 - 使用Monaco编辑器自带的SQL支持
-  quickSuggestions: {
-    other: true,
-    comments: true,
-    strings: true
-  },
-  suggestOnTriggerCharacters: true,
-  acceptSuggestionOnEnter: 'on',
-  tabCompletion: 'on',
-  wordBasedSuggestions: true,
-  // 设置触发建议的字符数
-  quickSuggestionsDelay: 0, // 立即显示提示
-  // 自动提示相关配置
-  suggest: {
-    showKeywords: true,
-    showSnippets: true,
-    showWords: true,
-    showFunctions: true,
-    showIcons: true,
-    maxVisibleSuggestions: 15,
-    filterGraceful: false, // 不过滤建议,显示所有可能的选项
-    snippetSuggestions: 'top' // 将代码片段放在提示列表顶部
-  }
-};
+// SQL编辑器已经在组件内部处理了关键字和自动完成功能
 
 // SQL内容变化回调
-const onSqlContentChange = (value) => {
+const onSqlContentChange = (value: string) => {
+  // 这里可以添加其他需要的处理逻辑
   formData.sqlContent = value;
-  // 计算行数
-  const lines = value ? value.split('\n').length : 1;
-  lineCount.value = Math.max(1, lines);
 };
 
-// 编辑器将要加载时的回调
-const editorWillMount = (monaco) => {
-  // 自定义SQL关键字高亮
-  monaco.editor.defineTheme('sqlTheme', {
-    base: 'vs-dark',
-    inherit: true,
-    rules: [
-      { token: 'keyword', foreground: '569cd6', fontStyle: 'bold' },
-      { token: 'operator', foreground: 'd4d4d4' },
-      { token: 'string', foreground: 'ce9178' },
-      { token: 'number', foreground: 'b5cea8' },
-      { token: 'comment', foreground: '6a9955', fontStyle: 'italic' }
-    ],
-    colors: {}
-  });
-};
-
-// 编辑器挂载完成回调
-const editorDidMount = (editor) => {
-  // 初始化行数
-  const lines = formData.sqlContent ? formData.sqlContent.split('\n').length : 1;
-  lineCount.value = Math.max(1, lines);
-
-  // 使用editor实例以避免lint警告
-  if (editor) {
-    // 设置焦点
-    editor.focus();
-    
-    // 添加输入监听,在用户开始输入时触发提示
-    editor.onDidChangeModelContent(() => {
-      // 触发自动提示
-      editor.trigger('keyboard', 'editor.action.triggerSuggest', {});
-    });
-    
-    // 初始化时就触发一次提示
-    setTimeout(() => {
-      editor.trigger('keyboard', 'editor.action.triggerSuggest', {});
-    }, 100);
-    
-    // 添加键盘事件处理,阻止回车键触发表单提交
-    editor.onKeyDown((e) => {
-      // 当用户在编辑器中按下回车键时
-      if (e.keyCode === 13 || e.code === 'Enter') {
-        // 阻止事件冒泡到表单
-        e.stopPropagation();
-      }
-    });
-  }
-};
+// 全屏显示控制
+const isFullscreen = ref(false);
 
 // 检查哪个标签页有验证错误并切换到该标签页
 const checkTabWithErrors = () => {
   // 获取所有验证失败的字段
   const fields = formRef.value.fields;
   const errorFields = [];
-  
+
   // 收集所有错误字段
   for (const field in fields) {
     if (fields[field].validateState === 'error') {
       errorFields.push(field);
     }
   }
-  
+
   if (errorFields.length === 0) return;
-  
+
   // 基本信息标签页的字段
   const basicTabFields = ['name', 'path', 'version', 'description', 'groupKey'];
   // 执行器标签页的字段
@@ -383,10 +296,10 @@ const checkTabWithErrors = () => {
       pluginsTabFields.push(field);
     }
   });
-  
+
   // 检查每个标签页并切换到有错误的标签页
   const errorTabs = [];
-  
+
   if (errorFields.some(field => basicTabFields.includes(field))) {
     errorTabs.push({ tab: 'basic', name: '基本信息' });
   }
@@ -396,17 +309,17 @@ const checkTabWithErrors = () => {
   if (pluginsTabFields.length > 0) {
     errorTabs.push({ tab: 'plugins', name: '全局插件' });
   }
-  
+
   if (errorTabs.length > 0) {
     // 如果当前标签页不在错误标签页列表中,切换到第一个错误标签页
     const currentTabHasError = errorTabs.some(tab => tab.tab === activeTab.value);
     if (!currentTabHasError) {
       activeTab.value = errorTabs[0].tab;
     }
-    
+
     // 显示错误提示
     showErrorTooltip(errorTabs);
-    
+
     // 等待DOM更新后滚动到错误字段
     nextTick(() => {
       const errorElement = document.querySelector('.is-error');
@@ -428,7 +341,7 @@ const showErrorTooltip = (errorTabs) => {
   });
   errorMessage += '</ul>';
   errorMessage += '</div>';
-  
+
   // 显示消息提示
   ElMessage({
     dangerouslyUseHTMLString: true,
@@ -441,7 +354,7 @@ const showErrorTooltip = (errorTabs) => {
       removeErrorTabClickListeners();
     }
   });
-  
+
   // 等待DOM更新后添加点击事件
   nextTick(() => {
     addErrorTabClickListeners();
@@ -506,6 +419,9 @@ const baseForm = {
   plugins: [],
   groupKey: "",
   description: "",
+  contentType: "application/x-www-form-urlencoded", // 默认为 application/x-www-form-urlencoded
+  example: "", // 请求参数示例
+  private: 0, // 访问权限类型:0为公开,1为私有
 };
 
 const formData = reactive(JSON.parse(JSON.stringify(baseForm)));
@@ -612,10 +528,10 @@ const onSubmit = () => {
       // 验证失败
       // 直接显示错误提示
       showValidationErrorMessage(fields);
-      
+
       // 验证失败时检查是哪个标签页的必填项出错
       checkTabWithErrors();
-      
+
       return false;
     }
   });
@@ -626,15 +542,15 @@ const showValidationErrorMessage = (fields) => {
   // 获取所有错误字段
   const errorFields = Object.keys(fields);
   if (errorFields.length === 0) return;
-  
+
   // 基本信息标签页的字段
   const basicTabFields = ['name', 'path', 'version', 'description', 'groupKey'];
   // 执行器标签页的字段
   const executorTabFields = ['dataSourceKey', 'sqlType', 'sqlContent', 'returnFormat', 'status'];
-  
+
   // 检查每个标签页的错误
   const errorTabs = [];
-  
+
   if (errorFields.some(field => basicTabFields.includes(field))) {
     errorTabs.push('基本信息');
   }
@@ -644,7 +560,7 @@ const showValidationErrorMessage = (fields) => {
   if (errorFields.some(field => field.startsWith('plugins['))) {
     errorTabs.push('全局插件');
   }
-  
+
   if (errorTabs.length > 0) {
     // 显示错误消息
     ElMessage({
@@ -706,8 +622,57 @@ const open = async (row?: any, defaultGroupKey?: string) => {
       // 如果是编辑模式,需要先获取详情
       // 实际使用时,可能需要先调用API获取完整数据
       // 这里模拟直接使用传入的行数据
+      const rowData = JSON.parse(JSON.stringify(row));
+
+      // 打印原始数据,查看后端返回的字段结构
+      console.log('编辑模式下原始数据:', rowData);
+
+      // 处理字段名称的映射,确保前端和后端字段名称一致
+      // 处理 ContentType 字段(可能后端是 ContentType,前端是 contentType)
+      if (rowData.ContentType !== undefined && rowData.contentType === undefined) {
+        rowData.contentType = rowData.ContentType;
+        console.log('映射 ContentType -> contentType:', rowData.contentType);
+      }
+
+      // 处理 Example 字段(可能后端是 Example,前端是 example)
+      if (rowData.Example !== undefined && rowData.example === undefined) {
+        rowData.example = rowData.Example;
+        console.log('映射 Example -> example:', rowData.example);
+      }
+
+      // 处理 Private 字段(可能后端是 Private,前端是 private)
+      if (rowData.Private !== undefined && rowData.private === undefined) {
+        rowData.private = rowData.Private;
+        console.log('映射 Private -> private:', rowData.private);
+      }
+
+      // 如果没有 contentType,设置默认值
+      if (rowData.contentType === undefined || rowData.contentType === null || rowData.contentType === '') {
+        rowData.contentType = 'application/x-www-form-urlencoded';
+        console.log('设置默认 contentType:', rowData.contentType);
+      }
 
-      Object.assign(formData, JSON.parse(JSON.stringify(row)));
+      // 处理 private 字段的数据类型,确保是数字类型
+      if (rowData.private !== undefined) {
+        // 如果是字符串,转换为数字
+        if (typeof rowData.private === 'string') {
+          rowData.private = parseInt(rowData.private, 10) || 0;
+          console.log('将 private 从字符串转换为数字:', rowData.private);
+        }
+      } else {
+        // 如果没有 private,设置默认值
+        rowData.private = 0;
+        console.log('设置默认 private:', rowData.private);
+      }
+
+      Object.assign(formData, rowData);
+
+      // 打印最终表单数据,查看是否正确设置了这三个字段
+      console.log('最终表单数据:', {
+        contentType: formData.contentType,
+        example: formData.example,
+        private: formData.private
+      });
 
       // 确保参数数组存在
       if (!formData.parameters) {
@@ -936,8 +901,8 @@ defineExpose({ open });
   width: 100%;
 }
 
-.path-prefix {
-  background-color: #f5f7fa;
+.path-prefix2 {
+  background-color: #eee;
   padding: 0 10px;
   height: 32px;
   line-height: 32px;
@@ -948,6 +913,13 @@ defineExpose({ open });
   white-space: nowrap;
 }
 
+/* 深色主题下的样式 */
+[data-theme='dark'] .path-prefix2 {
+  background-color: #333;
+  border-color: #424242;
+  color: #dadada;
+}
+
 .path-input {
   flex: 1;
 }

+ 49 - 12
src/views/apihub/component/group.vue

@@ -8,7 +8,7 @@
         <el-input v-model="formData.groupKey" placeholder="请输入分组唯一标识,留空则自动生成" />
       </el-form-item>
       <el-form-item label="父级分组" prop="parentId">
-        <el-cascader v-model="formData.parentId" :options="groupOptions" :props="{ checkStrictly: true, emitPath: false, value: 'Id', label: 'Name', children: 'Children' }" placeholder="请选择父级分组,不选择则为顶级分组" clearable style="width: 100%" />
+        <el-cascader v-model="formData.parentId" :options="groupOptions" :props="cascaderProps" placeholder="请选择父级分组,不选择则为顶级分组" clearable style="width: 100%" />
       </el-form-item>
       <el-form-item label="排序号" prop="sort">
         <el-input-number v-model="formData.sort" :min="0" :max="999" />
@@ -37,6 +37,13 @@ const emit = defineEmits(["refresh"]);
 const showDialog = ref(false);
 const formRef = ref();
 const groupOptions = ref<any[]>([]);
+const cascaderProps = {
+  checkStrictly: true,
+  emitPath: false,
+  value: 'Id',
+  label: 'Name',
+  children: 'Children'
+};
 let treeData: any[] = [];
 
 // 定义基础表单数据
@@ -128,27 +135,57 @@ const loadGroupOptions = async (excludeId?: string) => {
   }
 };
 
+// 根据GroupKey查找Id
+const findIdByGroupKey = (groupKey: string, tree: any[]): string | null => {
+  for (const node of tree) {
+    if (node.GroupKey === groupKey) {
+      return node.Id;
+    }
+    if (node.Children && node.Children.length > 0) {
+      const foundId = findIdByGroupKey(groupKey, node.Children);
+      if (foundId) return foundId;
+    }
+  }
+  return null;
+};
+
 // 打开对话框
-const open = async (row?: any, parentId?: string) => {
+const open = async (row?: any, parentGroupKey?: string) => {
+  // 先重置表单
   resetForm();
   showDialog.value = true;
 
   // 加载分组选项
   await loadGroupOptions(row?.Id);
 
+  // 等待DOM更新
+  await nextTick();
+  
   if (row) {
     // 编辑模式,填充表单数据
-    nextTick(() => {
-      formData.id = row.Id;
-      formData.groupKey = row.GroupKey;
-      formData.name = row.Name;
-      formData.parentId = row.ParentId;
-      formData.sort = row.Sort;
-      formData.description = row.Description;
-    });
-  } else if (parentId) {
+    formData.id = row.Id;
+    formData.groupKey = row.GroupKey;
+    formData.name = row.Name;
+    formData.parentId = row.ParentId;
+    formData.sort = row.Sort;
+    formData.description = row.Description;
+  } else if (parentGroupKey) {
     // 新增子分组模式
-    formData.parentId = parentId;
+    // 先根据GroupKey查找Id
+    const parentId = findIdByGroupKey(parentGroupKey, treeData);
+    
+    if (parentId) {
+      // 使用setTimeout确保在下一个事件循环中设置值
+      setTimeout(() => {
+        // 设置父级分组ID
+        formData.parentId = parentId;
+        
+        // 手动触发表单验证,强制更新显示
+        if (formRef.value) {
+          formRef.value.validateField('parentId');
+        }
+      }, 200);
+    }
   }
 };
 

+ 107 - 1
src/views/apihub/component/test.vue

@@ -2,7 +2,15 @@
   <el-dialog class="api-test" v-model="showDialog" title="测试API" width="800px" :close-on-click-modal="false" :close-on-press-escape="false">
     <el-descriptions :column="2" border>
       <el-descriptions-item label="API名称" :span="2">{{ apiData.name }}</el-descriptions-item>
-      <el-descriptions-item label="API路径" :span="2">{{ apiData.path }}</el-descriptions-item>
+      <el-descriptions-item label="API路径" :span="2">
+        <div class="api-path-container">
+            <span class="domain">{{ originUrl }}/apihub/</span>
+            <span class="path">{{ apiData.path }}</span>
+          <el-tooltip content="复制API完整路径" placement="top">
+            <el-icon class="copy-icon" @click="copyApiPath"><CopyDocument /></el-icon>
+          </el-tooltip>
+        </div>
+      </el-descriptions-item>
       <el-descriptions-item label="请求方法">
         <el-tag :type="getMethodTagType(apiData.method)" size="small">{{ apiData.method }}</el-tag>
       </el-descriptions-item>
@@ -49,10 +57,14 @@
 <script lang="ts" setup>
 import { ref, reactive, watch } from "vue";
 import apiHub from "/@/api/modules/apiHub";
+import { ElMessage } from "element-plus";
+import { CopyDocument } from "@element-plus/icons-vue";
+import getOrigin from "/@/utils/origin";
 
 const showDialog = ref(false);
 const loading = ref(false);
 const activeTab = ref("data");
+const originUrl: string = getOrigin("");
 
 // API数据
 const apiData = reactive<any>({
@@ -191,6 +203,21 @@ const runTest = async () => {
     });
 };
 
+// 复制API完整路径
+const copyApiPath = () => {
+  // 实现复制功能
+  const fullPath = `${originUrl}/apihub/${apiData.path}`;
+
+  // 使用Clipboard API复制到剪贴板
+  navigator.clipboard.writeText(fullPath)
+    .then(() => {
+      ElMessage.success('已复制API完整路径到剪贴板');
+    })
+    .catch(() => {
+      ElMessage.error('复制失败,请手动复制');
+    });
+};
+
 // 打开对话框
 const open = async (row: any) => {
   // 清空数据
@@ -243,12 +270,26 @@ defineExpose({ open });
   background-color: #f5f7fa;
   border-radius: 4px;
 }
+/* 深色主题下的样式 */
+[data-theme='dark'] .test-placeholder {
+  text-align: center;
+  color: #e4e3e3;
+  padding: 20px;
+  background-color: #424040;
+  border-radius: 4px;
+}
 
 .result-container {
   margin-top: 15px;
   border: 1px solid #e4e7ed;
   border-radius: 4px;
 }
+/* 深色主题下的样式 */
+[data-theme='dark'] .result-container {
+  margin-top: 15px;
+  border: 1px solid #575656;
+  border-radius: 4px;
+}
 
 .result-json {
   background-color: #f5f7fa;
@@ -260,4 +301,69 @@ defineExpose({ open });
   max-height: 300px;
   overflow: auto;
 }
+/* 深色主题下的样式 */
+[data-theme='dark'] .result-json {
+  background-color: #3c3b3b;
+  color: #ffffff;
+  padding: 10px;
+  font-family: monospace;
+  white-space: pre-wrap;
+  word-break: break-all;
+  margin: 0;
+  max-height: 300px;
+  overflow: auto;
+}
+
+
+.el-empty{
+  height: 120px;
+}
+
+.api-path-container {
+  display: inline-block;
+  text-decoration: none;
+  color: black; /* 设置整体文字颜色 */
+  align-items: center; /* 垂直居中对齐 */
+}
+/* 深色主题下的样式 */
+[data-theme='dark'] .api-path-container {
+  display: inline-block;
+  text-decoration: none;
+  color: #fff; /* 设置整体文字颜色 */
+}
+.api-path-container .domain {
+  background-color: #eee; /* 设置域名部分的背景颜色 */
+  color: black; /* 设置域名部分的文字颜色 */
+  padding: 2px 4px; /* 添加一些内边距 */
+  display: inline-block;
+  border-radius: 15px; /* 增加圆角 */
+}
+/* 深色主题下的样式 */
+[data-theme='dark'] .api-path-container .domain{
+  background-color: #3c3b3b; /* 设置域名部分的背景颜色 */
+  color: white; /* 设置域名部分的文字颜色 */
+  padding: 2px 4px; /* 添加一些内边距 */
+  display: inline-block;
+  border-radius: 15px; /* 增加圆角 */
+}
+.api-path-container .path {
+  display: inline-block;
+}
+
+
+.copy-icon {
+  cursor: pointer;
+  color: var(--el-color-primary);
+  transition: all 0.2s;
+  font-size: 24px;
+  margin-left: 8px;
+  padding: 4px;
+  border-radius: 4px;
+}
+
+.copy-icon:hover {
+  transform: scale(1.1);
+  color: var(--el-color-primary-dark-2);
+  background-color: var(--el-color-primary-light-9);
+}
 </style>

+ 85 - 1
src/views/apihub/component/view.vue

@@ -2,7 +2,15 @@
   <el-dialog class="api-view" v-model="showDialog" title="API详情" width="800px" :close-on-click-modal="false" :close-on-press-escape="false">
     <el-descriptions :column="2" border>
       <el-descriptions-item label="API名称" :span="2">{{ apiData.name }}</el-descriptions-item>
-      <el-descriptions-item label="API路径" :span="2">{{ apiData.path }}</el-descriptions-item>
+      <el-descriptions-item label="API路径" :span="2">
+        <div class="api-path-container">
+          <span class="domain">{{ originUrl }}/apihub/</span>
+          <span class="path">{{ apiData.path }}</span>
+          <el-tooltip content="复制API完整路径" placement="top">
+            <el-icon class="copy-icon" @click="copyApiPath"><CopyDocument /></el-icon>
+          </el-tooltip>
+        </div>
+      </el-descriptions-item>
       <el-descriptions-item label="请求方法">
         <el-tag :type="getMethodTagType(apiData.method)" size="small">{{ apiData.method }}</el-tag>
       </el-descriptions-item>
@@ -60,6 +68,9 @@
 
 <script lang="ts" setup>
 import { ref, reactive } from "vue";
+import getOrigin from "/@/utils/origin";
+import { CopyDocument } from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
 
 const emit = defineEmits(["test"]);
 const showDialog = ref(false);
@@ -81,6 +92,7 @@ const apiData = reactive({
   createdAt: "",
   updatedAt: "",
 });
+const originUrl: string = getOrigin("");
 
 // 根据请求方法返回不同的标签类型
 const getMethodTagType = (method: string) => {
@@ -109,6 +121,21 @@ const testApi = () => {
   emit("test", apiData);
 };
 
+// 复制API完整路径
+const copyApiPath = () => {
+  // 实现复制功能
+  const fullPath = `${originUrl}/apihub/${apiData.path}`;
+
+  // 使用Clipboard API复制到剪贴板
+  navigator.clipboard.writeText(fullPath)
+      .then(() => {
+        ElMessage.success('已复制API完整路径到剪贴板');
+      })
+      .catch(() => {
+        ElMessage.error('复制失败,请手动复制');
+      });
+};
+
 // 打开对话框
 const open = async (row: any) => {
   // 清空数据
@@ -152,4 +179,61 @@ defineExpose({ open });
   white-space: pre-wrap;
   word-break: break-all;
 }
+/* 深色主题下的样式 */
+[data-theme='dark'] .code-block {
+  background-color: #2f3030;
+  border: 1px solid #575656;
+  border-radius: 4px;
+  padding: 10px;
+  font-family: monospace;
+  white-space: pre-wrap;
+  word-break: break-all;
+}
+
+
+.api-path-container {
+  display: inline-block;
+  text-decoration: none;
+  color: black; /* 设置整体文字颜色 */
+  align-items: center; /* 垂直居中对齐 */
+}
+/* 深色主题下的样式 */
+[data-theme='dark'] .api-path-container {
+  display: inline-block;
+  text-decoration: none;
+  color: #fff; /* 设置整体文字颜色 */
+}
+.api-path-container .domain {
+  background-color: #eee; /* 设置域名部分的背景颜色 */
+  color: black; /* 设置域名部分的文字颜色 */
+  padding: 2px 4px; /* 添加一些内边距 */
+  display: inline-block;
+  border-radius: 15px; /* 增加圆角 */
+}
+/* 深色主题下的样式 */
+[data-theme='dark'] .api-path-container .domain{
+  background-color: #3c3b3b; /* 设置域名部分的背景颜色 */
+  color: white; /* 设置域名部分的文字颜色 */
+  padding: 2px 4px; /* 添加一些内边距 */
+  display: inline-block;
+  border-radius: 15px; /* 增加圆角 */
+}
+.api-path-container .path {
+  display: inline-block;
+}
+.copy-icon {
+  cursor: pointer;
+  color: var(--el-color-primary);
+  transition: all 0.2s;
+  font-size: 24px;
+  margin-left: 8px;
+  padding: 4px;
+  border-radius: 4px;
+}
+
+.copy-icon:hover {
+  transform: scale(1.1);
+  color: var(--el-color-primary-dark-2);
+  background-color: var(--el-color-primary-light-9);
+}
 </style>

+ 89 - 53
src/views/apihub/datasource.vue

@@ -3,8 +3,8 @@
     <el-card shadow="never">
       <!-- 搜索表单 -->
       <el-form :model="params" inline ref="queryRef">
-        <el-form-item label="数据源名称" prop="keyWord">
-          <el-input v-model="params.keyWord" placeholder="输入数据源名称" clearable style="width: 200px" @keyup.enter.native="getList(1)" />
+        <el-form-item label="关键字" prop="keyWord">
+          <el-input v-model="params.keyWord" placeholder="输入名称或备注" clearable style="width: 200px" @keyup.enter.native="getList(1)" />
         </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" />-->
@@ -50,36 +50,54 @@
 
     <!-- 数据源表单对话框 -->
     <el-dialog v-model="dialogVisible" :title="formData.id ? '编辑数据源' : '新增数据源'" width="600px" :close-on-click-modal="false" :close-on-press-escape="false">
-      <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
+      <el-form ref="formRef" :model="formData" :rules="rules" label-width="125px">
         <el-form-item label="数据源名称" prop="name">
           <el-input v-model="formData.name" placeholder="请输入数据源名称" />
         </el-form-item>
-        <el-form-item label="数据库类型" prop="type">
-          <el-select v-model="formData.type" placeholder="请选择数据库类型" style="width: 100%">
-            <template v-if="database_type.length">
-              <el-option v-for="item in database_type" :key="item.value" :label="item.label" :value="item.value"></el-option>
-            </template>
-            <template v-else>
-              <el-option label="MySQL" value="mysql" />
-            </template>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="主机地址" prop="host">
-          <el-input v-model="formData.host" placeholder="请输入主机地址" />
-        </el-form-item>
-        <el-form-item label="端口" prop="port">
-          <el-input-number v-model="formData.port" :min="1" :max="65535" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="用户名" prop="username">
-          <el-input v-model="formData.username" placeholder="请输入用户名" />
-        </el-form-item>
-        <el-form-item label="密码" prop="password">
-          <el-input v-model="formData.password" type="password" placeholder="请输入密码" show-password />
-          <div class="form-tip" v-if="formData.id">不修改请留空</div>
-        </el-form-item>
-        <el-form-item label="数据库名" prop="database">
-          <el-input v-model="formData.database" placeholder="请输入数据库名" />
-        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="数据库类型" prop="type">
+              <el-select v-model="formData.type" placeholder="请选择数据库类型" style="width: 100%">
+                <template v-if="database_type.length">
+                  <el-option v-for="item in database_type" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                </template>
+                <template v-else>
+                  <el-option label="MySQL" value="mysql" />
+                </template>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="数据库名" prop="database">
+              <el-input v-model="formData.database" placeholder="请输入数据库名" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="主机地址" prop="host">
+              <el-input v-model="formData.host" placeholder="请输入主机地址" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="端口" prop="port">
+              <el-input-number v-model="formData.port" :min="1" :max="65535" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="用户名" prop="username">
+              <el-input v-model="formData.username" placeholder="请输入用户名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="密码" prop="password">
+              <el-input v-model="formData.password" type="password" placeholder="请输入密码" show-password />
+              <div class="form-tip" v-if="formData.id">不修改请留空</div>
+            </el-form-item>
+          </el-col>
+        </el-row>
         <el-form-item label="最大打开连接数" prop="maxOpenConns">
           <el-input-number v-model="formData.maxOpenConns" :min="1" style="width: 100%" />
         </el-form-item>
@@ -104,30 +122,48 @@
     </el-dialog>
 
     <!-- 测试连接对话框 -->
-    <el-dialog v-model="testDialogVisible" title="测试数据源连接" width="500px">
-      <el-form ref="testFormRef" :model="testFormData" :rules="testRules" label-width="120px">
-        <el-form-item label="数据库类型" prop="type">
-          <el-select v-model="testFormData.type" placeholder="请选择数据库类型" style="width: 100%">
-            <el-option label="MySQL" value="MySQL" />
-            <el-option label="PostgreSQL" value="PostgreSQL" />
-            <el-option label="SQL Server" value="SQL Server" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="主机地址" prop="host">
-          <el-input v-model="testFormData.host" placeholder="请输入主机地址" />
-        </el-form-item>
-        <el-form-item label="端口" prop="port">
-          <el-input-number v-model="testFormData.port" :min="1" :max="65535" style="width: 100%" />
-        </el-form-item>
-        <el-form-item label="用户名" prop="username">
-          <el-input v-model="testFormData.username" placeholder="请输入用户名" />
-        </el-form-item>
-        <el-form-item label="密码" prop="password">
-          <el-input v-model="testFormData.password" type="password" placeholder="请输入密码" show-password />
-        </el-form-item>
-        <el-form-item label="数据库名" prop="database">
-          <el-input v-model="testFormData.database" placeholder="请输入数据库名" />
-        </el-form-item>
+    <el-dialog v-model="testDialogVisible" title="测试数据源连接" width="600px">
+      <el-form ref="testFormRef" :model="testFormData" :rules="testRules" label-width="125px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="数据库类型" prop="type">
+              <el-select v-model="testFormData.type" placeholder="请选择数据库类型" style="width: 100%">
+                <el-option label="MySQL" value="MySQL" />
+                <el-option label="PostgreSQL" value="PostgreSQL" />
+                <el-option label="SQL Server" value="SQL Server" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="数据库名" prop="database">
+              <el-input v-model="testFormData.database" placeholder="请输入数据库名" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="主机地址" prop="host">
+              <el-input v-model="testFormData.host" placeholder="请输入主机地址" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="端口" prop="port">
+              <el-input-number v-model="testFormData.port" :min="1" :max="65535" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="用户名" prop="username">
+              <el-input v-model="testFormData.username" placeholder="请输入用户名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="密码" prop="password">
+              <el-input v-model="testFormData.password" type="password" placeholder="请输入密码" show-password />
+            </el-form-item>
+          </el-col>
+        </el-row>
       </el-form>
       <template #footer>
         <div class="dialog-footer">

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

@@ -0,0 +1,818 @@
+<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="`插件测试 - ${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">
+            <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-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-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-icon v-if="!testLoading"><video-play /></el-icon>
+            <span>执行测试</span>
+          </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";
+import { ArrowDown, Document, Warning, VideoPlay, CopyDocument } from '@element-plus/icons-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);
+    // 处理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) {
+    // 获取插件列表失败
+    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 => {
+      // 处理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 = '新增插件';
+  }
+  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 => {
+    // 处理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;
+  });
+};
+
+// 删除插件
+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 currentTestPluginName = ref('');
+const testForm = reactive({
+  context: '{}'
+});
+const testResult = reactive({
+  success: false,
+  message: '',
+  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 {
+    // 发送测试请求
+    const contextData = JSON.parse(testForm.context);
+    const res = await api.plugin.test({
+      id: currentTestPluginId.value,
+      context: contextData
+    });
+    
+    // 处理不同格式的响应
+    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 = 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();
+});
+</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;
+}
+
+/* 测试界面样式 */
+.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;
+  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>

+ 1 - 1
src/views/iot/rule-engine/index.vue

@@ -93,7 +93,7 @@ const searchParams = reactive({
 });
 
 // 监听搜索参数变化,确保在API请求中正确传递搜索条件
-watch(searchParams, (newVal) => {
+watch(searchParams, (newVal: any) => {
   // 处理搜索参数的值
   for (const key in newVal) {
     if (newVal[key] === '' || newVal[key] === null) {

+ 17 - 0
src/views/iot/rule-engine/log.vue

@@ -0,0 +1,17 @@
+<template>
+	<el-card shadow="nover" class="page">
+		<el-table :data="tableData" style="width: 100%" row-key="id" v-loading="loading">
+			<el-table-column prop="id" label="ID" width="100" show-overflow-tooltip v-col="'id'"></el-table-column>
+			<el-table-column label="操作" align="center"></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>
+</template>
+
+<script lang="ts" setup>
+import api from '/@/api/modules/policy'
+import { useSearch } from '/@/hooks/useCommon'
+
+const { params, tableData, getList, loading } = useSearch<any[]>(api.log.getList, 'Data', { status: undefined })
+getList()
+</script>

+ 0 - 175
src/views/iot/rule-engine/send.vue

@@ -1,175 +0,0 @@
-<template>
-  <div class="page">
-    <el-card shadow="nover">
-      <el-form inline>
-        <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-FolderAdd />
-            </el-icon>
-            新增数据转发
-          </el-button>
-        </el-form-item>
-      </el-form>
-      <el-table :data="tableData" style="width: 100%" v-loading="loading">
-        <el-table-column type="index" label="序号" width="80" align="center" />
-        <el-table-column prop="name" label="名称" show-overflow-tooltip></el-table-column>
-        <el-table-column prop="expound" label="说明" show-overflow-tooltip></el-table-column>
-        <el-table-column prop="createdAt" label="创建时间" min-width="100" align="center"></el-table-column>
-        <el-table-column prop="status" label="状态" width="100" align="center">
-          <template #default="scope">
-            <el-tag type="success" size="small" v-if="scope.row.status == 1">已启动</el-tag>
-            <el-tag type="info" size="small" v-else>已停止</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="200" align="center">
-          <template #default="scope">
-            <el-button size="small" text type="info" v-auth="'startOrStop'" v-if="scope.row.status" @click="setStatus(scope.row, 0)">停止</el-button>
-            <el-button size="small" text type="primary" v-auth="'startOrStop'" v-else @click="setStatus(scope.row, 1)">启动</el-button>
-            <el-button size="small" text type="primary" v-auth="'edit'" @click="addOrEdit(scope.row)">编辑</el-button>
-            <el-button size="small" text type="warning" @click="edit(scope.row)">规则编辑</el-button>
-            <el-button size="small" text type="danger" v-auth="'del'" @click="onDel(scope.row)">删除</el-button>
-          </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()" />
-      <EditForm :model="model" ref="editFormRef" @getList="getList(1)" :types="1"></EditForm>
-    </el-card>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { ref } from "vue";
-import api from "/@/api/rule";
-import { ElMessageBox, ElMessage } from "element-plus";
-import { useSearch } from "/@/hooks/useCommon";
-import EditForm from "./edit.vue";
-import axios from "axios";
-import { getToken } from "/@/utils/auth";
-
-const editFormRef = ref();
-
-// 规则引擎模式 node-red sagoo-rule
-const model: "node-red" | "sagoo-rule" = import.meta.env.VITE_RULE_MODEL;
-
-const { params, tableData, getList, loading } = useSearch<any[]>(api.getList, "Data", { types: 1 });
-
-const headers = {
-  Authorization: "Bearer " + getToken(),
-};
-const flowsUrl = window.location.origin + "/rule-engine/flows";
-
-getList();
-
-const addOrEdit = async (row?: any) => {
-  if (row) {
-    editFormRef.value.open(row);
-    return;
-  } else {
-    editFormRef.value.open();
-  }
-};
-
-const setStatus = async (row: any, status: number) => {
-  if (model === "sagoo-rule") {
-    axios
-      .get(`${import.meta.env.VITE_RULE_SERVER_URL}/api/v1/rules/${row.flowId}/${status ? "enable" : "stop"}`, { headers })
-      .then(() => {
-        api
-          .setStatus(row.id, status)
-          .then(() => {
-            ElMessage.success("操作成功");
-            getList();
-          })
-          .catch(() => {
-            ElMessage.error("操作失败");
-          });
-      })
-      .catch(() => {
-        ElMessage.error("操作失败");
-      });
-  } else {
-    // 找到所有规则
-    const { data: flows } = await axios.get(flowsUrl, { headers });
-
-    const flow = flows.find((item: any) => item.id === row.flowId);
-
-    if (!flow) {
-      ElMessage.error("规则不存在");
-      return;
-    }
-
-    // 改变指定规则状态
-    flow.disabled = status ? false : true;
-    // 设置规则状态
-    await axios.post(flowsUrl, flows, { headers });
-    api
-      .setStatus(row.id, status)
-      .then(() => {
-        ElMessage.success("操作成功");
-        getList();
-      })
-      .catch(() => {
-        ElMessage.error("操作失败");
-      });
-  }
-};
-
-const edit = async (row: any) => {
-  if (model == "sagoo-rule") {
-    localStorage.setItem("auth-tokens", `{"access_token":"${getToken()}"}`);
-    const url = "/plugin/rule/index.html#" + row.flowId;
-    window.open(url);
-  } else if (model == "node-red") {
-    localStorage.setItem("auth-tokens", `{"access_token":"${getToken()}"}`);
-    const url = "/rule-engine/#flow/" + row.flowId;
-    window.open(url);
-  }
-};
-
-const onDel = (row: any) => {
-  ElMessageBox.confirm(`此操作将删除:“${row.name}”,是否继续?`, "提示", {
-    confirmButtonText: "确认",
-    cancelButtonText: "取消",
-    type: "warning",
-  }).then(async () => {
-    if (model == "sagoo-rule") {
-      await axios.delete(import.meta.env.VITE_RULE_SERVER_URL + "/api/v1/rules/" + row.flowId, { headers }).catch(() => {
-        ElMessage.error("规则不存在");
-      });
-    } else if (model == "node-red") {
-      // 找到所有规则
-      const { data: flows } = await axios.get(flowsUrl, { headers });
-
-      const flowIndex = flows.findIndex((item: any) => item.id === row.flowId);
-
-      if (flowIndex >= 0) {
-        // 删除指定规则
-        flows.splice(flowIndex, 1);
-
-        // 删除当前规则下的各个节点信息
-        const newFlows = flows.filter((item: any) => {
-          if (item.z === row.flowId) {
-            return false;
-          } else {
-            return true;
-          }
-        });
-
-        // 设置规则状态
-        await axios.post(flowsUrl, newFlows, { headers });
-      }
-    }
-
-    await api.del([row.id as string]);
-    ElMessage.success("删除成功");
-    getList();
-  });
-};
-</script>

+ 1 - 1
writeEnv.mjs

@@ -20,7 +20,7 @@ const configJson = {
 		imgServer: VITE_SERVER_ORIGIN + VITE_NGINX_PROXY,
 	},
 	rule: {
-		server: VITE_RULE_SERVER_URL,
+		server: VITE_SERVER_ORIGIN + VITE_RULE_SERVER_URL,
 		iotServer: baseUrl,
 	},
 	designer: {