modifyVideoPage.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. <template>
  2. <!-- 新增编辑视频 -->
  3. <div class="modify-video-page-wrap">
  4. <t-form :data="form" :rules="rules" ref="formRules">
  5. <t-form-item label="所属分类" name="ClassifyId">
  6. <t-cascader placeholder="选择所属分类"
  7. v-model="form.ClassifyId"
  8. :options="classifyList"
  9. style="width: 300px;"
  10. clearable
  11. :keys="{
  12. emitPath:false,
  13. label:'ClassifyName',
  14. value:'ClassifyId',
  15. children:'Children'}">
  16. </t-cascader>
  17. </t-form-item>
  18. <t-form-item label="视频名称" name="Title">
  19. <t-input style="width: 300px;" v-model="form.Title" placeholder="请输入视频名称"></t-input>
  20. </t-form-item>
  21. <t-form-item label="视频简介" name="Introduce">
  22. <t-textarea v-model="form.Introduce"
  23. placeholder="请输入视频简介"
  24. maxlength="50" show-word-limit :autosize="{ minRows: 5, maxRows: 5 }"></t-textarea >
  25. </t-form-item>
  26. <t-form-item label="上传封面" name="CoverImg">
  27. <div style="display: block;">
  28. <t-upload accept="image/*"
  29. :http-request="handleUploadImg" :show-file-list="false" :disabled="isImageUploading">
  30. <t-button theme="primary" :loading="isImageUploading">点击上传</t-button>
  31. <span style="color:#999999;margin-left: 5px;" @click.stop>建议尺寸比例3:2,支持png、jpg、gif、jpeg格式</span>
  32. </t-upload>
  33. <div class="img-box">
  34. <img :src="form.CoverImg" v-if="form.CoverImg">
  35. <span v-else style="color:#999999;line-height: 100px;">请上传封面图</span>
  36. </div>
  37. </div>
  38. </t-form-item>
  39. <t-form-item label="上传视频" name="VideoUrl">
  40. <div style="display: block;">
  41. <t-upload accept=".mp4"
  42. :request-method="handleUploadVideo" :show-file-list="false" :disabled="isVideoUploading">
  43. <t-button theme="primary" :loading="isVideoUploading">点击上传</t-button>
  44. <span style="color:#999999;margin-left: 5px;" @click.stop>仅支持mp4格式</span>
  45. </t-upload>
  46. <div class="img-box">
  47. <t-progress type="circle" :percentage="percentage" width="40" v-if="isVideoUploading"></t-progress>
  48. <span v-if="form.VideoUrl&&!form.VideoId" class="duration">{{timeDuration}}</span>
  49. <img :src="form.CoverImg" v-if="form.VideoUrl" @click="handlePreviewVideo">
  50. <span v-else style="color:#999999;line-height: 100px;">请上传视频</span>
  51. </div>
  52. </div>
  53. </t-form-item>
  54. <t-form-item label="视频标签" name="TagIds">
  55. <t-button variant="text" theme="primary" @click="isModifyDialogShow = true">选择标签</t-button>
  56. <div class="tag-list" :key="tagIdKey">
  57. <span class="tag-item" v-for="tag in form.TagIds" :key="tag.TagId">
  58. <span>{{tag.TagName}}</span>
  59. <span @click.stop="removeTag(tag)"><i class="t-icon-close"></i></span>
  60. </span>
  61. </div>
  62. </t-form-item>
  63. </t-form>
  64. <div class="btn-box">
  65. <t-button variant="outline" plain @click="changeRoute">取消</t-button>
  66. <t-button theme="primary" @click="modifyVideo" :loading="isImageUploading||isVideoUploading">保存</t-button>
  67. <t-button theme="primary" @click="publishVideo" :loading="isImageUploading||isVideoUploading">发布</t-button>
  68. </div>
  69. <!-- 选择标签弹窗 -->
  70. <AddTags
  71. :isModifyDialogShow="isModifyDialogShow"
  72. :Tags="form.TagIds"
  73. :tagList="tagList"
  74. :getTagList="getTagList"
  75. @modify="modifyTags"
  76. @close="isModifyDialogShow=false"
  77. />
  78. <!-- 预览视频弹窗 -->
  79. <t-dialog
  80. :visible.sync="previewPop"
  81. :modal-append-to-body='false'
  82. width="60vw"
  83. :title="previewPopTitle"
  84. @close="endingPreview"
  85. >
  86. <video style="width: 100%;height: 100%;max-height: 70vh;outline: none;"
  87. controls :src="previewVideoUrl" autoplay ref="previewVideo">
  88. 您的浏览器暂不支持,请更换浏览器
  89. </video>
  90. </t-dialog>
  91. </div>
  92. </template>
  93. <script setup>
  94. import { ref, onMounted } from 'vue';
  95. import MD5 from 'js-md5';
  96. import { VideoInterface, TagInterface, ClassifyInterface } from '@/api/modules/trainingApi';
  97. import AddTags from './components/addTags.vue';
  98. import _ from 'lodash';
  99. import { useRoute, useRouter } from 'vue-router'
  100. const router=useRouter()
  101. const route=useRoute()
  102. const form = ref({
  103. Title: '',
  104. Introduce: '',
  105. ClassifyId: '',
  106. TagIds: [],
  107. CoverImg: '',
  108. VideoUrl: ''
  109. });
  110. const rules = {
  111. ClassifyId: [{ required: true, message: '请选择所属分类' }],
  112. Title: [{ required: true, message: '请输入视频名称' }],
  113. CoverImg: [{ required: true, message: '请选择视频封面' }],
  114. VideoUrl: [{ required: true, message: '请上传视频' }],
  115. TagIds: [{
  116. required: true,
  117. validator: (rule, value, callback) => {
  118. if (!value.length) {
  119. return callback(new Error("请至少选择一个标签"));
  120. } else {
  121. return callback();
  122. }
  123. }
  124. }]
  125. }
  126. const tagIdKey = ref(0);
  127. const isModifyDialogShow = ref(false);
  128. const isImageUploading = ref(false);
  129. const isVideoUploading = ref(false);
  130. const percentage = ref(0);
  131. const timeDuration = ref('');
  132. const previewVideoUrl = ref('');
  133. const previewPop = ref(false);
  134. const previewPopTitle = ref('');
  135. const tagList = ref([]);
  136. const classifyList = ref([]);
  137. const previewVideo = ref(null);
  138. const formRules = ref(null);
  139. // 定义全局变量
  140. let ALOSSINS = null;
  141. let ALOSSAbortCheckpoint = null;
  142. // 获取分类列表
  143. const getClassifyList = async (type = '') => {
  144. try {
  145. const res = await ClassifyInterface.getClassifyList({});
  146. if (res.Ret !== 200) return;
  147. classifyList.value = (res.Data && res.Data.List) || [];
  148. filterNodes(classifyList.value);
  149. classifyList.value = classifyList.value.map(item => {
  150. if (!item.Children) {
  151. item.disabled = true;
  152. }
  153. return item;
  154. });
  155. } catch (error) {
  156. console.error('获取分类列表失败:', error);
  157. }
  158. };
  159. // 过滤节点
  160. const filterNodes = (arr) => {
  161. if (arr.length) {
  162. arr.forEach(item => {
  163. if (item.Children && item.Children.length) {
  164. filterNodes(item.Children);
  165. }
  166. if (item.Children && !item.Children.length) {
  167. delete item.Children;
  168. }
  169. });
  170. }
  171. };
  172. // 获取标签列表
  173. const getTagList = async (keyword = '') => {
  174. try {
  175. const res = await TagInterface.getTagList({
  176. Keyword: keyword,
  177. PageSize: 1000,
  178. CurrentIndex: 1
  179. });
  180. if (res.Ret !== 200) return;
  181. tagList.value = (res.Data && res.Data.List) || [];
  182. } catch (error) {
  183. console.error('获取标签列表失败:', error);
  184. }
  185. };
  186. // 检查图片是否合法
  187. const handleUploadImg = (file) => {
  188. isImageUploading.value = true;
  189. const { type } = file.file;
  190. if (!['image/png', 'image/jpeg'].includes(type)) {
  191. MessagePlugin.warning('仅支持png、jpg格式的图片');
  192. isImageUploading.value = false;
  193. return;
  194. }
  195. uploadImg(file);
  196. };
  197. // 上传图片
  198. const uploadImg = (file) => {
  199. const formData = new FormData();
  200. formData.append('file', file.file);
  201. VideoInterface.bannerupload(formData).then(res => {
  202. isImageUploading.value = false;
  203. if (res.Ret !== 200) return;
  204. form.value.CoverImg = res.Data.ResourceUrl;
  205. });
  206. };
  207. // 检查视频是否合法,并获取视频时长
  208. const handleUploadVideo = async (file) => {
  209. if (file.type !== 'video/mp4') {
  210. MessagePlugin.warning('上传失败,上传视频格式不正确');
  211. return;
  212. }
  213. const duration = await handleGetDuration(file);
  214. timeDuration.value = `${String(parseInt(duration / 60)).padStart(2, '0')}:${String(parseInt(duration % 60)).padStart(2, '0')}`;
  215. uploadVideo(file);
  216. isVideoUploading.value = true;
  217. };
  218. // 获取视频时长的Promise
  219. const handleGetDuration = (file) => {
  220. console.log(file);
  221. return new Promise((resolve, reject) => {
  222. const fileUrl = URL.createObjectURL(file.raw);
  223. const audioEl = new Audio(fileUrl);
  224. audioEl.addEventListener('loadedmetadata', (e) => {
  225. const t = e.composedPath()[0].duration;
  226. resolve(t);
  227. });
  228. });
  229. };
  230. // 上传视频
  231. const uploadVideo = async (file) => {
  232. const res = await VideoInterface.getOSSSign();
  233. if (res.Ret === 200) {
  234. handleUploadToOSS(file.raw, res.Data);
  235. }
  236. };
  237. // 上传到阿里云
  238. const handleUploadToOSS = async (file, { AccessKeyId, AccessKeySecret, SecurityToken }) => {
  239. ALOSSINS = new OSS({
  240. region: 'oss-cn-shanghai',
  241. accessKeyId: AccessKeyId,
  242. accessKeySecret: AccessKeySecret,
  243. stsToken: SecurityToken,
  244. bucket: 'hzchart',
  245. endpoint: 'hzstatic.hzinsights.com',
  246. cname: true,
  247. timeout: 600000000000
  248. });
  249. const t = new Date().getTime().toString();
  250. const temName = `static/yb/video/${MD5(t)}.${file.type.split('/')[1]}`;
  251. const options = {
  252. progress: (p, cpt, res) => {
  253. ALOSSAbortCheckpoint = cpt;
  254. percentage.value = parseInt(p * 100);
  255. },
  256. parallel: 10,
  257. partSize: 1024 * 1024 * 10
  258. };
  259. try {
  260. console.log(temName, file, { ...options });
  261. const res = await ALOSSINS.multipartUpload(temName, file, { ...options });
  262. console.log('上传结果', res);
  263. if (res.res.status === 200) {
  264. form.value.VideoUrl = `https://hzstatic.hzinsights.com/${res.name}`;
  265. percentage.value = 0;
  266. ALOSSAbortCheckpoint = null;
  267. isVideoUploading.value = false;
  268. }
  269. } catch (error) {
  270. if (error.name !== 'cancel') {
  271. MessagePlugin.warning('上传失败,请刷新重试');
  272. }
  273. percentage.value = 0;
  274. ALOSSAbortCheckpoint = null;
  275. isVideoUploading.value = false;
  276. }
  277. };
  278. // 删除所选标签
  279. const removeTag = (tag) => {
  280. const index = form.value.TagIds.findIndex(i => i.TagId === tag.TagId);
  281. if (index !== -1) {
  282. form.value.TagIds.splice(index, 1);
  283. }
  284. tagIdKey.value++;
  285. };
  286. // 改变所选标签
  287. const modifyTags = (tags) => {
  288. form.value.TagIds = _.cloneDeep(tags);
  289. isModifyDialogShow.value = false;
  290. };
  291. // 获取视频信息
  292. const getVideoDetail = () => {
  293. const { VideoId } = route.query;
  294. if (!Number(VideoId)) return;
  295. VideoInterface.getVideoDetail({ VideoId: Number(VideoId) }).then(res => {
  296. if (res.Ret !== 200) return;
  297. form.value = res.Data || {};
  298. if (form.value.Classify) {
  299. const classifyArr = getDataClassify(form.value.Classify);
  300. form.value.ClassifyId = classifyArr[classifyArr.length - 1];
  301. delete form.value.Classify;
  302. }
  303. if (form.value.Tags) {
  304. form.value.TagIds = form.value.Tags;
  305. delete form.value.Tags;
  306. }
  307. });
  308. };
  309. // 获取视频分类路径
  310. const getDataClassify = (classify, classifyArr = []) => {
  311. classifyArr.push(classify.ClassifyId);
  312. if (classify.Children && classify.Children.length) {
  313. return getDataClassify(classify.Children[0], classifyArr);
  314. }
  315. return classifyArr;
  316. };
  317. // 添加/编辑视频
  318. const modifyVideo = async (type = 'modify') => {
  319. const valid = await formRules.value.validate()
  320. if (valid !== true) return
  321. let res = null;
  322. const params = { ...form.value, TagIds: form.value.TagIds.map(t => t.TagId) };
  323. if (!form.value.VideoId) {
  324. res = await VideoInterface.addVideo(params);
  325. } else {
  326. res = await VideoInterface.editVideo(params);
  327. }
  328. if (res.Ret !== 200) return;
  329. if (type !== 'publish') {
  330. MessagePlugin.success(`${form.value.VideoId ? '编辑' : '添加'}成功`);
  331. changeRoute();
  332. }
  333. if (!form.value.VideoId) {
  334. form.value.VideoId = res.Data || '';
  335. }
  336. };
  337. // 发布视频
  338. const publishVideo = async () => {
  339. let res = {};
  340. await modifyVideo('publish');
  341. if (form.value.VideoId) {
  342. res = await VideoInterface.publishVideo({ VideoId: Number(form.value.VideoId), PublishState: 1 });
  343. if (res.Ret !== 200) return;
  344. MessagePlugin.success('发布成功');
  345. }
  346. changeRoute();
  347. };
  348. // 改变路由
  349. const changeRoute = () => {
  350. if (ALOSSAbortCheckpoint) {
  351. console.log('终止上传');
  352. ALOSSINS.abortMultipartUpload(ALOSSAbortCheckpoint.name, ALOSSAbortCheckpoint.uploadId);
  353. }
  354. router.push('/training/trainingVideo');
  355. };
  356. // 预览视频
  357. const handlePreviewVideo = () => {
  358. if (isVideoUploading.value || !form.value.VideoUrl) return;
  359. previewVideo.play();
  360. previewPopTitle.value = form.value.Title || '暂无标题';
  361. previewVideoUrl.value = form.value.VideoUrl;
  362. previewPop.value = true;
  363. };
  364. // 结束预览弹窗关闭回调 -- 暂停视频
  365. const endingPreview = () => {
  366. previewVideo.pause();
  367. };
  368. // 生命周期钩子 - 组件挂载时执行
  369. onMounted(() => {
  370. getClassifyList('leaf');
  371. getTagList();
  372. getVideoDetail();
  373. });
  374. </script>
  375. <style lang="scss">
  376. .modify-video-page-wrap{
  377. .t-textarea__inner{
  378. resize: none;
  379. }
  380. .t-form-item{
  381. .t-form-item__content{
  382. display: flex;
  383. flex-direction: column;
  384. align-items:flex-start;
  385. }
  386. }
  387. }
  388. </style>
  389. <style scoped lang="scss">
  390. .modify-video-page-wrap{
  391. box-sizing: border-box;
  392. padding:40px;
  393. background-color: #fff;
  394. border-radius: 4px;
  395. .t-form{
  396. .t-input,.t-select,.t-textarea{
  397. width:500px;
  398. }
  399. .img-box{
  400. /* background-color: #D9D9D9; */
  401. border:1px dashed #d9d9d9;
  402. width:150px;
  403. height:100px;
  404. text-align: center;
  405. margin-top: 10px;
  406. position: relative;
  407. line-height: normal;
  408. img{
  409. width:100%;
  410. height:100%;
  411. }
  412. .duration{
  413. position:absolute;
  414. right:0;
  415. bottom:0;
  416. background-color: black;
  417. color:#fff;
  418. padding:4px;
  419. }
  420. .t-progress{
  421. position: absolute;
  422. left:50%;
  423. top:50%;
  424. transform: translate(-50%,-50%);
  425. background-color: white;
  426. border-radius: 50%;
  427. }
  428. }
  429. .tag-list{
  430. display: flex;
  431. gap:10px;
  432. line-height:0;
  433. .tag-item{
  434. cursor: pointer;
  435. text-align: center;
  436. box-sizing: border-box;
  437. padding:12px;
  438. min-width:78px;
  439. color: #409EFF;
  440. background-color: #EAF3FE;
  441. border:1px solid #409EFF;
  442. border-radius: 2px;
  443. }
  444. }
  445. }
  446. .btn-box{
  447. margin-top: 20px;
  448. text-align: center;
  449. .t-button{
  450. margin-right: 50px;
  451. width:120px;
  452. text-align: center;
  453. }
  454. }
  455. }
  456. </style>