代码优化

This commit is contained in:
kron
2025-09-30 16:47:00 +08:00
parent e636d02657
commit 1daa830ed0
3 changed files with 210 additions and 81 deletions

View File

@@ -46,7 +46,7 @@ onMounted(() => {
<text>{{ data.createAt }}</text> <text>{{ data.createAt }}</text>
</view> </view>
<view> <view>
<text>黄心率{{ Number(data.yellowRate.toFixed(2)) * 100 }}%</text> <text>黄心率{{ data.yellowRate * 100 }}%</text>
<text>10环数{{ data.tenRings }}</text> <text>10环数{{ data.tenRings }}</text>
<text>平均{{ data.averageRing }}</text> <text>平均{{ data.averageRing }}</text>
</view> </view>
@@ -131,7 +131,7 @@ onMounted(() => {
width: 60px; width: 60px;
display: flex; display: flex;
justify-content: center; justify-content: center;
top: calc(50% - 11px); top: calc(50% - 13px);
left: calc(50% - 30px); left: calc(50% - 30px);
} }
.arrow-amount > text:nth-child(2) { .arrow-amount > text:nth-child(2) {

View File

@@ -150,7 +150,7 @@ function getHeatColor(density) {
const red = Math.round(50 * (intensity - 0.5) * 2); const red = Math.round(50 * (intensity - 0.5) * 2);
const green = Math.round(180 + 75 * (1 - intensity)); const green = Math.round(180 + 75 * (1 - intensity));
const blue = Math.round(30 * (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 * @returns {Promise} 绘制完成的Promise
*/ */
export function drawKDEHeatmap(canvasId, width, height, points, options = {}) { export function drawKDEHeatmap(canvasId, width, height, points, options = {}) {
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
try { 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 { const {
bandwidth = 0.8, bandwidth = 0.8,
gridSize = 100, gridSize = 100,
@@ -177,18 +185,126 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) {
pointColor = "rgba(255, 255, 255, 0.9)", pointColor = "rgba(255, 255, 255, 0.9)",
} = options; } = options;
// 创建绘图上下文 // 创建绘图上下文 - iOS兼容性检查
const ctx = uni.createCanvasContext(canvasId); 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); ctx.clearRect(0, 0, width, height);
// iOS兼容性设置全局合成操作让颜色叠加更自然
try {
ctx.globalCompositeOperation = 'screen';
} catch (error) {
console.warn("设置全局合成操作失败,使用默认设置:", error);
}
// 如果没有数据,直接绘制 // 如果没有数据,直接绘制
if (!points || points.length === 0) { if (!points || points.length === 0) {
ctx.draw(false, () => resolve()); ctx.draw(false, () => resolve());
return; 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基于参数和数据点的哈希 // 生成缓存key基于参数和数据点的哈希
const cacheKey = `${bandwidth}-${gridSize}-${range.join(',')}-${points.length}-${JSON.stringify(points.slice(0, 10))}`; 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('使用缓存的热力图数据'); console.log('使用缓存的热力图数据');
const cachedDensityData = heatmapCache.get(cacheKey); const cachedDensityData = heatmapCache.get(cacheKey);
// 直接使用缓存数据绘制 // 使用分片处理绘制缓存数据
const cellWidth = width / gridSize; await processInChunks(cachedDensityData, 200); // 每批处理200个点减少单次处理量
const cellHeight = height / gridSize;
const xRange = range[1] - range[0];
const yRange = range[1] - range[0];
cachedDensityData.forEach((point) => { // 绘制原始数据点 - iOS兼容性优化
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();
});
// 绘制原始数据点
if (showPoints) { if (showPoints) {
ctx.setFillStyle(pointColor); ctx.setFillStyle(pointColor);
ctx.beginPath(); // 开始批量路径
const xRange = range[1] - range[0];
const yRange = range[1] - range[0];
let validPoints = 0;
points.forEach((point) => { points.forEach((point) => {
const [x, y] = point; const [x, y] = point;
const normalizedX = (x - range[0]) / xRange; const normalizedX = (x - range[0]) / xRange;
@@ -227,15 +331,25 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) {
const canvasX = normalizedX * width; const canvasX = normalizedX * width;
const canvasY = normalizedY * height; const canvasY = normalizedY * height;
ctx.beginPath(); // iOS兼容性确保坐标有效
if (!isNaN(canvasX) && !isNaN(canvasY) && isFinite(canvasX) && isFinite(canvasY)) {
ctx.arc(canvasX, canvasY, 2.5, 0, 2 * Math.PI); ctx.arc(canvasX, canvasY, 2.5, 0, 2 * Math.PI);
ctx.fill(); validPoints++;
}
}); });
// 只有在有有效点的情况下才执行填充
if (validPoints > 0) {
ctx.fill(); // 一次性填充所有圆点
}
} }
ctx.draw(false, () => { ctx.draw(false, () => {
console.log("KDE热力图绘制完成缓存"); console.log("KDE热力图绘制完成缓存");
resolve(); resolve();
}, (error) => {
console.error("KDE热力图绘制失败缓存:", error);
reject(new Error("Canvas绘制失败缓存: " + error));
}); });
return; return;
} }
@@ -258,40 +372,15 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) {
const xRange = range[1] - range[0]; const xRange = range[1] - range[0];
const yRange = range[1] - range[0]; const yRange = range[1] - range[0];
// 绘制热力图网格 - 批量绘制优化 // 使用分片处理绘制热力图网格
const colorGroups = new Map(); await processInChunks(densityData, 200); // 每批处理200个点减少单次处理量
// 按颜色分组减少setFillStyle调用 // 绘制原始数据点 - iOS兼容性优化
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();
});
});
// 绘制原始数据点 - 批量绘制优化
if (showPoints) { if (showPoints) {
ctx.setFillStyle(pointColor); ctx.setFillStyle(pointColor);
ctx.beginPath(); // 开始批量路径 ctx.beginPath(); // 开始批量路径
let validPoints = 0;
points.forEach((point) => { points.forEach((point) => {
const [x, y] = point; const [x, y] = point;
const normalizedX = (x - range[0]) / xRange; const normalizedX = (x - range[0]) / xRange;
@@ -299,15 +388,26 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) {
const canvasX = normalizedX * width; const canvasX = normalizedX * width;
const canvasY = normalizedY * height; const canvasY = normalizedY * height;
// iOS兼容性确保坐标有效
if (!isNaN(canvasX) && !isNaN(canvasY) && isFinite(canvasX) && isFinite(canvasY)) {
ctx.arc(canvasX, canvasY, 2.5, 0, 2 * Math.PI); ctx.arc(canvasX, canvasY, 2.5, 0, 2 * Math.PI);
validPoints++;
}
}); });
// 只有在有有效点的情况下才执行填充
if (validPoints > 0) {
ctx.fill(); // 一次性填充所有圆点 ctx.fill(); // 一次性填充所有圆点
} }
}
// 执行绘制 // 执行绘制 - iOS兼容性优化
ctx.draw(false, () => { ctx.draw(false, () => {
console.log("KDE热力图绘制完成"); console.log("KDE热力图绘制完成");
resolve(); resolve();
}, (error) => {
console.error("KDE热力图绘制失败:", error);
reject(new Error("Canvas绘制失败: " + error));
}); });
} catch (error) { } catch (error) {
console.error("KDE热力图绘制失败:", error); console.error("KDE热力图绘制失败:", error);
@@ -330,13 +430,15 @@ export function generateKDEHeatmapImage(
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
drawKDEHeatmap(canvasId, width, height, points, options) drawKDEHeatmap(canvasId, width, height, points, options)
.then(() => { .then(() => {
// 生成图片 // 生成图片 - iOS兼容性优化
uni.canvasToTempFilePath({ uni.canvasToTempFilePath({
canvasId: canvasId, canvasId: canvasId,
width: width, width: width,
height: height, height: height,
destWidth: width * 3, // 提高输出分辨率,让图像更细腻 destWidth: width * 2, // iOS兼容性降低分辨率避免内存问题
destHeight: height * 3, destHeight: height * 2,
fileType: "png", // iOS兼容性明确指定png格式
quality: 1, // iOS兼容性最高质量
success: (res) => { success: (res) => {
console.log("KDE热力图图片生成成功:", res.tempFilePath); console.log("KDE热力图图片生成成功:", res.tempFilePath);
resolve(res.tempFilePath); resolve(res.tempFilePath);

View File

@@ -76,29 +76,56 @@ const loadData = async () => {
else if (result2.checkInCount >= 5) hot = 3; else if (result2.checkInCount >= 5) hot = 3;
else if (result2.checkInCount === 7) hot = 4; else if (result2.checkInCount === 7) hot = 4;
uni.$emit("update-hot", hot); 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 { try {
const imagePath = await generateKDEHeatmapImage( // 渐进式渲染:数据量大时先快速渲染粗略版本
if (weekArrows.length > 1000) {
const quickPath = await generateKDEHeatmapImage(
"heatMapCanvas", "heatMapCanvas",
rect.width, rect.width,
rect.height, rect.height,
result2.weekArrows weekArrows,
.filter((item) => item.x && item.y)
.map((item) => [item.x, item.y]),
{ {
range: [0, 1], // 适配0-1坐标范围 range: [0, 1],
gridSize: 120, // 更高的网格密度,减少锯齿 gridSize: 80, // 先使用较小的gridSize快速显示
bandwidth: 0.15, // 稍小的带宽,让热力图更细腻 bandwidth: 0.2,
showPoints: false, // 显示白色原始数据点 showPoints: false,
} }
); );
heatMapImageSrc.value = imagePath; // 存储生成的图片地址 heatMapImageSrc.value = quickPath;
// 延迟后再渲染精细版本
await new Promise((resolve) => setTimeout(resolve, 500));
}
// 渲染最终精细版本
const finalPath = await generateKDEHeatmapImage(
"heatMapCanvas",
rect.width,
rect.height,
weekArrows,
{
range: [0, 1],
gridSize: 120, // 更高的网格密度,减少锯齿
bandwidth: 0.15, // 稍小的带宽,让热力图更细腻
showPoints: false,
}
);
heatMapImageSrc.value = finalPath;
loadImage.value = false; loadImage.value = false;
console.log("热力图图片地址:", imagePath); console.log("热力图图片地址:", finalPath);
} catch (error) { } catch (error) {
console.error("生成热力图图片失败:", error); console.error("生成热力图图片失败:", error);
loadImage.value = false;
} }
}, 300); };
// 异步生成热力图不阻塞UI
generateHeatmapAsync();
}; };
watch( watch(
@@ -224,19 +251,19 @@ onShareTimeline(() => {
<view class="statistics"> <view class="statistics">
<view> <view>
<text>{{ data.todayTotalArrow || "-" }}</text> <text>{{ data.todayTotalArrow || "-" }}</text>
<text>今日射箭</text> <text>今日射箭()</text>
</view> </view>
<view> <view>
<text>{{ data.totalArrow || "-" }}</text> <text>{{ data.totalArrow || "-" }}</text>
<text>累计射箭</text> <text>累计射箭()</text>
</view> </view>
<view> <view>
<text>{{ data.totalDay || "-" }}</text> <text>{{ data.totalDay || "-" }}</text>
<text>已训练天数</text> <text>已训练天数()</text>
</view> </view>
<view> <view>
<text>{{ data.averageRing || "-" }}</text> <text>{{ data.averageRing || "-" }}</text>
<text>平均环数</text> <text>平均环数()</text>
</view> </view>
<view> <view>
<text>{{ <text>{{
@@ -244,7 +271,7 @@ onShareTimeline(() => {
? Number(data.yellowRate * 100).toFixed(2) ? Number(data.yellowRate * 100).toFixed(2)
: "-" : "-"
}}</text> }}</text>
<text>黄心率%</text> <text>黄心率(%)</text>
</view> </view>
<view> <view>
<button hover-class="none" @click="startScoring"> <button hover-class="none" @click="startScoring">
@@ -276,7 +303,7 @@ onShareTimeline(() => {
" "
/> />
</view> </view>
<view class="reward" v-if="data.totalArrow && !loadImage"> <view class="reward" v-if="data.totalArrow">
<button hover-class="none" @click="showTip = true"> <button hover-class="none" @click="showTip = true">
<image src="../static/reward-us.png" mode="widthFix" /> <image src="../static/reward-us.png" mode="widthFix" />
</button> </button>