细节修改

This commit is contained in:
kron
2025-12-30 18:10:31 +08:00
parent 44913a6f2e
commit 910530748d
7 changed files with 270 additions and 72 deletions

View File

@@ -128,9 +128,42 @@ class AudioManager {
this.failedLoadKeys = new Set(); this.failedLoadKeys = new Set();
// 加载代数,用于 reloadAll 时作废旧的加载循环 // 加载代数,用于 reloadAll 时作废旧的加载循环
this.loadGeneration = 0; this.loadGeneration = 0;
// 本地路径缓存 Map: { url: localPath }
this.localFileCache = uni.getStorageSync("audio_local_files") || {};
// 启动时自动清理过期的缓存文件URL 已不在 audioFils 中的文件)
this.cleanObsoleteCache();
this.initAudios(); this.initAudios();
} }
// 清理不再使用的缓存文件
cleanObsoleteCache() {
const activeUrls = new Set(Object.values(audioFils));
const cachedUrls = Object.keys(this.localFileCache);
let hasChanges = false;
for (const url of cachedUrls) {
if (!activeUrls.has(url)) {
debugLog(`发现废弃音频缓存,正在清理: ${url}`);
const path = this.localFileCache[url];
// 移除物理文件
uni.removeSavedFile({
filePath: path,
complete: () => {
// 忽略移除结果,直接移除记录
},
});
// 移除记录
delete this.localFileCache[url];
hasChanges = true;
}
}
if (hasChanges) {
uni.setStorageSync("audio_local_files", this.localFileCache);
debugLog("废弃缓存清理完成");
}
}
// 初始化音频(两阶段:首轮串行加载全部,次轮仅串行加载失败项一次) // 初始化音频(两阶段:首轮串行加载全部,次轮仅串行加载失败项一次)
initAudios() { initAudios() {
if (this.isLoading) { if (this.isLoading) {
@@ -244,7 +277,7 @@ class AudioManager {
}); });
} }
// 创建单个音频实例(统一在失败时记录,而非立即重试 // 创建单个音频实例(支持本地缓存
createAudio(key, callback) { createAudio(key, callback) {
this.currentLoadingIndex++; this.currentLoadingIndex++;
const src = audioFils[key]; const src = audioFils[key];
@@ -287,7 +320,7 @@ class AudioManager {
clearTimeout(loadTimeout); clearTimeout(loadTimeout);
this.readyMap.set(key, true); this.readyMap.set(key, true);
this.failedLoadKeys.delete(key); this.failedLoadKeys.delete(key);
debugLog(`音频 ${key} 已加载完成`, this.getLoadProgress()); // debugLog(`音频 ${key} 已加载完成`, this.getLoadProgress());
uni.$emit("audioLoaded", key); uni.$emit("audioLoaded", key);
const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {}; const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {};
loadedAudioKeys[key] = true; loadedAudioKeys[key] = true;
@@ -298,11 +331,20 @@ class AudioManager {
audio.onError((res) => { audio.onError((res) => {
clearTimeout(loadTimeout); clearTimeout(loadTimeout);
debugLog(`音频 ${key} 加载失败:`, res.errMsg); debugLog(`音频 ${key} 加载失败:`, res.errMsg);
// 如果是本地文件加载失败,可能是文件损坏,清除缓存以便下次重新下载
if (realSrc !== src && this.localFileCache[src] === realSrc) {
debugLog(`本地缓存失效,移除记录: ${key}`);
delete this.localFileCache[src];
uni.setStorageSync("audio_local_files", this.localFileCache);
// 移除文件
uni.removeSavedFile({ filePath: realSrc });
}
this.recordLoadFailure(key); this.recordLoadFailure(key);
this.audioMap.delete(key); this.audioMap.delete(key);
audio.destroy(); audio.destroy();
if (this.readyMap.get(key)) { if (this.readyMap.get(key)) {
this.readyMap.set(key, false); // 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载
// this.readyMap.set(key, false);
} else { } else {
if (callback) callback(); if (callback) callback();
} }
@@ -326,23 +368,72 @@ class AudioManager {
this.audioMap.set(key, audio); this.audioMap.set(key, audio);
}; };
// 统一先下载到本地再加载 // 检查是否有可用的本地缓存
uni.downloadFile({ this.checkLocalFile(src).then((localPath) => {
url: src, if (localPath) {
timeout: 20000, debugLog(`命中本地缓存: ${key}`);
success: (res) => { setupAudio(localPath);
// 成功必须有临时文件路径 } else {
if (res.tempFilePath) { console.log("download");
setupAudio(res.tempFilePath); // 下载并尝试保存
} else { uni.downloadFile({
this.recordLoadFailure(key); url: src,
if (callback) callback(); timeout: 20000,
} success: (res) => {
}, if (res.tempFilePath) {
fail: () => { // 尝试保存文件到本地存储(持久化)
this.recordLoadFailure(key); uni.saveFile({
if (callback) callback(); tempFilePath: res.tempFilePath,
}, success: (saveRes) => {
const savedPath = saveRes.savedFilePath;
this.localFileCache[src] = savedPath;
uni.setStorageSync("audio_local_files", this.localFileCache);
debugLog(`音频已缓存到本地: ${key}`);
setupAudio(savedPath);
},
fail: (err) => {
debugLog(
`保存音频失败(可能空间不足),使用临时文件: ${key}`,
err
);
setupAudio(res.tempFilePath);
},
});
} else {
this.recordLoadFailure(key);
if (callback) callback();
}
},
fail: () => {
this.recordLoadFailure(key);
if (callback) callback();
},
});
}
});
}
// 检查本地文件是否有效
checkLocalFile(url) {
return new Promise((resolve) => {
const path = this.localFileCache[url];
if (!path) {
resolve(null);
return;
}
// 检查文件是否存在
uni.getFileSystemManager().getFileInfo({
filePath: path,
success: () => {
resolve(path);
},
fail: () => {
// 文件不存在,清理记录
delete this.localFileCache[url];
uni.setStorageSync("audio_local_files", this.localFileCache);
resolve(null);
},
});
}); });
} }
@@ -550,9 +641,28 @@ class AudioManager {
return Number((loaded / total).toFixed(2)); return Number((loaded / total).toFixed(2));
} }
// 清理本地音频缓存文件
clearCache() {
debugLog("开始清理本地音频缓存...");
const cache = uni.getStorageSync("audio_local_files") || {};
const paths = Object.values(cache);
for (const path of paths) {
uni.removeSavedFile({
filePath: path,
complete: (res) => {
// 无论成功失败都继续
},
});
}
uni.removeStorageSync("audio_local_files");
this.localFileCache = {};
debugLog("本地音频缓存清理完成");
}
// 手动重置并重新加载所有音频(用于卡住时恢复) // 手动重置并重新加载所有音频(用于卡住时恢复)
reloadAll() { reloadAll() {
debugLog("执行 reloadAll: 重置所有状态并重新加载");
// 1. 停止所有播放 // 1. 停止所有播放
this.stopAll(); this.stopAll();
@@ -573,6 +683,10 @@ class AudioManager {
this.sequenceIndex = 0; this.sequenceIndex = 0;
this.isSequenceRunning = false; this.isSequenceRunning = false;
// 清理一下可能损坏的缓存(可选,如果用户因为缓存坏了卡住,这一步很有用)
// 这里选择不自动全清,而是依赖 onError 里的单点清除。如果需要彻底重置,可取消注释:
// this.clearCache();
// 4. 强制重置加载锁 // 4. 强制重置加载锁
this.isLoading = false; this.isLoading = false;
this.loadingPromise = null; this.loadingPromise = null;

View File

@@ -1,6 +1,8 @@
<script setup> <script setup>
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue"; import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
import StartCountdown from "@/components/StartCountdown.vue"; // import StartCountdown from "@/components/StartCountdown.vue";
import PointSwitcher from "@/components/PointSwitcher.vue";
import { MESSAGETYPES } from "@/constants"; import { MESSAGETYPES } from "@/constants";
import { simulShootAPI } from "@/apis"; import { simulShootAPI } from "@/apis";
import useStore from "@/store"; import useStore from "@/store";
@@ -17,10 +19,6 @@ const props = defineProps({
type: Number, type: Number,
default: 0, default: 0,
}, },
avatar: {
type: String,
default: "",
},
scores: { scores: {
type: Array, type: Array,
default: () => [], default: () => [],
@@ -43,6 +41,7 @@ const props = defineProps({
}, },
}); });
const pMode = ref(true);
const latestOne = ref(null); const latestOne = ref(null);
const bluelatestOne = ref(null); const bluelatestOne = ref(null);
const prevScores = ref([]); const prevScores = ref([]);
@@ -217,36 +216,36 @@ onBeforeUnmount(() => {
<block v-for="(bow, index) in scores" :key="index"> <block v-for="(bow, index) in scores" :key="index">
<view <view
v-if="bow.ring > 0" v-if="bow.ring > 0"
:class="`hit ${ :class="`hit ${pMode ? 'b' : 's'}-point ${
index === scores.length - 1 && latestOne ? 'pump-in' : '' index === scores.length - 1 && latestOne ? 'pump-in' : ''
}`" }`"
:style="{ :style="{
left: calcRealX(bow.x), left: calcRealX(bow.x, pMode ? '3.4' : '2'),
top: calcRealY(bow.y), top: calcRealY(bow.y, pMode ? '3.4' : '2'),
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000', backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
}" }"
><text>{{ index + 1 }}</text></view ><text v-if="pMode">{{ index + 1 }}</text></view
> >
</block> </block>
<block v-for="(bow, index) in blueScores" :key="index"> <block v-for="(bow, index) in blueScores" :key="index">
<view <view
v-if="bow.ring > 0" v-if="bow.ring > 0"
:class="`hit ${ :class="`hit ${pMode ? 'b' : 's'}-point ${
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : '' index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
}`" }`"
:style="{ :style="{
left: calcRealX(bow.x), left: calcRealX(bow.x, pMode ? '3.4' : '2'),
top: calcRealY(bow.y), top: calcRealY(bow.y, pMode ? '3.4' : '2'),
backgroundColor: '#1840FF', backgroundColor: '#1840FF',
}" }"
> >
<text>{{ index + 1 }}</text> <text v-if="pMode">{{ index + 1 }}</text>
</view> </view>
</block> </block>
<image src="../static/bow-target.png" mode="widthFix" /> <image src="../static/bow-target.png" mode="widthFix" />
</view> </view>
<view v-if="avatar" class="footer"> <view class="footer">
<image :src="avatar" mode="widthFix" /> <PointSwitcher :onChange="(val) => (pMode = val)" />
</view> </view>
<view class="simul" v-if="env !== 'release'"> <view class="simul" v-if="env !== 'release'">
<button @click="simulShoot">模拟</button> <button @click="simulShoot">模拟</button>
@@ -271,11 +270,10 @@ onBeforeUnmount(() => {
margin: 10px; margin: 10px;
width: calc(100% - 20px); width: calc(100% - 20px);
height: calc(100% - 20px); height: calc(100% - 20px);
z-index: -1;
} }
.e-value { .e-value {
position: absolute; position: absolute;
/* top: 30%;
left: 60%; */
background-color: #0006; background-color: #0006;
color: #fff; color: #fff;
font-size: 12px; font-size: 12px;
@@ -287,8 +285,6 @@ onBeforeUnmount(() => {
} }
.round-tip { .round-tip {
position: absolute; position: absolute;
/* top: 38%; */
/* left: 60%; */
color: #fff; color: #fff;
font-size: 30px; font-size: 30px;
font-weight: bold; font-weight: bold;
@@ -306,30 +302,39 @@ onBeforeUnmount(() => {
} }
.hit { .hit {
position: absolute; position: absolute;
border-radius: 50%;
z-index: 1;
color: #fff;
transition: all 0.3s ease;
}
.s-point {
width: 4px;
height: 4px;
min-width: 4px;
min-height: 4px;
}
.b-point {
width: 10px; width: 10px;
height: 10px; height: 10px;
min-width: 10px; min-width: 10px;
min-height: 10px; min-height: 10px;
border-radius: 50%;
border: 1px solid #fff; border: 1px solid #fff;
z-index: 1; z-index: 1;
color: #fff;
box-sizing: border-box; box-sizing: border-box;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.hit > text { .b-point > text {
font-size: 16rpx; font-size: 16rpx;
font-family: "DINCondensed", "PingFang SC", "Helvetica Neue", Arial, color: #fff;
sans-serif; font-family: "DINCondensed";
text-align: center; /* text-align: center;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);*/
margin-top: 1px; margin-top: 2rpx;
} }
.header { .header {
width: 100%; width: 100%;
@@ -353,6 +358,7 @@ onBeforeUnmount(() => {
padding: 0 10px; padding: 0 10px;
display: flex; display: flex;
margin-top: -40px; margin-top: -40px;
justify-content: flex-end;
} }
.footer > image { .footer > image {
width: 40px; width: 40px;
@@ -363,7 +369,7 @@ onBeforeUnmount(() => {
} }
.simul { .simul {
position: absolute; position: absolute;
bottom: 40px; top: 0;
right: 20px; right: 20px;
margin-left: 20px; margin-left: 20px;
z-index: 999; z-index: 999;

View File

@@ -59,14 +59,19 @@ const hideGlobalHint = () => {
showHint.value = false; showHint.value = false;
}; };
const restart = () => {
uni.restartMiniProgram({
path: "/pages/index",
});
};
const checkAudioProgress = async () => { const checkAudioProgress = async () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
audioInitProgress.value = AudioManager.getLoadProgress(); audioInitProgress.value = AudioManager.getLoadProgress();
if (audioInitProgress.value === 1) return resolve(); if (audioInitProgress.value === 1) return resolve();
audioTimer.value = setInterval(() => { audioTimer.value = setInterval(() => {
const result = AudioManager.getLoadProgress(); audioProgress.value = AudioManager.getLoadProgress();
audioProgress.value = result;
if (audioProgress.value === 1) { if (audioProgress.value === 1) {
setTimeout(() => { setTimeout(() => {
audioInitProgress.value = 1; audioInitProgress.value = 1;
@@ -220,17 +225,15 @@ const goCalibration = async () => {
/> />
<view> <view>
<view :style="{ width: `${audioFinalProgress * 100}%` }"> <view :style="{ width: `${audioFinalProgress * 100}%` }">
<image <!-- <image
src="https://static.shelingxingqiu.com/attachment/2025-11-24/degu91a7si77sg9jqv.png" src="https://static.shelingxingqiu.com/attachment/2025-11-24/degu91a7si77sg9jqv.png"
mode="widthFix" mode="widthFix"
/> /> -->
</view> </view>
</view> </view>
<view> <view>
<text>若加载时间过长</text> <text>若加载时间过长</text>
<button hover-class="none" @click="AudioManager.reloadAll"> <button hover-class="none" @click="restart">点击这里重启</button>
点击这里重启
</button>
</view> </view>
</view> </view>
</view> </view>
@@ -290,7 +293,7 @@ const goCalibration = async () => {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
background: rgb(0 0 0 / 0.6); background: rgb(0 0 0 / 0.8);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -304,7 +307,7 @@ const goCalibration = async () => {
.audio-progress > view:nth-child(2) { .audio-progress > view:nth-child(2) {
width: 380rpx; width: 380rpx;
height: 6rpx; height: 6rpx;
background: #000000; background: #595959;
border-radius: 4rpx; border-radius: 4rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -328,7 +331,6 @@ const goCalibration = async () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 20rpx;
} }
.audio-progress > view:nth-child(3) > text { .audio-progress > view:nth-child(3) > text {
font-size: 22rpx; font-size: 22rpx;
@@ -340,5 +342,6 @@ const goCalibration = async () => {
font-size: 22rpx; font-size: 22rpx;
color: #ffe431; color: #ffe431;
line-height: 32rpx; line-height: 32rpx;
padding: 20rpx 0;
} }
</style> </style>

View File

@@ -1,30 +1,34 @@
<script setup> <script setup>
import useStore from "@/store";
import { storeToRefs } from "pinia";
const { user } = storeToRefs(useStore());
defineProps({ defineProps({
avatar: { player: {
type: String, type: Object,
default: "", default: () => ({}),
},
name: {
type: String,
default: "",
}, },
scores: { scores: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
}); });
const rowCount = new Array(6).fill(0); const rowCount = new Array(6).fill(0);
</script> </script>
<template> <template>
<view class="container"> <view
class="container"
:style="{ borderColor: player.id === user.id ? '#FED847' : '#fff3' }"
>
<image <image
:style="{ opacity: scores.length === 12 ? 1 : 0 }" :style="{ opacity: scores.length === 12 ? 1 : 0 }"
src="../static/checked-green.png" src="../static/checked-green.png"
mode="widthFix" mode="widthFix"
/> />
<image :src="avatar || '../static/user-icon.png'" mode="widthFix" /> <image :src="player.avatar || '../static/user-icon.png'" mode="widthFix" />
<text>{{ name }}</text> <text>{{ player.name }}</text>
<view> <view>
<view> <view>
<view v-for="(_, index) in rowCount" :key="index"> <view v-for="(_, index) in rowCount" :key="index">

View File

@@ -0,0 +1,70 @@
<script setup>
import { ref } from "vue";
const props = defineProps({
onChange: {
type: Function,
default: () => {},
},
});
const mode = ref(true);
const onClick = () => {
mode.value = !mode.value;
props.onChange(mode.value);
};
</script>
<template>
<view
class="point-switcher"
:style="{ borderColor: mode ? '#D8D8D8' : '#53EF56' }"
>
<view
@click="onClick"
:style="{ transform: 'translateX(' + (mode ? '-58' : '2') + 'rpx)' }"
>
<text>放大</text>
<view :style="{ background: mode ? '#D8D8D8' : '#53EF56' }"></view>
<text>真实</text>
</view>
</view>
</template>
<style scoped>
.point-switcher {
width: 100rpx;
height: 40rpx;
border-radius: 22rpx;
border: 2rpx solid;
display: flex;
overflow: hidden;
}
.point-switcher > view {
position: relative;
display: flex;
align-items: center;
line-height: 40rpx;
color: #ffffff;
font-weight: 500;
font-size: 20rpx;
word-break: keep-all;
padding: 0 12rpx;
transition: all 0.3s ease;
}
.point-switcher > view > text:first-child {
color: #53ef56;
}
.point-switcher > view > view {
width: 36rpx;
height: 36rpx;
flex: 0 0 auto;
border-radius: 50%;
margin: 0 10rpx;
transition: all 0.3s ease;
}
.point-switcher > view > text:last-child {
color: #d8d8d8;
}
</style>

View File

@@ -9,6 +9,7 @@ import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue"; import Signin from "@/components/Signin.vue";
import BubbleTip from "@/components/BubbleTip.vue"; import BubbleTip from "@/components/BubbleTip.vue";
import ScreenHint2 from "@/components/ScreenHint2.vue"; import ScreenHint2 from "@/components/ScreenHint2.vue";
import { import {
getAppConfig, getAppConfig,
getRankListAPI, getRankListAPI,

View File

@@ -48,6 +48,7 @@ watch(
function recoverData(battleInfo) { function recoverData(battleInfo) {
uni.removeStorageSync("last-awake-time"); uni.removeStorageSync("last-awake-time");
// 注释用于测试
battleId.value = battleInfo.id; battleId.value = battleInfo.id;
players.value = [...battleInfo.blueTeam, ...battleInfo.redTeam]; players.value = [...battleInfo.blueTeam, ...battleInfo.redTeam];
players.value.forEach((p) => { players.value.forEach((p) => {
@@ -221,8 +222,7 @@ onHide(() => {
v-if="start" v-if="start"
v-for="(player, index) in playersSorted" v-for="(player, index) in playersSorted"
:key="index" :key="index"
:name="player.name" :player="player"
:avatar="player.avatar"
:scores="playersScores[player.id] || []" :scores="playersScores[player.id] || []"
/> />
</view> </view>