9 Commits
27 changed files with 273 additions and 183 deletions
+15
View File
@@ -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,
@@ -510,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",
+2
View File
@@ -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",
射击无效: 射击无效:
+1 -1
View File
@@ -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 {
+16 -3
View File
@@ -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(() => {
+65 -24
View File
@@ -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"
/> />
+10 -10
View File
@@ -85,25 +85,25 @@ const handleUpdateClick = async () => {
<!-- 悬浮图标溢出卡片顶部 --> <!-- 悬浮图标溢出卡片顶部 -->
<image <image
v-if="isNewVersion" v-if="isNewVersion"
src="../static/ota/ota-mascot.png" src="https://static.shelingxingqiu.com/shootmini/static/ota/ota-mascot.png"
mode="aspectFit" mode="aspectFit"
class="float-icon float-mascot" class="float-icon float-mascot"
/> />
<image <image
v-else-if="isSuccess" v-else-if="isSuccess"
src="../static/ota/check-char.png" src="https://static.shelingxingqiu.com/shootmini/static/ota/check-char.png"
mode="aspectFit" mode="aspectFit"
class="float-icon float-check" class="float-icon float-check"
/> />
<image <image
v-else-if="isFailure" v-else-if="isFailure"
src="../static/ota/close-char.png" src="https://static.shelingxingqiu.com/shootmini/static/ota/close-char.png"
mode="aspectFit" mode="aspectFit"
class="float-icon float-close" class="float-icon float-close"
/> />
<image <image
v-else-if="isProgress" v-else-if="isProgress"
src="../static/ota/target-char.png" src="https://static.shelingxingqiu.com/shootmini/static/ota/target-char.png"
mode="aspectFit" mode="aspectFit"
class="float-icon float-target" class="float-icon float-target"
/> />
@@ -111,7 +111,7 @@ const handleUpdateClick = async () => {
<!-- 弹窗卡片overflow:visible 允许按钮溢出底部背景图通过 ota-bg-clip 独立裁剪保持圆角 --> <!-- 弹窗卡片overflow:visible 允许按钮溢出底部背景图通过 ota-bg-clip 独立裁剪保持圆角 -->
<view class="ota-dialog"> <view class="ota-dialog">
<view class="ota-bg-clip"> <view class="ota-bg-clip">
<image src="../static/ota/ota-bg.png" mode="aspectFill" class="ota-bg" /> <image src="https://static.shelingxingqiu.com/shootmini/static/ota/ota-bg.png" mode="aspectFill" class="ota-bg" />
</view> </view>
<view <view
class="ota-content" class="ota-content"
@@ -120,9 +120,9 @@ const handleUpdateClick = async () => {
<!-- 发现新版本new-ver.png 已包含标题图不再重复文字版本号使用 ota-ver.png 胶囊背景 --> <!-- 发现新版本new-ver.png 已包含标题图不再重复文字版本号使用 ota-ver.png 胶囊背景 -->
<block v-if="isNewVersion"> <block v-if="isNewVersion">
<image src="../static/ota/new-ver.png" mode="aspectFit" class="new-ver-img" /> <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"> <view v-if="version" class="version-tag-wrap">
<image src="../static/ota/ota-ver.png" mode="aspectFit" class="version-tag-bg-img" /> <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> <text class="version-tag">{{ version }}</text>
</view> </view>
<!-- 副标题新版本将优化智能弓体验离下方详情 12rpx --> <!-- 副标题新版本将优化智能弓体验离下方详情 12rpx -->
@@ -139,7 +139,7 @@ const handleUpdateClick = async () => {
<!-- 更新成功图片左边距 34rpx文案左边距 44rpx按钮浮动底部居中 --> <!-- 更新成功图片左边距 34rpx文案左边距 44rpx按钮浮动底部居中 -->
<block v-else-if="isSuccess"> <block v-else-if="isSuccess">
<image src="../static/ota/update-ok.png" mode="aspectFit" class="result-title-img" style="width: 220rpx; height: 62rpx;" /> <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> <text class="dialog-desc">请关机并重启智能弓</text>
<view class="btn-group-result"> <view class="btn-group-result">
<view class="primary-btn" @click="emit('done')"> <view class="primary-btn" @click="emit('done')">
@@ -150,7 +150,7 @@ const handleUpdateClick = async () => {
<!-- 更新中复用成功标题图正文区域展示进度条无底部按钮 --> <!-- 更新中复用成功标题图正文区域展示进度条无底部按钮 -->
<block v-else-if="isProgress"> <block v-else-if="isProgress">
<image src="../static/ota/update_progress.png" mode="aspectFit" class="result-title-img" style="width: 220rpx; height: 62rpx;" /> <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-wrap">
<view class="progress-track"> <view class="progress-track">
<view class="progress-fill" :style="{ width: `${progressValue}%` }"></view> <view class="progress-fill" :style="{ width: `${progressValue}%` }"></view>
@@ -160,7 +160,7 @@ const handleUpdateClick = async () => {
<!-- 更新失败图片左边距 34rpx文案左对齐 44rpx按钮浮动底部居中 --> <!-- 更新失败图片左边距 34rpx文案左对齐 44rpx按钮浮动底部居中 -->
<block v-else-if="isFailure"> <block v-else-if="isFailure">
<image src="../static/ota/update-fail.png" mode="aspectFit" class="result-title-img" style="width: 222rpx; height: 62rpx;" /> <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">请确保</text>
<text class="dialog-desc">1智能弓已开启</text> <text class="dialog-desc">1智能弓已开启</text>
<text class="dialog-desc">2网路连接稳定</text> <text class="dialog-desc">2网路连接稳定</text>
+1 -1
View File
@@ -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"
? "还需要持续改进哦~" ? "还需要持续改进哦~"
+6 -1
View File
@@ -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) {
+6 -5
View File
@@ -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;
} }
} }
+5
View File
@@ -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"
+1
View File
@@ -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">
+125 -116
View File
@@ -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,11 +18,8 @@ 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,
}; };
}); });
}; };
@@ -263,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 () => {
@@ -317,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",
@@ -330,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;
} }
@@ -365,9 +375,6 @@ const onPay = async () => {
onShow(loadVipConfig); onShow(loadVipConfig);
onBeforeUnmount(() => {
clearRefreshTimer();
});
</script> </script>
<template> <template>
@@ -449,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)"
@@ -460,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>
@@ -473,13 +480,14 @@ 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">
@@ -746,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;
} }
@@ -755,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;
+3 -5
View File
@@ -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>
+5 -5
View File
@@ -632,7 +632,7 @@ onUnmounted(() => {
> >
<!-- Hero: wifi1.png 单图已内含吉祥物 --> <!-- Hero: wifi1.png 单图已内含吉祥物 -->
<view class="hero-area"> <view class="hero-area">
<image src="../static/ota/wifi1.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" /> <image src="https://static.shelingxingqiu.com/shootmini/static/ota/wifi1.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" />
</view> </view>
<text class="page-title" :class="{ 'connected-page-title': currentState === 'CONNECTED' }"> <text class="page-title" :class="{ 'connected-page-title': currentState === 'CONNECTED' }">
@@ -721,7 +721,7 @@ onUnmounted(() => {
<!-- UPDATING --> <!-- UPDATING -->
<view v-else-if="currentState === 'UPDATING'" class="center-page"> <view v-else-if="currentState === 'UPDATING'" class="center-page">
<image src="../static/ota/target-char.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" /> <image src="https://static.shelingxingqiu.com/shootmini/static/ota/target-char.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" />
<text class="page-title" style="margin-top: 24rpx;">更新中,请稍等片刻...</text> <text class="page-title" style="margin-top: 24rpx;">更新中,请稍等片刻...</text>
<view class="progress-wrap"> <view class="progress-wrap">
<view class="progress-track"> <view class="progress-track">
@@ -733,7 +733,7 @@ onUnmounted(() => {
<!-- DONE --> <!-- DONE -->
<view v-else-if="currentState === 'DONE'" class="center-page"> <view v-else-if="currentState === 'DONE'" class="center-page">
<image src="../static/ota/check-char.png" mode="aspectFit" style="width: 194rpx; height: 166rpx;" /> <image src="https://static.shelingxingqiu.com/shootmini/static/ota/check-char.png" mode="aspectFit" style="width: 194rpx; height: 166rpx;" />
<text class="page-title" style="margin-top: 24rpx;">更新完成</text> <text class="page-title" style="margin-top: 24rpx;">更新完成</text>
<text class="page-desc-white">请关机并重启智能弓</text> <text class="page-desc-white">请关机并重启智能弓</text>
<view class="primary-btn done-btn" style="margin-top:20px" @click="handleDone"> <view class="primary-btn done-btn" style="margin-top:20px" @click="handleDone">
@@ -743,7 +743,7 @@ onUnmounted(() => {
<!-- FAILED --> <!-- FAILED -->
<view v-else-if="currentState === 'FAILED'" class="center-page"> <view v-else-if="currentState === 'FAILED'" class="center-page">
<image src="../static/ota/close-char.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" /> <image src="https://static.shelingxingqiu.com/shootmini/static/ota/close-char.png" mode="aspectFit" style="width: 194rpx; height: 164rpx;" />
<text class="page-title fail-title" style="margin-top: 24rpx;">更新失败</text> <text class="page-title fail-title" style="margin-top: 24rpx;">更新失败</text>
<text class="page-desc-white">请确保</text> <text class="page-desc-white">请确保</text>
<text class="page-desc-white">1智能弓已开启</text> <text class="page-desc-white">1智能弓已开启</text>
@@ -756,7 +756,7 @@ onUnmounted(() => {
<!-- CONNECTING 底部弹窗 --> <!-- CONNECTING 底部弹窗 -->
<view v-if="currentState === 'CONNECTING'" class="sheet-mask"> <view v-if="currentState === 'CONNECTING'" class="sheet-mask">
<view class="sheet" :style="{ marginBottom: keyboardHeight + 'px' }" @click.stop=""> <view class="sheet" :style="{ marginBottom: keyboardHeight + 'px' }" @click.stop="">
<image src="../static/ota/ota-bg.png" mode="aspectFill" class="sheet-bg" /> <image src="https://static.shelingxingqiu.com/shootmini/static/ota/ota-bg.png" mode="aspectFill" class="sheet-bg" />
<view class="sheet-inner"> <view class="sheet-inner">
<!-- 有密码网络 --> <!-- 有密码网络 -->
+2 -2
View File
@@ -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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB