videoManageENAdd.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. <script setup>
  2. import { reactive, ref } from 'vue'
  3. import MD5 from 'js-md5'
  4. import { customInterence, videoInterface, departInterence, getOSSSign } from '@/api/api.js'
  5. import * as classifyEnInterface from "@/api/modules/classifyEnApi.js";
  6. import { videoENInterface } from '@/api/modules/reportEnApi'
  7. import { ElMessage } from 'element-plus';
  8. import { useRouter, onBeforeRouteLeave } from 'vue-router';
  9. let ALOSSINS = null //阿里云上传实例
  10. let ALOSSAbortCheckpoint = null //阿里云上传实例中断点
  11. const router = useRouter()
  12. const pageState = reactive({
  13. overviewConfig: {
  14. toolbarButtons: [
  15. 'textColor',
  16. 'bold',
  17. 'italic',
  18. 'underline',
  19. 'strikeThrough',
  20. 'subscript',
  21. 'superscript',
  22. 'fontFamily',
  23. 'fontSize',
  24. 'color',
  25. 'inlineClass',
  26. 'inlineStyle',
  27. 'paragraphStyle',
  28. 'lineHeight',
  29. 'paragraphFormat',
  30. 'align',
  31. 'outdent',
  32. 'indent',
  33. 'quote',
  34. 'specialCharacters',
  35. 'insertHR',
  36. 'selectAll',
  37. 'clearFormatting',
  38. 'undo',
  39. 'redo',
  40. ],
  41. height: 300,
  42. fontSizeDefaultSelection: "16",
  43. quickInsertEnabled: false,
  44. theme: "dark", //主题
  45. placeholderText: '请输入overview',
  46. language: "zh_cn",
  47. //允许粘贴的样式
  48. pasteAllowedStyleProps: ['font-family', 'font-size', 'color']
  49. },
  50. formData: {
  51. id: 0,
  52. title: '',
  53. classify: [],
  54. des: '',
  55. videoUrl: '',
  56. VideoSeconds: 0,//时长
  57. imgUrl: '',
  58. overview: '',
  59. state: 1,
  60. },
  61. rules: {
  62. title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
  63. classify: [{ required: true, message: '请选择标签', trigger: 'change' }],
  64. des: [{ required: true, message: '请填写摘要', trigger: 'blur' }],
  65. videoUrl: [{ required: true, message: '请上传视频', trigger: 'change' }],
  66. imgUrl: [{ required: true, message: '请上传视频封面', trigger: 'change' }],
  67. overview: [{ required: true, message: '请填写overview', trigger: 'blur' }],
  68. },
  69. options: [],
  70. percentage: 0,
  71. startUpload: false,//开始上传
  72. chooseImgPop: false,
  73. imgPopPage: 1,
  74. imgPopPagesize: 20,
  75. imgPopTotal: 0,
  76. popImgList: [],
  77. })
  78. // 分类变化设置标题
  79. function handleClassifyChange(e) {
  80. if (pageState.formData.id) return //编辑情况不改
  81. let name = ''
  82. pageState.options.forEach(item => {
  83. if (item.Id == pageState.formData.classify[0]) {
  84. item.Child.forEach(_item => {
  85. if (_item.Id == pageState.formData.classify[1]) {
  86. _item.Child.forEach(_it => {
  87. if (_it.Id == pageState.formData.classify[2]) {
  88. name = _it.ClassifyName
  89. }
  90. })
  91. }
  92. })
  93. }
  94. });
  95. pageState.formData.title = 'Online Presentation:' + name
  96. }
  97. //获取视频时长的promise
  98. function handleGetDuration(file) {
  99. return new Promise((resolve, reject) => {
  100. const fileUrl = URL.createObjectURL(file)
  101. const audioEl = new Audio(fileUrl)
  102. audioEl.addEventListener('loadedmetadata', (e) => {
  103. console.log('e.path', e.path)
  104. console.log('e.composedPath', e.composedPath())
  105. console.log('获取视频时长', e.composedPath()[0].duration);
  106. console.log(audioEl.duration);
  107. const t = e.composedPath()[0].duration
  108. resolve(t)
  109. })
  110. })
  111. }
  112. //上传视频判断格式
  113. function handelBeforeUploadVideo(e) {
  114. if (e.type != 'video/mp4') {
  115. ElMessage.warning('上传失败,上传视频格式不正确')
  116. return false
  117. }
  118. }
  119. // 上传视频
  120. async function handleUpload(e) {
  121. const duration = await handleGetDuration(e.file)
  122. if (duration > 600 && pageState.navType === 1) {
  123. ElMessage.warning('视频时长不得超过10分钟')
  124. return
  125. }
  126. console.log(e);
  127. pageState.formData.VideoSeconds = duration
  128. const res = await getOSSSign()
  129. if (res.Ret === 200) {
  130. let accessKeyId = res.Data.AccessKeyId
  131. let accessKeySecret = res.Data.AccessKeySecret
  132. let stsToken = res.Data.SecurityToken
  133. handleUploadToOSS(e.file, accessKeyId, accessKeySecret, stsToken)
  134. }
  135. }
  136. //上传到阿里云
  137. async function handleUploadToOSS(file, accessKeyId, accessKeySecret, stsToken) {
  138. pageState.startUpload = true
  139. ALOSSINS = new OSS({
  140. // yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
  141. region: "oss-cn-shanghai",
  142. // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
  143. accessKeyId: accessKeyId,
  144. accessKeySecret: accessKeySecret,
  145. // 从STS服务获取的安全令牌(SecurityToken)。
  146. stsToken: stsToken,
  147. // 填写Bucket名称,例如examplebucket。
  148. bucket: "hzchart",
  149. endpoint: 'hzstatic.hzinsights.com',
  150. cname: true,
  151. timeout: 600000000000
  152. });
  153. // 生成文件名
  154. const t = new Date().getTime().toString()
  155. const temName = `static/yb/en/video/${MD5(t)}.${file.type.split('/')[1]}`
  156. console.log(temName);
  157. const options = {
  158. // 获取分片上传进度、断点和返回值。
  159. progress: (p, cpt, res) => {
  160. console.log(p);
  161. ALOSSAbortCheckpoint = cpt
  162. pageState.percentage = parseInt(p * 100)
  163. },
  164. // 设置并发上传的分片数量。
  165. parallel: 10,
  166. // 设置分片大小。默认值为1 MB,最小值为100 KB。
  167. partSize: 1024 * 1024 * 10, // 10MB
  168. };
  169. try {
  170. const res = await ALOSSINS.multipartUpload(temName, file, { ...options })
  171. console.log('上传结果', res);
  172. if (res.res.status === 200) {
  173. pageState.formData.videoUrl = 'https://hzstatic.hzinsights.com/' + res.name
  174. pageState.startUpload = false
  175. pageState.percentage = 0
  176. ALOSSAbortCheckpoint = null
  177. }
  178. } catch (error) {
  179. console.log('上传到阿里云失败', error);
  180. if (error.name !== "cancel") {//不是取消上传的则给错误提示
  181. ElMessage.warning('上传失败,请刷新重试')
  182. }
  183. pageState.startUpload = false
  184. pageState.percentage = 0
  185. ALOSSAbortCheckpoint = null
  186. }
  187. }
  188. onBeforeRouteLeave(() => {
  189. if (ALOSSAbortCheckpoint) {
  190. console.log('终止上传');
  191. ALOSSINS.abortMultipartUpload(ALOSSAbortCheckpoint.name, ALOSSAbortCheckpoint.uploadId)
  192. }
  193. })
  194. //显示选择视频封面列表
  195. function handleshowChooseImgPop() {
  196. pageState.imgPopPage = 1
  197. handleGetPopImgList()
  198. pageState.chooseImgPop = true
  199. }
  200. //弹窗中选择视频封面的列表数据
  201. async function handleGetPopImgList() {
  202. const res = await videoENInterface.videoCoverImgList({
  203. PageSize: pageState.imgPopPagesize,
  204. CurrentIndex: pageState.imgPopPage,
  205. })
  206. if (res.Ret === 200) {
  207. pageState.imgPopTotal = res.Data.Paging.Totals
  208. pageState.popImgList = res.Data.List || []
  209. }
  210. }
  211. function handleChooseImg(item) {
  212. pageState.formData.imgUrl = item.CoverUrl
  213. pageState.chooseImgPop = false
  214. }
  215. //保存数据
  216. const form = ref(null)
  217. function handleSave(e) {
  218. form.value.validate(async (valid) => {
  219. if (!valid) return
  220. let params = {
  221. Id: pageState.formData.id,
  222. // 传二级和三级分类
  223. ClassifyIdFirst: pageState.formData.classify[1],
  224. ClassifyIdSecond: pageState.formData.classify[2],
  225. Title: pageState.formData.title,
  226. Abstract: pageState.formData.des,
  227. State: pageState.formData.state,//状态:1:未发布,2:已发布
  228. VideoUrl: pageState.formData.videoUrl,
  229. VideoCoverUrl: pageState.formData.imgUrl,
  230. VideoSeconds: pageState.formData.VideoSeconds.toString(),
  231. Overview: pageState.formData.overview.replace(/<p data-f-id=\"pbf\".*?<\/p>/g, '')
  232. }
  233. const res = await videoENInterface.roadVideoSave(params)
  234. if (res.Ret === 200) {
  235. if (e === 'publish') {
  236. let res2 = await videoENInterface.roadVideoPublished({ Id: res.Data.Id })
  237. if (res2.Ret === 200) {
  238. ElMessage.success('发布成功')
  239. router.back()
  240. }
  241. } else {
  242. ElMessage.success('保存成功')
  243. router.back()
  244. }
  245. }
  246. })
  247. }
  248. //初始化
  249. function initPage() {
  250. const obj = sessionStorage.getItem('videoENItem') ? JSON.parse(sessionStorage.getItem('videoENItem')) : ''
  251. console.log(obj);
  252. if (obj) {
  253. pageState.formData.id = obj.Id
  254. pageState.formData.title = obj.Title
  255. pageState.formData.classify = [obj.ClassifyIdRoot, obj.ClassifyIdFirst, obj.ClassifyIdSecond]
  256. pageState.formData.des = obj.Abstract
  257. pageState.formData.videoUrl = obj.VideoUrl
  258. pageState.formData.VideoSeconds = obj.VideoSeconds
  259. pageState.formData.imgUrl = obj.VideoCoverUrl
  260. pageState.formData.overview = obj.Overview
  261. pageState.formData.state = obj.State
  262. }
  263. getClassify()
  264. }
  265. initPage()
  266. //获取分类数据
  267. async function getClassify() {
  268. const res = await classifyEnInterface.classifyList({ CurrentIndex: 1, PageSize: 1000, ClassifyType: 1 })
  269. if (res.Ret === 200) {
  270. const arr = res.Data.List || []
  271. pageState.options = arr.map(item => {
  272. if (item.Child && item.Child.length > 0) {
  273. item.Child.map(it => {
  274. console.log(it);
  275. it.disabled = it.Child ? false : true
  276. })
  277. }
  278. return {
  279. ...item,
  280. disabled: item.Child ? false : true
  281. }
  282. })
  283. }
  284. }
  285. </script>
  286. <template>
  287. <div class="video-add-page">
  288. <el-form
  289. ref="form"
  290. :model="pageState.formData"
  291. label-width="80px"
  292. :rules="pageState.rules"
  293. >
  294. <div style="display: flex">
  295. <div style="width: 50%">
  296. <el-form-item label="分类" prop="classify">
  297. <el-cascader
  298. style="width: 100%"
  299. :options="pageState.options"
  300. :show-all-levels="true"
  301. :props="{
  302. value: 'Id',
  303. label: 'ClassifyName',
  304. children: 'Child',
  305. }"
  306. v-model="pageState.formData.classify"
  307. placeholder="请选择分类"
  308. @change="handleClassifyChange"
  309. ></el-cascader>
  310. </el-form-item>
  311. <el-form-item label="标题" prop="title">
  312. <el-input
  313. v-model="pageState.formData.title"
  314. placeholder="请填写标题"
  315. ></el-input>
  316. </el-form-item>
  317. <el-form-item label="摘要" prop="des">
  318. <el-input
  319. v-model="pageState.formData.des"
  320. type="textarea"
  321. :rows="2"
  322. placeholder="请填写摘要"
  323. ></el-input>
  324. </el-form-item>
  325. </div>
  326. <div style="width: 50%">
  327. <el-form-item label="视频" prop="videoUrl">
  328. <div style="display: flex">
  329. <el-input
  330. readonly
  331. v-model="pageState.formData.videoUrl"
  332. placeholder="请上传视频"
  333. ></el-input>
  334. <el-upload
  335. style="display: inline-block; margin-left: 5px"
  336. action=""
  337. accept=".mp4"
  338. :http-request="handleUpload"
  339. :before-upload="handelBeforeUploadVideo"
  340. :show-file-list="false"
  341. :disabled="pageState.startUpload"
  342. >
  343. <el-button type="primary" :loading="pageState.startUpload"
  344. >上传视频</el-button
  345. >
  346. </el-upload>
  347. <el-progress
  348. type="circle"
  349. :percentage="pageState.percentage"
  350. width="40"
  351. style="margin-left: 10px"
  352. v-if="pageState.startUpload"
  353. ></el-progress>
  354. </div>
  355. <p style="color: #999">*注:要求视频格式为mp4,编码器为H.264</p>
  356. </el-form-item>
  357. <el-form-item label="封面" prop="imgUrl">
  358. <el-image
  359. style="width: 120px; height: 120px"
  360. :src="require('@/assets/img/icons/add-img.png')"
  361. v-if="!pageState.formData.imgUrl"
  362. @click="handleshowChooseImgPop"
  363. />
  364. <div v-else class="pre-img-box">
  365. <el-image
  366. style="width: 120px; height: 120px"
  367. :src="pageState.formData.imgUrl"
  368. :preview-src-list="[pageState.formData.imgUrl]"
  369. >
  370. </el-image>
  371. <span class="del" @click="pageState.formData.imgUrl = ''"
  372. >删除</span
  373. >
  374. </div>
  375. </el-form-item>
  376. </div>
  377. </div>
  378. <el-form-item label="Overview" prop="overview">
  379. <div id="leftoverview">
  380. <froala
  381. :id="`overview-editor`"
  382. :style="{ display: 'none' }"
  383. :ref="`overviewEditor`"
  384. :tag="'textarea'"
  385. :config="pageState.overviewConfig"
  386. v-model="pageState.formData.overview"
  387. >
  388. </froala>
  389. </div>
  390. </el-form-item>
  391. <div style="text-align: left; margin: 50px 0">
  392. <el-button type="primary" @click="handleSave">保存</el-button>
  393. <el-button type="primary" plain @click="handleSave('publish')"
  394. >发布</el-button
  395. >
  396. <el-button @click="$router.back()">取消</el-button>
  397. </div>
  398. </el-form>
  399. <!-- 选择视频封面弹窗 -->
  400. <el-dialog
  401. v-model="pageState.chooseImgPop"
  402. :modal-append-to-body="false"
  403. draggable
  404. width="60vw"
  405. title="视频封面库"
  406. >
  407. <div class="choose-img-wrap">
  408. <div class="list">
  409. <div
  410. class="item"
  411. v-for="item in pageState.popImgList"
  412. :key="item.CommunityVideoCoverId"
  413. @click="handleChooseImg(item)"
  414. >
  415. <el-image style="width: 120px; height: 120px" :src="item.CoverUrl">
  416. </el-image>
  417. <div>{{ item.CoverName }}</div>
  418. </div>
  419. </div>
  420. <div>
  421. <el-pagination
  422. layout="total,prev,pager,next"
  423. background
  424. :current-page="pageState.imgPopPage"
  425. @current-change="handleimgPopPageChange"
  426. :page-size="pageState.imgPopPagesize"
  427. :total="pageState.imgPopTotal"
  428. style="float: right; margin-top: 20px"
  429. >
  430. </el-pagination>
  431. </div>
  432. </div>
  433. </el-dialog>
  434. </div>
  435. </template>
  436. <style lang="scss" scoped>
  437. .video-add-page {
  438. border: 1px solid #dcdfe6;
  439. background-color: #fff;
  440. border-radius: 4px;
  441. padding: 30px;
  442. .choose-img-wrap {
  443. padding-bottom: 80px;
  444. .list {
  445. display: flex;
  446. flex-wrap: wrap;
  447. }
  448. .item {
  449. flex-shrink: 0;
  450. text-align: center;
  451. margin-right: 20px;
  452. margin-bottom: 20px;
  453. border: 1px solid rgb(199, 198, 198);
  454. padding: 5px;
  455. border-radius: 4px;
  456. }
  457. }
  458. .pre-img-box {
  459. position: relative;
  460. width: 120px;
  461. height: 120px;
  462. .del {
  463. display: none;
  464. color: #fff;
  465. position: absolute;
  466. bottom: 0;
  467. left: 0;
  468. right: 0;
  469. text-align: center;
  470. line-height: 24px;
  471. background-color: rgba($color: #000000, $alpha: 0.8);
  472. cursor: pointer;
  473. }
  474. &:hover {
  475. .del {
  476. display: block;
  477. }
  478. }
  479. }
  480. }
  481. </style>