441 lines
13 KiB
JavaScript
441 lines
13 KiB
JavaScript
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;
|