游虾商品详情页前端性能优化实战

简介: 游虾出境游商品详情页前端性能优化实战:针对套餐化产品、10万+长尾SKU、实拍图大体积等挑战,通过AVIF/WebP自适应画廊、虚拟列表渲染、多供应商库存聚合、富文本分块加载及实时价格推送等方案,LCP↓51%、CLS↓73%、首屏图片加载↓68%,转化率提升52%。

游虾商品详情页前端性能优化实战

一、业务背景与性能挑战

1.1 游虾业务特点

游虾作为专注于出境自由行的旅游平台,其商品详情页具有以下特征:
• 套餐化产品:机票+酒店+签证+当地玩乐组合销售

• 长尾商品:目的地覆盖全球200+城市,SKU超过10万+

• 内容密集型:攻略、游记、用户评价、实拍图并存

• 季节性波动:淡旺季流量差异巨大(3-5倍)

• 移动端主导:85%+订单来自移动端

• 多供应商对接:实时库存、价格同步复杂

1.2 性能痛点分析

┌─────────────────────────────────────────────────────────────────┐
│ 游虾详情页性能瓶颈 │
├─────────────┬─────────────┬─────────────┬──────────────┤
│ 图片画廊 │ 套餐渲染 │ 实时库存 │ 内容加载 │
│ 40% │ 25% │ 20% │ 15% │
└─────────────┴─────────────┴─────────────┴──────────────┘

具体问题:
• 目的地实拍图平均5-8MB,画廊加载卡顿

• 套餐组合逻辑复杂,JSON数据量达200KB+

• 多供应商API串行调用,接口耗时累计2-3秒

• 富文本内容(攻略、游记)HTML体积大且无优化

• 移动端低端机型白屏时间过长

二、图片画廊性能优化专项

2.1 游虾特色图片画廊

// 游虾图片画廊优化管理器
class YouxiaGalleryOptimizer {
constructor() {
this.deviceCapabilities = this.detectDeviceCapabilities();
this.viewportInfo = this.getViewportInfo();
this.galleryConfig = this.getGalleryConfig();
}

// 检测设备能力
detectDeviceCapabilities() {
const connection = navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection;

return {
  isLowEndDevice: this.checkLowEndDevice(),
  supportsWebP: this.checkWebPSupport(),
  supportsAVIF: this.checkAVIFSupport(),
  memoryGB: this.estimateMemory(),
  cores: navigator.hardwareConcurrency || 2,
  networkType: connection?.effectiveType || '4g',
  downlink: connection?.downlink || 10,
  isSaveData: connection?.saveData || false,
  pixelRatio: Math.min(window.devicePixelRatio || 1, 2) // 限制最大2x
};

}

checkLowEndDevice() {
const ua = navigator.userAgent;
const isOldAndroid = /Android [1-5]/.test(ua);
const isOldIOS = /OS [1-9]_/.test(ua);
const hasLowMemory = navigator.deviceMemory && navigator.deviceMemory < 2;
const hasSlowCPU = navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4;

return isOldAndroid || isOldIOS || hasLowMemory || hasSlowCPU;

}

checkWebPSupport() {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}

checkAVIFSupport() {
const avif = new Image();
avif.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKBzgABpAQIAAAAwMjIwMJABAA==';
return new Promise(resolve => {
avif.onload = () => resolve(true);
avif.onerror = () => resolve(false);
});
}

estimateMemory() {
if (navigator.deviceMemory) {
return navigator.deviceMemory;
}
// 基于设备类型估算
const ua = navigator.userAgent;
if (/iPhone/.test(ua)) return 2;
if (/iPad/.test(ua)) return 4;
if (/Android/.test(ua)) {
if (/SM-G9/.test(ua) || /Redmi/.test(ua)) return 1;
if (/SM-[A-Z]/.test(ua) || /Mi /.test(ua)) return 2;
}
return 4; // 默认值
}

getViewportInfo() {
return {
width: window.innerWidth,
height: window.innerHeight,
orientation: window.innerWidth > window.innerHeight ? 'landscape' : 'portrait',
availableWidth: document.documentElement.clientWidth
};
}

getGalleryConfig() {
const { isLowEndDevice, supportsAVIF, supportsWebP, isSaveData, networkType } = this.deviceCapabilities;

// 根据设备能力确定配置
let config = {
  thumbnailSize: 200,
  previewSize: 800,
  fullSize: 1920,
  quality: 85,
  format: 'jpeg',
  concurrentLoads: 3,
  preloadDistance: 2
};

if (isSaveData || networkType === 'slow-2g' || networkType === '2g') {
  config = {
    ...config,
    thumbnailSize: 100,
    previewSize: 400,
    fullSize: 800,
    quality: 60,
    concurrentLoads: 1,
    preloadDistance: 1
  };
} else if (isLowEndDevice) {
  config = {
    ...config,
    thumbnailSize: 150,
    previewSize: 600,
    fullSize: 1200,
    quality: 75,
    concurrentLoads: 2,
    preloadDistance: 1
  };
}

// 选择最佳格式
if (supportsAVIF) {
  config.format = 'avif';
} else if (supportsWebP) {
  config.format = 'webp';
}

return config;

}

// 游虾图片URL生成(对接游虾CDN)
generateYouxiaImageUrl(originalUrl, options = {}) {
const config = { ...this.galleryConfig, ...options };

// 游虾CDN参数格式
const params = new URLSearchParams();

// 尺寸参数
if (config.width) params.set('w', config.width);
if (config.height) params.set('h', config.height);
if (config.fit) params.set('fit', config.fit);

// 质量参数
params.set('q', config.quality);

// 格式参数
params.set('fmt', config.format);

// 锐化增强(游虾特色)
params.set('sharpen', '1.2');

// 色彩增强
params.set('sat', '1.1');

// 水印控制
if (config.watermark !== false) {
  params.set('wm', 'youxia_logo');
  params.set('wmp', 'bottom-right');
}

return `${originalUrl}?${params.toString()}`;

}

// 渐进式图片加载(游虾特色:支持放大预览)
createProgressiveGallery(container, images, options = {}) {
const config = { ...this.galleryConfig, ...options };
const gallery = this.createGalleryStructure(container);

// 创建图片索引
const imageIndex = this.buildImageIndex(images);

// 加载策略:首屏优先 + 懒加载
this.loadVisibleImages(gallery, imageIndex, config);

// 绑定滚动监听
this.setupScrollListener(gallery, imageIndex, config);

return gallery;

}

buildImageIndex(images) {
return images.map((img, index) => ({
...img,
index,
loaded: false,
loading: false,
error: false,
thumbnailUrl: this.generateYouxiaImageUrl(img.url, {
width: 200,
height: 150,
fit: 'cover'
}),
previewUrl: this.generateYouxiaImageUrl(img.url, {
width: 800,
height: 600,
fit: 'inside'
}),
fullUrl: this.generateYouxiaImageUrl(img.url, {
width: 1920,
height: 1440,
fit: 'inside'
})
}));
}

createGalleryStructure(container) {
const gallery = document.createElement('div');
gallery.className = 'youxia-gallery';
gallery.innerHTML = <div class="gallery-main"> <div class="main-image-container"> <img class="main-image" alt="商品图片"> <div class="loading-spinner"></div> <div class="zoom-indicator">🔍 点击放大</div> </div> <div class="gallery-nav"> <button class="nav-btn prev">❮</button> <button class="nav-btn next">❯</button> </div> </div> <div class="gallery-thumbnails"> <div class="thumbnails-track"></div> </div> <div class="gallery-counter">1 / 1</div>;

container.appendChild(gallery);
return gallery;

}

loadVisibleImages(gallery, imageIndex, config) {
const mainImage = gallery.querySelector('.main-image');
const thumbnailsTrack = gallery.querySelector('.thumbnails-track');
const counter = gallery.querySelector('.gallery-counter');

// 加载第一张图片
if (imageIndex.length > 0) {
  this.loadSingleImage(imageIndex[0], 'preview').then(() => {
    mainImage.src = imageIndex[0].previewUrl;
    mainImage.classList.add('loaded');
    counter.textContent = `1 / ${imageIndex.length}`;
  });

  // 加载缩略图
  this.loadThumbnails(thumbnailsTrack, imageIndex, config);
}

}

async loadThumbnails(container, imageIndex, config) {
const fragment = document.createDocumentFragment();
const batchSize = config.concurrentLoads;

for (let i = 0; i < Math.min(batchSize, imageIndex.length); i++) {
  const thumb = this.createThumbnail(imageIndex[i]);
  fragment.appendChild(thumb);
}

container.appendChild(fragment);

// 异步加载剩余缩略图
setTimeout(() => {
  for (let i = batchSize; i < imageIndex.length; i++) {
    const thumb = this.createThumbnail(imageIndex[i]);
    container.appendChild(thumb);
  }
}, 100);

}

createThumbnail(imageData) {
const thumb = document.createElement('div');
thumb.className = 'thumbnail';
thumb.dataset.index = imageData.index;

const img = document.createElement('img');
img.alt = `缩略图 ${imageData.index + 1}`;
img.loading = 'lazy';

// 加载缩略图
const tempImg = new Image();
tempImg.onload = () => {
  img.src = tempImg.src;
  thumb.classList.add('loaded');
};
tempImg.src = imageData.thumbnailUrl;

thumb.appendChild(img);
return thumb;

}

async loadSingleImage(imageData, quality = 'preview') {
if (imageData.loaded) return;

imageData.loading = true;

const url = quality === 'preview' ? imageData.previewUrl : imageData.fullUrl;

try {
  await this.loadImageWithTimeout(url, 10000);
  imageData.loaded = true;
} catch (error) {
  imageData.error = true;
  console.error(`Failed to load image: ${url}`, error);
} finally {
  imageData.loading = false;
}

}

loadImageWithTimeout(url, timeout) {
return new Promise((resolve, reject) => {
const img = new Image();
let timer;

  img.onload = () => {
    clearTimeout(timer);
    resolve(img);
  };

  img.onerror = () => {
    clearTimeout(timer);
    reject(new Error(`Image load failed: ${url}`));
  };

  timer = setTimeout(() => {
    reject(new Error(`Image load timeout: ${url}`));
  }, timeout);

  img.src = url;
});

}

setupScrollListener(gallery, imageIndex, config) {
let ticking = false;
const mainImage = gallery.querySelector('.main-image');
const counter = gallery.querySelector('.gallery-counter');
const thumbnails = gallery.querySelectorAll('.thumbnail');

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      this.updateVisibleImages(gallery, imageIndex, config, mainImage, counter, thumbnails);
      ticking = false;
    });
    ticking = true;
  }
}, { passive: true });

}

updateVisibleImages(gallery, imageIndex, config, mainImage, counter, thumbnails) {
// 检测当前可见的图片
const visibleIndices = this.getVisibleImageIndices(thumbnails, config);

// 预加载临近图片
visibleIndices.forEach(index => {
  if (index >= 0 && index < imageIndex.length) {
    this.loadSingleImage(imageIndex[index], 'preview');
  }
});

}

getVisibleImageIndices(thumbnails, config) {
const indices = [];
const preloadRange = config.preloadDistance;

thumbnails.forEach(thumb => {
  const rect = thumb.getBoundingClientRect();
  const isVisible = rect.top < window.innerHeight + preloadRange * 100 
                  && rect.bottom > -preloadRange * 100;

  if (isVisible) {
    indices.push(parseInt(thumb.dataset.index));
  }
});

return indices;

}
}

2.2 图片预加载与缓存策略

// 游虾图片预加载管理器
class YouxiaImagePreloader {
constructor() {
this.cache = new Map();
this.loadingQueue = new Set();
this.maxCacheSize = this.calculateMaxCacheSize();
this.prefetchEnabled = true;
}

calculateMaxCacheSize() {
const memory = navigator.deviceMemory || 4;
// 根据设备内存计算缓存大小
if (memory <= 1) return 5;
if (memory <= 2) return 10;
if (memory <= 4) return 20;
return 30;
}

// 智能预加载策略
async smartPrefetch(images, context = {}) {
if (!this.prefetchEnabled) return;

const { currentIndex, viewHistory, userIntent } = context;

// 分析用户意图
const intent = this.analyzeUserIntent(images, currentIndex, viewHistory);

// 根据意图决定预加载策略
const prefetchPlan = this.createPrefetchPlan(images, currentIndex, intent);

// 执行预加载
await this.executePrefetchPlan(prefetchPlan);

}

analyzeUserIntent(images, currentIndex, viewHistory) {
// 分析浏览历史
const avgViewDuration = this.calculateAverageViewDuration(viewHistory);
const rapidScrolling = avgViewDuration < 1000;

// 分析当前位置
const isFirstImage = currentIndex === 0;
const isLastImage = currentIndex === images.length - 1;
const isMiddleSection = currentIndex > 0 && currentIndex < images.length - 1;

// 判断用户意图
if (rapidScrolling) {
  return { type: 'browse', depth: 1 };
} else if (isFirstImage) {
  return { type: 'explore', depth: 3 };
} else if (isLastImage) {
  return { type: 'review', depth: 2 };
} else {
  return { type: 'detail', depth: 4 };
}

}

calculateAverageViewDuration(viewHistory) {
if (viewHistory.length === 0) return 2000;

const totalDuration = viewHistory.reduce((sum, h) => sum + h.duration, 0);
return totalDuration / viewHistory.length;

}

createPrefetchPlan(images, currentIndex, intent) {
const plan = {
immediate: [],
background: [],
lowPriority: []
};

const { depth } = intent;

// 立即加载:当前图片的高质量版本
if (images[currentIndex]) {
  plan.immediate.push({
    image: images[currentIndex],
    quality: 'full',
    priority: 'high'
  });
}

// 后台加载:临近图片的预览版本
for (let i = 1; i <= depth; i++) {
  const nextIndex = currentIndex + i;
  const prevIndex = currentIndex - i;

  if (nextIndex < images.length) {
    plan.background.push({
      image: images[nextIndex],
      quality: 'preview',
      priority: 'medium'
    });
  }

  if (prevIndex >= 0) {
    plan.background.push({
      image: images[prevIndex],
      quality: 'preview',
      priority: 'medium'
    });
  }
}

// 低优先级:更远处的缩略图
for (let i = depth + 1; i <= depth + 3; i++) {
  const nextIndex = currentIndex + i;
  const prevIndex = currentIndex - i;

  if (nextIndex < images.length) {
    plan.lowPriority.push({
      image: images[nextIndex],
      quality: 'thumbnail',
      priority: 'low'
    });
  }

  if (prevIndex >= 0) {
    plan.lowPriority.push({
      image: images[prevIndex],
      quality: 'thumbnail',
      priority: 'low'
    });
  }
}

return plan;

}

async executePrefetchPlan(plan) {
// 首先执行高优先级加载
await this.loadImagesInParallel(plan.immediate);

// 然后执行中优先级加载
setTimeout(() => {
  this.loadImagesInParallel(plan.background);
}, 100);

// 最后执行低优先级加载
setTimeout(() => {
  this.loadImagesInParallel(plan.lowPriority);
}, 500);

}

async loadImagesInParallel(images) {
const promises = images.map(item => this.loadAndCacheImage(item));
await Promise.allSettled(promises);
}

async loadAndCacheImage({ image, quality, priority }) {
const cacheKey = ${image.id}_${quality};

if (this.cache.has(cacheKey)) {
  return this.cache.get(cacheKey);
}

if (this.loadingQueue.has(cacheKey)) {
  return this.waitForLoading(cacheKey);
}

this.loadingQueue.add(cacheKey);

try {
  let url;
  switch (quality) {
    case 'full':
      url = image.fullUrl;
      break;
    case 'preview':
      url = image.previewUrl;
      break;
    case 'thumbnail':
      url = image.thumbnailUrl;
      break;
    default:
      url = image.previewUrl;
  }

  const blob = await this.fetchImageAsBlob(url);
  const objectUrl = URL.createObjectURL(blob);

  this.cache.set(cacheKey, objectUrl);
  this.enforceCacheLimit();

  return objectUrl;
} catch (error) {
  console.error(`Failed to preload image: ${cacheKey}`, error);
  return null;
} finally {
  this.loadingQueue.delete(cacheKey);
}

}

async fetchImageAsBlob(url) {
const response = await fetch(url, {
priority: 'low',
cache: 'force-cache'
});

if (!response.ok) {
  throw new Error(`HTTP ${response.status}: ${url}`);
}

return response.blob();

}

waitForLoading(cacheKey) {
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (this.cache.has(cacheKey) || !this.loadingQueue.has(cacheKey)) {
clearInterval(checkInterval);
resolve(this.cache.get(cacheKey) || null);
}
}, 50);

  // 超时保护
  setTimeout(() => {
    clearInterval(checkInterval);
    resolve(null);
  }, 10000);
});

}

enforceCacheLimit() {
while (this.cache.size > this.maxCacheSize) {
// 删除最旧的缓存项
const oldestKey = this.cache.keys().next().value;
const oldUrl = this.cache.get(oldestKey);
if (oldUrl.startsWith('blob:')) {
URL.revokeObjectURL(oldUrl);
}
this.cache.delete(oldestKey);
}
}

// 清理缓存
clearCache() {
this.cache.forEach((url, key) => {
if (url.startsWith('blob:')) {
URL.revokeObjectURL(url);
}
});
this.cache.clear();
}

// 禁用预加载(节省流量)
disablePrefetch() {
this.prefetchEnabled = false;
this.clearCache();
}
}

三、套餐组合渲染优化

3.1 游虾套餐数据结构优化

// 游虾套餐数据优化器
class YouxiaPackageOptimizer {
constructor() {
this.packageSchema = this.definePackageSchema();
}

definePackageSchema() {
return {
// 基础信息
id: 'string',
name: 'string',
description: 'string',
destination: 'string',
duration: 'number',

  // 价格信息(分离存储,按需加载)
  pricing: {
    basePrice: 'number',
    currency: 'string',
    taxes: 'object',
    fees: 'array'
  },

  // 包含项目(压缩存储)
  inclusions: {
    flights: 'compressed',
    hotels: 'compressed',
    visas: 'boolean',
    activities: 'compressed'
  },

  // 库存信息(独立接口)
  inventory: 'reference',

  // 评价信息(汇总数据)
  ratings: {
    average: 'number',
    count: 'number',
    breakdown: 'object'
  }
};

}

// 数据压缩与转换
optimizePackageData(rawData) {
const optimized = {
// 只保留首屏必需字段
_meta: {
id: rawData.id,
version: '1.0',
timestamp: Date.now()
},
basic: this.extractBasicInfo(rawData),
pricing: this.optimizePricing(rawData.pricing),
inclusions: this.compressInclusions(rawData.inclusions),
ratings: this.summarizeRatings(rawData.reviews)
};

return optimized;

}

extractBasicInfo(data) {
return {
id: data.id,
name: data.name,
shortDesc: data.description?.substring(0, 100) + '...',
destination: data.destination,
duration: data.duration,
image: data.mainImage,
tags: this.extractTags(data)
};
}

extractTags(data) {
const tags = [];

if (data.flights?.direct) tags.push('直飞');
if (data.hotels?.starRating >= 4) tags.push(`${data.hotels.starRating}星酒店`);
if (data.visa?.included) tags.push('含签证');
if (data.activities?.length > 0) tags.push('含活动');

return tags;

}

optimizePricing(pricing) {
// 只返回基础价格,详细税费单独请求
return {
basePrice: pricing.basePrice,
currency: pricing.currency,
showPrice: this.formatPrice(pricing.basePrice, pricing.currency),
priceNote: '含税价以预订时为准'
};
}

compressInclusions(inclusions) {
// 使用位图压缩包含项目
const flags = {
hasFlight: !!inclusions.flights,
hasHotel: !!inclusions.hotels,
hasVisa: !!inclusions.visas,
hasActivities: !!inclusions.activities,
flightDirect: inclusions.flights?.direct || false,
hotelBreakfast: inclusions.hotels?.breakfast || false,
visaFree: inclusions.visas?.free || false
};

return {
  flags,
  summary: this.generateInclusionSummary(flags)
};

}

generateInclusionSummary(flags) {
const items = [];

if (flags.hasFlight) {
  items.push(flags.flightDirect ? '直飞机票' : '转机机票');
}
if (flags.hasHotel) {
  items.push('精选酒店住宿');
}
if (flags.hasVisa) {
  items.push(flags.visaFree ? '免签服务' : '签证办理');
}
if (flags.hasActivities) {
  items.push('当地活动安排');
}

return items.join(' · ');

}

summarizeRatings(reviews) {
if (!reviews || reviews.length === 0) {
return { average: 0, count: 0, level: '暂无评价' };
}

const totalRating = reviews.reduce((sum, r) => sum + r.rating, 0);
const average = (totalRating / reviews.length).toFixed(1);

let level = '好评';
if (average < 3.5) level = '一般';
else if (average < 4.5) level = '良好';

return {
  average: parseFloat(average),
  count: reviews.length,
  level,
  distribution: this.calculateDistribution(reviews)
};

}

calculateDistribution(reviews) {
const distribution = { 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 };

reviews.forEach(r => {
  const rating = Math.round(r.rating);
  if (distribution[rating] !== undefined) {
    distribution[rating]++;
  }
});

// 转换为百分比
const total = reviews.length;
return Object.entries(distribution).map(([star, count]) => ({
  star: parseInt(star),
  percentage: total > 0 ? Math.round((count / total) * 100) : 0
}));

}

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, ',')}`;

}
}

3.2 虚拟列表渲染套餐

// 游虾套餐虚拟列表组件
class YouxiaPackageVirtualList {
constructor(container, options = {}) {
this.container = container;
this.options = {
itemHeight: 280,
bufferSize: 5,
overscan: 3,
...options
};

this.packages = [];
this.visiblePackages = [];
this.scrollTop = 0;
this.containerHeight = 0;

this.init();

}

init() {
this.createDOMStructure();
this.bindEvents();
this.calculateDimensions();
}

createDOMStructure() {
this.container.innerHTML = <div class="package-virtual-list"> <div class="list-content" style="height: 0px; position: relative;"> </div> <div class="list-loading" style="display: none;"> <div class="spinner"></div> <span>加载更多套餐...</span> </div> </div>;

this.listContent = this.container.querySelector('.list-content');
this.loadingIndicator = this.container.querySelector('.list-loading');

}

bindEvents() {
// 滚动事件(使用passive优化)
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 });

// 触摸事件优化
this.touchStartY = 0;
this.touchHandler = this.handleTouch.bind(this);
this.container.addEventListener('touchstart', this.touchHandler, { 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.updateVisibleItems();
}

}

handleResize() {
this.calculateDimensions();
this.updateVisibleItems();
}

handleTouch(e) {
this.touchStartY = e.touches[0].clientY;
}

// 设置套餐数据
setPackages(packages) {
this.packages = packages;
this.updateTotalHeight();
this.updateVisibleItems();
}

updateTotalHeight() {
const totalHeight = this.packages.length * this.options.itemHeight;
this.listContent.style.height = ${totalHeight}px;
}

updateVisibleItems() {
const { itemHeight, bufferSize, overscan } = this.options;

// 计算可见范围
const startIndex = Math.max(0, Math.floor(this.scrollTop / itemHeight) - bufferSize);
const endIndex = Math.min(
  this.packages.length,
  Math.ceil((this.scrollTop + this.containerHeight) / itemHeight) + bufferSize
);

// 计算实际需要渲染的范围(包含overscan)
const renderStart = Math.max(0, startIndex - overscan);
const renderEnd = Math.min(this.packages.length, endIndex + overscan);

// 获取新的可见项目
const newVisiblePackages = [];
for (let i = renderStart; i < renderEnd; i++) {
  newVisiblePackages.push({
    package: this.packages[i],
    index: i,
    top: i * itemHeight
  });
}

// 比较并更新DOM
this.updateDOM(newVisiblePackages, startIndex, endIndex);

}

updateDOM(newVisiblePackages, startIndex, endIndex) {
const fragment = document.createDocumentFragment();
const existingElements = new Map();

// 收集现有元素
this.listContent.querySelectorAll('.package-item').forEach(el => {
  const index = parseInt(el.dataset.index);
  existingElements.set(index, el);
});

// 创建或更新元素
newVisiblePackages.forEach(({ package: pkg, index, top }) => {
  let element = existingElements.get(index);

  if (!element) {
    element = this.createPackageElement(pkg, 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.visiblePackages = newVisiblePackages.filter(
  ({ index }) => index >= startIndex && index < endIndex
);

}

createPackageElement(pkg, index) {
const element = document.createElement('div');
element.className = 'package-item';
element.dataset.index = index;

element.innerHTML = `
  <div class="package-card">
    <div class="package-image">
      <img src="${pkg.basic.image}" alt="${pkg.basic.name}" loading="lazy">
      <div class="package-tags">
        ${pkg.basic.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
      </div>
    </div>
    <div class="package-info">
      <h3 class="package-name">${pkg.basic.name}</h3>
      <p class="package-desc">${pkg.basic.shortDesc}</p>
      <div class="package-meta">
        <span class="destination">📍 ${pkg.basic.destination}</span>
        <span class="duration">⏱️ ${pkg.basic.duration}天</span>
      </div>
      <div class="package-inclusions">
        <span class="inclusion-summary">${pkg.inclusions.summary}</span>
      </div>
      <div class="package-footer">
        <div class="package-rating">
          <span class="stars">${this.renderStars(pkg.ratings.average)}</span>
          <span class="count">(${pkg.ratings.count}条评价)</span>
        </div>
        <div class="package-price">
          <span class="price">${pkg.pricing.showPrice}</span>
          <span class="unit">起/人</span>
        </div>
      </div>
    </div>
  </div>
`;

// 绑定点击事件
element.addEventListener('click', () => {
  this.onPackageClick(pkg);
});

return element;

}

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;
}

onPackageClick(pkg) {
// 触发自定义事件
this.container.dispatchEvent(new CustomEvent('packageClick', {
detail: { package: pkg }
}));
}

// 显示/隐藏加载指示器
setLoading(loading) {
this.loadingIndicator.style.display = loading ? 'flex' : 'none';
}

// 滚动到指定索引
scrollToIndex(index, behavior = 'smooth') {
const offsetTop = index * this.options.itemHeight;
this.container.scrollTo({
top: offsetTop,
behavior
});
}

// 销毁组件
destroy() {
this.container.removeEventListener('scroll', this.scrollHandler);
window.removeEventListener('resize', this.resizeHandler);
this.container.removeEventListener('touchstart', this.touchHandler);
this.listContent.innerHTML = '';
}
}

四、实时库存与价格优化

4.1 游虾多供应商库存聚合

// 游虾库存聚合管理器
class YouxiaInventoryAggregator {
constructor() {
this.suppliers = new Map();
this.cache = new Map();
this.pendingRequests = new Map();
this.rateLimiter = new RateLimiter(10, 1000); // 10请求/秒
}

// 注册供应商适配器
registerSupplier(supplierId, adapter) {
this.suppliers.set(supplierId, {
adapter,
health: 100,
lastResponseTime: 0,
errorCount: 0
});
}

// 聚合库存查询
async aggregateInventory(productId, dates, options = {}) {
const cacheKey = this.generateCacheKey(productId, dates, options);

// 检查缓存
const cached = this.getFromCache(cacheKey);
if (cached && !this.isCacheExpired(cached)) {
  return cached.data;
}

// 检查是否有相同请求正在进行
if (this.pendingRequests.has(cacheKey)) {
  return this.pendingRequests.get(cacheKey);
}

// 执行聚合查询
const queryPromise = this.executeAggregatedQuery(productId, dates, options);
this.pendingRequests.set(cacheKey, queryPromise);

try {
  const result = await queryPromise;
  this.setCache(cacheKey, result);
  return result;
} finally {
  this.pendingRequests.delete(cacheKey);
}

}

generateCacheKey(productId, dates, options) {
const dateStr = Array.isArray(dates) ? dates.sort().join(',') : dates;
const optionStr = JSON.stringify(options);
return inv_${productId}_${dateStr}_${optionStr};
}

async executeAggregatedQuery(productId, dates, options) {
// 获取所有相关供应商
const relevantSuppliers = this.getRelevantSuppliers(productId);

// 并行查询各供应商
const queries = relevantSuppliers.map(supplier => 
  this.querySupplier(supplier, productId, dates, options)
);

const results = await Promise.allSettled(queries);

// 合并结果
return this.mergeResults(results, relevantSuppliers);

}

getRelevantSuppliers(productId) {
// 根据产品ID确定需要查询的供应商
// 这里可以根据业务逻辑实现
return Array.from(this.suppliers.entries()).map(([id, config]) => ({
id,
...config
}));
}

async querySupplier(supplier, productId, dates, options) {
// 速率限制
await this.rateLimiter.acquire();

const startTime = Date.now();

try {
  const result = await supplier.adapter.queryInventory(productId, dates, options);

  // 更新供应商健康度
  this.updateSupplierHealth(supplier.id, true, Date.now() - startTime);

  return {
    supplier: supplier.id,
    success: true,
    data: result,
    responseTime: Date.now() - startTime
  };
} catch (error) {
  // 更新供应商健康度
  this.updateSupplierHealth(supplier.id, false, Date.now() - startTime);

  return {
    supplier: supplier.id,
    success: false,
    error: error.message,
    responseTime: Date.now() - startTime
  };
}

}

mergeResults(results, suppliers) {
const merged = {
productId: null,
available: false,
totalStock: 0,
prices: [],
suppliers: [],
warnings: []
};

results.forEach((result, index) => {
  if (result.status === 'fulfilled' && result.value.success) {
    const { supplier, data, responseTime } = result.value;

    // 合并库存
    if (merged.productId === null) {
      merged.productId = data.productId;
    }

    merged.totalStock += data.stock || 0;
    merged.available = merged.available || data.available;

    // 合并价格(取最优)
    if (data.prices && data.prices.length > 0) {
      merged.prices.push(...data.prices.map(p => ({
        ...p,
        supplier,
        responseTime
      })));
    }

    // 记录供应商信息
    merged.suppliers.push({
      id: supplier,
      stock: data.stock,
      available: data.available,
      responseTime,
      health: suppliers[index].health
    });
  } else if (result.status === 'rejected' || !result.value.success) {
    const errorMsg = result.status === 'rejected' 
      ? result.reason?.message 
      : result.value.error;

    merged.warnings.push({
      type: 'supplier_error',
      supplier: result.value?.supplier || 'unknown',
      message: errorMsg
    });
  }
});

// 计算最优价格
if (merged.prices.length > 0) {
  merged.bestPrice = this.findBestPrice(merged.prices);
}

return merged;

}

findBestPrice(prices) {
return prices.reduce((best, current) => {
if (!best || current.amount < best.amount) {
return current;
}
return best;
});
}

updateSupplierHealth(supplierId, success, responseTime) {
const supplier = this.suppliers.get(supplierId);
if (!supplier) return;

if (success) {
  supplier.health = Math.min(100, supplier.health + 5);
  supplier.lastResponseTime = responseTime;
  supplier.errorCount = Math.max(0, supplier.errorCount - 1);
} else {
  supplier.health = Math.max(0, supplier.health - 10);
  supplier.errorCount++;
}

}

getFromCache(key) {
const cached = this.cache.get(key);
if (!cached) return null;

// 检查是否过期
if (Date.now() - cached.timestamp > 30000) { // 30秒缓存
  this.cache.delete(key);
  return null;
}

return cached;

}

setCache(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});

// 限制缓存大小
if (this.cache.size > 100) {
  const oldestKey = this.cache.keys().next().value;
  this.cache.delete(oldestKey);
}

}

isCacheExpired(cached) {
return Date.now() - cached.timestamp > 30000;
}
}

// 速率限制器
class RateLimiter {
constructor(maxRequests, timeWindow) {
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
this.requests = [];
}

async acquire() {
const now = Date.now();

// 清理过期的请求记录
this.requests = this.requests.filter(time => now - time < this.timeWindow);

if (this.requests.length >= this.maxRequests) {
  // 计算需要等待的时间
  const oldestRequest = this.requests[0];
  const waitTime = this.timeWindow - (now - oldestRequest);

  if (waitTime > 0) {
    await this.sleep(waitTime);
  }
}

this.requests.push(Date.now());

}

sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

4.2 价格更新与推送机制

// 游虾实时价格管理器
class YouxiaPriceManager {
constructor() {
this.subscribers = new Map();
this.priceCache = new Map();
this.updateQueue = [];
this.isProcessing = false;
this.socketConnection = null;
}

// 订阅价格更新
subscribe(productId, callback, options = {}) {
const subscriptionId = this.generateSubscriptionId();

if (!this.subscribers.has(productId)) {
  this.subscribers.set(productId, new Map());
}

this.subscribers.get(productId).set(subscriptionId, {
  callback,
  options,
  lastNotifiedPrice: null
});

// 建立WebSocket连接(如果尚未建立)
this.ensureConnection();

return {
  id: subscriptionId,
  unsubscribe: () => this.unsubscribe(productId, subscriptionId)
};

}

unsubscribe(productId, subscriptionId) {
const productSubscribers = this.subscribers.get(productId);
if (productSubscribers) {
productSubscribers.delete(subscriptionId);

  if (productSubscribers.size === 0) {
    this.subscribers.delete(productId);
    this.closeConnectionIfEmpty();
  }
}

}

generateSubscriptionId() {
return sub_${Date.now()}_${Math.random().toString(36).substr(2, 9)};
}

ensureConnection() {
if (this.socketConnection) return;

// 建立WebSocket连接
this.socketConnection = new WebSocket('wss://api.youxia.com/ws/prices');

this.socketConnection.onopen = () => {
  console.log('Price WebSocket connected');
  this.subscribeToProducts();
};

this.socketConnection.onmessage = (event) => {
  this.handlePriceUpdate(JSON.parse(event.data));
};

this.socketConnection.onclose = () => {
  console.log('Price WebSocket disconnected');
  this.socketConnection = null;
  // 断线重连
  setTimeout(() => this.ensureConnection(), 5000);
};

this.socketConnection.onerror = (error) => {
  console.error('Price WebSocket error:', error);
};

}

closeConnectionIfEmpty() {
if (this.subscribers.size === 0 && this.socketConnection) {
this.socketConnection.close();
this.socketConnection = null;
}
}

subscribeToProducts() {
if (!this.socketConnection) return;

const productIds = Array.from(this.subscribers.keys());
if (productIds.length > 0) {
  this.socketConnection.send(JSON.stringify({
    type: 'subscribe',
    products: productIds
  }));
}

}

handlePriceUpdate(update) {
const { productId, prices, timestamp } = update;

// 更新缓存
this.updatePriceCache(productId, prices);

// 通知订阅者
this.notifySubscribers(productId, prices, timestamp);

}

updatePriceCache(productId, prices) {
const cached = this.priceCache.get(productId) || {
prices: [],
lastUpdate: 0
};

// 合并新价格
prices.forEach(newPrice => {
  const existingIndex = cached.prices.findIndex(
    p => p.supplier === newPrice.supplier && p.date === newPrice.date
  );

  if (existingIndex >= 0) {
    cached.prices[existingIndex] = {
      ...cached.prices[existingIndex],
      ...newPrice,
      lastUpdated: Date.now()
    };
  } else {
    cached.prices.push({
      ...newPrice,
      lastUpdated: Date.now()
    });
  }
});

cached.lastUpdate = Date.now();
this.priceCache.set(productId, cached);

}

notifySubscribers(productId, prices, timestamp) {
const productSubscribers = this.subscribers.get(productId);
if (!productSubscribers) return;

productSubscribers.forEach((subscriber, subscriptionId) => {
  const { callback, options, lastNotifiedPrice } = subscriber;

  // 检查是否需要通知(价格变化超过阈值)
  if (this.shouldNotify(lastNotifiedPrice, prices, options)) {
    try {
      callback({
        productId,
        prices,
        timestamp,
        changes: this.calculateChanges(lastNotifiedPrice, prices)
      });

      // 更新最后通知的价格
      subscriber.lastNotifiedPrice = prices;
    } catch (error) {
      console.error(`Error in price update callback for ${subscriptionId}:`, error);
    }
  }
});

}

shouldNotify(lastNotifiedPrice, newPrices, options) {
if (!lastNotifiedPrice) return true;

const threshold = options.threshold || 0.01; // 默认1%变化才通知

for (const newPrice of newPrices) {
  const oldPrice = lastNotifiedPrice.find(
    p => p.supplier === newPrice.supplier && p.date === newPrice.date
  );

  if (oldPrice) {
    const changePercent = Math.abs(
      (newPrice.amount - oldPrice.amount) / oldPrice.amount
    );

    if (changePercent >= threshold) {
      return true;
    }
  } else {
    // 新价格出现
    return true;
  }
}

return false;

}

calculateChanges(lastNotifiedPrice, newPrices) {
const changes = [];

newPrices.forEach(newPrice => {
  const oldPrice = lastNotifiedPrice?.find(
    p => p.supplier === newPrice.supplier && p.date === newPrice.date
  );

  if (oldPrice) {
    const changeAmount = newPrice.amount - oldPrice.amount;
    const changePercent = (changeAmount / oldPrice.amount) * 100;

    changes.push({
      supplier: newPrice.supplier,
      date: newPrice.date,
      oldPrice: oldPrice.amount,
      newPrice: newPrice.amount,
      changeAmount,
      changePercent
    });
  } else {
    changes.push({
      supplier: newPrice.supplier,
      date: newPrice.date,
      newPrice: newPrice.amount,
      isNew: true
    });
  }
});

return changes;

}

// 获取当前缓存价格
getCachedPrice(productId) {
return this.priceCache.get(productId);
}

// 手动刷新价格
async refreshPrice(productId) {
try {
const response = await fetch(/api/products/${productId}/prices);
const data = await response.json();

  this.handlePriceUpdate({
    productId,
    prices: data.prices,
    timestamp: Date.now()
  });

  return data;
} catch (error) {
  console.error(`Failed to refresh price for ${productId}:`, error);
  throw error;
}

}
}

五、内容加载与渲染优化

5.1 游虾富文本内容优化

// 游虾富文本内容处理器
class YouxiaRichContentProcessor {
constructor() {
this.contentCache = new Map();
this.imageLazyLoaders = new Map();
}

// 处理富文本内容
async processContent(htmlContent, options = {}) {
const cacheKey = this.generateContentCacheKey(htmlContent, options);

// 检查缓存
if (this.contentCache.has(cacheKey)) {
  return this.contentCache.get(cacheKey);
}

// 解析和处理HTML
const processedContent = await this.parseAndOptimize(htmlContent, options);

// 缓存结果
this.contentCache.set(cacheKey, processedContent);

return processedContent;

}

generateContentCacheKey(content, options) {
const contentHash = this.simpleHash(content);
const optionStr = JSON.stringify(options);
return content_${contentHash}_${optionStr};
}

simpleHash(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 parseAndOptimize(htmlContent, options) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');

// 优化图片
await this.optimizeImages(doc, options);

// 优化链接
this.optimizeLinks(doc);

// 提取关键信息
const extractedInfo = this.extractKeyInformation(doc);

// 生成摘要
const summary = this.generateSummary(doc, options.summaryLength || 150);

// 序列化处理后的HTML
const processedHtml = doc.body.innerHTML;

return {
  html: processedHtml,
  summary,
  extractedInfo,
  stats: {
    originalLength: htmlContent.length,
    processedLength: processedHtml.length,
    imageCount: doc.querySelectorAll('img').length,
    linkCount: doc.querySelectorAll('a').length
  }
};

}

async optimizeImages(doc, options) {
const images = doc.querySelectorAll('img');
const optimizationPromises = [];

images.forEach((img, index) => {
  const promise = this.optimizeSingleImage(img, index, options);
  optimizationPromises.push(promise);
});

await Promise.all(optimizationPromises);

}

async optimizeSingleImage(img, index, options) {
const originalSrc = img.src || img.dataset.src;
if (!originalSrc) return;

// 生成优化的图片URL
const optimizedSrc = this.generateOptimizedImageUrl(originalSrc, options);

// 设置懒加载
img.loading = 'lazy';
img.decoding = 'async';

// 使用IntersectionObserver实现懒加载
const lazyLoadPromise = this.setupLazyLoading(img, optimizedSrc);
this.imageLazyLoaders.set(index, lazyLoadPromise);

// 添加错误处理
img.onerror = () => {
  img.src = this.getPlaceholderImage();
  img.classList.add('image-error');
};

// 添加加载占位符
img.style.backgroundColor = '#f0f2f5';
img.style.minHeight = '200px';

}

generateOptimizedImageUrl(originalSrc, options) {
const params = new URLSearchParams();

// 根据容器宽度确定图片尺寸
const containerWidth = options.containerWidth || 800;
const devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2);
const targetWidth = Math.round(containerWidth * devicePixelRatio);

params.set('w', targetWidth);
params.set('q', options.quality || 85);
params.set('fmt', this.getOptimalFormat());
params.set('fit', 'inside');

// 添加游虾特定的优化参数
params.set('sharpen', '1.1');
params.set('sat', '1.05');

return `${originalSrc}?${params.toString()}`;

}

getOptimalFormat() {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;

if (canvas.toDataURL('image/avif').indexOf('data:image/avif') === 0) {
  return 'avif';
}
if (canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0) {
  return 'webp';
}
return 'jpeg';

}

setupLazyLoading(img, src) {
return new Promise((resolve) => {
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
img.src = src;
img.onload = () => {
img.style.backgroundColor = '';
img.style.minHeight = '';
resolve();
};
observer.unobserve(img);
}
});
}, {
rootMargin: '200px 0px',
threshold: 0.1
});

    observer.observe(img);
  } else {
    // 降级处理
    img.src = src;
    resolve();
  }
});

}

getPlaceholderImage() {
return 'data:image/svg+xml,' + encodeURIComponent(<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300"> <rect fill="#f0f2f5" width="400" height="300"/> <text fill="#999" font-family="Arial" font-size="16" text-anchor="middle" x="200" y="150"> 图片加载失败 </text> </svg>);
}

optimizeLinks(doc) {
const links = doc.querySelectorAll('a[href]');

links.forEach(link => {
  const href = link.getAttribute('href');

  // 将外部链接标记为noopener
  if (href.startsWith('http') && !href.includes(location.hostname)) {
    link.setAttribute('rel', 'noopener noreferrer');
    link.setAttribute('target', '_blank');
  }

  // 为内部链接添加数据属性
  if (href.startsWith('/') || href.includes(location.hostname)) {
    link.dataset.internalLink = 'true';
  }
});

}

extractKeyInformation(doc) {
const info = {
headings: [],
images: [],
links: [],
lists: [],
tables: 0
};

// 提取标题
const headings = doc.querySelectorAll('h1, h2, h3, h4, h5, h6');
headings.forEach(heading => {
  info.headings.push({
    level: parseInt(heading.tagName[1]),
    text: heading.textContent.trim()
  });
});

// 提取图片信息
const images = doc.querySelectorAll('img');
images.forEach(img => {
  info.images.push({
    src: img.src || img.dataset.src,
    alt: img.alt || '',
    hasCaption: !!img.closest('figure')
  });
});

// 提取链接信息
const links = doc.querySelectorAll('a[href]');
links.forEach(link => {
  info.links.push({
    href: link.getAttribute('href'),
    text: link.textContent.trim(),
    isExternal: link.getAttribute('target') === '_blank'
  });
});

// 统计列表
info.lists = {
  ordered: doc.querySelectorAll('ol').length,
  unordered: doc.querySelectorAll('ul').length
};

// 统计表格
info.tables = doc.querySelectorAll('table').length;

return info;

}

generateSummary(doc, maxLength) {
// 获取纯文本
const text = doc.body.textContent || doc.body.innerText || '';

// 清理文本
const cleanedText = text
  .replace(/\s+/g, ' ')
  .replace(/[\r\n\t]+/g, ' ')
  .trim();

// 截取摘要
if (cleanedText.length <= maxLength) {
  return cleanedText;
}

// 在句子边界截断
const truncated = cleanedText.substring(0, maxLength);
const lastSentence = truncated.lastIndexOf('。');
const lastPeriod = truncated.lastIndexOf('.');
const lastBreak = Math.max(lastSentence, lastPeriod);

if (lastBreak > maxLength * 0.8) {
  return truncated.substring(0, lastBreak + 1);
}

return truncated + '...';

}

// 清理缓存
clearCache() {
this.contentCache.clear();
}
}

5.2 内容分块加载策略

// 游虾内容分块加载器
class YouxiaContentChunkLoader {
constructor() {
this.chunkRegistry = new Map();
this.loadedChunks = new Set();
this.loadingChunks = new Map();
}

// 注册内容块
registerChunk(name, loader, options = {}) {
this.chunkRegistry.set(name, {
loader,
priority: options.priority || 'normal',
dependencies: options.dependencies || [],
condition: options.condition || (() => true),
cacheable: options.cacheable !== false,
ttl: options.ttl || 300000 // 5分钟默认缓存
});
}

// 加载单个内容块
async loadChunk(chunkName, forceReload = false) {
const chunk = this.chunkRegistry.get(chunkName);
if (!chunk) {
throw new Error(Unknown content chunk: ${chunkName});
}

// 检查是否已加载
if (this.loadedChunks.has(chunkName) && !forceReload) {
  return this.getChunkData(chunkName);
}

// 检查是否正在加载
if (this.loadingChunks.has(chunkName)) {
  return this.loadingChunks.get(chunkName);
}

// 检查条件
if (!chunk.condition()) {
  return null;
}

// 检查缓存
if (chunk.cacheable && !forceReload) {
  const cached = this.getFromCache(chunkName);
  if (cached) {
    this.loadedChunks.add(chunkName);
    return cached;
  }
}

// 加载依赖
if (chunk.dependencies.length > 0) {
  await Promise.all(
    chunk.dependencies.map(dep => this.loadChunk(dep))
  );
}

// 执行加载
const loadPromise = this.executeChunkLoad(chunkName, chunk);
this.loadingChunks.set(chunkName, loadPromise);

try {
  const data = await loadPromise;
  this.storeChunkData(chunkName, data, chunk);
  this.loadedChunks.add(chunkName);

  return data;
} finally {
  this.loadingChunks.delete(chunkName);
}

}

async executeChunkLoad(chunkName, chunk) {
const startTime = performance.now();

try {
  const data = await chunk.loader();

  const loadTime = performance.now() - startTime;
  if (loadTime > 1000) {
    console.warn(`Slow chunk load: ${chunkName} took ${loadTime.toFixed(0)}ms`);
  }

  return data;
} catch (error) {
  console.error(`Failed to load chunk ${chunkName}:`, error);
  throw error;
}

}

// 批量加载内容块
async loadChunks(chunkNames, options = {}) {
const { parallel = true, priority = 'normal' } = options;

// 按优先级排序
const sortedChunks = chunkNames
  .map(name => ({ name, ...this.chunkRegistry.get(name) }))
  .filter(chunk => chunk.name)
  .sort((a, b) => {
    const priorityOrder = { high: 0, normal: 1, low: 2 };
    return priorityOrder[a.priority] - priorityOrder[b.priority];
  });

if (parallel) {
  // 并行加载
  const promises = sortedChunks.map(chunk => 
    this.loadChunk(chunk.name).catch(error => ({ error, chunk: chunk.name }))
  );
  return Promise.all(promises);
} else {
  // 串行加载
  const results = [];
  for (const chunk of sortedChunks) {
    try {
      const data = await this.loadChunk(chunk.name);
      results.push({ chunk: chunk.name, data });
    } catch (error) {
      results.push({ chunk: chunk.name, error });
    }
  }
  return results;
}

}

// 游虾详情页专用加载策略
async loadHotelDetailPage(productId) {
// 定义内容块
this.registerChunk('basic-info',
() => this.fetchBasicInfo(productId),
{ priority: 'high', cacheable: true, ttl: 600000 }
);

this.registerChunk('gallery',
  () => this.fetchGallery(productId),
  { priority: 'high', dependencies: ['basic-info'], cacheable: true, ttl: 900000 }
);

this.registerChunk('packages',
  () => this.fetchPackages(productId),
  { priority: 'high', dependencies: ['basic-info'], cacheable: true, ttl: 300000 }
);

this.registerChunk('reviews-summary',
  () => this.fetchReviewsSummary(productId),
  { priority: 'normal', cacheable: true, ttl: 180000 }
);

this.registerChunk('full-reviews',
  () => this.fetchFullReviews(productId),
  { priority: 'low', dependencies: ['reviews-summary'], cacheable: true, ttl: 300000 }
);

this.registerChunk('itinerary',
  () => this.fetchItinerary(productId),
  { priority: 'normal', dependencies: ['basic-info'], cacheable: true, ttl: 600000 }
);

this.registerChunk('faqs',
  () => this.fetchFAQs(productId),
  { priority: 'low', cacheable: true, ttl: 900000 }
);

this.registerChunk('related-products',
  () => this.fetchRelatedProducts(productId),
  { priority: 'low', dependencies: ['basic-info'], cacheable: true, ttl: 600000 }
);

// 分阶段加载
const phase1 = await this.loadChunks(['basic-info', 'gallery', 'packages'], {
  parallel: true
});

// 骨架屏替换为实际内容
this.emitEvent('phase1-complete', phase1);

// 第二阶段加载
const phase2 = await this.loadChunks([
  'reviews-summary', 
  'itinerary'
], { parallel: true });

this.emitEvent('phase2-complete', phase2);

// 第三阶段加载(低优先级)
setTimeout(() => {
  this.loadChunks([
    'full-reviews', 
    'faqs', 
    'related-products'
  ], { parallel: true }).then(phase3 => {
    this.emitEvent('phase3-complete', phase3);
  });
}, 2000);

return { phase1, phase2 };

}

// 模拟API调用
async fetchBasicInfo(productId) {
const response = await fetch(/api/products/${productId}/basic);
return response.json();
}

async fetchGallery(productId) {
const response = await fetch(/api/products/${productId}/gallery);
return response.json();
}

async fetchPackages(productId) {
const response = await fetch(/api/products/${productId}/packages);
return response.json();
}

async fetchReviewsSummary(productId) {
const response = await fetch(/api/products/${productId}/reviews/summary);
return response.json();
}

async fetchFullReviews(productId) {
const response = await fetch(/api/products/${productId}/reviews/full);
return response.json();
}

async fetchItinerary(productId) {
const response = await fetch(/api/products/${productId}/itinerary);
return response.json();
}

async fetchFAQs(productId) {
const response = await fetch(/api/products/${productId}/faqs);
return response.json();
}

async fetchRelatedProducts(productId) {
const response = await fetch(/api/products/${productId}/related);
return response.json();
}

// 缓存管理
storeChunkData(chunkName, data, chunk) {
if (!chunk.cacheable) return;

const cacheEntry = {
  data,
  timestamp: Date.now(),
  ttl: chunk.ttl
};

try {
  sessionStorage.setItem(
    `chunk_${chunkName}`,
    JSON.stringify(cacheEntry)
  );
} catch (e) {
  // 存储空间不足时忽略
}

}

getFromCache(chunkName) {
try {
const cached = sessionStorage.getItem(chunk_${chunkName});
if (!cached) return null;

  const entry = JSON.parse(cached);
  const age = Date.now() - entry.timestamp;

  if (age > entry.ttl) {
    sessionStorage.removeItem(`chunk_${chunkName}`);
    return null;
  }

  return entry.data;
} catch (e) {
  return null;
}

}

getChunkData(chunkName) {
return this.getFromCache(chunkName);
}

// 事件发射
emitEvent(eventName, data) {
const event = new CustomEvent(youxia:${eventName}, { detail: data });
document.dispatchEvent(event);
}

// 清除所有缓存
clearAllCache() {
this.loadedChunks.clear();
const keysToRemove = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key.startsWith('chunk_')) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => sessionStorage.removeItem(key));
}
}

六、性能监控与优化效果

6.1 游虾专属性能监控

// 游虾性能监控器
class YouxiaPerformanceMonitor {
constructor(config = {}) {
this.config = {
endpoint: '/api/performance/report',
sampleRate: 0.15, // 15%采样率
enableRealUserMonitoring: true,
enableBusinessMetrics: true,
...config
};

this.sessionId = this.generateSessionId();
this.userId = this.getUserId();
this.metrics = {};
this.businessEvents = [];

this.init();

}

generateSessionId() {
return yx_session_${Date.now()}_${Math.random().toString(36).substr(2, 9)};
}

getUserId() {
return localStorage.getItem('yx_user_id') || 'anonymous';
}

init() {
// 页面加载完成后收集指标
if (document.readyState === 'complete') {
this.collectMetrics();
} else {
window.addEventListener('load', () => this.collectMetrics());
}

// 绑定业务事件追踪
this.bindBusinessEventTracking();

// 绑定资源追踪
this.bindResourceTracking();

// 定时上报
this.setupReporting();

}

collectMetrics() {
this.measureCoreWebVitals();
this.measureCustomMetrics();
this.measureBusinessMetrics();
}

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'] });

// FCP
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.name === 'first-contentful-paint') {
      this.recordMetric('fcp', {
        value: entry.startTime,
        timestamp: Date.now()
      });
    }
  }
}).observe({ entryTypes: ['paint'] });

}

measureCustomMetrics() {
// 游虾特定指标
this.measureGalleryLoadTime();
this.measurePackageRenderTime();
this.measureContentLoadTime();
}

measureGalleryLoadTime() {
const galleryStart = performance.mark('gallery-start');

// 监听首张图片加载完成
const firstImage = document.querySelector('.gallery-main img');
if (firstImage) {
  if (firstImage.complete) {
    this.recordGalleryMetric();
  } else {
    firstImage.addEventListener('load', () => this.recordGalleryMetric());
    firstImage.addEventListener('error', () => this.recordGalleryMetric());
  }
}

// 备用:使用MutationObserver
const observer = new MutationObserver(() => {
  const loadedImages = document.querySelectorAll('.gallery-main img.loaded');
  if (loadedImages.length > 0) {
    this.recordGalleryMetric();
    observer.disconnect();
  }
});

observer.observe(document.querySelector('.gallery-main'), {
  childList: true,
  subtree: true
});

}

recordGalleryMetric() {
const galleryLoadTime = performance.now() - performance.getEntriesByName('gallery-start')[0]?.startTime;

this.recordMetric('gallery-load-time', {
  value: galleryLoadTime,
  timestamp: Date.now()
});

}

measurePackageRenderTime() {
const packageStart = performance.now();

// 使用requestAnimationFrame确保DOM更新完成
requestAnimationFrame(() => {
  requestAnimationFrame(() => {
    const packageRenderTime = performance.now() - packageStart;

    this.recordMetric('package-render-time', {
      value: packageRenderTime,
      timestamp: Date.now()
    });
  });
});

}

measureContentLoadTime() {
const contentStart = performance.now();

// 检测主要内容区域加载完成
const checkContentReady = setInterval(() => {
  const mainContent = document.querySelector('.main-content');
  const hasContent = mainContent && mainContent.children.length > 0;

  if (hasContent) {
    clearInterval(checkContentReady);
    const contentLoadTime = performance.now() - contentStart;

    this.recordMetric('content-load-time', {
      value: contentLoadTime,
      timestamp: Date.now()
    });
  }
}, 100);

// 超时保护
setTimeout(() => clearInterval(checkContentReady), 10000);

}

measureBusinessMetrics() {
if (!this.config.enableBusinessMetrics) return;

// 页面停留时间
this.pageStartTime = Date.now();

// 滚动深度
this.maxScrollDepth = 0;
this.scrollHandler = this.trackScrollDepth.bind(this);
window.addEventListener('scroll', this.scrollHandler, { passive: true });

// 点击率追踪
this.clickHandler = this.trackClicks.bind(this);
document.addEventListener('click', this.clickHandler, { capture: true });

}

trackScrollDepth() {
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,
      timestamp: Date.now()
    });
  }
}

}

trackClicks(event) {
const target = event.target.closest('a, button, [data-track-click]');
if (!target) return;

const clickData = {
  element: this.getElementIdentifier(target),
  type: target.tagName.toLowerCase(),
  text: target.textContent?.trim().substring(0, 50),
  coordinates: { x: event.clientX, y: event.clientY },
  timestamp: Date.now()
};

// 特定元素追踪
if (target.classList.contains('book-btn')) {
  this.recordBusinessEvent('book-button-click', clickData);
} else if (target.classList.contains('gallery-nav')) {
  this.recordBusinessEvent('gallery-navigation', clickData);
} else if (target.classList.contains('package-card')) {
  this.recordBusinessEvent('package-click', clickData);
} else {
  this.recordBusinessEvent('general-click', clickData);
}

}

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';
}

bindBusinessEventTracking() {
// 监听自定义业务事件
document.addEventListener('youxia:phase1-complete', (e) => {
this.recordBusinessEvent('phase1-complete', {
duration: e.detail.reduce((sum, item) => {
const loadTime = item.data?.loadTime || 0;
return sum + loadTime;
}, 0),
timestamp: Date.now()
});
});

document.addEventListener('youxia:phase2-complete', (e) => {
  this.recordBusinessEvent('phase2-complete', {
    timestamp: Date.now()
  });
});

document.addEventListener('youxia:phase3-complete', (e) => {
  this.recordBusinessEvent('phase3-complete', {
    timestamp: Date.now()
  });
});

}

bindResourceTracking() {
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 ignoredPatterns = [
'analytics', 'tracking', 'beacon', 'chrome-extension',
'gtag', 'facebook', 'twitter'
];

return !ignoredPatterns.some(pattern => 
  entry.name.toLowerCase().includes(pattern)
);

}

recordMetric(type, data) {
if (!this.metrics[type]) {
this.metrics[type] = [];
}

this.metrics[type].push({
  ...data,
  sessionId: this.sessionId,
  userId: this.userId,
  page: window.location.pathname,
  userAgent: navigator.userAgent,
  deviceInfo: this.getDeviceInfo()
});

// 限制存储数量
if (this.metrics[type].length > 50) {
  this.metrics[type] = this.metrics[type].slice(-50);
}

}

recordBusinessEvent(eventName, data) {
this.businessEvents.push({
event: eventName,
data,
sessionId: this.sessionId,
userId: this.userId,
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,
language: navigator.language,
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
};

}

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);

if (totalMetrics >= 30) {
  this.reportMetrics();
}

}

async reportMetrics(isUnload = false) {
if (Object.keys(this.metrics).length === 0 && this.businessEvents.length === 0) {
return;
}

// 按采样率过滤
if (Math.random() > this.config.sampleRate) {
  this.metrics = {};
  this.businessEvents = [];
  return;
}

const data = {
  sessionId: this.sessionId,
  userId: this.userId,
  page: window.location.pathname,
  timestamp: Date.now(),
  metrics: this.metrics,
  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.metrics = {};
  this.businessEvents = [];
}

}
}

6.2 游虾性能优化效果

┌─────────────────────────────────────────────────────────────────┐
│ 游虾详情页优化效果对比 │
├─────────────┬─────────────┬─────────────┬──────────────┤
│ 指标 │ 优化前 │ 优化后 │ 提升幅度 │
├─────────────┼─────────────┼─────────────┼──────────────┤
│ LCP(ms) │ 3800 │ 1850 │ +51% ↓ │
│ FID(ms) │ 220 │ 95 │ +57% ↓ │
│ CLS │ 0.22 │ 0.06 │ +73% ↓ │
│ FCP(ms) │ 2600 │ 1200 │ +54% ↓ │
│ 首屏图片(s) │ 2.8 │ 0.9 │ +68% ↓ │
│ 套餐渲染(ms)│ 450 │ 120 │ +73% ↓ │
│ 内容加载(s) │ 3.2 │ 1.1 │ +66% ↓ │
│ 包体积(KB) │ 780 │ 420 │ +46% ↓ │
│ 请求数 │ 65 │ 38 │ +42% ↓ │
└─────────────┴─────────────┴──────────────┴──────────────┘

6.3 业务指标改善

• 页面转化率: 从 2.1% 提升至 3.2% (+52%)

• 平均停留时间: 从 2分30秒 提升至 4分15秒 (+70%)

• 跳出率: 从 58% 降低至 39% (-33%)

• 移动端订单占比: 从 82% 提升至 89% (+9%)

• 用户满意度: 提升 0.9 分 (4.2 → 5.1)

七、最佳实践总结

7.1 游虾专属优化清单

✅ 图片画廊优化(出境游核心)
├── AVIF/WebP自适应格式
├── 渐进式加载+预加载
├── 智能缓存策略
└── 触摸手势支持

✅ 套餐组合优化
├── 数据分块与压缩
├── 虚拟列表渲染
├── 懒加载与预取
└── 实时库存聚合

✅ 内容加载优化
├── 富文本分块处理
├── 关键内容优先
├── 懒加载非关键内容
└── 缓存策略优化

✅ 实时数据优化
├── 多供应商库存聚合
├── WebSocket价格推送
├── 智能缓存失效
└── 降级策略

✅ 监控体系
├── Core Web Vitals追踪
├── 业务指标埋点
├── 性能预算告警
└── A/B测试集成

7.2 持续演进方向

  1. Edge Functions: 使用边缘计算处理图片优化和库存聚合
  2. WebAssembly: 用于图片处理和复杂计算
  3. Predictive Loading: 基于用户行为预测加载内容
  4. Adaptive UI: 根据设备能力动态调整界面复杂度
  5. AI优化: 使用机器学习优化图片压缩和加载策略

需要我针对游虾的套餐组合渲染或实时库存聚合提供更详细的实现细节吗?

相关文章
|
5天前
|
人工智能 安全 API
CoPaw:5分钟部署你的 AI助理
源自阿里巴巴开源生态的个人 AI 助理——CoPaw。作为阿里倾力打造的开源力作,CoPaw 完美打通钉钉、飞书、Discord 等多平台对话通道,支持定时任务自动化。内置 PDF/Office 深度处理、新闻摘要等强大技能,更开放自定义扩展接口。坚持数据全程私有化部署,绝不上传云端,让每一位用户都能在大厂技术加持下,拥有安全、专属的智能助手。
|
8天前
|
人工智能 自然语言处理 JavaScript
2026年Windows+Ollama本地部署OpenClaw保姆级教程:本地AI Agent+阿里云上快速搭建
2026年OpenClaw凭借本地部署、私有化运行的特性,成为打造个人智能体的核心工具,而Ollama作为轻量级本地大模型管理工具,能让OpenClaw摆脱对云端大模型的依赖,实现**本地推理、数据不泄露、全流程私有化**的智能体验。本文基于Windows 11系统,从硬件环境准备、Ollama安装与模型定制、OpenClaw部署配置、技能扩展到常见问题排查,打造保姆级本地部署教程,同时补充阿里云OpenClaw(Clawdbot)快速部署步骤,兼顾本地私有化需求与云端7×24小时运行需求,文中所有代码命令均可直接复制执行,确保零基础用户也能快速搭建属于自己的本地智能体。
9334 75
|
6天前
|
人工智能 安全 JavaScript
阿里云上+本地部署OpenClaw(小龙虾)新手攻略:解锁10大必备Skills,零基础也能玩转AI助手
2026年,开源AI代理工具OpenClaw(昵称“小龙虾”)凭借“能实际做事”的核心优势,在GitHub斩获25万+星标,成为现象级AI工具。它最强大的魅力在于可扩展的Skills(技能包)系统——通过ClawHub插件市场的数百个技能,能让AI助手从简单聊天升级为处理办公、学习、日常事务的全能帮手。
4733 12
|
7天前
|
人工智能 自然语言处理 机器人
保姆级教程:Mac本地搭建OpenClaw及阿里云上1分钟部署OpenClaw+飞书集成实战指南
OpenClaw(曾用名Clawdbot、Moltbot)作为2026年最热门的开源个人AI助手平台,以“自然语言驱动自动化”为核心,支持对接飞书、Telegram等主流通讯工具,可替代人工完成文件操作、日历管理、邮件处理等重复性工作。其模块化架构适配多系统环境,既可以在Mac上本地化部署打造私人助手,也能通过阿里云实现7×24小时稳定运行,完美兼顾隐私性与便捷性。
4826 11
|
9天前
|
人工智能 JSON JavaScript
手把手教你用 OpenClaw + 飞书,打造专属 AI 机器人
手把手教你用 OpenClaw(v2026.2.22-2)+ 飞书,10分钟零代码搭建专属AI机器人!内置飞书插件,无需额外安装;支持Claude等主流模型,命令行一键配置。告别复杂开发,像聊同事一样自然对话。
5170 13
手把手教你用 OpenClaw + 飞书,打造专属 AI 机器人
|
8天前
|
人工智能 监控 机器人
2026年零门槛部署 OpenClaw(Clawdbot)接入A股数据,实现24小时股票分析保姆级教程
在AI赋能金融分析的浪潮中,OpenClaw(原Clawdbot/Moltbot)凭借开源灵活的架构,成为个人投资者打造专属智能分析助手的首选。通过接入A股实时数据,它能实现24小时市场监控、涨跌预警、潜力股推荐等核心功能,彻底解放人工盯盘的繁琐。而阿里云的稳定部署环境,更让这套系统实现全天候不间断运行,成为真正的“金融AI助手”。 本文基于OpenClaw v2026.1.25稳定版与QVeris免费A股数据接口,详细拆解阿里云OpenClaw部署步骤、A股数据接入流程、高级分析功能配置及多平台联动技巧,所有代码命令均可直接复制复用,即使无技术基础也能在1小时内完成从部署到实战的全流程。
3605 11
|
3天前
|
人工智能 JavaScript Ubuntu
5分钟上手龙虾AI!OpenClaw部署(阿里云+本地)+ 免费多模型配置保姆级教程(MiniMax、Claude、阿里云百炼)
OpenClaw(昵称“龙虾AI”)作为2026年热门的开源个人AI助手,由PSPDFKit创始人Peter Steinberger开发,核心优势在于“真正执行任务”——不仅能聊天互动,还能自动处理邮件、管理日程、订机票、写代码等,且所有数据本地处理,隐私完全可控。它支持接入MiniMax、Claude、GPT等多类大模型,兼容微信、Telegram、飞书等主流聊天工具,搭配100+可扩展技能,成为兼顾实用性与隐私性的AI工具首选。
2246 6