Procházet zdrojové kódy

feat: watermark (#203)

* feat: add watermark
啝裳 před 3 roky
rodič
revize
d43316f7c9

+ 2 - 2
mock/asyncRoutes.ts

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

+ 2 - 0
src/components/ReIcon/src/iconifyIconOffline.ts

@@ -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";

+ 1 - 0
src/layout/hooks/nav.ts

@@ -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";

+ 4 - 2
src/plugins/i18n/en/menus.ts

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

+ 4 - 2
src/plugins/i18n/zh-CN/menus.ts

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

+ 37 - 0
src/router/modules/able.ts

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

+ 1 - 1
src/router/modules/components.ts

@@ -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",

+ 1 - 1
src/router/modules/editor.ts

@@ -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",

+ 2 - 2
src/router/modules/error.ts

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

+ 1 - 1
src/router/modules/externalLink.ts

@@ -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",

+ 1 - 1
src/router/modules/flowchart.ts

@@ -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",

+ 2 - 2
src/router/modules/guide.ts

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

+ 1 - 1
src/router/modules/home.ts

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

+ 2 - 2
src/router/modules/index.ts

@@ -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
 ];

+ 0 - 28
src/router/modules/menuTree.ts

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

+ 2 - 2
src/router/modules/nested.ts

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

+ 1 - 1
src/router/modules/remaining.ts

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

+ 1 - 1
src/router/utils.ts

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

+ 15 - 0
src/utils/operate/index.ts

@@ -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 - 0
src/utils/watermark.ts

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

+ 0 - 0
src/views/menu-tree/index.vue → src/views/able/menu-tree.vue


+ 13 - 0
src/views/able/watermark.vue

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