upload.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. <template>
  2. <v-row justify="center" align="center">
  3. <v-col>
  4. <v-card
  5. class="mx-auto"
  6. outlined
  7. >
  8. <v-row justify="center">
  9. <v-col cols="10">
  10. <h2>发布视频贴</h2>
  11. </v-col>
  12. </v-row>
  13. <v-row justify="center">
  14. <div>
  15. <uploader
  16. class="uploader-example"
  17. :options="options"
  18. :autoStart="true"
  19. @file-added="onFileAdded"
  20. @file-success="onFileSuccess"
  21. @file-progress="onFileProgress"
  22. @file-error="onFileError"
  23. >
  24. <uploader-unsupport />
  25. <uploader-drop>
  26. <p>拖动视频文件到此处或</p>
  27. <uploader-btn :attrs="attrs">选择视频文件</uploader-btn>
  28. </uploader-drop>
  29. <uploader-list />
  30. </uploader>
  31. </div>
  32. </v-row>
  33. <v-row justify="center">
  34. <v-col cols="10">
  35. <h2>基本信息</h2>
  36. </v-col>
  37. </v-row>
  38. <v-row justify="center">
  39. <v-col cols="5">
  40. <v-card outlined>
  41. <v-img :src="videoPost.coverUrl" aspect-ratio="1.77" contain max-height="150" alt="封面图,推荐16:9" />
  42. </v-card>
  43. </v-col>
  44. <v-col cols="5">
  45. <v-file-input
  46. :rules="rules"
  47. accept="image/png, image/jpeg, image/bmp"
  48. prepend-icon="mdi-camera"
  49. placeholder="上传视频封面"
  50. label="封面"
  51. @change="setFile"
  52. />
  53. <v-btn color="primary" @click="uploadFile">
  54. 上传
  55. </v-btn>
  56. </v-col>
  57. </v-row>
  58. <v-row justify="center">
  59. <v-col cols="5">
  60. <v-select
  61. :items="category"
  62. label="分区"
  63. @change="getCategory"
  64. />
  65. </v-col>
  66. <v-col cols="5">
  67. <v-select
  68. :items="childCategory"
  69. label="子分区"
  70. @change="getChildCategory"
  71. />
  72. </v-col>
  73. </v-row>
  74. <v-row justify="center">
  75. <v-col cols="10">
  76. <v-text-field
  77. v-model="videoPost.title"
  78. placeholder="标题"
  79. label="标题(50字以内)"
  80. clearable
  81. :rules="[() => videoPost.title != null || '标题不能为空']"
  82. />
  83. </v-col>
  84. </v-row>
  85. <v-row justify="center">
  86. <v-col cols="10">
  87. <v-textarea
  88. v-model="videoPost.description"
  89. label="简介(200字以内)"
  90. clearable
  91. placeholder="填写更全面的视频信息,让更多的人找到你!"
  92. />
  93. </v-col>
  94. </v-row>
  95. <v-row justify="center">
  96. <v-col cols="10">
  97. <v-combobox
  98. v-model="videoPost.tags"
  99. label="添加标签让更多人找到你(最多6个)"
  100. multiple
  101. chips
  102. clearable
  103. />
  104. </v-col>
  105. </v-row>
  106. <v-row justify="center">
  107. <v-col cols="10">
  108. <v-btn large color="primary" @click="publish">立即投稿</v-btn>
  109. </v-col>
  110. </v-row>
  111. </v-card>
  112. </v-col>
  113. <v-snackbar
  114. v-model="showMessage"
  115. :top="true"
  116. :timeout="3000"
  117. >
  118. {{ message }}
  119. <template v-slot:action="{ attrs }">
  120. <v-btn
  121. color="pink"
  122. text
  123. v-bind="attrs"
  124. @click="showMessage = false"
  125. >
  126. 关闭
  127. </v-btn>
  128. </template>
  129. </v-snackbar>
  130. </v-row>
  131. </template>
  132. <script>
  133. import { videoCategory, submitVideoPost } from '@/api/media/video'
  134. import { hashFile } from '@/utils/hash'
  135. export default {
  136. data() {
  137. return {
  138. options: {
  139. target: '//localhost:8000' + '/api/file/upload/video',
  140. chunkSize: 1024 * 1024 * 20,
  141. forceChunkSize: true,
  142. fileParameterName: 'file',
  143. maxChunkRetries: 3,
  144. testChunks: true,
  145. checkChunkUploadedByResponse: function(chunk, message) {
  146. const objMessage = JSON.parse(message)
  147. console.log('分片文件检验')
  148. console.log(objMessage)
  149. if (objMessage.skipUpload) {
  150. return true
  151. }
  152. return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
  153. },
  154. headers: {
  155. Authorization: 'Bearer ' + this.$store.getters.token
  156. }
  157. },
  158. attrs: {
  159. accept: 'video/*'
  160. },
  161. rules: [
  162. value => !value || value.size < 2000000 || 'Avatar size should be less than 2 MB!'
  163. ],
  164. // 提交给后端的数据
  165. videoPost: {
  166. fileId: '',
  167. videoUrl: '',
  168. coverUrl: '',
  169. title: '',
  170. description: '',
  171. duration: 0,
  172. categoryId: -1,
  173. tags: []
  174. },
  175. categoryMap: {
  176. Set: function(key, value) { this[key] = value },
  177. Get: function(key) { return this[key] },
  178. Contains: function(key) { return this.Get(key) !== null },
  179. Remove: function(key) { delete this[key] }
  180. },
  181. category: [],
  182. childCategory: [],
  183. nowCategory: {},
  184. files: [],
  185. showMessage: false,
  186. message: ''
  187. }
  188. },
  189. created() {
  190. this.getVideoCategory()
  191. },
  192. methods: {
  193. onFileAdded(file) {
  194. file.pause()
  195. hashFile(file.file).then(res => {
  196. this.setTitle(file.file.name)
  197. const formData = new FormData()
  198. formData.append('filename', file.file.name)
  199. formData.append('size', file.file.size)
  200. formData.append('sha256sum', res.sha256sum)
  201. fetch(`//localhost:8000` + `/api/file/upload/video/prepare`, {
  202. headers: {
  203. Authorization: 'Bearer ' + this.$store.getters.token
  204. },
  205. method: 'POST',
  206. credentials: 'include',
  207. body: formData
  208. }).then(response => response.json())
  209. .then(json => {
  210. const uploadId = json.data.uploadId
  211. const exist = json.data.exist
  212. if (exist) {
  213. this.message = '视频已存在'
  214. this.showMessage = true
  215. file.cancel()
  216. } else {
  217. this.videoPost.videoId = uploadId
  218. file.uniqueIdentifier = uploadId
  219. file.resume()
  220. }
  221. })
  222. .catch(e => {
  223. return null
  224. })
  225. })
  226. },
  227. onFileProgress(rootFile, file, chunk) {
  228. },
  229. onFileSuccess(rootFile, file, response, chunk) {
  230. const res = JSON.parse(response)
  231. if (res.code === 0) {
  232. const resData = res.data
  233. if (resData.merged) {
  234. this.videoPost.fileId = resData.uploadId
  235. this.videoPost.videoUrl = resData.videoUrl
  236. this.videoPost.coverUrl = resData.coverUrl
  237. this.videoPost.duration = resData.duration
  238. }
  239. }
  240. },
  241. onFileError(rootFile, file, response, chunk) {
  242. console.log('文件上传错误')
  243. },
  244. publish() {
  245. if (!this.videoPost.fileId) {
  246. this.message = '你还没有上传视频'
  247. this.showMessage = true
  248. return
  249. }
  250. if (this.videoPost.title === '' || this.videoPost.coverUrl === '' || this.videoPost.tags.length === 0 || this.videoPost.categoryId === -1) {
  251. this.message = '标题,封面,标签,分区不能为空'
  252. this.showMessage = true
  253. return
  254. }
  255. if (this.videoPost.tags.length > 6) {
  256. this.message = '标签超过6个'
  257. this.showMessage = true
  258. return
  259. }
  260. submitVideoPost(this.videoPost)
  261. .then(res => {
  262. if (res.code === 0) {
  263. this.message = '投稿成功,等待审核通过后你就可以看到你的视频了'
  264. this.showMessage = true
  265. this.$router.push('/studio')
  266. } else {
  267. this.message = res.msg
  268. this.showMessage = true
  269. }
  270. })
  271. .catch(error => {
  272. console.error(error.message)
  273. })
  274. },
  275. setFile(value) {
  276. this.files = []
  277. this.files.push(value)
  278. },
  279. setTitle(title) {
  280. if (title.length > 50) {
  281. this.videoPost.title = title.substring(0, 50)
  282. } else {
  283. this.videoPost.title = title
  284. }
  285. },
  286. uploadFile() {
  287. if (this.files.length === 0) {
  288. this.message = '请先选择视频封面,然后上传!'
  289. this.showMessage = true
  290. return
  291. }
  292. const formData = new FormData()
  293. for (let i = 0; i < this.files.length; i++) {
  294. formData.append('file[]', this.files[i])
  295. }
  296. fetch(`http://file.reghao.cn/api/file/upload/image`, {
  297. headers: {
  298. 'Authorization': 'Bearer ' + this.$store.getters.token
  299. },
  300. method: 'POST',
  301. credentials: 'include',
  302. body: formData
  303. }).then(response => response.json())
  304. .then(json => {
  305. if (json.code === 0) {
  306. this.videoPost.coverUrl = json.data[0].url
  307. } else {
  308. this.message = '上传失败,请重试!' + json.message
  309. this.showMessage = true
  310. }
  311. })
  312. .catch(e => {
  313. return null
  314. })
  315. },
  316. getVideoCategory() {
  317. videoCategory()
  318. .then(res => {
  319. if (res.code === 0) {
  320. for (let i = 0; i < res.data.length; i++) {
  321. const name = res.data[i].name
  322. this.category.push(name)
  323. this.categoryMap.Set(name, res.data[i])
  324. }
  325. } else {
  326. console.error(res.msg)
  327. }
  328. })
  329. .catch(error => {
  330. console.error(error.message)
  331. })
  332. },
  333. getCategory(name) {
  334. // 重置子分区,清除前一次选择分区时留下的缓存
  335. this.childCategory = []
  336. this.currentCategory = this.categoryMap.Get(name)
  337. this.videoPost.categoryId = this.currentCategory.id
  338. const c = this.currentCategory.children
  339. if (c) {
  340. for (let i = 0; i < c.length; i++) {
  341. this.childCategory.push(c[i].name)
  342. }
  343. }
  344. },
  345. getChildCategory(name) {
  346. const c = this.currentCategory.children
  347. for (let i = 0; i < c.length; i++) {
  348. if (c[i].name === name) {
  349. this.videoPost.categoryId = c[i].id
  350. }
  351. }
  352. }
  353. }
  354. }
  355. </script>
  356. <style>
  357. .uploader-example {
  358. width: 500px;
  359. padding: 15px;
  360. margin: 40px auto 0;
  361. font-size: 12px;
  362. box-shadow: 0 0 10px rgba(0, 0, 0, .4);
  363. }
  364. .uploader-example .uploader-btn {
  365. margin-right: 4px;
  366. }
  367. .uploader-example .uploader-list {
  368. max-height: 440px;
  369. overflow: auto;
  370. overflow-x: hidden;
  371. overflow-y: auto;
  372. }
  373. </style>