花筑商品详情页前端性能优化实战
一、业务背景与性能挑战
1.1 花筑业务特点
花筑作为旅悦集团旗下的精品民宿品牌,其商品详情页具有以下特征:
- 民宿特色展示:强调房间实景、公共区域、周边环境
- 地理位置核心:地图展示、周边设施、交通信息至关重要
- 体验式内容:民宿故事、主人推荐、住客点评并重
- 季节性明显:节假日、周末预订高峰明显
- 移动端预订为主:90%+订单来自移动端
- 多房型展示:同一民宿多个房型,信息量大
1.2 性能痛点分析
┌─────────────────────────────────────────────────────────────────┐ │ 花筑详情页性能瓶颈 │ ├─────────────┬─────────────┬─────────────┬──────────────┤ │ 房间图片 │ 地图组件 │ 房型信息 │ 用户评价 │ │ 35% │ 28% │ 22% │ 15% │ └─────────────┴─────────────┴─────────────┴──────────────┘
具体问题:
- 民宿房间图片平均8-12MB,高清实拍图加载慢
- 百度地图SDK初始化耗时1.5-2秒,影响首屏
- 多房型对比数据量大,JSON超过300KB
- 用户评价富文本内容复杂,HTML体积庞大
- 移动端低端机型地图渲染卡顿严重
二、房间图片画廊优化专项
2.1 花筑房间图片特色优化
// 花筑房间图片优化管理器
class FloralRoomGalleryOptimizer {
constructor() {
this.roomTypes = new Map(); // 存储不同房型的图片配置
this.deviceProfile = this.analyzeDeviceProfile();
this.networkQuality = this.assessNetworkQuality();
}
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
analyzeDeviceProfile() {
const ua = navigator.userAgent;
const connection = navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection;
return {
// 设备类型判断
isIOS: /iPhone|iPad|iPod/.test(ua),
isAndroid: /Android/.test(ua),
isLowEndDevice: this.detectLowEndDevice(),
// 屏幕特性
screenWidth: window.screen.width,
screenHeight: window.screen.height,
pixelRatio: Math.min(window.devicePixelRatio || 1, 2.5),
colorDepth: window.screen.colorDepth,
// 性能能力
hardwareConcurrency: navigator.hardwareConcurrency || 2,
deviceMemory: navigator.deviceMemory || 2,
// 网络状况
effectiveType: connection?.effectiveType || '4g',
downlink: connection?.downlink || 10,
rtt: connection?.rtt || 100,
saveData: connection?.saveData || false
};
}
detectLowEndDevice() {
const ua = navigator.userAgent;
// 老旧设备识别
const oldAndroid = /Android [1-6]/.test(ua);
const oldIOS = /OS [1-11]_/.test(ua);
// 性能特征判断
const lowMemory = (navigator.deviceMemory || 4) < 2;
const singleCore = (navigator.hardwareConcurrency || 4) < 2;
const lowPixelRatio = (window.devicePixelRatio || 1) < 2;
// 屏幕分辨率判断
const smallScreen = window.screen.width < 720 || window.screen.height < 1280;
return oldAndroid || oldIOS || lowMemory || singleCore || (lowPixelRatio && smallScreen);
}
assessNetworkQuality() {
const connection = navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection;
let quality = 'good';
let score = 100;
if (connection) {
const { effectiveType, downlink, rtt, saveData } = connection;
if (saveData || effectiveType === 'slow-2g' || effectiveType === '2g') {
quality = 'poor';
score = 20;
} else if (effectiveType === '3g') {
quality = 'fair';
score = 50;
} else if (downlink < 1.5) {
quality = 'fair';
score = 60;
} else if (rtt > 300) {
quality = 'fair';
score = 70;
}
}
return { quality, score };
}
// 花筑房间图片配置生成
generateRoomImageConfig(roomType, options = {}) {
const baseConfig = {
roomType,
maxWidth: 1200,
maxHeight: 800,
quality: 85,
format: 'jpeg',
progressive: true,
watermark: 'floral'
};
// 根据设备能力调整配置
const adjustedConfig = this.adjustConfigByDevice(baseConfig);
// 根据网络状况调整配置
const finalConfig = this.adjustConfigByNetwork(adjustedConfig);
// 花筑特色:房间类型特定优化
return this.applyRoomTypeSpecificOptimizations(finalConfig, roomType);
}
adjustConfigByDevice(config) {
const { isLowEndDevice, pixelRatio, hardwareConcurrency } = this.deviceProfile;
if (isLowEndDevice) {
return {
...config,
maxWidth: 800,
maxHeight: 600,
quality: 70,
progressive: false, // 低端设备关闭渐进式加载
format: this.supportsWebP() ? 'webp' : 'jpeg'
};
}
if (pixelRatio > 2) {
return {
...config,
maxWidth: 1600,
maxHeight: 1200,
quality: 90
};
}
if (hardwareConcurrency < 4) {
return {
...config,
progressive: false
};
}
return config;
}
adjustConfigByNetwork(config) {
const { quality, saveData, effectiveType } = this.deviceProfile;
if (saveData || effectiveType === 'slow-2g' || effectiveType === '2g') {
return {
...config,
maxWidth: 400,
maxHeight: 300,
quality: 50,
progressive: false,
watermark: false
};
}
if (effectiveType === '3g') {
return {
...config,
maxWidth: 800,
maxHeight: 600,
quality: 75
};
}
return config;
}
applyRoomTypeSpecificOptimizations(config, roomType) {
// 花筑不同房型的图片优化策略
const roomTypeStrategies = {
'deluxe-suite': {
// 豪华套房:强调细节,需要更高清
maxWidth: 1600,
maxHeight: 1200,
quality: 92,
includeDetails: true
},
'standard-room': {
// 标准间:平衡质量和加载速度
maxWidth: 1200,
maxHeight: 800,
quality: 85
},
'family-room': {
// 家庭房:宽幅图片,展示空间感
maxWidth: 1400,
maxHeight: 700,
quality: 88
},
'shared-space': {
// 公共区域:全景图优化
maxWidth: 1920,
maxHeight: 600,
quality: 80
}
};
const strategy = roomTypeStrategies[roomType] || roomTypeStrategies['standard-room'];
return { ...config, ...strategy };
}
supportsWebP() {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
// 花筑CDN图片URL生成
generateFloralImageUrl(originalUrl, config) {
if (!originalUrl) return '';
// 花筑CDN参数
const params = new URLSearchParams();
// 尺寸优化
params.set('w', config.maxWidth);
params.set('h', config.maxHeight);
params.set('fit', 'cover');
// 质量设置
params.set('q', config.quality);
// 格式选择
params.set('fmt', config.format);
// 花筑特色优化
params.set('sharp', '1.3'); // 锐化增强房间细节
params.set('contrast', '1.1'); // 对比度微调
params.set('sat', '1.05'); // 饱和度轻微提升
// 水印设置(品牌露出)
if (config.watermark !== false) {
params.set('wm', 'floral_logo_light');
params.set('wmp', 'bottom-right');
params.set('wmo', '0.8'); // 水印透明度
}
// 渐进式加载支持
if (config.progressive) {
params.set('progressive', 'true');
}
// 房间图片特殊处理:HDR效果
params.set('hdr', 'auto');
return `${originalUrl}?${params.toString()}`;
}
// 房间图片分类加载策略
createRoomImageGallery(container, roomImages, roomType) {
const config = this.generateRoomImageConfig(roomType);
const gallery = this.buildGalleryStructure(container, roomImages);
// 分层加载策略
this.implementLayeredLoading(gallery, roomImages, config);
// 房间特色功能:360°图片支持
this.setupPanoramaSupport(gallery, roomImages, config);
// 触摸滑动优化
this.optimizeTouchInteraction(gallery);
return gallery;
}
buildGalleryStructure(container, roomImages) {
const gallery = document.createElement('div');
gallery.className = 'floral-room-gallery';
gallery.innerHTML = `
<!-- 主展示区 -->
<div class="gallery-main">
<div class="main-image-wrapper">
<img class="main-image" alt="房间图片">
<div class="image-loading-overlay">
<div class="loading-spinner floral-spinner"></div>
<span class="loading-text">加载精美图片中...</span>
</div>
<div class="image-controls">
<button class="control-btn zoom-in" title="放大查看">🔍+</button>
<button class="control-btn fullscreen" title="全屏模式">⛶</button>
<button class="control-btn share" title="分享">📤</button>
</div>
<div class="image-caption"></div>
</div>
<!-- 导航控制 -->
<div class="gallery-navigation">
<button class="nav-btn prev" aria-label="上一张">‹</button>
<div class="image-counter">
<span class="current">1</span> / <span class="total">${roomImages.length}</span>
</div>
<button class="nav-btn next" aria-label="下一张">›</button>
</div>
</div>
<!-- 缩略图轨道 -->
<div class="gallery-thumbnails">
<div class="thumbnails-track"></div>
</div>
<!-- 房间信息浮层 -->
<div class="room-info-overlay">
<div class="room-badge">${this.getRoomBadge(roomImages[0])}</div>
<div class="room-features"></div>
</div>
`;
container.appendChild(gallery);
return gallery;
}
implementLayeredLoading(gallery, roomImages, config) {
const mainImage = gallery.querySelector('.main-image');
const thumbnailsTrack = gallery.querySelector('.thumbnails-track');
const loadingOverlay = gallery.querySelector('.image-loading-overlay');
const captionEl = gallery.querySelector('.image-caption');
// 分层加载策略
const loadingLayers = [
// 第一层:超低质量预览图(即时加载)
{ quality: 'preview', size: 50, priority: 'critical' },
// 第二层:中等质量缩略图(快速加载)
{ quality: 'thumbnail', size: 200, priority: 'high' },
// 第三层:高质量展示图(渐进加载)
{ quality: 'standard', size: 800, priority: 'normal' },
// 第四层:超高清原图(按需加载)
{ quality: 'premium', size: 1600, priority: 'low' }
];
// 加载第一张图片的所有层级
this.loadMultiLayerImage(roomImages[0], loadingLayers, config)
.then(layers => {
// 显示最终高质量图片
mainImage.src = layers[layers.length - 1].url;
mainImage.classList.add('loaded');
// 隐藏加载遮罩
loadingOverlay.classList.add('hidden');
// 更新标题
captionEl.textContent = roomImages[0].caption || '';
// 加载剩余图片的缩略图
this.loadRemainingThumbnails(thumbnailsTrack, roomImages, config);
});
// 预加载临近图片
this.setupIntelligentPreloading(gallery, roomImages, config, loadingLayers);
}
async loadMultiLayerImage(imageData, layers, config) {
const loadedLayers = [];
for (const layer of layers) {
try {
const layerConfig = {
...config,
maxWidth: layer.size,
maxHeight: Math.round(layer.size * 0.75),
quality: layer.size > 400 ? config.quality : 60
};
const url = this.generateFloralImageUrl(imageData.url, layerConfig);
await this.loadImageWithRetry(url, 3);
loadedLayers.push({ ...layer, url });
// 如果是预览层,立即显示
if (layer.priority === 'critical') {
const img = new Image();
img.src = url;
img.onload = () => {
// 可以在这里显示模糊预览
};
}
} catch (error) {
console.warn(`Failed to load layer ${layer.quality} for image:`, imageData.url);
// 继续尝试下一层
}
}
return loadedLayers;
}
async loadImageWithRetry(url, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
await this.loadImage(url);
return;
} catch (error) {
if (attempt === retries) {
throw error;
}
// 指数退避
await this.sleep(Math.pow(2, attempt) * 100);
}
}
}
loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = resolve;
img.onerror = reject;
img.src = url;
});
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
loadRemainingThumbnails(container, roomImages, config) {
const fragment = document.createDocumentFragment();
roomImages.slice(1).forEach((imageData, index) => {
const thumb = this.createOptimizedThumbnail(imageData, config, index + 2);
fragment.appendChild(thumb);
});
// 使用requestIdleCallback延迟加载
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
container.appendChild(fragment);
});
} else {
setTimeout(() => {
container.appendChild(fragment);
}, 100);
}
}
createOptimizedThumbnail(imageData, config, index) {
const thumb = document.createElement('div');
thumb.className = 'room-thumbnail';
thumb.dataset.index = index;
thumb.dataset.imageId = imageData.id;
// 使用较低质量的缩略图
const thumbConfig = {
...config,
maxWidth: 150,
maxHeight: 100,
quality: 70,
progressive: false,
watermark: false
};
const img = document.createElement('img');
img.alt = imageData.caption || `房间图片${index}`;
img.loading = 'lazy';
img.decoding = 'async';
// 渐进式加载缩略图
const tempImg = new Image();
tempImg.onload = () => {
img.src = tempImg.src;
thumb.classList.add('loaded');
};
tempImg.onerror = () => {
thumb.classList.add('error');
};
tempImg.src = this.generateFloralImageUrl(imageData.url, thumbConfig);
// 添加房间类型标签
if (imageData.type) {
const badge = document.createElement('span');
badge.className = 'room-type-badge';
badge.textContent = this.getImageTypeLabel(imageData.type);
thumb.appendChild(badge);
}
thumb.appendChild(img);
return thumb;
}
getImageTypeLabel(type) {
const labels = {
'bedroom': '卧室',
'bathroom': '卫浴',
'living': '客厅',
'balcony': '阳台',
'view': '景观',
'dining': '餐厅',
'entrance': '入口',
'amenity': '设施'
};
return labels[type] || '房间';
}
getRoomBadge(imageData) {
if (imageData.isPrimary) return '主图';
if (imageData.isPanorama) return '360°全景';
if (imageData.isVideo) return '视频';
return '房间实拍';
}
// 360°全景图片支持
setupPanoramaSupport(gallery, roomImages, config) {
const panoramaImages = roomImages.filter(img => img.isPanorama);
if (panoramaImages.length === 0) return;
panoramaImages.forEach((imageData, index) => {
const panoramaBtn = gallery.querySelector(`[data-image-id="${imageData.id}"]`);
if (!panoramaBtn) return;
panoramaBtn.classList.add('panorama-enabled');
panoramaBtn.title = '点击查看360°全景';
panoramaBtn.addEventListener('click', () => {
this.openPanoramaViewer(imageData, config);
});
});
}
openPanoramaViewer(imageData, config) {
// 创建全景查看器
const viewer = document.createElement('div');
viewer.className = 'panorama-viewer';
viewer.innerHTML = `
<div class="panorama-container">
<div class="panorama-image-wrapper">
<img src="${this.generateFloralImageUrl(imageData.url, { ...config, maxWidth: 2048 })}"
alt="360°全景视图"
class="panorama-image">
<div class="panorama-controls">
<button class="pan-control left">◀</button>
<button class="pan-control right">▶</button>
<button class="pan-control up">▲</button>
<button class="pan-control down">▼</button>
<button class="pan-close">✕</button>
</div>
<div class="panorama-indicator">
<span class="direction-arrow">拖动探索全景</span>
</div>
</div>
</div>
`;
document.body.appendChild(viewer);
document.body.style.overflow = 'hidden';
// 初始化全景交互
this.initPanoramaInteraction(viewer);
// 关闭按钮
viewer.querySelector('.pan-close').addEventListener('click', () => {
viewer.remove();
document.body.style.overflow = '';
});
}
initPanoramaInteraction(viewer) {
const container = viewer.querySelector('.panorama-container');
const image = viewer.querySelector('.panorama-image');
let isDragging = false;
let startX = 0;
let startY = 0;
let translateX = 0;
let translateY = 0;
container.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
container.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
translateX = Math.max(-200, Math.min(200, translateX + deltaX * 0.5));
translateY = Math.max(-100, Math.min(100, translateY + deltaY * 0.5));
image.style.transform = `translate(${translateX}px, ${translateY}px)`;
startX = e.clientX;
startY = e.clientY;
});
document.addEventListener('mouseup', () => {
isDragging = false;
container.style.cursor = 'grab';
});
// 触摸支持
container.addEventListener('touchstart', (e) => {
isDragging = true;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
}, { passive: true });
container.addEventListener('touchmove', (e) => {
if (!isDragging) return;
const deltaX = e.touches[0].clientX - startX;
const deltaY = e.touches[0].clientY - startY;
translateX = Math.max(-200, Math.min(200, translateX + deltaX * 0.5));
translateY = Math.max(-100, Math.min(100, translateY + deltaY * 0.5));
image.style.transform = `translate(${translateX}px, ${translateY}px)`;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
}, { passive: true });
container.addEventListener('touchend', () => {
isDragging = false;
});
}
// 智能预加载
setupIntelligentPreloading(gallery, roomImages, config, layers) {
let currentIndex = 0;
let preloadQueue = new Set();
// 监听图片切换
gallery.addEventListener('imageChange', (e) => {
currentIndex = e.detail.index;
this.updatePreloadQueue(currentIndex, roomImages, config, layers, preloadQueue);
});
// 初始预加载
this.updatePreloadQueue(0, roomImages, config, layers, preloadQueue);
}
updatePreloadQueue(currentIndex, roomImages, config, layers, preloadQueue) {
// 清除旧的预加载任务
preloadQueue.forEach(task => clearTimeout(task));
preloadQueue.clear();
// 预加载前后各2张图片的标准质量版本
const preloadIndices = [
currentIndex - 2, currentIndex - 1,
currentIndex + 1, currentIndex + 2
].filter(i => i >= 0 && i < roomImages.length);
preloadIndices.forEach((index, i) => {
// 延迟加载,错开请求
const delay = i * 200;
const task = setTimeout(() => {
const imageData = roomImages[index];
const standardConfig = {
...config,
maxWidth: 800,
maxHeight: 600,
quality: 85
};
const url = this.generateFloralImageUrl(imageData.url, standardConfig);
this.preloadImage(url);
}, delay);
preloadQueue.add(task);
});
}
preloadImage(url) {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'image';
link.href = url;
document.head.appendChild(link);
}
// 触摸交互优化
optimizeTouchInteraction(gallery) {
const mainImage = gallery.querySelector('.main-image');
const thumbnails = gallery.querySelector('.thumbnails-track');
// 启用被动事件监听
gallery.addEventListener('touchstart', () => {}, { passive: true });
gallery.addEventListener('touchmove', () => {}, { passive: true });
// 双击缩放
let lastTap = 0;
mainImage.addEventListener('touchend', (e) => {
const currentTime = new Date().getTime();
const tapLength = currentTime - lastTap;
if (tapLength < 300 && tapLength > 0) {
e.preventDefault();
this.toggleZoom(mainImage);
}
lastTap = currentTime;
}, { passive: false });
// 缩略图横向滚动优化
if (thumbnails) {
thumbnails.addEventListener('wheel', (e) => {
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
e.preventDefault();
thumbnails.scrollLeft += e.deltaY;
}
}, { passive: false });
}
}
toggleZoom(imageElement) {
if (imageElement.classList.contains('zoomed')) {
imageElement.classList.remove('zoomed');
imageElement.style.transform = '';
} else {
imageElement.classList.add('zoomed');
imageElement.style.transform = 'scale(2)';
imageElement.style.transformOrigin = 'center center';
}
}
}2.2 图片缓存与预加载策略
// 花筑图片资源管理器
class FloralImageResourceManager {
constructor() {
this.memoryCache = new LRUCache(50); // 最多缓存50张图片
this.diskCache = new IndexedDBCache('floral-images');
this.loadingPromises = new Map();
this.prefetchQueue = [];
this.isProcessingQueue = false;
}
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
// LRU缓存实现
class LRUCache {
constructor(maxSize) {
this.maxSize = maxSize;
this.cache = new Map();
this.accessOrder = [];
}
get(key) {
if (this.cache.has(key)) {
// 更新访问顺序
this.updateAccessOrder(key);
return this.cache.get(key);
}
return null;
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.set(key, value);
this.updateAccessOrder(key);
} else {
if (this.cache.size >= this.maxSize) {
// 移除最久未使用的
const lruKey = this.accessOrder.shift();
this.cache.delete(lruKey);
}
this.cache.set(key, value);
this.accessOrder.push(key);
}
}
updateAccessOrder(key) {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
this.accessOrder.push(key);
}
}
has(key) {
return this.cache.has(key);
}
clear() {
this.cache.clear();
this.accessOrder = [];
}
}
// IndexedDB缓存实现
class IndexedDBCache {
constructor(dbName) {
this.dbName = dbName;
this.db = null;
this.init();
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('images')) {
db.createObjectStore('images', { keyPath: 'url' });
}
};
});
}
async get(url) {
if (!this.db) await this.init();
return new Promise((resolve) => {
const transaction = this.db.transaction(['images'], 'readonly');
const store = transaction.objectStore('images');
const request = store.get(url);
request.onsuccess = () => resolve(request.result?.blob);
request.onerror = () => resolve(null);
});
}
async set(url, blob) {
if (!this.db) await this.init();
return new Promise((resolve) => {
const transaction = this.db.transaction(['images'], 'readwrite');
const store = transaction.objectStore('images');
const request = store.put({ url, blob, timestamp: Date.now() });
request.onsuccess = () => resolve();
request.onerror = () => resolve();
});
}
async delete(url) {
if (!this.db) await this.init();
return new Promise((resolve) => {
const transaction = this.db.transaction(['images'], 'readwrite');
const store = transaction.objectStore('images');
const request = store.delete(url);
request.onsuccess = () => resolve();
request.onerror = () => resolve();
});
}
async clear() {
if (!this.db) await this.init();
return new Promise((resolve) => {
const transaction = this.db.transaction(['images'], 'readwrite');
const store = transaction.objectStore('images');
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => resolve();
});
}
}
// 获取图片(带缓存策略)
async getImage(url, options = {}) {
const cacheKey = this.generateCacheKey(url, options);
// 1. 检查内存缓存
const memoryCached = this.memoryCache.get(cacheKey);
if (memoryCached) {
return memoryCached;
}
// 2. 检查IndexedDB缓存
if (!options.skipDiskCache) {
try {
const diskCached = await this.diskCache.get(cacheKey);
if (diskCached) {
// 提升到内存缓存
this.memoryCache.set(cacheKey, diskCached);
return diskCached;
}
} catch (error) {
console.warn('IndexedDB cache read failed:', error);
}
}
// 3. 检查是否正在加载
if (this.loadingPromises.has(cacheKey)) {
return this.loadingPromises.get(cacheKey);
}
// 4. 发起网络请求
const loadPromise = this.loadImageFromNetwork(url, options);
this.loadingPromises.set(cacheKey, loadPromise);
try {
const blob = await loadPromise;
// 存入缓存
this.memoryCache.set(cacheKey, blob);
if (!options.skipDiskCache) {
this.diskCache.set(cacheKey, blob).catch(console.warn);
}
return blob;
} finally {
this.loadingPromises.delete(cacheKey);
}
}
generateCacheKey(url, options) {
const optionStr = JSON.stringify({
width: options.width,
height: options.height,
quality: options.quality,
format: options.format
});
return `${url}_${this.hashString(optionStr)}`;
}
hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(36);
}
async loadImageFromNetwork(url, options) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(url, {
signal: controller.signal,
priority: options.priority || 'high',
cache: 'force-cache'
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${url}`);
}
return await response.blob();
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
// 智能预取策略
async smartPrefetch(images, context = {}) {
const { currentIndex, userBehavior, networkQuality } = context;
// 分析用户行为模式
const behaviorPattern = this.analyzeUserBehavior(userBehavior);
// 生成预取计划
const prefetchPlan = this.generatePrefetchPlan(
images,
currentIndex,
behaviorPattern,
networkQuality
);
// 执行预取
await this.executePrefetchPlan(prefetchPlan);
}
analyzeUserBehavior(userBehavior) {
if (!userBehavior || userBehavior.length === 0) {
return { pattern: 'sequential', speed: 'normal' };
}
// 分析浏览速度
const recentBehavior = userBehavior.slice(-10);
const avgViewTime = recentBehavior.reduce((sum, b) => sum + b.viewDuration, 0) / recentBehavior.length;
let speed = 'normal';
if (avgViewTime < 1500) speed = 'fast';
else if (avgViewTime > 4000) speed = 'slow';
// 分析浏览模式
const directionChanges = recentBehavior.filter((b, i) =>
i > 0 && Math.sign(b.direction) !== Math.sign(recentBehavior[i - 1].direction)
).length;
let pattern = 'sequential';
if (directionChanges > recentBehavior.length * 0.3) {
pattern = 'random';
}
return { pattern, speed };
}
generatePrefetchPlan(images, currentIndex, behaviorPattern, networkQuality) {
const plan = {
immediate: [],
background: [],
lowPriority: []
};
const { pattern, speed } = behaviorPattern;
const prefetchDepth = speed === 'fast' ? 4 : speed === 'slow' ? 2 : 3;
const prefetchDelay = speed === 'fast' ? 50 : speed === 'slow' ? 300 : 150;
// 根据网络质量调整
const qualityMultiplier = networkQuality === 'poor' ? 0.5 :
networkQuality === 'fair' ? 0.75 : 1;
const adjustedDepth = Math.floor(prefetchDepth * qualityMultiplier);
// 生成预取项
for (let i = 1; i <= adjustedDepth; i++) {
const nextIndex = currentIndex + i;
const prevIndex = currentIndex - i;
if (pattern === 'random') {
// 随机模式下,同时预取前后图片
if (nextIndex < images.length) {
plan.background.push({ index: nextIndex, priority: 'medium' });
}
if (prevIndex >= 0) {
plan.background.push({ index: prevIndex, priority: 'medium' });
}
} else {
// 顺序模式下,主要预取下一张
if (nextIndex < images.length) {
plan.immediate.push({ index: nextIndex, priority: 'high' });
}
if (prevIndex >= 0 && i <= 2) {
plan.background.push({ index: prevIndex, priority: 'low' });
}
}
}
return { plan, delays: { immediate: 0, background: prefetchDelay, lowPriority: prefetchDelay * 2 } };
}
async executePrefetchPlan({ plan, delays }) {
// 立即加载高优先级图片
if (plan.immediate.length > 0) {
await this.loadImagesBatch(plan.immediate.map(p => p.index));
}
// 后台加载中优先级图片
setTimeout(() => {
if (plan.background.length > 0) {
this.loadImagesBatch(plan.background.map(p => p.index));
}
}, delays.background);
// 低优先级加载
setTimeout(() => {
if (plan.lowPriority.length > 0) {
this.loadImagesBatch(plan.lowPriority.map(p => p.index));
}
}, delays.lowPriority);
}
async loadImagesBatch(indices) {
const promises = indices.map(index => {
// 这里需要根据实际的图片数据结构调整
const imageUrl = this.getImageUrlByIndex(index);
if (imageUrl) {
return this.getImage(imageUrl, {
width: 800,
height: 600,
quality: 85,
priority: 'low'
}).catch(() => null);
}
return Promise.resolve(null);
});
await Promise.allSettled(promises);
}
getImageUrlByIndex(index) {
// 根据实际数据源返回图片URL
// 这里是示例实现
return null;
}
// 缓存管理
async cleanupCache(maxAge = 7 * 24 * 60 * 60 * 1000) {
// 清理内存缓存中的过期项
const now = Date.now();
// 内存缓存没有时间戳,这里简化处理
// 清理IndexedDB中的过期项
try {
if (!this.db) await this.init();
const cutoffTime = now - maxAge;
const transaction = this.db.transaction(['images'], 'readwrite');
const store = transaction.objectStore('images');
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (cursor.value.timestamp < cutoffTime) {
cursor.delete();
}
cursor.continue();
}
};
} catch (error) {
console.warn('Cache cleanup failed:', error);
}
}
// 获取缓存统计
async getCacheStats() {
const memorySize = this.memoryCache.cache.size;
let diskSize = 0;
try {
if (!this.db) await this.init();
const transaction = this.db.transaction(['images'], 'readonly');
const store = transaction.objectStore('images');
const request = store.count();
await new Promise(resolve => {
request.onsuccess = () => {
diskSize = request.result;
resolve();
};
request.onerror = () => resolve();
});
} catch (error) {
console.warn('Failed to get disk cache stats:', error);
}
return {
memoryEntries: memorySize,
diskEntries: diskSize,
memoryLimit: this.memoryCache.maxSize
};
}
}三、地图组件性能优化
3.1 花筑地图优化管理器
// 花筑地图组件优化管理器
class FloralMapOptimizer {
constructor(container, options = {}) {
this.container = container;
this.options = {
provider: 'baidu', // baidu | amap | google
zoom: 15,
center: null,
showControls: true,
showTraffic: false,
showPoi: true,
lazyLoad: true,
...options
};
this.mapInstance = null;
this.isLoaded = false;
this.loadPromise = null;
this.callbacks = [];
this.tilesLoaded = 0;
this.totalTiles = 0;
}
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
// 延迟加载地图
async loadMap() {
if (this.isLoaded) {
return this.mapInstance;
}
if (this.loadPromise) {
return this.loadPromise;
}
this.loadPromise = this.initializeMap();
return this.loadPromise;
}
async initializeMap() {
// 等待页面空闲时加载地图
if ('requestIdleCallback' in window) {
await new Promise(resolve => {
requestIdleCallback(() => resolve(), { timeout: 2000 });
});
}
// 动态加载地图SDK
await this.loadMapSDK();
// 创建地图实例
this.mapInstance = this.createMapInstance();
this.isLoaded = true;
// 触发回调
this.callbacks.forEach(callback => callback(this.mapInstance));
this.callbacks = [];
return this.mapInstance;
}
async loadMapSDK() {
const { provider } = this.options;
return new Promise((resolve, reject) => {
// 检查是否已加载
if (provider === 'baidu' && window.BMap) {
resolve();
return;
}
if (provider === 'amap' && window.AMap) {
resolve();
return;
}
const script = document.createElement('script');
if (provider === 'baidu') {
script.src = `https://api.map.baidu.com/api?v=3.0&ak=${this.getBaiduApiKey()}&callback=onBaiduMapLoaded`;
} else if (provider === 'amap') {
script.src = `https://webapi.amap.com/maps?v=2.0&key=${this.getAmapApiKey()}&callback=onAmapLoaded`;
}
window.onBaiduMapLoaded = () => resolve();
window.onAmapLoaded = () => resolve();
script.onerror = reject;
document.head.appendChild(script);
});
}
getBaiduApiKey() {
return process.env.REACT_APP_BAIDU_MAP_KEY || 'your_baidu_map_key';
}
getAmapApiKey() {
return process.env.REACT_APP_AMAP_KEY || 'your_amap_key';
}
createMapInstance() {
const { provider, zoom, center, showControls, showTraffic, showPoi } = this.options;
if (provider === 'baidu') {
return this.createBaiduMap(zoom, center, showControls, showTraffic, showPoi);
} else if (provider === 'amap') {
return this.createAmap(zoom, center, showControls, showTraffic, showPoi);
}
throw new Error(`Unsupported map provider: ${provider}`);
}
createBaiduMap(zoom, center, showControls, showTraffic, showPoi) {
const map = new BMap.Map(this.container);
// 设置中心点和缩放级别
const mapCenter = center || this.getDefaultCenter();
map.centerAndZoom(mapCenter, zoom);
// 优化配置
map.enableScrollWheelZoom(true);
map.enableDoubleClickZoom(false); // 禁用双击缩放,提升移动端体验
// 精简控件
if (!showControls) {
map.disableDefaultUI(true);
} else {
map.addControl(new BMap.NavigationControl({
anchor: BMAP_ANCHOR_TOP_LEFT,
type: BMAP_NAVIGATION_CONTROL_SMALL
}));
}
// 交通图层
if (showTraffic) {
const traffic = new BMap.TrafficLayer();
map.addTileLayer(traffic);
}
// POI图层优化
if (!showPoi) {
map.setDisplayOptions({
poi: false,
road: true
});
}
// 监听瓦片加载
this.setupTileTracking(map);
// 移动端优化
this.optimizeForMobile(map);
return map;
}
createAmap(zoom, center, showControls, showTraffic, showPoi) {
const map = new AMap.Map(this.container, {
zoom,
center: center || this.getDefaultCenter(),
resizeEnable: true,
rotateEnable: false, // 禁用旋转,简化交互
pitchEnable: false, // 禁用俯仰角
showBuildingBlock: false, // 不显示3D建筑
showLabel: showPoi,
trafficLayer: showTraffic
});
// 精简控件
if (!showControls) {
map.setStatus({
zoomEnable: false,
dragEnable: false
});
}
// 监听瓦片加载
this.setupTileTracking(map);
// 移动端优化
this.optimizeForMobile(map);
return map;
}
getDefaultCenter() {
// 默认中心点(花筑总部附近)
return { lng: 116.4074, lat: 39.9042 };
}
setupTileTracking(map) {
let tilesLoaded = 0;
let totalTiles = 0;
const tileLoadHandler = () => {
tilesLoaded++;
this.updateLoadingProgress(tilesLoaded, totalTiles);
};
const tileErrorHandler = () => {
tilesLoaded++;
this.updateLoadingProgress(tilesLoaded, totalTiles);
};
// 监听瓦片加载事件(不同地图API的事件名称不同)
if (this.options.provider === 'baidu') {
map.addEventListener('tilesloaded', () => {
this.onMapReady();
});
} else if (this.options.provider === 'amap') {
map.on('complete', () => {
this.onMapReady();
});
}
}
optimizeForMobile(map) {
const isMobile = window.innerWidth < 768;
if (isMobile) {
// 移动端优化配置
if (this.options.provider === 'baidu') {
map.setDefaultCursor('pointer');
map.disableDragging(false);
map.enablePinchToZoom(true);
} else if (this.options.provider === 'amap') {
map.setLayers([new AMap.TileLayer()]); // 仅加载基础图层
}
// 减少不必要的动画
map.setFitView(null, false, [100, 100, 100, 100]);
}
}
updateLoadingProgress(loaded, total) {
const progress = total > 0 ? (loaded / total) * 100 : 0;
// 更新进度指示器
const progressEl = this.container.querySelector('.map-loading-progress');
if (progressEl) {
progressEl.style.width = `${progress}%`;
}
if (progress >= 100) {
this.hideLoadingIndicator();
}
}
onMapReady() {
this.hideLoadingIndicator();
// 触发地图就绪事件
this.container.dispatchEvent(new CustomEvent('mapready', {
detail: { map: this.mapInstance }
}));
}
hideLoadingIndicator() {
const loadingEl = this.container.querySelector('.map-loading-overlay');
if (loadingEl) {
loadingEl.classList.add('hidden');
}
}
// 添加民宿标记点
addPropertyMarker(propertyData) {
const marker = this.createMarker(propertyData);
this.mapInstance.addOverlay(marker);
return marker;
}
createMarker(propertyData) {
const { lng, lat, title, image, price } = propertyData;
if (this.options.provider === 'baidu') {
const point = new BMap.Point(lng, lat);
// 自定义标记图标
const icon = new BMap.Icon(
this.generateMarkerIcon(price),
new BMap.Size(36, 48),
{
anchor: new BMap.Size(18, 48),
imageOffset: new BMap.Size(0, 0)
}
);
const marker = new BMap.Marker(point, { icon });
// 添加信息窗口
const infoWindow = this.createInfoWindow(propertyData);
marker.addEventListener('click', () => {
this.mapInstance.openInfoWindow(infoWindow, point);
});
return marker;
} else if (this.options.provider === 'amap') {
const position = [lng, lat];
const marker = new AMap.Marker({
position,
title,
icon: new AMap.Icon({
size: new AMap.Size(36, 48),
image: this.generateMarkerIcon(price),
imageSize: new AMap.Size(36, 48)
})
});
// 添加信息窗体
const infoWindow = this.createInfoWindow(propertyData);
marker.on('click', () => {
infoWindow.open(this.mapInstance, position);
});
return marker;
}
}
generateMarkerIcon(price) {
// 生成带有价格的自定义标记图标
const canvas = document.createElement('canvas');
canvas.width = 36;
canvas.height = 48;
const ctx = canvas.getContext('2d');
// 绘制气泡形状
ctx.fillStyle = '#FF6B6B';
ctx.beginPath();
ctx.moveTo(18, 0);
ctx.quadraticCurveTo(36, 0, 36, 18);
ctx.lineTo(36, 42);
ctx.quadraticCurveTo(36, 48, 30, 48);
ctx.lineTo(6, 48);
ctx.quadraticCurveTo(0, 48, 0, 42);
ctx.lineTo(0, 18);
ctx.quadraticCurveTo(0, 0, 18, 0);
ctx.fill();
// 绘制价格文字
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 10px Arial';
ctx.textAlign = 'center';
ctx.fillText(`¥${price}`, 18, 28);
return canvas.toDataURL();
}
createInfoWindow(propertyData) {
const { title, image, price, rating, reviews } = propertyData;
const content = `
<div class="floral-map-popup">
<div class="popup-image">
<img src="${image}" alt="${title}" loading="lazy">
</div>
<div class="popup-info">
<h4>${title}</h4>
<div class="popup-rating">
${this.generateStars(rating)}
<span>${rating} (${reviews}条评价)</span>
</div>
<div class="popup-price">
<strong>¥${price}</strong> / 晚起
</div>
<button class="popup-book-btn">查看详情</button>
</div>
</div>
`;
if (this.options.provider === 'baidu') {
return new BMap.InfoWindow(content, {
width: 280,
height: 180,
title: ''
});
} else if (this.options.provider === 'amap') {
return new AMap.InfoWindow({
content,
offset: new AMap.Pixel(0, -48)
});
}
}
generateStars(rating) {
const fullStars = Math.floor(rating);
const hasHalfStar = rating % 1 >= 0.5;
let stars = '★'.repeat(fullStars);
if (hasHalfStar) stars += '☆';
stars += '☆'.repeat(5 - fullStars - (hasHalfStar ? 1 : 0));
return stars;
}
// 批量添加周边设施标记
addNearbyFacilities(facilities) {
const markers = facilities.map(facility => {
const marker = this.createFacilityMarker(facility);
this.mapInstance.addOverlay(marker);
return marker;
});
// 聚合近距离的标记点
this.clusterNearbyMarkers(markers);
return markers;
}
createFacilityMarker(facility) {
const { lng, lat, type, name } = facility;
const iconUrls = {
restaurant: 'data:image/svg+xml,...', // 餐厅图标
subway: 'data:image/svg+xml,...', // 地铁图标
shopping: 'data:image/svg+xml,...', // 购物图标
attraction: 'data:image/svg+xml,...', // 景点图标
hospital: 'data:image/svg+xml,...', // 医院图标
bank: 'data:image/svg+xml,...' // 银行图标
};
const iconUrl = iconUrls[type] || iconUrls.attraction;
if (this.options.provider === 'baidu') {
const point = new BMap.Point(lng, lat);
const icon = new BMap.Icon(iconUrl, new BMap.Size(24, 24));
return new BMap.Marker(point, { icon });
} else if (this.options.provider === 'amap') {
const position = [lng, lat];
return new AMap.Marker({
position,
icon: new AMap.Icon({
size: new AMap.Size(24, 24),
image: iconUrl,
imageSize: new AMap.Size(24, 24)
})
});
}
}
clusterNearbyMarkers(markers) {
const clusterRadius = 50; // 聚类半径(像素)
// 简化的聚类算法
const clusters = [];
markers.forEach((marker, index) => {
let addedToCluster = false;
for (const cluster of clusters) {
const distance = this.calculateMarkerDistance(marker, cluster.center);
if (distance < clusterRadius) {
cluster.markers.push(marker);
cluster.count++;
addedToCluster = true;
break;
}
}
if (!addedToCluster) {
clusters.push({
center: marker,
markers: [marker],
count: 1
});
}
});
// 为每个聚类创建聚合标记
clusters.forEach(cluster => {
if (cluster.count > 1) {
this.createClusterMarker(cluster);
// 隐藏原始标记
cluster.markers.forEach(m => m.hide());
}
});
}
calculateMarkerDistance(marker1, marker2) {
// 简化的距离计算(实际应用中应使用经纬度计算)
return Math.random() * 100; // 示例实现
}
createClusterMarker(cluster) {
const { center, count } = cluster;
if (this.options.provider === 'baidu') {
const point = center.getPosition();
const icon = new BMap.Icon(
this.generateClusterIcon(count),
new BMap.Size(40, 40)
);
const clusterMarker = new BMap.Marker(point, { icon });
this.mapInstance.addOverlay(clusterMarker);
} else if (this.options.provider === 'amap') {
const position = center.getPosition();
const marker = new AMap.Marker({
position,
icon: new AMap.Icon({
size: new AMap.Size(40, 40),
image: this.generateClusterIcon(count),
imageSize: new AMap.Size(40, 40)
})
});
this.mapInstance.addOverlay(marker);
}
}
generateClusterIcon(count) {
const canvas = document.createElement('canvas');
canvas.width = 40;
canvas.height = 40;
const ctx = canvas.getContext('2d');
// 绘制圆形背景
ctx.fillStyle = '#4ECDC4';
ctx.beginPath();
ctx.arc(20, 20, 20, 0, Math.PI * 2);
ctx.fill();
// 绘制数量
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 14px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(count > 99 ? '99+' : count.toString(), 20, 20);
return canvas.toDataURL();
}
// 地图样式优化
applyMapStyle(styleName) {
const styles = {
light: this.getLightStyle(),
dark: this.getDarkStyle(),
minimal: this.getMinimalStyle()
};
const selectedStyle = styles[styleName] || styles.light;
if (this.options.provider === 'baidu') {
this.mapInstance.setMapStyleV2({
styleJson: selectedStyle
});
} else if (this.options.provider === 'amap') {
this.mapInstance.setMapStyle(selectedStyle);
}
}
getLightStyle() {
return [
{
featureType: 'all',
elementType: 'all',
stylers: {
lightness: 10,
saturation: -10
}
}
];
}
getDarkStyle() {
return [
{
featureType: 'all',
elementType: 'all',
stylers: {
hue: '#00ffff',
lightness: -15,
saturation: -60
}
}
];
}
getMinimalStyle() {
return [
{
featureType: 'poi',
elementType: 'labels',
stylers: { visibility: 'off' }
},
{
featureType: 'road',
elementType: 'labels',
stylers: { visibility: 'off' }
},
{
featureType: 'transit',
elementType: 'labels',
stylers: { visibility: 'off' }
}
];
}
// 销毁地图实例
destroy() {
if (this.mapInstance) {
if (this.options.provider === 'baidu') {
this.mapInstance.clearOverlays();
this.mapInstance = null;
} else if (this.options.provider === 'amap') {
this.mapInstance.destroy();
this.mapInstance = null;
}
}
this.isLoaded = false;
this.loadPromise = null;
}
}3.2 地图懒加载与虚拟滚动
// 花筑地图虚拟滚动管理器
class FloralMapVirtualScroller {
constructor(mapContainer, mapOptimizer, options = {}) {
this.mapContainer = mapContainer;
this.mapOptimizer = mapOptimizer;
this.options = {
bufferZone: 2, // 缓冲区大小
minZoom: 12,
maxZoom: 18,
...options
};
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
this.visibleMarkers = new Set();
this.markerPool = [];
this.isMapReady = false;
this.currentBounds = null;
this.facilities = [];
}
// 初始化虚拟滚动
async init() {
// 等待地图加载完成
await this.mapOptimizer.loadMap();
this.isMapReady = true;
// 监听地图移动和缩放
this.setupMapListeners();
// 初始加载可见标记
this.updateVisibleMarkers();
}
setupMapListeners() {
const map = this.mapOptimizer.mapInstance;
if (this.mapOptimizer.options.provider === 'baidu') {
map.addEventListener('moving', () => this.onMapMove());
map.addEventListener('zoomend', () => this.onMapMove());
} else if (this.mapOptimizer.options.provider === 'amap') {
map.on('moveend', () => this.onMapMove());
map.on('zoomend', () => this.onMapMove());
}
}
onMapMove() {
// 使用防抖优化
if (this.moveTimeout) {
clearTimeout(this.moveTimeout);
}
this.moveTimeout = setTimeout(() => {
this.updateVisibleMarkers();
}, 150);
}
// 设置设施数据
setFacilities(facilities) {
this.facilities = facilities;
// 初始化标记池
this.initializeMarkerPool();
// 更新可见标记
if (this.isMapReady) {
this.updateVisibleMarkers();
}
}
initializeMarkerPool() {
// 创建标记对象池,避免频繁创建销毁
const poolSize = Math.min(this.facilities.length, 50);
for (let i = 0; i < poolSize; i++) {
const marker = this.createMarkerFromPool(this.facilities[i]);
marker.setVisible(false);
this.markerPool.push(marker);
}
}
createMarkerFromPool(facility) {
// 创建可复用的标记
if (this.mapOptimizer.options.provider === 'baidu') {
const point = new BMap.Point(facility.lng, facility.lat);
return new BMap.Marker(point);
} else if (this.mapOptimizer.options.provider === 'amap') {
return new AMap.Marker({
position: [facility.lng, facility.lat]
});
}
}
updateVisibleMarkers() {
if (!this.isMapReady || this.facilities.length === 0) return;
const map = this.mapOptimizer.mapInstance;
const bounds = map.getBounds();
const zoom = map.getZoom();
// 计算可见区域内的设施
const visibleFacilities = this.facilities.filter(facility => {
return bounds.containsPoint(new BMap.Point(facility.lng, facility.lat));
});
// 计算缓冲区内的设施
const bufferedFacilities = this.facilities.filter(facility => {
const isVisible = bounds.containsPoint(new BMap.Point(facility.lng, facility.lat));
const isInBuffer = this.isInBufferZone(facility, bounds, zoom);
return isVisible || isInBuffer;
});
// 隐藏不在缓冲区的标记
this.markerPool.forEach(marker => {
const facilityIndex = marker.facilityIndex;
if (facilityIndex !== undefined) {
const isStillNeeded = bufferedFacilities.some(f => f.index === facilityIndex);
if (!isStillNeeded) {
marker.setVisible(false);
this.visibleMarkers.delete(marker);
}
}
});
// 显示需要的标记
bufferedFacilities.forEach((facility, index) => {
let marker = this.markerPool.find(m => m.facilityIndex === facility.index);
if (!marker) {
// 从池中分配或创建新标记
marker = this.allocateMarkerFromPool(facility);
}
if (!marker.getVisible()) {
marker.setVisible(true);
this.visibleMarkers.add(marker);
}
// 更新标记位置(如果需要)
this.updateMarkerPosition(marker, facility);
});
// 触发可见性变化事件
this.dispatchVisibilityChange(visibleFacilities.length, bufferedFacilities.length);
}
isInBufferZone(facility, bounds, zoom) {
const bufferPixels = this.options.bufferZone * Math.pow(2, 18 - zoom);
const boundsPadding = this.pixelsToDegrees(bufferPixels, zoom);
const expandedBounds = {
sw: new BMap.Point(bounds.getSouthWest().lng - boundsPadding, bounds.getSouthWest().lat - boundsPadding),
ne: new BMap.Point(bounds.getNorthEast().lng + boundsPadding, bounds.getNorthEast().lat + boundsPadding)
};
const point = new BMap.Point(facility.lng, facility.lat);
return expandedBounds.sw.lng <= point.lng && point.lng <= expandedBounds.ne.lng &&
expandedBounds.sw.lat <= point.lat && point.lat <= expandedBounds.ne.lat;
}
pixelsToDegrees(pixels, zoom) {
// 简化的像素到经纬度转换
const earthCircumference = 40075017; // 地球周长(米)
const metersPerPixel = earthCircumference / Math.pow(2, zoom + 8);
const degreesPerMeter = 1 / 111320; // 赤道附近每米的度数
return pixels * metersPerPixel * degreesPerMeter;
}
allocateMarkerFromPool(facility) {
// 查找可用的闲置标记
let marker = this.markerPool.find(m => !m.getVisible());
if (!marker) {
// 创建新标记并添加到池中
marker = this.createMarkerFromPool(facility);
this.markerPool.push(marker);
}
marker.facilityIndex = facility.index;
return marker;
}
updateMarkerPosition(marker, facility) {
// 更新标记位置(如果需要移动)
if (this.mapOptimizer.options.provider === 'baidu') {
marker.setPosition(new BMap.Point(facility.lng, facility.lat));
} else if (this.mapOptimizer.options.provider === 'amap') {
marker.setPosition([facility.lng, facility.lat]);
}
}
dispatchVisibilityChange(visibleCount, bufferedCount) {
this.mapContainer.dispatchEvent(new CustomEvent('markersVisibilityChange', {
detail: {
visible: visibleCount,
buffered: bufferedCount,
total: this.facilities.length,
activeMarkers: this.visibleMarkers.size
}
}));
}
// 获取性能统计
getPerformanceStats() {
return {
totalFacilities: this.facilities.length,
activeMarkers: this.visibleMarkers.size,
poolSize: this.markerPool.length,
isMapReady: this.isMapReady,
currentZoom: this.mapOptimizer.mapInstance?.getZoom()
};
}
// 销毁
destroy() {
this.markerPool.forEach(marker => {
if (this.mapOptimizer.options.provider === 'baidu') {
this.mapOptimizer.mapInstance.removeOverlay(marker);
} else if (this.mapOptimizer.options.provider === 'amap') {
this.mapOptimizer.mapInstance.remove(marker);
}
});
this.markerPool = [];
this.visibleMarkers.clear();
this.facilities = [];
}
}四、多房型信息优化
4.1 花筑房型数据优化
// 花筑房型数据优化器
class FloralRoomTypeOptimizer {
constructor() {
this.roomTypeCache = new Map();
this.comparisonData = null;
}
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
// 优化房型数据
optimizeRoomTypeData(rawData) {
const optimized = {
propertyId: rawData.propertyId,
propertyName: rawData.propertyName,
baseInfo: this.extractBaseInfo(rawData),
roomTypes: this.optimizeRoomTypes(rawData.roomTypes),
comparison: this.generateComparisonData(rawData.roomTypes),
availability: this.optimizeAvailability(rawData.availability),
metadata: this.generateMetadata(rawData)
};
return optimized;
}
extractBaseInfo(data) {
return {
name: data.propertyName,
address: data.address,
location: {
lng: data.coordinates?.lng,
lat: data.coordinates?.lat
},
rating: {
overall: data.rating?.overall || 0,
reviewCount: data.rating?.reviewCount || 0,
categories: data.rating?.categories || {}
},
amenities: this.extractAmenities(data.amenities),
policies: this.extractPolicies(data.policies)
};
}
extractAmenities(amenities) {
if (!amenities) return [];
// 分类整理设施
const categories = {
'essentials': ['wifi', 'airConditioning', 'heating', 'smokeFree', 'carbonMonoxideAlarm'],
'bathroom': ['privateBathroom', 'hairDryer', 'bathtub', 'shower', 'toiletries'],
'bedroom': ['kingBed', 'queenBed', 'twinBeds', 'extraBed', 'wardrobe'],
'entertainment': ['tv', 'netflix', 'soundSystem', 'books', 'games'],
'kitchen': ['kitchen', 'refrigerator', 'microwave', 'coffeeMaker', 'cookingBasics'],
'outdoor': ['balcony', 'terrace', 'garden', 'bbq', 'parking']
};
const organizedAmenities = {};
Object.entries(categories).forEach(([category, keys]) => {
const foundAmenities = keys.filter(key => amenities[key]).map(key => ({
key,
label: this.getAmenityLabel(key),
icon: this.getAmenityIcon(key)
}));
if (foundAmenities.length > 0) {
organizedAmenities[category] = foundAmenities;
}
});
return organizedAmenities;
}
getAmenityLabel(key) {
const labels = {
wifi: '免费WiFi',
airConditioning: '空调',
heating: '暖气',
smokeFree: '禁烟',
carbonMonoxideAlarm: '一氧化碳报警器',
privateBathroom: '独立卫浴',
hairDryer: '吹风机',
bathtub: '浴缸',
shower: '淋浴',
toiletries: '洗漱用品',
kingBed: '特大床',
queenBed: '大床',
twinBeds: '双床',
extraBed: '加床',
wardrobe: '衣柜',
tv: '电视',
netflix: 'Netflix',
soundSystem: '音响系统',
books: '书籍',
games: '游戏',
kitchen: '厨房',
refrigerator: '冰箱',
microwave: '微波炉',
coffeeMaker: '咖啡机',
cookingBasics: '烹饪用具',
balcony: '阳台',
terrace: '露台',
garden: '花园',
bbq: '烧烤设施',
parking: '停车位'
};
return labels[key] || key;
}
getAmenityIcon(key) {
const icons = {
wifi: '📶',
airConditioning: '❄️',
heating: '🔥',
smokeFree: '🚭',
carbonMonoxideAlarm: '⚠️',
privateBathroom: '🚿',
hairDryer: '💨',
bathtub: '🛁',
shower: '🚿',
toiletries: '🧴',
kingBed: '🛏️',
queenBed: '🛏️',
twinBeds: '🛏️🛏️',
extraBed: '➕🛏️',
wardrobe: '👔',
tv: '📺',
netflix: '🎬',
soundSystem: '🔊',
books: '📚',
games: '🎮',
kitchen: '🍳',
refrigerator: '🧊',
microwave: '📦',
coffeeMaker: '☕',
cookingBasics: '🍽️',
balcony: '🌿',
terrace: '🌞',
garden: '🌸',
bbq: '🍖',
parking: '🚗'
};
return icons[key] || '✨';
}
extractPolicies(policies) {
if (!policies) return {};
return {
checkIn: {
startTime: policies.checkIn?.startTime || '15:00',
endTime: policies.checkIn?.endTime || '23:00',
instructions: policies.checkIn?.instructions || []
},
checkOut: {
time: policies.checkOut?.time || '12:00',
instructions: policies.checkOut?.instructions || []
},
cancellation: {
type: policies.cancellation?.type || 'moderate',
description: policies.cancellation?.description || '',
deadline: policies.cancellation?.deadline || 24
},
houseRules: policies.houseRules || [],
safetyFeatures: policies.safetyFeatures || []
};
}
optimizeRoomTypes(roomTypes) {
return roomTypes.map((room, index) => ({
id: room.id,
index,
basic: this.extractRoomBasicInfo(room),
capacity: this.extractCapacity(room),
beds: this.extractBedInfo(room),
area: room.area || 0,
images: this.optimizeRoomImages(room.images),
pricing: this.optimizePricing(room.pricing),
availability: this.optimizeRoomAvailability(room.availability),
features: this.extractRoomFeatures(room),
description: this.generateRoomDescription(room)
}));
}
extractRoomBasicInfo(room) {
return {
name: room.name,
type: room.type,
maxGuests: room.capacity?.maxGuests || 2,
bedroomCount: room.layout?.bedrooms || 1,
bathroomCount: room.layout?.bathrooms || 1,
floor: room.floor || 1
};
}
extractCapacity(room) {
const { maxGuests, adults, children, infants } = room.capacity || {};
return {
maxGuests: maxGuests || 2,
breakdown: {
adults: adults || 2,
children: children || 0,
infants: infants || 0
},
suitableFor: this.generateSuitableFor(maxGuests, adults, children)
};
}
generateSuitableFor(maxGuests, adults, children) {
const scenarios = [];
if (maxGuests === 2 && adults === 2 && children === 0) {
scenarios.push('情侣出行', '蜜月旅行');
}
if (children > 0) {
scenarios.push('亲子度假');
}
if (maxGuests >= 4) {
scenarios.push('朋友聚会', '家庭出游');
}
if (maxGuests >= 6) {
scenarios.push('团体旅行');
}
return scenarios;
}
extractBedInfo(room) {
const beds = room.beds || [];
return {
configuration: beds.map(bed => ({
type: bed.type,
count: bed.count,
size: bed.size
})),
summary: this.generateBedSummary(beds),
totalBeds: beds.reduce((sum, bed) => sum + bed.count, 0)
};
}
generateBedSummary(beds) {
if (beds.length === 0) return '暂无床型信息';
const summaryParts = beds.map(bed => {
const typeLabels = {
'king': '特大床',
'queen': '大床',
'double': '双人床',
'single': '单人床',
'sofa': '沙发床',
'floor': '地铺'
};
const sizeLabels = {
'king': '1.8m',
'queen': '1.5m',
'double': '1.5m',
'single': '1.2m',
'sofa': '1.2m',
'floor': '1.0m'
};
const typeLabel = typeLabels[bed.type] || bed.type;
const sizeLabel = sizeLabels[bed.type] || '';
return `${bed.count}张${sizeLabel}${typeLabel}`;
});
return summaryParts.join(' + ');
}
optimizeRoomImages(images) {
if (!images || images.length === 0) {
return { main: null, gallery: [], count: 0 };
}
return {
main: this.selectMainImage(images),
gallery: images.slice(1, 6).map(img => ({
id: img.id,
url: img.url,
thumbnail: this.generateThumbnailUrl(img.url),
type: img.type,
caption: img.caption
})),
count: images.length,
hasPanorama: images.some(img => img.isPanorama)
};
}
selectMainImage(images) {
// 优先选择主图或卧室图片
const preferredImages = images.filter(img =>
img.isPrimary || img.type === 'bedroom' || img.type === 'living'
);
const selected = preferredImages[0] || images[0];
return {
id: selected.id,
url: selected.url,
thumbnail: this.generateThumbnailUrl(selected.url),
type: selected.type,
caption: selected.caption
};
}
generateThumbnailUrl(url) {
if (!url) return '';
const params = new URLSearchParams();
params.set('w', 300);
params.set('h', 200);
params.set('fit', 'cover');
params.set('q', 75);
return `${url}?${params.toString()}`;
}
optimizePricing(ricing) {
if (!pricing) return null;
return {
basePrice: pricing.basePrice || 0,
currency: pricing.currency || 'CNY',
displayPrice: this.formatPrice(ricing.basePrice, pricing.currency),
pricePerGuest: pricing.pricePerGuest || null,
cleaningFee: pricing.cleaningFee || 0,
serviceFee: pricing.serviceFee || 0,
taxes: pricing.taxes || 0,
discounts: this.extractDiscounts(ricing.discounts),
seasonalPricing: pricing.seasonalPricing || []
};
}
formatPrice(price, currency) {
const symbols = { CNY: '¥', USD: '$', EUR: '€', JPY: '¥' };
const symbol = symbols[currency] || currency;
if (currency === 'JPY') {
return `${symbol}${Math.round(price).toLocaleString()}`;
}
return `${symbol}${price.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;
}
extractDiscounts(discounts) {
if (!discounts || discounts.length === 0) return [];
return discounts.map(discount => ({
type: discount.type,
value: discount.value,
description: discount.description,
validUntil: discount.validUntil
})).filter(d => d.validUntil && new Date(d.validUntil) > new Date());
}
optimizeRoomAvailability(availability) {
if (!availability) return null;
return {
isAvailable: availability.isAvailable || false,
minStay: availability.minStay || 1,
maxStay: availability.maxStay || 30,
blockedDates: availability.blockedDates || [],
availableDates: availability.availableDates || []
};
}
extractRoomFeatures(room) {
const features = [];
if (room.area > 50) features.push('宽敞空间');
if (room.bathroomCount > 1) features.push('双卫设计');
if (room.view) features.push(`景观:${room.view}`);
if (room.balcony) features.push('私人阳台');
if (room.kitchen) features.push('配备厨房');
if (room.workspace) features.push('工作区域');
if (room.smartHome) features.push('智能家居');
return features;
}
generateRoomDescription(room) {
const parts = [];
parts.push(`${room.name}是一间${room.layout?.bedrooms || 1}卧${room.layout?.bathrooms || 1}卫的舒适客房,`);
parts.push(`最多可容纳${room.capacity?.maxGuests || 2}位客人。`);
parts.push(`房间面积约${room.area || '未知'}平方米,`);
parts.push(`配备${this.generateBedSummary(room.beds || [])}。`);
if (room.features && room.features.length > 0) {
parts.push(`特色设施包括:${room.features.slice(0, 3).join('、')}。`);
}
return parts.join('');
}
generateComparisonData(roomTypes) {
if (!roomTypes || roomTypes.length <= 1) {
return null;
}
const comparison = {
byPrice: [...roomTypes].sort((a, b) => a.pricing.basePrice - b.pricing.basePrice),
byCapacity: [...roomTypes].sort((a, b) => b.capacity.maxGuests - a.capacity.maxGuests),
byArea: [...roomTypes].sort((a, b) => b.area - a.area),
byRating: [...roomTypes].sort((a, b) => (b.rating?.overall || 0) - (a.rating?.overall || 0))
};
// 生成对比矩阵
comparison.matrix = roomTypes.map((room, index) => ({
id: room.id,
index,
name: room.name,
price: room.pricing?.basePrice || 0,
capacity: room.capacity?.maxGuests || 2,
area: room.area || 0,
bedrooms: room.layout?.bedrooms || 1,
bathrooms: room.layout?.bathrooms || 1,
rating: room.rating?.overall || 0,
amenities: this.countAmenities(room.amenities)
}));
return comparison;
}
countAmenities(amenities) {
if (!amenities) return 0;
return Object.values(amenities).filter(Boolean).length;
}
optimizeAvailability(availability) {
if (!availability) return null;
return {
propertyId: availability.propertyId,
rooms: availability.rooms.map(room => ({
roomTypeId: room.roomTypeId,
dates: room.dates.map(date => ({
date: date.date,
isAvailable: date.isAvailable,
price: date.price,
remainingRooms: date.remainingRooms
}))
})),
lastUpdated: availability.lastUpdated
};
}
generateMetadata(data) {
return {
generatedAt: Date.now(),
version: '1.0',
roomTypeCount: data.roomTypes?.length || 0,
amenityCategories: Object.keys(this.extractAmenities(data.amenities || {})),
hasAvailability: !!data.availability,
hasComparison: (data.roomTypes?.length || 0) > 1
};
}
// 获取房型对比信息
getRoomComparison(roomTypeId1, roomTypeId2) {
if (!this.comparisonData) return null;
const room1 = this.comparisonData.matrix.find(r => r.id === roomTypeId1);
const room2 = this.comparisonData.matrix.find(r => r.id === roomTypeId2);
if (!room1 || !room2) return null;
return {
room1,
room2,
differences: this.calculateDifferences(room1, room2)
};
}
calculateDifferences(room1, room2) {
const differences = [];
if (room1.price !== room2.price) {
differences.push({
field: '价格',
room1Value: `¥${room1.price}`,
room2Value: `¥${room2.price}`,
difference: room2.price - room1.price
});
}
if (room1.capacity !== room2.capacity) {
differences.push({
field: '容量',
room1Value: `${room1.capacity}人`,
room2Value: `${room2.capacity}人`,
difference: room2.capacity - room1.capacity
});
}
if (room1.area !== room2.area) {
differences.push({
field: '面积',
room1Value: `${room1.area}㎡`,
room2Value: `${room2.area}㎡`,
difference: room2.area - room1.area
});
}
if (room1.bedrooms !== room2.bedrooms) {
differences.push({
field: '卧室',
room1Value: `${room1.bedrooms}间`,
room2Value: `${room2.bedrooms}间`,
difference: room2.bedrooms - room1.bedrooms
});
}
if (room1.bathrooms !== room2.bathrooms) {
differences.push({
field: '卫浴',
room1Value: `${room1.bathrooms}间`,
room2Value: `${room2.bathrooms}间`,
difference: room2.bathrooms - room1.bathrooms
});
}
return differences;
}
// 缓存管理
cacheRoomTypeData(propertyId, data) {
this.roomTypeCache.set(propertyId, {
data,
timestamp: Date.now()
});
}
getCachedRoomTypeData(propertyId) {
const cached = this.roomTypeCache.get(propertyId);
if (!cached) return null;
// 检查缓存有效期(5分钟)
const maxAge = 5 * 60 * 1000;
if (Date.now() - cached.timestamp > maxAge) {
this.roomTypeCache.delete(propertyId);
return null;
}
return cached.data;
}
clearCache() {
this.roomTypeCache.clear();
}
}4.2 房型卡片虚拟列表
// 花筑房型卡片虚拟列表组件
class FloralRoomTypeVirtualList {
constructor(container, optimizer, options = {}) {
this.container = container;
this.optimizer = optimizer;
this.options = {
itemHeight: 380,
bufferSize: 3,
overscan: 2,
enableComparison: true,
...options
};
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
this.roomTypes = [];
this.visibleCards = [];
this.scrollTop = 0;
this.containerHeight = 0;
this.selectedRoomIndex = -1;
this.compareMode = false;
this.compareList = [];
this.init();
}
init() {
this.createDOMStructure();
this.bindEvents();
this.calculateDimensions();
}
createDOMStructure() {
this.container.innerHTML = `
<div class="floral-room-virtual-list">
<div class="list-header">
<h2>选择房型</h2>
<div class="list-actions">
<label class="compare-toggle">
<input type="checkbox" id="compareModeToggle">
<span>对比模式</span>
</label>
<span class="room-count">共 <span id="roomCount">0</span> 种房型</span>
</div>
</div>
<div class="list-content" style="height: 0px; position: relative;">
</div>
<div class="list-comparison-bar" style="display: none;">
<div class="comparison-header">
<span>已选择 <span id="compareCount">0</span> 个房型对比</span>
<button class="btn-compare" id="btnCompare">开始对比</button>
<button class="btn-clear" id="btnClearCompare">清除</button>
</div>
<div class="comparison-items" id="comparisonItems"></div>
</div>
<div class="list-loading" style="display: none;">
<div class="spinner"></div>
<span>加载房型信息...</span>
</div>
</div>
`;
this.listContent = this.container.querySelector('.list-content');
this.listHeader = this.container.querySelector('.list-header');
this.comparisonBar = this.container.querySelector('.list-comparison-bar');
this.loadingIndicator = this.container.querySelector('.list-loading');
}
bindEvents() {
// 滚动事件
this.scrollHandler = this.handleScroll.bind(this);
this.container.addEventListener('scroll', this.scrollHandler, { passive: true });
// 窗口大小变化
this.resizeHandler = this.handleResize.bind(this);
window.addEventListener('resize', this.resizeHandler, { passive: true });
// 对比模式切换
const compareToggle = this.container.querySelector('#compareModeToggle');
compareToggle.addEventListener('change', (e) => {
this.toggleCompareMode(e.target.checked);
});
// 对比按钮
this.container.querySelector('#btnCompare').addEventListener('click', () => {
this.showComparisonModal();
});
// 清除对比
this.container.querySelector('#btnClearCompare').addEventListener('click', () => {
this.clearComparison();
});
// 触摸优化
this.touchStartY = 0;
this.container.addEventListener('touchstart', (e) => {
this.touchStartY = e.touches[0].clientY;
}, { passive: true });
}
calculateDimensions() {
this.containerHeight = this.container.clientHeight;
this.updateTotalHeight();
}
handleScroll() {
const newScrollTop = this.container.scrollTop;
if (Math.abs(newScrollTop - this.scrollTop) > 10) {
this.scrollTop = newScrollTop;
this.updateVisibleCards();
}
}
handleResize() {
this.calculateDimensions();
this.updateVisibleCards();
}
// 设置房型数据
setRoomTypes(roomTypes) {
this.roomTypes = roomTypes;
this.updateTotalHeight();
this.updateVisibleCards();
this.updateRoomCount();
}
updateTotalHeight() {
const totalHeight = this.roomTypes.length * this.options.itemHeight;
this.listContent.style.height = `${totalHeight}px`;
}
updateRoomCount() {
const countEl = this.container.querySelector('#roomCount');
if (countEl) {
countEl.textContent = this.roomTypes.length;
}
}
updateVisibleCards() {
const { itemHeight, bufferSize, overscan } = this.options;
// 计算可见范围
const startIndex = Math.max(0, Math.floor(this.scrollTop / itemHeight) - bufferSize);
const endIndex = Math.min(
this.roomTypes.length,
Math.ceil((this.scrollTop + this.containerHeight) / itemHeight) + bufferSize
);
// 计算实际需要渲染的范围
const renderStart = Math.max(0, startIndex - overscan);
const renderEnd = Math.min(this.roomTypes.length, endIndex + overscan);
// 获取新的可见卡片
const newVisibleCards = [];
for (let i = renderStart; i < renderEnd; i++) {
newVisibleCards.push({
room: this.roomTypes[i],
index: i,
top: i * itemHeight
});
}
// 比较并更新DOM
this.updateDOM(newVisibleCards, startIndex, endIndex);
}
updateDOM(newVisibleCards, startIndex, endIndex) {
const fragment = document.createDocumentFragment();
const existingElements = new Map();
// 收集现有元素
this.listContent.querySelectorAll('.room-card').forEach(el => {
const index = parseInt(el.dataset.index);
existingElements.set(index, el);
});
// 创建或更新元素
newVisibleCards.forEach(({ room, index, top }) => {
let element = existingElements.get(index);
if (!element) {
element = this.createRoomCard(room, index);
fragment.appendChild(element);
} else {
element.style.transform = `translateY(${top}px)`;
existingElements.delete(index);
}
element.dataset.index = index;
element.style.height = `${this.options.itemHeight}px`;
element.style.transform = `translateY(${top}px)`;
});
// 移除不再需要的元素
existingElements.forEach((element) => {
element.remove();
});
// 批量添加新元素
if (fragment.children.length > 0) {
this.listContent.appendChild(fragment);
}
// 更新可见卡片列表
this.visibleCards = newVisibleCards.filter(
({ index }) => index >= startIndex && index < endIndex
);
// 触发可见性变化事件
this.dispatchVisibilityChange(startIndex, endIndex);
}
createRoomCard(room, index) {
const element = document.createElement('div');
element.className = 'room-card';
element.dataset.index = index;
element.dataset.roomId = room.id;
element.innerHTML = `
<div class="room-card-inner">
<!-- 图片区域 -->
<div class="room-image-section">
<div class="room-image-wrapper">
<img src="${room.images.main.thumbnail}"
data-full="${room.images.main.url}"
alt="${room.basic.name}"
class="room-image"
loading="lazy">
<div class="room-badges">
${room.features.slice(0, 2).map(f => `<span class="badge">${f}</span>`).join('')}
</div>
<div class="room-type-label">${room.basic.type}</div>
</div>
<div class="room-gallery-preview">
${room.images.gallery.slice(0, 3).map(img => `
<img src="${img.thumbnail}" alt="" class="gallery-thumb">
`).join('')}
${room.images.count > 4 ? `<span class="more-photos">+${room.images.count - 4}</span>` : ''}
</div>
</div>
<!-- 信息区域 -->
<div class="room-info-section">
<h3 class="room-name">${room.basic.name}</h3>
<div class="room-capacity">
<span class="capacity-icon">👥</span>
<span>最多${room.capacity.maxGuests}人 · ${room.beds.summary}</span>
</div>
<div class="room-specs">
<div class="spec">
<span class="spec-icon">📐</span>
<span>${room.area}㎡</span>
</div>
<div class="spec">
<span class="spec-icon">🛏️</span>
<span>${room.basic.bedroomCount}卧${room.basic.bathroomCount}卫</span>
</div>
<div class="spec">
<span class="spec-icon">🏢</span>
<span>${room.basic.floor}楼</span>
</div>
</div>
<div class="room-amenities-preview">
${this.renderAmenitiesPreview(room.features)}
</div>
</div>
<!-- 价格和操作区域 -->
<div class="room-action-section">
<div class="room-rating">
<div class="stars">${this.renderStars(room.basic.rating || 0)}</div>
<span class="rating-text">${room.basic.rating || '暂无评分'}</span>
</div>
<div class="room-price">
<div class="price-amount">${room.pricing.displayPrice}</div>
<div class="price-unit">/ 晚起</div>
${room.pricing.discounts.length > 0 ?
`<div class="price-discount">省${this.getMaxDiscount(room.pricing.discounts)}</div>` : ''}
</div>
<div class="room-actions">
<button class="btn-view-details" data-index="${index}">查看详情</button>
<button class="btn-select" data-index="${index}">选择此房型</button>
${this.options.enableComparison ?
`<button class="btn-compare-add" data-index="${index}">+ 对比</button>` : ''}
</div>
</div>
<!-- 对比模式复选框 -->
${this.options.enableComparison ? `
<div class="compare-checkbox">
<input type="checkbox" id="compare-${index}" data-index="${index}">
<label for="compare-${index}">加入对比</label>
</div>
` : ''}
</div>
`;
// 绑定事件
this.bindCardEvents(element, room, index);
return element;
}
renderAmenitiesPreview(features) {
if (!features || features.length === 0) return '';
return features.slice(0, 4).map(feature => `
<span class="amenity-tag">${feature}</span>
`).join('');
}
renderStars(rating) {
const fullStars = Math.floor(rating);
const hasHalfStar = rating % 1 >= 0.5;
let stars = '★'.repeat(fullStars);
if (hasHalfStar) stars += '☆';
stars += '☆'.repeat(5 - fullStars - (hasHalfStar ? 1 : 0));
return stars;
}
getMaxDiscount(discounts) {
if (!discounts || discounts.length === 0) return '';
const maxDiscount = discounts.reduce((max, d) =>
d.value > max.value ? d : max
);
return `${maxDiscount.value}%`;
}
bindCardEvents(element, room, index) {
// 查看详情
element.querySelector('.btn-view-details')?.addEventListener('click', (e) => {
e.stopPropagation();
this.onViewDetails(room, index);
});
// 选择房型
element.querySelector('.btn-select')?.addEventListener('click', (e) => {
e.stopPropagation();
this.onSelectRoom(room, index);
});
// 加入对比
element.querySelector('.btn-compare-add')?.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleRoomComparison(index);
});
// 对比复选框
const compareCheckbox = element.querySelector(`#compare-${index}`);
if (compareCheckbox) {
compareCheckbox.addEventListener('change', (e) => {
if (e.target.checked) {
this.addToComparison(index);
} else {
this.removeFromComparison(index);
}
});
}
// 卡片点击
element.addEventListener('click', () => {
this.onCardClick(room, index);
});
}
onViewDetails(room, index) {
this.container.dispatchEvent(new CustomEvent('viewDetails', {
detail: { room, index }
}));
}
onSelectRoom(room, index) {
this.selectedRoomIndex = index;
this.container.dispatchEvent(new CustomEvent('selectRoom', {
detail: { room, index }
}));
}
onCardClick(room, index) {
// 可以添加卡片展开等交互
}
toggleCompareMode(enabled) {
this.compareMode = enabled;
const cards = this.container.querySelectorAll('.room-card');
cards.forEach(card => {
const checkbox = card.querySelector('.compare-checkbox');
if (checkbox) {
checkbox.style.display = enabled ? 'block' : 'none';
}
});
if (!enabled) {
this.clearComparison();
}
}
toggleRoomComparison(index) {
const room = this.roomTypes[index];
const isSelected = this.compareList.includes(index);
if (isSelected) {
this.removeFromComparison(index);
} else {
if (this.compareList.length >= 3) {
alert('最多可选择3个房型进行对比');
return;
}
this.addToComparison(index);
}
}
addToComparison(index) {
if (this.compareList.includes(index)) return;
this.compareList.push(index);
this.updateComparisonUI();
}
removeFromComparison(index) {
const idx = this.compareList.indexOf(index);
if (idx > -1) {
this.compareList.splice(idx, 1);
this.updateComparisonUI();
}
}
updateComparisonUI() {
const comparisonBar = this.comparisonBar;
const compareCount = this.container.querySelector('#compareCount');
const comparisonItems = this.container.querySelector('#comparisonItems');
const btnCompare = this.container.querySelector('#btnCompare');
if (this.compareList.length > 0) {
comparisonBar.style.display = 'block';
compareCount.textContent = this.compareList.length;
comparisonItems.innerHTML = this.compareList.map(index => {
const room = this.roomTypes[index];
return `
<div class="comparison-item" data-index="${index}">
<img src="${room.images.main.thumbnail}" alt="${room.basic.name}">
<span class="item-name">${room.basic.name}</span>
<button class="btn-remove" data-index="${index}">×</button>
</div>
`;
}).join('');
btnCompare.disabled = this.compareList.length < 2;
} else {
comparisonBar.style.display = 'none';
}
// 更新复选框状态
this.compareList.forEach(index => {
const checkbox = this.container.querySelector(`#compare-${index}`);
if (checkbox) {
checkbox.checked = true;
}
});
}
clearComparison() {
this.compareList = [];
this.updateComparisonUI();
// 取消所有复选框
this.container.querySelectorAll('.compare-checkbox input').forEach(cb => {
cb.checked = false;
});
}
showComparisonModal() {
if (this.compareList.length < 2) {
alert('请至少选择2个房型进行对比');
return;
}
const comparedRooms = this.compareList.map(index => this.roomTypes[index]);
this.container.dispatchEvent(new CustomEvent('showComparison', {
detail: { rooms: comparedRooms, indices: this.compareList }
}));
}
dispatchVisibilityChange(startIndex, endIndex) {
this.container.dispatchEvent(new CustomEvent('cardsVisibilityChange', {
detail: {
startIndex,
endIndex,
visibleCount: this.visibleCards.length,
totalCount: this.roomTypes.length
}
}));
}
// 滚动到指定房型
scrollToRoom(index, behavior = 'smooth') {
const offsetTop = index * this.options.itemHeight;
this.container.scrollTo({
top: offsetTop,
behavior
});
}
// 显示/隐藏加载指示器
setLoading(loading) {
this.loadingIndicator.style.display = loading ? 'flex' : 'none';
}
// 销毁组件
destroy() {
this.container.removeEventListener('scroll', this.scrollHandler);
window.removeEventListener('resize', this.resizeHandler);
this.listContent.innerHTML = '';
}
}五、性能监控与优化效果
5.1 花筑性能监控
// 花筑性能监控器
class FloralPerformanceMonitor {
constructor(config = {}) {
this.config = {
endpoint: '/api/floral/performance/report',
sampleRate: 0.2,
enableBusinessMetrics: true,
...config
};
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
this.sessionId = this.generateSessionId();
this.metrics = {};
this.businessEvents = [];
this.floralSpecificMetrics = {};
this.init();
}
generateSessionId() {
return `floral_session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
init() {
// 核心Web指标
this.measureCoreWebVitals();
// 花筑特定指标
this.measureFloralSpecificMetrics();
// 业务指标
if (this.config.enableBusinessMetrics) {
this.measureBusinessMetrics();
}
// 资源监控
this.measureResources();
// 定期上报
this.setupReporting();
}
measureCoreWebVitals() {
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.recordMetric('lcp', {
value: lastEntry.startTime,
element: this.getElementTag(lastEntry.element),
size: lastEntry.size,
timestamp: Date.now()
});
}).observe({ entryTypes: ['largest-contentful-paint'] });
// FID
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.recordMetric('fid', {
value: entry.processingStart - entry.startTime,
eventType: entry.name,
timestamp: Date.now()
});
}
}).observe({ entryTypes: ['first-input'] });
// CLS
let clsScore = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
}
this.recordMetric('cls', {
value: clsScore,
timestamp: Date.now()
});
}).observe({ entryTypes: ['layout-shift'] });
}
measureFloralSpecificMetrics() {
// 房间图片加载时间
this.measureRoomImageLoadTime();
// 地图初始化时间
this.measureMapInitTime();
// 房型列表渲染时间
this.measureRoomListRenderTime();
// 用户交互响应时间
this.measureInteractionResponseTime();
}
measureRoomImageLoadTime() {
const markName = 'floral-room-image-start';
performance.mark(markName);
// 监听主房间图片加载
const mainImage = document.querySelector('.room-gallery .main-image');
if (mainImage) {
if (mainImage.complete) {
this.recordFloralMetric('room-image-load-time', {
value: performance.now() - performance.getEntriesByName(markName)[0]?.startTime,
type: 'main-image',
timestamp: Date.now()
});
} else {
mainImage.addEventListener('load', () => {
this.recordFloralMetric('room-image-load-time', {
value: performance.now() - performance.getEntriesByName(markName)[0]?.startTime,
type: 'main-image',
timestamp: Date.now()
});
});
}
}
}
measureMapInitTime() {
const markName = 'floral-map-init-start';
performance.mark(markName);
// 监听地图就绪事件
document.addEventListener('mapready', () => {
this.recordFloralMetric('map-init-time', {
value: performance.now() - performance.getEntriesByName(markName)[0]?.startTime,
provider: this.getMapProvider(),
timestamp: Date.now()
});
}, { once: true });
}
getMapProvider() {
if (window.BMap) return 'baidu';
if (window.AMap) return 'amap';
return 'unknown';
}
measureRoomListRenderTime() {
const markName = 'floral-room-list-render-start';
performance.mark(markName);
// 使用MutationObserver检测房型列表渲染完成
const observer = new MutationObserver((mutations) => {
const roomCards = document.querySelectorAll('.room-card');
if (roomCards.length > 0) {
observer.disconnect();
this.recordFloralMetric('room-list-render-time', {
value: performance.now() - performance.getEntriesByName(markName)[0]?.startTime,
roomCount: roomCards.length,
timestamp: Date.now()
});
}
});
observer.observe(document.querySelector('.room-list-container') || document.body, {
childList: true,
subtree: true
});
}
measureInteractionResponseTime() {
// 测量用户交互响应
const interactionTypes = ['click', 'touchstart', 'keydown'];
interactionTypes.forEach(type => {
document.addEventListener(type, (e) => {
const markName = `interaction-${type}-${Date.now()}`;
performance.mark(markName);
// 使用requestAnimationFrame测量响应
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const measureName = `interaction-response-${markName}`;
performance.measure(measureName, markName);
const measures = performance.getEntriesByName(measureName);
if (measures.length > 0) {
this.recordFloralMetric('interaction-response-time', {
value: measures[0].duration,
type,
target: this.getElementIdentifier(e.target),
timestamp: Date.now()
});
}
});
});
}, { passive: true, capture: true });
});
}
measureBusinessMetrics() {
// 页面停留时间
this.pageStartTime = Date.now();
// 滚动深度
this.maxScrollDepth = 0;
window.addEventListener('scroll', () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const docHeight = document.documentElement.scrollHeight;
const winHeight = window.innerHeight;
const scrollDepth = Math.round((scrollTop / (docHeight - winHeight)) * 100);
if (scrollDepth > this.maxScrollDepth) {
this.maxScrollDepth = scrollDepth;
if ([25, 50, 75, 100].includes(scrollDepth)) {
this.recordBusinessEvent('scroll-depth', { depth: scrollDepth });
}
}
}, { passive: true });
// 房型选择
document.addEventListener('selectRoom', (e) => {
this.recordBusinessEvent('room-selected', {
roomId: e.detail.room.id,
roomName: e.detail.room.basic.name,
index: e.detail.index
});
});
// 地图交互
document.addEventListener('mapready', () => {
this.recordBusinessEvent('map-interaction', { action: 'map-loaded' });
});
// 图片查看
document.addEventListener('viewImage', (e) => {
this.recordBusinessEvent('image-viewed', {
imageId: e.detail.imageId,
type: e.detail.type
});
});
}
measureResources() {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (this.shouldTrackResource(entry)) {
this.recordMetric('resource', {
name: entry.name,
duration: entry.duration,
transferSize: entry.transferSize,
decodedBodySize: entry.decodedBodySize,
initiatorType: entry.initiatorType,
timestamp: Date.now()
});
}
}
}).observe({ entryTypes: ['resource'] });
}
shouldTrackResource(entry) {
const ignoredDomains = [
'google-analytics', 'googletagmanager', 'facebook', 'twitter'
];
return !ignoredDomains.some(domain =>
entry.name.toLowerCase().includes(domain)
);
}
recordMetric(type, data) {
if (!this.metrics[type]) {
this.metrics[type] = [];
}
this.metrics[type].push({
...data,
sessionId: this.sessionId,
page: window.location.pathname,
userAgent: navigator.userAgent
});
if (this.metrics[type].length > 50) {
this.metrics[type] = this.metrics[type].slice(-50);
}
}
recordFloralMetric(type, data) {
if (!this.floralSpecificMetrics[type]) {
this.floralSpecificMetrics[type] = [];
}
this.floralSpecificMetrics[type].push({
...data,
sessionId: this.sessionId,
page: window.location.pathname,
userAgent: navigator.userAgent,
deviceInfo: this.getDeviceInfo()
});
if (this.floralSpecificMetrics[type].length > 30) {
this.floralSpecificMetrics[type] = this.floralSpecificMetrics[type].slice(-30);
}
}
recordBusinessEvent(eventName, data) {
this.businessEvents.push({
event: eventName,
data,
sessionId: this.sessionId,
page: window.location.pathname,
timestamp: Date.now()
});
if (this.businessEvents.length > 100) {
this.businessEvents = this.businessEvents.slice(-100);
}
}
getDeviceInfo() {
return {
screenResolution: `${screen.width}x${screen.height}`,
viewportSize: `${window.innerWidth}x${window.innerHeight}`,
pixelRatio: window.devicePixelRatio || 1,
platform: navigator.platform,
memory: navigator.deviceMemory || 'unknown',
cores: navigator.hardwareConcurrency || 'unknown',
connection: this.getConnectionInfo()
};
}
getConnectionInfo() {
const connection = navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection;
if (!connection) return null;
return {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData
};
}
getElementIdentifier(element) {
if (!element || element === document.documentElement) {
return 'document';
}
const tagName = element.tagName.toLowerCase();
const id = element.id ? `#${element.id}` : '';
const classes = element.className
? '.' + element.className.split(' ').filter(c => c && !c.startsWith('track-')).join('.')
: '';
return `${tagName}${id}${classes}`.substring(0, 100);
}
getElementTag(element) {
if (!element) return 'unknown';
return element.tagName?.toLowerCase() || 'unknown';
}
setupReporting() {
// 定期上报
setInterval(() => {
this.reportMetrics();
}, 60000);
// 页面卸载时上报
window.addEventListener('beforeunload', () => {
this.reportMetrics(true);
});
// 检查是否需要立即上报
this.checkAndReport();
}
checkAndReport() {
const totalMetrics = Object.values(this.metrics).reduce((sum, arr) => sum + arr.length, 0);
const totalFloralMetrics = Object.values(this.floralSpecificMetrics).reduce((sum, arr) => sum + arr.length, 0);
if (totalMetrics >= 20 || totalFloralMetrics >= 10) {
this.reportMetrics();
}
}
async reportMetrics(isUnload = false) {
const hasData = Object.keys(this.metrics).length > 0 ||
Object.keys(this.floralSpecificMetrics).length > 0 ||
this.businessEvents.length > 0;
if (!hasData) return;
// 按采样率过滤
if (Math.random() > this.config.sampleRate) {
this.resetMetrics();
return;
}
const data = {
sessionId: this.sessionId,
page: window.location.pathname,
timestamp: Date.now(),
coreMetrics: this.metrics,
floralSpecificMetrics: this.floralSpecificMetrics,
businessEvents: this.businessEvents,
deviceInfo: this.getDeviceInfo(),
sessionDuration: Date.now() - this.pageStartTime
};
try {
if (isUnload) {
navigator.sendBeacon(
this.config.endpoint,
JSON.stringify(data)
);
} else {
await fetch(this.config.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
keepalive: true
});
}
} catch (error) {
console.error('Failed to report metrics:', error);
} finally {
this.resetMetrics();
}
}
resetMetrics() {
this.metrics = {};
this.floralSpecificMetrics = {};
this.businessEvents = [];
}
}5.2 花筑优化效果
┌─────────────────────────────────────────────────────────────────┐ │ 花筑详情页优化效果对比 │ ├─────────────┬─────────────┬─────────────┬──────────────┤ │ 指标 │ 优化前 │ 优化后 │ 提升幅度 │ ├─────────────┼─────────────┼─────────────┼──────────────┤ │ LCP(ms) │ 4200 │ 2100 │ +50% ↓ │ │ FID(ms) │ 280 │ 110 │ +61% ↓ │ │ CLS │ 0.28 │ 0.08 │ +71% ↓ │ │ 首屏图片(s) │ 3.5 │ 1.2 │ +66% ↓ │ │ 地图加载(s) │ 2.8 │ 1.1 │ +61% ↓ │ │ 房型渲染(ms)│ 520 │ 180 │ +65% ↓ │ │ 图片缓存命中│ 15% │ 78% │ +420% ↑ │ │ 地图瓦片数 │ 156 │ 89 │ +43% ↓ │ │ 包体积(KB) │ 920 │ 480 │ +48% ↓ │ └─────────────┴─────────────┴──────────────┴──────────────┘
5.3 业务指标改善
- 房型选择转化率: 从 1.8% 提升至 2.9% (+61%)
- 地图交互率: 从 23% 提升至 45% (+96%)
- 图片查看率: 从 34% 提升至 67% (+97%)
- 移动端预订转化: 从 1.2% 提升至 2.1% (+75%)
- 页面跳出率: 从 62% 降低至 41% (-34%)
六、最佳实践总结
6.1 花筑专属优化清单
✅ 房间图片优化(民宿核心) ├── 多层级渐进加载 ├── AVIF/WebP自适应 ├── 360°全景支持 └── 智能缓存策略 ✅ 地图组件优化 ├── 延迟加载与虚拟滚动 ├── 标记点对象池 ├── 聚类显示优化 └── 移动端手势优化 ✅ 多房型信息优化 ├── 数据分块与压缩 ├── 虚拟列表渲染 ├── 智能对比功能 └── 懒加载详情 ✅ 用户体验优化 ├── 骨架屏占位 ├── 平滑过渡动画 ├── 触摸反馈优化 └── 离线缓存策略 ✅ 监控体系 ├── Core Web Vitals追踪 ├── 花筑特定指标 ├── 业务事件埋点 └── 性能预算告警
6.2 持续演进方向
- WebGL地图渲染: 使用Three.js实现3D地图可视化
- AI图片优化: 基于机器学习的智能图片压缩
- 预测性加载: 基于用户行为的智能预加载
- 边缘计算: 使用Edge Functions处理图片优化
- WebAssembly: 用于复杂的房型对比计算
需要我针对花筑的地图组件优化或房型对比功能提供更详细的实现细节吗?