Image.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <template>
  2. <div class="upload-box">
  3. <el-upload
  4. action="#"
  5. :id="uuid"
  6. :class="['upload', self_disabled ? 'disabled' : '', drag ? 'no-border' : '']"
  7. :multiple="false"
  8. :disabled="self_disabled"
  9. :show-file-list="false"
  10. :http-request="handleHttpUpload"
  11. :before-upload="beforeUpload"
  12. :on-success="uploadSuccess"
  13. :on-error="uploadError"
  14. :drag="drag"
  15. :accept="fileType.join(',')"
  16. >
  17. <template v-if="imageUrl">
  18. <!-- 如果返回的是OSS 地址则不需要增加 baseURL -->
  19. <img :src="imageUrl.includes('http') ? imageUrl : baseURL + imageUrl" class="upload-image" />
  20. <div class="upload-handle" @click.stop>
  21. <div class="handle-icon" @click="editImg" v-if="!self_disabled">
  22. <el-icon :size="props.iconSize"><Edit /></el-icon>
  23. <span v-if="!props.iconSize">{{ $t('common.editBtn') }}</span>
  24. </div>
  25. <div class="handle-icon" @click="imgViewVisible = true">
  26. <el-icon :size="props.iconSize"><ZoomIn /></el-icon>
  27. <span v-if="!props.iconSize">{{ $t('common.viewBtn') }}</span>
  28. </div>
  29. <div class="handle-icon" @click="deleteImg" v-if="!self_disabled">
  30. <el-icon :size="props.iconSize"><Delete /></el-icon>
  31. <span v-if="!props.iconSize">{{ $t('common.delBtn') }}</span>
  32. </div>
  33. </div>
  34. </template>
  35. <template v-else>
  36. <div class="upload-empty">
  37. <slot name="empty">
  38. <el-icon><Plus /></el-icon>
  39. <!-- <span>请上传图片</span> -->
  40. </slot>
  41. </div>
  42. </template>
  43. </el-upload>
  44. <div class="el-upload__tip">
  45. <slot name="tip"></slot>
  46. </div>
  47. <el-image-viewer
  48. :teleported="true"
  49. v-if="imgViewVisible"
  50. @close="imgViewVisible = false"
  51. :url-list="[imageUrl.includes('http') ? imageUrl : baseURL + imageUrl]"
  52. />
  53. </div>
  54. </template>
  55. <script setup lang="ts" name="UploadImg">
  56. import { ref, computed, inject } from 'vue';
  57. import { ElNotification, formContextKey, formItemContextKey } from 'element-plus';
  58. import type { UploadProps, UploadRequestOptions } from 'element-plus';
  59. import { generateUUID } from '/@/utils/other';
  60. import request from '/@/utils/request';
  61. interface UploadFileProps {
  62. imageUrl?: string; // 图片地址 ==> 必传
  63. uploadFileUrl?: string; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传
  64. drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true)
  65. disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false)
  66. fileSize?: number; // 图片大小限制 ==> 非必传(默认为 5M)
  67. fileType?: File.ImageMimeType[]; // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
  68. height?: string; // 组件高度 ==> 非必传(默认为 150px)
  69. width?: string; // 组件宽度 ==> 非必传(默认为 150px)
  70. borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为 8px)
  71. iconSize?: number;
  72. dir?: string; // 文件目录
  73. fileId?: string; //文件id
  74. }
  75. // 接受父组件参数
  76. const props = withDefaults(defineProps<UploadFileProps>(), {
  77. imageUrl: '',
  78. uploadFileUrl: '/admin/sys-file/upload',
  79. drag: true,
  80. disabled: false,
  81. fileSize: 5,
  82. fileType: () => ['image/jpeg', 'image/png', 'image/gif'],
  83. height: '150px',
  84. width: '150px',
  85. borderRadius: '8px',
  86. dir: '',
  87. fileId: '',
  88. });
  89. // 生成组件唯一id
  90. const uuid = ref('id-' + generateUUID());
  91. // 查看图片
  92. const imgViewVisible = ref(false);
  93. // 获取 el-form 组件上下文
  94. const formContext = inject(formContextKey, void 0);
  95. // 获取 el-form-item 组件上下文
  96. const formItemContext = inject(formItemContextKey, void 0);
  97. // 判断是否禁用上传和删除
  98. const self_disabled = computed(() => {
  99. return props.disabled || formContext?.disabled;
  100. });
  101. /**
  102. * @description 图片上传
  103. * @param options upload 所有配置项
  104. * */
  105. interface UploadEmits {
  106. (e: 'update:imageUrl', value: string): void;
  107. (e: 'update:fileId', value: string): void;
  108. (e: 'change', value: string): void;
  109. }
  110. const emit = defineEmits<UploadEmits>();
  111. const handleHttpUpload = async (options: UploadRequestOptions) => {
  112. let formData = new FormData();
  113. formData.append('file', options.file);
  114. formData.append('dir', props.dir);
  115. try {
  116. const { data } = await request({
  117. url: props.uploadFileUrl,
  118. method: 'post',
  119. headers: {
  120. 'Content-Type': 'multipart/form-data',
  121. },
  122. data: formData,
  123. });
  124. emit('update:imageUrl', data.url);
  125. emit('update:fileId', data.fileId);
  126. emit('change', data.url);
  127. // 调用 el-form 内部的校验方法(可自动校验)
  128. formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]);
  129. } catch (error) {
  130. options.onError(error as any);
  131. }
  132. };
  133. /**
  134. * @description 删除图片
  135. * */
  136. const deleteImg = () => {
  137. emit('update:imageUrl', '');
  138. emit('update:fileId', '');
  139. emit('change', '');
  140. };
  141. /**
  142. * @description 编辑图片
  143. * */
  144. const editImg = () => {
  145. const dom = document.querySelector(`#${uuid.value} .el-upload__input`);
  146. dom && dom.dispatchEvent(new MouseEvent('click'));
  147. };
  148. /**
  149. * @description 文件上传之前判断
  150. * @param rawFile 选择的文件
  151. * */
  152. const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
  153. const imgSize = rawFile.size / 1024 / 1024 < props.fileSize;
  154. const imgType = props.fileType.includes(rawFile.type as File.ImageMimeType);
  155. if (!imgType)
  156. ElNotification({
  157. title: '温馨提示',
  158. message: '上传图片不符合所需的格式!',
  159. type: 'warning',
  160. });
  161. if (!imgSize)
  162. setTimeout(() => {
  163. ElNotification({
  164. title: '温馨提示',
  165. message: `上传图片大小不能超过 ${props.fileSize}M!`,
  166. type: 'warning',
  167. });
  168. }, 0);
  169. return imgType && imgSize;
  170. };
  171. /**
  172. * @description 图片上传成功
  173. * */
  174. const uploadSuccess = () => {
  175. ElNotification({
  176. title: '温馨提示',
  177. message: '图片上传成功!',
  178. type: 'success',
  179. });
  180. };
  181. /**
  182. * @description 图片上传错误
  183. * */
  184. const uploadError = () => {
  185. ElNotification({
  186. title: '温馨提示',
  187. message: '图片上传失败,请您重新上传!',
  188. type: 'error',
  189. });
  190. };
  191. </script>
  192. <style scoped lang="scss">
  193. .is-error {
  194. .upload {
  195. :deep(.el-upload),
  196. :deep(.el-upload-dragger) {
  197. border: 1px dashed var(--el-color-danger) !important;
  198. &:hover {
  199. border-color: var(--el-color-primary) !important;
  200. }
  201. }
  202. }
  203. }
  204. :deep(.disabled) {
  205. .el-upload,
  206. .el-upload-dragger {
  207. cursor: not-allowed !important;
  208. background: var(--el-disabled-bg-color);
  209. border: 1px dashed var(--el-border-color-darker) !important;
  210. &:hover {
  211. border: 1px dashed var(--el-border-color-darker) !important;
  212. }
  213. }
  214. }
  215. .upload-box {
  216. .no-border {
  217. :deep(.el-upload) {
  218. border: none !important;
  219. }
  220. }
  221. :deep(.upload) {
  222. .el-upload {
  223. position: relative;
  224. display: flex;
  225. align-items: center;
  226. justify-content: center;
  227. width: v-bind(width);
  228. height: v-bind(height);
  229. overflow: hidden;
  230. border: 1px dashed var(--el-border-color-darker);
  231. border-radius: v-bind(borderRadius);
  232. transition: var(--el-transition-duration-fast);
  233. &:hover {
  234. border-color: var(--el-color-primary);
  235. .upload-handle {
  236. opacity: 1;
  237. }
  238. }
  239. .el-upload-dragger {
  240. display: flex;
  241. align-items: center;
  242. justify-content: center;
  243. width: 100%;
  244. height: 100%;
  245. padding: 0;
  246. overflow: hidden;
  247. background-color: transparent;
  248. border: 1px dashed var(--el-border-color-darker);
  249. border-radius: v-bind(borderRadius);
  250. &:hover {
  251. border: 1px dashed var(--el-color-primary);
  252. }
  253. }
  254. .el-upload-dragger.is-dragover {
  255. background-color: var(--el-color-primary-light-9);
  256. border: 2px dashed var(--el-color-primary) !important;
  257. }
  258. .upload-image {
  259. width: 100%;
  260. height: 100%;
  261. object-fit: contain;
  262. }
  263. .upload-empty {
  264. position: relative;
  265. display: flex;
  266. flex-direction: column;
  267. align-items: center;
  268. justify-content: center;
  269. font-size: 12px;
  270. line-height: 30px;
  271. color: var(--el-color-info);
  272. .el-icon {
  273. font-size: 28px;
  274. color: var(--el-text-color-secondary);
  275. }
  276. }
  277. .upload-handle {
  278. position: absolute;
  279. top: 0;
  280. right: 0;
  281. box-sizing: border-box;
  282. display: flex;
  283. align-items: center;
  284. justify-content: center;
  285. width: 100%;
  286. height: 100%;
  287. cursor: pointer;
  288. background: rgb(0 0 0 / 60%);
  289. opacity: 0;
  290. transition: var(--el-transition-duration-fast);
  291. .handle-icon {
  292. display: flex;
  293. flex-direction: column;
  294. align-items: center;
  295. justify-content: center;
  296. padding: 0 6%;
  297. color: aliceblue;
  298. .el-icon {
  299. margin-bottom: 40%;
  300. font-size: 130%;
  301. line-height: 130%;
  302. }
  303. span {
  304. font-size: 85%;
  305. line-height: 85%;
  306. }
  307. }
  308. }
  309. }
  310. }
  311. .el-upload__tip {
  312. line-height: 18px;
  313. text-align: center;
  314. }
  315. }
  316. </style>