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)
: "-"
}}
- 黄心率(%)
+ 黄心率(%)
-
+