mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-03-08 15:41:08 +08:00
feat(theme): 新增按钮水波纹样式配置选项
添加按钮水波纹效果的自定义配置功能,支持默认、禁用、内嵌、抖动和欢乐五种样式。用户可在主题设置中选择不同效果,增强交互视觉体验。 - 在主题配置类型中添加 buttonWaveMode 字段 - 新增按钮水波纹配置组件和样式实现 - 更新中英文国际化文本 - 在应用配置中集成水波纹效果
This commit is contained in:
@@ -8,6 +8,7 @@ import { App, ConfigProvider, theme } from 'antdv-next';
|
||||
|
||||
import { antdLocale } from '#/locales';
|
||||
|
||||
import { waveConfigs } from './components/global/button-wave';
|
||||
import { PopupContext } from './utils/context';
|
||||
|
||||
defineOptions({ name: 'App' });
|
||||
@@ -30,10 +31,17 @@ const tokenTheme = computed(() => {
|
||||
token: tokens,
|
||||
};
|
||||
});
|
||||
|
||||
// 按钮水波纹样式配置
|
||||
const waveConfig = computed(() => {
|
||||
const { buttonWaveMode } = preferences.theme;
|
||||
const found = waveConfigs.find((item) => item.name === buttonWaveMode);
|
||||
return found ? found.wave : {};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
|
||||
<ConfigProvider :locale="antdLocale" :theme="tokenTheme" :wave="waveConfig">
|
||||
<App>
|
||||
<RouterView />
|
||||
<PopupContext />
|
||||
|
||||
142
apps/web-antd/src/components/global/button-wave.ts
Normal file
142
apps/web-antd/src/components/global/button-wave.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { ConfigProviderProps } from 'antdv-next';
|
||||
|
||||
import type { ThemePreferences } from '@vben/preferences';
|
||||
|
||||
const createHolder = (node: HTMLElement) => {
|
||||
const { borderWidth } = getComputedStyle(node);
|
||||
const borderWidthNum = Number.parseInt(borderWidth, 10);
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.style.position = 'absolute';
|
||||
div.style.inset = `-${borderWidthNum}px`;
|
||||
div.style.borderRadius = 'inherit';
|
||||
div.style.background = 'transparent';
|
||||
div.style.zIndex = '999';
|
||||
div.style.pointerEvents = 'none';
|
||||
div.style.overflow = 'hidden';
|
||||
node.append(div);
|
||||
|
||||
return div;
|
||||
};
|
||||
|
||||
const createDot = (
|
||||
holder: HTMLElement,
|
||||
color: string,
|
||||
left: number,
|
||||
top: number,
|
||||
size = 0,
|
||||
) => {
|
||||
const dot = document.createElement('div');
|
||||
dot.style.position = 'absolute';
|
||||
dot.style.left = `${left}px`;
|
||||
dot.style.top = `${top}px`;
|
||||
dot.style.width = `${size}px`;
|
||||
dot.style.height = `${size}px`;
|
||||
dot.style.borderRadius = '50%';
|
||||
dot.style.background = color;
|
||||
dot.style.transform = 'translate3d(-50%, -50%, 0)';
|
||||
dot.style.transition = 'all 1s ease-out';
|
||||
holder.append(dot);
|
||||
return dot;
|
||||
};
|
||||
|
||||
type WaveConfig = NonNullable<ConfigProviderProps['wave']>;
|
||||
|
||||
const showInsetEffect: WaveConfig['showEffect'] = (
|
||||
node,
|
||||
{ event, component },
|
||||
) => {
|
||||
if (component !== 'Button') {
|
||||
return;
|
||||
}
|
||||
|
||||
const holder = createHolder(node);
|
||||
const rect = holder.getBoundingClientRect();
|
||||
const left = event.clientX - rect.left;
|
||||
const top = event.clientY - rect.top;
|
||||
|
||||
const dot = createDot(holder, 'rgba(255, 255, 255, 0.65)', left, top);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
dot.addEventListener('transitionend', () => {
|
||||
holder.remove();
|
||||
});
|
||||
|
||||
dot.style.width = '200px';
|
||||
dot.style.height = '200px';
|
||||
dot.style.opacity = '0';
|
||||
});
|
||||
};
|
||||
|
||||
const showShakeEffect: WaveConfig['showEffect'] = (node, { component }) => {
|
||||
if (component !== 'Button') {
|
||||
return;
|
||||
}
|
||||
|
||||
const seq = [0, -15, 15, -5, 5, 0];
|
||||
const itv = 10;
|
||||
let steps = 0;
|
||||
|
||||
const loop = () => {
|
||||
cancelAnimationFrame((node as any).effectTimeout);
|
||||
(node as any).effectTimeout = requestAnimationFrame(() => {
|
||||
const currentStep = Math.floor(steps / itv);
|
||||
const current = seq[currentStep];
|
||||
const next = seq[currentStep + 1];
|
||||
|
||||
if (next === undefined || next === null) {
|
||||
node.style.transform = '';
|
||||
node.style.transition = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const angle =
|
||||
(current ?? 0) + ((next - (current ?? 0)) / itv) * (steps % itv);
|
||||
|
||||
node.style.transform = `rotate(${angle}deg)`;
|
||||
node.style.transition = 'none';
|
||||
|
||||
steps += 1;
|
||||
loop();
|
||||
});
|
||||
};
|
||||
|
||||
loop();
|
||||
};
|
||||
|
||||
const showHappyEffect: WaveConfig['showEffect'] = (
|
||||
node,
|
||||
{ event, component },
|
||||
) => {
|
||||
if (component !== 'Button') {
|
||||
return;
|
||||
}
|
||||
|
||||
const holder = createHolder(node);
|
||||
const rect = holder.getBoundingClientRect();
|
||||
const left = event.clientX - rect.left;
|
||||
const top = event.clientY - rect.top;
|
||||
const color = `hsl(${Math.floor(Math.random() * 360)}, 80%, 70%)`;
|
||||
|
||||
const dot = createDot(holder, color, left, top, 16);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
dot.addEventListener('transitionend', () => {
|
||||
holder.remove();
|
||||
});
|
||||
dot.style.width = '220px';
|
||||
dot.style.height = '220px';
|
||||
dot.style.opacity = '0';
|
||||
});
|
||||
};
|
||||
|
||||
export const waveConfigs: Array<{
|
||||
name: ThemePreferences['buttonWaveMode'];
|
||||
wave: WaveConfig;
|
||||
}> = [
|
||||
{ name: 'Disabled', wave: { disabled: true } },
|
||||
{ name: 'Default', wave: {} },
|
||||
{ name: 'Inset', wave: { showEffect: showInsetEffect } },
|
||||
{ name: 'Shake', wave: { showEffect: showShakeEffect } },
|
||||
{ name: 'Happy', wave: { showEffect: showHappyEffect } },
|
||||
];
|
||||
@@ -110,6 +110,7 @@ const defaultPreferences: Preferences = {
|
||||
},
|
||||
theme: {
|
||||
builtinType: 'default',
|
||||
buttonWaveMode: 'Default',
|
||||
colorDestructive: 'hsl(348 100% 61%)',
|
||||
colorPrimary: 'hsl(215 100% 54%)',
|
||||
colorSuccess: 'hsl(144 57% 58%)',
|
||||
|
||||
@@ -231,6 +231,8 @@ interface TabbarPreferences {
|
||||
interface ThemePreferences {
|
||||
/** 内置主题名 */
|
||||
builtinType: BuiltinThemeType;
|
||||
/** 按钮波纹模式 */
|
||||
buttonWaveMode: 'Default' | 'Disabled' | 'Happy' | 'Inset' | 'Shake';
|
||||
/** 错误色 */
|
||||
colorDestructive: string;
|
||||
/** 主题色 */
|
||||
|
||||
@@ -14,6 +14,7 @@ export { default as Widget } from './layout/widget.vue';
|
||||
export { default as GlobalShortcutKeys } from './shortcut-keys/global.vue';
|
||||
export { default as SwitchItem } from './switch-item.vue';
|
||||
export { default as BuiltinTheme } from './theme/builtin.vue';
|
||||
export { default as ButtonWaveMode } from './theme/button-wave-mode.vue';
|
||||
export { default as ColorMode } from './theme/color-mode.vue';
|
||||
export { default as FontSize } from './theme/font-size.vue';
|
||||
export { default as Radius } from './theme/radius.vue';
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { ToggleGroup, ToggleGroupItem } from '@vben-core/shadcn-ui';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceButtonWaveMode',
|
||||
});
|
||||
|
||||
const modelValue = defineModel<string>('themeButtonWaveMode', {
|
||||
default: 'default',
|
||||
});
|
||||
|
||||
const items = [
|
||||
{ label: 'Default', value: 'Default' },
|
||||
{ label: 'Disabled', value: 'Disabled' },
|
||||
{ label: 'Happy', value: 'Happy' },
|
||||
{ label: 'Inset', value: 'Inset' },
|
||||
{ label: 'Shake', value: 'Shake' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ToggleGroup
|
||||
v-model="modelValue"
|
||||
class="gap-2"
|
||||
size="sm"
|
||||
type="single"
|
||||
variant="outline"
|
||||
>
|
||||
<template v-for="item in items" :key="item.value">
|
||||
<ToggleGroupItem
|
||||
:value="item.value"
|
||||
class="data-[state=on]:bg-primary data-[state=on]:text-primary-foreground h-7 rounded-sm px-2"
|
||||
>
|
||||
{{ item.label }}
|
||||
</ToggleGroupItem>
|
||||
</template>
|
||||
</ToggleGroup>
|
||||
</template>
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
Block,
|
||||
Breadcrumb,
|
||||
BuiltinTheme,
|
||||
ButtonWaveMode,
|
||||
ColorMode,
|
||||
Content,
|
||||
Copyright,
|
||||
@@ -86,6 +87,7 @@ const themeColorPrimary = defineModel<string>('themeColorPrimary');
|
||||
const themeBuiltinType = defineModel<BuiltinThemeType>('themeBuiltinType');
|
||||
const themeMode = defineModel<ThemeModeType>('themeMode');
|
||||
const themeRadius = defineModel<string>('themeRadius');
|
||||
const themeButtonWaveMode = defineModel<string>('themeButtonWaveMode');
|
||||
const themeFontSize = defineModel<number>('themeFontSize');
|
||||
const themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar');
|
||||
const themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader');
|
||||
@@ -330,6 +332,9 @@ async function handleReset() {
|
||||
<Block :title="$t('preferences.theme.radius')">
|
||||
<Radius v-model="themeRadius" />
|
||||
</Block>
|
||||
<Block :title="$t('preferences.theme.buttonWaveMode')">
|
||||
<ButtonWaveMode v-model="themeButtonWaveMode" />
|
||||
</Block>
|
||||
<Block :title="$t('preferences.theme.fontSize')">
|
||||
<FontSize v-model="themeFontSize" />
|
||||
</Block>
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
"theme": {
|
||||
"title": "Theme",
|
||||
"radius": "Radius",
|
||||
"buttonWaveMode": "Button Wave Mode",
|
||||
"fontSize": "Font Size",
|
||||
"fontSizeTip": "Adjust global font size with real-time preview",
|
||||
"light": "Light",
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
"theme": {
|
||||
"title": "主题",
|
||||
"radius": "圆角",
|
||||
"buttonWaveMode": "按钮水波纹样式",
|
||||
"fontSize": "字体大小",
|
||||
"fontSizeTip": "调整全局字体大小,实时预览效果",
|
||||
"light": "浅色",
|
||||
|
||||
Reference in New Issue
Block a user