diff --git a/.gitignore b/.gitignore
index c91affc..23a9575 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
.history
.github
+.claude
openspec
CLAUDE.md
docs
diff --git a/src/App.vue b/src/App.vue
index 08b356c..b62dd14 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -258,6 +258,69 @@
text-overflow: ellipsis;
}
+ .member-nickname {
+ position: relative;
+ display: inline-flex;
+ max-width: 100%;
+ overflow: hidden;
+ }
+
+ .member-nickname__text,
+ .member-nickname__shine {
+ display: block;
+ max-width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .member-nickname--vip .member-nickname__text {
+ color: #E7BA80;
+ }
+
+ .member-nickname--svip .member-nickname__text {
+ background: linear-gradient(90deg, #ffb86c, #ff4fd8, #7c5cff, #35d6ff);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ }
+
+ .member-nickname__shine {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 110deg,
+ transparent 0%,
+ transparent 38%,
+ rgba(255, 255, 255, 0.15) 45%,
+ rgba(255, 255, 255, 1) 50%,
+ rgba(255, 255, 255, 0.15) 55%,
+ transparent 62%,
+ transparent 100%
+ );
+ background-size: 220% 100%;
+ background-position: 120% 0;
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ pointer-events: none;
+ animation: memberNicknameShine 3.5s infinite ease-in-out;
+ }
+
+ @keyframes memberNicknameShine {
+ 0%,
+ 50% {
+ background-position: 120% 0;
+ }
+
+ 100% {
+ background-position: -200% 0;
+ }
+ }
+
.modal {
height: 100%;
display: flex;
diff --git a/src/apis.js b/src/apis.js
index a816bf7..de645e1 100644
--- a/src/apis.js
+++ b/src/apis.js
@@ -23,7 +23,10 @@ try {
console.error("获取环境信息失败,使用默认正式环境", e);
}
-function request(method, url, data = {}) {
+const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot");
+
+// 统一处理业务接口请求,包含登录态、业务错误和 WiFi 连接空响应兼容。
+function request(method, url, data = {}, baseUrl = BASE_URL) {
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
@@ -31,16 +34,21 @@ function request(method, url, data = {}) {
if (token) header.Authorization = `Bearer ${token || ""}`;
return new Promise((resolve, reject) => {
uni.request({
- url: `${BASE_URL}${url}`,
+ url: `${baseUrl}${url}`,
method,
header,
data,
timeout: 10000,
success: (res) => {
+ if (url === "/user/hardwareBox/connectWifi" && res.statusCode === 200 && res.data && Object.keys(res.data).length === 0) {
+ resolve({});
+ return;
+ }
if (res.data) {
const {code, data, message} = res.data;
if (code === 0) resolve(data);
else if (message) {
+ const error = {code, data, message};
if (message.indexOf("登录身份已失效") !== -1) {
console.log('1111111111111111111,token失效')
uni.removeStorageSync(
@@ -50,6 +58,10 @@ function request(method, url, data = {}) {
reject({ type: "AUTH_INVALID", message });
return;
}
+ if (message.indexOf("已达上限") !== -1) {
+ reject(error);
+ return;
+ }
if (message === "ROOM_FULL") {
resolve({full: true});
return;
@@ -97,8 +109,10 @@ function request(method, url, data = {}) {
title: message,
icon: "none",
});
+ reject(error);
+ return;
}
- reject("");
+ reject({code, data, message});
}
},
fail: (err) => {
@@ -167,6 +181,10 @@ export const getAppConfig = () => {
return request("GET", "/index/appConfig");
};
+export const getDailyCountAPI = () => {
+ return request("GET", "/index/dailyCount", {}, ADDONS_BASE_URL);
+};
+
export const getHomeData = (seasonId) => {
return request("GET", `/user/myHome?seasonId=${seasonId}`);
};
@@ -342,9 +360,23 @@ export const createOrderAPI = (vipId) => {
quanity: 1,
tradeType: "mini",
payType: "wxpay",
+ returnUrl: "",
+ remark: "",
+ mockTest: false,
});
};
+export const virtualPayOrderAPI = (vipId = 0, code = "") => {
+ return request("POST", "/user/virtualPay/createOrder", {
+ vipId,
+ code,
+ });
+};
+
+export const getOrderDetailAPI = (orderId) => {
+ return request("GET", `/user/order/detail?orderId=${encodeURIComponent(orderId)}`);
+};
+
export const payOrderAPI = (id) => {
return request("POST", "/user/order/pay", {
id,
@@ -457,6 +489,26 @@ export const getDeviceBatteryAPI = async () => {
return request("GET", "/user/device/battery");
};
+// 设备连接指定 WiFi,只下发 WiFi 凭证,不触发 OTA 升级。
+export const connectDeviceWifiAPI = async (ssid, password) => {
+ return request("POST", "/user/hardwareBox/connectWifi", {ssid, password});
+};
+
+// 获取硬件盒子版本信息,用于判断当前设备是否需要 OTA 升级。
+export const getHardwareBoxVersionAPI = async () => {
+ return request("GET", "/user/hardwareBox/version");
+};
+
+// 发送硬件盒子 OTA 更新指令,服务端会返回后续轮询使用的任务 ID。
+export const sendHardwareBoxUpdateAPI = async (data) => {
+ return request("POST", "/user/hardwareBox/sendUpdate", data);
+};
+
+// 根据任务 ID 获取硬件盒子 OTA 更新状态。
+export const getHardwareBoxTaskStatusAPI = async (taskId) => {
+ return request("GET", `/user/hardwareBox/taskStatus?taskId=${taskId}`);
+};
+
export const addNoteAPI = async (id, remark) => {
return request("POST", "/user/score/sheet/remark", {id, remark});
};
diff --git a/src/audioManager.js b/src/audioManager.js
index a0f55bb..156553c 100644
--- a/src/audioManager.js
+++ b/src/audioManager.js
@@ -40,6 +40,8 @@ export const audioFils = {
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya59b6pu0ur4um.mp3",
比赛开始:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcuu5z3a3lumkutske.mp3",
+ 下半场开始:
+ "https://static.shelingxingqiu.com/shootmini/static/audio/%E4%B8%8B%E5%8D%8A%E5%9C%BA%E5%BC%80%E5%A7%8B.mp3",
请开始射击:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrl5u0iromqhf.mp3",
射击无效:
@@ -131,6 +133,10 @@ class AudioManager {
this.lastPlayKey = null;
this.lastPlayAt = 0;
this.isInterrupted = false;
+ this.interruptedAt = 0;
+ this.interruptionFallbackMs = 5000;
+ this.playWatchdogMs = 8000;
+ this.playWatchdogTimers = new Map();
// 静音开关
this.isMuted = false;
@@ -157,6 +163,7 @@ class AudioManager {
const begin = () => {
if (this.isInterrupted) return;
this.isInterrupted = true;
+ this.interruptedAt = Date.now();
this.stopAll();
this.isSequenceRunning = false;
this.sequenceQueue = [];
@@ -168,6 +175,7 @@ class AudioManager {
const end = () => {
if (!this.isInterrupted) return;
this.isInterrupted = false;
+ this.interruptedAt = 0;
uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
void this.reloadAll();
};
@@ -350,9 +358,14 @@ class AudioManager {
const loadTimeout = setTimeout(() => {
debugLog(`音频 ${key} 加载超时`);
this.recordLoadFailure(key);
+ this.audioMap.delete(key);
try {
audio.destroy();
} catch (_) {}
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
if (callback) callback();
}, 10000);
@@ -386,7 +399,13 @@ class AudioManager {
}
this.recordLoadFailure(key);
this.audioMap.delete(key);
- audio.destroy();
+ try {
+ audio.destroy();
+ } catch (_) {}
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
if (this.readyMap.get(key)) {
// 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载
// this.readyMap.set(key, false);
@@ -396,19 +415,14 @@ class AudioManager {
});
audio.onEnded(() => {
- if (this.currentPlayingKey === key) {
- this.currentPlayingKey = null;
- }
- this.allowPlayMap.set(key, false);
- this.onAudioEnded(key);
- uni.$emit('audioEnded', key);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
});
audio.onStop(() => {
- if (this.currentPlayingKey === key) {
- this.currentPlayingKey = null;
- }
- this.allowPlayMap.set(key, false);
+ this.finishPlayback(key);
});
this.audioMap.set(key, audio);
@@ -446,11 +460,19 @@ class AudioManager {
});
} else {
this.recordLoadFailure(key);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
if (callback) callback();
}
},
fail: () => {
this.recordLoadFailure(key);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
if (callback) callback();
},
});
@@ -487,15 +509,137 @@ class AudioManager {
this.failedLoadKeys.add(key);
}
+ clearPlayWatchdog(key) {
+ const timer = this.playWatchdogTimers.get(key);
+ if (timer) {
+ clearTimeout(timer);
+ this.playWatchdogTimers.delete(key);
+ }
+ }
+
+ clearAllPlayWatchdogs() {
+ for (const timer of this.playWatchdogTimers.values()) {
+ clearTimeout(timer);
+ }
+ this.playWatchdogTimers.clear();
+ }
+
+ startPlayWatchdog(key) {
+ this.clearPlayWatchdog(key);
+ const timer = setTimeout(() => {
+ if (this.currentPlayingKey !== key) return;
+ debugLog(`音频 ${key} 播放超时,跳过当前音频并继续队列`);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
+ this.reloadAudioKey(key);
+ }, this.playWatchdogMs);
+ this.playWatchdogTimers.set(key, timer);
+ }
+
+ finishPlayback(key, { advanceSequence = false, emitEnded = false, force = false } = {}) {
+ const wasCurrent = this.currentPlayingKey === key;
+ const isSequenceCurrent =
+ this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key;
+
+ this.clearPlayWatchdog(key);
+ this.allowPlayMap.set(key, false);
+
+ if (!force && !wasCurrent && !isSequenceCurrent) return false;
+
+ if (wasCurrent) {
+ this.currentPlayingKey = null;
+ }
+
+ if (advanceSequence && isSequenceCurrent) {
+ this.onAudioEnded(key);
+ }
+
+ if (emitEnded) {
+ uni.$emit("audioEnded", key);
+ }
+
+ return true;
+ }
+
+ recoverFromInterruptionIfStale(force = false) {
+ if (!this.isInterrupted) return false;
+ const interruptedFor = Date.now() - (this.interruptedAt || Date.now());
+ if (!force && interruptedFor < this.interruptionFallbackMs) return false;
+
+ debugLog("音频中断状态超时,执行兜底恢复");
+ this.isInterrupted = false;
+ this.interruptedAt = 0;
+ uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
+ void this.reloadAll();
+ return true;
+ }
+
+ recoverIfStale(expectedKey) {
+ if (this.recoverFromInterruptionIfStale(true)) return;
+
+ const key =
+ expectedKey || this.currentPlayingKey || this.sequenceQueue[this.sequenceIndex];
+ if (!key) {
+ if (this.isSequenceRunning) {
+ this.sequenceQueue = [];
+ this.sequenceIndex = 0;
+ this.isSequenceRunning = false;
+ }
+ return;
+ }
+
+ const isStaleCurrent =
+ this.currentPlayingKey === key ||
+ (this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key);
+ if (!isStaleCurrent) return;
+
+ debugLog(`音频 ${key} 等待超时,执行轻量恢复`);
+ const audio = this.audioMap.get(key);
+ if (audio) {
+ try {
+ audio.stop();
+ } catch (_) {}
+ }
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
+ this.reloadAudioKey(key);
+ }
+
+ reloadAudioKey(key) {
+ const audio = this.audioMap.get(key);
+ if (audio) {
+ try {
+ audio.destroy();
+ } catch (_) {}
+ this.audioMap.delete(key);
+ }
+ this.readyMap.set(key, false);
+ this.retryLoadAudio(key);
+ }
+
// 重新加载音频
retryLoadAudio(key) {
+ this.clearPlayWatchdog(key);
const oldAudio = this.audioMap.get(key);
- if (oldAudio) oldAudio.destroy();
+ if (oldAudio) {
+ try {
+ oldAudio.destroy();
+ } catch (_) {}
+ }
this.createAudio(key);
}
// 播放指定音频或音频数组(数组则按顺序连续播放)
play(input, interrupt = true) {
+ if (this.isInterrupted) {
+ this.recoverFromInterruptionIfStale();
+ }
if (this.isInterrupted) {
debugLog("音频处理中断状态,忽略播放请求");
return;
@@ -553,6 +697,9 @@ class AudioManager {
// 内部方法:播放单个 key
_playSingle(key, forceStopAll = false) {
+ if (this.isInterrupted) {
+ this.recoverFromInterruptionIfStale();
+ }
if (this.isInterrupted) {
debugLog(`音频处理中断状态,跳过播放: ${key}`);
return;
@@ -561,6 +708,11 @@ class AudioManager {
const now = Date.now();
if (this.lastPlayKey === key && now - this.lastPlayAt < 250) {
debugLog(`忽略快速重复播放: ${key}`);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
return;
}
@@ -603,21 +755,34 @@ class AudioManager {
try {
audio.play();
} catch (err) {
- this.allowPlayMap.set(key, false);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
debugLog(`音频 ${key} 播放调用失败`, err?.errMsg || err);
return;
}
this.currentPlayingKey = key;
this.lastPlayKey = key;
this.lastPlayAt = Date.now();
+ this.startPlayWatchdog(key);
} else {
debugLog(`音频 ${key} 不存在,尝试重新加载...`);
this.retryLoadAudio(key);
+ let loadWaitTimer = null;
+ const cleanup = () => {
+ try {
+ uni.$off("audioLoaded", handler);
+ } catch (_) {}
+ if (loadWaitTimer) {
+ clearTimeout(loadWaitTimer);
+ loadWaitTimer = null;
+ }
+ };
const handler = (loadedKey) => {
if (loadedKey === key) {
- try {
- uni.$off("audioLoaded", handler);
- } catch (_) {}
+ cleanup();
// 再次校验是否存在且就绪
const a = this.audioMap.get(key);
if (a && this.readyMap.get(key)) {
@@ -628,6 +793,7 @@ class AudioManager {
try {
uni.$on("audioLoaded", handler);
} catch (_) {}
+ loadWaitTimer = setTimeout(cleanup, 12000);
}
}
@@ -653,6 +819,7 @@ class AudioManager {
// 停止指定音频
stop(key) {
const audio = this.audioMap.get(key);
+ this.clearPlayWatchdog(key);
if (audio) {
audio.stop();
this.allowPlayMap.set(key, false);
@@ -664,6 +831,7 @@ class AudioManager {
// 停止所有音频
stopAll() {
+ this.clearAllPlayWatchdogs();
for (const [k, audio] of this.audioMap.entries()) {
try {
audio.stop();
@@ -737,6 +905,7 @@ class AudioManager {
this.readyMap.clear();
this.failedLoadKeys.clear();
this.allowPlayMap.clear();
+ this.clearAllPlayWatchdogs();
this.currentPlayingKey = null;
this.sequenceQueue = [];
this.sequenceIndex = 0;
diff --git a/src/canvas.js b/src/canvas.js
index e5e41f4..7c5b66b 100644
--- a/src/canvas.js
+++ b/src/canvas.js
@@ -456,23 +456,29 @@ export const generateShareImage = async (canvasId, data) => {
// 2D 即时绘制,无需 ctx.draw()
} catch (e) {
console.error("generateShareImage 绘制失败:", e);
+ throw e;
}
};
// 顶部导入与工具方法
async function getCanvas2DContext(canvasId, targetWidth, targetHeight) {
- return new Promise((resolve) => {
+ return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery();
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
- const { node: canvas } = res[0] || {};
+ const canvasInfo = res && res[0];
+ const { node: canvas } = canvasInfo || {};
+ if (!canvas || typeof canvas.getContext !== "function") {
+ reject(new Error(`canvas ${canvasId} not found`));
+ return;
+ }
const ctx = canvas.getContext("2d");
const dpr = uni.getSystemInfoSync().pixelRatio || 1;
- const w = targetWidth || res[0].width;
- const h = targetHeight || res[0].height;
+ const w = targetWidth || canvasInfo.width;
+ const h = targetHeight || canvasInfo.height;
canvas.width = w * dpr;
canvas.height = h * dpr;
@@ -561,6 +567,7 @@ export const sharePointData = async (canvasId, data) => {
// 2D 即时绘制,无需 ctx.draw()
} catch (e) {
console.error("generateShareImage 绘制失败:", e);
+ throw e;
}
};
@@ -778,6 +785,7 @@ export async function sharePractiseData(canvasId, type, user, data) {
// 2D 模式下无需 ctx.draw()
} catch (err) {
console.log(err);
+ throw err;
}
}
diff --git a/src/components/AppBackground.vue b/src/components/AppBackground.vue
index 134b66e..00feb51 100644
--- a/src/components/AppBackground.vue
+++ b/src/components/AppBackground.vue
@@ -57,6 +57,12 @@ const props = defineProps({
src="https://static.shelingxingqiu.com/shootmini/static/rank/rank-bg.png"
mode="widthFix"
/>
+
diff --git a/src/components/AppFooter.vue b/src/components/AppFooter.vue
index 05f2ebe..bfe2831 100644
--- a/src/components/AppFooter.vue
+++ b/src/components/AppFooter.vue
@@ -8,7 +8,7 @@ const tabs = [
function handleTabClick(index) {
if (index === 0) {
uni.navigateTo({
- url: "/pages/be-vip",
+ url: "/pages/member/be-vip",
});
}
if (index === 1) {
diff --git a/src/components/BackToGame.vue b/src/components/BackToGame.vue
index b5a509c..5297d74 100644
--- a/src/components/BackToGame.vue
+++ b/src/components/BackToGame.vue
@@ -18,12 +18,14 @@ const props = defineProps({
},
});
const loading = ref(false);
+const navigating = ref(false);
/** 统一获取当前环境 token,用于守卫:无有效 token 时不发起接口请求 */
const getToken = () =>
uni.getStorageSync(`${uni.getAccountInfoSync().miniProgram.envVersion}_token`);
onShow(async () => {
+ navigating.value = false;
if (user.value.id && getToken()) {
setTimeout(async () => {
const state = await getUserGameState();
@@ -45,28 +47,35 @@ watch(
}
);
+const navigateOnce = (url) =>
+ new Promise((resolve, reject) => {
+ navigating.value = true;
+ uni.navigateTo({
+ url,
+ success: resolve,
+ fail: (error) => {
+ navigating.value = false;
+ reject(error);
+ },
+ });
+ });
+
const onClick = debounce(async () => {
- if (loading.value) return;
+ if (loading.value || navigating.value) return;
try {
loading.value = true;
const result = await getBattleAPI();
if (result && result.matchId) {
await uni.$checkAudio();
if (result.mode <= 3) {
- uni.navigateTo({
- url: `/pages/team-battle/index?battleId=${result.matchId}`,
- });
+ await navigateOnce(`/pages/team-battle/index?battleId=${result.matchId}`);
} else {
- uni.navigateTo({
- url: `/pages/melee-battle?battleId=${result.matchId}`,
- });
+ await navigateOnce(`/pages/melee-battle?battleId=${result.matchId}`);
}
return;
}
if (game.value.roomID) {
- uni.navigateTo({
- url: "/pages/battle-room?roomNumber=" + game.value.roomID,
- });
+ await navigateOnce("/pages/battle-room?roomNumber=" + game.value.roomID);
} else {
updateGame(false, "");
}
diff --git a/src/components/BattleHeader.vue b/src/components/BattleHeader.vue
index b12d84f..b5cafcf 100644
--- a/src/components/BattleHeader.vue
+++ b/src/components/BattleHeader.vue
@@ -27,6 +27,14 @@ defineProps({
default: true,
},
});
+
+const getMemberNicknameClass = (player = {}) => [
+ "member-nickname",
+ player.vip === true && player.sVip !== true ? "member-nickname--vip" : "",
+ player.sVip === true ? "member-nickname--svip" : "",
+];
+
+const isMember = (player = {}) => player.vip === true || player.sVip === true;
@@ -51,7 +59,16 @@ defineProps({
}"
>
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
@@ -172,7 +207,7 @@ defineProps({
justify-content: center;
color: #fff9;
font-size: 12px;
- padding-top: 7px;
+ /* padding-top: 7px; */
flex: 0 0 auto;
}
.player-name {
@@ -183,6 +218,13 @@ defineProps({
text-overflow: ellipsis;
text-align: center;
}
+view.player-name {
+ justify-content: center;
+}
+.player-name .member-nickname__text,
+.player-name .member-nickname__shine {
+ font-size: 12px;
+}
.left-winner-badge {
position: absolute;
width: 50px;
diff --git a/src/components/BowData.vue b/src/components/BowData.vue
index 7854645..409f98f 100644
--- a/src/components/BowData.vue
+++ b/src/components/BowData.vue
@@ -1,4 +1,5 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/BowTarget.vue b/src/components/BowTarget.vue
index 1a26f5a..997daf7 100644
--- a/src/components/BowTarget.vue
+++ b/src/components/BowTarget.vue
@@ -1,6 +1,15 @@
-
+
-
+
@@ -292,8 +455,15 @@ onBeforeUnmount(() => {
}}环
+
{
>
+
{
{{ index + 1 }}
+
-
+