🎉
This commit is contained in:
15
web/src/directives/auth/index.ts
Normal file
15
web/src/directives/auth/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { hasAuth } from "@/router/utils";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
export const auth: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding<string | Array<string>>) {
|
||||
const { value } = binding;
|
||||
if (value) {
|
||||
!hasAuth(value) && el.parentNode?.removeChild(el);
|
||||
} else {
|
||||
throw new Error(
|
||||
"[Directive: auth]: need auths! Like v-auth=\"['btn.add','btn.edit']\""
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
33
web/src/directives/copy/index.ts
Normal file
33
web/src/directives/copy/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { message } from "@/utils/message";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import { copyTextToClipboard } from "@pureadmin/utils";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
export interface CopyEl extends HTMLElement {
|
||||
copyValue: string;
|
||||
}
|
||||
|
||||
/** 文本复制指令(默认双击复制) */
|
||||
export const copy: Directive = {
|
||||
mounted(el: CopyEl, binding: DirectiveBinding<string>) {
|
||||
const { value } = binding;
|
||||
if (value) {
|
||||
el.copyValue = value;
|
||||
const arg = binding.arg ?? "dblclick";
|
||||
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
|
||||
useEventListener(el, arg, () => {
|
||||
const success = copyTextToClipboard(el.copyValue);
|
||||
success
|
||||
? message("复制成功", { type: "success" })
|
||||
: message("复制失败", { type: "error" });
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
'[Directive: copy]: need value! Like v-copy="modelValue"'
|
||||
);
|
||||
}
|
||||
},
|
||||
updated(el: CopyEl, binding: DirectiveBinding) {
|
||||
el.copyValue = binding.value;
|
||||
}
|
||||
};
|
5
web/src/directives/index.ts
Normal file
5
web/src/directives/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./auth";
|
||||
export * from "./copy";
|
||||
export * from "./longpress";
|
||||
export * from "./optimize";
|
||||
export * from "./ripple";
|
63
web/src/directives/longpress/index.ts
Normal file
63
web/src/directives/longpress/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
import { subBefore, subAfter, isFunction } from "@pureadmin/utils";
|
||||
|
||||
export const longpress: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding<Function>) {
|
||||
const cb = binding.value;
|
||||
if (cb && isFunction(cb)) {
|
||||
let timer = null;
|
||||
let interTimer = null;
|
||||
let num = 500;
|
||||
let interNum = null;
|
||||
const isInter = binding?.arg?.includes(":") ?? false;
|
||||
|
||||
if (isInter) {
|
||||
num = Number(subBefore(binding.arg, ":"));
|
||||
interNum = Number(subAfter(binding.arg, ":"));
|
||||
} else if (binding.arg) {
|
||||
num = Number(binding.arg);
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
if (interTimer) {
|
||||
clearInterval(interTimer);
|
||||
interTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const onDownInter = (ev: PointerEvent) => {
|
||||
ev.preventDefault();
|
||||
if (interTimer === null) {
|
||||
interTimer = setInterval(() => cb(), interNum);
|
||||
}
|
||||
};
|
||||
|
||||
const onDown = (ev: PointerEvent) => {
|
||||
clear();
|
||||
ev.preventDefault();
|
||||
if (timer === null) {
|
||||
timer = isInter
|
||||
? setTimeout(() => {
|
||||
cb();
|
||||
onDownInter(ev);
|
||||
}, num)
|
||||
: setTimeout(() => cb(), num);
|
||||
}
|
||||
};
|
||||
|
||||
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
|
||||
useEventListener(el, "pointerdown", onDown);
|
||||
useEventListener(el, "pointerup", clear);
|
||||
useEventListener(el, "pointerleave", clear);
|
||||
} else {
|
||||
throw new Error(
|
||||
'[Directive: longpress]: need callback and callback must be a function! Like v-longpress="callback"'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
68
web/src/directives/optimize/index.ts
Normal file
68
web/src/directives/optimize/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
isArray,
|
||||
throttle,
|
||||
debounce,
|
||||
isObject,
|
||||
isFunction
|
||||
} from "@pureadmin/utils";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
export interface OptimizeOptions {
|
||||
/** 事件名 */
|
||||
event: string;
|
||||
/** 事件触发的方法 */
|
||||
fn: (...params: any) => any;
|
||||
/** 是否立即执行 */
|
||||
immediate?: boolean;
|
||||
/** 防抖或节流的延迟时间(防抖默认:`200`毫秒、节流默认:`1000`毫秒) */
|
||||
timeout?: number;
|
||||
/** 传递的参数 */
|
||||
params?: any;
|
||||
}
|
||||
|
||||
/** 防抖(v-optimize或v-optimize:debounce)、节流(v-optimize:throttle)指令 */
|
||||
export const optimize: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding<OptimizeOptions>) {
|
||||
const { value } = binding;
|
||||
const optimizeType = binding.arg ?? "debounce";
|
||||
const type = ["debounce", "throttle"].find(t => t === optimizeType);
|
||||
if (type) {
|
||||
if (value && value.event && isFunction(value.fn)) {
|
||||
let params = value?.params;
|
||||
if (params) {
|
||||
if (isArray(params) || isObject(params)) {
|
||||
params = isObject(params) ? Array.of(params) : params;
|
||||
} else {
|
||||
throw new Error(
|
||||
"[Directive: optimize]: `params` must be an array or object"
|
||||
);
|
||||
}
|
||||
}
|
||||
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
|
||||
useEventListener(
|
||||
el,
|
||||
value.event,
|
||||
type === "debounce"
|
||||
? debounce(
|
||||
params ? () => value.fn(...params) : value.fn,
|
||||
value?.timeout ?? 200,
|
||||
value?.immediate ?? false
|
||||
)
|
||||
: throttle(
|
||||
params ? () => value.fn(...params) : value.fn,
|
||||
value?.timeout ?? 1000
|
||||
)
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
"[Directive: optimize]: `event` and `fn` are required, and `fn` must be a function"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"[Directive: optimize]: only `debounce` and `throttle` are supported"
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
48
web/src/directives/ripple/index.scss
Normal file
48
web/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;
|
||||
}
|
||||
}
|
||||
}
|
229
web/src/directives/ripple/index.ts
Normal file
229
web/src/directives/ripple/index.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import "./index.scss";
|
||||
import { isObject } from "@pureadmin/utils";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
export interface RippleOptions {
|
||||
/** 自定义`ripple`颜色,支持`tailwindcss` */
|
||||
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);
|
||||
}
|
||||
|
||||
export const Ripple: Directive = {
|
||||
mounted,
|
||||
unmounted,
|
||||
updated
|
||||
};
|
Reference in New Issue
Block a user