diff --git a/src/App.vue b/src/App.vue
index c852548..b62dd14 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -22,7 +22,8 @@
const {
updateUser,
updateOnline,
- clearSessionState
+ clearSessionState,
+ clearDevice
} = store;
watch(
@@ -63,6 +64,11 @@
updateOnline(data.online);
}
+ function onDeviceBindInvalid() {
+ clearDevice();
+ uni.setStorageSync("calibration", false);
+ }
+
function onDeviceShoot() {
// audioManager.play("射箭声音")
}
@@ -78,6 +84,7 @@
uni.$on("update-user", emitUpdateUser);
uni.$on("update-online", emitUpdateOnline);
uni.$on("session-kicked-out", onSessionKickedOut);
+ uni.$on("device-bind-invalid", onDeviceBindInvalid);
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
@@ -91,6 +98,7 @@
uni.$off("update-user", emitUpdateUser);
uni.$off("update-online", emitUpdateOnline);
uni.$off("session-kicked-out", onSessionKickedOut);
+ uni.$off("device-bind-invalid", onDeviceBindInvalid);
websocket.closeWebSocket();
});
@@ -250,6 +258,69 @@
text-overflow: ellipsis;
}
+ .member-nickname {
+ position: relative;
+ display: inline-flex;
+ max-width: 100%;
+ overflow: hidden;
+ }
+
+ .member-nickname__text,
+ .member-nickname__shine {
+ display: block;
+ max-width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .member-nickname--vip .member-nickname__text {
+ color: #E7BA80;
+ }
+
+ .member-nickname--svip .member-nickname__text {
+ background: linear-gradient(90deg, #ffb86c, #ff4fd8, #7c5cff, #35d6ff);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ }
+
+ .member-nickname__shine {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 110deg,
+ transparent 0%,
+ transparent 38%,
+ rgba(255, 255, 255, 0.15) 45%,
+ rgba(255, 255, 255, 1) 50%,
+ rgba(255, 255, 255, 0.15) 55%,
+ transparent 62%,
+ transparent 100%
+ );
+ background-size: 220% 100%;
+ background-position: 120% 0;
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ pointer-events: none;
+ animation: memberNicknameShine 3.5s infinite ease-in-out;
+ }
+
+ @keyframes memberNicknameShine {
+ 0%,
+ 50% {
+ background-position: 120% 0;
+ }
+
+ 100% {
+ background-position: -200% 0;
+ }
+ }
+
.modal {
height: 100%;
display: flex;
diff --git a/src/apis.js b/src/apis.js
index 5ddb095..6d2b0c8 100644
--- a/src/apis.js
+++ b/src/apis.js
@@ -23,8 +23,9 @@ try {
console.error("获取环境信息失败,使用默认正式环境", e);
}
-// 统一处理业务接口请求,包含登录态、业务错误和 WiFi 连接空响应兼容。
-function request(method, url, data = {}) {
+const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot");
+
+function request(method, url, data = {}, baseUrl = BASE_URL) {
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
@@ -32,20 +33,17 @@ function request(method, url, data = {}) {
if (token) header.Authorization = `Bearer ${token || ""}`;
return new Promise((resolve, reject) => {
uni.request({
- url: `${BASE_URL}${url}`,
+ url: `${baseUrl}${url}`,
method,
header,
data,
timeout: 10000,
success: (res) => {
- if (url === "/user/hardwareBox/connectWifi" && res.statusCode === 200 && res.data && Object.keys(res.data).length === 0) {
- resolve({});
- return;
- }
if (res.data) {
const {code, data, message} = res.data;
if (code === 0) resolve(data);
else if (message) {
+ const error = {code, data, message};
if (message.indexOf("登录身份已失效") !== -1) {
console.log('1111111111111111111,token失效')
uni.removeStorageSync(
@@ -55,6 +53,10 @@ function request(method, url, data = {}) {
reject({ type: "AUTH_INVALID", message });
return;
}
+ if (message.indexOf("已达上限") !== -1) {
+ reject(error);
+ return;
+ }
if (message === "ROOM_FULL") {
resolve({full: true});
return;
@@ -75,6 +77,15 @@ function request(method, url, data = {}) {
resolve({binded: true});
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") {
uni.showToast({
title: "当前有未支付订单",
@@ -89,16 +100,14 @@ function request(method, url, data = {}) {
icon: "none",
});
}
- if (url === "/user/hardwareBox/connectWifi") {
- reject({code, message});
- return;
- }
uni.showToast({
title: message,
icon: "none",
});
+ reject(error);
+ return;
}
- reject("");
+ reject({code, data, message});
}
},
fail: (err) => {
@@ -167,6 +176,10 @@ export const getAppConfig = () => {
return request("GET", "/index/appConfig");
};
+export const getDailyCountAPI = () => {
+ return request("GET", "/index/dailyCount", {}, ADDONS_BASE_URL);
+};
+
export const getHomeData = (seasonId) => {
return request("GET", `/user/myHome?seasonId=${seasonId}`);
};
@@ -342,6 +355,9 @@ export const createOrderAPI = (vipId) => {
quanity: 1,
tradeType: "mini",
payType: "wxpay",
+ returnUrl: "",
+ remark: "",
+ mockTest: false,
});
};
@@ -457,26 +473,6 @@ export const getDeviceBatteryAPI = async () => {
return request("GET", "/user/device/battery");
};
-// 设备连接指定 WiFi,只下发 WiFi 凭证,不触发 OTA 升级。
-export const connectDeviceWifiAPI = async (ssid, password) => {
- return request("POST", "/user/hardwareBox/connectWifi", {ssid, password});
-};
-
-// 获取硬件盒子版本信息,用于判断当前设备是否需要 OTA 升级。
-export const getHardwareBoxVersionAPI = async () => {
- return request("GET", "/user/hardwareBox/version");
-};
-
-// 发送硬件盒子 OTA 更新指令,服务端会返回后续轮询使用的任务 ID。
-export const sendHardwareBoxUpdateAPI = async (data) => {
- return request("POST", "/user/hardwareBox/sendUpdate", data);
-};
-
-// 根据任务 ID 获取硬件盒子 OTA 更新状态。
-export const getHardwareBoxTaskStatusAPI = async (taskId) => {
- return request("GET", `/user/hardwareBox/taskStatus?taskId=${taskId}`);
-};
-
export const addNoteAPI = async (id, remark) => {
return request("POST", "/user/score/sheet/remark", {id, remark});
};
diff --git a/src/audioManager.js b/src/audioManager.js
index a0f55bb..242373d 100644
--- a/src/audioManager.js
+++ b/src/audioManager.js
@@ -131,6 +131,10 @@ class AudioManager {
this.lastPlayKey = null;
this.lastPlayAt = 0;
this.isInterrupted = false;
+ this.interruptedAt = 0;
+ this.interruptionFallbackMs = 5000;
+ this.playWatchdogMs = 8000;
+ this.playWatchdogTimers = new Map();
// 静音开关
this.isMuted = false;
@@ -157,6 +161,7 @@ class AudioManager {
const begin = () => {
if (this.isInterrupted) return;
this.isInterrupted = true;
+ this.interruptedAt = Date.now();
this.stopAll();
this.isSequenceRunning = false;
this.sequenceQueue = [];
@@ -168,6 +173,7 @@ class AudioManager {
const end = () => {
if (!this.isInterrupted) return;
this.isInterrupted = false;
+ this.interruptedAt = 0;
uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
void this.reloadAll();
};
@@ -350,9 +356,14 @@ class AudioManager {
const loadTimeout = setTimeout(() => {
debugLog(`音频 ${key} 加载超时`);
this.recordLoadFailure(key);
+ this.audioMap.delete(key);
try {
audio.destroy();
} catch (_) {}
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
if (callback) callback();
}, 10000);
@@ -386,7 +397,13 @@ class AudioManager {
}
this.recordLoadFailure(key);
this.audioMap.delete(key);
- audio.destroy();
+ try {
+ audio.destroy();
+ } catch (_) {}
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
if (this.readyMap.get(key)) {
// 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载
// this.readyMap.set(key, false);
@@ -396,19 +413,14 @@ class AudioManager {
});
audio.onEnded(() => {
- if (this.currentPlayingKey === key) {
- this.currentPlayingKey = null;
- }
- this.allowPlayMap.set(key, false);
- this.onAudioEnded(key);
- uni.$emit('audioEnded', key);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
});
audio.onStop(() => {
- if (this.currentPlayingKey === key) {
- this.currentPlayingKey = null;
- }
- this.allowPlayMap.set(key, false);
+ this.finishPlayback(key);
});
this.audioMap.set(key, audio);
@@ -446,11 +458,19 @@ class AudioManager {
});
} else {
this.recordLoadFailure(key);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
if (callback) callback();
}
},
fail: () => {
this.recordLoadFailure(key);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ });
if (callback) callback();
},
});
@@ -487,15 +507,137 @@ class AudioManager {
this.failedLoadKeys.add(key);
}
+ clearPlayWatchdog(key) {
+ const timer = this.playWatchdogTimers.get(key);
+ if (timer) {
+ clearTimeout(timer);
+ this.playWatchdogTimers.delete(key);
+ }
+ }
+
+ clearAllPlayWatchdogs() {
+ for (const timer of this.playWatchdogTimers.values()) {
+ clearTimeout(timer);
+ }
+ this.playWatchdogTimers.clear();
+ }
+
+ startPlayWatchdog(key) {
+ this.clearPlayWatchdog(key);
+ const timer = setTimeout(() => {
+ if (this.currentPlayingKey !== key) return;
+ debugLog(`音频 ${key} 播放超时,跳过当前音频并继续队列`);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
+ this.reloadAudioKey(key);
+ }, this.playWatchdogMs);
+ this.playWatchdogTimers.set(key, timer);
+ }
+
+ finishPlayback(key, { advanceSequence = false, emitEnded = false, force = false } = {}) {
+ const wasCurrent = this.currentPlayingKey === key;
+ const isSequenceCurrent =
+ this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key;
+
+ this.clearPlayWatchdog(key);
+ this.allowPlayMap.set(key, false);
+
+ if (!force && !wasCurrent && !isSequenceCurrent) return false;
+
+ if (wasCurrent) {
+ this.currentPlayingKey = null;
+ }
+
+ if (advanceSequence && isSequenceCurrent) {
+ this.onAudioEnded(key);
+ }
+
+ if (emitEnded) {
+ uni.$emit("audioEnded", key);
+ }
+
+ return true;
+ }
+
+ recoverFromInterruptionIfStale(force = false) {
+ if (!this.isInterrupted) return false;
+ const interruptedFor = Date.now() - (this.interruptedAt || Date.now());
+ if (!force && interruptedFor < this.interruptionFallbackMs) return false;
+
+ debugLog("音频中断状态超时,执行兜底恢复");
+ this.isInterrupted = false;
+ this.interruptedAt = 0;
+ uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
+ void this.reloadAll();
+ return true;
+ }
+
+ recoverIfStale(expectedKey) {
+ if (this.recoverFromInterruptionIfStale(true)) return;
+
+ const key =
+ expectedKey || this.currentPlayingKey || this.sequenceQueue[this.sequenceIndex];
+ if (!key) {
+ if (this.isSequenceRunning) {
+ this.sequenceQueue = [];
+ this.sequenceIndex = 0;
+ this.isSequenceRunning = false;
+ }
+ return;
+ }
+
+ const isStaleCurrent =
+ this.currentPlayingKey === key ||
+ (this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key);
+ if (!isStaleCurrent) return;
+
+ debugLog(`音频 ${key} 等待超时,执行轻量恢复`);
+ const audio = this.audioMap.get(key);
+ if (audio) {
+ try {
+ audio.stop();
+ } catch (_) {}
+ }
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
+ this.reloadAudioKey(key);
+ }
+
+ reloadAudioKey(key) {
+ const audio = this.audioMap.get(key);
+ if (audio) {
+ try {
+ audio.destroy();
+ } catch (_) {}
+ this.audioMap.delete(key);
+ }
+ this.readyMap.set(key, false);
+ this.retryLoadAudio(key);
+ }
+
// 重新加载音频
retryLoadAudio(key) {
+ this.clearPlayWatchdog(key);
const oldAudio = this.audioMap.get(key);
- if (oldAudio) oldAudio.destroy();
+ if (oldAudio) {
+ try {
+ oldAudio.destroy();
+ } catch (_) {}
+ }
this.createAudio(key);
}
// 播放指定音频或音频数组(数组则按顺序连续播放)
play(input, interrupt = true) {
+ if (this.isInterrupted) {
+ this.recoverFromInterruptionIfStale();
+ }
if (this.isInterrupted) {
debugLog("音频处理中断状态,忽略播放请求");
return;
@@ -553,6 +695,9 @@ class AudioManager {
// 内部方法:播放单个 key
_playSingle(key, forceStopAll = false) {
+ if (this.isInterrupted) {
+ this.recoverFromInterruptionIfStale();
+ }
if (this.isInterrupted) {
debugLog(`音频处理中断状态,跳过播放: ${key}`);
return;
@@ -561,6 +706,11 @@ class AudioManager {
const now = Date.now();
if (this.lastPlayKey === key && now - this.lastPlayAt < 250) {
debugLog(`忽略快速重复播放: ${key}`);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
return;
}
@@ -603,21 +753,34 @@ class AudioManager {
try {
audio.play();
} catch (err) {
- this.allowPlayMap.set(key, false);
+ this.finishPlayback(key, {
+ advanceSequence: true,
+ emitEnded: true,
+ force: true,
+ });
debugLog(`音频 ${key} 播放调用失败`, err?.errMsg || err);
return;
}
this.currentPlayingKey = key;
this.lastPlayKey = key;
this.lastPlayAt = Date.now();
+ this.startPlayWatchdog(key);
} else {
debugLog(`音频 ${key} 不存在,尝试重新加载...`);
this.retryLoadAudio(key);
+ let loadWaitTimer = null;
+ const cleanup = () => {
+ try {
+ uni.$off("audioLoaded", handler);
+ } catch (_) {}
+ if (loadWaitTimer) {
+ clearTimeout(loadWaitTimer);
+ loadWaitTimer = null;
+ }
+ };
const handler = (loadedKey) => {
if (loadedKey === key) {
- try {
- uni.$off("audioLoaded", handler);
- } catch (_) {}
+ cleanup();
// 再次校验是否存在且就绪
const a = this.audioMap.get(key);
if (a && this.readyMap.get(key)) {
@@ -628,6 +791,7 @@ class AudioManager {
try {
uni.$on("audioLoaded", handler);
} catch (_) {}
+ loadWaitTimer = setTimeout(cleanup, 12000);
}
}
@@ -653,6 +817,7 @@ class AudioManager {
// 停止指定音频
stop(key) {
const audio = this.audioMap.get(key);
+ this.clearPlayWatchdog(key);
if (audio) {
audio.stop();
this.allowPlayMap.set(key, false);
@@ -664,6 +829,7 @@ class AudioManager {
// 停止所有音频
stopAll() {
+ this.clearAllPlayWatchdogs();
for (const [k, audio] of this.audioMap.entries()) {
try {
audio.stop();
@@ -737,6 +903,7 @@ class AudioManager {
this.readyMap.clear();
this.failedLoadKeys.clear();
this.allowPlayMap.clear();
+ this.clearAllPlayWatchdogs();
this.currentPlayingKey = null;
this.sequenceQueue = [];
this.sequenceIndex = 0;
diff --git a/src/canvas.js b/src/canvas.js
index e5e41f4..7c5b66b 100644
--- a/src/canvas.js
+++ b/src/canvas.js
@@ -456,23 +456,29 @@ export const generateShareImage = async (canvasId, data) => {
// 2D 即时绘制,无需 ctx.draw()
} catch (e) {
console.error("generateShareImage 绘制失败:", e);
+ throw e;
}
};
// 顶部导入与工具方法
async function getCanvas2DContext(canvasId, targetWidth, targetHeight) {
- return new Promise((resolve) => {
+ return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery();
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
- const { node: canvas } = res[0] || {};
+ const canvasInfo = res && res[0];
+ const { node: canvas } = canvasInfo || {};
+ if (!canvas || typeof canvas.getContext !== "function") {
+ reject(new Error(`canvas ${canvasId} not found`));
+ return;
+ }
const ctx = canvas.getContext("2d");
const dpr = uni.getSystemInfoSync().pixelRatio || 1;
- const w = targetWidth || res[0].width;
- const h = targetHeight || res[0].height;
+ const w = targetWidth || canvasInfo.width;
+ const h = targetHeight || canvasInfo.height;
canvas.width = w * dpr;
canvas.height = h * dpr;
@@ -561,6 +567,7 @@ export const sharePointData = async (canvasId, data) => {
// 2D 即时绘制,无需 ctx.draw()
} catch (e) {
console.error("generateShareImage 绘制失败:", e);
+ throw e;
}
};
@@ -778,6 +785,7 @@ export async function sharePractiseData(canvasId, type, user, data) {
// 2D 模式下无需 ctx.draw()
} catch (err) {
console.log(err);
+ throw err;
}
}
diff --git a/src/components/AppBackground.vue b/src/components/AppBackground.vue
index 134b66e..00feb51 100644
--- a/src/components/AppBackground.vue
+++ b/src/components/AppBackground.vue
@@ -57,6 +57,12 @@ const props = defineProps({
src="https://static.shelingxingqiu.com/shootmini/static/rank/rank-bg.png"
mode="widthFix"
/>
+
diff --git a/src/components/AppFooter.vue b/src/components/AppFooter.vue
index 05f2ebe..bfe2831 100644
--- a/src/components/AppFooter.vue
+++ b/src/components/AppFooter.vue
@@ -8,7 +8,7 @@ const tabs = [
function handleTabClick(index) {
if (index === 0) {
uni.navigateTo({
- url: "/pages/be-vip",
+ url: "/pages/member/be-vip",
});
}
if (index === 1) {
diff --git a/src/components/BackToGame.vue b/src/components/BackToGame.vue
index b5a509c..5297d74 100644
--- a/src/components/BackToGame.vue
+++ b/src/components/BackToGame.vue
@@ -18,12 +18,14 @@ const props = defineProps({
},
});
const loading = ref(false);
+const navigating = ref(false);
/** 统一获取当前环境 token,用于守卫:无有效 token 时不发起接口请求 */
const getToken = () =>
uni.getStorageSync(`${uni.getAccountInfoSync().miniProgram.envVersion}_token`);
onShow(async () => {
+ navigating.value = false;
if (user.value.id && getToken()) {
setTimeout(async () => {
const state = await getUserGameState();
@@ -45,28 +47,35 @@ watch(
}
);
+const navigateOnce = (url) =>
+ new Promise((resolve, reject) => {
+ navigating.value = true;
+ uni.navigateTo({
+ url,
+ success: resolve,
+ fail: (error) => {
+ navigating.value = false;
+ reject(error);
+ },
+ });
+ });
+
const onClick = debounce(async () => {
- if (loading.value) return;
+ if (loading.value || navigating.value) return;
try {
loading.value = true;
const result = await getBattleAPI();
if (result && result.matchId) {
await uni.$checkAudio();
if (result.mode <= 3) {
- uni.navigateTo({
- url: `/pages/team-battle/index?battleId=${result.matchId}`,
- });
+ await navigateOnce(`/pages/team-battle/index?battleId=${result.matchId}`);
} else {
- uni.navigateTo({
- url: `/pages/melee-battle?battleId=${result.matchId}`,
- });
+ await navigateOnce(`/pages/melee-battle?battleId=${result.matchId}`);
}
return;
}
if (game.value.roomID) {
- uni.navigateTo({
- url: "/pages/battle-room?roomNumber=" + game.value.roomID,
- });
+ await navigateOnce("/pages/battle-room?roomNumber=" + game.value.roomID);
} else {
updateGame(false, "");
}
diff --git a/src/components/BattleHeader.vue b/src/components/BattleHeader.vue
index b12d84f..1ade377 100644
--- a/src/components/BattleHeader.vue
+++ b/src/components/BattleHeader.vue
@@ -27,6 +27,14 @@ defineProps({
default: true,
},
});
+
+const getMemberNicknameClass = (player = {}) => [
+ "member-nickname",
+ player.vip === true && player.sVip !== true ? "member-nickname--vip" : "",
+ player.sVip === true ? "member-nickname--svip" : "",
+];
+
+const isMember = (player = {}) => player.vip === true || player.sVip === true;
@@ -51,7 +59,16 @@ defineProps({
}"
>
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
@@ -183,6 +218,13 @@ defineProps({
text-overflow: ellipsis;
text-align: center;
}
+view.player-name {
+ justify-content: center;
+}
+.player-name .member-nickname__text,
+.player-name .member-nickname__shine {
+ font-size: 12px;
+}
.left-winner-badge {
position: absolute;
width: 50px;
diff --git a/src/components/BowData.vue b/src/components/BowData.vue
index 7854645..409f98f 100644
--- a/src/components/BowData.vue
+++ b/src/components/BowData.vue
@@ -1,4 +1,5 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/BowTarget.vue b/src/components/BowTarget.vue
index b1e6c5c..0c9745a 100644
--- a/src/components/BowTarget.vue
+++ b/src/components/BowTarget.vue
@@ -1,6 +1,7 @@
-
+
-
+
@@ -169,20 +387,14 @@ onBeforeUnmount(() => {
经验 +1
{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
}}环
@@ -193,52 +405,65 @@ 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="getExperienceTipStyle(bluelatestOne)"
>
经验 +1
{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
}}环
+
{{ index + 1 }}
+
{{ index + 1 }}
+
-
+
{{ title }}
@@ -151,12 +174,12 @@ onBeforeUnmount(() => {
- 凹造型
+ 箭前准备
-
感知距离 {
width: 36rpx;
height: 36rpx;
}
-.user-header > text:nth-child(2) {
+.user-header > text:nth-child(2),
+.user-header > .point-book-user-name {
font-weight: 500;
font-size: 30rpx;
color: #333333;
@@ -316,7 +340,7 @@ onBeforeUnmount(() => {
width: 156rpx;
height: 28rpx;
font-weight: 400;
- font-size: 20rpx;
+ font-size: 24rpx;
color: #ffffff;
text-align: center;
line-height: 28rpx;
diff --git a/src/components/ModalDialog.vue b/src/components/ModalDialog.vue
new file mode 100644
index 0000000..069a0f3
--- /dev/null
+++ b/src/components/ModalDialog.vue
@@ -0,0 +1,234 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+ {{ content }}
+
+
+
+
+
+ {{ cancelText }}
+
+
+ {{ confirmText }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/PlayerScore.vue b/src/components/PlayerScore.vue
index 54fea32..f32c431 100644
--- a/src/components/PlayerScore.vue
+++ b/src/components/PlayerScore.vue
@@ -15,6 +15,19 @@ const props = defineProps({
});
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 getMemberNicknameClass = (player = {}) => [
+ "member-nickname",
+ player.vip === true && player.sVip !== true ? "member-nickname--vip" : "",
+ player.sVip === true ? "member-nickname--svip" : "",
+];
@@ -31,27 +44,32 @@ const rowCount = new Array(6).fill(0);
mode="widthFix"
/>
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
- {{
- scores[0] && scores[0][index] ? `${scores[0][index].ring}环` : "-"
- }}
+ {{ getRingText(scores[0]?.[index]) }}
- {{
- scores[1] && scores[1][index] ? `${scores[1][index].ring}环` : "-"
- }}
+ {{ getRingText(scores[1]?.[index]) }}
{{
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)
}}环
@@ -96,6 +114,13 @@ const rowCount = new Array(6).fill(0);
text-overflow: ellipsis;
width: 20%;
}
+.player-score-name {
+ width: 20%;
+}
+.player-score-name .member-nickname__text,
+.player-score-name .member-nickname__shine {
+ font-size: 14px;
+}
.container > view:nth-child(4) {
display: flex;
flex-direction: column;
diff --git a/src/components/PointRankItem.vue b/src/components/PointRankItem.vue
index 863b024..7d52145 100644
--- a/src/components/PointRankItem.vue
+++ b/src/components/PointRankItem.vue
@@ -22,6 +22,15 @@ const props = defineProps({
const like = ref(props.data.ifLike);
const likeCount = ref(props.data.likeTotal || 0);
+const isMember = (data = {}) => data.vip === true || data.sVip === true;
+
+const getMemberNicknameClass = (data = {}) => [
+ "point-rank-name",
+ "member-nickname",
+ data.vip === true && data.sVip !== true ? "member-nickname--vip" : "",
+ data.sVip === true ? "member-nickname--svip" : "",
+];
+
watch(
() => props.data,
(newVal) => {
@@ -53,7 +62,13 @@ const onClick = async () => {
- {{ data.name }}
+
+ {{ data.name }}
+
+ {{ data.name }}
+
+
+ {{ data.name }}
{{ data.totalDay }}天
@@ -118,6 +133,14 @@ const onClick = async () => {
color: #333333;
margin-bottom: 5rpx;
}
+.rank-item > view:nth-child(2) > view:last-child > .point-rank-name {
+ width: 200rpx;
+ margin-bottom: 5rpx;
+}
+.point-rank-name .member-nickname__text,
+.point-rank-name .member-nickname__shine {
+ font-size: 28rpx;
+}
.rank-item > view:nth-child(2) > view:last-child > view {
display: flex;
align-items: center;
diff --git a/src/components/ScoreResult.vue b/src/components/ScoreResult.vue
index 8447ac1..3464483 100644
--- a/src/components/ScoreResult.vue
+++ b/src/components/ScoreResult.vue
@@ -70,6 +70,11 @@ const arrows = computed(() => {
});
const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
+const isMember = computed(() => user.value.vip === true || user.value.sVip === true);
+const openCoachComment = () => {
+ if (!isMember.value) return;
+ showComment.value = true;
+};
@@ -112,9 +117,10 @@ const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
:onClick="onClickShare"
/>
-import { ref } from "vue";
+import { ref, watch } from "vue";
import { onShow } from "@dcloudio/uni-app";
import SModal from "@/components/SModal.vue";
import Avatar from "@/components/Avatar.vue";
@@ -16,7 +16,7 @@ import {
import useStore from "@/store";
const store = useStore();
-const { updateUser, updateDevice, updateOnline } = store;
+const { updateUser, updateDevice, updateOnline, clearDevice } = store;
const props = defineProps({
show: {
@@ -60,6 +60,21 @@ function onNicknameChange(e) {
nickName.value = e.detail.value;
}
+const resetForm = () => {
+ loading.value = false;
+ agree.value = false;
+ phone.value = "";
+ avatarUrl.value = "";
+ nickName.value = "";
+};
+
+watch(
+ () => props.show,
+ (show) => {
+ if (show) resetForm();
+ }
+);
+
const handleLogin = async () => {
if (loading.value) return;
if (!phone.value) {
@@ -107,6 +122,8 @@ async function doLogin() {
);
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
+ } else {
+ clearDevice();
}
props.onClose();
} catch (error) {
@@ -137,11 +154,7 @@ const openPrivacyLink = () => {
};
onShow(() => {
- loading.value = false;
- agree.value = false;
- phone.value = "";
- avatarUrl.value = "";
- nickName.value = "";
+ resetForm();
});
@@ -187,10 +200,11 @@ onShow(() => {
昵称:
diff --git a/src/components/Swiper.vue b/src/components/Swiper.vue
index 043a91b..849812a 100644
--- a/src/components/Swiper.vue
+++ b/src/components/Swiper.vue
@@ -1,5 +1,5 @@
-
-
-
-
-
-
-
-
- VIP 介绍
-
-
-
-
-
-
-
-
- 会员续费
-
-
- (selectedVIP = index)"
- >
- {{ item.name }}
-
-
-
- 支付
-
-
- 我的订单
-
-
-
-
-
-
-
-
-
diff --git a/src/pages/first-try.vue b/src/pages/first-try.vue
index e33dc79..cc40e37 100644
--- a/src/pages/first-try.vue
+++ b/src/pages/first-try.vue
@@ -1,5 +1,5 @@
-
+
{
,这是新人必刷小任务,0基础小白也能快速掌握弓箭技巧和游戏规则哦~:)
这是我们人帅技高的高教练。首先,请按教练示范,尝试自己去做这些动作和手势吧。这位就是人帅技高的高教练!接下来请跟随教练指引,做好射箭前期准备。
+ 请按下方步骤完成智能弓校准,让瞄准器和靶子保持对齐。
你知道5米射程有多远吗?
在我们的排位赛中,射程小于5米的成绩无效、哦!建议平时练习距离至少5米。现在,来边射箭边调整你的站位点吧!
-
+
一切准备就绪
试着完成一个真正的弓箭手任务吧!
-
+
新手试炼场通关啦,优秀!
反曲弓运动基本知识和射灵世界系统规则你已Get,是不是挺容易呀:) {
src="https://static.shelingxingqiu.com/attachment/2025-11-17/deas80ef1sf9td0leq.png"
class="try-tip"
mode="widthFix"
- v-if="step === 3"
+ v-if="step === 4"
/>
-
+
-
-
+
+
+
+ {{ index + 1 }}
+ {{ guide.title }}
+
+
+
+ 请完成以上步骤校准智能弓
+
+
+
{
step === 1 ? "学会了,我摆得比教练还帅" : "我找到合适的点位了"
}}
- {{ stepButtonTexts[step] }}
+ {{ currentStepButtonText }}
@@ -301,4 +387,43 @@ const onClose = async () => {
width: calc(100% - 20px);
margin: 0 10px;
}
+.calibration-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+.calibration-guide {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ font-size: 26rpx;
+ color: #ffffff;
+ margin-bottom: 15rpx;
+}
+.calibration-guide > view {
+ width: 100%;
+ margin: 25rpx 0;
+ display: flex;
+ align-items: center;
+}
+.calibration-guide > view > text:first-child {
+ font-size: 24rpx;
+ background: #e89024;
+ border-radius: 50%;
+ width: 32rpx;
+ height: 32rpx;
+ line-height: 32rpx;
+ display: block;
+ text-align: center;
+ margin-right: 15rpx;
+}
+.calibration-guide > image {
+ width: 630rpx;
+ height: 250rpx;
+}
+.calibration-container > text {
+ font-size: 24rpx;
+ color: #fff9;
+ margin: 30rpx;
+}
diff --git a/src/pages/friend-battle-result.vue b/src/pages/friend-battle-result.vue
index f231ce2..4139afd 100644
--- a/src/pages/friend-battle-result.vue
+++ b/src/pages/friend-battle-result.vue
@@ -92,6 +92,14 @@ const mvpTeam = computed(() => {
return blueTeamPlayers.value.some((p) => p.id === mvpPlayer.value.id) ? 1 : 2;
});
+const isMember = (player = {}) => player.vip === true || player.sVip === true;
+
+const getMemberNicknameClass = (player = {}) => [
+ "member-nickname",
+ player.vip === true && player.sVip !== true ? "member-nickname--vip" : "",
+ player.sVip === true ? "member-nickname--svip" : "",
+];
+
/**
* 激励语图片 URL(在 onLoad 中确定 ifWin 后赋值,避免 Math.random 放在 computed 里产生缓存不一致问题)
*/
@@ -128,6 +136,8 @@ const meleeRankList = computed(() => {
id: p.id,
avatar: p.avatar || "",
name: p.name || "",
+ vip: p.vip,
+ sVip: p.sVip,
// rank_lvl 字段可能缺失,缺失时显示空字符串,避免 getLvlName(undefined) 返回错误段位名
lvlName: p.rank_lvl != null ? getLvlName(p.rank_lvl) : "",
totalRing: resultItem.totalRing ?? 0,
@@ -305,7 +315,7 @@ function goBack() {
@@ -332,7 +342,20 @@ function goBack() {
- {{ p.name }}
+
+ {{ p.name }}
+
+ {{ p.name }}
+
+
+ {{ p.name }}
@@ -340,7 +363,20 @@ function goBack() {
- {{ p.name }}
+
+ {{ p.name }}
+
+ {{ p.name }}
+
+
+ {{ p.name }}
@@ -385,7 +421,16 @@ function goBack() {
:size="53"
:borderColor="mvpTeam === 1 ? '#5FADFF' : '#FF6060'"
/>
- {{ mvpPlayer.name }}
+
+ {{ mvpPlayer.name }}
+
+ {{ mvpPlayer.name }}
+
+
+ {{ mvpPlayer.name }}
@@ -433,7 +478,16 @@ function goBack() {
- {{ item.name }}
+
+ {{ item.name }}
+
+ {{ item.name }}
+
+
+ {{ item.name }}
{{ item.lvlName }}
@@ -613,6 +667,12 @@ function goBack() {
white-space: nowrap;
}
+.player-name .member-nickname__text,
+.player-name .member-nickname__shine {
+ font-size: 22rpx;
+ font-weight: 400;
+}
+
/* ---- 得分行 ---- */
.vs-scores-row {
position: relative;
@@ -752,6 +812,11 @@ function goBack() {
white-space: nowrap;
}
+.mvp-name .member-nickname__text,
+.mvp-name .member-nickname__shine {
+ font-size: 24rpx;
+}
+
/* ============================
查看完整成绩链接
============================ */
@@ -1072,6 +1137,12 @@ function goBack() {
white-space: nowrap;
}
+.rank-player-name .member-nickname__text,
+.rank-player-name .member-nickname__shine {
+ font-size: 28rpx;
+ font-weight: 500;
+}
+
.rank-player-lvl {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.5);
diff --git a/src/pages/friend-battle.vue b/src/pages/friend-battle.vue
index 2cbdda1..fd53ad6 100644
--- a/src/pages/friend-battle.vue
+++ b/src/pages/friend-battle.vue
@@ -1,5 +1,5 @@
-
-
+
+
-
+ v-for="level in levels"
+ :id="`level-${level}`"
+ :key="level"
+ :class="[
+ 'level-node',
+ level < currentLevel ? 'level-node--done' : '',
+ level === currentLevel ? 'level-node--current' : '',
+ ]"
+ >
+
+
+
+
+ {{ level }}
+
-
+
@@ -65,28 +100,80 @@ const { user } = storeToRefs(store);
.level-progress {
width: 100%;
- height: 32rpx;
- display: flex;
- justify-content: center;
- padding-top: 20rpx;
- padding-bottom: 40rpx;
+ white-space: nowrap;
+ padding: 24rpx 0 34rpx;
+ box-sizing: border-box;
}
-.progress-dot {
+.level-track {
+ display: inline-flex;
+ align-items: flex-start;
+ padding-left: 20rpx;
+ padding-right: 20rpx;
+}
+
+.level-node {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ width: 72rpx;
+ flex: 0 0 72rpx;
+}
+
+.level-node__top {
display: flex;
align-items: center;
+ width: 100%;
}
-.progress-dot > view:first-child {
- width: 3.8vw;
- height: 3.8vw;
+
+.level-node__dot {
+ width: 28rpx;
+ height: 28rpx;
border-radius: 50%;
- border: 1px solid #fff9;
+ border: 3rpx solid rgba(255, 255, 255, 0.45);
+ box-sizing: border-box;
}
-.progress-dot > view:last-child {
- width: 3.8vw;
- height: 1px;
- margin: 0 2px;
- background-color: #fff9;
+
+.level-node__line {
+ flex: 1;
+ height: 2rpx;
+ background-color: rgba(255, 255, 255, 0.45);
+}
+
+.level-node__label {
+ width: 28rpx;
+ margin-top: 14rpx;
+ color: rgba(255, 255, 255, 0.45);
+ font-size: 24rpx;
+ line-height: 28rpx;
+ text-align: center;
+}
+
+.level-node--done .level-node__dot {
+ background-color: #ffffff;
+ border-color: #ffffff;
+}
+
+.level-node--done .level-node__line {
+ background-color: rgba(255, 255, 255, 0.8);
+}
+
+.level-node--done .level-node__label {
+ color: rgba(255, 255, 255, 0.72);
+}
+
+.level-node--current .level-node__dot {
+ background-color: #fed847;
+ border-color: #fed847;
+}
+
+.level-node--current .level-node__line {
+ background-color: rgba(255, 255, 255, 0.45);
+}
+
+.level-node--current .level-node__label {
+ color: #fed847;
+ font-weight: 700;
}
.body {
diff --git a/src/pages/index.vue b/src/pages/index.vue
index c814f87..abc3f6a 100644
--- a/src/pages/index.vue
+++ b/src/pages/index.vue
@@ -30,6 +30,7 @@ const {
updateConfig,
updateUser,
updateDevice,
+ clearDevice,
getLvlName,
getLvlNameByScore,
updateOnline,
@@ -332,6 +333,8 @@ onShow(async (options) => {
);
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
+ } else {
+ clearDevice();
}
}
}
diff --git a/src/pages/match-detail.vue b/src/pages/match-detail.vue
index f5d5e38..b604584 100644
--- a/src/pages/match-detail.vue
+++ b/src/pages/match-detail.vue
@@ -67,7 +67,7 @@ onLoad(async (options) => {
const checkBowData = (selected) => {
if (data.value.mode <= 3) {
uni.navigateTo({
- url: `/pages/team-bow-data?battleId=${battleId.value}&selected=${selected}`,
+ url: `/pages/team-battle/team-bow-data?battleId=${battleId.value}&selected=${selected}`,
});
} else {
uni.navigateTo({
diff --git a/src/pages/match-page.vue b/src/pages/match-page.vue
index 11d2435..f02de99 100644
--- a/src/pages/match-page.vue
+++ b/src/pages/match-page.vue
@@ -3,12 +3,15 @@ import { ref, onMounted, onBeforeUnmount } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Matching from "@/components/Matching.vue";
+import ModalDialog from "@/components/ModalDialog.vue";
import { matchGameAPI, getBattleAPI } from "@/apis";
import { MESSAGETYPESV2 } from "@/constants";
+import { isLimitError } from "@/util";
const gameType = ref(0);
const teamSize = ref(0);
const onComplete = ref(null);
+const showLimitModal = ref(false);
/** 匹配超时计时器,用于检测 WS 消息丢失或真正超时 */
const matchTimeoutTimer = ref(null);
@@ -58,6 +61,18 @@ async function stopMatch() {
uni.$showHint(3);
}
+const closeLimitModal = () => {
+ showLimitModal.value = false;
+ uni.navigateBack();
+};
+
+const goVipPage = () => {
+ showLimitModal.value = false;
+ uni.redirectTo({
+ url: "/pages/member/be-vip",
+ });
+};
+
/**
* 取消匹配,带容错处理:
* - 取消成功 → 返回大厅
@@ -136,10 +151,19 @@ onBeforeUnmount(() => {
onShow(async () => {
if (gameType.value && teamSize.value) {
- matchGameAPI(true, gameType.value, teamSize.value);
- // 启动超时计时器,防止 WS 消息丢失或长时间无对手导致用户卡死
- clearMatchTimeout();
- matchTimeoutTimer.value = setTimeout(handleMatchTimeout, MATCH_TIMEOUT_MS);
+ try {
+ await matchGameAPI(true, gameType.value, teamSize.value);
+ // 启动超时计时器,防止 WS 消息丢失或长时间无对手导致用户卡死
+ clearMatchTimeout();
+ matchTimeoutTimer.value = setTimeout(handleMatchTimeout, MATCH_TIMEOUT_MS);
+ } catch (error) {
+ clearMatchTimeout();
+ if (isLimitError(error)) {
+ showLimitModal.value = true;
+ return;
+ }
+ uni.navigateBack();
+ }
}
});
@@ -154,6 +178,14 @@ onHide(() => {
+
diff --git a/src/pages/member/agreementData.js b/src/pages/member/agreementData.js
new file mode 100644
index 0000000..969a40d
--- /dev/null
+++ b/src/pages/member/agreementData.js
@@ -0,0 +1,322 @@
+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;
+};
diff --git a/src/pages/member/be-vip.vue b/src/pages/member/be-vip.vue
new file mode 100644
index 0000000..c42215f
--- /dev/null
+++ b/src/pages/member/be-vip.vue
@@ -0,0 +1,885 @@
+
+
+
+
+
+
+
+ {{ item.tab }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 有效期至:{{ getActiveVipExpiredDate(type) }}
+
+
+
+
+ 订单管理
+
+
+
+
+
+
+ {{ type.benefitTitle }}
+
+
+
+
+
+
+
+
+ {{ benefit.label }}
+
+
+
+
+
+
+
+
+
+ {{ pack.name }}
+
+ ¥
+ {{ pack.price }}
+
+
+ ¥{{ pack.original }}
+
+
+
+
+
+
+
+
+
+
+ 支付即同意
+ 《 会员自动续费服务协议》
+ 《 扣款授权服务协议》
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/order-detail.vue b/src/pages/member/order-detail.vue
similarity index 81%
rename from src/pages/order-detail.vue
rename to src/pages/member/order-detail.vue
index 9746055..ebd0f8d 100644
--- a/src/pages/order-detail.vue
+++ b/src/pages/member/order-detail.vue
@@ -61,6 +61,18 @@ const goPay = async () => {
}
};
+const copyOrderId = (orderId) => {
+ uni.setClipboardData({
+ data: String(orderId),
+ success: () => {
+ uni.showToast({
+ title: "复制成功",
+ icon: "success",
+ });
+ },
+ });
+};
+
const cancelOrder = async () => {
const result = await cancelOrderListAPI(data.value.orderId);
data.value = result;
@@ -78,8 +90,13 @@ const cancelOrder = async () => {
>
- 商品名:{{ data.vipName }}
- 订单号:{{ data.orderId }}
+ {{ data.vipName }}
+
+ 订单号:{{ data.orderId }}
+ 复制
+
下单时间:{{ data.vipCreateAt }}
支付时间:{{
@@ -141,4 +158,25 @@ const cancelOrder = async () => {
text-align: center;
font-size: 11px;
}
+
+.order-number {
+ display: flex;
+ align-items: center;
+ color: #666666;
+ font-size: 26rpx;
+ margin-top: 10rpx;
+}
+.order-number > text:first-child {
+ flex: 1;
+ min-width: 0;
+ word-break: break-all;
+}
+.copy-action {
+ flex-shrink: 0;
+ margin-left: 16rpx;
+ padding: 2rpx 14rpx;
+ color: #1f6ed4;
+ font-size: 24rpx;
+ line-height: 34rpx;
+}
diff --git a/src/pages/orders.vue b/src/pages/member/orders.vue
similarity index 57%
rename from src/pages/orders.vue
rename to src/pages/member/orders.vue
index 79bb987..5ddd8fe 100644
--- a/src/pages/orders.vue
+++ b/src/pages/member/orders.vue
@@ -3,6 +3,7 @@ import { ref, onMounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ScrollList from "@/components/ScrollList.vue";
+import ModalDialog from "@/components/ModalDialog.vue";
import { getOrderListAPI } from "@/apis";
import useStore from "@/store";
import { orderStatusNames, getStatusColor } from "@/constants";
@@ -10,13 +11,34 @@ import { storeToRefs } from "pinia";
const store = useStore();
const { user, config } = storeToRefs(store);
+const autoRenewDialogVisible = ref(false);
+const selectedRenewOrder = ref(null);
+
const toDetailPage = (detail) => {
uni.setStorageSync("order", detail);
uni.navigateTo({
- url: `/pages/order-detail`,
+ url: "/pages/member/order-detail",
});
};
+const openAutoRenewDialog = (detail) => {
+ selectedRenewOrder.value = detail;
+ autoRenewDialogVisible.value = true;
+};
+
+const closeAutoRenewDialog = () => {
+ autoRenewDialogVisible.value = false;
+ selectedRenewOrder.value = null;
+};
+
+const confirmAutoRenewDialog = () => {
+ autoRenewDialogVisible.value = false;
+ uni.showToast({
+ title: "功能实现中",
+ icon: "none",
+ });
+}
+
const list = ref([]);
const onLoading = async (page) => {
@@ -44,7 +66,7 @@ onShow(() => {
-
+
{
>{{ orderStatusNames[item.orderStatus] }}
{{ item.vipName }}
-
-
- 订单号:{{ item.orderId }}
+ 创建时间:{{ item.orderCreateAt }}
+
金额:{{ item.total }} 元
- 支付方式:微信
+
+
+
+
@@ -78,15 +113,15 @@ onShow(() => {
width: 100%;
height: 100%;
background-color: #f5f5f5;
- padding-top: 10px;
+ padding-top: 16rpx;
}
.order-item {
position: relative;
background-color: #fff;
- margin-bottom: 10px;
+ margin-bottom: 16rpx;
display: flex;
flex-direction: column;
- padding: 15px;
+ padding: 28rpx 30rpx 18rpx 30rpx;
}
.order-item > view:first-child {
position: absolute;
@@ -98,12 +133,20 @@ onShow(() => {
font-size: 11px;
}
.order-item > text:nth-child(2) {
- color: #000;
- font-size: 16px;
+ color: #333333;
+ font-size: 30rpx;
+ font-weight: bold;
}
.order-item > text {
color: #666666;
- font-size: 13px;
- margin-top: 5px;
+ font-size: 26rpx;
+ margin-bottom: 10rpx;
+}
+.order-item > .renew-action {
+ position: absolute;
+ right: 30rpx;
+ bottom: 18rpx;
+ color: #1f6ed4;
+ margin-bottom: 0;
}
diff --git a/src/pages/member/vip-intro.vue b/src/pages/member/vip-intro.vue
new file mode 100644
index 0000000..3c2584c
--- /dev/null
+++ b/src/pages/member/vip-intro.vue
@@ -0,0 +1,244 @@
+
+
+
+
+
+
+ 射灵星球会员权益
+
+
+ 核心特权:
+ 解锁约战、段位评级、实时排位赛、AI智能教练点评四大核心功能。
+
+
+
+ 专属服务:
+ 享全年不同阶段VIP专属客服,快速解决技术故障、规则疑问等所有问题。
+
+
+
+ 新用户福利:
+ 所有初次绑定设备的用户,免费赠送6个月普通会员。
+
+
+
+
+ 加入射灵星球,在真实射箭运动中体验在线竞技的乐趣,结识全球志同道合的弓友,持续享受新功能与系统升级,不断挑战自我,创造属于你的辉煌战绩!
+
+
+
+
+
+
+ 初次绑定设备赠送6个月
+
+
+
+ 特权
+ 基础用户
+ 普通会员
+ 超级会员
+
+
+ 专属落点标识
+ 无
+ 无
+ 螺旋
+
+
+ 专属命中效果
+ 无
+ 无
+ 玻璃裂纹
+
+
+ 箭矢飞行特效
+ 无
+ 无
+ 光箭
+
+
+ 每日约战次数
+ 2次
+ 22次
+ 无限
+
+
+ 每日排位赛次数
+ 2次
+ 22次
+ 无限
+
+
+ 教练点评
+ 无
+ 专享
+ 专享
+
+
+ 昵称美化
+ 无
+ 专享
+ 专享
+
+
+ 专属客服
+ 无
+ 专享
+ 专享
+
+
+
+
+ 会员时长叠加与生效规则
+
+
+ 等级优先级:
+
+ 同时拥有 超级会员 和 普通会员 时,优先使用超级会员权益,普通会员时长自动顺延,待 超级会员 到期后自动生效。
+
+
+
+
+ 连续套餐叠加:
+
+ 已有月 / 半年 / 年卡时再购买连续包月 / 包年,总有效期直接累加;连续套餐从下单日起算,下个周期正常自动扣费。
+
+
+
+
+
+ 示例:1 月 1 日买半年卡(7 月 1 日到期),1 月 10 日买连续包月,总有效期延至 8 月 1 日,8 月 1 日会发起首次自动扣款。
+
+
+
+
+ 升级超级会员规则:
+
+ 购买升级 超级会员 后立即生效,可升级时长以购买页面提示为准;未升级的剩余 普通会员 时长,将在 超级会员 到期后继续使用。
+
+
+
+
+
+
+
+
diff --git a/src/pages/mine-bow-data.vue b/src/pages/mine-bow-data.vue
index 1b5311e..22a57cb 100644
--- a/src/pages/mine-bow-data.vue
+++ b/src/pages/mine-bow-data.vue
@@ -11,12 +11,14 @@ import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const arrows = ref([]);
+const isSvip = ref(false);
const total = ref(0);
onLoad(async (options) => {
if (!options.id) return;
const result = await getPractiseAPI(options.id || 176);
arrows.value = result.details;
+ isSvip.value = result.sVip === true;
total.value = result.details.length;
});
@@ -34,7 +36,7 @@ onLoad(async (options) => {
-->
-
+
{{ arrows.length }}
diff --git a/src/pages/my-device.vue b/src/pages/my-device.vue
index a6f1327..94a7588 100644
--- a/src/pages/my-device.vue
+++ b/src/pages/my-device.vue
@@ -1,5 +1,5 @@
@@ -214,36 +239,49 @@ onShow(() => {
mode="widthFix"
:style="{ borderRadius: '50%' }"
/>
- {{ user.nickName }}
+
+ {{ user.nickName }}
+ {{
+ user.nickName
+ }}
+
-
- 进入新手试炼
-
- 返回首页
-
-
-
+
恭喜,你的弓箭和账号已成功绑定!
已赠送6个月射灵世界会员
-
+
+
+
+ 返回首页
+
+
+ 进入新手试炼
+
+
+
@@ -270,7 +308,18 @@ onShow(() => {
mode="widthFix"
:style="{ borderRadius: '50%' }"
/>
- {{ user.nickName }}
+
+ {{ user.nickName }}
+ {{
+ user.nickName
+ }}
+
@@ -404,7 +453,7 @@ onShow(() => {
width: 140rpx;
height: 140rpx;
margin-bottom: 5px;
- border-radius: 10px;
+ border-radius: 12px;
}
.device-binded > view > text {
width: 120px;
@@ -413,6 +462,15 @@ onShow(() => {
text-overflow: ellipsis;
text-align: center;
}
+.device-binded .member-nickname {
+ justify-content: center;
+ width: 120px;
+}
+.device-binded .member-nickname__text,
+.device-binded .member-nickname__shine {
+ font-size: 26rpx;
+ text-align: center;
+}
.device-binded > image {
width: 100rpx;
margin: 0 20px;
diff --git a/src/pages/point-book-detail.vue b/src/pages/point-book-detail.vue
index 74fc1ab..98b2666 100644
--- a/src/pages/point-book-detail.vue
+++ b/src/pages/point-book-detail.vue
@@ -81,9 +81,17 @@ const loading = ref(false);
const shareImage = async () => {
if (loading.value) return;
loading.value = true;
- await generateShareImage("shareImageCanvas", record.value);
- await wxShare("shareImageCanvas");
- loading.value = false;
+ try {
+ await generateShareImage("shareImageCanvas", record.value);
+ await wxShare("shareImageCanvas");
+ } catch (e) {
+ uni.showToast({
+ title: "海报生成失败,请稍后重试",
+ icon: "none",
+ });
+ } finally {
+ loading.value = false;
+ }
};
onLoad(async (options) => {
diff --git a/src/pages/point-book-rank.vue b/src/pages/point-book-rank.vue
index eeddf96..2df8bda 100644
--- a/src/pages/point-book-rank.vue
+++ b/src/pages/point-book-rank.vue
@@ -15,11 +15,22 @@ const list = ref([]);
const mine = ref({
averageRing: 0,
});
+const sharing = ref(false);
const shareImage = async () => {
- if (!mine.value.id) return;
- await sharePointData("shareCanvas", mine.value);
- await wxShare("shareCanvas");
+ if (!mine.value.id || sharing.value) return;
+ sharing.value = true;
+ try {
+ await sharePointData("shareCanvas", mine.value);
+ await wxShare("shareCanvas");
+ } catch (e) {
+ uni.showToast({
+ title: "海报生成失败,请稍后重试",
+ icon: "none",
+ });
+ } finally {
+ sharing.value = false;
+ }
};
onMounted(async () => {
diff --git a/src/pages/practise-one.vue b/src/pages/practise-one.vue
index a6cbcd3..a779994 100644
--- a/src/pages/practise-one.vue
+++ b/src/pages/practise-one.vue
@@ -30,14 +30,16 @@ const { user } = storeToRefs(store);
const start = ref(false);
const scores = ref([]);
+const isSvip = ref(false);
const total = 12;
-/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */
+/** 当前练习中连续 10 环及以上计数,用于触发 tententen 音效 */
const xRingStreak = ref(0);
const practiseResult = ref({});
const practiseId = ref("");
const showGuide = ref(false);
const tips = ref("");
const targetType = ref(1);
+const sharing = ref(false);
onLoad((options) => {
if (options.target) {
@@ -48,6 +50,7 @@ onLoad((options) => {
const onReady = async () => {
await startPractiseAPI();
scores.value = [];
+ isSvip.value = false;
xRingStreak.value = 0; // 新一局开始,重置 X 环连续计数
start.value = true;
audioManager.play("练习开始");
@@ -59,19 +62,23 @@ const onOver = async () => {
};
/**
- * 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效
- * @param {boolean} isXRing - 本次射击是否为 X 环
+ * 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效
+ * @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
*/
-function checkAndPlayTententen(isXRing) {
- if (isXRing) {
+function isTenPlusRing(shot) {
+ return !!(shot?.ringX || Number(shot?.ring) >= 10);
+}
+
+function checkAndPlayTententen(isTenPlusRingShot) {
+ if (isTenPlusRingShot) {
xRingStreak.value += 1;
- // 连续 3 箭均为 X 环,在环数播报入队后追加 tententen,避免播放顺序颠倒
+ // 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen,避免播放顺序颠倒
if (xRingStreak.value >= 3) {
xRingStreak.value = 0;
nextTick(() => audioManager.play("tententen", false));
}
} else {
- // 非 X 环则重置连续计数
+ // 低于 10 环或未上靶则重置连续计数
xRingStreak.value = 0;
}
}
@@ -79,11 +86,12 @@ function checkAndPlayTententen(isXRing) {
async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult) {
const prevLen = scores.value.length;
+ isSvip.value = msg.sVip === true;
scores.value = msg.details;
- // 有新箭时取最后一箭判断是否 X 环并检测连续计数
+ // 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数
if (scores.value.length > prevLen) {
const latestArrow = scores.value[scores.value.length - 1];
- checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring));
+ checkAndPlayTententen(isTenPlusRing(latestArrow));
}
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
// setTimeout(onOver, 1500);
@@ -101,6 +109,7 @@ async function onComplete() {
practiseResult.value = {};
start.value = false;
scores.value = [];
+ isSvip.value = false;
xRingStreak.value = 0; // 重新开始练习,重置 X 环连续计数
const result = await createPractiseAPI(total, 120);
if (result) practiseId.value = result.id;
@@ -108,8 +117,19 @@ async function onComplete() {
}
const onClickShare = debounce(async () => {
- await sharePractiseData("shareCanvas", 2, user.value, practiseResult.value);
- await wxShare("shareCanvas");
+ if (sharing.value) return;
+ sharing.value = true;
+ try {
+ await sharePractiseData("shareCanvas", 2, user.value, practiseResult.value);
+ await wxShare("shareCanvas");
+ } catch (e) {
+ uni.showToast({
+ title: "海报生成失败,请稍后重试",
+ icon: "none",
+ });
+ } finally {
+ sharing.value = false;
+ }
});
function onAudioEnded(s) {
@@ -174,6 +194,7 @@ onBeforeUnmount(() => {
:totalRound="start ? total / 4 : 0"
:currentRound="scores.length % 3"
:scores="scores"
+ :isSvip="isSvip"
/>
{
if (options.target) {
@@ -47,6 +49,7 @@ onLoad((options) => {
const onReady = async () => {
await startPractiseAPI();
scores.value = [];
+ isSvip.value = false;
xRingStreak.value = 0; // 新一局开始,重置 X 环连续计数
start.value = true;
audioManager.play("练习开始");
@@ -58,19 +61,23 @@ const onOver = async () => {
};
/**
- * 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效
- * @param {boolean} isXRing - 本次射击是否为 X 环
+ * 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效
+ * @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
*/
-function checkAndPlayTententen(isXRing) {
- if (isXRing) {
+function isTenPlusRing(shot) {
+ return !!(shot?.ringX || Number(shot?.ring) >= 10);
+}
+
+function checkAndPlayTententen(isTenPlusRingShot) {
+ if (isTenPlusRingShot) {
xRingStreak.value += 1;
- // 连续 3 箭均为 X 环,在环数播报入队后追加 tententen,避免播放顺序颠倒
+ // 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen,避免播放顺序颠倒
if (xRingStreak.value >= 3) {
xRingStreak.value = 0;
nextTick(() => audioManager.play("tententen", false));
}
} else {
- // 非 X 环则重置连续计数
+ // 低于 10 环或未上靶则重置连续计数
xRingStreak.value = 0;
}
}
@@ -78,11 +85,12 @@ function checkAndPlayTententen(isXRing) {
async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult) {
const prevLen = scores.value.length;
+ isSvip.value = msg.sVip === true;
scores.value = msg.details;
- // 有新箭时取最后一箭判断是否 X 环并检测连续计数
+ // 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数
if (scores.value.length > prevLen) {
const latestArrow = scores.value[scores.value.length - 1];
- checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring));
+ checkAndPlayTententen(isTenPlusRing(latestArrow));
}
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(onOver, 1500);
@@ -116,6 +124,7 @@ async function onComplete() {
practiseResult.value = {};
start.value = false;
scores.value = [];
+ isSvip.value = false;
xRingStreak.value = 0; // 重新开始练习,重置 X 环连续计数
const result = await createPractiseAPI(total, 3600);
if (result) practiseId.value = result.id;
@@ -123,8 +132,19 @@ async function onComplete() {
}
const onClickShare = debounce(async () => {
- await sharePractiseData("shareCanvas", 3, user.value, practiseResult.value);
- await wxShare("shareCanvas");
+ if (sharing.value) return;
+ sharing.value = true;
+ try {
+ await sharePractiseData("shareCanvas", 3, user.value, practiseResult.value);
+ await wxShare("shareCanvas");
+ } catch (e) {
+ uni.showToast({
+ title: "海报生成失败,请稍后重试",
+ icon: "none",
+ });
+ } finally {
+ sharing.value = false;
+ }
});
onMounted(async () => {
@@ -175,6 +195,7 @@ onBeforeUnmount(() => {
:currentRound="scores.length"
:totalRound="start ? total : 0"
:scores="scores"
+ :isSvip="isSvip"
/>
-import { ref } from "vue";
+import { computed, ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Guide from "@/components/Guide.vue";
@@ -15,6 +15,8 @@ const { user, device, online } = storeToRefs(store);
const data = ref({});
const showTargetPicker = ref(false);
const pendingPractiseType = ref("");
+const isSVip = computed(() => user.value.sVip === true);
+const isVip = computed(() => user.value.vip === true && !isSVip.value);
const goPractise = async (type) => {
if (!canEenter(user.value, device.value, online.value)) return;
@@ -49,7 +51,18 @@ onShow(async () => {
- {{ user.nickName }}
+
+ {{ user.nickName }}
+ {{
+ user.nickName
+ }}
+
已练习打卡
@@ -132,12 +145,15 @@ onShow(async () => {
display: flex;
align-items: flex-end;
}
-.practise-data > view:first-child > view:first-child > text {
+.practise-data > view:first-child > view:first-child .member-nickname {
color: #fff;
margin-left: 10px;
- font-size: 16px;
width: 120px;
}
+.practise-data > view:first-child > view:first-child .member-nickname__text,
+.practise-data > view:first-child > view:first-child .member-nickname__shine {
+ font-size: 16px;
+}
.practise-data > view:first-child > view:last-child > text:nth-child(2) {
color: #f7d247;
margin: 0 3px;
diff --git a/src/pages/rank-list.vue b/src/pages/rank-list.vue
index 3963ca5..cff6161 100644
--- a/src/pages/rank-list.vue
+++ b/src/pages/rank-list.vue
@@ -102,6 +102,8 @@ const buildDefaultMyData = () => ({
userId: user.value.id,
name: user.value.nickName,
avatar: user.value.avatar,
+ vip: user.value.vip,
+ sVip: user.value.sVip,
totalScore: 0,
mvpCount: 0,
tenRings: 0,
@@ -154,6 +156,15 @@ const getRankUnit = (index = selectedIndex.value) => {
return "次";
};
+const isMember = (item = {}) => item.vip === true || item.sVip === true;
+
+const getMemberNicknameClass = (item = {}) => [
+ "rank-list-player-name",
+ "member-nickname",
+ item.vip === true && item.sVip !== true ? "member-nickname--vip" : "",
+ item.sVip === true ? "member-nickname--svip" : "",
+];
+
// 统一设置页面当前的视觉滚动状态,避免吸顶和顶部背景不同步。
const syncScrollVisualState = (scrollTop = 0) => {
currentScrollTop.value = scrollTop;
@@ -441,7 +452,13 @@ const measureTabsMetrics = () => {
- {{ item.name }}
+
+ {{ item.name }}
+
+ {{ item.name }}
+
+
+ {{ item.name }}
{{ formatLevelText(item) }}
@@ -479,7 +496,23 @@ const measureTabsMetrics = () => {
{{ getDisplayMyRank(currentMyData) }}
- {{ currentMyData.name || user.nickName }}
+
+
+ {{ currentMyData.name || user.nickName }}
+
+
+ {{ currentMyData.name || user.nickName }}
+
+
+
+ {{ currentMyData.name || user.nickName }}
+
{{ formatLevelText(currentMyData) }}
@@ -653,6 +686,18 @@ const measureTabsMetrics = () => {
width: 120px;
}
+.rank-list-player-name {
+ color: #fff;
+ font-size: 14px;
+ margin-bottom: 3px;
+ width: 120px;
+}
+
+.rank-list-player-name .member-nickname__text,
+.rank-list-player-name .member-nickname__shine {
+ font-size: 14px;
+}
+
.rank-list-item > text:last-child {
margin-right: 10px;
width: 56px;
diff --git a/src/pages/ranking.vue b/src/pages/ranking.vue
index 6b275b6..3024ae0 100644
--- a/src/pages/ranking.vue
+++ b/src/pages/ranking.vue
@@ -3,21 +3,23 @@ import { computed, ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Avatar from "@/components/Avatar.vue";
+import ModalDialog from "@/components/ModalDialog.vue";
import { topThreeColors } from "@/constants";
import {
+ getDailyCountAPI,
getSeasonList,
getSeasonStats,
getScoreRankList,
getTenRingRankList,
getMvpRankList,
} from "@/apis";
-import { canEenter } from "@/util";
+import { canEenter, getLimitCountText, isLimitReached } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
-const { user, device, online, game } = storeToRefs(store);
-const { getLvlName } = store;
+const { user, device, online, game, dailyCount } = storeToRefs(store);
+const { getLvlName, updateDailyCount } = store;
const defaultSeasonStats = {
nickName: "",
@@ -55,6 +57,21 @@ const rankLoading = ref(false);
const scoreRankList = ref([]);
const mvpRankList = ref([]);
const tenRingRankList = ref([]);
+const showLimitModal = ref(false);
+const isSVip = computed(() => user.value.sVip === true);
+const isVip = computed(() => user.value.vip === true && !isSVip.value);
+const rankedLimitText = computed(() =>
+ getLimitCountText("排位", dailyCount.value.ranked)
+);
+
+const isMember = (item = {}) => item.vip === true || item.sVip === true;
+
+const getMemberNicknameClass = (item = {}) => [
+ "rank-preview-name",
+ "member-nickname",
+ item.vip === true && item.sVip !== true ? "member-nickname--vip" : "",
+ item.sVip === true ? "member-nickname--svip" : "",
+];
// 根据接口返回结构提取榜单数组,兼容数组和对象两种返回形式。
const getRankListFromResponse = (result) => {
@@ -104,12 +121,40 @@ const toMatchPage = async (gameType, teamSize) => {
uni.$showHint(1);
return;
}
+ const countData = await loadDailyCount();
+ if (isLimitReached(countData.ranked)) {
+ showLimitModal.value = true;
+ return;
+ }
await uni.$checkAudio();
uni.navigateTo({
url: `/pages/match-page?gameType=${gameType}&teamSize=${teamSize}`,
});
};
+const closeLimitModal = () => {
+ showLimitModal.value = false;
+};
+
+const goVipPage = () => {
+ showLimitModal.value = false;
+ uni.navigateTo({
+ url: "/pages/member/be-vip",
+ });
+};
+
+const loadDailyCount = async () => {
+ if (!user.value.id) return dailyCount.value;
+ try {
+ const result = await getDailyCountAPI();
+ updateDailyCount(result);
+ return result || dailyCount.value;
+ } catch (error) {
+ console.log("load daily count error", error);
+ return dailyCount.value;
+ }
+};
+
const toMyGrowthPage = () => {
uni.navigateTo({
url: "/pages/my-growth",
@@ -235,7 +280,10 @@ const onChangeSeason = async (seasonId, name) => {
// 页面显示时先拿赛季列表,再拉当前赛季统计和默认榜单数据。
onShow(async () => {
try {
- const seasonResult = await getSeasonList();
+ const [seasonResult] = await Promise.all([
+ getSeasonList(),
+ loadDailyCount(),
+ ]);
seasonData.value = seasonResult.list || [];
if (!seasonData.value.length) {
@@ -271,7 +319,12 @@ onShow(async () => {
-
+
@@ -314,7 +367,20 @@ onShow(async () => {
:rankLvl="seasonStats.rankLvl"
:size="30"
/>
- {{ seasonStats.nickName || user.nickName }}
+
+
+ {{ seasonStats.nickName || user.nickName }}
+
+
+ {{ seasonStats.nickName || user.nickName }}
+
+
{
:style="{ borderColor: index < 3 ? topThreeColors[index] : '' }"
/>
- {{ item.name }}
+
+ {{ item.name }}
+
+ {{ item.name }}
+
+
+ {{ item.name }}
{{ formatRankSubTitle(item) }}
{{ item.totalScore || 0 }}分
@@ -499,7 +571,13 @@ onShow(async () => {
:style="{ borderColor: index < 3 ? topThreeColors[index] : '' }"
/>
- {{ item.name }}
+
+ {{ item.name }}
+
+ {{ item.name }}
+
+
+ {{ item.name }}
{{ formatRankSubTitle(item) }}
{{ item.mvpCount || 0 }}次
@@ -522,7 +600,13 @@ onShow(async () => {
:style="{ borderColor: index < 3 ? topThreeColors[index] : '' }"
/>
- {{ item.name }}
+
+ {{ item.name }}
+
+ {{ item.name }}
+
+
+ {{ item.name }}
{{ formatRankSubTitle(item) }}
{{ item.tenRings ?? item.TenRings ?? 0 }}次
@@ -538,6 +622,14 @@ onShow(async () => {
+
diff --git a/src/pages/team-battle.vue b/src/pages/team-battle.vue
index 466319d..1a0d802 100644
--- a/src/pages/team-battle.vue
+++ b/src/pages/team-battle.vue
@@ -49,7 +49,7 @@ const battleWay = ref(0);
const lastToSomeoneShootKey = ref("");
/** 控制设备离线提示弹窗的显示状态 */
const showOfflineModal = ref(false);
-/** 记录每位玩家当前轮连续 X 环数,key 为 playerId,用于触发 tententen 音效 */
+/** 记录每位玩家当前轮连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
const xRingStreaks = ref({});
/**
@@ -234,22 +234,26 @@ function onNewRound(msg, prevRound) {
}
/**
- * 检测指定射手连续 X 环是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
+ * 检测指定射手连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
* @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 (isXRing) {
+ if (isTenPlusRingShot) {
xRingStreaks.value[shooterId] = (xRingStreaks.value[shooterId] || 0) + 1;
- // 同一玩家连续 3 箭均为 X 环,追加到环数音效队列尾部播放
+ // 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
if (xRingStreaks.value[shooterId] >= 3) {
xRingStreaks.value[shooterId] = 0;
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
nextTick(() => audioManager.play("tententen", false));
}
} else {
- // 非 X 环则重置该玩家的连续计数
+ // 低于 10 环或未上靶则重置该玩家的连续计数
xRingStreaks.value[shooterId] = 0;
}
}
@@ -268,9 +272,9 @@ async function onReceiveMessage(msg) {
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
showRoundTip.value = false;
recoverData(msg, {arrowOnly: true});
- // 检测同一玩家三箭全 X 环,触发 tententen 音效
+ // 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
// currentShooterId 在 ToSomeoneShoot 时写入,ShootResult 不会覆盖,可靠识别本次射手
- checkAndPlayTententen(currentShooterId.value, !!(msg.shootData?.ringX && msg.shootData?.ring));
+ checkAndPlayTententen(currentShooterId.value, isTenPlusRing(msg.shootData));
} else if (msg.type === MESSAGETYPESV2.NewRound) {
// 在进入延迟前先捕获当前轮次,供 onNewRound 使用,防止 800ms 内 ToSomeoneShoot 提前更新 currentRound 造成 Tip 展示错轮
const prevRound = currentRound.value;
diff --git a/src/pages/team-battle/components/BackToGame.vue b/src/pages/team-battle/components/BackToGame.vue
index 748db2e..2ca9a7c 100644
--- a/src/pages/team-battle/components/BackToGame.vue
+++ b/src/pages/team-battle/components/BackToGame.vue
@@ -18,12 +18,14 @@ const props = defineProps({
},
});
const loading = ref(false);
+const navigating = ref(false);
/** 统一获取当前环境 token,用于守卫:无有效 token 时不发起接口请求 */
const getToken = () =>
uni.getStorageSync(`${uni.getAccountInfoSync().miniProgram.envVersion}_token`);
onShow(async () => {
+ navigating.value = false;
if (user.value.id && getToken()) {
setTimeout(async () => {
const state = await getUserGameState();
@@ -45,28 +47,35 @@ watch(
}
);
+const navigateOnce = (url) =>
+ new Promise((resolve, reject) => {
+ navigating.value = true;
+ uni.navigateTo({
+ url,
+ success: resolve,
+ fail: (error) => {
+ navigating.value = false;
+ reject(error);
+ },
+ });
+ });
+
const onClick = debounce(async () => {
- if (loading.value) return;
+ if (loading.value || navigating.value) return;
try {
loading.value = true;
const result = await getBattleAPI();
if (result && result.matchId) {
await uni.$checkAudio();
if (result.mode <= 3) {
- uni.navigateTo({
- url: `/pages/team-battle/index?battleId=${result.matchId}`,
- });
+ await navigateOnce(`/pages/team-battle/index?battleId=${result.matchId}`);
} else {
- uni.navigateTo({
- url: `/pages/melee-battle?battleId=${result.matchId}`,
- });
+ await navigateOnce(`/pages/melee-battle?battleId=${result.matchId}`);
}
return;
}
if (game.value.roomID) {
- uni.navigateTo({
- url: "/pages/battle-room?roomNumber=" + game.value.roomID,
- });
+ await navigateOnce("/pages/battle-room?roomNumber=" + game.value.roomID);
} else {
updateGame(false, "");
}
diff --git a/src/pages/team-battle/components/BattleFooter.vue b/src/pages/team-battle/components/BattleFooter.vue
index c7c2442..8bc544f 100644
--- a/src/pages/team-battle/components/BattleFooter.vue
+++ b/src/pages/team-battle/components/BattleFooter.vue
@@ -51,7 +51,7 @@ const normalRounds = computed(() => {
@@ -86,7 +86,7 @@ const normalRounds = computed(() => {
diff --git a/src/pages/team-battle/components/BattleHeader.vue b/src/pages/team-battle/components/BattleHeader.vue
index 633d086..65681f0 100644
--- a/src/pages/team-battle/components/BattleHeader.vue
+++ b/src/pages/team-battle/components/BattleHeader.vue
@@ -27,6 +27,14 @@ defineProps({
default: true,
},
});
+
+const getMemberNicknameClass = (player = {}) => [
+ "member-nickname",
+ player.vip === true && player.sVip !== true ? "member-nickname--vip" : "",
+ player.sVip === true ? "member-nickname--svip" : "",
+];
+
+const isMember = (player = {}) => player.vip === true || player.sVip === true;
@@ -51,7 +59,16 @@ defineProps({
}"
>
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
- {{ player.name }}
+
+ {{ player.name }}
+
+ {{ player.name }}
+
+
+ {{ player.name }}
@@ -183,6 +218,13 @@ defineProps({
text-overflow: ellipsis;
text-align: center;
}
+view.player-name {
+ justify-content: center;
+}
+.player-name .member-nickname__text,
+.player-name .member-nickname__shine {
+ font-size: 12px;
+}
.left-winner-badge {
position: absolute;
width: 50px;
diff --git a/src/pages/team-battle/components/BowTarget.vue b/src/pages/team-battle/components/BowTarget.vue
index 18f82cc..c303250 100644
--- a/src/pages/team-battle/components/BowTarget.vue
+++ b/src/pages/team-battle/components/BowTarget.vue
@@ -1,6 +1,15 @@
-
+
-
+
@@ -164,20 +441,14 @@ onBeforeUnmount(() => {
经验 +1
{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
}}环
@@ -188,52 +459,78 @@ 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="getExperienceTipStyle(bluelatestOne)"
>
经验 +1
{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
}}环
+
{{ index + 1 }}
+
{{ index + 1 }}
+
{{ title }}
@@ -151,12 +151,12 @@ onBeforeUnmount(() => {
- 凹造型
+ 箭前准备
-
感知距离 {
width: 156rpx;
height: 28rpx;
font-weight: 400;
- font-size: 20rpx;
+ font-size: 24rpx;
color: #ffffff;
text-align: center;
line-height: 28rpx;
diff --git a/src/pages/team-battle/components/TeamAvatars.vue b/src/pages/team-battle/components/TeamAvatars.vue
index 3eb7a3b..ca80b11 100644
--- a/src/pages/team-battle/components/TeamAvatars.vue
+++ b/src/pages/team-battle/components/TeamAvatars.vue
@@ -17,6 +17,7 @@ const props = defineProps({
const players = ref({});
const currentTeam = ref(false);
const firstName = ref("");
+const currentPlayer = ref(null);
// 抽出判断:当前队伍且该玩家排序为 0(队伍首位)
const isFirst = (id) =>
@@ -30,6 +31,18 @@ const getPos = (id) => {
return sort * 40;
};
+const getMemberNicknameClass = () => [
+ "current-shooter-name",
+ "member-nickname",
+ currentPlayer.value?.vip === true && currentPlayer.value?.sVip !== true
+ ? "member-nickname--vip"
+ : "",
+ currentPlayer.value?.sVip === true ? "member-nickname--svip" : "",
+];
+
+const isCurrentPlayerMember = () =>
+ currentPlayer.value?.vip === true || currentPlayer.value?.sVip === true;
+
const syncPlayers = () => {
const nextPlayers = {};
const shooterId = props.currentShooterId;
@@ -40,12 +53,14 @@ const syncPlayers = () => {
currentTeam.value = !!shooterId && shooterIndex >= 0;
firstName.value = "";
+ currentPlayer.value = null;
if (currentTeam.value) {
const target = nextTeam.splice(shooterIndex, 1)[0];
if (target) {
nextTeam.unshift(target);
firstName.value = target.name || "";
+ currentPlayer.value = target;
}
}
@@ -93,8 +108,20 @@ watch(
>{{ isRed ? "红队" : "蓝队" }}
+
+ {{ firstName }}
+
+ {{ firstName }}
+
+
{
:scores="scores"
:blueScores="blueScores"
:latestShotFlash="latestShotFlash"
+ :redTeam="redTeam"
+ :blueTeam="blueTeam"
/>
{
设备已离线
检测到设备已断开连接,请检查设备后继续比赛
- 我知道了
+ 我知道了
diff --git a/src/pages/team-battle/team-bow-data.vue b/src/pages/team-battle/team-bow-data.vue
new file mode 100644
index 0000000..3b310cd
--- /dev/null
+++ b/src/pages/team-battle/team-bow-data.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+ onClickTab(index)"
+ :class="selected === index ? 'selected-tab' : ''"
+ >
+ {{ tab }}
+
+
+
+
+
+
+
+
+
+
+ {{ score.ringX ? "X" : score.ring }}
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/user.vue b/src/pages/user.vue
index e2f2fcd..f0f529e 100644
--- a/src/pages/user.vue
+++ b/src/pages/user.vue
@@ -13,7 +13,7 @@ const { updateUser } = store;
const toOrderPage = () => {
uni.navigateTo({
- url: "/pages/orders",
+ url: "/pages/member/orders",
});
};
@@ -27,7 +27,7 @@ const toFristTryPage = async () => {
};
const toBeVipPage = () => {
uni.navigateTo({
- url: "/pages/be-vip",
+ url: "/pages/member/be-vip",
});
};
const toMyGrowthPage = () => {
diff --git a/src/static/bow-target.png b/src/static/bow-target.png
index a175ed6..45354e5 100644
Binary files a/src/static/bow-target.png and b/src/static/bow-target.png differ
diff --git a/src/static/common/dialog-bg.png b/src/static/common/dialog-bg.png
new file mode 100644
index 0000000..56635f6
Binary files /dev/null and b/src/static/common/dialog-bg.png differ
diff --git a/src/static/common/dialog-icon.png b/src/static/common/dialog-icon.png
new file mode 100644
index 0000000..919e07d
Binary files /dev/null and b/src/static/common/dialog-icon.png differ
diff --git a/src/static/common/dialog-light.png b/src/static/common/dialog-light.png
new file mode 100644
index 0000000..a6ce9bf
Binary files /dev/null and b/src/static/common/dialog-light.png differ
diff --git a/src/static/device-icon.png b/src/static/device-icon.png
index 065eaf1..3d75f21 100644
Binary files a/src/static/device-icon.png and b/src/static/device-icon.png differ
diff --git a/src/static/vip/intro-toast.png b/src/static/vip/intro-toast.png
new file mode 100644
index 0000000..f942f6d
Binary files /dev/null and b/src/static/vip/intro-toast.png differ
diff --git a/src/static/vip/super-battle.png b/src/static/vip/super-battle.png
new file mode 100644
index 0000000..6193cbb
Binary files /dev/null and b/src/static/vip/super-battle.png differ
diff --git a/src/static/vip/super-button.png b/src/static/vip/super-button.png
new file mode 100644
index 0000000..04cceff
Binary files /dev/null and b/src/static/vip/super-button.png differ
diff --git a/src/static/vip/svip-arrow.png b/src/static/vip/svip-arrow.png
new file mode 100644
index 0000000..1d16efb
Binary files /dev/null and b/src/static/vip/svip-arrow.png differ
diff --git a/src/static/vip/svip-badge.png b/src/static/vip/svip-badge.png
new file mode 100644
index 0000000..fe78d88
Binary files /dev/null and b/src/static/vip/svip-badge.png differ
diff --git a/src/static/vip/svip-battle.png b/src/static/vip/svip-battle.png
new file mode 100644
index 0000000..18d2778
Binary files /dev/null and b/src/static/vip/svip-battle.png differ
diff --git a/src/static/vip/svip-comment.png b/src/static/vip/svip-comment.png
new file mode 100644
index 0000000..3195eb3
Binary files /dev/null and b/src/static/vip/svip-comment.png differ
diff --git a/src/static/vip/svip-hit.png b/src/static/vip/svip-hit.png
new file mode 100644
index 0000000..d80f87d
Binary files /dev/null and b/src/static/vip/svip-hit.png differ
diff --git a/src/static/vip/svip-jian.png b/src/static/vip/svip-jian.png
new file mode 100644
index 0000000..f4fb264
Binary files /dev/null and b/src/static/vip/svip-jian.png differ
diff --git a/src/static/vip/svip-lie.png b/src/static/vip/svip-lie.png
new file mode 100644
index 0000000..8fb9577
Binary files /dev/null and b/src/static/vip/svip-lie.png differ
diff --git a/src/static/vip/svip-off.png b/src/static/vip/svip-off.png
new file mode 100644
index 0000000..da413db
Binary files /dev/null and b/src/static/vip/svip-off.png differ
diff --git a/src/static/vip/svip-on.png b/src/static/vip/svip-on.png
new file mode 100644
index 0000000..610b63d
Binary files /dev/null and b/src/static/vip/svip-on.png differ
diff --git a/src/static/vip/svip-order.png b/src/static/vip/svip-order.png
new file mode 100644
index 0000000..fbd3c35
Binary files /dev/null and b/src/static/vip/svip-order.png differ
diff --git a/src/static/vip/svip-point.png b/src/static/vip/svip-point.png
new file mode 100644
index 0000000..4f4b77f
Binary files /dev/null and b/src/static/vip/svip-point.png differ
diff --git a/src/static/vip/svip-rank.png b/src/static/vip/svip-rank.png
new file mode 100644
index 0000000..79bccc0
Binary files /dev/null and b/src/static/vip/svip-rank.png differ
diff --git a/src/static/vip/svip-service.png b/src/static/vip/svip-service.png
new file mode 100644
index 0000000..3e0f0fe
Binary files /dev/null and b/src/static/vip/svip-service.png differ
diff --git a/src/static/vip/svip-xuan.png b/src/static/vip/svip-xuan.png
new file mode 100644
index 0000000..48d9b2c
Binary files /dev/null and b/src/static/vip/svip-xuan.png differ
diff --git a/src/static/vip/vip-badge.png b/src/static/vip/vip-badge.png
new file mode 100644
index 0000000..d8b7dd0
Binary files /dev/null and b/src/static/vip/vip-badge.png differ
diff --git a/src/static/vip/vip-battle.png b/src/static/vip/vip-battle.png
new file mode 100644
index 0000000..3d46318
Binary files /dev/null and b/src/static/vip/vip-battle.png differ
diff --git a/src/static/vip/vip-comment.png b/src/static/vip/vip-comment.png
new file mode 100644
index 0000000..016de68
Binary files /dev/null and b/src/static/vip/vip-comment.png differ
diff --git a/src/static/vip/vip-off.png b/src/static/vip/vip-off.png
new file mode 100644
index 0000000..d9020c7
Binary files /dev/null and b/src/static/vip/vip-off.png differ
diff --git a/src/static/vip/vip-on.png b/src/static/vip/vip-on.png
new file mode 100644
index 0000000..eae1e2f
Binary files /dev/null and b/src/static/vip/vip-on.png differ
diff --git a/src/static/vip/vip-order.png b/src/static/vip/vip-order.png
new file mode 100644
index 0000000..54962a8
Binary files /dev/null and b/src/static/vip/vip-order.png differ
diff --git a/src/static/vip/vip-rank.png b/src/static/vip/vip-rank.png
new file mode 100644
index 0000000..8aac306
Binary files /dev/null and b/src/static/vip/vip-rank.png differ
diff --git a/src/static/vip/vip-service.png b/src/static/vip/vip-service.png
new file mode 100644
index 0000000..422b8a4
Binary files /dev/null and b/src/static/vip/vip-service.png differ
diff --git a/src/store.js b/src/store.js
index bacc5e5..45a4510 100644
--- a/src/store.js
+++ b/src/store.js
@@ -26,6 +26,17 @@ const getDefaultGame = () => ({
tips: "",
});
+const getDefaultDailyCount = () => ({
+ challenge: {
+ count: 0,
+ limit: -1,
+ },
+ ranked: {
+ count: 0,
+ limit: -1,
+ },
+});
+
const getLvlName = (rankLvl, rankList = []) => {
if (!rankList) return;
let lvlName = "";
@@ -99,6 +110,7 @@ export default defineStore("store", {
totalShot: 0, // 轮次总箭数(用于 HeaderProgress 恢复状态)
tips: "", // 当前提示文案(用于 HeaderProgress 恢复状态,替代 uni.$emit 避免时序问题)
},
+ dailyCount: getDefaultDailyCount(),
}),
// 计算属性
@@ -137,6 +149,10 @@ export default defineStore("store", {
this.device.deviceId = deviceId;
this.device.deviceName = deviceName;
},
+ clearDevice() {
+ this.device = getDefaultDevice();
+ this.online = false;
+ },
async updateConfig(config) {
this.config = config;
if (this.user.scores !== undefined) {
@@ -167,12 +183,19 @@ export default defineStore("store", {
updateRoomNumber(number) {
this.game.roomNumber = number;
},
+ updateDailyCount(data = {}) {
+ this.dailyCount = {
+ ...getDefaultDailyCount(),
+ ...(data || {}),
+ };
+ },
clearSessionState() {
this.$patch({
user: getDefaultUser(),
device: getDefaultDevice(),
online: false,
game: getDefaultGame(),
+ dailyCount: getDefaultDailyCount(),
});
uni.removeStorageSync(PERSISTED_STORE_KEY);
setTimeout(() => {
diff --git a/src/util.js b/src/util.js
index d9a9e1d..484b686 100644
--- a/src/util.js
+++ b/src/util.js
@@ -1,5 +1,5 @@
export const formatTimestamp = (timestamp) => {
- const date = new Date(timestamp * 1000);
+ const date = new Date(timestamp);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
@@ -11,11 +11,13 @@ export const debounce = (fn, delay = 300) => {
let timer = null;
return async (...args) => {
if (timer) clearTimeout(timer);
- return new Promise((resolve) => {
+ return new Promise((resolve, reject) => {
timer = setTimeout(async () => {
try {
const result = await fn(...args);
resolve(result);
+ } catch (error) {
+ reject(error);
} finally {
timer = null;
}
@@ -24,6 +26,28 @@ export const debounce = (fn, delay = 300) => {
};
};
+export const isUnlimitedLimit = (item = {}) => Number(item.limit) === -1;
+
+export const getLimitCountText = (label, item = {}) => {
+ if (!item || isUnlimitedLimit(item)) return "";
+ const count = Number(item.count || 0);
+ const limit = Number(item.limit || 0);
+ return `今日${label}次数:${count}/${limit}`;
+};
+
+export const isLimitReached = (item = {}) => {
+ if (!item || isUnlimitedLimit(item)) return false;
+ return Number(item.count || 0) >= Number(item.limit || 0);
+};
+
+export const isLimitError = (error) => {
+ const message = typeof error === "string" ? error : error?.message;
+ return String(message || "").includes("已达上限");
+};
+
+const isShareCancel = (error) =>
+ String(error?.errMsg || error || "").toLowerCase().includes("cancel");
+
export const wxShare = async (canvasId = "shareCanvas") => {
try {
// 先尝试按 id 查找