plugin.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  1. <template>
  2. <div class="page">
  3. <div class="plugin-container">
  4. <el-card shadow="never">
  5. <div class="plugin-header">
  6. <h3>API插件管理</h3>
  7. </div>
  8. <el-form :model="params" inline ref="queryRef">
  9. <el-form-item label="关键字" prop="keyWord">
  10. <el-input v-model="params.keyWord" placeholder="输入插件名称或描述" clearable style="width: 170px" @keyup.enter.native="getList(1)" />
  11. </el-form-item>
  12. <el-form-item label="类型" prop="type">
  13. <el-select v-model="params.type" placeholder="请选择类型" clearable style="width: 140px">
  14. <el-option label="全部" value="" />
  15. <!-- <el-option label="Go" value="Go" /> -->
  16. <el-option label="JavaScript" value="JavaScript" />
  17. <el-option label="LUA" value="LUA" />
  18. </el-select>
  19. </el-form-item>
  20. <el-form-item label="分类" prop="category">
  21. <el-select v-model="params.category" placeholder="请选择分类" clearable style="width: 140px">
  22. <el-option label="全部" value="" />
  23. <el-option label="前置插件" value="Before" />
  24. <el-option label="后置插件" value="After" />
  25. </el-select>
  26. </el-form-item>
  27. <!-- <el-form-item label="日期范围" prop="dateRange">
  28. <el-date-picker v-model="params.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 240px" />
  29. </el-form-item> -->
  30. <el-form-item>
  31. <el-button type="primary" class="ml10" @click="getList(1)">
  32. <el-icon>
  33. <ele-Search />
  34. </el-icon>
  35. 查询
  36. </el-button>
  37. <el-button type="primary" @click="addOrEdit()" v-auth="'add'">
  38. <el-icon>
  39. <ele-Plus />
  40. </el-icon>
  41. 新增插件
  42. </el-button>
  43. </el-form-item>
  44. </el-form>
  45. <el-table :data="tableData" style="width: 100%" v-loading="loading" row-key="id">
  46. <el-table-column type="selection" width="40" align="center" />
  47. <el-table-column prop="name" label="插件名称" v-col="'name'" min-width="140" show-overflow-tooltip></el-table-column>
  48. <el-table-column prop="type" label="类型" v-col="'type'" width="120" align="center">
  49. <template #default="scope">
  50. <el-tag size="small" type="success" v-if="scope.row.type === 'Go'">Go</el-tag>
  51. <el-tag size="small" v-else-if="scope.row.type === 'JavaScript'">JavaScript</el-tag>
  52. <el-tag size="small" type="warning" v-else-if="scope.row.type === 'LUA'">LUA</el-tag>
  53. <span v-else>{{ scope.row.type }}</span>
  54. </template>
  55. </el-table-column>
  56. <el-table-column prop="category" label="分类" v-col="'category'" width="120" align="center">
  57. <template #default="scope">
  58. <el-tag size="small" type="info" v-if="scope.row.category === 'Before'">前置插件</el-tag>
  59. <el-tag size="small" type="info" v-else-if="scope.row.category === 'After'">后置插件</el-tag>
  60. <span v-else>{{ scope.row.category }}</span>
  61. </template>
  62. </el-table-column>
  63. <el-table-column prop="description" label="描述" v-col="'description'" show-overflow-tooltip></el-table-column>
  64. <!-- <el-table-column prop="createdAt" label="创建时间" width="160" align="center"></el-table-column> -->
  65. <el-table-column label="操作" width="250" align="center" fixed="right" v-col="'handle'">
  66. <template #default="scope">
  67. <div class="flex-row">
  68. <el-button size="small" text type="primary" @click="viewDetail(scope.row)" v-auth="'view'">查看</el-button>
  69. <el-button size="small" text type="warning" @click="addOrEdit(scope.row)" v-auth="'edit'">编辑</el-button>
  70. <el-button size="small" text type="success" @click="testPlugin(scope.row)" v-auth="'test'">测试</el-button>
  71. <el-button size="small" text type="danger" @click="deletePlugin(scope.row)" v-auth="'del'">删除</el-button>
  72. </div>
  73. </template>
  74. </el-table-column>
  75. </el-table>
  76. <pagination v-if="params.total" :total="params.total" v-model:page="params.pageNum" v-model:limit="params.pageSize" @pagination="getList()" />
  77. </el-card>
  78. </div>
  79. <!-- 插件表单弹窗 -->
  80. <el-dialog v-model="dialogVisible" :title="formTitle" width="800px" destroy-on-close>
  81. <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
  82. <el-form-item label="插件名称" prop="name">
  83. <el-input v-model="form.name" placeholder="请输入插件名称" />
  84. </el-form-item>
  85. <el-form-item label="插件类型" prop="type">
  86. <el-select v-model="form.type" placeholder="请选择插件类型" style="width: 100%">
  87. <el-option label="Go" value="Go" />
  88. <el-option label="JavaScript" value="JavaScript" />
  89. <el-option label="LUA" value="LUA" />
  90. </el-select>
  91. </el-form-item>
  92. <el-form-item label="插件分类" prop="category">
  93. <el-select v-model="form.category" placeholder="请选择插件分类" style="width: 100%">
  94. <el-option label="前置插件" value="Before" />
  95. <el-option label="后置插件" value="After" />
  96. </el-select>
  97. </el-form-item>
  98. <el-form-item label="插件内容" prop="content">
  99. <plugin-editor v-model="form.content" :height="300" :language="form.type" />
  100. <div class="form-tip">
  101. <template v-if="form.type === 'Go'">插件内容为Go插件的路径</template>
  102. <template v-else>插件内容为脚本代码</template>
  103. </div>
  104. </el-form-item>
  105. <el-form-item label="描述" prop="description">
  106. <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入描述信息" />
  107. </el-form-item>
  108. </el-form>
  109. <template #footer>
  110. <span class="dialog-footer">
  111. <el-button @click="dialogVisible = false">取消</el-button>
  112. <el-button type="primary" @click="submitForm" :loading="submitLoading">确定</el-button>
  113. </span>
  114. </template>
  115. </el-dialog>
  116. <!-- 插件详情弹窗 -->
  117. <el-dialog v-model="detailVisible" title="插件详情" width="800px" destroy-on-close>
  118. <el-descriptions :column="1" border>
  119. <el-descriptions-item label="插件名称">{{ detail.name }}</el-descriptions-item>
  120. <el-descriptions-item label="插件类型">
  121. <el-tag size="small" type="success" v-if="detail.type === 'Go'">Go</el-tag>
  122. <el-tag size="small" v-else-if="detail.type === 'JavaScript'">JavaScript</el-tag>
  123. <el-tag size="small" type="warning" v-else-if="detail.type === 'LUA'">LUA</el-tag>
  124. <span v-else>{{ detail.type }}</span>
  125. </el-descriptions-item>
  126. <el-descriptions-item label="插件分类">
  127. <el-tag size="small" type="info" v-if="detail.category === 'Before'">前置插件</el-tag>
  128. <el-tag size="small" type="info" v-else-if="detail.category === 'After'">后置插件</el-tag>
  129. <span v-else>{{ detail.category }}</span>
  130. </el-descriptions-item>
  131. <el-descriptions-item label="插件内容">
  132. <div class="code-preview">
  133. <plugin-editor v-model="detail.content" :height="200" :language="detail.type" :readonly="true" />
  134. </div>
  135. </el-descriptions-item>
  136. <el-descriptions-item label="描述">{{ detail.description || '-' }}</el-descriptions-item>
  137. <el-descriptions-item label="创建时间">{{ detail.createdAt }}</el-descriptions-item>
  138. <el-descriptions-item label="更新时间">{{ detail.updatedAt }}</el-descriptions-item>
  139. </el-descriptions>
  140. <template #footer>
  141. <span class="dialog-footer">
  142. <el-button @click="detailVisible = false">关闭</el-button>
  143. </span>
  144. </template>
  145. </el-dialog>
  146. <!-- 插件测试弹窗 -->
  147. <el-dialog v-model="testVisible" :title="`插件测试 - ${currentTestPluginName}`" width="900px" destroy-on-close>
  148. <div class="test-container">
  149. <el-form :model="testForm" ref="testFormRef" label-width="100px">
  150. <el-form-item label="上下文数据" prop="context">
  151. <div class="context-toolbar">
  152. <el-button-group>
  153. <el-tooltip content="格式化JSON" placement="top">
  154. <el-button type="primary" :icon="Document" @click="formatJson" plain size="small">格式化</el-button>
  155. </el-tooltip>
  156. <el-tooltip content="模板帮助你快速创建测试数据" placement="top">
  157. <el-dropdown @command="useTestTemplate" trigger="click">
  158. <el-button type="primary" plain size="small">
  159. 使用模板 <el-icon class="el-icon--right"><arrow-down /></el-icon>
  160. </el-button>
  161. <template #dropdown>
  162. <el-dropdown-menu>
  163. <el-dropdown-item v-for="(temp, index) in testTemplates" :key="index" :command="temp.value">
  164. {{ temp.name }}
  165. </el-dropdown-item>
  166. </el-dropdown-menu>
  167. </template>
  168. </el-dropdown>
  169. </el-tooltip>
  170. </el-button-group>
  171. </div>
  172. <el-input
  173. v-model="testForm.context"
  174. type="textarea"
  175. :rows="8"
  176. placeholder="请输入测试用的上下文数据,JSON格式。例如:{ &quot;key&quot;: &quot;value&quot; }"
  177. class="json-textarea"
  178. :class="{ 'textarea-error': testResult.error }"
  179. />
  180. <div v-if="testResult.error" class="error-message">
  181. <el-icon><warning /></el-icon> {{ testResult.error }}
  182. </div>
  183. </el-form-item>
  184. </el-form>
  185. <div class="test-result" v-if="testResult.message">
  186. <div class="result-header">
  187. <div class="result-title">测试结果</div>
  188. <el-tag :type="testResult.success ? 'success' : 'danger'" effect="dark">
  189. {{ testResult.success ? '测试成功' : '测试失败' }}
  190. </el-tag>
  191. </div>
  192. <div class="result-message">{{ testResult.message }}</div>
  193. <div class="result-data" v-if="testResult.data">
  194. <div class="result-data-header">
  195. <div class="result-label">返回数据</div>
  196. <el-button type="primary" link size="small" @click="copyToClipboard(testResult.data)">
  197. <el-icon><copy-document /></el-icon> 复制
  198. </el-button>
  199. </div>
  200. <pre class="data-preview">{{ testResult.data }}</pre>
  201. </div>
  202. </div>
  203. </div>
  204. <template #footer>
  205. <span class="dialog-footer">
  206. <el-button @click="testVisible = false">关闭</el-button>
  207. <el-button type="primary" @click="runTest" :loading="testLoading">
  208. <el-icon v-if="!testLoading"><video-play /></el-icon>
  209. <span>执行测试</span>
  210. </el-button>
  211. </span>
  212. </template>
  213. </el-dialog>
  214. </div>
  215. </template>
  216. <script lang="ts" setup>
  217. import { ref, reactive, onMounted } from "vue";
  218. import { ElMessageBox, ElMessage } from "element-plus";
  219. import api from "/@/api/modules/apiHub";
  220. import PluginEditor from "./component/PluginEditor.vue";
  221. import { ArrowDown, Document, Warning, VideoPlay, CopyDocument } from '@element-plus/icons-vue';
  222. // 定义插件接口类型
  223. interface PluginInfo {
  224. id?: number;
  225. name: string;
  226. type: string;
  227. category: string;
  228. content: string;
  229. description?: string;
  230. createdAt?: string;
  231. updatedAt?: string;
  232. }
  233. // 引用组件
  234. const queryRef = ref();
  235. const formRef = ref();
  236. const testFormRef = ref();
  237. // 查询参数
  238. const params = reactive({
  239. keyWord: '',
  240. type: '',
  241. category: '',
  242. dateRange: [],
  243. pageNum: 1,
  244. pageSize: 10,
  245. total: 0
  246. });
  247. // 表格数据
  248. const tableData = ref<PluginInfo[]>([]);
  249. const loading = ref(false);
  250. // 获取插件列表
  251. const getList = async (pageNum?: number) => {
  252. if (pageNum) {
  253. params.pageNum = pageNum;
  254. }
  255. loading.value = true;
  256. try {
  257. const res = await api.plugin.list(params);
  258. // 处理API响应数据
  259. // 尝试所有可能的数据结构
  260. tableData.value = []; // 清空当前数据
  261. if (res) {
  262. // 1. 检查 res.data
  263. if (res.data) {
  264. // 1.1 直接是数组
  265. if (Array.isArray(res.data)) {
  266. tableData.value = res.data;
  267. params.total = res.total || res.data.length || 0;
  268. }
  269. // 1.2 有data属性
  270. else if (res.data.data && Array.isArray(res.data.data)) {
  271. tableData.value = res.data.data;
  272. params.total = res.data.total || res.data.data.length || 0;
  273. }
  274. // 1.3 有Data属性(首字母大写)
  275. else if (res.data.Data && Array.isArray(res.data.Data)) {
  276. tableData.value = res.data.Data;
  277. params.total = res.data.Total || res.data.Data.length || 0;
  278. }
  279. }
  280. // 2. 检查 res.Data(可能首字母大写)
  281. else if (res.Data) {
  282. if (Array.isArray(res.Data)) {
  283. tableData.value = res.Data;
  284. params.total = res.Total || res.Data.length || 0;
  285. }
  286. }
  287. }
  288. // 检查表格数据中的属性名称是否首字母大写,如果是,则转换为小写
  289. // 这是因为表格组件的prop属性期望的是小写属性名
  290. if (tableData.value.length > 0) {
  291. tableData.value = tableData.value.map(item => {
  292. // 如果有首字母大写的属性,增加小写形式的副本
  293. const newItem = {...item};
  294. if (newItem.Id !== undefined) newItem.id = newItem.Id;
  295. if (newItem.Name !== undefined) newItem.name = newItem.Name;
  296. if (newItem.Type !== undefined) newItem.type = newItem.Type;
  297. if (newItem.Category !== undefined) newItem.category = newItem.Category;
  298. if (newItem.Content !== undefined) newItem.content = newItem.Content;
  299. if (newItem.Description !== undefined) newItem.description = newItem.Description;
  300. if (newItem.CreatedAt !== undefined) newItem.createdAt = newItem.CreatedAt;
  301. if (newItem.UpdatedAt !== undefined) newItem.updatedAt = newItem.UpdatedAt;
  302. return newItem;
  303. });
  304. }
  305. } catch (error) {
  306. // 获取插件列表失败
  307. tableData.value = [];
  308. params.total = 0;
  309. } finally {
  310. loading.value = false;
  311. }
  312. };
  313. // 表单相关
  314. const dialogVisible = ref(false);
  315. const formTitle = ref('');
  316. const submitLoading = ref(false);
  317. const form = reactive<PluginInfo>({
  318. name: '',
  319. type: 'JavaScript',
  320. category: 'Before',
  321. content: '',
  322. description: ''
  323. });
  324. // 表单验证规则
  325. const rules = {
  326. name: [{ required: true, message: '请输入插件名称', trigger: 'blur' }],
  327. type: [{ required: true, message: '请选择插件类型', trigger: 'change' }],
  328. category: [{ required: true, message: '请选择插件分类', trigger: 'change' }],
  329. content: [{ required: true, message: '请输入插件内容', trigger: 'blur' }]
  330. };
  331. // 重置查询表单
  332. const resetQuery = () => {
  333. if (queryRef.value) {
  334. queryRef.value.resetFields();
  335. }
  336. params.keyWord = '';
  337. params.type = '';
  338. params.category = '';
  339. params.dateRange = [];
  340. getList(1);
  341. };
  342. // 新增或编辑插件
  343. const addOrEdit = (row?: PluginInfo) => {
  344. resetForm();
  345. if (row && row.id) {
  346. formTitle.value = '编辑插件';
  347. // 获取详情
  348. api.plugin.get(row.id).then(res => {
  349. // 处理API返回数据
  350. let pluginData = null;
  351. // 检查各种可能的数据结构
  352. if (res.data) {
  353. pluginData = res.data;
  354. } else if (res.Data) {
  355. pluginData = res.Data;
  356. } else if (res) {
  357. pluginData = res;
  358. }
  359. // 首字母大写转小写处理
  360. if (pluginData) {
  361. // 如果有首字母大写的属性,添加小写形式
  362. if (pluginData.Id !== undefined) pluginData.id = pluginData.Id;
  363. if (pluginData.Name !== undefined) pluginData.name = pluginData.Name;
  364. if (pluginData.Type !== undefined) pluginData.type = pluginData.Type;
  365. if (pluginData.Category !== undefined) pluginData.category = pluginData.Category;
  366. if (pluginData.Content !== undefined) pluginData.content = pluginData.Content;
  367. if (pluginData.Description !== undefined) pluginData.description = pluginData.Description;
  368. }
  369. // 将处理后的数据赋值给表单
  370. Object.assign(form, pluginData);
  371. });
  372. } else {
  373. formTitle.value = '新增插件';
  374. }
  375. dialogVisible.value = true;
  376. };
  377. // 重置表单
  378. const resetForm = () => {
  379. if (formRef.value) {
  380. formRef.value.resetFields();
  381. }
  382. form.id = undefined;
  383. form.name = '';
  384. form.type = 'JavaScript';
  385. form.category = 'Before';
  386. form.content = '';
  387. form.description = '';
  388. };
  389. // 提交表单
  390. const submitForm = () => {
  391. if (!formRef.value) return;
  392. formRef.value.validate(async (valid: boolean) => {
  393. if (!valid) return;
  394. submitLoading.value = true;
  395. try {
  396. if (form.id) {
  397. // 编辑
  398. await api.plugin.edit(form);
  399. ElMessage.success('编辑成功');
  400. } else {
  401. // 新增
  402. await api.plugin.add(form);
  403. ElMessage.success('新增成功');
  404. }
  405. dialogVisible.value = false;
  406. getList();
  407. } catch (error) {
  408. // 保存插件失败
  409. } finally {
  410. submitLoading.value = false;
  411. }
  412. });
  413. };
  414. // 插件详情
  415. const detailVisible = ref(false);
  416. const detail = reactive<PluginInfo>({
  417. name: '',
  418. type: 'JavaScript',
  419. category: 'Before',
  420. content: '',
  421. description: ''
  422. });
  423. // 查看插件详情
  424. const viewDetail = (row: PluginInfo) => {
  425. api.plugin.get(row.id).then(res => {
  426. // 处理API返回数据
  427. let pluginData = null;
  428. // 检查各种可能的数据结构
  429. if (res.data) {
  430. pluginData = res.data;
  431. } else if (res.Data) {
  432. pluginData = res.Data;
  433. } else if (res) {
  434. pluginData = res;
  435. }
  436. // 首字母大写转小写处理
  437. if (pluginData) {
  438. // 如果有首字母大写的属性,添加小写形式
  439. if (pluginData.Id !== undefined) pluginData.id = pluginData.Id;
  440. if (pluginData.Name !== undefined) pluginData.name = pluginData.Name;
  441. if (pluginData.Type !== undefined) pluginData.type = pluginData.Type;
  442. if (pluginData.Category !== undefined) pluginData.category = pluginData.Category;
  443. if (pluginData.Content !== undefined) pluginData.content = pluginData.Content;
  444. if (pluginData.Description !== undefined) pluginData.description = pluginData.Description;
  445. }
  446. // 将处理后的数据赋值给详情对象
  447. Object.assign(detail, pluginData);
  448. detailVisible.value = true;
  449. });
  450. };
  451. // 删除插件
  452. const deletePlugin = (row: PluginInfo) => {
  453. ElMessageBox.confirm(`确定要删除插件 "${row.name}" 吗?`, '提示', {
  454. confirmButtonText: '确定',
  455. cancelButtonText: '取消',
  456. type: 'warning'
  457. }).then(async () => {
  458. try {
  459. await api.plugin.delete([row.id]);
  460. ElMessage.success('删除成功');
  461. getList();
  462. } catch (error) {
  463. // 删除插件失败
  464. }
  465. }).catch(() => {});
  466. };
  467. // 插件测试相关
  468. const testVisible = ref(false);
  469. const testLoading = ref(false);
  470. const currentTestPluginId = ref<number>(0);
  471. const currentTestPluginName = ref('');
  472. const testForm = reactive({
  473. context: '{}'
  474. });
  475. const testResult = reactive({
  476. success: false,
  477. message: '',
  478. data: null,
  479. error: ''
  480. });
  481. // 测试模板
  482. const testTemplates = [
  483. { name: '空对象', value: '{}' },
  484. { name: '简单示例', value: '{\n "key": "value",\n "number": 123,\n "boolean": true\n}' },
  485. { name: '请求参数示例', value: '{\n "headers": {\n "content-type": "application/json"\n },\n "body": {\n "data": "example"\n },\n "query": {\n "id": 1\n }\n}' }
  486. ];
  487. // 打开测试窗口
  488. const testPlugin = (row: PluginInfo) => {
  489. currentTestPluginId.value = row.id;
  490. currentTestPluginName.value = row.name || `插件ID: ${row.id}`;
  491. testForm.context = '{}';
  492. testResult.success = false;
  493. testResult.message = '';
  494. testResult.data = null;
  495. testResult.error = '';
  496. testVisible.value = true;
  497. };
  498. // 使用测试模板
  499. const useTestTemplate = (template: string) => {
  500. testForm.context = template;
  501. };
  502. // 格式化JSON
  503. const formatJson = () => {
  504. try {
  505. const obj = JSON.parse(testForm.context);
  506. testForm.context = JSON.stringify(obj, null, 2);
  507. testResult.error = '';
  508. } catch (e) {
  509. testResult.error = `JSON格式错误: ${e instanceof Error ? e.message : String(e)}`;
  510. }
  511. };
  512. // 执行测试
  513. const runTest = async () => {
  514. if (!currentTestPluginId.value) return;
  515. // 先格式化JSON
  516. try {
  517. const parsedJson = JSON.parse(testForm.context);
  518. testForm.context = JSON.stringify(parsedJson, null, 2);
  519. testResult.error = '';
  520. } catch (e) {
  521. testResult.error = `JSON格式错误: ${e instanceof Error ? e.message : String(e)}`;
  522. ElMessage.error(`JSON格式错误: ${e instanceof Error ? e.message : String(e)}`);
  523. return;
  524. }
  525. testLoading.value = true;
  526. testResult.success = false;
  527. testResult.message = '';
  528. testResult.data = null;
  529. try {
  530. // 发送测试请求
  531. const contextData = JSON.parse(testForm.context);
  532. const res = await api.plugin.test({
  533. id: currentTestPluginId.value,
  534. context: contextData
  535. });
  536. // 处理不同格式的响应
  537. if (res.data) {
  538. if (typeof res.data.success === 'boolean') {
  539. // 标准格式的响应
  540. testResult.success = res.data.success;
  541. testResult.message = res.data.message || (
  542. testResult.success ? '测试成功' : '测试失败'
  543. );
  544. testResult.data = res.data.data;
  545. } else {
  546. // 直接返回的数据
  547. testResult.success = true;
  548. testResult.message = '测试成功';
  549. testResult.data = res.data;
  550. }
  551. } else if (res.success !== undefined) {
  552. // 有success字段在根级别的响应
  553. testResult.success = res.success;
  554. testResult.message = res.message || (
  555. testResult.success ? '测试成功' : '测试失败'
  556. );
  557. testResult.data = res.data;
  558. } else {
  559. // 其他格式
  560. testResult.success = true;
  561. testResult.message = '测试成功';
  562. testResult.data = res;
  563. }
  564. // 格式化响应数据显示
  565. if (testResult.data && typeof testResult.data === 'object') {
  566. testResult.data = JSON.stringify(testResult.data, null, 2);
  567. }
  568. } catch (error) {
  569. // 测试插件失败
  570. testResult.success = false;
  571. testResult.message = '测试发生错误';
  572. testResult.data = error instanceof Error ? error.message : String(error);
  573. } finally {
  574. testLoading.value = false;
  575. }
  576. };
  577. // 复制到剪切板
  578. const copyToClipboard = (text: string) => {
  579. try {
  580. navigator.clipboard.writeText(text).then(() => {
  581. ElMessage.success('已复制到剪切板');
  582. }).catch(() => {
  583. ElMessage.error('复制失败,请手动复制');
  584. });
  585. } catch (error) {
  586. // 兼容不支持 Clipboard API 的浏览器
  587. const textarea = document.createElement('textarea');
  588. textarea.textContent = text;
  589. textarea.style.position = 'fixed';
  590. document.body.appendChild(textarea);
  591. textarea.select();
  592. try {
  593. document.execCommand('copy');
  594. ElMessage.success('已复制到剪切板');
  595. } catch (err) {
  596. ElMessage.error('复制失败,请手动复制');
  597. } finally {
  598. document.body.removeChild(textarea);
  599. }
  600. }
  601. };
  602. // 组件挂载后获取数据
  603. onMounted(() => {
  604. getList();
  605. });
  606. </script>
  607. <style scoped>
  608. .plugin-container {
  609. width: 100%;
  610. }
  611. .plugin-header {
  612. display: flex;
  613. justify-content: space-between;
  614. align-items: center;
  615. margin-bottom: 20px;
  616. }
  617. .plugin-header h3 {
  618. margin: 0;
  619. font-size: 18px;
  620. font-weight: 500;
  621. }
  622. .flex-row {
  623. display: flex;
  624. flex-wrap: nowrap;
  625. justify-content: center;
  626. }
  627. .code-preview {
  628. width: 100%;
  629. background-color: #f5f7fa;
  630. border-radius: 4px;
  631. }
  632. /* 测试界面样式 */
  633. .test-container {
  634. padding: 10px;
  635. }
  636. .context-toolbar {
  637. display: flex;
  638. justify-content: flex-start;
  639. align-items: center;
  640. margin-bottom: 10px;
  641. }
  642. .json-textarea {
  643. margin-bottom: 5px;
  644. font-family: monospace;
  645. }
  646. .textarea-error {
  647. border-color: #f56c6c;
  648. }
  649. .error-message {
  650. color: #f56c6c;
  651. font-size: 12px;
  652. display: flex;
  653. align-items: center;
  654. margin-top: 5px;
  655. }
  656. .error-message .el-icon {
  657. margin-right: 5px;
  658. }
  659. .test-result {
  660. margin-top: 20px;
  661. padding: 15px;
  662. background-color: #f8f8f8;
  663. border-radius: 4px;
  664. border: 1px solid #e6e6e6;
  665. }
  666. .result-header {
  667. display: flex;
  668. justify-content: space-between;
  669. align-items: center;
  670. margin-bottom: 15px;
  671. }
  672. .result-title {
  673. font-weight: bold;
  674. font-size: 16px;
  675. }
  676. .result-message {
  677. margin: 10px 0;
  678. word-break: break-word;
  679. }
  680. .result-data {
  681. margin-top: 15px;
  682. }
  683. .result-data-header {
  684. display: flex;
  685. justify-content: space-between;
  686. align-items: center;
  687. margin-bottom: 8px;
  688. }
  689. .result-label {
  690. font-weight: bold;
  691. color: #606266;
  692. }
  693. .data-preview {
  694. background-color: #f5f7fa;
  695. padding: 10px;
  696. border-radius: 4px;
  697. margin: 0;
  698. overflow: auto;
  699. max-height: 300px;
  700. font-family: monospace;
  701. white-space: pre-wrap;
  702. word-break: break-word;
  703. }
  704. .form-tip {
  705. font-size: 12px;
  706. color: #909399;
  707. margin-top: 4px;
  708. }
  709. .test-container {
  710. width: 100%;
  711. }
  712. .test-result {
  713. margin-top: 20px;
  714. padding: 16px;
  715. background-color: #f5f7fa;
  716. border-radius: 4px;
  717. }
  718. .result-title {
  719. font-weight: bold;
  720. margin-bottom: 10px;
  721. }
  722. .result-message {
  723. margin-top: 10px;
  724. padding: 8px;
  725. background-color: #fff;
  726. border-radius: 4px;
  727. }
  728. .result-data {
  729. margin-top: 10px;
  730. }
  731. .result-label {
  732. font-weight: bold;
  733. margin-bottom: 5px;
  734. }
  735. .result-data pre {
  736. background-color: #fff;
  737. padding: 8px;
  738. border-radius: 4px;
  739. white-space: pre-wrap;
  740. word-break: break-all;
  741. max-height: 200px;
  742. overflow-y: auto;
  743. }
  744. </style>