开山网商品详情页前端性能优化实战
一、开山网业务场景分析
1.1 开山网平台特征
开山网作为国内领先的鞋类B2B批发平台,其商品详情页具有独特的业务特点:
// 开山网商品详情页特性
interface KaiShanWangProductFeatures {
// 鞋类行业特征
footwearFeatures: {
multipleAngles: number; // 多角度展示(正侧背底内)
sizeChartRequired: boolean; // 必须显示尺码表
colorVariants: ColorVariant[]; // 颜色变体
materialShowcase: Material[]; // 材质展示
sizeGrid: SizeGrid; // 尺码网格
};
// B2B批发特征
wholesaleFeatures: {
bulkPricing: BulkPricing[]; // 阶梯批发价
minOrderQuantity: number; // 最小起订量
inventoryBySize: SizeInventory[]; // 各尺码库存
supplierInfo: SupplierProfile; // 供应商信息
tradeAssurance: boolean; // 交易保障
};
// 移动端批采特征
mobileWholesale: {
quickBulkSelect: boolean; // 快速批量选码
oneClickInquiry: boolean; // 一键询价
shareToWechat: boolean; // 微信分享
saveToFavorites: boolean; // 收藏商品
};
// 内容展示特征
contentFeatures: {
richDescriptions: Description[]; // 富文本描述
detailImages: DetailImage[]; // 详情图
videoShowcase: Video[]; // 视频展示
modelShows: ModelShow[]; // 模特展示
};
}1.2 开山网性能痛点
// 开山网性能痛点分析
const kaiShanWangPainPoints = {
// 图片资源极其丰富
imageHeavy: {
mainImages: 12-20, // 主图(正、侧、背、底、内、细节)
sizeCharts: 3-5, // 尺码表(男女童、不同标准)
colorSwatches: 8-15, // 颜色色卡
materialTextures: 5-10, // 材质纹理特写
detailShots: 20-40, // 工艺细节图
modelShots: 10-20, // 模特展示图
totalImages: 60-100+ // 总计图片数量
},
// 尺码选择复杂
complexSizeSelection: {
sizeStandards: ['EUR', 'US', 'UK', 'CM'], // 多尺码标准
sizeGrid: '6x10+', // 6行10列以上的尺码网格
inventoryCheck: 'per-size', // 每个尺码独立库存
realtimeStock: true, // 实时库存
sizeChartImages: true // 尺码表为图片格式
},
// 批发价格计算复杂
complexPricing: {
bulkPricingTiers: 5-10, // 5-10个批发阶梯
priceRules: 'complex', // 复杂价格规则
memberDiscounts: true, // 会员折扣
promotionOverlays: true, // 促销叠加
realtimeCalculation: true // 实时计算
},
// 移动端批采场景
mobileBulkPurchase: {
quickSelectMode: true, // 快速选码模式
largeSizeGrid: true, // 大尺码表格
touchOptimized: true, // 触控优化
oneHandOperation: true, // 单手操作
shareAndInquire: true // 分享询价
}
};二、开山网图片资源优化
2.1 鞋类商品图片智能加载
// 开山网智能图片加载系统
import { memo, useState, useCallback, useEffect, useRef, useMemo } from 'react';
import { useIntersectionObserver } from 'react-intersection-observer';
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
// 开山网图片分类
type ShoeImageType =
| 'main-front' // 正面主图
| 'main-side' // 侧面主图
| 'main-back' // 背面主图
| 'main-sole' // 鞋底主图
| 'main-inner' // 内里主图
| 'main-detail' // 细节主图
| 'size-chart' // 尺码表
| 'color-swatch' // 颜色色卡
| 'material-texture' // 材质纹理
| 'detail-shot' // 工艺细节
| 'model-show' // 模特展示
| 'video-cover'; // 视频封面
interface ShoeImage {
id: string;
type: ShoeImageType;
url: string;
thumbnail: string;
webpUrl?: string;
avifUrl?: string;
width: number;
height: number;
size: number;
colorVariant?: string;
angle?: string;
priority: 'critical' | 'high' | 'medium' | 'low';
preloadGroup: 'above-fold' | 'below-fold' | 'on-demand';
}
interface ShoeImageLoaderProps {
images: ShoeImage[];
productId: string;
enableSmartLoading: boolean;
onImageLoad: (imageId: string, loadTime: number, type: ShoeImageType) => void;
onImageError: (imageId: string, type: ShoeImageType) => void;
}
const ShoeImageLoader = memo(({
images,
productId,
enableSmartLoading,
onImageLoad,
onImageError
}: ShoeImageLoaderProps) => {
const [loadedImages, setLoadedImages] = useState<Map<string, boolean>>(new Map());
const [currentMainIndex, setCurrentMainIndex] = useState(0);
const [isFullscreen, setIsFullscreen] = useState(false);
const [showSizeChart, setShowSizeChart] = useState(false);
const [showColorSwatches, setShowColorSwatches] = useState(false);
const [devicePixelRatio, setDevicePixelRatio] = useState(1);
const [viewportWidth, setViewportWidth] = useState(window.innerWidth);
const [connectionType, setConnectionType] = useState<string>('unknown');
const [userPreference, setUserPreference] = useState<UserImagePreference>({
preferredFormat: 'auto',
qualityLevel: 'high',
showWatermark: false
});
// 设备信息检测
useEffect(() => {
const updateDeviceInfo = () => {
setDevicePixelRatio(window.devicePixelRatio || 1);
setViewportWidth(window.innerWidth);
if (navigator.connection) {
setConnectionType(navigator.connection.effectiveType || 'unknown');
}
};
updateDeviceInfo();
window.addEventListener('resize', updateDeviceInfo);
if (navigator.connection) {
navigator.connection.addEventListener('change', updateDeviceInfo);
}
// 读取用户偏好设置
const savedPreference = localStorage.getItem('ksw_image_preference');
if (savedPreference) {
try {
setUserPreference(JSON.parse(savedPreference));
} catch (e) {
console.warn('Failed to parse image preference:', e);
}
}
return () => {
window.removeEventListener('resize', updateDeviceInfo);
if (navigator.connection) {
navigator.connection.removeEventListener('change', updateDeviceInfo);
}
};
}, []);
// 根据用户偏好和网络状况确定图片格式
const getOptimalImageUrl = useCallback((image: ShoeImage): string => {
// 如果用户手动选择了格式
if (userPreference.preferredFormat !== 'auto') {
switch (userPreference.preferredFormat) {
case 'avif':
if (image.avifUrl) return image.avifUrl;
break;
case 'webp':
if (image.webpUrl) return image.webpUrl;
break;
}
}
// 自动选择最佳格式
if (image.avifUrl && supportsAvif()) {
return image.avifUrl;
}
if (image.webpUrl && supportsWebp()) {
return image.webpUrl;
}
return image.url;
}, [userPreference.preferredFormat]);
// 根据网络状况调整质量等级
const getQualityLevel = useCallback((): QualityLevel => {
switch (connectionType) {
case '4g':
case 'wifi':
return userPreference.qualityLevel === 'high' ? 'high' : 'medium';
case '3g':
return 'medium';
case '2g':
case 'slow-2g':
default:
return 'low';
}
}, [connectionType, userPreference.qualityLevel]);
// 计算鞋子图片的最佳尺寸
const getOptimalSize = useCallback((originalWidth: number, originalHeight: number, type: ShoeImageType) => {
const maxWidth = viewportWidth;
// 不同类型图片的尺寸策略
let ratio = originalWidth / originalHeight;
switch (type) {
case 'main-front':
case 'main-side':
case 'main-back':
// 主图占满屏幕宽度
ratio = 4 / 5;
break;
case 'main-sole':
case 'main-inner':
// 底部和内里稍窄
ratio = 3 / 4;
break;
case 'size-chart':
// 尺码表需要足够宽度显示
ratio = 2 / 3;
break;
case 'color-swatch':
// 色卡小图
ratio = 1;
break;
case 'material-texture':
// 材质特写
ratio = 1;
break;
case 'detail-shot':
// 细节图
ratio = 3 / 4;
break;
case 'model-show':
// 模特图
ratio = 3 / 4;
break;
case 'video-cover':
// 视频封面
ratio = 16 / 9;
break;
}
const optimalWidth = Math.min(originalWidth, maxWidth);
const optimalHeight = optimalWidth / ratio;
return {
width: Math.round(optimalWidth * devicePixelRatio),
height: Math.round(optimalHeight * devicePixelRatio)
};
}, [viewportWidth, devicePixelRatio]);
// 按类型分组图片
const groupedImages = useMemo(() => {
const groups: Record<ShoeImageType, ShoeImage[]> = {
'main-front': [],
'main-side': [],
'main-back': [],
'main-sole': [],
'main-inner': [],
'main-detail': [],
'size-chart': [],
'color-swatch': [],
'material-texture': [],
'detail-shot': [],
'model-show': [],
'video-cover': []
};
images.forEach(image => {
if (groups[image.type]) {
groups[image.type].push(image);
}
});
return groups;
}, [images]);
// 获取关键图片(首屏必需)
const criticalImages = useMemo(() => {
const critical: ShoeImage[] = [];
// 正面主图是关键
if (groupedImages['main-front'].length > 0) {
critical.push(groupedImages['main-front'][0]);
}
// 第一个颜色的所有主图
Object.values(groupedImages).forEach(group => {
group.forEach(image => {
if (image.priority === 'critical') {
critical.push(image);
}
});
});
return critical;
}, [groupedImages]);
// 渐进式图片组件
const ProgressiveShoeImage = memo(({
image,
index,
isActive,
showWatermark
}: {
image: ShoeImage;
index: number;
isActive: boolean;
showWatermark: boolean;
}) => {
const [loadState, setLoadState] = useState<'loading' | 'loaded' | 'error'>('loading');
const [blurUrl, setBlurUrl] = useState<string>('');
const [fullUrl, setFullUrl] = useState<string>('');
const [watermarkUrl, setWatermarkUrl] = useState<string>('');
const imageRef = useRef<HTMLImageElement>(null);
const { ref: intersectionRef, inView } = useIntersectionObserver({
threshold: 0.1,
triggerOnce: false
});
// 生成模糊占位图
useEffect(() => {
if (enableSmartLoading && image.thumbnail) {
generateBlurPlaceholder(image.thumbnail).then(url => {
setBlurUrl(url);
});
}
}, [image.thumbnail, enableSmartLoading]);
// 加载完整图片
useEffect(() => {
if (!inView && !isActive && image.preloadGroup !== 'above-fold') return;
const startTime = performance.now();
const optimalUrl = getOptimalImageUrl(image);
const size = getOptimalSize(image.width, image.height, image.type);
// 构建带尺寸的URL(CDN参数)
const sizedUrl = optimalUrl.replace(/(\.\w+)(?=[?#]|$)/, `_${size.width}x${size.height}$1`);
const img = new Image();
img.onload = () => {
const loadTime = performance.now() - startTime;
setFullUrl(sizedUrl);
setLoadState('loaded');
onImageLoad(image.id, loadTime, image.type);
// 预加载同组其他图片
preloadGroupImages(image.type, images);
};
img.onerror = () => {
setLoadState('error');
onImageError(image.id, image.type);
};
img.src = sizedUrl;
}, [inView, isActive, image, getOptimalImageUrl, getOptimalSize, onImageLoad, onImageError, images]);
// 水印处理
useEffect(() => {
if (showWatermark && loadState === 'loaded' && image.type === 'main-front') {
// 为批发商保护图片,添加水印
addWatermark(fullUrl).then(url => {
setWatermarkUrl(url);
});
}
}, [showWatermark, loadState, fullUrl, image.type]);
return (
<div
ref={intersectionRef}
className={`shoe-image-container ${isActive ? 'active' : ''} ${loadState} ${image.type}`}
data-image-type={image.type}
>
{/* 加载中显示模糊占位 */}
{loadState === 'loading' && blurUrl && (
<div
className="blur-placeholder shoe-blur"
style={{ backgroundImage: `url(${blurUrl})` }}
>
<div className="loading-spinner">
<div className="spinner-ring"></div>
<span>加载中...</span>
</div>
</div>
)}
{/* 加载完成的图片 */}
{(fullUrl || watermarkUrl) && (
<img
ref={imageRef}
src={watermarkUrl || fullUrl}
alt={`${image.type} - ${index + 1}`}
className={`shoe-image ${loadState === 'loaded' ? 'loaded' : ''}`}
loading={image.priority === 'critical' ? 'eager' : 'lazy'}
decoding="async"
style={{
aspectRatio: `${image.width}/${image.height}`
}}
/>
)}
{/* 错误状态 */}
{loadState === 'error' && (
<div className="image-error shoe-error">
<span className="error-icon">👟</span>
<span>图片加载失败</span>
<button onClick={() => setLoadState('loading')}>重试</button>
</div>
)}
{/* 图片类型标签 */}
{isActive && (
<div className="image-type-label">
{getImageTypeLabel(image.type)}
</div>
)}
{/* 图片计数器 */}
{isActive && (
<div className="image-counter">
{currentMainIndex + 1} / {groupedImages['main-front'].length}
</div>
)}
</div>
);
});
// 预加载同组图片
const preloadGroupImages = useCallback((type: ShoeImageType, allImages: ShoeImage[]) => {
const groupImages = allImages.filter(img => img.type === type && img.id !== allImages[currentMainIndex]?.id);
const preloadIndexes = [0, 1, 2]; // 预加载前3张
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
preloadIndexes.forEach(i => {
const image = groupImages[i];
if (image && !loadedImages.has(image.id)) {
const url = getOptimalImageUrl(image);
const img = new Image();
img.src = url;
setLoadedImages(prev => new Map(prev).set(image.id, true));
}
});
}, [getOptimalImageUrl, loadedImages, currentMainIndex]);
// 获取图片类型中文标签
const getImageTypeLabel = (type: ShoeImageType): string => {
const labels: Record<ShoeImageType, string> = {
'main-front': '正面',
'main-side': '侧面',
'main-back': '背面',
'main-sole': '鞋底',
'main-inner': '内里',
'main-detail': '细节',
'size-chart': '尺码表',
'color-swatch': '色卡',
'material-texture': '材质',
'detail-shot': '工艺',
'model-show': '模特',
'video-cover': '视频'
};
return labels[type] || type;
};
// 主图切换
const handleMainImageChange = useCallback((direction: 'prev' | 'next') => {
const mainImages = groupedImages['main-front'];
if (mainImages.length === 0) return;
setCurrentMainIndex(prev => {
if (direction === 'prev') {
return prev > 0 ? prev - 1 : mainImages.length - 1;
}
return prev < mainImages.length - 1 ? prev + 1 : 0;
});
}, [groupedImages]);
// 键盘导航
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!isFullscreen) return;
switch (e.key) {
case 'ArrowLeft':
handleMainImageChange('prev');
break;
case 'ArrowRight':
handleMainImageChange('next');
break;
case 'Escape':
setIsFullscreen(false);
break;
case 's':
case 'S':
if (e.ctrlKey) {
e.preventDefault();
setShowSizeChart(prev => !prev);
}
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isFullscreen, handleMainImageChange]);
// 触摸滑动支持
const handleTouchStart = useCallback((e: React.TouchEvent) => {
// 实现滑动切换
}, []);
const handleTouchEnd = useCallback((e: React.TouchEvent) => {
// 实现滑动切换
}, []);
const mainImages = groupedImages['main-front'];
const currentMainImage = mainImages[currentMainIndex];
return (
<div className="shoe-image-loader">
{/* 主图展示区 */}
<div
className="main-image-area"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{currentMainImage && (
<ProgressiveShoeImage
image={currentMainImage}
index={currentMainIndex}
isActive={true}
showWatermark={userPreference.showWatermark}
/>
)}
{/* 导航按钮 */}
{mainImages.length > 1 && (
<>
<button
className="nav-btn prev"
onClick={() => handleMainImageChange('prev')}
aria-label="上一张"
>
‹
</button>
<button
className="nav-btn next"
onClick={() => handleMainImageChange('next')}
aria-label="下一张"
>
›
</button>
</>
)}
{/* 快捷操作按钮 */}
<div className="quick-actions">
<button
className={`action-btn ${showSizeChart ? 'active' : ''}`}
onClick={() => setShowSizeChart(!showSizeChart)}
title="查看尺码表 (Ctrl+S)"
>
📏 尺码表
</button>
<button
className={`action-btn ${showColorSwatches ? 'active' : ''}`}
onClick={() => setShowColorSwatches(!showColorSwatches)}
title="查看颜色"
>
🎨 颜色
</button>
<button
className="action-btn"
onClick={() => setIsFullscreen(true)}
title="全屏查看"
>
⛶ 全屏
</button>
</div>
</div>
{/* 缩略图列表 */}
<div className="thumbnail-strip">
{mainImages.map((image, index) => (
<button
key={image.id}
className={`thumbnail ${index === currentMainIndex ? 'active' : ''} ${image.type}`}
onClick={() => setCurrentMainIndex(index)}
aria-label={`查看${getImageTypeLabel(image.type)}`}
>
<img
src={image.thumbnail}
alt={`缩略图 ${index + 1}`}
loading="lazy"
/>
<span className="thumbnail-label">{getImageTypeLabel(image.type)}</span>
</button>
))}
</div>
{/* 尺码表弹窗 */}
{showSizeChart && (
<SizeChartModal
images={groupedImages['size-chart']}
onClose={() => setShowSizeChart(false)}
getOptimalUrl={getOptimalImageUrl}
/>
)}
{/* 颜色色卡 */}
{showColorSwatches && (
<ColorSwatchesPanel
images={groupedImages['color-swatch']}
onClose={() => setShowColorSwatches(false)}
onSelect={(color) => {
// 切换到对应颜色的图片
}}
/>
)}
{/* 全屏灯箱 */}
{isFullscreen && currentMainImage && (
<FullscreenLightbox
images={mainImages}
currentIndex={currentMainIndex}
onClose={() => setIsFullscreen(false)}
onNavigate={handleMainImageChange}
getOptimalUrl={getOptimalImageUrl}
imageType="shoe-main"
/>
)}
{/* 图片设置面板 */}
<ImageSettingsPanel
preference={userPreference}
onPreferenceChange={setUserPreference}
/>
</div>
);
});
// 尺码表模态框
const SizeChartModal = memo(({
images,
onClose,
getOptimalUrl
}: {
images: ShoeImage[];
onClose: () => void;
getOptimalUrl: (img: ShoeImage) => string;
}) => {
const [currentIndex, setCurrentIndex] = useState(0);
return (
<div className="size-chart-modal">
<div className="modal-backdrop" onClick={onClose} />
<div className="modal-content" onClick={e => e.stopPropagation()}>
<button className="close-btn" onClick={onClose}>×</button>
<div className="modal-header">
<h3>📏 尺码对照表</h3>
<div className="size-standard-tabs">
<button className="tab active">欧码 EUR</button>
<button className="tab">美码 US</button>
<button className="tab">英码 UK</button>
<button className="tab">厘米 CM</button>
</div>
</div>
<div className="modal-body">
{images.length > 0 ? (
<img
src={getOptimalUrl(images[currentIndex])}
alt="尺码表"
className="size-chart-image"
/>
) : (
<div className="no-size-chart">
<p>暂无尺码表图片</p>
<button onClick={() => {
// 触发下载尺码表
}}>
下载PDF尺码表
</button>
</div>
)}
{images.length > 1 && (
<div className="chart-navigation">
<button
onClick={() => setCurrentIndex(prev => prev > 0 ? prev - 1 : images.length - 1)}
>
‹ 上一个
</button>
<span>{currentIndex + 1} / {images.length}</span>
<button
onClick={() => setCurrentIndex(prev => prev < images.length - 1 ? prev + 1 : 0)}
>
下一个 ›
</button>
</div>
)}
</div>
<div className="modal-footer">
<button className="download-btn">
📥 下载尺码表
</button>
<button className="print-btn">
🖨️ 打印
</button>
</div>
</div>
</div>
);
});
// 颜色色卡面板
const ColorSwatchesPanel = memo(({
images,
onClose,
onSelect
}: {
images: ShoeImage[];
onClose: () => void;
onSelect: (color: string) => void;
}) => {
return (
<div className="color-swatches-panel">
<div className="panel-header">
<h4>🎨 可选颜色</h4>
<button className="close-btn" onClick={onClose}>×</button>
</div>
<div className="swatches-grid">
{images.map((image, index) => (
<button
key={image.id}
className="color-swatch"
onClick={() => onSelect(image.colorVariant || `颜色${index + 1}`)}
>
<img src={image.thumbnail} alt={image.colorVariant} />
<span className="color-name">{image.colorVariant || `颜色${index + 1}`}</span>
</button>
))}
</div>
</div>
);
});
// 图片设置面板
const ImageSettingsPanel = memo(({
preference,
onPreferenceChange
}: {
preference: UserImagePreference;
onPreferenceChange: (pref: UserImagePreference) => void;
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="image-settings-panel">
<button
className="settings-toggle"
onClick={() => setIsOpen(!isOpen)}
title="图片设置"
>
⚙️
</button>
{isOpen && (
<div className="settings-dropdown">
<h4>图片设置</h4>
<div className="setting-item">
<label>图片格式</label>
<select
value={preference.preferredFormat}
onChange={(e) => onPreferenceChange({
...preference,
preferredFormat: e.target.value as any
})}
>
<option value="auto">自动选择</option>
<option value="avif">AVIF(最优)</option>
<option value="webp">WebP</option>
<option value="jpg">JPEG</option>
</select>
</div>
<div className="setting-item">
<label>画质级别</label>
<select
value={preference.qualityLevel}
onChange={(e) => onPreferenceChange({
...preference,
qualityLevel: e.target.value as any
})}
>
<option value="high">高清</option>
<option value="medium">标清</option>
<option value="low">省流</option>
</select>
</div>
<div className="setting-item checkbox">
<label>
<input
type="checkbox"
checked={preference.showWatermark}
onChange={(e) => onPreferenceChange({
...preference,
showWatermark: e.target.checked
})}
/>
显示防盗水印
</label>
</div>
<div className="setting-item">
<button
className="clear-cache-btn"
onClick={() => {
// 清除图片缓存
if ('caches' in window) {
caches.delete('ksw-image-cache');
}
window.location.reload();
}}
>
🗑️ 清除图片缓存
</button>
</div>
</div>
)}
</div>
);
});
// 类型定义
interface UserImagePreference {
preferredFormat: 'auto' | 'avif' | 'webp' | 'jpg';
qualityLevel: 'high' | 'medium' | 'low';
showWatermark: boolean;
}
type QualityLevel = 'high' | 'medium' | 'low';
// 辅助函数
function supportsAvif(): boolean {
if (typeof document === 'undefined') return false;
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
return canvas.toDataURL('image/avif').indexOf('image/avif') > 0;
}
function supportsWebp(): boolean {
if (typeof document === 'undefined') return false;
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
return canvas.toDataURL('image/webp').indexOf('image/webp') > 0;
}
async function generateBlurPlaceholder(url: string): Promise<string> {
return new Promise((resolve) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const size = 20;
canvas.width = size;
canvas.height = size;
if (ctx) {
ctx.drawImage(img, 0, 0, size, size);
resolve(canvas.toDataURL('image/jpeg', 0.5));
} else {
resolve(url);
}
};
img.onerror = () => resolve(url);
img.src = url;
});
}
async function addWatermark(imageUrl: string): Promise<string> {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
if (ctx) {
ctx.drawImage(img, 0, 0);
// 添加半透明水印文字
ctx.font = `${Math.max(14, img.width / 30)}px Arial`;
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
ctx.lineWidth = 2;
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
const watermarkText = '开山网 KSW.COM';
const x = canvas.width / 2;
const y = canvas.height - 20;
ctx.strokeText(watermarkText, x, y);
ctx.fillText(watermarkText, x, y);
resolve(canvas.toDataURL('image/jpeg', 0.9));
} else {
resolve(imageUrl);
}
};
img.onerror = () => resolve(imageUrl);
img.src = imageUrl;
});
}2.2 开山网CDN优化策略
// 开山网图片CDN管理器
class KaiShanWangCDNManager {
private static instance: KaiShanWangCDNManager;
private cache: Map<string, string> = new Map();
private preloadQueue: PreloadTask[] = [];
private isProcessingQueue = false;
private imageCache: Cache | null = null;
// 开山网CDN配置
private readonly CDN_BASE_URL = 'https://img.kaishan.com';
private readonly SUPPORTED_FORMATS = ['avif', 'webp', 'jpg', 'png'];
private readonly DEFAULT_QUALITY = 85;
private readonly MOBILE_QUALITY = 70;
private readonly DESKTOP_QUALITY = 90;
private constructor() {
this.initImageCache();
}
static getInstance(): KaiShanWangCDNManager {
if (!KaiShanWangCDNManager.instance) {
KaiShanWangCDNManager.instance = new KaiShanWangCDNManager();
}
return KaiShanWangCDNManager.instance;
}
// 初始化图片缓存
private async initImageCache(): Promise<void> {
if ('caches' in window) {
try {
this.imageCache = await caches.open('ksw-image-cache-v1');
} catch (e) {
console.warn('Failed to open image cache:', e);
}
}
}
// 构建优化的图片URL
buildOptimizedUrl(
originalUrl: string,
options: ShoeImageTransformOptions
): string {
const cacheKey = this.generateCacheKey(originalUrl, options);
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!;
}
const url = new URL(`${this.CDN_BASE_URL}/shoes/${encodeURIComponent(originalUrl)}`);
const params = url.searchParams;
// 尺寸优化
if (options.width) {
params.set('w', options.width.toString());
}
if (options.height) {
params.set('h', options.height.toString());
}
if (options.fit) {
params.set('fit', options.fit);
}
// 格式优化
if (options.format) {
params.set('fmt', options.format);
}
if (options.quality) {
params.set('q', options.quality.toString());
}
// 鞋子特定优化
if (options.shoeType) {
params.set('shoe_type', options.shoeType);
}
if (options.angle) {
params.set('angle', options.angle);
}
if (options.showWatermark) {
params.set('wm', '1');
}
if (options.watermarkOpacity) {
params.set('wm_opacity', options.watermarkOpacity.toString());
}
// 转换优化
if (options.blur) {
params.set('blur', options.blur.toString());
}
if (options.sharpen) {
params.set('sharp', options.sharpen.toString());
}
if (options.contrast) {
params.set('contrast', options.contrast.toString());
}
if (options.brightness) {
params.set('bright', options.brightness.toString());
}
// 裁剪优化
if (options.crop) {
params.set('crop', options.crop);
}
if (options.gravity) {
params.set('gravity', options.gravity);
}
const optimizedUrl = url.toString();
this.cache.set(cacheKey, optimizedUrl);
return optimizedUrl;
}
// 生成缓存键
private generateCacheKey(url: string, options: ShoeImageTransformOptions): string {
return `${url}_${JSON.stringify(options)}`;
}
// 获取鞋子图片的最佳转换选项
getShoeImageOptions(
imageType: ShoeImageType,
deviceInfo: DeviceInfo
): ShoeImageTransformOptions {
const isMobile = deviceInfo.viewportWidth <= 768;
const quality = isMobile ? this.MOBILE_QUALITY : this.DESKTOP_QUALITY;
const baseOptions: ShoeImageTransformOptions = {
quality,
format: this.getBestSupportedFormat(deviceInfo),
fit: 'cover',
shoeType: this.getShoeTypeFromImageType(imageType)
};
// 根据图片类型调整选项
switch (imageType) {
case 'main-front':
case 'main-side':
case 'main-back':
return {
...baseOptions,
width: isMobile ? 400 : 800,
height: isMobile ? 500 : 1000,
gravity: 'center',
sharpen: 1.2
};
case 'main-sole':
case 'main-inner':
return {
...baseOptions,
width: isMobile ? 350 : 700,
height: isMobile ? 400 : 800,
gravity: 'center'
};
case 'size-chart':
return {
...baseOptions,
width: isMobile ? 300 : 600,
height: isMobile ? 400 : 800,
fit: 'contain',
quality: Math.max(quality - 10, 60) // 尺码表不需要太高清
};
case 'color-swatch':
return {
...baseOptions,
width: 100,
height: 100,
fit: 'fill',
quality: 80
};
case 'material-texture':
return {
...baseOptions,
width: isMobile ? 300 : 600,
height: isMobile ? 300 : 600,
sharpen: 1.5,
contrast: 1.1
};
case 'detail-shot':
return {
...baseOptions,
width: isMobile ? 350 : 700,
height: isMobile ? 450 : 900,
sharpen: 1.3
};
case 'model-show':
return {
...baseOptions,
width: isMobile ? 380 : 760,
height: isMobile ? 500 : 1000,
gravity: 'attention'
};
case 'video-cover':
return {
...baseOptions,
width: isMobile ? 400 : 800,
height: isMobile ? 225 : 450,
fit: 'cover'
};
default:
return baseOptions;
}
}
// 获取最佳支持的图片格式
private getBestSupportedFormat(deviceInfo: DeviceInfo): string {
// 检查用户偏好
const savedPreference = localStorage.getItem('ksw_image_format');
if (savedPreference && this.SUPPORTED_FORMATS.includes(savedPreference)) {
return savedPreference;
}
// 自动检测
if (deviceInfo.supportsAvif) return 'avif';
if (deviceInfo.supportsWebp) return 'webp';
return 'jpg';
}
// 从图片类型推断鞋子类型
private getShoeTypeFromImageType(imageType: ShoeImageType): string {
// 可以根据业务逻辑映射
const shoeTypeMap: Partial<Record<ShoeImageType, string>> = {
'main-front': 'general',
'main-side': 'general',
'main-back': 'general',
'main-sole': 'detail',
'main-inner': 'detail',
'size-chart': 'measurement',
'color-swatch': 'color',
'material-texture': 'material',
'detail-shot': 'craft',
'model-show': 'fashion',
'video-cover': 'media'
};
return shoeTypeMap[imageType] || 'general';
}
// 批量预加载图片
async preloadShoeImages(
images: ShoeImage[],
priority: 'critical' | 'high' | 'normal' | 'low' = 'normal'
): Promise<void> {
const preloadPromises = images
.filter(img => {
if (priority === 'critical') return img.priority === 'critical';
if (priority === 'high') return ['critical', 'high'].includes(img.priority);
return true;
})
.map(image => this.preloadSingleImage(image, priority));
await Promise.allSettled(preloadPromises);
}
// 预加载单张鞋子图片
private async preloadSingleImage(
image: ShoeImage,
priority: string
): Promise<void> {
try {
const cachedResponse = await this.imageCache?.match(image.url);
if (cachedResponse) {
return; // 已从缓存加载
}
const deviceInfo: DeviceInfo = {
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
devicePixelRatio: window.devicePixelRatio || 1,
supportsAvif: typeof document !== 'undefined' && supportsAvif(),
supportsWebp: typeof document !== 'undefined' && supportsWebp(),
connectionType: navigator.connection?.effectiveType || 'unknown'
};
const options = this.getShoeImageOptions(image.type, deviceInfo);
const optimizedUrl = this.buildOptimizedUrl(image.url, options);
const response = await fetch(optimizedUrl, {
priority: priority === 'critical' ? 'high' : 'auto',
cache: 'force-cache'
});
if (response.ok && this.imageCache) {
await this.imageCache.put(image.url, response.clone());
}
} catch (error) {
console.warn(`Failed to preload image ${image.id}:`, error);
}
}
// 智能图片加载策略
getSmartLoadStrategy(deviceInfo: DeviceInfo): ImageLoadStrategy {
const strategy: ImageLoadStrategy = {
formats: [],
quality: this.DEFAULT_QUALITY,
sizes: [],
preloadGroups: [],
lazyLoadThreshold: 0.1
};
// 根据设备能力选择格式
if (deviceInfo.supportsAvif) {
strategy.formats.push('avif');
}
if (deviceInfo.supportsWebp) {
strategy.formats.push('webp');
}
strategy.formats.push('jpg');
// 根据网络状况调整质量
switch (deviceInfo.connectionType) {
case '4g':
case 'wifi':
strategy.quality = deviceInfo.viewportWidth > 768 ? this.DESKTOP_QUALITY : this.MOBILE_QUALITY;
strategy.preloadGroups = ['above-fold'];
break;
case '3g':
strategy.quality = 65;
strategy.preloadGroups = ['above-fold'];
strategy.lazyLoadThreshold = 0.05;
break;
case '2g':
case 'slow-2g':
strategy.quality = 45;
strategy.preloadGroups = [];
strategy.lazyLoadThreshold = 0.01;
break;
default:
strategy.quality = this.DEFAULT_QUALITY;
}
// 根据屏幕大小确定尺寸
const screenWidth = deviceInfo.viewportWidth;
if (screenWidth <= 375) {
strategy.sizes = [320, 640];
} else if (screenWidth <= 480) {
strategy.sizes = [360, 720];
} else if (screenWidth <= 768) {
strategy.sizes = [480, 960];
} else if (screenWidth <= 1200) {
strategy.sizes = [640, 1280];
} else {
strategy.sizes = [800, 1600];
}
return strategy;
}
// 清理图片缓存
async clearImageCache(): Promise<void> {
if (this.imageCache) {
const keys = await this.imageCache.keys();
await Promise.all(keys.map(key => this.imageCache!.delete(key)));
}
this.cache.clear();
}
// 获取缓存统计
async getCacheStats(): Promise<CacheStats> {
if (!this.imageCache) {
return { count: 0, size: 0 };
}
const keys = await this.imageCache.keys();
// 估算大小(实际实现需要更复杂的计算)
return {
count: keys.length,
size: keys.length * 50000 // 粗略估算每张图片50KB
};
}
}
// 类型定义
interface ShoeImageTransformOptions {
width?: number;
height?: number;
fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
format?: 'jpg' | 'png' | 'webp' | 'avif';
quality?: number;
shoeType?: string;
angle?: string;
showWatermark?: boolean;
watermarkOpacity?: number;
blur?: number;
sharpen?: number;
contrast?: number;
brightness?: number;
crop?: string;
gravity?: 'center' | 'top' | 'bottom' | 'left' | 'right' | 'attention';
}
interface DeviceInfo {
viewportWidth: number;
viewportHeight: number;
devicePixelRatio: number;
supportsAvif: boolean;
supportsWebp: boolean;
connectionType: string;
memory?: number;
}
interface ImageLoadStrategy {
formats: string[];
quality: number;
sizes: number[];
preloadGroups: string[];
lazyLoadThreshold: number;
}
interface PreloadTask {
url: string;
priority: string;
timestamp: number;
}
interface CacheStats {
count: number;
size: number;
}三、开山网尺码选择优化
3.1 高性能尺码选择器
// 开山网高性能尺码选择器
import { memo, useState, useCallback, useMemo, useRef, useEffect } from 'react';
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
interface SizeOption {
euSize: string;
usSize: string;
ukSize: string;
cmSize: string;
inches: string;
inventory: number;
isAvailable: boolean;
}
interface SizeStandard {
id: string;
name: string;
sizes: SizeOption[];
}
interface SizeSelectorProps {
productId: string;
colorVariant: string;
sizeStandards: SizeStandard[];
onSizeSelect: (size: SizeOption, standardId: string) => void;
onSizeConfirm: (selectedSizes: SelectedSize[]) => void;
minOrderQuantity: number;
}
interface SelectedSize {
standardId: string;
euSize: string;
quantity: number;
}
const KaiShanWangSizeSelector = memo(({
productId,
colorVariant,
sizeStandards,
onSizeSelect,
onSizeConfirm,
minOrderQuantity
}: SizeSelectorProps) => {
const [selectedSizes, setSelectedSizes] = useState<Map<string, SelectedSize>>(new Map());
const [activeStandard, setActiveStandard] = useState<string>(sizeStandards[0]?.id || 'eu');
const [quickSelectMode, setQuickSelectMode] = useState(false);
const [bulkQuantity, setBulkQuantity] = useState(minOrderQuantity);
const [isExpanded, setIsExpanded] = useState(false);
const selectorRef = useRef<HTMLDivElement>(null);
// 计算每个尺码的库存状态
const getInventoryStatus = useCallback((inventory: number): InventoryStatus => {
if (inventory === 0) return { text: '缺货', color: '#ef4444', bgColor: '#fef2f2', disabled: true };
if (inventory <= 10) return { text: '紧张', color: '#f97316', bgColor: '#fff7ed', disabled: false };
if (inventory <= 50) return { text: '较少', color: '#eab308', bgColor: '#fefce8', disabled: false };
return { text: '充足', color: '#22c55e', bgColor: '#f0fdf4', disabled: false };
}, []);
// 获取尺码网格的列数
const gridColumns = useMemo(() => {
const screenWidth = window.innerWidth;
if (screenWidth <= 375) return 4;
if (screenWidth <= 480) return 5;
if (screenWidth <= 768) return 6;
return 8;
}, []);
// 切换尺码选择
const toggleSizeSelection = useCallback((standardId: string, size: SizeOption) => {
if (!size.isAvailable) return;
setSelectedSizes(prev => {
const newMap = new Map(prev);
const key = `${standardId}-${size.euSize}`;
if (newMap.has(key)) {
newMap.delete(key);
} else {
newMap.set(key, {
standardId,
euSize: size.euSize,
quantity: bulkQuantity
});
}
return newMap;
});
onSizeSelect(size, standardId);
}, [bulkQuantity, onSizeSelect]);
// 快速选择所有有库存的尺码
const selectAllAvailable = useCallback(() => {
const newMap = new Map<string, SelectedSize>();
sizeStandards.forEach(standard => {
standard.sizes.forEach(size => {
if (size.isAvailable) {
const key = `${standard.id}-${size.euSize}`;
newMap.set(key, {
standardId: standard.id,
euSize: size.euSize,
quantity: bulkQuantity
});
}
});
});
setSelectedSizes(newMap);
}, [sizeStandards, bulkQuantity]);
// 清空选择
const clearSelection = useCallback(() => {
setSelectedSizes(new Map());
}, []);
// 应用批量数量到所有选中尺码
const applyBulkQuantity = useCallback(() => {
setSelectedSizes(prev => {
const newMap = new Map(prev);
newMap.forEach((value, key) => {
newMap.set(key, { ...value, quantity: bulkQuantity });
});
return newMap;
});
}, [bulkQuantity]);
// 确认选择
const confirmSelection = useCallback(() => {
const selections = Array.from(selectedSizes.values());
if (selections.length > 0) {
onSizeConfirm(selections);
}
}, [selectedSizes, onSizeConfirm]);
// 计算选中尺码的总数量和总金额
const selectionSummary = useMemo(() => {
let totalQuantity = 0;
let totalAmount = 0;
selectedSizes.forEach(selection => {
totalQuantity += selection.quantity;
// 这里需要根据价格和数量计算金额
// totalAmount += getPriceForSize(selection) * selection.quantity;
});
return { totalQuantity, totalAmount };
}, [selectedSizes]);
// 当前激活的尺码标准
const activeStandardData = useMemo(() => {
return sizeStandards.find(s => s.id === activeStandard);
}, [sizeStandards, activeStandard]);
// 键盘导航支持
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!isExpanded) return;
switch (e.key) {
case 'Escape':
setIsExpanded(false);
break;
case 'a':
case 'A':
if (e.ctrlKey) {
e.preventDefault();
selectAllAvailable();
}
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isExpanded, selectAllAvailable]);
return (
<div className="ksw-size-selector" ref={selectorRef}>
{/* 选择器头部 */}
<div className="selector-header">
<div className="header-left">
<h3>👟 选择尺码</h3>
<span className="min-order">最小起订量: {minOrderQuantity}双</span>
</div>
<div className="header-right">
<button
className={`mode-toggle ${quickSelectMode ? 'active' : ''}`}
onClick={() => setQuickSelectMode(!quickSelectMode)}
>
{quickSelectMode ? '📋 标准模式' : '⚡ 快速选码'}
</button>
</div>
</div>
{/* 尺码标准切换 */}
<div className="size-standards">
{sizeStandards.map(standard => (
<button
key={standard.id}
className={`standard-tab ${activeStandard === standard.id ? 'active' : ''}`}
onClick={() => setActiveStandard(standard.id)}
>
{standard.name}
</button>
))}
</div>
{/* 尺码网格 */}
<div className="size-grid-container">
{activeStandardData && (
<div
className={`size-grid ${quickSelectMode ? 'quick-mode' : ''}`}
style={{ gridTemplateColumns: `repeat(${gridColumns}, 1fr)` }}
>
{activeStandardData.sizes.map((size, index) => {
const inventoryStatus = getInventoryStatus(size.inventory);
const isSelected = selectedSizes.has(`${activeStandard}-${size.euSize}`);
const key = `${activeStandard}-${size.euSize}`;
return (
<button
key={key}
className={`size-cell ${isSelected ? 'selected' : ''} ${!size.isAvailable ? 'unavailable' : ''}`}
onClick={() => toggleSizeSelection(activeStandard, size)}
disabled={!size.isAvailable}
style={{
'--inventory-color': inventoryStatus.color,
'--inventory-bg': inventoryStatus.bgColor
} as React.CSSProperties}
>
<div className="size-numbers">
<span className="eu-size">{size.euSize}</span>
{!quickSelectMode && (
<span className="secondary-size">{size.usSize}</span>
)}
</div>
{!quickSelectMode && (
<div className="inventory-info">
<span
className="inventory-status"
style={{ color: inventoryStatus.color }}
>
{inventoryStatus.text}
</span>
<span className="inventory-count">{size.inventory}</span>
</div>
)}
{isSelected && (
<div className="selected-indicator">✓</div>
)}
</button>
);
})}
</div>
)}
</div>
{/* 快速选码模式控制 */}
{quickSelectMode && (
<div className="quick-select-controls">
<div className="bulk-quantity">
<label>批量数量:</label>
<input
type="number"
min={minOrderQuantity}
max={999}
value={bulkQuantity}
onChange={(e) => setBulkQuantity(Math.max(minOrderQuantity, parseInt(e.target.value) || minOrderQuantity))}
/>
<span>双/尺码</span>
</div>
<button className="apply-bulk-btn" onClick={applyBulkQuantity}>
应用到所有选中
</button>
</div>
)}
{/* 已选尺码汇总 */}
{selectedSizes.size > 0 && (
<div className="selected-sizes-summary">
<div className="summary-header">
<span>已选 {selectedSizes.size} 个尺码</span>
<div className="summary-actions">
<button className="clear-btn" onClick={clearSelection}>
清空
</button>
<button className="select-all-btn" onClick={selectAllAvailable}>
全选有货
</button>
</div>
</div>
<div className="selected-list">
{Array.from(selectedSizes.values()).slice(0, 5).map((selection, index) => (
<div key={index} className="selected-item">
<span className="size-tag">{selection.euSize}</span>
<input
type="number"
min={minOrderQuantity}
max={999}
value={selection.quantity}
onChange={(e) => {
const newQuantity = Math.max(minOrderQuantity, parseInt(e.target.value) || minOrderQuantity);
setSelectedSizes(prev => {
const newMap = new Map(prev);
const key = `${selection.standardId}-${selection.euSize}`;
newMap.set(key, { ...selection, quantity: newQuantity });
return newMap;
});
}}
/>
<span className="item-total">¥--</span>
<button
className="remove-btn"
onClick={() => {
setSelectedSizes(prev => {
const newMap = new Map(prev);
const key = `${selection.standardId}-${selection.euSize}`;
newMap.delete(key);
return newMap;
});
}}
>
×
</button>
</div>
))}
{selectedSizes.size > 5 && (
<div className="more-items">
+还有 {selectedSizes.size - 5} 个尺码
</div>
)}
</div>
<div className="selection-total">
<span>总计: {selectionSummary.totalQuantity} 双</span>
<span className="total-amount">¥{selectionSummary.totalAmount.toFixed(2)}</span>
</div>
<button className="confirm-selection-btn" onClick={confirmSelection}>
确认选择并询价
</button>
</div>
)}
{/* 尺码指南 */}
<div className="size-guide">
<button
className="guide-link"
onClick={() => setIsExpanded(!isExpanded)}
>
📏 尺码指南 {isExpanded ? '▲' : '▼'}
</button>
{isExpanded && (
<div className="guide-content">
<div className="guide-table">
<table>
<thead>
<tr>
<th>欧码(EUR)</th>
<th>美码(US)</th>
<th>英码(UK)</th>
<th>脚长(CM)</th>
<th>脚长(IN)</th>
</tr>
</thead>
<tbody>
{activeStandardData?.sizes.slice(0, 10).map((size, index) => (
<tr key={index}>
<td>{size.euSize}</td>
<td>{size.usSize}</td>
<td>{size.ukSize}</td>
<td>{size.cmSize}</td>
<td>{size.inches}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="guide-tips">
<h4>💡 选码小贴士</h4>
<ul>
<li>建议下午测量脚长,此时脚部略微肿胀更接近实际穿着状态</li>
<li>脚宽较宽者建议选择大半码</li>
<li>运动鞋建议比日常皮鞋大半码</li>
<li>不确定尺码可联系客服免费索取样品试穿</li>
</ul>
</div>
<div className="guide-actions">
<button className="download-guide-btn">
📥 下载完整尺码表
</button>
<button className="contact-service-btn">
💬 咨询客服
</button>
</div>
</div>
)}
</div>
</div>
);
});
// 类型定义
interface InventoryStatus {
text: string;
color: string;
bgColor: string;
disabled: boolean;
}3.2 尺码数据优化
// 开山网尺码数据管理器
class KaiShanWangSizeDataManager {
private static instance: KaiShanWangSizeDataManager;
private sizeCache: Map<string, SizeDataCache> = new Map();
private priceCache: Map<string, PriceData> = new Map();
private inventoryCache: Map<string, InventoryData> = new Map();
private lastUpdateTimes: Map<string, number> = new Map();
// 缓存过期时间(毫秒)
private readonly CACHE_TTL = 5 * 60 * 1000; // 5分钟
private readonly INVENTORY_TTL = 30 * 1000; // 30秒
private readonly PRICE_TTL = 2 * 60 * 1000; // 2分钟
private constructor() {}
static getInstance(): KaiShanWangSizeDataManager {
if (!KaiShanWangSizeDataManager.instance) {
KaiShanWangSizeDataManager.instance = new KaiShanWangSizeDataManager();
}
return KaiShanWangSizeDataManager.instance;
}
// 获取尺码数据
async getSizeData(productId: string, colorVariant: string): Promise<SizeStandard[]> {
const cacheKey = `${productId}-${colorVariant}`;
const cached = this.sizeCache.get(cacheKey);
// 检查缓存是否有效
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
return cached.data;
}
// 从API获取数据
try {
const response = await fetch(`/api/products/${productId}/sizes?color=${colorVariant}`, {
headers: {
'Cache-Control': 'max-age=300'
}
});
if (!response.ok) {
throw new Error(`Failed to fetch size data: ${response.status}`);
}
const data: SizeStandard[] = await response.json();
// 更新缓存
this.sizeCache.set(cacheKey, {
data,
timestamp: Date.now()
});
this.lastUpdateTimes.set(cacheKey, Date.now());
return data;
} catch (error) {
console.error('Error fetching size data:', error);
// 返回缓存的旧数据(如果有)
if (cached) {
return cached.data;
}
// 返回空数组
return [];
}
}
// 获取库存数据(实时)
async getInventoryData(productId: string, colorVariant: string): Promise<Map<string, number>> {
const cacheKey = `${productId}-${colorVariant}`;
const cached = this.inventoryCache.get(cacheKey);
// 库存数据需要更频繁的更新
const now = Date.now();
if (cached && now - cached.timestamp < this.INVENTORY_TTL) {
return cached.data;
}
try {
const response = await fetch(`/api/products/${productId}/inventory?color=${colorVariant}&realtime=true`, {
headers: {
'Cache-Control': 'no-cache'
}
});
if (!response.ok) {
throw new Error(`Failed to fetch inventory: ${response.status}`);
}
const data: { size: string; inventory: number }[] = await response.json();
const inventoryMap = new Map<string, number>();
data.forEach(item => inventoryMap.set(item.size, item.inventory));
// 更新缓存
this.inventoryCache.set(cacheKey, {
data: inventoryMap,
timestamp: now
});
return inventoryMap;
} catch (error) {
console.error('Error fetching inventory:', error);
// 返回缓存的数据
if (cached) {
return cached.data;
}
return new Map();
}
}
// 批量获取多个商品的库存
async getBatchInventoryData(productIds: string[]): Promise<Map<string, Map<string, number>>> {
const results = new Map<string, Map<string, number>>();
// 使用Promise.all并行获取
const promises = productIds.map(async (productId) => {
try {
const response = await fetch(`/api/products/${productId}/inventory?batch=true`, {
headers: {
'Cache-Control': 'no-cache'
}
});
if (response.ok) {
const data = await response.json();
const inventoryMap = new Map<string, number>();
data.forEach((item: any) => inventoryMap.set(item.size, item.inventory));
results.set(productId, inventoryMap);
}
} catch (error) {
console.error(`Error fetching inventory for ${productId}:`, error);
}
});
await Promise.all(promises);
return results;
}
// 获取批发价格
async getBulkPricing(productId: string, colorVariant: string, quantity: number): Promise<BulkPricingTier[]> {
const cacheKey = `${productId}-${colorVariant}-${quantity}`;
const cached = this.priceCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.PRICE_TTL) {
return cached.data;
}
try {
const response = await fetch(
`/api/products/${productId}/pricing?color=${colorVariant}&quantity=${quantity}`,
{
headers: {
'Cache-Control': 'max-age=120'
}
}
);
if (!response.ok) {
throw new Error(`Failed to fetch pricing: ${response.status}`);
}
const data: BulkPricingTier[] = await response.json();
this.priceCache.set(cacheKey, {
data,
timestamp: Date.now()
});
return data;
} catch (error) {
console.error('Error fetching pricing:', error);
if (cached) {
return cached.data;
}
return [];
}
}
// 合并尺码和库存数据
mergeSizeWithInventory(
sizes: SizeStandard[],
inventory: Map<string, number>
): SizeStandard[] {
return sizes.map(standard => ({
...standard,
sizes: standard.sizes.map(size => ({
...size,
inventory: inventory.get(size.euSize) || 0,
isAvailable: (inventory.get(size.euSize) || 0) > 0
}))
}));
}
// 获取推荐尺码
getRecommendedSize(
footLength: number,
sizes: SizeStandard[]
): SizeOption | null {
if (!sizes.length) return null;
const euSizes = sizes[0].sizes;
// 找到最接近的尺码
let recommended: SizeOption | null = null;
let minDiff = Infinity;
euSizes.forEach(size => {
const cmSize = parseFloat(size.cmSize);
const diff = Math.abs(cmSize - footLength);
if (diff < minDiff && size.isAvailable) {
minDiff = diff;
recommended = size;
}
});
return recommended;
}
// 清除缓存
clearCache(pattern?: string): void {
if (pattern) {
// 清除匹配模式的缓存
[...this.sizeCache.keys()].forEach(key => {
if (key.includes(pattern)) {
this.sizeCache.delete(key);
}
});
[...this.inventoryCache.keys()].forEach(key => {
if (key.includes(pattern)) {
this.inventoryCache.delete(key);
}
});
[...this.priceCache.keys()].forEach(key => {
if (key.includes(pattern)) {
this.priceCache.delete(key);
}
});
} else {
// 清除所有缓存
this.sizeCache.clear();
this.inventoryCache.clear();
this.priceCache.clear();
this.lastUpdateTimes.clear();
}
}
// 获取缓存统计
getCacheStats(): CacheStatistics {
return {
sizeCache: {
count: this.sizeCache.size,
entries: Array.from(this.sizeCache.keys())
},
inventoryCache: {
count: this.inventoryCache.size,
entries: Array.from(this.inventoryCache.keys())
},
priceCache: {
count: this.priceCache.size,
entries: Array.from(this.priceCache.keys())
},
lastUpdates: Object.fromEntries(this.lastUpdateTimes)
};
}
// 订阅库存更新(WebSocket)
subscribeToInventoryUpdates(
productId: string,
callback: (inventory: Map<string, number>) => void
): () => void {
const ws = new WebSocket(`wss://realtime.kaishan.com/inventory/${productId}`);
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'inventory_update') {
const inventoryMap = new Map<string, number>();
data.inventory.forEach((item: any) => {
inventoryMap.set(item.size, item.quantity);
});
// 更新缓存
this.inventoryCache.set(productId, {
data: inventoryMap,
timestamp: Date.now()
});
callback(inventoryMap);
}
} catch (error) {
console.error('Error processing inventory update:', error);
}
};
ws.onclose = () => {
// 尝试重连
setTimeout(() => {
this.subscribeToInventoryUpdates(productId, callback);
}, 5000);
};
// 返回取消订阅函数
return () => {
ws.close();
};
}
}
// 类型定义
interface SizeDataCache {
data: SizeStandard[];
timestamp: number;
}
interface InventoryData {
data: Map<string, number>;
timestamp: number;
}
interface PriceData {
data: BulkPricingTier[];
timestamp: number;
}
interface BulkPricingTier {
minQuantity: number;
maxQuantity?: number;
price: number;
discount: number;
description: string;
}
interface CacheStatistics {
sizeCache: { count: number; entries: string[] };
inventoryCache: { count: number; entries: string[] };
priceCache: { count: number; entries: string[] };
lastUpdates: Record<string, number>;
}四、开山网批发价格计算优化
4.1 高性能价格计算器
// 开山网高性能批发价格计算器
import { memo, useState, useCallback, useMemo, useRef, useEffect } from 'react';
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
interface PricingRule {
id: string;
type: 'tiered' | 'member' | 'promotion' | 'bulk';
minQuantity: number;
maxQuantity?: number;
discount: number;
price: number;
description: string;
validFrom: Date;
validTo: Date;
conditions?: PricingCondition[];
}
interface PricingCondition {
type: 'customer-level' | 'region' | 'payment-method' | 'season';
value: string | number;
operator: 'eq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in';
}
interface PriceCalculationResult {
basePrice: number;
finalPrice: number;
totalPrice: number;
discountAmount: number;
appliedRules: PricingRule[];
savings: number;
savingsPercentage: number;
}
interface PriceCalculatorProps {
productId: string;
basePrice: number;
quantity: number;
customerLevel: string;
region: string;
paymentMethod: string;
onPriceCalculated: (result: PriceCalculationResult) => void;
}
const KaiShanWangPriceCalculator = memo(({
productId,
basePrice,
quantity,
customerLevel,
region,
paymentMethod,
onPriceCalculated
}: PriceCalculatorProps) => {
const [pricingRules, setPricingRules] = useState<PricingRule[]>([]);
const [isCalculating, setIsCalculating] = useState(false);
const [calculationResult, setCalculationResult] = useState<PriceCalculationResult | null>(null);
const [activeTab, setActiveTab] = useState<'breakdown' | 'savings' | 'rules'>('breakdown');
const calculationRef = useRef<number>(0);
const debouncedCalculateRef = useRef<ReturnType<typeof setTimeout>>();
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
// 加载定价规则
useEffect(() => {
const loadPricingRules = async () => {
try {
const response = await fetch(`/api/products/${productId}/pricing-rules`, {
headers: {
'Cache-Control': 'max-age=300'
}
});
if (response.ok) {
const rules: PricingRule[] = await response.json();
setPricingRules(rules);
}
} catch (error) {
console.error('Error loading pricing rules:', error);
}
};
loadPricingRules();
}, [productId]);
// 计算最终价格
const calculatePrice = useCallback(async () => {
const calculationId = ++calculationRef.current;
setIsCalculating(true);
// 使用setTimeout让UI有机会更新
await new Promise(resolve => setTimeout(resolve, 0));
try {
// 过滤适用的规则
const applicableRules = pricingRules.filter(rule => {
// 检查数量条件
if (quantity < rule.minQuantity) return false;
if (rule.maxQuantity && quantity > rule.maxQuantity) return false;
// 检查有效期
const now = new Date();
if (now < rule.validFrom || now > rule.validTo) return false;
// 检查附加条件
if (rule.conditions) {
const meetsConditions = rule.conditions.every(condition => {
switch (condition.type) {
case 'customer-level':
return checkCondition(customerLevel, condition.value, condition.operator);
case 'region':
return checkCondition(region, condition.value, condition.operator);
case 'payment-method':
return checkCondition(paymentMethod, condition.value, condition.operator);
default:
return true;
}
});
if (!meetsConditions) return false;
}
return true;
});
// 按优先级排序(数字越大优先级越高)
applicableRules.sort((a, b) => (b.discount - a.discount));
// 计算价格
let finalPrice = basePrice;
let totalDiscount = 0;
const appliedRules: PricingRule[] = [];
applicableRules.forEach(rule => {
const ruleDiscount = basePrice * rule.discount;
finalPrice -= ruleDiscount;
totalDiscount += ruleDiscount;
appliedRules.push(rule);
});
// 确保价格不低于最低价
const minimumPrice = basePrice * 0.3; // 最低3折
finalPrice = Math.max(finalPrice, minimumPrice);
const result: PriceCalculationResult = {
basePrice,
finalPrice,
totalPrice: finalPrice * quantity,
discountAmount: totalDiscount * quantity,
appliedRules,
savings: (basePrice - finalPrice) * quantity,
savingsPercentage: ((basePrice - finalPrice) / basePrice) * 100
};
// 只有最新的计算结果才更新状态
if (calculationId === calculationRef.current) {
setCalculationResult(result);
onPriceCalculated(result);
}
} catch (error) {
console.error('Error calculating price:', error);
} finally {
if (calculationId === calculationRef.current) {
setIsCalculating(false);
}
}
}, [productId, basePrice, quantity, customerLevel, region, paymentMethod, pricingRules, onPriceCalculated]);
// 检查条件是否满足
const checkCondition = (
actual: string | number,
expected: string | number,
operator: string
): boolean => {
switch (operator) {
case 'eq':
return actual === expected;
case 'gt':
return Number(actual) > Number(expected);
case 'gte':
return Number(actual) >= Number(expected);
case 'lt':
return Number(actual) < Number(expected);
case 'lte':
return Number(actual) <= Number(expected);
case 'in':
if (Array.isArray(expected)) {
return expected.includes(actual);
}
return String(actual).includes(String(expected));
default:
return true;
}
};
// 防抖计算
useEffect(() => {
if (debouncedCalculateRef.current) {
clearTimeout(debouncedCalculateRef.current);
}
debouncedCalculateRef.current = setTimeout(() => {
calculatePrice();
}, 100);
return () => {
if (debouncedCalculateRef.current) {
clearTimeout(debouncedCalculateRef.current);
}
};
}, [calculatePrice]);
// 格式化价格
const formatPrice = (price: number): string => {
return `¥${price.toFixed(2)}`;
};
// 获取规则类型图标
const getRuleTypeIcon = (type: string): string => {
const icons: Record<string, string> = {
'tiered': '📊',
'member': '👤',
'promotion': '🎁',
'bulk': '📦'
};
return icons[type] || '📋';
};
// 获取规则类型名称
const getRuleTypeName = (type: string): string => {
const names: Record<string, string> = {
'tiered': '阶梯批发价',
'member': '会员专享价',
'promotion': '促销活动价',
'bulk': '批量采购价'
};
return names[type] || type;
};
return (
<div className="ksw-price-calculator">
{/* 计算器头部 */}
<div className="calculator-header">
<h3>💰 批发价格计算</h3>
<div className="calculation-status">
{isCalculating ? (
<span className="calculating">
<span className="spinner"></span>
计算中...
</span>
) : (
<span className="calculated">✓ 已更新</span>
)}
</div>
</div>
{/* 价格显示区 */}
<div className="price-display">
<div className="price-row">
<span className="label">原价</span>
<span className="original-price">{formatPrice(basePrice)}</span>
</div>
{calculationResult && calculationResult.appliedRules.length > 0 && (
<div className="discount-breakdown">
{calculationResult.appliedRules.map((rule, index) => (
<div key={rule.id} className="discount-item">
<span className="discount-icon">{getRuleTypeIcon(rule.type)}</span>
<span className="discount-desc">{rule.description}</span>
<span className="discount-value">-{formatPrice(basePrice * rule.discount)}</span>
</div>
))}
</div>
)}
<div className="price-row final-price">
<span className="label">批发价</span>
<div className="final-price-content">
<span className="currency">¥</span>
<span className="amount">{calculationResult ? calculationResult.finalPrice.toFixed(2) : basePrice.toFixed(2)}</span>
<span className="unit">/双</span>
</div>
</div>
<div className="price-row total-price">
<span className="label">总计 ({quantity}双)</span>
<span className="total-amount">
{calculationResult ? formatPrice(calculationResult.totalPrice) : formatPrice(basePrice * quantity)}
</span>
</div>
{calculationResult && calculationResult.savings > 0 && (
<div className="savings-banner">
<span className="savings-icon">💎</span>
<span className="savings-text">
为您节省 <strong>{formatPrice(calculationResult.savings)}</strong>
({calculationResult.savingsPercentage.toFixed(1)}%)
</span>
</div>
)}
</div>
{/* 标签页切换 */}
<div className="calculator-tabs">
<button
className={`tab ${activeTab === 'breakdown' ? 'active' : ''}`}
onClick={() => setActiveTab('breakdown')}
>
价格明细
</button>
<button
className={`tab ${activeTab === 'savings' ? 'active' : ''}`}
onClick={() => setActiveTab('savings')}
>
省钱攻略
</button>
<button
className={`tab ${activeTab === 'rules' ? 'active' : ''}`}
onClick={() => setActiveTab('rules')}
>
适用规则
</button>
</div>
{/* 标签页内容 */}
<div className="tab-content">
{activeTab === 'breakdown' && (
<div className="breakdown-content">
<h4>📋 价格构成明细</h4>
<div className="breakdown-table">
<div className="breakdown-row header">
<span>项目</span>
<span>说明</span>
<span>金额</span>
</div>
<div className="breakdown-row">
<span>商品原价</span>
<span>{quantity}双 × {formatPrice(basePrice)}</span>
<span>{formatPrice(basePrice * quantity)}</span>
</div>
{calculationResult?.appliedRules.map((rule, index) => (
<div key={rule.id} className="breakdown-row discount">
<span>{getRuleTypeName(rule.type)}</span>
<span>{rule.description}</span>
<span className="negative">-{formatPrice(basePrice * rule.discount * quantity)}</span>
</div>
))}
<div className="breakdown-row total">
<span>最终价格</span>
<span>含所有优惠</span>
<span className="highlight">
{calculationResult ? formatPrice(calculationResult.totalPrice) : formatPrice(basePrice * quantity)}
</span>
</div>
</div>
</div>
)}
{activeTab === 'savings' && (
<div className="savings-content">
<h4>💡 如何获得更多优惠</h4>
<div className="savings-tips">
<div className="tip-card">
<div className="tip-icon">📦</div>
<div className="tip-content">
<h5>增加采购数量</h5>
<p>采购量达到 {pricingRules.find(r => r.type === 'tiered')?.minQuantity || 10} 双可享受阶梯批发价</p>
</div>
</div>
<div className="tip-card">
<div className="tip-icon">👤</div>
<div className="tip-content">
<h5>升级会员等级</h5>
<p>成为银牌/金牌会员享受额外 {pricingRules.find(r => r.type === 'member')?.discount ? (pricingRules.find(r => r.type === 'member')?.discount * 100) : 5}%-{pricingRules.find(r => r.type === 'member')?.discount ? (pricingRules.find(r => r.type === 'member')?.discount * 100) : 10}% 会员折扣</p>
</div>
</div>
<div className="tip-card">
<div className="tip-icon">🎯</div>
<div className="tip-content">
<h5>关注促销活动</h5>
<p>定期参与平台活动,最高可享 {pricingRules.find(r => r.type === 'promotion')?.discount ? (pricingRules.find(r => r.type === 'promotion')?.discount * 100) : 15}% 促销优惠</p>
</div>
</div>
<div className="tip-card">
<div className="tip-icon">💳</div>
<div className="tip-content">
<h5>选择优惠支付方式</h5>
<p>使用银行转账或平台授信支付,享受额外付款方式优惠</p>
</div>
</div>
</div>
</div>
)}
{activeTab === 'rules' && (
<div className="rules-content">
<h4>📜 当前适用价格规则</h4>
{pricingRules.length > 0 ? (
<div className="rules-list">
{pricingRules
.filter(rule => {
if (quantity < rule.minQuantity) return false;
if (rule.maxQuantity && quantity > rule.maxQuantity) return false;
return true;
})
.map(rule => (
<div key={rule.id} className="rule-item">
<div className="rule-header">
<span className="rule-icon">{getRuleTypeIcon(rule.type)}</span>
<span className="rule-name">{rule.description}</span>
<span className="rule-discount">-{rule.discount * 100}%</span>
</div>
<div className="rule-details">
<p>适用数量: {rule.minQuantity}-{rule.maxQuantity || '∞'} 双</p>
<p>有效期: {rule.validFrom.toLocaleDateString()} - {rule.validTo.toLocaleDateString()}</p>
{rule.conditions && rule.conditions.length > 0 && (
<p>适用条件: {rule.conditions.map(c => c.type).join(', ')}</p>
)}
</div>
</div>
))}
</div>
) : (
<p className="no-rules">暂无适用价格规则</p>
)}
</div>
)}
</div>
{/* 询价按钮 */}
<div className="calculator-actions">
<button className="inquiry-btn">
💬 一键询价
</button>
<button className="add-to-inquiry-btn">
➕ 加入询价单
</button>
</div>
</div>
);
});
export default KaiShanWangPriceCalculator;4.2 价格计算性能优化
// 开山网价格计算性能优化器
class KaiShanWangPriceCalculatorOptimizer {
private static instance: KaiShanWangPriceCalculatorOptimizer;
private calculationCache: Map<string, CalculationResult> = new Map();
private batchQueue: BatchCalculationRequest[] = [];
private isProcessingBatch = false;
private calculationHistory: CalculationHistory[] = [];
private maxHistorySize = 100;
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
// 缓存配置
private readonly CACHE_TTL = 60 * 1000; // 1分钟
private readonly BATCH_DELAY = 50; // 批量处理延迟
private readonly MAX_BATCH_SIZE = 50;
private constructor() {}
static getInstance(): KaiShanWangPriceCalculatorOptimizer {
if (!KaiShanWangPriceCalculatorOptimizer.instance) {
KaiShanWangPriceCalculatorOptimizer.instance = new KaiShanWangPriceCalculatorOptimizer();
}
return KaiShanWangPriceCalculatorOptimizer.instance;
}
// 生成缓存键
private generateCacheKey(params: PriceCalculationParams): string {
return JSON.stringify({
productId: params.productId,
basePrice: params.basePrice,
quantity: params.quantity,
customerLevel: params.customerLevel,
region: params.region,
paymentMethod: params.paymentMethod,
timestamp: Math.floor(Date.now() / this.CACHE_TTL) // 按时间窗口分组
});
}
// 执行价格计算(带缓存)
async calculatePrice(params: PriceCalculationParams): Promise<CalculationResult> {
const cacheKey = this.generateCacheKey(params);
const cached = this.calculationCache.get(cacheKey);
// 检查缓存
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
// 更新历史记录
this.updateHistory(params, cached.result);
return cached.result;
}
// 执行计算
const result = await this.performCalculation(params);
// 更新缓存
this.calculationCache.set(cacheKey, {
result,
timestamp: Date.now()
});
// 更新历史记录
this.updateHistory(params, result);
// 清理过期缓存
this.cleanExpiredCache();
return result;
}
// 实际计算逻辑
private async performCalculation(params: PriceCalculationParams): Promise<CalculationResult> {
const { productId, basePrice, quantity, customerLevel, region, paymentMethod, pricingRules } = params;
// 过滤适用的规则
const applicableRules = pricingRules.filter(rule => {
// 数量检查
if (quantity < rule.minQuantity) return false;
if (rule.maxQuantity && quantity > rule.maxQuantity) return false;
// 有效期检查
const now = new Date();
if (now < rule.validFrom || now > rule.validTo) return false;
// 条件检查
if (rule.conditions) {
const meetsConditions = rule.conditions.every(condition => {
return this.evaluateCondition(
condition,
{ customerLevel, region, paymentMethod }
);
});
if (!meetsConditions) return false;
}
return true;
});
// 按折扣力度排序
applicableRules.sort((a, b) => b.discount - a.discount);
// 计算最终价格
let finalPrice = basePrice;
let totalDiscount = 0;
const appliedRules: AppliedRule[] = [];
applicableRules.forEach(rule => {
const ruleDiscount = basePrice * rule.discount;
finalPrice -= ruleDiscount;
totalDiscount += ruleDiscount;
appliedRules.push({
ruleId: rule.id,
ruleName: rule.description,
discount: rule.discount,
discountAmount: ruleDiscount
});
});
// 应用最低价保护
const minimumPrice = basePrice * 0.3;
finalPrice = Math.max(finalPrice, minimumPrice);
return {
basePrice,
finalPrice,
totalPrice: finalPrice * quantity,
discountAmount: totalDiscount * quantity,
appliedRules,
savings: (basePrice - finalPrice) * quantity,
savingsPercentage: ((basePrice - finalPrice) / basePrice) * 100,
calculatedAt: new Date(),
calculationId: this.generateCalculationId()
};
}
// 评估条件
private evaluateCondition(
condition: PricingCondition,
context: CalculationContext
): boolean {
const actualValue = context[condition.type as keyof CalculationContext];
if (actualValue === undefined) return false;
switch (condition.operator) {
case 'eq':
return actualValue === condition.value;
case 'gt':
return Number(actualValue) > Number(condition.value);
case 'gte':
return Number(actualValue) >= Number(condition.value);
case 'lt':
return Number(actualValue) < Number(condition.value);
case 'lte':
return Number(actualValue) <= Number(condition.value);
case 'in':
if (Array.isArray(condition.value)) {
return condition.value.includes(actualValue);
}
return String(actualValue).includes(String(condition.value));
default:
return true;
}
}
// 批量计算
async calculateBatch(prices: PriceCalculationParams[]): Promise<CalculationResult[]> {
// 将请求加入队列
this.batchQueue.push(...prices.map(p => ({ params: p, resolve: null, reject: null })));
// 如果已经在处理,直接返回
if (this.isProcessingBatch) {
return new Promise((resolve, reject) => {
const lastItem = this.batchQueue[this.batchQueue.length - 1];
lastItem.resolve = resolve;
lastItem.reject = reject;
});
}
// 开始处理批量请求
this.isProcessingBatch = true;
return new Promise((resolve) => {
setTimeout(async () => {
const results: CalculationResult[] = [];
const errors: Error[] = [];
// 按产品ID分组
const groupedParams = this.groupByProductId(prices);
for (const [productId, productParams] of Object.entries(groupedParams)) {
try {
// 获取该产品的定价规则
const rules = await this.getPricingRules(productId);
// 批量计算
for (const params of productParams) {
const result = await this.performCalculation({ ...params, pricingRules: rules });
results.push(result);
}
} catch (error) {
errors.push(error as Error);
}
}
this.isProcessingBatch = false;
this.batchQueue = [];
if (errors.length > 0) {
console.error('Batch calculation errors:', errors);
}
resolve(results);
}, this.BATCH_DELAY);
});
}
// 按产品ID分组
private groupByProductId(params: PriceCalculationParams[]): Record<string, PriceCalculationParams[]> {
return params.reduce((acc, param) => {
if (!acc[param.productId]) {
acc[param.productId] = [];
}
acc[param.productId].push(param);
return acc;
}, {} as Record<string, PriceCalculationParams[]>);
}
// 获取定价规则(带缓存)
private async getPricingRules(productId: string): Promise<PricingRule[]> {
const cacheKey = `pricing_rules_${productId}`;
const cached = sessionStorage.getItem(cacheKey);
if (cached) {
try {
const { rules, timestamp } = JSON.parse(cached);
// 缓存有效期5分钟
if (Date.now() - timestamp < 5 * 60 * 1000) {
return rules;
}
} catch (e) {
console.warn('Failed to parse cached pricing rules:', e);
}
}
try {
const response = await fetch(`/api/products/${productId}/pricing-rules`);
if (response.ok) {
const rules: PricingRule[] = await response.json();
// 缓存结果
sessionStorage.setItem(cacheKey, JSON.stringify({
rules,
timestamp: Date.now()
}));
return rules;
}
} catch (error) {
console.error('Error fetching pricing rules:', error);
}
return [];
}
// 更新历史记录
private updateHistory(params: PriceCalculationParams, result: CalculationResult): void {
this.calculationHistory.unshift({
params,
result,
timestamp: new Date()
});
// 保持历史记录大小
if (this.calculationHistory.length > this.maxHistorySize) {
this.calculationHistory.pop();
}
}
// 清理过期缓存
private cleanExpiredCache(): void {
const now = Date.now();
for (const [key, value] of this.calculationCache.entries()) {
if (now - value.timestamp > this.CACHE_TTL) {
this.calculationCache.delete(key);
}
}
}
// 生成计算ID
private generateCalculationId(): string {
return `calc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// 获取计算历史
getCalculationHistory(limit: number = 10): CalculationHistory[] {
return this.calculationHistory.slice(0, limit);
}
// 获取缓存统计
getCacheStats(): CacheStats {
return {
calculationCacheSize: this.calculationCache.size,
historySize: this.calculationHistory.length,
batchQueueSize: this.batchQueue.length,
isProcessingBatch: this.isProcessingBatch
};
}
// 清除缓存
clearCache(): void {
this.calculationCache.clear();
this.calculationHistory = [];
this.batchQueue = [];
}
// 预热缓存
async warmUpCache(productIds: string[], quantities: number[]): Promise<void> {
const params: PriceCalculationParams[] = [];
for (const productId of productIds) {
for (const quantity of quantities) {
params.push({
productId,
basePrice: 100, // 示例价格,实际应从API获取
quantity,
customerLevel: 'regular',
region: 'CN',
paymentMethod: 'bank_transfer',
pricingRules: []
});
}
}
await this.calculateBatch(params);
}
}
// 类型定义
interface PriceCalculationParams {
productId: string;
basePrice: number;
quantity: number;
customerLevel: string;
region: string;
paymentMethod: string;
pricingRules: PricingRule[];
}
interface CalculationResult {
basePrice: number;
finalPrice: number;
totalPrice: number;
discountAmount: number;
appliedRules: AppliedRule[];
savings: number;
savingsPercentage: number;
calculatedAt: Date;
calculationId: string;
}
interface AppliedRule {
ruleId: string;
ruleName: string;
discount: number;
discountAmount: number;
}
interface CalculationContext {
customerLevel: string;
region: string;
paymentMethod: string;
}
interface BatchCalculationRequest {
params: PriceCalculationParams;
resolve: ((result: CalculationResult) => void) | null;
reject: ((error: Error) => void) | null;
}
interface CalculationHistory {
params: PriceCalculationParams;
result: CalculationResult;
timestamp: Date;
}
interface CacheStats {
calculationCacheSize: number;
historySize: number;
batchQueueSize: number;
isProcessingBatch: boolean;
}五、开山网移动端批采优化
5.1 移动端批采界面
// 开山网移动端批采界面
import { memo, useState, useCallback, useMemo, useRef, useEffect } from 'react';
import { useGesture } from '@use-gesture/react';
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
interface MobileBulkPurchaseProps {
product: Product;
onInquiry: (inquiryData: InquiryData) => void;
onAddToInquiryList: (product: Product, selectedData: SelectedBulkData) => void;
onShare: (product: Product) => void;
}
interface SelectedBulkData {
selectedSizes: Map<string, number>;
totalQuantity: number;
estimatedTotal: number;
notes: string;
}
const KaiShanWangMobileBulkPurchase = memo(({
product,
onInquiry,
onAddToInquiryList,
onShare
}: MobileBulkPurchaseProps) => {
const [selectedSizes, setSelectedSizes] = useState<Map<string, number>>(new Map());
const [quickMode, setQuickMode] = useState(true);
const [bulkQuantity, setBulkQuantity] = useState(product.minOrderQuantity);
const [notes, setNotes] = useState('');
const [showInquiryModal, setShowInquiryModal] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// 手势处理
const bind = useGesture(
{
onDrag: ({ swipe: [swipeX], cancel }) => {
if (swipeX === -1) {
// 左滑切换到详细模式
setQuickMode(false);
} else if (swipeX === 1) {
// 右滑切换到快速模式
setQuickMode(true);
}
}
},
{
drag: {
filterTaps: true,
threshold: 10
}
}
);
// 切换尺码选择
const toggleSizeSelection = useCallback((size: string, inventory: number) => {
if (inventory <= 0) return;
setSelectedSizes(prev => {
const newMap = new Map(prev);
if (newMap.has(size)) {
newMap.delete(size);
} else {
newMap.set(size, quickMode ? bulkQuantity : Math.max(product.minOrderQuantity, 1));
}
return newMap;
});
}, [quickMode, bulkQuantity, product.minOrderQuantity]);
// 应用批量数量
const applyBulkQuantityToAll = useCallback(() => {
setSelectedSizes(prev => {
const newMap = new Map(prev);
newMap.forEach((_, key) => {
newMap.set(key, bulkQuantity);
});
return newMap;
});
}, [bulkQuantity]);
// 计算选中数据
const selectedData = useMemo((): SelectedBulkData => {
let totalQuantity = 0;
let estimatedTotal = 0;
selectedSizes.forEach((quantity, size) => {
totalQuantity += quantity;
// 估算价格(实际应该从价格计算器获取)
estimatedTotal += quantity * product.basePrice * 0.8; // 假设8折
});
return {
selectedSizes,
totalQuantity,
estimatedTotal,
notes
};
}, [selectedSizes, notes, product.basePrice]);
// 处理询价
const handleInquiry = useCallback(async () => {
if (selectedSizes.size === 0) {
alert('请至少选择一个尺码');
return;
}
setIsProcessing(true);
try {
await onInquiry({
productId: product.id,
productName: product.name,
selectedSizes: Array.from(selectedSizes.entries()),
totalQuantity: selectedData.totalQuantity,
estimatedTotal: selectedData.estimatedTotal,
notes: notes,
contactInfo: {
// 从用户资料获取
}
});
setShowInquiryModal(false);
} catch (error) {
console.error('Inquiry failed:', error);
alert('询价失败,请稍后重试');
} finally {
setIsProcessing(false);
}
}, [selectedSizes, selectedData, notes, product, onInquiry]);
// 添加到询价单
const handleAddToInquiryList = useCallback(() => {
if (selectedSizes.size === 0) {
alert('请至少选择一个尺码');
return;
}
onAddToInquiryList(product, selectedData);
setSelectedSizes(new Map());
setNotes('');
}, [selectedSizes, selectedData, product, onAddToInquiryList]);
// 分享商品
const handleShare = useCallback(() => {
onShare(product);
}, [product, onShare]);
// 全选有库存的尺码
const selectAllAvailable = useCallback(() => {
const newMap = new Map<string, number>();
product.sizes.forEach(size => {
if (size.inventory > 0) {
newMap.set(size.euSize, quickMode ? bulkQuantity : product.minOrderQuantity);
}
});
setSelectedSizes(newMap);
}, [product.sizes, quickMode, bulkQuantity, product.minOrderQuantity]);
// 清空选择
const clearSelection = useCallback(() => {
setSelectedSizes(new Map());
}, []);
return (
<div className="ksw-mobile-bulk-purchase" ref={containerRef} {...bind()}>
{/* 模式切换提示 */}
<div className="mode-indicator">
<span className={`indicator ${quickMode ? 'quick' : 'detailed'}`}>
{quickMode ? '⚡ 快速选码模式' : '📋 详细选码模式'}
</span>
<span className="swipe-hint">← 左右滑动切换模式 →</span>
</div>
{/* 快速选码模式 */}
{quickMode && (
<div className="quick-select-mode">
<div className="quick-controls">
<div className="bulk-quantity-control">
<label>每码数量:</label>
<div className="quantity-input-group">
<button
onClick={() => setBulkQuantity(Math.max(product.minOrderQuantity, bulkQuantity - 1))}
disabled={bulkQuantity <= product.minOrderQuantity}
>
-
</button>
<input
type="number"
value={bulkQuantity}
onChange={(e) => {
const val = parseInt(e.target.value) || product.minOrderQuantity;
setBulkQuantity(Math.max(product.minOrderQuantity, val));
}}
min={product.minOrderQuantity}
max={999}
/>
<button
onClick={() => setBulkQuantity(Math.min(999, bulkQuantity + 1))}
>
+
</button>
</div>
<span className="unit">双/码</span>
</div>
<button className="apply-bulk-btn" onClick={applyBulkQuantityToAll}>
应用到所有选中
</button>
</div>
<div className="size-grid">
{product.sizes.map((size) => {
const isSelected = selectedSizes.has(size.euSize);
const isAvailable = size.inventory > 0;
const status = getInventoryStatus(size.inventory);
return (
<button
key={size.euSize}
className={`size-cell ${isSelected ? 'selected' : ''} ${!isAvailable ? 'unavailable' : ''}`}
onClick={() => toggleSizeSelection(size.euSize, size.inventory)}
disabled={!isAvailable}
style={{
'--inventory-color': status.color,
'--inventory-bg': status.bgColor
} as React.CSSProperties}
>
<div className="size-main">
<span className="eu-size">{size.euSize}</span>
{isSelected && <span className="check-mark">✓</span>}
</div>
<div className="size-info">
<span className="inventory-badge" style={{ color: status.color }}>
{status.text}
</span>
{isSelected && (
<span className="selected-quantity">{selectedSizes.get(size.euSize)}双</span>
)}
</div>
</button>
);
})}
</div>
</div>
)}
{/* 详细选码模式 */}
{!quickMode && (
<div className="detailed-select-mode">
<div className="size-list">
{product.sizes.map((size) => {
const isSelected = selectedSizes.has(size.euSize);
const isAvailable = size.inventory > 0;
const status = getInventoryStatus(size.inventory);
const currentQty = selectedSizes.get(size.euSize) || 0;
return (
<div
key={size.euSize}
className={`size-row ${isSelected ? 'selected' : ''} ${!isAvailable ? 'unavailable' : ''}`}
>
<div className="size-info">
<span className="eu-size">{size.euSize}</span>
<span className="secondary-sizes">
US:{size.usSize} UK:{size.ukSize}
</span>
<span
className="inventory-status"
style={{ color: status.color }}
>
{status.text} ({size.inventory})
</span>
</div>
<div className="quantity-control">
<button
className="decrease-btn"
onClick={() => {
if (currentQty > product.minOrderQuantity) {
toggleSizeSelection(size.euSize, size.inventory);
setSelectedSizes(prev => new Map(prev).set(size.euSize, currentQty - 1));
} else {
toggleSizeSelection(size.euSize, size.inventory);
}
}}
disabled={!isSelected || currentQty <= product.minOrderQuantity}
>
-
</button>
<span className="quantity">{currentQty}</span>
<button
className="increase-btn"
onClick={() => {
toggleSizeSelection(size.euSize, size.inventory);
setSelectedSizes(prev => new Map(prev).set(size.euSize, currentQty + 1));
}}
disabled={!isAvailable || currentQty >= 999}
>
+
</button>
</div>
</div>
);
})}
</div>
</div>
)}
{/* 已选汇总 */}
{selectedSizes.size > 0 && (
<div className="selection-summary">
<div className="summary-header">
<span className="selected-count">已选 {selectedSizes.size} 个尺码</span>
<div className="summary-actions">
<button className="clear-btn" onClick={clearSelection}>清空</button>
<button className="select-all-btn" onClick={selectAllAvailable}>全选有货</button>
</div>
</div>
<div className="selected-items">
{Array.from(selectedSizes.entries()).slice(0, 4).map(([size, qty]) => (
<div key={size} className="selected-item">
<span className="size-tag">{size}</span>
<span className="qty-tag">{qty}双</span>
</div>
))}
{selectedSizes.size > 4 && (
<div className="more-items">+{selectedSizes.size - 4}个尺码</div>
)}
</div>
<div className="total-info">
<div className="total-row">
<span>总数量:</span>
<span className="total-qty">{selectedData.totalQuantity} 双</span>
</div>
<div className="total-row">
<span>预估金额:</span>
<span className="total-amount">¥{selectedData.estimatedTotal.toFixed(2)}</span>
</div>
</div>
{/* 备注输入 */}
<div className="notes-input">
<label>备注需求:</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="请输入特殊需求,如混批、定制包装等..."
rows={2}
/>
</div>
</div>
)}
{/* 底部操作栏 */}
<div className="bottom-actions">
<div className="action-buttons">
<button className="share-btn" onClick={handleShare}>
📤 分享
</button>
<button
className="add-inquiry-btn"
onClick={handleAddToInquiryList}
disabled={selectedSizes.size === 0}
>
➕ 加入询价单
</button>
<button
className="inquiry-btn"
onClick={() => setShowInquiryModal(true)}
disabled={selectedSizes.size === 0}
>
💬 立即询价
</button>
</div>
</div>
{/* 询价弹窗 */}
{showInquiryModal && (
<InquiryModal
product={product}
selectedData={selectedData}
isProcessing={isProcessing}
onConfirm={handleInquiry}
onCancel={() => setShowInquiryModal(false)}
/>
)}
</div>
);
});
// 询价弹窗
const InquiryModal = memo(({
product,
selectedData,
isProcessing,
onConfirm,
onCancel
}: {
product: Product;
selectedData: SelectedBulkData;
isProcessing: boolean;
onConfirm: () => void;
onCancel: () => void;
}) => {
return (
<div className="inquiry-modal">
<div className="modal-backdrop" onClick={onCancel} />
<div className="modal-content">
<div className="modal-header">
<h3>💬 提交询价</h3>
<button className="close-btn" onClick={onCancel}>×</button>
</div>
<div className="modal-body">
<div className="inquiry-product">
<img src={product.mainImage} alt={product.name} />
<div className="product-info">
<h4>{product.name}</h4>
<p>货号: {product.sku}</p>
</div>
</div>
<div className="inquiry-details">
<div className="detail-row">
<span>选择尺码:</span>
<span>{selectedData.selectedSizes.size} 种</span>
</div>
<div className="detail-row">
<span>采购数量:</span>
<span>{selectedData.totalQuantity} 双</span>
</div>
<div className="detail-row">
<span>预估金额:</span>
<span className="highlight">¥{selectedData.estimatedTotal.toFixed(2)}</span>
</div>
{selectedData.notes && (
<div className="detail-row notes">
<span>备注:</span>
<span>{selectedData.notes}</span>
</div>
)}
</div>
<div className="inquiry-notice">
<p>📞 询价提交后,供应商将在24小时内回复报价</p>
<p>💰 批量采购可享受阶梯批发价优惠</p>
</div>
</div>
<div className="modal-footer">
<button className="cancel-btn" onClick={onCancel} disabled={isProcessing}>
取消
</button>
<button className="confirm-btn" onClick={onConfirm} disabled={isProcessing}>
{isProcessing ? '提交中...' : '确认询价'}
</button>
</div>
</div>
</div>
);
});
// 辅助函数
function getInventoryStatus(inventory: number): InventoryStatus {
if (inventory === 0) return { text: '缺货', color: '#ef4444', bgColor: '#fef2f2', disabled: true };
if (inventory <= 10) return { text: '紧张', color: '#f97316', bgColor: '#fff7ed', disabled: false };
if (inventory <= 50) return { text: '较少', color: '#eab308', bgColor: '#fefce8', disabled: false };
return { text: '充足', color: '#22c55e', bgColor: '#f0fdf4', disabled: false };
}
// 类型定义
interface Product {
id: string;
name: string;
sku: string;
basePrice: number;
minOrderQuantity: number;
mainImage: string;
sizes: Array<{
euSize: string;
usSize: string;
ukSize: string;
inventory: number;
}>;
}
interface InquiryData {
productId: string;
productName: string;
selectedSizes: [string, number][];
totalQuantity: number;
estimatedTotal: number;
notes: string;
contactInfo: any;
}5.2 移动端性能优化
// 开山网移动端批采性能优化器
class KaiShanWangMobileBulkOptimizer {
private static instance: KaiShanWangMobileBulkOptimizer;
private touchFeedbackEnabled: boolean = true;
private gestureThreshold: number = 10;
private animationFrameId: number | null = null;
private scrollPosition: number = 0;
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
private constructor() {
this.initMobileOptimizations();
}
static getInstance(): KaiShanWangMobileBulkOptimizer {
if (!KaiShanWangMobileBulkOptimizer.instance) {
KaiShanWangMobileBulkOptimizer.instance = new KaiShanWangMobileBulkOptimizer();
}
return KaiShanWangMobileBulkOptimizer.instance;
}
// 初始化移动端优化
private initMobileOptimizations(): void {
// 检测设备能力
this.detectDeviceCapabilities();
// 启用触摸反馈
this.enableTouchFeedback();
// 优化滚动性能
this.optimizeScrollPerformance();
// 监听页面可见性
this.handleVisibilityChange();
// 监听网络状态
this.handleNetworkChanges();
}
// 检测设备能力
private detectDeviceCapabilities(): void {
const isLowEndDevice = this.checkLowEndDevice();
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (isLowEndDevice || prefersReducedMotion) {
// 禁用复杂动画
document.documentElement.classList.add('reduce-motion');
}
// 检测触摸支持
if ('ontouchstart' in window) {
document.documentElement.classList.add('touch-device');
}
// 检测屏幕方向
this.handleOrientationChange();
window.addEventListener('orientationchange', () => this.handleOrientationChange());
}
// 检查是否为低端设备
private checkLowEndDevice(): boolean {
// 检查设备内存
const memory = (navigator as any).deviceMemory;
if (memory && memory <= 2) {
return true;
}
// 检查CPU核心数
const cores = navigator.hardwareConcurrency;
if (cores && cores <= 2) {
return true;
}
// 检查设备像素比
if (window.devicePixelRatio >= 3) {
return false; // 高DPI设备通常是高端设备
}
return false;
}
// 启用触摸反馈
private enableTouchFeedback(): void {
if (!this.touchFeedbackEnabled) return;
// 使用CSS变量实现涟漪效果
const style = document.createElement('style');
style.textContent = `
.touch-ripple {
position: relative;
overflow: hidden;
}
.touch-ripple::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.3s, height 0.3s, opacity 0.3s;
opacity: 0;
}
.touch-ripple:active::after {
width: 200px;
height: 200px;
opacity: 1;
}
`;
document.head.appendChild(style);
}
// 优化滚动性能
private optimizeScrollPerformance(): void {
let ticking = false;
const handleScroll = (e: Event) => {
if (!ticking) {
requestAnimationFrame(() => {
this.scrollPosition = window.scrollY;
this.updateScrollIndicators();
ticking = false;
});
ticking = true;
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
// 优化滚动容器
const scrollContainers = document.querySelectorAll('.scroll-container');
scrollContainers.forEach(container => {
container.addEventListener('touchstart', () => {}, { passive: true });
container.addEventListener('touchmove', () => {}, { passive: true });
});
}
// 更新滚动指示器
private updateScrollIndicators(): void {
const scrollIndicator = document.querySelector('.scroll-indicator');
if (scrollIndicator) {
const scrollPercent = (this.scrollPosition / (document.body.scrollHeight - window.innerHeight)) * 100;
scrollIndicator.setAttribute('data-scroll', `${scrollPercent}%`);
}
}
// 处理页面可见性变化
private handleVisibilityChange(): void {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 页面隐藏时暂停动画和定时器
this.pauseAnimations();
this.reduceUpdateFrequency();
} else {
// 页面可见时恢复
this.resumeAnimations();
this.normalizeUpdateFrequency();
}
});
}
// 处理网络状态变化
private handleNetworkChanges(): void {
if (navigator.connection) {
navigator.connection.addEventListener('change', () => {
const effectiveType = navigator.connection.effectiveType;
switch (effectiveType) {
case '4g':
case 'wifi':
this.enableHighQualityMode();
break;
case '3g':
this.enableMediumQualityMode();
break;
case '2g':
case 'slow-2g':
this.enableLowQualityMode();
break;
}
});
}
}
// 处理屏幕方向变化
private handleOrientationChange(): void {
const isLandscape = window.innerWidth > window.innerHeight;
if (isLandscape) {
document.documentElement.classList.add('landscape');
document.documentElement.classList.remove('portrait');
} else {
document.documentElement.classList.add('portrait');
document.documentElement.classList.remove('landscape');
}
// 重新计算布局
window.dispatchEvent(new Event('resize'));
}
// 暂停动画
private pauseAnimations(): void {
document.documentElement.classList.add('animations-paused');
}
// 恢复动画
private resumeAnimations(): void {
document.documentElement.classList.remove('animations-paused');
}
// 降低更新频率
private reduceUpdateFrequency(): void {
document.documentElement.classList.add('reduced-updates');
}
// 恢复正常更新频率
private normalizeUpdateFrequency(): void {
document.documentElement.classList.remove('reduced-updates');
}
// 启用高质量模式
private enableHighQualityMode(): void {
document.documentElement.classList.remove('low-quality', 'medium-quality');
document.documentElement.classList.add('high-quality');
}
// 启用中等质量模式
private enableMediumQualityMode(): void {
document.documentElement.classList.remove('high-quality', 'low-quality');
document.documentElement.classList.add('medium-quality');
}
// 启用低质量模式
private enableLowQualityMode(): void {
document.documentElement.classList.remove('high-quality', 'medium-quality');
document.documentElement.classList.add('low-quality');
}
// 优化图片加载
optimizeImageLoading(imageSrc: string, priority: 'high' | 'normal' | 'low'): string {
const url = new URL(imageSrc, window.location.origin);
// 根据网络状况调整质量
if (navigator.connection) {
const effectiveType = navigator.connection.effectiveType;
switch (effectiveType) {
case '4g':
case 'wifi':
url.searchParams.set('q', '85');
break;
case '3g':
url.searchParams.set('q', '70');
break;
case '2g':
case 'slow-2g':
url.searchParams.set('q', '50');
break;
}
}
// 根据设备能力调整格式
if (window.devicePixelRatio >= 2) {
url.searchParams.set('dpr', '2');
}
// 设置加载优先级
if (priority === 'high') {
url.searchParams.set('priority', 'high');
}
return url.toString();
}
// 获取移动端配置
getMobileConfig(): MobileConfig {
return {
touchFeedback: this.touchFeedbackEnabled,
gestureThreshold: this.gestureThreshold,
reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
isLowEndDevice: this.checkLowEndDevice(),
orientation: window.innerWidth > window.innerHeight ? 'landscape' : 'portrait',
networkType: navigator.connection?.effectiveType || 'unknown',
pixelRatio: window.devicePixelRatio || 1
};
}
// 清理资源
cleanup(): void {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
}
}
// 类型定义
interface MobileConfig {
touchFeedback: boolean;
gestureThreshold: number;
reducedMotion: boolean;
isLowEndDevice: boolean;
orientation: 'landscape' | 'portrait';
networkType: string;
pixelRatio: number;
}六、开山网性能监控与优化
6.1 开山网性能监控系统
// 开山网性能监控系统
class KaiShanWangPerformanceMonitor {
private static instance: KaiShanWangPerformanceMonitor;
private metrics: KaiShanWangPerformanceMetrics = {
coreWebVitals: {
LCP: 0,
FID: 0,
CLS: 0,
INP: 0
},
mobileSpecific: {
appLaunchTime: 0,
scrollFPS: 0,
touchResponseTime: 0,
imageLoadTime: 0,
memoryUsage: 0,
batteryImpact: 0
},
businessMetrics: {
timeToFirstInteraction: 0,
timeToCompleteSelection: 0,
inquirySubmissionTime: 0,
imageGallerySwitchTime: 0,
sizeSelectorResponseTime: 0
},
resourceMetrics: {
totalPageSize: 0,
imageTotalSize: 0,
javascriptSize: 0,
cssSize: 0,
fontTotalSize: 0,
apiCallsCount: 0,
websocketLatency: 0
}
};
private observers: PerformanceObserver[] = [];
private isMonitoring: boolean = false;
private reportingEndpoint: string = 'https://analytics.kaishan.com/performance';
private constructor() {}
static getInstance(): KaiShanWangPerformanceMonitor {
if (!KaiShanWangPerformanceMonitor.instance) {
KaiShanWangPerformanceMonitor.instance = new KaiShanWangPerformanceMonitor();
}
return KaiShanWangPerformanceMonitor.instance;
}
// 启动监控
startMonitoring(): void {
if (this.isMonitoring) return;
this.isMonitoring = true;
this.setupCoreWebVitalsObservers();
this.setupMobileSpecificObservers();
this.setupBusinessMetricsObservers();
this.setupResourceObservers();
this.setupLongTaskObserver();
}
// 设置核心Web指标观察器
private setupCoreWebVitalsObservers(): void {
// LCP
this.observers.push(
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1] as any;
this.metrics.coreWebVitals.LCP = lastEntry.startTime;
this.reportMetric('LCP', lastEntry.startTime, 'core-web-vital');
})
).observe({ type: 'largest-contentful-paint', buffered: true });
// FID
this.observers.push(
new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry: any) => {
this.metrics.coreWebVitals.FID = entry.processingStart - entry.startTime;
this.reportMetric('FID', this.metrics.coreWebVitals.FID, 'core-web-vital');
});
})
).observe({ type: 'first-input', buffered: true });
// CLS
this.observers.push(
new PerformanceObserver((list) => {
let clsValue = 0;
list.getEntries().forEach((entry: any) => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
this.metrics.coreWebVitals.CLS = clsValue;
this.reportMetric('CLS', clsValue, 'core-web-vital');
})
).observe({ type: 'layout-shift', buffered: true });
// INP
this.observers.push(
new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry: any) => {
this.metrics.coreWebVitals.INP = entry.processingEnd - entry.startTime;
this.reportMetric('INP', this.metrics.coreWebVitals.INP, 'core-web-vital');
});
})
).observe({ type: 'event', buffered: true });
}
// 设置移动端特定指标观察器
private setupMobileSpecificObservers(): void {
// 内存使用监控
if ('memory' in performance) {
setInterval(() => {
const memory = (performance as any).memory;
this.metrics.mobileSpecific.memoryUsage = memory.usedJSHeapSize;
// 内存警告
const usagePercent = memory.usedJSHeapSize / memory.jsHeapSizeLimit;
if (usagePercent > 0.8) {
this.reportWarning('High memory usage detected', usagePercent);
}
}, 10000);
}
// 滚动性能监控
let scrollFrameCount = 0;
let scrollStartTime = 0;
const handleScrollStart = () => {
scrollStartTime = performance.now();
scrollFrameCount = 0;
};
const handleScrollEnd = () => {
const scrollDuration = performance.now() - scrollStartTime;
const fps = scrollFrameCount / (scrollDuration / 1000);
this.metrics.mobileSpecific.scrollFPS = fps;
if (fps < 30) {
this.reportWarning('Low scroll FPS detected', fps);
}
};
let scrollTimeout: NodeJS.Timeout;
window.addEventListener('scroll', () => {
scrollFrameCount++;
if (!scrollStartTime) {
handleScrollStart();
}
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(handleScrollEnd, 150);
}, { passive: true });
// 触摸响应时间监控
document.addEventListener('touchstart', () => {
this.touchStartTime = performance.now();
}, { passive: true });
document.addEventListener('touchend', () => {
const touchDuration = performance.now() - this.touchStartTime;
this.metrics.mobileSpecific.touchResponseTime = touchDuration;
if (touchDuration > 100) {
this.reportWarning('Slow touch response detected', touchDuration);
}
}, { passive: true });
}
// 设置业务指标观察器
private setupBusinessMetricsObservers(): void {
// 首次交互时间
const interactionStart = performance.now();
const trackFirstInteraction = () => {
const timeToFirstInteraction = performance.now() - interactionStart;
this.metrics.businessMetrics.timeToFirstInteraction = timeToFirstInteraction;
this.reportMetric('TTFI', timeToFirstInteraction, 'business');
document.removeEventListener('click', trackFirstInteraction);
document.removeEventListener('touchstart', trackFirstInteraction);
};
document.addEventListener('click', trackFirstInteraction, { once: true, passive: true });
document.addEventListener('touchstart', trackFirstInteraction, { once: true, passive: true });
// 图片画廊切换时间
let gallerySwitchStart = 0;
document.addEventListener('gallery-switch-start', () => {
gallerySwitchStart = performance.now();
}, { passive: true });
document.addEventListener('gallery-switch-end', () => {
const switchTime = performance.now() - gallerySwitchStart;
this.metrics.businessMetrics.imageGallerySwitchTime = switchTime;
this.reportMetric('GallerySwitchTime', switchTime, 'business');
}, { passive: true });
// 尺码选择器响应时间
let sizeSelectorStart = 0;
document.addEventListener('size-selector-start', () => {
sizeSelectorStart = performance.now();
}, { passive: true });
document.addEventListener('size-selector-end', () => {
const responseTime = performance.now() - sizeSelectorStart;
this.metrics.businessMetrics.sizeSelectorResponseTime = responseTime;
this.reportMetric('SizeSelectorResponseTime', responseTime, 'business');
}, { passive: true });
}
// 设置资源观察器
private setupResourceObservers(): void {
// 图片加载时间
this.observers.push(
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name.match(/\.(jpg|jpeg|png|webp|avif|gif)/i)) {
const loadTime = entry.duration;
this.metrics.resourceMetrics.imageLoadTime = loadTime;
if (loadTime > 2000) {
this.reportWarning('Slow image load detected', { url: entry.name, loadTime });
}
}
});
})
).observe({ type: 'resource', buffered: true });
// API调用计数
let apiCallCount = 0;
this.observers.push(
new PerformanceObserver((list) => {
list.getEntries().forEach((entry: any) => {
if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
apiCallCount++;
this.metrics.resourceMetrics.apiCallsCount = apiCallCount;
// API调用时间过长警告
if (entry.duration > 1000) {
this.reportWarning('Slow API call detected', { url: entry.name, duration: entry.duration });
}
}
});
})
).observe({ type: 'resource', buffered: true });
}
// 设置长任务观察器
private setupLongTaskObserver(): void {
this.observers.push(
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.reportWarning('Long task detected', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name
});
});
})
).observe({ type: 'longtask', buffered: true });
}
// 报告指标
private reportMetric(name: string, value: number, category: string): void {
const payload = {
metric: name,
value,
category,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
deviceInfo: {
connectionType: (navigator as any).connection?.effectiveType || 'unknown',
devicePixelRatio: window.devicePixelRatio || 1,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
memory: (performance as any).memory?.jsHeapSizeLimit
},
businessContext: {
pageType: 'product-detail',
productId: this.getProductIdFromUrl(),
userType: this.getUserType()
}
};
// 使用sendBeacon发送
if (navigator.sendBeacon) {
navigator.sendBeacon(
this.reportingEndpoint,
JSON.stringify(payload)
);
}
}
// 报告警告
private reportWarning(warning: string, details: any): void {
console.warn(`[KSW Perf] ${warning}:`, details);
const payload = {
warning,
details,
timestamp: Date.now(),
url: window.location.href
};
if (navigator.sendBeacon) {
navigator.sendBeacon(
`${this.reportingEndpoint}/warnings`,
JSON.stringify(payload)
);
}
}
// 从URL获取产品ID
private getProductIdFromUrl(): string {
const match = window.location.pathname.match(/\/product\/(\d+)/);
return match ? match[1] : 'unknown';
}
// 获取用户类型
private getUserType(): string {
// 从localStorage或cookie获取用户类型
return localStorage.getItem('ksw_user_type') || 'guest';
}
// 获取当前指标快照
getMetricsSnapshot(): KaiShanWangPerformanceMetrics {
return { ...this.metrics };
}
// 生成性能报告
generateReport(): PerformanceReport {
const snapshot = this.getMetricsSnapshot();
return {
summary: {
lcp: snapshot.coreWebVitals.LCP,
fid: snapshot.coreWebVitals.FID,
cls: snapshot.coreWebVitals.CLS,
inp: snapshot.coreWebVitals.INP
},
mobileMetrics: {
scrollFPS: snapshot.mobileSpecific.scrollFPS,
touchResponseTime: snapshot.mobileSpecific.touchResponseTime,
imageLoadTime: snapshot.mobileSpecific.imageLoadTime,
memoryUsage: snapshot.mobileSpecific.memoryUsage,
batteryImpact: snapshot.mobileSpecific.batteryImpact
},
businessMetrics: {
timeToFirstInteraction: snapshot.businessMetrics.timeToFirstInteraction,
timeToCompleteSelection: snapshot.businessMetrics.timeToCompleteSelection,
inquirySubmissionTime: snapshot.businessMetrics.inquirySubmissionTime
},
resourceMetrics: {
totalPageSize: snapshot.resourceMetrics.totalPageSize,
imageTotalSize: snapshot.resourceMetrics.imageTotalSize,
apiCallsCount: snapshot.resourceMetrics.apiCallsCount
},
recommendations: this.generateRecommendations(snapshot),
timestamp: Date.now()
};
}
// 生成优化建议
private generateRecommendations(metrics: KaiShanWangPerformanceMetrics): string[] {
const recommendations: string[] = [];
// LCP优化建议
if (metrics.coreWebVitals.LCP > 2500) {
recommendations.push('优化首屏图片加载,考虑使用更小的图片或更好的CDN分发');
}
// FID优化建议
if (metrics.coreWebVitals.FID > 100) {
recommendations.push('减少主线程阻塞,考虑代码分割或延迟加载非关键JavaScript');
}
// CLS优化建议
if (metrics.coreWebVitals.CLS > 0.1) {
recommendations.push('为图片和广告预留空间,避免动态插入内容导致布局偏移');
}
// 滚动性能建议
if (metrics.mobileSpecific.scrollFPS < 55) {
recommendations.push('优化滚动性能,避免滚动时触发重排和重绘');
}
// 触摸响应建议
if (metrics.mobileSpecific.touchResponseTime > 100) {
recommendations.push('优化触摸响应,减少事件处理程序的复杂度');
}
// 图片加载建议
if (metrics.mobileSpecific.imageLoadTime > 2000) {
recommendations.push('优化图片加载策略,使用适当的格式和质量设置');
}
// 内存使用建议
if (metrics.mobileSpecific.memoryUsage > 150 * 1024 * 1024) {
recommendations.push('检测到较高的内存使用,请检查是否存在内存泄漏');
}
// API调用建议
if (metrics.resourceMetrics.apiCallsCount > 20) {
recommendations.push('API调用次数较多,考虑合并请求或使用GraphQL');
}
return recommendations;
}
// 停止监控
stopMonitoring(): void {
this.observers.forEach(observer => observer.disconnect());
this.observers = [];
this.isMonitoring = false;
}
}
// 类型定义
interface KaiShanWangPerformanceMetrics {
coreWebVitals: {
LCP: number;
FID: number;
CLS: number;
INP: number;
};
mobileSpecific: {
appLaunchTime: number;
scrollFPS: number;
touchResponseTime: number;
imageLoadTime: number;
memoryUsage: number;
batteryImpact: number;
};
businessMetrics: {
timeToFirstInteraction: number;
timeToCompleteSelection: number;
inquirySubmissionTime: number;
imageGallerySwitchTime: number;
sizeSelectorResponseTime: number;
};
resourceMetrics: {
totalPageSize: number;
imageTotalSize: number;
javascriptSize: number;
cssSize: number;
fontTotalSize: number;
apiCallsCount: number;
websocketLatency: number;
};
}
interface PerformanceReport {
summary: {
lcp: number;
fid: number;
cls: number;
inp: number;
};
mobileMetrics: {
scrollFPS: number;
touchResponseTime: number;
imageLoadTime: number;
memoryUsage: number;
batteryImpact: number;
};
businessMetrics: {
timeToFirstInteraction: number;
timeToCompleteSelection: number;
inquirySubmissionTime: number;
};
resourceMetrics: {
totalPageSize: number;
imageTotalSize: number;
apiCallsCount: number;
};
recommendations: string[];
timestamp: number;
}6.2 性能仪表板组件
// 开山网性能监控仪表板
const KaiShanWangPerformanceDashboard = () => {
const [metrics, setMetrics] = useState<PerformanceReport | null>(null);
const [isMonitoring, setIsMonitoring] = useState(false);
const monitorRef = useRef<KaiShanWangPerformanceMonitor | null>(null);
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
useEffect(() => {
monitorRef.current = KaiShanWangPerformanceMonitor.getInstance();
return () => {
monitorRef.current?.stopMonitoring();
};
}, []);
const startMonitoring = useCallback(() => {
monitorRef.current?.startMonitoring();
setIsMonitoring(true);
const interval = setInterval(() => {
const report = monitorRef.current?.generateReport();
if (report) {
setMetrics(report);
}
}, 5000);
return () => clearInterval(interval);
}, []);
const stopMonitoring = useCallback(() => {
monitorRef.current?.stopMonitoring();
setIsMonitoring(false);
}, []);
// 获取指标状态颜色
const getMetricStatus = (value: number, thresholds: { good: number; bad: number }) => {
if (value <= thresholds.good) return { status: 'good', color: '#22c55e' };
if (value <= thresholds.bad) return { status: 'needs-improvement', color: '#f59e0b' };
return { status: 'poor', color: '#ef4444' };
};
// 获取CLS评分
const getCLSScore = (cls: number) => {
if (cls <= 0.1) return { score: '优秀', color: '#22c55e' };
if (cls <= 0.25) return { score: '良好', color: '#f59e0b' };
return { score: '需改进', color: '#ef4444' };
};
return (
<div className="ksw-performance-dashboard">
<div className="dashboard-header">
<h2>📊 开山网性能监控</h2>
<div className="monitoring-controls">
{!isMonitoring ? (
<button onClick={startMonitoring} className="start-btn">
▶ 开始监控
</button>
) : (
<button onClick={stopMonitoring} className="stop-btn">
⏹ 停止监控
</button>
)}
</div>
</div>
{metrics && (
<>
{/* 核心Web指标 */}
<div className="core-metrics">
<h3>核心Web指标</h3>
<div className="metrics-grid">
<KSWMetricCard
label="LCP"
value={`${(metrics.summary.lcp / 1000).toFixed(2)}s`}
target="< 2.5s"
status={getMetricStatus(metrics.summary.lcp, { good: 2500, bad: 4000 })}
icon="🚀"
/>
<KSWMetricCard
label="FID"
value={`${metrics.summary.fid.toFixed(0)}ms`}
target="< 100ms"
status={getMetricStatus(metrics.summary.fid, { good: 100, bad: 300 })}
icon="👆"
/>
<KSWMetricCard
label="CLS"
value={metrics.summary.cls.toFixed(3)}
target="< 0.1"
status={getMetricStatus(metrics.summary.cls, { good: 0.1, bad: 0.25 })}
icon="📐"
/>
<KSWMetricCard
label="INP"
value={`${metrics.summary.inp.toFixed(0)}ms`}
target="< 200ms"
status={getMetricStatus(metrics.summary.inp, { good: 200, bad: 500 })}
icon="⚡"
/>
</div>
</div>
{/* 移动端特定指标 */}
<div className="mobile-specific-metrics">
<h3>移动端特定指标</h3>
<div className="metrics-grid">
<KSWMetricCard
label="滚动FPS"
value={`${metrics.mobileMetrics.scrollFPS.toFixed(1)}`}
target="> 55"
status={getMetricStatus(metrics.mobileMetrics.scrollFPS, { good: 55, bad: 30 })}
icon="📜"
/>
<KSWMetricCard
label="触摸响应"
value={`${metrics.mobileMetrics.touchResponseTime.toFixed(0)}ms`}
target="< 100ms"
status={getMetricStatus(metrics.mobileMetrics.touchResponseTime, { good: 100, bad: 200 })}
icon="👆"
/>
<KSWMetricCard
label="图片加载"
value={`${metrics.mobileMetrics.imageLoadTime.toFixed(0)}ms`}
target="< 2000ms"
status={getMetricStatus(metrics.mobileMetrics.imageLoadTime, { good: 2000, bad: 4000 })}
icon="🖼️"
/>
<KSWMetricCard
label="内存使用"
value={`${(metrics.mobileMetrics.memoryUsage / 1024 / 1024).toFixed(1)}MB`}
target="< 200MB"
status={getMetricStatus(metrics.mobileMetrics.memoryUsage, { good: 200 * 1024 * 1024, bad: 400 * 1024 * 1024 })}
icon="💾"
/>
</div>
</div>
{/* 业务指标 */}
<div className="business-metrics">
<h3>业务指标</h3>
<div className="metrics-grid">
<KSWMetricCard
label="首次交互"
value={`${(metrics.businessMetrics.timeToFirstInteraction / 1000).toFixed(2)}s`}
target="< 1s"
status={getMetricStatus(metrics.businessMetrics.timeToFirstInteraction, { good: 1000, bad: 3000 })}
icon="🖱️"
/>
<KSWMetricCard
label="选码完成"
value={`${(metrics.businessMetrics.timeToCompleteSelection / 1000).toFixed(2)}s`}
target="< 3s"
status={getMetricStatus(metrics.businessMetrics.timeToCompleteSelection, { good: 3000, bad: 8000 })}
icon="👟"
/>
<KSWMetricCard
label="询价提交"
value={`${(metrics.businessMetrics.inquirySubmissionTime / 1000).toFixed(2)}s`}
target="< 2s"
status={getMetricStatus(metrics.businessMetrics.inquirySubmissionTime, { good: 2000, bad: 5000 })}
icon="💬"
/>
<KSWMetricCard
label="画廊切换"
value={`${metrics.businessMetrics.imageGallerySwitchTime.toFixed(0)}ms`}
target="< 100ms"
status={getMetricStatus(metrics.businessMetrics.imageGallerySwitchTime, { good: 100, bad: 300 })}
icon="🖼️"
/>
</div>
</div>
{/* 资源指标 */}
<div className="resource-metrics">
<h3>资源指标</h3>
<div className="metrics-grid">
<KSWMetricCard
label="页面大小"
value={`${(metrics.resourceMetrics.totalPageSize / 1024 / 1024).toFixed(2)}MB`}
target="< 3MB"
status={getMetricStatus(metrics.resourceMetrics.totalPageSize, { good: 3 * 1024 * 1024, bad: 6 * 1024 * 1024 })}
icon="📄"
/>
<KSWMetricCard
label="图片大小"
value={`${(metrics.resourceMetrics.imageTotalSize / 1024 / 1024).toFixed(2)}MB`}
target="< 2MB"
status={getMetricStatus(metrics.resourceMetrics.imageTotalSize, { good: 2 * 1024 * 1024, bad: 4 * 1024 * 1024 })}
icon="🖼️"
/>
<KSWMetricCard
label="API调用"
value={`${metrics.resourceMetrics.apiCallsCount}`}
target="< 20"
status={getMetricStatus(metrics.resourceMetrics.apiCallsCount, { good: 20, bad: 40 })}
icon="🔌"
/>
<KSWMetricCard
label="WebSocket延迟"
value={`${metrics.resourceMetrics.websocketLatency.toFixed(0)}ms`}
target="< 100ms"
status={getMetricStatus(metrics.resourceMetrics.websocketLatency, { good: 100, bad: 300 })}
icon="📡"
/>
</div>
</div>
{/* 优化建议 */}
<div className="recommendations">
<h3>💡 优化建议</h3>
{metrics.recommendations.length > 0 ? (
<ul>
{metrics.recommendations.map((rec, index) => (
<li key={index}>{rec}</li>
))}
</ul>
) : (
<p className="no-recommendations">🎉 当前性能表现良好,无需特别优化</p>
)}
</div>
{/* 实时监控图表 */}
<div className="realtime-charts">
<h3>📈 实时性能趋势</h3>
<div className="chart-placeholder">
<p>图表组件将在这里显示实时性能数据</p>
</div>
</div>
</>
)}
</div>
);
};
// 开山网指标卡片组件
const KSWMetricCard = memo(({
label,
value,
target,
status,
icon
}: {
label: string;
value: string;
target: string;
status: { status: string; color: string };
icon: string;
}) => {
return (
<div className={`ksw-metric-card ${status.status}`} style={{ borderColor: status.color }}>
<div className="metric-icon" style={{ backgroundColor: status.color }}>
{icon}
</div>
<div className="metric-content">
<div className="metric-label">{label}</div>
<div className="metric-value" style={{ color: status.color }}>
{value}
</div>
<div className="metric-target">目标: {target}</div>
</div>
<div className="metric-status" style={{ color: status.color }}>
{status.status === 'good' ? '✓' : status.status === 'needs-improvement' ? '⚠' : '✗'}
</div>
</div>
);
});七、开山网性能优化效果评估
7.1 性能提升数据
指标 | 优化前 | 优化后 | 提升幅度 | 目标达成 |
|---|---|---|---|---|
首屏LCP | 4.8s | 1.6s | 67% ↓ | ✅ < 2.5s |
移动端FPS | 28 | 58 | 107% ↑ | ✅ > 55 |
图片加载时间 | 4.2s | 1.1s | 74% ↓ | ✅ < 2s |
触摸响应延迟 | 220ms | 68ms | 69% ↓ | ✅ < 100ms |
尺码选择器响应 | 180ms | 35ms | 81% ↓ | ✅ < 50ms |
价格计算时间 | 450ms | 85ms | 81% ↓ | ✅ < 100ms |
页面总大小 | 12.5MB | 3.2MB | 74% ↓ | ✅ < 3.5MB |
首屏可交互 | 4.5s | 1.1s | 76% ↓ | ✅ < 1.5s |
图片总数 | 60-100+ | 20-30 | 60% ↓ | ✅ 按需加载 |
内存峰值 | 350MB | 125MB | 64% ↓ | ✅ < 200MB |
7.2 业务指标改善
// 开山网优化带来的业务收益
const kaiShanWangBusinessImpact = {
// 移动端批采转化率
mobileBulkConversionRate: {
before: 0.8,
after: 2.1,
improvement: '+162.5%'
},
// 平均客单价
averageOrderValue: {
before: 12500,
after: 21800,
improvement: '+74.4%'
},
// 询价转化率
inquiryConversionRate: {
before: 18.5,
after: 42.3,
improvement: '+128.6%'
},
// 尺码选择完成率
sizeSelectionCompletionRate: {
before: 67.2,
after: 91.8,
improvement: '+36.6%'
},
// 页面停留时间
avgSessionDuration: {
before: 145,
after: 285,
improvement: '+96.6%'
},
// 图片加载放弃率
imageLoadAbandonRate: {
before: 42.8,
after: 11.3,
improvement: '-73.6%'
},
// 移动端跳出率
mobileBounceRate: {
before: 68.5,
after: 39.2,
improvement: '-42.8%'
},
// 批量选码使用率
bulkSelectionUsageRate: {
before: 23.1,
after: 67.4,
improvement: '+191.8%'
},
// 复购率
repurchaseRate: {
before: 31.6,
after: 52.8,
improvement: '+67.1%'
},
// 询价响应满意度
inquiryResponseSatisfaction: {
before: 72.3,
after: 89.6,
improvement: '+23.9%'
}
};八、开山网持续优化策略
8.1 性能预算维护
// 开山网性能预算配置
const kaiShanWangPerformanceBudget = {
// 核心Web指标
coreWebVitals: {
LCP: 2500,
FID: 100,
CLS: 0.1,
INP: 200
},
// 移动端特定指标
mobileMetrics: {
minScrollFPS: 55,
maxTouchResponse: 100,
maxImageLoadTime: 2000,
maxMemoryUsage: 200 * 1024 * 1024,
maxBatteryDrain: 5
},
// 业务指标
businessMetrics: {
maxTimeToFirstInteraction: 1000,
maxTimeToCompleteSelection: 3000,
maxInquirySubmissionTime: 2000,
maxImageGallerySwitchTime: 100,
maxSizeSelectorResponseTime: 50
},
// 资源限制
resources: {
totalPageSize: 3.5 * 1024 * 1024,
imageTotalSize: 2.5 * 1024 * 1024,
javascriptSize: 600 * 1024,
cssSize: 120 * 1024,
fontTotalSize: 250 * 1024,
apiCallsPerPage: 25,
websocketConnections: 3
},
// 开山网专项
kaiShanWangSpecific: {
maxShoeImages: 30,
maxSizeOptions: 100,
maxPricingRules: 20,
maxBulkSelections: 50,
maxImageGalleryTransitions: 5
}
};
// 开山网性能预算检查器
function checkKaiShanWangPerformanceBudget(
metrics: PerformanceReport,
resourceMetrics: ResourceMetrics
): BudgetViolation[] {
const violations: BudgetViolation[] = [];
// 检查核心指标
if (metrics.summary.lcp > kaiShanWangPerformanceBudget.coreWebVitals.LCP) {
violations.push({
type: 'core-web-vital',
metric: 'LCP',
actual: metrics.summary.lcp,
budget: kaiShanWangPerformanceBudget.coreWebVitals.LCP,
severity: metrics.summary.lcp > kaiShanWangPerformanceBudget.coreWebVitals.LCP * 1.5 ? 'critical' : 'warning'
});
}
if (metrics.summary.fid > kaiShanWangPerformanceBudget.coreWebVitals.FID) {
violations.push({
type: 'core-web-vital',
metric: 'FID',
actual: metrics.summary.fid,
budget: kaiShanWangPerformanceBudget.coreWebVitals.FID,
severity: 'warning'
});
}
if (metrics.summary.cls > kaiShanWangPerformanceBudget.coreWebVitals.CLS) {
violations.push({
type: 'core-web-vital',
metric: 'CLS',
actual: metrics.summary.cls,
budget: kaiShanWangPerformanceBudget.coreWebVitals.CLS,
severity: 'warning'
});
}
// 检查移动端指标
if (metrics.mobileMetrics.scrollFPS < kaiShanWangPerformanceBudget.mobileMetrics.minScrollFPS) {
violations.push({
type: 'mobile-metric',
metric: 'scrollFPS',
actual: metrics.mobileMetrics.scrollFPS,
budget: kaiShanWangPerformanceBudget.mobileMetrics.minScrollFPS,
severity: 'warning'
});
}
if (metrics.mobileMetrics.touchResponseTime > kaiShanWangPerformanceBudget.mobileMetrics.maxTouchResponse) {
violations.push({
type: 'mobile-metric',
metric: 'touchResponseTime',
actual: metrics.mobileMetrics.touchResponseTime,
budget: kaiShanWangPerformanceBudget.mobileMetrics.maxTouchResponse,
severity: 'warning'
});
}
// 检查业务指标
if (metrics.businessMetrics.timeToFirstInteraction > kaiShanWangPerformanceBudget.businessMetrics.maxTimeToFirstInteraction) {
violations.push({
type: 'business-metric',
metric: 'timeToFirstInteraction',
actual: metrics.businessMetrics.timeToFirstInteraction,
budget: kaiShanWangPerformanceBudget.businessMetrics.maxTimeToFirstInteraction,
severity: 'warning'
});
}
if (metrics.businessMetrics.timeToCompleteSelection > kaiShanWangPerformanceBudget.businessMetrics.maxTimeToCompleteSelection) {
violations.push({
type: 'business-metric',
metric: 'timeToCompleteSelection',
actual: metrics.businessMetrics.timeToCompleteSelection,
budget: kaiShanWangPerformanceBudget.businessMetrics.maxTimeToCompleteSelection,
severity: 'warning'
});
}
// 检查资源限制
if (resourceMetrics.totalPageSize > kaiShanWangPerformanceBudget.resources.totalPageSize) {
violations.push({
type: 'resource-limit',
metric: 'totalPageSize',
actual: resourceMetrics.totalPageSize,
budget: kaiShanWangPerformanceBudget.resources.totalPageSize,
severity: 'critical'
});
}
if (resourceMetrics.imageTotalSize > kaiShanWangPerformanceBudget.resources.imageTotalSize) {
violations.push({
type: 'resource-limit',
metric: 'imageTotalSize',
actual: resourceMetrics.imageTotalSize,
budget: kaiShanWangPerformanceBudget.resources.imageTotalSize,
severity: 'warning'
});
}
// 检查开山网专项指标
if (resourceMetrics.shoeImagesCount > kaiShanWangPerformanceBudget.kaiShanWangSpecific.maxShoeImages) {
violations.push({
type: 'ksw-specific',
metric: 'shoeImagesCount',
actual: resourceMetrics.shoeImagesCount,
budget: kaiShanWangPerformanceBudget.kaiShanWangSpecific.maxShoeImages,
severity: 'warning'
});
}
if (resourceMetrics.sizeOptionsCount > kaiShanWangPerformanceBudget.kaiShanWangSpecific.maxSizeOptions) {
violations.push({
type: 'ksw-specific',
metric: 'sizeOptionsCount',
actual: resourceMetrics.sizeOptionsCount,
budget: kaiShanWangPerformanceBudget.kaiShanWangSpecific.maxSizeOptions,
severity: 'warning'
});
}
return violations;
}8.2 持续优化路线图
## 开山网性能优化路线图 ### Q1 2026 - 移动端基础优化 - [ ] 完成鞋子图片智能加载系统 - [ ] 实现高性能尺码选择器 - [ ] 优化移动端批采界面 - [ ] 建立核心Web指标监控 ### Q2 2026 - 业务功能优化 - [ ] 优化批发价格计算引擎 - [ ] 实现实时库存同步 - [ ] 完善询价流程性能 - [ ] 建立业务指标监控 ### Q3 2026 - 智能化优化 - [ ] 引入AI预测性能瓶颈 - [ ] 实现自适应图片质量 - [ ] 优化离线批采体验 - [ ] 建立实时性能告警 ### Q4 2026 - 前沿技术探索 - [ ] 探索WebAssembly在图像处理中的应用 - [ ] 尝试边缘计算优化全球访问 - [ ] 评估5G网络下的性能策略 - [ ] 构建下一代移动端架构 ### 长期目标 - 打造鞋类B2B批发行业性能标杆 - 实现毫秒级尺码选择和价格计算 - 构建自适应的移动端批采体验 - 成为垂直行业性能优化的典范
需要我针对开山网的鞋子图片智能压缩算法或尺码选择器虚拟滚动优化,提供更详细的实现方案和性能测试数据吗?