feat: watermark (#203)

* feat: add watermark
This commit is contained in:
啝裳 2022-03-01 10:44:26 +08:00 committed by GitHub
parent 6971ba6c53
commit d43316f7c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 209 additions and 49 deletions

View File

@ -41,7 +41,7 @@ const permissionRouter = {
title: "menus.permission",
icon: "lollipop",
i18n: true,
rank: 3
rank: 7
},
children: [
{
@ -72,7 +72,7 @@ const tabsRouter = {
icon: "IF-team-icontabs",
title: "menus.hstabs",
i18n: true,
rank: 8
rank: 10
},
children: [
{

View File

@ -52,10 +52,12 @@ import arrowRightSLine from "@iconify-icons/ri/arrow-right-s-line";
import arrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line";
import logoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import nodeTree from "@iconify-icons/ri/node-tree";
import ubuntuFill from "@iconify-icons/ri/ubuntu-fill";
addIcon("arrow-right-s-line", arrowRightSLine);
addIcon("arrow-left-s-line", arrowLeftSLine);
addIcon("logout-circle-r-line", logoutCircleRLine);
addIcon("node-tree", nodeTree);
addIcon("ubuntu-fill", ubuntuFill);
// Font Awesome 4
import faUser from "@iconify-icons/fa/user";

View File

@ -5,6 +5,7 @@ import { routeMetaType } from "../types";
import { transformI18n } from "/@/plugins/i18n";
import { storageSession } from "/@/utils/storage";
import { useAppStoreHook } from "/@/store/modules/app";
import { remainingPaths } from "/@/router/modules/index";
import { Title } from "../../../public/serverConfig.json";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { remainingPaths } from "/@/router/modules/index";

View File

@ -32,7 +32,9 @@ export default {
permissionPage: "Page Permission",
permissionButton: "Button Permission",
hstabs: "Tabs Operate",
hsMenuTree: "Menu Tree",
hsguide: "Guide",
externalLink: "External Link"
externalLink: "External Link",
hsAble: "Able",
hsMenuTree: "Menu Tree",
hsWatermark: "Water Mark"
};

View File

@ -32,7 +32,9 @@ export default {
permissionPage: "页面权限",
permissionButton: "按钮权限",
hstabs: "标签页操作",
hsMenuTree: "菜单树结构",
hsguide: "引导页",
externalLink: "外链"
externalLink: "外链",
hsAble: "功能",
hsMenuTree: "菜单树结构",
hsWatermark: "水印"
};

View File

@ -0,0 +1,37 @@
import { $t } from "/@/plugins/i18n";
const Layout = () => import("/@/layout/index.vue");
const ableRouter = {
path: "/able",
name: "components",
component: Layout,
redirect: "/able/menuTree",
meta: {
icon: "ubuntu-fill",
title: $t("menus.hsAble"),
i18n: true,
rank: 3
},
children: [
{
path: "/able/menuTree",
name: "reMenuTree",
component: () => import("/@/views/able/menu-tree.vue"),
meta: {
title: $t("menus.hsMenuTree"),
i18n: true
}
},
{
path: "/able/watermark",
name: "reWatermark",
component: () => import("/@/views/able/watermark.vue"),
meta: {
title: $t("menus.hsWatermark"),
i18n: true
}
}
]
};
export default ableRouter;

View File

@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const componentsRouter = {
path: "/components",

View File

@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const editorRouter = {
path: "/editor",

View File

@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const errorRouter = {
path: "/error",
@ -10,7 +10,7 @@ const errorRouter = {
icon: "position",
title: $t("menus.hserror"),
i18n: true,
rank: 7
rank: 9
},
children: [
{

View File

@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const externalLink = {
path: "/external",

View File

@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const flowChartRouter = {
path: "/flowChart",

View File

@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const guideRouter = {
path: "/guide",
@ -10,7 +10,7 @@ const guideRouter = {
icon: "guide",
title: $t("menus.hsguide"),
i18n: true,
rank: 10
rank: 11
},
children: [
{

View File

@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const homeRouter = {
path: "/",

View File

@ -1,10 +1,10 @@
// 静态路由
import homeRouter from "./home";
import ableRouter from "./able";
import errorRouter from "./error";
import guideRouter from "./guide";
import editorRouter from "./editor";
import nestedRouter from "./nested";
import menuTreeRouter from "./menuTree";
import externalLink from "./externalLink";
import flowChartRouter from "./flowchart";
import remainingRouter from "./remaining";
@ -21,12 +21,12 @@ import { buildHierarchyTree } from "/@/utils/tree";
// 原始静态路由(未做任何处理)
const routes = [
homeRouter,
ableRouter,
errorRouter,
guideRouter,
nestedRouter,
externalLink,
editorRouter,
menuTreeRouter,
flowChartRouter,
componentsRouter
];

View File

@ -1,28 +0,0 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const menuTreeRouter = {
path: "/menuTree",
name: "reMenuTree",
component: Layout,
redirect: "/menuTree/index",
meta: {
icon: "node-tree",
title: $t("menus.hsMenuTree"),
i18n: true,
rank: 9
},
children: [
{
path: "/menuTree/index",
name: "reMenuTree",
component: () => import("/@/views/menu-tree/index.vue"),
meta: {
title: $t("menus.hsMenuTree"),
i18n: true
}
}
]
};
export default menuTreeRouter;

View File

@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const nestedRouter = {
path: "/nested",
@ -10,7 +10,7 @@ const nestedRouter = {
title: $t("menus.hsmenus"),
icon: "histogram",
i18n: true,
rank: 5
rank: 8
},
children: [
{

View File

@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const remainingRouter = [
{

View File

@ -8,11 +8,11 @@ import {
} from "vue-router";
import { router } from "./index";
import { loadEnv } from "../../build";
import Layout from "/@/layout/index.vue";
import { useTimeoutFn } from "@vueuse/core";
import { RouteConfigs } from "/@/layout/types";
import { buildHierarchyTree } from "/@/utils/tree";
import { usePermissionStoreHook } from "/@/store/modules/permission";
const Layout = () => import("/@/layout/index.vue");
// https://cn.vitejs.dev/guide/features.html#glob-import
const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");

View File

@ -1,3 +1,5 @@
import type { FunctionArgs } from "@vueuse/core";
export const hasClass = (ele: RefType<any>, cls: string): any => {
return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
};
@ -40,3 +42,16 @@ export const toggleClass = (
className = className.replace(clsName, "");
targetEl.className = flag ? `${className} ${clsName} ` : className;
};
export function useRafThrottle<T extends FunctionArgs>(fn: T): T {
let locked = false;
// @ts-ignore
return function (...args) {
if (locked) return;
locked = true;
window.requestAnimationFrame(() => {
fn.apply(this, args);
locked = false;
});
};
}

116
src/utils/watermark.ts Normal file
View File

@ -0,0 +1,116 @@
import {
ref,
Ref,
unref,
shallowRef,
onBeforeUnmount,
getCurrentInstance
} from "vue";
import { isDef } from "/@/utils/is";
import { useRafThrottle } from "/@/utils/operate";
import { addResizeListener, removeResizeListener } from "/@/utils/resize";
const domSymbol = Symbol("watermark-dom");
type attr = {
font?: string;
fillStyle?: string;
};
export function useWatermark(
appendEl: Ref<HTMLElement | null> = ref(document.body) as Ref<HTMLElement>
) {
const func = useRafThrottle(function () {
const el = unref(appendEl);
if (!el) return;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ height, width });
});
const id = domSymbol.toString();
const watermarkEl = shallowRef<HTMLElement>();
const clear = () => {
const domId = unref(watermarkEl);
watermarkEl.value = undefined;
const el = unref(appendEl);
if (!el) return;
domId && el.removeChild(domId);
removeResizeListener(el, func);
};
function createBase64(str: string, attr?: attr) {
const can = document.createElement("canvas");
const width = 300;
const height = 240;
Object.assign(can, { width, height });
const cans = can.getContext("2d");
if (cans) {
cans.rotate((-20 * Math.PI) / 120);
cans.font = attr?.font ?? "15px Reggae One";
cans.fillStyle = attr?.fillStyle ?? "rgba(0, 0, 0, 0.15)";
cans.textAlign = "left";
cans.textBaseline = "middle";
cans.fillText(str, width / 20, height);
}
return can.toDataURL("image/png");
}
function updateWatermark(
options: {
width?: number;
height?: number;
str?: string;
attr?: attr;
} = {}
) {
const el = unref(watermarkEl);
if (!el) return;
if (isDef(options.width)) {
el.style.width = `${options.width}px`;
}
if (isDef(options.height)) {
el.style.height = `${options.height}px`;
}
if (isDef(options.str)) {
el.style.background = `url(${createBase64(
options.str,
options.attr
)}) left top repeat`;
}
}
const createWatermark = (str: string, attr?: attr) => {
if (unref(watermarkEl)) {
updateWatermark({ str, attr });
return id;
}
const div = document.createElement("div");
watermarkEl.value = div;
div.id = id;
div.style.pointerEvents = "none";
div.style.top = "0px";
div.style.left = "0px";
div.style.position = "absolute";
div.style.zIndex = "100000";
const el = unref(appendEl);
if (!el) return id;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ str, width, height, attr });
el.appendChild(div);
return id;
};
function setWatermark(str: string, attr?: attr) {
createWatermark(str, attr);
addResizeListener(document.documentElement, func);
const instance = getCurrentInstance();
if (instance) {
onBeforeUnmount(() => {
clear();
});
}
}
return { setWatermark, clear };
}

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import { useWatermark } from "/@/utils/watermark";
const { setWatermark, clear } = useWatermark();
</script>
<template>
<div>
<el-button @click="setWatermark('vue-pure-admin')">创建</el-button>
<el-button @click="clear">清除</el-button>
</div>
</template>
<style scoped></style>