mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-03-29 01:53:22 +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 { antdLocale } from '#/locales';
|
||||||
|
|
||||||
|
import { waveConfigs } from './components/global/button-wave';
|
||||||
import { PopupContext } from './utils/context';
|
import { PopupContext } from './utils/context';
|
||||||
|
|
||||||
defineOptions({ name: 'App' });
|
defineOptions({ name: 'App' });
|
||||||
@@ -30,10 +31,17 @@ const tokenTheme = computed(() => {
|
|||||||
token: tokens,
|
token: tokens,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 按钮水波纹样式配置
|
||||||
|
const waveConfig = computed(() => {
|
||||||
|
const { buttonWaveMode } = preferences.theme;
|
||||||
|
const found = waveConfigs.find((item) => item.name === buttonWaveMode);
|
||||||
|
return found ? found.wave : {};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
|
<ConfigProvider :locale="antdLocale" :theme="tokenTheme" :wave="waveConfig">
|
||||||
<App>
|
<App>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<PopupContext />
|
<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: {
|
theme: {
|
||||||
builtinType: 'default',
|
builtinType: 'default',
|
||||||
|
buttonWaveMode: 'Default',
|
||||||
colorDestructive: 'hsl(348 100% 61%)',
|
colorDestructive: 'hsl(348 100% 61%)',
|
||||||
colorPrimary: 'hsl(215 100% 54%)',
|
colorPrimary: 'hsl(215 100% 54%)',
|
||||||
colorSuccess: 'hsl(144 57% 58%)',
|
colorSuccess: 'hsl(144 57% 58%)',
|
||||||
|
|||||||
@@ -231,6 +231,8 @@ interface TabbarPreferences {
|
|||||||
interface ThemePreferences {
|
interface ThemePreferences {
|
||||||
/** 内置主题名 */
|
/** 内置主题名 */
|
||||||
builtinType: BuiltinThemeType;
|
builtinType: BuiltinThemeType;
|
||||||
|
/** 按钮波纹模式 */
|
||||||
|
buttonWaveMode: 'Default' | 'Disabled' | 'Happy' | 'Inset' | 'Shake';
|
||||||
/** 错误色 */
|
/** 错误色 */
|
||||||
colorDestructive: string;
|
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 GlobalShortcutKeys } from './shortcut-keys/global.vue';
|
||||||
export { default as SwitchItem } from './switch-item.vue';
|
export { default as SwitchItem } from './switch-item.vue';
|
||||||
export { default as BuiltinTheme } from './theme/builtin.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 ColorMode } from './theme/color-mode.vue';
|
||||||
export { default as FontSize } from './theme/font-size.vue';
|
export { default as FontSize } from './theme/font-size.vue';
|
||||||
export { default as Radius } from './theme/radius.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,
|
Block,
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BuiltinTheme,
|
BuiltinTheme,
|
||||||
|
ButtonWaveMode,
|
||||||
ColorMode,
|
ColorMode,
|
||||||
Content,
|
Content,
|
||||||
Copyright,
|
Copyright,
|
||||||
@@ -86,6 +87,7 @@ const themeColorPrimary = defineModel<string>('themeColorPrimary');
|
|||||||
const themeBuiltinType = defineModel<BuiltinThemeType>('themeBuiltinType');
|
const themeBuiltinType = defineModel<BuiltinThemeType>('themeBuiltinType');
|
||||||
const themeMode = defineModel<ThemeModeType>('themeMode');
|
const themeMode = defineModel<ThemeModeType>('themeMode');
|
||||||
const themeRadius = defineModel<string>('themeRadius');
|
const themeRadius = defineModel<string>('themeRadius');
|
||||||
|
const themeButtonWaveMode = defineModel<string>('themeButtonWaveMode');
|
||||||
const themeFontSize = defineModel<number>('themeFontSize');
|
const themeFontSize = defineModel<number>('themeFontSize');
|
||||||
const themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar');
|
const themeSemiDarkSidebar = defineModel<boolean>('themeSemiDarkSidebar');
|
||||||
const themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader');
|
const themeSemiDarkHeader = defineModel<boolean>('themeSemiDarkHeader');
|
||||||
@@ -330,6 +332,9 @@ async function handleReset() {
|
|||||||
<Block :title="$t('preferences.theme.radius')">
|
<Block :title="$t('preferences.theme.radius')">
|
||||||
<Radius v-model="themeRadius" />
|
<Radius v-model="themeRadius" />
|
||||||
</Block>
|
</Block>
|
||||||
|
<Block :title="$t('preferences.theme.buttonWaveMode')">
|
||||||
|
<ButtonWaveMode v-model="themeButtonWaveMode" />
|
||||||
|
</Block>
|
||||||
<Block :title="$t('preferences.theme.fontSize')">
|
<Block :title="$t('preferences.theme.fontSize')">
|
||||||
<FontSize v-model="themeFontSize" />
|
<FontSize v-model="themeFontSize" />
|
||||||
</Block>
|
</Block>
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"title": "Theme",
|
"title": "Theme",
|
||||||
"radius": "Radius",
|
"radius": "Radius",
|
||||||
|
"buttonWaveMode": "Button Wave Mode",
|
||||||
"fontSize": "Font Size",
|
"fontSize": "Font Size",
|
||||||
"fontSizeTip": "Adjust global font size with real-time preview",
|
"fontSizeTip": "Adjust global font size with real-time preview",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"title": "主题",
|
"title": "主题",
|
||||||
"radius": "圆角",
|
"radius": "圆角",
|
||||||
|
"buttonWaveMode": "按钮水波纹样式",
|
||||||
"fontSize": "字体大小",
|
"fontSize": "字体大小",
|
||||||
"fontSizeTip": "调整全局字体大小,实时预览效果",
|
"fontSizeTip": "调整全局字体大小,实时预览效果",
|
||||||
"light": "浅色",
|
"light": "浅色",
|
||||||
|
|||||||
Reference in New Issue
Block a user