mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-27 14:13:40 +08:00
feat: add ripple directive
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
## Wip
|
## Wip
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- 新增 `v-ripple`水波纹指令
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
- 修复混合模式下滚动条丢失问题
|
- 修复混合模式下滚动条丢失问题
|
||||||
|
21
src/directives/ripple/index.less
Normal file
21
src/directives/ripple/index.less
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.ripple-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ripple-effect {
|
||||||
|
position: relative;
|
||||||
|
z-index: 9999;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
191
src/directives/ripple/index.ts
Normal file
191
src/directives/ripple/index.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { Directive } from 'vue';
|
||||||
|
import './index.less';
|
||||||
|
export interface RippleOptions {
|
||||||
|
event: string;
|
||||||
|
transition: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RippleProto {
|
||||||
|
background?: string;
|
||||||
|
zIndex?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventType = Event & MouseEvent & TouchEvent;
|
||||||
|
|
||||||
|
const options: RippleOptions = {
|
||||||
|
event: 'mousedown',
|
||||||
|
transition: 400,
|
||||||
|
};
|
||||||
|
|
||||||
|
const RippleDirective: Directive & RippleProto = {
|
||||||
|
beforeMount: (el: HTMLElement, binding) => {
|
||||||
|
if (binding.value === false) return;
|
||||||
|
|
||||||
|
const bg = el.getAttribute('ripple-background');
|
||||||
|
setProps(Object.keys(binding.modifiers), options);
|
||||||
|
|
||||||
|
const background = bg || RippleDirective.background;
|
||||||
|
const zIndex = RippleDirective.zIndex;
|
||||||
|
|
||||||
|
el.addEventListener(options.event, (event: EventType) => {
|
||||||
|
rippler({
|
||||||
|
event,
|
||||||
|
el,
|
||||||
|
background,
|
||||||
|
zIndex,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updated(el, binding) {
|
||||||
|
if (!binding.value) {
|
||||||
|
el?.clearRipple?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bg = el.getAttribute('ripple-background');
|
||||||
|
el?.setBackground?.(bg);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function rippler({
|
||||||
|
event,
|
||||||
|
el,
|
||||||
|
zIndex,
|
||||||
|
background,
|
||||||
|
}: { event: EventType; el: HTMLElement } & RippleProto) {
|
||||||
|
const targetBorder = parseInt(getComputedStyle(el).borderWidth.replace('px', ''));
|
||||||
|
const clientX = event.clientX || event.touches[0].clientX;
|
||||||
|
const clientY = event.clientY || event.touches[0].clientY;
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const { left, top } = rect;
|
||||||
|
const { offsetWidth: width, offsetHeight: height } = el;
|
||||||
|
const { transition } = options;
|
||||||
|
const dx = clientX - left;
|
||||||
|
const dy = clientY - top;
|
||||||
|
const maxX = Math.max(dx, width - dx);
|
||||||
|
const maxY = Math.max(dy, height - dy);
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
const radius = Math.sqrt(maxX * maxX + maxY * maxY);
|
||||||
|
const border = targetBorder > 0 ? targetBorder : 0;
|
||||||
|
|
||||||
|
const ripple = document.createElement('div');
|
||||||
|
const rippleContainer = document.createElement('div');
|
||||||
|
|
||||||
|
// Styles for ripple
|
||||||
|
|
||||||
|
Object.assign(ripple.style ?? {}, {
|
||||||
|
className: 'ripple',
|
||||||
|
marginTop: '0px',
|
||||||
|
marginLeft: '0px',
|
||||||
|
width: '1px',
|
||||||
|
height: '1px',
|
||||||
|
transition: `all ${transition}ms cubic-bezier(0.4, 0, 0.2, 1)`,
|
||||||
|
borderRadius: '50%',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: zIndex ?? '9999',
|
||||||
|
backgroundColor: background ?? 'rgba(0, 0, 0, 0.12)',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Styles for rippleContainer
|
||||||
|
Object.assign(rippleContainer.style ?? {}, {
|
||||||
|
className: 'ripple-container',
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${0 - border}px`,
|
||||||
|
top: `${0 - border}px`,
|
||||||
|
height: '0',
|
||||||
|
width: '0',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
const storedTargetPosition =
|
||||||
|
el.style.position.length > 0 ? el.style.position : getComputedStyle(el).position;
|
||||||
|
|
||||||
|
if (storedTargetPosition !== 'relative') {
|
||||||
|
el.style.position = 'relative';
|
||||||
|
}
|
||||||
|
|
||||||
|
rippleContainer.appendChild(ripple);
|
||||||
|
el.appendChild(rippleContainer);
|
||||||
|
|
||||||
|
Object.assign(ripple.style, {
|
||||||
|
marginTop: `${dy}px`,
|
||||||
|
marginLeft: `${dx}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
borderTopLeftRadius,
|
||||||
|
borderTopRightRadius,
|
||||||
|
borderBottomLeftRadius,
|
||||||
|
borderBottomRightRadius,
|
||||||
|
} = style;
|
||||||
|
Object.assign(rippleContainer.style, {
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
direction: 'ltr',
|
||||||
|
borderTopLeftRadius,
|
||||||
|
borderTopRightRadius,
|
||||||
|
borderBottomLeftRadius,
|
||||||
|
borderBottomRightRadius,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const wh = `${radius * 2}px`;
|
||||||
|
Object.assign(ripple.style ?? {}, {
|
||||||
|
width: wh,
|
||||||
|
height: wh,
|
||||||
|
marginLeft: `${dx - radius}px`,
|
||||||
|
marginTop: `${dy - radius}px`,
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
function clearRipple() {
|
||||||
|
setTimeout(() => {
|
||||||
|
ripple.style.backgroundColor = 'rgba(0, 0, 0, 0)';
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
rippleContainer?.parentNode?.removeChild(rippleContainer);
|
||||||
|
}, 850);
|
||||||
|
el.removeEventListener('mouseup', clearRipple, false);
|
||||||
|
el.removeEventListener('mouseleave', clearRipple, false);
|
||||||
|
el.removeEventListener('dragstart', clearRipple, false);
|
||||||
|
setTimeout(() => {
|
||||||
|
let clearPosition = true;
|
||||||
|
for (let i = 0; i < el.childNodes.length; i++) {
|
||||||
|
if ((el.childNodes[i] as any).className === 'ripple-container') {
|
||||||
|
clearPosition = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearPosition) {
|
||||||
|
el.style.position = storedTargetPosition !== 'static' ? storedTargetPosition : '';
|
||||||
|
}
|
||||||
|
}, options.transition + 260);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'mousedown') {
|
||||||
|
el.addEventListener('mouseup', clearRipple, false);
|
||||||
|
el.addEventListener('mouseleave', clearRipple, false);
|
||||||
|
el.addEventListener('dragstart', clearRipple, false);
|
||||||
|
} else {
|
||||||
|
clearRipple();
|
||||||
|
}
|
||||||
|
|
||||||
|
(el as any).setBackground = (bgColor: string) => {
|
||||||
|
if (!bgColor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ripple.style.backgroundColor = bgColor;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProps(modifiers: { [key: string]: any }, props: Record<string, any>) {
|
||||||
|
modifiers.forEach((item: any) => {
|
||||||
|
if (isNaN(Number(item))) props.event = item;
|
||||||
|
else props.transition = item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RippleDirective;
|
@@ -9,6 +9,7 @@ export default {
|
|||||||
copy: 'Clipboard',
|
copy: 'Clipboard',
|
||||||
msg: 'Message prompt',
|
msg: 'Message prompt',
|
||||||
watermark: 'Watermark',
|
watermark: 'Watermark',
|
||||||
|
ripple: 'Ripple',
|
||||||
fullScreen: 'Full Screen',
|
fullScreen: 'Full Screen',
|
||||||
errorLog: 'Error Log',
|
errorLog: 'Error Log',
|
||||||
tab: 'Tab with parameters',
|
tab: 'Tab with parameters',
|
||||||
|
@@ -9,6 +9,7 @@ export default {
|
|||||||
copy: '剪切板',
|
copy: '剪切板',
|
||||||
msg: '消息提示',
|
msg: '消息提示',
|
||||||
watermark: '水印',
|
watermark: '水印',
|
||||||
|
ripple: '水波纹',
|
||||||
fullScreen: '全屏',
|
fullScreen: '全屏',
|
||||||
errorLog: '错误日志',
|
errorLog: '错误日志',
|
||||||
tab: 'Tab带参',
|
tab: 'Tab带参',
|
||||||
|
@@ -5,7 +5,7 @@ import router, { setupRouter } from '/@/router';
|
|||||||
import { setupStore } from '/@/store';
|
import { setupStore } from '/@/store';
|
||||||
import { setupAntd } from '/@/setup/ant-design-vue';
|
import { setupAntd } from '/@/setup/ant-design-vue';
|
||||||
import { setupErrorHandle } from '/@/setup/error-handle';
|
import { setupErrorHandle } from '/@/setup/error-handle';
|
||||||
import { setupGlobDirectives } from '/@/setup/directives';
|
import { setupGlobDirectives } from '/@/directives';
|
||||||
import { setupI18n } from '/@/setup/i18n';
|
import { setupI18n } from '/@/setup/i18n';
|
||||||
import { setupProdMockServer } from '../mock/_createProductionServer';
|
import { setupProdMockServer } from '../mock/_createProductionServer';
|
||||||
import { setApp } from '/@/setup/App';
|
import { setApp } from '/@/setup/App';
|
||||||
|
@@ -6,6 +6,9 @@ const menu: MenuModule = {
|
|||||||
menu: {
|
menu: {
|
||||||
name: t('routes.demo.feat.feat'),
|
name: t('routes.demo.feat.feat'),
|
||||||
path: '/feat',
|
path: '/feat',
|
||||||
|
tag: {
|
||||||
|
dot: true,
|
||||||
|
},
|
||||||
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -44,6 +47,13 @@ const menu: MenuModule = {
|
|||||||
path: 'watermark',
|
path: 'watermark',
|
||||||
name: t('routes.demo.feat.watermark'),
|
name: t('routes.demo.feat.watermark'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'ripple',
|
||||||
|
name: t('routes.demo.feat.ripple'),
|
||||||
|
tag: {
|
||||||
|
content: 'new',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'full-screen',
|
path: 'full-screen',
|
||||||
name: t('routes.demo.feat.fullScreen'),
|
name: t('routes.demo.feat.fullScreen'),
|
||||||
|
@@ -86,6 +86,14 @@ const feat: AppRouteModule = {
|
|||||||
title: t('routes.demo.feat.watermark'),
|
title: t('routes.demo.feat.watermark'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'ripple',
|
||||||
|
name: 'RippleDemo',
|
||||||
|
component: () => import('/@/views/demo/feat/ripple/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: t('routes.demo.feat.ripple'),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'full-screen',
|
path: 'full-screen',
|
||||||
name: 'FullScreenDemo',
|
name: 'FullScreenDemo',
|
||||||
|
33
src/views/demo/feat/ripple/index.vue
Normal file
33
src/views/demo/feat/ripple/index.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="demo-box" v-ripple>content</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { Alert } from 'ant-design-vue';
|
||||||
|
import RippleDirective from '/@/directives/ripple';
|
||||||
|
export default defineComponent({
|
||||||
|
components: { Alert },
|
||||||
|
directives: {
|
||||||
|
Ripple: RippleDirective,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.demo-box {
|
||||||
|
display: flex;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #fff;
|
||||||
|
background: #408ede;
|
||||||
|
border-radius: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
Reference in New Issue
Block a user