A:动画及测试文件

This commit is contained in:
jimmy committed 2025-07-07 10:57:53 +08:00
1 parent f6d1409295
commit 7716850e16
17 files changed
+11653

No files matched your search

@@ -0,0 +1,726 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>全局虚拟滚动列表</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f5f5f7;
color: #333;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: #fff;
min-height: 100vh;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.05);
}
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
position: relative;
overflow: hidden;
}
.page-header h1 {
font-size: 24px;
font-weight: 600;
position: relative;
z-index: 1;
}
.page-header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.list-section {
position: relative;
border-bottom: 1px solid #f1f3f5;
}
.list-header {
background: #f8f9fa;
padding: 15px 20px;
font-weight: 600;
font-size: 16px;
color: #495057;
border-bottom: 2px solid #e9ecef;
position: sticky;
top: 0;
z-index: 10;
backdrop-filter: blur(10px);
background: rgba(248, 249, 250, 0.95);
transition: all 0.3s ease;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
justify-content: space-between;
}
.list-header:hover {
background: rgba(248, 249, 250, 1);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.list-header span {
display: flex;
align-items: center;
}
.list-header span::before {
content: '📋';
margin-right: 8px;
}
.header-info {
font-size: 12px;
opacity: 0.7;
margin-left: 10px;
}
.list-header::after {
content: '▼';
font-size: 12px;
transition: transform 0.3s ease;
margin-left: auto;
}
.list-header.collapsed::after {
transform: rotate(-90deg);
}
.virtual-list-container {
position: relative;
transition: max-height 0.3s ease-out;
}
.virtual-list-container.collapsed {
max-height: 0 !important;
overflow: hidden;
transition: max-height 0.3s ease-in;
}
.virtual-list-wrapper {
position: relative;
width: 100%;
}
.virtual-list-spacer {
width: 100%;
pointer-events: none;
}
.virtual-list-viewport {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.virtual-list-item {
position: absolute;
width: 100%;
height: 60px;
padding: 15px 20px;
border-bottom: 1px solid #f1f3f5;
transition: background-color 0.2s ease, padding-left 0.2s ease;
background: white;
display: flex;
align-items: center;
overflow: hidden;
}
.virtual-list-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 3px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.virtual-list-item:hover {
background-color: #f8f9fa;
padding-left: 30px;
}
.virtual-list-item:hover::before {
transform: translateX(0);
}
.item-number {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
width: 28px;
height: 28px;
text-align: center;
line-height: 28px;
border-radius: 50%;
font-size: 12px;
font-weight: 600;
margin-right: 12px;
flex-shrink: 0;
}
.item-content {
flex: 1;
font-size: 14px;
}
/* 不同分类的颜色主题 */
.list-section:nth-child(2n) .list-header {
background: rgba(255, 243, 224, 0.95);
color: #ff6b6b;
}
.list-section:nth-child(2n) .list-header span::before {
content: '🍔';
}
.list-section:nth-child(3n) .list-header {
background: rgba(224, 255, 224, 0.95);
color: #51cf66;
}
.list-section:nth-child(3n) .list-header span::before {
content: '🥗';
}
.list-section:nth-child(4n) .list-header {
background: rgba(224, 240, 255, 0.95);
color: #339af0;
}
.list-section:nth-child(4n) .list-header span::before {
content: '🍰';
}
.list-section:nth-child(5n) .list-header {
background: rgba(255, 224, 255, 0.95);
color: #ae3ec9;
}
.list-section:nth-child(5n) .list-header span::before {
content: '🥤';
}
/* 滚动提示 */
.scroll-indicator {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 15px;
border-radius: 20px;
font-size: 12px;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.scroll-indicator.show {
opacity: 1;
}
.performance-info {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;
border-radius: 15px;
font-size: 11px;
font-family: monospace;
z-index: 1000;
max-width: 300px;
white-space: pre-line;
}
/* 移动端优化 */
@media (max-width: 768px) {
.container {
box-shadow: none;
}
.page-header h1 {
font-size: 20px;
}
.list-header {
padding: 12px 15px;
font-size: 15px;
}
.virtual-list-item {
padding: 12px 15px;
height: 55px;
}
.performance-info {
display: none;
}
}
</style>
</head>
<body>
<div class="performance-info" id="performanceInfo">
渲染: 0 / 0 项目
</div>
<div class="container">
<div class="page-header">
<h1>全局虚拟滚动列表演示</h1>
<p style="font-size: 14px; margin-top: 10px; opacity: 0.9;">支持大量数据,只有一个全局滚动条</p>
</div>
<div id="listContainer"></div>
</div>
<div class="scroll-indicator" id="scrollIndicator">继续滚动查看更多</div>
<script>
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 优化的节流函数
function throttle(func, delay) {
let timeoutId;
let lastExecTime = 0;
return function (...args) {
const currentTime = Date.now();
if (currentTime - lastExecTime > delay) {
func.apply(this, args);
lastExecTime = currentTime;
} else {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
lastExecTime = Date.now();
}, delay - (currentTime - lastExecTime));
}
};
}
class GlobalVirtualList {
constructor(container, data, options = {}) {
this.container = container;
this.data = data;
this.itemHeight = options.itemHeight || 60;
this.isCollapsed = options.collapsed || false;
this.startIndex = 0;
this.endIndex = 0;
this.bufferSize = 10;
this.listId = Math.random().toString(36).substr(2, 9);
// 稳定性优化参数
this.lastScrollTop = 0;
this.scrollDirection = 'down';
this.stableRangeCache = null;
this.lastRenderTime = 0;
this.minRenderInterval = 50; // 最小渲染间隔
this.init();
this.render();
}
init() {
// 创建虚拟列表容器
this.listContainer = document.createElement('div');
this.listContainer.className = 'virtual-list-container';
if (this.isCollapsed) {
this.listContainer.classList.add('collapsed');
}
// 创建包装器
this.wrapper = document.createElement('div');
this.wrapper.className = 'virtual-list-wrapper';
// 创建占位空间元素(维持总高度)
this.spacer = document.createElement('div');
this.spacer.className = 'virtual-list-spacer';
this.spacer.style.height = `${this.data.length * this.itemHeight}px`;
// 创建视口
this.viewport = document.createElement('div');
this.viewport.className = 'virtual-list-viewport';
this.wrapper.appendChild(this.spacer);
this.wrapper.appendChild(this.viewport);
this.listContainer.appendChild(this.wrapper);
this.container.appendChild(this.listContainer);
}
// 获取绝对位置
getAbsoluteOffset() {
const rect = this.wrapper.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
return rect.top + scrollTop;
}
getVisibleRange() {
if (this.isCollapsed) {
return { start: 0, end: 0 };
}
// 获取当前滚动位置
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
// 检测滚动方向
this.scrollDirection = scrollTop > this.lastScrollTop ? 'down' : 'up';
this.lastScrollTop = scrollTop;
// 获取容器的绝对位置
const containerTop = this.getAbsoluteOffset();
const containerHeight = this.data.length * this.itemHeight;
// 计算可见区域
const viewportTop = scrollTop;
const viewportBottom = scrollTop + windowHeight;
// 计算列表区域
const listTop = containerTop;
const listBottom = containerTop + containerHeight;
// 检查是否有交集
if (listBottom <= viewportTop || listTop >= viewportBottom) {
return { start: 0, end: 0 };
}
// 计算可见范围
const visibleStartInList = Math.max(0, viewportTop - listTop);
const visibleEndInList = Math.min(containerHeight, viewportBottom - listTop);
// 计算项目索引 - 使用稳定的整数运算
let startIndex = Math.floor(visibleStartInList / this.itemHeight);
let endIndex = Math.ceil(visibleEndInList / this.itemHeight);
// 确保索引在有效范围内
startIndex = Math.max(0, Math.min(startIndex, this.data.length - 1));
endIndex = Math.max(startIndex + 1, Math.min(endIndex, this.data.length));
// 动态调整缓冲区大小
const dynamicBuffer = this.scrollDirection === 'down'
? { top: Math.floor(this.bufferSize * 0.3), bottom: this.bufferSize }
: { top: this.bufferSize, bottom: Math.floor(this.bufferSize * 0.3) };
// 添加缓冲区
const bufferedStart = Math.max(0, startIndex - dynamicBuffer.top);
const bufferedEnd = Math.min(this.data.length, endIndex + dynamicBuffer.bottom);
return {
start: bufferedStart,
end: bufferedEnd
};
}
shouldUpdate(newStart, newEnd) {
// 时间限流
const now = Date.now();
if (now - this.lastRenderTime < this.minRenderInterval) {
return false;
}
// 如果范围完全相同,不更新
if (newStart === this.startIndex && newEnd === this.endIndex) {
return false;
}
// 计算重叠区域
const overlapStart = Math.max(this.startIndex, newStart);
const overlapEnd = Math.min(this.endIndex, newEnd);
const overlapSize = Math.max(0, overlapEnd - overlapStart);
const currentSize = this.endIndex - this.startIndex;
const newSize = newEnd - newStart;
// 如果重叠区域占当前显示区域的大部分,且变化不大,跳过更新
if (currentSize > 0 && overlapSize / currentSize > 0.7) {
const startDiff = Math.abs(newStart - this.startIndex);
const endDiff = Math.abs(newEnd - this.endIndex);
// 如果变化很小,跳过
if (startDiff <= 2 && endDiff <= 2) {
return false;
}
}
return true;
}
render() {
const { start, end } = this.getVisibleRange();
// 检查是否需要更新
if (!this.shouldUpdate(start, end)) {
return;
}
console.log(`List ${this.listId} 渲染范围: ${start} - ${end} (之前: ${this.startIndex} - ${this.endIndex})`);
this.startIndex = start;
this.endIndex = end;
this.lastRenderTime = Date.now();
// 清除现有项目
this.viewport.innerHTML = '';
// 如果折叠状态或没有可见项目,直接返回
if (this.isCollapsed || start >= end) {
return;
}
// 创建文档片段提高性能
const fragment = document.createDocumentFragment();
// 渲染可见项目
for (let i = start; i < end && i < this.data.length; i++) {
const item = this.createItem(this.data[i], i);
item.style.transform = `translateY(${i * this.itemHeight}px)`;
fragment.appendChild(item);
}
this.viewport.appendChild(fragment);
}
createItem(data, index) {
const item = document.createElement('div');
item.className = 'virtual-list-item';
item.innerHTML = `
<span class="item-number">${index + 1}</span>
<span class="item-content">${data}</span>
`;
// 添加点击效果
item.addEventListener('click', () => {
const scale = item.style.transform.includes('scale') ? '' : ' scale(0.98)';
item.style.transform = `translateY(${index * this.itemHeight}px)${scale}`;
if (scale) {
setTimeout(() => {
item.style.transform = `translateY(${index * this.itemHeight}px)`;
}, 200);
}
});
return item;
}
toggle() {
this.isCollapsed = !this.isCollapsed;
this.listContainer.classList.toggle('collapsed');
if (!this.isCollapsed) {
// 展开时重新渲染
setTimeout(() => {
this.startIndex = -1; // 强制重新渲染
this.endIndex = -1;
this.render();
}, 350);
} else {
// 折叠时清除内容
this.viewport.innerHTML = '';
}
}
updateOnScroll() {
this.render();
}
forceRefresh() {
this.startIndex = -1;
this.endIndex = -1;
this.lastRenderTime = 0;
this.render();
}
}
// 生成大量测试数据
function generateLargeDataset(prefix, count) {
const items = [];
const foods = [
'宫保鸡丁', '麻婆豆腐', '红烧肉', '鱼香肉丝', '青椒肉丝', '糖醋里脊',
'回锅肉', '蛋炒饭', '扬州炒饭', '牛肉面', '兰州拉面', '炸酱面',
'担担面', '热干面', '重庆小面', '小笼包', '煎饺', '蒸饺',
'包子', '馒头', '花卷', '烧饼', '油条', '豆浆', '胡辣汤', '煎蛋',
'培根', '香肠', '吐司', '三明治', '汉堡', '披萨', '意面', '拉面'
];
for (let i = 0; i < count; i++) {
const food = foods[i % foods.length];
const variant = Math.floor(i / foods.length) + 1;
const rating = '★'.repeat(Math.floor(Math.random() * 5) + 1);
items.push(`${prefix}${food}${variant > 1 ? ` (${variant})` : ''} ${rating}`);
}
return items;
}
// 创建数据集
const datasets = [
{
title: '早餐推荐',
data: generateLargeDataset('精品', 200),
collapsed: false
},
{
title: '主食类',
data: generateLargeDataset('招牌', 800),
collapsed: false
},
{
title: '素食专区',
data: generateLargeDataset('健康', 300),
collapsed: false
},
{
title: '甜品点心',
data: generateLargeDataset('甜蜜', 500),
collapsed: false
},
{
title: '饮品推荐',
data: generateLargeDataset('特调', 400),
collapsed: false
}
];
// 计算总项目数
window.totalItems = datasets.reduce((sum, d) => sum + d.data.length, 0);
// 初始化列表
const listContainer = document.getElementById('listContainer');
const virtualLists = [];
datasets.forEach((dataset, index) => {
// 创建列表区域
const section = document.createElement('div');
section.className = 'list-section';
// 创建表头
const header = document.createElement('div');
header.className = 'list-header';
if (dataset.collapsed) {
header.classList.add('collapsed');
}
header.innerHTML = `
<span>${dataset.title}</span>
<span class="header-info">${dataset.data.length} 项</span>
`;
section.appendChild(header);
listContainer.appendChild(section);
// 创建虚拟列表
const virtualList = new GlobalVirtualList(section, dataset.data, {
itemHeight: 60,
collapsed: dataset.collapsed
});
virtualLists.push(virtualList);
// 绑定表头点击事件
header.addEventListener('click', function() {
virtualList.toggle();
this.classList.toggle('collapsed');
// 添加点击动画
this.style.transform = 'scale(0.98)';
setTimeout(() => {
this.style.transform = 'scale(1)';
}, 200);
// 折叠/展开后需要重新渲染所有列表
setTimeout(() => {
virtualLists.forEach(list => list.forceRefresh());
updatePerformanceInfo();
}, 400);
});
});
// 全局滚动事件监听 - 使用节流
const throttledGlobalRender = throttle(() => {
virtualLists.forEach(list => list.updateOnScroll());
updatePerformanceInfo();
}, 32); // 约30fps,更保守的刷新率
window.addEventListener('scroll', throttledGlobalRender, { passive: true });
// 窗口大小变化时重新渲染
window.addEventListener('resize', debounce(() => {
virtualLists.forEach(list => list.forceRefresh());
updatePerformanceInfo();
}, 300));
// 更新性能信息
function updatePerformanceInfo() {
const allRendered = document.querySelectorAll('.virtual-list-item').length;
const perfInfo = document.getElementById('performanceInfo');
if (perfInfo) {
perfInfo.textContent = `渲染: ${allRendered} / ${window.totalItems} 项目`;
}
}
// 滚动提示功能
let scrollTimer;
const scrollIndicator = document.getElementById('scrollIndicator');
window.addEventListener('scroll', throttle(() => {
scrollIndicator.classList.add('show');
clearTimeout(scrollTimer);
scrollTimer = setTimeout(() => {
scrollIndicator.classList.remove('show');
}, 1000);
}, 100));
// 初始渲染
setTimeout(() => {
virtualLists.forEach(list => list.updateOnScroll());
updatePerformanceInfo();
}, 100);
</script>
</body>
</html>