upload-file.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. import {
  2. NModal,
  3. NSpin,
  4. NUpload,
  5. NUploadDragger,
  6. UploadFileInfo,
  7. useMessage
  8. } from 'naive-ui';
  9. import { defineComponent, watch, PropType, reactive, ref } from 'vue';
  10. import { policy } from '@/components/upload-file/api';
  11. import Copper from '@/components/upload-file/copper';
  12. import axios from 'axios';
  13. import styles from './index.module.less';
  14. import iconUploadAdd from '../../../images/icon-upload-add.png';
  15. import { NaturalTypeEnum, PageEnum } from '@/enums/pageEnum';
  16. import { formatUrlType } from '.';
  17. /**
  18. * 1. 图片上传可以进行裁剪
  19. * 2. 视频上传可以选择某一帧做为封面
  20. * 3. 音频只用限制某一种格式
  21. * 4. 只支持单个上传,因为多个上传没有办法去处理,即有视频,图片等
  22. */
  23. export default defineComponent({
  24. name: 'upload-file',
  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. cropper: {
  76. // 是否裁切, 只有图片才支持 - 失效(不支持)
  77. type: Boolean as PropType<boolean>,
  78. default: false
  79. },
  80. options: {
  81. type: Object,
  82. default: () => {
  83. return {
  84. viewMode: 0,
  85. autoCrop: true, //是否默认生成截图框
  86. enlarge: 1, // 图片放大倍数
  87. autoCropWidth: 200, //默认生成截图框宽度
  88. autoCropHeight: 200, //默认生成截图框高度
  89. fixedBox: false, //是否固定截图框大小 不允许改变
  90. previewsCircle: true, //预览图是否是原图形
  91. title: '上传图片'
  92. };
  93. }
  94. }
  95. },
  96. emits: [
  97. 'update:fileList',
  98. 'close',
  99. 'readFileInputEventAsArrayBuffer',
  100. 'remove',
  101. 'finished'
  102. ],
  103. setup(props, { emit, expose, slots }) {
  104. const ossUploadUrl = `https://${props.bucketName}.ks3-cn-beijing.ksyuncs.com/`;
  105. const message = useMessage();
  106. const visiable = ref<boolean>(false);
  107. const btnLoading = ref<boolean>(false);
  108. const tempFiileBuffer = ref();
  109. const uploadRef = ref();
  110. const state = reactive([
  111. // {
  112. // policy: '',
  113. // signature: '',
  114. // key: '',
  115. // KSSAccessKeyId: '',
  116. // acl: 'public-read',
  117. // name: ''
  118. // }
  119. ]) as any;
  120. const fileListRef = ref<UploadFileInfo[]>([]);
  121. const initFileList = () => {
  122. if (props.fileList) {
  123. const splitName = props.fileList.split('/');
  124. fileListRef.value = [
  125. {
  126. id: new Date().getTime().toString(),
  127. name: splitName[splitName.length - 1],
  128. status: 'finished',
  129. url: props.fileList
  130. }
  131. ];
  132. } else {
  133. fileListRef.value = [];
  134. }
  135. };
  136. initFileList();
  137. watch(
  138. () => props.imageList,
  139. () => {
  140. initFileList();
  141. }
  142. );
  143. watch(
  144. () => props.fileList,
  145. () => {
  146. initFileList();
  147. }
  148. );
  149. const handleClearFile = () => {
  150. uploadRef.value?.clear();
  151. };
  152. expose({
  153. handleClearFile
  154. });
  155. const CropperModal = ref();
  156. const onBeforeUpload = async (options: any) => {
  157. const file = options.file;
  158. // 文件大小
  159. let isLt2M = true;
  160. const type = file.type.includes('image')
  161. ? NaturalTypeEnum.IMG
  162. : file.type.includes('audio')
  163. ? NaturalTypeEnum.SONG
  164. : NaturalTypeEnum.VIDEO;
  165. const size = type === 'IMG' ? 2 : type === 'SONG' ? 20 : 500;
  166. if (size) {
  167. isLt2M = file.file.size / 1024 / 1024 < size;
  168. if (!isLt2M) {
  169. message.error(`文件大小不能超过${size}M`);
  170. return false;
  171. }
  172. }
  173. if (!isLt2M) {
  174. return isLt2M;
  175. }
  176. // 是否裁切
  177. // if (props.cropper && type === 'IMG') {
  178. // getBase64(file.file, (imageUrl: any) => {
  179. // const target = Object.assign({}, props.options, {
  180. // img: imageUrl,
  181. // name: file.file.name // 上传文件名
  182. // });
  183. // visiable.value = true;
  184. // setTimeout(() => {
  185. // CropperModal.value?.edit(target);
  186. // }, 100);
  187. // });
  188. // return false;
  189. // }
  190. try {
  191. btnLoading.value = true;
  192. const name = file.file.name;
  193. const suffix = name.slice(name.lastIndexOf('.'));
  194. const fileName = `${props.path}${
  195. props.fileName || Date.now() + suffix
  196. }`;
  197. const obj = {
  198. filename: fileName,
  199. bucketName: props.bucketName,
  200. postData: {
  201. filename: fileName,
  202. acl: 'public-read',
  203. key: fileName,
  204. unknowValueField: []
  205. }
  206. };
  207. const { data } = await policy(obj);
  208. state.push({
  209. id: file.id,
  210. tempFiileBuffer: file.file,
  211. policy: data.policy,
  212. signature: data.signature,
  213. acl: 'public-read',
  214. key: fileName,
  215. KSSAccessKeyId: data.kssAccessKeyId,
  216. name: fileName
  217. });
  218. // tempFiileBuffer.value = file.file;
  219. } catch {
  220. //
  221. // message.error('上传失败')
  222. btnLoading.value = false;
  223. return false;
  224. }
  225. return true;
  226. };
  227. const getBase64 = async (img: any, callback: any) => {
  228. const reader = new FileReader();
  229. reader.addEventListener('load', () => callback(reader.result));
  230. reader.readAsDataURL(img);
  231. };
  232. const onFinish = (options: any) => {
  233. console.log(options, 'onFinish');
  234. onFinishAfter(options);
  235. };
  236. const onFinishAfter = async (options: any) => {
  237. const item = state.find((c: any) => c.id == options.file.id);
  238. const url = ossUploadUrl + item.key;
  239. const type = formatUrlType(url);
  240. let coverImg = '';
  241. if (type === 'IMG') {
  242. coverImg = url;
  243. } else if (type === 'SONG') {
  244. coverImg = PageEnum.SONG_DEFAULT_COVER;
  245. } else if (type === 'VIDEO') {
  246. // 获取视频封面图
  247. coverImg = await getVideoCoverImg(item.tempFiileBuffer);
  248. }
  249. emit('update:fileList', url);
  250. emit('readFileInputEventAsArrayBuffer', item.tempFiileBuffer);
  251. console.log(url, 'url onFinishAfter');
  252. emit('finished', {
  253. coverImg,
  254. content: url
  255. });
  256. options.file.url = url;
  257. visiable.value = false;
  258. btnLoading.value = false;
  259. };
  260. const getVideoMsg = (file: any) => {
  261. return new Promise(resolve => {
  262. // let dataURL = '';
  263. const videoElement = document.createElement('video');
  264. videoElement.currentTime = 1;
  265. videoElement.src = URL.createObjectURL(file);
  266. videoElement.addEventListener('loadeddata', function () {
  267. const canvas: any = document.createElement('canvas'),
  268. width = videoElement.videoWidth, //canvas的尺寸和图片一样
  269. height = videoElement.videoHeight;
  270. canvas.width = width;
  271. canvas.height = height;
  272. canvas.getContext('2d').drawImage(videoElement, 0, 0, width, height); //绘制canvas
  273. // dataURL = canvas.toDataURL('image/jpeg'); //转换为base64
  274. console.log(canvas);
  275. canvas.toBlob((blob: any) => {
  276. // console.log(blob);
  277. resolve(blob);
  278. });
  279. });
  280. });
  281. };
  282. const getVideoCoverImg = async (file: any) => {
  283. try {
  284. btnLoading.value = true;
  285. const imgBlob: any = await getVideoMsg(file || tempFiileBuffer.value);
  286. const fileName = `${props.path}${Date.now() + '.png'}`;
  287. const obj = {
  288. filename: fileName,
  289. bucketName: props.bucketName,
  290. postData: {
  291. filename: fileName,
  292. acl: 'public-read',
  293. key: fileName,
  294. unknowValueField: []
  295. }
  296. };
  297. const { data } = await policy(obj);
  298. const fileParams = {
  299. policy: data.policy,
  300. signature: data.signature,
  301. key: fileName,
  302. acl: 'public-read',
  303. KSSAccessKeyId: data.kssAccessKeyId,
  304. name: fileName
  305. } as any;
  306. const formData = new FormData();
  307. for (const key in fileParams) {
  308. formData.append(key, fileParams[key]);
  309. }
  310. formData.append('file', imgBlob);
  311. await axios.post(ossUploadUrl, formData);
  312. const url = ossUploadUrl + fileName;
  313. return url;
  314. } finally {
  315. btnLoading.value = false;
  316. }
  317. };
  318. const onRemove = async () => {
  319. emit('update:fileList', '');
  320. emit('remove');
  321. btnLoading.value = false;
  322. };
  323. // 裁切失败
  324. // const cropperNo = () => {}
  325. // 裁切成功
  326. const cropperOk = async (blob: any) => {
  327. try {
  328. const fileName = `${props.path}${
  329. props.fileName || new Date().getTime() + '.png'
  330. }`;
  331. const obj = {
  332. filename: fileName,
  333. bucketName: props.bucketName,
  334. postData: {
  335. filename: fileName,
  336. acl: 'public-read',
  337. key: fileName,
  338. unknowValueField: []
  339. }
  340. };
  341. const { data } = await policy(obj);
  342. state.policy = data.policy;
  343. state.signature = data.signature;
  344. state.key = fileName;
  345. state.KSSAccessKeyId = data.kssAccessKeyId;
  346. state.name = fileName;
  347. const formData = new FormData();
  348. for (const key in state) {
  349. formData.append(key, state[key]);
  350. }
  351. formData.append('file', blob);
  352. await axios.post(ossUploadUrl, formData).then(() => {
  353. const url = ossUploadUrl + state.key;
  354. const splitName = url.split('/');
  355. fileListRef.value = [
  356. {
  357. id: new Date().getTime().toString(),
  358. name: splitName[splitName.length - 1],
  359. status: 'finished',
  360. url: url
  361. }
  362. ];
  363. emit('update:fileList', url);
  364. emit('finished', {
  365. coverImg: url,
  366. content: url
  367. });
  368. visiable.value = false;
  369. });
  370. } catch {
  371. return false;
  372. }
  373. };
  374. return () => (
  375. <div class={styles.uploadFile}>
  376. <NSpin show={btnLoading.value} description="上传中...">
  377. <NUpload
  378. ref={uploadRef}
  379. action={ossUploadUrl}
  380. data={(file: any) => {
  381. const item = state.find((c: any) => {
  382. return c.id == file.file.id;
  383. });
  384. const { id, tempFiileBuffer, ...more } = item;
  385. return { ...more };
  386. }}
  387. v-model:fileList={fileListRef.value}
  388. accept={props.accept}
  389. multiple={props.multiple}
  390. max={props.max}
  391. disabled={props.disabled}
  392. directoryDnd={props.directoryDnd}
  393. showFileList={props.showFileList}
  394. showPreviewButton
  395. onBeforeUpload={(options: any) => onBeforeUpload(options)}
  396. onFinish={(options: any) => {
  397. onFinish(options);
  398. }}
  399. onChange={(options: any) => {
  400. // console.log(options, 'change');
  401. }}
  402. onRemove={() => onRemove()}>
  403. <NUploadDragger>
  404. {props.showType === 'default' && (
  405. <div class={styles.uploadBtn}>
  406. <img src={iconUploadAdd} class={styles.iconUploadAdd} />
  407. <p>上传</p>
  408. </div>
  409. )}
  410. {props.showType === 'custom' && slots.custom && slots.custom()}
  411. </NUploadDragger>
  412. </NUpload>
  413. </NSpin>
  414. <NModal
  415. v-model:show={visiable.value}
  416. preset="dialog"
  417. showIcon={false}
  418. class={['modalTitle background']}
  419. title="上传图片"
  420. style={{ width: '800px' }}>
  421. {/* @cropper-no="error" @cropper-ok="success" */}
  422. <Copper
  423. ref={CropperModal}
  424. onClose={() => (visiable.value = false)}
  425. onCropperOk={cropperOk}
  426. />
  427. </NModal>
  428. </div>
  429. );
  430. }
  431. });