mirror of
				https://github.com/pure-admin/vue-pure-admin.git
				synced 2025-11-03 13:44:47 +08:00 
			
		
		
		
	Merge branch 'main' into gitee
This commit is contained in:
		
						commit
						87a2af7181
					
				@ -90,6 +90,7 @@ menus:
 | 
			
		||||
  hsMenuTree: Menu Tree
 | 
			
		||||
  hsVideoFrame: Video Frame Capture
 | 
			
		||||
  hsWavesurfer: Audio Visualization
 | 
			
		||||
  hsRipple: Ripple
 | 
			
		||||
  hsOptimize: Debounce、Throttle、Copy、Longpress Directives
 | 
			
		||||
  hsWatermark: Water Mark
 | 
			
		||||
  hsPrint: Print
 | 
			
		||||
 | 
			
		||||
@ -90,6 +90,7 @@ menus:
 | 
			
		||||
  hsMenuTree: 菜单树结构
 | 
			
		||||
  hsVideoFrame: 视频帧截取-wasm版
 | 
			
		||||
  hsWavesurfer: 音频可视化
 | 
			
		||||
  hsRipple: 波纹(Ripple)
 | 
			
		||||
  hsOptimize: 防抖、截流、复制、长按指令
 | 
			
		||||
  hsWatermark: 水印
 | 
			
		||||
  hsPrint: 打印
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								package.json
									
									
									
									
									
								
							@ -49,7 +49,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@amap/amap-jsapi-loader": "^1.0.1",
 | 
			
		||||
    "@howdyjs/mouse-menu": "2.0.9",
 | 
			
		||||
    "@howdyjs/mouse-menu": "^2.1.3",
 | 
			
		||||
    "@logicflow/core": "^1.2.22",
 | 
			
		||||
    "@logicflow/extension": "^1.2.22",
 | 
			
		||||
    "@pureadmin/descriptions": "^1.2.0",
 | 
			
		||||
@ -78,7 +78,7 @@
 | 
			
		||||
    "path": "^0.12.7",
 | 
			
		||||
    "pinia": "^2.1.7",
 | 
			
		||||
    "pinyin-pro": "^3.19.6",
 | 
			
		||||
    "plus-pro-components": "^0.0.1",
 | 
			
		||||
    "plus-pro-components": "^0.0.2",
 | 
			
		||||
    "qrcode": "^1.5.3",
 | 
			
		||||
    "qs": "^6.11.2",
 | 
			
		||||
    "responsive-storage": "^2.2.0",
 | 
			
		||||
@ -91,7 +91,7 @@
 | 
			
		||||
    "vue": "^3.4.21",
 | 
			
		||||
    "vue-i18n": "^9.10.1",
 | 
			
		||||
    "vue-json-pretty": "^2.3.0",
 | 
			
		||||
    "vue-pdf-embed": "1.2.1",
 | 
			
		||||
    "vue-pdf-embed": "^2.0.2",
 | 
			
		||||
    "vue-router": "^4.3.0",
 | 
			
		||||
    "vue-tippy": "^6.4.1",
 | 
			
		||||
    "vue-types": "^5.1.1",
 | 
			
		||||
@ -122,11 +122,11 @@
 | 
			
		||||
    "@types/qrcode": "^1.5.5",
 | 
			
		||||
    "@types/qs": "^6.9.12",
 | 
			
		||||
    "@types/sortablejs": "^1.15.8",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^7.1.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^7.1.0",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^7.1.1",
 | 
			
		||||
    "@typescript-eslint/parser": "^7.1.1",
 | 
			
		||||
    "@vitejs/plugin-vue": "^5.0.4",
 | 
			
		||||
    "@vitejs/plugin-vue-jsx": "^3.1.0",
 | 
			
		||||
    "autoprefixer": "^10.4.17",
 | 
			
		||||
    "autoprefixer": "^10.4.18",
 | 
			
		||||
    "boxen": "^7.1.1",
 | 
			
		||||
    "cloc": "^2.11.0",
 | 
			
		||||
    "cssnano": "^6.0.5",
 | 
			
		||||
@ -147,14 +147,14 @@
 | 
			
		||||
    "rollup-plugin-visualizer": "^5.12.0",
 | 
			
		||||
    "sass": "^1.71.1",
 | 
			
		||||
    "stylelint": "^16.2.1",
 | 
			
		||||
    "stylelint-config-recess-order": "^4.6.0",
 | 
			
		||||
    "stylelint-config-recess-order": "^5.0.0",
 | 
			
		||||
    "stylelint-config-recommended-vue": "^1.5.0",
 | 
			
		||||
    "stylelint-config-standard-scss": "^13.0.0",
 | 
			
		||||
    "stylelint-prettier": "^5.0.0",
 | 
			
		||||
    "svgo": "^3.2.0",
 | 
			
		||||
    "tailwindcss": "^3.4.1",
 | 
			
		||||
    "typescript": "^5.3.3",
 | 
			
		||||
    "vite": "^5.1.4",
 | 
			
		||||
    "vite": "^5.1.5",
 | 
			
		||||
    "vite-plugin-cdn-import": "^0.3.5",
 | 
			
		||||
    "vite-plugin-compression": "^0.5.1",
 | 
			
		||||
    "vite-plugin-fake-server": "^2.1.1",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										501
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										501
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -2,3 +2,4 @@ export * from "./auth";
 | 
			
		||||
export * from "./copy";
 | 
			
		||||
export * from "./longpress";
 | 
			
		||||
export * from "./optimize";
 | 
			
		||||
export * from "./ripple";
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								src/directives/ripple/index.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/directives/ripple/index.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
			
		||||
/* stylelint-disable-next-line scss/dollar-variable-colon-space-after */
 | 
			
		||||
$ripple-animation-transition-in:
 | 
			
		||||
  transform 0.4s cubic-bezier(0, 0, 0.2, 1),
 | 
			
		||||
  opacity 0.2s cubic-bezier(0, 0, 0.2, 1) !default;
 | 
			
		||||
$ripple-animation-transition-out: opacity 0.5s cubic-bezier(0, 0, 0.2, 1) !default;
 | 
			
		||||
$ripple-animation-visible-opacity: 0.25 !default;
 | 
			
		||||
 | 
			
		||||
.v-ripple {
 | 
			
		||||
  &__container {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    z-index: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    border-radius: inherit;
 | 
			
		||||
    contain: strict;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__animation {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    background: currentcolor;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    will-change: transform, opacity;
 | 
			
		||||
 | 
			
		||||
    &--enter {
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
      transition: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--in {
 | 
			
		||||
      opacity: $ripple-animation-visible-opacity;
 | 
			
		||||
      transition: $ripple-animation-transition-in;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--out {
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
      transition: $ripple-animation-transition-out;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										234
									
								
								src/directives/ripple/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								src/directives/ripple/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,234 @@
 | 
			
		||||
import "./index.scss";
 | 
			
		||||
import { isObject } from "@pureadmin/utils";
 | 
			
		||||
import type { Directive, DirectiveBinding } from "vue";
 | 
			
		||||
 | 
			
		||||
interface RippleOptions {
 | 
			
		||||
  class?: string;
 | 
			
		||||
  center?: boolean;
 | 
			
		||||
  circle?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RippleDirectiveBinding
 | 
			
		||||
  extends Omit<DirectiveBinding, "modifiers" | "value"> {
 | 
			
		||||
  value?: boolean | { class: string };
 | 
			
		||||
  modifiers: {
 | 
			
		||||
    center?: boolean;
 | 
			
		||||
    circle?: boolean;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function transform(el: HTMLElement, value: string) {
 | 
			
		||||
  el.style.transform = value;
 | 
			
		||||
  el.style.webkitTransform = value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const calculate = (
 | 
			
		||||
  e: PointerEvent,
 | 
			
		||||
  el: HTMLElement,
 | 
			
		||||
  value: RippleOptions = {}
 | 
			
		||||
) => {
 | 
			
		||||
  const offset = el.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
  // 获取点击位置距离 el 的垂直和水平距离
 | 
			
		||||
  let localX = e.clientX - offset.left;
 | 
			
		||||
  let localY = e.clientY - offset.top;
 | 
			
		||||
 | 
			
		||||
  let radius = 0;
 | 
			
		||||
  let scale = 0.3;
 | 
			
		||||
  // 计算点击位置到 el 顶点最远距离,即为圆的最大半径(勾股定理)
 | 
			
		||||
  if (el._ripple?.circle) {
 | 
			
		||||
    scale = 0.15;
 | 
			
		||||
    radius = el.clientWidth / 2;
 | 
			
		||||
    radius = value.center
 | 
			
		||||
      ? radius
 | 
			
		||||
      : radius + Math.sqrt((localX - radius) ** 2 + (localY - radius) ** 2) / 4;
 | 
			
		||||
  } else {
 | 
			
		||||
    radius = Math.sqrt(el.clientWidth ** 2 + el.clientHeight ** 2) / 2;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 中心点坐标
 | 
			
		||||
  const centerX = `${(el.clientWidth - radius * 2) / 2}px`;
 | 
			
		||||
  const centerY = `${(el.clientHeight - radius * 2) / 2}px`;
 | 
			
		||||
 | 
			
		||||
  // 点击位置坐标
 | 
			
		||||
  const x = value.center ? centerX : `${localX - radius}px`;
 | 
			
		||||
  const y = value.center ? centerY : `${localY - radius}px`;
 | 
			
		||||
 | 
			
		||||
  return { radius, scale, x, y, centerX, centerY };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ripples = {
 | 
			
		||||
  show(e: PointerEvent, el: HTMLElement, value: RippleOptions = {}) {
 | 
			
		||||
    if (!el?._ripple?.enabled) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 创建 ripple 元素和 ripple 父元素
 | 
			
		||||
    const container = document.createElement("span");
 | 
			
		||||
    const animation = document.createElement("span");
 | 
			
		||||
 | 
			
		||||
    container.appendChild(animation);
 | 
			
		||||
    container.className = "v-ripple__container";
 | 
			
		||||
 | 
			
		||||
    if (value.class) {
 | 
			
		||||
      container.className += ` ${value.class}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { radius, scale, x, y, centerX, centerY } = calculate(e, el, value);
 | 
			
		||||
 | 
			
		||||
    // ripple 圆大小
 | 
			
		||||
    const size = `${radius * 2}px`;
 | 
			
		||||
 | 
			
		||||
    animation.className = "v-ripple__animation";
 | 
			
		||||
    animation.style.width = size;
 | 
			
		||||
    animation.style.height = size;
 | 
			
		||||
 | 
			
		||||
    el.appendChild(container);
 | 
			
		||||
 | 
			
		||||
    // 获取目标元素样式表
 | 
			
		||||
    const computed = window.getComputedStyle(el);
 | 
			
		||||
    // 防止 position 被覆盖导致 ripple 位置有问题
 | 
			
		||||
    if (computed && computed.position === "static") {
 | 
			
		||||
      el.style.position = "relative";
 | 
			
		||||
      el.dataset.previousPosition = "static";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    animation.classList.add("v-ripple__animation--enter");
 | 
			
		||||
    animation.classList.add("v-ripple__animation--visible");
 | 
			
		||||
    transform(
 | 
			
		||||
      animation,
 | 
			
		||||
      `translate(${x}, ${y}) scale3d(${scale},${scale},${scale})`
 | 
			
		||||
    );
 | 
			
		||||
    animation.dataset.activated = String(performance.now());
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      animation.classList.remove("v-ripple__animation--enter");
 | 
			
		||||
      animation.classList.add("v-ripple__animation--in");
 | 
			
		||||
      transform(animation, `translate(${centerX}, ${centerY}) scale3d(1,1,1)`);
 | 
			
		||||
    }, 0);
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  hide(el: HTMLElement | null) {
 | 
			
		||||
    if (!el?._ripple?.enabled) return;
 | 
			
		||||
 | 
			
		||||
    const ripples = el.getElementsByClassName("v-ripple__animation");
 | 
			
		||||
 | 
			
		||||
    if (ripples.length === 0) return;
 | 
			
		||||
    const animation = ripples[ripples.length - 1] as HTMLElement;
 | 
			
		||||
 | 
			
		||||
    if (animation.dataset.isHiding) return;
 | 
			
		||||
    else animation.dataset.isHiding = "true";
 | 
			
		||||
 | 
			
		||||
    const diff = performance.now() - Number(animation.dataset.activated);
 | 
			
		||||
    const delay = Math.max(250 - diff, 0);
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      animation.classList.remove("v-ripple__animation--in");
 | 
			
		||||
      animation.classList.add("v-ripple__animation--out");
 | 
			
		||||
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        const ripples = el.getElementsByClassName("v-ripple__animation");
 | 
			
		||||
        if (ripples.length === 1 && el.dataset.previousPosition) {
 | 
			
		||||
          el.style.position = el.dataset.previousPosition;
 | 
			
		||||
          delete el.dataset.previousPosition;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (animation.parentNode?.parentNode === el)
 | 
			
		||||
          el.removeChild(animation.parentNode);
 | 
			
		||||
      }, 300);
 | 
			
		||||
    }, delay);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function isRippleEnabled(value: any): value is true {
 | 
			
		||||
  return typeof value === "undefined" || !!value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function rippleShow(e: PointerEvent) {
 | 
			
		||||
  const value: RippleOptions = {};
 | 
			
		||||
  const element = e.currentTarget as HTMLElement | undefined;
 | 
			
		||||
 | 
			
		||||
  if (!element?._ripple || element._ripple.touched) return;
 | 
			
		||||
 | 
			
		||||
  value.center = element._ripple.centered;
 | 
			
		||||
  if (element._ripple.class) {
 | 
			
		||||
    value.class = element._ripple.class;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ripples.show(e, element, value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function rippleHide(e: Event) {
 | 
			
		||||
  const element = e.currentTarget as HTMLElement | null;
 | 
			
		||||
  if (!element?._ripple) return;
 | 
			
		||||
 | 
			
		||||
  window.setTimeout(() => {
 | 
			
		||||
    if (element._ripple) {
 | 
			
		||||
      element._ripple.touched = false;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  ripples.hide(element);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateRipple(
 | 
			
		||||
  el: HTMLElement,
 | 
			
		||||
  binding: RippleDirectiveBinding,
 | 
			
		||||
  wasEnabled: boolean
 | 
			
		||||
) {
 | 
			
		||||
  const { value, modifiers } = binding;
 | 
			
		||||
  const enabled = isRippleEnabled(value);
 | 
			
		||||
  if (!enabled) {
 | 
			
		||||
    ripples.hide(el);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  el._ripple = el._ripple ?? {};
 | 
			
		||||
  el._ripple.enabled = enabled;
 | 
			
		||||
  el._ripple.centered = modifiers.center;
 | 
			
		||||
  el._ripple.circle = modifiers.circle;
 | 
			
		||||
  if (isObject(value) && value.class) {
 | 
			
		||||
    el._ripple.class = value.class;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (enabled && !wasEnabled) {
 | 
			
		||||
    el.addEventListener("pointerdown", rippleShow);
 | 
			
		||||
    el.addEventListener("pointerup", rippleHide);
 | 
			
		||||
  } else if (!enabled && wasEnabled) {
 | 
			
		||||
    removeListeners(el);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeListeners(el: HTMLElement) {
 | 
			
		||||
  el.removeEventListener("pointerdown", rippleShow);
 | 
			
		||||
  el.removeEventListener("pointerup", rippleHide);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function mounted(el: HTMLElement, binding: RippleDirectiveBinding) {
 | 
			
		||||
  updateRipple(el, binding, false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function unmounted(el: HTMLElement) {
 | 
			
		||||
  delete el._ripple;
 | 
			
		||||
  removeListeners(el);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updated(el: HTMLElement, binding: RippleDirectiveBinding) {
 | 
			
		||||
  if (binding.value === binding.oldValue) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const wasEnabled = isRippleEnabled(binding.oldValue);
 | 
			
		||||
  updateRipple(el, binding, wasEnabled);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @description 指令 v-ripple
 | 
			
		||||
 * @use 用法如下
 | 
			
		||||
 * 1. v-ripple 代表启用基本的 ripple 功能
 | 
			
		||||
 * 2. v-ripple="{ class: 'text-red' }" 代表自定义 ripple 颜色,支持 tailwindcss,生效样式是 color
 | 
			
		||||
 * 3. v-ripple.center 代表从中心扩散
 | 
			
		||||
 */
 | 
			
		||||
export const Ripple: Directive = {
 | 
			
		||||
  mounted,
 | 
			
		||||
  unmounted,
 | 
			
		||||
  updated
 | 
			
		||||
};
 | 
			
		||||
@ -42,6 +42,15 @@ export default {
 | 
			
		||||
        title: $t("menus.hsExcel")
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: "/components/ripple",
 | 
			
		||||
      name: "Ripple",
 | 
			
		||||
      component: () => import("@/views/able/ripple.vue"),
 | 
			
		||||
      meta: {
 | 
			
		||||
        title: $t("menus.hsRipple"),
 | 
			
		||||
        extraIcon: "IF-pure-iconfont-new svg"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: "/able/debounce",
 | 
			
		||||
      name: "Debounce",
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 全局覆盖element-plus的el-dialog、el-drawer、el-message-box、el-notification组件右上角关闭图标和el-upload上传文件列表右侧关闭图标的样式,表现更鲜明 */
 | 
			
		||||
/* 全局覆盖element-plus的el-tour、el-dialog、el-drawer、el-message-box、el-notification组件右上角关闭图标和el-upload上传文件列表右侧关闭图标的样式,表现更鲜明 */
 | 
			
		||||
.el-dialog__headerbtn,
 | 
			
		||||
.el-message-box__headerbtn {
 | 
			
		||||
  &:hover {
 | 
			
		||||
@ -75,6 +75,7 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-icon {
 | 
			
		||||
  &.el-tour__close,
 | 
			
		||||
  &.el-dialog__close,
 | 
			
		||||
  &.el-drawer__close,
 | 
			
		||||
  &.el-message-box__close,
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ const source =
 | 
			
		||||
 | 
			
		||||
const handleDocumentRender = () => {
 | 
			
		||||
  loading.value = false;
 | 
			
		||||
  pageCount.value = pdfRef.value.pageCount;
 | 
			
		||||
  pageCount.value = pdfRef.value.doc.numPages;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showAllPagesChange = () => {
 | 
			
		||||
@ -29,6 +29,7 @@ const showAllPagesChange = () => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onPrint = () => {
 | 
			
		||||
  // 如果在打印时,打印页面是本身的两倍,在打印配置 页面 设置 仅限页码为奇数的页面 即可
 | 
			
		||||
  pdfRef.value.print();
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										71
									
								
								src/views/able/ripple.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/views/able/ripple.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: "Ripple"
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <el-card shadow="never">
 | 
			
		||||
    <template #header>
 | 
			
		||||
      <div class="font-medium">波纹(Ripple)</div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <div class="mb-5">组件中的波纹</div>
 | 
			
		||||
    <el-alert
 | 
			
		||||
      title="v-ripple在某些组件中使用波纹特效会异常,这是因为v-ripple指令只能作用于当前元素,某些组件有多层元素嵌套,且目标元素没在顶层,所以会导致特效异常"
 | 
			
		||||
      type="warning"
 | 
			
		||||
      :closable="false"
 | 
			
		||||
    />
 | 
			
		||||
    <el-space wrap class="my-5">
 | 
			
		||||
      <el-button v-ripple>Default</el-button>
 | 
			
		||||
      <el-button v-ripple type="primary">Primary</el-button>
 | 
			
		||||
      <el-button v-ripple type="success">Success</el-button>
 | 
			
		||||
      <el-button v-ripple type="info">Info</el-button>
 | 
			
		||||
      <el-button v-ripple type="warning">Warning</el-button>
 | 
			
		||||
      <el-button v-ripple type="danger">Danger</el-button>
 | 
			
		||||
    </el-space>
 | 
			
		||||
    <el-card v-ripple class="mb-5 w-[510px] select-none" shadow="hover">
 | 
			
		||||
      卡片
 | 
			
		||||
    </el-card>
 | 
			
		||||
 | 
			
		||||
    <div class="mb-5">
 | 
			
		||||
      只要在组件或HTML元素上使用v-ripple指令,就可以启用基本的ripple功能
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
      v-ripple
 | 
			
		||||
      class="mb-5 text-center shadow-md rounded-md p-8 text-lg select-none"
 | 
			
		||||
    >
 | 
			
		||||
      HTML元素
 | 
			
		||||
    </div>
 | 
			
		||||
    <span
 | 
			
		||||
      v-ripple
 | 
			
		||||
      class="inline-block shadow-md rounded-md p-8 text-lg select-none"
 | 
			
		||||
    >
 | 
			
		||||
      行内元素需要添加display: block或display: inline-block才能生效
 | 
			
		||||
    </span>
 | 
			
		||||
 | 
			
		||||
    <div class="my-5">
 | 
			
		||||
      当使用v-ripple.center时,将始终从目标的中心处产生波纹
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
      v-ripple.center
 | 
			
		||||
      class="mb-5 text-center shadow-md rounded-md p-8 text-lg select-none"
 | 
			
		||||
    >
 | 
			
		||||
      始终从中心触发波纹
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="mb-5">
 | 
			
		||||
      使用v-ripple="{ class: '' }"添加类来自定义波纹颜色,支持tailwindcss
 | 
			
		||||
    </div>
 | 
			
		||||
    <el-alert
 | 
			
		||||
      title="自定义样式生效为文字颜色,例如:color: 'red';"
 | 
			
		||||
      type="warning"
 | 
			
		||||
      :closable="false"
 | 
			
		||||
    />
 | 
			
		||||
    <div
 | 
			
		||||
      v-ripple="{ class: 'text-red-500' }"
 | 
			
		||||
      class="my-5 text-center shadow-md rounded-md p-4 text-lg select-none"
 | 
			
		||||
    >
 | 
			
		||||
      自定义波纹颜色
 | 
			
		||||
    </div>
 | 
			
		||||
  </el-card>
 | 
			
		||||
</template>
 | 
			
		||||
@ -75,7 +75,9 @@ Object.keys(devDependencies).forEach(key => {
 | 
			
		||||
          <span class="font-medium">平台信息</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
      <PureDescriptions border :columns="columns" :column="4" />
 | 
			
		||||
      <el-scrollbar>
 | 
			
		||||
        <PureDescriptions border :columns="columns" :column="4" />
 | 
			
		||||
      </el-scrollbar>
 | 
			
		||||
    </el-card>
 | 
			
		||||
 | 
			
		||||
    <el-card class="m-4 box-card" shadow="never">
 | 
			
		||||
@ -84,28 +86,30 @@ Object.keys(devDependencies).forEach(key => {
 | 
			
		||||
          <span class="font-medium">生产环境依赖</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
      <el-descriptions border size="small" :column="6">
 | 
			
		||||
        <el-descriptions-item
 | 
			
		||||
          v-for="(item, index) in schema"
 | 
			
		||||
          :key="index"
 | 
			
		||||
          :label="item.label"
 | 
			
		||||
          :label-class-name="getMainLabel(item.label)"
 | 
			
		||||
          class-name="pure-version"
 | 
			
		||||
          label-align="right"
 | 
			
		||||
        >
 | 
			
		||||
          <a
 | 
			
		||||
            :href="'https://www.npmjs.com/package/' + item.label"
 | 
			
		||||
            target="_blank"
 | 
			
		||||
      <el-scrollbar>
 | 
			
		||||
        <el-descriptions border size="small" :column="6">
 | 
			
		||||
          <el-descriptions-item
 | 
			
		||||
            v-for="(item, index) in schema"
 | 
			
		||||
            :key="index"
 | 
			
		||||
            :label="item.label"
 | 
			
		||||
            :label-class-name="getMainLabel(item.label)"
 | 
			
		||||
            class-name="pure-version"
 | 
			
		||||
            label-align="right"
 | 
			
		||||
          >
 | 
			
		||||
            <span
 | 
			
		||||
              :class="getMainLabel(item.label)"
 | 
			
		||||
              style="color: var(--el-color-primary)"
 | 
			
		||||
            <a
 | 
			
		||||
              :href="'https://www.npmjs.com/package/' + item.label"
 | 
			
		||||
              target="_blank"
 | 
			
		||||
            >
 | 
			
		||||
              {{ item.field }}
 | 
			
		||||
            </span>
 | 
			
		||||
          </a>
 | 
			
		||||
        </el-descriptions-item>
 | 
			
		||||
      </el-descriptions>
 | 
			
		||||
              <span
 | 
			
		||||
                :class="getMainLabel(item.label)"
 | 
			
		||||
                style="color: var(--el-color-primary)"
 | 
			
		||||
              >
 | 
			
		||||
                {{ item.field }}
 | 
			
		||||
              </span>
 | 
			
		||||
            </a>
 | 
			
		||||
          </el-descriptions-item>
 | 
			
		||||
        </el-descriptions>
 | 
			
		||||
      </el-scrollbar>
 | 
			
		||||
    </el-card>
 | 
			
		||||
 | 
			
		||||
    <el-card class="m-4 box-card" shadow="never">
 | 
			
		||||
@ -114,28 +118,30 @@ Object.keys(devDependencies).forEach(key => {
 | 
			
		||||
          <span class="font-medium">开发环境依赖</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
      <el-descriptions border size="small" :column="5">
 | 
			
		||||
        <el-descriptions-item
 | 
			
		||||
          v-for="(item, index) in devSchema"
 | 
			
		||||
          :key="index"
 | 
			
		||||
          :label="item.label"
 | 
			
		||||
          :label-class-name="getMainLabel(item.label)"
 | 
			
		||||
          class-name="pure-version"
 | 
			
		||||
          label-align="right"
 | 
			
		||||
        >
 | 
			
		||||
          <a
 | 
			
		||||
            :href="'https://www.npmjs.com/package/' + item.label"
 | 
			
		||||
            target="_blank"
 | 
			
		||||
      <el-scrollbar>
 | 
			
		||||
        <el-descriptions border size="small" :column="5">
 | 
			
		||||
          <el-descriptions-item
 | 
			
		||||
            v-for="(item, index) in devSchema"
 | 
			
		||||
            :key="index"
 | 
			
		||||
            :label="item.label"
 | 
			
		||||
            :label-class-name="getMainLabel(item.label)"
 | 
			
		||||
            class-name="pure-version"
 | 
			
		||||
            label-align="right"
 | 
			
		||||
          >
 | 
			
		||||
            <span
 | 
			
		||||
              :class="getMainLabel(item.label)"
 | 
			
		||||
              style="color: var(--el-color-primary)"
 | 
			
		||||
            <a
 | 
			
		||||
              :href="'https://www.npmjs.com/package/' + item.label"
 | 
			
		||||
              target="_blank"
 | 
			
		||||
            >
 | 
			
		||||
              {{ item.field }}
 | 
			
		||||
            </span>
 | 
			
		||||
          </a>
 | 
			
		||||
        </el-descriptions-item>
 | 
			
		||||
      </el-descriptions>
 | 
			
		||||
              <span
 | 
			
		||||
                :class="getMainLabel(item.label)"
 | 
			
		||||
                style="color: var(--el-color-primary)"
 | 
			
		||||
              >
 | 
			
		||||
                {{ item.field }}
 | 
			
		||||
              </span>
 | 
			
		||||
            </a>
 | 
			
		||||
          </el-descriptions-item>
 | 
			
		||||
        </el-descriptions>
 | 
			
		||||
      </el-scrollbar>
 | 
			
		||||
    </el-card>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@ -1,63 +1,75 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import intro from "intro.js";
 | 
			
		||||
import "intro.js/minified/introjs.min.css";
 | 
			
		||||
 | 
			
		||||
type GuideStep = {
 | 
			
		||||
  element: string | HTMLElement;
 | 
			
		||||
  title: string;
 | 
			
		||||
  intro: string;
 | 
			
		||||
  position: "left" | "right" | "top" | "bottom";
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: "Guide"
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const GUIDE_STEPS = [
 | 
			
		||||
  {
 | 
			
		||||
    element: document.querySelector(".sidebar-logo-container") as
 | 
			
		||||
      | string
 | 
			
		||||
      | HTMLElement,
 | 
			
		||||
    title: "项目名称和Logo",
 | 
			
		||||
    intro: "您可以在这里设置项目名称和Logo",
 | 
			
		||||
    position: "left"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    element: document.querySelector("#header-search") as string | HTMLElement,
 | 
			
		||||
    title: "搜索菜单",
 | 
			
		||||
    intro: "您可以在这里搜索想要查看的菜单",
 | 
			
		||||
    position: "left"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    element: document.querySelector("#header-notice") as string | HTMLElement,
 | 
			
		||||
    title: "消息通知",
 | 
			
		||||
    intro: "您可以在这里查看管理员发送的消息",
 | 
			
		||||
    position: "left"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    element: document.querySelector("#header-translation") as
 | 
			
		||||
      | string
 | 
			
		||||
      | HTMLElement,
 | 
			
		||||
    title: "国际化",
 | 
			
		||||
    intro: "您可以在这里进行语言切换",
 | 
			
		||||
    position: "left"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    element: document.querySelector(".set-icon") as string | HTMLElement,
 | 
			
		||||
    title: "项目配置",
 | 
			
		||||
    intro: "您可以在这里查看项目配置",
 | 
			
		||||
    position: "left"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    element: document.querySelector(".tags-view") as string | HTMLElement,
 | 
			
		||||
    title: "多标签页",
 | 
			
		||||
    intro: "这里是您访问过的页面的历史",
 | 
			
		||||
    position: "bottom"
 | 
			
		||||
  }
 | 
			
		||||
] as Partial<GuideStep>[];
 | 
			
		||||
 | 
			
		||||
const tourOpen = ref(false);
 | 
			
		||||
 | 
			
		||||
const onGuide = () => {
 | 
			
		||||
  intro()
 | 
			
		||||
    .setOptions({
 | 
			
		||||
      steps: [
 | 
			
		||||
        {
 | 
			
		||||
          element: document.querySelector(".sidebar-logo-container") as
 | 
			
		||||
            | string
 | 
			
		||||
            | HTMLElement,
 | 
			
		||||
          title: "项目名称和Logo",
 | 
			
		||||
          intro: "您可以在这里设置项目名称和Logo",
 | 
			
		||||
          position: "left"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          element: document.querySelector("#header-search") as
 | 
			
		||||
            | string
 | 
			
		||||
            | HTMLElement,
 | 
			
		||||
          title: "搜索菜单",
 | 
			
		||||
          intro: "您可以在这里搜索想要查看的菜单",
 | 
			
		||||
          position: "left"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          element: document.querySelector("#header-notice") as
 | 
			
		||||
            | string
 | 
			
		||||
            | HTMLElement,
 | 
			
		||||
          title: "消息通知",
 | 
			
		||||
          intro: "您可以在这里查看管理员发送的消息",
 | 
			
		||||
          position: "left"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          element: document.querySelector("#header-translation") as
 | 
			
		||||
            | string
 | 
			
		||||
            | HTMLElement,
 | 
			
		||||
          title: "国际化",
 | 
			
		||||
          intro: "您可以在这里进行语言切换",
 | 
			
		||||
          position: "left"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          element: document.querySelector(".set-icon") as string | HTMLElement,
 | 
			
		||||
          title: "项目配置",
 | 
			
		||||
          intro: "您可以在这里查看项目配置",
 | 
			
		||||
          position: "left"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          element: document.querySelector(".tags-view") as string | HTMLElement,
 | 
			
		||||
          title: "多标签页",
 | 
			
		||||
          intro: "这里是您访问过的页面的历史",
 | 
			
		||||
          position: "bottom"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
      steps: GUIDE_STEPS
 | 
			
		||||
    })
 | 
			
		||||
    .start();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onTour = () => {
 | 
			
		||||
  tourOpen.value = true;
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
@ -69,6 +81,18 @@ const onGuide = () => {
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <el-button @click="onGuide"> 打开引导页 </el-button>
 | 
			
		||||
    <el-button @click="onGuide"> 打开引导页 (intro.js) </el-button>
 | 
			
		||||
    <el-button @click="onTour"> 打开引导页 (el-tour) </el-button>
 | 
			
		||||
 | 
			
		||||
    <el-tour v-model="tourOpen">
 | 
			
		||||
      <el-tour-step
 | 
			
		||||
        v-for="step in GUIDE_STEPS"
 | 
			
		||||
        :key="step.title"
 | 
			
		||||
        :target="() => step.element"
 | 
			
		||||
        :title="step.title"
 | 
			
		||||
        :description="step.intro"
 | 
			
		||||
        :placement="step.position"
 | 
			
		||||
      />
 | 
			
		||||
    </el-tour>
 | 
			
		||||
  </el-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								types/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								types/global.d.ts
									
									
									
									
										vendored
									
									
								
							@ -180,4 +180,18 @@ declare global {
 | 
			
		||||
    $storage: ResponsiveStorage;
 | 
			
		||||
    $config: PlatformConfigs;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 扩展 `Elemet`
 | 
			
		||||
   */
 | 
			
		||||
  interface Element {
 | 
			
		||||
    // v-ripple 作用于 src/directives/ripple/index.ts 文件
 | 
			
		||||
    _ripple?: {
 | 
			
		||||
      enabled?: boolean;
 | 
			
		||||
      centered?: boolean;
 | 
			
		||||
      class?: string;
 | 
			
		||||
      circle?: boolean;
 | 
			
		||||
      touched?: boolean;
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user