feat(sj_1.1.0-beta3): 新增全局水印功能

This commit is contained in:
xlsea 2024-07-08 10:46:49 +08:00
parent 06fae444b4
commit 5f0c25e7b5
8 changed files with 222 additions and 1 deletions

View File

@ -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<HTMLElement | null>;
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<HTMLElement>);
const watermarkEl = shallowRef<HTMLElement>();
/** 绘制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 };
}

View File

@ -101,6 +101,12 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
>
<NSwitch v-model:value="themeStore.footer.right" />
</SettingItem>
<SettingItem key="8" :label="$t('theme.watermark.visible')">
<NSwitch v-model:value="themeStore.watermark.visible" />
</SettingItem>
<SettingItem key="8-1" :label="$t('theme.watermark.text')">
<NInput v-model:value="themeStore.watermark.text" size="small" :step="1" class="w-120px" />
</SettingItem>
</TransitionGroup>
</template>

View File

@ -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: {

View File

@ -301,6 +301,10 @@ const local: App.I18n.Schema = {
copySuccessMsg: '复制成功,请替换 src/theme/settings.ts 中的变量 themeSettings',
resetConfig: '重置配置',
resetSuccessMsg: '重置成功'
},
watermark: {
visible: '开启',
text: '水印文字'
}
},
route: {

View File

@ -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
};
});

View File

@ -46,6 +46,10 @@ export const themeSettings: App.Theme.ThemeSetting = {
fixed: false,
height: 48,
right: true
},
watermark: {
visible: true,
text: 'Snail Job'
}
};

11
src/typings/app.d.ts vendored
View File

@ -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<I18nRouteKey, string>;
page: {

View File

@ -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);
}
};
}