From 8ef64f8f42aca61c0b6201f0e75daf211d527aa2 Mon Sep 17 00:00:00 2001
From: linyimin <18316471919@139.com>
Date: Tue, 26 May 2026 11:43:35 +0800
Subject: [PATCH 1/2] =?UTF-8?q?feat=EF=BC=9A=E6=AF=94=E8=B5=9B=E4=BC=98?=
=?UTF-8?q?=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/audioManager.js | 53 +-
src/components/BackToGame.vue | 2 +-
src/components/Container.vue | 2 +-
src/components/Header.vue | 8 +-
src/components/TeamAvatars.vue | 54 +-
src/pages.json | 2 +-
src/pages/battle-room.vue | 2 +-
src/pages/friend-battle-result.vue | 4 +-
src/pages/match-page.vue | 6 +-
.../team-battle/components/AppBackground.vue | 91 ++
src/pages/team-battle/components/Avatar.vue | 123 ++
.../team-battle/components/BackToGame.vue | 138 ++
.../team-battle/components/BattleFooter.vue | 203 +++
.../team-battle/components/BattleHeader.vue | 200 +++
src/pages/team-battle/components/BowPower.vue | 40 +
.../team-battle/components/BowTarget.vue | 451 ++++++
.../team-battle/components/Container.vue | 353 +++++
src/pages/team-battle/components/Guide.vue | 60 +
src/pages/team-battle/components/Header.vue | 328 +++++
.../team-battle/components/HeaderProgress.vue | 83 ++
.../team-battle/components/IconButton.vue | 39 +
.../team-battle/components/PointSwitcher.vue | 71 +
.../team-battle/components/RoundEndTip.vue | 172 +++
src/pages/team-battle/components/SButton.vue | 96 ++
src/pages/team-battle/components/SModal.vue | 108 ++
.../team-battle/components/ScreenHint.vue | 89 ++
.../team-battle/components/ShootProgress2.vue | 215 +++
.../team-battle/components/TeamAvatars.vue | 150 ++
.../team-battle/components/TestDistance.vue | 193 +++
src/pages/team-battle/index.vue | 1277 +++++++++++++++++
30 files changed, 4575 insertions(+), 38 deletions(-)
create mode 100644 src/pages/team-battle/components/AppBackground.vue
create mode 100644 src/pages/team-battle/components/Avatar.vue
create mode 100644 src/pages/team-battle/components/BackToGame.vue
create mode 100644 src/pages/team-battle/components/BattleFooter.vue
create mode 100644 src/pages/team-battle/components/BattleHeader.vue
create mode 100644 src/pages/team-battle/components/BowPower.vue
create mode 100644 src/pages/team-battle/components/BowTarget.vue
create mode 100644 src/pages/team-battle/components/Container.vue
create mode 100644 src/pages/team-battle/components/Guide.vue
create mode 100644 src/pages/team-battle/components/Header.vue
create mode 100644 src/pages/team-battle/components/HeaderProgress.vue
create mode 100644 src/pages/team-battle/components/IconButton.vue
create mode 100644 src/pages/team-battle/components/PointSwitcher.vue
create mode 100644 src/pages/team-battle/components/RoundEndTip.vue
create mode 100644 src/pages/team-battle/components/SButton.vue
create mode 100644 src/pages/team-battle/components/SModal.vue
create mode 100644 src/pages/team-battle/components/ScreenHint.vue
create mode 100644 src/pages/team-battle/components/ShootProgress2.vue
create mode 100644 src/pages/team-battle/components/TeamAvatars.vue
create mode 100644 src/pages/team-battle/components/TestDistance.vue
create mode 100644 src/pages/team-battle/index.vue
diff --git a/src/audioManager.js b/src/audioManager.js
index 44fe3a5..a0f55bb 100644
--- a/src/audioManager.js
+++ b/src/audioManager.js
@@ -1,3 +1,6 @@
+export const AUDIO_INTERRUPTION_BEGIN_EVENT = "audio-interruption-begin";
+export const AUDIO_INTERRUPTION_END_EVENT = "audio-interruption-end";
+
export const audioFils = {
tententen: "https://static.shelingxingqiu.com/shootmini/static/audio/tententen.mp3",
点击按钮: "https://static.shelingxingqiu.com/shootmini/static/audio/%E7%82%B9%E5%87%BB%E6%8C%89%E9%92%AE.mp3",
@@ -97,7 +100,7 @@ function debugLog(...args) {
const envVersion = accountInfo.miniProgram.envVersion;
// 只在体验版打印日志,正式版(release)和开发版(develop)不打印
- if (envVersion === "trial") {
+ if (envVersion === "trial" || envVersion === "develop") {
console.log(...args);
}
}
@@ -127,6 +130,7 @@ class AudioManager {
// 防重复播放保护
this.lastPlayKey = null;
this.lastPlayAt = 0;
+ this.isInterrupted = false;
// 静音开关
this.isMuted = false;
@@ -141,10 +145,41 @@ class AudioManager {
this.localFileCache = uni.getStorageSync("audio_local_files") || {};
// 启动时自动清理过期的缓存文件(URL 已不在 audioFils 中的文件)
this.cleanObsoleteCache();
+ this.bindAudioInterruptionEvents();
this.initAudios();
}
+ bindAudioInterruptionEvents() {
+ if (this._audioInterruptionBound) return;
+ this._audioInterruptionBound = true;
+
+ const begin = () => {
+ if (this.isInterrupted) return;
+ this.isInterrupted = true;
+ this.stopAll();
+ this.isSequenceRunning = false;
+ this.sequenceQueue = [];
+ this.sequenceIndex = 0;
+ this.pendingPlayKey = null;
+ uni.$emit(AUDIO_INTERRUPTION_BEGIN_EVENT);
+ };
+
+ const end = () => {
+ if (!this.isInterrupted) return;
+ this.isInterrupted = false;
+ uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
+ void this.reloadAll();
+ };
+
+ if (typeof uni?.onAudioInterruptionBegin === "function") {
+ uni.onAudioInterruptionBegin(begin);
+ }
+ if (typeof uni?.onAudioInterruptionEnd === "function") {
+ uni.onAudioInterruptionEnd(end);
+ }
+ }
+
// 清理不再使用的缓存文件
cleanObsoleteCache() {
const activeUrls = new Set(Object.values(audioFils));
@@ -461,6 +496,10 @@ class AudioManager {
// 播放指定音频或音频数组(数组则按顺序连续播放)
play(input, interrupt = true) {
+ if (this.isInterrupted) {
+ debugLog("音频处理中断状态,忽略播放请求");
+ return;
+ }
// 统一规范化为队列
let queue = [];
if (Array.isArray(input)) {
@@ -514,6 +553,10 @@ class AudioManager {
// 内部方法:播放单个 key
_playSingle(key, forceStopAll = false) {
+ if (this.isInterrupted) {
+ debugLog(`音频处理中断状态,跳过播放: ${key}`);
+ return;
+ }
// 200ms 内的同 key 重复播放直接忽略,避免“比比赛开始”这类重复首音
const now = Date.now();
if (this.lastPlayKey === key && now - this.lastPlayAt < 250) {
@@ -557,7 +600,13 @@ class AudioManager {
// 显式授权播放并立即播放
this.allowPlayMap.set(key, true);
- audio.play();
+ try {
+ audio.play();
+ } catch (err) {
+ this.allowPlayMap.set(key, false);
+ debugLog(`音频 ${key} 播放调用失败`, err?.errMsg || err);
+ return;
+ }
this.currentPlayingKey = key;
this.lastPlayKey = key;
this.lastPlayAt = Date.now();
diff --git a/src/components/BackToGame.vue b/src/components/BackToGame.vue
index 43057af..b5a509c 100644
--- a/src/components/BackToGame.vue
+++ b/src/components/BackToGame.vue
@@ -54,7 +54,7 @@ const onClick = debounce(async () => {
await uni.$checkAudio();
if (result.mode <= 3) {
uni.navigateTo({
- url: `/pages/team-battle?battleId=${result.matchId}`,
+ url: `/pages/team-battle/index?battleId=${result.matchId}`,
});
} else {
uni.navigateTo({
diff --git a/src/components/Container.vue b/src/components/Container.vue
index 280959c..36840f2 100644
--- a/src/components/Container.vue
+++ b/src/components/Container.vue
@@ -116,7 +116,7 @@ const backToGame = debounce(async () => {
await checkAudioProgress();
if (result.mode <= 3) {
uni.navigateTo({
- url: `/pages/team-battle?battleId=${result.matchId}`,
+ url: `/pages/team-battle/index?battleId=${result.matchId}`,
});
} else {
uni.navigateTo({
diff --git a/src/components/Header.vue b/src/components/Header.vue
index 828b0a8..8dadfcd 100644
--- a/src/components/Header.vue
+++ b/src/components/Header.vue
@@ -185,7 +185,13 @@ onBeforeUnmount(() => {
}}
-
+
diff --git a/src/components/TeamAvatars.vue b/src/components/TeamAvatars.vue
index 37bd8c8..8a41949 100644
--- a/src/components/TeamAvatars.vue
+++ b/src/components/TeamAvatars.vue
@@ -1,5 +1,5 @@
@@ -70,7 +74,7 @@ watch(
/>
{
if (battle && battle.matchId) {
uni.showToast({ title: "匹配成功,正在进入...", icon: "none" });
if (battle.mode <= 3) {
- uni.redirectTo({ url: `/pages/team-battle?battleId=${battle.matchId}` });
+ uni.redirectTo({ url: `/pages/team-battle/index?battleId=${battle.matchId}` });
} else {
uni.redirectTo({ url: `/pages/melee-battle?battleId=${battle.matchId}` });
}
@@ -76,7 +76,7 @@ async function cancelMatch() {
const battle = await getBattleAPI();
if (battle && battle.matchId) {
if (battle.mode <= 3) {
- uni.redirectTo({ url: `/pages/team-battle?battleId=${battle.matchId}` });
+ uni.redirectTo({ url: `/pages/team-battle/index?battleId=${battle.matchId}` });
} else {
uni.redirectTo({ url: `/pages/melee-battle?battleId=${battle.matchId}` });
}
@@ -100,7 +100,7 @@ async function onReceiveMessage(msg) {
// mode <= 3 为团队对抗,mode > 3 为大乱斗,覆盖全部 gameType(1~5),不再遗漏
if (msg.mode <= 3) {
uni.redirectTo({
- url: `/pages/team-battle?battleId=${msg.id}`,
+ url: `/pages/team-battle/index?battleId=${msg.id}`,
});
} else {
uni.redirectTo({
diff --git a/src/pages/team-battle/components/AppBackground.vue b/src/pages/team-battle/components/AppBackground.vue
new file mode 100644
index 0000000..134b66e
--- /dev/null
+++ b/src/pages/team-battle/components/AppBackground.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/Avatar.vue b/src/pages/team-battle/components/Avatar.vue
new file mode 100644
index 0000000..c5ba5bb
--- /dev/null
+++ b/src/pages/team-battle/components/Avatar.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+ {{ rank }}
+
+
+
+
+
diff --git a/src/pages/team-battle/components/BackToGame.vue b/src/pages/team-battle/components/BackToGame.vue
new file mode 100644
index 0000000..748db2e
--- /dev/null
+++ b/src/pages/team-battle/components/BackToGame.vue
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+ 返回进行中的对局
+
+
+ 返回房间
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/BattleFooter.vue b/src/pages/team-battle/components/BattleFooter.vue
new file mode 100644
index 0000000..c7c2442
--- /dev/null
+++ b/src/pages/team-battle/components/BattleFooter.vue
@@ -0,0 +1,203 @@
+=
+
+
+
+
+
+
+
+
+
+
+
+
+ 蓝队({{ bluePoints }})
+ 红队({{ redPoints }})
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ result.shoots[1] && result.shoots[1].length
+ ? result.shoots[1]
+ .map((item) => item.ring)
+ .reduce((last, next) => last + next, 0)
+ : ""
+ }}
+ 环
+
+
+
+
+
+
+
+ 环
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ result.shoots[2] && result.shoots[2].length
+ ? result.shoots[2]
+ .map((item) => item.ring)
+ .reduce((last, next) => last + next, 0)
+ : ""
+ }}
+ 环
+
+
+
+
+
+
+
+ 环
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/BattleHeader.vue b/src/pages/team-battle/components/BattleHeader.vue
new file mode 100644
index 0000000..633d086
--- /dev/null
+++ b/src/pages/team-battle/components/BattleHeader.vue
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+ {{ player.name }}
+
+
+
+
+
+
+ {{ player.name }}
+
+
+
+
+
+
+
+ players.length > 5 && e.stopPropagation()"
+ :style="{ paddingTop: showHeader ? '15px' : '0' }"
+ >
+
+
+
+ {{ player.name }}
+
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/BowPower.vue b/src/pages/team-battle/components/BowPower.vue
new file mode 100644
index 0000000..9153a2b
--- /dev/null
+++ b/src/pages/team-battle/components/BowPower.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+ 电量{{ power || 1 }}%
+
+
+
+
diff --git a/src/pages/team-battle/components/BowTarget.vue b/src/pages/team-battle/components/BowTarget.vue
new file mode 100644
index 0000000..18f82cc
--- /dev/null
+++ b/src/pages/team-battle/components/BowTarget.vue
@@ -0,0 +1,451 @@
+
+
+
+
+
+
+
+
+
+
+
+ 中场休息
+
+ 经验 +1
+
+ {{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
+ }}环
+
+
+ 经验 +1
+
+ {{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
+ }}环
+
+ {{ index + 1 }}
+
+
+
+ {{ index + 1 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/Container.vue b/src/pages/team-battle/components/Container.vue
new file mode 100644
index 0000000..5e3ce8e
--- /dev/null
+++ b/src/pages/team-battle/components/Container.vue
@@ -0,0 +1,353 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 完成进行中的对局才能开启新的。
+ 您有正在进行中的对局,是否进入?
+
+
+
+
+
+
+ 离开比赛可能会导致比赛失败,
+ 确认离开吗?
+
+
+
+
+
+
+ 今天不玩了吗?
+
+
+
+
+
+
+ 完成智能弓校准,即可解锁全部功能
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ loading ? loadingText || "加载中..." : "若加载时间过长,请" }}
+
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/Guide.vue b/src/pages/team-battle/components/Guide.vue
new file mode 100644
index 0000000..0400dd6
--- /dev/null
+++ b/src/pages/team-battle/components/Guide.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/Header.vue b/src/pages/team-battle/components/Header.vue
new file mode 100644
index 0000000..24a51e3
--- /dev/null
+++ b/src/pages/team-battle/components/Header.vue
@@ -0,0 +1,328 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+ 凹造型
+ -
+ 感知距离
+ -
+ 小试牛刀
+
+
+
+
+ {{ pointBook.bowType.name }}
+ {{ pointBook.distance }} 米
+ {{
+ pointBook.bowtargetType.name.substring(
+ 0,
+ pointBook.bowtargetType.name.length - 3
+ )
+ }}
+ {{
+ pointBook.bowtargetType.name.substring(
+ pointBook.bowtargetType.name.length - 3
+ )
+ }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/HeaderProgress.vue b/src/pages/team-battle/components/HeaderProgress.vue
new file mode 100644
index 0000000..ce1db3e
--- /dev/null
+++ b/src/pages/team-battle/components/HeaderProgress.vue
@@ -0,0 +1,83 @@
+
+
+
+
+ {{ (tips || "").replace(/你/g, "").replace(/重回/g, "") }}
+ ({{ currentShot }}/{{ totalShot }})
+
+
+
+
+
diff --git a/src/pages/team-battle/components/IconButton.vue b/src/pages/team-battle/components/IconButton.vue
new file mode 100644
index 0000000..bc466dd
--- /dev/null
+++ b/src/pages/team-battle/components/IconButton.vue
@@ -0,0 +1,39 @@
+
+
+
+
+ {{ name }}
+
+
+
+
diff --git a/src/pages/team-battle/components/PointSwitcher.vue b/src/pages/team-battle/components/PointSwitcher.vue
new file mode 100644
index 0000000..1edf96e
--- /dev/null
+++ b/src/pages/team-battle/components/PointSwitcher.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+ 放大
+
+ 真实
+
+
+
+
+
diff --git a/src/pages/team-battle/components/RoundEndTip.vue b/src/pages/team-battle/components/RoundEndTip.vue
new file mode 100644
index 0000000..cc9a6f2
--- /dev/null
+++ b/src/pages/team-battle/components/RoundEndTip.vue
@@ -0,0 +1,172 @@
+
+
+
+
+ 第{{ round }}轮射箭结束
+
+
+ 本轮蓝队
+ {{
+ (roundData.shoots[1] || []).reduce(
+ (last, next) => last + next.ring,
+ 0
+ )
+ }}
+ 环,红队
+ {{
+ (roundData.shoots[2] || []).reduce(
+ (last, next) => last + next.ring,
+ 0
+ )
+ }}
+ 环
+
+
+ 连续3个来回双方均无人射箭,比赛取消。
+
+
+ 红队、蓝队各得{{
+ redPoint
+ }}分
+
+
+ 蓝队获胜,得{{
+ bluePoint
+ }}分
+
+
+ 红队获胜,得{{
+ redPoint
+ }}分
+
+
+
+
+ 蓝队
+ {{ bluePoint }}
+ 分,红队
+ {{ redPoint }}
+ 分
+
+ 同分僵局!最后一箭定江山
+
+ {{ count }}
+ 秒后蓝红双方
+ 决金箭
+ 一箭决胜负
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/SButton.vue b/src/pages/team-battle/components/SButton.vue
new file mode 100644
index 0000000..a4d1bcf
--- /dev/null
+++ b/src/pages/team-battle/components/SButton.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/SModal.vue b/src/pages/team-battle/components/SModal.vue
new file mode 100644
index 0000000..3a0ba89
--- /dev/null
+++ b/src/pages/team-battle/components/SModal.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/ScreenHint.vue b/src/pages/team-battle/components/ScreenHint.vue
new file mode 100644
index 0000000..e3fb03d
--- /dev/null
+++ b/src/pages/team-battle/components/ScreenHint.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/team-battle/components/ShootProgress2.vue b/src/pages/team-battle/components/ShootProgress2.vue
new file mode 100644
index 0000000..89af288
--- /dev/null
+++ b/src/pages/team-battle/components/ShootProgress2.vue
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+ 剩余{{ remain }}秒
+ ···
+
+
+
+
+
diff --git a/src/pages/team-battle/components/TeamAvatars.vue b/src/pages/team-battle/components/TeamAvatars.vue
new file mode 100644
index 0000000..3eb7a3b
--- /dev/null
+++ b/src/pages/team-battle/components/TeamAvatars.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+ {{ isRed ? "红队" : "蓝队" }}
+
+ {{ firstName }}
+
+
+
+
diff --git a/src/pages/team-battle/components/TestDistance.vue b/src/pages/team-battle/components/TestDistance.vue
new file mode 100644
index 0000000..16a27e0
--- /dev/null
+++ b/src/pages/team-battle/components/TestDistance.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+ 请确保站距达到5米
+ 低于5米的射箭无效
+
+
+
+
+
+
+
+ 当前距离{{ distance }}米
+ 已达到距离要求
+ 请调整站位
+
+
+ 请射箭,测试站距
+
+
+
+
+
+
+
+
+
+
+ 具体正式比赛还有
+ {{ count }}
+ 秒
+
+ 进入中...
+
+
+
+
+
diff --git a/src/pages/team-battle/index.vue b/src/pages/team-battle/index.vue
new file mode 100644
index 0000000..b269f64
--- /dev/null
+++ b/src/pages/team-battle/index.vue
@@ -0,0 +1,1277 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 设备已离线
+ 检测到设备已断开连接,请检查设备后继续比赛
+ 我知道了
+
+
+
+
+
+
+
From c5a8100c380cced80c765202bca08ebe7498ba4f Mon Sep 17 00:00:00 2001
From: zhangyibo95 <690096405@qq.com>
Date: Tue, 26 May 2026 17:58:54 +0800
Subject: [PATCH 2/2] =?UTF-8?q?update:=E4=BF=AE=E5=A4=8D=E9=9D=B6=E7=BA=B8?=
=?UTF-8?q?=E8=B4=B4=E8=BE=B9=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/BowTarget.vue | 141 +++++++++++++-----
.../team-battle/components/BowTarget.vue | 141 +++++++++++++-----
2 files changed, 208 insertions(+), 74 deletions(-)
diff --git a/src/components/BowTarget.vue b/src/components/BowTarget.vue
index b1e6c5c..79ee41d 100644
--- a/src/components/BowTarget.vue
+++ b/src/components/BowTarget.vue
@@ -34,6 +34,18 @@ const props = defineProps({
type: Boolean,
default: false,
},
+ targetRadius: {
+ type: Number,
+ default: 20,
+ },
+ hitRadiusPx: {
+ type: Number,
+ default: 2,
+ },
+ zoomHitRadiusPx: {
+ type: Number,
+ default: 5,
+ },
});
const pMode = ref(true);
@@ -80,13 +92,79 @@ watch(
}
);
-function calcRealX(num, offset = 3.4) {
- const len = 20.4 + num;
- return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
+const safeTargetRadius = computed(() => {
+ const radius = Number(props.targetRadius);
+ return Number.isFinite(radius) && radius > 0 ? radius : 20;
+});
+
+const currentHitRadiusPx = computed(() => {
+ const radius = Number(
+ pMode.value ? props.zoomHitRadiusPx : props.hitRadiusPx
+ );
+ return Number.isFinite(radius) && radius >= 0 ? radius : 0;
+});
+
+function getShotPoint(shot, fallbackCenter = false) {
+ const x = Number(shot?.x);
+ const y = Number(shot?.y);
+ if (Number.isFinite(x) && Number.isFinite(y)) return { x, y };
+ return fallbackCenter ? { x: 0, y: 0 } : null;
}
-function calcRealY(num, offset = 3.4) {
- const len = num < 0 ? Math.abs(num) + 20.4 : 20.4 - num;
- return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
+
+function getPointDirection(point) {
+ if (!point) return null;
+ const distance = Math.sqrt(point.x * point.x + point.y * point.y);
+ if (distance === 0) return null;
+
+ return {
+ x: point.x / distance,
+ y: point.y / distance,
+ };
+}
+
+function formatPxOffset(value) {
+ if (!value) return "";
+ const operator = value > 0 ? "+" : "-";
+ return ` ${operator} ${Math.abs(value)}px`;
+}
+
+function formatTargetPosition(percent, offset) {
+ const pxOffset = formatPxOffset(offset);
+ return pxOffset ? `calc(${percent}%${pxOffset})` : `${percent}%`;
+}
+
+function getTargetPositionStyle(point, offsetPx = 0) {
+ if (!point) return { display: "none" };
+
+ const radius = safeTargetRadius.value;
+ const diameter = radius * 2;
+ const direction = getPointDirection(point);
+ const xOffset = direction ? direction.x * offsetPx : 0;
+ const yOffset = direction ? -direction.y * offsetPx : 0;
+ const leftPercent = ((point.x + radius) / diameter) * 100;
+ const topPercent = ((radius - point.y) / diameter) * 100;
+
+ return {
+ left: formatTargetPosition(leftPercent, xOffset),
+ top: formatTargetPosition(topPercent, yOffset),
+ transform: "translate(-50%, -50%)",
+ };
+}
+
+function getHitStyle(shot) {
+ const radius = currentHitRadiusPx.value;
+ const point = getShotPoint(shot);
+
+ return {
+ ...getTargetPositionStyle(point, radius),
+ width: `${radius * 2}px`,
+ height: `${radius * 2}px`,
+ };
+}
+
+function getTipStyle(shot) {
+ const point = getShotPoint(shot, true);
+ return getTargetPositionStyle(point, shot?.ring ? currentHitRadiusPx.value : 0);
}
const simulShoot = async () => {
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
@@ -169,20 +247,14 @@ onBeforeUnmount(() => {
经验 +1
{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
}}环
@@ -193,20 +265,14 @@ onBeforeUnmount(() => {
user.id === bluelatestOne.playerId
"
class="e-value fade-in-out"
- :style="{
- left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 20),
- top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 40),
- }"
+ :style="getTipStyle(bluelatestOne)"
>
经验 +1
{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
}}环
@@ -217,8 +283,7 @@ onBeforeUnmount(() => {
index === scores.length - 1 && latestOne ? 'pump-in' : ''
}`"
:style="{
- left: calcRealX(bow.x, pMode ? '3.4' : '2'),
- top: calcRealY(bow.y, pMode ? '3.4' : '2'),
+ ...getHitStyle(bow),
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
}"
>{{ index + 1 }} {
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
}`"
:style="{
- left: calcRealX(bow.x, pMode ? '3.4' : '2'),
- top: calcRealY(bow.y, pMode ? '3.4' : '2'),
+ ...getHitStyle(bow),
backgroundColor: '#1840FF',
}"
>
@@ -302,21 +366,11 @@ onBeforeUnmount(() => {
z-index: 1;
color: #fff;
transition: all 0.3s ease;
-}
-.s-point {
- width: 4px;
- height: 4px;
- min-width: 4px;
- min-height: 4px;
+ box-sizing: border-box;
}
.b-point {
- width: 10px;
- height: 10px;
- min-width: 10px;
- min-height: 10px;
border: 1px solid #fff;
z-index: 1;
- box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
@@ -332,6 +386,19 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%);*/
margin-top: 2rpx;
}
+@keyframes target-pump-in {
+ from {
+ transform: translate(-50%, -50%) scale(2);
+ }
+
+ to {
+ transform: translate(-50%, -50%) scale(1);
+ }
+}
+.hit.pump-in {
+ animation: target-pump-in 0.3s ease-out forwards;
+ transform-origin: center center;
+}
.header {
width: 100%;
display: flex;
diff --git a/src/pages/team-battle/components/BowTarget.vue b/src/pages/team-battle/components/BowTarget.vue
index 18f82cc..c855fea 100644
--- a/src/pages/team-battle/components/BowTarget.vue
+++ b/src/pages/team-battle/components/BowTarget.vue
@@ -38,6 +38,18 @@ const props = defineProps({
type: Boolean,
default: false,
},
+ targetRadius: {
+ type: Number,
+ default: 20,
+ },
+ hitRadiusPx: {
+ type: Number,
+ default: 2,
+ },
+ zoomHitRadiusPx: {
+ type: Number,
+ default: 5,
+ },
});
const pMode = ref(true);
@@ -75,13 +87,79 @@ watch(
{ immediate: true }
);
-function calcRealX(num, offset = 3.4) {
- const len = 20.4 + num;
- return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
+const safeTargetRadius = computed(() => {
+ const radius = Number(props.targetRadius);
+ return Number.isFinite(radius) && radius > 0 ? radius : 20;
+});
+
+const currentHitRadiusPx = computed(() => {
+ const radius = Number(
+ pMode.value ? props.zoomHitRadiusPx : props.hitRadiusPx
+ );
+ return Number.isFinite(radius) && radius >= 0 ? radius : 0;
+});
+
+function getShotPoint(shot, fallbackCenter = false) {
+ const x = Number(shot?.x);
+ const y = Number(shot?.y);
+ if (Number.isFinite(x) && Number.isFinite(y)) return { x, y };
+ return fallbackCenter ? { x: 0, y: 0 } : null;
}
-function calcRealY(num, offset = 3.4) {
- const len = num < 0 ? Math.abs(num) + 20.4 : 20.4 - num;
- return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
+
+function getPointDirection(point) {
+ if (!point) return null;
+ const distance = Math.sqrt(point.x * point.x + point.y * point.y);
+ if (distance === 0) return null;
+
+ return {
+ x: point.x / distance,
+ y: point.y / distance,
+ };
+}
+
+function formatPxOffset(value) {
+ if (!value) return "";
+ const operator = value > 0 ? "+" : "-";
+ return ` ${operator} ${Math.abs(value)}px`;
+}
+
+function formatTargetPosition(percent, offset) {
+ const pxOffset = formatPxOffset(offset);
+ return pxOffset ? `calc(${percent}%${pxOffset})` : `${percent}%`;
+}
+
+function getTargetPositionStyle(point, offsetPx = 0) {
+ if (!point) return { display: "none" };
+
+ const radius = safeTargetRadius.value;
+ const diameter = radius * 2;
+ const direction = getPointDirection(point);
+ const xOffset = direction ? direction.x * offsetPx : 0;
+ const yOffset = direction ? -direction.y * offsetPx : 0;
+ const leftPercent = ((point.x + radius) / diameter) * 100;
+ const topPercent = ((radius - point.y) / diameter) * 100;
+
+ return {
+ left: formatTargetPosition(leftPercent, xOffset),
+ top: formatTargetPosition(topPercent, yOffset),
+ transform: "translate(-50%, -50%)",
+ };
+}
+
+function getHitStyle(shot) {
+ const radius = currentHitRadiusPx.value;
+ const point = getShotPoint(shot);
+
+ return {
+ ...getTargetPositionStyle(point, radius),
+ width: `${radius * 2}px`,
+ height: `${radius * 2}px`,
+ };
+}
+
+function getTipStyle(shot) {
+ const point = getShotPoint(shot, true);
+ return getTargetPositionStyle(point, shot?.ring ? currentHitRadiusPx.value : 0);
}
const simulShoot = async () => {
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
@@ -164,20 +242,14 @@ onBeforeUnmount(() => {
经验 +1
{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
}}环
@@ -188,20 +260,14 @@ onBeforeUnmount(() => {
user.id === bluelatestOne.playerId
"
class="e-value fade-in-out"
- :style="{
- left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 20),
- top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 40),
- }"
+ :style="getTipStyle(bluelatestOne)"
>
经验 +1
{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
}}环
@@ -212,8 +278,7 @@ onBeforeUnmount(() => {
index === scores.length - 1 && latestOne ? 'pump-in' : ''
}`"
:style="{
- left: calcRealX(bow.x, pMode ? '3.4' : '2'),
- top: calcRealY(bow.y, pMode ? '3.4' : '2'),
+ ...getHitStyle(bow),
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
}"
>{{ index + 1 }} {
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
}`"
:style="{
- left: calcRealX(bow.x, pMode ? '3.4' : '2'),
- top: calcRealY(bow.y, pMode ? '3.4' : '2'),
+ ...getHitStyle(bow),
backgroundColor: '#1840FF',
}"
>
@@ -297,21 +361,11 @@ onBeforeUnmount(() => {
z-index: 1;
color: #fff;
transition: all 0.3s ease;
-}
-.s-point {
- width: 4px;
- height: 4px;
- min-width: 4px;
- min-height: 4px;
+ box-sizing: border-box;
}
.b-point {
- width: 10px;
- height: 10px;
- min-width: 10px;
- min-height: 10px;
border: 1px solid #fff;
z-index: 1;
- box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
@@ -327,6 +381,19 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%);*/
margin-top: 2rpx;
}
+@keyframes target-pump-in {
+ from {
+ transform: translate(-50%, -50%) scale(2);
+ }
+
+ to {
+ transform: translate(-50%, -50%) scale(1);
+ }
+}
+.hit.pump-in {
+ animation: target-pump-in 0.3s ease-out forwards;
+ transform-origin: center center;
+}
.header {
width: 100%;
display: flex;