This commit is contained in:
Ray.Hao
2026-01-14 11:05:56 +08:00
12 changed files with 202 additions and 70 deletions

View File

@@ -1,28 +1,29 @@
<template>
<el-table-column
:label="label"
:fixed="fixed"
:align="align"
:show-overflow-tooltip="showOverflowTooltip"
:width="finalWidth"
>
<el-table-column :prop :label :fixed :align :show-overflow-tooltip :width="finalWidth">
<template #default="{ row }">
<div v-auto-width class="operation-buttons">
<slot :row="row"></slot>
<div v-auto class="operation-button">
<slot :row="row" />
</div>
</template>
</el-table-column>
</template>
<script setup lang="ts">
<script lang="ts" setup>
interface Props {
/**
* 表格数据长度
* 用于性能优化,避免多次计算宽度
*/
listDataLength: number;
prop?: string;
label?: string;
fixed?: string;
align?: string;
width?: number;
showOverflowTooltip?: boolean;
/**
* 最小宽度优先级高于自动计算宽度默认80px
* @default 80px
*/
minWidth?: number;
}
@@ -30,62 +31,43 @@ const props = withDefaults(defineProps<Props>(), {
label: "操作",
fixed: "right",
align: "center",
minWidth: 80,
});
const count = ref(0);
const operationWidth = ref(props.minWidth || 80);
// 霈∠<E99C88><E288A0><EFBFBD><E6BBA2>堒捐摨?
const maxWidth = ref(80);
const calculateWidth = () => {
count.value++;
if (count.value !== props.listDataLength) return;
const maxWidth = getOperationMaxWidth();
operationWidth.value = Math.max(maxWidth, props.minWidth);
let totalWidth = 0;
maxWidth.value = 80; // 重置为初始值
const els = document.getElementsByClassName("operation-button");
Array.from(els).forEach((el) => {
const buttons = el.querySelectorAll(".el-button");
totalWidth = Array.from(buttons).reduce((prev, button) => {
// 14 是按钮之间的距离
// 组成:按钮的左边距(Element Plus默认为12px)+按钮的padding(Element Plus默认为2px)
return prev + button.scrollWidth + 14;
}, 24); // 24 是左右内边距
maxWidth.value = Math.max(maxWidth.value, totalWidth);
});
count.value = 0;
};
// 霈∠<E99C88><E288A0><EFBFBD><EFBFBD>捐摨?
const vAuto = {
mounted: () => {
// 初次挂载的时候计算一次
calculateWidth();
},
updated: () => {
// 数据更新时重新计算一次
calculateWidth();
},
};
const finalWidth = computed(() => {
return props.width || operationWidth.value || props.minWidth;
return props.minWidth || maxWidth.value;
});
// <20><EFBFBD><E88AB7><EFBFBD>摰賢漲<E8B3A2><E6BCB2>
const vAutoWidth = {
mounted() {
// <20>脲活<E884B2><E6B4BB><EFBFBD><E89DB8>𧒄<EFBFBD>躰恣蝞𦯀<E89D9E>甈?
calculateWidth();
},
updated() {
// <20>唳旿<E594B3>湔鰵<E6B994><EFBFBD><E59C92>啗恣蝞𦯀<E89D9E>甈?
calculateWidth();
},
};
/**
* <20><EFBFBD><E79195>厰僼<E58EB0><EFBFBD><E59C88><EFBFBD>捐撣行䔉<E8A18C><EFBFBD><E79195><EFBFBD><EFBFBD><E89D8F><EFBFBD><EFBFBD>憭批捐摨?
* 瘜冽<E7989C>雿輻鍂<E8BCBB><EFBFBD><EFBFBD><EFBFBD>?`class="operation-buttons"` <20><><EFBFBD>蝑曉<E89D91>鋆寞<E98B86>雿𨀣<E99BBF><F0A880A3>?
* @returns {number} 餈𥪜<E9A488><F0A5AA9C><EFBFBD><EFBFBD><E89D8F><EFBFBD><EFBFBD>憭批捐摨?
*/
const getOperationMaxWidth = () => {
const el = document.getElementsByClassName("operation-buttons");
// <20>𡝗<EFBFBD>雿𦦵<E99BBF><F0A6A6B5><EFBFBD><EFBFBD>憭批捐摨?
let maxWidth = 0;
let totalWidth: any = 0;
Array.prototype.forEach.call(el, (item) => {
// <20><EFBFBD>瘥譍葵item<65><6D>om
const buttons = item.querySelectorAll(".el-button");
// <20><EFBFBD>瘥讛<E798A5><E8AE9B>厰僼<E58EB0><E583BC><EFBFBD>餃捐摨?
totalWidth = Array.from(buttons).reduce((acc, button: any) => {
return acc + button.scrollWidth + 22; // 瘥譍葵<E8AD8D>厰僼<E58EB0><E583BC>捐摨血<E691A8>銝𢠃<E98A9D><F0A2A083>坔捐摨?
}, 0);
// <20><EFBFBD><E79195><EFBFBD>憭抒<E686AD>摰賢漲
if (totalWidth > maxWidth) maxWidth = totalWidth;
});
return maxWidth;
};
</script>

View File

@@ -42,6 +42,7 @@ export const STORAGE_KEYS = {
SHOW_TAGS_VIEW: `${APP_PREFIX}:ui:show_tags_view`,
SHOW_APP_LOGO: `${APP_PREFIX}:ui:show_app_logo`,
SHOW_WATERMARK: `${APP_PREFIX}:ui:show_watermark`,
PAGE_SWITCHING_ANIMATION: `${APP_PREFIX}:ui:page_switching_animation`,
ENABLE_AI_ASSISTANT: `${APP_PREFIX}:ui:enable_ai_assistant`,
LAYOUT: `${APP_PREFIX}:ui:layout`,
SIDEBAR_COLOR_SCHEME: `${APP_PREFIX}:ui:sidebar_color_scheme`,

View File

@@ -121,3 +121,31 @@ export const enum DeviceEnum {
*/
MOBILE = "mobile",
}
/**
* 页面切换动画枚举
*/
export const enum PageSwitchingAnimationEnum {
/**
* 无动画
*/
NONE = "none",
/**
* 淡入淡出
*/
FADE = "fade",
/**
* 平滑切换
*/
FADE_SLIDE = "fade-slide",
/**
* 缩放切换
*/
FADE_SCALE = "fade-scale",
}
export const PageSwitchingAnimationOptions: Record<string, OptionItem> = {
none: { value: "none", label: "无动画" },
fade: { value: "fade", label: "淡入淡出" },
"fade-slide": { value: "fade-slide", label: "平滑切换" },
"fade-scale": { value: "fade-scale", label: "缩放切换" },
};

View File

@@ -72,6 +72,11 @@
"showAppLogo": "Show App Logo",
"sidebarColorScheme": "Sidebar Color Scheme",
"showWatermark": "Show Watermark",
"pageSwitchingAnimation": "Page Switching Animation",
"none": "None",
"fade": "Fade",
"fade-slide": "Fade Slide",
"fade-scale": "Fade Scale",
"classicBlue": "Classic Blue",
"minimalWhite": "Minimal White",
"copyConfig": "Copy Config",

View File

@@ -75,6 +75,11 @@
"showTagsView": "显示页签",
"showAppLogo": "显示Logo",
"showWatermark": "显示水印",
"pageSwitchingAnimation": "页面切换动画",
"none": "无动画",
"fade": "淡入淡出",
"fade-slide": "平滑切换",
"fade-scale": "缩放切换",
"classicBlue": "经典蓝",
"minimalWhite": "极简白",
"copyConfig": "复制配置",

View File

@@ -2,7 +2,7 @@
<section class="app-main" :style="{ height: appMainHeight }">
<router-view>
<template #default="{ Component, route }">
<transition enter-active-class="animate__animated animate__fadeIn" mode="out-in">
<transition :name="transitionName" mode="out-in">
<keep-alive :include="cachedViews">
<component :is="currentComponent(Component, route)" :key="route.fullPath" />
</keep-alive>
@@ -25,6 +25,8 @@ import Error404 from "@/views/error/404.vue";
const { cachedViews } = toRefs(useTagsViewStore());
const settingsStore = useSettingsStore();
// 当前组件
const wrapperMap = new Map<string, Component>();
const currentComponent = (component: Component, route: RouteLocationNormalized) => {
@@ -60,12 +62,17 @@ const currentComponent = (component: Component, route: RouteLocationNormalized)
};
const appMainHeight = computed(() => {
if (useSettingsStore().showTagsView) {
if (settingsStore.showTagsView) {
return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`;
} else {
return `calc(100vh - ${variables["navbar-height"]})`;
}
});
// 页面切换动画名称
const transitionName = computed(() => {
return settingsStore.pageSwitchingAnimation ?? "";
});
</script>
<style lang="scss" scoped>
@@ -74,18 +81,42 @@ const appMainHeight = computed(() => {
overflow-y: auto;
background-color: var(--el-bg-color-page);
/* 布局切换动画优化 */
&.animate__animated {
animation-duration: 0.4s;
animation-fill-mode: forwards;
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
&.animate__fadeOut {
animation-timing-function: ease-in;
/* fade-slide */
.fade-slide-leave-active,
.fade-slide-enter-active {
transition: all 0.3s;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(30px);
}
&.animate__fadeIn {
animation-timing-function: ease-out;
/* fade-scale */
.fade-scale-leave-active,
.fade-scale-enter-active {
transition: all 0.28s;
}
.fade-scale-enter-from {
opacity: 0;
transform: scale(1.2);
}
.fade-scale-leave-to {
opacity: 0;
transform: scale(0.8);
}
}
</style>

View File

@@ -49,6 +49,18 @@
<el-switch v-model="settingsStore.showWatermark" />
</div>
<div class="config-item flex-x-between">
<span class="text-xs">{{ t("settings.pageSwitchingAnimation") }}</span>
<el-select v-model="settingsStore.pageSwitchingAnimation" style="width: 150px">
<el-option
v-for="(item, key) in pageSwitchingAnimationOptions"
:key
:label="t(`settings.${item.value}`)"
:value="item.value"
/>
</el-select>
</div>
<div class="config-item flex-x-between">
<span class="text-xs">灰色模式</span>
<el-switch v-model="settingsStore.grayMode" />
@@ -159,10 +171,13 @@
import { DocumentCopy, RefreshLeft, Check } from "@element-plus/icons-vue";
const { t } = useI18n();
import { LayoutMode, SidebarColor, ThemeMode } from "@/enums";
import { LayoutMode, PageSwitchingAnimationOptions, SidebarColor, ThemeMode } from "@/enums";
import { useSettingsStore } from "@/store";
import { themeColorPresets, appConfig } from "@/settings";
// 页面切换动画选项
const pageSwitchingAnimationOptions: Record<string, OptionItem> = PageSwitchingAnimationOptions;
// 按钮图标
const copyIcon = markRaw(DocumentCopy);
const resetIcon = markRaw(RefreshLeft);

View File

@@ -34,6 +34,7 @@ export const defaults = {
showTagsView: true,
showAppLogo: true,
showWatermark: false,
pageSwitchingAnimation: "fade-slide",
showSettings: true,
watermarkContent: pkg.name,
} as const;

View File

@@ -10,6 +10,10 @@ export const useSettingsStore = defineStore("setting", () => {
const showTagsView = useStorage(STORAGE_KEYS.SHOW_TAGS_VIEW, defaults.showTagsView);
const showAppLogo = useStorage(STORAGE_KEYS.SHOW_APP_LOGO, defaults.showAppLogo);
const showWatermark = useStorage(STORAGE_KEYS.SHOW_WATERMARK, defaults.showWatermark);
const pageSwitchingAnimation = useStorage(
STORAGE_KEYS.PAGE_SWITCHING_ANIMATION,
defaults.pageSwitchingAnimation
);
// 布局
const layout = useStorage<LayoutMode>(STORAGE_KEYS.LAYOUT, defaults.layout as LayoutMode);
@@ -66,6 +70,7 @@ export const useSettingsStore = defineStore("setting", () => {
showTagsView.value = defaults.showTagsView;
showAppLogo.value = defaults.showAppLogo;
showWatermark.value = defaults.showWatermark;
pageSwitchingAnimation.value = defaults.pageSwitchingAnimation;
userEnableAi.value = false;
grayMode.value = false;
colorWeak.value = false;
@@ -80,6 +85,7 @@ export const useSettingsStore = defineStore("setting", () => {
showTagsView,
showAppLogo,
showWatermark,
pageSwitchingAnimation,
enableAiAssistant,
userEnableAi,
grayMode,