liveReplay.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. <template>
  2. <div class="app-container" v-loading.fullscreen.lock="loading">
  3. <!-- 直播回放开关区域 -->
  4. <div class="switch-area">
  5. <span class="switch-label">直播回放:</span>
  6. <el-switch
  7. v-model="replayForm.isPlaybackOpen"
  8. @change="handlePlaybackSwitch"
  9. :disabled="isViewOnly"
  10. />
  11. <span class="switch-desc" style="color: #9c9c9c;margin-left: 10px">开启回放,直播结束后学员可查看回放视频</span>
  12. </div>
  13. <!-- 回放模式区域 -->
  14. <div class="playback-mode" v-if="replayForm.isPlaybackOpen">
  15. <div class="mode-title">回放模式:</div>
  16. <div class="mode-option">
  17. <el-radio v-model="replayForm.playbackMode" label="1">模式一:以直播间全部直播视频作为回看</el-radio>
  18. </div>
  19. <!-- <div class="mode-option">-->
  20. <!-- <el-radio v-model="playbackMode" label="2">模式二:以直播间最后一次直播视频作为回看</el-radio>-->
  21. <!-- </div>-->
  22. <!-- <div class="mode-option">-->
  23. <!-- <el-radio v-model="playbackMode" label="3">模式三:自主选择本直播间或素材库任一视频作为回看</el-radio>-->
  24. <!-- </div>-->
  25. </div>
  26. <!-- 回放有效期区域 -->
  27. <div class="validity-period" v-if="replayForm.isPlaybackOpen">
  28. <div class="period-title">回放有效期:</div>
  29. <div class="period-option">
  30. <el-radio @change="updateForm" v-model="replayForm.validityType" label="permanent">永久有效</el-radio>
  31. </div>
  32. <div class="period-option">
  33. <el-radio v-model="replayForm.validityType" label="days">
  34. 直播结束后
  35. <el-input
  36. v-model.number="replayForm.validDays"
  37. type="number"
  38. @change="updateForm"
  39. :min="1"
  40. placeholder="天数"
  41. />
  42. 天有效
  43. <el-tooltip
  44. content="以直播实际结束时间为准"
  45. placement="top"
  46. effect="light"
  47. >
  48. <i class="el-icon-question" style="color: #999; margin-left: 5px" />
  49. </el-tooltip> </el-radio>
  50. </div>
  51. <div class="period-desc">
  52. 到期后,学员无法观看回放内容
  53. </div>
  54. <!-- 倍速播放区域 -->
  55. <div class="speed-play">
  56. <div class="speed-label">倍速播放/快进:</div>
  57. <el-radio @change="updateForm" v-model="replayForm.isSpeedAllowed" label="1">允许</el-radio>
  58. <el-radio @change="updateForm" v-model="replayForm.isSpeedAllowed" label="0">禁止</el-radio>
  59. <span class="speed-desc">禁止时,课程未学完学员不可倍速播放、拖动进度条</span>
  60. <span class="pro-tag">专业版</span>
  61. </div>
  62. </div>
  63. <!-- 回放内容区域 -->
  64. <div class="playback-content">
  65. <div>
  66. <span>回放内容:</span>
  67. <!-- <el-button-->
  68. <!-- class="upload-btn"-->
  69. <!-- type="text"-->
  70. <!-- icon="el-icon-plus"-->
  71. <!-- @click="handleUploadVideo"-->
  72. <!-- >上传视频</el-button>-->
  73. <!-- <span class="upload-tip">上传视频大小不可超过5GB</span>-->
  74. </div>
  75. <!-- 视频列表 -->
  76. <el-table
  77. :data="videoList"
  78. border
  79. style="width: 100%; margin-top: 10px"
  80. >
  81. <el-table-column prop="videoName" label="视频名称">
  82. <template slot-scope="scope">
  83. <div class="video-item">
  84. <!-- <img-->
  85. <!-- :src="scope.row.videoCover"-->
  86. <!-- class="video-cover"-->
  87. <!-- alt="视频封面"-->
  88. <!-- />-->
  89. <div class="video-info">
  90. <div class="video-name">{{ scope.row.videoName }}</div>
  91. </div>
  92. </div>
  93. </template>
  94. </el-table-column>
  95. <el-table-column prop="duration" label="时长" />
  96. <el-table-column prop="status" label="状态">
  97. <template slot-scope="scope">
  98. <span v-if="scope.row.status === '回放中'">
  99. <span class="status-dot" />{{ scope.row.status }}
  100. </span>
  101. <span v-else>{{ scope.row.status }}</span>
  102. </template>
  103. </el-table-column>
  104. <el-table-column prop="updateTime" label="更新时间" />
  105. <el-table-column label="视频地址" prop="videoUrl" >
  106. <template slot-scope="scope">
  107. <el-tooltip
  108. :content="scope.row.videoUrl"
  109. placement="top"
  110. effect="dark"
  111. >
  112. <a
  113. :href="scope.row.videoUrl"
  114. target="_blank"
  115. class="video-url-container"
  116. rel="noopener noreferrer"
  117. >
  118. {{
  119. scope.row.videoUrl.length > 32
  120. ? scope.row.videoUrl.substring(0, 32) + '...'
  121. : scope.row.videoUrl
  122. }}
  123. <i class="el-icon-external-link video-url-icon"></i>
  124. </a>
  125. </el-tooltip>
  126. </template>
  127. </el-table-column>
  128. <!-- <el-table-column label="操作" prop="videoUrl" >-->
  129. <!-- <template slot-scope="scope">-->
  130. <!-- &lt;!&ndash; <el-button&ndash;&gt;-->
  131. <!-- &lt;!&ndash; type="text"&ndash;&gt;-->
  132. <!-- &lt;!&ndash; size="small"&ndash;&gt;-->
  133. <!-- &lt;!&ndash; @click="handleCut(scope.row)"&ndash;&gt;-->
  134. <!-- &lt;!&ndash; >剪切</el-button>&ndash;&gt;-->
  135. <!-- &lt;!&ndash; <el-button&ndash;&gt;-->
  136. <!-- &lt;!&ndash; type="text"&ndash;&gt;-->
  137. <!-- &lt;!&ndash; size="small"&ndash;&gt;-->
  138. <!-- &lt;!&ndash; @click="handleGenerateAiSummary(scope.row)"&ndash;&gt;-->
  139. <!-- &lt;!&ndash; >生成AI速览</el-button>&ndash;&gt;-->
  140. <!-- &lt;!&ndash; <el-button&ndash;&gt;-->
  141. <!-- &lt;!&ndash; type="text"&ndash;&gt;-->
  142. <!-- &lt;!&ndash; size="small"&ndash;&gt;-->
  143. <!-- &lt;!&ndash; @click="handleGenerateSubtitle(scope.row)"&ndash;&gt;-->
  144. <!-- &lt;!&ndash; >生成字幕</el-button>&ndash;&gt;-->
  145. <!-- &lt;!&ndash; <el-button&ndash;&gt;-->
  146. <!-- &lt;!&ndash; type="text"&ndash;&gt;-->
  147. <!-- &lt;!&ndash; size="small"&ndash;&gt;-->
  148. <!-- &lt;!&ndash; @click="handlePlaybackSegment(scope.row)"&ndash;&gt;-->
  149. <!-- &lt;!&ndash; >回放片段</el-button>&ndash;&gt;-->
  150. <!-- </template>-->
  151. <!-- </el-table-column>-->
  152. </el-table>
  153. </div>
  154. </div>
  155. </template>
  156. <script>
  157. import {getLive, updateLive,} from '@/api/live/live'
  158. import {getLiveVideoByLiveId,} from '@/api/live/liveVideo'
  159. export default {
  160. props:{
  161. isViewOnly: {
  162. type: Boolean,
  163. default: false,
  164. }
  165. },
  166. name: "LiveReplay",
  167. data() {
  168. return {
  169. loading: true,
  170. replayForm:{
  171. isPlaybackOpen: false, // 直播回放开关状态
  172. playbackMode: "1", // 回放模式,默认模式一
  173. validityType: "days", // 回放有效期类型,默认天数
  174. validDays: 7, // 有效天数
  175. isSpeedAllowed: "1", // 是否允许倍速播放,默认允许
  176. },
  177. videoList: [
  178. ], // 视频列表数据,实际需从接口获取
  179. liveId: null,
  180. liveInfo: null,
  181. };
  182. },
  183. watch: {
  184. // 监听路由的 query 参数变化
  185. '$route.query': {
  186. handler(newQuery) {
  187. if (this.$route.params.liveId) {
  188. this.liveId = this.$route.params.liveId;
  189. }else {
  190. this.liveId = this.$route.query.liveId;
  191. }
  192. if(this.liveId == null) {
  193. return;
  194. }
  195. this.getLive();
  196. },
  197. // 初始化时立即执行一次
  198. immediate: true
  199. }
  200. },
  201. created() {
  202. // if (this.$route.params.liveId) {
  203. // this.liveId = this.$route.params.liveId;
  204. // }else {
  205. // this.liveId = this.$route.query.liveId;
  206. // }
  207. // if(this.liveId == null) {
  208. // this.$message.error("页面错误,请联系管理员");
  209. // return;
  210. // }
  211. // this.getLive();
  212. },
  213. methods: {
  214. getLiveVideo() {
  215. getLiveVideoByLiveId(this.liveId).then(res => {
  216. let dataEntity =
  217. {
  218. duration: "00:00",
  219. status: "回放中",
  220. updateTime: "2025-09-08 14:28:24",
  221. };
  222. //将秒数转为时分秒
  223. dataEntity.duration = this.convertSeconds(res.data.duration);
  224. dataEntity.updateTime = res.data.updateTime;
  225. dataEntity.videoUrl = res.data.videoUrl;
  226. dataEntity.videoName = this.extractFileName(dataEntity.videoUrl)
  227. this.videoList.push(dataEntity);
  228. });
  229. },
  230. /**
  231. * 提取文件名称核心方法
  232. * 逻辑:通过split('/')分割路径,取最后一个非空元素作为文件名称
  233. */
  234. extractFileName(data) {
  235. try {
  236. this.errorMsg = '';
  237. const trimmedUrl = data.trim();
  238. // 用'/'分割路径,过滤空字符串(避免路径末尾有'/'导致的空元素)
  239. const pathSegments = trimmedUrl.split('/').filter(segment => segment);
  240. if (pathSegments.length === 0) {
  241. throw new Error('输入的路径格式无效,请检查后重新输入');
  242. }
  243. // 最后一个分段即为文件名称
  244. const fileName = pathSegments[pathSegments.length - 1];
  245. // 简单校验是否为常见视频文件格式(可选,根据需求调整)
  246. const videoExtensions = ['mp4', 'mov', 'avi', 'flv', 'mkv'];
  247. const fileExtension = fileName.split('.').pop()?.toLowerCase();
  248. if (!fileExtension || !videoExtensions.includes(fileExtension)) {
  249. this.errorMsg = '提示:提取到的文件可能不是常见视频格式,请注意校验';
  250. }
  251. return fileName;
  252. } catch (err) {
  253. this.errorMsg = err.message;
  254. return '';
  255. }
  256. },
  257. convertSeconds(data) {
  258. // 确保输入是有效的数字
  259. const totalSeconds = Math.max(0, parseInt(data) || 0);
  260. // 计算时、分、秒
  261. const hours = Math.floor(totalSeconds / 3600);
  262. const minutes = Math.floor((totalSeconds % 3600) / 60);
  263. const seconds = totalSeconds % 60;
  264. // 格式化每个部分为两位数
  265. return `${this.padWithZero(hours)}:${this.padWithZero(minutes)}:${this.padWithZero(seconds)}`;
  266. },
  267. padWithZero(num) {
  268. // 将数字转换为两位数格式
  269. return num.toString().padStart(2, '0');
  270. },
  271. getLive() {
  272. getLive(this.liveId).then(res => {
  273. this.liveInfo = res.data;
  274. if(res.data.liveConfig){
  275. this.replayForm = JSON.parse(res.data.liveConfig);
  276. this.getLiveVideo();
  277. }else{
  278. this.resetForm();
  279. }
  280. this.loading = false;
  281. });
  282. },
  283. resetForm() {
  284. this.replayForm={
  285. isPlaybackOpen: false, // 直播回放开关状态
  286. playbackMode: "1", // 回放模式,默认模式一
  287. validityType: "days", // 回放有效期类型,默认天数
  288. validDays: 7, // 有效天数
  289. isSpeedAllowed: "1", // 是否允许倍速播放,默认允许
  290. }
  291. },
  292. // 直播回放开关变更处理
  293. handlePlaybackSwitch(val) {
  294. if(this.isViewOnly) return;
  295. if (this.liveInfo.liveType != 1 && this.liveInfo.liveType != 3) {
  296. this.replayForm.isPlaybackOpen = !val;
  297. this.$message.error("直播回放开关仅支持直播");
  298. return;
  299. }
  300. if (!this.liveInfo.finishTime) {
  301. this.replayForm.isPlaybackOpen = !val;
  302. this.$message.error("直播未结束");
  303. return;
  304. }
  305. this.replayForm.isPlaybackOpen = val;
  306. this.updateForm()
  307. },
  308. updateForm(){
  309. let param = {
  310. liveId: this.liveId,
  311. startTime:this.liveInfo.startTime,
  312. finishTime:this.liveInfo.finishTime,
  313. status:4,
  314. liveConfig: JSON.stringify(this.replayForm)
  315. };
  316. updateLive(param).then(res => {
  317. if (res.code != 200) {
  318. this.$message.error(res.msg);
  319. }
  320. });
  321. },
  322. // 上传视频处理
  323. handleUploadVideo() {
  324. // 模拟上传视频逻辑,实际需调用上传组件或接口
  325. this.$message.info("点击了上传视频按钮,实际需对接上传逻辑");
  326. },
  327. // 剪切视频处理
  328. handleCut(video) {
  329. this.$message.info(`点击了剪切视频,视频名称:${video.videoName}`);
  330. // 实际剪切逻辑
  331. },
  332. // 生成AI速览处理
  333. handleGenerateAiSummary(video) {
  334. this.$message.info(`点击了生成AI速览,视频名称:${video.videoName}`);
  335. // 实际生成AI速览逻辑
  336. },
  337. // 生成字幕处理
  338. handleGenerateSubtitle(video) {
  339. this.$message.info(`点击了生成字幕,视频名称:${video.videoName}`);
  340. // 实际生成字幕逻辑
  341. },
  342. // 回放片段处理
  343. handlePlaybackSegment(video) {
  344. this.$message.info(`点击了回放片段,视频名称:${video.videoName}`);
  345. // 实际回放片段逻辑
  346. },
  347. },
  348. };
  349. </script>
  350. <style scoped>
  351. .live-playback-setting {
  352. font-family: "Microsoft Yahei", sans-serif;
  353. color: #333;
  354. background-color: #fff;
  355. padding: 20px;
  356. border-radius: 4px;
  357. box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
  358. }
  359. .switch-area {
  360. display: flex;
  361. align-items: center;
  362. margin-bottom: 20px;
  363. }
  364. .switch-area .switch-label {
  365. margin-right: 10px;
  366. font-size: 14px;
  367. }
  368. .switch-area .switch-desc {
  369. color: #999;
  370. font-size: 12px;
  371. }
  372. .playback-mode {
  373. background-color: #f9fafc;
  374. padding: 15px;
  375. border-radius: 4px;
  376. margin-bottom: 20px;
  377. }
  378. .playback-mode .mode-title {
  379. font-size: 14px;
  380. margin-bottom: 10px;
  381. }
  382. .playback-mode .mode-option {
  383. display: flex;
  384. align-items: center;
  385. margin-bottom: 8px;
  386. }
  387. .playback-mode .mode-option .el-radio {
  388. margin-left: 2%;
  389. }
  390. .playback-mode .mode-option label {
  391. font-size: 13px;
  392. }
  393. .validity-period {
  394. background-color: #f9fafc;
  395. padding: 15px;
  396. border-radius: 4px;
  397. margin-bottom: 20px;
  398. }
  399. .validity-period .period-title {
  400. font-size: 14px;
  401. margin-bottom: 10px;
  402. }
  403. .validity-period .period-option {
  404. display: flex;
  405. align-items: center;
  406. margin-bottom: 8px;
  407. }
  408. .validity-period .period-option .el-radio {
  409. margin-left: 2%;
  410. }
  411. .validity-period .period-option label {
  412. font-size: 13px;
  413. }
  414. .validity-period .period-desc {
  415. color: #999;
  416. font-size: 12px;
  417. margin-top: 5px;
  418. margin-left: 2%;
  419. }
  420. .validity-period .day-input {
  421. width: 60px;
  422. height: 28px;
  423. border: 1px solid #ddd;
  424. border-radius: 4px;
  425. padding: 0 8px;
  426. margin: 0 5px;
  427. }
  428. .speed-play {
  429. display: flex;
  430. align-items: center;
  431. margin-top: 15px;
  432. }
  433. .speed-play .speed-label {
  434. font-size: 13px;
  435. margin-right: 10px;
  436. }
  437. .speed-play .el-radio {
  438. margin-right: 5px;
  439. }
  440. .speed-play .speed-desc {
  441. color: #999;
  442. font-size: 12px;
  443. margin-left: 10px;
  444. }
  445. .pro-tag {
  446. display: inline-block;
  447. background-color: #007bff;
  448. color: #fff;
  449. font-size: 10px;
  450. padding: 2px 5px;
  451. border-radius: 3px;
  452. margin-left: 5px;
  453. vertical-align: middle;
  454. }
  455. .playback-content {
  456. margin-top: 20px;
  457. }
  458. .playback-content .upload-btn {
  459. display: inline-block;
  460. background-color: #fff;
  461. border: 1px dashed #ddd;
  462. color: #007bff;
  463. padding: 8px 15px;
  464. border-radius: 4px;
  465. cursor: pointer;
  466. font-size: 13px;
  467. margin-bottom: 15px;
  468. }
  469. .playback-content .upload-btn:hover {
  470. border-color: #007bff;
  471. }
  472. .playback-content .upload-tip {
  473. font-size: 12px;
  474. color: #999;
  475. margin-left: 10px;
  476. }
  477. .video-item {
  478. display: flex;
  479. align-items: center;
  480. }
  481. .video-cover {
  482. width: 80px;
  483. height: 45px;
  484. border-radius: 4px;
  485. margin-right: 10px;
  486. }
  487. .video-info .video-name {
  488. font-size: 13px;
  489. margin-bottom: 3px;
  490. }
  491. .video-info .video-desc {
  492. font-size: 12px;
  493. color: #999;
  494. }
  495. .video-duration,
  496. .video-source,
  497. .video-status,
  498. .video-update-time {
  499. color: #666;
  500. }
  501. .video-status .status-dot {
  502. display: inline-block;
  503. width: 8px;
  504. height: 8px;
  505. background-color: #007bff;
  506. border-radius: 50%;
  507. margin-right: 5px;
  508. vertical-align: middle;
  509. }
  510. .video-operation .el-button {
  511. color: #007bff;
  512. font-size: 12px;
  513. margin-right: 10px;
  514. }
  515. .video-url-container {
  516. cursor: pointer;
  517. white-space: nowrap;
  518. overflow: hidden;
  519. text-overflow: ellipsis;
  520. max-width: 100%;
  521. }
  522. </style>