upload-file.tsx 12 KB

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