diff --git a/src/hooks/common/watermark.ts b/src/hooks/common/watermark.ts new file mode 100644 index 0000000..0d31dd3 --- /dev/null +++ b/src/hooks/common/watermark.ts @@ -0,0 +1,130 @@ +import type { Ref } from 'vue'; +import { ref, shallowRef, unref } from 'vue'; +import { debounce } from '@/utils/common'; + +type Attrs = { + textStyles?: { + font?: string; + fillStyle?: string; + rotate?: number; + width?: number; + height?: number; + }; + styles?: { [key: string]: any }; +}; + +type WatermarkOpts = { + appendEl?: Ref; + id?: string; +}; + +export function useWatermark(opts: WatermarkOpts = {}) { + // const id = `waterMark_${Math.random().toString().slice(-10)}_${+new Date()}` + const appendEl = opts.appendEl || (ref(document.body) as Ref); + const watermarkEl = shallowRef(); + + /** 绘制canvas文字背景图 */ + const createCanvasBase64 = (str: string, attrs: Attrs = {}) => { + const can = document.createElement('canvas'); + const { rotate, font, fillStyle, width = 200, height = 140 } = attrs.textStyles || {}; + Object.assign(can, { width, height }); + const cans = can.getContext('2d'); + if (cans) { + cans.rotate((-(rotate ?? 20) * Math.PI) / 180); + cans.font = font || '12px Vedana'; + cans.fillStyle = fillStyle || 'rgba(200, 200, 200, 0.3)'; + cans.textAlign = 'left'; + cans.textBaseline = 'middle'; + cans.fillText(str, can.width / 10, can.height / 2); + } + return can.toDataURL('image/png'); + }; + + /** 页面随窗口调整更新水印 */ + const updateWatermark = ( + watermarkOpts: { + str?: string; + attrs?: Attrs; + width?: number; + height?: number; + } = {} + ) => { + const el = unref(watermarkEl); + if (!el) return; + if (typeof watermarkOpts.width !== 'undefined') { + el.style.width = `${watermarkOpts.width}px`; + } + if (typeof watermarkOpts.height !== 'undefined') { + el.style.height = `${watermarkOpts.height}px`; + } + if (typeof watermarkOpts.str !== 'undefined') { + el.style.background = `url(${createCanvasBase64(watermarkOpts.str, watermarkOpts.attrs)}) left top repeat`; + } + }; + + /** 绘制水印层 */ + const createWatermark = (str: string, attrs: Attrs = {}) => { + if (watermarkEl.value) { + updateWatermark({ str, attrs }); + return watermarkEl; + } + const div = document.createElement('div'); + watermarkEl.value = div; + if (opts.id) { + const last_el = document.getElementById(opts.id); + if (last_el) { + document.body.removeChild(last_el); + } + div.id = opts.id; + } + Object.assign( + div.style, + { + pointerEvents: 'none', + top: '0px', + left: '0px', + position: 'fixed', + zIndex: '100000' + }, + attrs.styles || {} + ); + const el = unref(appendEl); + if (!el) return watermarkEl; + const { clientHeight: height, clientWidth: width } = el; + updateWatermark({ str, attrs, width, height }); + el.appendChild(div); + return watermarkEl; + }; + + const debounceUpdateResize = debounce( + () => { + const el = unref(appendEl); + if (!el) return; + const { clientHeight: height, clientWidth: width } = el; + updateWatermark({ width, height }); + }, + 30, + false + ); + + /** 对外提供的设置水印方法 */ + const setWatermark = (str: string, attrs: Attrs = {}) => { + createWatermark(str, attrs); + window.addEventListener('resize', debounceUpdateResize); + }; + + /** 清除水印 */ + const clearWatermark = () => { + let domId: HTMLElement | null | undefined = unref(watermarkEl); + if (!domId && opts.id) { + domId = document.getElementById(opts.id); + } + watermarkEl.value = undefined; + const el = unref(appendEl); + if (!el) return; + domId && el.removeChild(domId); + window.removeEventListener('resize', debounceUpdateResize); + }; + + return { setWatermark, clearWatermark }; +} diff --git a/src/layouts/modules/theme-drawer/modules/page-fun.vue b/src/layouts/modules/theme-drawer/modules/page-fun.vue index 8f80994..a002f3d 100644 --- a/src/layouts/modules/theme-drawer/modules/page-fun.vue +++ b/src/layouts/modules/theme-drawer/modules/page-fun.vue @@ -101,6 +101,12 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra > + + + + + + diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts index 704dcd9..7368054 100644 --- a/src/locales/langs/en-us.ts +++ b/src/locales/langs/en-us.ts @@ -301,6 +301,10 @@ const local: App.I18n.Schema = { copySuccessMsg: 'Copy Success, Please replace the variable "themeSettings" in "src/theme/settings.ts"', resetConfig: 'Reset Config', resetSuccessMsg: 'Reset Success' + }, + watermark: { + visible: 'Watermark Visible', + text: 'Watermark Text' } }, route: { diff --git a/src/locales/langs/zh-cn.ts b/src/locales/langs/zh-cn.ts index 8bbcc5e..9ce9603 100644 --- a/src/locales/langs/zh-cn.ts +++ b/src/locales/langs/zh-cn.ts @@ -301,6 +301,10 @@ const local: App.I18n.Schema = { copySuccessMsg: '复制成功,请替换 src/theme/settings.ts 中的变量 themeSettings', resetConfig: '重置配置', resetSuccessMsg: '重置成功' + }, + watermark: { + visible: '开启', + text: '水印文字' } }, route: { diff --git a/src/store/modules/theme/index.ts b/src/store/modules/theme/index.ts index 7685c54..e0f4125 100644 --- a/src/store/modules/theme/index.ts +++ b/src/store/modules/theme/index.ts @@ -5,6 +5,7 @@ import { useEventListener, usePreferredColorScheme } from '@vueuse/core'; import { getPaletteColorByNumber } from '@sa/color'; import { SetupStoreId } from '@/enum'; import { localStg } from '@/utils/storage'; +import { useWatermark } from '@/hooks/common/watermark'; import { addThemeVarsToHtml, createThemeToken, @@ -54,6 +55,26 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => { */ const settingsJson = computed(() => JSON.stringify(settings.value)); + /** Watermarks */ + const { setWatermark, clearWatermark } = useWatermark({ id: 'global_watermark_id' }); + + /** 开启水印 */ + function toggleWatermark(visible: boolean) { + visible ? setWatermark(settings.value.watermark.text) : clearWatermark(); + } + + /** 修改水印文案 */ + function setWatermarkText(text: string) { + if (!text) { + clearWatermark(); + return; + } + if (settings.value.watermark.visible) { + settings.value.watermark.text = text; + setWatermark(settings.value.watermark.text); + } + } + /** Reset store */ function resetStore() { const themeStore = useThemeStore(); @@ -171,6 +192,15 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => { }, { immediate: true } ); + + watch( + settings.value.watermark, + val => { + toggleWatermark(val.visible); + setWatermarkText(val.text); + }, + { immediate: true } + ); }); /** On scope dispose */ @@ -189,6 +219,8 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => { setThemeScheme, toggleThemeScheme, updateThemeColors, - setThemeLayout + setThemeLayout, + setWatermarkText, + toggleWatermark }; }); diff --git a/src/theme/settings.ts b/src/theme/settings.ts index 78ebada..339a80a 100644 --- a/src/theme/settings.ts +++ b/src/theme/settings.ts @@ -46,6 +46,10 @@ export const themeSettings: App.Theme.ThemeSetting = { fixed: false, height: 48, right: true + }, + watermark: { + visible: true, + text: 'Snail Job' } }; diff --git a/src/typings/app.d.ts b/src/typings/app.d.ts index 40c174b..5e28fc7 100644 --- a/src/typings/app.d.ts +++ b/src/typings/app.d.ts @@ -97,6 +97,13 @@ declare namespace App { /** Whether float the footer to the right when the layout is 'horizontal-mix' */ right: boolean; }; + /** Watermark */ + watermark: { + /** Whether to show the watermark */ + visible: boolean; + /** WatermarkText */ + text: string; + }; } interface OtherColor { @@ -521,6 +528,10 @@ declare namespace App { resetConfig: string; resetSuccessMsg: string; }; + watermark: { + visible: string; + text: string; + }; }; route: Record; page: { diff --git a/src/utils/common.ts b/src/utils/common.ts index 9029fa3..fc28f14 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -220,3 +220,33 @@ export function stringToContent( return result as any; } + +/** + * 函数防抖 + * + * @param func 函数 + * @param wait 延迟毫秒数 + * @param immediate true/false (是/否)即执行 + */ +export function debounce(func: () => any, wait: number, immediate?: boolean) { + let timeout: any; + + return () => { + /* eslint-disable */ + // @ts-ignore + const context = this + const args: any = arguments + if (timeout) clearTimeout(timeout); + if (immediate) { + const callNow = !timeout; + timeout = setTimeout(() => { + timeout = null; + }, wait); + if (callNow) func.apply(context, args); + } else { + timeout = setTimeout(() => { + func.apply(context, args); + }, wait); + } + }; +}