🎨 新增上传图片时水印处理
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