Merge branch 'main' into gitee

This commit is contained in:
xiaoxian521 2024-03-05 22:24:55 +08:00
commit 87a2af7181
14 changed files with 877 additions and 231 deletions

View File

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

View File

@ -90,6 +90,7 @@ menus:
hsMenuTree: 菜单树结构
hsVideoFrame: 视频帧截取-wasm版
hsWavesurfer: 音频可视化
hsRipple: 波纹(Ripple)
hsOptimize: 防抖、截流、复制、长按指令
hsWatermark: 水印
hsPrint: 打印

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,3 +2,4 @@ export * from "./auth";
export * from "./copy";
export * from "./longpress";
export * from "./optimize";
export * from "./ripple";

View 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;
}
}
}

View 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
};

View File

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

View File

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

View File

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

View File

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

View File

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

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