diff --git a/src/apis.js b/src/apis.js
index 2873cb5..f00f561 100644
--- a/src/apis.js
+++ b/src/apis.js
@@ -8,7 +8,7 @@ try {
switch (envVersion) {
case "develop": // 开发版
- // BASE_URL = "http://192.168.1.5:8000/api/shoot";
+ // BASE_URL = "http://192.168.1.2:8000/api/shoot";
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
break;
case "trial": // 体验版
@@ -269,6 +269,10 @@ export const createPractiseV2API = (trainingType, difficultyLevel) => {
});
};
+export const getCurrentPractiseAPI = () => {
+ return request("GET", "/user/practice/current");
+};
+
export const startPractiseAPI = (id) => {
return request("POST", "/user/practice/begin", { id });
};
diff --git a/src/canvas.js b/src/canvas.js
index 7c5b66b..853fae6 100644
--- a/src/canvas.js
+++ b/src/canvas.js
@@ -635,7 +635,7 @@ export function renderScores(ctx, arrows = [], bgImg) {
);
} else {
ctx.drawImage(
- "../static/score-bg.png",
+ "/static/score-bg.png",
16 + (i % 9) * 30,
290 + Math.ceil((i + 1) / 9) * 30,
27,
@@ -657,7 +657,7 @@ export function renderScores(ctx, arrows = [], bgImg) {
ctx.drawImage(bgImg, 24 + rowIndex * 42, i > 5 ? 362 : 320, 38, 38);
} else {
ctx.drawImage(
- "../static/score-bg.png",
+ "/static/score-bg.png",
24 + rowIndex * 42,
i > 5 ? 362 : 320,
38,
@@ -706,22 +706,39 @@ export async function sharePractiseData(canvasId, type, user, data) {
);
const bgImg = await loadCanvasImage(canvas, bgImgSrc);
- const avatarImgPromise = loadImage(user.avatar).then((path) =>
- loadCanvasImage(canvas, path)
- );
- const lvlImgPromise = loadImage(user.lvlImage).then((path) =>
- loadCanvasImage(canvas, path)
- );
+ // 头像与段位框属于装饰图片,缺失时使用兜底或跳过,避免阻断分享。
+ const loadProfileImage = async (src, fallbackSrc = "", label = "") => {
+ const normalizedSrc =
+ typeof src === "string" && src.startsWith("../static/")
+ ? src.slice(2)
+ : src;
+ if (normalizedSrc) {
+ try {
+ const path = await loadImage(normalizedSrc);
+ return await loadCanvasImage(canvas, path);
+ } catch (error) {
+ console.warn(`share ${label} image load failed`, error);
+ }
+ }
+ return fallbackSrc ? loadCanvasImage(canvas, fallbackSrc) : null;
+ };
- let titleImageSrc = "../static/first-try-title.png";
+ const avatarImgPromise = loadProfileImage(
+ user?.avatar,
+ "/static/user-icon.png",
+ "avatar"
+ );
+ const lvlImgPromise = loadProfileImage(user?.lvlImage, "", "level");
+
+ let titleImageSrc = "/static/first-try-title.png";
if (type == 2) {
- titleImageSrc = "../static/practise-one-title.png";
+ titleImageSrc = "/static/practise-one-title.png";
} else if (type == 3) {
- titleImageSrc = "../static/practise-two-title.png";
+ titleImageSrc = "/static/practise-two-title.png";
}
const titleImgPromise = loadCanvasImage(canvas, titleImageSrc);
- const scoreBgImgPromise = loadCanvasImage(canvas, "../static/score-bg.png");
- const qrCodeImgPromise = loadCanvasImage(canvas, "../static/qr-code.png");
+ const scoreBgImgPromise = loadCanvasImage(canvas, "/static/score-bg.png");
+ const qrCodeImgPromise = loadCanvasImage(canvas, "/static/qr-code.png");
const [avatarImg, lvlImg, titleImg, scoreBgImg, qrCodeImg] =
await Promise.all([
@@ -738,8 +755,8 @@ export async function sharePractiseData(canvasId, type, user, data) {
ctx.drawImage(bgImg, 0, 0, width, height);
- drawRoundImage(ctx, avatarImg, 17, 20, 32, 32, 20);
- ctx.drawImage(lvlImg, 12, 15, 42, 42);
+ if (avatarImg) drawRoundImage(ctx, avatarImg, 17, 20, 32, 32, 20);
+ if (lvlImg) ctx.drawImage(lvlImg, 12, 15, 42, 42);
renderText(ctx, user.nickName, 13, "#fff", 58, 34);
renderRankTitle(ctx, user.lvlName);
diff --git a/src/components/BattleHeader.vue b/src/components/BattleHeader.vue
index 8bf6458..9cdc95e 100644
--- a/src/components/BattleHeader.vue
+++ b/src/components/BattleHeader.vue
@@ -38,7 +38,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true;
-
+
player.vip === true || player.sVip === true;
player.vip === true || player.sVip === true;
.container > image:first-child {
position: absolute;
width: 100%;
- top: -5px;
+ /* top: -5px; */
z-index: 1;
pointer-events: none;
}
diff --git a/src/components/BowTarget.vue b/src/components/BowTarget.vue
index 7da471d..910e3cf 100644
--- a/src/components/BowTarget.vue
+++ b/src/components/BowTarget.vue
@@ -12,7 +12,7 @@ import PointSwitcher from "@/components/PointSwitcher.vue";
import BowShotEffect from "@/components/BowShotEffect.vue";
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
-import { simulShootAPI } from "@/apis";
+import { simulShootAPI, laserAimAPI, laserCloseAPI } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
@@ -357,6 +357,14 @@ const simulShoot2 = async () => {
}
};
+const openAim = async () => {
+ await laserAimAPI();
+};
+
+const closeAim = async () => {
+ await laserCloseAPI();
+};
+
const env = computed(() => {
const accountInfo = uni.getAccountInfoSync();
return accountInfo.miniProgram.envVersion;
@@ -524,6 +532,8 @@ onBeforeUnmount(() => {
+
+
diff --git a/src/matchWebsocket.js b/src/matchWebsocket.js
index 28bc04d..4e4ad12 100644
--- a/src/matchWebsocket.js
+++ b/src/matchWebsocket.js
@@ -849,6 +849,7 @@ export function connectMatchWebSocket(options = {}) {
token,
mode,
requestPracticeInfoOnOpen = false,
+ appHideResumable = false,
force = false,
reconnecting = false,
reconnectReason = "",
@@ -920,6 +921,7 @@ export function connectMatchWebSocket(options = {}) {
isMelee:
Number.isFinite(normalizedMode) ? normalizedMode > 3 : undefined,
requestPracticeInfoOnOpen: requestPracticeInfoOnOpen === true,
+ appHideResumable: appHideResumable === true,
meleeHalfRest: isSameContext
? currentContext?.meleeHalfRest === true
: false,
@@ -1016,9 +1018,22 @@ export function connectMatchWebSocketFromNotice(notice, fallbackUserId) {
});
}
+export function setMatchAppHideResumable(enabled) {
+ if (!currentContext) return;
+ currentContext.appHideResumable = enabled === true;
+}
+
export function closeMatchWebSocket(options = {}) {
// 默认关闭时会发送 LEAVE;内部切换连接可通过 sendLeave:false 跳过。
- const { sendLeave: shouldSendLeave = true, reason = "manual" } = options;
+ const { reason = "manual" } = options;
+ // 可恢复训练切后台只断开传输层,不能把它上报成主动离场。
+ const shouldSendLeave =
+ options.sendLeave === undefined
+ ? !(
+ reason === "app-hide" &&
+ currentContext?.appHideResumable === true
+ )
+ : options.sendLeave === true;
manualClose = true;
clearReconnectTimer();
@@ -1051,6 +1066,7 @@ export default {
ServerMessageType,
connectMatchWebSocket,
connectMatchWebSocketFromNotice,
+ setMatchAppHideResumable,
closeMatchWebSocket,
forceReconnectMatchWebSocket,
handleMatchNetworkStatusChange,
diff --git a/src/pages/match-detail.vue b/src/pages/match-detail.vue
index b604584..e2bc460 100644
--- a/src/pages/match-detail.vue
+++ b/src/pages/match-detail.vue
@@ -13,10 +13,10 @@ const data = ref({
rounds: [],
});
const players = ref([]);
+const isLoading = ref(true);
+const loadError = ref("");
-onLoad(async (options) => {
- if (!options.battleId) return;
- battleId.value = options.battleId || "60510101693403136";
+const loadBattle = async () => {
const result = await getBattleAPI(battleId.value);
data.value = result;
if (result.mode > 3) {
@@ -62,6 +62,32 @@ onLoad(async (options) => {
players.value = [...rankedPlayers, ...unrankedPlayers];
}
+
+ // 数据和派生列表均已就绪后再关闭占位,避免空壳闪烁。
+ isLoading.value = false;
+};
+
+const requestBattle = () => {
+ if (!battleId.value) return;
+
+ isLoading.value = true;
+ loadError.value = "";
+ loadBattle().catch((error) => {
+ console.error("加载比赛详情失败:", error);
+ loadError.value = "赛况加载失败";
+ isLoading.value = false;
+ });
+};
+
+onLoad((options) => {
+ if (!options.battleId) {
+ loadError.value = "缺少比赛信息";
+ isLoading.value = false;
+ return;
+ }
+
+ battleId.value = options.battleId;
+ requestBattle();
});
const checkBowData = (selected) => {
@@ -80,6 +106,17 @@ const checkBowData = (selected) => {
+
+ 赛况加载中...
+
+
+ {{ loadError }}
+ 点击重新加载
+
{
width: 100%;
height: 100%;
}
+.page-state {
+ min-height: 60vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 26rpx;
+}
+.page-state--error {
+ color: rgba(255, 255, 255, 0.72);
+}
+.retry-text {
+ margin-top: 16rpx;
+ color: #fed847;
+}
.score-header,
.score-row {
display: flex;
diff --git a/src/pages/practise-one.vue b/src/pages/practise-one.vue
index 28390ad..251e055 100644
--- a/src/pages/practise-one.vue
+++ b/src/pages/practise-one.vue
@@ -1,6 +1,6 @@