list.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. <template>
  2. <div class="gpu-list">
  3. <!-- 顶部筛选区域 -->
  4. <div class="filter-section">
  5. <el-select
  6. v-model="selectedBucket"
  7. placeholder="请选择知识库"
  8. @change="handleBucketChange"
  9. >
  10. <el-option
  11. v-for="item in buckets"
  12. :key="item.id"
  13. :label="item.name"
  14. :value="item.id"
  15. />
  16. </el-select>
  17. <el-select
  18. v-model="selectedType"
  19. placeholder="请选择目录"
  20. :disabled="!selectedBucket"
  21. class="ml-15"
  22. >
  23. <el-option
  24. v-for="item in types"
  25. :key="item.id"
  26. :label="item.name"
  27. :value="item.id"
  28. />
  29. </el-select>
  30. <el-button
  31. type="primary"
  32. :loading="isSyncing"
  33. :disabled="!selectedType"
  34. class="ml-15"
  35. @click="handleSyncData"
  36. >
  37. {{ isSyncing ? "同步中..." : "同步数据" }}
  38. </el-button>
  39. </div>
  40. <el-tabs v-model="activeName" @tab-click="handleClick">
  41. <!-- 原有的文献列表 tab -->
  42. <el-tab-pane label="文献列表" name="first">
  43. <!-- 添加搜索表单 -->
  44. <div class="search-form">
  45. <el-form
  46. :inline="true"
  47. :model="searchForm"
  48. class="demo-form-inline"
  49. label-width="100px"
  50. ><el-form-item label="名称:">
  51. <el-input
  52. v-model="searchForm.name"
  53. clearable
  54. placeholder="请输入文献名称"
  55. ></el-input>
  56. </el-form-item>
  57. <el-form-item label="创建时间:">
  58. <el-date-picker
  59. v-model="searchForm.createTimeRange"
  60. type="daterange"
  61. range-separator="至"
  62. start-placeholder="开始日期"
  63. end-placeholder="结束日期"
  64. value-format="yyyy-MM-dd"
  65. />
  66. </el-form-item>
  67. <!-- <el-form-item label="文本时间:">
  68. <el-date-picker
  69. v-model="searchForm.textTimeRange"
  70. type="daterange"
  71. range-separator="至"
  72. start-placeholder="开始日期"
  73. end-placeholder="结束日期"
  74. value-format="yyyy-MM-dd"
  75. />
  76. </el-form-item> -->
  77. <el-form-item label="标签:">
  78. <el-select
  79. v-model="searchForm.id"
  80. multiple
  81. filterable
  82. remote
  83. reserve-keyword
  84. :remote-method="searchFormTags"
  85. :loading="searchTagsLoading"
  86. placeholder="请输入标签关键词"
  87. clearable
  88. >
  89. <el-option
  90. v-for="tag in searchTagOptions"
  91. :key="tag.id"
  92. :label="tag.name"
  93. :value="tag.name"
  94. />
  95. </el-select>
  96. </el-form-item>
  97. <el-form-item>
  98. <el-button type="primary" @click="handleSearch">搜索</el-button>
  99. <el-button @click="resetSearch">重置</el-button>
  100. </el-form-item>
  101. </el-form>
  102. </div>
  103. <div class="batch-operations">
  104. <el-button
  105. type="primary"
  106. @click="handleBatchProcess"
  107. :loading="isProcessing"
  108. :disabled="selectedRows.length === 0"
  109. class="ml-15"
  110. >
  111. {{ isProcessing ? "处理中..." : "批量处理文献" }}
  112. </el-button>
  113. <el-button
  114. type="primary"
  115. @click="handleBatchTag"
  116. :disabled="selectedRows.length == 0"
  117. >批量绑定标签</el-button
  118. >
  119. </div>
  120. <!-- 数据表格区域 -->
  121. <el-table
  122. v-loading="isLoading"
  123. :data="gpuList"
  124. style="width: 100%"
  125. @selection-change="handleSelectionChange"
  126. >
  127. <el-table-column type="selection" width="55" />
  128. <el-table-column prop="id" label="ID" width="80" />
  129. <el-table-column prop="name" label="名称">
  130. <!-- <template #default="{ row }">
  131. <el-link @click="handleNameClick(row)">{{ row.name }}</el-link>
  132. </template> -->
  133. </el-table-column>
  134. <el-table-column prop="tags" label="已绑定标签">
  135. <template #default="{ row }">
  136. <el-tag
  137. v-for="(item, index) in row.tags"
  138. :key="index"
  139. type=""
  140. :style="index !== 0 ? 'margin-left: 5px;' : ''"
  141. >
  142. {{ item }}
  143. </el-tag>
  144. </template>
  145. </el-table-column>
  146. <el-table-column prop="type" label="文档类型" width="100" />
  147. <el-table-column prop="create_time" label="创建时间" width="200" />
  148. <el-table-column prop="status" label="处理状态" width="100">
  149. <template #default="{ row }">
  150. <el-tag :type="getStatusType(row.processing_status)">
  151. {{ getStatusText(row.processing_status) }}
  152. </el-tag>
  153. </template>
  154. </el-table-column>
  155. <el-table-column label="操作" width="100">
  156. <template #default="{ row }">
  157. <el-dropdown trigger="click">
  158. <el-button
  159. type="text"
  160. size="small"
  161. circle
  162. class="operation-button"
  163. style="font-size: 15px; margin-left: 10px"
  164. icon="el-icon-more"
  165. ></el-button>
  166. <template #dropdown>
  167. <el-dropdown-menu>
  168. <el-dropdown-item @click.native="handleNameClick(row)">
  169. <i class="el-icon-view"></i> 查看
  170. </el-dropdown-item>
  171. <el-dropdown-item @click.native="handlePreview(row)">
  172. <i class="el-icon-tickets"></i> 预览
  173. </el-dropdown-item>
  174. <el-dropdown-item @click.native="handleSingleProcess(row)">
  175. <i class="el-icon-refresh"></i> 处理文献
  176. </el-dropdown-item>
  177. <!-- <el-dropdown-item
  178. @click.native="firstHandleUpload(scope.$index, scope.row)"
  179. >
  180. <i class="el-icon-refresh"></i> 批量处理
  181. </el-dropdown-item> -->
  182. <!-- <el-dropdown-item
  183. @click.native="firstHandleDelete(scope.$index, scope.row)"
  184. >
  185. <i class="el-icon-delete"></i> 删除
  186. </el-dropdown-item> -->
  187. </el-dropdown-menu>
  188. </template>
  189. </el-dropdown>
  190. </template>
  191. </el-table-column>
  192. </el-table>
  193. <!-- 分页器 -->
  194. <el-pagination
  195. class="pagination"
  196. @size-change="handleSizeChange"
  197. @current-change="handleCurrentChange"
  198. :current-page="currentPage"
  199. :page-sizes="[10, 20, 50, 100]"
  200. :page-size="pageSize"
  201. layout="total, sizes, prev, pager, next, jumper"
  202. :total="total"
  203. />
  204. </el-tab-pane>
  205. <!-- 添加 SOTA详情 tab -->
  206. <el-tab-pane label="SOTA详情" name="second">
  207. <el-table :data="sotaDetailsList" style="width: 100%">
  208. <el-table-column label="Identifier" prop="identifier" />
  209. <el-table-column label="Device" prop="device" />
  210. <el-table-column label="Outcomes" prop="outcomes" />
  211. <el-table-column label="Unit for analysis" prop="unit" />
  212. <el-table-column label="Mean" prop="mean" />
  213. <!-- 操作列与原有类似 -->
  214. </el-table>
  215. </el-tab-pane>
  216. <!-- 添加 SOTA汇总表 tab -->
  217. <el-tab-pane label="SOTA汇总表" name="third">
  218. <el-table :data="sotaSummaryList" style="width: 100%">
  219. <el-table-column label="Summary" prop="summary" />
  220. <el-table-column label="EqMD" prop="eqmd" />
  221. <el-table-column label="Pain:VAS" prop="painVas" />
  222. <!-- 操作列与原有类似 -->
  223. </el-table>
  224. </el-tab-pane>
  225. </el-tabs>
  226. <!-- 添加标签对话框 -->
  227. <el-dialog
  228. title="批量绑定标签"
  229. :visible.sync="tagDialogVisible"
  230. width="800px"
  231. >
  232. <el-form :model="tagForm" label-width="150px">
  233. <el-form-item label="标签:">
  234. <el-select
  235. style="width: 350px"
  236. v-model="tagForm.tags"
  237. multiple
  238. filterable
  239. remote
  240. reserve-keyword
  241. :remote-method="searchTags"
  242. :loading="tagsLoading"
  243. placeholder="请输入关键词搜索标签"
  244. >
  245. <el-option
  246. v-for="tag in tagOptions"
  247. :key="tag.id"
  248. :label="tag.name"
  249. :value="tag.id"
  250. />
  251. </el-select>
  252. </el-form-item>
  253. </el-form>
  254. <template #footer>
  255. <span class="dialog-footer">
  256. <el-button @click="tagDialogVisible = false">取消</el-button>
  257. <el-button type="primary" @click="submitBatchTag">确定</el-button>
  258. </span>
  259. </template>
  260. </el-dialog>
  261. </div>
  262. </template>
  263. <script>
  264. import { debounce } from "lodash-es";
  265. import {
  266. GET_list,
  267. GET_sync,
  268. listBuckets,
  269. selectTypeList,
  270. getDetail,
  271. search_tags,
  272. edit_tags,
  273. search,
  274. process_model_generation,
  275. } from "@/api/knowledge";
  276. import axios from "axios";
  277. // 状态映射表
  278. const STATUS_MAP = {
  279. 0: { text: "未处理", type: "info" },
  280. 1: { text: "处理中", type: "warning" },
  281. 2: { text: "处理完成", type: "success" },
  282. 3: { text: "处理失败", type: "danger" },
  283. };
  284. export default {
  285. name: "GpuList",
  286. data() {
  287. return {
  288. gpuList: [],
  289. currentPage: 1,
  290. pageSize: 10,
  291. total: 0,
  292. isLoading: false,
  293. isSyncing: false,
  294. buckets: [],
  295. types: [],
  296. selectedBucket: null,
  297. selectedType: null,
  298. activeName: "first", // 当前激活的tab
  299. sotaDetailsList: [], // SOTA详情列表数据
  300. sotaSummaryList: [], // SOTA汇总表数据
  301. searchForm: {
  302. name: "",
  303. createTimeRange: [],
  304. textTimeRange: [],
  305. id: [],
  306. },
  307. selectedRows: [], // 选中的行
  308. tagDialogVisible: false, // 标签对话框显示状态
  309. tagForm: {
  310. tags: [], // 选中的标签ID数组
  311. },
  312. tagOptions: [], // 标签选项
  313. tagsLoading: false, // 标签加载状态
  314. searchTagOptions: [], // 搜索表单的标签选项
  315. searchTagsLoading: false, // 搜索表单的标签加载状态
  316. isProcessing: false, // 批量处理的加载状态
  317. pollingTimers: {}, // 新增:存储每个文献的轮询定时器
  318. };
  319. },
  320. created() {
  321. // 使用防抖处理数据加载
  322. this.debouncedFetchGpuList = debounce(this.fetchGpuList, 300);
  323. },
  324. mounted() {
  325. this.initialize();
  326. },
  327. methods: {
  328. // 在中添加单个处理方法
  329. async handleSingleProcess(row) {
  330. try {
  331. await this.$confirm("确认要处理该文献吗?", "提示", {
  332. confirmButtonText: "确定",
  333. cancelButtonText: "取消",
  334. type: "warning",
  335. });
  336. const res = await process_model_generation([row.id]);
  337. if (res.status === 200) {
  338. this.$message.success("处理任务已提交");
  339. await this.fetchGpuList(); // 刷新列表
  340. } else {
  341. throw new Error(res.message || "处理失败");
  342. }
  343. } catch (error) {
  344. if (error === "cancel") {
  345. return;
  346. }
  347. this.$message.error(error.message || "处理提交失败");
  348. console.error("处理错误:", error);
  349. }
  350. },
  351. /* 搜索栏模糊查找 */
  352. /* 搜索表单的标签搜索 */
  353. async searchFormTags(query) {
  354. if (query !== "") {
  355. this.searchTagsLoading = true;
  356. try {
  357. const { data } = await search_tags({ keyword: query });
  358. this.searchTagOptions = data.data.items;
  359. } catch (error) {
  360. this.$message.error("搜索标签失败");
  361. console.error(error);
  362. } finally {
  363. this.searchTagsLoading = false;
  364. }
  365. } else {
  366. this.searchTagOptions = [];
  367. }
  368. },
  369. // 处理表格选择变化
  370. handleSelectionChange(rows) {
  371. this.selectedRows = rows;
  372. },
  373. // 处理批量标签
  374. handleBatchTag() {
  375. this.tagDialogVisible = true;
  376. },
  377. /* 批量处理文献 */
  378. async handleBatchProcess() {
  379. if (this.selectedRows.length === 0) {
  380. this.$message.warning("请选择需要处理的文献");
  381. return;
  382. }
  383. try {
  384. this.isProcessing = true;
  385. const ids = this.selectedRows.map((row) => row.id);
  386. // 添加确认对话框
  387. await this.$confirm("确认要批量处理选中的文献吗?", "提示", {
  388. confirmButtonText: "确定",
  389. cancelButtonText: "取消",
  390. type: "warning",
  391. });
  392. const res = await process_model_generation(ids);
  393. if (res.status === 200) {
  394. this.$message.success("批量处理已提交");
  395. this.selectedRows = []; // 清空选中
  396. await this.fetchGpuList(); // 刷新列表
  397. } else {
  398. throw new Error(res.message || "处理失败");
  399. }
  400. } catch (error) {
  401. if (error === "cancel") {
  402. return;
  403. }
  404. this.$message.error(error.message || "批量处理提交失败");
  405. console.error("批量处理错误:", error);
  406. } finally {
  407. this.isProcessing = false;
  408. }
  409. },
  410. /* 模糊搜索 */
  411. async searchTags(query) {
  412. if (query !== "") {
  413. this.tagsLoading = true;
  414. try {
  415. const { data } = await search_tags({
  416. keyword: query,
  417. });
  418. this.tagOptions = data.data.items;
  419. } catch (error) {
  420. this.$message.error("搜索标签失败");
  421. console.error(error);
  422. } finally {
  423. this.tagsLoading = false;
  424. }
  425. } else {
  426. this.tagOptions = []; // 当搜索词为空时清空选项
  427. }
  428. },
  429. // 修改提交方法
  430. async submitBatchTag() {
  431. try {
  432. const ids = this.selectedRows.map((row) => row.id);
  433. // 这里需要添加实际的API调用
  434. /* await edit_tags({
  435. ids,
  436. tagIds: this.tagForm.tags, // 现在传递的是标签ID数组
  437. }); */
  438. const idObject = { literature_ids: ids, tag_ids: this.tagForm.tags };
  439. const response = await axios.post(
  440. `${process.env.VUE_APP_BASE_API}/literature-tags/relation_tags/`,
  441. idObject,
  442. {
  443. headers: {
  444. "Content-Type": "application/json",
  445. },
  446. }
  447. );
  448. if (response.status !== 200) return;
  449. console.log(this.tagForm.tags);
  450. this.$message.success("标签更新成功");
  451. this.tagDialogVisible = false;
  452. this.fetchGpuList(); // 刷新列表
  453. } catch (error) {
  454. this.$message.error("标签更新失败");
  455. }
  456. },
  457. // 处理搜索
  458. handleSearch() {
  459. this.currentPage = 1; // 重置页码
  460. this.fetchGpuList();
  461. },
  462. // 重置搜索
  463. resetSearch() {
  464. this.searchForm = {
  465. name: "",
  466. createTimeRange: [],
  467. textTimeRange: [],
  468. id: [],
  469. };
  470. this.handleSearch();
  471. },
  472. // 初始化数据
  473. async initialize() {
  474. try {
  475. await this.fetchBuckets();
  476. await this.fetchGpuList();
  477. } catch (error) {
  478. this.$message.error("初始化数据失败");
  479. console.error("初始化失败:", error);
  480. }
  481. },
  482. // 获取状态文本
  483. getStatusText(status) {
  484. return STATUS_MAP[status]?.text || "未知状态";
  485. },
  486. // 获取状态样式类型
  487. getStatusType(status) {
  488. return STATUS_MAP[status]?.type || "info";
  489. },
  490. // 获取知识库列表
  491. async fetchBuckets() {
  492. try {
  493. const { data } = await listBuckets();
  494. this.buckets = data;
  495. } catch (error) {
  496. this.$message.error("获取知识库列表失败");
  497. console.error("获取知识库失败:", error);
  498. }
  499. },
  500. // 处理知识库变更
  501. async handleBucketChange(val) {
  502. this.selectedType = null;
  503. this.types = [];
  504. if (!val) return;
  505. try {
  506. const { data } = await selectTypeList({ kb_id: val });
  507. this.types = data.dataList;
  508. this.debouncedFetchGpuList();
  509. } catch (error) {
  510. this.$message.error("获取目录列表失败");
  511. }
  512. },
  513. // 同步数据处理
  514. async handleSyncData() {
  515. if (!this.selectedType) {
  516. this.$message.warning("请先选择目录");
  517. return;
  518. }
  519. this.isSyncing = true;
  520. try {
  521. await GET_sync({ doc_type_id: this.selectedType });
  522. this.$message.success("同步成功");
  523. await this.fetchGpuList();
  524. } catch (error) {
  525. this.$message.error("同步失败");
  526. } finally {
  527. this.isSyncing = false;
  528. }
  529. },
  530. // 修改 fetchGpuList 方法中的分页处理部分
  531. async fetchGpuList() {
  532. this.isLoading = true;
  533. try {
  534. const params = {
  535. page: this.currentPage,
  536. page_size: this.pageSize,
  537. doc_type_id: this.selectedType || '',
  538. keyword: this.searchForm.name,
  539. tag_names: this.searchForm.id,
  540. };
  541. const response = await axios.post(
  542. `${process.env.VUE_APP_BASE_API}/literature-data/search/`,
  543. params,
  544. {
  545. headers: {
  546. "Content-Type": "application/json",
  547. },
  548. }
  549. );
  550. if (response.status !== 200) return;
  551. // 更新数据和分页信息
  552. const { list, total, current_page, page_size } = response.data.data;
  553. this.gpuList = list.map((item) => {
  554. const nameParts = item.name.split(".");
  555. const processedItem = {
  556. ...item,
  557. name: nameParts[0],
  558. type: nameParts.length > 1 ? nameParts.pop() : "未知",
  559. };
  560. // 检查处理状态,如果是处理中且没有设置轮询,则开始轮询
  561. if (
  562. processedItem.processing_status === 1 &&
  563. !this.pollingTimers[processedItem.id]
  564. ) {
  565. this.startPolling(processedItem.id);
  566. }
  567. // 如果状态不是处理中,但存在轮询,则停止轮询
  568. else if (
  569. processedItem.processing_status !== 1 &&
  570. this.pollingTimers[processedItem.id]
  571. ) {
  572. this.stopPolling(processedItem.id);
  573. }
  574. return processedItem;
  575. });
  576. // 确保这些值都是数字类型
  577. this.total = parseInt(total);
  578. this.currentPage = parseInt(current_page);
  579. this.pageSize = parseInt(page_size);
  580. } catch (error) {
  581. console.error("获取列表数据失败:", error);
  582. this.$message.error("获取列表数据失败");
  583. } finally {
  584. this.isLoading = false;
  585. }
  586. },
  587. // 新增:开始轮询
  588. startPolling(id) {
  589. if (this.pollingTimers[id]) return; // 如果已经在轮询,则不重复设置
  590. this.pollingTimers[id] = setInterval(async () => {
  591. try {
  592. const response = await axios.post(
  593. `${process.env.VUE_APP_BASE_API}/literature-data/search/`,
  594. {
  595. page: this.currentPage,
  596. page_size: this.pageSize,
  597. doc_type_id: this.selectedType || 36,
  598. keyword: this.searchForm.name,
  599. tag_names: this.searchForm.id,
  600. }
  601. );
  602. if (response.status === 200) {
  603. const item = response.data.data.list.find((item) => item.id === id);
  604. if (item && item.processing_status !== 1) {
  605. // 如果状态不再是"处理中",停止轮询
  606. this.stopPolling(id);
  607. await this.fetchGpuList(); // 刷新列表
  608. }
  609. }
  610. } catch (error) {
  611. console.error("轮询检查状态失败:", error);
  612. this.stopPolling(id); // 发生错误时停止轮询
  613. }
  614. }, 30000); // 每分钟检查一次
  615. },
  616. // 新增:停止轮询
  617. stopPolling(id) {
  618. if (this.pollingTimers[id]) {
  619. clearInterval(this.pollingTimers[id]);
  620. delete this.pollingTimers[id];
  621. }
  622. },
  623. // 修改分页处理方法
  624. handleSizeChange(val) {
  625. this.pageSize = val;
  626. this.currentPage = 1; // 切换每页数量时重置为第一页
  627. this.fetchGpuList(); // 移除防抖,直接调用
  628. },
  629. handleCurrentChange(val) {
  630. this.currentPage = val;
  631. this.fetchGpuList(); // 移除防抖,直接调用
  632. },
  633. /* 预览 */
  634. handlePreview(row) {
  635. console.log(row);
  636. let _this = this;
  637. let a = document.createElement("a");
  638. a.href = `#/infoList?id=${row.id}&type=${row.type.toLowerCase()}`; // 使用 hash
  639. a.target = "_blank";
  640. a.click();
  641. },
  642. // 处理名称点击
  643. async handleNameClick(row) {
  644. try {
  645. /* const res = await getDetail({ id: row.id })
  646. console.log('详情数据:', res) */
  647. this.$router.push({
  648. path: "/knowledge/middPage/index",
  649. query: {
  650. id: row.id,
  651. name: row.name,
  652. tag: JSON.stringify(row.tags),
  653. identifier: row.doc_identifier,
  654. },
  655. });
  656. // 这里可以添加详情处理逻辑
  657. } catch (error) {
  658. this.$message.error("获取详情失败");
  659. }
  660. },
  661. // Tab切换处理
  662. handleClick(tab) {
  663. // 根据tab加载相应数据
  664. switch (tab.name) {
  665. case "second":
  666. this.loadSotaDetails();
  667. break;
  668. case "third":
  669. this.loadSotaSummary();
  670. break;
  671. }
  672. },
  673. // 加载SOTA详情数据
  674. async loadSotaDetails() {
  675. try {
  676. // 这里调用API获取SOTA详情数据
  677. const response = await getSotaDetails();
  678. this.sotaDetailsList = response.data;
  679. } catch (error) {
  680. /* this.$message.error("获取SOTA详情失败");
  681. console.error(error); */
  682. }
  683. },
  684. // 加载SOTA汇总表数据
  685. async loadSotaSummary() {
  686. try {
  687. // 这里调用API获取SOTA汇总数据
  688. const response = await getSotaSummary();
  689. this.sotaSummaryList = response.data;
  690. } catch (error) {
  691. /* this.$message.error("获取SOTA汇总数据失败");
  692. console.error(error); */
  693. }
  694. },
  695. },
  696. // 在组件销毁时清理所有轮询
  697. beforeDestroy() {
  698. Object.keys(this.pollingTimers).forEach((id) => {
  699. this.stopPolling(id);
  700. });
  701. },
  702. };
  703. </script>
  704. <style scoped>
  705. .gpu-list {
  706. padding: 20px;
  707. }
  708. .filter-section {
  709. margin-bottom: 20px;
  710. }
  711. .ml-15 {
  712. margin-left: 15px;
  713. }
  714. .pagination {
  715. display: flex;
  716. justify-content: flex-end;
  717. margin-top: 15px;
  718. }
  719. </style>