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