Compare commits
3
Commits
ota-mode
...
6aa3c6fd6c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aa3c6fd6c | ||
|
|
8c6374b2c5 | ||
|
|
3e99bb8ad9 |
+11
@@ -352,6 +352,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) => {
|
||||
return request("POST", "/user/order/pay", {
|
||||
id,
|
||||
|
||||
@@ -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/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",
|
||||
射击无效:
|
||||
|
||||
@@ -207,7 +207,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true;
|
||||
justify-content: center;
|
||||
color: #fff9;
|
||||
font-size: 12px;
|
||||
padding-top: 7px;
|
||||
/* padding-top: 7px; */
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.player-name {
|
||||
|
||||
@@ -143,7 +143,7 @@ const openCoachComment = () => {
|
||||
}}</text
|
||||
>环的成绩,所有箭支上靶后的平均点间距为<text
|
||||
:style="{ color: '#fed847' }"
|
||||
>{{ Number((result.average_distance || 0).toFixed(2)) }}</text
|
||||
>{{ Number((result?.interpretation?.spreadStability || 0).toFixed(2)) }}</text
|
||||
>,{{
|
||||
result.spreadEvaluation === "Dispersed"
|
||||
? "还需要持续改进哦~"
|
||||
|
||||
@@ -38,6 +38,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
halfRest: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onStop: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
@@ -136,8 +140,9 @@ const updateSound = () => {
|
||||
async function onReceiveMessage(msg) {
|
||||
if (Array.isArray(msg)) return;
|
||||
if (msg.type === MESSAGETYPESV2.BattleStart) {
|
||||
const audioKey = props.melee && (halfTime.value || props.halfRest) ? "下半场开始" : "比赛开始";
|
||||
halfTime.value = false;
|
||||
audioManager.play("比赛开始");
|
||||
audioManager.play(audioKey);
|
||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||
audioManager.play("比赛结束", false);
|
||||
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { storeToRefs } from "pinia";
|
||||
const store = useStore();
|
||||
const { user } = storeToRefs(store);
|
||||
const scores = ref([]);
|
||||
const isSvip = ref(false);
|
||||
const step = ref(0);
|
||||
const total = 12;
|
||||
const stepButtonTexts = [
|
||||
@@ -114,6 +115,7 @@ const onOver = async () => {
|
||||
|
||||
async function onReceiveMessage(msg) {
|
||||
if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||
isSvip.value = msg.sVip === true;
|
||||
scores.value = msg.details;
|
||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||
setTimeout(onOver, 1500);
|
||||
@@ -204,6 +206,7 @@ const nextStep = async () => {
|
||||
title.value = "小试牛刀";
|
||||
await startPractiseAPI();
|
||||
scores.value = [];
|
||||
isSvip.value = false;
|
||||
step.value = 5;
|
||||
start.value = true;
|
||||
setTimeout(() => {
|
||||
@@ -230,6 +233,7 @@ const onClose = async () => {
|
||||
practiseResult.value = {};
|
||||
start.value = false;
|
||||
scores.value = [];
|
||||
isSvip.value = false;
|
||||
step.value = 4;
|
||||
const result = await createPractiseAPI(total, 120);
|
||||
if (result) practiseId.value = result.id;
|
||||
@@ -343,6 +347,7 @@ const onClose = async () => {
|
||||
:currentRound="step === 5 ? scores.length : 0"
|
||||
:totalRound="step === 5 ? total : 0"
|
||||
:scores="scores"
|
||||
:isSvip="isSvip"
|
||||
/>
|
||||
<ScorePanel
|
||||
v-if="step === 5"
|
||||
|
||||
@@ -236,6 +236,7 @@ onShow(async () => {
|
||||
:tips="tips"
|
||||
:total="90"
|
||||
:melee="true"
|
||||
:halfRest="halfRest"
|
||||
:battleId="battleId"
|
||||
/>
|
||||
<view v-if="start" class="user-row">
|
||||
|
||||
+122
-84
@@ -1,10 +1,10 @@
|
||||
<script setup>
|
||||
import { computed, ref, onBeforeUnmount } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
import Signin from "@/components/Signin.vue";
|
||||
import { createOrderAPI, getAppConfig, getHomeData } from "@/apis";
|
||||
import { capsuleHeight } from "@/util";
|
||||
import { virtualPayOrderAPI, getAppConfig, getHomeData, getOrderDetailAPI } from "@/apis";
|
||||
import { capsuleHeight, wxLogin } from "@/util";
|
||||
import useStore from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
@@ -18,9 +18,6 @@ const showModal = ref(false);
|
||||
const loadingConfig = ref(false);
|
||||
const paying = ref(false);
|
||||
const refreshing = ref(false);
|
||||
const timer = ref(null);
|
||||
const lastDate = ref(user.value.expiredAt || 0);
|
||||
const maxRefreshTimes = 12;
|
||||
|
||||
// 会员页核心展示数据:视觉、权益、套餐均按蓝湖当前两张设计稿拆分。
|
||||
const memberTypes = [
|
||||
@@ -100,16 +97,6 @@ const getVipExpiredValue = (type, source = user.value) => {
|
||||
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) => {
|
||||
return toTimestamp(getVipExpiredValue(type)) > Date.now();
|
||||
@@ -158,6 +145,23 @@ const getMenuPrice = (item) => {
|
||||
return value ? String(value).replace("¥", "").replace("¥", "") : "";
|
||||
};
|
||||
|
||||
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 vipType = Number(item.vipType);
|
||||
if (vipType === 1) return "normal";
|
||||
@@ -180,7 +184,7 @@ const getPackageMonths = (name) => {
|
||||
};
|
||||
|
||||
const matchPackageSource = (type, pack, index) => {
|
||||
const menus = configMenus.value;
|
||||
const menus = sortMenusBySort(configMenus.value);
|
||||
const typedMenus = menus.filter((item) => {
|
||||
const menuType = getMenuType(item);
|
||||
if (type.key === "super") return menuType === "super";
|
||||
@@ -197,17 +201,24 @@ const matchPackageSource = (type, pack, index) => {
|
||||
};
|
||||
|
||||
const getPackages = (type) => {
|
||||
return type.packages.map((item, index) => {
|
||||
const source = matchPackageSource(type, item, index);
|
||||
return {
|
||||
...item,
|
||||
source,
|
||||
name: source ? getMenuName(source) || item.name : item.name,
|
||||
price: source ? getMenuPrice(source) || item.price : item.price,
|
||||
icon: source && source.icon,
|
||||
id: source && source.id,
|
||||
};
|
||||
});
|
||||
return type.packages
|
||||
.map((item, index) => {
|
||||
const source = matchPackageSource(type, item, index);
|
||||
return {
|
||||
...item,
|
||||
source,
|
||||
name: source ? getMenuName(source) || item.name : item.name,
|
||||
price: source ? getMenuPrice(source) || item.price : item.price,
|
||||
icon: source && source.icon,
|
||||
id: source && source.id,
|
||||
};
|
||||
})
|
||||
.map((item, index) => ({ item, index }))
|
||||
.sort((a, b) => {
|
||||
const sortDiff = getMenuSortValue(a.item.source) - getMenuSortValue(b.item.source);
|
||||
return sortDiff || a.index - b.index;
|
||||
})
|
||||
.map(({ item }) => item);
|
||||
};
|
||||
|
||||
const currentPackages = computed(() => {
|
||||
@@ -263,41 +274,38 @@ const loadVipConfig = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const clearRefreshTimer = () => {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
const refreshUserAfterPay = async () => {
|
||||
refreshing.value = true;
|
||||
try {
|
||||
const result = await getHomeData();
|
||||
if (result.user) {
|
||||
updateUser(result.user);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("refresh user after pay error", error);
|
||||
} finally {
|
||||
refreshing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshUserAfterPay = () => {
|
||||
clearRefreshTimer();
|
||||
let refreshTimes = 0;
|
||||
// 先记录支付前的最大到期时间,轮询到更大的值就认为会员状态已刷新。
|
||||
lastDate.value = getLatestVipExpiredTime();
|
||||
refreshing.value = true;
|
||||
timer.value = setInterval(async () => {
|
||||
refreshTimes += 1;
|
||||
try {
|
||||
const result = await getHomeData();
|
||||
const latestExpiredAt = getLatestVipExpiredTime(result.user);
|
||||
if (result.user && latestExpiredAt > lastDate.value) {
|
||||
lastDate.value = latestExpiredAt;
|
||||
updateUser(result.user);
|
||||
clearRefreshTimer();
|
||||
refreshing.value = false;
|
||||
} else if (refreshTimes >= maxRefreshTimes) {
|
||||
clearRefreshTimer();
|
||||
refreshing.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("refresh user after pay error", error);
|
||||
if (refreshTimes >= maxRefreshTimes) {
|
||||
clearRefreshTimer();
|
||||
refreshing.value = false;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
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;
|
||||
}
|
||||
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 () => {
|
||||
@@ -317,12 +325,22 @@ const onPay = async () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (typeof wx === "undefined" || !wx.requestVirtualPayment) {
|
||||
uni.showToast({
|
||||
title: "当前环境不支持微信虚拟支付",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
paying.value = true;
|
||||
let waitingPayment = false;
|
||||
let payToast = null;
|
||||
try {
|
||||
const result = await createOrderAPI(vipId);
|
||||
const params = result?.pay?.order?.jsApi?.params;
|
||||
if (!params?.timeStamp || !params?.nonceStr || !params?.package || !params?.paySign) {
|
||||
// 微信虚拟支付创建订单前需要登录 code,服务端用它换取本次支付签名参数。
|
||||
const wxResult = await wxLogin();
|
||||
const result = await virtualPayOrderAPI(vipId, wxResult.code);
|
||||
const finalSignData = getVirtualPaySignData(result);
|
||||
if (!finalSignData || !result?.paySig || !result?.signature) {
|
||||
uni.showToast({
|
||||
title: "支付参数生成失败",
|
||||
icon: "none",
|
||||
@@ -330,34 +348,57 @@ const onPay = async () => {
|
||||
refreshing.value = false;
|
||||
return;
|
||||
}
|
||||
waitingPayment = true;
|
||||
wx.requestPayment({
|
||||
timeStamp: params.timeStamp, // 微信支付时间戳
|
||||
nonceStr: params.nonceStr, // 微信支付随机串
|
||||
package: params.package, // 预支付交易会话标识
|
||||
paySign: params.paySign, // 微信支付签名
|
||||
signType: params.signType || "RSA",
|
||||
success() {
|
||||
uni.showToast({
|
||||
wx.requestVirtualPayment({
|
||||
signData: finalSignData,
|
||||
paySig: result.paySig,
|
||||
signature: result.signature,
|
||||
mode: "short_series_goods",
|
||||
async success() {
|
||||
payToast = {
|
||||
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();
|
||||
},
|
||||
fail(res) {
|
||||
console.log("pay error", res);
|
||||
if (res.errMsg && res.errMsg.indexOf("cancel") !== -1) return;
|
||||
uni.showToast({
|
||||
title: "支付失败,请稍后重试",
|
||||
console.log("virtual pay error", res);
|
||||
if (isVirtualPayCancel(res)) {
|
||||
payToast = {
|
||||
title: "支付已取消",
|
||||
icon: "none",
|
||||
};
|
||||
return;
|
||||
}
|
||||
payToast = {
|
||||
title: res.message || "支付失败,请稍后重试",
|
||||
icon: "none",
|
||||
});
|
||||
};
|
||||
},
|
||||
complete() {
|
||||
paying.value = false;
|
||||
if (payToast) {
|
||||
setTimeout(() => {
|
||||
uni.showToast(payToast);
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
});
|
||||
waitingPayment = true;
|
||||
} 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 {
|
||||
if (!waitingPayment) paying.value = false;
|
||||
}
|
||||
@@ -365,9 +406,6 @@ const onPay = async () => {
|
||||
|
||||
onShow(loadVipConfig);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearRefreshTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -449,7 +487,7 @@ onBeforeUnmount(() => {
|
||||
<view class="package-list">
|
||||
<view
|
||||
v-for="(pack, index) in getPackages(type)"
|
||||
:key="`${type.key}-${pack.name}`"
|
||||
:key="`${type.key}-${pack.id || pack.name}`"
|
||||
class="package-card"
|
||||
:class="{ 'package-card--active': selectedPackageIndex === index }"
|
||||
@click="selectPackage(index)"
|
||||
|
||||
@@ -97,12 +97,10 @@ const cancelOrder = async () => {
|
||||
>复制</text
|
||||
>
|
||||
</view>
|
||||
<text>下单时间:{{ data.vipCreateAt }}</text>
|
||||
<text
|
||||
>支付时间:{{
|
||||
<text>创建时间:{{ data.orderCreateAt }}</text>
|
||||
<text v-if="data.orderStatus === 4">支付时间:{{
|
||||
data.orderStatus === 4 ? data.paymentTime : ""
|
||||
}}</text
|
||||
>
|
||||
}}</text>
|
||||
<text>金额:{{ data.total }} 元</text>
|
||||
<text>支付方式:微信</text>
|
||||
</view>
|
||||
|
||||
@@ -13,7 +13,7 @@ import Container from "@/components/Container.vue";
|
||||
<view class="section">
|
||||
<view class="title">一、段位体系概述</view>
|
||||
<view class="text">
|
||||
我们的段位体系分为多个等级,从低到高依次为:倔强青铜、秩序白银、黄金王者、永恒钻石、最强王者、非凡王者、无双王者、绝世王者、至圣王者、荣耀王者和传奇王者。每个大段位下又分为若干小段位,玩家需要通过积累积分来提升段位。
|
||||
我们的段位体系分为多个等级,从低到高依次为:倔强青铜、秩序白银、荣耀黄金、永恒钻石、最强王者、非凡王者、无双王者、绝世王者、至圣王者、荣耀王者和传奇王者。每个大段位下又分为若干小段位,玩家需要通过积累积分来提升段位。
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -79,7 +79,7 @@ import Container from "@/components/Container.vue";
|
||||
<text>每个小段位需要满 3颗星才能晋升到下一个段位,共9颗星。</text>
|
||||
</view>
|
||||
<view class="table-row">
|
||||
<text>黄金王者</text>
|
||||
<text>荣耀黄金</text>
|
||||
<view>
|
||||
<text>黄金1*</text>
|
||||
<text>黄金2*</text>
|
||||
|
||||
Reference in New Issue
Block a user