Просмотр исходного кода

feat(视频上传): 重构视频上传和Bilibili搜索组件

重构视频上传对话框和Bilibili搜索对话框,优化UI交互体验
简化视频来源选择提示文本,统一多语言文案
新增批量添加视频功能,支持多选和分页浏览
lsc 4 месяцев назад
Родитель
Сommit
d6ea49ce52

+ 2 - 2
src/components/pages/pptEasy/addCourse3.vue

@@ -917,10 +917,10 @@ import $ from "jquery";
 import appDialog from "../components/appDialog.vue";
 import { myMixin } from "@/mixins/mixin.js";
 import { uploadFileMixin } from "../../tools/uploadFileMixin.js";
-import BilibiliSearchDialog from "./dialog/BilibiliSearchDialog.vue";
+import BilibiliSearchDialog from "./dialog/BilibiliSearchDialog2.vue";
 import { v4 as uuidv4 } from "uuid";
 import InteractiveToolDialog from "./dialog/InteractiveToolDialog.vue";
-import VideoUploadDialog from "./dialog/VideoUploadDialog.vue";
+import VideoUploadDialog from "./dialog/VideoUploadDialog2.vue";
 
 var OpenCC = require("opencc-js");
 let converter = OpenCC.Converter({

+ 767 - 0
src/components/pages/pptEasy/dialog/BilibiliSearchDialog2.vue

@@ -0,0 +1,767 @@
+<template>
+	<div class="bilibili-search-modal" v-if="dialogVisible">
+		<div class="modal-overlay"></div>
+		<div class="modal-content bilibili-search-content">
+			<div class="modal-header">
+				<h3 class="modal-title">{{ lang.ssBilibiliVideoSearch }}</h3>
+				<div class="modal-close" @click="handleClose()">×</div>
+			</div>
+			<div class="modal-body" v-loading="loading" :element-loading-text="lang.ssBilibiliVideoSearchLoading">
+				<!-- 搜索和筛选区域 -->
+				<div class="search-filter-box">
+					<div class="search-item">
+						<div class="search-input-wrapper">
+							<el-input 
+								class="inputC" 
+								:placeholder="lang.ssBilibiliVideoSearchKeyword" 
+								v-model="searchKeyword"
+								@keyup.enter.native="handleSearch"
+							></el-input>
+							<div class="search_img" @click="handleSearch">
+								<img src="../../../../assets/icon/search.png" alt />
+							</div>
+						</div>
+					</div>
+					<div class="filter-item">
+						<el-select v-model="navActive" @change="navClick" :placeholder="lang.ssBiSort" class="sort-select">
+							<el-option :label="lang.ssBiSort" :value="0"></el-option>
+							<el-option :label="lang.ssBiPlay" :value="1"></el-option>
+							<el-option :label="lang.ssBiRelease" :value="2"></el-option>
+							<el-option :label="lang.ssBitDanmaku" :value="3"></el-option>
+							<el-option :label="lang.ssBiFavorite" :value="4"></el-option>
+						</el-select>
+					</div>
+					<div class="add-item">
+						<el-button type="primary" style="background-color: #FF9300;border-color: #FF9300;" @click="batchAdd" :disabled="selectedVideos.length === 0">{{ lang.ssAddbl }}</el-button>
+					</div>
+				</div>
+				<!-- 视频列表 -->
+				<div class="Box" v-if="lang.lang == 'cn'">
+					<div 
+						class="video_box video_box2" 
+						:class="{ 'selected': isVideoSelected(item) }"
+						v-for="(item, index) in paginatedData" 
+						:key="index"
+						@click="toggleVideoSelection(item)"
+					>
+						<div class="video-checkbox" @click.stop>
+							<el-checkbox 
+                class="check"
+								:value="isVideoSelected(item)" 
+								@change="toggleVideoSelection(item)"
+							></el-checkbox>
+						</div>
+						<span class="name" v-html="item.title"></span>
+						<span class="detail">{{ item.description || '' }}</span>
+						<div class="video-info">
+							<span class="author">{{ lang.ssAut }}:{{ item.author || '' }}</span>
+							<span class="duration">{{ lang.ssDuration }}:{{ item.duration || '' }}</span>
+						</div>
+						<div class="tag" v-if="item.tag && typeof item.tag === 'string' && item.tag.split(',').length > 0">
+							<el-tooltip :content="tag" placement="top" effect="dark" v-for="(tag, index) in item.tag.split(',').slice(0, 5)" :key="index">
+								<span>{{ tag }}</span>
+							</el-tooltip>
+						</div>
+						<div class="external-link" @click.stop="openUrl(item.bvid)" :title="lang.ssView">
+							<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">
+								<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>
+								<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>
+								<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>
+							</svg>
+						</div>
+					</div>
+					<div v-if="data.length == 0" class="no_data">{{ lang.ssNoData }}</div>
+				</div>
+				<div class="Box" v-else>
+					<div 
+						class="video_box video_box2" 
+						:class="{ 'selected': isVideoSelected(item) }"
+						v-for="(item, index) in paginatedData" 
+						:key="index"
+						@click="toggleVideoSelection(item)"
+					>
+						<div class="video-checkbox" @click.stop>
+							<el-checkbox 
+								:value="isVideoSelected(item)" 
+								@change="toggleVideoSelection(item)"
+							></el-checkbox>
+						</div>
+						<span class="name" v-html="item.title"></span>
+						<span class="detail">{{ item.description || '' }}</span>
+						<div class="video-info">
+							<span class="author">{{ lang.ssAut }}:{{ item.channel.title || '' }}</span>
+							<span class="duration">{{ lang.ssDuration }}:{{ item.length || '' }}</span>
+						</div>
+						<div class="tag" v-if="item.badges && item.badges.length > 0">
+							<el-tooltip :content="tag" placement="top" effect="dark" v-for="(tag, index) in item.badges" :key="index">
+								<span>{{ tag }}</span>
+							</el-tooltip>
+						</div>
+						<div class="external-link" @click.stop="openUrl(item.id)" :title="lang.ssView">
+							<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">
+								<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>
+								<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>
+								<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>
+							</svg>
+						</div>
+					</div>
+					<div v-if="data.length == 0" class="no_data">{{ lang.ssNoData }}</div>
+				</div>
+
+				<!-- 分页控件 -->
+				<div class="pagination-box" v-if="data.length > 0">
+					<el-button 
+						:disabled="currentPage === 1" 
+						@click="prevPage"
+						size="small"
+					>←{{ lang.ssPrevPage }}</el-button>
+					<span class="page-info">{{ lang.ssPageInfo.replace('*', currentPage / totalPages) }}</span>
+					<el-button 
+						:disabled="currentPage >= totalPages" 
+						@click="nextPage"
+						size="small"
+					>{{ lang.ssNextPage }}→</el-button>
+				</div>
+			</div>
+			<div class="modal-footer">
+				<el-button @click="handleClose" style="background-color: #fff;border-color: #DCDFE6;color: #606266;">{{ lang.ssClose }}</el-button>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import _ from "lodash";
+
+export default {
+  name: "BilibiliSearchDialog",
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    courseName: {
+      type: String,
+      default: ""
+    },
+    grade: {
+      type: String,
+      default: ""
+    }
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      searchKeyword: "",
+      loading: false,
+      data: [],
+      navActive: 0,
+      selectedVideos: [], // 选中的视频列表
+      currentPage: 1, // 当前页码
+      pageSize: 50, // 每页显示数量
+      totalPages: 1 // 总页数
+    };
+  },
+  watch: {
+    visible(newVal) {
+      this.dialogVisible = newVal;
+      if (newVal) {
+        this.initDialog();
+      }
+    },
+    dialogVisible(newVal) {
+      this.$emit('update:visible', newVal);
+    }
+  },
+  methods: {
+    handleClose() {
+      this.dialogVisible = false;
+      this.selectedVideos = [];
+      this.searchKeyword = "";
+      this.data = [];
+      this.currentPage = 1;
+    },
+    initDialog() {
+      this.selectedVideos = [];
+      // 默认以课程标题和已选择的年级为关键词
+      let keywords = [];
+      if (this.courseName) {
+        keywords.push(this.courseName);
+      }
+      if (this.grade) {
+        // 如果grade是多个年级用逗号分隔的,也需要处理
+        keywords.push(this.grade);
+      }
+      this.searchKeyword = keywords.join(',');
+      this.data = [];
+      this.currentPage = 1;
+      if (this.searchKeyword) {
+        this.handleSearch();
+      }
+    },
+    // 搜索视频
+    async handleSearch() {
+      if (!this.searchKeyword) {
+        this.$message.error(this.lang.ssEnterKeyword);
+        return;
+      }
+      try {
+        this.loading = true;
+        let keywords = [];
+        if (this.searchKeyword.split(",").length > 1) {
+          keywords = this.searchKeyword.split(",");
+        } else {
+          keywords = this.searchKeyword.split(",");
+        }
+        this.data = [];
+        let data2 = [];
+        for (var a = 0; a < keywords.length; a++) {
+          let _data = this.lang.lang == 'cn' ? await this.searchBilibili(keywords[a]) : await this.aiGet2(keywords[a]);
+          data2[a] = _data;
+        }
+        this.data = _.flatMap(_.zip(...data2), (pair) => pair.filter(value => value !== undefined));
+        this.currentPage = 1;
+        this.updateTotalPages();
+        this.loading = false;
+      } catch (error) {
+        console.log(error);
+        this.loading = false;
+        this.$message.error(this.lang.ssSearchFail);
+      }
+    },
+    async aiGet2(msg) {
+            let _this = this
+            return new Promise((resolve, reject) => {
+                this.ajax.post(`https://gpt4.cocorobo.cn/get_network_search`, {
+                    engine: "youtubeNew",
+                    keyword: msg,
+                    page: 1,
+                    page_size: 50,
+                    order: this.navActive,
+                    duration: 0,
+                }).then(response => {
+                    console.log(response);
+                    // _this.data = [..._this.data,...response.data.FunctionResponse]
+                    resolve(response.data.FunctionResponse)
+                })
+                    .catch((error) => {
+                        resolve([])
+                        console.log(error);
+                    });
+            });
+        },
+    // Bilibili搜索接口
+    async searchBilibili(keyword) {
+      return new Promise((resolve, reject) => {
+        this.ajax.post(`https://gpt4.cocorobo.cn/get_network_search`, {
+          engine: "bilibiliNew",
+          keyword: keyword,
+          page: 1,
+          page_size: 50,
+          order: this.navActive,
+          duration: 0,
+        }).then(response => {
+          console.log(response);
+          resolve(response.data.FunctionResponse || []);
+        }).catch((error) => {
+          console.log(error);
+          resolve([]);
+        });
+      });
+    },
+    // 切换排序方式
+    navClick(value) {
+      if (this.navActive == value) {
+        return;
+      }
+      this.navActive = value;
+      if (this.searchKeyword) {
+        this.handleSearch();
+      }
+    },
+    // 打开视频链接
+    openUrl(bvid) {
+      if (this.lang.lang == 'cn') {
+        window.open(`//www.bilibili.com/video/${bvid}`);
+      } else {
+        window.open('https://www.youtube.com/watch?v='+bvid)
+      }
+      // window.open(`//www.bilibili.com/video/${bvid}`);
+    },
+    // 添加单个视频
+    addSingleVideo(item) {
+      const videoTitle = item.title.replace(/<[^>]*>?/gm, ''); // 移除HTML标签
+      let url = '';
+      if (this.lang.lang == 'cn') {
+        url = `//player.bilibili.com/player.html?isOutside=true&bvid=${item.bvid}`;
+      } else {
+        url = 'https://www.youtube.com/embed/'+item.id;
+      }
+      const videoData = {
+        name: "链接",
+        title: item.title.replace(/<[^>]*>?/gm, ''),
+        url: url,
+        id: new Date().getTime(),
+        bvid: this.lang.lang == 'cn' ? item.bvid : item.id,
+        type: 75  // B站视频使用type 75
+      };
+      this.$emit('addVideo', videoData);
+      this.$message.success(this.lang.ssAddSuccess);
+      // 添加成功后关闭弹窗
+      this.handleClose();
+    },
+    // 检查视频是否被选中
+    isVideoSelected(item) {
+      if (this.lang.lang == 'cn') {
+        return this.selectedVideos.some(v => v.bvid === item.bvid);
+      } else {
+        return this.selectedVideos.some(v => v.id === item.id);
+      }
+    },
+    // 切换视频选择状态
+    toggleVideoSelection(item) {
+      let index = 0;
+      if (this.lang.lang == 'cn') {
+        index = this.selectedVideos.findIndex(v => v.bvid === item.bvid);
+      } else {
+        index = this.selectedVideos.findIndex(v => v.id === item.id);
+      }
+      // this.selectedVideos.findIndex(v => v.bvid === item.bvid);
+      if (index > -1) {
+        this.selectedVideos.splice(index, 1);
+      } else {
+        this.selectedVideos.push(item);
+      }
+    },
+    // 批量添加选中的视频
+    batchAdd() {
+      if (this.selectedVideos.length === 0) {
+        this.$message.warning(this.lang.ssSelectVideo);
+        return;
+      }
+      this.selectedVideos.forEach(video => {
+        const videoTitle = video.title.replace(/<[^>]*>?/gm, ''); // 移除HTML标签
+        let url = '';
+        if (this.lang.lang == 'cn') {
+          url = `//player.bilibili.com/player.html?isOutside=true&bvid=${video.bvid}`;
+        } else {
+          url = 'https://www.youtube.com/embed/'+video.id;
+        }
+        const videoData = {
+          name: this.lang.ssLink,
+          title: video.title.replace(/<[^>]*>?/gm, ''),
+          url: url,
+          id: new Date().getTime(),
+          bvid: this.lang.lang == 'cn' ? video.bvid : video.id,
+          type: 75  // B站视频使用type 75
+        };
+        this.$emit('addVideo', videoData);
+      });
+      this.$message.success(this.lang.ssAddSuccessVideo.replace('*', this.selectedVideos.length));
+      this.selectedVideos = [];
+      // 批量添加成功后关闭弹窗
+      this.handleClose();
+    },
+    // 清空选择
+    clearSelection() {
+      this.selectedVideos = [];
+    },
+    // 更新总页数
+    updateTotalPages() {
+      this.totalPages = Math.ceil(this.data.length / this.pageSize);
+    },
+    // 上一页
+    prevPage() {
+      if (this.currentPage > 1) {
+        this.currentPage--;
+      }
+    },
+    // 下一页
+    nextPage() {
+      if (this.currentPage < this.totalPages) {
+        this.currentPage++;
+      }
+    }
+  },
+  computed: {
+    // 获取当前页的数据
+    paginatedData() {
+      const start = (this.currentPage - 1) * this.pageSize;
+      const end = start + this.pageSize;
+      return this.data.slice(start, end);
+    }
+  }
+};
+</script>
+
+<style scoped>
+/* Bilibili搜索弹窗 */
+.bilibili-search-modal {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 1002;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.modal-overlay {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-color: rgba(0, 0, 0, 0.5);
+}
+
+.modal-content {
+	position: relative;
+	z-index: 1003;
+	background-color: white;
+	border-radius: 8px;
+	box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+	overflow: hidden;
+	width: 660px;
+	max-height: 90vh;
+	display: flex;
+	flex-direction: column;
+}
+
+.modal-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 20px 24px;
+	/* border-bottom: 1px solid #f0f0f0; */
+}
+
+.modal-title {
+	font-size: 18px;
+	font-weight: 600;
+	color: #333;
+	margin: 0;
+}
+
+.modal-close {
+	font-size: 24px;
+	color: #999;
+	cursor: pointer;
+	transition: all 0.3s;
+	width: 32px;
+	height: 32px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	border-radius: 4px;
+}
+
+.modal-close:hover {
+	background-color: #f3f4f6;
+}
+
+.modal-body {
+	flex: 1;
+	overflow-y: auto;
+	padding: 0;
+	background: #fff;
+}
+
+.modal-footer {
+	padding: 15px 20px;
+	/* border-top: 1px solid #f0f0f0; */
+	background: #fff;
+	display: flex;
+	justify-content: flex-end;
+}
+
+/* 搜索和筛选区域 */
+.search-filter-box {
+    display: flex;
+    align-items: center;
+    gap: 15px;
+    margin-bottom: 15px;
+    padding: 15px;
+}
+
+.search-item {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.search-label {
+    min-width: 80px;
+    font-size: 14px;
+    color: #606266;
+    text-align: right;
+}
+
+.search-input-wrapper {
+    flex: 1;
+    position: relative;
+}
+
+.inputC>>>.el-input__inner {
+    padding-right: 35px;
+    height: 40px;
+    line-height: 40px;
+}
+
+.search_img {
+    width: 20px;
+    height: 20px;
+    position: absolute;
+    right: 10px;
+    top: 50%;
+    transform: translateY(-50%);
+    cursor: pointer;
+    z-index: 10;
+}
+
+.search_img>img {
+    width: 100%;
+    height: 100%;
+}
+
+.filter-item {
+    width: 150px;
+}
+
+.sort-select {
+    width: 100%;
+}
+
+.add-item {
+    width: 80px;
+}
+
+.Box {
+    width: 100%;
+    height: 440px;
+    overflow: auto;
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+    padding: 15px;
+    box-sizing: border-box;
+    background: #fff;
+}
+
+.video_box {
+    width: calc(50% - 10px);
+    margin-right: 10px;
+    display: flex;
+    flex-direction: column;
+    margin-bottom: 15px;
+    cursor: pointer;
+}
+
+.video_box2 {
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    padding: 15px 20px;
+    box-sizing: border-box;
+    border-radius: 8px;
+    position: relative;
+    background: #fafbfc;
+    cursor: pointer;
+    transition: all 0.3s;
+    border: 1px solid #e5e7eb;
+}
+
+.video_box2:hover {
+    background-color: #fffdfa;
+    border-color: #fffdfa;
+    color: #FF9300;
+    box-shadow: 0 2px 8px rgba(40, 92, 245, 0.1);
+    transform: translateY(-2px);
+}
+
+.video_box2.selected {
+    border: 2px solid #FF9300;
+    background-color: #fffdfa;
+}
+
+/* .video_box2.selected .name {
+    color: #FF9300;
+}
+
+.video_box2.selected .detail {
+    color: #FF9300;
+}
+
+.video_box2.selected .video-info {
+    color: #FF9300;
+} */
+
+.video_box2.selected .tag span {
+    background: #fff5e6;
+    color: #FF9300;
+}
+
+.video-checkbox {
+    position: absolute;
+    top: 8px;
+    right: 8px;
+    z-index: 10;
+    background: rgba(255, 255, 255, 0.95);
+    padding: 2px;
+    border-radius: 4px;
+}
+
+.video-checkbox .check >>> .el-checkbox__input.is-checked .el-checkbox__inner {
+    background-color: #FF9300;
+    border-color: #FF9300;
+}
+
+.video-thumbnail {
+    width: 100%;
+    height: 120px;
+    background: #f0f0f0;
+    border-radius: 4px;
+    margin-bottom: 8px;
+    margin-top: 5px;
+}
+
+.video_box>.detail {
+    color: #cecece;
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 3;
+    line-clamp: 3;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    margin: 0 0 5px;
+}
+
+.video_box>.name {
+    color: #000;
+    margin: 5px 0 8px 0;
+    font-weight: bold;
+    font-size: 14px;
+    min-height: 22px;
+    max-height: 44px;
+    line-height: 22px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: block;
+    white-space: normal;
+    word-break: break-all;
+    /* 强制换行成两行,如果内容过多则省略号,且不会折成一行 */
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 2;
+    width: calc(100% - 20px);
+}
+
+.video-info {
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+    margin-bottom: 8px;
+    font-size: 12px;
+    color: #909399;
+}
+
+.video_box>.author {
+    margin: 0;
+    width: 100%;
+    display: block;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.video-info .duration {
+    margin: 0;
+    width: 100%;
+    display: block;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.video_box>.tag {
+    display: flex;
+    flex-wrap: nowrap;
+    margin-top: auto;
+    width: calc(100% - 24px);
+}
+
+.video_box>.tag>span {
+    background: #eef3fb;
+    color: #0061ff;
+    padding: 5px 10px;
+    border-radius: 15px;
+    font-size: 0.9em;
+    margin: 0 5px 0 0;
+    max-width: 100%;
+    overflow: hidden;
+    box-sizing: border-box;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    text-align: center;
+}
+
+.external-link {
+    position: absolute;
+    bottom: 10px;
+    right: 10px;
+    width: 24px;
+    height: 24px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    border-radius: 4px;
+    transition: background 0.2s;
+}
+
+.external-link:hover {
+    background: rgba(0, 0, 0, 0.05);
+}
+
+.external-link svg {
+    width: 16px;
+    height: 16px;
+}
+
+.no_data {
+    height: 500px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+}
+
+.pagination-box {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin: 20px 0;
+    gap: 15px;
+}
+
+.page-info {
+    font-size: 14px;
+    color: #606266;
+}
+
+
+@media screen and (max-width: 1080px) {
+    .video_box {
+        width: calc(100% / 3 - 10px) !important;
+    }
+}
+
+@media screen and (max-width: 760px) {
+    .video_box {
+        width: calc(100% / 2 - 10px) !important;
+    }
+}
+</style>
+

+ 293 - 0
src/components/pages/pptEasy/dialog/VideoUploadDialog2.vue

@@ -0,0 +1,293 @@
+<template>
+	<div class="video-source-modal" v-if="show">
+		<div class="modal-overlay"></div>
+		<div class="modal-content video-source-content">
+			<div class="modal-header">
+				<h3 class="modal-title">{{ lang.ssSelectVideoSource }}</h3>
+				<div class="modal-close" @click="close()">×</div>
+			</div>
+			<div class="modal-body video-source-body">
+				<div class="source-options">
+					<div class="source-option" @click="handleLocalUpload">
+						<div class="option-icon">
+							<svg width="48" height="48" viewBox="0 0 48 48" fill="none"
+								xmlns="http://www.w3.org/2000/svg">
+								<g id="Component 1">
+									<path id="Vector" d="M24 10V38" stroke="currentColor" stroke-width="4" />
+									<path id="Vector_2" d="M10 24L24 10L38 24" stroke="currentColor" stroke-width="4" />
+								</g>
+							</svg>
+						</div>
+						<div class="option-text">{{ lang.ssLocalUpload }}</div>
+					</div>
+					<div class="source-option" @click="handleBilibiliSearch">
+						<div class="option-icon">
+							<svg width="48" height="48" viewBox="0 0 48 48" fill="none"
+								xmlns="http://www.w3.org/2000/svg">
+								<g id="Component 1">
+									<path id="Vector"
+										d="M38 8H10C7.79086 8 6 9.79086 6 12V32C6 34.2091 7.79086 36 10 36H38C40.2091 36 42 34.2091 42 32V12C42 9.79086 40.2091 8 38 8Z"
+										stroke="currentColor" stroke-width="4" />
+									<path id="Vector_2" d="M20 18L32 24L20 30V18Z" stroke="currentColor" stroke-width="4" />
+								</g>
+							</svg>
+						</div>
+						<div class="option-text">{{ lang.ssBiliSearch }}</div>
+					</div>
+				</div>
+			</div>
+		</div>
+		<!-- 隐藏的文件输入 -->
+		<input ref="fileInput" type="file" accept="video/*" style="display: none" @change="handleFileChange" />
+	</div>
+</template>
+
+<script>
+import "../../../../common/aws-sdk-2.235.1.min.js";
+
+export default {
+	data() {
+		return {
+			show: false,
+			data: null,
+		};
+	},
+	computed: {},
+	methods: {
+		open(data) {
+			this.data = data;
+			this.show = true;
+		},
+		close() {
+			this.show = false;
+			this.init();
+		},
+		init() {
+			this.data = null;
+		},
+		// 处理本地文件上传
+		handleLocalUpload() {
+			this.$refs.fileInput.click();
+		},
+		// 处理文件选择
+		handleFileChange(event) {
+			const file = event.target.files[0];
+			if (file) {
+				// 验证文件类型
+				if (!file.type.startsWith('video/')) {
+					this.$message.error('请选择视频文件');
+					event.target.value = "";
+					return;
+				}
+				// 上传文件
+				this.uploadVideo(file);
+			}
+			// 重置input,以便可以再次选择同一个文件
+			event.target.value = "";
+		},
+		// 上传视频文件
+		uploadVideo(file) {
+			const uploadId = new Date().getTime();
+			const fileName = file.name;
+
+			// 初始化进度信息
+			this.$emit("uploadProgress", {
+				id: uploadId,
+				fileName: fileName,
+				progress: 0,
+				loaded: 0,
+				total: file.size,
+				status: 'uploading'
+			});
+
+			var credentials = {
+				accessKeyId: "AKIATLPEDU37QV5CHLMH",
+				secretAccessKey: "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"
+			};
+			window.AWS.config.update(credentials);
+			window.AWS.config.region = "cn-northwest-1";
+
+			var bucket = new window.AWS.S3({ params: { Bucket: "ccrb" } });
+			var _this = this;
+
+			var params = {
+				Key:
+					file.name.split(".")[0] +
+					new Date().getTime() +
+					"." +
+					file.name.split(".")[file.name.split(".").length - 1],
+				ContentType: file.type,
+				Body: file,
+				"Access-Control-Allow-Credentials": "*",
+				ACL: "public-read"
+			};
+			var options = {
+				partSize: 2048 * 1024 * 1024,
+				queueSize: 2,
+				leavePartsOnError: true
+			};
+			bucket
+				.upload(params, options)
+				.on("httpUploadProgress", function (evt) {
+					// 更新上传进度
+					const progress = Math.round((evt.loaded * 100) / evt.total);
+					_this.$emit("uploadProgress", {
+						id: uploadId,
+						fileName: fileName,
+						progress: progress,
+						loaded: evt.loaded,
+						total: evt.total,
+						status: 'uploading'
+					});
+				})
+				.send(function (err, data) {
+					if (err) {
+						// 上传失败,更新进度状态
+						_this.$emit("uploadProgress", {
+							id: uploadId,
+							fileName: fileName,
+							progress: 0,
+							loaded: 0,
+							total: file.size,
+							status: 'error'
+						});
+						_this.$message.error("上传失败");
+					} else {
+						// 上传成功,触发事件传递文件信息
+						_this.$emit("uploadLocalVideo", {
+							id: uploadId,
+							file: file,
+							url: data.Location,
+							name: file.name
+						});
+						_this.close();
+					}
+				});
+		},
+		// 处理Bilibili搜索
+		handleBilibiliSearch() {
+			// 触发Bilibili搜索事件
+			this.$emit("searchBilibili");
+			this.close();
+		},
+	},
+};
+</script>
+
+<style scoped>
+/* 视频来源选择弹窗 */
+.video-source-modal {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 1000;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.modal-overlay {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-color: rgba(0, 0, 0, 0.5);
+}
+
+.modal-content {
+	position: relative;
+	z-index: 1001;
+	background-color: white;
+	border-radius: 8px;
+	box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+	overflow: hidden;
+	width: 600px;
+}
+
+.modal-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 20px 24px;
+	/* border-bottom: 1px solid #f0f0f0; */
+}
+
+.modal-title {
+	font-size: 18px;
+	font-weight: 600;
+	color: #333;
+	margin: 0;
+}
+
+.modal-close {
+	font-size: 24px;
+	color: #999;
+	cursor: pointer;
+	transition: all 0.3s;
+}
+
+.modal-close:hover {
+  /* color: #409eff; */
+  background-color: #f3f4f6;
+}
+
+.video-source-body {
+	padding: 0px 24px 30px;
+}
+
+.source-options {
+	display: flex;
+	justify-content: center;
+	gap: 40px;
+}
+
+.source-option {
+	flex: 1;
+	max-width: 200px;
+	padding: 15px 20px;
+	border: 1px solid #e5e7eb;
+	border-radius: 8px;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	cursor: pointer;
+	transition: all 0.3s;
+	background-color: #fafbfc;
+}
+
+.source-option:hover {
+	background-color: #fffdfa;
+	border-color: #fffdfa;
+	color: #FF9300;
+	box-shadow: 0 2px 8px rgba(40, 92, 245, 0.1);
+}
+
+.source-option:hover .option-icon{
+	color: #FF9300;
+}
+
+.option-icon {
+	width: 95%;
+	height: 80px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	margin-bottom: 20px;
+	border: 1px solid #e5e7eb;
+	color: #6b7280;
+    background: #fff;
+	border-radius: 4px;
+	padding: 10px;
+}
+
+.option-text {
+	font-size: 16px;
+	font-weight: 500;
+	color: #333;
+	text-align: center;
+}
+</style>

+ 1 - 1
src/lang/cn.json

@@ -1941,7 +1941,7 @@
   "ssSearchFail":"搜索失败,请重试",
   "ssSelectVideo":"请先选择要添加的视频",
   "ssAddSuccessVideo":"成功添加*个视频",
-  "ssSelectVideoSource":"选择一种视频来源",
+  "ssSelectVideoSource":"选择视频来源",
   "ssLocalUpload":"从本地文件上传",
   "ssBiliSearch":"从Bilibili检索",
   "ssNewGrade":"新年级",

+ 1 - 1
src/lang/en.json

@@ -1940,7 +1940,7 @@
   "ssSearchFail":"Search failed, please try again",
   "ssSelectVideo":"Please select the video to add",
   "ssAddSuccessVideo":"Successfully added * videos",
-  "ssSelectVideoSource":"Select a Video Source",
+  "ssSelectVideoSource":"Select Video Source",
   "ssLocalUpload":"Upload from Device",
   "ssBiliSearch":"Search on YouTube",
   "ssNewGrade":"New Grade",

+ 1 - 1
src/lang/hk.json

@@ -1940,7 +1940,7 @@
   "ssSearchFail":"搜索失敗,請重試",
   "ssSelectVideo":"請先選擇要添加的視頻",
   "ssAddSuccessVideo":"成功添加*個視頻",
-  "ssSelectVideoSource":"選擇一種視頻來源",
+  "ssSelectVideoSource":"選擇視頻來源",
   "ssLocalUpload":"從本地文件上傳",
   "ssBiliSearch":"從Bilibili檢索",
   "ssNewGrade":"新年級",