Merge branch 'feat-vip' into test
This commit is contained in:
+11
@@ -366,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,
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ const openCoachComment = () => {
|
|||||||
}}</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"
|
||||||
? "还需要持续改进哦~"
|
? "还需要持续改进哦~"
|
||||||
|
|||||||
@@ -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 = [
|
||||||
@@ -114,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);
|
||||||
@@ -204,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(() => {
|
||||||
@@ -230,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;
|
||||||
@@ -343,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"
|
||||||
|
|||||||
+78
-64
@@ -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";
|
||||||
|
|
||||||
@@ -18,9 +18,6 @@ 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 = [
|
||||||
@@ -100,16 +97,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();
|
||||||
@@ -263,41 +250,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 () => {
|
||||||
@@ -317,12 +301,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",
|
||||||
@@ -330,34 +324,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;
|
||||||
}
|
}
|
||||||
@@ -365,9 +382,6 @@ const onPay = async () => {
|
|||||||
|
|
||||||
onShow(loadVipConfig);
|
onShow(loadVipConfig);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
clearRefreshTimer();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user