Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad8bc1f152 | ||
|
|
348d490de8 | ||
|
|
d3e0307854 | ||
|
|
0a678cb387 | ||
|
|
3c7467fbdf | ||
|
|
6aa3c6fd6c | ||
|
|
8b11e41957 | ||
|
|
8c6374b2c5 | ||
|
|
78a495de60 | ||
|
|
ef13ff6a2b | ||
|
|
8b7a94b8c4 | ||
|
|
3e99bb8ad9 | ||
|
|
40b94dbad8 | ||
|
|
422a93dbe2 | ||
|
|
5c74edf001 | ||
|
|
937308e51d | ||
|
|
165f1a40bd | ||
|
|
35c49bbb10 | ||
|
|
ad0fb0b337 | ||
|
|
02290615e8 | ||
|
|
df628b48c6 | ||
|
|
ca6bba4dff | ||
|
|
c435372d33 | ||
|
|
4c32bbfb13 | ||
|
|
b6b517be9e | ||
|
|
4bea563109 | ||
|
|
bd352c04a8 | ||
|
|
caa6ed4284 | ||
|
|
4abb0de1e7 | ||
|
|
5fe998adf2 | ||
|
|
54e5427a77 | ||
|
|
9b47b313a7 | ||
|
|
350c6373ab | ||
|
|
048b0e9fa9 | ||
|
|
39d34d5bff | ||
|
|
37f4f0c019 | ||
|
|
e9cd9c8c9f | ||
|
|
73a41ab897 | ||
|
|
32c37e283f | ||
|
|
209103742d | ||
|
|
27d11a8129 | ||
|
|
c9ea368a19 | ||
|
|
b5e7e72104 | ||
|
|
7a00c1bb1f |
@@ -10,6 +10,7 @@ lerna-debug.log*
|
|||||||
node_modules
|
node_modules
|
||||||
.history
|
.history
|
||||||
.github
|
.github
|
||||||
|
.claude
|
||||||
openspec
|
openspec
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
docs
|
docs
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ try {
|
|||||||
console.error("获取环境信息失败,使用默认正式环境", e);
|
console.error("获取环境信息失败,使用默认正式环境", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
function request(method, url, data = {}) {
|
const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot");
|
||||||
|
|
||||||
|
// 统一处理业务接口请求,包含登录态、业务错误和 WiFi 连接空响应兼容。
|
||||||
|
function request(method, url, data = {}, baseUrl = BASE_URL) {
|
||||||
const token = uni.getStorageSync(
|
const token = uni.getStorageSync(
|
||||||
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
);
|
);
|
||||||
@@ -31,16 +34,21 @@ function request(method, url, data = {}) {
|
|||||||
if (token) header.Authorization = `Bearer ${token || ""}`;
|
if (token) header.Authorization = `Bearer ${token || ""}`;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
uni.request({
|
uni.request({
|
||||||
url: `${BASE_URL}${url}`,
|
url: `${baseUrl}${url}`,
|
||||||
method,
|
method,
|
||||||
header,
|
header,
|
||||||
data,
|
data,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
|
if (url === "/user/hardwareBox/connectWifi" && res.statusCode === 200 && res.data && Object.keys(res.data).length === 0) {
|
||||||
|
resolve({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
const {code, data, message} = res.data;
|
const {code, data, message} = res.data;
|
||||||
if (code === 0) resolve(data);
|
if (code === 0) resolve(data);
|
||||||
else if (message) {
|
else if (message) {
|
||||||
|
const error = {code, data, message};
|
||||||
if (message.indexOf("登录身份已失效") !== -1) {
|
if (message.indexOf("登录身份已失效") !== -1) {
|
||||||
console.log('1111111111111111111,token失效')
|
console.log('1111111111111111111,token失效')
|
||||||
uni.removeStorageSync(
|
uni.removeStorageSync(
|
||||||
@@ -50,6 +58,10 @@ function request(method, url, data = {}) {
|
|||||||
reject({ type: "AUTH_INVALID", message });
|
reject({ type: "AUTH_INVALID", message });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (message.indexOf("已达上限") !== -1) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (message === "ROOM_FULL") {
|
if (message === "ROOM_FULL") {
|
||||||
resolve({full: true});
|
resolve({full: true});
|
||||||
return;
|
return;
|
||||||
@@ -97,8 +109,10 @@ function request(method, url, data = {}) {
|
|||||||
title: message,
|
title: message,
|
||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
});
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
reject("");
|
reject({code, data, message});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
@@ -167,6 +181,10 @@ export const getAppConfig = () => {
|
|||||||
return request("GET", "/index/appConfig");
|
return request("GET", "/index/appConfig");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDailyCountAPI = () => {
|
||||||
|
return request("GET", "/index/dailyCount", {}, ADDONS_BASE_URL);
|
||||||
|
};
|
||||||
|
|
||||||
export const getHomeData = (seasonId) => {
|
export const getHomeData = (seasonId) => {
|
||||||
return request("GET", `/user/myHome?seasonId=${seasonId}`);
|
return request("GET", `/user/myHome?seasonId=${seasonId}`);
|
||||||
};
|
};
|
||||||
@@ -348,6 +366,17 @@ export const createOrderAPI = (vipId) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const virtualPayOrderAPI = (vipId = 0, code = "") => {
|
||||||
|
return request("POST", "/user/virtualPay/createOrder", {
|
||||||
|
vipId,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOrderDetailAPI = (orderId) => {
|
||||||
|
return request("GET", `/user/order/detail?orderId=${encodeURIComponent(orderId)}`);
|
||||||
|
};
|
||||||
|
|
||||||
export const payOrderAPI = (id) => {
|
export const payOrderAPI = (id) => {
|
||||||
return request("POST", "/user/order/pay", {
|
return request("POST", "/user/order/pay", {
|
||||||
id,
|
id,
|
||||||
@@ -460,6 +489,26 @@ export const getDeviceBatteryAPI = async () => {
|
|||||||
return request("GET", "/user/device/battery");
|
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) => {
|
export const addNoteAPI = async (id, remark) => {
|
||||||
return request("POST", "/user/score/sheet/remark", {id, remark});
|
return request("POST", "/user/score/sheet/remark", {id, remark});
|
||||||
};
|
};
|
||||||
@@ -472,6 +521,10 @@ export const getPhoneNumberAPI = (data) => {
|
|||||||
return request("POST", "/index/getPhone", data);
|
return request("POST", "/index/getPhone", data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPhoneNumberAPIv2 = (data) => {
|
||||||
|
return request("POST", "/index/getPhone/v2", data);
|
||||||
|
};
|
||||||
|
|
||||||
export const getPointBookRankListAPI = (page = 1) => {
|
export const getPointBookRankListAPI = (page = 1) => {
|
||||||
return request(
|
return request(
|
||||||
"GET",
|
"GET",
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export const audioFils = {
|
|||||||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya59b6pu0ur4um.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya59b6pu0ur4um.mp3",
|
||||||
比赛开始:
|
比赛开始:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcuu5z3a3lumkutske.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcuu5z3a3lumkutske.mp3",
|
||||||
|
下半场开始:
|
||||||
|
"https://static.shelingxingqiu.com/shootmini/static/audio/%E4%B8%8B%E5%8D%8A%E5%9C%BA%E5%BC%80%E5%A7%8B.mp3",
|
||||||
请开始射击:
|
请开始射击:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrl5u0iromqhf.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrl5u0iromqhf.mp3",
|
||||||
射击无效:
|
射击无效:
|
||||||
|
|||||||
@@ -456,23 +456,29 @@ export const generateShareImage = async (canvasId, data) => {
|
|||||||
// 2D 即时绘制,无需 ctx.draw()
|
// 2D 即时绘制,无需 ctx.draw()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("generateShareImage 绘制失败:", e);
|
console.error("generateShareImage 绘制失败:", e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 顶部导入与工具方法
|
// 顶部导入与工具方法
|
||||||
async function getCanvas2DContext(canvasId, targetWidth, targetHeight) {
|
async function getCanvas2DContext(canvasId, targetWidth, targetHeight) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
const query = uni.createSelectorQuery();
|
const query = uni.createSelectorQuery();
|
||||||
query
|
query
|
||||||
.select(`#${canvasId}`)
|
.select(`#${canvasId}`)
|
||||||
.fields({ node: true, size: true })
|
.fields({ node: true, size: true })
|
||||||
.exec((res) => {
|
.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 ctx = canvas.getContext("2d");
|
||||||
const dpr = uni.getSystemInfoSync().pixelRatio || 1;
|
const dpr = uni.getSystemInfoSync().pixelRatio || 1;
|
||||||
|
|
||||||
const w = targetWidth || res[0].width;
|
const w = targetWidth || canvasInfo.width;
|
||||||
const h = targetHeight || res[0].height;
|
const h = targetHeight || canvasInfo.height;
|
||||||
|
|
||||||
canvas.width = w * dpr;
|
canvas.width = w * dpr;
|
||||||
canvas.height = h * dpr;
|
canvas.height = h * dpr;
|
||||||
@@ -561,6 +567,7 @@ export const sharePointData = async (canvasId, data) => {
|
|||||||
// 2D 即时绘制,无需 ctx.draw()
|
// 2D 即时绘制,无需 ctx.draw()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("generateShareImage 绘制失败:", e);
|
console.error("generateShareImage 绘制失败:", e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -778,6 +785,7 @@ export async function sharePractiseData(canvasId, type, user, data) {
|
|||||||
// 2D 模式下无需 ctx.draw()
|
// 2D 模式下无需 ctx.draw()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true;
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #fff9;
|
color: #fff9;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding-top: 7px;
|
/* padding-top: 7px; */
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
.player-name {
|
.player-name {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const phase = ref("idle");
|
|||||||
const activePlayKey = ref("");
|
const activePlayKey = ref("");
|
||||||
const animationKey = ref("");
|
const animationKey = ref("");
|
||||||
const impactEmitted = ref(false);
|
const impactEmitted = ref(false);
|
||||||
|
const activeShot = ref(null);
|
||||||
let timers = [];
|
let timers = [];
|
||||||
|
|
||||||
const isActive = computed(() => phase.value !== "idle");
|
const isActive = computed(() => phase.value !== "idle");
|
||||||
@@ -54,9 +55,17 @@ const safeTargetSize = computed(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function hasShotPoint(shot) {
|
||||||
|
const x = Number(shot?.x);
|
||||||
|
const y = Number(shot?.y);
|
||||||
|
return Number.isFinite(x) && Number.isFinite(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveShot = computed(() => activeShot.value || props.shot);
|
||||||
|
|
||||||
const shotPoint = computed(() => {
|
const shotPoint = computed(() => {
|
||||||
const x = Number(props.shot?.x);
|
const x = Number(effectiveShot.value?.x);
|
||||||
const y = Number(props.shot?.y);
|
const y = Number(effectiveShot.value?.y);
|
||||||
return {
|
return {
|
||||||
x: Number.isFinite(x) ? x : 0,
|
x: Number.isFinite(x) ? x : 0,
|
||||||
y: Number.isFinite(y) ? y : 0,
|
y: Number.isFinite(y) ? y : 0,
|
||||||
@@ -179,16 +188,20 @@ function finish(playKey) {
|
|||||||
clearTimers();
|
clearTimers();
|
||||||
phase.value = "idle";
|
phase.value = "idle";
|
||||||
activePlayKey.value = "";
|
activePlayKey.value = "";
|
||||||
|
activeShot.value = null;
|
||||||
emit("complete", playKey);
|
emit("complete", playKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
function play() {
|
function play() {
|
||||||
if (!props.playKey || !props.shot || !props.shot.ring) return;
|
if (!props.playKey || !props.shot || !props.shot.ring || !hasShotPoint(props.shot)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
clearTimers();
|
clearTimers();
|
||||||
activePlayKey.value = props.playKey;
|
activePlayKey.value = props.playKey;
|
||||||
animationKey.value = `${props.playKey}`;
|
animationKey.value = `${props.playKey}`;
|
||||||
impactEmitted.value = false;
|
impactEmitted.value = false;
|
||||||
|
activeShot.value = { ...props.shot };
|
||||||
phase.value = "playing";
|
phase.value = "playing";
|
||||||
|
|
||||||
queueTimer(() => {
|
queueTimer(() => {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onBeforeUnmount, computed, nextTick } from "vue";
|
import {
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
computed,
|
||||||
|
nextTick,
|
||||||
|
getCurrentInstance,
|
||||||
|
} from "vue";
|
||||||
import PointSwitcher from "@/components/PointSwitcher.vue";
|
import PointSwitcher from "@/components/PointSwitcher.vue";
|
||||||
import BowShotEffect from "@/components/BowShotEffect.vue";
|
import BowShotEffect from "@/components/BowShotEffect.vue";
|
||||||
|
|
||||||
@@ -56,8 +64,6 @@ const props = defineProps({
|
|||||||
const pMode = ref(true);
|
const pMode = ref(true);
|
||||||
const latestOne = ref(null);
|
const latestOne = ref(null);
|
||||||
const bluelatestOne = ref(null);
|
const bluelatestOne = ref(null);
|
||||||
const prevScores = ref([]);
|
|
||||||
const prevBlueScores = ref([]);
|
|
||||||
const timer = ref(null);
|
const timer = ref(null);
|
||||||
const dirTimer = ref(null);
|
const dirTimer = ref(null);
|
||||||
const angle = ref(null);
|
const angle = ref(null);
|
||||||
@@ -66,7 +72,9 @@ const shotEffect = ref(null);
|
|||||||
const hiddenRedLatestKey = ref("");
|
const hiddenRedLatestKey = ref("");
|
||||||
const hiddenBlueLatestKey = ref("");
|
const hiddenBlueLatestKey = ref("");
|
||||||
const targetShaking = ref(false);
|
const targetShaking = ref(false);
|
||||||
|
const targetSize = ref({ width: 0, height: 0 });
|
||||||
const shakeTimer = ref(null);
|
const shakeTimer = ref(null);
|
||||||
|
const instance = getCurrentInstance();
|
||||||
const ROUND_TIP_OFFSET_Y = -32;
|
const ROUND_TIP_OFFSET_Y = -32;
|
||||||
const EXPERIENCE_TIP_OFFSET_Y = -68;
|
const EXPERIENCE_TIP_OFFSET_Y = -68;
|
||||||
|
|
||||||
@@ -82,8 +90,14 @@ function buildShotEffectKey(team, shot, index) {
|
|||||||
].join("-");
|
].join("-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasShotPoint(shot) {
|
||||||
|
const x = Number(shot?.x);
|
||||||
|
const y = Number(shot?.y);
|
||||||
|
return Number.isFinite(x) && Number.isFinite(y);
|
||||||
|
}
|
||||||
|
|
||||||
function shouldPlayShotEffect(shot) {
|
function shouldPlayShotEffect(shot) {
|
||||||
return props.isSvip && !!shot && Number(shot.ring) > 0;
|
return props.isSvip && !!shot && Number(shot.ring) > 0 && hasShotPoint(shot);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearTipTimer() {
|
function clearTipTimer() {
|
||||||
@@ -154,6 +168,29 @@ function shakeTarget() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateTargetSize() {
|
||||||
|
nextTick(() => {
|
||||||
|
const query = instance?.proxy
|
||||||
|
? uni.createSelectorQuery().in(instance.proxy)
|
||||||
|
: uni.createSelectorQuery();
|
||||||
|
|
||||||
|
query
|
||||||
|
.select(".target")
|
||||||
|
.boundingClientRect((rect) => {
|
||||||
|
const width = Number(rect?.width);
|
||||||
|
const height = Number(rect?.height);
|
||||||
|
if (!Number.isFinite(width) || !Number.isFinite(height)) return;
|
||||||
|
if (width <= 0 || height <= 0) return;
|
||||||
|
targetSize.value = { width, height };
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWindowResize() {
|
||||||
|
updateTargetSize();
|
||||||
|
}
|
||||||
|
|
||||||
function shouldHideRedHit(index) {
|
function shouldHideRedHit(index) {
|
||||||
return !!hiddenRedLatestKey.value && index === props.scores.length - 1;
|
return !!hiddenRedLatestKey.value && index === props.scores.length - 1;
|
||||||
}
|
}
|
||||||
@@ -163,46 +200,44 @@ function shouldHideBlueHit(index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.scores,
|
() => props.scores.length,
|
||||||
(newVal) => {
|
(newLen, oldLen) => {
|
||||||
if (newVal.length - prevScores.value.length === 1) {
|
if (newLen === oldLen + 1) {
|
||||||
const latestShot = newVal[newVal.length - 1];
|
const latestShot = props.scores[newLen - 1];
|
||||||
if (shouldPlayShotEffect(latestShot)) {
|
if (shouldPlayShotEffect(latestShot)) {
|
||||||
triggerShotEffect("red", latestShot, newVal.length - 1);
|
triggerShotEffect("red", latestShot, newLen - 1);
|
||||||
} else {
|
} else {
|
||||||
showShotTip("red", latestShot);
|
showShotTip("red", latestShot);
|
||||||
}
|
}
|
||||||
} else if (newVal.length <= prevScores.value.length) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newLen < oldLen) {
|
||||||
latestOne.value = null;
|
latestOne.value = null;
|
||||||
hiddenRedLatestKey.value = "";
|
hiddenRedLatestKey.value = "";
|
||||||
if (shotEffect.value?.team === "red") shotEffect.value = null;
|
if (shotEffect.value?.team === "red") shotEffect.value = null;
|
||||||
}
|
}
|
||||||
prevScores.value = [...newVal];
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deep: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.blueScores,
|
() => props.blueScores.length,
|
||||||
(newVal) => {
|
(newLen, oldLen) => {
|
||||||
if (newVal.length - prevBlueScores.value.length === 1) {
|
if (newLen === oldLen + 1) {
|
||||||
const latestShot = newVal[newVal.length - 1];
|
const latestShot = props.blueScores[newLen - 1];
|
||||||
if (shouldPlayShotEffect(latestShot)) {
|
if (shouldPlayShotEffect(latestShot)) {
|
||||||
triggerShotEffect("blue", latestShot, newVal.length - 1);
|
triggerShotEffect("blue", latestShot, newLen - 1);
|
||||||
} else {
|
} else {
|
||||||
showShotTip("blue", latestShot);
|
showShotTip("blue", latestShot);
|
||||||
}
|
}
|
||||||
} else if (newVal.length <= prevBlueScores.value.length) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newLen < oldLen) {
|
||||||
bluelatestOne.value = null;
|
bluelatestOne.value = null;
|
||||||
hiddenBlueLatestKey.value = "";
|
hiddenBlueLatestKey.value = "";
|
||||||
if (shotEffect.value?.team === "blue") shotEffect.value = null;
|
if (shotEffect.value?.team === "blue") shotEffect.value = null;
|
||||||
}
|
}
|
||||||
prevBlueScores.value = [...newVal];
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deep: true,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -349,6 +384,8 @@ async function onReceiveMessage(message) {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
|
updateTargetSize();
|
||||||
|
if (uni.onWindowResize) uni.onWindowResize(handleWindowResize);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -365,6 +402,7 @@ onBeforeUnmount(() => {
|
|||||||
shakeTimer.value = null;
|
shakeTimer.value = null;
|
||||||
}
|
}
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
uni.$off("socket-inbox", onReceiveMessage);
|
||||||
|
if (uni.offWindowResize) uni.offWindowResize(handleWindowResize);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -461,6 +499,9 @@ onBeforeUnmount(() => {
|
|||||||
:shot="shotEffect && shotEffect.shot"
|
:shot="shotEffect && shotEffect.shot"
|
||||||
:playKey="shotEffect ? shotEffect.key : ''"
|
:playKey="shotEffect ? shotEffect.key : ''"
|
||||||
:targetRadius="safeTargetRadius"
|
:targetRadius="safeTargetRadius"
|
||||||
|
:targetWidth="targetSize.width"
|
||||||
|
:targetHeight="targetSize.height"
|
||||||
|
:hitOffsetPx="currentHitRadiusPx"
|
||||||
@impact="shakeTarget"
|
@impact="shakeTarget"
|
||||||
@complete="completeShotEffect"
|
@complete="completeShotEffect"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
titleStyle: {
|
||||||
|
type: [String, Object, Array],
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
showBottom: {
|
showBottom: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
@@ -155,6 +159,7 @@ const goCalibration = async () => {
|
|||||||
:title="title"
|
:title="title"
|
||||||
:onBack="onBack"
|
:onBack="onBack"
|
||||||
:whiteBackArrow="whiteBackArrow"
|
:whiteBackArrow="whiteBackArrow"
|
||||||
|
:titleStyle="titleStyle"
|
||||||
/>
|
/>
|
||||||
<BackToGame v-if="showBackToGame" />
|
<BackToGame v-if="showBackToGame" />
|
||||||
<scroll-view
|
<scroll-view
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
titleStyle: {
|
||||||
|
type: [String, Object, Array],
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
@@ -118,7 +122,9 @@ onBeforeUnmount(() => {
|
|||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view :style="{ color: whiteBackArrow ? '#fff' : '#000' }">
|
<view
|
||||||
|
:style="[{ color: whiteBackArrow ? '#fff' : '#000' }, titleStyle]"
|
||||||
|
>
|
||||||
<view
|
<view
|
||||||
v-if="currentPage === 'pages/point-book'"
|
v-if="currentPage === 'pages/point-book'"
|
||||||
class="user-header"
|
class="user-header"
|
||||||
|
|||||||
@@ -0,0 +1,429 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { getDeviceBatteryAPI } from "@/apis";
|
||||||
|
|
||||||
|
const OTA_MIN_BATTERY = 50;
|
||||||
|
const OTA_LOW_BATTERY_TEXT = "电量不足 50%,暂不支持 OTA 升级";
|
||||||
|
const OTA_OFFLINE_TEXT = "请先开启智能弓";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
type: String,
|
||||||
|
default: "new_version", // new_version | update_progress | update_success | update_failure
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
type: Number,
|
||||||
|
default: 40,
|
||||||
|
},
|
||||||
|
// 副标题:如“新版本将优化智能弓体验”
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
// 详细说明:如“升级前请确保:...”
|
||||||
|
changelog: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
forceUpdate: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update", "skip", "close", "done", "retry"]);
|
||||||
|
|
||||||
|
const isNewVersion = computed(() => props.state === "new_version");
|
||||||
|
const isProgress = computed(() => props.state === "update_progress");
|
||||||
|
const isSuccess = computed(() => props.state === "update_success");
|
||||||
|
const isFailure = computed(() => props.state === "update_failure");
|
||||||
|
// Clamp progress to keep the progress bar width within its container.
|
||||||
|
const progressValue = computed(() => Math.min(100, Math.max(0, Number(props.progress) || 0)));
|
||||||
|
|
||||||
|
// 点击立即更新前先校验设备在线状态,再校验设备电量。
|
||||||
|
const handleUpdateClick = async () => {
|
||||||
|
try {
|
||||||
|
const deviceStatus = await getDeviceBatteryAPI();
|
||||||
|
if (deviceStatus?.online !== true) {
|
||||||
|
uni.showToast({
|
||||||
|
title: OTA_OFFLINE_TEXT,
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(deviceStatus?.battery) <= OTA_MIN_BATTERY) {
|
||||||
|
uni.showToast({
|
||||||
|
title: OTA_LOW_BATTERY_TEXT,
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
emit("update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("update");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view v-if="visible" class="ota-mask">
|
||||||
|
<!-- 图标 + 弹窗卡片 容器 -->
|
||||||
|
<view
|
||||||
|
class="ota-outer"
|
||||||
|
:class="isNewVersion ? 'outer-new' : 'outer-result'"
|
||||||
|
>
|
||||||
|
<!-- 悬浮图标(溢出卡片顶部) -->
|
||||||
|
<image
|
||||||
|
v-if="isNewVersion"
|
||||||
|
src="https://static.shelingxingqiu.com/shootmini/static/ota/ota-mascot.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="float-icon float-mascot"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-else-if="isSuccess"
|
||||||
|
src="https://static.shelingxingqiu.com/shootmini/static/ota/check-char.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="float-icon float-check"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-else-if="isFailure"
|
||||||
|
src="https://static.shelingxingqiu.com/shootmini/static/ota/close-char.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="float-icon float-close"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-else-if="isProgress"
|
||||||
|
src="https://static.shelingxingqiu.com/shootmini/static/ota/target-char.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="float-icon float-target"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 弹窗卡片:overflow:visible 允许按钮溢出底部,背景图通过 ota-bg-clip 独立裁剪保持圆角 -->
|
||||||
|
<view class="ota-dialog">
|
||||||
|
<view class="ota-bg-clip">
|
||||||
|
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/ota-bg.png" mode="aspectFill" class="ota-bg" />
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="ota-content"
|
||||||
|
:class="{ 'content-new': isNewVersion, 'content-result': isProgress || isSuccess || isFailure }"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- 发现新版本:new-ver.png 已包含标题图,不再重复文字;版本号使用 ota-ver.png 胶囊背景 -->
|
||||||
|
<block v-if="isNewVersion">
|
||||||
|
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/new-ver.png" mode="aspectFit" class="new-ver-img" />
|
||||||
|
<view v-if="version" class="version-tag-wrap">
|
||||||
|
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/ota-ver.png" mode="aspectFit" class="version-tag-bg-img" />
|
||||||
|
<text class="version-tag">{{ version }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 副标题:如“新版本将优化智能弓体验”,离下方详情 12rpx -->
|
||||||
|
<text v-if="description" class="desc-text">{{ description }}</text>
|
||||||
|
<!-- 详细说明:如“升级前请确保:...” -->
|
||||||
|
<text v-if="changelog" class="changelog-text">{{ changelog }}</text>
|
||||||
|
<view class="btn-group">
|
||||||
|
<view class="primary-btn" @click="handleUpdateClick">
|
||||||
|
<text class="primary-btn-text">立即更新</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="!forceUpdate" class="skip-text" @click="emit('skip')">暂不更新</text>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<!-- 更新成功:图片左边距 34rpx,文案左边距 44rpx,按钮浮动底部居中 -->
|
||||||
|
<block v-else-if="isSuccess">
|
||||||
|
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/update-ok.png" mode="aspectFit" class="result-title-img" style="width: 220rpx; height: 62rpx;" />
|
||||||
|
<text class="dialog-desc">请关机并重启智能弓</text>
|
||||||
|
<view class="btn-group-result">
|
||||||
|
<view class="primary-btn" @click="emit('done')">
|
||||||
|
<text class="primary-btn-text">完成</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<!-- 更新中:复用成功标题图,正文区域展示进度条,无底部按钮 -->
|
||||||
|
<block v-else-if="isProgress">
|
||||||
|
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/update_progress.png" mode="aspectFit" class="result-title-img" style="width: 220rpx; height: 62rpx;" />
|
||||||
|
<view class="progress-wrap">
|
||||||
|
<view class="progress-track">
|
||||||
|
<view class="progress-fill" :style="{ width: `${progressValue}%` }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<!-- 更新失败:图片左边距 34rpx,文案左对齐 44rpx,按钮浮动底部居中 -->
|
||||||
|
<block v-else-if="isFailure">
|
||||||
|
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/update-fail.png" mode="aspectFit" class="result-title-img" style="width: 222rpx; height: 62rpx;" />
|
||||||
|
<text class="dialog-desc">请确保:</text>
|
||||||
|
<text class="dialog-desc">1、智能弓已开启</text>
|
||||||
|
<text class="dialog-desc">2、网路连接稳定</text>
|
||||||
|
<view class="btn-group-result">
|
||||||
|
<view class="primary-btn" @click="emit('retry')">
|
||||||
|
<text class="primary-btn-text">重试</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关闭按钮(仅新版本状态,非强制更新时,位于弹窗下方) -->
|
||||||
|
<view
|
||||||
|
v-if="(isNewVersion || isFailure) && !forceUpdate"
|
||||||
|
class="ota-close-below"
|
||||||
|
@click="emit('close')"
|
||||||
|
>
|
||||||
|
<image src="../static/sicon/close.png" mode="aspectFit" style="width: 56rpx; height: 56rpx;" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ota-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 外层容器:相对定位,为浮动图标创造溢出空间 */
|
||||||
|
.ota-outer {
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
/* 设计图:吉祥物向上突出弹窗顶部 40px(375px基准)× 2 = 80rpx */
|
||||||
|
.outer-new {
|
||||||
|
padding-top: 80rpx;
|
||||||
|
}
|
||||||
|
.outer-result {
|
||||||
|
padding-top: 80rpx;
|
||||||
|
padding-bottom: 66rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浮动图标(绝对定位,位于卡片顶部上方) */
|
||||||
|
.float-icon {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
/* 吉祥物尺寸:设计图 149×109px(375px基准)× 2 = 298×218rpx */
|
||||||
|
.float-mascot {
|
||||||
|
width: 298rpx;
|
||||||
|
height: 218rpx;
|
||||||
|
top: -5px;
|
||||||
|
right: -74rpx;
|
||||||
|
}
|
||||||
|
.float-check {
|
||||||
|
width: 194rpx;
|
||||||
|
height: 166rpx;
|
||||||
|
top: 20px;
|
||||||
|
right: 30rpx;
|
||||||
|
}
|
||||||
|
.float-close {
|
||||||
|
width: 194rpx;
|
||||||
|
height: 164rpx;
|
||||||
|
top: 20px;
|
||||||
|
right: 30rpx;
|
||||||
|
}
|
||||||
|
.float-target {
|
||||||
|
width: 194rpx;
|
||||||
|
height: 166rpx;
|
||||||
|
top: 20px;
|
||||||
|
right: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗卡片:overflow:visible 允许按钮溢出底部,背景通过 ota-bg-clip 独立裁剪 */
|
||||||
|
.ota-dialog {
|
||||||
|
position: relative;
|
||||||
|
width: 482rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border: 2rpx solid #F9D5A1;
|
||||||
|
overflow: visible;
|
||||||
|
background-color: #392F1D;
|
||||||
|
}
|
||||||
|
/* 背景图裁剪层:独立 overflow:hidden + border-radius 保持圆角效果 */
|
||||||
|
.ota-bg-clip {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.ota-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.ota-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* 按钮以外内容均左对齐 */
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.content-new {
|
||||||
|
padding: 30rpx 0 40rpx 0;
|
||||||
|
}
|
||||||
|
.content-result {
|
||||||
|
padding: 30rpx 0 66rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 发现新版本内容 */
|
||||||
|
.new-ver-img {
|
||||||
|
width: 274rpx;
|
||||||
|
height: 62rpx;
|
||||||
|
/* 左边距 34rpx,去掉 margin-bottom */
|
||||||
|
margin-left: 34rpx;
|
||||||
|
}
|
||||||
|
/* 版本号胶囊容器:相对定位,使 ota-ver.png 作为背景衬底 */
|
||||||
|
.version-tag-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 116rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
/* 离标题图 -10rpx,左边距 50rpx,离下方副标题 22rpx */
|
||||||
|
margin-top: -10rpx;
|
||||||
|
margin-left: 50rpx;
|
||||||
|
margin-bottom: 22rpx;
|
||||||
|
}
|
||||||
|
/* ota-ver.png 胶囊背景图 */
|
||||||
|
.version-tag-bg-img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
/* 版本号文字:浮于背景图之上 */
|
||||||
|
.version-tag {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
color: rgba(254, 222, 100, 1);
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 8rpx 22rpx 4rpx 24rpx;
|
||||||
|
}
|
||||||
|
/* 副标题(如"新版本将优化智能弓体验"):左边距 44rpx,离下方文案 12rpx */
|
||||||
|
.desc-text {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
line-height: 36rpx;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 44rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
/* 详细说明文案(如“升级前请确保:...”):左边距 44rpx */
|
||||||
|
.changelog-text {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 44rpx;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮组(新版本状态):离上方文案 30rpx,内部按钮间距 24rpx */
|
||||||
|
.btn-group {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
/* 按钮组(结果状态):绝对定位,溢出卡片底边 -35rpx 悬浮在底边中间 */
|
||||||
|
.btn-group-result {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -35rpx;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
/* 主按钮:按照设计规范 width: 232rpx, height: 70rpx */
|
||||||
|
.primary-btn {
|
||||||
|
width: 232rpx;
|
||||||
|
height: 70rpx;
|
||||||
|
background-color: #FED847;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.primary-btn-text {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #000000;
|
||||||
|
line-height: 36rpx;
|
||||||
|
}
|
||||||
|
/* 暂不更新:设计规范颜色 #5FADFF 蓝色 */
|
||||||
|
.skip-text {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #5FADFF;
|
||||||
|
line-height: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 更新结果内容:图片左边距 34rpx,下边距 16rpx */
|
||||||
|
.result-title-img {
|
||||||
|
margin-left: 34rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
/* 结果页文案:左对齐,左边距 44rpx,与 new_version 保持一致 */
|
||||||
|
.dialog-desc {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 44rpx;
|
||||||
|
}
|
||||||
|
.progress-wrap {
|
||||||
|
width: 394rpx;
|
||||||
|
margin-top: 40rpx;
|
||||||
|
margin-left: 44rpx;
|
||||||
|
}
|
||||||
|
.progress-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 18rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.28);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #FED847;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关闭按钮(位于弹窗下方) */
|
||||||
|
.ota-close-below {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -70,6 +70,11 @@ const arrows = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
|
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;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -112,9 +117,10 @@ const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
|
|||||||
:onClick="onClickShare"
|
:onClick="onClickShare"
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
v-if="isMember"
|
||||||
name="教练点评"
|
name="教练点评"
|
||||||
src="../static/review.png"
|
src="../static/review.png"
|
||||||
:onClick="() => (showComment = true)"
|
:onClick="openCoachComment"
|
||||||
/>
|
/>
|
||||||
</block>
|
</block>
|
||||||
<SButton
|
<SButton
|
||||||
@@ -137,7 +143,7 @@ const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
|
|||||||
}}</text
|
}}</text
|
||||||
>环的成绩,所有箭支上靶后的平均点间距为<text
|
>环的成绩,所有箭支上靶后的平均点间距为<text
|
||||||
:style="{ color: '#fed847' }"
|
:style="{ color: '#fed847' }"
|
||||||
>{{ Number((result.average_distance || 0).toFixed(2)) }}</text
|
>{{ Number((result?.interpretation?.spreadStability || 0).toFixed(2)) }}</text
|
||||||
>,{{
|
>,{{
|
||||||
result.spreadEvaluation === "Dispersed"
|
result.spreadEvaluation === "Dispersed"
|
||||||
? "还需要持续改进哦~"
|
? "还需要持续改进哦~"
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
halfRest: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
onStop: {
|
onStop: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
@@ -136,8 +140,9 @@ const updateSound = () => {
|
|||||||
async function onReceiveMessage(msg) {
|
async function onReceiveMessage(msg) {
|
||||||
if (Array.isArray(msg)) return;
|
if (Array.isArray(msg)) return;
|
||||||
if (msg.type === MESSAGETYPESV2.BattleStart) {
|
if (msg.type === MESSAGETYPESV2.BattleStart) {
|
||||||
|
const audioKey = props.melee && (halfTime.value || props.halfRest) ? "下半场开始" : "比赛开始";
|
||||||
halfTime.value = false;
|
halfTime.value = false;
|
||||||
audioManager.play("比赛开始");
|
audioManager.play(audioKey);
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
audioManager.play("比赛结束", false);
|
audioManager.play("比赛结束", false);
|
||||||
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
loginAPI,
|
loginAPI,
|
||||||
getHomeData,
|
getHomeData,
|
||||||
getPhoneNumberAPI,
|
getPhoneNumberAPI,
|
||||||
|
getPhoneNumberAPIv2,
|
||||||
getDeviceBatteryAPI,
|
getDeviceBatteryAPI,
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
|
|
||||||
@@ -43,12 +44,12 @@ const handleAgree = () => {
|
|||||||
|
|
||||||
async function getphonenumber(e) {
|
async function getphonenumber(e) {
|
||||||
if (e.detail.code) {
|
if (e.detail.code) {
|
||||||
const wxResult = await wxLogin();
|
// const wxResult = await wxLogin();
|
||||||
const result = await getPhoneNumberAPI({
|
const result = await getPhoneNumberAPIv2({
|
||||||
...e.detail,
|
// ...e.detail,
|
||||||
code: wxResult.code,
|
code: e.detail.code,
|
||||||
});
|
});
|
||||||
if (result.phone) phone.value = result.phone;
|
if (result.purePhoneNumber) phone.value = result.purePhoneNumber;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,11 @@
|
|||||||
"usingComponents" : true,
|
"usingComponents" : true,
|
||||||
"darkmode" : true,
|
"darkmode" : true,
|
||||||
"themeLocation" : "theme.json",
|
"themeLocation" : "theme.json",
|
||||||
"permission" : {},
|
"permission" : {
|
||||||
|
"scope.userLocation": {
|
||||||
|
"desc": "用于扫描附近 WiFi,完成设备 OTA 升级网络连接"
|
||||||
|
}
|
||||||
|
},
|
||||||
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
|
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,9 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/member/vip-intro"
|
"path": "pages/member/vip-intro"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/member/agreement"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/grade-intro"
|
"path": "pages/grade-intro"
|
||||||
},
|
},
|
||||||
@@ -122,6 +125,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/mine-bow-data"
|
"path": "pages/mine-bow-data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/ota-wifi",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import GuideTwo from "@/components/GuideTwo.vue";
|
|||||||
import SButton from "@/components/SButton.vue";
|
import SButton from "@/components/SButton.vue";
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import Avatar from "@/components/Avatar.vue";
|
||||||
import ScreenHint from "@/components/ScreenHint.vue";
|
import ScreenHint from "@/components/ScreenHint.vue";
|
||||||
|
import ModalDialog from "@/components/ModalDialog.vue";
|
||||||
import {
|
import {
|
||||||
getRoomAPI,
|
getRoomAPI,
|
||||||
exitRoomAPI,
|
exitRoomAPI,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
getReadyAPI,
|
getReadyAPI,
|
||||||
kickPlayerAPI,
|
kickPlayerAPI,
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
|
import { isLimitError } from "@/util";
|
||||||
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
@@ -56,6 +58,7 @@ const ready = ref(false);
|
|||||||
const allReady = ref(false);
|
const allReady = ref(false);
|
||||||
const timer = ref(null);
|
const timer = ref(null);
|
||||||
const goBattle = ref(false);
|
const goBattle = ref(false);
|
||||||
|
const showLimitModal = ref(false);
|
||||||
/** 从结算页返回时为 true,跳过进场靶纸语音 */
|
/** 从结算页返回时为 true,跳过进场靶纸语音 */
|
||||||
const skipTargetAudio = ref(false);
|
const skipTargetAudio = ref(false);
|
||||||
|
|
||||||
@@ -137,7 +140,26 @@ async function refreshRoomData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getReady = async () => {
|
const getReady = async () => {
|
||||||
|
try {
|
||||||
await getReadyAPI(roomNumber.value);
|
await getReadyAPI(roomNumber.value);
|
||||||
|
} catch (error) {
|
||||||
|
if (isLimitError(error)) {
|
||||||
|
showLimitModal.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("room ready error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeLimitModal = () => {
|
||||||
|
showLimitModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const goVipPage = () => {
|
||||||
|
showLimitModal.value = false;
|
||||||
|
uni.navigateTo({
|
||||||
|
url: "/pages/member/be-vip",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshMembers = (members = []) => {
|
const refreshMembers = (members = []) => {
|
||||||
@@ -456,6 +478,14 @@ onBeforeUnmount(() => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
</Container>
|
||||||
|
<ModalDialog
|
||||||
|
:show="showLimitModal"
|
||||||
|
:content="'今日约战次数已经用完\n开通会员可增加次数'"
|
||||||
|
cancelText="知道了"
|
||||||
|
confirmText="去开通"
|
||||||
|
:onCancel="closeLimitModal"
|
||||||
|
:onConfirm="goVipPage"
|
||||||
|
/>
|
||||||
<!-- 踢出玩家二次确认弹窗(不传 onClose,屏蔽 X 关闭按钮) -->
|
<!-- 踢出玩家二次确认弹窗(不传 onClose,屏蔽 X 关闭按钮) -->
|
||||||
<ScreenHint :show="showKickConfirm">
|
<ScreenHint :show="showKickConfirm">
|
||||||
<view class="kick-confirm">
|
<view class="kick-confirm">
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { storeToRefs } from "pinia";
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { user } = storeToRefs(store);
|
const { user } = storeToRefs(store);
|
||||||
const scores = ref([]);
|
const scores = ref([]);
|
||||||
|
const isSvip = ref(false);
|
||||||
const step = ref(0);
|
const step = ref(0);
|
||||||
const total = 12;
|
const total = 12;
|
||||||
const stepButtonTexts = [
|
const stepButtonTexts = [
|
||||||
@@ -48,6 +49,7 @@ const practiseId = ref("");
|
|||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const laserActive = ref(false);
|
const laserActive = ref(false);
|
||||||
const guideSwiperIndex = ref(0);
|
const guideSwiperIndex = ref(0);
|
||||||
|
const sharing = ref(false);
|
||||||
|
|
||||||
const guideImages = [
|
const guideImages = [
|
||||||
"https://static.shelingxingqiu.com/shootmini/static/target.png",
|
"https://static.shelingxingqiu.com/shootmini/static/target.png",
|
||||||
@@ -113,6 +115,7 @@ const onOver = async () => {
|
|||||||
|
|
||||||
async function onReceiveMessage(msg) {
|
async function onReceiveMessage(msg) {
|
||||||
if (msg.type === MESSAGETYPESV2.ShootResult) {
|
if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||||
|
isSvip.value = msg.sVip === true;
|
||||||
scores.value = msg.details;
|
scores.value = msg.details;
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
setTimeout(onOver, 1500);
|
setTimeout(onOver, 1500);
|
||||||
@@ -139,8 +142,19 @@ async function onReceiveMessage(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onClickShare = debounce(async () => {
|
const onClickShare = debounce(async () => {
|
||||||
|
if (sharing.value) return;
|
||||||
|
sharing.value = true;
|
||||||
|
try {
|
||||||
await sharePractiseData("shareCanvas", 1, user.value, practiseResult.value);
|
await sharePractiseData("shareCanvas", 1, user.value, practiseResult.value);
|
||||||
await wxShare("shareCanvas");
|
await wxShare("shareCanvas");
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "海报生成失败,请稍后重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
sharing.value = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -192,6 +206,7 @@ const nextStep = async () => {
|
|||||||
title.value = "小试牛刀";
|
title.value = "小试牛刀";
|
||||||
await startPractiseAPI();
|
await startPractiseAPI();
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
|
isSvip.value = false;
|
||||||
step.value = 5;
|
step.value = 5;
|
||||||
start.value = true;
|
start.value = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -218,6 +233,7 @@ const onClose = async () => {
|
|||||||
practiseResult.value = {};
|
practiseResult.value = {};
|
||||||
start.value = false;
|
start.value = false;
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
|
isSvip.value = false;
|
||||||
step.value = 4;
|
step.value = 4;
|
||||||
const result = await createPractiseAPI(total, 120);
|
const result = await createPractiseAPI(total, 120);
|
||||||
if (result) practiseId.value = result.id;
|
if (result) practiseId.value = result.id;
|
||||||
@@ -331,6 +347,7 @@ const onClose = async () => {
|
|||||||
:currentRound="step === 5 ? scores.length : 0"
|
:currentRound="step === 5 ? scores.length : 0"
|
||||||
:totalRound="step === 5 ? total : 0"
|
:totalRound="step === 5 ? total : 0"
|
||||||
:scores="scores"
|
:scores="scores"
|
||||||
|
:isSvip="isSvip"
|
||||||
/>
|
/>
|
||||||
<ScorePanel
|
<ScorePanel
|
||||||
v-if="step === 5"
|
v-if="step === 5"
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ import SModal from "@/components/SModal.vue";
|
|||||||
import Signin from "@/components/Signin.vue";
|
import Signin from "@/components/Signin.vue";
|
||||||
import CreateRoom from "@/components/CreateRoom.vue";
|
import CreateRoom from "@/components/CreateRoom.vue";
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import Avatar from "@/components/Avatar.vue";
|
||||||
|
import ModalDialog from "@/components/ModalDialog.vue";
|
||||||
|
|
||||||
import { getRoomAPI, joinRoomAPI, getBattleDataAPI } from "@/apis";
|
import { getRoomAPI, joinRoomAPI, getBattleDataAPI, getDailyCountAPI } from "@/apis";
|
||||||
import { debounce, canEenter } from "@/util";
|
import { debounce, canEenter, getLimitCountText, isLimitReached } from "@/util";
|
||||||
|
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { user, device, online, game } = storeToRefs(store);
|
const { user, device, online, game, dailyCount } = storeToRefs(store);
|
||||||
|
const { updateDailyCount } = store;
|
||||||
|
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
const showSignin = ref(false);
|
const showSignin = ref(false);
|
||||||
@@ -24,8 +26,12 @@ const roomNumber = ref("");
|
|||||||
const data = ref({});
|
const data = ref({});
|
||||||
const roomID = ref("");
|
const roomID = ref("");
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const showLimitModal = ref(false);
|
||||||
const isSVip = computed(() => user.value.sVip === true);
|
const isSVip = computed(() => user.value.sVip === true);
|
||||||
const isVip = computed(() => user.value.vip === true && !isSVip.value);
|
const isVip = computed(() => user.value.vip === true && !isSVip.value);
|
||||||
|
const challengeLimitText = computed(() =>
|
||||||
|
getLimitCountText("约战", dailyCount.value.challenge)
|
||||||
|
);
|
||||||
|
|
||||||
const enterRoom = debounce(async (number) => {
|
const enterRoom = debounce(async (number) => {
|
||||||
if (loading.value) return;
|
if (loading.value) return;
|
||||||
@@ -67,9 +73,37 @@ const enterRoom = debounce(async (number) => {
|
|||||||
});
|
});
|
||||||
const onCreateRoom = async () => {
|
const onCreateRoom = async () => {
|
||||||
if (!canEenter(user.value, device.value, online.value)) return;
|
if (!canEenter(user.value, device.value, online.value)) return;
|
||||||
|
const countData = await loadDailyCount();
|
||||||
|
if (isLimitReached(countData.challenge)) {
|
||||||
|
showLimitModal.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
warnning.value = "";
|
warnning.value = "";
|
||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 onSignin = () => {
|
const onSignin = () => {
|
||||||
if (roomID.value && user.value.id) enterRoom(roomID.value);
|
if (roomID.value && user.value.id) enterRoom(roomID.value);
|
||||||
showSignin.value = false;
|
showSignin.value = false;
|
||||||
@@ -83,7 +117,10 @@ const goMyRecord = () => {
|
|||||||
};
|
};
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
if (user.value.id) {
|
if (user.value.id) {
|
||||||
const result = await getBattleDataAPI();
|
const [result] = await Promise.all([
|
||||||
|
getBattleDataAPI(),
|
||||||
|
loadDailyCount(),
|
||||||
|
]);
|
||||||
data.value = result;
|
data.value = result;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -168,6 +205,9 @@ onLoad(async (options) => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
|
<view v-if="challengeLimitText" class="pp-text">
|
||||||
|
{{ challengeLimitText }}
|
||||||
|
</view>
|
||||||
<SButton width="80%" :rounded="30" :onClick="() => $clickSound(onCreateRoom)">
|
<SButton width="80%" :rounded="30" :onClick="() => $clickSound(onCreateRoom)">
|
||||||
创建约战房
|
创建约战房
|
||||||
</SButton>
|
</SButton>
|
||||||
@@ -183,6 +223,14 @@ onLoad(async (options) => {
|
|||||||
<Signin :show="showSignin" :onClose="onSignin" />
|
<Signin :show="showSignin" :onClose="onSignin" />
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
</Container>
|
||||||
|
<ModalDialog
|
||||||
|
:show="showLimitModal"
|
||||||
|
:content="'今日约战次数已经用完\n开通会员可增加次数'"
|
||||||
|
cancelText="知道了"
|
||||||
|
confirmText="去开通"
|
||||||
|
:onCancel="closeLimitModal"
|
||||||
|
:onConfirm="goVipPage"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -268,7 +316,7 @@ onLoad(async (options) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.create-room>view:nth-child(3) {
|
.create-room>view:nth-child(3) {
|
||||||
margin: 12vw auto;
|
margin: 12vw auto 5vw auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -304,6 +352,13 @@ onLoad(async (options) => {
|
|||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pp-text{
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 22rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.warnning {
|
.warnning {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,31 +1,66 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { user } = storeToRefs(store);
|
const { user } = storeToRefs(store);
|
||||||
|
|
||||||
|
const MAX_LEVEL = 100;
|
||||||
|
const LEVEL_NODE_WIDTH_RPX = 72;
|
||||||
|
const TRACK_PADDING_RPX = 20;
|
||||||
|
const windowWidth = uni.getSystemInfoSync().windowWidth;
|
||||||
|
const rpxToPx = (value) => (value * windowWidth) / 750;
|
||||||
|
const levels = Array.from({ length: MAX_LEVEL }, (_, index) => index + 1);
|
||||||
|
const currentLevel = computed(() => {
|
||||||
|
const level = Number(user.value?.lvl) || 1;
|
||||||
|
return Math.min(Math.max(level, 1), MAX_LEVEL);
|
||||||
|
});
|
||||||
|
const currentLevelScrollLeft = computed(() => {
|
||||||
|
const nodeWidth = rpxToPx(LEVEL_NODE_WIDTH_RPX);
|
||||||
|
const trackPadding = rpxToPx(TRACK_PADDING_RPX);
|
||||||
|
const contentWidth = rpxToPx(
|
||||||
|
TRACK_PADDING_RPX * 2 + MAX_LEVEL * LEVEL_NODE_WIDTH_RPX
|
||||||
|
);
|
||||||
|
const currentCenter =
|
||||||
|
trackPadding + (currentLevel.value - 1) * nodeWidth + nodeWidth / 2;
|
||||||
|
const targetLeft = currentCenter - windowWidth / 2;
|
||||||
|
const maxScrollLeft = Math.max(contentWidth - windowWidth, 0);
|
||||||
|
|
||||||
|
return Math.min(Math.max(targetLeft, 0), maxScrollLeft);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container title="等级介绍">
|
<Container title="等级介绍">
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<!-- 等级进度条 -->
|
<!-- 等级进度条 -->
|
||||||
<view class="level-progress">
|
<scroll-view
|
||||||
<view v-for="(_, index) in 10" :key="index" class="progress-dot">
|
class="level-progress"
|
||||||
|
scroll-x
|
||||||
|
:show-scrollbar="false"
|
||||||
|
:scroll-left="currentLevelScrollLeft"
|
||||||
|
scroll-with-animation
|
||||||
|
>
|
||||||
|
<view class="level-track">
|
||||||
<view
|
<view
|
||||||
:style="{
|
v-for="level in levels"
|
||||||
backgroundColor:
|
:id="`level-${level}`"
|
||||||
index + 1 < user.lvl
|
:key="level"
|
||||||
? '#fff9'
|
:class="[
|
||||||
: index + 1 === user.lvl
|
'level-node',
|
||||||
? '#fed847'
|
level < currentLevel ? 'level-node--done' : '',
|
||||||
: 'transparent',
|
level === currentLevel ? 'level-node--current' : '',
|
||||||
borderColor: index + 1 === user.lvl ? '#fed847' : '#fff9',
|
]"
|
||||||
}"
|
>
|
||||||
/>
|
<view class="level-node__top">
|
||||||
<view />
|
<view class="level-node__dot" />
|
||||||
|
<view v-if="level < MAX_LEVEL" class="level-node__line" />
|
||||||
|
</view>
|
||||||
|
<text class="level-node__label">{{ level }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
<!-- 说明文本 -->
|
<!-- 说明文本 -->
|
||||||
<view class="body">
|
<view class="body">
|
||||||
@@ -65,28 +100,80 @@ const { user } = storeToRefs(store);
|
|||||||
|
|
||||||
.level-progress {
|
.level-progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 32rpx;
|
white-space: nowrap;
|
||||||
display: flex;
|
padding: 24rpx 0 34rpx;
|
||||||
justify-content: center;
|
box-sizing: border-box;
|
||||||
padding-top: 20rpx;
|
|
||||||
padding-bottom: 40rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.progress-dot > view:first-child {
|
|
||||||
width: 3.8vw;
|
.level-node__dot {
|
||||||
height: 3.8vw;
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
border-radius: 50%;
|
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;
|
.level-node__line {
|
||||||
height: 1px;
|
flex: 1;
|
||||||
margin: 0 2px;
|
height: 2rpx;
|
||||||
background-color: #fff9;
|
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 {
|
.body {
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref} from "vue";
|
import {onMounted, onUnmounted, ref} from "vue";
|
||||||
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
|
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import AppFooter from "@/components/AppFooter.vue";
|
import AppFooter from "@/components/AppFooter.vue";
|
||||||
import UserHeader from "@/components/UserHeader.vue";
|
import UserHeader from "@/components/UserHeader.vue";
|
||||||
import Signin from "@/components/Signin.vue";
|
import Signin from "@/components/Signin.vue";
|
||||||
import BubbleTip from "@/components/BubbleTip.vue";
|
import BubbleTip from "@/components/BubbleTip.vue";
|
||||||
|
import OtaModal from "@/components/OtaModal.vue";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
checkUserBindAPI,
|
checkUserBindAPI,
|
||||||
getAppConfig,
|
getAppConfig,
|
||||||
getDeviceBatteryAPI,
|
getDeviceBatteryAPI,
|
||||||
|
getHardwareBoxTaskStatusAPI,
|
||||||
|
getHardwareBoxVersionAPI,
|
||||||
getHomeData,
|
getHomeData,
|
||||||
getMyDevicesAPI,
|
getMyDevicesAPI,
|
||||||
getScoreRankList,
|
getScoreRankList,
|
||||||
|
sendHardwareBoxUpdateAPI,
|
||||||
silentLoginAPI,
|
silentLoginAPI,
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
import {topThreeColors} from "@/constants";
|
import {topThreeColors} from "@/constants";
|
||||||
@@ -37,6 +41,208 @@ const showModal = ref(false);
|
|||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const scoreRankList = ref([]);
|
const scoreRankList = ref([]);
|
||||||
|
|
||||||
|
// OTA 相关
|
||||||
|
const otaVisible = ref(false);
|
||||||
|
const otaState = ref("new_version");
|
||||||
|
const otaProgress = ref(0);
|
||||||
|
const otaInfo = ref({
|
||||||
|
versionNumber: "",
|
||||||
|
versionInfo: "",
|
||||||
|
resourceUrl: "",
|
||||||
|
forceUpdate: false,
|
||||||
|
});
|
||||||
|
const isStartingOta = ref(false);
|
||||||
|
let otaProgressTimer = null;
|
||||||
|
let otaStatusTimer = null;
|
||||||
|
let otaTimeoutTimer = null;
|
||||||
|
|
||||||
|
// 清理首页 OTA 更新定时器,避免弹窗关闭或页面卸载后继续轮询。
|
||||||
|
const clearOtaUpdateTimers = () => {
|
||||||
|
clearInterval(otaProgressTimer);
|
||||||
|
clearTimeout(otaStatusTimer);
|
||||||
|
clearTimeout(otaTimeoutTimer);
|
||||||
|
otaProgressTimer = null;
|
||||||
|
otaStatusTimer = null;
|
||||||
|
otaTimeoutTimer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 启动首页 OTA 本地进度动画,最终成功失败以后端任务状态为准。
|
||||||
|
const startOtaProgressAnimation = () => {
|
||||||
|
clearInterval(otaProgressTimer);
|
||||||
|
otaProgressTimer = setInterval(() => {
|
||||||
|
if (otaProgress.value >= 90) {
|
||||||
|
clearInterval(otaProgressTimer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const increment = Math.max(0.5, 2 - otaProgress.value / 60);
|
||||||
|
otaProgress.value = Math.min(90, otaProgress.value + increment);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取并保存后端返回的 OTA 版本信息,供弹窗展示和更新接口使用。
|
||||||
|
const applyOtaVersionInfo = (versionInfo) => {
|
||||||
|
otaInfo.value = {
|
||||||
|
versionNumber: versionInfo?.versionNumber || "",
|
||||||
|
versionInfo: versionInfo?.versionInfo || "",
|
||||||
|
resourceUrl: versionInfo?.resourceUrl || "",
|
||||||
|
forceUpdate: Number(versionInfo?.forceUpdate) === 1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查当前设备盒子是否存在可升级版本。
|
||||||
|
const checkOtaUpdate = async () => {
|
||||||
|
let versionInfo;
|
||||||
|
try {
|
||||||
|
versionInfo = await getHardwareBoxVersionAPI();
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!versionInfo?.needUpdate) return;
|
||||||
|
applyOtaVersionInfo(versionInfo);
|
||||||
|
|
||||||
|
const dismissedAt = uni.getStorageSync("ota_dismissed_at");
|
||||||
|
const now = Date.now();
|
||||||
|
if (!otaInfo.value.forceUpdate && dismissedAt && now - dismissedAt < 24 * 60 * 60 * 1000) return;
|
||||||
|
otaState.value = "new_version";
|
||||||
|
otaVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拼接 OTA WiFi 页参数,让未连 WiFi 的设备继续使用同一份版本信息。
|
||||||
|
const getOtaWifiUrl = () => {
|
||||||
|
const { versionNumber, resourceUrl } = otaInfo.value;
|
||||||
|
const query = [
|
||||||
|
`versionNumber=${encodeURIComponent(versionNumber)}`,
|
||||||
|
`resourceUrl=${encodeURIComponent(resourceUrl)}`,
|
||||||
|
].join("&");
|
||||||
|
return `/pages/ota-wifi?${query}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 OTA 弹窗暂不更新,强制更新时不允许关闭。
|
||||||
|
const handleOtaDismiss = () => {
|
||||||
|
if (otaInfo.value.forceUpdate) return;
|
||||||
|
uni.setStorageSync("ota_dismissed_at", Date.now());
|
||||||
|
otaVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将首页 OTA 直连更新流程标记为失败。
|
||||||
|
const failHomeOtaUpdate = () => {
|
||||||
|
clearOtaUpdateTimers();
|
||||||
|
isStartingOta.value = false;
|
||||||
|
otaState.value = "update_failure";
|
||||||
|
otaVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将首页 OTA 直连更新流程标记为成功。
|
||||||
|
const completeHomeOtaUpdate = () => {
|
||||||
|
clearOtaUpdateTimers();
|
||||||
|
isStartingOta.value = false;
|
||||||
|
otaProgress.value = 100;
|
||||||
|
setTimeout(() => {
|
||||||
|
otaState.value = "update_success";
|
||||||
|
otaVisible.value = true;
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 轮询首页直接发起的 OTA 更新任务状态。
|
||||||
|
const pollHomeOtaTaskStatus = (taskId) => {
|
||||||
|
clearTimeout(otaStatusTimer);
|
||||||
|
otaStatusTimer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const taskStatus = await getHardwareBoxTaskStatusAPI(taskId);
|
||||||
|
const status = Number(taskStatus?.status);
|
||||||
|
if (status === 2) {
|
||||||
|
completeHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status === 3) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status === 0 || status === 1) {
|
||||||
|
pollHomeOtaTaskStatus(taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
} catch (err) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设备盒子已连 WiFi 时,从首页直接传空 WiFi 信息发起 OTA 更新。
|
||||||
|
const startHomeOtaUpdate = async () => {
|
||||||
|
otaState.value = "update_progress";
|
||||||
|
otaVisible.value = true;
|
||||||
|
otaProgress.value = 0;
|
||||||
|
startOtaProgressAnimation();
|
||||||
|
otaTimeoutTimer = setTimeout(() => {
|
||||||
|
if (otaState.value === "update_progress") {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
}
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updateResult = await sendHardwareBoxUpdateAPI({
|
||||||
|
versionNumber: otaInfo.value.versionNumber,
|
||||||
|
wifiSsid: "",
|
||||||
|
wifiPassword: "",
|
||||||
|
resourceUrl: otaInfo.value.resourceUrl,
|
||||||
|
});
|
||||||
|
if (!updateResult?.taskId) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pollHomeOtaTaskStatus(updateResult.taskId);
|
||||||
|
} catch (err) {
|
||||||
|
failHomeOtaUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击立即更新时先判断设备是否在线并已通过 WiFi 联网,已联网则首页直接更新,否则跳转 WiFi 页面。
|
||||||
|
const handleOtaUpdate = async () => {
|
||||||
|
if (isStartingOta.value) return;
|
||||||
|
isStartingOta.value = true;
|
||||||
|
let deviceStatus;
|
||||||
|
try {
|
||||||
|
deviceStatus = await getDeviceBatteryAPI();
|
||||||
|
} catch (err) {
|
||||||
|
isStartingOta.value = false;
|
||||||
|
uni.showToast({
|
||||||
|
title: "获取设备状态失败,请重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceStatus?.online !== true) {
|
||||||
|
isStartingOta.value = false;
|
||||||
|
uni.showToast({
|
||||||
|
title: "请先开启智能弓",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(deviceStatus?.netType || "").toLowerCase() === "wifi") {
|
||||||
|
startHomeOtaUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isStartingOta.value = false;
|
||||||
|
otaVisible.value = false;
|
||||||
|
uni.navigateTo({ url: getOtaWifiUrl() });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 OTA 更新成功后的完成按钮,关闭结果弹窗。
|
||||||
|
const handleOtaDone = () => {
|
||||||
|
otaVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 OTA 更新失败后的重试按钮,重新走立即更新判断流程。
|
||||||
|
const handleOtaRetry = () => {
|
||||||
|
handleOtaUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
// 提取积分榜接口返回的榜单数组,兼容数组和对象两种返回格式。
|
// 提取积分榜接口返回的榜单数组,兼容数组和对象两种返回格式。
|
||||||
const getScoreRankData = (result) => {
|
const getScoreRankData = (result) => {
|
||||||
if (Array.isArray(result)) return result;
|
if (Array.isArray(result)) return result;
|
||||||
@@ -64,10 +270,18 @@ const toRankListPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onShow(async () => {
|
onShow(async (options) => {
|
||||||
const env = uni.getAccountInfoSync().miniProgram.envVersion;
|
const env = uni.getAccountInfoSync().miniProgram.envVersion;
|
||||||
const token = uni.getStorageSync(`${env}_token`);
|
const token = uni.getStorageSync(`${env}_token`);
|
||||||
|
|
||||||
|
// 检查是否从 OTA 更新页面返回
|
||||||
|
if (options && options.updateResult) {
|
||||||
|
otaState.value = options.updateResult;
|
||||||
|
otaVisible.value = true;
|
||||||
|
} else if (token || user.value.id) {
|
||||||
|
await checkOtaUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.value.id && !token) {
|
if (!user.value.id && !token) {
|
||||||
// showModal.value = true;
|
// showModal.value = true;
|
||||||
// try {
|
// try {
|
||||||
@@ -141,6 +355,10 @@ onMounted(async () => {
|
|||||||
console.log("全局配置:", config);
|
console.log("全局配置:", config);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearOtaUpdateTimers();
|
||||||
|
});
|
||||||
|
|
||||||
onShareAppMessage(() => {
|
onShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
|
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
|
||||||
@@ -161,6 +379,21 @@ onShareTimeline(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container :isHome="true" :showBackToGame="true">
|
<Container :isHome="true" :showBackToGame="true">
|
||||||
|
<!-- OTA 升级弹窗:使用 visible 控制显隐,description 为副标题,changelog 为详细说明 -->
|
||||||
|
<OtaModal
|
||||||
|
:visible="otaVisible"
|
||||||
|
:state="otaState"
|
||||||
|
:version="otaInfo.versionNumber"
|
||||||
|
:progress="otaProgress"
|
||||||
|
:description="''"
|
||||||
|
:changelog="otaInfo.versionInfo"
|
||||||
|
:forceUpdate="otaInfo.forceUpdate"
|
||||||
|
@update="handleOtaUpdate"
|
||||||
|
@skip="handleOtaDismiss"
|
||||||
|
@close="handleOtaDismiss"
|
||||||
|
@done="handleOtaDone"
|
||||||
|
@retry="handleOtaRetry"
|
||||||
|
/>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<view class="top-theme">
|
<view class="top-theme">
|
||||||
<!-- <image
|
<!-- <image
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import { ref, onMounted, onBeforeUnmount } from "vue";
|
|||||||
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import Matching from "@/components/Matching.vue";
|
import Matching from "@/components/Matching.vue";
|
||||||
|
import ModalDialog from "@/components/ModalDialog.vue";
|
||||||
import { matchGameAPI, getBattleAPI } from "@/apis";
|
import { matchGameAPI, getBattleAPI } from "@/apis";
|
||||||
import { MESSAGETYPESV2 } from "@/constants";
|
import { MESSAGETYPESV2 } from "@/constants";
|
||||||
|
import { isLimitError } from "@/util";
|
||||||
|
|
||||||
const gameType = ref(0);
|
const gameType = ref(0);
|
||||||
const teamSize = ref(0);
|
const teamSize = ref(0);
|
||||||
const onComplete = ref(null);
|
const onComplete = ref(null);
|
||||||
|
const showLimitModal = ref(false);
|
||||||
|
|
||||||
/** 匹配超时计时器,用于检测 WS 消息丢失或真正超时 */
|
/** 匹配超时计时器,用于检测 WS 消息丢失或真正超时 */
|
||||||
const matchTimeoutTimer = ref(null);
|
const matchTimeoutTimer = ref(null);
|
||||||
@@ -58,6 +61,18 @@ async function stopMatch() {
|
|||||||
uni.$showHint(3);
|
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 () => {
|
onShow(async () => {
|
||||||
if (gameType.value && teamSize.value) {
|
if (gameType.value && teamSize.value) {
|
||||||
matchGameAPI(true, gameType.value, teamSize.value);
|
try {
|
||||||
|
await matchGameAPI(true, gameType.value, teamSize.value);
|
||||||
// 启动超时计时器,防止 WS 消息丢失或长时间无对手导致用户卡死
|
// 启动超时计时器,防止 WS 消息丢失或长时间无对手导致用户卡死
|
||||||
clearMatchTimeout();
|
clearMatchTimeout();
|
||||||
matchTimeoutTimer.value = setTimeout(handleMatchTimeout, MATCH_TIMEOUT_MS);
|
matchTimeoutTimer.value = setTimeout(handleMatchTimeout, MATCH_TIMEOUT_MS);
|
||||||
|
} catch (error) {
|
||||||
|
clearMatchTimeout();
|
||||||
|
if (isLimitError(error)) {
|
||||||
|
showLimitModal.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.navigateBack();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,6 +178,14 @@ onHide(() => {
|
|||||||
<Matching :stopMatch="stopMatch" :onComplete="onComplete" />
|
<Matching :stopMatch="stopMatch" :onComplete="onComplete" />
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
</Container>
|
||||||
|
<ModalDialog
|
||||||
|
:show="showLimitModal"
|
||||||
|
:content="'今日排位赛次数已经用完\n开通会员可增加次数'"
|
||||||
|
cancelText="知道了"
|
||||||
|
confirmText="去开通"
|
||||||
|
:onCancel="closeLimitModal"
|
||||||
|
:onConfirm="goVipPage"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -295,6 +295,7 @@ onShow(async () => {
|
|||||||
:tips="tips"
|
:tips="tips"
|
||||||
:total="90"
|
:total="90"
|
||||||
:melee="true"
|
:melee="true"
|
||||||
|
:halfRest="halfRest"
|
||||||
:battleId="battleId"
|
:battleId="battleId"
|
||||||
/>
|
/>
|
||||||
<view v-if="start" class="user-row">
|
<view v-if="start" class="user-row">
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
|
import Container from "@/components/Container.vue";
|
||||||
|
import { getMemberAgreement } from "./agreementData";
|
||||||
|
|
||||||
|
const agreementType = ref("renew");
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
agreementType.value = options.type || "renew";
|
||||||
|
});
|
||||||
|
|
||||||
|
const agreement = computed(() => getMemberAgreement(agreementType.value));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Container :title="agreement.navTitle">
|
||||||
|
<scroll-view scroll-y class="agreement-page" :show-scrollbar="false">
|
||||||
|
<view class="content">
|
||||||
|
<view class="page-title">{{ agreement.title }}</view>
|
||||||
|
<view v-if="agreement.meta" class="meta">{{ agreement.meta }}</view>
|
||||||
|
|
||||||
|
<block v-for="(item, index) in agreement.content" :key="index">
|
||||||
|
<view v-if="item.type === 'heading'" class="section-title">
|
||||||
|
{{ item.text }}
|
||||||
|
</view>
|
||||||
|
<view v-else-if="item.type === 'list'" class="list">
|
||||||
|
<view v-for="(listItem, listIndex) in item.items" :key="listIndex" class="list-item">
|
||||||
|
<text class="list-dot">•</text>
|
||||||
|
<text class="list-text">{{ listItem }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="paragraph">
|
||||||
|
<text>{{ item.text }}</text>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<view class="company">{{ agreement.company }}</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.agreement-page {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title,
|
||||||
|
.section-title {
|
||||||
|
color: #333333;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 40rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
margin-bottom: 26rpx;
|
||||||
|
color: #666666;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 34rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin: 42rpx 0 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph,
|
||||||
|
.list-item,
|
||||||
|
.company {
|
||||||
|
color: #333333;
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 38rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph {
|
||||||
|
margin-bottom: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin-bottom: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-dot {
|
||||||
|
width: 28rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 38rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-text {
|
||||||
|
flex: 1;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 38rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company {
|
||||||
|
margin-top: 42rpx;
|
||||||
|
padding-bottom: 30rpx;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, onBeforeUnmount } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
import { onShow } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import Signin from "@/components/Signin.vue";
|
import Signin from "@/components/Signin.vue";
|
||||||
import { createOrderAPI, getAppConfig, getHomeData } from "@/apis";
|
import { virtualPayOrderAPI, getAppConfig, getHomeData, getOrderDetailAPI } from "@/apis";
|
||||||
import { capsuleHeight } from "@/util";
|
import { capsuleHeight, wxLogin } from "@/util";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
@@ -12,17 +12,14 @@ const store = useStore();
|
|||||||
const { user, config } = storeToRefs(store);
|
const { user, config } = storeToRefs(store);
|
||||||
const { updateConfig, updateUser } = store;
|
const { updateConfig, updateUser } = store;
|
||||||
|
|
||||||
const currentTypeIndex = ref(0);
|
const currentTypeIndex = ref(1);
|
||||||
const selectedPackageIndex = ref(0);
|
const selectedPackageIndex = ref(0);
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
const loadingConfig = ref(false);
|
const loadingConfig = ref(false);
|
||||||
const paying = ref(false);
|
const paying = ref(false);
|
||||||
const refreshing = ref(false);
|
const refreshing = ref(false);
|
||||||
const timer = ref(null);
|
|
||||||
const lastDate = ref(user.value.expiredAt || 0);
|
|
||||||
const maxRefreshTimes = 12;
|
|
||||||
|
|
||||||
// 会员页核心展示数据:视觉、权益、套餐均按蓝湖当前两张设计稿拆分。
|
// 会员页核心展示数据:视觉、权益按蓝湖当前两张设计稿拆分,套餐完全使用接口数据。
|
||||||
const memberTypes = [
|
const memberTypes = [
|
||||||
{
|
{
|
||||||
key: "normal",
|
key: "normal",
|
||||||
@@ -44,11 +41,6 @@ const memberTypes = [
|
|||||||
{ label: "排位赛\n每日+20次", icon: "../../static/vip/vip-rank.png" },
|
{ label: "排位赛\n每日+20次", icon: "../../static/vip/vip-rank.png" },
|
||||||
{ label: "约战\n每日+20次", icon: "../../static/vip/vip-battle.png" },
|
{ label: "约战\n每日+20次", icon: "../../static/vip/vip-battle.png" },
|
||||||
],
|
],
|
||||||
packages: [
|
|
||||||
{ name: "连续包月", price: "20", original: "35" },
|
|
||||||
{ name: "12个月", price: "300", original: "420" },
|
|
||||||
{ name: "3个月", price: "84", original: "105" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "super",
|
key: "super",
|
||||||
@@ -73,11 +65,6 @@ const memberTypes = [
|
|||||||
{ label: "排位赛无限制", icon: "../../static/vip/svip-rank.png" },
|
{ label: "排位赛无限制", icon: "../../static/vip/svip-rank.png" },
|
||||||
{ label: "专享SVIP客服", icon: "../../static/vip/svip-service.png" },
|
{ label: "专享SVIP客服", icon: "../../static/vip/svip-service.png" },
|
||||||
],
|
],
|
||||||
packages: [
|
|
||||||
{ name: "连续包月", price: "20", original: "35" },
|
|
||||||
{ name: "12个月", price: "300", original: "420" },
|
|
||||||
{ name: "3个月", price: "84", original: "105" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -100,16 +87,6 @@ const getVipExpiredValue = (type, source = user.value) => {
|
|||||||
return type.key === "super" ? source.superVipExpiredAt : source.normalVipExpiredAt;
|
return type.key === "super" ? source.superVipExpiredAt : source.normalVipExpiredAt;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 支付后轮询需要比较最新会员到期时间,兼容旧字段 expiredAt。
|
|
||||||
const getLatestVipExpiredTime = (source = user.value) => {
|
|
||||||
if (!source) return 0;
|
|
||||||
return Math.max(
|
|
||||||
toTimestamp(source.normalVipExpiredAt),
|
|
||||||
toTimestamp(source.superVipExpiredAt),
|
|
||||||
toTimestamp(source.expiredAt)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 未过期才展示“会员生效中”样式,已过期或无值继续展示未开通样式。
|
// 未过期才展示“会员生效中”样式,已过期或无值继续展示未开通样式。
|
||||||
const isVipActive = (type) => {
|
const isVipActive = (type) => {
|
||||||
return toTimestamp(getVipExpiredValue(type)) > Date.now();
|
return toTimestamp(getVipExpiredValue(type)) > Date.now();
|
||||||
@@ -153,9 +130,34 @@ const getMenuName = (item) => {
|
|||||||
return item.name || item.vipName || item.title || "";
|
return item.name || item.vipName || item.title || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatPrice = (value) => {
|
||||||
|
if (value === undefined || value === null || value === "") return "";
|
||||||
|
return String(value).replace("¥", "").replace("¥", "");
|
||||||
|
};
|
||||||
|
|
||||||
const getMenuPrice = (item) => {
|
const getMenuPrice = (item) => {
|
||||||
const value = item.price || item.total || item.amount || item.money;
|
return formatPrice(item && item.price);
|
||||||
return value ? String(value).replace("¥", "").replace("¥", "") : "";
|
};
|
||||||
|
|
||||||
|
const getMenuOriginalPrice = (item) => {
|
||||||
|
return formatPrice(item && item.originalPrice);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMenuSortValue = (item) => {
|
||||||
|
const value = item && item.sort;
|
||||||
|
if (value === undefined || value === null || value === "") return Number.MAX_SAFE_INTEGER;
|
||||||
|
const sort = Number(value);
|
||||||
|
return Number.isFinite(sort) ? sort : Number.MAX_SAFE_INTEGER;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortMenusBySort = (menus = []) => {
|
||||||
|
return menus
|
||||||
|
.map((item, index) => ({ item, index }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const sortDiff = getMenuSortValue(a.item) - getMenuSortValue(b.item);
|
||||||
|
return sortDiff || a.index - b.index;
|
||||||
|
})
|
||||||
|
.map(({ item }) => item);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMenuType = (item) => {
|
const getMenuType = (item) => {
|
||||||
@@ -173,39 +175,17 @@ const getMenuType = (item) => {
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPackageMonths = (name) => {
|
|
||||||
if (/连续包月/.test(name)) return 1;
|
|
||||||
const match = String(name || "").match(/(\d+)\s*个?月/);
|
|
||||||
return match ? Number(match[1]) : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const matchPackageSource = (type, pack, index) => {
|
|
||||||
const menus = configMenus.value;
|
|
||||||
const typedMenus = menus.filter((item) => {
|
|
||||||
const menuType = getMenuType(item);
|
|
||||||
if (type.key === "super") return menuType === "super";
|
|
||||||
return menuType === "normal";
|
|
||||||
});
|
|
||||||
const pool = typedMenus.length ? typedMenus : menus;
|
|
||||||
const packMonths = getPackageMonths(pack.name);
|
|
||||||
return (
|
|
||||||
pool.find((item) => getMenuName(item).indexOf(pack.name) !== -1) ||
|
|
||||||
pool.find((item) => packMonths && getPackageMonths(getMenuName(item)) === packMonths) ||
|
|
||||||
pool.find((item) => getMenuPrice(item) === pack.price) ||
|
|
||||||
pool[index]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPackages = (type) => {
|
const getPackages = (type) => {
|
||||||
return type.packages.map((item, index) => {
|
return sortMenusBySort(configMenus.value)
|
||||||
const source = matchPackageSource(type, item, index);
|
.filter((item) => getMenuType(item) === type.key)
|
||||||
|
.map((item) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
source: item,
|
||||||
source,
|
id: item.id,
|
||||||
name: source ? getMenuName(source) || item.name : item.name,
|
name: getMenuName(item),
|
||||||
price: source ? getMenuPrice(source) || item.price : item.price,
|
price: getMenuPrice(item),
|
||||||
icon: source && source.icon,
|
originalPrice: getMenuOriginalPrice(item),
|
||||||
id: source && source.id,
|
icon: item.icon,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -244,6 +224,12 @@ const toOrderPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toAgreement = (type) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/member/agreement?type=${type}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const loadVipConfig = async () => {
|
const loadVipConfig = async () => {
|
||||||
if (loadingConfig.value || configMenus.value.length) return;
|
if (loadingConfig.value || configMenus.value.length) return;
|
||||||
loadingConfig.value = true;
|
loadingConfig.value = true;
|
||||||
@@ -257,41 +243,38 @@ const loadVipConfig = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearRefreshTimer = () => {
|
const refreshUserAfterPay = async () => {
|
||||||
if (timer.value) {
|
|
||||||
clearInterval(timer.value);
|
|
||||||
timer.value = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshUserAfterPay = () => {
|
|
||||||
clearRefreshTimer();
|
|
||||||
let refreshTimes = 0;
|
|
||||||
// 先记录支付前的最大到期时间,轮询到更大的值就认为会员状态已刷新。
|
|
||||||
lastDate.value = getLatestVipExpiredTime();
|
|
||||||
refreshing.value = true;
|
refreshing.value = true;
|
||||||
timer.value = setInterval(async () => {
|
|
||||||
refreshTimes += 1;
|
|
||||||
try {
|
try {
|
||||||
const result = await getHomeData();
|
const result = await getHomeData();
|
||||||
const latestExpiredAt = getLatestVipExpiredTime(result.user);
|
if (result.user) {
|
||||||
if (result.user && latestExpiredAt > lastDate.value) {
|
|
||||||
lastDate.value = latestExpiredAt;
|
|
||||||
updateUser(result.user);
|
updateUser(result.user);
|
||||||
clearRefreshTimer();
|
|
||||||
refreshing.value = false;
|
|
||||||
} else if (refreshTimes >= maxRefreshTimes) {
|
|
||||||
clearRefreshTimer();
|
|
||||||
refreshing.value = false;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("refresh user after pay error", error);
|
console.log("refresh user after pay error", error);
|
||||||
if (refreshTimes >= maxRefreshTimes) {
|
} finally {
|
||||||
clearRefreshTimer();
|
|
||||||
refreshing.value = false;
|
refreshing.value = false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVirtualPaySignData = (result) => {
|
||||||
|
// 后端可能返回字符串或对象,这里统一转成微信虚拟支付需要的 JSON 字符串。
|
||||||
|
const signDataObj = typeof result?.signData === "string" ? JSON.parse(result.signData) : result?.signData;
|
||||||
|
if (!signDataObj) return "";
|
||||||
|
|
||||||
|
// 微信虚拟支付短剧/虚拟商品模式需要购买数量;后端未返回时使用订单数量兜底。
|
||||||
|
if (!signDataObj.buyQuantity) {
|
||||||
|
signDataObj.buyQuantity = result.quantity || 1;
|
||||||
}
|
}
|
||||||
}, 1000);
|
return JSON.stringify(signDataObj);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isVirtualPayCancel = (res = {}) => {
|
||||||
|
const message = String(res.errMsg || res.message || "").toLowerCase();
|
||||||
|
const errCode = Number(res.errCode);
|
||||||
|
const errno = Number(res.errno);
|
||||||
|
|
||||||
|
return message.includes("cancel") || errCode === -23 || errno === -2;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPay = async () => {
|
const onPay = async () => {
|
||||||
@@ -311,12 +294,22 @@ const onPay = async () => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (typeof wx === "undefined" || !wx.requestVirtualPayment) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "当前环境不支持微信虚拟支付",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
paying.value = true;
|
paying.value = true;
|
||||||
let waitingPayment = false;
|
let waitingPayment = false;
|
||||||
|
let payToast = null;
|
||||||
try {
|
try {
|
||||||
const result = await createOrderAPI(vipId);
|
// 微信虚拟支付创建订单前需要登录 code,服务端用它换取本次支付签名参数。
|
||||||
const params = result?.pay?.order?.jsApi?.params;
|
const wxResult = await wxLogin();
|
||||||
if (!params?.timeStamp || !params?.nonceStr || !params?.package || !params?.paySign) {
|
const result = await virtualPayOrderAPI(vipId, wxResult.code);
|
||||||
|
const finalSignData = getVirtualPaySignData(result);
|
||||||
|
if (!finalSignData || !result?.paySig || !result?.signature) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: "支付参数生成失败",
|
title: "支付参数生成失败",
|
||||||
icon: "none",
|
icon: "none",
|
||||||
@@ -324,34 +317,57 @@ const onPay = async () => {
|
|||||||
refreshing.value = false;
|
refreshing.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
waitingPayment = true;
|
wx.requestVirtualPayment({
|
||||||
wx.requestPayment({
|
signData: finalSignData,
|
||||||
timeStamp: params.timeStamp, // 微信支付时间戳
|
paySig: result.paySig,
|
||||||
nonceStr: params.nonceStr, // 微信支付随机串
|
signature: result.signature,
|
||||||
package: params.package, // 预支付交易会话标识
|
mode: "short_series_goods",
|
||||||
paySign: params.paySign, // 微信支付签名
|
async success() {
|
||||||
signType: params.signType || "RSA",
|
payToast = {
|
||||||
success() {
|
|
||||||
uni.showToast({
|
|
||||||
title: "支付成功",
|
title: "支付成功",
|
||||||
icon: "none",
|
icon: "success",
|
||||||
});
|
};
|
||||||
|
if (result?.outTradeNo) {
|
||||||
|
try {
|
||||||
|
const orderDetail = await getOrderDetailAPI(result.outTradeNo);
|
||||||
|
console.log("virtual pay order detail", orderDetail);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("virtual pay order detail error", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 客户端支付成功后,会员是否真正生效仍以服务端用户信息刷新结果为准。
|
||||||
refreshUserAfterPay();
|
refreshUserAfterPay();
|
||||||
},
|
},
|
||||||
fail(res) {
|
fail(res) {
|
||||||
console.log("pay error", res);
|
console.log("virtual pay error", res);
|
||||||
if (res.errMsg && res.errMsg.indexOf("cancel") !== -1) return;
|
if (isVirtualPayCancel(res)) {
|
||||||
uni.showToast({
|
payToast = {
|
||||||
title: "支付失败,请稍后重试",
|
title: "支付已取消",
|
||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payToast = {
|
||||||
|
title: res.message || "支付失败,请稍后重试",
|
||||||
|
icon: "none",
|
||||||
|
};
|
||||||
},
|
},
|
||||||
complete() {
|
complete() {
|
||||||
paying.value = false;
|
paying.value = false;
|
||||||
|
if (payToast) {
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.showToast(payToast);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
waitingPayment = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("create vip order error", error);
|
console.log("create virtual pay order error", error);
|
||||||
|
uni.showToast({
|
||||||
|
title: error.message || "下单失败",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (!waitingPayment) paying.value = false;
|
if (!waitingPayment) paying.value = false;
|
||||||
}
|
}
|
||||||
@@ -359,9 +375,6 @@ const onPay = async () => {
|
|||||||
|
|
||||||
onShow(loadVipConfig);
|
onShow(loadVipConfig);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
clearRefreshTimer();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -443,7 +456,7 @@ onBeforeUnmount(() => {
|
|||||||
<view class="package-list">
|
<view class="package-list">
|
||||||
<view
|
<view
|
||||||
v-for="(pack, index) in getPackages(type)"
|
v-for="(pack, index) in getPackages(type)"
|
||||||
:key="`${type.key}-${pack.name}`"
|
:key="`${type.key}-${pack.id || pack.name || index}`"
|
||||||
class="package-card"
|
class="package-card"
|
||||||
:class="{ 'package-card--active': selectedPackageIndex === index }"
|
:class="{ 'package-card--active': selectedPackageIndex === index }"
|
||||||
@click="selectPackage(index)"
|
@click="selectPackage(index)"
|
||||||
@@ -454,8 +467,8 @@ onBeforeUnmount(() => {
|
|||||||
<text class="package-price__symbol">¥</text>
|
<text class="package-price__symbol">¥</text>
|
||||||
<text class="package-price__value">{{ pack.price }}</text>
|
<text class="package-price__value">{{ pack.price }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="package-origin">
|
<view v-if="pack.originalPrice" class="package-origin">
|
||||||
<text>¥{{ pack.original }}</text>
|
<text>¥{{ pack.originalPrice }}</text>
|
||||||
<view class="package-origin__line" />
|
<view class="package-origin__line" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -467,19 +480,20 @@ onBeforeUnmount(() => {
|
|||||||
hover-class="none"
|
hover-class="none"
|
||||||
class="activate-btn"
|
class="activate-btn"
|
||||||
:class="type.buttonClass"
|
:class="type.buttonClass"
|
||||||
:disabled="loadingConfig || paying || refreshing"
|
:disabled="loadingConfig || paying || refreshing || !selectedPackage"
|
||||||
@click="onPay"
|
@click="onPay"
|
||||||
>
|
>
|
||||||
<text v-if="loadingConfig">加载套餐中</text>
|
<text v-if="loadingConfig">加载套餐中</text>
|
||||||
<text v-else-if="paying">创建订单中</text>
|
<text v-else-if="paying">创建订单中</text>
|
||||||
<text v-else-if="!refreshing">¥ {{ selectedPackage.price }} 一键激活</text>
|
<text v-else-if="refreshing">刷新会员状态中</text>
|
||||||
<text v-else>刷新会员状态中</text>
|
<text v-else-if="selectedPackage">¥ {{ selectedPackage.price }} 一键激活</text>
|
||||||
|
<text v-else>套餐暂不可购买</text>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<view class="agreement">
|
<view class="agreement">
|
||||||
<text>支付即同意</text>
|
<text>支付即同意</text>
|
||||||
《 <text class="agreement__link">会员自动续费服务协议</text>》
|
《 <text class="agreement__link" @click.stop="toAgreement('renew')">会员自动续费服务协议</text>》
|
||||||
《 <text class="agreement__link">扣款授权服务协议</text>》
|
《 <text class="agreement__link" @click.stop="toAgreement('deduct')">扣款授权服务协议</text>》
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@@ -740,8 +754,8 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.package-list {
|
.package-list {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
width: 984rpx;
|
min-width: 100%;
|
||||||
padding: 6rpx 84rpx 6rpx 6rpx;
|
padding: 6rpx 84rpx 6rpx 6rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -749,6 +763,7 @@ onBeforeUnmount(() => {
|
|||||||
.package-card {
|
.package-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 264rpx;
|
width: 264rpx;
|
||||||
|
flex: 0 0 264rpx;
|
||||||
height: 224rpx;
|
height: 224rpx;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
border: 2rpx solid #999999;
|
border: 2rpx solid #999999;
|
||||||
|
|||||||
@@ -97,12 +97,10 @@ const cancelOrder = async () => {
|
|||||||
>复制</text
|
>复制</text
|
||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
<text>下单时间:{{ data.vipCreateAt }}</text>
|
<text>创建时间:{{ data.orderCreateAt }}</text>
|
||||||
<text
|
<text v-if="data.orderStatus === 4">支付时间:{{
|
||||||
>支付时间:{{
|
|
||||||
data.orderStatus === 4 ? data.paymentTime : ""
|
data.orderStatus === 4 ? data.paymentTime : ""
|
||||||
}}</text
|
}}</text>
|
||||||
>
|
|
||||||
<text>金额:{{ data.total }} 元</text>
|
<text>金额:{{ data.total }} 元</text>
|
||||||
<text>支付方式:微信</text>
|
<text>支付方式:微信</text>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ import Container from "@/components/Container.vue";
|
|||||||
<text class="table-cell">专享</text>
|
<text class="table-cell">专享</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="table-row">
|
<view class="table-row">
|
||||||
<text class="table-cell table-cell--feature">会员标识</text>
|
<text class="table-cell table-cell--feature">昵称美化</text>
|
||||||
<text class="table-cell">无</text>
|
<text class="table-cell">无</text>
|
||||||
<text class="table-cell">专享</text>
|
<text class="table-cell">专享</text>
|
||||||
<text class="table-cell">专享</text>
|
<text class="table-cell">专享</text>
|
||||||
|
|||||||
@@ -103,6 +103,11 @@ const unbindDevice = async () => {
|
|||||||
clearDevice();
|
clearDevice();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 连接wifi跳转到wifi列表页面 */
|
||||||
|
const joinWifi = () => {
|
||||||
|
uni.navigateTo({ url: "/pages/ota-wifi" });
|
||||||
|
};
|
||||||
|
|
||||||
const toDeviceIntroPage = () => {
|
const toDeviceIntroPage = () => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/device-intro",
|
url: "/pages/device-intro",
|
||||||
@@ -322,6 +327,11 @@ onShow(async () => {
|
|||||||
>解绑</SButton
|
>解绑</SButton
|
||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
|
<view :style="{ marginTop: '20rpx' }">
|
||||||
|
<SButton :onClick="() => $clickSound(joinWifi)" width="80vw" :rounded="40"
|
||||||
|
>设备连接WIFI</SButton
|
||||||
|
>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -81,9 +81,17 @@ const loading = ref(false);
|
|||||||
const shareImage = async () => {
|
const shareImage = async () => {
|
||||||
if (loading.value) return;
|
if (loading.value) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
try {
|
||||||
await generateShareImage("shareImageCanvas", record.value);
|
await generateShareImage("shareImageCanvas", record.value);
|
||||||
await wxShare("shareImageCanvas");
|
await wxShare("shareImageCanvas");
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "海报生成失败,请稍后重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onLoad(async (options) => {
|
onLoad(async (options) => {
|
||||||
|
|||||||
@@ -15,11 +15,22 @@ const list = ref([]);
|
|||||||
const mine = ref({
|
const mine = ref({
|
||||||
averageRing: 0,
|
averageRing: 0,
|
||||||
});
|
});
|
||||||
|
const sharing = ref(false);
|
||||||
|
|
||||||
const shareImage = async () => {
|
const shareImage = async () => {
|
||||||
if (!mine.value.id) return;
|
if (!mine.value.id || sharing.value) return;
|
||||||
|
sharing.value = true;
|
||||||
|
try {
|
||||||
await sharePointData("shareCanvas", mine.value);
|
await sharePointData("shareCanvas", mine.value);
|
||||||
await wxShare("shareCanvas");
|
await wxShare("shareCanvas");
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "海报生成失败,请稍后重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
sharing.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const practiseId = ref("");
|
|||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const tips = ref("");
|
const tips = ref("");
|
||||||
const targetType = ref(1);
|
const targetType = ref(1);
|
||||||
|
const sharing = ref(false);
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
if (options.target) {
|
if (options.target) {
|
||||||
@@ -116,8 +117,19 @@ async function onComplete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onClickShare = debounce(async () => {
|
const onClickShare = debounce(async () => {
|
||||||
|
if (sharing.value) return;
|
||||||
|
sharing.value = true;
|
||||||
|
try {
|
||||||
await sharePractiseData("shareCanvas", 2, user.value, practiseResult.value);
|
await sharePractiseData("shareCanvas", 2, user.value, practiseResult.value);
|
||||||
await wxShare("shareCanvas");
|
await wxShare("shareCanvas");
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "海报生成失败,请稍后重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
sharing.value = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function onAudioEnded(s) {
|
function onAudioEnded(s) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const practiseResult = ref({});
|
|||||||
const practiseId = ref("");
|
const practiseId = ref("");
|
||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const targetType = ref(1);
|
const targetType = ref(1);
|
||||||
|
const sharing = ref(false);
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
if (options.target) {
|
if (options.target) {
|
||||||
@@ -131,8 +132,19 @@ async function onComplete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onClickShare = debounce(async () => {
|
const onClickShare = debounce(async () => {
|
||||||
|
if (sharing.value) return;
|
||||||
|
sharing.value = true;
|
||||||
|
try {
|
||||||
await sharePractiseData("shareCanvas", 3, user.value, practiseResult.value);
|
await sharePractiseData("shareCanvas", 3, user.value, practiseResult.value);
|
||||||
await wxShare("shareCanvas");
|
await wxShare("shareCanvas");
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "海报生成失败,请稍后重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
sharing.value = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Container from "@/components/Container.vue";
|
|||||||
<view class="section">
|
<view class="section">
|
||||||
<view class="title">一、段位体系概述</view>
|
<view class="title">一、段位体系概述</view>
|
||||||
<view class="text">
|
<view class="text">
|
||||||
我们的段位体系分为多个等级,从低到高依次为:倔强青铜、秩序白银、黄金王者、永恒钻石、最强王者、非凡王者、无双王者、绝世王者、至圣王者、荣耀王者和传奇王者。每个大段位下又分为若干小段位,玩家需要通过积累积分来提升段位。
|
我们的段位体系分为多个等级,从低到高依次为:倔强青铜、秩序白银、荣耀黄金、永恒钻石、最强王者、非凡王者、无双王者、绝世王者、至圣王者、荣耀王者和传奇王者。每个大段位下又分为若干小段位,玩家需要通过积累积分来提升段位。
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ import Container from "@/components/Container.vue";
|
|||||||
<text>每个小段位需要满 3颗星才能晋升到下一个段位,共9颗星。</text>
|
<text>每个小段位需要满 3颗星才能晋升到下一个段位,共9颗星。</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="table-row">
|
<view class="table-row">
|
||||||
<text>黄金王者</text>
|
<text>荣耀黄金</text>
|
||||||
<view>
|
<view>
|
||||||
<text>黄金1*</text>
|
<text>黄金1*</text>
|
||||||
<text>黄金2*</text>
|
<text>黄金2*</text>
|
||||||
|
|||||||
@@ -3,21 +3,23 @@ import { computed, ref } from "vue";
|
|||||||
import { onShow } from "@dcloudio/uni-app";
|
import { onShow } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import Avatar from "@/components/Avatar.vue";
|
||||||
|
import ModalDialog from "@/components/ModalDialog.vue";
|
||||||
import { topThreeColors } from "@/constants";
|
import { topThreeColors } from "@/constants";
|
||||||
import {
|
import {
|
||||||
|
getDailyCountAPI,
|
||||||
getSeasonList,
|
getSeasonList,
|
||||||
getSeasonStats,
|
getSeasonStats,
|
||||||
getScoreRankList,
|
getScoreRankList,
|
||||||
getTenRingRankList,
|
getTenRingRankList,
|
||||||
getMvpRankList,
|
getMvpRankList,
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
import { canEenter } from "@/util";
|
import { canEenter, getLimitCountText, isLimitReached } from "@/util";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { user, device, online, game } = storeToRefs(store);
|
const { user, device, online, game, dailyCount } = storeToRefs(store);
|
||||||
const { getLvlName } = store;
|
const { getLvlName, updateDailyCount } = store;
|
||||||
|
|
||||||
const defaultSeasonStats = {
|
const defaultSeasonStats = {
|
||||||
nickName: "",
|
nickName: "",
|
||||||
@@ -55,8 +57,12 @@ const rankLoading = ref(false);
|
|||||||
const scoreRankList = ref([]);
|
const scoreRankList = ref([]);
|
||||||
const mvpRankList = ref([]);
|
const mvpRankList = ref([]);
|
||||||
const tenRingRankList = ref([]);
|
const tenRingRankList = ref([]);
|
||||||
|
const showLimitModal = ref(false);
|
||||||
const isSVip = computed(() => user.value.sVip === true);
|
const isSVip = computed(() => user.value.sVip === true);
|
||||||
const isVip = computed(() => user.value.vip === true && !isSVip.value);
|
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 isMember = (item = {}) => item.vip === true || item.sVip === true;
|
||||||
|
|
||||||
@@ -115,12 +121,40 @@ const toMatchPage = async (gameType, teamSize) => {
|
|||||||
uni.$showHint(1);
|
uni.$showHint(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const countData = await loadDailyCount();
|
||||||
|
if (isLimitReached(countData.ranked)) {
|
||||||
|
showLimitModal.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
await uni.$checkAudio();
|
await uni.$checkAudio();
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/match-page?gameType=${gameType}&teamSize=${teamSize}`,
|
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 = () => {
|
const toMyGrowthPage = () => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/my-growth",
|
url: "/pages/my-growth",
|
||||||
@@ -246,7 +280,10 @@ const onChangeSeason = async (seasonId, name) => {
|
|||||||
// 页面显示时先拿赛季列表,再拉当前赛季统计和默认榜单数据。
|
// 页面显示时先拿赛季列表,再拉当前赛季统计和默认榜单数据。
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
try {
|
try {
|
||||||
const seasonResult = await getSeasonList();
|
const [seasonResult] = await Promise.all([
|
||||||
|
getSeasonList(),
|
||||||
|
loadDailyCount(),
|
||||||
|
]);
|
||||||
seasonData.value = seasonResult.list || [];
|
seasonData.value = seasonResult.list || [];
|
||||||
|
|
||||||
if (!seasonData.value.length) {
|
if (!seasonData.value.length) {
|
||||||
@@ -282,7 +319,12 @@ onShow(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container title="排位赛" :showBackToGame="true" :bgType="6">
|
<Container
|
||||||
|
:title="rankedLimitText ? rankedLimitText : '排位赛'"
|
||||||
|
:titleStyle="rankedLimitText ? { fontSize: '24rpx', fontWeight: 'normal' } : {}"
|
||||||
|
:showBackToGame="true"
|
||||||
|
:bgType="6"
|
||||||
|
>
|
||||||
<view class="battle-types-box">
|
<view class="battle-types-box">
|
||||||
<view class="battle-types">
|
<view class="battle-types">
|
||||||
<view class="first">
|
<view class="first">
|
||||||
@@ -580,6 +622,14 @@ onShow(async () => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
</Container>
|
||||||
|
<ModalDialog
|
||||||
|
:show="showLimitModal"
|
||||||
|
:content="'今日排位赛次数已经用完\n开通会员可增加次数'"
|
||||||
|
cancelText="知道了"
|
||||||
|
confirmText="去开通"
|
||||||
|
:onCancel="closeLimitModal"
|
||||||
|
:onConfirm="goVipPage"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -926,4 +976,19 @@ onShow(async () => {
|
|||||||
font-size: 10px !important;
|
font-size: 10px !important;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
}
|
}
|
||||||
|
.flex-box{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
}
|
||||||
|
.lf-text{
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #bf985e;
|
||||||
|
}
|
||||||
|
.rg-text{
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 277 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 603 B |
|
After Width: | Height: | Size: 395 B |
|
After Width: | Height: | Size: 457 B |
|
After Width: | Height: | Size: 407 B |
|
After Width: | Height: | Size: 494 B |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 548 B |
@@ -26,6 +26,17 @@ const getDefaultGame = () => ({
|
|||||||
tips: "",
|
tips: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getDefaultDailyCount = () => ({
|
||||||
|
challenge: {
|
||||||
|
count: 0,
|
||||||
|
limit: -1,
|
||||||
|
},
|
||||||
|
ranked: {
|
||||||
|
count: 0,
|
||||||
|
limit: -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const getLvlName = (rankLvl, rankList = []) => {
|
const getLvlName = (rankLvl, rankList = []) => {
|
||||||
if (!rankList) return;
|
if (!rankList) return;
|
||||||
let lvlName = "";
|
let lvlName = "";
|
||||||
@@ -99,6 +110,7 @@ export default defineStore("store", {
|
|||||||
totalShot: 0, // 轮次总箭数(用于 HeaderProgress 恢复状态)
|
totalShot: 0, // 轮次总箭数(用于 HeaderProgress 恢复状态)
|
||||||
tips: "", // 当前提示文案(用于 HeaderProgress 恢复状态,替代 uni.$emit 避免时序问题)
|
tips: "", // 当前提示文案(用于 HeaderProgress 恢复状态,替代 uni.$emit 避免时序问题)
|
||||||
},
|
},
|
||||||
|
dailyCount: getDefaultDailyCount(),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
@@ -171,12 +183,19 @@ export default defineStore("store", {
|
|||||||
updateRoomNumber(number) {
|
updateRoomNumber(number) {
|
||||||
this.game.roomNumber = number;
|
this.game.roomNumber = number;
|
||||||
},
|
},
|
||||||
|
updateDailyCount(data = {}) {
|
||||||
|
this.dailyCount = {
|
||||||
|
...getDefaultDailyCount(),
|
||||||
|
...(data || {}),
|
||||||
|
};
|
||||||
|
},
|
||||||
clearSessionState() {
|
clearSessionState() {
|
||||||
this.$patch({
|
this.$patch({
|
||||||
user: getDefaultUser(),
|
user: getDefaultUser(),
|
||||||
device: getDefaultDevice(),
|
device: getDefaultDevice(),
|
||||||
online: false,
|
online: false,
|
||||||
game: getDefaultGame(),
|
game: getDefaultGame(),
|
||||||
|
dailyCount: getDefaultDailyCount(),
|
||||||
});
|
});
|
||||||
uni.removeStorageSync(PERSISTED_STORE_KEY);
|
uni.removeStorageSync(PERSISTED_STORE_KEY);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const formatTimestamp = (timestamp) => {
|
export const formatTimestamp = (timestamp) => {
|
||||||
const date = new Date(timestamp * 1000);
|
const date = new Date(timestamp);
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = date.getMonth() + 1;
|
const month = date.getMonth() + 1;
|
||||||
const day = date.getDate();
|
const day = date.getDate();
|
||||||
@@ -11,11 +11,13 @@ export const debounce = (fn, delay = 300) => {
|
|||||||
let timer = null;
|
let timer = null;
|
||||||
return async (...args) => {
|
return async (...args) => {
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer);
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
timer = setTimeout(async () => {
|
timer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await fn(...args);
|
const result = await fn(...args);
|
||||||
resolve(result);
|
resolve(result);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
} finally {
|
} finally {
|
||||||
timer = null;
|
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") => {
|
export const wxShare = async (canvasId = "shareCanvas") => {
|
||||||
try {
|
try {
|
||||||
// 先尝试按 id 查找 <canvas type="2d"> 节点
|
// 先尝试按 id 查找 <canvas type="2d"> 节点
|
||||||
@@ -54,35 +78,63 @@ export const wxShare = async (canvasId = "shareCanvas") => {
|
|||||||
quality: 1,
|
quality: 1,
|
||||||
success: (r) => {
|
success: (r) => {
|
||||||
const p = r.tempFilePath || r.apFilePath || r.filePath;
|
const p = r.tempFilePath || r.apFilePath || r.filePath;
|
||||||
|
if (!p) {
|
||||||
|
reject(new Error("share image path is empty"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
resolve(p);
|
resolve(p);
|
||||||
},
|
},
|
||||||
fail: reject,
|
fail: reject,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
wx.showShareImageMenu({
|
wx.showShareImageMenu({
|
||||||
entrancePath: "pages/index",
|
entrancePath: "pages/index",
|
||||||
path: tempPath,
|
path: tempPath,
|
||||||
|
success: resolve,
|
||||||
|
fail: (error) => {
|
||||||
|
if (isShareCancel(error)) {
|
||||||
|
resolve({ canceled: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return tempPath;
|
return tempPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回退:旧版非 2D 画布(通过 canvasId 导出)
|
// 回退:旧版非 2D 画布(通过 canvasId 导出)
|
||||||
const res = await uni.canvasToTempFilePath({
|
const res = await new Promise((resolve, reject) => {
|
||||||
|
uni.canvasToTempFilePath({
|
||||||
canvasId,
|
canvasId,
|
||||||
fileType: "png",
|
fileType: "png",
|
||||||
quality: 1,
|
quality: 1,
|
||||||
|
success: resolve,
|
||||||
|
fail: reject,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
const tempPath = res.tempFilePath || res.apFilePath || res.filePath;
|
||||||
|
if (!tempPath) {
|
||||||
|
throw new Error("share image path is empty");
|
||||||
|
}
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
wx.showShareImageMenu({
|
wx.showShareImageMenu({
|
||||||
entrancePath: "pages/index",
|
entrancePath: "pages/index",
|
||||||
path: res.tempFilePath,
|
path: tempPath,
|
||||||
|
success: resolve,
|
||||||
|
fail: (error) => {
|
||||||
|
if (isShareCancel(error)) {
|
||||||
|
resolve({ canceled: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return res.tempFilePath;
|
});
|
||||||
|
return tempPath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("生成图片失败:", error);
|
console.log("生成图片失败:", error);
|
||||||
uni.showToast({
|
|
||||||
title: "生成图片失败",
|
|
||||||
icon: "error",
|
|
||||||
});
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||