Merge branch 'feat-vip' into test

This commit is contained in:
2026-06-25 18:16:54 +08:00
13 changed files with 351 additions and 52 deletions

View File

@@ -23,7 +23,9 @@ try {
console.error("获取环境信息失败,使用默认正式环境", e); console.error("获取环境信息失败,使用默认正式环境", e);
} }
function request(method, url, data = {}) { const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot");
function request(method, url, data = {}, baseUrl = BASE_URL) {
const token = uni.getStorageSync( const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token` `${uni.getAccountInfoSync().miniProgram.envVersion}_token`
); );
@@ -31,7 +33,7 @@ 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,
@@ -41,6 +43,7 @@ function request(method, url, 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 +53,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 +104,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 +176,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}`);
}; };

View File

@@ -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;
} }
} }

View File

@@ -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 () => {
await getReadyAPI(roomNumber.value); try {
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">

View File

@@ -48,6 +48,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",
@@ -139,8 +140,19 @@ async function onReceiveMessage(msg) {
} }
const onClickShare = debounce(async () => { const onClickShare = debounce(async () => {
await sharePractiseData("shareCanvas", 1, user.value, practiseResult.value); if (sharing.value) return;
await wxShare("shareCanvas"); sharing.value = true;
try {
await sharePractiseData("shareCanvas", 1, user.value, practiseResult.value);
await wxShare("shareCanvas");
} catch (e) {
uni.showToast({
title: "海报生成失败,请稍后重试",
icon: "none",
});
} finally {
sharing.value = false;
}
}); });
onMounted(() => { onMounted(() => {

View File

@@ -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,8 +205,8 @@ onLoad(async (options) => {
</view> </view>
</view> </view>
<view> <view>
<view class="pp-text"> <view v-if="challengeLimitText" class="pp-text">
<!-- 今日约战次数2/2 --> {{ challengeLimitText }}
</view> </view>
<SButton width="80%" :rounded="30" :onClick="() => $clickSound(onCreateRoom)"> <SButton width="80%" :rounded="30" :onClick="() => $clickSound(onCreateRoom)">
创建约战房 创建约战房
@@ -186,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>

View File

@@ -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 {
// 启动超时计时器,防止 WS 消息丢失或长时间无对手导致用户卡死 await matchGameAPI(true, gameType.value, teamSize.value);
clearMatchTimeout(); // 启动超时计时器,防止 WS 消息丢失或长时间无对手导致用户卡死
matchTimeoutTimer.value = setTimeout(handleMatchTimeout, MATCH_TIMEOUT_MS); clearMatchTimeout();
matchTimeoutTimer.value = setTimeout(handleMatchTimeout, MATCH_TIMEOUT_MS);
} catch (error) {
clearMatchTimeout();
if (isLimitError(error)) {
showLimitModal.value = true;
return;
}
uni.navigateBack();
}
} }
}); });
@@ -154,6 +178,14 @@ onHide(() => {
<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>

View File

@@ -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;
await generateShareImage("shareImageCanvas", record.value); try {
await wxShare("shareImageCanvas"); await generateShareImage("shareImageCanvas", record.value);
loading.value = false; await wxShare("shareImageCanvas");
} catch (e) {
uni.showToast({
title: "海报生成失败,请稍后重试",
icon: "none",
});
} finally {
loading.value = false;
}
}; };
onLoad(async (options) => { onLoad(async (options) => {

View File

@@ -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;
await sharePointData("shareCanvas", mine.value); sharing.value = true;
await wxShare("shareCanvas"); try {
await sharePointData("shareCanvas", mine.value);
await wxShare("shareCanvas");
} catch (e) {
uni.showToast({
title: "海报生成失败,请稍后重试",
icon: "none",
});
} finally {
sharing.value = false;
}
}; };
onMounted(async () => { onMounted(async () => {

View File

@@ -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 () => {
await sharePractiseData("shareCanvas", 2, user.value, practiseResult.value); if (sharing.value) return;
await wxShare("shareCanvas"); sharing.value = true;
try {
await sharePractiseData("shareCanvas", 2, user.value, practiseResult.value);
await wxShare("shareCanvas");
} catch (e) {
uni.showToast({
title: "海报生成失败,请稍后重试",
icon: "none",
});
} finally {
sharing.value = false;
}
}); });
function onAudioEnded(s) { function onAudioEnded(s) {

View File

@@ -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 () => {
await sharePractiseData("shareCanvas", 3, user.value, practiseResult.value); if (sharing.value) return;
await wxShare("shareCanvas"); sharing.value = true;
try {
await sharePractiseData("shareCanvas", 3, user.value, practiseResult.value);
await wxShare("shareCanvas");
} catch (e) {
uni.showToast({
title: "海报生成失败,请稍后重试",
icon: "none",
});
} finally {
sharing.value = false;
}
}); });
onMounted(async () => { onMounted(async () => {

View File

@@ -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) {
@@ -283,12 +320,12 @@ onShow(async () => {
<template> <template>
<Container <Container
:title="'排位赛'" :title="rankedLimitText ? rankedLimitText : '排位赛'"
:titleStyle="rankedLimitText ? { fontSize: '24rpx', fontWeight: 'normal' } : {}"
:showBackToGame="true" :showBackToGame="true"
:bgType="6" :bgType="6"
> >
<view class="battle-types-box"> <view class="battle-types-box">
<!-- :title="'今日排位次数2/2'" :titleStyle="{ fontSize: '24rpx', fontWeight: 'normal' }" -->
<view class="battle-types"> <view class="battle-types">
<view class="first"> <view class="first">
<image src="../static/rank/battle-choose.png" mode="widthFix" /> <image src="../static/rank/battle-choose.png" mode="widthFix" />
@@ -585,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>

View File

@@ -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(() => {

View File

@@ -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,
}); });
}); });
wx.showShareImageMenu({ await new Promise((resolve, reject) => {
entrancePath: "pages/index", wx.showShareImageMenu({
path: tempPath, entrancePath: "pages/index",
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) => {
canvasId, uni.canvasToTempFilePath({
fileType: "png", canvasId,
quality: 1, fileType: "png",
quality: 1,
success: resolve,
fail: reject,
});
}); });
wx.showShareImageMenu({ const tempPath = res.tempFilePath || res.apFilePath || res.filePath;
entrancePath: "pages/index", if (!tempPath) {
path: res.tempFilePath, throw new Error("share image path is empty");
}
await new Promise((resolve, reject) => {
wx.showShareImageMenu({
entrancePath: "pages/index",
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;
} }
}; };