upload-file.tsx 13 KB

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