PublishVideo.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. <template>
  2. <el-row class="movie-list">
  3. <el-col :md="12" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  4. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  5. <el-card class="box-card">
  6. <div slot="header" class="clearfix">
  7. <span>上传视频文件</span>
  8. </div>
  9. <div class="text item">
  10. <uploader
  11. class="uploader-example"
  12. :options="options"
  13. :auto-start="true"
  14. @file-added="onFileAdded"
  15. @file-success="onFileSuccess"
  16. @file-progress="onFileProgress"
  17. @file-error="onFileError"
  18. >
  19. <uploader-unsupport />
  20. <uploader-drop>
  21. <p>拖动视频文件到此处或</p>
  22. <uploader-btn :attrs="attrs">选择视频文件</uploader-btn>
  23. </uploader-drop>
  24. <uploader-list />
  25. </uploader>
  26. </div>
  27. </el-card>
  28. </el-row>
  29. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  30. <el-card class="box-card">
  31. <div slot="header" class="clearfix">
  32. <span>上传视频封面</span>
  33. </div>
  34. <div class="text item">
  35. <el-tooltip class="item" effect="dark" content="点击上传图片" placement="top-end">
  36. <el-upload
  37. class="avatar-uploader"
  38. action="//oss.reghao.cn/"
  39. :headers="imgHeaders"
  40. :data="imgData"
  41. :with-credentials="true"
  42. :show-file-list="false"
  43. :before-upload="beforeAvatarUpload"
  44. :on-success="handleAvatarSuccess"
  45. :on-change="handleOnChange"
  46. >
  47. <img v-if="coverUrl" :src="coverUrl" class="avatar">
  48. <i v-else class="el-icon-plus avatar-uploader-icon" />
  49. </el-upload>
  50. </el-tooltip>
  51. </div>
  52. </el-card>
  53. </el-row>
  54. </el-col>
  55. <el-col :md="12" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  56. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  57. <el-card class="box-card">
  58. <div slot="header" class="clearfix">
  59. <span>稿件信息</span>
  60. <el-button style="float: right; padding: 3px 0" type="text" @click="onSubmit">发布</el-button>
  61. </div>
  62. <div class="text item">
  63. <el-form ref="form" :model="form" label-width="80px">
  64. <el-form-item label="标题">
  65. <el-input v-model="form.title" style="padding-right: 1px" placeholder="标题不能超过 50 个字符" />
  66. </el-form-item>
  67. <el-form-item label="描述">
  68. <el-input v-model="form.description" type="textarea" autosize style="padding-right: 1px;" />
  69. </el-form-item>
  70. <el-form-item label="分区">
  71. <el-select v-model="form.categoryPid" placeholder="请选择分区" @change="getCategory">
  72. <el-option
  73. v-for="item in pCategoryList" :key="item.value"
  74. :label="item.label" :value="item.value">
  75. </el-option>
  76. </el-select>
  77. <el-select v-model="form.categoryId" placeholder="请选择子分区">
  78. <el-option
  79. v-for="item in categoryList" :key="item.value"
  80. :label="item.label" :value="item.value">
  81. </el-option>
  82. </el-select>
  83. </el-form-item>
  84. <el-form-item label="标签">
  85. <el-select v-model="form.tags" style="padding-right: 1px" placeholder="输入标签,用回车添加" @change="getRecommendTags" clearable multiple filterable allow-create default-first-option>
  86. <el-option v-for="item in rcmdTags" :key="item.value" :label="item.label" :value="item.label"></el-option>
  87. </el-select>
  88. </el-form-item>
  89. <el-form-item label="可见范围">
  90. <el-select v-model="form.scope" placeholder="选择稿件的可见范围">
  91. <el-option label="所有人可见" value="1" />
  92. <el-option label="验证码可见" value="2" />
  93. <el-option label="VIP 可见" value="3" />
  94. <el-option label="仅自己可见" value="4" />
  95. </el-select>
  96. </el-form-item>
  97. <el-form-item label="定时发布">
  98. <el-date-picker
  99. v-model="form.scheduledPubDate"
  100. type="datetime"
  101. placeholder="选择定时发布的时间">
  102. </el-date-picker>
  103. </el-form-item>
  104. </el-form>
  105. </div>
  106. </el-card>
  107. </el-row>
  108. </el-col>
  109. </el-row>
  110. </template>
  111. <script>
  112. import { getServerInfo } from '@/api/file'
  113. import {videoCategory, addVideoPost } from '@/api/video'
  114. export default {
  115. name: 'PublishVideo',
  116. data() {
  117. return {
  118. /***********************************************************************/
  119. options: {
  120. target: '//oss.reghao.cn/',
  121. chunkSize: 1024 * 1024 * 1024 * 10, // 10GiB
  122. fileParameterName: 'file',
  123. testChunks: false,
  124. query: (file, chunk) => {
  125. return {
  126. channelId: 2
  127. }
  128. },
  129. headers: {
  130. Authorization: ''
  131. },
  132. withCredentials: true,
  133. },
  134. attrs: {
  135. accept: 'video/*'
  136. },
  137. /***********************************************************************/
  138. imgOssUrl: null,
  139. imgHeaders: {
  140. Authorization: ''
  141. },
  142. imgData: {
  143. channelId: 5
  144. },
  145. coverUrl: null,
  146. /***********************************************************************/
  147. categoryMap: {
  148. Set: function(key, value) { this[key] = value },
  149. Get: function(key) { return this[key] },
  150. Contains: function(key) { return this.Get(key) !== null },
  151. Remove: function(key) { delete this[key] }
  152. },
  153. pCategoryList: [],
  154. categoryList: [],
  155. rcmdTags: [
  156. /*{ label: "知识点1" }*/
  157. ],
  158. form: {
  159. videoFileId: null,
  160. coverFileId: null,
  161. title: null,
  162. description: null,
  163. categoryPid: null,
  164. categoryId: null,
  165. tags: [],
  166. scope: "1",
  167. scheduledPubDate: null
  168. },
  169. }
  170. },
  171. created() {
  172. getServerInfo(2).then(res => {
  173. if (res.code === 0) {
  174. const resData = res.data
  175. this.options.target = resData.ossUrl
  176. this.options.chunkSize = resData.maxSize
  177. this.options.headers.Authorization = "Bearer " + resData.token
  178. } else {
  179. this.$notify({
  180. title: '提示',
  181. message: '获取 OSS 服务器地址失败, 暂时无法上传文件',
  182. type: 'error',
  183. duration: 3000
  184. })
  185. }
  186. }).catch(error => {
  187. this.$notify({
  188. title: '提示',
  189. message: error.message,
  190. type: 'warning',
  191. duration: 3000
  192. })
  193. })
  194. getServerInfo(5).then(res => {
  195. if (res.code === 0) {
  196. const resData = res.data
  197. this.imgOssUrl = resData.ossUrl
  198. this.imgHeaders.Authorization = "Bearer " + resData.token
  199. } else {
  200. this.$notify({
  201. title: '提示',
  202. message: '获取 OSS 服务器地址失败, 暂时无法上传文件',
  203. type: 'error',
  204. duration: 3000
  205. })
  206. }
  207. }).catch(error => {
  208. this.$notify({
  209. title: '提示',
  210. message: error.message,
  211. type: 'warning',
  212. duration: 3000
  213. })
  214. })
  215. this.getVideoCategory()
  216. },
  217. mounted() {
  218. },
  219. methods: {
  220. /***********************************************************************/
  221. onFileAdded(file) {
  222. if (file.file.size > 1024*1024*1024*5) {
  223. file.cancel()
  224. this.$notify({
  225. title: '提示',
  226. message: '视频文件应小于 5GiB',
  227. type: 'warning',
  228. duration: 3000
  229. })
  230. return
  231. }
  232. this.setTitle(file.file.name)
  233. this.processVideo(file.file)
  234. },
  235. onFileProgress(rootFile, file, chunk) {
  236. },
  237. onFileSuccess(rootFile, file, response, chunk) {
  238. const res = JSON.parse(response)
  239. if (res.code === 0) {
  240. const resData = res.data
  241. this.form.videoFileId = resData.uploadId
  242. this.$notify({
  243. title: '提示',
  244. message: '视频已上传',
  245. type: 'warning',
  246. duration: 3000
  247. })
  248. } else {
  249. console.log(chunk)
  250. this.$notify({
  251. title: '提示',
  252. message: '视频文件上传失败',
  253. type: 'warning',
  254. duration: 3000
  255. })
  256. }
  257. },
  258. onFileError(rootFile, file, response, chunk) {
  259. const res = JSON.parse(response)
  260. console.log(res.msg)
  261. this.$notify({
  262. title: '提示',
  263. message: '视频文件上传错误',
  264. type: 'warning',
  265. duration: 3000
  266. })
  267. },
  268. /***********************************************************************/
  269. // 选择视频后获取视频的分辨率和时长, 并截取第一秒的内容作为封面
  270. processVideo(file) {
  271. return new Promise((resolve, reject) => {
  272. const canvas = document.createElement('canvas')
  273. const canvasCtx = canvas.getContext('2d')
  274. const videoElem = document.createElement('video')
  275. const dataUrl = window.URL.createObjectURL(file)
  276. // 当前帧的数据是可用的
  277. videoElem.onloadeddata = function() {
  278. resolve(videoElem)
  279. }
  280. videoElem.onerror = function() {
  281. reject('video 后台加载失败')
  282. }
  283. // 设置 auto 预加载数据, 否则会出现截图为黑色图片的情况
  284. videoElem.setAttribute('preload', 'auto')
  285. videoElem.src = dataUrl
  286. // 预加载完成后才会获取到视频的宽高和时长数据
  287. videoElem.addEventListener('canplay', this.onCanPlay(videoElem, canvas, canvasCtx))
  288. })
  289. },
  290. onCanPlay(videoElem, canvas, canvasCtx) {
  291. setTimeout(() => {
  292. // 视频视频分辨率
  293. const videoWidth = videoElem.videoWidth
  294. const videoHeight = videoElem.videoHeight
  295. this.form.width = videoWidth
  296. this.form.height = videoHeight
  297. this.form.duration = videoElem.duration
  298. videoElem.pause()
  299. // 设置画布尺寸
  300. canvas.width = videoWidth
  301. canvas.height = videoHeight
  302. canvasCtx.drawImage(videoElem, 0, 0, canvas.width, canvas.height)
  303. // 把图标base64编码后变成一段url字符串
  304. const urlData = canvas.toDataURL('image/jpeg')
  305. if (typeof urlData !== 'string') {
  306. alert('urlData不是字符串')
  307. return
  308. }
  309. var arr = urlData.split(',')
  310. var bstr = atob(arr[1])
  311. var n = bstr.length
  312. var u8arr = new Uint8Array(n)
  313. while (n--) {
  314. u8arr[n] = bstr.charCodeAt(n)
  315. }
  316. const coverFile = new File([u8arr], 'cover.jpg', { type: 'image/jpeg' })
  317. if (coverFile instanceof File) {
  318. if (coverFile.size === 0) {
  319. this.$notify({
  320. title: '提示',
  321. message: '自动获取视频封面失败,请手动选择!',
  322. type: 'warning',
  323. duration: 3000
  324. })
  325. return;
  326. }
  327. const formData = new FormData()
  328. formData.append('file', coverFile)
  329. formData.append('channelId', 5)
  330. fetch(`//oss.reghao.cn/`, {
  331. headers: this.imgHeaders,
  332. method: 'POST',
  333. credentials: 'include',
  334. body: formData
  335. }).then(response => response.json()).then(json => {
  336. if (json.code === 0) {
  337. this.coverUrl = URL.createObjectURL(coverFile)
  338. const resData = json.data
  339. this.form.coverFileId = resData.uploadId
  340. } else {
  341. this.$notify({
  342. title: '提示',
  343. message: '视频封面上传失败,请重试!' + json.msg,
  344. type: 'warning',
  345. duration: 3000
  346. })
  347. }
  348. }).catch(e => {
  349. return null
  350. })
  351. }
  352. }, 1000) // 1000毫秒,就是截取第一秒,2000毫秒就是截取第2秒,视频1秒通常24帧,也可以换算成截取第几帧。
  353. // 防止拖动进度条的时候重复触发
  354. // videoElem.removeEventListener('canplay', arguments.callee)
  355. },
  356. /***********************************************************************/
  357. beforeAvatarUpload(file) {
  358. const isJPG = file.type === 'image/jpeg'
  359. const isLt2M = file.size / 1024 / 1024 < 10
  360. if (!isJPG) {
  361. this.$message.error('封面图片只能是 JPG 格式!')
  362. }
  363. if (!isLt2M) {
  364. this.$message.error('封面图片大小不能超过 10MB!')
  365. }
  366. return isJPG && isLt2M
  367. },
  368. handleAvatarSuccess(res, file) {
  369. if (res.code === 0) {
  370. const resData = res.data
  371. this.coverUrl = URL.createObjectURL(file.raw);
  372. this.form.coverFileId = resData.uploadId
  373. } else {
  374. this.$notify({
  375. title: '提示',
  376. message: '视频封面上传失败,请重试!' + res.msg,
  377. type: 'warning',
  378. duration: 3000
  379. })
  380. }
  381. },
  382. handleOnChange(file, fileList) {
  383. console.log('封面改变')
  384. },
  385. /***********************************************************************/
  386. setTitle(title) {
  387. if (title.length > 50) {
  388. this.form.title = title.substring(0, 50)
  389. this.form.description = title
  390. } else {
  391. this.form.title = title
  392. }
  393. },
  394. getVideoCategory() {
  395. videoCategory().then(res => {
  396. if (res.code === 0) {
  397. const resData = res.data
  398. for (let i = 0; i < resData.length; i++) {
  399. const name = resData[i].name
  400. const id = resData[i].id
  401. this.pCategoryList.push({label: name, value: id})
  402. this.categoryMap.Set(id, resData[i].children)
  403. }
  404. } else {
  405. this.$notify({
  406. title: '提示',
  407. message: res.msg,
  408. type: 'warning',
  409. duration: 3000
  410. })
  411. }
  412. }).catch(error => {
  413. this.$notify({
  414. title: '提示',
  415. message: error.message,
  416. type: 'error',
  417. duration: 3000
  418. })
  419. })
  420. },
  421. getCategory(id) {
  422. // 重置子分区,清除前一次选择分区时留下的缓存
  423. this.categoryList = []
  424. for (const item of this.categoryMap.Get(id)) {
  425. this.categoryList.push({label: item.name, value: item.id})
  426. }
  427. },
  428. // 根据输入的标签获取相似的标签
  429. getRecommendTags(tags) {
  430. for (const tag of tags) {
  431. // console.log(tag)
  432. }
  433. },
  434. onSubmit() {
  435. if (!this.form.videoFileId) {
  436. this.$notify({
  437. title: '提示',
  438. message: '你还没有上传视频',
  439. type: 'warning',
  440. duration: 3000
  441. }
  442. )
  443. return
  444. }
  445. if (!this.form.coverFileId) {
  446. this.$notify({
  447. title: '提示',
  448. message: '你还没有上传视频封面',
  449. type: 'warning',
  450. duration: 3000
  451. }
  452. )
  453. return
  454. }
  455. if (this.form.title === '' || this.form.categoryId === -1) {
  456. this.$notify({
  457. title: '提示',
  458. message: '分区和稿件标题不能为空',
  459. type: 'warning',
  460. duration: 3000
  461. }
  462. )
  463. return
  464. }
  465. if (this.form.tags.length === 0 || this.form.tags.length > 10) {
  466. this.$notify({
  467. title: '提示',
  468. message: '标签最少 1 个, 最多 10 个',
  469. type: 'warning',
  470. duration: 3000
  471. }
  472. )
  473. return
  474. }
  475. if (this.form.scheduledPubDate !== null) {
  476. console.log(this.form.scheduledPubDate)
  477. if (false) {
  478. this.$notify({
  479. title: '提示',
  480. message: '定时发布的时间必须在当前时间之后',
  481. type: 'warning',
  482. duration: 3000
  483. })
  484. }
  485. }
  486. addVideoPost(this.form).then(res => {
  487. if (res.code === 0) {
  488. this.$notify({
  489. title: '提示',
  490. message: '投稿成功,等待审核通过后其他人就可以看到你的视频了',
  491. type: 'warning',
  492. duration: 3000
  493. })
  494. this.$router.push('/post/video')
  495. } else {
  496. this.$notify({
  497. title: '提示',
  498. message: res.msg,
  499. type: 'warning',
  500. duration: 3000
  501. })
  502. }
  503. }).catch(error => {
  504. this.$notify({
  505. title: '提示',
  506. message: error.message,
  507. type: 'warning',
  508. duration: 3000
  509. })
  510. })
  511. }
  512. }
  513. }
  514. </script>
  515. <style>
  516. .uploader-example {
  517. width: 500px;
  518. padding: 15px;
  519. margin: 40px auto 0;
  520. font-size: 12px;
  521. box-shadow: 0 0 10px rgba(0, 0, 0, .4);
  522. }
  523. .uploader-example .uploader-btn {
  524. margin-right: 4px;
  525. }
  526. .uploader-example .uploader-list {
  527. max-height: 440px;
  528. overflow: auto;
  529. overflow-x: hidden;
  530. overflow-y: auto;
  531. }
  532. .avatar-uploader .el-upload {
  533. border: 1px dashed #d9d9d9;
  534. border-radius: 6px;
  535. cursor: pointer;
  536. position: relative;
  537. overflow: hidden;
  538. }
  539. .avatar-uploader .el-upload:hover {
  540. border-color: #409EFF;
  541. }
  542. .avatar-uploader-icon {
  543. font-size: 28px;
  544. color: #8c939d;
  545. width: 320px;
  546. height: 240px;
  547. line-height: 178px;
  548. text-align: center;
  549. }
  550. .avatar {
  551. width: 320px;
  552. height: 240px;
  553. display: block;
  554. }
  555. </style>