feat(sj_1.1.0-beta3): 新增全局水印功能
This commit is contained in:
parent
06fae444b4
commit
5f0c25e7b5
130
src/hooks/common/watermark.ts
Normal file
130
src/hooks/common/watermark.ts
Normal 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 };
|
||||
}
|
@ -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>
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -301,6 +301,10 @@ const local: App.I18n.Schema = {
|
||||
copySuccessMsg: '复制成功,请替换 src/theme/settings.ts 中的变量 themeSettings',
|
||||
resetConfig: '重置配置',
|
||||
resetSuccessMsg: '重置成功'
|
||||
},
|
||||
watermark: {
|
||||
visible: '开启',
|
||||
text: '水印文字'
|
||||
}
|
||||
},
|
||||
route: {
|
||||
|
@ -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
|
||||
};
|
||||
});
|
||||
|
@ -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
11
src/typings/app.d.ts
vendored
@ -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: {
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user