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