diff --git a/src/assets/bot_logo.png b/src/assets/bot_logo.png new file mode 100644 index 0000000..b5ea9a6 Binary files /dev/null and b/src/assets/bot_logo.png differ diff --git a/src/components/bottomInfo/bottomInfo.vue b/src/components/bottomInfo/bottomInfo.vue index 376de05..9d362bd 100644 --- a/src/components/bottomInfo/bottomInfo.vue +++ b/src/components/bottomInfo/bottomInfo.vue @@ -11,22 +11,12 @@ Gin-Vue-Admin - - - -
- Copyright - - flipped-aurora团队Echo
+ @@ -35,10 +25,4 @@ 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' - ) diff --git a/src/components/richtext/rich-edit.vue b/src/components/richtext/rich-edit.vue index 25158da..e238518 100644 --- a/src/components/richtext/rich-edit.vue +++ b/src/components/richtext/rich-edit.vue @@ -27,6 +27,7 @@ import { ElMessage } from 'element-plus' import { getUrl } from '@/utils/image' + import botLogo from '@/assets/bot_logo.png' const emits = defineEmits(['change', 'update:modelValue']) @@ -39,6 +40,10 @@ modelValue: { type: String, default: '' + }, + useWatermark: { + type: Boolean, + default: false } }) @@ -54,6 +59,60 @@ fieldName: 'file', maxFileSize: 1024 * 1024 * 10, // 限制图片大小为10MB 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) { if (res.code === 0) { const urlPath = getUrl(res.data.file.url) @@ -110,6 +169,94 @@ 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 })) + } + }) + } diff --git a/src/pathInfo.json b/src/pathInfo.json index e688821..0fa9f30 100644 --- a/src/pathInfo.json +++ b/src/pathInfo.json @@ -26,6 +26,7 @@ "/src/view/goods/article/edit.vue": "Edit", "/src/view/goods/article/index.vue": "Index", "/src/view/goods/index.vue": "goods", + "/src/view/goods/vip/index.vue": "VipList", "/src/view/init/index.vue": "Init", "/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu", "/src/view/layout/aside/asideComponent/index.vue": "AsideComponent", diff --git a/src/view/bot/bot/botForm.vue b/src/view/bot/bot/botForm.vue index 3afe384..b4773c9 100644 --- a/src/view/bot/bot/botForm.vue +++ b/src/view/bot/bot/botForm.vue @@ -29,8 +29,6 @@ defineOptions({ name: 'BotForm' }) -// 自动获取字典 -import { getDictFunc } from '@/utils/format' import { useRoute, useRouter } from "vue-router" import { ElMessage } from 'element-plus' import { ref, reactive } from 'vue'