index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. import { PropType, computed, defineComponent, reactive, ref } from 'vue';
  2. import styles from './index.module.less';
  3. import {
  4. NButton,
  5. NSpace,
  6. NUpload,
  7. NUploadDragger,
  8. UploadCustomRequestOptions,
  9. UploadFileInfo,
  10. useMessage
  11. } from 'naive-ui';
  12. // import iconUploadAdd from '../../../images/icon-upload-add.png';
  13. import { NaturalTypeEnum, PageEnum } from '/src/enums/pageEnum';
  14. // import { policy } from '/src/components/upload-file/api';
  15. import { formatUrlType } from '../upload-modal';
  16. // import axios from 'axios';
  17. import {
  18. getUploadSign,
  19. onFileUpload,
  20. onOnlyFileUpload
  21. // ossSwitch
  22. } from '/src/helpers/oss-file-upload';
  23. export default defineComponent({
  24. name: 'save-modal',
  25. props: {
  26. fileList: {
  27. type: String,
  28. default: ''
  29. },
  30. imageList: {
  31. type: Array,
  32. default: () => []
  33. },
  34. accept: {
  35. // 支持类型
  36. type: String,
  37. default: '.jpg,.png,.jpeg,.gif'
  38. },
  39. showType: {
  40. type: String as PropType<'default' | 'custom'>,
  41. default: 'default'
  42. },
  43. showFileList: {
  44. type: Boolean,
  45. default: true
  46. },
  47. max: {
  48. type: Number as PropType<number>,
  49. default: 1
  50. },
  51. multiple: {
  52. type: Boolean as PropType<boolean>,
  53. default: false
  54. },
  55. disabled: {
  56. type: Boolean as PropType<boolean>,
  57. default: false
  58. },
  59. bucketName: {
  60. type: String,
  61. default: 'gyt'
  62. },
  63. directoryDnd: {
  64. type: Boolean as PropType<boolean>,
  65. default: false
  66. },
  67. path: {
  68. type: String,
  69. default: ''
  70. },
  71. fileName: {
  72. type: String,
  73. default: ''
  74. }
  75. },
  76. emits: ['close', 'confrim'],
  77. setup(props, { emit }) {
  78. const ossUploadUrl = `https://${props.bucketName}.ks3-cn-beijing.ksyuncs.com/`;
  79. const message = useMessage();
  80. const visiable = ref<boolean>(false);
  81. const btnLoading = ref<boolean>(false);
  82. const tempFiileBuffer = ref();
  83. const uploadRef = ref();
  84. const state = reactive([]) as any;
  85. const fileListRef = ref<UploadFileInfo[]>([]);
  86. const uploadList = ref([] as any);
  87. // 获取文件后缀名
  88. function getFileExtension(filename: string) {
  89. return filename.slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2);
  90. }
  91. // 判断是否是允许的文件类型
  92. function isAllowedFileType(filename: string, allowedExtensions: any) {
  93. const extension = getFileExtension(filename).toLowerCase();
  94. return allowedExtensions.includes(extension);
  95. }
  96. const onBeforeUpload = async (options: any) => {
  97. const file = options.file;
  98. // 文件大小
  99. let isLt2M = true;
  100. const allowedExtensions = [
  101. 'jpg',
  102. 'jpeg',
  103. 'png',
  104. 'mp4',
  105. 'ppt',
  106. 'pptx',
  107. 'mp3'
  108. ];
  109. if (!isAllowedFileType(file.file.name, allowedExtensions)) {
  110. message.error('文件格式不支持');
  111. return false;
  112. }
  113. const type = file.type.includes('image')
  114. ? NaturalTypeEnum.IMG
  115. : file.type.includes('audio')
  116. ? NaturalTypeEnum.SONG
  117. : file.type.includes('video')
  118. ? NaturalTypeEnum.VIDEO
  119. : file.type.includes(
  120. 'vnd.openxmlformats-officedocument.presentationml.presentation'
  121. ) || file.type.includes('vnd.ms-powerpoint')
  122. ? NaturalTypeEnum.PPT
  123. : 'other';
  124. if (type === 'other') {
  125. message.error(`文件格式不支持`);
  126. return false;
  127. }
  128. const size = type === 'IMG' ? 2 : type === 'SONG' ? 20 : 500;
  129. if (size) {
  130. isLt2M = file.file.size / 1024 / 1024 < size;
  131. if (!isLt2M) {
  132. const typeStr =
  133. type === 'IMG' ? '图片' : type === 'SONG' ? '音频' : '视频';
  134. message.error(`${typeStr}大小不能超过${size}M`);
  135. return false;
  136. }
  137. }
  138. if (!isLt2M) {
  139. return isLt2M;
  140. }
  141. // 是否裁切
  142. // if (props.cropper && type === 'IMG') {
  143. // getBase64(file.file, (imageUrl: any) => {
  144. // const target = Object.assign({}, props.options, {
  145. // img: imageUrl,
  146. // name: file.file.name // 上传文件名
  147. // });
  148. // visiable.value = true;
  149. // setTimeout(() => {
  150. // CropperModal.value?.edit(target);
  151. // }, 100);
  152. // });
  153. // return false;
  154. // }
  155. try {
  156. btnLoading.value = true;
  157. const name = file.file.name;
  158. const suffix = name.slice(name.lastIndexOf('.'));
  159. const fileName = `${props.path}${Date.now() + file.id + suffix}`;
  160. const obj = {
  161. filename: fileName,
  162. bucketName: props.bucketName,
  163. postData: {
  164. filename: fileName,
  165. acl: 'public-read',
  166. key: fileName,
  167. unknowValueField: []
  168. }
  169. };
  170. // const { data } = await policy(obj);
  171. const { data } = await getUploadSign(obj);
  172. state.push({
  173. id: file.id,
  174. tempFiileBuffer: file.file,
  175. policy: data.policy,
  176. signature: data.signature,
  177. acl: 'public-read',
  178. key: fileName,
  179. KSSAccessKeyId: data.kssAccessKeyId,
  180. name: fileName
  181. });
  182. } catch {
  183. //
  184. // message.error('上传失败')
  185. btnLoading.value = false;
  186. return false;
  187. }
  188. return true;
  189. };
  190. // const getBase64 = async (img: any, callback: any) => {
  191. // const reader = new FileReader();
  192. // reader.addEventListener('load', () => callback(reader.result));
  193. // reader.readAsDataURL(img);
  194. // };
  195. const onFinish = (options: any) => {
  196. onFinishAfter(options);
  197. };
  198. const onFinishAfter = async (options: any) => {
  199. console.log(options, 'onFinishAfter');
  200. const item = state.find((c: any) => c.id == options.file.id);
  201. const type = formatUrlType(options.file.url);
  202. let coverImg = '';
  203. if (type === 'IMG') {
  204. coverImg = options.file.url;
  205. } else if (type === 'SONG') {
  206. coverImg = PageEnum.SONG_DEFAULT_COVER;
  207. } else if (type === 'PPT') {
  208. coverImg = PageEnum.PPT_DEFAULT_COVER;
  209. } else if (type === 'VIDEO') {
  210. // 获取视频封面图
  211. coverImg = await getVideoCoverImg(item.tempFiileBuffer);
  212. }
  213. uploadList.value.push({
  214. coverImg,
  215. content: options.file.url,
  216. id: options.file.id,
  217. name: options.file.name
  218. ? options.file.name.slice(0, options.file.name.lastIndexOf('.'))
  219. : ''
  220. });
  221. visiable.value = false;
  222. btnLoading.value = false;
  223. };
  224. const getVideoMsg = (file: any) => {
  225. return new Promise((resolve, reject) => {
  226. // let dataURL = '';
  227. const videoElement = document.createElement('video');
  228. videoElement.setAttribute('crossOrigin', 'Anonymous'); // 处理跨域
  229. videoElement.setAttribute('preload', 'auto'); // auto|metadata|none
  230. videoElement.muted = true;
  231. videoElement.autoplay = true;
  232. videoElement.src = URL.createObjectURL(file);
  233. // Listen for 'canplay' to ensure the video is ready for frame capture
  234. videoElement.addEventListener('loadedmetadata', () => {
  235. // 这里开始播放
  236. videoElement.play();
  237. setTimeout(() => {
  238. // 过500ms 暂停, 解决空白问题
  239. videoElement.currentTime = 0;
  240. videoElement.pause();
  241. // 创建canvas元素
  242. const canvas: any = document.createElement('canvas');
  243. canvas.width = videoElement.videoWidth;
  244. canvas.height = videoElement.videoHeight;
  245. // 将视频帧绘制到canvas上
  246. // const ctx = canvas.getContext('2d');
  247. // console.log(videoElement);
  248. // ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
  249. canvas
  250. .getContext('2d')
  251. .drawImage(videoElement, 0, 0, canvas.width, canvas.height);
  252. // 将canvas图像转换为base64格式的数据URI
  253. // const dataUrl = canvas.toDataURL('image/png');
  254. // 返回base64格式的数据URI
  255. // resolve(dataUrl);
  256. canvas.toBlob((blob: any) => {
  257. resolve(blob);
  258. });
  259. }, 500);
  260. });
  261. // 如果视频加载出错,则返回错误信息
  262. videoElement.addEventListener('error', (e: any) => {
  263. reject(e);
  264. });
  265. // return new Promise((resolve, reject) => {
  266. // videoElement.addEventListener('loadedmetadata', () => {
  267. // // 这里开始播放
  268. // videoElement.play()
  269. // setTimeout(() => {
  270. // // 过500ms 暂停, 解决空白问题
  271. // videoElement.pause()
  272. // // 创建canvas元素
  273. // const canvas = document.createElement('canvas');
  274. // canvas.width = videoElement.videoWidth;
  275. // canvas.height = videoElement.videoHeight;
  276. // // 将视频帧绘制到canvas上
  277. // const ctx = canvas.getContext('2d');
  278. // console.log(videoElement);
  279. // ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
  280. // // 将canvas图像转换为base64格式的数据URI
  281. // const dataUrl = canvas.toDataURL('image/png');
  282. // console.log(dataUrl);
  283. // thumbnail.src = dataUrl;
  284. // thumbnail.style.display = 'inline';
  285. // // 返回base64格式的数据URI
  286. // resolve(dataUrl);
  287. // }, 500);
  288. // });
  289. // // 如果视频加载出错,则返回错误信息
  290. // videoElement.addEventListener('error', () => {
  291. // reject(`Failed to load video: ${videoUrl}`);
  292. // });
  293. // });
  294. // videoElement.addEventListener('canplay', function () {
  295. // console.log('Video can play');
  296. // const canvas: any = document.createElement('canvas'),
  297. // width = videoElement.videoWidth,
  298. // height = videoElement.videoHeight;
  299. // canvas.width = width;
  300. // canvas.height = height;
  301. // canvas.getContext('2d').drawImage(videoElement, 0, 0, width, height);
  302. // canvas.toBlob((blob: any) => {
  303. // resolve(blob);
  304. // });
  305. // });
  306. // videoElement.addEventListener('error', function (e) {
  307. // console.error('Error loading video:', e);
  308. // reject(e);
  309. // });
  310. });
  311. };
  312. const getVideoCoverImg = async (file: any) => {
  313. try {
  314. btnLoading.value = true;
  315. const imgBlob: any = await getVideoMsg(file || tempFiileBuffer.value);
  316. const fileName = `${props.path}${Date.now() + '.png'}`;
  317. const obj = {
  318. filename: fileName,
  319. bucketName: props.bucketName,
  320. postData: {
  321. filename: fileName,
  322. acl: 'public-read',
  323. key: fileName,
  324. unknowValueField: []
  325. }
  326. };
  327. const { data } = await getUploadSign(obj);
  328. const fileParams = {
  329. policy: data.policy,
  330. signature: data.signature,
  331. key: fileName,
  332. acl: 'public-read',
  333. KSSAccessKeyId: data.kssAccessKeyId,
  334. name: fileName,
  335. file: imgBlob
  336. } as any;
  337. const res = await onOnlyFileUpload(ossUploadUrl, fileParams);
  338. return res;
  339. } finally {
  340. btnLoading.value = false;
  341. }
  342. };
  343. const onRemove = async (file: any) => {
  344. const index = uploadList.value.findIndex(
  345. (update: any) => update.id === file.file.id
  346. );
  347. uploadList.value.splice(index, 1);
  348. btnLoading.value = false;
  349. return true;
  350. };
  351. const uploadStatus = computed(() => {
  352. let status = false;
  353. fileListRef.value.forEach((file: any) => {
  354. if (file.status !== 'finished') {
  355. status = true;
  356. }
  357. });
  358. return status || fileListRef.value.length <= 0;
  359. });
  360. const onCustomRequest = ({
  361. file,
  362. // data,
  363. // headers,
  364. // withCredentials,
  365. action,
  366. onFinish,
  367. onError,
  368. onProgress
  369. }: UploadCustomRequestOptions) => {
  370. const item = state.find((c: any) => {
  371. return c.id == file.id;
  372. });
  373. item.file = file;
  374. onFileUpload({ file, action, data: item, onProgress, onFinish, onError });
  375. };
  376. const onSubmit = async () => {
  377. const list: any = [];
  378. fileListRef.value.forEach((file: any) => {
  379. const item = uploadList.value.find(
  380. (child: any) => child.id === file.id
  381. );
  382. if (item) {
  383. list.push(item);
  384. }
  385. });
  386. emit('confrim', list);
  387. };
  388. return () => (
  389. <div class={styles.saveModal}>
  390. <NUpload
  391. ref={uploadRef}
  392. action={ossUploadUrl}
  393. // data={(file: any) => {
  394. // return { ...more };
  395. // }}
  396. customRequest={onCustomRequest}
  397. v-model:fileList={fileListRef.value}
  398. accept=".jpg,jpeg,.png,audio/mp3,video/mp4,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation"
  399. multiple={true}
  400. max={10}
  401. // disabled={props.disabled}
  402. showFileList={true}
  403. showPreviewButton
  404. onBeforeUpload={(options: any) => onBeforeUpload(options)}
  405. onFinish={(options: any) => {
  406. onFinish(options);
  407. }}
  408. onRemove={(options: any) => onRemove(options)}>
  409. <NUploadDragger>
  410. <div class={styles.uploadBtn}>
  411. <div class={styles.iconUploadAdd} />
  412. <h3>点击或者拖动文件到该区域来上传</h3>
  413. <p>
  414. 仅支持JPG、PNG、MP3、MP4、PPT格式文件,单次最多支持
  415. <br />
  416. 上传10个文件
  417. </p>
  418. </div>
  419. </NUploadDragger>
  420. </NUpload>
  421. <NSpace class={styles.btnGroup} justify="center">
  422. <NButton round onClick={() => emit('close')}>
  423. 取消
  424. </NButton>
  425. <NButton
  426. round
  427. type="primary"
  428. disabled={uploadStatus.value || btnLoading.value}
  429. onClick={onSubmit}>
  430. 确定
  431. </NButton>
  432. </NSpace>
  433. </div>
  434. );
  435. }
  436. });