BilibiliSearchDialog.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. <template>
  2. <el-dialog
  3. title="Bilibili视频检索"
  4. :visible.sync="dialogVisible"
  5. :append-to-body="true"
  6. width="660px"
  7. :before-close="handleClose"
  8. class="dialog_diy bilibili-search-dialog"
  9. >
  10. <div style="box-sizing:border-box;padding:15px" v-loading="loading" element-loading-text="正在检索中,请稍等...">
  11. <!-- 搜索和筛选区域 -->
  12. <div class="search-filter-box">
  13. <div class="search-item">
  14. <div class="search-input-wrapper">
  15. <el-input
  16. class="inputC"
  17. placeholder="搜索视频关键字(如需搜索多个可,隔开)"
  18. v-model="searchKeyword"
  19. @keyup.enter.native="handleSearch"
  20. ></el-input>
  21. <div class="search_img" @click="handleSearch">
  22. <img src="../../../../assets/icon/search.png" alt />
  23. </div>
  24. </div>
  25. </div>
  26. <div class="filter-item">
  27. <el-select v-model="navActive" @change="navClick" placeholder="综合排序" class="sort-select">
  28. <el-option label="综合排序" :value="0"></el-option>
  29. <el-option label="最多播放" :value="1"></el-option>
  30. <el-option label="最新发布" :value="2"></el-option>
  31. <el-option label="最多弹幕" :value="3"></el-option>
  32. <el-option label="最多收藏" :value="4"></el-option>
  33. </el-select>
  34. </div>
  35. <div class="add-item">
  36. <el-button type="primary" @click="batchAdd" :disabled="selectedVideos.length === 0">加入</el-button>
  37. </div>
  38. </div>
  39. <!-- 视频列表 -->
  40. <div class="Box">
  41. <div
  42. class="video_box video_box2"
  43. :class="{ 'selected': isVideoSelected(item) }"
  44. v-for="(item, index) in paginatedData"
  45. :key="index"
  46. @click="toggleVideoSelection(item)"
  47. >
  48. <div class="video-checkbox" @click.stop>
  49. <el-checkbox
  50. :value="isVideoSelected(item)"
  51. @change="toggleVideoSelection(item)"
  52. ></el-checkbox>
  53. </div>
  54. <span class="name" v-html="item.title"></span>
  55. <span class="detail">{{ item.description || '' }}</span>
  56. <div class="video-info">
  57. <span class="author">作者:{{ item.author || '' }}</span>
  58. <span class="duration">时长:{{ item.duration || '' }}</span>
  59. </div>
  60. <div class="tag" v-if="item.tag && typeof item.tag === 'string' && item.tag.split(',').length > 0">
  61. <el-tooltip :content="tag" placement="top" effect="dark" v-for="(tag, index) in item.tag.split(',').slice(0, 5)" :key="index">
  62. <span>{{ tag }}</span>
  63. </el-tooltip>
  64. </div>
  65. <div class="external-link" @click.stop="openUrl(item.bvid)" title="查看">
  66. <svg t="1755224440400" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4991" width="16" height="16">
  67. <path d="M853.333 469.333a42.667 42.667 0 0 0-42.666 42.667v256a42.667 42.667 0 0 1-42.667 42.667H256a42.667 42.667 0 0 1-42.667-42.667V256a42.667 42.667 0 0 1 42.667-42.667h256a42.667 42.667 0 0 0 0-85.333H256a128 128 0 0 0-128 128v512a128 128 0 0 0 128 128h512a128 128 0 0 0 128-128V512a42.667 42.667 0 0 0-42.667-42.667z" fill="#909399" p-id="4992"></path>
  68. <path d="M682.667 213.333h270.933v270.933a42.667 42.667 0 1 0 85.334 0V170.667a42.667 42.667 0 0 0-42.667-42.667H682.667a42.667 42.667 0 0 0 0 85.333z" fill="#909399" p-id="4993"></path>
  69. <path d="M894.293 174.293a42.667 42.667 0 0 0-60.586 0L591.147 417.707a42.667 42.667 0 0 0 60.586 60.586L894.293 234.88a42.667 42.667 0 0 0 0-60.587z" fill="#909399" p-id="4994"></path>
  70. </svg>
  71. </div>
  72. </div>
  73. <div v-if="data.length == 0" class="no_data">暂无数据</div>
  74. </div>
  75. <!-- 分页控件 -->
  76. <div class="pagination-box" v-if="data.length > 0">
  77. <el-button
  78. :disabled="currentPage === 1"
  79. @click="prevPage"
  80. size="small"
  81. >←上一页</el-button>
  82. <span class="page-info">第 {{ currentPage }} / {{ totalPages }} 页</span>
  83. <el-button
  84. :disabled="currentPage >= totalPages"
  85. @click="nextPage"
  86. size="small"
  87. >下一页→</el-button>
  88. </div>
  89. </div>
  90. <span slot="footer" class="dialog-footer">
  91. <el-button @click="handleClose">关 闭</el-button>
  92. </span>
  93. </el-dialog>
  94. </template>
  95. <script>
  96. import _ from "lodash";
  97. export default {
  98. name: "BilibiliSearchDialog",
  99. props: {
  100. visible: {
  101. type: Boolean,
  102. default: false
  103. },
  104. courseName: {
  105. type: String,
  106. default: ""
  107. },
  108. grade: {
  109. type: String,
  110. default: ""
  111. }
  112. },
  113. data() {
  114. return {
  115. dialogVisible: false,
  116. searchKeyword: "",
  117. loading: false,
  118. data: [],
  119. navActive: 0,
  120. selectedVideos: [], // 选中的视频列表
  121. currentPage: 1, // 当前页码
  122. pageSize: 20, // 每页显示数量
  123. totalPages: 1 // 总页数
  124. };
  125. },
  126. watch: {
  127. visible(newVal) {
  128. this.dialogVisible = newVal;
  129. if (newVal) {
  130. this.initDialog();
  131. }
  132. },
  133. dialogVisible(newVal) {
  134. this.$emit('update:visible', newVal);
  135. }
  136. },
  137. methods: {
  138. handleClose() {
  139. this.dialogVisible = false;
  140. this.selectedVideos = [];
  141. this.searchKeyword = "";
  142. this.data = [];
  143. this.currentPage = 1;
  144. },
  145. initDialog() {
  146. this.selectedVideos = [];
  147. // 默认以课程标题和已选择的年级为关键词
  148. let keywords = [];
  149. if (this.courseName) {
  150. keywords.push(this.courseName);
  151. }
  152. if (this.grade) {
  153. // 如果grade是多个年级用逗号分隔的,也需要处理
  154. keywords.push(this.grade);
  155. }
  156. this.searchKeyword = keywords.join(',');
  157. this.data = [];
  158. this.currentPage = 1;
  159. if (this.searchKeyword) {
  160. this.handleSearch();
  161. }
  162. },
  163. // 搜索视频
  164. async handleSearch() {
  165. if (!this.searchKeyword) {
  166. this.$message.error("请输入关键字");
  167. return;
  168. }
  169. try {
  170. this.loading = true;
  171. let keywords = [];
  172. if (this.searchKeyword.split(",").length > 1) {
  173. keywords = this.searchKeyword.split(",");
  174. } else {
  175. keywords = this.searchKeyword.split(",");
  176. }
  177. this.data = [];
  178. let data2 = [];
  179. for (var a = 0; a < keywords.length; a++) {
  180. let _data = await this.searchBilibili(keywords[a]);
  181. data2[a] = _data;
  182. }
  183. this.data = _.flatMap(_.zip(...data2), (pair) => pair.filter(value => value !== undefined));
  184. this.currentPage = 1;
  185. this.updateTotalPages();
  186. this.loading = false;
  187. } catch (error) {
  188. console.log(error);
  189. this.loading = false;
  190. this.$message.error("搜索失败,请重试");
  191. }
  192. },
  193. // Bilibili搜索接口
  194. searchBilibili(keyword) {
  195. return new Promise((resolve, reject) => {
  196. this.ajax.post(`https://gpt4.cocorobo.cn/get_network_search`, {
  197. engine: "bilibiliNew",
  198. keyword: keyword,
  199. page: 1,
  200. page_size: 20,
  201. order: this.navActive,
  202. duration: 0,
  203. }).then(response => {
  204. console.log(response);
  205. resolve(response.data.FunctionResponse || []);
  206. }).catch((error) => {
  207. console.log(error);
  208. resolve([]);
  209. });
  210. });
  211. },
  212. // 切换排序方式
  213. navClick(value) {
  214. if (this.navActive == value) {
  215. return;
  216. }
  217. this.navActive = value;
  218. if (this.searchKeyword) {
  219. this.handleSearch();
  220. }
  221. },
  222. // 打开视频链接
  223. openUrl(bvid) {
  224. window.open(`//www.bilibili.com/video/${bvid}`);
  225. },
  226. // 添加单个视频
  227. addSingleVideo(item) {
  228. const videoTitle = item.title.replace(/<[^>]*>?/gm, ''); // 移除HTML标签
  229. const videoData = {
  230. name: "链接",
  231. title: item.title.replace(/<[^>]*>?/gm, ''),
  232. url: `//player.bilibili.com/player.html?isOutside=true&bvid=${item.bvid}`,
  233. id: new Date().getTime(),
  234. bvid: item.bvid,
  235. type: 75 // B站视频使用type 75
  236. };
  237. this.$emit('addVideo', videoData);
  238. this.$message.success('加入成功');
  239. // 添加成功后关闭弹窗
  240. this.handleClose();
  241. },
  242. // 检查视频是否被选中
  243. isVideoSelected(item) {
  244. return this.selectedVideos.some(v => v.bvid === item.bvid);
  245. },
  246. // 切换视频选择状态
  247. toggleVideoSelection(item) {
  248. const index = this.selectedVideos.findIndex(v => v.bvid === item.bvid);
  249. if (index > -1) {
  250. this.selectedVideos.splice(index, 1);
  251. } else {
  252. this.selectedVideos.push(item);
  253. }
  254. },
  255. // 批量添加选中的视频
  256. batchAdd() {
  257. if (this.selectedVideos.length === 0) {
  258. this.$message.warning('请先选择要添加的视频');
  259. return;
  260. }
  261. this.selectedVideos.forEach(video => {
  262. const videoTitle = video.title.replace(/<[^>]*>?/gm, ''); // 移除HTML标签
  263. const videoData = {
  264. name: "链接",
  265. title: video.title.replace(/<[^>]*>?/gm, ''),
  266. url: `//player.bilibili.com/player.html?isOutside=true&bvid=${video.bvid}`,
  267. id: new Date().getTime(),
  268. bvid: video.bvid,
  269. type: 75 // B站视频使用type 75
  270. };
  271. this.$emit('addVideo', videoData);
  272. });
  273. this.$message.success(`成功添加 ${this.selectedVideos.length} 个视频`);
  274. this.selectedVideos = [];
  275. // 批量添加成功后关闭弹窗
  276. this.handleClose();
  277. },
  278. // 清空选择
  279. clearSelection() {
  280. this.selectedVideos = [];
  281. },
  282. // 更新总页数
  283. updateTotalPages() {
  284. this.totalPages = Math.ceil(this.data.length / this.pageSize);
  285. },
  286. // 上一页
  287. prevPage() {
  288. if (this.currentPage > 1) {
  289. this.currentPage--;
  290. }
  291. },
  292. // 下一页
  293. nextPage() {
  294. if (this.currentPage < this.totalPages) {
  295. this.currentPage++;
  296. }
  297. }
  298. },
  299. computed: {
  300. // 获取当前页的数据
  301. paginatedData() {
  302. const start = (this.currentPage - 1) * this.pageSize;
  303. const end = start + this.pageSize;
  304. return this.data.slice(start, end);
  305. }
  306. }
  307. };
  308. </script>
  309. <style scoped>
  310. .dialog_diy>>>.el-dialog {
  311. height: auto;
  312. margin: 50px auto 0 !important;
  313. }
  314. .dialog_diy>>>.el-dialog__header {
  315. background: #454545 !important;
  316. padding: 15px 20px;
  317. }
  318. .dialog_diy>>>.el-dialog__body {
  319. height: calc(100% - 124px);
  320. box-sizing: border-box;
  321. padding: 0px;
  322. }
  323. .dialog_diy>>>.el-dialog__title {
  324. color: #fff;
  325. }
  326. .dialog_diy>>>.el-dialog__headerbtn {
  327. top: 19px;
  328. }
  329. .dialog_diy>>>.el-dialog__headerbtn .el-dialog__close {
  330. color: #fff;
  331. }
  332. .dialog_diy>>>.el-dialog__headerbtn .el-dialog__close:hover {
  333. color: #fff;
  334. }
  335. .dialog_diy>>>.el-dialog__body,
  336. .dialog_diy>>>.el-dialog__footer {
  337. background: #fafafa;
  338. }
  339. /* 搜索和筛选区域 */
  340. .search-filter-box {
  341. display: flex;
  342. align-items: center;
  343. gap: 15px;
  344. margin-bottom: 15px;
  345. padding: 10px 0;
  346. }
  347. .search-item {
  348. flex: 1;
  349. display: flex;
  350. align-items: center;
  351. gap: 10px;
  352. }
  353. .search-label {
  354. min-width: 80px;
  355. font-size: 14px;
  356. color: #606266;
  357. text-align: right;
  358. }
  359. .search-input-wrapper {
  360. flex: 1;
  361. position: relative;
  362. }
  363. .inputC>>>.el-input__inner {
  364. padding-right: 35px;
  365. height: 40px;
  366. line-height: 40px;
  367. }
  368. .search_img {
  369. width: 20px;
  370. height: 20px;
  371. position: absolute;
  372. right: 10px;
  373. top: 50%;
  374. transform: translateY(-50%);
  375. cursor: pointer;
  376. z-index: 10;
  377. }
  378. .search_img>img {
  379. width: 100%;
  380. height: 100%;
  381. }
  382. .filter-item {
  383. width: 150px;
  384. }
  385. .sort-select {
  386. width: 100%;
  387. }
  388. .add-item {
  389. width: 80px;
  390. }
  391. .Box {
  392. width: 100%;
  393. height: 500px;
  394. overflow: auto;
  395. display: flex;
  396. flex-direction: row;
  397. justify-content: flex-start;
  398. flex-wrap: wrap;
  399. padding: 5px;
  400. box-sizing: border-box;
  401. }
  402. .video_box {
  403. width: calc(50% - 10px);
  404. margin-right: 10px;
  405. display: flex;
  406. flex-direction: column;
  407. margin-bottom: 15px;
  408. cursor: pointer;
  409. }
  410. .video_box2 {
  411. box-shadow: 0px 0px 4px 2px #d2d2d282;
  412. padding: 10px;
  413. box-sizing: border-box;
  414. border-radius: 5px;
  415. position: relative;
  416. background: #fff;
  417. cursor: pointer;
  418. transition: all 0.2s;
  419. border: 2px solid transparent;
  420. }
  421. .video_box2:hover {
  422. box-shadow: 0px 0px 8px 4px #d2d2d282;
  423. transform: translateY(-2px);
  424. }
  425. .video_box2.selected {
  426. border: 2px solid #409eff;
  427. /* box-shadow: 0px 0px 8px 4px rgba(64, 158, 255, 0.3); */
  428. }
  429. .video-checkbox {
  430. position: absolute;
  431. top: 8px;
  432. right: 8px;
  433. z-index: 10;
  434. background: rgba(255, 255, 255, 0.95);
  435. padding: 2px;
  436. border-radius: 4px;
  437. }
  438. .video-thumbnail {
  439. width: 100%;
  440. height: 120px;
  441. background: #f0f0f0;
  442. border-radius: 4px;
  443. margin-bottom: 8px;
  444. margin-top: 5px;
  445. }
  446. .video_box>.detail {
  447. color: #cecece;
  448. display: -webkit-box;
  449. -webkit-box-orient: vertical;
  450. -webkit-line-clamp: 3;
  451. line-clamp: 3;
  452. overflow: hidden;
  453. text-overflow: ellipsis;
  454. margin: 0 0 5px;
  455. }
  456. .video_box>.name {
  457. color: #000;
  458. margin: 5px 0 8px 0;
  459. font-weight: bold;
  460. font-size: 14px;
  461. min-height: 22px;
  462. max-height: 44px;
  463. line-height: 22px;
  464. overflow: hidden;
  465. text-overflow: ellipsis;
  466. display: block;
  467. white-space: normal;
  468. word-break: break-all;
  469. /* 强制换行成两行,如果内容过多则省略号,且不会折成一行 */
  470. display: -webkit-box;
  471. -webkit-box-orient: vertical;
  472. -webkit-line-clamp: 2;
  473. width: calc(100% - 20px);
  474. }
  475. .video-info {
  476. display: flex;
  477. flex-direction: column;
  478. gap: 5px;
  479. margin-bottom: 8px;
  480. font-size: 12px;
  481. color: #909399;
  482. }
  483. .video_box>.author {
  484. margin: 0;
  485. width: 100%;
  486. display: block;
  487. overflow: hidden;
  488. text-overflow: ellipsis;
  489. white-space: nowrap;
  490. }
  491. .video-info .duration {
  492. margin: 0;
  493. width: 100%;
  494. display: block;
  495. overflow: hidden;
  496. text-overflow: ellipsis;
  497. white-space: nowrap;
  498. }
  499. .video_box>.tag {
  500. display: flex;
  501. flex-wrap: nowrap;
  502. margin-top: auto;
  503. width: calc(100% - 24px);
  504. }
  505. .video_box>.tag>span {
  506. background: #eef3fb;
  507. color: #0061ff;
  508. padding: 5px 10px;
  509. border-radius: 15px;
  510. font-size: 0.9em;
  511. margin: 0 5px 0 0;
  512. max-width: 100%;
  513. overflow: hidden;
  514. box-sizing: border-box;
  515. text-overflow: ellipsis;
  516. white-space: nowrap;
  517. text-align: center;
  518. }
  519. .external-link {
  520. position: absolute;
  521. bottom: 10px;
  522. right: 10px;
  523. width: 24px;
  524. height: 24px;
  525. display: flex;
  526. align-items: center;
  527. justify-content: center;
  528. cursor: pointer;
  529. border-radius: 4px;
  530. transition: background 0.2s;
  531. }
  532. .external-link:hover {
  533. background: rgba(0, 0, 0, 0.05);
  534. }
  535. .external-link svg {
  536. width: 16px;
  537. height: 16px;
  538. }
  539. .no_data {
  540. height: 500px;
  541. display: flex;
  542. align-items: center;
  543. justify-content: center;
  544. width: 100%;
  545. }
  546. .pagination-box {
  547. display: flex;
  548. justify-content: center;
  549. align-items: center;
  550. margin: 20px 0;
  551. gap: 15px;
  552. }
  553. .page-info {
  554. font-size: 14px;
  555. color: #606266;
  556. }
  557. @media screen and (max-width: 1080px) {
  558. .video_box {
  559. width: calc(100% / 3 - 10px) !important;
  560. }
  561. }
  562. @media screen and (max-width: 760px) {
  563. .video_box {
  564. width: calc(100% / 2 - 10px) !important;
  565. }
  566. }
  567. </style>