🎨 新增上传图片时水印处理
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								src/assets/bot_logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/bot_logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 35 KiB | 
| @@ -11,22 +11,12 @@ | |||||||
|       <span> |       <span> | ||||||
|         <a |         <a | ||||||
|           class="font-bold text-active" |           class="font-bold text-active" | ||||||
|           href="https://github.com/flipped-aurora/gin-vue-admin" |           href="https://echol.cn" | ||||||
|           >Gin-Vue-Admin</a |           >Echo</a | ||||||
|         > |  | ||||||
|       </span> |  | ||||||
|     </div> |  | ||||||
|     <slot /> |  | ||||||
|     <div class="text-center"> |  | ||||||
|       <span class="mr-1">Copyright</span> |  | ||||||
|       <span> |  | ||||||
|         <a |  | ||||||
|           class="font-bold text-active" |  | ||||||
|           href="https://github.com/flipped-aurora" |  | ||||||
|           >flipped-aurora团队</a |  | ||||||
|         > |         > | ||||||
|       </span> |       </span> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -35,10 +25,4 @@ | |||||||
|     name: 'BottomInfo' |     name: 'BottomInfo' | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   console.log( |  | ||||||
|     `%c powered by %c flipped-aurorae %c`, |  | ||||||
|     'background:#0081ff; padding: 1px; border-radius: 3px 0 0 3px; color: #fff', |  | ||||||
|     'background:#354855; padding: 1px 5px; border-radius: 0 3px 3px 0; color: #fff; font-weight: bold;', |  | ||||||
|     'background:transparent' |  | ||||||
|   ) |  | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ | |||||||
|  |  | ||||||
|   import { ElMessage } from 'element-plus' |   import { ElMessage } from 'element-plus' | ||||||
|   import { getUrl } from '@/utils/image' |   import { getUrl } from '@/utils/image' | ||||||
|  |   import botLogo from '@/assets/bot_logo.png' | ||||||
|  |  | ||||||
|   const emits = defineEmits(['change', 'update:modelValue']) |   const emits = defineEmits(['change', 'update:modelValue']) | ||||||
|  |  | ||||||
| @@ -39,6 +40,10 @@ | |||||||
|     modelValue: { |     modelValue: { | ||||||
|       type: String, |       type: String, | ||||||
|       default: '' |       default: '' | ||||||
|  |     }, | ||||||
|  |     useWatermark: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
| @@ -54,6 +59,60 @@ | |||||||
|         fieldName: 'file', |         fieldName: 'file', | ||||||
|         maxFileSize: 1024 * 1024 * 10, // 限制图片大小为10MB |         maxFileSize: 1024 * 1024 * 10, // 限制图片大小为10MB | ||||||
|         maxNumberOfFiles: 1, |         maxNumberOfFiles: 1, | ||||||
|  |         async customUpload(file, insertFn) { | ||||||
|  |           // 未开启水印则直接上传原图 | ||||||
|  |           if (!props.useWatermark) { | ||||||
|  |             const formData = new FormData() | ||||||
|  |             formData.append('file', file) | ||||||
|  |             const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', { method: 'POST', body: formData }) | ||||||
|  |             const res = await resp.json() | ||||||
|  |             if (res.code === 0) { | ||||||
|  |               const urlPath = getUrl(res.data.file.url) | ||||||
|  |               insertFn(urlPath, res.data.file.name) | ||||||
|  |             } else { | ||||||
|  |               ElMessage.error(res.msg || '上传失败') | ||||||
|  |             } | ||||||
|  |             return | ||||||
|  |           } | ||||||
|  |           try { | ||||||
|  |             const watermarkedBlob = await addBottomWatermark(file, { | ||||||
|  |               stripRatio: 0.18, // 水印条高度占原图高度比例 | ||||||
|  |               background: 'rgba(255,255,255,0.96)', | ||||||
|  |               text: '老陈机器人', | ||||||
|  |               textColor: '#333', | ||||||
|  |               fontFamily: 'PingFang SC, Microsoft YaHei, Arial', | ||||||
|  |               logo: botLogo | ||||||
|  |             }) | ||||||
|  |  | ||||||
|  |             const newFile = new File([watermarkedBlob], file.name, { type: watermarkedBlob.type || file.type }) | ||||||
|  |             const formData = new FormData() | ||||||
|  |             formData.append('file', newFile) | ||||||
|  |             const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', { | ||||||
|  |               method: 'POST', | ||||||
|  |               body: formData | ||||||
|  |             }) | ||||||
|  |             const res = await resp.json() | ||||||
|  |             if (res.code === 0) { | ||||||
|  |               const urlPath = getUrl(res.data.file.url) | ||||||
|  |               insertFn(urlPath, res.data.file.name) | ||||||
|  |             } else { | ||||||
|  |               ElMessage.error(res.msg || '上传失败') | ||||||
|  |             } | ||||||
|  |           } catch { | ||||||
|  |             ElMessage.error('处理水印失败') | ||||||
|  |             // 降级:直接走原图上传 | ||||||
|  |             const formData = new FormData() | ||||||
|  |             formData.append('file', file) | ||||||
|  |             const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', { method: 'POST', body: formData }) | ||||||
|  |             const res = await resp.json() | ||||||
|  |             if (res.code === 0) { | ||||||
|  |               const urlPath = getUrl(res.data.file.url) | ||||||
|  |               insertFn(urlPath, res.data.file.name) | ||||||
|  |             } else { | ||||||
|  |               ElMessage.error(res.msg || '上传失败') | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|         customInsert(res, insertFn) { |         customInsert(res, insertFn) { | ||||||
|           if (res.code === 0) { |           if (res.code === 0) { | ||||||
|             const urlPath = getUrl(res.data.file.url) |             const urlPath = getUrl(res.data.file.url) | ||||||
| @@ -110,6 +169,94 @@ | |||||||
|       valueHtml.value = props.modelValue |       valueHtml.value = props.modelValue | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|  |   async function addBottomWatermark(file, options) { | ||||||
|  |     const { stripRatio = 0.18, background = 'rgba(255,255,255,0.96)', text = '', textColor = '#333', fontFamily = 'Arial', logo } = options || {} | ||||||
|  |  | ||||||
|  |     const img = await fileToImage(file) | ||||||
|  |     const width = img.naturalWidth || img.width | ||||||
|  |     const height = img.naturalHeight || img.height | ||||||
|  |     const stripHeight = Math.max(60, Math.floor(height * stripRatio)) | ||||||
|  |  | ||||||
|  |     const canvas = document.createElement('canvas') | ||||||
|  |     canvas.width = width | ||||||
|  |     canvas.height = height + stripHeight | ||||||
|  |     const ctx = canvas.getContext('2d') | ||||||
|  |  | ||||||
|  |     // 原图 | ||||||
|  |     ctx.drawImage(img, 0, 0, width, height) | ||||||
|  |  | ||||||
|  |     // 底部水印条背景 | ||||||
|  |     ctx.fillStyle = background | ||||||
|  |     ctx.fillRect(0, height, width, stripHeight) | ||||||
|  |  | ||||||
|  |     // 左侧 Logo(可选) | ||||||
|  |     let logoSize = Math.floor(stripHeight * 0.6) | ||||||
|  |     let logoPadding = Math.floor(stripHeight * 0.2) | ||||||
|  |     if (logo) { | ||||||
|  |       try { | ||||||
|  |         const logoImg = await srcToImage(logo) | ||||||
|  |         const ratio = logoImg.width / logoImg.height | ||||||
|  |         const drawW = logoSize | ||||||
|  |         const drawH = Math.floor(drawW / ratio) | ||||||
|  |         const y = height + Math.floor((stripHeight - drawH) / 2) | ||||||
|  |         ctx.drawImage(logoImg, logoPadding, y, drawW, drawH) | ||||||
|  |       } catch { void 0 } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 右侧文字 | ||||||
|  |     ctx.fillStyle = textColor | ||||||
|  |     const fontSize = Math.floor(stripHeight * 0.38) | ||||||
|  |     ctx.font = `${fontSize}px ${fontFamily}` | ||||||
|  |     ctx.textBaseline = 'middle' | ||||||
|  |     ctx.textAlign = 'right' | ||||||
|  |     const textPadding = Math.floor(stripHeight * 0.25) | ||||||
|  |     ctx.fillText(text, width - textPadding, height + Math.floor(stripHeight / 2)) | ||||||
|  |  | ||||||
|  |     const blob = await canvasToBlob(canvas, file.type || 'image/png') | ||||||
|  |     return blob | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function fileToImage(file) { | ||||||
|  |     return new Promise((resolve, reject) => { | ||||||
|  |       const reader = new FileReader() | ||||||
|  |       reader.onload = () => { | ||||||
|  |         const img = new Image() | ||||||
|  |         img.onload = () => resolve(img) | ||||||
|  |         img.onerror = reject | ||||||
|  |         img.src = reader.result | ||||||
|  |       } | ||||||
|  |       reader.onerror = reject | ||||||
|  |       reader.readAsDataURL(file) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function srcToImage(src) { | ||||||
|  |     return new Promise((resolve, reject) => { | ||||||
|  |       const img = new Image() | ||||||
|  |       img.crossOrigin = 'anonymous' | ||||||
|  |       img.onload = () => resolve(img) | ||||||
|  |       img.onerror = reject | ||||||
|  |       img.src = src | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function canvasToBlob(canvas, mime) { | ||||||
|  |     return new Promise((resolve) => { | ||||||
|  |       if (canvas.toBlob) { | ||||||
|  |         canvas.toBlob((blob) => resolve(blob), mime, 0.92) | ||||||
|  |       } else { | ||||||
|  |         // 兼容处理 | ||||||
|  |         const dataURL = canvas.toDataURL(mime) | ||||||
|  |         const arr = dataURL.split(',') | ||||||
|  |         const bstr = atob(arr[1]) | ||||||
|  |         let n = bstr.length | ||||||
|  |         const u8arr = new Uint8Array(n) | ||||||
|  |         while (n--) u8arr[n] = bstr.charCodeAt(n) | ||||||
|  |         resolve(new Blob([u8arr], { type: mime })) | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style scoped lang="scss"></style> | <style scoped lang="scss"></style> | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ | |||||||
|   "/src/view/goods/article/edit.vue": "Edit", |   "/src/view/goods/article/edit.vue": "Edit", | ||||||
|   "/src/view/goods/article/index.vue": "Index", |   "/src/view/goods/article/index.vue": "Index", | ||||||
|   "/src/view/goods/index.vue": "goods", |   "/src/view/goods/index.vue": "goods", | ||||||
|  |   "/src/view/goods/vip/index.vue": "VipList", | ||||||
|   "/src/view/init/index.vue": "Init", |   "/src/view/init/index.vue": "Init", | ||||||
|   "/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu", |   "/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu", | ||||||
|   "/src/view/layout/aside/asideComponent/index.vue": "AsideComponent", |   "/src/view/layout/aside/asideComponent/index.vue": "AsideComponent", | ||||||
|   | |||||||
| @@ -29,8 +29,6 @@ defineOptions({ | |||||||
|     name: 'BotForm' |     name: 'BotForm' | ||||||
| }) | }) | ||||||
|  |  | ||||||
| // 自动获取字典 |  | ||||||
| import { getDictFunc } from '@/utils/format' |  | ||||||
| import { useRoute, useRouter } from "vue-router" | import { useRoute, useRouter } from "vue-router" | ||||||
| import { ElMessage } from 'element-plus' | import { ElMessage } from 'element-plus' | ||||||
| import { ref, reactive } from 'vue' | import { ref, reactive } from 'vue' | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user