Files
shoot-miniprograms/src/util.js
T
2026-08-20 16:56:34 +08:00

441 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const formatTimestamp = (timestamp) => {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}-${month}-${day}`;
};
export const debounce = (fn, delay = 300) => {
let timer = null;
return async (...args) => {
if (timer) clearTimeout(timer);
return new Promise((resolve, reject) => {
timer = setTimeout(async () => {
try {
const result = await fn(...args);
resolve(result);
} catch (error) {
reject(error);
} finally {
timer = null;
}
}, delay);
});
};
};
export const isUnlimitedLimit = (item = {}) => Number(item.limit) === -1;
export const getLimitCountText = (label, item = {}) => {
if (!item || isUnlimitedLimit(item)) return "";
const count = Number(item.count || 0);
const limit = Number(item.limit || 0);
return `今日${label}次数:${count}/${limit}`;
};
export const isLimitReached = (item = {}) => {
if (!item || isUnlimitedLimit(item)) return false;
return Number(item.count || 0) >= Number(item.limit || 0);
};
export const isLimitError = (error) => {
const message = typeof error === "string" ? error : error?.message;
return String(message || "").includes("已达上限");
};
const isShareCancel = (error) =>
String(error?.errMsg || error || "").toLowerCase().includes("cancel");
export const wxShare = async (canvasId = "shareCanvas") => {
try {
// 先尝试按 id 查找 <canvas type="2d"> 节点
const { pixelRatio: dpr = 1 } = uni.getSystemInfoSync() || {};
const info = await new Promise((resolve) => {
const query = uni.createSelectorQuery();
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec((res) => resolve(res && res[0]));
});
const canvas = info?.node;
const cssWidth = info?.width;
const cssHeight = info?.height;
if (canvas && typeof canvas.getContext === "function") {
// 2D 画布导出:传入 canvas 节点
const tempPath = await new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
canvas,
width: cssWidth,
height: cssHeight,
// 按 DPR 导出更清晰的图片
destWidth: Math.round(cssWidth * dpr),
destHeight: Math.round(cssHeight * dpr),
fileType: "png",
quality: 1,
success: (r) => {
const p = r.tempFilePath || r.apFilePath || r.filePath;
if (!p) {
reject(new Error("share image path is empty"));
return;
}
resolve(p);
},
fail: reject,
});
});
await new Promise((resolve, reject) => {
wx.showShareImageMenu({
entrancePath: "pages/index",
path: tempPath,
success: resolve,
fail: (error) => {
if (isShareCancel(error)) {
resolve({ canceled: true });
return;
}
reject(error);
},
});
});
return tempPath;
}
// 回退:旧版非 2D 画布(通过 canvasId 导出)
const res = await new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
canvasId,
fileType: "png",
quality: 1,
success: resolve,
fail: reject,
});
});
const tempPath = res.tempFilePath || res.apFilePath || res.filePath;
if (!tempPath) {
throw new Error("share image path is empty");
}
await new Promise((resolve, reject) => {
wx.showShareImageMenu({
entrancePath: "pages/index",
path: tempPath,
success: resolve,
fail: (error) => {
if (isShareCancel(error)) {
resolve({ canceled: true });
return;
}
reject(error);
},
});
});
return tempPath;
} catch (error) {
console.log("生成图片失败:", error);
throw error;
}
};
// 获取元素尺寸和位置信息
export const getElementRect = (classname) => {
return new Promise((resolve) => {
const query = uni.createSelectorQuery();
query
.select(classname)
.boundingClientRect((rect) => {
resolve(rect);
})
.exec();
});
};
const calcNormalBowTarget = (x, y, diameter, arrowRadius) => {
// 将弓箭左上角坐标转换为圆心坐标
const arrowCenterX = x + arrowRadius;
const arrowCenterY = y + arrowRadius;
// 计算靶心坐标(靶纸中心)
const centerX = (diameter + arrowRadius * 2) / 2;
const centerY = (diameter + arrowRadius * 2) / 2;
// 计算弓箭圆心到靶心的距离
const deltaX = arrowCenterX - centerX;
const deltaY = arrowCenterY - centerY;
const distanceToCenter = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// 计算弓箭边缘到靶心的最近距离
const distance = Math.max(0, distanceToCenter - arrowRadius);
// 计算靶纸半径(取宽高中较小值的一半)
const targetRadius = diameter / 2;
// 计算相对距离(0-1之间)
let relativeDistance = distance / targetRadius;
// 全环靶有10个环,每个环占半径的10%
// 从外到内:1环到10环
// 距离越近靶心,环数越高
if (relativeDistance <= 0.05) return "X";
if (relativeDistance <= 0.1) return 10;
if (relativeDistance <= 0.2) return 9;
if (relativeDistance <= 0.3) return 8;
if (relativeDistance <= 0.4) return 7;
if (relativeDistance <= 0.5) return 6;
if (relativeDistance <= 0.6) return 5;
if (relativeDistance <= 0.7) return 4;
if (relativeDistance <= 0.8) return 3;
if (relativeDistance <= 0.9) return 2;
if (relativeDistance <= 1) return 1;
return 0; // 脱靶
};
const calcHalfBowTarget = (x, y, diameter, arrowRadius, noX = false) => {
// 将弓箭左上角坐标转换为圆心坐标
const arrowCenterX = x + arrowRadius;
const arrowCenterY = y + arrowRadius;
// 计算靶心坐标(靶纸中心)
const centerX = (diameter + arrowRadius * 2) / 2;
const centerY = (diameter + arrowRadius * 2) / 2;
// 计算弓箭圆心到靶心的距离
const deltaX = arrowCenterX - centerX;
const deltaY = arrowCenterY - centerY;
const distanceToCenter = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// 计算弓箭边缘到靶心的最近距离
const distance = Math.max(0, distanceToCenter - arrowRadius);
// 计算靶纸半径(取宽高中较小值的一半)
const targetRadius = diameter / 2;
// 计算相对距离(0-1之间)
let relativeDistance = distance / targetRadius;
if (relativeDistance <= 0.1) return noX ? 10 : "X";
if (relativeDistance <= 0.2) return noX ? 9 : 10;
if (relativeDistance <= 0.4) return 9;
if (relativeDistance <= 0.6) return 8;
if (relativeDistance <= 0.8) return 7;
if (relativeDistance <= 1) return 6;
return 0; // 脱靶
};
export const calcTripleBowTarget = (
x,
y,
diameter,
arrowRadius,
noX = false
) => {
const side = diameter * 0.325;
if (y / diameter >= 0.661) {
return calcHalfBowTarget(
x - diameter * 0.336,
y - diameter * 0.675,
side,
arrowRadius,
noX
);
}
if (y / diameter >= 0.323) {
return calcHalfBowTarget(
x - diameter * 0.336,
y - diameter * 0.337,
side,
arrowRadius,
noX
);
}
if (y / diameter >= -0.026) {
return calcHalfBowTarget(
x - diameter * 0.336,
y - diameter * 0.0,
side,
arrowRadius,
noX
);
}
return 0;
};
export const calcPinBowTarget = (x, y, diameter, arrowRadius, noX = false) => {
const side = diameter * 0.482;
let r1 = 0;
let r2 = 0;
let r3 = 0;
if (x / diameter >= 0.23 && y / diameter >= 0.005) {
r1 = calcHalfBowTarget(
x - diameter * 0.26,
y - diameter * 0.034,
side,
arrowRadius,
noX
);
}
if (x / diameter >= -0.03 && y / diameter >= 0.456) {
r2 = calcHalfBowTarget(x, y - diameter * 0.484, side, arrowRadius, noX);
}
if (x / diameter >= 0.49 && y / diameter >= 0.456) {
r3 = calcHalfBowTarget(
x - diameter * 0.52,
y - diameter * 0.485,
side,
arrowRadius,
noX
);
}
return r1 || r2 || r3;
};
export const calcRing = (bowtargetId, x, y, diameter, arrowRadius = 5) => {
if (bowtargetId < 4) {
return calcNormalBowTarget(x, y, diameter, arrowRadius);
} else if (bowtargetId < 7) {
return calcHalfBowTarget(x, y, diameter, arrowRadius);
} else if (bowtargetId === 7) {
return calcTripleBowTarget(x, y, diameter, arrowRadius);
} else if (bowtargetId === 8) {
return calcPinBowTarget(x, y, diameter, arrowRadius);
} else if (bowtargetId === 9) {
return calcTripleBowTarget(x, y, diameter, arrowRadius, true);
} else if (bowtargetId === 10) {
return calcPinBowTarget(x, y, diameter, arrowRadius, true);
}
return 0;
};
export const getDirectionText = (angle = 0) => {
if (angle < 0) return "";
if (angle >= 337.5 || angle < 22.5) {
return "下";
} else if (angle >= 22.5 && angle < 67.5) {
return "左下";
} else if (angle >= 67.5 && angle < 112.5) {
return "左";
} else if (angle >= 112.5 && angle < 157.5) {
return "左上";
} else if (angle >= 157.5 && angle < 202.5) {
return "上";
} else if (angle >= 202.5 && angle < 247.5) {
return "右上";
} else if (angle >= 247.5 && angle < 292.5) {
return "右";
} else if (angle >= 292.5 && angle < 337.5) {
return "右下";
}
};
// 射箭数据中的距离单位为厘米;旧版本会明确上报 target_ok=true。
export const getShootValidation = (shootData) => {
if (!shootData || typeof shootData !== "object") {
return {
rawDistance: 0,
distance: 0,
distanceOk: false,
targetOk: false,
targetDetected: false,
};
}
const rawDistance = Number(shootData.distance ?? shootData.dst ?? 0);
const finiteDistance = Number.isFinite(rawDistance) ? rawDistance : 0;
const targetFlag = shootData.targetOk ?? shootData.target_ok;
const targetDetected = finiteDistance !== 0;
const targetOk = finiteDistance > 0 && targetFlag === true;
return {
rawDistance: finiteDistance,
distance: Math.max(0, finiteDistance / 100),
// 旧固件以负距离表示靶纸错误,此时只提示靶纸问题,不叠加距离不足。
distanceOk: finiteDistance < 0 || finiteDistance >= 500,
targetOk: targetDetected && targetOk,
targetDetected,
};
};
// 测距阶段提示音:按站距与靶纸组合播放新版测距语音。
export const getDistanceCheckAudioKey = (shootData) => {
const result = getShootValidation(shootData);
if (!result.targetDetected) return "未识别到靶纸,请瞄准靶纸射箭";
if (!result.distanceOk && !result.targetOk) {
return "站距过近,靶纸错误";
}
if (!result.distanceOk) return "站距过近,靶纸正确";
if (!result.targetOk) return "站距合格,靶纸错误";
return "站距合格,靶纸正确";
};
export const getDistanceCheckText = (shootData) => {
const result = getShootValidation(shootData);
if (!result.targetDetected) return "未识别到靶纸,请瞄准靶纸射箭";
if (result.rawDistance < 0) return "靶纸错误";
return `${result.distanceOk ? "站距合格" : "站距过近"},${
result.targetOk ? "靶纸正确" : "靶纸错误"
}`;
};
// 正式射箭阶段的距离和靶纸异常类型选择对应语音。
export const getInvalidShotAudioKey = (shootData) => {
const result = getShootValidation(shootData);
if (!result.targetDetected) return "射箭无效,未识别到靶纸";
if (!result.distanceOk && !result.targetOk) {
return "射箭无效,距离不足且靶纸错误";
}
if (!result.targetOk) return "射箭无效,靶纸错误";
if (!result.distanceOk) return "射箭无效,距离不足";
return "射击无效";
};
export const getInvalidShotText = (shootData) =>
getInvalidShotAudioKey(shootData);
export const wxLogin = () => {
return new Promise((resolve, reject) => {
uni.login({
provider: "weixin",
success: resolve,
fail: (err) => {
uni.showToast({
title: "登录失败",
icon: "none",
});
console.error("登录失败:", err);
reject(err);
},
});
});
};
export const canEenter = (user, device, online, page = "") => {
const { miniProgram } = uni.getAccountInfoSync();
if (!device.deviceId) {
uni.showToast({
title: "请先绑定设备",
icon: "none",
});
return false;
}
if (miniProgram.envVersion !== "release") return true;
if (!online) {
uni.showToast({
title: "智能弓未连接",
icon: "none",
});
return false;
}
if (!user.trio && page !== "/pages/first-try") {
uni.showToast({
title: "请先完成新手试炼",
icon: "none",
});
return false;
}
return true;
};
export const capsuleHeight = uni.getMenuButtonBoundingClientRect().top - 9;