96 UI optimization (#122)

* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96

卸载路由时清除requestAnimationFrame

* feat:  文件存储使用Blob格式

* style: 💄 修改部分类型any为具体类型

* feat:  界面设置中模块使用瀑布流布局 #96

* fix: 🐛 md文档更换文件夹解决控制台警告

* style: 💄 switch按钮改回使用daisyui组件

* refactor: ♻️ 所有人员列表提取tableColumn

* style: 💄 奖项列表中的图片类型修复
This commit is contained in:
LOG1997
2025-12-18 17:32:00 +08:00
committed by GitHub
parent 92254cb750
commit 5b8682bb7c
29 changed files with 351 additions and 146 deletions

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import { throttle } from 'lodash-es' // lodash-es 节流
import Masonry from 'masonry-layout'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
// 布局参数 Props
interface MasonryWaterfallProps {
columnWidth?: number | string // 列宽px/选择器)
gutter?: number // 列/行间距
fitWidth?: boolean // 容器宽度自适应居中
}
// 默认参数
const props = withDefaults(defineProps<MasonryWaterfallProps>(), {
columnWidth: 120,
gutter: 16,
fitWidth: true,
})
// Vue Ref 管理 DOM 容器和 masonry 实例
const masonryContainer: Ref<HTMLDivElement | null> = ref(null)
const masonryInstance: Ref<Masonry | null> = ref(null)
// 初始化 masonry仅执行一次因卡片固定
async function initMasonry() {
if (!masonryContainer.value)
return
// 等待插槽内容(固定卡片)完全渲染
await nextTick()
// 初始化 masonry 实例(固定卡片无需销毁旧实例)
masonryInstance.value = new Masonry(masonryContainer.value, {
itemSelector: '.masonry-container > *', // 匹配所有固定子项
columnWidth: props.columnWidth,
gutter: props.gutter,
fitWidth: props.fitWidth,
initLayout: true, // 固定卡片直接初始化布局
})
}
// 刷新布局(仅用于卡片内部内容高度变化)
async function refreshLayout() {
await nextTick()
if (masonryInstance.value) {
masonryInstance.value.layout?.()
}
}
// 窗口缩放节流重排(优化性能)
const handleResize = throttle(() => {
if (masonryInstance.value) {
masonryInstance.value.layout?.()
}
}, 300)
// 生命周期:挂载时初始化,卸载时清理
onMounted(async () => {
await initMasonry()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
// 销毁实例 + 释放内存
if (masonryInstance.value) {
masonryInstance.value.destroy?.()
masonryInstance.value = null
}
// 移除监听 + 取消节流任务
window.removeEventListener('resize', handleResize)
handleResize.cancel()
})
// 仅暴露刷新方法(适配卡片内部内容变化)
defineExpose({ refreshLayout })
</script>
<template>
<!-- masonry 容器ref 绑定接收固定插槽内容 -->
<div ref="masonryContainer" class="masonry-container">
<!-- 插槽直接传入固定的卡片/组件 -->
<slot />
</div>
</template>
<style scoped>
.masonry-container {
width: 100%;
/* max-width: 1400px; */
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
}
/* 固定卡片基础样式 */
.masonry-container > * {
margin-bottom: v-bind('`${gutter}px`');
box-sizing: border-box;
break-inside: avoid; /* 兼容 Safari */
min-height: 100px; /* 避免内容过矮导致布局异常 */
}
/* 响应式适配:小屏调整列宽 */
/* @media (max-width: 768px) {
.masonry-container {
padding: 10px;
}
.masonry-container > * {
width: calc(50% - v-bind('`${gutter}px`')) !important;
}
} */
</style>