diff --git a/src/components/PointRecord.vue b/src/components/PointRecord.vue index 05ad7c6..d03044e 100644 --- a/src/components/PointRecord.vue +++ b/src/components/PointRecord.vue @@ -46,7 +46,7 @@ onMounted(() => { {{ data.createAt }} - 黄心率:{{ Number(data.yellowRate.toFixed(2)) * 100 }}% + 黄心率:{{ data.yellowRate * 100 }}% 10环数:{{ data.tenRings }} 平均:{{ data.averageRing }} @@ -131,7 +131,7 @@ onMounted(() => { width: 60px; display: flex; justify-content: center; - top: calc(50% - 11px); + top: calc(50% - 13px); left: calc(50% - 30px); } .arrow-amount > text:nth-child(2) { diff --git a/src/kde-heatmap.js b/src/kde-heatmap.js index ef21d76..14e3dbc 100644 --- a/src/kde-heatmap.js +++ b/src/kde-heatmap.js @@ -150,7 +150,7 @@ function getHeatColor(density) { const red = Math.round(50 * (intensity - 0.5) * 2); const green = Math.round(180 + 75 * (1 - intensity)); const blue = Math.round(30 * (1 - intensity)); - return `rgba(${red}, ${green}, ${blue}, ${alpha * 0.7})`; + return `rgba(${red}, ${green}, ${blue}, ${alpha * 0.8})`; } } @@ -167,8 +167,16 @@ const heatmapCache = new Map(); * @returns {Promise} 绘制完成的Promise */ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) { - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { try { + // iOS兼容性:限制canvas尺寸,避免内存问题 + const maxCanvasSize = 1500; // iOS设备最大建议尺寸 + if (width > maxCanvasSize || height > maxCanvasSize) { + const scale = Math.min(maxCanvasSize / width, maxCanvasSize / height); + width = Math.floor(width * scale); + height = Math.floor(height * scale); + console.log(`iOS兼容性:限制canvas尺寸为 ${width}x${height}`); + } const { bandwidth = 0.8, gridSize = 100, @@ -177,11 +185,28 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) { pointColor = "rgba(255, 255, 255, 0.9)", } = options; - // 创建绘图上下文 - const ctx = uni.createCanvasContext(canvasId); + // 创建绘图上下文 - iOS兼容性检查 + let ctx; + try { + ctx = uni.createCanvasContext(canvasId); + if (!ctx) { + throw new Error("无法创建canvas上下文"); + } + } catch (error) { + console.error("创建canvas上下文失败:", error); + reject(new Error("iOS兼容性:Canvas上下文创建失败")); + return; + } // 清空画布 ctx.clearRect(0, 0, width, height); + + // iOS兼容性:设置全局合成操作,让颜色叠加更自然 + try { + ctx.globalCompositeOperation = 'screen'; + } catch (error) { + console.warn("设置全局合成操作失败,使用默认设置:", error); + } // 如果没有数据,直接绘制 if (!points || points.length === 0) { @@ -189,6 +214,97 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) { return; } + // 使用分片处理,避免长时间阻塞主线程 + const processInChunks = (data, chunkSize = 1000) => { + return new Promise((resolve) => { + let index = 0; + let frameCount = 0; + + const processChunk = () => { + // iOS兼容性:使用Date.now()作为performance.now的回退 + const startTime = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now(); + const endIndex = Math.min(index + chunkSize, data.length); + + // 批量处理多个点,减少函数调用开销 + ctx.save(); // 保存当前状态 + + for (let i = index; i < endIndex; i++) { + const point = data[i]; + // 处理单个点的绘制逻辑 + processPoint(point); + + // 每处理50个点检查一次时间,避免超时 + const currentTime = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now(); + if (i % 50 === 0 && currentTime - startTime > 8) { + // 如果处理时间超过8ms,保存状态并中断 + index = i + 1; + ctx.restore(); + // iOS兼容性:更安全的requestAnimationFrame检测 + if (typeof requestAnimationFrame === 'function') { + try { + requestAnimationFrame(processChunk); + } catch (e) { + // 如果requestAnimationFrame失败,使用setTimeout + setTimeout(processChunk, 2); + } + } else { + setTimeout(processChunk, 2); // 小延迟后继续 + } + return; + } + } + + ctx.restore(); // 恢复状态 + index = endIndex; + frameCount++; + + if (index < data.length) { + // 动态调整延迟:如果处理时间超过16ms(一帧),使用更大延迟 + const currentTime = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now(); + const processingTime = currentTime - startTime; + const delay = processingTime > 16 ? 8 : 1; // 根据处理时间动态调整 + + // iOS兼容性:更安全的requestAnimationFrame检测 + if (typeof requestAnimationFrame === 'function') { + try { + requestAnimationFrame(processChunk); + } catch (e) { + // 如果requestAnimationFrame失败,使用setTimeout + setTimeout(processChunk, delay); + } + } else { + setTimeout(processChunk, delay); + } + } else { + resolve(); + } + }; + + processChunk(); + }); + }; + + // 处理单个点的函数 - iOS兼容性优化 + const processPoint = (point) => { + const [x, y, density] = point; + const normalizedX = (x - range[0]) / (range[1] - range[0]); + const normalizedY = (y - range[0]) / (range[1] - range[0]); + const canvasX = normalizedX * width; + const canvasY = normalizedY * height; + const color = getHeatColor(density); + + // iOS兼容性:确保数值有效 + if (isNaN(canvasX) || isNaN(canvasY) || !isFinite(canvasX) || !isFinite(canvasY)) { + return; + } + + ctx.setFillStyle(color); + ctx.beginPath(); + const radius = Math.max(1, Math.min(width / gridSize, height / gridSize) * 0.6); // 确保半径至少为1 + ctx.arc(canvasX, canvasY, radius, 0, 2 * Math.PI); + ctx.fill(); + }; + // 生成缓存key(基于参数和数据点的哈希) const cacheKey = `${bandwidth}-${gridSize}-${range.join(',')}-${points.length}-${JSON.stringify(points.slice(0, 10))}`; @@ -197,29 +313,17 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) { console.log('使用缓存的热力图数据'); const cachedDensityData = heatmapCache.get(cacheKey); - // 直接使用缓存数据绘制 - const cellWidth = width / gridSize; - const cellHeight = height / gridSize; - const xRange = range[1] - range[0]; - const yRange = range[1] - range[0]; + // 使用分片处理绘制缓存数据 + await processInChunks(cachedDensityData, 200); // 每批处理200个点,减少单次处理量 - cachedDensityData.forEach((point) => { - const [x, y, density] = point; - const normalizedX = (x - range[0]) / xRange; - const normalizedY = (y - range[0]) / yRange; - const canvasX = normalizedX * width; - const canvasY = normalizedY * height; - const color = getHeatColor(density); - - ctx.setFillStyle(color); - ctx.beginPath(); - ctx.arc(canvasX, canvasY, Math.min(cellWidth, cellHeight) * 0.6, 0, 2 * Math.PI); - ctx.fill(); - }); - - // 绘制原始数据点 + // 绘制原始数据点 - iOS兼容性优化 if (showPoints) { ctx.setFillStyle(pointColor); + ctx.beginPath(); // 开始批量路径 + const xRange = range[1] - range[0]; + const yRange = range[1] - range[0]; + let validPoints = 0; + points.forEach((point) => { const [x, y] = point; const normalizedX = (x - range[0]) / xRange; @@ -227,15 +331,25 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) { const canvasX = normalizedX * width; const canvasY = normalizedY * height; - ctx.beginPath(); - ctx.arc(canvasX, canvasY, 2.5, 0, 2 * Math.PI); - ctx.fill(); + // iOS兼容性:确保坐标有效 + if (!isNaN(canvasX) && !isNaN(canvasY) && isFinite(canvasX) && isFinite(canvasY)) { + ctx.arc(canvasX, canvasY, 2.5, 0, 2 * Math.PI); + validPoints++; + } }); + + // 只有在有有效点的情况下才执行填充 + if (validPoints > 0) { + ctx.fill(); // 一次性填充所有圆点 + } } ctx.draw(false, () => { console.log("KDE热力图绘制完成(缓存)"); resolve(); + }, (error) => { + console.error("KDE热力图绘制失败(缓存):", error); + reject(new Error("Canvas绘制失败(缓存): " + error)); }); return; } @@ -258,40 +372,15 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) { const xRange = range[1] - range[0]; const yRange = range[1] - range[0]; - // 绘制热力图网格 - 批量绘制优化 - const colorGroups = new Map(); - - // 按颜色分组,减少setFillStyle调用 - densityData.forEach((point) => { - const [x, y, density] = point; - const color = getHeatColor(density); - - if (!colorGroups.has(color)) { - colorGroups.set(color, []); - } - colorGroups.get(color).push(point); - }); - - // 批量绘制相同颜色的点 - colorGroups.forEach((points, color) => { - ctx.setFillStyle(color); - points.forEach((point) => { - const [x, y, density] = point; - const normalizedX = (x - range[0]) / xRange; - const normalizedY = (y - range[0]) / yRange; - const canvasX = normalizedX * width; - const canvasY = normalizedY * height; - - ctx.beginPath(); - ctx.arc(canvasX, canvasY, Math.min(cellWidth, cellHeight) * 0.6, 0, 2 * Math.PI); - ctx.fill(); - }); - }); + // 使用分片处理绘制热力图网格 + await processInChunks(densityData, 200); // 每批处理200个点,减少单次处理量 - // 绘制原始数据点 - 批量绘制优化 + // 绘制原始数据点 - iOS兼容性优化 if (showPoints) { ctx.setFillStyle(pointColor); ctx.beginPath(); // 开始批量路径 + let validPoints = 0; + points.forEach((point) => { const [x, y] = point; const normalizedX = (x - range[0]) / xRange; @@ -299,15 +388,26 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) { const canvasX = normalizedX * width; const canvasY = normalizedY * height; - ctx.arc(canvasX, canvasY, 2.5, 0, 2 * Math.PI); + // iOS兼容性:确保坐标有效 + if (!isNaN(canvasX) && !isNaN(canvasY) && isFinite(canvasX) && isFinite(canvasY)) { + ctx.arc(canvasX, canvasY, 2.5, 0, 2 * Math.PI); + validPoints++; + } }); - ctx.fill(); // 一次性填充所有圆点 + + // 只有在有有效点的情况下才执行填充 + if (validPoints > 0) { + ctx.fill(); // 一次性填充所有圆点 + } } - // 执行绘制 + // 执行绘制 - iOS兼容性优化 ctx.draw(false, () => { console.log("KDE热力图绘制完成"); resolve(); + }, (error) => { + console.error("KDE热力图绘制失败:", error); + reject(new Error("Canvas绘制失败: " + error)); }); } catch (error) { console.error("KDE热力图绘制失败:", error); @@ -330,13 +430,15 @@ export function generateKDEHeatmapImage( return new Promise((resolve, reject) => { drawKDEHeatmap(canvasId, width, height, points, options) .then(() => { - // 生成图片 + // 生成图片 - iOS兼容性优化 uni.canvasToTempFilePath({ canvasId: canvasId, width: width, height: height, - destWidth: width * 3, // 提高输出分辨率,让图像更细腻 - destHeight: height * 3, + destWidth: width * 2, // iOS兼容性:降低分辨率避免内存问题 + destHeight: height * 2, + fileType: "png", // iOS兼容性:明确指定png格式 + quality: 1, // iOS兼容性:最高质量 success: (res) => { console.log("KDE热力图图片生成成功:", res.tempFilePath); resolve(res.tempFilePath); diff --git a/src/pages/point-book.vue b/src/pages/point-book.vue index 04490e0..583ea2c 100644 --- a/src/pages/point-book.vue +++ b/src/pages/point-book.vue @@ -76,29 +76,56 @@ const loadData = async () => { else if (result2.checkInCount >= 5) hot = 3; else if (result2.checkInCount === 7) hot = 4; uni.$emit("update-hot", hot); - setTimeout(async () => { + // 异步生成热力图,不阻塞UI + const generateHeatmapAsync = async () => { + const weekArrows = result2.weekArrows + .filter((item) => item.x && item.y) + .map((item) => [item.x, item.y]); + try { - const imagePath = await generateKDEHeatmapImage( + // 渐进式渲染:数据量大时先快速渲染粗略版本 + if (weekArrows.length > 1000) { + const quickPath = await generateKDEHeatmapImage( + "heatMapCanvas", + rect.width, + rect.height, + weekArrows, + { + range: [0, 1], + gridSize: 80, // 先使用较小的gridSize快速显示 + bandwidth: 0.2, + showPoints: false, + } + ); + heatMapImageSrc.value = quickPath; + // 延迟后再渲染精细版本 + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + // 渲染最终精细版本 + const finalPath = await generateKDEHeatmapImage( "heatMapCanvas", rect.width, rect.height, - result2.weekArrows - .filter((item) => item.x && item.y) - .map((item) => [item.x, item.y]), + weekArrows, { - range: [0, 1], // 适配0-1坐标范围 + range: [0, 1], gridSize: 120, // 更高的网格密度,减少锯齿 bandwidth: 0.15, // 稍小的带宽,让热力图更细腻 - showPoints: false, // 显示白色原始数据点 + showPoints: false, } ); - heatMapImageSrc.value = imagePath; // 存储生成的图片地址 + heatMapImageSrc.value = finalPath; loadImage.value = false; - console.log("热力图图片地址:", imagePath); + console.log("热力图图片地址:", finalPath); } catch (error) { console.error("生成热力图图片失败:", error); + loadImage.value = false; } - }, 300); + }; + + // 异步生成热力图,不阻塞UI + generateHeatmapAsync(); }; watch( @@ -224,19 +251,19 @@ onShareTimeline(() => { {{ data.todayTotalArrow || "-" }} - 今日射箭(箭) + 今日射箭(箭) {{ data.totalArrow || "-" }} - 累计射箭(箭) + 累计射箭(箭) {{ data.totalDay || "-" }} - 已训练天数(天) + 已训练天数(天) {{ data.averageRing || "-" }} - 平均环数(箭) + 平均环数(箭) {{ @@ -244,7 +271,7 @@ onShareTimeline(() => { ? Number(data.yellowRate * 100).toFixed(2) : "-" }} - 黄心率(%) + 黄心率(%)