9 Commits

18 changed files with 385 additions and 544 deletions

View File

@@ -22,7 +22,8 @@
const { const {
updateUser, updateUser,
updateOnline, updateOnline,
clearSessionState clearSessionState,
clearDevice
} = store; } = store;
watch( watch(
@@ -63,6 +64,11 @@
updateOnline(data.online); updateOnline(data.online);
} }
function onDeviceBindInvalid() {
clearDevice();
uni.setStorageSync("calibration", false);
}
function onDeviceShoot() { function onDeviceShoot() {
// audioManager.play("射箭声音") // audioManager.play("射箭声音")
} }
@@ -78,6 +84,7 @@
uni.$on("update-user", emitUpdateUser); uni.$on("update-user", emitUpdateUser);
uni.$on("update-online", emitUpdateOnline); uni.$on("update-online", emitUpdateOnline);
uni.$on("session-kicked-out", onSessionKickedOut); uni.$on("session-kicked-out", onSessionKickedOut);
uni.$on("device-bind-invalid", onDeviceBindInvalid);
const token = uni.getStorageSync( const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token` `${uni.getAccountInfoSync().miniProgram.envVersion}_token`
); );
@@ -91,6 +98,7 @@
uni.$off("update-user", emitUpdateUser); uni.$off("update-user", emitUpdateUser);
uni.$off("update-online", emitUpdateOnline); uni.$off("update-online", emitUpdateOnline);
uni.$off("session-kicked-out", onSessionKickedOut); uni.$off("session-kicked-out", onSessionKickedOut);
uni.$off("device-bind-invalid", onDeviceBindInvalid);
websocket.closeWebSocket(); websocket.closeWebSocket();
}); });
</script> </script>

View File

@@ -70,6 +70,15 @@ function request(method, url, data = {}) {
resolve({binded: true}); resolve({binded: true});
return; return;
} }
if (message === "BIND_FAILD") {
uni.$emit("device-bind-invalid");
uni.showToast({
title: "设备绑定状态已失效,请重新绑定",
icon: "none",
});
reject({type: "DEVICE_BIND_INVALID", message});
return;
}
if (message === "ERROR_ORDER_UNPAY") { if (message === "ERROR_ORDER_UNPAY") {
uni.showToast({ uni.showToast({
title: "当前有未支付订单", title: "当前有未支付订单",

View File

@@ -131,6 +131,10 @@ class AudioManager {
this.lastPlayKey = null; this.lastPlayKey = null;
this.lastPlayAt = 0; this.lastPlayAt = 0;
this.isInterrupted = false; this.isInterrupted = false;
this.interruptedAt = 0;
this.interruptionFallbackMs = 5000;
this.playWatchdogMs = 8000;
this.playWatchdogTimers = new Map();
// 静音开关 // 静音开关
this.isMuted = false; this.isMuted = false;
@@ -157,6 +161,7 @@ class AudioManager {
const begin = () => { const begin = () => {
if (this.isInterrupted) return; if (this.isInterrupted) return;
this.isInterrupted = true; this.isInterrupted = true;
this.interruptedAt = Date.now();
this.stopAll(); this.stopAll();
this.isSequenceRunning = false; this.isSequenceRunning = false;
this.sequenceQueue = []; this.sequenceQueue = [];
@@ -168,6 +173,7 @@ class AudioManager {
const end = () => { const end = () => {
if (!this.isInterrupted) return; if (!this.isInterrupted) return;
this.isInterrupted = false; this.isInterrupted = false;
this.interruptedAt = 0;
uni.$emit(AUDIO_INTERRUPTION_END_EVENT); uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
void this.reloadAll(); void this.reloadAll();
}; };
@@ -350,9 +356,14 @@ class AudioManager {
const loadTimeout = setTimeout(() => { const loadTimeout = setTimeout(() => {
debugLog(`音频 ${key} 加载超时`); debugLog(`音频 ${key} 加载超时`);
this.recordLoadFailure(key); this.recordLoadFailure(key);
this.audioMap.delete(key);
try { try {
audio.destroy(); audio.destroy();
} catch (_) {} } catch (_) {}
this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
});
if (callback) callback(); if (callback) callback();
}, 10000); }, 10000);
@@ -386,7 +397,13 @@ class AudioManager {
} }
this.recordLoadFailure(key); this.recordLoadFailure(key);
this.audioMap.delete(key); this.audioMap.delete(key);
try {
audio.destroy(); audio.destroy();
} catch (_) {}
this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
});
if (this.readyMap.get(key)) { if (this.readyMap.get(key)) {
// 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载 // 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载
// this.readyMap.set(key, false); // this.readyMap.set(key, false);
@@ -396,19 +413,14 @@ class AudioManager {
}); });
audio.onEnded(() => { audio.onEnded(() => {
if (this.currentPlayingKey === key) { this.finishPlayback(key, {
this.currentPlayingKey = null; advanceSequence: true,
} emitEnded: true,
this.allowPlayMap.set(key, false); });
this.onAudioEnded(key);
uni.$emit('audioEnded', key);
}); });
audio.onStop(() => { audio.onStop(() => {
if (this.currentPlayingKey === key) { this.finishPlayback(key);
this.currentPlayingKey = null;
}
this.allowPlayMap.set(key, false);
}); });
this.audioMap.set(key, audio); this.audioMap.set(key, audio);
@@ -446,11 +458,19 @@ class AudioManager {
}); });
} else { } else {
this.recordLoadFailure(key); this.recordLoadFailure(key);
this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
});
if (callback) callback(); if (callback) callback();
} }
}, },
fail: () => { fail: () => {
this.recordLoadFailure(key); this.recordLoadFailure(key);
this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
});
if (callback) callback(); if (callback) callback();
}, },
}); });
@@ -487,15 +507,137 @@ class AudioManager {
this.failedLoadKeys.add(key); 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) { retryLoadAudio(key) {
this.clearPlayWatchdog(key);
const oldAudio = this.audioMap.get(key); const oldAudio = this.audioMap.get(key);
if (oldAudio) oldAudio.destroy(); if (oldAudio) {
try {
oldAudio.destroy();
} catch (_) {}
}
this.createAudio(key); this.createAudio(key);
} }
// 播放指定音频或音频数组(数组则按顺序连续播放) // 播放指定音频或音频数组(数组则按顺序连续播放)
play(input, interrupt = true) { play(input, interrupt = true) {
if (this.isInterrupted) {
this.recoverFromInterruptionIfStale();
}
if (this.isInterrupted) { if (this.isInterrupted) {
debugLog("音频处理中断状态,忽略播放请求"); debugLog("音频处理中断状态,忽略播放请求");
return; return;
@@ -553,6 +695,9 @@ class AudioManager {
// 内部方法:播放单个 key // 内部方法:播放单个 key
_playSingle(key, forceStopAll = false) { _playSingle(key, forceStopAll = false) {
if (this.isInterrupted) {
this.recoverFromInterruptionIfStale();
}
if (this.isInterrupted) { if (this.isInterrupted) {
debugLog(`音频处理中断状态,跳过播放: ${key}`); debugLog(`音频处理中断状态,跳过播放: ${key}`);
return; return;
@@ -561,6 +706,11 @@ class AudioManager {
const now = Date.now(); const now = Date.now();
if (this.lastPlayKey === key && now - this.lastPlayAt < 250) { if (this.lastPlayKey === key && now - this.lastPlayAt < 250) {
debugLog(`忽略快速重复播放: ${key}`); debugLog(`忽略快速重复播放: ${key}`);
this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
force: true,
});
return; return;
} }
@@ -603,21 +753,34 @@ class AudioManager {
try { try {
audio.play(); audio.play();
} catch (err) { } catch (err) {
this.allowPlayMap.set(key, false); this.finishPlayback(key, {
advanceSequence: true,
emitEnded: true,
force: true,
});
debugLog(`音频 ${key} 播放调用失败`, err?.errMsg || err); debugLog(`音频 ${key} 播放调用失败`, err?.errMsg || err);
return; return;
} }
this.currentPlayingKey = key; this.currentPlayingKey = key;
this.lastPlayKey = key; this.lastPlayKey = key;
this.lastPlayAt = Date.now(); this.lastPlayAt = Date.now();
this.startPlayWatchdog(key);
} else { } else {
debugLog(`音频 ${key} 不存在,尝试重新加载...`); debugLog(`音频 ${key} 不存在,尝试重新加载...`);
this.retryLoadAudio(key); this.retryLoadAudio(key);
const handler = (loadedKey) => { let loadWaitTimer = null;
if (loadedKey === key) { const cleanup = () => {
try { try {
uni.$off("audioLoaded", handler); uni.$off("audioLoaded", handler);
} catch (_) {} } catch (_) {}
if (loadWaitTimer) {
clearTimeout(loadWaitTimer);
loadWaitTimer = null;
}
};
const handler = (loadedKey) => {
if (loadedKey === key) {
cleanup();
// 再次校验是否存在且就绪 // 再次校验是否存在且就绪
const a = this.audioMap.get(key); const a = this.audioMap.get(key);
if (a && this.readyMap.get(key)) { if (a && this.readyMap.get(key)) {
@@ -628,6 +791,7 @@ class AudioManager {
try { try {
uni.$on("audioLoaded", handler); uni.$on("audioLoaded", handler);
} catch (_) {} } catch (_) {}
loadWaitTimer = setTimeout(cleanup, 12000);
} }
} }
@@ -653,6 +817,7 @@ class AudioManager {
// 停止指定音频 // 停止指定音频
stop(key) { stop(key) {
const audio = this.audioMap.get(key); const audio = this.audioMap.get(key);
this.clearPlayWatchdog(key);
if (audio) { if (audio) {
audio.stop(); audio.stop();
this.allowPlayMap.set(key, false); this.allowPlayMap.set(key, false);
@@ -664,6 +829,7 @@ class AudioManager {
// 停止所有音频 // 停止所有音频
stopAll() { stopAll() {
this.clearAllPlayWatchdogs();
for (const [k, audio] of this.audioMap.entries()) { for (const [k, audio] of this.audioMap.entries()) {
try { try {
audio.stop(); audio.stop();
@@ -737,6 +903,7 @@ class AudioManager {
this.readyMap.clear(); this.readyMap.clear();
this.failedLoadKeys.clear(); this.failedLoadKeys.clear();
this.allowPlayMap.clear(); this.allowPlayMap.clear();
this.clearAllPlayWatchdogs();
this.currentPlayingKey = null; this.currentPlayingKey = null;
this.sequenceQueue = []; this.sequenceQueue = [];
this.sequenceIndex = 0; this.sequenceIndex = 0;

View File

@@ -16,6 +16,11 @@ const props = defineProps({
const rowCount = new Array(6).fill(0); const rowCount = new Array(6).fill(0);
const getRingText = (arrow) => {
if (!arrow) return "-";
if (arrow.ringX && arrow.ring) return "X环";
return arrow.ring ? `${arrow.ring}` : "-";
};
const isMember = (player = {}) => player.vip === true || player.sVip === true; const isMember = (player = {}) => player.vip === true || player.sVip === true;
const getMemberNicknameClass = (player = {}) => [ const getMemberNicknameClass = (player = {}) => [
@@ -52,23 +57,19 @@ const getMemberNicknameClass = (player = {}) => [
<view> <view>
<view> <view>
<view v-for="(_, index) in rowCount" :key="index"> <view v-for="(_, index) in rowCount" :key="index">
<text>{{ <text>{{ getRingText(scores[0]?.[index]) }}</text>
scores[0] && scores[0][index] ? `${scores[0][index].ring}` : "-"
}}</text>
</view> </view>
</view> </view>
<view> <view>
<view v-for="(_, index) in rowCount" :key="index"> <view v-for="(_, index) in rowCount" :key="index">
<text>{{ <text>{{ getRingText(scores[1]?.[index]) }}</text>
scores[1] && scores[1][index] ? `${scores[1][index].ring}` : "-"
}}</text>
</view> </view>
</view> </view>
</view> </view>
<text <text
>{{ >{{
scores scores
.map((s) => s.reduce((last, next) => last + next.ring, 0)) .map((s) => (s || []).reduce((last, next) => last + next.ring, 0))
.reduce((last, next) => last + next, 0) .reduce((last, next) => last + next, 0)
}}</text }}</text
> >

View File

@@ -16,7 +16,7 @@ import {
import useStore from "@/store"; import useStore from "@/store";
const store = useStore(); const store = useStore();
const { updateUser, updateDevice, updateOnline } = store; const { updateUser, updateDevice, updateOnline, clearDevice } = store;
const props = defineProps({ const props = defineProps({
show: { show: {
@@ -122,6 +122,8 @@ async function doLogin() {
); );
const data = await getDeviceBatteryAPI(); const data = await getDeviceBatteryAPI();
updateOnline(data.online); updateOnline(data.online);
} else {
clearDevice();
} }
props.onClose(); props.onClose();
} catch (error) { } catch (error) {

View File

@@ -81,9 +81,6 @@
{ {
"path": "pages/member/vip-intro" "path": "pages/member/vip-intro"
}, },
{
"path": "pages/member/agreement"
},
{ {
"path": "pages/grade-intro" "path": "pages/grade-intro"
}, },

View File

@@ -315,7 +315,7 @@ function goBack() {
<Container <Container
:bgType="data.mode > 3 ? -1 : 0" :bgType="data.mode > 3 ? -1 : 0"
bgColor="#000000" bgColor="#000000"
:onBack="goBack" :onBack="exit"
> >
<!-- ----- Banner game 胜负展示图 NvN 对抗模式----- --> <!-- ----- Banner game 胜负展示图 NvN 对抗模式----- -->

View File

@@ -26,6 +26,7 @@ const {
updateConfig, updateConfig,
updateUser, updateUser,
updateDevice, updateDevice,
clearDevice,
getLvlName, getLvlName,
getLvlNameByScore, getLvlNameByScore,
updateOnline, updateOnline,
@@ -127,6 +128,8 @@ onShow(async () => {
); );
const data = await getDeviceBatteryAPI(); const data = await getDeviceBatteryAPI();
updateOnline(data.online); updateOnline(data.online);
} else {
clearDevice();
} }
} }
} }

View File

@@ -30,11 +30,64 @@ const playersSorted = ref([]);
const playersScores = ref([]); const playersScores = ref([]);
const halfTimeTip = ref(false); const halfTimeTip = ref(false);
const halfRest = ref(false); const halfRest = ref(false);
const HALF_REST_SECONDS = 20;
const halfRestRemain = ref(HALF_REST_SECONDS);
let halfRestTimer = null;
/** 控制设备离线提示弹窗的显示状态 */ /** 控制设备离线提示弹窗的显示状态 */
const showOfflineModal = ref(false); const showOfflineModal = ref(false);
/** 记录每位玩家当前半场连续 X 环key 为 playerId用于触发 tententen 音效 */ /** 记录每位玩家当前半场连续 10 环及以上次key 为 playerId用于触发 tententen 音效 */
const xRingStreaks = ref({}); const xRingStreaks = ref({});
function clearHalfRestCountdown() {
if (halfRestTimer) {
clearInterval(halfRestTimer);
halfRestTimer = null;
}
}
function getHalfRestSeconds(battleInfo) {
const remainCandidates = [
battleInfo?.halfRestRemain,
battleInfo?.halfRestRemainSeconds,
battleInfo?.restRemain,
battleInfo?.restRemainSeconds,
];
for (const item of remainCandidates) {
const remain = Number(item);
if (Number.isFinite(remain) && remain > 0 && remain <= HALF_REST_SECONDS) {
return Math.ceil(remain);
}
}
const endTime = Number(battleInfo?.halfRestEndTime ?? battleInfo?.restEndTime);
if (!Number.isFinite(endTime) || endTime <= 0) return HALF_REST_SECONDS;
const timestamp = endTime < 1e12 ? endTime * 1000 : endTime;
const diffSeconds = (timestamp - Date.now()) / 1000;
if (diffSeconds > 0 && diffSeconds <= HALF_REST_SECONDS) {
return Math.ceil(diffSeconds);
}
return HALF_REST_SECONDS;
}
function startHalfRestCountdown(seconds = HALF_REST_SECONDS) {
clearHalfRestCountdown();
halfRestRemain.value = Math.max(0, Math.ceil(Number(seconds) || HALF_REST_SECONDS));
if (halfRestRemain.value <= 0) return;
halfRestTimer = setInterval(() => {
if (halfRestRemain.value <= 1) {
halfRestRemain.value = 0;
clearHalfRestCountdown();
return;
}
halfRestRemain.value -= 1;
}, 1000);
}
const currentPlayer = computed(() => const currentPlayer = computed(() =>
players.value.find((player) => String(player?.id) === String(user.value.id)) players.value.find((player) => String(player?.id) === String(user.value.id))
); );
@@ -96,8 +149,7 @@ function recoverData(battleInfo, { force = false } = {}) {
halfTimeTip.value = true; halfTimeTip.value = true;
halfRest.value = true; halfRest.value = true;
tips.value = "准备下半场"; tips.value = "准备下半场";
// 剩余休息时间 startHalfRestCountdown(getHalfRestSeconds(battleInfo));
// const remain = (Date.now() - battleInfo.timeoutTime) / 1000;
setTimeout(() => { setTimeout(() => {
uni.$emit("update-remain", 0); uni.$emit("update-remain", 0);
}, 200); }, 200);
@@ -128,23 +180,27 @@ onLoad(async (options) => {
}); });
/** /**
* 检测指定玩家连续 X 环是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效 * 检测指定玩家连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
* @param {number|string} playerId - 本次射手的 ID大乱斗中 ShootResult 保留 playerId * @param {number|string} playerId - 本次射手的 ID大乱斗中 ShootResult 保留 playerId
* @param {boolean} isXRing - 本次射击是否为 X 环 * @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
*/ */
function checkAndPlayTententen(playerId, isXRing) { function isTenPlusRing(shot) {
return !!(shot?.ringX || Number(shot?.ring) >= 10);
}
function checkAndPlayTententen(playerId, isTenPlusRingShot) {
if (!playerId) return; if (!playerId) return;
const id = parseInt(playerId); const id = parseInt(playerId);
if (isXRing) { if (isTenPlusRingShot) {
xRingStreaks.value[id] = (xRingStreaks.value[id] || 0) + 1; xRingStreaks.value[id] = (xRingStreaks.value[id] || 0) + 1;
// 同一玩家连续 3 箭均为 X 环,追加到环数音效队列尾部播放 // 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
if (xRingStreaks.value[id] >= 3) { if (xRingStreaks.value[id] >= 3) {
xRingStreaks.value[id] = 0; xRingStreaks.value[id] = 0;
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen避免播放顺序颠倒 // nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen避免播放顺序颠倒
nextTick(() => audioManager.play("tententen", false)); nextTick(() => audioManager.play("tententen", false));
} }
} else { } else {
// 非 X 环则重置该玩家的连续计数 // 低于 10 环或未上靶则重置该玩家的连续计数
xRingStreaks.value[id] = 0; xRingStreaks.value[id] = 0;
} }
} }
@@ -152,6 +208,7 @@ function checkAndPlayTententen(playerId, isXRing) {
async function onReceiveMessage(msg) { async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return; if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.BattleStart) { if (msg.type === MESSAGETYPESV2.BattleStart) {
clearHalfRestCountdown();
halfTimeTip.value = false; halfTimeTip.value = false;
halfRest.value = false; halfRest.value = false;
recoverData(msg); recoverData(msg);
@@ -166,22 +223,23 @@ async function onReceiveMessage(msg) {
// 对比更新后数据找出箭数增加的玩家(即本次射手),并读取其最新箭的 ring 数据 // 对比更新后数据找出箭数增加的玩家(即本次射手),并读取其最新箭的 ring 数据
const newRound = playersScores.value[playersScores.value.length - 1] || {}; const newRound = playersScores.value[playersScores.value.length - 1] || {};
let shooterId = null; let shooterId = null;
let isXRing = false; let isTenPlusRingShot = false;
for (const pid of Object.keys(newRound)) { for (const pid of Object.keys(newRound)) {
const newLen = (newRound[pid] || []).length; const newLen = (newRound[pid] || []).length;
if (newLen > (prevCounts[pid] || 0)) { if (newLen > (prevCounts[pid] || 0)) {
shooterId = parseInt(pid); shooterId = parseInt(pid);
const shot = newRound[pid][newLen - 1]; const shot = newRound[pid][newLen - 1];
isXRing = !!(shot?.ringX && shot?.ring); isTenPlusRingShot = isTenPlusRing(shot);
break; break;
} }
} }
// 检测同一玩家三箭全 X 环,触发 tententen 音效 // 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
checkAndPlayTententen(shooterId, isXRing); checkAndPlayTententen(shooterId, isTenPlusRingShot);
} else if (msg.type === MESSAGETYPESV2.HalfRest) { } else if (msg.type === MESSAGETYPESV2.HalfRest) {
halfTimeTip.value = true; halfTimeTip.value = true;
halfRest.value = true; halfRest.value = true;
tips.value = "准备下半场"; tips.value = "准备下半场";
startHalfRestCountdown();
} else if (msg.type === MESSAGETYPESV2.BattleEnd) { } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(() => { setTimeout(() => {
// 全部跳转到新结算页 // 全部跳转到新结算页
@@ -202,6 +260,7 @@ onBeforeUnmount(() => {
uni.setKeepScreenOn({ uni.setKeepScreenOn({
keepScreenOn: false, keepScreenOn: false,
}); });
clearHalfRestCountdown();
uni.$off("socket-inbox", onReceiveMessage); uni.$off("socket-inbox", onReceiveMessage);
audioManager.stopAll(); audioManager.stopAll();
}); });
@@ -267,7 +326,7 @@ onShow(async () => {
> >
<view class="half-time-tip"> <view class="half-time-tip">
<text>上半场结束休息一下吧:</text> <text>上半场结束休息一下吧:</text>
<text>20秒后开始下半场</text> <text>{{ halfRestRemain }}秒后开始下半场</text>
</view> </view>
</ScreenHint> </ScreenHint>
<!-- 设备离线提示弹窗 --> <!-- 设备离线提示弹窗 -->

View File

@@ -1,120 +0,0 @@
<script setup>
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import { getMemberAgreement } from "./agreementData";
const agreementType = ref("renew");
onLoad((options) => {
agreementType.value = options.type || "renew";
});
const agreement = computed(() => getMemberAgreement(agreementType.value));
</script>
<template>
<Container :title="agreement.navTitle">
<scroll-view scroll-y class="agreement-page" :show-scrollbar="false">
<view class="content">
<view class="page-title">{{ agreement.title }}</view>
<view v-if="agreement.meta" class="meta">{{ agreement.meta }}</view>
<block v-for="(item, index) in agreement.content" :key="index">
<view v-if="item.type === 'heading'" class="section-title">
{{ item.text }}
</view>
<view v-else-if="item.type === 'list'" class="list">
<view v-for="(listItem, listIndex) in item.items" :key="listIndex" class="list-item">
<text class="list-dot"></text>
<text class="list-text">{{ listItem }}</text>
</view>
</view>
<view v-else class="paragraph">
<text>{{ item.text }}</text>
</view>
</block>
<view class="company">{{ agreement.company }}</view>
</view>
</scroll-view>
</Container>
</template>
<style scoped lang="scss">
.agreement-page {
width: 100%;
height: 100%;
background-color: #ffffff;
}
.content {
padding: 30rpx;
box-sizing: border-box;
}
.page-title,
.section-title {
color: #333333;
font-size: 28rpx;
line-height: 40rpx;
font-weight: 700;
}
.page-title {
margin-bottom: 20rpx;
}
.meta {
margin-bottom: 26rpx;
color: #666666;
font-size: 24rpx;
line-height: 34rpx;
}
.section-title {
margin: 42rpx 0 22rpx;
}
.paragraph,
.list-item,
.company {
color: #333333;
font-size: 26rpx;
line-height: 38rpx;
}
.paragraph {
margin-bottom: 22rpx;
}
.list {
margin-bottom: 22rpx;
}
.list-item {
display: flex;
align-items: flex-start;
margin-bottom: 14rpx;
}
.list-dot {
width: 28rpx;
flex-shrink: 0;
color: #333333;
line-height: 38rpx;
}
.list-text {
flex: 1;
color: #333333;
line-height: 38rpx;
}
.company {
margin-top: 42rpx;
padding-bottom: 30rpx;
text-align: right;
font-weight: 700;
}
</style>

View File

@@ -1,322 +0,0 @@
export const memberAgreements = {
renew: {
navTitle: "会员自动续费服务协议",
title: "射灵星球小程序会员自动续费服务协议",
content: [
{
type: "paragraph",
text: "欢迎您使用射灵星球小程序付费会员服务(以下简称“本服务”)!",
},
{
type: "paragraph",
text: "本服务是由广州光点飞舞网络有限公司(以下简称“公司”或“我们”)为您提供。本服务为付费服务。为了保障您的权益,请在使用本服务前详细阅读并遵守本《射灵星球小程序会员服务协议(含自动续费服务规则)》(以下简称“本协议”)以及公司已发布或将来可能发布的各项服务协议与规则。您在申请开通本服务并进入购买程序前,请务必审慎阅读、充分理解各服务协议及规则,特别是免除或限制责任条款、法律适用和争议解决条款。",
},
{
type: "paragraph",
text: "当您依照本服务开通页面提示进行阅读并同意本协议,完成全部服务开通程序后(包括但不限于点击“同意”、“下一步”或“确认支付”等确认按钮,或您开始使用本服务),即表示您已充分阅读、理解并接受本协议的全部内容,您已与本服务提供方达成一致,成为“射灵星球小程序”付费会员。本协议即在您与公司之间产生法律效力,成为对双方均具有约束力的法律文件。",
},
{ type: "heading", text: "一、定义及适用范围" },
{
type: "paragraph",
text: "1.1 射灵星球小程序付费会员:指已按照服务协议及规则完成射灵星球小程序登录的用户,在签署本协议并根据本服务开通页面所展示的收费标准支付相应费用后获取的特殊资格,在本协议中简称为“会员”或“您”。公司根据业务发展可能会新增、调整会员类型或名称,实际以开通页面展示为准,这不影响您的实际权益。",
},
{
type: "paragraph",
text: "1.2 付费会员权益:指用户基于其付费会员资格所享有的特殊权益,具体权益内容应以本服务购买页面展示内容及相关权益说明为准。您理解并同意,公司可能会根据设备型号、系统版本、客户端等因素开发不同的版本,不同版本实际可使用的具体权益或服务内容可能有所差别,具体以购买页面展示为准。",
},
{
type: "paragraph",
text: "1.3 本协议内容同时包括公司已经发布及后续可能不断发布的关于本服务的相关协议、规则等内容。前述内容一经正式发布,并以适当的方式送达您(服务购买页面、网站公布、系统通知等),即为本协议不可分割的组成部分,您应同样遵守。",
},
{
type: "paragraph",
text: "1.4 为了给会员用户提供更多选择,公司可能会与第三方合作推出联合会员服务,或为购买会员服务的用户赠送第三方会员服务。如果您选择购买或接受以上服务,则表示您理解并认可,我们仅提供射灵星球小程序付费会员服务,不对第三方的会员服务负责。第三方会员服务的权益、使用、收费规则等,将由第三方执行和向您解释。",
},
{ type: "heading", text: "二、服务开通、权益内容、服务期限及收费标准" },
{
type: "paragraph",
text: "2.1 本服务仅支持射灵星球小程序登录用户开通。您在开通本服务时,应仔细核对已登录的账号名称、会员类型、付费类型、服务期限等具体信息。因您个人原因充错账号、开通错服务类型或服务时长的,公司不予退还已收取的费用。",
},
{
type: "paragraph",
text: "2.2 您可通过已有和未来新增的支付渠道或公司指定支付方式完成本服务的购买。当您根据本服务页面提示进行确认、并成功支付了会员服务费和/或完成了成为付费会员的所有程序,您将成为射灵星球小程序付费会员。",
},
{
type: "paragraph",
text: "2.3 本服务的期限(以下简称“服务期限”)以您选择并成功开通的期限为准。会员计费与服务时长采用自然时间(自然月/自然年)方式计算。具体截止时间为相应续费到期自然日的对应时间点。服务期限届满后,公司将停止继续向您提供本服务。如您同时开通了多种会员服务,其消耗及重叠规则以开通页面的官方具体规则为准。",
},
{
type: "paragraph",
text: "2.4 本服务的收费标准及具体权益内容以本服务开通页面所展示的为准。基于市场与业务的发展及服务权益调整,公司可能会随时调整本服务开通所需费用及/或具体权益内容。费用或权益内容调整自公布之日起生效,您在调整生效前已开通的服务将不受影响,但该服务到期后的续费开通或自动续费扣款,则需按照调整后的标准执行。",
},
{
type: "paragraph",
text: "2.5 射灵星球小程序付费会员服务为虚拟内容消费,除因本服务存在重大瑕疵导致您完全无法使用等公司的违约情形、法律法规要求必须退款或公司同意退款等情形外,完成支付和购买后,不可进行退款或转让。您在会员到期前主动取消或终止会员资格的,已支付费用不予退还。",
},
{
type: "paragraph",
text: "2.6 为了保护您的账号安全、防止账号被盗风险,我们会对您开通会员服务的账号登录设备及使用范围作出合理限制。您不得将账号提供给多名第三方同时使用,否则公司有权根据安全风控能力随时调整或限制您的登录及使用权限。",
},
{ type: "heading", text: "三、连续购买和自动续费服务" },
{ type: "paragraph", text: "3.1 自动续费服务类型/计费周期" },
{
type: "paragraph",
text: "射灵星球小程序付费会员自动续费服务包含「连续包月」、「连续包年」等服务。本服务的自动续费及对应计费周期采用按自然时间(自然月/自然年对日顺延的方式计算。即若您在某月18日开通连续包月服务则下一次自动扣费续费日期为次月18日若开通当月无对应日期的例如1月31日开通2月无31日则自动调整为该自然月最后一日进行扣费续费。公司可能会根据会员需求增加或调整自动续费服务类型具体服务类型以服务开通页面展示为准。",
},
{ type: "paragraph", text: "3.2 自动续费服务说明" },
{
type: "paragraph",
text: "1本自动续费服务基于您对于自动续费的需求在您已开通会员服务的前提下为避免您因疏忽或其他原因导致未能及时续费而中断服务您授权公司可在您的会员服务期限到期前从您开通本自动续费服务时所绑定的Apple ID账户余额、或绑定的第三方支付账户包括但不限于微信支付、支付宝支付等余额中自动代扣下一个计费周期的费用从而延长对应的会员服务期限。",
},
{
type: "paragraph",
text: "2自动续费扣费日期以您开通本自动续费服务的支付渠道实际扣款时间为准如您是通过苹果公司iOS渠道开通本服务扣费日期通常为开通服务之日起每个计费周期的对应日期前24小时内如您是通过安卓Android渠道或直接在小程序内通过微信支付等渠道开通本服务扣费日期通常为每个计费周期到期前1至2日。如支付渠道根据实际情况或相关平台规则自行调整扣费时间的以实际扣款时间为准。",
},
{
type: "paragraph",
text: "3请您关注上述账户及可扣款余额情况保证上述账户扣款成功以确保会员服务顺利续期。如因上述账户中可扣款余额不足导致续费失败公司有权中断或终止相应的会员权益及服务由此导致的风险或损失将由您自行承担。",
},
{
type: "paragraph",
text: "4为了方便您知悉自动续费情况公司将在扣费日期前5日以站内信、小程序系统消息推送或短信等方式提示您即将发生续期扣费的信息第三方支付渠道也可能向您发送通知提醒即将扣费。",
},
{
type: "paragraph",
text: "5系统会在每个扣费日期自动从您开通服务时所绑定的支付账户扣费。如扣费日期因账户问题或余额不足导致续费失败若您未主动明确取消本服务将视为您同意公司在扣费日期后继续发出扣款尝试一旦您的账户扣款成功公司将继续为您提供相应会员服务权益。",
},
{
type: "paragraph",
text: "6如您未在每个扣费日期前操作取消自动续费服务则公司将根据此前与您达成的委托在扣费日期继续发出续费代扣指令一旦扣款成功公司将自动为您开通下一个计费周期的连续服务。对于已成功完成会员服务续费的费用原则上不予退还。",
},
{
type: "paragraph",
text: "7您在自动续费服务期间可以额外购买或通过参与活动等方式获取付费会员服务会员服务期限将在原服务期限基础上相应延长但如您未主动取消自动续费服务系统仍会按照您所开通的自动续费服务进行扣费及续期。",
},
{ type: "paragraph", text: "3.3 自动续费服务的退订" },
{
type: "paragraph",
text: "1您有权决定是否取消自动续费服务如果您希望取消自动续费服务需在每个扣费日期前至少提前24小时操作取消否则将视为您同意继续授权自动续费。",
},
{
type: "paragraph",
text: "2购买自动续费服务后您可在小程序的会员管理页中关闭自动续费或通过如下第三方支付渠道方式取消自动续费服务",
},
{
type: "list",
items: [
"iOS用户Apple ID订阅打开苹果手机“设置” -> 点击顶部的“Apple ID/机主姓名” -> 进入“订阅” -> 选择“射灵星球” -> 点击“取消订阅”或打开“App Store” -> 点击右上角头像进入“账户” -> 点击“订阅”进行管理。",
"微信支付自动续费用户打开微信APP -> 点击“我” -> “服务” -> “钱包” -> “支付设置” -> “自动续费” -> 选择“射灵星球小程序会员” -> 点击“关闭服务”。",
"支付宝自动续费用户打开支付宝APP -> 点击“我的” -> “设置” -> “支付设置” -> “免密支付/自动扣款” -> 选择“射灵星球小程序会员” -> 点击“关闭服务”。",
],
},
{
type: "paragraph",
text: "3.4 公司有权根据市场情况、业务规划、运营策略变化等原因单方面决定停止向您提供自动续费服务,并通过公告或站内信等方式通知您,您的付费会员服务期限自当前服务期限届满之日起终止。",
},
{ type: "heading", text: "四、服务使用规范与限制" },
{
type: "paragraph",
text: "4.1 本服务及会员权益仅限您本人使用。未经公司书面同意,禁止以任何形式赠与、借用、出租、转让、售卖或以其他方式许可他人使用该账号及账号项下的会员服务与权益。如发生泄漏、遗失、被盗等行为,而该等行为并非公司过错导致,损失将由您自行承担。",
},
{
type: "paragraph",
text: "4.2 如您存在如下违法或不当使用本服务的情形,公司有权取消您的会员资格、作废会员权益且不予退还您所支付的会员服务费用,并有权向您追偿给公司造成的损失:",
},
{
type: "list",
items: [
"以盗窃、利用系统漏洞、通过任何非公司官方或授权渠道获得本服务的行为;",
"利用本服务进行盈利或非法获利,或以各种形式转让、借用您的会员权益;",
"通过非法手段对本服务会员账户的服务期限、交易状态进行修改或篡改;",
"主动对公司用于保护本服务会员权益的任何安全措施技术进行破解、更改、反操作或破坏;",
"其他违反法律法规、诚实信用原则、服务协议及相关小程序运营规则的行为。",
],
},
{ type: "heading", text: "五、服务中止、终止及变更" },
{
type: "paragraph",
text: "5.1 因国家或相关政府监管部门要求、发生不可抗力事件、或由于用户违反本协议约定,公司有权中止或终止向用户提供服务。",
},
{
type: "paragraph",
text: "5.2 您知悉并确认,您开通本服务后,如您中途主动取消本服务、放弃会员权益或终止资格,您将无法退还部分或全部会员服务费用。本协议终止后,用户无权要求公司继续向其提供任何服务,且不影响终止前基于本协议已产生的权利义务。",
},
{
type: "paragraph",
text: "5.3 公司可根据国家法律法规变化、业务实际变更需求、保护用户权益的需要等,不时修改本协议,并按照法律法规规定的程序及方式进行公告。如用户不同意变更后的内容,则用户有权主动停止使用本服务;如用户在变更内容生效后仍继续使用本服务,即视为用户同意该等内容的变更。",
},
{ type: "heading", text: "六、责任限制与免责条款" },
{
type: "paragraph",
text: "6.1 您理解并同意,本服务是按照现有技术和条件所能达到的现状提供的。公司将尽最大努力确保服务的连贯性和安全性,但不能随时预见和防范技术以及其他风险,包括但不限于不可抗力、网络原因、第三方服务瑕疵等原因可能导致的服务中断、数据丢失以及其他的损失和风险。",
},
{
type: "paragraph",
text: "6.2 基于收益与赔偿相一致及公平合理的原则,如因公司原因造成本服务不正常中断或服务不可用,您所可能获得的最高赔偿额不超过本协议项下公司就该计费周期已实际收取您的相关服务费用总额。",
},
{ type: "heading", text: "七、法律适用与争议解决" },
{
type: "paragraph",
text: "7.1 本协议的成立、生效、履行、解释及争议的解决均应适用中华人民共和国法律。",
},
{
type: "paragraph",
text: "7.2 本协议的签订地为广东省广州市南沙区。若您因本协议与公司发生任何争议,双方应尽量友好协商解决;如协商不成的,任何一方均同意应将相关争议提交至本协议签订地有管辖权的人民法院诉讼解决。",
},
{
type: "paragraph",
text: "7.3 本协议任一条款被视为废止、无效或不可执行,该条应视为可分的且并不影响本协议其余条款的有效性及可执行性。",
},
],
company: "广州光点飞舞网络有限公司",
},
deduct: {
navTitle: "扣款授权服务协议",
title: "扣费授权服务协议",
meta: "版本号V1.0 生效日期2026年6月22日",
content: [
{ type: "heading", text: "一、授权声明" },
{
type: "paragraph",
text: "本人(即授权人,以下简称“乙方”)作为射灵星球小程序的注册用户,在自愿、平等、知悉全部授权内容的基础上,同意向广州光点飞舞网络有限公司(以下简称“甲方”)及甲方委托的第三方支付机构(包含微信支付、支付宝、苹果支付等,以下简称“支付机构”)作出本扣费授权,双方就授权扣费相关事宜达成如下协议。",
},
{ type: "heading", text: "二、授权主体" },
{
type: "paragraph",
text: "授权人乙方射灵星球小程序注册用户用户ID以平台系统记录为准。",
},
{
type: "paragraph",
text: "被授权人(甲方):广州光点飞舞网络有限公司,作为会员服务提供方及扣费指令发起方。",
},
{
type: "paragraph",
text: "支付执行方:乙方绑定的第三方支付机构,受甲方委托执行扣费操作。",
},
{ type: "heading", text: "三、授权内容" },
{
type: "paragraph",
text: "授权场景:乙方开通射灵星球会员自动续费服务后,授权甲方在每个会员周期届满前,向支付机构发起扣费指令,用于支付下一周期的会员服务费用。",
},
{
type: "paragraph",
text: "授权金额:扣费金额以乙方开通时选择的自动续费套餐对应价格为准,具体以会员购买页面公示价格为准;若价格调整,甲方将提前通过公示或通知形式告知乙方,乙方继续使用服务视为认可并接受调整后的金额。",
},
{
type: "paragraph",
text: "授权支付账户:乙方开通自动续费时绑定的微信支付、支付宝等第三方支付账户,账户信息以支付机构记录为准。",
},
{
type: "paragraph",
text: "授权扣费次数:本授权为周期性授权。在授权有效期内,甲方可按会员周期重复发起扣费指令,直至乙方依法撤销授权为止。",
},
{ type: "heading", text: "四、授权有效期" },
{
type: "paragraph",
text: "1. 本授权自乙方点击确认开通自动续费服务之日起生效,至乙方成功取消自动续费服务之日终止。",
},
{
type: "paragraph",
text: "2. 若乙方注销射灵星球小程序账号,本授权自账号注销完成之日自动终止。",
},
{
type: "paragraph",
text: "3. 若因国家政策调整、支付机构规则变更、乙方支付账户注销/冻结等非甲方主观原因导致授权无法履行的,本授权自动终止。",
},
{ type: "heading", text: "五、扣费与提醒规则" },
{
type: "paragraph",
text: "1. 扣费提醒甲方将在每个自动续费扣费日期前5日通过微信服务通知、小程序系统消息或短信等显著方式提示乙方即将发生续期扣费的信息以保障乙方的知情权与选择权。",
},
{
type: "paragraph",
text: "2. 扣费时机甲方将在乙方当前会员有效期届满前24小时内发起下一周期的扣费指令支付机构根据指令从乙方授权账户中划扣对应费用。",
},
{
type: "paragraph",
text: "3. 扣费失败处理:若因账户余额不足等原因导致首次扣费失败的,甲方及支付机构可在合规范围内依法尝试补扣;若补扣仍失败或账户处于异常状态的,本期自动续费暂停,乙方会员到期后自动失效。",
},
{
type: "paragraph",
text: "4. 扣费凭证:支付机构的扣费记录作为扣费成功的有效凭证,乙方可在支付账户账单中查询明细;甲方同步为乙方提供电子扣费记录,可在会员中心查看。",
},
{ type: "heading", text: "六、授权的变更与撤销" },
{
type: "paragraph",
text: "1. 授权变更:本授权内容如需变更(如更换支付账户、调整续费套餐),乙方需先取消原自动续费服务,再重新开通新套餐。新授权自重新开通成功之日起生效,原授权同步终止。",
},
{
type: "paragraph",
text: "2. 授权撤销:乙方可随时退订本自动续费服务,解约后不影响乙方已生效周期的会员服务。根据乙方开通时选择的支付执行方,具体撤销路径如下:",
},
{
type: "list",
items: [
"微信支付用户可通过微信APP内路径我 -> 服务 -> 钱包 -> 支付设置 -> 自动续费)或在小程序会员中心页面撤销本授权。",
"支付宝用户可通过支付宝APP内路径我的 -> 设置 -> 支付设置 -> 免密支付/自动扣款)或在小程序会员中心页面撤销本授权。",
"苹果支付iOS/App Store订阅用户必须通过苹果系统自带的功能进行取消路径iOS设备“设置” -> 点击顶部的Apple ID -> 订阅 -> 选择“射灵星球” -> 取消订阅)。",
],
},
{
type: "paragraph",
text: "3. 退订生效时效:",
},
{
type: "list",
items: [
"针对微信支付和支付宝:撤销操作成功后,本授权即时终止,甲方不得再发起新的扣费指令。因第三方支付平台系统结算延迟等客观原因,导致退订生效前系统已自动扣款成功的,该期会员权益将正常发放,已扣费用原则上不予退还。",
"针对苹果支付根据苹果公司政策乙方需在当前计费周期届满前至少24小时手动取消订阅否则苹果系统可能会自动续订并扣款。苹果渠道的扣费与退款均由苹果公司独立处理甲方无权干涉因乙方未及时取消导致扣费的由乙方自行承担或向苹果公司申诉。",
],
},
{ type: "heading", text: "七、免责条款" },
{
type: "paragraph",
text: "1. 因乙方支付账户余额不足、账户冻结、挂失、注销、限额等非甲方原因导致扣费失败的,甲方不承担任何责任,由此造成的会员权益中断等后果由乙方自行承担。",
},
{
type: "paragraph",
text: "2. 因支付机构系统故障、网络中断、政策调整等第三方不可抗力或外部原因导致扣费延迟、失败或错误的,甲方将协助乙方协调解决,但不承担相应的赔偿责任。",
},
{
type: "paragraph",
text: "3. 因乙方泄露支付账户密码、账号被盗用等自身保管不当原因导致的异常扣费,甲方不承担责任,乙方应自行向支付机构或公安机关主张权利。",
},
{ type: "heading", text: "八、信息保密" },
{
type: "paragraph",
text: "1. 甲方及支付机构应对乙方的授权信息、支付信息、个人身份信息严格保密,不得用于本授权以外的任何用途。",
},
{
type: "paragraph",
text: "2. 除法律法规规定、司法机关或监管部门依法要求外,甲方不得向任何第三方泄露乙方的授权及支付相关信息。",
},
{ type: "heading", text: "九、争议解决" },
{
type: "paragraph",
text: "1. 本授权协议的订立、效力、履行及争议解决均适用中华人民共和国法律。",
},
{
type: "paragraph",
text: "2. 因本协议产生的任何争议,双方应尽量友好协商解决;协商不成的,任何一方均可向甲方住所地(广东省广州市天河区)有管辖权的人民法院提起诉讼。",
},
{ type: "heading", text: "十、其他" },
{
type: "paragraph",
text: "1. 本协议为《射灵星球小程序会员服务协议》的配套协议,与该协议具有同等法律效力;本协议未约定事项,参照主协议执行。",
},
{
type: "paragraph",
text: "2. 甲方有权根据业务及监管要求调整本协议内容,调整后的协议将在平台公示,乙方继续使用自动续费服务视为接受调整后的内容。",
},
{
type: "paragraph",
text: "3. 甲方客服联系方式:请通过射灵星球小程序内【在线客服】或发送邮件至官方客服邮箱进行反馈。",
},
],
company: "广州光点飞舞网络有限公司",
},
};
export const getMemberAgreement = (type) => {
return memberAgreements[type] || memberAgreements.renew;
};

View File

@@ -12,7 +12,7 @@ const store = useStore();
const { user, config } = storeToRefs(store); const { user, config } = storeToRefs(store);
const { updateConfig, updateUser } = store; const { updateConfig, updateUser } = store;
const currentTypeIndex = ref(1); const currentTypeIndex = ref(0);
const selectedPackageIndex = ref(0); const selectedPackageIndex = ref(0);
const showModal = ref(false); const showModal = ref(false);
const loadingConfig = ref(false); const loadingConfig = ref(false);
@@ -244,12 +244,6 @@ const toOrderPage = () => {
}); });
}; };
const toAgreement = (type) => {
uni.navigateTo({
url: `/pages/member/agreement?type=${type}`,
});
};
const loadVipConfig = async () => { const loadVipConfig = async () => {
if (loadingConfig.value || configMenus.value.length) return; if (loadingConfig.value || configMenus.value.length) return;
loadingConfig.value = true; loadingConfig.value = true;
@@ -484,8 +478,8 @@ onBeforeUnmount(() => {
<view class="agreement"> <view class="agreement">
<text>支付即同意</text> <text>支付即同意</text>
&nbsp;<text class="agreement__link" @click.stop="toAgreement('renew')">会员自动续费服务协议</text> &nbsp;<text class="agreement__link">会员自动续费服务协议</text>
&nbsp;<text class="agreement__link" @click.stop="toAgreement('deduct')">扣款授权服务协议</text> &nbsp;<text class="agreement__link">扣款授权服务协议</text>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>

View File

@@ -16,7 +16,7 @@ const showTip = ref(false);
const confirmBindTip = ref(false); const confirmBindTip = ref(false);
const addDevice = ref(); const addDevice = ref();
const store = useStore(); const store = useStore();
const { updateDevice } = store; const { updateDevice, clearDevice } = store;
const { user, device } = storeToRefs(store); const { user, device } = storeToRefs(store);
const justBind = ref(false); const justBind = ref(false);
const calibration = ref(false); const calibration = ref(false);
@@ -86,13 +86,21 @@ const toFristTryPage = () => {
}; };
const unbindDevice = async () => { const unbindDevice = async () => {
try {
await unbindDeviceAPI(device.value.deviceId); await unbindDeviceAPI(device.value.deviceId);
} catch (error) {
if (error?.type === "DEVICE_BIND_INVALID") {
uni.setStorageSync("calibration", false);
clearDevice();
}
return;
}
uni.setStorageSync("calibration", false); uni.setStorageSync("calibration", false);
uni.showToast({ uni.showToast({
title: "解绑成功", title: "解绑成功",
icon: "success", icon: "success",
}); });
device.value = {}; clearDevice();
}; };
const toDeviceIntroPage = () => { const toDeviceIntroPage = () => {
@@ -124,8 +132,23 @@ const goCalibration = async () => {
}); });
}; };
onShow(() => { const syncDeviceBinding = async () => {
if (!user.value.id) return;
try {
const devices = await getMyDevicesAPI();
if (devices.bindings && devices.bindings.length) {
updateDevice(devices.bindings[0].deviceId, devices.bindings[0].deviceName);
} else {
clearDevice();
}
} catch (error) {
console.log("sync device binding error", error);
}
};
onShow(async () => {
calibration.value = uni.getStorageSync("calibration"); calibration.value = uni.getStorageSync("calibration");
await syncDeviceBinding();
}); });
</script> </script>

View File

@@ -32,7 +32,7 @@ const start = ref(false);
const scores = ref([]); const scores = ref([]);
const isSvip = ref(false); const isSvip = ref(false);
const total = 12; const total = 12;
/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */ /** 当前练习中连续 10 环及以上计数,用于触发 tententen 音效 */
const xRingStreak = ref(0); const xRingStreak = ref(0);
const practiseResult = ref({}); const practiseResult = ref({});
const practiseId = ref(""); const practiseId = ref("");
@@ -61,19 +61,23 @@ const onOver = async () => {
}; };
/** /**
* 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效 * 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效
* @param {boolean} isXRing - 本次射击是否为 X 环 * @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
*/ */
function checkAndPlayTententen(isXRing) { function isTenPlusRing(shot) {
if (isXRing) { return !!(shot?.ringX || Number(shot?.ring) >= 10);
}
function checkAndPlayTententen(isTenPlusRingShot) {
if (isTenPlusRingShot) {
xRingStreak.value += 1; xRingStreak.value += 1;
// 连续 3 箭均为 X 环,在环数播报入队后追加 tententen避免播放顺序颠倒 // 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen避免播放顺序颠倒
if (xRingStreak.value >= 3) { if (xRingStreak.value >= 3) {
xRingStreak.value = 0; xRingStreak.value = 0;
nextTick(() => audioManager.play("tententen", false)); nextTick(() => audioManager.play("tententen", false));
} }
} else { } else {
// 非 X 环则重置连续计数 // 低于 10 环或未上靶则重置连续计数
xRingStreak.value = 0; xRingStreak.value = 0;
} }
} }
@@ -83,10 +87,10 @@ async function onReceiveMessage(msg) {
const prevLen = scores.value.length; const prevLen = scores.value.length;
isSvip.value = msg.sVip === true; isSvip.value = msg.sVip === true;
scores.value = msg.details; scores.value = msg.details;
// 有新箭时取最后一箭判断是否 X 环并检测连续计数 // 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数
if (scores.value.length > prevLen) { if (scores.value.length > prevLen) {
const latestArrow = scores.value[scores.value.length - 1]; const latestArrow = scores.value[scores.value.length - 1];
checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring)); checkAndPlayTententen(isTenPlusRing(latestArrow));
} }
} else if (msg.type === MESSAGETYPESV2.BattleEnd) { } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
// setTimeout(onOver, 1500); // setTimeout(onOver, 1500);

View File

@@ -32,7 +32,7 @@ const start = ref(false);
const scores = ref([]); const scores = ref([]);
const isSvip = ref(false); const isSvip = ref(false);
const total = 36; const total = 36;
/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */ /** 当前练习中连续 10 环及以上计数,用于触发 tententen 音效 */
const xRingStreak = ref(0); const xRingStreak = ref(0);
const practiseResult = ref({}); const practiseResult = ref({});
const practiseId = ref(""); const practiseId = ref("");
@@ -60,19 +60,23 @@ const onOver = async () => {
}; };
/** /**
* 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效 * 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效
* @param {boolean} isXRing - 本次射击是否为 X 环 * @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
*/ */
function checkAndPlayTententen(isXRing) { function isTenPlusRing(shot) {
if (isXRing) { return !!(shot?.ringX || Number(shot?.ring) >= 10);
}
function checkAndPlayTententen(isTenPlusRingShot) {
if (isTenPlusRingShot) {
xRingStreak.value += 1; xRingStreak.value += 1;
// 连续 3 箭均为 X 环,在环数播报入队后追加 tententen避免播放顺序颠倒 // 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen避免播放顺序颠倒
if (xRingStreak.value >= 3) { if (xRingStreak.value >= 3) {
xRingStreak.value = 0; xRingStreak.value = 0;
nextTick(() => audioManager.play("tententen", false)); nextTick(() => audioManager.play("tententen", false));
} }
} else { } else {
// 非 X 环则重置连续计数 // 低于 10 环或未上靶则重置连续计数
xRingStreak.value = 0; xRingStreak.value = 0;
} }
} }
@@ -82,10 +86,10 @@ async function onReceiveMessage(msg) {
const prevLen = scores.value.length; const prevLen = scores.value.length;
isSvip.value = msg.sVip === true; isSvip.value = msg.sVip === true;
scores.value = msg.details; scores.value = msg.details;
// 有新箭时取最后一箭判断是否 X 环并检测连续计数 // 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数
if (scores.value.length > prevLen) { if (scores.value.length > prevLen) {
const latestArrow = scores.value[scores.value.length - 1]; const latestArrow = scores.value[scores.value.length - 1];
checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring)); checkAndPlayTententen(isTenPlusRing(latestArrow));
} }
} else if (msg.type === MESSAGETYPESV2.BattleEnd) { } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(onOver, 1500); setTimeout(onOver, 1500);

View File

@@ -49,7 +49,7 @@ const battleWay = ref(0);
const lastToSomeoneShootKey = ref(""); const lastToSomeoneShootKey = ref("");
/** 控制设备离线提示弹窗的显示状态 */ /** 控制设备离线提示弹窗的显示状态 */
const showOfflineModal = ref(false); const showOfflineModal = ref(false);
/** 记录每位玩家当前轮连续 X 环key 为 playerId用于触发 tententen 音效 */ /** 记录每位玩家当前轮连续 10 环及以上次key 为 playerId用于触发 tententen 音效 */
const xRingStreaks = ref({}); const xRingStreaks = ref({});
/** /**
@@ -234,22 +234,26 @@ function onNewRound(msg, prevRound) {
} }
/** /**
* 检测指定射手连续 X 环是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效 * 检测指定射手连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
* @param {number} shooterId - 本次射手的 ID取自 currentShooterId.value * @param {number} shooterId - 本次射手的 ID取自 currentShooterId.value
* @param {boolean} isXRing - 本次射击是否为 X 环 * @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
*/ */
function checkAndPlayTententen(shooterId, isXRing) { function isTenPlusRing(shot) {
return !!(shot?.ringX || Number(shot?.ring) >= 10);
}
function checkAndPlayTententen(shooterId, isTenPlusRingShot) {
if (!shooterId) return; if (!shooterId) return;
if (isXRing) { if (isTenPlusRingShot) {
xRingStreaks.value[shooterId] = (xRingStreaks.value[shooterId] || 0) + 1; xRingStreaks.value[shooterId] = (xRingStreaks.value[shooterId] || 0) + 1;
// 同一玩家连续 3 箭均为 X 环,追加到环数音效队列尾部播放 // 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
if (xRingStreaks.value[shooterId] >= 3) { if (xRingStreaks.value[shooterId] >= 3) {
xRingStreaks.value[shooterId] = 0; xRingStreaks.value[shooterId] = 0;
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen避免播放顺序颠倒 // nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen避免播放顺序颠倒
nextTick(() => audioManager.play("tententen", false)); nextTick(() => audioManager.play("tententen", false));
} }
} else { } else {
// 非 X 环则重置该玩家的连续计数 // 低于 10 环或未上靶则重置该玩家的连续计数
xRingStreaks.value[shooterId] = 0; xRingStreaks.value[shooterId] = 0;
} }
} }
@@ -268,9 +272,9 @@ async function onReceiveMessage(msg) {
} else if (msg.type === MESSAGETYPESV2.ShootResult) { } else if (msg.type === MESSAGETYPESV2.ShootResult) {
showRoundTip.value = false; showRoundTip.value = false;
recoverData(msg, {arrowOnly: true}); recoverData(msg, {arrowOnly: true});
// 检测同一玩家三箭全 X 环,触发 tententen 音效 // 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
// currentShooterId 在 ToSomeoneShoot 时写入ShootResult 不会覆盖,可靠识别本次射手 // currentShooterId 在 ToSomeoneShoot 时写入ShootResult 不会覆盖,可靠识别本次射手
checkAndPlayTententen(currentShooterId.value, !!(msg.shootData?.ringX && msg.shootData?.ring)); checkAndPlayTententen(currentShooterId.value, isTenPlusRing(msg.shootData));
} else if (msg.type === MESSAGETYPESV2.NewRound) { } else if (msg.type === MESSAGETYPESV2.NewRound) {
// 在进入延迟前先捕获当前轮次,供 onNewRound 使用,防止 800ms 内 ToSomeoneShoot 提前更新 currentRound 造成 Tip 展示错轮 // 在进入延迟前先捕获当前轮次,供 onNewRound 使用,防止 800ms 内 ToSomeoneShoot 提前更新 currentRound 造成 Tip 展示错轮
const prevRound = currentRound.value; const prevRound = currentRound.value;

View File

@@ -432,7 +432,10 @@ function playAudioKeys(keys, { interrupt = false, timeout } = {}) {
resolve(); resolve();
}, },
}; };
const timer = setTimeout(waiter.done, waitTime); const timer = setTimeout(() => {
audioManager.recoverIfStale(expectedKey);
waiter.done();
}, waitTime);
audioWaiters.add(waiter); audioWaiters.add(waiter);
audioManager.play(audioKeys, interrupt); audioManager.play(audioKeys, interrupt);
}); });
@@ -473,17 +476,14 @@ function updateTeams(battleInfo) {
} }
function updateGoldenRound(battleInfo) { function updateGoldenRound(battleInfo) {
const rounds = Array.isArray(battleInfo?.rounds) ? battleInfo.rounds : []; if (!battleInfo?.current?.goldRound) {
const currentRoundNo = Number(battleInfo?.current?.round || 0); goldenRound.value = 0;
const currentRoundInfo = rounds.find((round) => Number(round?.round) === currentRoundNo); return;
const activeGoldRoundInfo = rounds.find( }
(round) => Number(round?.goldRound || 0) > 0 && round?.status === 1 const rounds = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : [];
); const finishedGoldCount = rounds.filter((round) => !!round?.ifGold).length;
const roundGoldRound = Number(currentRoundInfo?.goldRound || 0); // goldenRound.value = Math.max(1, finishedGoldCount + (battleInfo.current?.playerId ? 1 : 0));
const activeGoldRound = Number(activeGoldRoundInfo?.goldRound || 0); goldenRound.value = Math.max(1, finishedGoldCount);
const currentGoldRound = Number(battleInfo?.current?.goldRound || 0);
const nextGoldRound = roundGoldRound || activeGoldRound || currentGoldRound;
goldenRound.value = nextGoldRound > 0 ? nextGoldRound : 0;
} }
// Restore an info snapshot whose eventType points at the NewRound phase. // Restore an info snapshot whose eventType points at the NewRound phase.
@@ -861,10 +861,14 @@ async function runToSomeoneShootTask(task, runId) {
}); });
} }
function updateXRingStreak(shooterId, isXRing) { function isTenPlusRing(shot) {
return !!(shot?.ringX || Number(shot?.ring) >= 10);
}
function updateXRingStreak(shooterId, isTenPlusRingShot) {
if (!shooterId) return false; if (!shooterId) return false;
const id = String(shooterId); const id = String(shooterId);
if (!isXRing) { if (!isTenPlusRingShot) {
xRingStreaks.value[id] = 0; xRingStreaks.value[id] = 0;
saveXRingStreaks(); saveXRingStreaks();
return false; return false;
@@ -909,7 +913,7 @@ async function runShootResultTask(task) {
const isTententen = updateXRingStreak( const isTententen = updateXRingStreak(
currentShooterId.value, currentShooterId.value,
!!(battleInfo.shootData?.ringX && battleInfo.shootData?.ring) isTenPlusRing(battleInfo.shootData)
); );
const audioKeys = buildShootResultAudioKeys(battleInfo.shootData); const audioKeys = buildShootResultAudioKeys(battleInfo.shootData);
if (isTententen) audioKeys.push("tententen"); if (isTententen) audioKeys.push("tententen");
@@ -1255,7 +1259,7 @@ onShow(() => {
<view class="offline-modal"> <view class="offline-modal">
<text class="offline-title">设备已离线</text> <text class="offline-title">设备已离线</text>
<text class="offline-desc">检测到设备已断开连接请检查设备后继续比赛</text> <text class="offline-desc">检测到设备已断开连接请检查设备后继续比赛</text>
<SButton @click="showOfflineModal = false">我知道了</SButton> <SButton :onClick="() => (showOfflineModal = false)">我知道了</SButton>
</view> </view>
</SModal> </SModal>
</view> </view>

View File

@@ -137,6 +137,10 @@ export default defineStore("store", {
this.device.deviceId = deviceId; this.device.deviceId = deviceId;
this.device.deviceName = deviceName; this.device.deviceName = deviceName;
}, },
clearDevice() {
this.device = getDefaultDevice();
this.online = false;
},
async updateConfig(config) { async updateConfig(config) {
this.config = config; this.config = config;
if (this.user.scores !== undefined) { if (this.user.scores !== undefined) {