9 Commits

Author SHA1 Message Date
kron
08e91a8c18 补全代码 2025-11-10 14:14:40 +08:00
kron
00a52f60b5 UI更新 2025-11-10 14:02:09 +08:00
kron
b853d52a26 计分详情里添加注释功能 2025-10-27 16:56:11 +08:00
kron
ea0c54b767 添加重置密码页面 2025-10-27 16:26:15 +08:00
kron
2bbe9f1aab 添加修改用户信息UI 2025-10-27 15:36:02 +08:00
kron
3af68d968c 添加编辑头像弹窗 2025-10-27 14:40:17 +08:00
kron
63c002ed56 页面翻译 2025-10-27 14:21:31 +08:00
kron
14f43e929f 完成ios首页改造 2025-10-27 13:56:27 +08:00
kron
a9168201b3 添加ios首页和注册登录页 2025-10-24 15:16:44 +08:00
171 changed files with 7482 additions and 19074 deletions

5
.gitignore vendored
View File

@@ -8,11 +8,6 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
.history
.github
openspec
CLAUDE.md
dosc
.DS_Store
dist
*.local

View File

@@ -1,3 +0,0 @@
{
"i18n-ally.localesPaths": []
}

10532
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,6 @@
"@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-xhs": "3.0.0-4060620250520001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
"@dcloudio/uni-ui": "^1.5.11",
"pinia": "2.0.36",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"

View File

@@ -1,28 +1,12 @@
<script setup>
import {
watch
} from "vue";
import {
onShow,
onHide
} from "@dcloudio/uni-app";
import { watch } from "vue";
import { onShow, onHide } from "@dcloudio/uni-app";
import websocket from "@/websocket";
import {
getDeviceBatteryAPI
} from "@/apis";
import useStore from "@/store";
import {
storeToRefs
} from "pinia";
import audioManager from "./audioManager";
import { storeToRefs } from "pinia";
const store = useStore();
const {
user
} = storeToRefs(store);
const {
updateUser,
updateOnline
} = store;
const { user } = storeToRefs(store);
const { updateUser } = store;
watch(
() => user.value.id,
@@ -31,52 +15,33 @@
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
if (newVal && token) {
websocket.createWebSocket(token, onShootWsMsg);
websocket.createWebSocket(token, (content) => {
uni.$emit("socket-inbox", content);
});
}
if (!newVal) {
websocket.closeWebSocket();
}
}, {
},
{
deep: false, // 如果 user 是一个对象或数组,建议开启
immediate: false, // 若想在初始化时立即执行一次回调,可开启。
}
);
function emitUpdateUser(value) {
updateUser(value);
}
async function emitUpdateOnline() {
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
}
function onDeviceShoot() {
// audioManager.play("射箭声音")
}
function onShootWsMsg(content) {
if(content.type === 'shoot-trigger'){
onDeviceShoot()
}
uni.$emit("socket-inbox", content);
}
onShow(() => {
uni.$on("update-user", emitUpdateUser);
uni.$on("update-online", emitUpdateOnline);
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
if (user.value.id && token) {
console.log("回到前台,重新连接 websocket");
websocket.createWebSocket(token, onShootWsMsg);
websocket.createWebSocket(token, (content) => {
uni.$emit("socket-inbox", content);
});
}
});
onHide(() => {
uni.$off("update-user", emitUpdateUser);
uni.$off("update-online", emitUpdateOnline);
websocket.closeWebSocket();
});
</script>
@@ -112,32 +77,23 @@
.guide-tips {
display: flex;
flex-direction: column;
font-size: 28rpx;
}
.guide-tips > text:first-child {
color: #fed847;
}
.guide-tips>text:nth-child(2) {
font-size: 24rpx;
}
@keyframes fadeInOut {
0% {
transform: translateY(20px);
opacity: 0;
}
30% {
transform: translateY(0);
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
@@ -152,7 +108,6 @@
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(20px);
opacity: 0;
@@ -168,7 +123,6 @@
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
@@ -185,7 +139,6 @@
transform: scale(1);
opacity: 1;
}
to {
transform: scale(0);
opacity: 0;
@@ -201,7 +154,6 @@
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
@@ -211,7 +163,6 @@
from {
transform: scale(2);
}
to {
transform: scale(1);
}
@@ -224,7 +175,7 @@
.share-canvas {
width: 300px;
height: 530px;
height: 534px;
position: absolute;
top: -1000px;
left: 0;
@@ -243,7 +194,6 @@
align-items: center;
justify-content: center;
}
.user-row {
display: flex;
align-items: center;
@@ -252,7 +202,6 @@
padding-top: 7px;
position: relative;
}
.half-time-tip {
width: 100%;
display: flex;
@@ -260,33 +209,23 @@
justify-content: center;
align-items: center;
}
.half-time-tip > text:last-child {
margin-top: 20px;
color: #fff9;
}
.see-more {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
margin-top: 5px;
}
.see-more > text {
color: #39a8ff;
margin-top: 2px;
font-size: 13px;
}
.see-more > image {
width: 15px;
}
@font-face {
font-family: "DINCondensed";
src: url("https://static.shelingxingqiu.com/font/DIN-Condensed-Bold-2.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
margin-top: 2px;
}
</style>

View File

@@ -6,8 +6,8 @@ try {
switch (envVersion) {
case "develop": // 开发版
BASE_URL = "http://localhost:8000/api/shoot";
// BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
// BASE_URL = "http://192.168.1.242:8000/api/shoot";
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
break;
case "trial": // 体验版
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
@@ -45,7 +45,6 @@ function request(method, url, data = {}) {
uni.removeStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
uni.$emit("update-user");
}
if (message === "ROOM_FULL") {
resolve({ full: true });
@@ -163,14 +162,13 @@ export const getProvinceData = () => {
return request("GET", "/index/provinces/list");
};
export const loginAPI = async (phone, nickName, avatarData, code) => {
export const loginAPI = async (nickName, avatarData, code) => {
const result = await request("POST", "/index/code", {
appName: "shoot",
appId: "wxa8f5989dcd45cc23",
nickName,
avatarData,
code,
phone,
});
uni.setStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`,
@@ -179,39 +177,12 @@ export const loginAPI = async (phone, nickName, avatarData, code) => {
return result;
};
export const silentLoginAPI = async (code) => {
const result = await request("POST", "/index/code", {
appName: "shoot",
appId: "wxa8f5989dcd45cc23",
code,
});
uni.setStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`,
result.token
);
return result;
};
export const checkUserBindAPI = async (code) => {
return request("POST", "/index/checkBind", {
appName: "shoot",
appId: "wxa8f5989dcd45cc23",
code,
});
};
export const bindDeviceAPI = (device) => {
return request("POST", "/user/device/bindDevice", {
device,
});
};
export const bindDeviceAPIV2 = (token) => {
return request("POST", "/user/device/bindDevice/v2", {
token: token,
});
};
export const unbindDeviceAPI = (deviceId) => {
return request("POST", "/user/device/unbindDevice", {
deviceId,
@@ -223,31 +194,23 @@ export const getMyDevicesAPI = () => {
return request("GET", "/user/device/getBindings");
};
export const createPractiseAPI = (arrows, time, target) => {
export const createPractiseAPI = (arrows) => {
return request("POST", "/user/practice/create", {
shootNumber: arrows,
shootTime: time,
targetType: target * 20,
arrows,
});
};
export const startPractiseAPI = () => {
return request("POST", "/user/practice/begin");
};
export const endPractiseAPI = () => {
return request("POST", "/user/practice/stop");
};
export const getPractiseAPI = async (id) => {
return request("GET", `/user/practice/get?id=${id}`);
const result = await request("GET", `/user/practice/get?id=${id}`);
const data = { ...(result.UserPracticeRound || {}) };
if (data.arrows) data.arrows = JSON.parse(data.arrows);
return data;
};
export const createRoomAPI = (gameType, teamSize, targetType) => {
export const createRoomAPI = (gameType, teamSize) => {
return request("POST", "/user/createroom", {
gameType,
teamSize,
targetType,
});
};
@@ -265,10 +228,9 @@ export const destroyRoomAPI = (roomNumber) => {
});
};
export const exitRoomAPI = (number, userId) => {
export const exitRoomAPI = (number) => {
return request("POST", "/user/room/exitRoom", {
number,
userId,
});
};
@@ -289,8 +251,6 @@ export const matchGameAPI = (match, gameType, teamSize) => {
match,
gameType,
teamSize,
readyTime: 15,
targetType: 20,
});
};
@@ -300,6 +260,78 @@ export const readyGameAPI = (battleId) => {
});
};
export const getGameAPI = async (battleId) => {
const result = await request("POST", "/user/battle/detail", {
id: battleId,
});
if (!result.battleStats) return {};
const {
battleStats = {},
playerStats = {},
goldenRoundRecords = [],
} = result;
const data = {
id: battleId,
mode: battleStats.mode, // 1.几V几 2.大乱斗
gameMode: battleStats.gameMode, // 1.约战 2.排位
teamSize: battleStats.teamSize,
};
if (battleStats && battleStats.mode === 1) {
data.winner = battleStats.winner;
data.roundsData = {};
data.redPlayers = {};
data.bluePlayers = {};
data.mvps = [];
data.goldenRounds =
goldenRoundRecords && goldenRoundRecords.length ? goldenRoundRecords : [];
playerStats.forEach((item) => {
const { playerBattleStats = {}, roundRecords = [] } = item;
if (playerBattleStats.team === 0) {
data.redPlayers[playerBattleStats.playerId] = playerBattleStats;
}
if (playerBattleStats.team === 1) {
data.bluePlayers[playerBattleStats.playerId] = playerBattleStats;
}
if (playerBattleStats.mvp) {
data.mvps.push(playerBattleStats);
}
roundRecords.forEach((round) => {
data.roundsData[round.roundNumber] = {
...data.roundsData[round.roundNumber],
[round.playerId]: round.arrowHistory,
};
});
});
const totalRounds = Object.keys(data.roundsData).length;
(goldenRoundRecords || []).forEach((item, index) => {
item.arrowHistory.forEach((arrow) => {
if (!data.roundsData[totalRounds + index + 1]) {
data.roundsData[totalRounds + index + 1] = {};
}
if (!data.roundsData[totalRounds + index + 1][arrow.playerId]) {
data.roundsData[totalRounds + index + 1][arrow.playerId] = [];
}
data.roundsData[totalRounds + index + 1][arrow.playerId].push(arrow);
});
});
data.mvps.sort((a, b) => b.totalRings - a.totalRings);
}
if (battleStats && battleStats.mode === 2) {
data.players = [];
playerStats.forEach((item) => {
data.players.push({
...item.playerBattleStats,
arrowHistory: item.roundRecords[0].arrowHistory,
});
});
data.players = data.players.sort((a, b) => b.totalScore - a.totalScore);
}
// console.log("game result:", result);
// console.log("format data:", data);
return data;
};
export const simulShootAPI = (device_id, x, y) => {
const data = {
device_id,
@@ -312,12 +344,39 @@ export const simulShootAPI = (device_id, x, y) => {
};
export const getBattleListAPI = async (page, battleType) => {
const data = [];
const result = await request("POST", "/user/battle/details/list", {
page,
pageSize: 10,
battleType,
modeType: 0,
});
return result.list;
(result.Battles || []).forEach((item) => {
let name = "";
if (item.battleStats.mode === 1) {
name = `${item.playerStats.length / 2}V${item.playerStats.length / 2}`;
}
if (item.battleStats.mode === 2) {
name = `${item.playerStats.length}人大乱斗`;
}
data.push({
name,
battleId: item.battleStats.battleId,
mode: item.battleStats.mode,
createdAt: item.battleStats.createdAt,
gameEndAt: item.battleStats.gameEndAt,
winner: item.battleStats.winner,
players: item.playerStats
.map((p) => p.playerBattleStats)
.sort((a, b) => b.totalScore - a.totalScore),
redPlayers: item.playerStats
.filter((p) => p.playerBattleStats.team === 0)
.map((p) => p.playerBattleStats),
bluePlayers: item.playerStats
.filter((p) => p.playerBattleStats.team === 1)
.map((p) => p.playerBattleStats),
});
});
return data;
};
export const getRankListAPI = () => {
@@ -350,8 +409,15 @@ export const cancelOrderListAPI = async (id) => {
return request("POST", "/user/order/cancelOrder", { id });
};
export const getUserGameState = () => {
return request("GET", "/user/state");
export const isGamingAPI = async () => {
const result = await request("GET", "/user/isGaming");
return result.gaming || false;
};
export const getCurrentGameAPI = async () => {
uni.$emit("update-header-loading", true);
const result = await request("GET", "/user/join/battle");
return result.currentGame || {};
};
export const getPointBookConfigAPI = async () => {
@@ -452,47 +518,3 @@ export const addNoteAPI = async (id, remark) => {
export const removePointRecord = async (id) => {
return request("DELETE", `/user/score/sheet/delete?id=${id}`);
};
export const getPhoneNumberAPI = (data) => {
return request("POST", "/index/getPhone", data);
};
export const getPointBookRankListAPI = (page = 1) => {
return request(
"GET",
`/user/score/sheet/week/shoot/rank/list?pageNum=${page}&pageSize=100`
);
};
export const clickLikeAPI = (userId, ifLike) => {
return request("POST", "/user/score/sheet/week/shoot/rank/like", {
userId,
ifLike,
});
};
export const getMyLikeList = (page = 1, pageSize = 10) => {
return request(
"GET",
`/user/score/sheet/week/shoot/rank/like/list?pageNum=${page}&pageSize=${pageSize}`
);
};
export const getReadyAPI = (roomId) => {
return request("POST", `/user/room/ready`, {
roomId,
});
};
export const getBattleAPI = async (battleId) => {
return request("POST", "/user/match/info", {
id: battleId,
});
};
export const kickPlayerAPI = (number, userId) => {
return request("POST", "/user/room/kicking", {
number,
userId,
});
};

View File

@@ -1,6 +1,4 @@
export const audioFils = {
// 激光已校准:
// "https://static.shelingxingqiu.com/attachment/2025-10-29/ddupaur1vdkyhzaqdc.mp3",
const audioFils = {
胜利: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcuo9yjp0kt5msvmvd.mp3",
失败: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcuo9yht2sdwhuqygy.mp3",
请射箭测试距离:
@@ -8,7 +6,7 @@ export const audioFils = {
距离合格:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutwrda0amn5kqr4j.mp3",
距离不足:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6hr2faw28t0ianh0.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya57qurnsj6pg4.mp3",
轮到你了:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrn4lxcpv8aqr.mp3",
第一轮:
@@ -38,52 +36,27 @@ export const audioFils = {
射击无效:
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya55ufiiw8oo55.mp3",
未上靶:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6n45o3tsm1v4unam.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcuuznjc78ljhzuw1o.mp3",
"1环":
"https://static.shelingxingqiu.com/shootaudio/v3/1.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin1aq7gxjih5l.mp3",
"2环":
"https://static.shelingxingqiu.com/shootaudio/v3/2.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin64tdgx2s4at.mp3",
"3环":
"https://static.shelingxingqiu.com/shootaudio/v3/3.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinlmf87vt8z65.mp3",
"4环":
"https://static.shelingxingqiu.com/shootaudio/v3/4.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinniv97sx0q9u.mp3",
"5环":
"https://static.shelingxingqiu.com/shootaudio/v3/5.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin7j01kknpb7k.mp3",
"6环":
"https://static.shelingxingqiu.com/shootaudio/v3/6.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin4syy1015rtq.mp3",
"7环":
"https://static.shelingxingqiu.com/shootaudio/v3/7.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin3iz3dvmjdai.mp3",
"8环":
"https://static.shelingxingqiu.com/shootaudio/v3/8.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinnjd42lhpfiw.mp3",
"9环":
"https://static.shelingxingqiu.com/shootaudio/v3/9.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin69nj1xh7yfz.mp3",
"10环":
"https://static.shelingxingqiu.com/shootaudio/v3/10.mp3",
X环: "https://static.shelingxingqiu.com/shootaudio/v4/v4/X%E7%8E%AF.mp3",
向上调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf5pfvu3l8dhr.mp3",
向右上调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf45v88pirarr.mp3",
向右调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6elleqnhrenggxsb.mp3",
向右下调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6elleo6q16qctf6a.mp3",
向下调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellek2mu2cri2n9.mp3",
向左下调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf25yu1pt2k5r.mp3",
向左调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellen3zoalxcb06.mp3",
向左上调整:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf37a2iw6w4pu.mp3",
最后30秒:
"https://static.shelingxingqiu.com/attachment/2025-11-13/de7kzzllq0futwynso.mp3",
练习开始:
"https://static.shelingxingqiu.com/attachment/2025-11-14/de88w0lmmt43nnfmoi.mp3",
射箭声音:
"https://static.shelingxingqiu.com/shootaudio/v4/v4/%E7%AE%AD%E9%A3%9E%E8%A1%8C.mp3",
命中:
"https://static.shelingxingqiu.com/shootaudio/%E5%91%BD%E4%B8%AD.mp3"
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinnvsx0tt7ksa.mp3",
};
// 版本控制日志函数
@@ -93,7 +66,7 @@ function debugLog(...args) {
const envVersion = accountInfo.miniProgram.envVersion;
// 只在体验版打印日志,正式版(release)和开发版(develop)不打印
if (envVersion === "trial") {
if (envVersion === 'trial') {
console.log(...args);
}
}
@@ -102,12 +75,8 @@ class AudioManager {
constructor() {
this.audioMap = new Map();
this.currentPlayingKey = null;
this.retryCount = new Map();
this.maxRetries = 3;
// 多轮统一重试:最多重试的轮次与每轮间隔
this.maxRetryRounds = 10;
this.retryRoundIntervalMs = 1500;
// 显式授权播放标记,防止 iOS 在设置 src 后误播
this.allowPlayMap = new Map();
// 串行加载相关属性
this.audioKeys = [];
@@ -115,485 +84,141 @@ class AudioManager {
this.isLoading = false;
this.loadingPromise = null;
// 连续播放队列相关属性
this.sequenceQueue = [];
this.sequenceIndex = 0;
this.isSequenceRunning = false;
// 防重复播放保护
this.lastPlayKey = null;
this.lastPlayAt = 0;
// 静音开关
this.isMuted = false;
this.pendingPlayKey = null;
// 新增:就绪状态映射
this.readyMap = new Map();
// 新增:首轮失败的音频集合与重试阶段标识
this.failedLoadKeys = new Set();
// 加载代数,用于 reloadAll 时作废旧的加载循环
this.loadGeneration = 0;
// 本地路径缓存 Map: { url: localPath }
this.localFileCache = uni.getStorageSync("audio_local_files") || {};
// 启动时自动清理过期的缓存文件URL 已不在 audioFils 中的文件)
this.cleanObsoleteCache();
this.initAudios();
}
// 清理不再使用的缓存文件
cleanObsoleteCache() {
const activeUrls = new Set(Object.values(audioFils));
const cachedUrls = Object.keys(this.localFileCache);
let hasChanges = false;
for (const url of cachedUrls) {
if (!activeUrls.has(url)) {
debugLog(`发现废弃音频缓存,正在清理: ${url}`);
const path = this.localFileCache[url];
// 移除物理文件
uni.removeSavedFile({
filePath: path,
complete: () => {
// 忽略移除结果,直接移除记录
},
});
// 移除记录
delete this.localFileCache[url];
hasChanges = true;
}
}
if (hasChanges) {
uni.setStorageSync("audio_local_files", this.localFileCache);
debugLog("废弃缓存清理完成");
}
}
// 初始化音频(两阶段:首轮串行加载全部,次轮仅串行加载失败项一次)
// 初始化音频
initAudios() {
if (this.isLoading) {
debugLog("音频正在加载中,跳过重复初始化");
return this.loadingPromise;
}
debugLog("开始串行加载音频...");
this.isLoading = true;
this.audioKeys = Object.keys(audioFils);
this.currentLoadingIndex = 0;
this.failedLoadKeys.clear();
// 增加代数,使得旧的加载循环失效
this.loadGeneration = (this.loadGeneration || 0) + 1;
const currentGen = this.loadGeneration;
this.loadingPromise = new Promise((resolve) => {
const finalize = () => {
if (currentGen !== this.loadGeneration) return;
const runRounds = (round) => {
if (currentGen !== this.loadGeneration) return;
// 达到最大轮次或没有失败项,收尾
if (this.failedLoadKeys.size === 0 || round > this.maxRetryRounds) {
this.isLoading = false;
resolve();
return;
}
const retryKeys = Array.from(this.failedLoadKeys);
this.failedLoadKeys.clear();
debugLog(`开始第 ${round} 轮串行加载,共 ${retryKeys.length}`);
this.loadKeysSequentially(
retryKeys,
() => {
if (currentGen !== this.loadGeneration) return;
// 如仍有失败项,继续下一轮;否则结束
if (this.failedLoadKeys.size > 0 && round < this.maxRetryRounds) {
setTimeout(
() => runRounds(round + 1),
this.retryRoundIntervalMs
);
} else {
this.isLoading = false;
resolve();
}
},
currentGen
);
};
// 启动第 1 轮重试(如有失败项)
runRounds(1);
};
this.loadNextAudio(finalize, currentGen);
this.loadNextAudio(resolve);
});
return this.loadingPromise;
}
// 按自定义列表串行加载音频(避免并发过多)
loadKeysSequentially(keys, onComplete, gen) {
if (gen !== undefined && gen !== this.loadGeneration) return;
let idx = 0;
const list = Array.from(keys);
const next = () => {
if (gen !== undefined && gen !== this.loadGeneration) return;
if (idx >= list.length) {
if (onComplete) onComplete();
return;
}
const k = list[idx++];
// 已就绪的音频不再重载,避免把 ready 状态重置为 false
if (this.readyMap.get(k)) {
setTimeout(next, 50);
return;
}
// 未就绪:已存在则重载;不存在则创建
if (this.audioMap.has(k)) {
this.retryLoadAudio(k);
setTimeout(next, 100);
} else {
this.createAudio(k, () => {
setTimeout(next, 100);
});
return; // createAudio 内部会触发 next
}
};
next();
}
// 串行加载下一个音频(首轮)
loadNextAudio(onComplete, gen) {
if (gen !== undefined && gen !== this.loadGeneration) return;
// 串行加载下一个音频
loadNextAudio(onComplete) {
if (this.currentLoadingIndex >= this.audioKeys.length) {
debugLog("首轮加载遍历完成", this.currentLoadingIndex);
debugLog("所有音频加载完成");
this.isLoading = false;
if (onComplete) onComplete();
return;
}
const key = this.audioKeys[this.currentLoadingIndex];
debugLog(
`开始加载音频 ${this.currentLoadingIndex + 1}/${
this.audioKeys.length
}: ${key}`
);
debugLog(`开始加载音频 ${this.currentLoadingIndex + 1}/${this.audioKeys.length}: ${key}`);
this.createAudio(key, () => {
this.currentLoadingIndex++;
setTimeout(() => {
this.loadNextAudio(onComplete, gen);
this.loadNextAudio(onComplete);
}, 100);
});
}
// 创建单个音频实例(支持本地缓存)
// 创建单个音频实例
createAudio(key, callback) {
this.currentLoadingIndex++;
const src = audioFils[key];
const setupAudio = (realSrc) => {
const audio = uni.createInnerAudioContext();
audio.src = src;
audio.autoplay = false;
audio.src = realSrc;
try {
if (typeof audio.volume === "number") {
audio.volume = this.isMuted ? 0 : 1;
} else if (typeof audio.muted !== "undefined") {
audio.muted = this.isMuted;
}
} catch (_) {}
this.allowPlayMap.set(key, false);
audio.onPlay(() => {
if (!this.allowPlayMap.get(key)) {
try {
audio.stop();
} catch (_) {}
}
});
// 设置加载超时
const loadTimeout = setTimeout(() => {
debugLog(`音频 ${key} 加载超时`);
this.recordLoadFailure(key);
try {
audio.destroy();
} catch (_) {}
if (callback) callback();
}, 10000);
// 监听加载状态
audio.onCanplay(() => {
if (!this.allowPlayMap.get(key)) {
try {
audio.pause();
} catch (_) {}
}
clearTimeout(loadTimeout);
this.readyMap.set(key, true);
this.failedLoadKeys.delete(key);
// debugLog(`音频 ${key} 已加载完成`, this.getLoadProgress());
uni.$emit("audioLoaded", key);
const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {};
loadedAudioKeys[key] = true;
uni.setStorageSync("loadedAudioKeys", loadedAudioKeys);
debugLog(`音频 ${key} 已加载完成`);
this.retryCount.set(key, 0);
if (callback) callback();
});
audio.onError((res) => {
clearTimeout(loadTimeout);
debugLog(`音频 ${key} 加载失败:`, res.errMsg);
// 如果是本地文件加载失败,可能是文件损坏,清除缓存以便下次重新下载
if (realSrc !== src && this.localFileCache[src] === realSrc) {
debugLog(`本地缓存失效,移除记录: ${key}`);
delete this.localFileCache[src];
uni.setStorageSync("audio_local_files", this.localFileCache);
// 移除文件
uni.removeSavedFile({ filePath: realSrc });
}
this.recordLoadFailure(key);
this.audioMap.delete(key);
audio.destroy();
if (this.readyMap.get(key)) {
// 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载
// this.readyMap.set(key, false);
} else {
this.handleAudioError(key);
if (callback) callback();
}
});
// 监听播放结束事件
audio.onEnded(() => {
if (this.currentPlayingKey === key) {
this.currentPlayingKey = null;
}
this.allowPlayMap.set(key, false);
this.onAudioEnded(key);
uni.$emit('audioEnded', key);
});
// 监听播放停止事件
audio.onStop(() => {
if (this.currentPlayingKey === key) {
this.currentPlayingKey = null;
}
this.allowPlayMap.set(key, false);
});
this.audioMap.set(key, audio);
};
if (!this.retryCount.has(key)) {
this.retryCount.set(key, 0);
}
}
// 检查是否有可用的本地缓存
this.checkLocalFile(src).then((localPath) => {
if (localPath) {
debugLog(`命中本地缓存: ${key}`);
setupAudio(localPath);
// 处理音频加载错误
handleAudioError(key) {
const currentRetries = this.retryCount.get(key) || 0;
if (currentRetries < this.maxRetries) {
this.retryCount.set(key, currentRetries + 1);
debugLog(`音频 ${key} 开始第 ${currentRetries + 1} 次重试...`);
setTimeout(() => {
this.retryLoadAudio(key);
}, 1000);
} else {
// 下载并尝试保存
uni.downloadFile({
url: src,
timeout: 20000,
success: (res) => {
if (res.tempFilePath) {
// 尝试保存文件到本地存储(持久化)
uni.getFileSystemManager().saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
const savedPath = saveRes.savedFilePath;
this.localFileCache[src] = savedPath;
uni.setStorageSync("audio_local_files", this.localFileCache);
debugLog(`音频已缓存到本地: ${key}`);
setupAudio(savedPath);
},
fail: (err) => {
debugLog(
`保存音频失败(可能空间不足),使用临时文件: ${key}`,
err
);
setupAudio(res.tempFilePath);
},
});
} else {
this.recordLoadFailure(key);
if (callback) callback();
console.error(`音频 ${key} 重试 ${this.maxRetries} 次后仍然失败,停止重试`);
const failedAudio = this.audioMap.get(key);
if (failedAudio) {
failedAudio.destroy();
this.audioMap.delete(key);
}
},
fail: () => {
this.recordLoadFailure(key);
if (callback) callback();
},
});
}
});
}
// 检查本地文件是否有效
checkLocalFile(url) {
return new Promise((resolve) => {
const path = this.localFileCache[url];
if (!path) {
resolve(null);
return;
}
// 检查文件是否存在
uni.getFileSystemManager().getFileInfo({
filePath: path,
success: () => {
resolve(path);
},
fail: () => {
// 文件不存在,清理记录
delete this.localFileCache[url];
uni.setStorageSync("audio_local_files", this.localFileCache);
resolve(null);
},
});
});
}
// 新增:记录失败(首轮与次轮都会用到)
recordLoadFailure(key) {
this.failedLoadKeys.add(key);
}
// 重新加载音频
retryLoadAudio(key) {
const oldAudio = this.audioMap.get(key);
if (oldAudio) oldAudio.destroy();
if (oldAudio) {
oldAudio.destroy();
}
this.createAudio(key);
}
// 播放指定音频或音频数组(数组则按顺序连续播放)
play(input, interrupt = true) {
// 统一规范化为队列
let queue = [];
if (Array.isArray(input)) {
queue = input.filter((k) => !!audioFils[k]);
} else if (typeof input === "string") {
queue = !!audioFils[input] ? [input] : [];
} else {
debugLog("play 参数类型无效,仅支持字符串或字符串数组");
return;
}
if (queue.length === 0) {
debugLog("连续播放队列为空或无效");
return;
}
if (interrupt) {
// 立即打断并启动新的播放序列
this.stopAll();
this.isSequenceRunning = false;
this.sequenceQueue = [];
this.sequenceIndex = 0;
this.sequenceQueue = queue;
this.sequenceIndex = 0;
this.isSequenceRunning = true;
this._playSingle(queue[0], false);
return;
}
// 不打断当前播放:把新的队列加入到序列中,等待当前播放结束后衔接
// 播放指定音频
play(key) {
// 如果有正在播放的音频,先停止
if (this.currentPlayingKey) {
if (this.isSequenceRunning) {
// 已有序列在跑:直接追加
this.sequenceQueue = this.sequenceQueue.concat(queue);
} else {
// 没有序列但当前有正在播放的:以当前为序列的起点
this.isSequenceRunning = true;
this.sequenceQueue = [this.currentPlayingKey].concat(queue);
this.sequenceIndex = 0;
// 不触发 _playSingle等待当前音频自然结束后由 onAudioEnded 接管
}
} else {
// 当前没有播放:直接启动新的序列
this.sequenceQueue = queue;
this.sequenceIndex = 0;
this.isSequenceRunning = true;
this._playSingle(queue[0], false);
}
}
// 内部方法:播放单个 key
_playSingle(key, forceStopAll = false) {
// 200ms 内的同 key 重复播放直接忽略,避免“比比赛开始”这类重复首音
const now = Date.now();
if (this.lastPlayKey === key && now - this.lastPlayAt < 250) {
debugLog(`忽略快速重复播放: ${key}`);
return;
}
if (forceStopAll) {
this.stopAll();
} else if (this.currentPlayingKey && this.currentPlayingKey !== key) {
this.stop(this.currentPlayingKey);
} else if (this.currentPlayingKey === key) {
// 同一音频正在播放:不重启,避免听到重复开头
return;
}
const audio = this.audioMap.get(key);
if (audio) {
// 播放前确保遵循当前静音状态
try {
if (typeof audio.volume === "number") {
audio.volume = this.isMuted ? 0 : 1;
} else if (typeof audio.muted !== "undefined") {
audio.muted = this.isMuted;
}
} catch (_) {}
// 同一音频:避免 stop() 触发 onStop 清除授权,使用 pause()+seek(0)
try {
audio.pause();
} catch (_) {}
try {
if (typeof audio.seek === "function") {
audio.seek(0);
} else {
audio.startTime = 0;
}
} catch (_) {
audio.startTime = 0;
}
// 显式授权播放并立即播放
this.allowPlayMap.set(key, true);
audio.play();
this.currentPlayingKey = key;
this.lastPlayKey = key;
this.lastPlayAt = Date.now();
} else {
debugLog(`音频 ${key} 不存在,尝试重新加载...`);
this.retryLoadAudio(key);
const handler = (loadedKey) => {
if (loadedKey === key) {
try {
uni.$off("audioLoaded", handler);
} catch (_) {}
// 再次校验是否存在且就绪
const a = this.audioMap.get(key);
if (a && this.readyMap.get(key)) {
this._playSingle(key, false);
}
}
};
try {
uni.$on("audioLoaded", handler);
} catch (_) {}
}
}
// 连续播放:在某个音频结束后,若处于队列播放状态则继续下一个
onAudioEnded(key) {
if (!this.isSequenceRunning) return;
const currentKey = this.sequenceQueue[this.sequenceIndex];
if (currentKey !== key) return;
const nextIndex = this.sequenceIndex + 1;
if (nextIndex < this.sequenceQueue.length) {
this.sequenceIndex = nextIndex;
const nextKey = this.sequenceQueue[nextIndex];
this._playSingle(nextKey, false);
} else {
// 队列播放完成
this.isSequenceRunning = false;
this.sequenceQueue = [];
this.sequenceIndex = 0;
this.reloadAudio(key);
}
}
@@ -602,104 +227,19 @@ class AudioManager {
const audio = this.audioMap.get(key);
if (audio) {
audio.stop();
this.allowPlayMap.set(key, false);
if (this.currentPlayingKey === key) {
this.currentPlayingKey = null;
}
}
}
// 停止所有音频
stopAll() {
for (const [k, audio] of this.audioMap.entries()) {
try {
audio.stop();
} catch (_) {}
this.allowPlayMap.set(k, false);
// 手动重新加载指定音频
reloadAudio(key) {
if (audioFils[key]) {
debugLog(`手动重新加载音频: ${key}`);
this.retryCount.set(key, 0);
this.retryLoadAudio(key);
}
this.currentPlayingKey = null;
}
// 设置静音开关true 静音false 取消静音
setMuted(muted) {
this.isMuted = !!muted;
for (const audio of this.audioMap.values()) {
try {
if (typeof audio.volume === "number") {
audio.volume = this.isMuted ? 0 : 1;
} else if (typeof audio.muted !== "undefined") {
audio.muted = this.isMuted;
}
} catch (_) {}
}
debugLog(`静音状态已设置为: ${this.isMuted}`);
}
// 新增返回音频加载进度0~1
getLoadProgress() {
const keys = Object.keys(audioFils);
const total = keys.length;
if (total === 0) return 0;
let loaded = 0;
for (const k of keys) {
if (this.readyMap.get(k)) loaded++;
}
return Number((loaded / total).toFixed(2));
}
// 清理本地音频缓存文件
clearCache() {
debugLog("开始清理本地音频缓存...");
const cache = uni.getStorageSync("audio_local_files") || {};
const paths = Object.values(cache);
for (const path of paths) {
uni.removeSavedFile({
filePath: path,
complete: (res) => {
// 无论成功失败都继续
},
});
}
uni.removeStorageSync("audio_local_files");
this.localFileCache = {};
debugLog("本地音频缓存清理完成");
}
// 手动重置并重新加载所有音频(用于卡住时恢复)
reloadAll() {
debugLog("执行 reloadAll: 重置所有状态并重新加载");
// 1. 停止所有播放
this.stopAll();
// 2. 销毁现有音频实例
for (const audio of this.audioMap.values()) {
try {
audio.destroy();
} catch (_) {}
}
this.audioMap.clear();
// 3. 重置状态
this.readyMap.clear();
this.failedLoadKeys.clear();
this.allowPlayMap.clear();
this.currentPlayingKey = null;
this.sequenceQueue = [];
this.sequenceIndex = 0;
this.isSequenceRunning = false;
// 清理一下可能损坏的缓存(可选,如果用户因为缓存坏了卡住,这一步很有用)
// 这里选择不自动全清,而是依赖 onError 里的单点清除。如果需要彻底重置,可取消注释:
// this.clearCache();
// 4. 强制重置加载锁
this.isLoading = false;
this.loadingPromise = null;
this.currentLoadingIndex = 0;
// 5. 重新初始化 (initAudios 会自增 loadGeneration从而终止之前的任何异步循环)
return this.initAudios();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
<script setup>
import { capsuleHeight } from "@/util";
import { ref, onMounted } from "vue";
const props = defineProps({
type: {
type: Number,
@@ -11,6 +10,11 @@ const props = defineProps({
default: "#050b19",
},
});
const capsuleHeight = ref(0);
onMounted(() => {
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
capsuleHeight.value = menuBtnInfo.top + 50 - 9;
});
</script>
<template>
@@ -31,7 +35,7 @@ const props = defineProps({
class="bg-image"
v-if="type === 2"
src="../static/app-bg3.png"
:style="{ height: capsuleHeight + 50 + 'px' }"
:style="{ height: capsuleHeight + 'px' }"
/>
<image
class="bg-image"
@@ -45,12 +49,6 @@ const props = defineProps({
src="../static/app-bg5.png"
mode="widthFix"
/>
<image
class="bg-image"
v-if="type === 5"
src="https://static.shelingxingqiu.com/attachment/2026-01-05/dfgf3b5kp459tfyn0f.png"
mode="widthFix"
/>
<view class="bg-overlay" v-if="type === 0"></view>
</view>
</template>

View File

@@ -1,4 +1,5 @@
<script setup>
const tabs = [
{ image: "../static/tab-vip.png" },
{ image: "../static/tab-point-book.png" },
@@ -33,7 +34,7 @@ function handleTabClick(index) {
class="tab-item"
@click="handleTabClick(index)"
:style="{
width: index === 1 ? '36%' : '20%',
width: index === 1 ? '36%' : '10%',
}"
>
<image :src="tab.image" mode="widthFix" />
@@ -43,13 +44,13 @@ function handleTabClick(index) {
<style scoped>
.footer {
height: 120px;
height: 117px;
width: 100vw;
position: relative;
display: flex;
justify-content: space-around;
align-items: center;
overflow: hidden;
overflow-x: hidden;
}
.footer-bg {
width: 100%;
@@ -63,13 +64,10 @@ function handleTabClick(index) {
justify-content: center;
}
.tab-item > image {
width: 65rpx;
}
.tab-item:last-child > image {
width: 85rpx;
width: 86%;
}
.tab-item:nth-child(2) {
transform: translate(10%, 40%);
transform: translate(25%, 30%);
}
.tab-item:nth-child(3) {
margin-bottom: 25rpx;
@@ -78,6 +76,6 @@ function handleTabClick(index) {
width: 140rpx;
}
.tab-item:nth-child(4) {
transform: translate(-10%, 44%);
transform: translate(-25%, 30%);
}
</style>

View File

@@ -1,76 +1,50 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { getBattleAPI, getUserGameState } from "@/apis";
import { isGamingAPI, getCurrentGameAPI } from "@/apis";
import { debounce } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, game } = storeToRefs(store);
const { updateGame } = store;
const { user } = storeToRefs(store);
const props = defineProps({
signin: {
type: Function,
default: () => {},
},
});
const loading = ref(false);
const show = ref(false);
onShow(async () => {
if (user.value.id) {
setTimeout(async () => {
const state = await getUserGameState();
updateGame(state.gaming, state.roomId);
}, 1000);
const isGaming = await isGamingAPI();
show.value = isGaming;
}
});
watch(
() => user.value,
async (value) => {
if (!value.id) {
updateGame(false, "");
show.value = false;
} else {
const state = await getUserGameState();
updateGame(state.gaming, state.roomId);
const isGaming = await isGamingAPI();
show.value = isGaming;
}
}
);
const onClick = debounce(async () => {
if (loading.value) return;
try {
loading.value = true;
const result = await getBattleAPI();
if (result && result.matchId) {
await uni.$checkAudio();
if (result.mode <= 3) {
uni.navigateTo({
url: `/pages/team-battle?battleId=${result.matchId}`,
});
const isGaming = await isGamingAPI();
show.value = isGaming;
if (isGaming) {
const result = await getCurrentGameAPI();
} else {
uni.navigateTo({
url: `/pages/melee-battle?battleId=${result.matchId}`,
uni.showToast({
title: "比赛已结束",
icon: "none",
});
}
return;
}
if (game.value.roomID) {
uni.navigateTo({
url: "/pages/battle-room?roomNumber=" + game.value.roomID,
});
} else {
updateGame(false, "");
}
} finally {
loading.value = false;
}
});
const gameOver = () => {
updateGame(false, "");
show.value = false;
};
onMounted(() => {
uni.$on("game-over", gameOver);
@@ -81,19 +55,10 @@ onBeforeUnmount(() => {
</script>
<template>
<view
v-if="game.inBattle || game.roomID"
class="back-to-game"
@click="onClick"
>
<view v-if="show" class="back-to-game" @click="onClick">
<image src="../static/back-to-game-bg.png" mode="widthFix" />
<block v-if="game.inBattle">
<image src="../static/pk-icon.png" mode="widthFix" />
<text>返回进行中的对局</text>
</block>
<block v-else-if="game.roomID">
<text>返回房间</text>
</block>
<image src="../static/back.png" mode="widthFix" />
</view>
</template>
@@ -113,18 +78,16 @@ onBeforeUnmount(() => {
.back-to-game > image:first-child {
position: absolute;
width: 100%;
height: 100rpx;
}
.back-to-game > image:nth-child(2) {
position: relative;
width: 60px;
height: 60px;
}
.back-to-game > text {
.back-to-game > text:nth-child(3) {
position: relative;
font-size: 14px;
}
.back-to-game > image:last-child {
.back-to-game > image:nth-child(4) {
position: relative;
width: 15px;
margin-left: 5px;

View File

@@ -1,9 +1,8 @@
=
<script setup>
import { computed } from "vue";
import BowPower from "@/components/BowPower.vue";
import { RoundImages } from "@/constants";
const props = defineProps({
defineProps({
roundResults: {
type: Array,
default: () => [],
@@ -16,16 +15,15 @@ const props = defineProps({
type: Number,
default: 0,
},
power: {
type: Number,
default: 0,
},
goldenRound: {
type: Number,
default: 0,
},
});
const normalRounds = computed(() => {
const count = props.roundResults.findIndex((item) => !!item.ifGold);
return count > 0 ? count : props.roundResults.length;
});
</script>
<template>
@@ -38,7 +36,7 @@ const normalRounds = computed(() => {
transform: 'scale(0.8) translateX(10px)',
}"
>
<BowPower />
<BowPower :power="power" />
</view>
</view>
<view>
@@ -49,9 +47,15 @@ const normalRounds = computed(() => {
<view class="players">
<view>
<view v-for="(result, index) in roundResults" :key="index">
<block v-if="index + 1 > normalRounds">
<block
v-if="goldenRound > 0 && index >= roundResults.length - goldenRound"
>
<image
:src="RoundImages[`gold${index + 1 - normalRounds}`]"
:src="
RoundImages[
`gold${index + 1 - (roundResults.length - goldenRound)}`
]
"
mode="widthFix"
/>
</block>
@@ -60,8 +64,8 @@ const normalRounds = computed(() => {
</block>
<view>
<text>{{
result.shoots[1] && result.shoots[1].length
? result.shoots[1]
result.blueArrows.length
? result.blueArrows
.map((item) => item.ring)
.reduce((last, next) => last + next, 0)
: ""
@@ -84,9 +88,15 @@ const normalRounds = computed(() => {
</view>
<view>
<view v-for="(result, index) in roundResults" :key="index">
<block v-if="index + 1 > normalRounds">
<block
v-if="goldenRound > 0 && index >= roundResults.length - goldenRound"
>
<image
:src="RoundImages[`gold${index + 1 - normalRounds}`]"
:src="
RoundImages[
`gold${index + 1 - (roundResults.length - goldenRound)}`
]
"
mode="widthFix"
/>
</block>
@@ -95,8 +105,8 @@ const normalRounds = computed(() => {
</block>
<view>
<text>{{
result.shoots[2] && result.shoots[2].length
? result.shoots[2]
result.redArrows.length
? result.redArrows
.map((item) => item.ring)
.reduce((last, next) => last + next, 0)
: ""
@@ -125,7 +135,7 @@ const normalRounds = computed(() => {
.container {
width: 100%;
overflow: hidden;
margin-top: -100rpx;
margin-top: -40px;
}
.container > view:nth-child(2) {
position: relative;
@@ -142,11 +152,6 @@ const normalRounds = computed(() => {
.container > view:nth-child(2) > text {
z-index: 1;
margin-top: 2px;
color: #8a323e;
font-weight: 500;
}
.container > view:nth-child(2) > text:nth-child(2) {
color: #004ac1;
}
.players {
display: flex;
@@ -161,13 +166,13 @@ const normalRounds = computed(() => {
padding-top: 5px;
}
.players > view:first-child > view {
background: linear-gradient(270deg, #172a86 0%, #0000 100%);
background: linear-gradient(270deg, #172a86 0%, #0006 100%);
}
.players > view:last-child > view {
background: linear-gradient(270deg, #0000 0%, #6a1212 100%);
background: linear-gradient(270deg, #0006 0%, #6a1212 100%);
}
.players > view > view {
min-height: 52rpx;
min-height: 25px;
width: calc(100% - 40px);
padding: 2px 20px;
margin-bottom: 5px;
@@ -176,7 +181,7 @@ const normalRounds = computed(() => {
align-items: center;
}
.players > view > view > image:first-child {
width: 135rpx;
width: 72px;
height: 20px;
}
.players > view > view > view:last-child {
@@ -186,7 +191,6 @@ const normalRounds = computed(() => {
font-size: 16px;
color: #fed847;
margin-right: 5px;
font-weight: 500;
}
.guide-row {
display: flex;
@@ -198,6 +202,6 @@ const normalRounds = computed(() => {
position: relative;
}
.guide-row > image {
width: 140rpx;
width: 18%;
}
</style>

View File

@@ -73,7 +73,7 @@ defineProps({
<text class="player-name">{{ player.name }}</text>
</view>
<image
v-if="winner === 2"
v-if="winner === 0"
src="../static/winner-badge.png"
mode="widthFix"
class="right-winner-badge"

View File

@@ -21,10 +21,6 @@ const props = defineProps({
type: Array,
default: () => [],
},
total: {
type: Number,
default: 0,
},
});
</script>
@@ -49,16 +45,16 @@ const props = defineProps({
<view class="desc">
<text>{{ arrows.length }}</text>
<text>支箭</text>
<text>{{ arrows.reduce((a, b) => a + (b.ring || 0), 0) }}</text>
<text>{{ arrows.reduce((a, b) => a + b.ring, 0) }}</text>
<text></text>
</view>
<ScorePanel
:completeEffect="false"
:rowCount="total === 12 ? 6 : 9"
:total="total"
:arrows="arrows"
:margin="total === 12 ? 4 : 1"
:fontSize="total === 12 ? 25 : 22"
:rowCount="arrows.length === 12 ? 6 : 9"
:total="arrows.length"
:scores="arrows.map((a) => a.ring)"
:margin="arrows.length === 12 ? 4 : 1"
:fontSize="arrows.length === 12 ? 25 : 22"
/>
</view>
</template>

View File

@@ -1,28 +1,16 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { getDeviceBatteryAPI } from "@/apis";
const power = ref(0);
const timer = ref(null);
onMounted(async () => {
const data = await getDeviceBatteryAPI();
power.value = data.battery;
timer.value = setInterval(async () => {
const data = await getDeviceBatteryAPI();
power.value = data.battery;
}, 1000 * 10);
});
onBeforeUnmount(() => {
clearInterval(timer.value);
defineProps({
power: {
type: Number,
default: 0,
},
});
</script>
<template>
<view class="container">
<view class="container" :style="{ opacity: power > 0 ? 1 : 0 }">
<image src="../static/b-power.png" mode="widthFix" />
<view>电量{{ power || 1 }}%</view>
<view>电量{{ power }}%</view>
</view>
</template>

View File

@@ -1,8 +1,7 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
import PointSwitcher from "@/components/PointSwitcher.vue";
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
import { ref, watch, onMounted } from "vue";
import BowPower from "@/components/BowPower.vue";
import StartCountdown from "@/components/StartCountdown.vue";
import { simulShootAPI } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
@@ -18,6 +17,14 @@ const props = defineProps({
type: Number,
default: 0,
},
avatar: {
type: String,
default: "",
},
power: {
type: Number,
default: 0,
},
scores: {
type: Array,
default: () => [],
@@ -30,21 +37,22 @@ const props = defineProps({
type: String,
default: "solo", // solo 单排team 双排
},
// start: {
// type: Boolean,
// default: false,
// },
stop: {
type: Boolean,
default: false,
},
});
const pMode = ref(true);
const showsimul = ref(false);
const latestOne = ref(null);
const bluelatestOne = ref(null);
const prevScores = ref([]);
const prevBlueScores = ref([]);
const timer = ref(null);
const dirTimer = ref(null);
const angle = ref(null);
const circleColor = ref("");
watch(
() => props.scores,
@@ -92,82 +100,30 @@ const simulShoot = async () => {
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
};
const simulShoot2 = async () => {
if (device.value.deviceId) {
const r1 = Math.random() > 0.5 ? 0.01 : 0.02;
await simulShootAPI(device.value.deviceId, r1, r1);
}
if (device.value.deviceId) await simulShootAPI(device.value.deviceId, 1, 1);
};
const env = computed(() => {
const accountInfo = uni.getAccountInfoSync();
return accountInfo.miniProgram.envVersion;
});
const arrowStyle = computed(() => {
return {
transform: `rotateX(180deg) translate(-50%, -50%) rotate(${
360 - angle.value
}deg) translateY(105%)`,
};
});
async function onReceiveMessage(message) {
if (Array.isArray(message)) return;
if (message.type === MESSAGETYPESV2.ShootResult && message.shootData) {
if (
message.shootData.playerId === user.value.id &&
!message.shootData.ring &&
message.shootData.angle >= 0
) {
angle.value = null;
setTimeout(() => {
if (props.scores[0]) {
circleColor.value =
message.shootData.playerId === props.scores[0].playerId
? "#ff4444"
: "#1840FF";
}
angle.value = message.shootData.angle;
}, 200);
}
}
}
onMounted(() => {
uni.$on("socket-inbox", onReceiveMessage);
});
onBeforeUnmount(() => {
if (timer.value) {
clearTimeout(timer.value);
timer.value = null;
}
if (dirTimer.value) {
clearTimeout(dirTimer.value);
dirTimer.value = null;
}
uni.$off("socket-inbox", onReceiveMessage);
const accountInfo = uni.getAccountInfoSync();
const envVersion = accountInfo.miniProgram.envVersion;
if (envVersion !== "release") showsimul.value = true;
});
</script>
<template>
<view class="container">
<view class="header" v-if="totalRound > 0">
<view class="header" v-if="totalRound > 0 || power">
<text v-if="totalRound > 0" class="round-count">{{
(currentRound > totalRound ? totalRound : currentRound) +
"/" +
totalRound
}}</text>
<BowPower :power="power" />
</view>
<view class="target">
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
<view :style="{ background: circleColor }">
<image src="../static/dot-circle.png" mode="widthFix" />
</view>
</view>
<view v-if="stop" class="stop-sign">中场休息</view>
<view
v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
v-if="latestOne && user.id === latestOne.playerId"
class="e-value fade-in-out"
:style="{
left: calcRealX(latestOne.ring ? latestOne.x : 0, 20),
@@ -183,74 +139,57 @@ onBeforeUnmount(() => {
left: calcRealX(latestOne.ring ? latestOne.x : 0, 28),
top: calcRealY(latestOne.ring ? latestOne.y : 0, 28),
}"
>{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
}}<text v-if="latestOne.ring"></text>
</view>
<view
v-if="
bluelatestOne &&
bluelatestOne.ring &&
user.id === bluelatestOne.playerId
"
class="e-value fade-in-out"
:style="{
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 20),
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 40),
}"
>
经验 +1
</view>
<view
v-if="bluelatestOne"
class="round-tip fade-in-out"
:style="{
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 28),
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 28),
}"
>{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
}}<text v-if="bluelatestOne.ring">环</text></view
>{{ latestOne.ring || "未上靶"
}}<text v-if="latestOne.ring"></text></view
>
<block v-for="(bow, index) in scores" :key="index">
<view
v-if="bow.ring > 0"
:class="`hit ${pMode ? 'b' : 's'}-point ${
:class="`hit ${
index === scores.length - 1 && latestOne ? 'pump-in' : ''
}`"
:style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
left: calcRealX(bow.x),
top: calcRealY(bow.y),
backgroundColor:
index === scores.length - 1 &&
!blueScores.length &&
latestOne &&
mode !== 'team'
? 'green'
: '#ff4444',
}"
><text v-if="pMode">{{ index + 1 }}</text></view
><text>{{ index + 1 }}</text></view
>
</block>
<block v-for="(bow, index) in blueScores" :key="index">
<view
v-if="bow.ring > 0"
:class="`hit ${pMode ? 'b' : 's'}-point ${
:class="`hit ${
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
}`"
:style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
backgroundColor: '#1840FF',
left: calcRealX(bow.x),
top: calcRealY(bow.y),
backgroundColor: 'blue',
}"
>
<text v-if="pMode">{{ index + 1 }}</text>
<text>{{ index + 1 }}</text>
</view>
</block>
<image src="../static/bow-target.png" mode="widthFix" />
</view>
<view class="footer">
<PointSwitcher
:onChange="(val) => (pMode = val)"
:style="{ zIndex: 999 }"
/>
<view v-if="avatar" class="footer">
<image :src="avatar" mode="widthFix" />
</view>
<view class="simul" v-if="env !== 'release'">
<view class="simul" v-if="showsimul">
<button @click="simulShoot">模拟</button>
<button @click="simulShoot2">射箭</button>
</view>
<!-- <text :style="{ color: '#fff', wordBreak: 'break-all' }">{{
scores.length ? scores[scores.length - 1] : ""
}}</text> -->
<!-- <StartCountdown :start="startCount" /> -->
</view>
</template>
@@ -266,10 +205,11 @@ onBeforeUnmount(() => {
margin: 10px;
width: calc(100% - 20px);
height: calc(100% - 20px);
z-index: -1;
}
.e-value {
position: absolute;
/* top: 30%;
left: 60%; */
background-color: #0006;
color: #fff;
font-size: 12px;
@@ -281,6 +221,8 @@ onBeforeUnmount(() => {
}
.round-tip {
position: absolute;
/* top: 38%; */
/* left: 60%; */
color: #fff;
font-size: 30px;
font-weight: bold;
@@ -298,39 +240,28 @@ onBeforeUnmount(() => {
}
.hit {
position: absolute;
width: 3.4%;
height: 3.4%;
min-width: 3.4%;
min-height: 3.4%;
border-radius: 50%;
z-index: 1;
color: #fff;
transition: all 0.3s ease;
}
.s-point {
width: 4px;
height: 4px;
min-width: 4px;
min-height: 4px;
}
.b-point {
width: 10px;
height: 10px;
min-width: 10px;
min-height: 10px;
border: 1px solid #fff;
z-index: 1;
color: #fff;
font-size: 2.1vw;
box-sizing: border-box;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
/* transform: translate(-50%, -50%); */
}
.b-point > text {
font-size: 16rpx;
color: #fff;
font-family: "DINCondensed";
/* text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);*/
margin-top: 2rpx;
.hit > text {
transform: scaleX(0.7) translateY(-0.5px);
display: block;
font-weight: bold;
width: 100%;
text-align: center;
}
.header {
width: 100%;
@@ -354,7 +285,6 @@ onBeforeUnmount(() => {
padding: 0 10px;
display: flex;
margin-top: -40px;
justify-content: flex-end;
}
.footer > image {
width: 40px;
@@ -365,10 +295,9 @@ onBeforeUnmount(() => {
}
.simul {
position: absolute;
top: 0;
bottom: 40px;
right: 20px;
margin-left: 20px;
z-index: 999;
}
.simul > button {
color: #fff;
@@ -385,72 +314,4 @@ onBeforeUnmount(() => {
z-index: 99;
font-weight: bold;
}
.arrow-dir {
position: absolute;
width: 100%;
height: 52%;
left: 50%;
bottom: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.arrow-dir > view {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
}
.arrow-dir > view > image {
width: 100rpx;
height: 100rpx;
transform: translate(-30%, -30%);
}
@keyframes spring-in {
0% {
transform: scale(2);
opacity: 0.4;
}
15% {
transform: scale(3);
opacity: 1;
}
30% {
transform: scale(2);
opacity: 0.4;
}
45% {
transform: scale(3);
opacity: 1;
}
60% {
transform: scale(2);
opacity: 0.4;
}
75% {
transform: scale(3);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@keyframes disappear {
0% {
opacity: 1;
}
75% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.arrow-dir > view {
animation: disappear 3s ease forwards;
}
.arrow-dir > view > image {
animation: spring-in 3s ease forwards;
width: 100%;
}
</style>

View File

@@ -1,111 +0,0 @@
<script setup>
import { ref, computed } from "vue";
const props = defineProps({
diameter: {
type: Number,
default: 90,
},
});
const side = computed(() => {
return props.diameter / 10;
});
const rings = ["X", "10", "9", "8", "7", "6"];
</script>
<template>
<view
class="container circle"
:style="{
width: diameter + 'vw',
height: diameter + 'vw',
background: '#00BAE9',
}"
>
<view
class="circle"
:style="{
background: '#FF5665',
width: side * 8 + 'vw',
height: side * 8 + 'vw',
}"
>
<view class="rings" :style="{ transform: `translateX(-${side}vw)` }">
<text
v-for="(ring, index) in rings"
:key="ring"
:style="{
width: side + 'vw',
transform: `translateX(${
index === 0 ? side / 2 : index === 1 ? side / 4.5 : 0
}vw)`,
}"
>{{ ring }}</text
>
</view>
<view
class="circle"
:style="{
background: '#FF5665',
width: side * 6 + 'vw',
height: side * 6 + 'vw',
}"
>
<view
class="circle"
:style="{
background: '#FDDC61',
width: side * 4 + 'vw',
height: side * 4 + 'vw',
}"
>
<view
class="circle"
:style="{
background: '#FDDC61',
width: side * 2 + 'vw',
height: side * 2 + 'vw',
}"
>
<view
class="circle"
:style="{
background: '#FDDC61',
width: side + 'vw',
height: side + 'vw',
}"
>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<style scoped>
.container {
position: relative;
}
.rings {
position: absolute;
display: flex;
align-items: center;
left: 50%;
}
.rings > text {
font-size: 24rpx;
color: #333;
text-align: center;
}
.circle {
border: 1rpx solid #3e3e3e66;
box-sizing: border-box;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { getElementRect, calcRing, capsuleHeight } from "@/util";
import { ref, onMounted } from "vue";
import { getElementRect, calcRing } from "@/util";
const props = defineProps({
id: {
@@ -19,15 +19,18 @@ const props = defineProps({
type: Function,
default: null,
},
editMode: {
type: Boolean,
default: true,
},
});
const rect = ref({});
const arrow = ref(null);
const isDragging = ref(false);
const dragStartPos = ref({ x: 0, y: 0 });
const capsuleHeight = ref(0);
const scale = ref(1);
const scrollTop = ref(0);
const selected = ref(null);
let lastMoveTime = 0;
// 点击靶纸创建新的点
@@ -40,33 +43,19 @@ const onClick = async (e) => {
return;
}
if (props.id === 7 || props.id === 9) {
if (
e.detail.x < rect.value.width * 0.2 ||
e.detail.x > rect.value.width * 0.8
)
return;
// 放大并通过滚动将点击位置置于视窗中心
scale.value = 1.4;
const viewportH = rect.value.width; // 容器高度等于宽度100vw
const contentH = scale.value * rect.value.width; // 内容高度
const clickYInContainer = e.detail.y - rect.value.top;
let target = clickYInContainer * scale.value - viewportH / 2;
target = Math.max(0, Math.min(contentH - viewportH, target));
setTimeout(() => {
scrollTop.value = target > 180 ? target + 10 : target;
}, 200);
scale.value = 1.5;
}
const newArrow = {
x: (e.detail.x - 6) * scale.value,
y: (e.detail.y - rect.value.top - capsuleHeight - 6) * scale.value,
y: (e.detail.y - rect.value.top - capsuleHeight.value - 6) * scale.value,
};
const side = rect.value.width;
newArrow.ring = calcRing(
props.id,
newArrow.x / scale.value - side * 0.05,
newArrow.y / scale.value - side * 0.05,
side * 0.9
newArrow.x / scale.value - rect.value.width * 0.05,
newArrow.y / scale.value - rect.value.width * 0.05,
rect.value.width * 0.9
);
arrow.value = {
...newArrow,
@@ -86,14 +75,12 @@ const confirmAdd = () => {
}
arrow.value = null;
scale.value = 1;
scrollTop.value = 0;
};
// 删除箭矢
const deleteArrow = () => {
arrow.value = null;
scale.value = 1;
scrollTop.value = 0;
};
// 开始拖拽 - 同样修复坐标获取
@@ -128,9 +115,9 @@ const onDrag = async (e) => {
);
arrow.value.ring = calcRing(
props.id,
arrow.value.x / scale.value - side * 0.05,
arrow.value.y / scale.value - side * 0.05,
side * 0.9
arrow.value.x / scale.value - rect.value.width * 0.05,
arrow.value.y / scale.value - rect.value.width * 0.05,
rect.value.width * 0.9
);
arrow.value.x = arrow.value.x / side;
@@ -147,63 +134,27 @@ const endDrag = (e) => {
const getNewPos = () => {
if (props.id === 7 || props.id === 9) {
if (arrow.value.y >= 1.33)
if (arrow.value.y > 1.4)
return { left: "-12px", bottom: "calc(50% - 12px)" };
} else {
if (arrow.value.y > 0.88) {
if (arrow.value.x < 0.05) {
return { left: "calc(100% - 12px)", bottom: "calc(100% - 12px)" };
}
return { left: "-12px", bottom: "calc(50% - 12px)" };
}
}
return { left: "calc(50% - 12px)", bottom: "-12px" };
};
const setEditArrow = (data) => {
selected.value = data;
// if (data === null) {
// arrow.value = null;
// scale.value = 1;
// scrollTop.value = 0;
// return;
// }
// if (props.id === 7 || props.id === 9) {
// scale.value = 1.4;
// const viewportH = rect.value.width; // 容器高度等于宽度100vw
// const contentH = scale.value * rect.value.width; // 内容高度
// const clickYInContainer = contentH * data.y - rect.value.top;
// let target = clickYInContainer * scale.value - viewportH / 2;
// target = Math.max(0, Math.min(contentH - viewportH, target));
// setTimeout(() => {
// scrollTop.value = target > 180 ? target + 10 : target;
// }, 200);
// }
// arrow.value = {
// ...data,
// x: data.x * scale.value,
// y: data.y * scale.value,
// };
};
onMounted(async () => {
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
capsuleHeight.value = menuBtnInfo.top - 9;
const result = await getElementRect(".container");
rect.value = result;
uni.$on("set-edit-arrow", setEditArrow);
});
onBeforeUnmount(() => {
uni.$off("set-edit-arrow", setEditArrow);
});
</script>
<template>
<scroll-view
:scroll-y="scale > 1"
scroll-with-animation
:scroll-top="scrollTop"
:show-scrollbar="false"
:enhanced="true"
<view
:style="{ overflowY: editMode ? 'auto' : 'hidden' }"
class="container"
@tap="onClick"
@touchmove="onDrag"
@@ -214,16 +165,16 @@ onBeforeUnmount(() => {
:style="{
width: scale * 100 + 'vw',
height: scale * 100 + 'vw',
transform: `translateX(${(100 - scale * 100) / 2}vw)`,
transform: `translate(${(100 - scale * 100) / 2}vw,${
(100 - scale * 100) / 2
}vw) translateY(${scale > 1 ? 16.7 : 0}%)`,
}"
>
<image :src="src" mode="widthFix" />
<view
v-for="(arrow, index) in arrows"
:key="index"
:class="`arrow-point ${
selected !== null && index === selected ? 'selected-arrow-point' : ''
}`"
class="arrow-point"
:style="{
left: (arrow.x !== undefined ? arrow.x : 0) * 100 + '%',
top: (arrow.y !== undefined ? arrow.y : 0) * 100 + '%',
@@ -232,6 +183,9 @@ onBeforeUnmount(() => {
<view
v-if="arrow.x !== undefined && arrow.y !== undefined"
class="point"
:style="{
transform: props.id === 7 || props.id === 9 ? 'scale(0.7)' : '',
}"
>
<text>{{ index + 1 }}</text>
</view>
@@ -245,10 +199,7 @@ onBeforeUnmount(() => {
:x="arrow ? rect.width * arrow.x : 0"
:y="arrow ? rect.width * arrow.y : 0"
>
<view
class="point"
:style="{ minWidth: 10 * scale + 'px', minHeight: 10 * scale + 'px' }"
>
<view class="point"> </view>
<view v-if="arrow" class="edit-buttons" @touchstart.stop>
<view class="edit-btn-text">
<text>{{ arrow.ring === 0 ? "M" : arrow.ring }}</text>
@@ -258,7 +209,7 @@ onBeforeUnmount(() => {
fontSize: '16px',
marginLeft: '2px',
}"
>环</text
>points</text
>
</view>
<view
@@ -271,18 +222,13 @@ onBeforeUnmount(() => {
<view class="edit-btn delete-btn" @touchstart.stop="deleteArrow">
<image src="../static/arrow-edit-delete.png" mode="widthFix" />
</view>
<view
class="edit-btn drag-btn"
@touchstart.stop="startDrag($event)"
>
<view class="edit-btn drag-btn" @touchstart.stop="startDrag($event)">
<image src="../static/arrow-edit-move.png" mode="widthFix" />
</view>
</view>
</view>
</movable-view>
<!-- <view class="test-view"></view> -->
</movable-area>
</scroll-view>
</view>
</template>
<style scoped>
@@ -322,35 +268,31 @@ onBeforeUnmount(() => {
.arrow-point {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
.point {
min-width: 10px;
min-height: 10px;
min-width: 12px;
min-height: 12px;
border-radius: 50%;
border: 1px solid #fff;
color: #fff;
font-size: 8px;
text-align: center;
line-height: 10px;
box-sizing: border-box;
background-color: #00bf04;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
background-color: #ff4444;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
transition: all 0.1s linear;
position: relative;
transform: translate(-50%, -50%);
}
.point > text {
transform: scaleX(0.7);
display: block;
font-size: 16rpx;
line-height: 10px;
position: absolute;
top: 50%;
left: 50%;
font-family: "DINCondensed", "PingFang SC", "Helvetica Neue", Arial,
sans-serif;
transform: translate(-50%, -50%);
margin-top: 1px;
font-weight: bold;
}
.edit-buttons {
@@ -369,6 +311,7 @@ onBeforeUnmount(() => {
width: 100%;
display: flex;
justify-content: center;
/* margin-left: 10px; */
}
.edit-btn-text > text {
@@ -406,31 +349,4 @@ onBeforeUnmount(() => {
right: -12px;
bottom: -12px;
}
.test-view {
position: absolute;
top: 29px;
left: 138px;
width: 115px;
height: 115px;
background-color: #ff000055;
}
.selected-arrow-point .point {
background: linear-gradient(180deg, #ffdaa6 0%, #e9a333 100%) !important;
box-shadow: 0rpx 2rpx 4rpx 0rpx rgba(0, 0, 0, 0.18);
animation: duang 0.35s ease-out;
}
@keyframes duang {
0% {
transform: translate(-50%, -50%) scale(0.7);
}
45% {
transform: translate(-50%, -50%) scale(1.4);
}
70% {
transform: translate(-50%, -50%) scale(0.9);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}
</style>

View File

@@ -26,14 +26,14 @@ const props = defineProps({
background-size: contain;
background-repeat: no-repeat;
background-position: center;
font-size: 24rpx;
font-size: 13px;
}
.normal {
background-image: url("../static/bubble-tip.png");
width: 157rpx;
width: 190rpx;
height: 105rpx;
padding-top: 10px;
padding-left: 30rpx;
padding-top: 5px;
padding-left: 49rpx;
}
.normal2 {
background-image: url("../static/bubble-tip4.png");

View File

@@ -1,13 +1,12 @@
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { ref, onMounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import AppBackground from "@/components/AppBackground.vue";
import Header from "@/components/Header.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import BackToGame from "@/components/BackToGame.vue";
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
import { capsuleHeight, debounce } from "@/util";
import AudioManager from "@/audioManager";
import { getCurrentGameAPI } from "@/apis";
import { debounce } from "@/util";
const props = defineProps({
title: {
type: String,
@@ -21,9 +20,9 @@ const props = defineProps({
type: Function,
default: null,
},
scroll: {
type: Boolean,
default: true,
overflow: {
type: String,
default: "auto",
},
isHome: {
type: Boolean,
@@ -41,18 +40,11 @@ const props = defineProps({
type: Boolean,
default: true,
},
showBottom: {
type: Boolean,
default: true,
},
});
const isIOS = uni.getDeviceInfo().osName === "ios";
const showHint = ref(false);
const hintType = ref(0);
const capsuleHeight = ref(0);
const isLoading = ref(false);
const audioInitProgress = ref(1);
const audioProgress = ref(0);
const audioTimer = ref(null);
const showGlobalHint = (type) => {
hintType.value = type;
@@ -63,46 +55,14 @@ const hideGlobalHint = () => {
showHint.value = false;
};
const restart = () => {
uni.restartMiniProgram({
path: "/pages/index",
});
};
const checkAudioProgress = async () => {
return new Promise((resolve, reject) => {
try {
audioInitProgress.value = AudioManager.getLoadProgress();
if (audioInitProgress.value === 1) return resolve();
audioTimer.value = setInterval(() => {
audioProgress.value = AudioManager.getLoadProgress();
if (audioProgress.value === 1) {
setTimeout(() => {
audioInitProgress.value = 1;
}, 200);
clearInterval(audioTimer.value);
resolve();
}
}, 200);
} catch (err) {
reject(err);
}
});
};
const audioFinalProgress = computed(() => {
const left = 1 - audioInitProgress.value;
return Math.max(0, (audioProgress.value - audioInitProgress.value) / left);
});
onBeforeUnmount(() => {
if (audioTimer.value) clearInterval(audioTimer.value);
onMounted(() => {
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
capsuleHeight.value = menuBtnInfo.top - 9;
});
onShow(() => {
uni.$showHint = showGlobalHint;
uni.$hideHint = hideGlobalHint;
uni.$checkAudio = checkAudioProgress;
showHint.value = false;
});
@@ -111,21 +71,37 @@ const backToGame = debounce(async () => {
try {
isLoading.value = true;
const result = await getBattleAPI();
if (result && result.matchId) {
await checkAudioProgress();
if (result.mode <= 3) {
// 设置请求超时
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), 10000); // 10秒超时
});
const result = await Promise.race([
getCurrentGameAPI(),
timeoutPromise
]);
// 处理返回结果
if (result && result.gameId) {
// 跳转到游戏页面
uni.navigateTo({
url: `/pages/team-battle?battleId=${result.matchId}`,
url: `/pages/battle-room?gameId=${result.gameId}`
});
} else {
uni.navigateTo({
url: `/pages/melee-battle?battleId=${result.matchId}`,
uni.showToast({
title: '没有进行中的对局',
icon: 'none'
});
}
}
showHint.value = false;
} catch (error) {
console.error("获取当前游戏失败:", error);
console.error('获取当前游戏失败:', error);
uni.showToast({
title: error.message || '网络请求失败,请重试',
icon: 'none'
});
} finally {
isLoading.value = false;
}
@@ -134,17 +110,6 @@ const backToGame = debounce(async () => {
const goBack = () => {
uni.navigateBack();
};
const cancelMatching = async () => {
uni.$emit("cancelMatching");
}
const goCalibration = async () => {
await laserAimAPI();
uni.navigateTo({
url: "/pages/calibration",
});
};
</script>
<template>
@@ -157,25 +122,14 @@ const goCalibration = async () => {
:whiteBackArrow="whiteBackArrow"
/>
<BackToGame v-if="showBackToGame" />
<scroll-view
:scroll-y="scroll"
:enhanced="true"
:bounces="false"
:show-scrollbar="false"
<view
class="content"
:style="{
height: `calc(100vh - ${capsuleHeight + (isHome ? 0 : 50)}px - ${
$slots.bottom && showBottom ? (isIOS ? '75px' : '65px') : '0px'
})`,
height: `calc(100vh - ${capsuleHeight + (isHome ? 0 : 50)}px)`,
overflow,
}"
>
<slot></slot>
</scroll-view>
<view
class="bottom-part"
v-if="$slots.bottom && showBottom"
:style="{ height: isIOS ? '65px' : '55px', paddingTop: '10px' }"
>
<slot name="bottom"></slot>
</view>
<ScreenHint :show="showHint">
<view v-if="hintType === 1" class="tip-content">
@@ -185,8 +139,12 @@ const goCalibration = async () => {
<button hover-class="none" @click="() => (showHint = false)">
不进入
</button>
<button hover-class="none" @click="backToGame" :disabled="isLoading">
{{ isLoading ? "加载中..." : "进入" }}
<button
hover-class="none"
@click="backToGame"
:disabled="isLoading"
>
{{ isLoading ? '加载中...' : '进入' }}
</button>
</view>
</view>
@@ -206,41 +164,24 @@ const goCalibration = async () => {
<button hover-class="none" @click="() => (showHint = false)">
取消
</button>
<button hover-class="none" @click="cancelMatching">确认</button>
</view>
</view>
<view v-if="hintType === 4" class="tip-content">
<text>完成智能弓校准即可解锁全部功能</text>
<view>
<button hover-class="none" @click="() => (showHint = false)">
取消
</button>
<button hover-class="none" @click="goCalibration">去校准</button>
<button hover-class="none" @click="goBack">确认</button>
</view>
</view>
</ScreenHint>
<view v-if="audioInitProgress < 1" class="audio-progress">
<image
src="https://static.shelingxingqiu.com/attachment/2025-11-26/deihtj15xjwcz3c1tx.png"
mode="widthFix"
/>
<view>
<view :style="{ width: `${audioFinalProgress * 100}%` }">
<!-- <image
src="https://static.shelingxingqiu.com/attachment/2025-11-24/degu91a7si77sg9jqv.png"
mode="widthFix"
/> -->
</view>
</view>
<view>
<text>若加载时间过长</text>
<button hover-class="none" @click="restart">点击这里重启</button>
</view>
</view>
</view>
</template>
<style scoped>
.content {
width: 100vw;
height: 100vh;
overflow-x: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
overflow-x: hidden;
}
.tip-content {
flex-direction: column;
display: flex;
@@ -278,62 +219,4 @@ const goCalibration = async () => {
color: #666;
opacity: 0.6;
}
.audio-progress {
z-index: 999;
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background: rgb(0 0 0 / 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.audio-progress > image:nth-child(1) {
width: 140rpx;
height: 150rpx;
margin-bottom: 20rpx;
}
.audio-progress > view:nth-child(2) {
width: 380rpx;
height: 6rpx;
background: #595959;
border-radius: 4rpx;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.audio-progress > view:nth-child(2) > view {
background: #ffe431;
min-height: 6rpx;
border-radius: 4rpx;
display: flex;
align-items: center;
justify-content: flex-end;
transition: width 0.5s ease;
}
.audio-progress > view:nth-child(2) > view > image {
width: 46rpx;
height: 26rpx;
}
.audio-progress > view:nth-child(3) {
display: flex;
align-items: center;
justify-content: center;
}
.audio-progress > view:nth-child(3) > text {
font-size: 22rpx;
color: #a2a2a2;
text-align: center;
line-height: 32rpx;
}
.audio-progress > view:nth-child(3) > button {
font-size: 22rpx;
color: #ffe431;
line-height: 32rpx;
padding: 20rpx 0;
}
</style>

View File

@@ -1,14 +1,7 @@
<script setup>
import { ref } from "vue";
import SButton from "@/components/SButton.vue";
import { joinRoomAPI, createRoomAPI } from "@/apis";
import { debounce } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, game } = storeToRefs(store);
import { joinRoomAPI, createRoomAPI, isGamingAPI } from "@/apis";
const props = defineProps({
onConfirm: {
@@ -18,12 +11,13 @@ const props = defineProps({
});
const battleMode = ref(1);
const targetMode = ref(1);
const step = ref(1);
const loading = ref(false);
const roomNumber = ref("");
const createRoom = debounce(async () => {
if (game.value.inBattle) {
const createRoom = async () => {
const isGaming = await isGamingAPI();
if (isGaming) {
uni.$showHint(1);
return;
}
@@ -33,80 +27,80 @@ const createRoom = debounce(async () => {
if (battleMode.value === 2) size = 10;
if (battleMode.value === 3) size = 4;
if (battleMode.value === 4) size = 6;
try {
const result = await createRoomAPI(
battleMode.value === 2 ? 2 : 1,
battleMode.value === 2 ? 10 : size,
targetMode.value*20,
battleMode.value === 2 ? 10 : size
);
if (result.number) {
props.onConfirm();
await joinRoomAPI(result.number);
uni.navigateTo({
url: "/pages/battle-room?roomNumber=" + result.number + "&target=" + targetMode.value,
});
}
} catch (error) {
console.log(error);
} finally {
if (result.number) roomNumber.value = result.number;
step.value = 2;
loading.value = false;
}
};
const enterRoom = async () => {
step.value = 1;
props.onConfirm();
await joinRoomAPI(roomNumber.value);
uni.navigateTo({
url: `/pages/battle-room?roomNumber=${roomNumber.value}`,
});
};
const setClipboardData = () => {
uni.setClipboardData({
data: roomNumber.value,
success() {
uni.showToast({ title: "复制成功" });
},
});
};
</script>
<template>
<view class="container">
<view class="target-options-header">
<view class="target-options-header-line-left"></view>
<image class="target-options-header-title-img" src="../static/choose-battle-mode.png" mode="widthFix" />
<view class="target-options-header-line-right"></view>
</view>
<view class="create-options">
<image
v-if="step === 1"
src="../static/choose-battle-mode.png"
mode="widthFix"
/>
<view v-if="step === 1" class="create-options">
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 1 }"
@click="() => (battleMode = 1)"
>
<text>对抗模式1V1</text>
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 2 }"
@click="() => (battleMode = 2)"
>
<text>乱斗模式3-10</text>
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 3 }"
@click="() => (battleMode = 3)"
>
<text>对抗模式2V2</text>
<!-- <text>敬请期待</text> -->
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 4 }"
@click="() => (battleMode = 4)"
>
<text>对抗模式3V3</text>
<!-- <text>敬请期待</text> -->
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 2 }"
@click="() => (battleMode = 2)"
</view>
<SButton v-if="step === 1" :onClick="createRoom">下一步</SButton>
<view v-if="step === 2" class="room-info">
<view>
<text>房间号</text>
<text>{{ roomNumber }}</text>
</view>
<view class="copy-room-number" @click="setClipboardData"
>复制房间信息邀请朋友进入</view
>
<text>乱斗模式3-10</text>
<SButton width="70vw" :onClick="enterRoom">进入房间</SButton>
<text>30分钟无人进入则房间无效</text>
</view>
</view>
<view class="target-options-header">
<view class="target-options-header-line-left"></view>
<view class="target-options-header-title">选择靶纸</view>
<view class="target-options-header-line-right"></view>
</view>
<view class="target-options">
<view
:class="{ 'battle-btn': true, 'battle-choosen': targetMode === 1 }"
@click="() => (targetMode = 1)"
>
<text>20厘米全环靶</text>
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': targetMode === 2 }"
@click="() => (targetMode = 2)"
>
<text>40厘米全环靶</text>
</view>
</view>
<SButton :onClick="createRoom">创建房间</SButton>
</view>
</template>
<style scoped>
@@ -116,7 +110,6 @@ const createRoom = debounce(async () => {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 44rpx;
}
.container > image:first-child {
width: 45%;
@@ -131,50 +124,6 @@ const createRoom = debounce(async () => {
justify-content: center;
margin-bottom: 15px;
}
.target-options-header{
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
}
.target-options-header-title-img{
width: 196rpx;
height: 40rpx;
}
.target-options-header-title{
width: 112rpx;
height: 40rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 28rpx;
text-align: center;
font-style: normal;
text-transform: none;
color: #FFEFBA;
margin: 0 18rpx;
}
.target-options-header-line-left{
width: 214rpx;
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 1rpx solid;
border-image: linear-gradient(90deg, rgba(133, 119, 96, 0), rgba(133, 119, 96, 1)) 1 1;
}
.target-options-header-line-right{
width: 214rpx;
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 1rpx solid;
border-image: linear-gradient(90deg, rgba(133, 119, 96, 1), rgba(133, 119, 96, 0)) 1 1;
}
.target-options {
width: 100%;
padding: 0 10px;
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 15px;
}
.battle-btn {
width: 45%;
height: 55px;
@@ -193,4 +142,42 @@ const createRoom = debounce(async () => {
border: 4rpx solid #fff3;
border-color: #fed847;
}
/* .battle-close {
background-color: #8889;
color: #b3b3b3;
}
.battle-close > text:last-child {
font-size: 12px;
} */
.room-info {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding-top: 40px;
}
.room-info > view:first-child {
font-size: 22px;
color: #fff;
margin-bottom: 20px;
}
.room-info > text {
color: #888686;
font-size: 14px;
margin: 10px 0;
}
.room-info > view:last-child {
color: #287fff;
margin: 20px 0;
font-size: 14px;
}
.copy-room-number {
width: calc(70vw - 20px);
color: #fed847;
border: 1px solid #fed847;
padding: 10px;
text-align: center;
border-radius: 10px;
margin-bottom: 20px;
}
</style>

View File

@@ -27,7 +27,7 @@ const props = defineProps({
default: "",
},
});
const itemTexts = ["选择弓种", "选择练习距离", "选择靶纸", "选择组/箭数"];
const itemTexts = ["Select Bow", "Select Distance", "Select Target", "Select Sets/Arrows"];
const distances = [5, 8, 10, 18, 25, 30, 50, 60, 70];
const groupArrows = [3, 6, 12, 18];
@@ -73,7 +73,7 @@ const onMeterChange = (e) => {
};
const onSetsChange = (e) => {
if (!e.detail.value) return;
sets.value = Math.min(30, Math.max(1, Number(e.detail.value)));
sets.value = Math.min(30, Number(e.detail.value));
if (!sets.value) return;
if (secondSelectIndex.value !== -1) {
props.onSelect(
@@ -88,7 +88,7 @@ const onSetsChange = (e) => {
};
const onArrowAmountChange = (e) => {
if (!e.detail.value) return;
arrowAmount.value = Math.min(60, Math.max(1, Number(e.detail.value)));
arrowAmount.value = Math.min(60, Number(e.detail.value));
if (!arrowAmount.value) return;
if (selectedIndex.value !== -1) {
props.onSelect(
@@ -153,15 +153,13 @@ const loadConfig = () => {
const formatSetAndAmount = computed(() => {
if (selectedIndex.value === -1 || secondSelectIndex.value === -1)
return itemTexts[props.itemIndex];
if (selectedIndex.value === 99 && !sets.value)
return itemTexts[props.itemIndex];
if (secondSelectIndex.value === 99 && !arrowAmount.value)
return itemTexts[props.itemIndex];
return `${selectedIndex.value === 99 ? sets.value : selectedIndex.value}组/${
if (selectedIndex.value === 99 && !sets.value) return itemTexts[props.itemIndex];
if (secondSelectIndex.value === 99 && !arrowAmount.value) return itemTexts[props.itemIndex];
return `${selectedIndex.value === 99 ? sets.value : selectedIndex.value} sets/${
secondSelectIndex.value === 99
? arrowAmount.value
: groupArrows[secondSelectIndex.value]
}`;
} arrows`;
});
onMounted(async () => {
const config = uni.getStorageSync("point-book-config");
@@ -187,13 +185,13 @@ onMounted(async () => {
<view></view>
<block>
<text v-if="expand" :style="{ color: '#999', fontWeight: 'normal' }">{{
itemIndex !== 3 ? itemTexts[itemIndex] : "选择组"
itemIndex !== 3 ? itemTexts[itemIndex] : "Select Sets"
}}</text>
<text v-if="!expand && itemIndex === 0">{{
value || itemTexts[itemIndex]
}}</text>
<text v-if="!expand && itemIndex === 1">{{
value && value > 0 ? value + "" : itemTexts[itemIndex]
value && value > 0 ? value + " m" : itemTexts[itemIndex]
}}</text>
<text v-if="!expand && itemIndex === 2">{{
value || itemTexts[itemIndex]
@@ -232,7 +230,7 @@ onMounted(async () => {
@click="onSelectItem(index)"
>
<text>{{ item }}</text>
<text></text>
<text>m</text>
</view>
<view
:style="{
@@ -242,12 +240,12 @@ onMounted(async () => {
<input
v-model="meter"
type="number"
placeholder="自定义"
placeholder="Custom"
placeholder-style="color: #DDDDDD"
@focus="() => (selectedIndex = 9)"
@blur="onMeterChange"
/>
<text></text>
<text>m</text>
</view>
</view>
<view v-if="itemIndex === 2" class="bowtarget-items">
@@ -274,7 +272,7 @@ onMounted(async () => {
@click="onSelectItem(i)"
>
<text>{{ i }}</text>
<text></text>
<text>sets</text>
</view>
<view
:style="{
@@ -289,7 +287,7 @@ onMounted(async () => {
@focus="() => (selectedIndex = 99)"
@blur="onSetsChange"
/>
<text></text>
<text>sets</text>
</view>
</view>
<view
@@ -299,7 +297,7 @@ onMounted(async () => {
color: '#999999',
textAlign: 'center',
}"
>选择每组的箭数</view
>Select arrows per set</view
>
<view class="amount-items">
<view
@@ -311,7 +309,7 @@ onMounted(async () => {
@click="onSelectSecondItem(index)"
>
<text>{{ item }}</text>
<text></text>
<text>arrows</text>
</view>
<view
:style="{
@@ -327,7 +325,7 @@ onMounted(async () => {
@focus="() => (secondSelectIndex = 99)"
@blur="onArrowAmountChange"
/>
<text></text>
<text>arrows</text>
</view>
</view>
</view>

View File

@@ -23,7 +23,7 @@ const bubbleTypes = [
<image
v-if="!noBg"
:src="bubbleTypes[type]"
:style="{ top: type === 2 ? '-6%' : '-13%' }"
:style="{ top: type === 2 ? '-6%' : '-12%' }"
mode="widthFix"
/>
<slot />
@@ -55,6 +55,6 @@ const bubbleTypes = [
}
.container > view {
color: #fff;
font-size: 28rpx;
font-size: 14px;
}
</style>

View File

@@ -1,54 +0,0 @@
<script setup>
defineProps({
noBg: {
type: Boolean,
default: false,
}
});
</script>
<template>
<view class="container">
<image class="shooter2" src="../static/shooter2.png" mode="widthFix" />
<view class="bg-box">
<image
class="bg"
v-if="!noBg"
src="../static/long-bubble-border.png"
mode="widthFix"
/>
<slot />
</view>
</view>
</template>
<style scoped>
.container {
display: flex;
align-items: center;
padding: 0 15px;
margin-bottom: 14rpx;
width: clac(100% - 30px);
}
.container .shooter2 {
width: 150rpx;
height: 162rpx;
}
.container .bg-box {
color: #fff;
font-size: 28rpx;
position: relative;
flex: 1;
min-height: 55px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.container .bg-box .bg {
position: absolute;
left: 0;
right: 0;
width: 100%;
}
</style>

View File

@@ -6,7 +6,7 @@ import Avatar from "@/components/Avatar.vue";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, game } = storeToRefs(store);
const { user } = storeToRefs(store);
const currentPage = computed(() => {
const pages = getCurrentPages();
@@ -51,15 +51,17 @@ const toUserPage = () => {
const signin = () => {
if (!user.value.id) {
uni.$emit("point-book-signin");
uni.navigateTo({
url: "/pages/sign-in",
});
}
};
const loading = ref(false);
const showLoader = ref(false);
const pointBook = ref(null);
const showProgress = ref(false);
const heat = ref(0);
const updateLoading = (value) => {
loading.value = value;
};
@@ -71,21 +73,23 @@ const updateHot = (value) => {
onMounted(() => {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
if (
currentPage.route === "pages/point-book-edit" ||
currentPage.route === "pages/point-book-detail"
) {
if (currentPage.route === "pages/point-book-edit") {
pointBook.value = uni.getStorageSync("point-book");
if (!pointBook.value) {
pointBook.value = uni.getStorageSync("last-point-book");
}
if (
currentPage.route === "pages/team-battle" ||
currentPage.route === "pages/melee-match"
) {
showLoader.value = true;
}
if (currentPage.route === "pages/team-battle") {
showProgress.value = true;
}
uni.$on("update-header-loading", updateLoading);
uni.$on("update-hot", updateHot);
});
onBeforeUnmount(() => {
uni.$off("update-header-loading", updateLoading);
uni.$off("update-hot", updateHot);
});
</script>
@@ -153,6 +157,12 @@ onBeforeUnmount(() => {
</view>
</block>
</view>
<image
:style="{ opacity: showLoader && loading ? 1 : 0 }"
src="../static/btn-loading.png"
mode="widthFix"
class="loading"
/>
<view v-if="pointBook" class="point-book-info">
<text>{{ pointBook.bowType.name }}</text>
<text>{{ pointBook.distance }} 米</text>
@@ -173,16 +183,6 @@ onBeforeUnmount(() => {
<view v-if="showProgress" class="battle-progress">
<HeaderProgress />
</view>
<!-- 对战房间:整个胶囊为分享按钮,房号从 Store 读取 -->
<button
v-if="currentPage === 'pages/battle-room' && game.roomNumber"
open-type="share"
hover-class="none"
class="battle-room-number"
>
<text class="battle-room-number__text">房号: {{ game.roomNumber }}</text>
<image src="../static/share2.png" mode="widthFix" class="battle-room-number__icon" />
</button>
</view>
</template>
@@ -226,6 +226,14 @@ onBeforeUnmount(() => {
font-size: 16px;
color: #fff;
}
.loading {
width: 20px;
height: 20px;
margin-left: 10px;
transition: all 0.3s ease;
background-blend-mode: darken;
animation: rotate 2s linear infinite;
}
.point-book-info {
color: #333;
position: fixed;
@@ -261,7 +269,6 @@ onBeforeUnmount(() => {
}
.user-header > image:last-child {
width: 36rpx;
height: 36rpx;
}
.user-header > text:nth-child(2) {
font-weight: 500;
@@ -270,37 +277,4 @@ onBeforeUnmount(() => {
margin: 0 20rpx;
max-width: 300rpx;
}
/* 对战房间:整个胶囊作为分享按钮,靠右对齐 */
.battle-room-number {
margin-left: auto;
margin-right: 10rpx;
display: flex;
align-items: center;
justify-content: center;
width: 240rpx;
height: 64rpx;
background: rgba(0, 0, 0, 0.15);
border-radius: 96rpx;
border: 1rpx solid #5b5758;
flex-shrink: 0;
padding: 0;
line-height: normal;
}
/* 重置 button 默认边框 */
.battle-room-number::after {
border: none;
}
.battle-room-number__text {
width: 156rpx;
height: 28rpx;
font-weight: 400;
font-size: 20rpx;
color: #ffffff;
text-align: center;
line-height: 28rpx;
}
.battle-room-number__icon {
width: 25rpx;
height: 26rpx;
}
</style>

View File

@@ -1,9 +1,7 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import audioManager from "@/audioManager";
import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText } from "@/util";
import { MESSAGETYPES } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
@@ -13,78 +11,107 @@ const tips = ref("");
const melee = ref(false);
const timer = ref(null);
const sound = ref(true);
const currentSound = ref("");
const currentRound = ref(0);
const currentRoundEnded = ref(false);
const ended = ref(false);
const halfTime = ref(false);
const currentShot = ref(0);
const totalShot = ref(0);
const yourTurn = ref(false);
watch(
() => tips.value,
(newVal) => {
let key = [];
if (newVal.includes("重回")) return;
let key = "";
if (newVal.includes("红队")) key = "请红方射箭";
if (newVal.includes("蓝队")) key = "请蓝方射箭";
if (!sound.value) return;
if (currentRoundEnded.value) {
currentRound.value += 1;
// 播放当前轮次语音
key.push(`${["一", "二", "三", "四", "五"][currentRound.value]}`);
}
key.push(
newVal.includes("你")
? "轮到你了"
: newVal.includes("红队")
? "请红方射箭"
: "请蓝方射箭"
audioManager.play(
`${["一", "二", "三", "四", "五"][currentRound.value - 1]}`
);
audioManager.play(key, false);
}
// 延迟播放队伍提示音
setTimeout(
() => {
if (key && !yourTurn.value) audioManager.play(key);
currentRoundEnded.value = false;
yourTurn.value = false;
},
currentRoundEnded.value ? 1000 : 0
);
}
);
const updateSound = () => {
sound.value = !sound.value;
audioManager.setMuted(!sound.value);
if (!sound.value) audioManager.stop(currentSound.value);
};
async function onReceiveMessage(message) {
if (ended.value) return;
if (Array.isArray(message)) return;
const { type, mode, current, shootData } = message;
if (type === MESSAGETYPESV2.BattleStart) {
melee.value = Boolean(mode > 3);
totalShot.value = mode === 1 ? 3 : 2;
currentRoundEnded.value = true;
audioManager.play("比赛开始");
} else if (type === MESSAGETYPESV2.BattleEnd) {
audioManager.play("比赛结束", false);
} else if (type === MESSAGETYPESV2.ShootResult) {
if (melee.value && current.playerId !== user.value.id) return;
if (current.playerId === user.value.id) currentShot.value++;
if (message.shootData) {
let key = [];
key.push(
shootData.ring
? `${shootData.ringX ? "X" : shootData.ring}`
: "未上靶"
);
if (shootData.angle !== null)
key.push(`${getDirectionText(shootData.angle)}调整`);
audioManager.play(key, false);
async function onReceiveMessage(messages = []) {
if (!sound.value || ended.value) return;
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootResult) {
if (melee.value && msg.userId !== user.value.id) return;
if (msg.userId === user.value.id) currentShot.value++;
if (!halfTime.value && msg.target) {
currentSound.value = msg.target.ring
? `${msg.target.ring}`
: "未上靶";
audioManager.play(currentSound.value);
}
} else if (type === MESSAGETYPESV2.NewRound) {
currentShot.value = 0;
currentRound.value = current.round;
currentRoundEnded.value = true;
} else if (type === MESSAGETYPESV2.InvalidShot) {
} else if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) {
yourTurn.value = user.value.id === msg.userId;
} else if (msg.constructor === MESSAGETYPES.InvalidShot) {
if (msg.userId === user.value.id) {
uni.showToast({
title: "距离不足,无效",
icon: "none",
});
audioManager.play("射击无效");
}
} else if (msg.constructor === MESSAGETYPES.AllReady) {
const { config } = msg.groupUserStatus;
if (config && config.mode === 1) {
totalShot.value = config.teamSize === 2 ? 3 : 2;
}
currentRoundEnded.value = true;
audioManager.play("比赛开始");
} else if (msg.constructor === MESSAGETYPES.MeleeAllReady) {
melee.value = true;
halfTime.value = false;
audioManager.play("比赛开始");
} else if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
currentShot.value = 0;
if (msg.preRoundResult && msg.preRoundResult.currentRound) {
currentRound.value = msg.preRoundResult.currentRound;
currentRoundEnded.value = true;
}
} else if (msg.constructor === MESSAGETYPES.HalfTimeOver) {
halfTime.value = true;
audioManager.play("中场休息");
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
audioManager.play("比赛结束");
} else if (msg.constructor === MESSAGETYPES.FinalShoot) {
totalShot.value = 0;
audioManager.play("决金箭轮");
tips.value = "即将开始...";
currentRoundEnded.value = false;
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
ended.value = true;
} else if (msg.constructor === MESSAGETYPES.BackToGame) {
if (msg.battleInfo) {
melee.value = msg.battleInfo.config.mode === 2;
}
}
});
}
const playSound = (key) => {
currentSound.value = key;
audioManager.play(key);
};
@@ -114,7 +141,7 @@ onBeforeUnmount(() => {
<template>
<view class="container">
<text>{{ (tips || "").replace(/你/g, "").replace(/重回/g, "") }}</text>
<text>{{ tips }}</text>
<text v-if="totalShot > 0"> ({{ currentShot }}/{{ totalShot }}) </text>
<button v-if="!!tips" hover-class="none" @click="updateSound">
<image
@@ -132,7 +159,6 @@ onBeforeUnmount(() => {
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
}
.container > button:last-child {
width: 36px;

View File

@@ -0,0 +1,84 @@
<script setup>
import { ref } from "vue";
const props = defineProps({
type: {
type: String,
default: "text",
},
btnType: {
type: String,
default: "",
},
onChange: {
type: Function,
default: null,
},
placeholder: {
type: String,
default: "",
},
width: {
type: String,
default: "90vw",
},
});
const hide = ref(true);
</script>
<template>
<view class="container" :style="{ width }">
<input
:type="type"
@change="onChange"
:placeholder="placeholder"
placeholder-style="color: #999;"
/>
<button v-if="btnType === 'code'" hover-class="none" class="get-code">
get verification code
</button>
<button
v-if="type === 'password'"
hover-class="none"
class="eye-btn"
@click="hide = !hide"
>
<image
:src="`../static/${hide ? 'eye-close' : 'eye-open'}.png`"
mode="widthFix"
/>
</button>
</view>
</template>
<style scoped>
.container {
height: 100rpx;
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-radius: 30rpx;
padding: 0 30rpx;
margin: 15rpx 0;
box-sizing: border-box;
}
.container > input {
width: 100%;
color: #333;
font-size: 26rpx;
}
.get-code {
color: #287fff;
font-size: 26rpx;
width: 80%;
}
.eye-btn {
padding: 20rpx;
}
.eye-btn > image {
width: 50rpx;
height: 32rpx;
}
</style>

View File

@@ -11,94 +11,91 @@ const props = defineProps({
},
});
const playerNames = [
"乐正青山",
"宇文玉兰",
"岑思宇",
"邬梓瑜",
"范子衿",
"彭妮·希利",
"埃琳娜·奥西波娃",
"凯西·考夫霍尔德",
"旗鼓相当的对手",
"乐子睿",
"时春晓",
"柏孤鸿",
"东宫锦瑟",
"段干流云",
"马乌罗·内斯波利",
"埃琳娜·奥西波娃",
"凯西·考夫霍尔德",
];
const textStyles = [
const totalTop = ref(0);
const timer = ref(null);
const textStyles = ref([]);
const getTextStyle = (top) => {
const styles = [
{
color: "#fff9",
fontSize: "18px",
fontSize: "20px",
},
{
color: "#fff",
fontSize: "22px",
fontSize: "24px",
},
{
color: "#fed847",
fontSize: "30px",
},
];
const rowHeight = 100 / 7;
const totalHeight = (playerNames.length / 7) * 100 + 7;
const currentTop = ref(-totalHeight + rowHeight * 0);
const timer = ref(null);
const getTextStyle = (top, index) => {
const count = Math.floor(
((totalHeight + (top + rowHeight / 3)) / rowHeight).toFixed(1)
);
if (index === 12 - count) return textStyles[0];
else if (index === 13 - count) return textStyles[1];
else if (index === 14 - count) return textStyles[2];
else if (index === 15 - count) return textStyles[1];
else if (index === 16 - count) return textStyles[0];
return {
const data = new Array(14).fill({
color: "#fff6",
fontSize: "14px",
};
fontSize: "16px",
});
const unitHeight = 100 / 7;
let style = {};
if (top >= 100 - unitHeight / 2) {
for (let j = 0; j < 5; j++) {
data[j + 1] = styles[j > 2 ? 4 - j : j];
}
} else {
new Array(7).fill(1).some((_, i) => {
if (
top >= unitHeight * i - unitHeight / 2 &&
top < unitHeight * (i + 1) - unitHeight / 2
) {
for (let j = 0; j < 5; j++) {
data[7 + j + 1 - i] = styles[j > 2 ? 4 - j : j];
}
return true;
}
return false;
});
}
return data;
};
watch(
() => props.onComplete,
(newVal) => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
(newVal, oldVal) => {
if (newVal && !oldVal) {
if (timer.value) clearInterval(timer.value);
timer.value = setInterval(() => {
const count = Math.round(
(
(totalHeight + (currentTop.value + rowHeight / 3)) /
rowHeight
).toFixed(1)
);
if (count === 10) {
if (totalTop.value === 100) {
clearInterval(timer.value);
timer.value = null;
setTimeout(newVal, 1500);
return;
}
// 这里不重置如果运行超13秒就不会循环了
if (currentTop.value >= -4) {
currentTop.value = -totalHeight;
setTimeout(() => {
newVal();
}, 1500);
} else {
currentTop.value += 2;
totalTop.value += 0.5;
}
textStyles.value = getTextStyle(totalTop.value);
}, 10);
}
}, 40);
}
);
onMounted(() => {
timer.value = setInterval(() => {
if (currentTop.value >= -4) {
currentTop.value = -totalHeight;
if (totalTop.value === 100) {
totalTop.value = 0;
} else {
currentTop.value += 2;
totalTop.value += 2;
}
textStyles.value = getTextStyle(totalTop.value);
}, 40);
});
onBeforeUnmount(() => {
if (timer.value) clearInterval(timer.value);
timer.value = null;
});
</script>
@@ -110,13 +107,30 @@ onBeforeUnmount(() => {
class="matching-bg"
/>
<view>
<view class="player-names" :style="{ top: `${currentTop}%` }">
<view
class="player-names"
:style="{
top: `${totalTop - 100}%`,
}"
>
<text
v-for="(name, index) in [...playerNames, ...playerNames]"
v-for="(name, index) in playerNames"
:key="index"
:style="{
lineHeight: `${rowHeight}vw`,
...getTextStyle(currentTop, index),
lineHeight: `${95 / 7}vw`,
...(textStyles[index] || {}),
}"
>
{{ name }}
</text>
</view>
<view class="player-names" :style="{ top: `${totalTop}%` }">
<text
v-for="(name, index) in playerNames"
:key="index"
:style="{
lineHeight: `${95 / 7}vw`,
...(textStyles[index + 7] || {}),
}"
>
{{ name }}
@@ -142,7 +156,7 @@ onBeforeUnmount(() => {
height: 95vw;
overflow: hidden;
position: absolute;
top: 30vw;
top: 30.5vw;
}
.matching-bg {
position: absolute;
@@ -162,6 +176,7 @@ onBeforeUnmount(() => {
}
.player-names {
width: 100%;
height: 95vw;
display: flex;
flex-direction: column;
position: absolute;

View File

@@ -0,0 +1,209 @@
<script setup>
import { ref, watch } from "vue";
import BowTarget from "@/components/BowTarget.vue";
import Avatar from "@/components/Avatar.vue";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const scores = ref([]);
const currentUser = ref({});
const props = defineProps({
show: {
type: Boolean,
default: false,
},
onClose: {
type: Function,
default: () => {},
},
data: {
type: Object,
default: () => ({}),
},
});
watch(
() => props.data,
(value) => {
const mine = value.players.find((p) => p.playerId === user.value.id);
currentUser.value = mine;
if (mine && mine.arrowHistory) {
scores.value = mine.arrowHistory;
}
},
{ deep: true, immediate: true }
);
const onSelect = (userId) => {
const user = props.data.players.find((p) => p.playerId === userId);
currentUser.value = user;
if (user && user.arrowHistory) {
scores.value = user.arrowHistory;
}
};
</script>
<template>
<view class="container" :style="{ display: show ? 'flex' : 'none' }">
<view>
<text>5人大乱斗</text>
<view @click="onClose">
<image src="../static/close-white.png" mode="widthFix" />
</view>
</view>
<view class="rank-rows">
<view
v-for="(player, index) in data.players"
:key="index"
@click="() => onSelect(player.playerId)"
>
<image v-if="index === 0" src="../static/champ1.png" mode="widthFix" />
<image v-if="index === 1" src="../static/champ2.png" mode="widthFix" />
<image v-if="index === 2" src="../static/champ3.png" mode="widthFix" />
<view v-if="index > 2" class="rank-view">{{ index + 1 }}</view>
<Avatar :src="player.avatar" :size="24" />
<text
>积分
{{
player.totalScore > 0 ? "+" + player.totalScore : player.totalScore
}}</text
>
<text>{{ player.totalRings }}</text>
<text v-for="(arrow, index2) in player.arrowHistory" :key="index2">
{{ arrow.ring }}
</text>
</view>
</view>
<view :style="{ width: '95%' }">
<BowTarget
:scores="scores"
:avatar="currentUser ? currentUser.avatar : ''"
/>
</view>
<view class="score-text"
><text :style="{ color: '#fed847' }">{{ scores.length }}</text
>支箭<text :style="{ color: '#fed847' }">{{
scores.reduce((last, next) => last + next.ring, 0)
}}</text
></view
>
<view class="score-row">
<view
v-for="(score, index) in scores"
:key="index"
class="score-item"
:style="{ width: '13vw', height: '13vw' }"
>
{{ score.ring }}
</view>
</view>
</view>
</template>
<style scoped>
.container {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background-color: #232323;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
}
.container > view:first-child {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 20px 0;
color: #fff;
position: relative;
font-size: 20px;
}
.container > view:first-child > view:last-child {
position: absolute;
left: 5px;
top: 25px;
}
.container > view:first-child > view:last-child > image {
width: 40px;
}
.score-text {
width: 100%;
color: #fff;
text-align: center;
font-size: 16px;
margin-bottom: 6px;
}
.score-row {
margin: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.score-item {
background-image: url("../static/score-bg.png");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
color: #fed847;
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
margin: 3px;
}
.rank-rows {
display: flex;
flex-direction: column;
width: 100%;
border-top: 1px solid #fff3;
}
.rank-rows > view {
width: clac(100% - 20px);
color: #fff9;
border-bottom: 1px solid #fff3;
padding: 7px 10px;
display: flex;
align-items: center;
font-size: 14px;
overflow-x: auto;
}
.rank-rows > view::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
.rank-rows > view > image:first-child,
.rank-rows > view > view:first-child {
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
font-size: 12px;
margin-right: 10px;
flex: 0 0 auto;
}
.rank-rows > view > view:first-child {
background-color: #6d6d6d;
border-radius: 50%;
}
.rank-rows > view > text {
margin-left: 10px;
flex: 0 0 auto;
display: block;
width: 25px;
}
.rank-rows > view > text:nth-child(3) {
width: 80px;
}
.rank-rows > view > text:nth-child(4) {
color: #fed847;
padding-right: 10px;
border-right: 1px solid #fff3;
width: 32px;
}
</style>

View File

@@ -1,58 +1,47 @@
<script setup>
import useStore from "@/store";
import { storeToRefs } from "pinia";
const { user } = storeToRefs(useStore());
const props = defineProps({
player: {
type: Object,
default: () => ({}),
defineProps({
avatar: {
type: String,
default: "",
},
name: {
type: String,
default: "",
},
scores: {
type: Array,
default: () => [],
},
});
const rowCount = new Array(6).fill(0);
</script>
<template>
<view
class="container"
:style="{ borderColor: player.id === user.id ? '#FED847' : '#fff3' }"
>
<view class="container">
<image
:style="{
opacity:
(scores[0] || []).length + (scores[1] || []).length === 12 ? 1 : 0,
}"
:style="{ opacity: scores.length === 12 ? 1 : 0 }"
src="../static/checked-green.png"
mode="widthFix"
/>
<image :src="player.avatar || '../static/user-icon.png'" mode="widthFix" />
<text>{{ player.name }}</text>
<image :src="avatar || '../static/user-icon.png'" mode="widthFix" />
<text>{{ name }}</text>
<view>
<view>
<view v-for="(_, index) in rowCount" :key="index">
<text>{{
scores[0] && scores[0][index] ? `${scores[0][index].ring}` : "-"
}}</text>
<text>{{ scores[index] ? `${scores[index].ring}` : "-" }}</text>
</view>
</view>
<view>
<view v-for="(_, index) in rowCount" :key="index">
<text>{{
scores[1] && scores[1][index] ? `${scores[0][index].ring}` : "-"
scores[index + 6] ? `${scores[index + 6].ring}` : "-"
}}</text>
</view>
</view>
</view>
<text
>{{
scores
.map((s) => s.reduce((last, next) => last + next.ring, 0))
.reduce((last, next) => last + next, 0)
scores.map((s) => s.ring).reduce((last, next) => last + next, 0)
}}</text
>
</view>
@@ -115,6 +104,5 @@ const rowCount = new Array(6).fill(0);
.container > text:nth-child(5) {
width: 40px;
text-align: right;
word-break: keep-all;
}
</style>

View File

@@ -9,7 +9,7 @@ defineProps({
type: String,
default: "",
},
arrows: {
scores: {
type: Array,
default: () => [],
},
@@ -21,6 +21,10 @@ defineProps({
type: Number,
default: 0,
},
totalRing: {
type: Number,
default: 0,
},
});
const rowCount = new Array(6).fill(0);
</script>
@@ -56,19 +60,19 @@ const rowCount = new Array(6).fill(0);
<view>
<view>
<view v-for="(_, index) in rowCount" :key="index">
<text>{{ arrows[index] ? `${arrows[index].ring}` : "-" }}</text>
<text>{{ scores[index] ? `${scores[index].ring}` : "-" }}</text>
</view>
</view>
<view>
<view v-for="(_, index) in rowCount" :key="index">
<text>{{
arrows[index + 6] ? `${arrows[index + 6].ring}` : "-"
scores[index + 6] ? `${scores[index + 6].ring}` : "-"
}}</text>
</view>
</view>
</view>
<view>
<text>{{ arrows.reduce((last, next) => last + next.ring, 0) }}</text>
<text>{{ totalRing }}</text>
<text>积分{{ totalScore }}</text>
</view>
</view>

View File

@@ -1,6 +1,4 @@
<script setup>
import Avatar from "@/components/Avatar.vue";
const props = defineProps({
total: {
type: Number,
@@ -10,10 +8,6 @@ const props = defineProps({
type: Array,
default: () => [],
},
removePlayer: {
type: Function,
default: () => {},
},
});
const seats = new Array(props.total).fill(1);
</script>
@@ -22,16 +16,11 @@ const seats = new Array(props.total).fill(1);
<view class="players">
<view v-for="(_, index) in seats" :key="index">
<image src="../static/player-bg.png" mode="widthFix" />
<view v-if="players[index] && players[index].name" class="avatar">
<Avatar
<image
v-if="players[index] && players[index].name"
:src="players[index].avatar || '../static/user-icon.png'"
:size="40"
mode="widthFix"
/>
<text
:style="{ opacity: players[index] && !!players[index].state ? 1 : 0 }"
>已准备</text
>
</view>
<view v-else class="player-unknow">
<image src="../static/question-mark.png" mode="widthFix" />
</view>
@@ -39,68 +28,48 @@ const seats = new Array(props.total).fill(1);
players[index].name
}}</text>
<text v-else :style="{ color: '#fff9' }">虚位以待</text>
<view v-if="index === 0" class="founder">管理员</view>
<!-- <image
<view v-if="index === 0" class="founder">创建者</view>
<image
:src="`../static/player-${index + 1}.png`"
mode="widthFix"
class="player-bg"
/> -->
<button
v-if="index > 0 && players[index]"
hover-class="none"
class="remove-player"
@click="() => removePlayer(players[index])"
>
<image src="../static/close-white.png" mode="widthFix" />
</button>
/>
</view>
</view>
</template>
<style scoped>
.players {
display: grid;
grid-template-columns: repeat(2, 1fr);
row-gap: 20rpx;
column-gap: 25rpx;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
-moz-column-gap: 20px;
column-gap: 14px;
margin-bottom: 20px;
font-size: 14px;
padding: 0 14px;
}
.players > view {
width: calc(50% - 7px);
display: flex;
align-items: center;
position: relative;
color: #fff;
height: 176rpx;
height: 100px;
overflow: hidden;
}
.players > view > image:first-child {
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
top: 0;
}
.avatar {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24rpx;
margin-top: 16rpx;
}
.avatar > text {
background-color: #2c261fb3;
border: 1rpx solid #a3793f66;
color: #fed847;
font-size: 16rpx;
border-radius: 20rpx;
width: 70rpx;
text-align: center;
margin-top: -16rpx;
position: relative;
height: 28rpx;
line-height: 28rpx;
.players > view > image:nth-child(2) {
width: 40px;
height: 40px;
min-height: 40px;
margin: 0 10px;
border: 1px solid #fff;
border-radius: 50%;
}
.players > view > text:nth-child(3) {
width: 20vw;
@@ -111,48 +80,30 @@ const seats = new Array(props.total).fill(1);
.founder {
position: absolute;
background-color: #fed847;
top: 0;
left: 0;
top: 6px;
color: #000;
font-size: 10px;
padding: 2px 5px;
border-top-left-radius: 10px;
border-bottom-right-radius: 10px;
}
/* .player-bg {
.player-bg {
position: absolute;
width: 52px;
right: 0;
} */
}
.player-unknow {
width: 84rpx;
height: 84rpx;
margin: 0 24rpx;
border: 1rpx solid #fff3;
width: 40px;
height: 40px;
margin: 0 10px;
border: 1px solid #fff3;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #69686866;
box-sizing: border-box;
}
.player-unknow > image {
width: 40%;
}
.remove-player {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 10rpx;
right: 0;
}
.remove-player > image {
width: 100%;
height: 100%;
opacity: 0.6;
}
</style>

View File

@@ -0,0 +1,137 @@
<script setup>
defineProps({
avatar: {
type: String,
default: "",
},
blueTeam: {
type: Array,
default: () => [],
},
redTeam: {
type: Array,
default: () => [],
},
currentShooterId: {
type: Number,
default: 0,
},
});
</script>
<template>
<view class="container">
<image v-if="avatar" class="avatar" :src="avatar" mode="widthFix" />
<view
v-if="blueTeam.length && redTeam.length"
:style="{ height: 20 + blueTeam.length * 20 + 'px' }"
>
<view
v-for="(player, index) in blueTeam"
:key="index"
:style="{
top: index * 20 + 'px',
zIndex: blueTeam.length - index,
left: 0,
}"
>
<image
class="avatar"
:src="player.avatar || '../static/user-icon.png'"
mode="widthFix"
:style="{
borderColor: currentShooterId === player.id ? '#5fadff' : '#fff',
}"
/>
<text
:style="{
color: currentShooterId === player.id ? '#5fadff' : '#fff',
fontSize: currentShooterId === player.id ? 16 : 12 + 'px',
}"
>
{{ player.name }}
</text>
</view>
</view>
<view
v-if="!avatar"
:style="{
height: 20 + redTeam.length * 20 + 'px',
}"
>
<view
v-for="(player, index) in redTeam"
:key="index"
:style="{
top: index * 20 + 'px',
zIndex: redTeam.length - index,
right: 0,
}"
>
<text
:style="{
color: currentShooterId === player.id ? '#ff6060' : '#fff',
fontSize: currentShooterId === player.id ? 16 : 12 + 'px',
textAlign: 'right',
}"
>
{{ player.name }}
</text>
<image
class="avatar"
:src="player.avatar || '../static/user-icon.png'"
mode="widthFix"
:style="{
borderColor: currentShooterId === player.id ? '#ff6060' : '#fff',
}"
/>
</view>
</view>
</view>
</template>
<style scoped>
.container {
width: calc(100% - 30px);
margin: 0 15px;
margin-top: 5px;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.container > view {
width: 50%;
position: relative;
}
.container > view > view {
position: absolute;
top: -20px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s linear;
}
.container > view > view > text {
margin: 0 10px;
overflow: hidden;
width: 120px;
transition: all 0.3s linear;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.avatar {
width: 40px;
height: 40px;
min-width: 40px;
min-height: 40px;
border: 1px solid #fff;
border-radius: 50%;
}
.red-avatar {
border: 1px solid #ff6060;
}
.blue-avatar {
border: 1px solid #5fadff;
}
</style>

View File

@@ -1,159 +0,0 @@
<script setup>
import { ref, watch } from "vue";
import Avatar from "@/components/Avatar.vue";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const { user, device, online } = storeToRefs(useStore());
import { clickLikeAPI } from "@/apis";
const props = defineProps({
data: {
type: Object,
default: () => {},
},
borderWidth: {
type: Number,
default: 1,
},
});
const like = ref(props.data.ifLike);
const likeCount = ref(props.data.likeTotal || 0);
watch(
() => props.data,
(newVal) => {
like.value = newVal.ifLike;
likeCount.value = newVal.likeTotal || 0;
}
);
const onClick = async () => {
if (!user.value.id) return;
if (user.value.id === props.data.id) {
return uni.navigateTo({
url: "/pages/my-like-list",
});
}
like.value = !like.value;
await clickLikeAPI(props.data.id, like.value);
if (like.value) likeCount.value++;
else likeCount.value--;
};
</script>
<template>
<view class="rank-item" :style="{ borderWidth: borderWidth + 'rpx' }">
<image v-if="data.rank === 1" src="../static/point-no1.png" />
<image v-else-if="data.rank === 2" src="../static/point-no2.png" />
<image v-else-if="data.rank === 3" src="../static/point-no3.png" />
<text v-else>{{ data.rank || "" }}</text>
<view>
<Avatar :src="data.avatar || '../static/user-icon.png'" :size="36" />
<view>
<text class="truncate">{{ data.name }}</text>
<view>
<text>{{ data.totalDay }}</text>
<view />
<text>平均{{ Number(data.averageRing.toFixed(1)) }}</text>
</view>
</view>
</view>
<view class="item-info">
<text>{{ data.weekArrow }}</text>
<text></text>
</view>
<view class="item-info">
<text>{{ Math.round(data.weekArrow * 1.6) }}</text>
<text>千卡</text>
</view>
<button hover-class="none" @click="onClick">
<text>{{ likeCount }}</text>
<image
:src="`../static/like-${like ? 'on' : 'off'}.png`"
mode="widthFix"
/>
</button>
</view>
</template>
<style scoped lang="scss">
.rank-item {
margin: 0 20rpx;
border-bottom: $uni-border;
display: flex;
align-items: center;
background: $uni-white;
height: 120rpx;
}
.rank-item > text:nth-child(1) {
width: 52rpx;
font-size: 28rpx;
color: #333333;
text-align: center;
}
.rank-item > image:nth-child(1) {
width: 52rpx;
height: 56rpx;
}
.rank-item > view:nth-child(2) {
flex: 1;
display: flex;
align-items: center;
margin-left: 20rpx;
}
.rank-item > view:nth-child(2) > view:last-child {
flex: 1;
display: flex;
flex-direction: column;
font-size: 22rpx;
color: #aaaaaa;
margin-left: 20rpx;
}
.rank-item > view:nth-child(2) > view:last-child > text:first-child {
width: 200rpx;
font-size: 28rpx;
color: #333333;
margin-bottom: 5rpx;
}
.rank-item > view:nth-child(2) > view:last-child > view {
display: flex;
align-items: center;
}
.rank-item > view:nth-child(2) > view:last-child > view > view {
height: 20rpx;
width: 1rpx;
margin: 0 10rpx;
background-color: #b3b3b3;
}
.item-info {
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 20rpx;
color: #777777;
width: 20%;
}
.item-info > text:first-child {
font-size: 28rpx;
color: #333333;
margin-right: 5rpx;
}
.rank-item > button:nth-child(5) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 22rpx;
color: #777777;
padding-left: 20rpx;
padding-right: 10rpx;
}
.rank-item > button:nth-child(5) > image {
width: 24rpx;
height: 22rpx;
margin-top: 10rpx;
}
</style>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, computed } from "vue";
import { ref, onMounted } from "vue";
const props = defineProps({
data: {
@@ -11,19 +11,6 @@ const bowOptions = ref({});
const targetOptions = ref({});
const toDetailPage = () => {
const config = uni.getStorageSync("point-book-config");
const bowType = config.bowOption.find(
(item) => item.id === props.data.bowType
);
const bowtargetType = config.targetOption.find(
(item) => item.id === props.data.targetType
);
uni.setStorageSync("point-book", {
bowType,
bowtargetType,
distance: props.data.distance,
amountGroup: props.data.groups,
});
uni.navigateTo({
url: `/pages/point-book-detail?id=${props.data.id}`,
});
@@ -42,7 +29,7 @@ onMounted(() => {
<template>
<view class="container" @click="toDetailPage">
<view class="left-part">
<view>
<view class="labels">
<view></view>
<text>{{
@@ -64,12 +51,12 @@ onMounted(() => {
<text>平均{{ data.averageRing }}</text>
</view>
</view>
<view class="right-part">
<view>
<image src="../static/bow-target.png" mode="widthFix" />
<view class="arrow-amount">
<text>{{ data.actualTotalRing }}</text>
<text>/</text>
<text>{{ data.totalRing }}</text>
<text></text>
<text>{{ data.arrows * data.groups }}</text>
<text></text>
</view>
</view>
</view>
@@ -83,13 +70,12 @@ onMounted(() => {
border-radius: 25rpx;
height: 200rpx;
border: 2rpx solid #fed848;
padding-left: 30rpx;
padding-right: 10rpx;
}
.container > view {
position: relative;
margin-left: 15px;
}
.left-part {
.container > view:first-child {
flex: 1;
display: flex;
flex-direction: column;
@@ -97,24 +83,20 @@ onMounted(() => {
height: calc(100% - 50rpx);
color: #333333;
}
.left-part > view {
.container > view:first-child > view {
width: 100%;
display: flex;
position: relative;
}
.left-part > view:nth-child(3) {
.container > view:first-child > view:nth-child(3) {
display: flex;
align-items: center;
font-size: 20rpx;
color: #666;
}
.left-part > view:nth-child(3) > text {
.container > view:first-child > view:nth-child(3) > text {
margin-right: 10rpx;
}
.right-part > image {
width: 180rpx;
height: 180rpx;
}
.labels {
align-items: flex-end !important;
}
@@ -132,21 +114,28 @@ onMounted(() => {
position: relative;
color: #333;
}
.container > view:last-child {
margin-right: 1vw;
}
.container > view:last-child > image {
width: 24vw;
}
.arrow-amount {
position: absolute;
background-color: #0009;
border-radius: 12px;
border-radius: 10px;
color: #fffc;
font-size: 24rpx;
line-height: 26px;
width: 64px;
font-size: 12px;
line-height: 22px;
width: 60px;
display: flex;
justify-content: center;
top: calc(50% - 15px);
left: calc(50% - 32px);
top: calc(50% - 13px);
left: calc(50% - 30px);
}
.arrow-amount > text:nth-child(1) {
font-size: 30rpx;
.arrow-amount > text:nth-child(2) {
color: #fff;
font-size: 14px;
margin: 0 3px;
}
</style>

View File

@@ -1,71 +0,0 @@
<script setup>
import { ref } from "vue";
const props = defineProps({
onChange: {
type: Function,
default: () => {},
},
});
const mode = ref(true);
const onClick = () => {
mode.value = !mode.value;
props.onChange(mode.value);
};
</script>
<template>
<view
class="point-switcher"
:style="{ borderColor: mode ? '#D8D8D8' : '#53EF56' }"
>
<view
@click="onClick"
:style="{ transform: 'translateX(' + (mode ? '-58' : '4') + 'rpx)' }"
>
<text>放大</text>
<view :style="{ background: mode ? '#D8D8D8' : '#53EF56' }"></view>
<text>真实</text>
</view>
</view>
</template>
<style scoped>
.point-switcher {
width: 100rpx;
height: 40rpx;
border-radius: 22rpx;
border: 2rpx solid;
display: flex;
overflow: hidden;
}
.point-switcher > view {
position: relative;
display: flex;
align-items: center;
line-height: 40rpx;
color: #ffffff;
font-weight: 500;
font-size: 20rpx;
word-break: keep-all;
padding: 0 12rpx;
transition: all 0.3s ease;
transform: translateX(-58rpx);
}
.point-switcher > view > text:first-child {
color: #53ef56;
}
.point-switcher > view > view {
width: 36rpx;
height: 36rpx;
flex: 0 0 auto;
border-radius: 50%;
margin: 0 10rpx;
transition: all 0.3s ease;
}
.point-switcher > view > text:last-child {
color: #d8d8d8;
}
</style>

View File

@@ -148,7 +148,7 @@ onMounted(async () => {
.container > image:first-child {
width: 200rpx;
position: absolute;
top: -112rpx;
top: -114rpx;
}
.container > text:nth-child(2) {
font-weight: 500;

View File

@@ -1,10 +1,14 @@
<script setup>
import { ref, computed, onMounted } from "vue";
import { ref, computed } from "vue";
const props = defineProps({
data: {
type: Object,
default: () => ({}),
default: Array,
},
total: {
type: Number,
default: 0,
},
});
@@ -40,12 +44,12 @@ const ringText = (ring) => {
<view>
<view v-for="(b, index) in bars" :key="index">
<text v-if="b && b.rate">
{{ `${Number((b.rate * 100).toFixed(1))}%` }}
{{ total === 0 ? `${Number((b.rate * 100).toFixed(1))}%` : b.rate }}
</text>
<view
:style="{
background: barColor(b.rate),
height: (b.rate === 1 ? 150 : b.rate * 240) + 'rpx',
background: barColor(total === 0 ? b.rate : b.rate / total),
height: (total === 0 ? b.rate : b.rate / total) * 300 + 'rpx',
}"
>
</view>
@@ -56,27 +60,18 @@ const ringText = (ring) => {
{{ b && b.ring !== undefined ? b.ring : "" }}
</text>
</view>
<text>环值</text>
</view>
</template>
<style scoped>
.container {
min-height: 150rpx;
display: flex;
flex-direction: column;
justify-content: flex-end;
position: relative;
}
.container > text {
position: absolute;
bottom: 2rpx;
left: 0;
font-size: 18rpx;
color: #999999;
}
.container > view {
padding-left: 40rpx;
padding-right: 10rpx;
padding: 0 10rpx;
}
.container > view:first-child {
display: flex;
@@ -97,15 +92,14 @@ const ringText = (ring) => {
transition: all 0.3s ease;
height: 0;
}
.container > view:nth-child(2) {
.container > view:last-child {
display: grid;
grid-template-columns: repeat(12, 1fr);
border-top: 1rpx solid #333;
font-size: 22rpx;
color: #333333;
padding-top: 2rpx;
}
.container > view:nth-child(2) > text {
.container > view:last-child > text {
text-align: center;
}
</style>

View File

@@ -52,14 +52,14 @@ onBeforeUnmount(() => {
<view class="point-view1" v-if="bluePoint !== 0 || redPoint !== 0">
<text>本轮蓝队</text>
<text>{{
(roundData.shoots[1] || []).reduce(
(roundData.blueArrows || []).reduce(
(last, next) => last + next.ring,
0
)
}}</text>
<text>红队</text>
<text>{{
(roundData.shoots[2] || []).reduce(
(roundData.redArrows || []).reduce(
(last, next) => last + next.ring,
0
)
@@ -117,10 +117,10 @@ onBeforeUnmount(() => {
display: flex;
flex-direction: column;
align-items: center;
font-size: 32rpx;
font-size: 16px;
}
.round-end-tip > text:first-child {
font-size: 36rpx;
font-size: 18px;
color: #fff;
}
.point-view1 {
@@ -137,7 +137,7 @@ onBeforeUnmount(() => {
}
.point-view2 {
margin: 12px 0;
font-size: 48rpx;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
@@ -155,8 +155,7 @@ onBeforeUnmount(() => {
align-items: center;
justify-content: center;
margin-top: 10px;
font-size: 28rpx;
word-break: keep-all;
font-size: 14px;
}
.final-shoot > text:nth-child(1) {
width: 20px;
@@ -164,7 +163,7 @@ onBeforeUnmount(() => {
}
.final-shoot > text:nth-child(1),
.final-shoot > text:nth-child(3) {
font-size: 32rpx;
font-size: 18px;
color: #fed847;
margin-left: 10px;
margin-right: 5px;

View File

@@ -8,7 +8,7 @@ const props = defineProps({
},
rounded: {
type: Number,
default: 10,
default: 45,
},
onClick: {
type: Function,
@@ -58,7 +58,7 @@ const onBtnClick = debounce(async () => {
hover-class="none"
:style="{
width: width,
borderRadius: rounded + 'px',
borderRadius: rounded + 'rpx',
backgroundColor: disabled ? disabledColor : backgroundColor,
color,
}"
@@ -77,10 +77,10 @@ const onBtnClick = debounce(async () => {
<style scoped>
.sbtn {
margin: 0 auto;
height: 44px;
height: 88rpx;
line-height: 44px;
font-weight: bold;
font-size: 15px;
font-size: 42rpx;
display: flex;
text-align: center;
justify-content: center;

View File

@@ -7,7 +7,7 @@ const props = defineProps({
},
height: {
type: String,
default: "650rpx",
default: "260px",
},
onClose: {
type: Function,
@@ -56,7 +56,7 @@ watch(
>
<image
v-if="!noBg"
src="https://static.shelingxingqiu.com/attachment/2025-12-04/dep11770wzxg6o2alo.png"
src="https://static.shelingxingqiu.com/attachment/2025-08-05/dbuaf19pf7qd8ps0uh.png"
mode="widthFix"
/>
<view class="close-btn" @click="onClose" v-if="!noBg">
@@ -81,14 +81,13 @@ watch(
align-items: center;
opacity: 0;
transition: all 0.3s ease;
z-index: 999;
z-index: 99;
}
.modal-content {
width: 100%;
transform: translateY(100%);
transition: all 0.3s ease;
position: relative;
background-color: #372E1D;
}
.modal-content > image:first-child {
width: 100%;

View File

@@ -9,7 +9,7 @@ const props = defineProps({
type: Number,
default: 0,
},
arrows: {
scores: {
type: Array,
default: () => [],
},
@@ -51,7 +51,7 @@ onBeforeUnmount(() => {
<template>
<view class="container">
<image
v-if="total > 0 && arrows.length === total && completeEffect"
v-if="total > 0 && scores.length === total && completeEffect"
:src="bgImages[bgIndex]"
class="complete-light"
:style="{
@@ -79,10 +79,8 @@ onBeforeUnmount(() => {
>
<image src="../static/score-bg.png" mode="widthFix" />
<text
:style="{ fontWeight: arrows[index] !== undefined ? 'bold' : 'normal' }"
>{{
!arrows[index] ? "-" : arrows[index].ringX ? "X" : arrows[index].ring
}}</text
:style="{ fontWeight: scores[index] !== undefined ? 'bold' : 'normal' }"
>{{ scores[index] !== undefined ? scores[index] : "-" }}</text
>
</view>
</view>

View File

@@ -1,6 +1,6 @@
<script setup>
const props = defineProps({
arrows: {
scores: {
type: Array,
default: () => [],
},
@@ -10,34 +10,37 @@ const getSum = (a, b, c) => {
return sum > 0 ? sum + "环" : "-";
};
const roundsName = ["第一轮", "第二轮", "第三轮", "第四轮"];
const getShowText = (arrow = {}) => {
return arrow.ring ? (arrow.ringX ? "X" : arrow.ring + "环") : "-";
};
</script>
<template>
<view class="container">
<view>
<text :style="{ transform: 'translateX(-10%)' }">总成绩</text>
<text>{{ arrows.reduce((last, next) => last + next.ring, 0) }}</text>
<text>{{ scores.reduce((last, next) => last + next, 0) }}</text>
</view>
<view
v-for="(_, index) in new Array(
Math.min(
Math.ceil(arrows.length / 3) + (arrows.length % 3 === 0 ? 1 : 0),
Math.ceil(scores.length / 3) + (scores.length % 3 === 0 ? 1 : 0),
4
)
).fill(1)"
:key="index"
>
<text>{{ roundsName[index] }}</text>
<text>{{ getShowText(arrows[index * 3 + 0]) }}</text>
<text>{{ getShowText(arrows[index * 3 + 1]) }}</text>
<text>{{ getShowText(arrows[index * 3 + 2]) }}</text>
<text>{{
scores[index * 3 + 0] ? scores[index * 3 + 0] + "环" : "-"
}}</text>
<text>{{
scores[index * 3 + 1] ? scores[index * 3 + 1] + "环" : "-"
}}</text>
<text>{{
scores[index * 3 + 2] ? scores[index * 3 + 2] + "环" : "-"
}}</text>
<text :style="{ width: '40%', transform: 'translateX(20%)' }">{{
getSum(
arrows[index * 3 + 0],
arrows[index * 3 + 1],
arrows[index * 3 + 2]
scores[index * 3 + 0],
scores[index * 3 + 1],
scores[index * 3 + 2]
)
}}</text>
</view>

View File

@@ -1,10 +1,11 @@
<script setup>
import { ref, onMounted, computed } from "vue";
import { ref, onMounted } from "vue";
import IconButton from "@/components/IconButton.vue";
import SButton from "@/components/SButton.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import BowData from "@/components/BowData.vue";
import UserUpgrade from "@/components/UserUpgrade.vue";
import { wxShare } from "@/util";
import { directionAdjusts } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
@@ -36,6 +37,7 @@ const showPanel = ref(true);
const showComment = ref(false);
const showBowData = ref(false);
const showUpgrade = ref(false);
const finished = ref(false);
const totalRing = ref(0);
const closePanel = () => {
showPanel.value = false;
@@ -43,33 +45,22 @@ const closePanel = () => {
props.onClose();
}, 300);
};
function onClickShare() {
uni.$emit("share-image");
}
onMounted(() => {
if (props.result.lvl > user.value.lvl) {
showUpgrade.value = true;
}
totalRing.value = (props.result.details || []).reduce(
if (props.result.arrows) {
totalRing.value = props.result.arrows.reduce(
(last, next) => last + next.ring,
0
);
}
finished.value =
props.result.arrows && props.result.arrows.length === props.total;
});
const getRing = (arrow) => {
if (arrow.ringX) return "X";
return arrow.ring ? arrow.ring : "-";
};
const arrows = computed(() => {
const data = new Array(props.total).fill({ ring: 0 });
(props.result.details || []).forEach((arrow, index) => {
data[index] = arrow;
});
return data;
});
const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
// setTimeout(() => {
// showPanel.value = true;
// }, 300);
</script>
<template>
@@ -78,8 +69,8 @@ const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
<image :src="tipSrc" mode="widthFix" />
<image src="../static/finish-frame.png" mode="widthFix" />
<text
>完成<text class="gold-text">{{ validArrows }}</text
>获得<text class="gold-text">{{ validArrows }}</text
>完成<text class="gold-text">{{ result.arrows.length }}</text
>获得<text class="gold-text">{{ result.arrows.length }}</text
>点经验</text
>
</view>
@@ -100,16 +91,16 @@ const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
</view>
<view :style="{ gridTemplateColumns: `repeat(${rowCount}, 1fr)` }">
<view v-for="(_, index) in new Array(total).fill(0)" :key="index">
{{ getRing(arrows[index])
}}<text v-if="getRing(arrows[index]) !== '-'"></text>
{{ result.arrows[index] ? result.arrows[index].ring : 0
}}<text></text>
</view>
</view>
<view>
<block v-if="validArrows === total">
<block v-if="finished">
<IconButton
name="分享"
src="../static/share.png"
:onClick="onClickShare"
:onClick="wxShare"
/>
<IconButton
name="教练点评"
@@ -118,10 +109,10 @@ const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
/>
</block>
<SButton
:width="validArrows === total ? '70vw' : 'calc(100vw - 20px)'"
:width="finished ? '70vw' : 'calc(100vw - 20px)'"
:rounded="30"
:onClick="closePanel"
>{{ validArrows === total ? "完成" : "返回" }}</SButton
>{{ finished ? "完成" : "重新挑战" }}</SButton
>
</view>
</view>
@@ -137,22 +128,17 @@ const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
}}</text
>环的成绩所有箭支上靶后的平均点间距为<text
:style="{ color: '#fed847' }"
>{{ Number((result.average_distance || 0).toFixed(2)) }}</text
>{{ Number(result.average_distance.toFixed(2)) }}</text
>{{
result.spreadEvaluation === "Dispersed"
? "还需要持续改进哦~"
? "还需要持续改进"
: "成绩优秀。"
}}
</text>
<view>
<image
src="https://static.shelingxingqiu.com/attachment/2025-11-26/deihtj15xjwcz3c1tx.png"
mode="widthFix"
/>
<text :style="{ marginTop: '12px' }"
>针对您本次的练习{{
result.spreadEvaluation === "Dispersed"
? "我们建议您充分练习推弓、靠位以及撒放动作一致性。"
? "我们建议您充分练习推弓、靠位以及撒放动作一致性,以持续提高成绩。"
: totalRing >= 100
? "我们建议您继续保持即可。"
: `我们建议您将设备的瞄准器${
@@ -161,11 +147,9 @@ const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
}}</text
>
</view>
</view>
</ScreenHint>
<BowData
:total="arrows.length"
:arrows="result.details"
:arrows="result.arrows"
:show="showBowData"
:onClose="() => (showBowData = false)"
/>
@@ -185,7 +169,7 @@ const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.8);
z-index: 999;
z-index: 5;
}
.container-header {
margin-top: 20vh;
@@ -242,12 +226,11 @@ const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
text-align: center;
line-height: 27px;
color: #333333;
font-size: 28rpx;
}
.container-content > view:nth-child(2) > view > text {
font-size: 20rpx;
font-size: 12px;
color: #666666;
margin-left: 5rpx;
margin-left: 3px;
}
.container-content > view:nth-child(3) {
width: 100%;
@@ -263,13 +246,6 @@ const validArrows = computed(() => arrows.value.filter((a) => !!a.ring).length);
display: flex;
flex-direction: column;
font-size: 14px;
}
.coach-comment > view {
display: flex;
}
.coach-comment > view > image {
width: 420rpx;
height: 420rpx;
margin-right: 20rpx;
margin-top: -20px;
}
</style>

View File

@@ -59,8 +59,9 @@ onShow(async () => {
<scroll-view
class="scroll-list"
scroll-y
enable-flex="true"
:show-scrollbar="false"
:enhanced="true"
enhanced="true"
:bounces="false"
refresher-default-style="white"
:refresher-enabled="true"
@@ -73,8 +74,8 @@ onShow(async () => {
>
<slot></slot>
<view class="tips">
<text v-if="loading">加载中...</text>
<text v-if="noMore">{{ count === 0 ? "暂无数据" : "没有更多了" }}</text>
<text v-if="loading">Loading...</text>
<text v-if="noMore">{{ count === 0 ? "No data" : "Thats all" }}</text>
</view>
</scroll-view>
</template>
@@ -83,9 +84,7 @@ onShow(async () => {
.scroll-list {
width: 100%;
height: 100%;
}
.tips {
height: 50rpx;
flex-direction: column;
}
.tips > text {
color: #d0d0d0;

View File

@@ -1,14 +1,11 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import audioManager from "@/audioManager";
import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText } from "@/util";
import { MESSAGETYPES } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const props = defineProps({
show: {
type: Boolean,
@@ -38,22 +35,17 @@ const props = defineProps({
type: Boolean,
default: false,
},
onStop: {
type: Function,
default: () => {},
},
});
const barColor = ref("#fed847");
const remain = ref(props.total);
const timer = ref(null);
const sound = ref(true);
const currentSound = ref("");
const currentRound = ref(props.currentRound);
const currentRoundEnded = ref(false);
const ended = ref(false);
const halfTime = ref(false);
const wait = ref(0);
const transitionStyle = ref("all 1s linear");
watch(
() => props.tips,
@@ -61,7 +53,7 @@ watch(
let key = "";
if (newVal.includes("红队")) key = "请红方射箭";
if (newVal.includes("蓝队")) key = "请蓝方射箭";
if (key) {
if (key && sound.value) {
if (currentRoundEnded.value) {
currentRound.value += 1;
currentRoundEnded.value = false;
@@ -80,103 +72,110 @@ watch(
}
);
const resetTimer = (count) => {
watch(
() => props.tips,
(newVal) => {
if (newVal.includes("红队")) barColor.value = "#FF6060";
if (newVal.includes("蓝队")) barColor.value = "#5FADFF";
if (newVal.includes("红队") || newVal.includes("蓝队")) {
if (timer.value) clearInterval(timer.value);
const newVal = Math.round(count);
// 如果剩余时间增加(如重置),瞬间变化无动画
if (newVal >= remain.value) {
transitionStyle.value = "none";
remain.value = newVal;
setTimeout(() => {
transitionStyle.value = "all 1s linear";
}, 50);
} else {
remain.value = newVal;
}
if (remain.value > 0) {
remain.value = props.total;
timer.value = setInterval(() => {
if (remain.value === 0) {
clearInterval(timer.value);
props.onStop();
}
if (remain.value > 0) remain.value--;
}, 1000);
}
};
watch(
() => props.start,
(newVal) => {
if (newVal) {
resetTimer(props.total);
} else {
remain.value = 0;
clearInterval(timer.value);
}
},
{
immediate: true,
}
);
const tipContent = computed(() => {
if (halfTime.value) {
return props.battleId ? "中场休息" : `中场休息(${wait.value}秒)`;
watch(
() => props.start,
(newVal) => {
if (timer.value) clearInterval(timer.value);
if (newVal) {
remain.value = props.total;
timer.value = setInterval(() => {
if (remain.value > 0) remain.value--;
}, 1000);
}
return props.start && remain.value === 0 ? "时间到!" : props.tips;
});
},
{
immediate: true,
}
);
const updateRemain = (value) => {
if (timer.value) clearInterval(timer.value);
remain.value = Math.round(value);
if (remain.value > 0) {
timer.value = setInterval(() => {
if (remain.value > 0) remain.value--;
}, 1000);
}
};
const updateSound = () => {
sound.value = !sound.value;
audioManager.setMuted(!sound.value);
if (!sound.value) audioManager.stop(currentSound.value);
};
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.BattleStart) {
halfTime.value = false;
audioManager.play("比赛开始");
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
audioManager.play("比赛结束");
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
let arrow = {};
if (msg.details && Array.isArray(msg.details)) {
arrow = msg.details[msg.details.length - 1];
} else {
if (msg.shootData.playerId !== user.value.id) return;
if (msg.shootData) arrow = msg.shootData;
async function onReceiveMessage(messages = []) {
if (!sound.value || ended.value) return;
messages.forEach((msg) => {
if (
(props.battleId && msg.constructor === MESSAGETYPES.ShootResult) ||
(!props.battleId && msg.constructor === MESSAGETYPES.ShootSyncMeArrowID)
) {
if (props.melee && msg.userId !== user.value.id) return;
if (!halfTime.value && msg.target) {
currentSound.value = msg.target.ring
? `${msg.target.ring}`
: "未上靶";
audioManager.play(currentSound.value);
}
let key = [];
key.push(arrow.ring ? `${arrow.ringX ? "X" : arrow.ring}` : "未上靶");
if (arrow.angle !== null)
key.push(`${getDirectionText(arrow.angle)}调整`);
audioManager.play(key, false);
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
halfTime.value = true;
audioManager.play("中场休息");
} else if (msg.type === MESSAGETYPESV2.InvalidShot) {
} else if (msg.constructor === MESSAGETYPES.InvalidShot) {
if (msg.userId === user.value.id) {
uni.showToast({
title: "距离不足,无效",
icon: "none",
});
audioManager.play("射击无效");
}
} else if (msg.constructor === MESSAGETYPES.AllReady) {
audioManager.play("比赛开始");
} else if (msg.constructor === MESSAGETYPES.MeleeAllReady) {
halfTime.value = false;
audioManager.play("比赛开始");
} else if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
currentRoundEnded.value = true;
} else if (msg.constructor === MESSAGETYPES.HalfTimeOver) {
halfTime.value = true;
audioManager.play("中场休息");
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
audioManager.play("比赛结束");
} else if (msg.constructor === MESSAGETYPES.FinalShoot) {
audioManager.play("决金箭轮");
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
ended.value = true;
}
});
}
const playSound = (key) => {
currentSound.value = key;
audioManager.play(key);
};
onMounted(() => {
uni.$on("update-remain", resetTimer);
uni.$on("update-ramain", updateRemain);
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("play-sound", playSound);
});
onBeforeUnmount(() => {
uni.$off("update-remain", resetTimer);
uni.$off("update-ramain", updateRemain);
uni.$off("socket-inbox", onReceiveMessage);
uni.$off("play-sound", playSound);
if (timer.value) clearInterval(timer.value);
@@ -187,7 +186,7 @@ onBeforeUnmount(() => {
<view class="container" :style="{ display: show ? 'block' : 'none' }">
<view>
<image src="../static/shooter.png" mode="widthFix" />
<text>{{ tipContent }}</text>
<text>{{ start && remain === 0 ? "时间到!" : tips }}</text>
<button hover-class="none" @click="updateSound">
<image
:src="`../static/sound${sound ? '' : '-off'}-yellow.png`"
@@ -201,7 +200,6 @@ onBeforeUnmount(() => {
width: `${(remain / total) * 100}%`,
backgroundColor: barColor,
right: tips.includes('红队') ? 0 : 'unset',
transition: transitionStyle,
}"
/>
<text>剩余{{ remain }}</text>
@@ -256,6 +254,7 @@ onBeforeUnmount(() => {
height: 15px;
border-radius: 15px;
z-index: -1;
transition: all 1s linear;
}
.container > view:last-child > text {
font-size: 10px;

View File

@@ -1,7 +1,6 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import { RoundGoldImages } from "@/constants";
const props = defineProps({
tips: {
type: String,
@@ -20,59 +19,41 @@ const props = defineProps({
const barColor = ref("");
const remain = ref(15);
const timer = ref(null);
const loading = ref(false);
const transitionStyle = ref("all 1s linear");
const currentTeam = ref(null);
const updateRemain = (value) => {
if (value.stop) {
if (timer.value) clearInterval(timer.value);
return
}
loading.value = false;
currentTeam.value = value.team
if (value.team === 'red')
barColor.value = "linear-gradient( 180deg, #FFA0A0 0%, #FF6060 100%)";
if (value.team === 'blue')
barColor.value = "linear-gradient( 180deg, #9AB3FF 0%, #4288FF 100%)";
if (value.reset) {
remain.value = value.value;
return;
}
const newVal = Math.round(value.value);
// 如果剩余时间增加(如轮次切换重置),瞬间变化无动画;否则保持动画
if (newVal >= remain.value) {
transitionStyle.value = "none";
remain.value = newVal;
setTimeout(() => {
transitionStyle.value = "all 1s linear";
}, 50);
} else {
remain.value = newVal;
}
timer.value = setInterval(() => {
loading.value = remain.value === 0;
if (remain.value > 0) remain.value--;
}, 1000);
};
watch(
() => props.tips,
(newVal) => {
if (newVal.includes("红队"))
barColor.value = "linear-gradient( 180deg, #FFA0A0 0%, #FF6060 100%)";
if (newVal.includes("蓝队"))
barColor.value = "linear-gradient( 180deg, #9AB3FF 0%, #4288FF 100%)";
if (newVal.includes("红队") || newVal.includes("蓝队")) {
if (timer.value) clearInterval(timer.value);
remain.value = props.total;
timer.value = setInterval(() => {
if (remain.value > 0) remain.value--;
}, 1000);
}
},
{
immediate: true,
}
);
const updateRemain = (value) => {
if (timer.value) clearInterval(timer.value);
remain.value = Math.round(value);
timer.value = setInterval(() => {
if (remain.value > 0) remain.value--;
}, 1000);
};
onMounted(() => {
uni.$on("update-remain", updateRemain);
uni.$on("update-ramain", updateRemain);
});
onBeforeUnmount(() => {
uni.$off("update-remain", updateRemain);
uni.$off("update-ramain", updateRemain);
if (timer.value) clearInterval(timer.value);
});
</script>
@@ -80,21 +61,15 @@ onBeforeUnmount(() => {
<template>
<view class="container">
<image :src="RoundGoldImages[props.currentRound]" mode="widthFix" />
<view
:style="{
justifyContent: currentTeam==='red' ? 'flex-end' : 'flex-start',
}"
>
<view>
<view
:style="{
width: `${(remain / total) * 100}%`,
background: barColor,
right: currentTeam==='red' ? 0 : 'unset',
transition: transitionStyle,
right: tips.includes('红队') ? 0 : 'unset',
}"
/>
<text v-if="!loading">剩余{{ remain }}</text>
<text v-else>···</text>
<text>剩余{{ remain }}</text>
</view>
</view>
</template>
@@ -105,37 +80,33 @@ onBeforeUnmount(() => {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 12vw;
}
.container > image {
width: 380rpx;
height: 80rpx;
transform: translateY(18rpx);
width: 100%;
transform: translateY(7px);
}
.container > view:last-child {
width: 100%;
text-align: center;
background-color: #444444;
border-radius: 20px;
height: 24rpx;
font-size: 12px;
height: 15px;
line-height: 15px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
}
.container > view:last-child > view {
height: 24rpx;
border-radius: 15px;
}
.container > view:last-child > text {
font-size: 18rpx;
color: #fff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 15px;
border-radius: 15px;
transition: all 1s linear;
}
.container > view:last-child > text {
font-size: 10px;
line-height: 15px;
color: #fff;
position: relative;
}
</style>

View File

@@ -1,28 +1,13 @@
<script setup>
import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import SModal from "@/components/SModal.vue";
import Avatar from "@/components/Avatar.vue";
import SButton from "@/components/SButton.vue";
import { wxLogin } from "@/util";
import {
getMyDevicesAPI,
loginAPI,
getHomeData,
getPhoneNumberAPI,
getDeviceBatteryAPI,
} from "@/apis";
import { getMyDevicesAPI, loginAPI, getHomeData } from "@/apis";
import useStore from "@/store";
const store = useStore();
const { updateUser, updateDevice, updateOnline } = store;
const { updateUser, updateDevice } = store;
const props = defineProps({
show: {
type: Boolean,
default: false,
},
noBg: {
type: Boolean,
default: false,
@@ -33,7 +18,6 @@ const props = defineProps({
},
});
const agree = ref(false);
const phone = ref("");
const avatarUrl = ref("");
const nickName = ref("");
const loading = ref(false);
@@ -41,17 +25,6 @@ const handleAgree = () => {
agree.value = !agree.value;
};
async function getphonenumber(e) {
if (e.detail.code) {
const wxResult = await wxLogin();
const result = await getPhoneNumberAPI({
...e.detail,
code: wxResult.code,
});
if (result.phone) phone.value = result.phone;
}
}
function onChooseAvatar(e) {
avatarUrl.value = e.detail.avatarUrl;
}
@@ -60,14 +33,8 @@ function onNicknameChange(e) {
nickName.value = e.detail.value;
}
const handleLogin = async () => {
const handleLogin = () => {
if (loading.value) return;
if (!phone.value) {
return uni.showToast({
title: "请获取手机号",
icon: "none",
});
}
if (!avatarUrl.value) {
return uni.showToast({
title: "请选择头像",
@@ -86,17 +53,15 @@ const handleLogin = async () => {
icon: "none",
});
}
await doLogin();
};
async function doLogin() {
loading.value = true;
try {
const wxResult = await wxLogin();
uni.login({
provider: "weixin",
success: async (loginRes) => {
const { code } = loginRes;
const fileManager = uni.getFileSystemManager();
const avatarBase64 = fileManager.readFileSync(avatarUrl.value, "base64");
const base64Url = `data:image/png;base64,${avatarBase64}`;
await loginAPI(phone.value, nickName.value, base64Url, wxResult.code);
const result = await loginAPI(nickName.value, base64Url, code);
const data = await getHomeData();
if (data.user) updateUser(data.user);
const devices = await getMyDevicesAPI();
@@ -105,15 +70,18 @@ async function doLogin() {
devices.bindings[0].deviceId,
devices.bindings[0].deviceName
);
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
}
props.onClose();
} catch (error) {
console.log("login error", error);
} finally {
},
fail: (err) => {
loading.value = false;
}
uni.showToast({
title: "登录失败",
icon: "none",
});
console.error("登录失败:", err);
},
});
};
const openServiceLink = () => {
@@ -138,34 +106,12 @@ const openPrivacyLink = () => {
onShow(() => {
loading.value = false;
agree.value = false;
phone.value = "";
avatarUrl.value = "";
nickName.value = "";
});
</script>
<template>
<SModal :show="show" :onClose="onClose" :noBg="noBg">
<view class="container" :style="{ background: noBg ? '#fff' : 'none' }">
<view class="avatar" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff3' }">
<text :style="{ color: noBg ? '#666' : '#fff' }">手机:</text>
<button
:open-type="!phone ? 'getPhoneNumber' : ''"
@getphonenumber="getphonenumber"
class="login-btn"
hover-class="none"
>
<text v-if="phone" :style="{ color: noBg ? '#333' : '#fff' }">{{
phone
}}</text>
<text v-else :style="{ color: noBg ? '#666' : '#fff9' }"
>点击获取</text
>
<image src="../static/enter.png" mode="widthFix" />
</button>
</view>
<view class="avatar" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff3' }">
<view class="avatar" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff' }">
<text :style="{ color: noBg ? '#666' : '#fff' }">头像:</text>
<button
open-type="chooseAvatar"
@@ -174,21 +120,16 @@ onShow(() => {
hover-class="none"
>
<Avatar v-if="avatarUrl" :src="avatarUrl" :size="30" />
<text v-else :style="{ color: noBg ? '#666' : '#fff9' }"
>点击获取</text
>
<text v-else :style="{ color: noBg ? '#666' : '#fff9' }">点击获取</text>
<image src="../static/enter.png" mode="widthFix" />
</button>
</view>
<view
class="nickname"
:style="{ borderColor: noBg ? '#E3E3E3' : '#fff3' }"
>
<view class="nickname" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff' }">
<text :style="{ color: noBg ? '#666' : '#fff' }">昵称:</text>
<input
type="nickname"
placeholder="请输入昵称"
:placeholder-style="`color: ${noBg ? '#666' : '#fff9'} `"
:placeholder-style="{ color: noBg ? '#666' : '#fff9' }"
@change="onNicknameChange"
@blur="onNicknameBlur"
:style="{ color: noBg ? '#333' : '#fff' }"
@@ -196,7 +137,12 @@ onShow(() => {
</view>
<SButton :rounded="20" width="80vw" :onClick="handleLogin">
<block v-if="!loading">
<text :style="{ color: '#000' }">手机号快捷登录</text>
<image
src="../static/wechat-icon.png"
mode="widthFix"
class="wechat-icon"
/>
<text :style="{ color: '#000' }">登录/注册</text>
</block>
<block v-else>
<image
@@ -207,29 +153,25 @@ onShow(() => {
</block>
</SButton>
<view class="protocol" @click="handleAgree">
<view
v-if="!agree"
:style="{ borderColor: noBg ? '#E3E3E3' : '#fff' }"
/>
<view v-if="!agree" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff' }" />
<image v-if="agree" src="../static/checked.png" mode="widthFix" />
<view>
<text>已同意并阅读</text>
<view
@click.stop="openServiceLink"
:style="{ color: noBg ? '#333' : '#ffffff99' }"
:style="{ color: noBg ? '#333' : '#fff' }"
>用户协议</view
>
<text></text>
<view
@click.stop="openPrivacyLink"
:style="{ color: noBg ? '#333' : '#ffffff99' }"
:style="{ color: noBg ? '#333' : '#fff' }"
>隐私协议</view
>
<text>内容</text>
</view>
</view>
</view>
</SModal>
</template>
<style scoped>
@@ -249,7 +191,7 @@ onShow(() => {
display: flex;
align-items: center;
margin-bottom: 20px;
border-bottom: 1rpx solid #ffffff1a;
border-bottom: 1rpx solid #fff3;
}
.avatar {
margin: 0;
@@ -258,7 +200,7 @@ onShow(() => {
.nickname > text {
width: 20%;
font-size: 14px;
line-height: 120rpx;
line-height: 55px;
}
.avatar > button > text {
color: #fff9;
@@ -267,7 +209,7 @@ onShow(() => {
.nickname > input {
flex: 1;
font-size: 14px;
line-height: 120rpx;
line-height: 55px;
}
.wechat-icon {
width: 24px;
@@ -278,8 +220,8 @@ onShow(() => {
display: flex;
justify-content: center;
align-items: center;
font-size: 22rpx;
margin: 30rpx 0;
font-size: 13px;
margin-top: 15px;
color: #8a8a8a;
}
.protocol > image {
@@ -292,7 +234,7 @@ onShow(() => {
height: 14px;
border-radius: 50%;
margin-right: 10px;
border: 1px solid #fff;
border: 1rpx solid #fff;
}
.protocol > view:last-child {
display: flex;

View File

@@ -0,0 +1,60 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
const props = defineProps({
start: {
type: Boolean,
default: false,
},
});
const count = ref(4);
const timer = ref(null);
const isIos = ref(true);
watch(
() => props.start,
(newVal) => {
if (newVal) {
if (timer.value) clearInterval(timer.value);
count.value = 4;
timer.value = setInterval(() => {
if (count.value <= 1) {
clearInterval(timer.value);
}
count.value -= 1;
}, 1000);
}
},
{
immediate: true,
}
);
onMounted(() => {
const deviceInfo = uni.getDeviceInfo();
isIos.value = deviceInfo.osName === "ios";
});
onBeforeUnmount(() => {
if (timer.value) clearInterval(timer.value);
});
</script>
<template>
<view class="container" :style="{ top: `calc(50% - ${isIos ? 56 : 64}px)` }">
<view class="number pump-in" v-if="count === 3">3</view>
<view class="number pump-in" v-if="count === 2">2</view>
<view class="number pump-in" v-if="count === 1">1</view>
</view>
</template>
<style scoped>
.container {
position: absolute;
top: calc(50% - 64px);
left: calc(50% - 30px);
}
.number {
color: #fff9;
font-size: 88px;
width: 60px;
text-align: center;
}
</style>

View File

@@ -75,7 +75,7 @@ const handleChange = (e) => {
.dots {
position: absolute;
bottom: 5%;
bottom: 15%;
left: 50%;
transform: translateX(-50%);
display: flex;
@@ -90,6 +90,6 @@ const handleChange = (e) => {
}
.dot.active {
background-color: #fed847;
background-color: #000;
}
</style>

View File

@@ -1,197 +0,0 @@
<script setup>
import { ref, watch } from "vue";
import SButton from "@/components/SButton.vue";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
onClose: {
type: Function,
default: () => {},
},
onConfirm: {
type: Function,
default: () => {},
},
});
const selectedTarget = ref(2);
const showContainer = ref(false);
const showContent = ref(false);
watch(
() => props.show,
(newValue) => {
if (newValue) {
showContainer.value = true;
setTimeout(() => {
showContent.value = true;
}, 100);
} else {
showContent.value = false;
setTimeout(() => {
showContainer.value = false;
}, 100);
}
},
{}
);
const handleConfirm = () => {
props.onConfirm(selectedTarget.value);
props.onClose();
};
</script>
<template>
<view
class="container"
v-if="showContainer"
:class="{ 'container-show': showContent }"
@click="onClose"
>
<view
class="modal-content"
:class="{ 'modal-show': showContent }"
@click.stop=""
>
<view class="header">
<view class="header-title">
<view class="header-title-line-left"></view>
<text>选择靶型</text>
<view class="header-title-line-right"></view>
</view>
<view class="close-btn" @click="onClose">
<image src="../static/close-yellow.png" mode="widthFix" />
</view>
</view>
<view class="target-options">
<view
:class="{ 'target-btn': true, 'target-choosen': selectedTarget === 1 }"
@click="() => (selectedTarget = 1)"
>
<text>20厘米全环靶</text>
</view>
<view style="width: 30rpx"></view>
<view
:class="{ 'target-btn': true, 'target-choosen': selectedTarget === 2 }"
@click="() => (selectedTarget = 2)"
>
<text>40厘米全环靶</text>
</view>
</view>
<SButton width="694rpx" :onClick="handleConfirm">确定</SButton>
</view>
</view>
</template>
<style scoped>
.container {
position: fixed;
top: 0;
left: 0;
background-color: #00000099;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
opacity: 0;
transition: all 0.3s ease;
z-index: 999;
}
.container-show {
opacity: 1;
}
.modal-content {
width: 100%;
transform: translateY(100%);
transition: all 0.3s ease;
background: url("https://static.shelingxingqiu.com/attachment/2025-12-04/dep11770wzxg6o2alo.png")
no-repeat center top;
background-size: 100% auto;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
padding-bottom: 68rpx;
padding-top: 44rpx;
}
.modal-show {
transform: translateY(0%);
}
.header {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
margin-bottom: 44rpx;
}
.header-title{
display: flex;
align-items: center;
justify-content: center;
}
.header-title text{
width: 196rpx;
height: 40rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 28rpx;
text-align: center;
font-style: normal;
text-transform: none;
color: #FFEFBA;
}
.header-title-line-left{
width: 214rpx;
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 1rpx solid;
border-image: linear-gradient(90deg, rgba(133, 119, 96, 1), rgba(133, 119, 96, 0)) 1 1;
}
.header-title-line-right{
width: 214rpx;
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 1rpx solid;
border-image: linear-gradient(90deg, rgba(133, 119, 96, 1), rgba(133, 119, 96, 0)) 1 1;
}
.close-btn {
position: absolute;
right: 0;
top: -10px;
}
.close-btn > image {
width: 40px;
height: 40px;
}
.target-options {
width: 750rpx;
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 38rpx;
}
.target-btn {
width: 332rpx;
height: 92rpx;
text-align: center;
border-radius: 10px;
border: 2rpx solid #fff3;
box-sizing: border-box;
color: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.target-choosen {
color: #fed847;
border: 4rpx solid #fed847;
}
</style>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, watch, onMounted, computed } from "vue";
import { ref, watch, onMounted } from "vue";
const props = defineProps({
isRed: {
type: Boolean,
@@ -13,23 +13,15 @@ const props = defineProps({
type: Number,
default: "",
},
youTurn: {
type: Boolean,
default: false,
},
});
const players = ref({});
const currentTeam = ref(false);
const youTurn = ref(false);
const firstName = ref("");
// 抽出判断:当前队伍且该玩家排序为 0队伍首位
const isFirst = (id) =>
currentTeam.value && ((players.value[id] || {}).sort || 0) === 0;
const getPos = (id) => {
const sort = (players.value[id] || {}).sort || 0;
if (currentTeam.value) {
return 30 * (sort + Math.ceil(sort / 2));
}
return sort * 40;
};
onMounted(() => {
props.team.forEach((p, index) => {
players.value[p.id] = { sort: index, ...p };
@@ -41,7 +33,7 @@ watch(
(newVal) => {
if (!newVal) return;
const index = props.team.findIndex((p) => p.id === newVal);
currentTeam.value = index >= 0;
youTurn.value = index >= 0;
if (index >= 0) {
const newPlayers = [...props.team];
const target = newPlayers.splice(index, 1)[0];
@@ -63,38 +55,39 @@ watch(
<image
:src="isRed ? '../static/flag-red.png' : '../static/flag-blue.png'"
class="flag"
:style="{
[isRed ? 'left' : 'right']: '10rpx',
top: currentTeam ? '-36rpx' : '-24rpx',
}"
:style="{ [isRed ? 'left' : 'right']: '10rpx' }"
/>
<view
v-for="(item, index) in team"
:key="index"
class="player"
:style="{
width: (isFirst(item.id) ? 80 : 60) + 'rpx',
height: (isFirst(item.id) ? 80 : 60) + 'rpx',
zIndex: team.length - ((players[item.id] || {}).sort || 0),
border: isFirst(item.id) ? '3.5rpx solid' : '2rpx solid',
width:
(youTurn ? 40 - ((players[item.id] || {}).sort || 0) * 5 : 35) + 'px',
height:
(youTurn ? 40 - ((players[item.id] || {}).sort || 0) * 5 : 35) + 'px',
borderColor: isRed ? '#ff6060' : '#5fadff',
top: isFirst(item.id) ? '0rpx' : '12rpx',
[isRed ? 'left' : 'right']: getPos(item.id) + 'rpx',
zIndex: team.length - ((players[item.id] || {}).sort || 0),
top: youTurn ? ((players[item.id] || {}).sort || 0) * 2 + 'px' : '6px',
left:
(isRed
? ((players[item.id] || {}).sort || 0) * 20
: 40 - ((players[item.id] || {}).sort || 0) * 20) + 'px',
}"
>
<image :src="item.avatar || '../static/user-icon.png'" mode="widthFix" />
<text
v-if="isFirst(item.id)"
v-if="youTurn && ((players[item.id] || {}).sort || 0) === 0"
:style="{ backgroundColor: isRed ? '#ff6060' : '#5fadff' }"
>{{ isRed ? "红队" : "蓝队" }}</text
>
</view>
<text
v-if="currentTeam"
v-if="youTurn"
class="truncate"
:style="{
color: isRed ? '#ff6060' : '#5fadff',
[isRed ? 'left' : 'right']: '-4rpx',
[isRed ? 'left' : 'right']: 0,
}"
>{{ firstName }}</text
>
@@ -107,22 +100,22 @@ watch(
align-items: center;
position: relative;
width: 20vw;
height: 10rpx;
height: 45px;
margin: 0 20rpx;
}
.container > text {
position: absolute;
font-size: 20rpx;
font-size: 10px;
text-align: center;
width: 80rpx;
bottom: -100rpx;
width: 40px;
bottom: -12px;
}
.player {
transition: all 0.3s ease;
position: absolute;
border-radius: 50%;
overflow: hidden;
box-sizing: border-box;
border: 1px solid;
}
.player > image {
width: 100%;
@@ -130,17 +123,17 @@ watch(
}
.player > text {
position: absolute;
font-size: 15rpx;
font-size: 8px;
text-align: center;
width: 76rpx;
left: 0;
bottom: 0;
width: 40px;
left: 0px;
bottom: 0px;
color: #fff;
}
.flag {
position: absolute;
width: 45rpx;
height: 45rpx;
transition: all 0.3s ease;
top: -30rpx;
}
</style>

View File

@@ -0,0 +1,214 @@
<script setup>
import { ref, watch } from "vue";
import BowTarget from "@/components/BowTarget.vue";
import Avatar from "@/components/Avatar.vue";
import { roundsName } from "@/constants";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
onClose: {
type: Function,
default: () => {},
},
data: {
type: Object,
default: () => ({}),
},
});
const selected = ref(0);
const redScores = ref([]);
const blueScores = ref([]);
const tabs = ref(["所有轮次"]);
const players = ref([]);
const allRoundsScore = ref({});
const onClickTab = (index) => {
selected.value = index;
redScores.value = [];
blueScores.value = [];
const { bluePlayers, redPlayers, roundsData } = props.data;
if (index === 0) {
Object.keys(bluePlayers).forEach((p) => {
allRoundsScore.value[p] = [];
Object.values(roundsData).forEach((round) => {
allRoundsScore.value[p].push(
round[p].reduce((last, next) => last + next.ring, 0)
);
round[p].forEach((arrow) => {
blueScores.value.push(arrow);
});
});
});
Object.keys(redPlayers).forEach((p) => {
allRoundsScore.value[p] = [];
Object.values(roundsData).forEach((round) => {
allRoundsScore.value[p].push(
round[p].reduce((last, next) => last + next.ring, 0)
);
round[p].forEach((arrow) => {
redScores.value.push(arrow);
});
});
});
} else {
Object.keys(bluePlayers).forEach((p) => {
roundsData[index][p].forEach((arrow) => {
blueScores.value.push(arrow);
});
});
Object.keys(redPlayers).forEach((p) => {
roundsData[index][p].forEach((arrow) => {
redScores.value.push(arrow);
});
});
}
};
watch(
() => props.data,
(value) => {
if (value.winner === 0) {
players.value = [
...Object.values(value.redPlayers),
...Object.values(value.bluePlayers),
];
} else if (value.winner === 1) {
players.value = [
...Object.values(value.bluePlayers),
...Object.values(value.redPlayers),
];
}
Object.keys(value.roundsData).forEach((key) => {
tabs.value.push(`${roundsName[key]}`);
});
onClickTab(0);
},
{ deep: true, immediate: true }
);
</script>
<template>
<view class="container" :style="{ display: show ? 'flex' : 'none' }">
<view>
<text>1v1排位赛</text>
<view @click="onClose">
<image src="../static/close-white.png" mode="widthFix" />
</view>
</view>
<view>
<view
v-for="(tab, index) in tabs"
:key="index"
@click="() => onClickTab(index)"
:class="selected === index ? 'selected-tab' : ''"
>
{{ tab }}
</view>
</view>
<view :style="{ width: '95%' }">
<BowTarget :scores="redScores" :blueScores="blueScores" />
</view>
<view class="score-row" v-for="(player, index) in players" :key="index">
<Avatar
:src="player.avatar"
:borderColor="data.bluePlayers[player.playerId] ? 1 : 2"
/>
<view
v-if="selected === 0"
v-for="(ring, index) in allRoundsScore[player.playerId]"
:key="index"
class="score-item"
:style="{ width: '13vw', height: '13vw' }"
>
{{ ring }}
</view>
<view
v-if="selected > 0"
v-for="(score, index) in data.roundsData[selected][player.playerId]"
:key="index"
class="score-item"
:style="{ width: '13vw', height: '13vw' }"
>
{{ score.ring }}
</view>
</view>
</view>
</template>
<style scoped>
.container {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background-color: #232323;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
}
.container > view:first-child {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #fff;
position: relative;
font-size: 20px;
}
.container > view:first-child > view:last-child {
position: absolute;
right: 5px;
top: 32px;
}
.container > view:first-child > view:last-child > image {
width: 40px;
}
.container > view:nth-child(2) {
display: flex;
align-items: center;
justify-content: flex-start;
width: calc(100% - 20px);
color: #fff9;
padding: 0 10px;
overflow-x: auto;
}
.container > view:nth-child(2)::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
.container > view:nth-child(2) > view {
border: 1px solid #fff9;
border-radius: 20px;
padding: 7px 10px;
margin: 0 5px;
font-size: 14px;
flex: 0 0 auto;
}
.selected-tab {
background-color: #fed847;
border-color: #fed847 !important;
color: #000;
}
.score-row {
margin: 10px;
display: flex;
align-items: center;
justify-content: flex-start;
}
.score-item {
background-image: url("../static/score-bg.png");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
color: #fed847;
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
margin-left: 10px;
}
</style>

View File

@@ -5,7 +5,8 @@ import BowPower from "@/components/BowPower.vue";
import Avatar from "@/components/Avatar.vue";
import audioManager from "@/audioManager";
import { simulShootAPI } from "@/apis";
import { MESSAGETYPESV2 } from "@/constants";
import { checkConnection } from "@/util";
import { MESSAGETYPES } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
@@ -19,15 +20,13 @@ const props = defineProps({
type: Boolean,
default: false,
},
count: {
type: Number,
default: 15,
},
});
const arrow = ref({});
const power = ref(0);
const distance = ref(0);
const debugInfo = ref("");
const showsimul = ref(false);
const count = ref(props.count);
const count = ref(15);
const timer = ref(null);
const updateTimer = (value) => {
@@ -35,12 +34,10 @@ const updateTimer = (value) => {
};
onMounted(() => {
audioManager.play("请射箭测试距离");
if (props.isBattle) {
timer.value = setInterval(() => {
count.value -= 1;
if (count.value < 0) clearInterval(timer.value);
if (count.value > 0) count.value -= 1;
else clearInterval(timer.value);
}, 1000);
}
uni.$on("update-timer", updateTimer);
});
onBeforeUnmount(() => {
@@ -48,13 +45,19 @@ onBeforeUnmount(() => {
uni.$off("update-timer", updateTimer);
});
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.TestDistance) {
distance.value = Number((msg.shootData.distance / 100).toFixed(2));
if (distance.value >= 5) audioManager.play("距离合格");
else audioManager.play("距离不足");
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
arrow.value = msg.target;
power.value = msg.target.battery;
distance.value = Number((msg.target.dst / 100).toFixed(2));
debugInfo.value = msg.target;
audioManager.play("距离合格");
} else if (msg.constructor === MESSAGETYPES.InvalidShot) {
distance.value = Number((msg.target.dst / 100).toFixed(2));
audioManager.play("距离不足");
}
});
}
const simulShoot = async () => {
@@ -62,6 +65,7 @@ const simulShoot = async () => {
};
onMounted(() => {
checkConnection();
uni.$on("socket-inbox", onReceiveMessage);
const accountInfo = uni.getAccountInfoSync();
const envVersion = accountInfo.miniProgram.envVersion;
@@ -76,10 +80,19 @@ onBeforeUnmount(() => {
<template>
<view class="container">
<Guide v-show="guide">
<view class="guide-tips">
<text>请确保站距达到5米</text>
<view
:style="{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: '10px',
}"
>
<view :style="{ display: 'flex', flexDirection: 'column' }">
<text :style="{ color: '#fed847' }">请确保站距达到5米</text>
<text>低于5米的射箭无效</text>
</view>
</view>
</Guide>
<view class="test-area">
<image
@@ -107,17 +120,16 @@ onBeforeUnmount(() => {
</view>
<view class="user-row">
<Avatar :src="user.avatar" :size="35" />
<BowPower />
<BowPower :power="power" />
</view>
</view>
<view v-if="isBattle" class="ready-timer">
<image src="../static/test-tip.png" mode="widthFix" />
<view v-if="count >= 0">
<view>
<text>具体正式比赛还有</text>
<text>{{ count }}</text>
<text></text>
</view>
<view v-else> 进入中... </view>
</view>
</view>
</template>

View File

@@ -36,7 +36,6 @@ const toRankListPage = () => {
url: "/pages/rank-list",
});
};
watch(
() => [config.value, user.value],
([n_config, n_user]) => {
@@ -67,7 +66,7 @@ watch(
:onClick="toUserPage"
:size="42"
/>
<view class="user-details" @click="toUserPage">
<view class="user-details" :onClick="toUserPage">
<view class="user-name">
<text>{{ user.nickName }}</text>
<image
@@ -78,6 +77,7 @@ watch(
</view>
<view class="user-stats">
<text class="level-tag level-tag-first">段位积分</text>
<!-- <text class="level-tag level-tag-second">LV{{ user.lvl }}</text> -->
<view class="rank-tag">
<view
class="rank-tag-progress"
@@ -112,12 +112,12 @@ watch(
</view>
</block>
<block v-else>
<view class="signin" @click="onSignin">
<view class="signin">
<image src="../static/user-icon.png" mode="widthFix" />
<view>
<view @click="() => (showModal = true)">
<text>新来的弓箭手你好呀~</text>
<view>
<text>登录</text>
<view @click="onSignin">
<text>微信登录</text>
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
</view>
</view>
@@ -158,8 +158,7 @@ watch(
.user-name-image {
margin-left: 5px;
width: 40rpx;
height: 40rpx;
width: 20px;
}
.user-stats {
@@ -173,9 +172,13 @@ watch(
}
.level-tag-first {
padding: 0 10rpx;
width: 50px;
background: #5f51ff;
word-break: keep-all;
}
.level-tag-second {
width: 60rpx;
background: #09c504;
}
.level-tag,
@@ -187,17 +190,14 @@ watch(
.rank-tag {
position: relative;
background-color: #00000038;
width: 140rpx;
width: 150rpx;
overflow: hidden;
word-break: keep-all;
}
.rank-tag-progress {
background: #ffa711;
height: 100%;
border-radius: 12px;
width: 0;
transition: width 0.3s ease;
}
.rank-tag-text {
@@ -210,26 +210,24 @@ watch(
}
.rank-info {
width: 95px;
height: 50px;
font-size: 24rpx;
width: 70px;
text-align: left;
font-size: 12px;
position: relative;
color: #b3b3b3;
padding-left: 12px;
padding-left: 8px;
margin-left: 15rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.rank-info-image {
position: absolute;
top: 0;
left: 0;
width: 95px;
top: -6px;
left: -9px;
width: 90px;
}
.rank-info > text {
text-align: center;
word-break: keep-all;
width: 83px;
}
.rank-number {
display: block;

View File

@@ -24,35 +24,17 @@ export const MESSAGETYPES = {
LvlUpdate: 3958625354,
TeamUpdate: 4168086616,
InvalidShot: 4168086617,
Calibration: 4168086625,
DeviceOnline: 4168086626,
DeviceOffline: 4168086627,
SomeoneIsReady: 4168086628,
};
export const MESSAGETYPESV2 = {
AboutToStart: 1,
BattleStart: 2,
ToSomeoneShoot: 3,
ShootResult: 4,
NewRound: 5,
BattleEnd: 6,
HalfRest: 7,
TestDistance: 8,
MatchSuccess: 9,
InvalidShot: 10,
};
export const topThreeColors = ["#FFD947", "#D2D2D2", "#FFA515"];
export const getMessageTypeName = (id) => {
for (let key in MESSAGETYPES) {
if (MESSAGETYPES[key] === id) return key;
if (MESSAGETYPES[key] === id) {
return key;
}
for (let key in MESSAGETYPESV2) {
if (MESSAGETYPESV2[key] === id) return key;
}
return id;
return null;
};
export const roundsName = {
@@ -118,7 +100,7 @@ export const getBattleResultTips = (
) => {
const getRandomIndex = (len) => Math.floor(Math.random() * len);
if (gameMode === 1) {
if (mode <= 3) {
if (mode === 1) {
if (win) {
const tests = [
"https://static.shelingxingqiu.com/attachment/2025-08-01/dbqq1fglywucyoh9zn.png",
@@ -140,7 +122,7 @@ export const getBattleResultTips = (
];
return tests[getRandomIndex(3)];
}
} else {
} else if (mode === 2) {
if (rank <= 3) {
const tests = [
"好成绩!全国排位赛等着你!",
@@ -152,7 +134,7 @@ export const getBattleResultTips = (
}
}
} else if (gameMode === 2) {
if (mode <= 3) {
if (mode === 1) {
if (win) {
const tests = [
"https://static.shelingxingqiu.com/attachment/2025-08-01/dbqq1fgtb29jbdus4g.png",
@@ -174,7 +156,7 @@ export const getBattleResultTips = (
];
return tests[getRandomIndex(3)];
}
} else {
} else if (mode === 2) {
if (score > 0) {
const tests = [
"王者一定属于你!",

View File

@@ -1,6 +1,6 @@
{
"name": "shoot-miniprograms",
"appid" : "__UNI__B03E251",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
@@ -40,9 +40,7 @@
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {
"dSYMs" : false
},
"ios": {},
"sdkConfigs": {}
}
},

View File

@@ -4,22 +4,19 @@
"path": "pages/index"
},
{
"path": "pages/friend-battle"
"path": "pages/reset-password"
},
{
"path": "pages/point-book"
},
{
"path": "pages/point-book-rank"
"path": "pages/edit-profile"
},
{
"path": "pages/my-like-list"
"path": "pages/sign-in"
},
{
"path": "pages/audio-test"
},
{
"path": "pages/calibration"
"path": "pages/sign-up"
},
{
"path": "pages/about-us"
@@ -30,15 +27,12 @@
"navigationBarTitleText": ""
}
},
{
"path": "pages/team-battle"
},
{
"path": "pages/melee-battle"
},
{
"path": "pages/battle-result"
},
{
"path": "pages/team-battle"
},
{
"path": "pages/point-book-edit"
},
@@ -52,10 +46,10 @@
"path": "pages/point-book-detail"
},
{
"path": "pages/point-book-detail-share"
"path": "pages/match-page"
},
{
"path": "pages/match-page"
"path": "pages/image-share"
},
{
"path": "pages/my-device"
@@ -97,7 +91,13 @@
"path": "pages/practise-two"
},
{
"path": "pages/battle-room"
"path": "pages/friend-battle"
},
{
"path": "pages/battle-room",
"style": {
"disableSwipeBack": true
}
},
{
"path": "pages/ranking"
@@ -105,6 +105,12 @@
{
"path": "pages/rank-list"
},
{
"path": "pages/team-match"
},
{
"path": "pages/melee-match"
},
{
"path": "pages/match-detail"
},
@@ -119,7 +125,7 @@
}
],
"globalStyle": {
"backgroundColor": "@bgColor",
"backgroundColor": "#fff",
"backgroundColorBottom": "@bgColorBottom",
"backgroundColorTop": "@bgColorTop",
"backgroundTextStyle": "@bgTxtStyle",
@@ -129,11 +135,5 @@
"navigationStyle": "custom",
"enablePullDownRefresh": false
},
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
"subPackages": []
}

View File

@@ -1,7 +1,24 @@
<script setup>
import { ref, onMounted } from "vue";
import Container from "@/components/Container.vue";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const isIOS = uni.getDeviceInfo().osName === "ios";
const isIos = ref(false);
const openLink = () => {
// uni.navigateTo({
// url:
// "/pages/webview?url=" +
// encodeURIComponent("https://beian.miit.gov.cn/"),
// });
};
onMounted(() => {
const deviceInfo = uni.getDeviceInfo();
isIos.value = deviceInfo.osName === "ios";
});
</script>
<template>
@@ -17,7 +34,8 @@ const isIOS = uni.getDeviceInfo().osName === "ios";
<view
class="copyright"
:style="{ paddingBottom: isIOS ? '40rpx' : '20rpx' }"
:style="{ paddingBottom: isIos ? '30rpx' : '20rpx' }"
@click="openLink"
>
<text>粤ICP备2025421150号-2X</text>
</view>
@@ -28,7 +46,7 @@ const isIOS = uni.getDeviceInfo().osName === "ios";
<style scoped>
.container {
width: calc(100% - 50rpx);
height: calc(100% - 50rpx);
height: 100%;
padding: 25rpx;
background-color: #ffffff;
position: relative;

View File

@@ -1,68 +0,0 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import Container from "@/components/Container.vue";
import audioManager, { audioFils } from "@/audioManager";
const loaded = ref({});
const playAudio = (key) => {
audioManager.play(key);
};
onMounted(() => {
const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {};
loaded.value = loadedAudioKeys;
uni.$on("audioLoaded", (key) => {
loaded.value[key] = true;
});
});
onBeforeUnmount(() => {
uni.$off("audioLoaded");
});
</script>
<template>
<Container title="音频测试">
<view class="container">
<view>
<text>连续播放1</text>
<button hover-class="none" @click="playAudio(['第一轮', '请蓝方射箭'])">
播放
</button>
</view>
<view>
<text>连续播放2</text>
<button hover-class="none" @click="playAudio(['第二轮', '请红方射箭'])">
播放
</button>
</view>
<view v-for="key in Object.keys(audioFils)" :key="key">
<text>{{ key }}</text>
<text v-if="!loaded[key]">未加载</text>
<button v-else hover-class="none" @click="playAudio(key)">播放</button>
</view>
</view>
</Container>
</template>
<style scoped>
.container {
display: flex;
flex-direction: column;
width: 100%;
}
.container > view {
width: calc(100% - 50rpx);
display: flex;
align-items: center;
justify-content: space-between;
padding: 25rpx;
color: #fff;
border-bottom: 1rpx solid #fff9;
}
.container > view > button {
color: #fff;
}
</style>

View File

@@ -1,9 +1,9 @@
<script setup>
import { ref, computed, onMounted } from "vue";
import { ref, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Avatar from "@/components/Avatar.vue";
import UserUpgrade from "@/components/UserUpgrade.vue";
import { getBattleAPI } from "@/apis";
import { getGameAPI } from "@/apis";
import { topThreeColors, getBattleResultTips } from "@/constants";
import audioManager from "@/audioManager";
import useStore from "@/store";
@@ -16,108 +16,134 @@ const ifWin = ref(false);
const data = ref({});
const totalPoints = ref(0);
const rank = ref(0);
const players = ref([]);
function exit() {
if (data.value.roomId) {
uni.redirectTo({
url: `/pages/battle-room?roomNumber=${data.value.roomId}`,
});
} else {
uni.navigateBack();
}
}
onLoad(async (options) => {
if (!options.battleId) return;
const myId = user.value.id;
const result = await getBattleAPI(options.battleId || "60049406950510592");
data.value = result;
if (result.winTeam) {
ifWin.value = result.teams[result.winTeam].players.some(
(p) => p.id === myId
if (options.battleId) {
const result = await getGameAPI(
options.battleId || "BATTLE-1758270367040321900-868"
);
}
if (result.mode <= 3) {
audioManager.play(ifWin.value ? "胜利" : "失败");
} else {
players.value = result.resultList.map((item, index) => {
const plist = result.teams[0] ? result.teams[0].players : [];
const p = plist.find((p) => p.id === item.userId);
if (p.id === user.value.id) {
totalPoints.value = p.score;
rank.value = index + 1;
}
return {
...item,
rank: index + 1,
name: p.name,
avatar: p.avatar || "",
data.value = {
...result,
battleMode: result.gameMode,
};
});
if (rank.value <= players.value * 0.3) {
audioManager.play("胜利");
if (result.mode === 1) {
data.value.redPlayers = Object.values(result.redPlayers);
data.value.bluePlayers = Object.values(result.bluePlayers);
if (result.redPlayers[myId]) {
totalPoints.value = result.redPlayers[myId].totalScore;
data.value.myTeam = result.redPlayers[myId].team;
ifWin.value = result.winner === 0;
}
if (result.bluePlayers[myId]) {
totalPoints.value = result.bluePlayers[myId].totalScore;
data.value.myTeam = result.bluePlayers[myId].team;
ifWin.value = result.winner === 1;
}
}
if (result.mode === 2) {
data.value.playerStats = result.players.map((p) => ({
...p,
id: p.playerId,
}));
const mine = result.players.find((p) => p.playerId === myId);
if (mine) totalPoints.value = mine.totalScore;
rank.value = result.players.findIndex((p) => p.playerId === myId) + 1;
}
} else {
const battleInfo = uni.getStorageSync("last-battle");
if (!battleInfo) return;
data.value = {
mvps: [],
...battleInfo,
};
if (battleInfo.mode === 1) {
battleInfo.playerStats.forEach((p) => {
if (p.team === 1) data.value.bluePlayers = [p];
if (p.team === 0) data.value.redPlayers = [p];
if (p.mvp) data.value.mvps.push(p);
});
data.value.mvps.sort((a, b) => b.totalRings - a.totalRings);
}
rank.value = 0;
const mine = battleInfo.playerStats.find((p, index) => {
rank.value = index + 1;
return p.id === myId;
});
if (mine) {
data.value.myTeam = mine.team;
totalPoints.value = mine.totalScore;
if (battleInfo.mode === 1) {
ifWin.value = mine.team === battleInfo.winner;
}
}
}
if (data.value.mode === 1) {
audioManager.play(ifWin.value ? "胜利" : "失败");
} else if (data.value.mode === 2) {
if (data.value.battleMode === 1) {
if (rank.value <= data.value.playerStats.length * 0.3) {
audioManager.play("胜利");
}
} else if (data.value.battleMode === 2) {
if (totalPoints.value > 0) {
audioManager.play("胜利");
} else if (totalPoints.value < 0) {
audioManager.play("失败");
}
}
});
const myTeam = computed(() => {
const teams = data.value.teams;
if (teams && teams.length) {
if (teams[1].players.some((p) => p.id === user.value.id)) return 1;
}
return 2;
});
const checkBowData = () => {
uni.navigateTo({
url: `/pages/match-detail?battleId=${data.value.matchId}`,
url: `/pages/match-detail?id=${data.value.id}`,
});
};
</script>
<template>
<view class="container">
<block v-if="data.mode <= 3">
<block v-if="data.mode === 1">
<view class="header-team" :style="{ marginTop: '25%' }">
<image src="../static/battle-result.png" mode="widthFix" />
<view class="header-solo" v-if="data.mode === 1">
<view class="header-solo" v-if="data.teamSize === 2">
<text
:style="{
background:
data.winTeam === 1
data.winner === 1
? 'linear-gradient(270deg, #3597ff 0%, rgba(0,0,0,0) 100%);'
: 'linear-gradient(270deg, #fd4444 0%, rgba(0, 0, 0, 0) 100%)',
}"
>{{ data.winTeam === 1 ? "蓝队" : "红队" }}获胜</text
>{{ data.winner === 1 ? "蓝队" : "红队" }}获胜</text
>
<Avatar
:size="32"
:src="
data.winTeam === 1
? data.teams[1].players[0].avatar
: data.teams[2].players[0].avatar
data.winner === 1
? data.bluePlayers[0].avatar
: data.redPlayers[0].avatar
"
:borderColor="data.winTeam === 1 ? '#5FADFF' : '#FF5656'"
:borderColor="data.winner === 1 ? '#5FADFF' : '#FF5656'"
mode="widthFix"
/>
</view>
</view>
<view class="header-mvp" v-if="data.mode === 2 || data.mode === 3">
<view class="header-mvp" v-if="data.teamSize !== 2">
<image
:src="`../static/${data.winTeam === 1 ? 'blue' : 'red'}-team-win.png`"
:src="`../static/${data.winner === 1 ? 'blue' : 'red'}-team-win.png`"
mode="widthFix"
/>
<view
:style="{
transform: `translateY(50px) rotate(-${
5 + (data.mvp || []).length
}deg)`,
transform: `translateY(50px) rotate(-${5 + data.mvps.length}deg)`,
}"
>
<view v-if="data.mvp && data.mvp[0].totalRings">
<view v-if="data.mvps && data.mvps[0].totalRings">
<image src="../static/title-mvp.png" mode="widthFix" />
<text
>斩获<text
@@ -127,22 +153,22 @@ const checkBowData = () => {
margin: '0 3px',
fontWeight: '600',
}"
>{{ data.mvp[0].totalRings }}</text
>{{ data.mvps[0].totalRings }}</text
></text
>
</view>
<view v-if="data.mvp && data.mvp.length">
<view v-for="(player, index) in data.mvp" :key="index">
<view v-if="data.mvps && data.mvps.length">
<view v-for="(player, index) in data.mvps" :key="index">
<view class="team-avatar">
<Avatar
:src="player.avatar"
:size="40"
:borderColor="myTeam === 1 ? '#5fadff' : '#ff6060'"
:borderColor="data.myTeam === 1 ? '#5fadff' : '#ff6060'"
/>
<text
v-if="player.id === user.id"
:style="{
backgroundColor: myTeam === 1 ? '#5fadff' : '#ff6060',
backgroundColor: data.myTeam === 1 ? '#5fadff' : '#ff6060',
}"
>自己</text
>
@@ -161,7 +187,7 @@ const checkBowData = () => {
/>
<image
:src="
getBattleResultTips(data.way, data.mode, {
getBattleResultTips(data.battleMode, data.mode, {
win: ifWin,
})
"
@@ -170,7 +196,7 @@ const checkBowData = () => {
/>
</view>
</block>
<block v-else>
<block v-if="data.mode === 2">
<view class="header-melee">
<view />
<image src="../static/battle-result.png" mode="widthFix" />
@@ -179,11 +205,11 @@ const checkBowData = () => {
<view
class="players"
:style="{
height: `${Math.max(players.length > 5 ? '330' : '300')}px`,
height: `${Math.max(data.playerStats.length > 5 ? '330' : '300')}px`,
}"
>
<view
v-for="(player, index) in players"
v-for="(player, index) in data.playerStats"
:key="index"
:style="{
border: player.id === user.id ? '1px solid #B04630' : 'none',
@@ -225,9 +251,7 @@ const checkBowData = () => {
src="../static/champ3.png"
mode="widthFix"
/>
<view v-if="player.rank > 3" class="view-crown">{{
player.rank
}}</view>
<view v-if="index > 2" class="view-crown">{{ index + 1 }}</view>
<Avatar
:src="player.avatar"
:size="36"
@@ -235,10 +259,10 @@ const checkBowData = () => {
/>
<view class="player-title">
<text class="truncate">{{ player.name }}</text>
<text>{{ getLvlName(player.rank_lvl) }}</text>
<text>{{ getLvlName(player.totalScore) }}</text>
</view>
<text
><text :style="{ color: '#fff' }">{{ player.totalRing }}</text>
><text :style="{ color: '#fff' }">{{ player.totalRings }}</text>
</text
>
</view>
@@ -246,36 +270,36 @@ const checkBowData = () => {
</block>
<view
class="battle-e"
:style="{ marginTop: data.mode > 3 ? '20px' : '20vw' }"
:style="{ marginTop: data.mode === 2 ? '20px' : '20vw' }"
>
<image src="../static/row-yellow-bg.png" mode="widthFix" />
<view class="team-avatar">
<Avatar
:src="user.avatar"
:size="40"
:borderColor="myTeam === 1 ? '#5fadff' : '#ff6060'"
:borderColor="data.myTeam === 1 ? '#5fadff' : '#ff6060'"
/>
<text
:style="{ backgroundColor: '#5fadff' }"
v-if="data.mode <= 3 && myTeam === 1"
v-if="data.mode === 1 && data.myTeam === 1"
>蓝队</text
>
<text
:style="{ backgroundColor: '#ff6060' }"
v-if="data.mode <= 3 && myTeam === 2"
v-if="data.mode === 1 && data.myTeam === 0"
>红队</text
>
</view>
<text v-if="data.way === 1">
<text v-if="data.battleMode === 1">
你的经验 {{ totalPoints > 0 ? "+" + totalPoints : totalPoints }}
</text>
<text v-if="data.way === 2">
<text v-if="data.battleMode === 2">
你的积分 {{ totalPoints > 0 ? "+" + totalPoints : totalPoints }}
</text>
</view>
<text v-if="data.mode > 3" class="description">
<text v-if="data.mode === 2" class="description">
{{
getBattleResultTips(data.way, data.mode, {
getBattleResultTips(data.battleMode, data.mode, {
win: ifWin,
score: totalPoints,
rank,
@@ -284,7 +308,7 @@ const checkBowData = () => {
</text>
<view class="op-btn">
<view @click="checkBowData">查看成绩</view>
<view @click="exit">返回</view>
<view @click="exit">退出</view>
</view>
<UserUpgrade />
</view>
@@ -394,7 +418,6 @@ const checkBowData = () => {
border-radius: 20px;
padding: 10px 0;
text-align: center;
color: #000;
}
.op-btn > view:last-child {
color: #fff;

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ import { ref, onMounted, onBeforeUnmount } from "vue";
import Container from "@/components/Container.vue";
import Avatar from "@/components/Avatar.vue";
import SButton from "@/components/SButton.vue";
import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue";
import UserHeader from "@/components/UserHeader.vue";
import { createOrderAPI, getHomeData, getVIPDescAPI } from "@/apis";
@@ -79,6 +80,7 @@ onBeforeUnmount(() => {
<template>
<Container title="会员说明">
<view :style="{ width: '100%', height: '100%' }">
<view v-if="user.id" class="header">
<view>
<Avatar :src="user.avatar" :size="35" />
@@ -104,7 +106,7 @@ onBeforeUnmount(() => {
</view>
<view
class="container"
:style="{ height: !user.id ? 'calc(100% - 10px)' : 'calc(100% - 62px)' }"
:style="{ height: !user.id ? '100%' : 'calc(100% - 62px)' }"
>
<view class="content vip-content">
<view class="title-bar">
@@ -113,6 +115,16 @@ onBeforeUnmount(() => {
</view>
<view :style="{ marginTop: '10rpx' }">
<rich-text :nodes="richContent" />
<!-- <text
>射灵星球VIP服务为全球弓箭手提供约战段位评级实时排位赛智能教练点评等专属特权会员可在酷帅的真实射箭运动中同步享受在线竞技的乐趣还能找到志同道合的伙伴并获得新鲜的功能体验和持续升级的系统
</text>
<text
>所有新注册用户我们都会默认赠送6个月超长会员到期之后可续费单月10元年度VIP100元我们鼓励每一位弓箭手长期坚持练习这项运动在对战的世界中尽情驰骋不断挑战自我创造属于自己的辉煌战绩
</text>
<text
>VIP会员还将获得专属客服支持当您在游戏中遇到任何问题无论是技术故障规则疑问还是其他需要帮助的情况都可联系我们的VIP专属客服团队他们将提供全年不间断的优质服务确保您的对战体验不受影响
</text>
<text>期待您的加入</text> -->
</view>
</view>
<view class="content">
@@ -139,13 +151,16 @@ onBeforeUnmount(() => {
</view>
</view>
<SButton :onClick="onPay">支付</SButton>
<SModal :show="showModal" :onClose="() => (showModal = false)">
<Signin :onClose="() => (showModal = false)" />
</SModal>
<view class="my-orders" v-if="user.id">
<view @click="toOrderPage">
<text>我的订单</text>
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
</view>
</view>
<Signin :show="showModal" :onClose="() => (showModal = false)" />
</view>
</view>
</Container>
</template>

View File

@@ -1,116 +0,0 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import Container from "@/components/Container.vue";
import SButton from "@/components/SButton.vue";
import { laserAimAPI, laserCloseAPI } from "@/apis";
import { MESSAGETYPES } from "@/constants";
// import audioManager from "@/audioManager";
const guides = [
{
title: "箭头面向靶子",
src: "https://static.shelingxingqiu.com/attachment/2025-10-30/ddv9p5fk5wscg7hrfo.png",
},
{
title: "摆出拉弓姿势",
src: "https://static.shelingxingqiu.com/attachment/2025-10-30/ddv9p5fk5b7ljrhx3o.png",
},
{
title: "调整瞄准器",
src: "https://static.shelingxingqiu.com/attachment/2025-10-29/dduexjgrcxf9wjaiv4.png",
},
];
const done = ref(true);
const onComplete = async () => {
await laserCloseAPI();
uni.navigateBack();
};
function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.Calibration) {
done.value = true;
uni.setStorageSync("calibration", true);
// audioManager.play("激光已校准");
}
});
}
onMounted(async () => {
uni.$on("socket-inbox", onReceiveMessage);
await laserAimAPI();
});
onBeforeUnmount(async () => {
uni.$off("socket-inbox", onReceiveMessage);
await laserCloseAPI();
});
</script>
<template>
<Container title="校准智能弓">
<view class="container">
<view v-for="(guide, index) in guides" :key="guide.title" class="guide">
<view>
<text>{{ index + 1 }}</text>
<text>{{ guide.title }}</text>
</view>
<image :src="guide.src" mode="widthFix" />
</view>
<text>请完成以上步骤校准智能弓</text>
<SButton
:onClick="onComplete"
width="60vw"
:rounded="40"
:disabled="!done"
>
我已校准
</SButton>
</view>
</Container>
</template>
<style scoped>
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.guide {
display: flex;
flex-direction: column;
align-items: center;
font-size: 26rpx;
color: #ffffff;
margin-bottom: 15rpx;
}
.guide > view {
width: 100%;
margin: 25rpx 0;
display: flex;
align-items: center;
}
.guide > view > text:first-child {
font-size: 24rpx;
background: #e89024;
border-radius: 50%;
width: 32rpx;
height: 32rpx;
line-height: 32rpx;
display: block;
text-align: center;
margin-right: 15rpx;
}
.guide > image {
width: 630rpx;
height: 250rpx;
}
.container > text {
font-size: 24rpx;
color: #fff9;
margin: 30rpx;
}
</style>

View File

@@ -2,21 +2,24 @@
import { ref, onMounted } from "vue";
import SButton from "@/components/SButton.vue";
import { capsuleHeight } from "@/util";
const images = [
"https://static.shelingxingqiu.com/mall/images/mall_01.jpg",
"https://static.shelingxingqiu.com/mall/images/mall_02.jpg",
"https://static.shelingxingqiu.com/mall/images/mall_03.jpg",
"https://static.shelingxingqiu.com/mall/images/mall_04.jpg",
"https://static.shelingxingqiu.com/mall/images/mall_05.jpg",
"https://static.shelingxingqiu.com/mall/images/mall_06.jpg",
"https://static.shelingxingqiu.com/mall/images/mall_07.jpg",
"https://static.shelingxingqiu.com/mall/images/mall_08.jpg",
"https://static.shelingxingqiu.com/mall/images/mall_09.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmf6yitekatwe.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmi475gqdtrvx.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmgy8ej5wuap5.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmg6y7nveaadv.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmfhqew0xhy6i.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmhs38abrqfyp.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmgnj4rttovk3.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmg68a8mezgzx.jpg",
"https://static.shelingxingqiu.com/attachment/2025-10-14/ddht51a3hiyw7ueli4.jpg",
];
const addBg = ref(false);
const addBg = ref("");
const capsuleHeight = ref(0);
onMounted(async () => {
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
capsuleHeight.value = menuBtnInfo.top - 9;
});
const onScrollView = (e) => {
addBg.value = e.detail.scrollTop > 100;
@@ -32,7 +35,8 @@ const onScrollView = (e) => {
}"
>
<image
:style="{ opacity: addBg ? 1 : 0 }"
v-if="addBg"
class="bg-image"
src="../static/app-bg.png"
mode="widthFix"
/>
@@ -42,17 +46,12 @@ const onScrollView = (e) => {
<text
:style="{ opacity: addBg ? 1 : 0, color: '#fff', fontWeight: 'bold' }"
>
本赛季排行榜
</text>
</view>
<scroll-view scroll-y @scroll="onScrollView" :style="{ height: '100vh' }">
<view class="images">
<image
v-for="src in images"
:key="src"
:src="src"
mode="widthFix"
show-menu-by-longpress
/>
<image v-for="src in images" :key="src" :src="src" mode="widthFix" show-menu-by-longpress />
</view>
</scroll-view>
</view>
@@ -72,6 +71,7 @@ const onScrollView = (e) => {
align-items: center;
position: fixed;
top: 0;
transition: all 0.3s ease;
z-index: 10;
overflow: hidden;
}
@@ -82,19 +82,12 @@ const onScrollView = (e) => {
margin-top: 5px;
position: relative;
}
.header > image:first-child {
.bg-image {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
transition: all 0.5s ease;
}
.header > text {
color: #fff;
font-weight: bold;
transition: all 0.5s ease;
position: relative;
}
.images {
display: flex;

102
src/pages/edit-profile.vue Normal file
View File

@@ -0,0 +1,102 @@
<script setup>
import { ref, onMounted, reactive } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
const type = ref("");
const formData = reactive({
name: "",
email: "",
code: "",
password: "",
confirmPassword: "",
});
onLoad((options) => {
type.value = options.type;
});
</script>
<template>
<Container
:bgType="2"
bgColor="#F5F5F5"
:whiteBackArrow="false"
:title="`Edit ${type}`"
>
<view v-if="type === 'Name'" class="input-view input-row">
<input
v-model="formData.name"
placeholder="name"
placeholder-style="color:#999;"
/>
<text>{{ formData.name.length }}/30</text>
</view>
<view v-else-if="type === 'Email'" class="input-view">
<view class="input-row">
<input
v-model="formData.email"
placeholder="email"
placeholder-style="color:#999;"
/>
</view>
<view class="input-row">
<input
v-model="formData.code"
placeholder="verification code"
placeholder-style="color:#999;"
/>
<button hover-class="none">get verification code</button>
</view>
</view>
<view v-else-if="type === 'Password'" class="input-view">
<view class="input-row">
<input
v-model="formData.password"
placeholder="password"
placeholder-style="color:#999;"
/>
</view>
<view class="input-row">
<input
v-model="formData.confirmPassword"
placeholder="Confirm your password"
placeholder-style="color:#999;"
/>
</view>
</view>
</Container>
</template>
<style scoped lang="scss">
.container {
width: 100%;
display: flex;
flex-direction: column;
}
.input-view {
padding: 0 30rpx;
border-radius: 25rpx;
color: $uni-text-color-grey;
background: $uni-bg-color;
margin-top: 25rpx;
width: calc(100% - 100rpx);
}
.input-view > view:not(:first-child) {
border-top: 1rpx solid #e3e3e3;
}
.input-row {
display: flex;
align-items: center;
font-size: 26rpx;
}
.input-row > input {
padding: 30rpx 0;
flex: 1;
}
.input-row > button {
color: $uni-link-color;
font-size: 26rpx;
line-height: 36rpx;
}
</style>

View File

@@ -12,16 +12,9 @@ import Avatar from "@/components/Avatar.vue";
import BowPower from "@/components/BowPower.vue";
import TestDistance from "@/components/TestDistance.vue";
import BubbleTip from "@/components/BubbleTip.vue";
import audioManager from "@/audioManager";
import {
createPractiseAPI,
startPractiseAPI,
endPractiseAPI,
getPractiseAPI,
} from "@/apis";
import { sharePractiseData } from "@/canvas";
import { wxShare, debounce } from "@/util";
import { MESSAGETYPESV2 } from "@/constants";
import { createPractiseAPI } from "@/apis";
import { generateCanvasImage } from "@/util";
import { MESSAGETYPES } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
@@ -40,18 +33,20 @@ const stepButtonTexts = [
const title = ref("新手试炼场");
const start = ref(false);
const practiseResult = ref({});
const power = ref(0);
const btnDisabled = ref(false);
const practiseId = ref("");
const showGuide = ref(false);
const guideImages = [
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wwdpgwt9e6du.png",
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wvv9sw4zioqk.png",
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0ww3khaycallu.png",
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wtkcvaxxv0s8.png",
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wry5tw7ltmxr.png",
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wu3kcdrwzwpd.png",
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wwr6hfjhyfn5.png",
"https://static.shelingxingqiu.com/attachment/2025-07-09/db77x68bs7z5elwvw7.png",
"https://static.shelingxingqiu.com/attachment/2025-07-09/db77x68qmi7grgreen.png",
"https://static.shelingxingqiu.com/attachment/2025-07-09/db77x68hgrw1ip4wae.png",
"https://static.shelingxingqiu.com/attachment/2025-07-09/db77x684x8zmfrmbla.png",
"https://static.shelingxingqiu.com/attachment/2025-07-09/db77x67sding7fodnk.png",
"https://static.shelingxingqiu.com/attachment/2025-07-09/db77x68mpug7cac4yt.png",
"https://static.shelingxingqiu.com/attachment/2025-07-09/db77x68my783mlmgxv.png",
"https://static.shelingxingqiu.com/attachment/2025-07-09/db77x68p48ylzirtb0.png",
];
const onSwiperIndexChange = (index) => {
@@ -61,53 +56,48 @@ const onSwiperIndexChange = (index) => {
};
const createPractise = async (arrows) => {
const result = await createPractiseAPI(arrows, 1);
const result = await createPractiseAPI(arrows);
if (result) practiseId.value = result.id;
};
const onOver = async () => {
practiseResult.value = await getPractiseAPI(practiseId.value);
start.value = false;
};
async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult) {
scores.value = msg.details;
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(onOver, 1500);
} else if (msg.type === MESSAGETYPESV2.TestDistance) {
if (msg.shootData.distance / 100 >= 5) {
audioManager.play("距离合格");
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
if (scores.value.length < total) {
scores.value.push(msg.target);
}
power.value = msg.target.battery;
// if (step.value === 2 && msg.target.dst / 100 >= 5) {
btnDisabled.value = false;
showGuide.value = true;
} else audioManager.play("距离不足");
// }
}
if (msg.constructor === MESSAGETYPES.ShootSyncMePracticeID) {
if (practiseId.value && practiseId.value === msg.practice.id) {
setTimeout(() => {
start.value = false;
practiseResult.value = {
...msg.practice,
arrows: JSON.parse(msg.practice.arrows),
lvl: msg.lvl,
};
generateCanvasImage(
"shareCanvas",
1,
user.value,
practiseResult.value
);
}, 1500);
}
// messages.forEach((msg) => {
// if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
// if (step.value === 2 && msg.target.dst / 100 >= 5) {
// btnDisabled.value = false;
// showGuide.value = true;
// } else if (scores.value.length < total) {
// scores.value.push(msg.target);
// }
// if (scores.value.length === total) {
// setTimeout(onOver, 1500);
// }
// }
// });
}
const onClickShare = debounce(async () => {
await sharePractiseData("shareCanvas", 1, user.value, practiseResult.value);
await wxShare("shareCanvas");
});
}
onMounted(() => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("share-image", onClickShare);
});
onBeforeUnmount(() => {
@@ -115,9 +105,6 @@ onBeforeUnmount(() => {
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
uni.$off("share-image", onClickShare);
audioManager.stopAll();
endPractiseAPI();
});
const nextStep = async () => {
@@ -129,15 +116,13 @@ const nextStep = async () => {
btnDisabled.value = true;
step.value = 2;
title.value = "-感知距离";
const result = await createPractiseAPI(total, 120);
if (result) practiseId.value = result.id;
} else if (step.value === 2) {
showGuide.value = false;
step.value = 3;
title.value = "-小试牛刀";
} else if (step.value === 3) {
title.value = "小试牛刀";
await startPractiseAPI();
await createPractise(total);
scores.value = [];
step.value = 4;
start.value = true;
@@ -151,11 +136,11 @@ const nextStep = async () => {
}
};
const onClose = async () => {
const validArrows = (practiseResult.value.details || []).filter(
(a) => a.x !== -30 && a.y !== -30
);
if (validArrows.length === total) {
const onClose = () => {
if (
practiseResult.value.arrows &&
practiseResult.value.arrows.length === total
) {
setTimeout(() => {
practiseResult.value = {};
showGuide.value = false;
@@ -166,14 +151,12 @@ const onClose = async () => {
start.value = false;
scores.value = [];
step.value = 3;
const result = await createPractiseAPI(total, 120);
if (result) practiseId.value = result.id;
}
};
</script>
<template>
<Container :bgType="1" :title="title" :showBottom="step !== 4">
<Container :bgType="1" :title="title">
<view class="container">
<Guide
v-if="step !== 4"
@@ -185,40 +168,37 @@ const onClose = async () => {
: 0
"
>
<text
v-if="step === 0"
:style="{
fontSize: '28rpx',
marginTop: user.nickName.length > 6 ? '-10rpx' : '0',
}"
>
<text v-if="step === 0">
hi<text :style="{ color: '#fed847' }">{{ user.nickName }}</text>
这是新人必刷小任务0基础小白也能快速掌握弓箭技巧和游戏规则哦~
</text>
<text v-if="step === 1" :style="{ fontSize: '28rpx' }"
<text v-if="step === 1"
>这是我们人帅技高的高教练首先请按教练示范尝试自己去做这些动作和手势吧</text
>
<view
class="guide-tips"
:style="{ marginTop: '8rpx' }"
v-if="step === 2"
>
<text>你知道5米射程有多远吗</text>
<view v-if="step === 2">
<view :style="{ display: 'flex', flexDirection: 'column' }">
<text :style="{ color: '#fed847' }">你知道5米射程有多远吗</text>
<text>
在我们的排位赛中射程小于5米的成绩无效建议平时练习距离至少5米现在来边射箭边调整你的站位点吧
</text>
</view>
<view class="guide-tips" v-if="step === 3">
<text>一切准备就绪</text>
<text :style="{ fontSize: '28rpx' }"
>试着完成一个真正的弓箭手任务吧</text
>
</view>
<view class="guide-tips" v-if="step === 5">
<text>新手试炼场通关啦优秀</text>
<text :style="{ fontSize: '28rpx' }"
<view v-if="step === 3">
<view :style="{ display: 'flex', flexDirection: 'column' }">
<text :style="{ color: '#fed847' }">一切准备就绪</text>
<text>试着完成一个真正的弓箭手任务吧</text>
</view>
</view>
<view v-if="step === 5">
<view
:style="{ display: 'flex', flexDirection: 'column', marginTop: 20 }"
>
<text :style="{ color: '#fed847' }">新手试炼场通关啦优秀</text>
<text
>反曲弓运动基本知识和射灵世界系统规则你已Get是不是挺容易呀</text
>
<text :style="{ opacity: 0 }">新手试炼场通关啦优秀</text>
</view>
</view>
</Guide>
<image
@@ -228,7 +208,7 @@ const onClose = async () => {
v-if="step === 0"
/>
<image
src="https://static.shelingxingqiu.com/attachment/2025-11-17/deas80ef1sf9td0leq.png"
src="https://static.shelingxingqiu.com/attachment/2025-07-01/db0ehpzl8hfzeswfrf.png"
class="try-tip"
mode="widthFix"
v-if="step === 3"
@@ -250,7 +230,7 @@ const onClose = async () => {
:style="{ marginBottom: step === 2 ? '40px' : '0' }"
>
<Avatar :src="user.avatar" :size="35" />
<BowPower />
<BowPower :power="power" />
</view>
<BowTarget
v-if="step === 4"
@@ -262,26 +242,22 @@ const onClose = async () => {
v-if="step === 4"
:total="total"
:rowCount="6"
:arrows="scores"
:scores="scores.map((s) => s.ring)"
/>
<ScoreResult
v-if="practiseResult.details"
v-if="practiseResult.arrows"
:rowCount="6"
:total="total"
:onClose="onClose"
:result="practiseResult"
:tipSrc="`../static/${
practiseResult.details.filter(
(arrow) => arrow.x !== -30 && arrow.y !== -30
).length < total
? 'un'
: ''
practiseResult.arrows.length < total ? 'un' : ''
}finish-tip.png`"
/>
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
<canvas class="share-canvas" canvas-id="shareCanvas"></canvas>
</view>
<template #bottom>
<SButton :onClick="nextStep" :disabled="btnDisabled">
<view :style="{ marginBottom: '20px' }">
<SButton v-if="step !== 4" :onClick="nextStep" :disabled="btnDisabled">
<BubbleTip v-if="showGuide" :type="step === 1 ? 'long' : 'short'">
<text :style="{ transform: 'translateY(-18rpx)' }">{{
step === 1 ? "学会了,我摆得比教练还帅" : "我找到合适的点位了"
@@ -289,7 +265,7 @@ const onClose = async () => {
</BubbleTip>
{{ stepButtonTexts[step] }}
</SButton>
</template>
</view>
</Container>
</template>

View File

@@ -1,111 +1,86 @@
<script setup>
import { ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import GuideTwo from "@/components/GuideTwo.vue";
import Guide from "@/components/Guide.vue";
import SButton from "@/components/SButton.vue";
import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue";
import CreateRoom from "@/components/CreateRoom.vue";
import Avatar from "@/components/Avatar.vue";
import { getRoomAPI, joinRoomAPI, getBattleDataAPI } from "@/apis";
import { debounce, canEenter } from "@/util";
import { getRoomAPI, joinRoomAPI, isGamingAPI, getBattleDataAPI } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, device, online, game } = storeToRefs(store);
const { user } = storeToRefs(store);
import { debounce } from "@/util";
const showModal = ref(false);
const showSignin = ref(false);
const warnning = ref("");
const roomNumber = ref("");
const data = ref({});
const roomID = ref("");
const loading = ref(false);
const enterRoom = debounce(async (number) => {
if (loading.value) return;
if (!canEenter(user.value, device.value, online.value)) return;
if (game.value.inBattle) {
const enterRoom = debounce(async () => {
const isGaming = await isGamingAPI();
if (isGaming) {
uni.$showHint(1);
return;
}
if (!number) {
if (!roomNumber.value) {
warnning.value = "请输入房间号";
showModal.value = true;
return;
}
try {
const room = await getRoomAPI(number);
if (!room.number) {
warnning.value = room.started ? "该房间对战已开始,无法加入" : "查无此房";
showModal.value = true;
return;
}
} else {
const room = await getRoomAPI(roomNumber.value);
if (room.number) {
const alreadyIn = room.members.find(
(item) => item.userInfo.id === user.value.id
);
if (!alreadyIn) {
const result = await joinRoomAPI(number);
const result = await joinRoomAPI(roomNumber.value);
if (result.full) {
warnning.value = "房间已满员";
showModal.value = true;
return;
}
}
loading.value = true;
roomNumber.value = "";
showModal.value = false;
uni.navigateTo({
url: "/pages/battle-room?roomNumber=" + number,
url: `/pages/battle-room?roomNumber=${room.number}`,
});
} finally {
loading.value = false;
} else {
warnning.value = room.started ? "该房间对战已开始,无法加入" : "查无此房";
showModal.value = true;
}
}
});
const onCreateRoom = async () => {
if (!canEenter(user.value, device.value, online.value)) return;
const isGaming = await isGamingAPI();
if (isGaming) {
uni.$showHint(1);
return;
}
warnning.value = "";
showModal.value = true;
};
const onSignin = () => {
if (roomID.value && user.value.id) enterRoom(roomID.value);
showSignin.value = false;
};
/** 跳转到我的战绩页面默认展示「好友约战」tab */
const goMyRecord = () => {
uni.navigateTo({
url: '/pages/my-growth?tab=1',
});
};
onShow(async () => {
if (user.value.id) {
const result = await getBattleDataAPI();
data.value = result;
}
});
onLoad(async (options) => {
if (options.roomID) {
roomID.value = options.roomID;
if (user.value.id) enterRoom(options.roomID);
else showSignin.value = true;
}
});
</script>
<template>
<Container title="好友约战" :showBackToGame="true">
<view :style="{ width: '100%', height: '100%' }">
<GuideTwo>
<text :style="{color: 'rgba(255,217,71,0.8)'}">约上朋友开几局欢乐多不寂寞</text>
<view :style="{ width: '100%' }">
<Guide>
<view class="guide-tips">
<text>约上朋友开几局欢乐多不寂寞</text>
<text>一起练升级更快早日加入全国排位赛</text>
</GuideTwo>
</view>
</Guide>
<view class="my-data">
<view>
<Avatar :rankLvl="user.rankLvl" :src="user.avatar" :size="30" />
<text class="truncate">{{ user.nickName }}</text>
<text class="my-record-btn" @click="goMyRecord">我的战绩</text>
</view>
<view>
<view>
@@ -125,9 +100,16 @@ onLoad(async (options) => {
<view>
<view class="stars">
<block v-for="i in 5" :key="i">
<image v-if="data.totalWinningRate >= i * 0.2" src="../static/star-full.png" mode="widthFix" />
<image v-else-if="data.totalWinningRate >= (i - 1) * 0.2 + 0.1" src="../static/star-half.png"
mode="widthFix" />
<image
v-if="data.totalWinningRate >= i * 0.2"
src="../static/star-full.png"
mode="widthFix"
/>
<image
v-else-if="data.totalWinningRate >= (i - 1) * 0.2 + 0.1"
src="../static/star-half.png"
mode="widthFix"
/>
<image v-else src="../static/star-empty.png" mode="widthFix" />
</block>
</view>
@@ -138,12 +120,19 @@ onLoad(async (options) => {
<view class="founded-room">
<image src="../static/founded-room.png" mode="widthFix" />
<view>
<input placeholder="输入房间号" v-model="roomNumber" placeholder-style="color: #ccc" />
<view @click="enterRoom(roomNumber)">进入房间</view>
<input
placeholder="输入房间号"
v-model="roomNumber"
placeholder-style="color: #ccc"
/>
<view @click="enterRoom">进入房间</view>
</view>
</view>
<view class="create-room">
<image src="https://static.shelingxingqiu.com/attachment/2025-07-15/dbcejys872iyun92h6.png" mode="widthFix" />
<image
src="https://static.shelingxingqiu.com/attachment/2025-07-15/dbcejys872iyun92h6.png"
mode="widthFix"
/>
<image src="../static/room-notfound-title.png" mode="widthFix" />
<view>
<image :src="user.avatar" mode="widthFix" />
@@ -158,13 +147,12 @@ onLoad(async (options) => {
</SButton>
</view>
</view>
<SModal :show="showModal" :onClose="() => (showModal = false)" height="716rpx">
<SModal :show="showModal" :onClose="() => (showModal = false)">
<view v-if="warnning" class="warnning">
{{ warnning }}
</view>
<CreateRoom v-if="!warnning" :onConfirm="() => (showModal = false)" />
</SModal>
<Signin :show="showSignin" :onClose="onSignin" />
</view>
</Container>
</template>
@@ -180,11 +168,9 @@ onLoad(async (options) => {
border-radius: 10px;
padding: 15px;
}
.founded-room > image {
width: 16vw;
}
.founded-room > view {
display: flex;
justify-content: space-between;
@@ -195,15 +181,13 @@ onLoad(async (options) => {
width: 100%;
overflow: hidden;
}
.founded-room > view > input {
width: 70%;
text-align: center;
font-size: 14px;
height: 40px;
color: #000;
color: #fff;
}
.founded-room > view > view {
background-color: #fed847;
width: 30%;
@@ -215,24 +199,20 @@ onLoad(async (options) => {
color: #000;
text-align: center;
}
.create-room {
position: relative;
margin: 15px;
height: 50vw;
}
.create-room > image:first-of-type {
position: absolute;
width: 100%;
}
.create-room > image:nth-of-type(2) {
padding: 15px;
width: 25vw;
position: relative;
}
.create-room > view:nth-child(3) {
margin: 12vw auto;
position: relative;
@@ -240,19 +220,16 @@ onLoad(async (options) => {
align-items: center;
justify-content: center;
}
.create-room > view > image:first-child {
width: 19vw;
transform: translateY(-60%);
border-radius: 50%;
position: relative;
}
.create-room > view > image:nth-child(2) {
width: 37vw;
position: relative;
}
.create-room > view > view:nth-child(3) {
position: relative;
width: 19vw;
@@ -264,12 +241,10 @@ onLoad(async (options) => {
align-items: center;
transform: translateY(60%);
}
.create-room > view > view:nth-child(3) > image {
width: 20px;
margin-right: 2px;
}
.warnning {
width: 100%;
height: 100%;
@@ -278,7 +253,6 @@ onLoad(async (options) => {
align-items: center;
color: #fff9;
}
.my-data {
width: calc(100% - 30px);
margin: 15px;
@@ -288,13 +262,11 @@ onLoad(async (options) => {
overflow: hidden;
background-color: #54431d33;
}
.my-data > view {
width: 100%;
display: flex;
color: #fff9;
}
.my-data > view:first-child {
width: calc(100% - 30px);
align-items: flex-end;
@@ -303,28 +275,15 @@ onLoad(async (options) => {
margin: 15px;
margin-bottom: 0;
}
.my-data>view:first-child>.my-record-btn {
font-weight: 400;
font-size: 24rpx;
color: #76D4FF;
text-align: center;
font-style: normal;
width: auto;
margin-left: auto;
}
.my-data > view:first-child > text {
color: #fff;
font-size: 17px;
margin-left: 10px;
width: 120px;
}
.my-data > view:last-child {
margin-bottom: 15px;
}
.my-data > view:last-child > view {
width: 33%;
margin-top: 15px;
@@ -333,32 +292,26 @@ onLoad(async (options) => {
align-items: center;
font-size: 12px;
}
.my-data > view:last-child > view > view {
margin-bottom: 5px;
}
.my-data > view:last-child > view > view > text:first-child {
color: #fff;
font-size: 20px;
margin-right: 5px;
transform: translateY(4px);
}
.my-data > view:last-child > view:nth-child(2) {
border-left: 1px solid #48494e;
border-right: 1px solid #48494e;
}
.my-data > view:last-child > view > view {
display: flex;
align-items: flex-end;
height: 20px;
}
.stars > image {
width: 4vw;
height: 4vw;
margin: 0 1px;
}
</style>

View File

@@ -28,7 +28,7 @@ const { user } = storeToRefs(store);
</view>
<!-- 说明文本 -->
<view class="body">
<view class="content">
<view class="intro-text">
在射灵世界中等级是衡量您射箭技能的重要指标而经验则是您提升等级的关键具体的要求如下
</view>
@@ -68,8 +68,8 @@ const { user } = storeToRefs(store);
height: 32rpx;
display: flex;
justify-content: center;
padding-top: 20rpx;
padding-bottom: 40rpx;
margin-top: 10px;
margin-bottom: 20px;
}
.progress-dot {
@@ -89,8 +89,8 @@ const { user } = storeToRefs(store);
background-color: #fff9;
}
.body {
height: calc(100% - 146rpx);
.content {
height: calc(100% - 148rpx);
background-color: #ffffff;
padding: 30rpx;
}

79
src/pages/image-share.vue Normal file
View File

@@ -0,0 +1,79 @@
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import { generateCanvasImage } from "@/util";
import { getPractiseAPI } from "@/apis";
import { wxShare } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
onLoad(async (options) => {
const id = options.id || 461;
const data = await getPractiseAPI(id);
if (!data.arrows.length) return;
generateCanvasImage("shareCanvas", options.type, user.value, data);
});
const saveImage = () => {
uni.canvasToTempFilePath({
canvasId: "shareCanvas",
success: (res) => {
const tempFilePath = res.tempFilePath;
// 保存图片到相册
uni.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: () => {
uni.showToast({ title: "保存成功" });
},
fail: () => {
uni.showToast({ title: "保存失败", icon: "error" });
},
});
},
});
};
</script>
<template>
<Container>
<view class="content">
<view :style="{ overflow: 'hidden', borderRadius: '10px' }">
<canvas
:style="{ width: '300px', height: '534px' }"
canvas-id="shareCanvas"
></canvas>
</view>
</view>
</Container>
</template>
<style scoped>
.content {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 20px;
}
.footer {
width: 100%;
display: flex;
justify-content: space-around;
margin-top: 50px;
}
.footer > button {
display: flex;
flex-direction: column;
align-items: center;
color: #fff;
font-size: 12px;
}
.footer > button > image {
width: 45px;
margin-bottom: 10px;
}
</style>

View File

@@ -1,52 +1,54 @@
<script setup>
import {onMounted, ref} from "vue";
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
import { ref, onMounted } from "vue";
import { onShow, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import AppFooter from "@/components/AppFooter.vue";
import AppBackground from "@/components/AppBackground.vue";
import UserHeader from "@/components/UserHeader.vue";
import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue";
import BubbleTip from "@/components/BubbleTip.vue";
import {
checkUserBindAPI,
getAppConfig,
getDeviceBatteryAPI,
getRankListAPI,
getHomeData,
getMyDevicesAPI,
getRankListAPI,
silentLoginAPI,
} from "@/apis";
import { topThreeColors } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const {
updateConfig,
updateUser,
updateDevice,
updateRank,
getLvlName,
getLvlNameByScore,
updateOnline,
} = store;
const {user, device, rankData, online, game} = storeToRefs(store);
const { updateConfig, updateUser, updateDevice, updateRank, getLvlName } =
store;
// 使用storeToRefs用于UI里显示保持响应性
const { user, device, rankData } = storeToRefs(store);
const showModal = ref(false);
const showGuide = ref(false);
const toPage = async (path) => {
const toPage = (path) => {
if (!user.value.id) {
showModal.value = true;
return;
}
// if (path === "/pages/first-try") {
// if (canEenter(user.value, device.value, online.value, path)) {
// await uni.$checkAudio();
// }
// }
uni.navigateTo({url: path});
if (
"/pages/first-try,/pages/practise,/pages/friend-battle".indexOf(path) !== -1
) {
if (!device.value.deviceId) {
return uni.showToast({
title: "请先绑定设备",
icon: "none",
});
}
if ("/pages/first-try".indexOf(path) === -1 && !user.value.trio) {
return uni.showToast({
title: "请先完成新手试炼",
icon: "none",
});
}
}
uni.navigateTo({
url: path,
});
};
const toRankListPage = () => {
@@ -56,36 +58,12 @@ const toRankListPage = () => {
};
onShow(async () => {
const env = uni.getAccountInfoSync().miniProgram.envVersion;
const token = uni.getStorageSync(`${env}_token`);
if (!user.value.id && !token) {
try {
const wxResult = await uni.login({provider: "weixin"});
const bindResult = await checkUserBindAPI(wxResult.code);
if (bindResult.binded) {
const newResult = await uni.login({provider: "weixin"});
const silentResult = await silentLoginAPI(newResult.code);
if (silentResult.user) updateUser(silentResult.user);
const devices = await getMyDevicesAPI();
if (devices.bindings && devices.bindings.length) {
updateDevice(
devices.bindings[0].deviceId,
devices.bindings[0].deviceName
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
}
} else {
showModal.value = true;
}
} catch (e) {
console.log("检查绑定状态失败", e);
}
}
const promises = [getRankListAPI()];
if (token || user.value.id) {
if (token) {
promises.push(getHomeData());
}
@@ -98,12 +76,6 @@ onShow(async () => {
console.log("首页数据:", homeData);
if (homeData.user) {
updateUser(homeData.user);
if ("823,209,293,257".indexOf(homeData.user.id) !== -1) {
const show = uni.getStorageSync("show-the-user");
if (!show) {
uni.setStorageSync("show-the-user", true);
}
}
if (homeData.user.trio <= 0) {
showGuide.value = true;
setTimeout(() => {
@@ -116,8 +88,6 @@ onShow(async () => {
devices.bindings[0].deviceId,
devices.bindings[0].deviceName
);
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
}
}
}
@@ -129,6 +99,13 @@ onMounted(async () => {
console.log("全局配置:", config);
});
const comingSoon = () => {
uni.showToast({
title: "敬请期待",
icon: "none",
});
};
onShareAppMessage(() => {
return {
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
@@ -150,39 +127,32 @@ onShareTimeline(() => {
<template>
<Container :isHome="true" :showBackToGame="true">
<view class="container">
<view class="top-theme">
<image
src="https://static.shelingxingqiu.com/attachment/2025-12-31/dfc9dxrq4xn7e6y2pp.png"
mode="widthFix"
/>
</view>
<UserHeader showRank :onSignin="() => (showModal = true)" />
<view :style="{ padding: '12px 10px' }">
<view class="feature-grid">
<view class="bow-card">
<image
v-if="online"
src="https://static.shelingxingqiu.com/attachment/2025-08-07/dbvt1o6dvhr2rop3kn.webp"
mode="widthFix"
@click="() => toPage('/pages/my-device')"
/>
<image
v-else
src="https://static.shelingxingqiu.com/attachment/2026-01-04/dffohwtk1gwh0xfa6h.png"
mode="widthFix"
@click="() => toPage('/pages/my-device')"
/>
<block v-if="user.id">
<text v-if="!device.deviceId">绑定我的智能弓</text>
<text v-else-if="!online">设备离线</text>
<text v-else-if="online">设备在线</text>
</block>
<text v-if="!user.id">我的弓箭</text>
<text v-if="user.id && !device.deviceId">连接智能弓箭</text>
<text
v-if="user.id && device.deviceId"
class="truncate"
:style="{ width: '90%', textAlign: 'center' }"
>{{ device.deviceName }}</text
>
<image
src="../static/first-try.png"
mode="widthFix"
@click="() => toPage('/pages/first-try')"
/>
<BubbleTip v-if="showGuide" :location="{ top: '60%', left: '47%' }">
<BubbleTip
v-if="showGuide"
:location="{ top: '60%', left: '40%', fontSize: '14px' }"
>
<text>新人必刷</text>
<text>快来报到吧~</text>
</BubbleTip>
@@ -247,9 +217,8 @@ onShareTimeline(() => {
<view>
<text>段位</text>
<text>{{
user.lvlName || "暂无"
}}
</text>
user.scores ? getLvlName(user.scores) : "暂无"
}}</text>
</view>
<view>
<text>赛季平均环数</text>
@@ -261,14 +230,68 @@ onShareTimeline(() => {
user.avg_win
? Number((user.avg_win * 100).toFixed(2)) + "%"
: "暂无"
}}
</text>
}}</text>
</view>
</view>
</view>
<!-- <view class="region-stats">
<view
v-for="(region, index) in [
{ name: '广东', score: 4291 },
{ name: '湖南', score: 3095 },
{ name: '内蒙', score: 2342 },
{ name: '海南', score: 1812 },
{ name: '四川', score: 1293 },
]"
:key="index"
class="region-item"
@click="comingSoon"
>
<image src="../static/region-bg.png" mode="widthFix" />
<image
v-if="index === 0"
src="../static/region-1.png"
mode="widthFix"
/>
<image
v-if="index === 1"
src="../static/region-2.png"
mode="widthFix"
/>
<image
v-if="index === 2"
src="../static/region-3.png"
mode="widthFix"
/>
<image
v-if="index === 3"
src="../static/region-4.png"
mode="widthFix"
/>
<image
v-if="index === 4"
src="../static/region-5.png"
mode="widthFix"
/>
<text>{{ region.name }}</text>
<view>
<text :style="{ color: '#fff', marginRight: '2px' }">{{
region.score
}}</text>
<text>分</text>
</view>
</view>
<Signin :show="showModal" :onClose="() => (showModal = false)"/>
<view class="region-more" @click="comingSoon">
<image src="../static/region-more.png" mode="widthFix" />
<text>...</text>
<text>更多</text>
</view>
</view> -->
</view>
</view>
<SModal :show="showModal" :onClose="() => (showModal = false)">
<Signin :onClose="() => (showModal = false)" />
</SModal>
</view>
<AppFooter />
</Container>
@@ -277,7 +300,6 @@ onShareTimeline(() => {
<style scoped>
.container {
width: 100%;
height: calc(100% - 120px);
}
.feature-grid {
@@ -294,8 +316,6 @@ onShareTimeline(() => {
.bow-card {
width: 50%;
border-radius: 25rpx;
overflow: hidden;
}
.feature-grid > view > image {
@@ -304,7 +324,7 @@ onShareTimeline(() => {
.bow-card > text {
position: absolute;
top: 66%;
top: 65%;
left: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
@@ -391,7 +411,6 @@ onShareTimeline(() => {
width: 32rpx;
height: 32rpx;
}
.player-avatar > view:first-child {
border-radius: 50%;
background: #777777;
@@ -402,7 +421,6 @@ onShareTimeline(() => {
height: 18px;
color: #fff;
}
.player-avatar > image:last-child {
width: 100%;
height: 100%;
@@ -422,21 +440,71 @@ onShareTimeline(() => {
color: #fff;
}
.region-stats {
display: flex;
grid-template-columns: repeat(6, 1fr);
margin-top: 20px;
justify-content: space-between;
}
.region-item,
.region-more {
border-radius: 10px;
text-align: center;
position: relative;
width: 13vw;
height: 13vw;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #c5c5c5;
font-size: 12px;
}
.region-item > text {
margin-top: 10px;
}
.region-more {
width: 8vw;
height: 13vw;
}
.region-item > image:first-child,
.region-more > image:first-child {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: -1;
}
.region-item > image:nth-of-type(2) {
position: absolute;
top: 0;
left: 0;
width: 18px;
}
.region-item > view:last-child {
display: flex;
justify-content: center;
font-size: 10px;
}
.region-more > text:first-of-type {
font-size: 30px;
line-height: 20px;
margin-bottom: 5px;
}
.my-data {
display: flex;
margin-top: 20px;
justify-content: space-between;
}
.my-data > view:first-child {
width: 28%;
}
.my-data > view:first-child > image {
width: 100%;
transform: translateX(-8px);
}
.my-data > view:nth-child(2) {
width: 68%;
font-size: 12px;
@@ -444,11 +512,9 @@ onShareTimeline(() => {
display: flex;
justify-content: space-between;
}
.my-data > view:nth-child(2) > view:nth-child(2) {
width: 38%;
}
.my-data > view:nth-child(2) > view {
width: 28%;
border-radius: 10px;
@@ -458,25 +524,9 @@ onShareTimeline(() => {
align-items: center;
justify-content: center;
}
.my-data > view:nth-child(2) > view > text:last-child {
color: #fff;
line-height: 25px;
font-weight: 500;
}
.top-theme {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 60px;
z-index: -1;
}
.top-theme > image {
width: 300rpx;
transform: translate(-4%, -14%);
}
</style>

View File

@@ -4,50 +4,91 @@ import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import BattleHeader from "@/components/BattleHeader.vue";
import Avatar from "@/components/Avatar.vue";
// import TeamResult from "@/components/TeamResult.vue";
// import MeleeResult from "@/components/MeleeResult.vue";
import PlayerScore2 from "@/components/PlayerScore2.vue";
import { getBattleAPI } from "@/apis";
import { getGameAPI } from "@/apis";
const blueTeam = ref([]);
const redTeam = ref([]);
const roundsData = ref([]);
const goldenRoundsData = ref([]);
const battleId = ref("");
const data = ref({
teams: [],
rounds: [],
players: [],
});
const players = ref([]);
// const show = ref(false);
onLoad(async (options) => {
if (!options.battleId) return;
battleId.value = options.battleId || "60510101693403136";
const result = await getBattleAPI(battleId.value);
if (options.id) {
battleId.value = options.id || "BATTLE-1755484626207409508-955";
const result = await getGameAPI(battleId.value);
data.value = result;
if (result.mode > 3) {
players.value = result.resultList.map((item, index) => {
const plist = result.teams[0] ? result.teams[0].players : [];
const p = plist.find((p) => p.id === item.userId);
const arrows = new Array(12);
result.rounds.forEach((r, index) => {
if (r.shoots[item.userId]) {
r.shoots[item.userId].forEach((s, index2) => {
arrows[index2 + index * 6] = s;
if (result.mode === 1) {
blueTeam.value = Object.values(result.bluePlayers || {});
redTeam.value = Object.values(result.redPlayers || {});
Object.values(result.roundsData).forEach((item) => {
let bluePoint = 1;
let redPoint = 1;
let blueTotalRings = 0;
let redTotalRings = 0;
let blueArrows = [];
let redArrows = [];
blueTeam.value.forEach((p) => {
if (!item[p.playerId]) return;
blueTotalRings += item[p.playerId].reduce((a, b) => a + b.ring, 0);
blueArrows = [...blueArrows, ...item[p.playerId]];
});
redTeam.value.forEach((p) => {
if (!item[p.playerId]) return;
redTotalRings += item[p.playerId].reduce((a, b) => a + b.ring, 0);
redArrows = [...redArrows, ...item[p.playerId]];
});
if (blueTotalRings > redTotalRings) {
bluePoint = 2;
redPoint = 0;
} else if (blueTotalRings < redTotalRings) {
bluePoint = 0;
redPoint = 2;
}
roundsData.value.push({
blue: {
avatars: blueTeam.value.map((p) => p.avatar),
arrows: blueArrows,
totalRing: blueTotalRings,
totalScore: bluePoint,
},
red: {
avatars: redTeam.value.map((p) => p.avatar),
arrows: redArrows,
totalRing: redTotalRings,
totalScore: redPoint,
},
});
});
result.goldenRounds.forEach((round) => {
goldenRoundsData.value.push({
blue: {
avatars: blueTeam.value.map((p) => p.avatar),
arrows: round.arrowHistory.filter((a) => a.team === 1),
},
red: {
avatars: redTeam.value.map((p) => p.avatar),
arrows: round.arrowHistory.filter((a) => a.team === 0),
},
winner: round.winner,
});
});
}
});
return {
...item,
rank: index + 1,
name: p.name,
avatar: p.avatar || "",
arrows,
};
});
}
});
const checkBowData = (selected) => {
if (data.value.mode <= 3) {
const checkBowData = () => {
if (data.value.mode === 1) {
uni.navigateTo({
url: `/pages/team-bow-data?battleId=${battleId.value}&selected=${selected}`,
url: `/pages/team-bow-data?battleId=${battleId.value}`,
});
} else {
} else if (data.value.mode === 2) {
uni.navigateTo({
url: `/pages/melee-bow-data?battleId=${battleId.value}`,
});
@@ -59,13 +100,13 @@ const checkBowData = (selected) => {
<Container title="详情">
<view class="container">
<BattleHeader
:winner="data.winTeam"
:blueTeam="data.teams[1] ? data.teams[1].players : []"
:redTeam="data.teams[2] ? data.teams[2].players : []"
:players="players"
:winner="data.winner"
:blueTeam="blueTeam"
:redTeam="redTeam"
:players="data.players"
/>
<view
v-if="data.mode >= 3"
v-if="data.players && data.players.length"
class="score-header"
:style="{ border: 'none', padding: '5px 15px' }"
>
@@ -76,64 +117,156 @@ const checkBowData = (selected) => {
</view>
</view>
<PlayerScore2
v-if="data.mode >= 3"
v-for="(player, index) in players"
v-if="data.players && data.players.length"
v-for="(player, index) in data.players"
:key="index"
:name="player.name"
:avatar="player.avatar"
:arrows="player.arrows"
:scores="player.arrowHistory"
:totalScore="player.totalScore"
:totalRing="player.totalRings"
:rank="index + 1"
/>
<view
v-if="data.mode <= 3"
v-for="(round, index) in data.rounds"
:key="index"
:style="{ marginBottom: '5px' }"
>
<block v-for="(round, index) in goldenRoundsData" :key="index">
<view class="score-header">
<text>{{ round.ifGold ? "决金箭" : `${index + 1}` }}</text>
<view @click="() => checkBowData(index)">
<text>决金箭轮环数</text>
<view @click="checkBowData">
<text>查看靶纸</text>
<image src="../static/back.png" mode="widthFix" />
</view>
</view>
<view
class="score-row"
v-for="team in Object.keys(round.shoots)"
:key="team"
>
<view class="score-row">
<view>
<view>
<image
v-for="(p, index) in data.teams[team].players"
v-for="(src, index) in round.blue.avatars"
:style="{
borderColor: '#64BAFF',
transform: `translateX(-${index * 15}px)`,
}"
:src="p.avatar || '../static/user-icon.png'"
:src="src"
:key="index"
mode="widthFix"
/>
</view>
<text
v-for="(arrow, index2) in round.shoots[team]"
:key="index2"
:style="{ color: arrow.ringX ? '#fed847' : '#ccc' }"
<text v-for="(arrow, index) in round.blue.arrows" :key="index">
{{ arrow.ring }}
</text>
</view>
<image
v-if="round.winner === 1"
src="../static/winner-badge.png"
mode="widthFix"
/>
</view>
<view class="score-row" :style="{ marginBottom: '5px' }">
<view>
<view>
<image
v-for="(src, index) in round.red.avatars"
:style="{
borderColor: '#FF6767',
transform: `translateX(-${index * 15}px)`,
}"
:src="src || '../static/user-icon.png'"
:key="index"
mode="widthFix"
/>
</view>
<text v-for="(arrow, index) in round.red.arrows" :key="index">
{{ arrow.ring }}
</text>
</view>
<image
v-if="round.winner === 0"
src="../static/winner-badge.png"
mode="widthFix"
/>
</view>
</block>
<view
v-for="(round, index) in roundsData"
:key="index"
:style="{ marginBottom: '5px' }"
>
{{ arrow.ringX ? "X" : `${arrow.ring}` }}
<block
v-if="
index < Object.keys(roundsData).length - goldenRoundsData.length
"
>
<view class="score-header">
<text>第{{ index + 1 }}轮</text>
<view @click="checkBowData">
<text>查看靶纸</text>
<image src="../static/back.png" mode="widthFix" />
</view>
</view>
<view class="score-row">
<view>
<view>
<image
v-for="(src, index) in round.blue.avatars"
:style="{
borderColor: '#64BAFF',
transform: `translateX(-${index * 15}px)`,
}"
:src="src || '../static/user-icon.png'"
:key="index"
mode="widthFix"
/>
</view>
<text v-for="(arrow, index2) in round.blue.arrows" :key="index2">
{{ arrow.ring }}环
</text>
</view>
<view>
<text :style="{ color: team == 1 ? '#64BAFF' : '#FF6767' }">
{{ round.shoots[team].reduce((acc, cur) => acc + cur.ring, 0) }}
<text :style="{ color: '#64BAFF' }">
{{ round.blue.totalRing }}环
</text>
<text>得分 {{ round.scores[team].score }}</text>
<text>得分 {{ round.blue.totalScore }}</text>
</view>
</view>
<view class="score-row">
<view>
<view>
<image
v-for="(src, index) in round.red.avatars"
:style="{
borderColor: '#FF6767',
transform: `translateX(-${index * 15}px)`,
}"
:src="src"
:key="index"
mode="widthFix"
/>
</view>
<text v-for="(arrow, index2) in round.red.arrows" :key="index2">
{{ arrow.ring }}环
</text>
</view>
<view>
<text :style="{ color: '#FF6767' }">
{{ round.red.totalRing }}环
</text>
<text>得分 {{ round.red.totalScore }}</text>
</view>
</view>
</block>
</view>
<view :style="{ height: '20px' }"></view>
</view>
<!-- <TeamResult
v-if="data.mode === 1"
:show="show"
:onClose="() => (show = false)"
:data="data"
/>
<MeleeResult
v-if="data.mode === 2"
:show="show"
:onClose="() => (show = false)"
:data="data"
/> -->
</Container>
</template>

View File

@@ -3,8 +3,10 @@ import { ref, onMounted, onBeforeUnmount } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Matching from "@/components/Matching.vue";
import RoundEndTip from "@/components/RoundEndTip.vue";
import TestDistance from "@/components/TestDistance.vue";
import { matchGameAPI } from "@/apis";
import { MESSAGETYPESV2 } from "@/constants";
import { MESSAGETYPES } from "@/constants";
const gameType = ref(0);
const teamSize = ref(0);
@@ -14,29 +16,34 @@ async function stopMatch() {
uni.$showHint(3);
}
async function cancelMatch() {
if (gameType.value && teamSize.value) {
await matchGameAPI(false, gameType.value, teamSize.value);
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.WaitForAllReady) {
if (!onComplete.value) {
onComplete.value = () => {
if (msg.groupUserStatus) {
uni.setStorageSync("red-team", msg.groupUserStatus.redTeam);
uni.setStorageSync("blue-team", msg.groupUserStatus.blueTeam);
uni.setStorageSync("melee-players", [
...msg.groupUserStatus.redTeam,
...msg.groupUserStatus.blueTeam,
]);
}
uni.navigateBack()
}
async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.MatchSuccess) {
onComplete.value = () => {}
}
if (msg.type === MESSAGETYPESV2.AboutToStart) {
uni.removeStorageSync("current-battle");
if (gameType.value == 1) {
uni.redirectTo({
url: `/pages/team-battle?battleId=${msg.id}&gameMode=2`,
});
} else if (gameType.value == 2) {
uni.redirectTo({
url: `/pages/melee-battle?battleId=${msg.id}&gameMode=2`,
url: `/pages/melee-match?battleId=${msg.id}&gameMode=2`,
});
}
};
}
}
});
}
onLoad(async (options) => {
if (options.gameType && options.teamSize) {
@@ -50,7 +57,6 @@ onMounted(() => {
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("cancelMatching", cancelMatch);
});
onBeforeUnmount(() => {
@@ -58,7 +64,9 @@ onBeforeUnmount(() => {
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
uni.$off("cancelMatching", cancelMatch);
if (gameType.value && teamSize.value) {
matchGameAPI(false, gameType.value, teamSize.value);
}
});
onShow(async () => {
@@ -68,7 +76,9 @@ onShow(async () => {
});
onHide(() => {
if (gameType.value && teamSize.value) {
matchGameAPI(false, gameType.value, teamSize.value);
}
});
</script>

View File

@@ -1,219 +0,0 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import BowTarget from "@/components/BowTarget.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import BattleHeader from "@/components/BattleHeader.vue";
import PlayerScore from "@/components/PlayerScore.vue";
import SButton from "@/components/SButton.vue";
import Avatar from "@/components/Avatar.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import TestDistance from "@/components/TestDistance.vue";
import audioManager from "@/audioManager";
import { getBattleAPI, laserCloseAPI } from "@/apis";
import { MESSAGETYPESV2 } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const title = ref("");
const start = ref(null);
const battleId = ref("");
const currentRound = ref(1);
const tips = ref("即将开始...");
const players = ref([]);
const playersSorted = ref([]);
const playersScores = ref([]);
const halfTimeTip = ref(false);
const halfRest = ref(false);
const navigateToResult = () => {
uni.redirectTo({ url: `/pages/battle-result?battleId=${battleId.value}` });
};
function recoverData(battleInfo, { force = false } = {}) {
if (!battleInfo) return;
try {
if (battleInfo.way === 1) title.value = "好友约战 - 大乱斗";
if (battleInfo.way === 2) title.value = "排位赛 - 大乱斗";
// 优先使用接口数据,否则使用缓存
if (battleInfo.teams?.[0]?.players) {
players.value = [...battleInfo.teams[0].players];
} else {
// 大乱斗可能存的是 players 列表
// 这里的缓存逻辑根据 AboutToStart 消息结构可能不同,假设也是 teams[0]
// 如果是从 match-page 过来的match-page 只存了 teams[1] 和 [2] 给对抗模式
// 大乱斗的匹配逻辑可能不同,暂时保持原样,只做安全保护
players.value = [];
}
start.value = battleInfo.status !== 0;
if (battleInfo.status === 0) {
const readyRemain = (Date.now() - (battleInfo.createTime || Date.now())) / 1000;
if (readyRemain > 0 && readyRemain < 15) {
setTimeout(() => uni.$emit("update-timer", 15 - readyRemain - 0.2), 200);
}
return;
}
tips.value =
(battleInfo.rounds.length !== 2 ? "上" : "下") + "半场请先射6箭";
playersScores.value = battleInfo.rounds.map((r) => ({ ...r.shoots }));
const totals = {};
players.value.forEach((p) => {
const total = playersScores.value.reduce((acc, round) => {
const arr = round[p.id] || [];
return acc + arr.length;
}, 0);
totals[p.id] = total;
});
playersSorted.value = players.value.slice().sort((a, b) => {
return totals[b.id] - totals[a.id];
});
if (battleInfo.status === 3) {
halfTimeTip.value = true;
halfRest.value = true;
tips.value = "准备下半场";
// 剩余休息时间
// const remain = (Date.now() - battleInfo.timeoutTime) / 1000;
setTimeout(() => {
uni.$emit("update-remain", 0);
}, 200);
return;
}
if (force) {
const remain = (Date.now() - (battleInfo.current?.startTime || Date.now())) / 1000;
console.log(`当前轮已进行${remain}`);
if (remain > 0 && remain < 90) {
setTimeout(() => {
uni.$emit("update-remain", 90 - remain - 0.2);
}, 200);
}
}
} catch (err) {
console.error("recoverData error:", err);
}
}
onLoad(async (options) => {
if (options.battleId) battleId.value = options.battleId;
// uni.enableAlertBeforeUnload({
// message: "离开比赛可能导致比赛失败,是否继续?",
// success: (res) => {
// console.log("已启用离开提示");
// },
// });
});
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.BattleStart) {
halfTimeTip.value = false;
halfRest.value = false;
recoverData(msg);
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
recoverData(msg);
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
halfTimeTip.value = true;
halfRest.value = true;
tips.value = "准备下半场";
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(() => {
uni.redirectTo({
url: "/pages/battle-result?battleId=" + msg.matchId,
});
}, 1000);
}
}
onMounted(async () => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
await laserCloseAPI();
});
onBeforeUnmount(() => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
audioManager.stopAll();
});
onShow(async () => {
if (battleId.value) {
const result = await getBattleAPI(battleId.value);
if (!result) return;
if (result.status === 2) {
uni.showToast({
title: "比赛已结束",
icon: "none",
});
uni.navigateBack({
delta: 2,
});
} else {
recoverData(result, { force: true });
}
}
});
</script>
<template>
<Container :title="title" :bgType="1">
<view class="container">
<BattleHeader v-if="!start" :players="players" />
<TestDistance v-if="start === false" :guide="false" :isBattle="true" />
<ShootProgress
:show="start"
:start="start && !halfRest"
:tips="tips"
:total="90"
:melee="true"
:battleId="battleId"
/>
<view v-if="start" class="user-row">
<Avatar :src="user.avatar" :size="35" />
<BowPower />
</view>
<BowTarget
v-if="start"
:currentRound="
playersScores.map((s) => s[user.id].length).reduce((a, b) => a + b, 0)
"
:totalRound="12"
:scores="playersScores.map((r) => r[user.id]).flat()"
:stop="halfRest"
/>
<view :style="{ paddingBottom: '20px' }">
<PlayerScore
v-if="start"
v-for="(player, index) in playersSorted"
:key="index"
:player="player"
:scores="playersScores.map((s) => s[player.id])"
/>
</view>
<ScreenHint
:show="halfTimeTip"
mode="small"
:onClose="() => (halfTimeTip = false)"
>
<view class="half-time-tip">
<text>上半场结束休息一下吧:</text>
<text>20秒后开始下半场</text>
</view>
</ScreenHint>
</view>
</Container>
</template>
<style scoped>
.container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -4,39 +4,28 @@ import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import BowTarget from "@/components/BowTarget.vue";
import Avatar from "@/components/Avatar.vue";
import { getBattleAPI } from "@/apis";
import { getGameAPI } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const { user } = storeToRefs(useStore());
const currentUser = ref({
arrows: [],
});
const players = ref([]);
onLoad(async (options) => {
if (!options.battleId) return;
const result = await getBattleAPI(options.battleId || "59348111700660224");
players.value = result.resultList.map((item, index) => {
const plist = result.teams[0] ? result.teams[0].players : [];
const p = plist.find((p) => p.id === item.userId);
const arrows = new Array(12);
result.rounds.forEach((r, index) => {
if (r.shoots[item.userId]) {
r.shoots[item.userId].forEach((s, index2) => {
arrows[index2 + index * 6] = s;
});
const store = useStore();
const { user } = storeToRefs(store);
const scores = ref([]);
const currentUser = ref({});
const data = ref({});
const onSelect = (userId) => {
const user = data.value.players.find((p) => p.playerId === userId);
currentUser.value = user;
if (user && user.arrowHistory) {
scores.value = user.arrowHistory;
}
});
return {
...item,
rank: index + 1,
name: p.name,
avatar: p.avatar || "",
arrows,
};
});
if (players.value[0]) {
currentUser.value = players.value[0];
onLoad(async (options) => {
if (options.battleId) {
const result = await getGameAPI(options.battleId);
data.value = result;
if (result.players && result.players[0]) {
onSelect(result.players[0].playerId);
}
}
});
</script>
@@ -44,26 +33,22 @@ onLoad(async (options) => {
<template>
<Container title="靶纸">
<view class="container">
<image
src="../static/battle-header-melee.png"
mode="widthFix"
:style="{ top: '-50rpx' }"
/>
<view class="players">
<image src="../static/battle-header-melee.png" mode="widthFix" />
<view class="players" v-if="data.players">
<view
v-for="(player, index) in players"
v-for="(player, index) in data.players"
:key="index"
:style="{
width: `${Math.max(100 / players.length, 18)}vw`,
color: player.userId === currentUser.userId ? '#000' : '#fff9',
width: `${Math.max(100 / data.players.length, 18)}vw`,
color: player.playerId === currentUser.playerId ? '#000' : '#fff9',
}"
@click="currentUser = player"
@click="() => onSelect(player.playerId)"
>
<image
v-if="player.userId === currentUser.userId"
v-if="player.playerId === currentUser.playerId"
src="../static/player-bg2.png"
:style="{
width: `${Math.max(100 / players.length, 18)}vw`,
width: `${Math.max(100 / data.players.length, 18)}vw`,
}"
class="player-bg"
/>
@@ -72,25 +57,23 @@ onLoad(async (options) => {
</view>
</view>
<view :style="{ marginTop: '10px' }">
<BowTarget :scores="currentUser.arrows" />
<BowTarget :scores="scores" />
</view>
<view class="score-text"
><text :style="{ color: '#fed847' }">{{
currentUser.arrows.length
}}</text
><text :style="{ color: '#fed847' }">{{ scores.length }}</text
>支箭<text :style="{ color: '#fed847' }">{{
currentUser.arrows.reduce((last, next) => last + next.ring, 0)
scores.reduce((last, next) => last + next.ring, 0)
}}</text
></view
>
<view class="score-row" v-if="currentUser.arrows">
<view class="score-row">
<view
v-for="(score, index) in currentUser.arrows"
v-for="(score, index) in scores"
:key="index"
class="score-item"
:style="{ width: '13vw', height: '13vw' }"
>
{{ score.ringX ? "X" : score.ring }}
{{ score.ring }}
</view>
</view>
</view>
@@ -114,7 +97,7 @@ onLoad(async (options) => {
display: flex;
width: 100%;
overflow-x: auto;
margin-top: 50rpx;
margin-top: 25px;
}
.players::-webkit-scrollbar {
width: 0;

244
src/pages/melee-match.vue Normal file
View File

@@ -0,0 +1,244 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import BowTarget from "@/components/BowTarget.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import BattleHeader from "@/components/BattleHeader.vue";
import PlayerScore from "@/components/PlayerScore.vue";
import SButton from "@/components/SButton.vue";
import Avatar from "@/components/Avatar.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import TestDistance from "@/components/TestDistance.vue";
import { getCurrentGameAPI } from "@/apis";
import { isGameEnded } from "@/util";
import { MESSAGETYPES } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const title = ref("大乱斗");
const start = ref(false);
const startCount = ref(true);
const battleId = ref("");
const currentRound = ref(1);
const power = ref(0);
const scores = ref([]);
const tips = ref("即将开始...");
const players = ref([]);
const playersSorted = ref([]);
const playersScores = ref({});
const halfTimeTip = ref(false);
const isEnded = ref(false);
watch(
() => [players.value, playersScores.value],
([n_players, n_scores]) => {
if (n_players.length) {
playersSorted.value = Object.keys(n_scores)
.sort((a, b) => n_scores[b].length - n_scores[a].length)
.map((pid) => n_players.find((p) => p.id == pid));
}
},
{
deep: true, // 添加深度监听
immediate: true,
}
);
function recoverData(battleInfo) {
uni.removeStorageSync("last-awake-time");
battleId.value = battleInfo.id;
players.value = [...battleInfo.blueTeam, ...battleInfo.redTeam];
players.value.forEach((p) => {
playersScores.value[p.id] = [...p.arrows];
if (p.id === user.value.id) scores.value = [...p.arrows];
});
const remain = Date.now() / 1000 - battleInfo.startTime;
console.log(`当前局已进行${remain}`);
if (battleInfo.status === 0) {
if (remain > 0) {
setTimeout(() => {
uni.$emit("update-timer", 15 - remain);
}, 200);
}
} else {
start.value = true;
}
if (battleInfo.status === 2) {
const elapsedTime = (Date.now() - Date.parse(battleInfo.createdAt)) / 1000;
console.log("elapsedTime:", elapsedTime);
startCount.value = true;
// 这里的开始时间不是游戏开始时间,而是上半场或者下半场或者中场的开始时间,还要根据状态来判断
tips.value = battleInfo.halfGame
? "下半场请再射6箭"
: "上半场请先射6箭";
setTimeout(() => {
uni.$emit("update-ramain", 90 - remain);
}, 200);
} else if (battleInfo.status === 9) {
startCount.value = false;
tips.value = "准备下半场";
setTimeout(() => {
uni.$emit("update-ramain", 0);
}, 200);
}
}
onLoad(async (options) => {
if (options.gameMode == 1) title.value = "好友约战 - 大乱斗";
if (options.gameMode == 2) title.value = "排位赛 - 大乱斗";
if (options.battleId) {
battleId.value = options.battleId;
const players = uni.getStorageSync("melee-players");
if (players) {
players.value = players;
players.value.forEach((p) => {
playersScores.value[p.id] = [];
});
}
const battleInfo = uni.getStorageSync("current-battle");
if (battleInfo) {
recoverData(battleInfo);
setTimeout(getCurrentGameAPI, 2000);
}
}
});
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.id !== battleId.value) return;
if (msg.constructor === MESSAGETYPES.MeleeAllReady) {
start.value = true;
startCount.value = true;
tips.value = scores.value.length
? "下半场请再射6箭"
: "上半场请先射6箭";
halfTimeTip.value = false;
}
if (msg.constructor === MESSAGETYPES.ShootResult) {
if (!start.value) getCurrentGameAPI();
if (msg.userId === user.value.id) {
scores.value.push({ ...msg.target });
power.value = msg.target.battery;
}
playersScores.value[msg.userId].push({ ...msg.target });
}
if (msg.constructor === MESSAGETYPES.HalfTimeOver) {
uni.$emit("update-ramain", 0);
[...msg.groupUserStatus.redTeam, ...msg.groupUserStatus.blueTeam].forEach(
(player) => {
playersScores.value[player.id] = [...player.arrows];
if (player.id === user.value.id) scores.value = [...player.arrows];
}
);
startCount.value = false;
halfTimeTip.value = true;
tips.value = "准备下半场";
}
if (msg.constructor === MESSAGETYPES.MatchOver) {
isEnded.value = true;
uni.setStorageSync("last-battle", msg.endStatus);
setTimeout(() => {
uni.redirectTo({
url: "/pages/battle-result",
});
}, 1000);
}
if (msg.constructor === MESSAGETYPES.BackToGame) {
uni.$emit("update-header-loading", false);
if (msg.battleInfo) recoverData(msg.battleInfo);
}
});
}
const onBack = () => {
uni.$showHint(2);
};
onMounted(() => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
});
onBeforeUnmount(() => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
});
const refreshTimer = ref(null);
onShow(async () => {
if (battleId.value) {
if (!isEnded.value && (await isGameEnded(battleId.value))) return;
getCurrentGameAPI();
const refreshData = () => {
const lastAwakeTime = uni.getStorageSync("last-awake-time");
if (lastAwakeTime) {
getCurrentGameAPI();
} else {
clearInterval(refreshTimer.value);
}
};
refreshTimer.value = setInterval(refreshData, 2000);
}
});
onHide(() => {
if (refreshTimer.value) clearInterval(refreshTimer.value);
uni.setStorageSync("last-awake-time", Date.now());
});
</script>
<template>
<Container :title="title" :bgType="1" :onBack="onBack">
<view class="container">
<BattleHeader v-if="!start" :players="players" />
<TestDistance v-if="!start" :guide="false" :isBattle="true" />
<ShootProgress
:show="start"
:start="start && startCount"
:tips="tips"
:total="90"
:melee="true"
:battleId="battleId"
/>
<view v-if="start" class="user-row">
<Avatar :src="user.avatar" :size="35" />
<BowPower :power="power" />
</view>
<BowTarget
v-if="start"
:currentRound="scores.length"
:totalRound="12"
:scores="scores"
:stop="!startCount"
/>
<view :style="{ paddingBottom: '20px' }">
<PlayerScore
v-if="start"
v-for="(player, index) in playersSorted"
:key="index"
:name="player.name"
:avatar="player.avatar"
:scores="playersScores[player.id] || []"
/>
</view>
<ScreenHint
:show="halfTimeTip"
mode="small"
:onClose="() => (halfTimeTip = false)"
>
<view class="half-time-tip">
<text>上半场结束休息一下吧:</text>
<text>20秒后开始下半场</text>
</view>
</ScreenHint>
</view>
</Container>
</template>
<style scoped>
.container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -14,10 +14,11 @@ const arrows = ref([]);
const total = ref(0);
onLoad(async (options) => {
if (!options.id) return;
const result = await getPractiseAPI(options.id || 176);
arrows.value = result.details;
total.value = result.details.length;
if (options.id) {
const result = await getPractiseAPI(options.id);
arrows.value = result.arrows;
total.value = result.completed_arrows;
}
});
</script>
@@ -46,7 +47,7 @@ onLoad(async (options) => {
:completeEffect="false"
:rowCount="total === 12 ? 6 : 9"
:total="total"
:arrows="arrows"
:scores="arrows.map((a) => a.ring)"
:margin="arrows.length === 12 ? 4 : 1"
:fontSize="arrows.length === 12 ? 25 : 22"
/>

View File

@@ -1,15 +1,9 @@
<script setup>
import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { ref, onMounted } from "vue";
import Container from "@/components/Container.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import SButton from "@/components/SButton.vue";
import {
bindDeviceAPI,
getMyDevicesAPI,
unbindDeviceAPI,
laserAimAPI, bindDeviceAPIV2,
} from "@/apis";
import { bindDeviceAPI, getMyDevicesAPI, unbindDeviceAPI } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const showTip = ref(false);
@@ -19,8 +13,6 @@ const store = useStore();
const { updateDevice } = store;
const { user, device } = storeToRefs(store);
const justBind = ref(false);
const calibration = ref(false);
const token = ref(null);
// 扫描二维码方法
const handleScan = () => {
@@ -31,14 +23,13 @@ const handleScan = () => {
scanType: ["qrCode"],
success: async (res) => {
try {
// const base64Decode = (str) => {
// // 将 base64 转换为 utf8 字符串
// const bytes = wx.base64ToArrayBuffer(str);
// return String.fromCharCode.apply(null, new Uint8Array(bytes));
// };
//
// addDevice.value = JSON.parse(base64Decode(res.result));
token.value = res.result;
const base64Decode = (str) => {
// 将 base64 转换为 utf8 字符串
const bytes = wx.base64ToArrayBuffer(str);
return String.fromCharCode.apply(null, new Uint8Array(bytes));
};
addDevice.value = JSON.parse(base64Decode(res.result));
confirmBindTip.value = true;
} catch (err) {
uni.showToast({
@@ -59,8 +50,8 @@ const handleScan = () => {
};
const confirmBind = async () => {
if (!justBind.value && token.value) {
const result = await bindDeviceAPIV2(token.value);
if (!justBind.value && addDevice.value.id) {
const result = await bindDeviceAPI(addDevice.value);
confirmBindTip.value = false;
if (result.binded) {
return uni.showToast({
@@ -68,7 +59,7 @@ const confirmBind = async () => {
icon: "none",
});
}
updateDevice(result.deviceId, result.name);
updateDevice(addDevice.value.id, addDevice.value.name);
justBind.value = true;
uni.showToast({
title: "绑定成功",
@@ -85,7 +76,6 @@ const toFristTryPage = () => {
const unbindDevice = async () => {
await unbindDeviceAPI(device.value.deviceId);
uni.setStorageSync("calibration", false);
uni.showToast({
title: "解绑成功",
icon: "success",
@@ -114,17 +104,6 @@ const copyEmail = () => {
},
});
};
const goCalibration = async () => {
await laserAimAPI();
uni.navigateTo({
url: "/pages/calibration",
});
};
onShow(() => {
calibration.value = uni.getStorageSync("calibration");
});
</script>
<template>
@@ -158,9 +137,7 @@ onShow(() => {
<text>已被绑定的弓箭无法再次绑定</text>
<view>
<text>如有任何疑问请随时联系</text>
<button hover-class="none" @click="copyEmail">
shelingxingqiu@163.com
</button>
<button hover-class="none" @click="copyEmail">shelingxingqiu@163.com</button>
</view>
</view>
</ScreenHint>
@@ -184,23 +161,10 @@ onShow(() => {
</ScreenHint>
</view>
<view v-if="justBind" class="just-bind">
<view
class="device-binded"
:style="{ marginBottom: calibration ? '250rpx' : '100rpx' }"
>
<view>
<view class="device-binded">
<view @click="toDeviceIntroPage">
<image src="../static/device-icon.png" mode="widthFix" />
<text>{{ device.deviceName }}</text>
<view class="calibration" v-if="calibration">
<button hover-class="none" @click="goCalibration">
<text>重新校准</text>
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
</button>
<view>
<image src="../static/calibration-tip.png" mode="widthFix" />
<text>如有场地/距离变化需重新校准以保证智能弓射箭精准度</text>
</view>
</view>
</view>
<image src="../static/bind-success.png" mode="widthFix" />
<view>
@@ -212,51 +176,23 @@ onShow(() => {
<text>{{ user.nickName }}</text>
</view>
</view>
<block v-if="calibration">
<SButton :onClick="toFristTryPage" width="60vw" :rounded="40"
>进入新手试炼</SButton
>
<view :style="{ marginTop: '15px' }">
<SButton
:onClick="backToHome"
backgroundColor="#fff3"
color="#fff"
width="60vw"
:rounded="40"
>返回首页</SButton
>
</view>
</block>
<block v-else>
<view>
<text>恭喜你的弓箭和账号已成功绑定</text>
<text :style="{ color: '#fed847' }">已赠送6个月射灵世界会员</text>
<text>赶快进入新手试炼场体验一下吧</text>
</view>
<SButton :onClick="goCalibration" width="60vw" :rounded="40">
开启智能弓进行校准
</SButton>
<text :style="{ marginTop: '20rpx', fontSize: '24rpx', color: '#fff9' }"
>校准时弓箭激光将开启请勿直视激光</text
<SButton :onClick="toFristTryPage">进入新手试炼</SButton>
<view :style="{ marginTop: '15px' }">
<SButton :onClick="backToHome" backgroundColor="#fff3" color="#fff"
>返回首页</SButton
>
</block>
</view>
</view>
<view v-if="device.deviceId && !justBind" class="has-device">
<view class="device-binded">
<view>
<view @click="toDeviceIntroPage">
<image src="../static/device-icon.png" mode="widthFix" />
<text>{{ device.deviceName }}</text>
<view class="calibration">
<button hover-class="none" @click="goCalibration">
<text>去校准</text>
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
</button>
<view>
<image src="../static/calibration-tip.png" mode="widthFix" />
<text
>首次绑定智能弓或场地/距离变化时应进行校准以确保射箭精度</text
>
</view>
</view>
</view>
<image src="../static/bind.png" mode="widthFix" />
<view>
@@ -268,11 +204,7 @@ onShow(() => {
<text>{{ user.nickName }}</text>
</view>
</view>
<view :style="{ marginTop: '240rpx' }">
<SButton :onClick="unbindDevice" width="80vw" :rounded="40"
>解绑</SButton
>
</view>
<SButton :onClick="unbindDevice">解绑</SButton>
</view>
</Container>
</template>
@@ -381,18 +313,16 @@ onShow(() => {
justify-content: center;
color: #fff;
font-size: 14px;
margin-top: 200rpx;
margin: 100px 0;
}
.device-binded > view {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
font-size: 26rpx;
}
.device-binded > view > image {
width: 140rpx;
height: 140rpx;
width: 24vw;
height: 24vw;
margin-bottom: 5px;
border-radius: 10px;
}
@@ -404,7 +334,7 @@ onShow(() => {
text-align: center;
}
.device-binded > image {
width: 100rpx;
width: 16vw;
margin: 0 20px;
}
.has-device,
@@ -417,42 +347,11 @@ onShow(() => {
display: flex;
flex-direction: column;
align-items: center;
font-size: 28rpx;
margin-bottom: 100rpx;
font-size: 14px;
margin: 75px 0;
}
.has-device > view:nth-child(2) > text,
.just-bind > view:nth-child(2) > text {
margin: 5px;
}
.calibration {
position: absolute;
bottom: -145rpx;
left: 20rpx;
}
.calibration > button {
font-size: 26rpx;
color: #287fff;
display: flex;
align-items: center;
padding-bottom: 15rpx;
padding-left: 50rpx;
}
.calibration > button > image {
width: 28rpx;
height: 28rpx;
}
.calibration > view {
position: relative;
font-size: 22rpx;
color: #fff9;
padding-top: 34rpx;
padding-left: 35rpx;
width: 322rpx;
}
.calibration > view > image {
position: absolute;
top: 0;
left: 0;
width: 370rpx;
}
</style>

View File

@@ -1,6 +1,5 @@
<script setup>
import { onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Avatar from "@/components/Avatar.vue";
import BowData from "@/components/BowData.vue";
@@ -18,7 +17,7 @@ const practiseList = ref([]);
const toMatchDetail = (id) => {
uni.navigateTo({
url: `/pages/match-detail?battleId=${id}`,
url: `/pages/match-detail?id=${id}`,
});
};
const getPractiseDetail = async (id) => {
@@ -53,25 +52,10 @@ const onPractiseLoading = async (page) => {
}
return result.length;
};
const getName = (battle) => {
if (battle.mode <= 3) return `${battle.mode}V${battle.mode}`;
return battle.mode + "人大乱斗";
};
/**
* 支持通过 URL 参数指定初始 tab
* @example /pages/my-growth?tab=1 跳转到「好友约战」tab
*/
onLoad((options) => {
if (options && options.tab !== undefined) {
const tabIndex = parseInt(options.tab, 10);
if (!isNaN(tabIndex)) selectedIndex.value = tabIndex;
}
});
</script>
<template>
<Container title="我的成长脚印" :scroll="false">
<Container title="我的成长脚印" overflow="hidden">
<view class="tabs">
<view
v-for="(rankType, index) in ['排位赛', '好友约战', '个人练习']"
@@ -86,59 +70,53 @@ onLoad((options) => {
</view>
</view>
<view class="contents">
<swiper
:current="selectedIndex"
@change="(e) => (selectedIndex = e.detail.current)"
:style="{ height: '100%' }"
>
<swiper-item>
<ScrollList :onLoading="onMatchLoading">
<ScrollList :show="selectedIndex === 0" :onLoading="onMatchLoading">
<view
v-for="(item, index) in matchList"
:key="index"
@click="() => toMatchDetail(item.id)"
@click="() => toMatchDetail(item.battleId)"
>
<view class="contest-header">
<text>{{ getName(item) }}</text>
<text>{{ item.createTime }}</text>
<text>{{ item.name }}</text>
<text>{{ item.createdAt }}</text>
<image src="../static/back.png" mode="widthFix" />
</view>
<BattleHeader
:players="item.teams[0] ? item.teams[0].players : []"
:blueTeam="item.teams[1] ? item.teams[1].players : []"
:redTeam="item.teams[2] ? item.teams[2].players : []"
:winner="item.winTeam"
:showRank="item.teams[0]"
:players="item.mode === 1 ? [] : item.players"
:blueTeam="item.bluePlayers"
:redTeam="item.redPlayers"
:winner="item.winner"
:showRank="item.mode === 2"
:showHeader="false"
/>
</view>
</ScrollList>
</swiper-item>
<swiper-item>
<ScrollList :onLoading="onBattleLoading">
<ScrollList :show="selectedIndex === 1" :onLoading="onBattleLoading">
<view
v-for="(item, index) in battleList"
:key="index"
@click="() => toMatchDetail(item.id)"
@click="() => toMatchDetail(item.battleId)"
>
<view class="contest-header">
<text>{{ getName(item) }}</text>
<text>{{ item.createTime }}</text>
<text>{{ item.name }}</text>
<text>{{ item.createdAt }}</text>
<image src="../static/back.png" mode="widthFix" />
</view>
<BattleHeader
:players="item.teams[0] ? item.teams[0].players : []"
:blueTeam="item.teams[1] ? item.teams[1].players : []"
:redTeam="item.teams[2] ? item.teams[2].players : []"
:winner="item.winTeam"
:showRank="item.teams[0]"
:players="item.mode === 1 ? [] : item.players"
:blueTeam="item.bluePlayers"
:redTeam="item.redPlayers"
:winner="item.winner"
:showRank="item.mode === 2"
:showHeader="false"
/>
</view>
</ScrollList>
</swiper-item>
<swiper-item>
<ScrollList :onLoading="onPractiseLoading" :pageSize="15">
<ScrollList
:show="selectedIndex === 2"
:onLoading="onPractiseLoading"
:pageSize="15"
>
<view
v-for="(item, index) in practiseList"
:key="index"
@@ -147,13 +125,11 @@ onLoad((options) => {
>
<text
>{{ item.completed_arrows === 36 ? "耐力挑战" : "单组练习" }}
{{ item.createTime }}</text
{{ item.createdAt }}</text
>
<image src="../static/back.png" mode="widthFix" />
</view>
</ScrollList>
</swiper-item>
</swiper>
</view>
</Container>
</template>

View File

@@ -1,66 +0,0 @@
<script setup>
import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Avatar from "@/components/Avatar.vue";
import ScrollList from "@/components/ScrollList.vue";
import { getMyLikeList } from "@/apis";
const list = ref([]);
const onListLoading = async (page) => {
const result = await getMyLikeList(page);
if (page === 1) list.value = result.list;
else list.value = list.value.concat(result.list);
return result.list.length;
};
</script>
<template>
<Container
:bgType="2"
bgColor="#F5F5F5"
:whiteBackArrow="false"
title="赞我的朋友"
>
<view class="container">
<ScrollList :onLoading="onListLoading">
<block v-for="item in list" :key="item.id">
<view class="like-item">
<Avatar :src="item.avatar" mode="widthFix" />
<text>{{ item.name }}</text>
</view>
<view class="like-bottom-line" />
</block>
</ScrollList>
</view>
</Container>
</template>
<style scoped lang="scss">
.container {
width: 100%;
height: 100%;
}
.like-item {
background: $uni-white;
height: 140rpx;
width: 100%;
font-weight: 500;
font-size: 26rpx;
color: #333;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0 25rpx;
}
.like-item > text {
margin-left: 25rpx;
}
.like-bottom-line {
width: calc(100% - 50rpx);
margin: 0 25rpx;
height: 1rpx;
background: #e5e5e5;
}
</style>

View File

@@ -4,7 +4,6 @@ import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import EditOption from "@/components/EditOption.vue";
import SButton from "@/components/SButton.vue";
import { getPointBookDataAPI } from "@/apis";
const expandIndex = ref(0);
const bowType = ref("");
@@ -49,23 +48,16 @@ const toEditPage = () => {
bowtargetType: bowtargetType.value,
amountGroup: amountGroup.value,
});
uni.redirectTo({
uni.navigateTo({
url: "/pages/point-book-edit",
});
} else {
uni.showToast({
title: "请完善信息",
title: "Please complete the information",
icon: "none",
});
}
};
// onShow(async () => {
// const result = await getPointBookDataAPI();
// if (result) {
// days.value = result.total_day || 0;
// arrows.value = result.total_arrow || 0;
// }
// });
onMounted(async () => {
const pointBook = uni.getStorageSync("last-point-book");
if (pointBook) {
@@ -85,6 +77,7 @@ onMounted(async () => {
title="选择参数"
>
<view class="container">
<view>
<EditOption
:itemIndex="0"
:expand="expandIndex === 0"
@@ -113,9 +106,10 @@ onMounted(async () => {
:onSelect="onSelect"
/>
</view>
<template #bottom>
<SButton :rounded="50" :onClick="toEditPage">下一步</SButton>
</template>
</view>
<view :style="{ marginBottom: '20px' }">
<SButton :rounded="50" :onClick="toEditPage">Next</SButton>
</view>
</Container>
</template>

View File

@@ -7,8 +7,7 @@ import ScreenHint2 from "@/components/ScreenHint2.vue";
import RingBarChart from "@/components/RingBarChart.vue";
import { getPointBookDetailAPI, addNoteAPI } from "@/apis";
import { wxShare } from "@/util";
import { generateShareImage, generateShareCard } from "@/canvas";
import { wxShare, generateShareCard, generateShareImage } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
@@ -24,7 +23,7 @@ const targetId = ref(0);
const targetSrc = ref("");
const arrows = ref([]);
const notes = ref("");
const hasPoint = ref(false);
const draftNotes = ref("");
const record = ref({
groups: [],
user: {},
@@ -44,11 +43,11 @@ const closeTip = () => {
};
const saveNote = async () => {
if (record.value.id && notes.value) {
if (record.value.remark !== notes.value) {
await addNoteAPI(record.value.id, notes.value);
}
notes.value = draftNotes.value;
draftNotes.value = "";
showTip3.value = false;
if (record.value.id) {
await addNoteAPI(record.value.id, notes.value);
}
};
@@ -62,10 +61,16 @@ const onSelect = (index) => {
const goBack = () => {
const pages = getCurrentPages();
const lastPage = pages[pages.length - 2];
if (pages.length > 1) {
const currentPage = pages[pages.length - 2];
uni.navigateBack({
delta: lastPage.route === "pages/point-book-edit" ? 2 : 1,
delta: currentPage.route === "pages/point-book" ? 1 : 2,
});
} else {
uni.redirectTo({
url: "/pages/index",
});
}
};
const ringRates = computed(() => {
@@ -81,34 +86,17 @@ const loading = ref(false);
const shareImage = async () => {
if (loading.value) return;
loading.value = true;
await generateShareImage("shareImageCanvas", record.value);
await generateShareImage("shareImageCanvas");
await wxShare("shareImageCanvas");
loading.value = false;
};
onLoad(async (options) => {
if (options.id) {
const result = await getPointBookDetailAPI(options.id || 247);
const result = await getPointBookDetailAPI(options.id || 209);
record.value = result;
const config = uni.getStorageSync("point-book-config");
const bowType = config.bowOption.find(
(item) => item.id === record.value.bowType
);
const bowtargetType = config.targetOption.find(
(item) => item.id === record.value.targetType
);
uni.setStorageSync("point-book", {
bowType,
bowtargetType,
distance: result.distance,
amountGroup: result.groups,
});
const arrowData =
record.value.groups && record.value.groups[0]
? record.value.groups[0]
: {};
hasPoint.value = (arrowData.list || []).some((arrow) => arrow.x && arrow.y);
notes.value = result.remark || "";
const config = uni.getStorageSync("point-book-config");
config.targetOption.some((item) => {
if (item.id === result.targetType) {
targetId.value = item.id;
@@ -183,9 +171,8 @@ onShareTimeline(async () => {
></canvas>
<canvas
class="share-canvas"
id="shareImageCanvas"
type="2d"
:style="`width: 375px; height: ${hasPoint ? 800 : 440}px`"
canvas-id="shareImageCanvas"
style="width: 375px; height: 860px"
></canvas>
<view class="detail-data">
<view>
@@ -193,7 +180,7 @@ onShareTimeline(async () => {
:style="{ display: 'flex', alignItems: 'center' }"
@click="() => openTip(1)"
>
<text>落点稳定性</text>
<text>Stability</text>
<image
src="../static/s-question-mark.png"
mode="widthFix"
@@ -203,19 +190,19 @@ onShareTimeline(async () => {
<text>{{ Number((data.stability || 0).toFixed(2)) }}</text>
</view>
<view>
<view>黄心率</view>
<view>Yellow Rate</view>
<text>{{ Number((data.yellowRate * 100).toFixed(2)) }}%</text>
</view>
<view>
<view>10环数</view>
<view>Gold Rings</view>
<text>{{ data.tenRings }}</text>
</view>
<view>
<view>平均环数</view>
<view>Avg Rings</view>
<text>{{ Number((data.averageRing || 0).toFixed(2)) }}</text>
</view>
<view>
<view>总环数</view>
<view>Total Rings</view>
<text>{{ data.userTotalRing }}/{{ data.totalRing }}</text>
</view>
<button
@@ -223,16 +210,13 @@ onShareTimeline(async () => {
@click="() => openTip(3)"
v-if="user.id === record.user.id"
>
<image
:src="`../static/${notes ? 'has' : 'add'}-note.png`"
mode="widthFix"
/>
<text>{{ notes ? "我的备注" : "添加备注" }}</text>
<image src="../static/edit.png" mode="widthFix" />
<text>Notes</text>
</button>
</view>
<view class="title-bar" v-if="hasPoint">
<view class="title-bar">
<view />
<text>落点分布</text>
<text>Distribution</text>
<!-- <button hover-class="none" @click="() => openTip(2)">
<image
src="../static/s-question-mark.png"
@@ -241,36 +225,23 @@ onShareTimeline(async () => {
/>
</button> -->
</view>
<view
:style="{ transform: 'translateY(-64rpx) scale(0.9)' }"
v-if="hasPoint"
>
<view :style="{ transform: 'translateY(-45rpx)' }">
<BowTargetEdit
:id="targetId"
:src="targetSrc"
:arrows="arrows.filter((item) => item.x && item.y)"
/>
</view>
<view :style="{ transform: hasPoint ? 'translateY(-100rpx)' : 'none' }">
<!-- <view class="title-bar">
<view />
<text>环值分布</text>
</view> -->
<view :style="{ transform: 'translateY(-60rpx)' }">
<view :style="{ padding: '0 30rpx' }">
<RingBarChart :data="ringRates" />
</view>
<!-- <view class="title-bar" :style="{ marginTop: '30rpx' }">
<view />
<text>{{
selectedIndex === 0 ? "每组环数" : `${selectedIndex}组环数`
}}</text>
</view> -->
<view class="ring-text-groups">
<view v-for="(item, index) in record.groups" :key="index">
<view v-if="selectedIndex === 0 && index !== 0">
<text>{{ index }}</text>
<text>{{ item.userTotalRing }}</text>
<text></text>
<text>Ring</text>
</view>
<view
v-if="
@@ -297,50 +268,55 @@ onShareTimeline(async () => {
class="btns"
:style="{
gridTemplateColumns: `repeat(${
user.id === record.user.id ? 2 : 1
user.id === record.user.id ? 1 : 1
}, 1fr)`,
}"
>
<button hover-class="none" @click="goBack">关闭</button>
<button
<button hover-class="none" @click="goBack">Close</button>
<!-- <button
hover-class="none"
@click="shareImage"
v-if="user.id === record.user.id"
>
分享
</button>
</button> -->
</view>
</view>
<ScreenHint2 :show="showTip || showTip2 || showTip3" :onClose="closeTip">
<ScreenHint2
:show="showTip || showTip2 || showTip3"
:onClose="!notes && showTip3 ? null : closeTip"
>
<view class="tip-content">
<block v-if="showTip">
<text>落点稳定性说明</text>
<text>Stability Description</text>
<text
>通过计算每支箭与其他箭的平均距离衡量射箭的稳定性,数字越小则说明射箭越稳定。该数据只能在用户标记落点的情况下生成。</text
>The stability of archery is measured by calculating the average
distance of each arrow to other arrows. The smaller the number,
the more stable the archery. This data can only be generated when
the user marks the landing point.</text
>
</block>
<block v-if="showTip2">
<text>落点分布说明</text>
<text>展示用户某次练习中射箭的点位</text>
<text>Distribution Description</text>
<text>Show the user's archery points in a practice session</text>
</block>
<block v-if="showTip3">
<text>备注</text>
<text>Notes</text>
<text v-if="notes">{{ notes }}</text>
<textarea
v-model="notes"
v-if="!notes"
v-model="draftNotes"
maxlength="300"
rows="3"
rows="4"
class="notes-input"
placeholder="写下本次射箭的补充信息与心得"
placeholder-style="color: #ccc;"
/>
<view>
<button
hover-class="none"
@click="saveNote"
:class="notes ? '' : 'button-disabled'"
>
保存备注
<view v-if="!notes">
<button hover-class="none" @click="showTip3 = false">
Cancel
</button>
<button hover-class="none" @click="saveNote">Save Notes</button>
</view>
</block>
</view>
@@ -418,15 +394,15 @@ onShareTimeline(async () => {
}
.detail-data > button {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
font-size: 24rpx;
color: #333333;
font-size: 26rpx;
color: #999999;
}
.detail-data > button > image {
width: 44rpx;
height: 44rpx;
width: 28rpx;
height: 28rpx;
margin-right: 10rpx;
margin-left: 20rpx;
}
.question-mark {
width: 28rpx;
@@ -476,7 +452,7 @@ onShareTimeline(async () => {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
justify-content: space-between;
}
.tip-content > view > input {
width: 80%;
@@ -489,21 +465,21 @@ onShareTimeline(async () => {
}
.tip-content > view > button {
width: 48%;
border-radius: 44rpx;
background: linear-gradient(180deg, #fbfbfb 0%, #f5f5f5 100%);
border-radius: 22px;
border: 1px solid #eeeeee;
padding: 12px 0;
font-size: 14px;
color: #000;
background: #fed847;
}
.button-disabled {
background: linear-gradient(180deg, #fbfbfb 0%, #f5f5f5 100%) !important;
color: #ccc !important;
.tip-content > view > button:last-child {
background: #fed847;
}
.ring-text-groups {
display: flex;
flex-direction: column;
padding: 20rpx;
padding-top: 50rpx;
padding-top: 40rpx;
font-size: 24rpx;
color: #999999;
}
@@ -513,9 +489,10 @@ onShareTimeline(async () => {
}
.ring-text-groups > view > view:first-child:nth-last-child(2) {
margin-top: 10rpx;
width: 115rpx;
margin-left: 30rpx;
width: 90rpx;
text-align: center;
justify-content: flex-start;
justify-content: flex-end;
font-size: 20rpx;
display: flex;
color: #999;
@@ -527,17 +504,18 @@ onShareTimeline(async () => {
> view
> view:first-child:nth-last-child(2)
> text:nth-child(2) {
font-size: 28rpx;
font-size: 40rpx;
/* min-width: 45rpx; */
color: #666;
margin-right: 6rpx;
margin-top: -5rpx;
font-weight: 500;
}
.ring-text-groups > view > view:last-child {
width: 80%;
display: flex;
flex-wrap: wrap;
margin-bottom: 30rpx;
transform: translateX(20rpx);
}
.ring-text-groups > view > view:last-child > text {
width: 16.6%;

View File

@@ -1,6 +1,5 @@
<script setup>
import { ref, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ScreenHint2 from "@/components/ScreenHint2.vue";
import SButton from "@/components/SButton.vue";
@@ -37,7 +36,7 @@ const onSubmit = async () => {
);
if (!isComplete) {
return uni.showToast({
title: "请完善信息",
title: "Please complete the information",
icon: "none",
});
}
@@ -55,7 +54,6 @@ const onSubmit = async () => {
Object.values(arrowGroups.value)
);
if (res.record_id) {
uni.removeStorageSync("last-point-record");
uni.redirectTo({
url: `/pages/point-book-detail?id=${res.record_id}`,
});
@@ -66,35 +64,17 @@ const onClickRing = (ring) => {
if (arrowGroups.value[currentGroup.value]) {
arrowGroups.value[currentGroup.value][currentArrow.value] = { ring };
if (currentArrow.value < amount.value - 1) currentArrow.value++;
uni.setStorageSync("last-point-record", arrowGroups.value);
}
};
const deleteArrow = () => {
const arrow = arrowGroups.value[currentGroup.value][currentArrow.value];
if (JSON.stringify(arrow) === "{}") {
currentArrow.value -= 1;
} else {
arrowGroups.value[currentGroup.value][currentArrow.value] = {};
}
uni.$emit("set-edit-arrow", null);
uni.setStorageSync("last-point-record", arrowGroups.value);
};
const onEditDone = (arrow) => {
arrowGroups.value[currentGroup.value][currentArrow.value] = arrow;
if (currentArrow.value < amount.value - 1) currentArrow.value++;
uni.setStorageSync("last-point-record", arrowGroups.value);
};
const onSelectArrow = (index) => {
currentArrow.value = index;
const arrow = arrowGroups.value[currentGroup.value][currentArrow.value];
if (arrow && arrow.x && arrow.y) {
uni.$emit("set-edit-arrow", index);
} else {
uni.$emit("set-edit-arrow", null);
}
};
onLoad((options) => {
onMounted(() => {
const pointBook = uni.getStorageSync("last-point-book");
if (pointBook.bowtargetType) {
bowtarget.value = pointBook.bowtargetType;
@@ -112,27 +92,16 @@ onLoad((options) => {
arrowGroups.value[i] = new Array(amount.value).fill({});
}
}
if (options.withDraft) {
const draft = uni.getStorageSync("last-point-record");
if (draft) {
Object.values(draft).some((arrows, index1) =>
arrows.some((arrow, index2) => {
currentArrow.value = index2;
currentGroup.value = index1 + 1;
return JSON.stringify(arrow) === "{}";
})
);
arrowGroups.value = draft;
}
}
// uni.enableAlertBeforeUnload({
// message: "现在离开会导致未提交的数据丢失,是否继续?",
// });
});
</script>
<template>
<Container :bgType="2" bgColor="#F5F5F5" :whiteBackArrow="false">
<Container
:bgType="2"
bgColor="#F5F5F5"
:whiteBackArrow="false"
:onBack="() => (showTip = true)"
>
<view class="container">
<BowTargetEdit
:onChange="onEditDone"
@@ -143,11 +112,11 @@ onLoad((options) => {
<view class="title-bar">
<view>
<view />
<text> {{ currentGroup }} </text>
<text>Set {{ currentGroup }}</text>
</view>
<view @click="deleteArrow">
<image src="../static/delete.png" />
<text>删除</text>
<text>Delete</text>
</view>
</view>
<view class="bow-arrows">
@@ -155,7 +124,7 @@ onLoad((options) => {
v-if="arrowGroups[currentGroup]"
v-for="(arrow, index) in arrowGroups[currentGroup]"
:key="index"
@click="onSelectArrow(index)"
@click="currentArrow = index"
:style="{
borderColor: currentArrow === index ? '#FED847' : '#eeeeee',
borderWidth: currentArrow === index ? '2px' : '1px',
@@ -164,12 +133,15 @@ onLoad((options) => {
isNaN(arrow.ring)
? arrow.ring
: arrow.ring
? arrow.ring + " "
? arrow.ring + " points"
: ""
}}
}}</view
>
</view>
</view>
<text>推荐在靶纸上落点计分这样可获得稳定性分析</text>
<text
>It is recommended to score on the target face to obtain stability
analysis</text
>
<view class="bow-rings">
<view
v-for="(item, index) in ringTypes"
@@ -186,22 +158,22 @@ onLoad((options) => {
</view>
<ScreenHint2 :show="showTip">
<view class="tip-content">
<text>现在离开会导致</text>
<text>未提交的数据丢失是否继续</text>
<text>Leaving now will result in the loss of unsaved data.</text>
<text>Are you sure you want to continue?</text>
<view>
<button hover-class="none" @click="onBack">退出</button>
<button hover-class="none" @click="onBack">Exit</button>
<button hover-class="none" @click="showTip = false">
继续记录
Continue
</button>
</view>
</view>
</ScreenHint2>
</view>
<template #bottom>
<view :style="{ marginBottom: '20px' }">
<SButton :rounded="50" :onClick="onSubmit">
{{ currentGroup === groups ? "保存并查看分析" : "下一组" }}
{{ currentGroup === groups ? "Submit for analysis" : "Next set" }}
</SButton>
</template>
</view>
</Container>
</template>

View File

@@ -0,0 +1,381 @@
<script setup>
import { ref, computed, onMounted, watch } from "vue";
import { onShow } from "@dcloudio/uni-app";
import PointRecord from "@/components/PointRecord.vue";
import RingBarChart from "@/components/RingBarChart.vue";
import { getPointBookConfigAPI, getPointBookStatisticsAPI } from "@/apis";
import { getElementRect } from "@/util";
import { generateKDEHeatmapImage } from "@/kde-heatmap";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const isIOS = computed(() => {
const systemInfo = uni.getDeviceInfo();
return systemInfo.osName === "ios";
});
const loadImage = ref(false);
const data = ref({
weeksCheckIn: [],
ringRate: [],
});
const bowTargetSrc = ref("");
const heatMapImageSrc = ref(""); // 存储热力图图片地址
const startScoring = () => {
if (user.value.id) {
uni.navigateTo({
url: "/pages/point-book-create",
});
} else {
showModal.value = true;
}
};
const loadData = async () => {
const result = await getPointBookStatisticsAPI();
data.value = result;
const rect = await getElementRect(".heat-map");
let hot = 0;
if (result.checkInCount > -3 && result.checkInCount < 3) hot = 1;
else if (result.checkInCount >= 3) hot = 2;
else if (result.checkInCount >= 5) hot = 3;
else if (result.checkInCount === 7) hot = 4;
uni.$emit("update-hot", hot);
return;
loadImage.value = true;
const generateHeatmapAsync = async () => {
const weekArrows = result.weekArrows
.filter((item) => item.x && item.y)
.map((item) => [item.x, item.y]);
try {
// 渐进式渲染:数据量大时先快速渲染粗略版本
if (weekArrows.length > 1000) {
const quickPath = await generateKDEHeatmapImage(
"heatMapCanvas",
rect.width,
rect.height,
weekArrows
);
heatMapImageSrc.value = quickPath;
// 延迟后再渲染精细版本
await new Promise((resolve) => setTimeout(resolve, 500));
}
// 渲染最终精细版本
const finalPath = await generateKDEHeatmapImage(
"heatMapCanvas",
rect.width,
rect.height,
weekArrows,
{
range: [0, 1],
gridSize: 120, // 更高的网格密度,减少锯齿
bandwidth: 0.15, // 稍小的带宽,让热力图更细腻
showPoints: false,
}
);
heatMapImageSrc.value = finalPath;
loadImage.value = false;
console.log("热力图图片地址:", finalPath);
} catch (error) {
console.error("生成热力图图片失败:", error);
loadImage.value = false;
}
};
// 异步生成热力图不阻塞UI
generateHeatmapAsync();
};
onMounted(async () => {
const config = await getPointBookConfigAPI();
uni.setStorageSync("point-book-config", config);
if (config.targetOption && config.targetOption[0]) {
bowTargetSrc.value = config.targetOption[0].icon;
}
});
watch(
() => user.value.id,
(id) => {
if (id) loadData();
}
);
onShow(async () => {
if (user.value.id) loadData();
});
</script>
<template>
<view class="container">
<view class="daily-signin">
<view>
<image src="../static/week-check.png" />
</view>
<view :class="data.weeksCheckIn[0] ? 'checked' : ''">
<image
v-if="data.weeksCheckIn[0]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>Mon</text>
</view>
<view :class="data.weeksCheckIn[1] ? 'checked' : ''">
<image
v-if="data.weeksCheckIn[1]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>Tue</text>
</view>
<view :class="data.weeksCheckIn[2] ? 'checked' : ''">
<image
v-if="data.weeksCheckIn[2]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>Wed</text>
</view>
<view :class="data.weeksCheckIn[3] ? 'checked' : ''">
<image
v-if="data.weeksCheckIn[3]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>Thu</text>
</view>
<view :class="data.weeksCheckIn[4] ? 'checked' : ''">
<image
v-if="data.weeksCheckIn[4]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>Fri</text>
</view>
<view :class="data.weeksCheckIn[5] ? 'checked' : ''">
<image
v-if="data.weeksCheckIn[5]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>Sat</text>
</view>
<view :class="data.weeksCheckIn[6] ? 'checked' : ''">
<image
v-if="data.weeksCheckIn[6]"
src="../static/checked-green2.png"
mode="widthFix"
/>
<view v-else></view>
<text>Sun</text>
</view>
</view>
<view class="statistics">
<view>
<text>{{ data.todayTotalArrow || "-" }}</text>
<text>Today's Arrows</text>
</view>
<view>
<text>{{ data.totalArrow || "-" }}</text>
<text>Total Arrows</text>
</view>
<view>
<text>{{ data.totalDay || "-" }}</text>
<text>Training Days</text>
</view>
<view>
<text>{{ data.averageRing || "-" }}</text>
<text>Avg Score</text>
</view>
<view>
<text>{{
data.yellowRate !== undefined
? Number((data.yellowRate * 100).toFixed(2)) + "%"
: "-"
}}</text>
<text>Gold Rate</text>
</view>
<view>
<button hover-class="none" @click="startScoring">
<image src="../static/start-scoring.png" mode="widthFix" />
</button>
</view>
</view>
<view class="title" :style="{ marginBottom: 0 }">
<image src="../static/point-book-title1.png" mode="widthFix" />
</view>
<view class="heat-map">
<image
:src="bowTargetSrc || '../static/bow-target.png'"
mode="widthFix"
/>
<image v-if="heatMapImageSrc" :src="heatMapImageSrc" mode="aspectFill" />
<view v-if="loadImage" class="load-image">
<text>Generating...</text>
</view>
<canvas
id="heatMapCanvas"
canvas-id="heatMapCanvas"
type="2d"
style="
width: 100%;
height: 100%;
position: absolute;
top: -1000px;
left: 0;
z-index: 2;
"
/>
</view>
<RingBarChart :data="data.ringRate" />
<view :style="{ height: '25rpx' }" />
</view>
</template>
<style scoped>
.container {
width: 100%;
height: 100%;
overflow: auto;
}
.statistics {
border-radius: 25rpx;
border-bottom-left-radius: 50rpx;
border-bottom-right-radius: 50rpx;
border: 4rpx solid #fed848;
background: #fff;
font-size: 22rpx;
display: flex;
flex-wrap: wrap;
padding: 25rpx 0;
margin-bottom: 10rpx;
}
.statistics > view {
width: 33.33%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.statistics > view:nth-child(-n + 3) {
margin-bottom: 25rpx;
}
.statistics > view:nth-child(2),
.statistics > view:nth-child(5) {
border-left: 1rpx solid #eeeeee;
border-right: 1rpx solid #eeeeee;
box-sizing: border-box;
}
.statistics > view > text {
text-align: center;
font-size: 22rpx;
color: #333333;
}
.statistics > view > text:first-child {
font-weight: 500;
font-size: 40rpx;
margin-bottom: 10rpx;
}
.statistics > view:last-child > button > image {
width: 164rpx;
}
.daily-signin {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 10rpx;
border-radius: 20rpx;
margin-bottom: 25rpx;
}
.daily-signin > view {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 12rpx;
box-sizing: border-box;
}
.daily-signin > view:not(:first-child) {
background: #f8f8f8;
box-sizing: border-box;
width: 78rpx;
height: 94rpx;
padding-top: 10rpx;
}
.daily-signin > view:not(:first-child) > image {
width: 32rpx;
height: 32rpx;
}
.daily-signin > view:not(:first-child) > view {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
box-sizing: border-box;
border: 2rpx solid #333;
}
.daily-signin > view > text {
font-size: 20rpx;
color: #999999;
font-weight: 500;
text-align: center;
margin-top: 10rpx;
}
.daily-signin > view:first-child > image {
width: 72rpx;
height: 94rpx;
}
.checked {
border: 2rpx solid #000;
}
.checked > text {
color: #333 !important;
}
.title {
width: 100%;
display: flex;
justify-content: center;
margin: 25rpx 0;
}
.title > image {
width: 566rpx;
}
.heat-map {
position: relative;
margin: 0 auto;
width: calc(100vw - 70rpx);
height: calc(100vw - 70rpx);
transform: scale(0.9);
}
.heat-map > image {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.load-image {
position: absolute;
width: 160rpx;
top: calc(50% - 65rpx);
left: calc(50% - 75rpx);
color: #525252;
font-size: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
}
</style>

View File

@@ -1,6 +1,5 @@
<script setup>
import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { ref, onMounted } from "vue";
import Container from "@/components/Container.vue";
import SModal from "@/components/SModal.vue";
import EditOption from "@/components/EditOption.vue";
@@ -17,7 +16,6 @@ const showModal = ref(false);
const selectorIndex = ref(0);
const list = ref([]);
const removeId = ref("");
const pointDraft = ref(null);
const onListLoading = async (page) => {
const result = await getPointBookListAPI(
@@ -49,9 +47,9 @@ const confirmRemove = async () => {
showTip.value = false;
await removePointRecord(removeId.value);
list.value = list.value.filter((it) => it.id !== removeId.value);
uni.showToast({ title: "已删除", icon: "none" });
uni.showToast({ title: "Deleted", icon: "none" });
} catch (e) {
uni.showToast({ title: "删除失败,请重试", icon: "none" });
uni.showToast({ title: "Delete failed, please retry", icon: "none" });
}
};
@@ -66,22 +64,6 @@ const onSelectOption = (itemIndex, value) => {
showModal.value = false;
onListLoading(1);
};
const onRemoveDraft = () => {
pointDraft.value = null;
uni.removeStorageSync("last-point-record");
};
const toEditPage = () => {
uni.navigateTo({
url: "/pages/point-book-edit?withDraft=true",
});
};
onShow(() => {
const draft = uni.getStorageSync("last-point-record");
pointDraft.value = draft ? uni.getStorageSync("last-point-book") : null;
});
</script>
<template>
@@ -89,79 +71,36 @@ onShow(() => {
:bgType="2"
bgColor="#F5F5F5"
:whiteBackArrow="false"
title="计分记录"
title="Point Records"
>
<view class="container">
<view class="selectors">
<view @click="() => openSelector(0)">
<text :style="{ color: bowType.name ? '#000' : '#999' }">{{
bowType.name || "请选择"
bowType.name || "Please select"
}}</text>
<image src="../static/arrow-grey.png" mode="widthFix" />
</view>
<view @click="() => openSelector(1)">
<text :style="{ color: distance ? '#000' : '#999' }">{{
distance ? distance + " " : "请选择"
distance ? distance + " m" : "Please select"
}}</text>
<image src="../static/arrow-grey.png" mode="widthFix" />
</view>
<view @click="() => openSelector(2)">
<text :style="{ color: bowtargetType.name ? '#000' : '#999' }">{{
bowtargetType.name || "请选择"
bowtargetType.name || "Please select"
}}</text>
<image src="../static/arrow-grey.png" mode="widthFix" />
</view>
</view>
<view class="point-records">
<ScrollList :onLoading="onListLoading">
<uni-swipe-action>
<block v-if="pointDraft">
<uni-swipe-action-item>
<template v-slot:right>
<view class="swipe-right" @click="onRemoveDraft">
<image
class="swipe-icon"
src="../static/delete-white.png"
mode="widthFix"
/>
<view v-for="(item, index) in list" :key="item.id">
<PointRecord :data="item" :onRemove="onRemoveRecord" />
<view v-if="index < list.length - 1" :style="{ height: '25rpx' }"></view>
</view>
</template>
<view class="point-draft" v-if="pointDraft" @click="toEditPage">
<text>{{ pointDraft.bowType.name }}</text>
<text>{{ pointDraft.distance }}</text>
<text>{{ pointDraft.bowtargetType.name }}</text>
<view>
<image src="../static/draft-icon.png" mode="widthFix" />
<text>本地草稿</text>
<view>
<text>计分待完成</text>
<image src="../static/back.png" mode="widthFix" />
</view>
</view>
</view>
</uni-swipe-action-item>
<view :style="{ height: '25rpx' }" />
</block>
<block v-for="(item, index) in list" :key="item.id">
<uni-swipe-action-item>
<template v-slot:right>
<view class="swipe-right" @click="onRemoveRecord(item)">
<image
class="swipe-icon"
src="../static/delete-white.png"
mode="widthFix"
/>
</view>
</template>
<PointRecord :data="item" />
</uni-swipe-action-item>
<view
v-if="index < list.length - 1"
:style="{ height: '25rpx' }"
/>
</block>
</uni-swipe-action>
<view class="no-data" v-if="list.length === 0">暂无数据</view>
<view class="no-data" v-if="list.length === 0">No data</view>
</ScrollList>
</view>
<SModal
@@ -202,10 +141,10 @@ onShow(() => {
</SModal>
<ScreenHint2 :show="showTip">
<view class="tip-content">
<text>确认删除该记录吗?</text>
<text>Are you sure to delete this record?</text>
<view>
<button hover-class="none" @click="showTip = false">取消</button>
<button hover-class="none" @click="confirmRemove">确认</button>
<button hover-class="none" @click="showTip = false">Cancel</button>
<button hover-class="none" @click="confirmRemove">Confirm</button>
</view>
</view>
</ScreenHint2>
@@ -306,67 +245,4 @@ onShow(() => {
.tip-content > view > button:last-child {
background: #fed847;
}
/* 右侧滑动按钮(自定义宽度与图标) */
.swipe-right {
width: 120rpx; /* 这里可按需调整按钮宽度 */
height: 100%;
background-color: #ff7c7c;
display: flex;
align-items: center;
justify-content: center;
}
.swipe-icon {
width: 44rpx;
height: 44rpx;
}
.point-draft {
height: 200rpx;
border-radius: 25rpx;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.point-draft > text {
font-weight: 500;
font-size: 40rpx;
color: #333333;
margin: 0 20rpx;
}
.point-draft > view:last-child {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: #000000b3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.point-draft > view:last-child > image {
width: 46rpx;
height: 38rpx;
margin-bottom: 10rpx;
}
.point-draft > view:last-child > text {
font-weight: 500;
font-size: 26rpx;
color: #ffffff;
}
.point-draft > view:last-child > view {
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
color: #ffffff;
transform: translateX(8rpx);
}
.point-draft > view:last-child > view > image {
width: 30rpx;
height: 30rpx;
transform: rotate(180deg);
}
</style>

View File

@@ -0,0 +1,201 @@
<script setup>
import { ref } from "vue";
import Avatar from "@/components/Avatar.vue";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const editAvatar = ref(false);
const toEditPage = (type) => {
uni.navigateTo({
url: "/pages/edit-profile?type=" + type,
});
};
const toSignInPage = () => {
uni.navigateTo({
url: "/pages/sign-in",
});
};
</script>
<template>
<view class="container">
<view class="header">
<image :src="user.avatar" mode="widthFix" />
<button hover-class="none" @click="editAvatar = true">
<image src="../static/pen-yellow.png" mode="widthFix" />
</button>
</view>
<view class="body">
<view>
<button hover-class="none" @click="toEditPage('Name')">
<image src="../static/user-yellow.png" mode="widthFix" />
<text>Name</text>
<image src="../static/back-grey.png" mode="widthFix" />
</button>
<button hover-class="none" @click="toEditPage('Email')">
<image src="../static/email-yellow.png" mode="widthFix" />
<text>Email</text>
<image src="../static/back-grey.png" mode="widthFix" />
</button>
<button hover-class="none" @click="toEditPage('Password')">
<image src="../static/password-yellow.png" mode="widthFix" />
<text>Password</text>
<image src="../static/back-grey.png" mode="widthFix" />
</button>
</view>
<button hover-class="none" @click="toSignInPage">Log out</button>
<view>
<text>Have questions? Please contact us through email: </text>
<text>shelingxingqiu@163.com</text>
</view>
</view>
<view
class="edit-avatar"
:style="{ height: editAvatar ? '100vh' : '0' }"
@click="editAvatar = false"
>
<image :src="user.avatar" mode="widthFix" />
<view>
<button hover-class="none">
<text>Take a photo</text>
<image src="../static/back-grey.png" mode="widthFix" />
</button>
<button hover-class="none">
<text>Choose photo</text>
<image src="../static/back-grey.png" mode="widthFix" />
</button>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.container {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
}
.header {
position: relative;
margin-top: -120rpx;
}
.header > image {
width: 180rpx;
height: 180rpx;
border-radius: 50%;
border: 4rpx solid #fff;
}
.header > button {
position: absolute;
right: 0;
bottom: 0;
}
.header > button > image {
width: 60rpx;
height: 60rpx;
}
.body {
width: 100%;
margin-top: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.body > view:first-child {
background-color: $uni-bg-color;
border-radius: 25px;
padding: 0 20px;
width: calc(100% - 80rpx);
}
.body > view:first-child > button {
display: flex;
align-items: center;
padding: 20px 0;
}
.body > view:first-child > button:not(:last-child) {
border-bottom: 1rpx solid #e3e3e3;
}
.body > view:first-child > button > image:first-child {
width: 40rpx;
height: 40rpx;
}
.body > view:first-child > button > text {
flex: 1;
font-size: 26rpx;
color: #333333;
text-align: left;
padding-left: 20rpx;
}
.body > view:first-child > button > image:last-child {
width: 28rpx;
height: 28rpx;
}
.body > button {
margin-top: 24rpx;
background: $uni-bg-color;
border-radius: 24rpx;
font-size: 26rpx;
color: $uni-link-color;
text-align: center;
padding: 20px 0;
width: 100%;
}
.body > view:last-child {
margin-top: auto;
padding-bottom: 25rpx;
display: flex;
flex-direction: column;
align-items: center;
font-size: 24rpx;
color: #666666;
}
.body > view:last-child > text:last-child {
color: $uni-link-color;
}
.edit-avatar {
position: fixed;
top: 0;
right: 0;
width: 100vw;
background: rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.edit-avatar > image {
width: 85vw;
height: 85vw;
border-radius: 50%;
}
.edit-avatar > view {
border-radius: 25rpx;
margin-top: 100rpx;
width: calc(100% - 150rpx);
padding: 0 40rpx;
background: #404040;
}
.edit-avatar > view > button {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 30rpx;
color: #ffffff;
padding: 40rpx 0;
}
.edit-avatar > view > button:not(:last-child) {
border-bottom: 1rpx solid #fff3;
border-radius: 0;
}
.edit-avatar > view > button > image {
width: 28rpx;
}
</style>

View File

@@ -1,152 +0,0 @@
<script setup>
import { ref, onMounted } from "vue";
import Container from "@/components/Container.vue";
import PointRankItem from "@/components/PointRankItem.vue";
import { getPointBookRankListAPI } from "@/apis";
import { capsuleHeight } from "@/util";
import { wxShare, debounce } from "@/util";
import { sharePointData } from "@/canvas";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const { user } = storeToRefs(useStore());
const list = ref([]);
const mine = ref({
averageRing: 0,
});
const shareImage = async () => {
if (!mine.value.id) return;
await sharePointData("shareCanvas", mine.value);
await wxShare("shareCanvas");
};
onMounted(async () => {
const result = await getPointBookRankListAPI();
mine.value = result.my;
list.value = result.list;
});
</script>
<template>
<Container :bgType="5" bgColor="#F5F5F5" :whiteBackArrow="false">
<view class="top-part">
<view>
<image src="../static/point-champion.png" mode="widthFix" />
<image
:src="list[0] && list[0].avatar ? list[0].avatar : ''"
mode="widthFix"
/>
</view>
<block v-if="list[0]">
<text>{{ list[0].name }}占领了封面</text>
<text>整整消耗了{{ Math.round(list[0].weekArrow * 1.6) }}大卡!</text>
</block>
</view>
<view class="rank-title-bar">
<text>排行</text>
<text>用户</text>
<text>本周箭数</text>
<text>消耗</text>
</view>
<view
class="data-list"
:style="{ marginBottom: '20rpx' }"
v-if="user.id && mine"
>
<PointRankItem :data="mine" :borderWidth="0" />
</view>
<view class="data-list">
<PointRankItem v-for="item in list" :key="item.id" :data="item" />
</view>
<view :style="{ height: '30rpx' }"></view>
<button
hover-class="none"
class="share-btn"
@click="shareImage"
v-if="user.id"
>
<image src="../static/share-icon.png" mode="widthFix" />
</button>
<canvas
class="share-canvas"
id="shareCanvas"
type="2d"
style="width: 375px; height: 460px"
></canvas>
</Container>
</template>
<style scoped lang="scss">
.container {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.top-part {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
font-size: 26rpx;
color: #333333;
height: 450rpx;
}
.top-part > view:first-child {
width: 310rpx;
height: 310rpx;
position: relative;
}
.top-part > view:first-child > image:first-child {
width: 100%;
}
.top-part > view:first-child > image:nth-child(2) {
position: absolute;
width: 140rpx;
height: 140rpx;
border-radius: 50%;
top: calc(50% - 70rpx);
left: calc(50% - 70rpx);
}
.top-part > text {
margin-bottom: 15rpx;
}
.rank-title-bar {
font-size: 24rpx;
color: #777777;
display: flex;
align-items: center;
text-align: center;
width: calc(100% - 80rpx);
line-height: 80rpx;
padding: 0 40rpx;
}
.rank-title-bar > text:nth-child(1) {
width: 60rpx;
}
.rank-title-bar > text:nth-child(2) {
flex: 1;
}
.rank-title-bar > text:nth-child(3) {
width: 18%;
}
.rank-title-bar > text:nth-child(4) {
width: 24%;
}
.data-list {
background: $uni-white;
border-radius: 25rpx;
margin: 0 25rpx;
}
.share-btn {
position: fixed;
right: 25rpx;
bottom: 25rpx;
}
.share-btn > image {
width: 116rpx;
height: 116rpx;
}
</style>

View File

@@ -4,16 +4,17 @@ import { onShow, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import PointRecord from "@/components/PointRecord.vue";
import RingBarChart from "@/components/RingBarChart.vue";
import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue";
import ScreenHint2 from "@/components/ScreenHint2.vue";
import RewardUs from "@/components/RewardUs.vue";
import PointRankItem from "@/components/PointRankItem.vue";
import {
getHomeData,
getPointBookConfigAPI,
getPointBookRankListAPI,
getPointBookListAPI,
getPointBookStatisticsAPI,
removePointRecord,
} from "@/apis";
import { getElementRect } from "@/util";
@@ -26,7 +27,10 @@ const store = useStore();
const { updateUser } = store;
const { user } = storeToRefs(store);
const isIOS = uni.getDeviceInfo().osName === "ios";
const isIOS = computed(() => {
const systemInfo = uni.getDeviceInfo();
return systemInfo.osName === "ios";
});
const loadImage = ref(false);
const showModal = ref(false);
@@ -40,22 +44,12 @@ const list = ref([]);
const bowTargetSrc = ref("");
const heatMapImageSrc = ref(""); // 存储热力图图片地址
const canvasVisible = ref(false); // 控制canvas显示状态
const strength = ref(0);
const removeId = ref("");
const toRecordPage = () => {
if (user.value.id) {
const toListPage = () => {
uni.navigateTo({
url: "/pages/point-book-list",
});
} else {
showModal.value = true;
}
};
const toRankPage = () => {
uni.navigateTo({
url: "/pages/point-book-rank",
});
};
const onSignin = () => {
@@ -64,39 +58,36 @@ const onSignin = () => {
const startScoring = () => {
if (user.value.id) {
const draft = uni.getStorageSync("last-point-record");
if (draft) {
showTip2.value = true;
return;
}
toScorePage();
uni.navigateTo({
url: "/pages/point-book-create",
});
} else {
showModal.value = true;
}
};
const toScorePage = (withDraft) => {
showTip2.value = false;
if (withDraft) {
return uni.navigateTo({
url: "/pages/point-book-edit?withDraft=true",
});
}
uni.removeStorageSync("last-point-record");
return uni.navigateTo({
url: "/pages/point-book-create",
});
const onRemoveRecord = (item) => {
removeId.value = item.id;
showTip2.value = true;
};
const closeHint = () => {
showTip.value = false;
const confirmRemove = async () => {
try {
showTip2.value = false;
await removePointRecord(removeId.value);
const result = await getPointBookListAPI(1);
list.value = result.slice(0, 3);
uni.showToast({ title: "Deleted", icon: "none" });
} catch (e) {
uni.showToast({ title: "Delete failed, please retry", icon: "none" });
}
};
const loadData = async () => {
const result = await getPointBookListAPI(1);
list.value = result.slice(0, 3);
const result2 = await getPointBookStatisticsAPI();
data.value = result2;
strength.value = Math.min(10, (5 / 60) * result2.todayTotalArrow);
const rect = await getElementRect(".heat-map");
let hot = 0;
@@ -140,9 +131,9 @@ const loadData = async () => {
);
heatMapImageSrc.value = finalPath;
loadImage.value = false;
console.log("热力图图片地址:", finalPath);
console.log("Heatmap image path:", finalPath);
} catch (error) {
console.error("生成热力图图片失败:", error);
console.error("Failed to generate heatmap image:", error);
loadImage.value = false;
}
};
@@ -151,10 +142,6 @@ const loadData = async () => {
generateHeatmapAsync();
};
const strengthText = computed(() => {
return strength.value > 6 ? "重度" : strength.value >= 4 ? "中度" : "轻度";
});
watch(
() => user.value.id,
(id) => {
@@ -165,11 +152,6 @@ watch(
onShow(async () => {
uni.removeStorageSync("point-book");
if (user.value.id) loadData();
const result = await getPointBookRankListAPI(1);
list.value = result.list.slice(0, 3);
if (user.value.id && list.value.every((item) => item.id !== user.value.id)) {
list.value = [result.my, ...result.list.slice(0, 3)];
}
});
onMounted(async () => {
@@ -192,22 +174,22 @@ onBeforeUnmount(() => {
uni.$off("point-book-signin", onSignin);
});
onShareAppMessage(() => {
return {
title: "高效记录每一次射箭,深度分析助你提升!",
path: "pages/point-book",
imageUrl:
"https://static.shelingxingqiu.com/attachment/2025-09-22/dcz4m4nbgycqqwknrv.png",
};
});
onShareTimeline(() => {
return {
title: "高效记录每一次射箭,深度分析助你提升!",
query: "from=timeline",
imageUrl:
"https://static.shelingxingqiu.com/attachment/2025-09-22/dcz4m4nbgycqqwknrv.png",
};
});
// onShareAppMessage(() => {
// return {
// title: "高效记录每一次射箭,深度分析助你提升!",
// path: "pages/point-book",
// imageUrl:
// "https://static.shelingxingqiu.com/attachment/2025-09-22/dcz4m4nbgycqqwknrv.png",
// };
// });
// onShareTimeline(() => {
// return {
// title: "高效记录每一次射箭,深度分析助你提升!",
// query: "from=timeline",
// imageUrl:
// "https://static.shelingxingqiu.com/attachment/2025-09-22/dcz4m4nbgycqqwknrv.png",
// };
// });
</script>
<template>
@@ -224,7 +206,7 @@ onShareTimeline(() => {
mode="widthFix"
/>
<view v-else></view>
<text>周一</text>
<text>Mon</text>
</view>
<view :class="data.weeksCheckIn[1] ? 'checked' : ''">
<image
@@ -233,7 +215,7 @@ onShareTimeline(() => {
mode="widthFix"
/>
<view v-else></view>
<text>周二</text>
<text>Tue</text>
</view>
<view :class="data.weeksCheckIn[2] ? 'checked' : ''">
<image
@@ -242,7 +224,7 @@ onShareTimeline(() => {
mode="widthFix"
/>
<view v-else></view>
<text>周三</text>
<text>Wed</text>
</view>
<view :class="data.weeksCheckIn[3] ? 'checked' : ''">
<image
@@ -251,7 +233,7 @@ onShareTimeline(() => {
mode="widthFix"
/>
<view v-else></view>
<text>周四</text>
<text>Thu</text>
</view>
<view :class="data.weeksCheckIn[4] ? 'checked' : ''">
<image
@@ -260,7 +242,7 @@ onShareTimeline(() => {
mode="widthFix"
/>
<view v-else></view>
<text>周五</text>
<text>Fri</text>
</view>
<view :class="data.weeksCheckIn[5] ? 'checked' : ''">
<image
@@ -269,7 +251,7 @@ onShareTimeline(() => {
mode="widthFix"
/>
<view v-else></view>
<text>周六</text>
<text>Sat</text>
</view>
<view :class="data.weeksCheckIn[6] ? 'checked' : ''">
<image
@@ -278,74 +260,43 @@ onShareTimeline(() => {
mode="widthFix"
/>
<view v-else></view>
<text>周日</text>
<text>Sun</text>
</view>
</view>
<view class="statistics">
<view>
<view class="statistics-item">
<text>{{ data.todayTotalArrow || "-" }}</text>
<text></text>
<text>今日射箭</text>
</view>
<view class="statistics-item" :style="{ padding: '20rpx 0' }">
<text>{{ Math.round(data.todayTotalArrow * 1.6) || "-" }}</text>
<text></text>
<text>今日消耗</text>
</view>
<view class="statistics-item">
<text>{{ strength || "-" }}</text>
<text v-show="strength" class="strength">{{ strengthText }}</text>
<text>运动强度</text>
</view>
<text>Arrows Today</text>
</view>
<view>
<view :style="{ paddingBottom: '20rpx' }">
<view class="statistics-item">
<text>{{ data.totalDay || "-" }}</text>
<text></text>
<text>训练天数</text>
</view>
<view class="statistics-item">
<text>{{ data.totalArrow || "-" }}</text>
<text></text>
<text>累计射箭</text>
<text>Total Arrows</text>
</view>
<view>
<text>{{ data.totalDay || "-" }}</text>
<text>Training Days</text>
</view>
<view :style="{ marginTop: '20rpx' }">
<view class="statistics-item">
<view>
<text>{{ data.averageRing || "-" }}</text>
<text>Average Rings</text>
</view>
<view>
<text>{{
data.yellowRate !== undefined
? Number((data.yellowRate * 100).toFixed(2))
? Number((data.yellowRate * 100).toFixed(2)) + "%"
: "-"
}}</text>
<text>%</text>
<text>黄心率</text>
</view>
<view class="statistics-item">
<text>{{ data.averageRing || "-" }}</text>
<text></text>
<text>平均环数</text>
</view>
<text>Gold Rate</text>
</view>
<view>
<button hover-class="none" @click="toRecordPage" class="image-btn">
<image src="../static/record-btn.png" mode="widthFix" />
</button>
<button hover-class="none" @click="startScoring" class="image-btn">
<button hover-class="none" @click="startScoring">
<image src="../static/start-scoring.png" mode="widthFix" />
</button>
</view>
</view>
</view>
<view class="title" :style="{ marginBottom: 0 }">
<image src="../static/point-book-title1.png" mode="widthFix" />
</view>
<image
src="https://static.shelingxingqiu.com/attachment/2025-12-31/dfc9dxrpyf4exh4rhd.png"
mode="widthFix"
class="bowtarget-theme"
/>
<view class="heat-map">
<image
:src="bowTargetSrc || '../static/bow-target.png'"
@@ -357,10 +308,11 @@ onShareTimeline(() => {
mode="aspectFill"
/>
<view v-if="loadImage" class="load-image">
<text>生成中...</text>
<text>Generating...</text>
</view>
<canvas
id="heatMapCanvas"
canvas-id="heatMapCanvas"
type="2d"
style="
width: 100%;
@@ -381,52 +333,47 @@ onShareTimeline(() => {
<view class="title" v-if="user.id">
<image src="../static/point-book-title2.png" mode="widthFix" />
</view>
<view class="top-list">
<view class="rank-title-bar">
<text>排行</text>
<text>用户</text>
<text>本周箭数</text>
<text>消耗</text>
</view>
<PointRankItem v-for="item in list" :key="item.id" :data="item" />
</view>
<block v-for="(item, index) in list" :key="item.id">
<PointRecord :data="item" />
<view
v-if="index < list.length - 1"
:style="{ height: '25rpx' }"
></view>
</block>
<view
class="see-more"
@click="toRankPage"
@click="toListPage"
v-if="list.length"
:style="{ marginBottom: isIOS ? '10rpx' : 0 }"
>
<text>查看完整榜单</text>
<text>View all records</text>
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
</view>
</view>
<Signin
:show="showModal"
:onClose="() => (showModal = false)"
:noBg="true"
/>
<ScreenHint2 :show="showTip || showTip2" :onClose="closeHint">
<SModal :show="showModal" :onClose="() => (showModal = false)" :noBg="true">
<Signin :onClose="() => (showModal = false)" :noBg="true" />
</SModal>
<ScreenHint2
:show="showTip || showTip2"
:onClose="showTip ? () => (showTip = false) : null"
>
<RewardUs
v-if="showTip"
:show="showTip"
:onClose="() => (showTip = false)"
/>
<view class="tip-content" v-if="showTip2">
<text>发现未完成的记分是否继续编辑</text>
<text>Are you sure to delete this record?</text>
<view>
<button hover-class="none" @click="toScorePage(false)">
重新计分
</button>
<button hover-class="none" @click="toScorePage(true)">
继续编辑
</button>
<button hover-class="none" @click="showTip2 = false">Cancel</button>
<button hover-class="none" @click="confirmRemove">Confirm</button>
</view>
</view>
</ScreenHint2>
</Container>
</template>
<style scoped lang="scss">
<style scoped>
.container {
width: calc(100% - 50rpx);
padding: 25rpx;
@@ -439,67 +386,38 @@ onShareTimeline(() => {
background: #fff;
font-size: 22rpx;
display: flex;
justify-content: space-between;
padding: 40rpx;
padding-left: 20rpx;
flex-wrap: wrap;
padding: 25rpx 0;
margin-bottom: 10rpx;
}
.statistics > view {
width: 33.33%;
display: flex;
flex-direction: column;
align-items: center;
}
.statistics > view:first-child {
align-items: center;
justify-content: center;
border-right: $uni-border;
}
.statistics > view:first-child > view {
width: 210rpx;
.statistics > view:nth-child(-n + 3) {
margin-bottom: 25rpx;
}
.statistics > view:last-child {
flex: 1;
.statistics > view:nth-child(2),
.statistics > view:nth-child(5) {
border-left: 1rpx solid #eeeeee;
border-right: 1rpx solid #eeeeee;
box-sizing: border-box;
}
.statistics > view:last-child > view {
display: flex;
align-items: center;
justify-content: space-around;
width: calc(100% - 20rpx);
padding-left: 20rpx;
.statistics > view > text {
text-align: center;
font-size: 22rpx;
color: #333333;
}
.statistics > view:last-child > view:first-child {
border-bottom: $uni-border;
}
.statistics-item {
width: 180rpx;
display: flex;
flex-wrap: wrap;
justify-content: center;
color: $uni-text-color;
font-size: 24rpx;
}
.statistics-item > text:first-child {
.statistics > view > text:first-child {
font-weight: 500;
font-size: 40rpx;
margin-right: 10rpx;
margin-bottom: 10rpx;
}
.statistics-item > text:nth-child(2) {
transform: translateY(16rpx);
}
.statistics-item > text:nth-child(3) {
width: 100%;
text-align: center;
}
.image-btn {
width: 170rpx;
height: 74rpx;
display: flex;
align-items: center;
overflow: unset;
margin-top: 30rpx;
}
.image-btn > image {
width: 100%;
height: 100%;
.statistics > view:last-child > button > image {
width: 164rpx;
}
.daily-signin {
display: grid;
@@ -566,8 +484,6 @@ onShareTimeline(() => {
width: calc(100vw - 70rpx);
height: calc(100vw - 70rpx);
transform: scale(0.9);
border-radius: 50%;
overflow: hidden;
}
.heat-map > image {
width: 100%;
@@ -632,48 +548,4 @@ onShareTimeline(() => {
.tip-content > view > button:last-child {
background: #fed847;
}
.bowtarget-theme {
width: 100vw;
margin-left: -25rpx;
margin-bottom: -30vw;
}
.top-list {
background: $uni-white;
border-radius: 25rpx;
border: 2rpx solid #fed848;
overflow: hidden;
}
.rank-title-bar {
background: $uni-white;
font-size: 24rpx;
color: #777777;
display: flex;
align-items: center;
text-align: center;
width: calc(100% - 40rpx);
line-height: 80rpx;
padding: 0 20rpx;
}
.rank-title-bar > text:nth-child(1) {
width: 55rpx;
}
.rank-title-bar > text:nth-child(2) {
flex: 1;
}
.rank-title-bar > text:nth-child(3) {
width: 16%;
}
.rank-title-bar > text:nth-child(4) {
width: 25%;
}
.strength {
font-size: 22rpx;
color: #777777;
border-radius: 8rpx;
border: 1rpx solid #777777;
height: 20rpx;
padding: 8rpx;
line-height: 20rpx;
transform: translateY(10rpx) !important;
}
</style>

View File

@@ -1,6 +1,5 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import BowTarget from "@/components/BowTarget.vue";
@@ -11,88 +10,92 @@ import Avatar from "@/components/Avatar.vue";
import BowPower from "@/components/BowPower.vue";
import TestDistance from "@/components/TestDistance.vue";
import BubbleTip from "@/components/BubbleTip.vue";
import audioManager from "@/audioManager";
import {
createPractiseAPI,
startPractiseAPI,
endPractiseAPI,
getPractiseAPI,
} from "@/apis";
import { sharePractiseData } from "@/canvas";
import { wxShare, debounce } from "@/util";
import { MESSAGETYPESV2, roundsName } from "@/constants";
import { createPractiseAPI } from "@/apis";
import { generateCanvasImage } from "@/util";
import { MESSAGETYPES, roundsName } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const start = ref(false);
const scores = ref([]);
const total = 12;
const currentRound = ref(0);
const practiseResult = ref({});
const power = ref(0);
const practiseId = ref("");
const showGuide = ref(false);
const tips = ref("");
const targetType = ref(1);
onLoad((options) => {
if (options.target) {
targetType.value = Number(options.target);
}
});
const onReady = async () => {
await startPractiseAPI();
const result = await createPractiseAPI(total);
if (result) practiseId.value = result.id;
currentRound.value = 0;
scores.value = [];
start.value = true;
audioManager.play("练习开始");
setTimeout(() => {
uni.$emit("play-sound", "请开始射击");
}, 300);
};
const onOver = async () => {
practiseResult.value = await getPractiseAPI(practiseId.value);
start.value = false;
};
async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult) {
scores.value = msg.details;
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(onOver, 1500);
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
if (scores.value.length < total) {
scores.value.push(msg.target);
currentRound.value += 1;
if (currentRound.value === 4) {
currentRound.value = 1;
}
if (practiseId && scores.value.length === total / 2) {
showGuide.value = true;
setTimeout(() => {
showGuide.value = false;
}, 3000);
}
}
power.value = msg.target.battery;
}
if (msg.constructor === MESSAGETYPES.ShootSyncMePracticeID) {
if (practiseId.value && practiseId.value === msg.practice.id) {
setTimeout(() => {
start.value = false;
practiseResult.value = {
...msg.practice,
arrows: JSON.parse(msg.practice.arrows),
lvl: msg.lvl,
};
generateCanvasImage(
"shareCanvas",
2,
user.value,
practiseResult.value
);
}, 1500);
}
}
});
}
async function onComplete() {
const validArrows = (practiseResult.value.details || []).filter(
(a) => a.x !== -30 && a.y !== -30
);
if (validArrows.length === total) {
if (
practiseResult.value.arrows &&
practiseResult.value.arrows.length === total
) {
uni.navigateBack();
} else {
practiseId.value = "";
practiseResult.value = {};
start.value = false;
scores.value = [];
const result = await createPractiseAPI(total, 120);
if (result) practiseId.value = result.id;
currentRound.value = 0;
}
}
const onClickShare = debounce(async () => {
await sharePractiseData("shareCanvas", 2, user.value, practiseResult.value);
await wxShare("shareCanvas");
});
onMounted(async () => {
// audioManager.play("第一轮");
onMounted(() => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("share-image", onClickShare);
const result = await createPractiseAPI(total, 120, targetType.value);
if (result) practiseId.value = result.id;
});
onBeforeUnmount(() => {
@@ -100,21 +103,14 @@ onBeforeUnmount(() => {
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
uni.$off("share-image", onClickShare);
audioManager.stopAll();
endPractiseAPI();
});
</script>
<template>
<Container
:bgType="1"
title="个人单组练习"
:showBottom="!start && !scores.length"
>
<Container :bgType="1" title="个人单组练习">
<view>
<TestDistance v-if="!start && !practiseResult.id" />
<block v-else>
<TestDistance v-if="!practiseId" />
<block v-if="practiseId">
<ShootProgress
:tips="`${
!start || scores.length === 12
@@ -124,7 +120,7 @@ onBeforeUnmount(() => {
}轮`
}`"
:start="start"
:onStop="onOver"
:total="120"
/>
<view class="user-row">
<Avatar :src="user.avatar" :size="35" />
@@ -132,34 +128,30 @@ onBeforeUnmount(() => {
<text>还有两场坚持</text>
<text>就是胜利💪</text>
</BubbleTip>
<BowPower />
<BowPower :power="power" />
</view>
<BowTarget
:totalRound="start ? total / 4 : 0"
:currentRound="scores.length % 3"
:currentRound="currentRound"
:scores="scores"
/>
<ScorePanel2 :arrows="scores" />
<ScorePanel2 :scores="scores.map((s) => s.ring)" />
<ScoreResult
v-if="practiseResult.details"
v-if="practiseResult.arrows"
:rowCount="6"
:total="total"
:onClose="onComplete"
:result="practiseResult"
:tipSrc="`../static/${
practiseResult.details.filter(
(arrow) => arrow.x !== -30 && arrow.y !== -30
).length < total
? 'un'
: ''
practiseResult.arrows.length < total ? 'un' : ''
}finish-tip.png`"
/>
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
<canvas class="share-canvas" canvas-id="shareCanvas"></canvas>
</block>
</view>
<template #bottom>
<SButton :onClick="onReady">准备好了直接开始</SButton>
</template>
<view :style="{ marginBottom: '20px' }">
<SButton v-if="!start" :onClick="onReady">准备好了直接开始</SButton>
</view>
</Container>
</template>

View File

@@ -10,103 +10,85 @@ import Avatar from "@/components/Avatar.vue";
import BowPower from "@/components/BowPower.vue";
import TestDistance from "@/components/TestDistance.vue";
import BubbleTip from "@/components/BubbleTip.vue";
import audioManager from "@/audioManager";
import {
createPractiseAPI,
startPractiseAPI,
endPractiseAPI,
getPractiseAPI,
} from "@/apis";
import { sharePractiseData } from "@/canvas";
import { wxShare, debounce } from "@/util";
import { MESSAGETYPESV2 } from "@/constants";
import { createPractiseAPI } from "@/apis";
import { generateCanvasImage } from "@/util";
import { MESSAGETYPES } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
import {onLoad} from "@dcloudio/uni-app";
const store = useStore();
const { user } = storeToRefs(store);
const start = ref(false);
const scores = ref([]);
const total = 36;
const practiseResult = ref({});
const power = ref(0);
const practiseId = ref("");
const showGuide = ref(false);
const targetType = ref(1);
onLoad((options) => {
if (options.target) {
targetType.value = Number(options.target);
}
});
const onReady = async () => {
await startPractiseAPI();
const result = await createPractiseAPI(total);
if (result) practiseId.value = result.id;
scores.value = [];
start.value = true;
audioManager.play("练习开始");
setTimeout(() => {
uni.$emit("play-sound", "请开始射击");
}, 300);
};
const onOver = async () => {
practiseResult.value = await getPractiseAPI(practiseId.value);
start.value = false;
};
async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult) {
scores.value = msg.details;
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(onOver, 1500);
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
if (scores.value.length < total) {
scores.value.push(msg.target);
if (practiseId && scores.value.length === total / 2) {
showGuide.value = true;
setTimeout(() => {
showGuide.value = false;
}, 3000);
}
// messages.forEach((msg) => {
// if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
// if (scores.value.length < total) {
// scores.value.push(msg.target);
// if (practiseId && scores.value.length === total / 2) {
// showGuide.value = true;
// setTimeout(() => {
// showGuide.value = false;
// }, 3000);
// }
// if (scores.value.length === total) {
// setTimeout(onOver, 1500);
// }
// }
// }
// });
}
power.value = msg.target.battery;
}
if (msg.constructor === MESSAGETYPES.ShootSyncMePracticeID) {
if (practiseId.value && practiseId.value === msg.practice.id) {
setTimeout(() => {
start.value = false;
practiseResult.value = {
...msg.practice,
arrows: JSON.parse(msg.practice.arrows),
lvl: msg.lvl,
};
generateCanvasImage(
"shareCanvas",
3,
user.value,
practiseResult.value
);
}, 1500);
}
}
});
}
async function onComplete() {
const validArrows = (practiseResult.value.details || []).filter(
(a) => a.x !== -30 && a.y !== -30
);
if (validArrows.length === total) {
if (
practiseResult.value.arrows &&
practiseResult.value.arrows.length === total
) {
uni.navigateBack();
} else {
practiseId.value = "";
practiseResult.value = {};
start.value = false;
scores.value = [];
const result = await createPractiseAPI(total, 360);
if (result) practiseId.value = result.id;
}
}
const onClickShare = debounce(async () => {
await sharePractiseData("shareCanvas", 3, user.value, practiseResult.value);
await wxShare("shareCanvas");
});
onMounted(async () => {
onMounted(() => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("share-image", onClickShare);
const result = await createPractiseAPI(total, 360, targetType.value);
if (result) practiseId.value = result.id;
});
onBeforeUnmount(() => {
@@ -114,26 +96,18 @@ onBeforeUnmount(() => {
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
uni.$off("share-image", onClickShare);
audioManager.stopAll();
endPractiseAPI();
});
</script>
<template>
<Container
:bgType="1"
title="日常耐力挑战"
:showBottom="!start && !scores.length"
>
<Container :bgType="1" title="日常耐力挑战">
<view>
<TestDistance v-if="!start && !practiseResult.id" />
<block v-else>
<TestDistance v-if="!practiseId" />
<block v-if="practiseId">
<ShootProgress
:tips="`请连续射${total}支箭`"
:start="start"
:total="360"
:onStop="onOver"
:tips="`请连续射${total}支箭`"
:total="120"
/>
<view class="user-row">
<Avatar :src="user.avatar" :size="35" />
@@ -141,7 +115,7 @@ onBeforeUnmount(() => {
<text>完成过半胜利</text>
<text>在望💪</text>
</BubbleTip>
<BowPower />
<BowPower :power="power" />
</view>
<BowTarget
:currentRound="scores.length"
@@ -150,32 +124,28 @@ onBeforeUnmount(() => {
/>
<ScorePanel
v-if="start"
:arrows="scores"
:scores="scores.map((s) => s.ring)"
:total="total"
:rowCount="total / 4"
:margin="1.5"
:font-size="20"
/>
<ScoreResult
v-if="practiseResult.details"
v-if="practiseResult.arrows"
:total="total"
:rowCount="9"
:onClose="onComplete"
:result="practiseResult"
:tipSrc="`../static/${
practiseResult.details.filter(
(arrow) => arrow.x !== -30 && arrow.y !== -30
).length < total
? '2un'
: ''
practiseResult.arrows.length < total ? '2un' : ''
}finish-tip.png`"
/>
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
<canvas class="share-canvas" canvas-id="shareCanvas"></canvas>
</block>
</view>
<template #bottom>
<SButton :onClick="onReady">准备好了直接开始</SButton>
</template>
<view :style="{ marginBottom: '20px' }">
<SButton v-if="!start" :onClick="onReady">准备好了直接开始</SButton>
</view>
</Container>
</template>

View File

@@ -4,29 +4,23 @@ import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Guide from "@/components/Guide.vue";
import Avatar from "@/components/Avatar.vue";
import TargetPicker from "@/components/TargetPicker.vue";
import { getPractiseDataAPI } from "@/apis";
import { canEenter } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, device, online } = storeToRefs(store);
const { user } = storeToRefs(store);
const data = ref({});
const showTargetPicker = ref(false);
const pendingPractiseType = ref("");
const goPractise = async (type) => {
if (!canEenter(user.value, device.value, online.value)) return;
pendingPractiseType.value = type;
showTargetPicker.value = true;
const toPractiseOne = () => {
uni.navigateTo({
url: "/pages/practise-one",
});
};
const handleTargetConfirm = (target) => {
showTargetPicker.value = false;
const type = pendingPractiseType.value;
const toPractiseTwo = () => {
uni.navigateTo({
url: `/pages/practise-${type}?target=${target}`,
url: "/pages/practise-two",
});
};
@@ -38,12 +32,14 @@ onShow(async () => {
<template>
<Container title="个人练习">
<view :style="{ width: '100%', height: '100%' }">
<view :style="{ width: '100%' }">
<Guide>
<view class="guide-tips">
<text>师傅领进门修行靠自身赶紧练起来吧</text>
<text>坚持练习就能你快速升级早日加入全国排位赛</text>
</view>
<text :style="{ color: '#fed847' }"
>师傅领进门修行靠自身赶紧练起来吧</text
>
<text :style="{ fontSize: '12px' }"
>坚持练习就能你快速升级早日加入全国排位赛</text
>
</Guide>
<view class="practise-data">
<view>
@@ -82,14 +78,14 @@ onShow(async () => {
</view>
</view>
</view>
<view class="practise-btn" @click="() => goPractise('one')">
<view class="practise-btn" @click="toPractiseOne">
<image
src="https://static.shelingxingqiu.com/attachment/2025-07-12/db9x668e2vdtqh0otq.png"
class="practise1"
mode="widthFix"
/>
</view>
<view class="practise-btn" @click="() => goPractise('two')">
<view class="practise-btn" @click="toPractiseTwo">
<image
src="https://static.shelingxingqiu.com/attachment/2025-07-12/db9x668eehkvyicc08.png"
class="practise2"
@@ -97,11 +93,6 @@ onShow(async () => {
/>
</view>
</view>
<TargetPicker
:show="showTargetPicker"
:onClose="() => (showTargetPicker = false)"
:onConfirm="handleTargetConfirm"
/>
</Container>
</template>

View File

@@ -13,7 +13,7 @@ import Container from "@/components/Container.vue";
<view class="section">
<view class="title">段位体系概述</view>
<view class="text">
我们的段位体系分为多个等级从低到高依次为倔强青铜秩序白银黄金王者永恒钻石最强王者非凡王者无双王者绝世王者至圣王者荣耀王者和传奇王者每个大段位下又分为若干小段位玩家需要通过积累积分来提升段位
我们的段位体系分为多个等级从低到高依次为铜牌青铜移动白银荣耀黄金永恒钻石璀璨王者非凡王者无双王者至尊王者荣耀王者和传奇王者每个大段位下又分为若干小段位玩家需要通过积累积分来提升段位
</view>
</view>
@@ -55,77 +55,77 @@ import Container from "@/components/Container.vue";
<view class="section">
<view class="title">表格</view>
<view class="rank-table">
<view class="table-row">
<text>大段位</text>
<text>小段位</text>
<text>积分100积分=1</text>
<view class="table-header">
<view>大段位</view>
<view>小段位</view>
<view>积分100积分=1</view>
</view>
<view class="table-row">
<text>倔强青铜</text>
<view>铜牌青铜</view>
<view>
<text>青铜1*</text>
<text>青铜2*</text>
<text>青铜3*</text>
<view>青铜1*</view>
<view>青铜2*</view>
<view>青铜3*</view>
</view>
<text>每个小段位需要 3星才能晋升到下一个段位共9</text>
<view>每个小段位需要累计3星才能晋升到下一个段位共9星</view>
</view>
<view class="table-row">
<text>秩序白银</text>
<view>移动白银</view>
<view>
<text>白铜1*</text>
<text>白铜2*</text>
<text>白铜3*</text>
<view>白铜1*</view>
<view>白铜2*</view>
<view>白铜3*</view>
</view>
<text>每个小段位需要 3星才能晋升到下一个段位共9</text>
<view>每个小段位需要累计3星才能晋升到下一个段位共9星</view>
</view>
<view class="table-row">
<text>黄金王者</text>
<view>黄金王者</view>
<view>
<text>黄金1*</text>
<text>黄金2*</text>
<text>黄金3*</text>
<text>黄金4*</text>
<view>黄金1*</view>
<view>黄金2*</view>
<view>黄金3*</view>
<view>黄金4*</view>
</view>
<text>每个小段位需要满4颗星才能晋升到下一个段位共16颗</text>
<view>每个小段位需要累计4星才能晋升到下一个段位共15</view>
</view>
<view class="table-row">
<text>永恒钻石</text>
<view>永恒钻石</view>
<view>
<text>钻石1*</text>
<text>钻石2*</text>
<text>钻石3*</text>
<text>钻石4*</text>
<text>钻石5*</text>
<view>钻石1*</view>
<view>钻石2*</view>
<view>钻石3*</view>
<view>钻石4*</view>
<view>钻石5*</view>
</view>
<text>每个小段位需要满5颗星才能晋升到下一个段位共25</text>
<view>每个小段位需要累计5星才能晋升到下一个段位共25星</view>
</view>
<view class="table-row2">
<text>最强王者</text>
<text>0-9</text>
<view>最强王者</view>
<view>0-9</view>
</view>
<view class="table-row2">
<text>非凡王者</text>
<text>10-19</text>
<view>非凡王者</view>
<view>0-9</view>
</view>
<view class="table-row2">
<text>无双王者</text>
<text>20-29</text>
<view>无双王者</view>
<view>10-19</view>
</view>
<view class="table-row2">
<text>绝世王者</text>
<text>30-39</text>
<view>至尊王者</view>
<view>20-29</view>
</view>
<view class="table-row2">
<text>至圣王者</text>
<text>40-49</text>
<view>荣耀王者</view>
<view>30-39</view>
</view>
<view class="table-row2">
<text>荣耀王者</text>
<text>50-99</text>
<view>璀璨王者</view>
<view>40-49</view>
</view>
<view class="table-row2">
<text>传奇王者</text>
<text>100+</text>
<view>传奇王者</view>
<view>100+</view>
</view>
</view>
</view>
@@ -134,7 +134,7 @@ import Container from "@/components/Container.vue";
</Container>
</template>
<style scoped lang="scss">
<style scoped>
.container {
width: 100%;
height: 100%;
@@ -195,47 +195,71 @@ import Container from "@/components/Container.vue";
}
.rank-table {
border: 1px solid #e4e4e4;
border-radius: 4px;
font-size: 14px;
color: #000;
width: calc(100vw - 20px);
}
.rank-table > view {
.table-header {
display: flex;
border-bottom: 1px solid #e4e4e4;
}
.rank-table > view > text:last-child {
margin-left: -1rpx;
.table-header > view {
padding: 5px 10px;
width: 20%;
}
.rank-table text {
padding: 10rpx 20rpx;
border: $uni-border;
box-sizing: border-box;
display: inline-block;
margin-top: -1rpx;
.table-header > view:last-child {
padding: 5px 10px;
width: 60%;
}
.table-row text {
width: 25%;
.table-header > view:nth-child(2) {
border-left: 1px solid #e4e4e4;
border-right: 1px solid #e4e4e4;
}
.table-row > view {
.table-row {
display: flex;
flex-direction: column;
width: 25%;
min-height: 44px;
border-bottom: 1px solid #e4e4e4;
}
.table-row > view > text {
width: 100%;
.table-row > view:first-child,
.table-row > view:last-child,
.table-row > view:nth-child(2) > view {
padding: 5px 10px;
}
.table-row > text:nth-child(3) {
width: 50%;
.table-row > view:nth-child(2) {
border-left: 1px solid #e4e4e4;
border-right: 1px solid #e4e4e4;
}
.table-row2 > text {
width: 50%;
.table-row > view:nth-child(2) > view {
border-bottom: 1px solid #e4e4e4;
}
.table-row > view:nth-child(2) > view:last-child {
border-bottom: none;
}
.table-row > view:first-child {
width: 20%;
}
.table-row > view:nth-child(2) {
width: 26.5%;
}
.table-row > view:last-child {
width: 60%;
display: flex;
justify-content: center;
align-items: center;
line-height: 2;
}
.table-row2 {
display: flex;
border-bottom: 1px solid #e4e4e4;
}
.table-row2 > view {
padding: 5px 10px;
}
.table-row2 > view:first-child {
border-right: 1px solid #e4e4e4;
width: 38.8%;
}
</style>

View File

@@ -1,19 +1,21 @@
<script setup>
import { ref, onMounted } from "vue";
import Avatar from "@/components/Avatar.vue";
import { capsuleHeight } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, rankData } = storeToRefs(store);
const { getLvlName } = store;
const capsuleHeight = ref(0);
const selectedIndex = ref(0);
const currentList = ref([]);
const myData = ref({});
const addBg = ref(false);
const addBg = ref("");
onMounted(async () => {
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
capsuleHeight.value = menuBtnInfo.top - 9;
handleSelect(0);
});
@@ -64,14 +66,19 @@ const subTitles = ["排位赛积分", "MVP次数", "十环次数"];
}"
>
<image
:style="{ opacity: addBg ? 1 : 0 }"
v-if="addBg"
class="bg-image"
src="../static/app-bg.png"
mode="widthFix"
/>
<navigator open-type="navigateBack">
<image class="header-back" src="../static/back.png" mode="widthFix" />
</navigator>
<text :style="{ opacity: addBg ? 1 : 0 }">本赛季排行榜</text>
<text
:style="{ opacity: addBg ? 1 : 0, color: '#fff', fontWeight: 'bold' }"
>
本赛季排行榜
</text>
</view>
<scroll-view
scroll-y
@@ -151,7 +158,9 @@ const subTitles = ["排位赛积分", "MVP次数", "十环次数"];
<Avatar :src="item.avatar" />
<view class="rank-item-content">
<text class="truncate">{{ item.name }}</text>
<text>{{ getLvlName(item.rankLvl) }}{{ item.TotalGames }}</text>
<text
>{{ getLvlName(item.totalScore) }}{{ item.TotalGames }}</text
>
</view>
<text class="rank-item-integral" v-if="selectedIndex === 0">
<text
@@ -224,28 +233,13 @@ const subTitles = ["排位赛积分", "MVP次数", "十环次数"];
align-items: center;
position: fixed;
top: 0;
transition: all 0.3s ease;
z-index: 10;
overflow: hidden;
}
.header-back {
width: 22px;
height: 22px;
margin: 0px 15px;
margin-top: 5px;
position: relative;
}
.header > image:first-child {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
transition: all 0.5s ease;
}
.header > text {
color: #fff;
font-weight: bold;
transition: all 0.5s ease;
.header text {
transition: all 0.3s ease;
line-height: 50px;
position: relative;
}
.rank-tabs {
@@ -392,4 +386,18 @@ const subTitles = ["排位赛积分", "MVP次数", "十环次数"];
color: #fff9;
font-size: 14px;
}
.header-back {
width: 22px;
height: 22px;
margin: 0px 15px;
margin-top: 5px;
position: relative;
}
.bg-image {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
}
</style>

View File

@@ -4,12 +4,11 @@ import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Avatar from "@/components/Avatar.vue";
import { topThreeColors } from "@/constants";
import { getHomeData } from "@/apis";
import { canEenter } from "@/util";
import { isGamingAPI, getHomeData } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, device, online, game } = storeToRefs(store);
const { user, device } = storeToRefs(store);
const { getLvlName } = store;
const defaultSeasonData = {
@@ -42,12 +41,23 @@ const handleSelect = (index) => {
};
const toMatchPage = async (gameType, teamSize) => {
if (!canEenter(user.value, device.value, online.value)) return;
if (game.value.inBattle) {
if (!device.value.deviceId) {
return uni.showToast({
title: "请先绑定设备",
icon: "none",
});
}
if (!user.value.trio) {
return uni.showToast({
title: "请先完成新手试炼",
icon: "none",
});
}
const isGaming = await isGamingAPI();
if (isGaming) {
uni.$showHint(1);
return;
}
await uni.$checkAudio();
uni.navigateTo({
url: `/pages/match-page?gameType=${gameType}&teamSize=${teamSize}`,
});
@@ -63,14 +73,14 @@ const toRankListPage = () => {
});
};
const onChangeSeason = async (seasonId, name) => {
showSeasonList.value = false;
if (name !== seasonName.value) {
handleSelect(selectedIndex.value);
const result = await getHomeData(seasonId);
rankData.value = result;
seasonName.value = name;
handleSelect(selectedIndex.value);
updateData();
}
showSeasonList.value = false;
};
const updateData = () => {
const { userGameStats, seasonList } = rankData.value;
@@ -110,7 +120,7 @@ onShow(async () => {
const result = await getHomeData();
rankData.value = result;
handleSelect(selectedIndex.value);
seasonData.value = result.seasonList || [];
seasonData.value = result.seasonList;
if (seasonData.value[0]) {
seasonName.value = seasonData.value[0].seasonName;
}
@@ -165,7 +175,7 @@ onShow(async () => {
<view>
<text>段位</text>
<text :style="{ color: '#83CDFF' }">{{
getLvlName(rankData.user.rankLvl) || "-"
getLvlName(rankData.user.scores) || "-"
}}</text>
</view>
<view>
@@ -197,22 +207,22 @@ onShow(async () => {
<image
src="../static/battle2v2.png"
mode="widthFix"
@click.stop="() => toMatchPage(2, 4)"
@click.stop="() => toMatchPage(1, 4)"
/>
<image
src="../static/battle3v3.png"
mode="widthFix"
@click.stop="() => toMatchPage(3, 6)"
@click.stop="() => toMatchPage(1, 6)"
/>
<image
src="../static/battle5.png"
mode="widthFix"
@click.stop="() => toMatchPage(4, 5)"
@click.stop="() => toMatchPage(2, 5)"
/>
<image
src="../static/battle10.png"
mode="widthFix"
@click.stop="() => toMatchPage(5, 10)"
@click.stop="() => toMatchPage(2, 10)"
/>
</view>
<view class="data-progress">
@@ -344,7 +354,7 @@ onShow(async () => {
<view>
<text class="truncate">{{ item.name }}</text>
<text>
{{ getLvlName(item.rankLvl) }}{{ item.TotalGames }}场
{{ getLvlName(item.totalScore) }}{{ item.TotalGames }}场
</text>
</view>
<text v-if="selectedIndex === 0">
@@ -501,11 +511,10 @@ onShow(async () => {
}
.ranking-data > view:first-of-type > view {
width: 25%;
padding: 7px 10px;
text-align: center;
border-radius: 20px;
font-size: 30rpx;
word-break: keep-all;
line-height: 70rpx;
}
.rank-item {
width: calc(100% - 30px);
@@ -595,19 +604,13 @@ onShow(async () => {
.season-list > view {
display: flex;
align-items: center;
padding: 10px 20px;
word-break: keep-all;
padding: 20rpx 0;
}
.season-list > view > text {
width: 140rpx;
text-align: right;
}
.season-list > view > image {
width: 24rpx;
height: 24rpx;
min-width: 24rpx;
min-height: 24rpx;
margin-left: 20rpx;
width: 12px;
height: 12px;
margin-left: 10px;
}
.my-rank-score {
position: absolute !important;

View File

@@ -0,0 +1,52 @@
<script setup>
import { ref } from "vue";
import InputRow from "@/components/InputRow.vue";
import SButton from "@/components/SButton.vue";
const toSignInPage = () => {
uni.navigateBack();
};
</script>
<template>
<view class="container">
<text class="title">Reset Password</text>
<text class="sub-title">Enter email address to reset password</text>
<InputRow placeholder="email" width="80vw" />
<InputRow
placeholder="verification code"
type="number"
width="80vw"
btnType="code"
/>
<InputRow type="password" placeholder="password" width="80vw" />
<InputRow type="password" placeholder="confirm password" width="80vw" />
<view :style="{ height: '20rpx' }"></view>
<SButton width="80vw">Submit</SButton>
</view>
</template>
<style scoped>
.container {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding-top: 240rpx;
background: #f5f5f5;
}
.title {
font-weight: 600;
font-size: 48rpx;
color: #333333;
width: 80vw;
margin-bottom: 10rpx;
}
.sub-title {
font-size: 24rpx;
color: #666666;
width: 80vw;
margin-bottom: 20rpx;
}
</style>

145
src/pages/sign-in.vue Normal file
View File

@@ -0,0 +1,145 @@
<script setup>
import { ref } from "vue";
import InputRow from "@/components/InputRow.vue";
import SButton from "@/components/SButton.vue";
const checked = ref(false);
const toSignUpPage = () => {
uni.navigateTo({
url: "/pages/sign-up",
});
};
const toResetPasswordPage = () => {
uni.navigateTo({
url: "/pages/reset-password",
});
};
</script>
<template>
<view class="container">
<image class="app-logo" src="../static/logo.png" mode="widthFix" />
<text class="app-name">ARCX</text>
<InputRow type="text" placeholder="email" width="80vw" />
<InputRow type="password" placeholder="password" width="80vw" />
<view class="btn-row">
<button hover-class="none" @click="toResetPasswordPage">
Forgot Password?
</button>
</view>
<SButton width="80vw">login</SButton>
<button
hover-class="none"
@click.stop="checked = !checked"
class="agreement"
>
<image :src="`../static/${checked ? 'checked' : 'unchecked'}.png`" />
<text>i read and accept</text>
<button hover-class="none" @click.stop="">user agreement</button>
<text>and</text>
<button hover-class="none" @click.stop="">privacy policy</button>
</button>
<view class="thrid-signin">
<button hover-class="none">
<image src="../static/google-icon.png" mode="widthFix" />
<text>login with google</text>
</button>
<button hover-class="none">
<image src="../static/apple-icon.png" mode="widthFix" />
<text>login with apple</text>
</button>
</view>
<view class="to-sign-up">
<text>don't have an account? </text>
<button hover-class="none" @click.stop="toSignUpPage">sign up ></button>
</view>
</view>
</template>
<style scoped>
.container {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f5f5f5;
}
.app-logo {
width: 176rpx;
height: 176rpx;
margin-top: 40rpx;
}
.app-name {
font-weight: 600;
font-size: 40rpx;
color: #333333;
margin: 20rpx 0;
}
.btn-row {
width: 80vw;
display: flex;
justify-content: flex-end;
}
.btn-row > button {
font-size: 24rpx;
color: #287fff;
margin-bottom: 25rpx;
line-height: 34rpx;
}
.agreement {
display: flex;
justify-content: flex-start;
align-items: center;
font-size: 24rpx;
margin-top: 24rpx;
margin-bottom: 50rpx;
color: #999999;
}
.agreement > image:first-child {
width: 32rpx;
height: 32rpx;
margin-right: 10rpx;
}
.agreement > button {
color: #333;
font-size: 24rpx;
margin: 0 10rpx;
}
.thrid-signin {
width: 80vw;
display: flex;
flex-direction: column;
margin: 60rpx 0;
}
.thrid-signin > button {
width: 100%;
height: 88rpx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 45rpx;
background-color: #fff;
font-size: 30rpx;
color: #333333;
margin: 20rpx 0;
}
.thrid-signin > button > image {
width: 40rpx;
margin-right: 20rpx;
}
.to-sign-up {
font-size: 24rpx;
color: #666666;
display: flex;
justify-content: center;
align-items: center;
}
.to-sign-up > button {
font-size: 24rpx;
color: #287fff;
margin-left: 20rpx;
}
</style>

91
src/pages/sign-up.vue Normal file
View File

@@ -0,0 +1,91 @@
<script setup>
import { ref } from "vue";
import InputRow from "@/components/InputRow.vue";
import SButton from "@/components/SButton.vue";
const toSignInPage = () => {
uni.navigateBack();
};
</script>
<template>
<view class="container">
<text class="title">Sign up</text>
<text class="sub-title">Create an Arcx account</text>
<InputRow placeholder="name" width="80vw" />
<InputRow placeholder="email" width="80vw" />
<InputRow placeholder="verification code" type="number" width="80vw" btnType="code" />
<InputRow type="password" placeholder="password" width="80vw" />
<InputRow type="password" placeholder="confirm password" width="80vw" />
<view :style="{ height: '20rpx' }"></view>
<SButton width="80vw">login</SButton>
<view class="agreement">
<text>By clicking Sign Up, you agree to our</text>
<button hover-class="none" @click.stop="">user agreement</button>
<text>and</text>
<button hover-class="none" @click.stop="">privacy policy</button>
</view>
<view class="to-sign-up">
<text>have an account? </text>
<button hover-class="none" @click.stop="toSignInPage">sign in ></button>
</view>
</view>
</template>
<style scoped>
.container {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f5f5f5;
}
.title {
font-weight: 600;
font-size: 48rpx;
color: #333333;
width: 80vw;
margin-bottom: 10rpx;
}
.sub-title {
font-size: 24rpx;
color: #666666;
width: 80vw;
margin-bottom: 20rpx;
}
.agreement {
width: 80vw;
display: flex;
justify-content: center;
align-items: center;
font-size: 24rpx;
margin-top: 24rpx;
margin-bottom: 50rpx;
color: #999999;
flex-wrap: wrap;
}
.agreement > image:first-child {
width: 32rpx;
height: 32rpx;
margin-right: 10rpx;
}
.agreement > button {
color: #333;
font-size: 24rpx;
margin: 0 10rpx;
}
.to-sign-up {
font-size: 24rpx;
color: #999;
display: flex;
justify-content: center;
align-items: center;
margin-top: 100rpx;
}
.to-sign-up > button {
font-size: 24rpx;
color: #287fff;
margin-left: 20rpx;
}
</style>

View File

@@ -4,6 +4,8 @@ import {onLoad, onShow, onHide} from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import BattleHeader from "@/components/BattleHeader.vue";
import BowTarget from "@/components/BowTarget.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import PlayersRow from "@/components/PlayersRow.vue";
import BattleFooter from "@/components/BattleFooter.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import SButton from "@/components/SButton.vue";
@@ -11,21 +13,23 @@ import RoundEndTip from "@/components/RoundEndTip.vue";
import TestDistance from "@/components/TestDistance.vue";
import TeamAvatars from "@/components/TeamAvatars.vue";
import ShootProgress2 from "@/components/ShootProgress2.vue";
import {laserCloseAPI, getBattleAPI} from "@/apis";
import {MESSAGETYPESV2} from "@/constants";
import { getCurrentGameAPI } from "@/apis";
import { isGameEnded } from "@/util";
import { MESSAGETYPES, roundsName } from "@/constants";
import audioManager from "@/audioManager";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const start = ref(null);
const start = ref(false);
const tips = ref("");
const battleId = ref("");
const currentRound = ref(0);
const currentRound = ref(1);
const goldenRound = ref(0);
const currentRedPoint = ref(0);
const currentBluePoint = ref(0);
const totalRounds = ref(0);
const power = ref(0);
const scores = ref([]);
const blueScores = ref([]);
const redTeam = ref([]);
@@ -36,195 +40,286 @@ const redPoints = ref(0);
const bluePoints = ref(0);
const showRoundTip = ref(false);
const isFinalShoot = ref(false);
const matchStatus = ref(undefined);
const updateRemainSecond = ref(0);
const isEnded = ref(false);
const recoverData = (battleInfo, {force = false, arrowOnly = false} = {}) => {
try {
battleId.value = battleInfo.matchId;
// 优先使用接口返回的队伍数据,如果没有则尝试从缓存读取(应对匹配刚完成接口未就绪的情况)
const t1 = battleInfo.teams?.[1] || {};
const t2 = battleInfo.teams?.[2] || {};
if (t1.players) blueTeam.value = [...t1.players];
else {
const cached = uni.getStorageSync("blue-team");
if (cached && cached.length) blueTeam.value = cached;
}
if (t2.players) redTeam.value = [...t2.players];
else {
const cached = uni.getStorageSync("red-team");
if (cached && cached.length) redTeam.value = cached;
}
start.value = battleInfo.status !== 0;
if (battleInfo.status === 0) {
const readyRemain = (Date.now() - (battleInfo.createTime || Date.now())) / 1000;
if (readyRemain > 0 && readyRemain < 15) {
setTimeout(() => {
uni.$emit("update-timer", 15 - readyRemain - 0.2);
}, 200);
}
return;
}
if (!arrowOnly) {
currentShooterId.value = battleInfo.current.playerId;
const redPlayer = battleInfo.teams[2].players.find(
(item) => item.id === battleInfo.current.playerId
);
let nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭";
if (force) nextTips += "重回";
if (
battleInfo.current.playerId === user.value.id &&
redTeam.value.length > 1
) {
nextTips += "你";
}
tips.value = nextTips;
uni.$emit("update-tips", nextTips);
uni.$emit("update-remain", {reset: true, value: 15, team: redTeam?'red':'blue'});
if (force) {
const remain = (Date.now() - battleInfo.current.startTime) / 1000;
console.log(`当前轮已进行${remain}`);
if (remain > 0 && remain < 15) {
updateRemainSecond.value = 15 - remain - 0.2
}
} else {
updateRemainSecond.value = battleInfo.readyTime
}
} else {
currentRound.value = battleInfo.current.round || 1;
const latestRound = battleInfo.rounds[currentRound.value - 1];
if (latestRound) {
blueScores.value = latestRound.shoots[1];
scores.value = latestRound.shoots[2];
}
}
roundResults.value = battleInfo.rounds || [];
isFinalShoot.value = battleInfo.current.goldRound;
bluePoints.value = battleInfo.teams[1].score;
redPoints.value = battleInfo.teams[2].score;
} catch (err) {
console.log(err);
}
const onBack = () => {
uni.$showHint(2);
};
function onAudioEnded(s) {
if (s.indexOf('请红方射箭') >= 0 || s.indexOf('请蓝方射箭') >= 0) {
let team = s.indexOf('请红方射箭') >= 0 ? 'red' : 'blue';
uni.$emit("update-remain", {stop: false, value: updateRemainSecond.value, team: team});
function recoverData(battleInfo) {
uni.removeStorageSync("last-awake-time");
battleId.value = battleInfo.id;
redTeam.value = battleInfo.redTeam;
blueTeam.value = battleInfo.blueTeam;
if (battleInfo.status === 0) {
const readyRemain = Date.now() / 1000 - battleInfo.startTime;
console.log(`当前局已进行${readyRemain}`);
if (readyRemain > 0) {
setTimeout(() => {
uni.$emit("update-timer", 15 - readyRemain);
}, 200);
}
if (s.indexOf("比赛结束") >= 0) {
onBattleEnd()
} else {
start.value = true;
bluePoints.value = 0;
redPoints.value = 0;
currentRound.value = battleInfo.currentRound;
totalRounds.value = battleInfo.maxRound;
roundResults.value = [...battleInfo.roundResults];
battleInfo.roundResults.forEach((round) => {
const blueTotal = round.blueArrows.reduce(
(last, next) => last + next.ring,
0
);
const redTotal = round.redArrows.reduce(
(last, next) => last + next.ring,
0
);
if (blueTotal === redTotal) {
bluePoints.value += 1;
redPoints.value += 1;
} else if (blueTotal > redTotal) {
bluePoints.value += 2;
} else {
redPoints.value += 2;
}
}
function onBattleEnd() {
if (matchStatus.value === 2) {
uni.redirectTo({
url: `/pages/battle-result?battleId=${battleId.value}`,
});
const hasCurrentRoundData =
battleInfo.redTeam.some(
(item) => !!item.shotHistory[battleInfo.currentRound]
) ||
battleInfo.blueTeam.some(
(item) => !!item.shotHistory[battleInfo.currentRound]
);
if (
battleInfo.currentRound > battleInfo.roundResults.length &&
hasCurrentRoundData
) {
const blueArrows = [];
const redArrows = [];
battleInfo.redTeam.forEach((item) =>
item.shotHistory[battleInfo.currentRound]
.filter((item) => !!item.playerId)
.forEach((item) => redArrows.push(item))
);
battleInfo.blueTeam.forEach((item) =>
item.shotHistory[battleInfo.currentRound]
.filter((item) => !!item.playerId)
.forEach((item) => blueArrows.push(item))
);
roundResults.value.push({
redArrows,
blueArrows,
});
}
if (battleInfo.goldenRound) {
const { ShotCount, RedRecords, BlueRecords } = battleInfo.goldenRound;
currentRound.value += ShotCount;
goldenRound.value += ShotCount;
isFinalShoot.value = true;
for (let i = 0; i < ShotCount; i++) {
const roundData = {
redArrows:
RedRecords && RedRecords[i] ? RedRecords[i].Arrows || [] : [],
blueArrows:
BlueRecords && BlueRecords[i] ? BlueRecords[i].Arrows || [] : [],
gold: true,
};
roundResults.value.push(roundData);
}
function onNewRound(msg) {
showRoundTip.value = true;
isFinalShoot.value = msg.current.goldRound;
const latestRound = msg.rounds[currentRound.value - 1];
if (latestRound) {
if (isFinalShoot.value) {
currentBluePoint.value = msg.teams[1].score;
currentRedPoint.value = msg.teams[2].score;
} else {
currentBluePoint.value = latestRound.scores[1].score;
currentRedPoint.value = latestRound.scores[2].score;
[...battleInfo.redTeam, ...battleInfo.blueTeam].some((p) => {
if (p.id === user.value.id) {
const roundArrows = Object.values(p.shotHistory);
if (roundArrows.length) {
uni.$emit("update-shot", {
currentShot: roundArrows[roundArrows.length - 1].length,
totalShot: battleInfo.config.teamSize === 2 ? 3 : 2,
});
}
return true;
}
return false;
});
}
const lastIndex = roundResults.value.length - 1;
if (roundResults.value[lastIndex]) {
const redArrows = roundResults.value[lastIndex].redArrows;
scores.value = [...redArrows].filter((item) => !!item.playerId);
const blueArrows = roundResults.value[lastIndex].blueArrows;
blueScores.value = [...blueArrows].filter((item) => !!item.playerId);
}
// if (battleInfo.status !== 11) return;
if (battleInfo.firePlayerIndex) {
currentShooterId.value = battleInfo.firePlayerIndex;
const redPlayer = redTeam.value.find(
(item) => item.id === currentShooterId.value
);
tips.value = redPlayer ? "请红队射箭" : "请蓝队射箭";
uni.$emit("update-tips", tips.value);
}
if (battleInfo.fireTime > 0) {
const remain = Date.now() / 1000 - battleInfo.fireTime;
console.log(`当前箭已过${remain}`);
if (remain > 0 && remain <= 15) {
// 等渲染好再通知
setTimeout(() => {
uni.$emit("update-ramain", 15 - remain);
}, 300);
}
}
}
}
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.BattleStart) {
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.AllReady) {
start.value = true;
} else if (msg.type === MESSAGETYPESV2.ToSomeoneShoot) {
recoverData(msg);
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
uni.$emit("update-remain", {stop: true})
showRoundTip.value = false;
recoverData(msg, {arrowOnly: true});
} else if (msg.type === MESSAGETYPESV2.NewRound) {
setTimeout(() => {
onNewRound(msg)
}, 800)
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
matchStatus.value = msg.status;
if (msg.status === 4) {
totalRounds.value = msg.groupUserStatus.config.maxRounds;
}
if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) {
if (currentShooterId.value !== msg.userId) {
currentShooterId.value = msg.userId;
const redPlayer = redTeam.value.find(
(item) => item.id === currentShooterId.value
);
if (msg.userId === user.value.id) audioManager.play("轮到你了");
const nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭";
if (nextTips !== tips.value) {
tips.value = nextTips;
uni.$emit("update-tips", tips.value);
} else {
uni.$emit("update-ramain", 15);
}
}
}
if (msg.constructor === MESSAGETYPES.ShootResult) {
if (currentShooterId.value !== msg.userId) return;
const isRed = redTeam.value.find((item) => item.id === msg.userId);
if (isRed) scores.value.push({ ...msg.target });
else blueScores.value.push({ ...msg.target });
// 下标从0开始的要减1
if (!roundResults.value[currentRound.value - 1]) {
roundResults.value.push({
redArrows: [],
blueArrows: [],
gold: goldenRound.value > 0,
});
}
roundResults.value[currentRound.value - 1][
isRed ? "redArrows" : "blueArrows"
].push({ ...msg.target });
}
if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
const result = msg.preRoundResult;
scores.value = [];
blueScores.value = [];
currentShooterId.value = 0;
currentBluePoint.value = result.blueScore;
currentRedPoint.value = result.redScore;
bluePoints.value += result.blueScore;
redPoints.value += result.redScore;
currentRound.value = result.currentRound + 1;
if (!result.goldenRound) {
showRoundTip.value = true;
}
}
if (msg.constructor === MESSAGETYPES.FinalShoot) {
currentShooterId.value = 0;
currentRound.value = msg.groupUserStatus.currentRound + 1;
goldenRound.value += 1;
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
currentBluePoint.value = bluePoints.value;
currentRedPoint.value = redPoints.value;
if (!isFinalShoot.value) {
isFinalShoot.value = true;
showRoundTip.value = true;
tips.value = "准备开始决金箭";
}
}
if (msg.constructor === MESSAGETYPES.MatchOver) {
if (msg.endStatus.noSaved) {
currentRound.value += 1;
currentBluePoint.value = 0;
currentRedPoint.value = 0;
showRoundTip.value = true;
isFinalShoot.value = false;
setTimeout(() => {
uni.navigateBack();
}, 2000);
}, 3000);
} else {
isEnded.value = true;
uni.setStorageSync("last-battle", msg.endStatus);
setTimeout(() => {
uni.redirectTo({
url: "/pages/battle-result",
});
}, 1000);
}
}
if (msg.constructor === MESSAGETYPES.BackToGame) {
uni.$emit("update-header-loading", false);
if (msg.battleInfo) recoverData(msg.battleInfo);
}
});
}
onLoad(async (options) => {
if (options.battleId) battleId.value = options.battleId;
// uni.enableAlertBeforeUnload({
// message: "离开比赛可能导致比赛失败,是否继续?",
// success: (res) => {
// console.log("已启用离开提示");
// },
// });
if (options.battleId) {
battleId.value = options.battleId;
redTeam.value = uni.getStorageSync("red-team");
blueTeam.value = uni.getStorageSync("blue-team");
const battleInfo = uni.getStorageSync("current-battle");
if (battleInfo) {
await nextTick(() => {
recoverData(battleInfo);
});
onMounted(async () => {
setTimeout(getCurrentGameAPI, 2000);
}
}
});
onMounted(() => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("audioEnded", onAudioEnded);
await laserCloseAPI();
});
onBeforeUnmount(() => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("audioEnded", onAudioEnded);
audioManager.stopAll();
uni.$off("socket-inbox", onReceiveMessage);
});
const refreshTimer = ref(null);
onShow(async () => {
if (battleId.value) {
const result = await getBattleAPI(battleId.value);
if (!result) return;
if (result.status === 2) {
uni.showToast({
title: "比赛已结束",
icon: "none",
});
uni.navigateBack({
delta: 2,
});
if (!isEnded.value && (await isGameEnded(battleId.value))) return;
getCurrentGameAPI();
const refreshData = () => {
const lastAwakeTime = uni.getStorageSync("last-awake-time");
if (lastAwakeTime) {
getCurrentGameAPI();
} else {
recoverData(result, {force: true});
clearInterval(refreshTimer.value);
}
};
refreshTimer.value = setInterval(refreshData, 2000);
}
});
onHide(() => {
if (refreshTimer.value) clearInterval(refreshTimer.value);
uni.setStorageSync("last-awake-time", Date.now());
});
</script>
<template>
<Container :bgType="start ? 3 : 1">
<Container :bgType="start ? 3 : 1" :onBack="onBack">
<view class="container">
<BattleHeader
v-if="start === false"
:redTeam="redTeam"
:blueTeam="blueTeam"
/>
<TestDistance v-if="start === false" :guide="false" :isBattle="true"/>
<BattleHeader v-if="!start" :redTeam="redTeam" :blueTeam="blueTeam" />
<TestDistance v-if="!start" :guide="false" :isBattle="true" />
<view v-if="start" class="players-row">
<TeamAvatars
:team="blueTeam"
@@ -242,6 +337,7 @@ onShow(async () => {
<BowTarget
v-if="start"
mode="team"
:power="start ? power : 0"
:scores="scores"
:blueScores="blueScores"
/>
@@ -251,6 +347,7 @@ onShow(async () => {
:redPoints="redPoints"
:bluePoints="bluePoints"
:goldenRound="goldenRound"
:power="power"
/>
<ScreenHint
:show="showRoundTip"
@@ -260,13 +357,13 @@ onShow(async () => {
<RoundEndTip
v-if="showRoundTip"
:isFinal="isFinalShoot"
:round="currentRound"
:round="currentRound - 1"
:bluePoint="currentBluePoint"
:redPoint="currentRedPoint"
:roundData="
roundResults[currentRound - 1] ? roundResults[currentRound - 1] : []
roundResults[currentRound - 2] ? roundResults[currentRound - 2] : []
"
:onAutoClose="()=>{ showRoundTip = false}"
:onAutoClose="() => (showRoundTip = false)"
/>
</ScreenHint>
</view>
@@ -278,12 +375,11 @@ onShow(async () => {
width: 100%;
height: 100%;
}
.players-row {
display: flex;
align-items: center;
justify-content: center;
margin-top: -2%;
margin-bottom: 6%;
margin-bottom: -7vw;
margin-top: -3vw;
}
</style>

View File

@@ -5,42 +5,105 @@ import Container from "@/components/Container.vue";
import BowTarget from "@/components/BowTarget.vue";
import Avatar from "@/components/Avatar.vue";
import { roundsName } from "@/constants";
import { getBattleAPI } from "@/apis";
import { getGameAPI } from "@/apis";
const selected = ref(0);
const redScores = ref([]);
const blueScores = ref([]);
const tabs = ref([]);
const tabs = ref(["所有轮次"]);
const players = ref([]);
const data = ref({});
const loadArrows = (round) => {
round.shoots[1].forEach((arrow) => {
blueScores.value.push(arrow);
const allRoundsScore = ref({});
const data = ref({
goldenRounds: [],
});
round.shoots[2].forEach((arrow) => {
redScores.value.push(arrow);
});
};
onLoad(async (options) => {
if (!options.battleId) return;
const result = await getBattleAPI(options.battleId || "57943107462893568");
if (options.battleId) {
const result = await getGameAPI(
options.battleId || "BATTLE-1756453741433684760-512"
);
data.value = result;
data.value.teams[1].players.forEach((p, index) => {
Object.values(result.bluePlayers).forEach((p, index) => {
players.value.push(p);
players.value.push(data.value.teams[2].players[index]);
if (
Object.values(result.redPlayers) &&
Object.values(result.redPlayers)[index]
) {
players.value.push(Object.values(result.redPlayers)[index]);
}
});
Object.values(data.value.rounds).forEach((round, index) => {
if (round.ifGold) tabs.value.push(`决金箭`);
else tabs.value.push(`${roundsName[index + 1]}`);
if (result.goldenRounds) {
result.goldenRounds.forEach(() => {
tabs.value.push("决金箭");
});
selected.value = Number(options.selected || 0);
onClickTab(selected.value);
}
Object.keys(result.roundsData).forEach((key, index) => {
if (
index <
Object.keys(result.roundsData).length - result.goldenRounds.length
) {
tabs.value.push(`${roundsName[key]}`);
}
});
onClickTab(0);
}
});
const onClickTab = (index) => {
selected.value = index;
redScores.value = [];
blueScores.value = [];
loadArrows(data.value.rounds[index]);
const { bluePlayers, redPlayers, roundsData, goldenRounds } = data.value;
let maxArrowLength = 0;
if (index === 0) {
Object.keys(bluePlayers).forEach((p) => {
allRoundsScore.value[p] = [];
Object.values(roundsData).forEach((round) => {
if (!round[p]) return;
allRoundsScore.value[p].push(
round[p].reduce((last, next) => last + next.ring, 0)
);
round[p].forEach((arrow) => {
blueScores.value.push(arrow);
});
});
});
Object.keys(redPlayers).forEach((p) => {
allRoundsScore.value[p] = [];
Object.values(roundsData).forEach((round) => {
if (!round[p]) return;
allRoundsScore.value[p].push(
round[p].reduce((last, next) => last + next.ring, 0)
);
round[p].forEach((arrow) => {
redScores.value.push(arrow);
});
});
});
} else if (index <= goldenRounds.length) {
const dataIndex =
Object.keys(roundsData).length - goldenRounds.length + index;
Object.keys(bluePlayers).forEach((p) => {
if (!roundsData[dataIndex][p]) return;
roundsData[dataIndex][p].forEach((arrow) => {
blueScores.value.push(arrow);
});
});
Object.keys(redPlayers).forEach((p) => {
if (!roundsData[dataIndex][p]) return;
roundsData[dataIndex][p].forEach((arrow) => {
redScores.value.push(arrow);
});
});
} else {
Object.keys(bluePlayers).forEach((p) => {
roundsData[index - goldenRounds.length][p].forEach((arrow) => {
blueScores.value.push(arrow);
});
});
Object.keys(redPlayers).forEach((p) => {
roundsData[index - goldenRounds.length][p].forEach((arrow) => {
redScores.value.push(arrow);
});
});
}
};
</script>
@@ -58,7 +121,7 @@ const onClickTab = (index) => {
</view>
</view>
<view :style="{ margin: '20px 0' }">
<BowTarget :scores="redScores" :blueScores="blueScores" mode="team" />
<BowTarget :scores="redScores" :blueScores="blueScores" />
</view>
<view class="score-container">
<view
@@ -71,18 +134,45 @@ const onClickTab = (index) => {
>
<Avatar
:src="player.avatar"
:borderColor="index % 2 === 0 ? '#64BAFF' : '#FF6767'"
:borderColor="
data.bluePlayers[player.playerId] ? '#64BAFF' : '#FF6767'
"
:size="36"
/>
<view>
<view
v-for="(score, index) in data.rounds[selected].shoots[
index % 2 === 0 ? 1 : 2
]"
v-if="selected === 0"
v-for="(ring, index) in allRoundsScore[player.playerId]"
:key="index"
class="score-item"
>
{{ score.ringX ? "X" : score.ring }}
{{ ring }}
</view>
<view
v-if="
selected > 0 &&
selected >= data.goldenRounds.length &&
selected <= data.goldenRounds.length
"
v-for="(score, index) in data.roundsData[
Object.keys(data.roundsData).length -
data.goldenRounds.length +
selected
][player.playerId]"
:key="index"
class="score-item"
>
{{ score.ring }}
</view>
<view
v-if="selected > data.goldenRounds.length"
v-for="(score, index) in data.roundsData[
selected - data.goldenRounds.length
][player.playerId]"
:key="index"
class="score-item"
>
{{ score.ring }}
</view>
</view>
</view>

363
src/pages/team-match.vue Normal file
View File

@@ -0,0 +1,363 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import BattleHeader from "@/components/BattleHeader.vue";
import BowTarget from "@/components/BowTarget.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import PlayersRow from "@/components/PlayersRow.vue";
import Timer from "@/components/Timer.vue";
import BattleFooter from "@/components/BattleFooter.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import SButton from "@/components/SButton.vue";
import RoundEndTip from "@/components/RoundEndTip.vue";
import TestDistance from "@/components/TestDistance.vue";
import { getCurrentGameAPI } from "@/apis";
import { isGameEnded } from "@/util";
import { MESSAGETYPES, roundsName } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const title = ref("1V1");
const start = ref(false);
const battleId = ref("");
const currentRound = ref(1);
const currentRedPoint = ref(0);
const currentBluePoint = ref(0);
const totalRounds = ref(0);
const power = ref(0);
const scores = ref([]);
const blueScores = ref([]);
const redTeam = ref([]);
const blueTeam = ref([]);
const currentShooterId = ref(0);
const tips = ref("即将开始...");
const roundResults = ref([]);
const redPoints = ref(0);
const bluePoints = ref(0);
const showRoundTip = ref(false);
const isFinalShoot = ref(false);
const isEnded = ref(false);
function recoverData(battleInfo) {
uni.removeStorageSync("last-awake-time");
battleId.value = battleInfo.id;
redTeam.value = battleInfo.redTeam;
blueTeam.value = battleInfo.blueTeam;
if (battleInfo.status === 0) {
const readyRemain = Date.now() / 1000 - battleInfo.startTime;
console.log(`当前局已进行${readyRemain}`);
if (readyRemain > 0) {
setTimeout(() => {
uni.$emit("update-timer", 15 - readyRemain);
}, 200);
}
} else {
start.value = true;
bluePoints.value = 0;
redPoints.value = 0;
currentRound.value = battleInfo.currentRound;
totalRounds.value = battleInfo.maxRound;
roundResults.value = battleInfo.roundResults;
battleInfo.roundResults.forEach((round) => {
const blueTotal = round.blueArrows.reduce(
(last, next) => last + next.ring,
0
);
const redTotal = round.redArrows.reduce(
(last, next) => last + next.ring,
0
);
if (blueTotal === redTotal) {
bluePoints.value += 1;
redPoints.value += 1;
} else if (blueTotal > redTotal) {
bluePoints.value += 2;
} else {
redPoints.value += 2;
}
});
if (
battleInfo.redTeam[0].shotHistory[battleInfo.currentRound] ||
battleInfo.blueTeam[0].shotHistory[battleInfo.currentRound]
) {
roundResults.value.push({
redArrows: battleInfo.redTeam[0].shotHistory[
battleInfo.currentRound
].filter((item) => !!item.playerId),
blueArrows: battleInfo.blueTeam[0].shotHistory[
battleInfo.currentRound
].filter((item) => !!item.playerId),
});
} else if (battleInfo.currentRound < 5) {
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
}
if (battleInfo.goldenRound) {
const { ShotCount, RedRecords, BlueRecords } = battleInfo.goldenRound;
const roundCount = Math.max(RedRecords.length, BlueRecords.length);
currentRound.value += roundCount;
isFinalShoot.value = true;
for (let i = 0; i < roundCount; i++) {
const roundData = {
redArrows:
RedRecords && RedRecords[i] ? RedRecords[i].Arrows || [] : [],
blueArrows:
BlueRecords && BlueRecords[i] ? BlueRecords[i].Arrows || [] : [],
};
if (roundResults.value[5 + i]) {
roundResults.value[5 + i] = roundData;
} else {
roundResults.value.push(roundData);
}
}
}
const lastIndex = roundResults.value.length - 1;
if (roundResults.value[lastIndex]) {
const redArrows = roundResults.value[lastIndex].redArrows;
scores.value = [...redArrows].filter((item) => !!item.playerId);
const blueArrows = roundResults.value[lastIndex].blueArrows;
blueScores.value = [...blueArrows].filter((item) => !!item.playerId);
}
// if (battleInfo.status !== 11) return;
if (battleInfo.firePlayerIndex) {
currentShooterId.value = battleInfo.firePlayerIndex;
const teamPrefix =
redTeam.value[0].id === currentShooterId.value
? "请红队射箭 - "
: "请蓝队射箭 - ";
const roundSuffix = isFinalShoot.value
? "决金箭"
: `${roundsName[currentRound.value]}`;
tips.value = teamPrefix + roundSuffix;
}
if (battleInfo.fireTime > 0) {
const remain = Date.now() / 1000 - battleInfo.fireTime;
console.log(`当前箭已过${remain}`);
if (remain > 0 && remain <= 15) {
// 等渲染好再通知
setTimeout(() => {
uni.$emit("update-ramain", 15 - remain);
}, 300);
}
}
}
}
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.id !== battleId.value) return;
if (msg.constructor === MESSAGETYPES.AllReady) {
start.value = true;
totalRounds.value = msg.groupUserStatus.config.maxRounds;
roundResults.value = [
{
redArrows: [],
blueArrows: [],
},
];
}
if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) {
if (currentShooterId.value !== msg.userId) {
currentShooterId.value = msg.userId;
const teamPrefix =
redTeam.value[0].id === currentShooterId.value
? "请红队射箭 - "
: "请蓝队射箭 - ";
const roundSuffix = isFinalShoot.value
? "决金箭"
: `${roundsName[currentRound.value]}`;
tips.value = teamPrefix + roundSuffix;
}
}
if (msg.constructor === MESSAGETYPES.ShootResult) {
if (currentShooterId.value !== msg.userId) return;
const isRed = redTeam.value.find((item) => item.id === msg.userId);
if (isRed) scores.value.push({ ...msg.target });
else blueScores.value.push({ ...msg.target });
if (!roundResults.value[currentRound.value - 1]) {
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
}
roundResults.value[currentRound.value - 1][
isRed ? "redArrows" : "blueArrows"
].push({ ...msg.target });
}
if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
const result = msg.preRoundResult;
scores.value = [];
blueScores.value = [];
currentShooterId.value = 0;
currentBluePoint.value = result.blueScore;
currentRedPoint.value = result.redScore;
bluePoints.value += result.blueScore;
redPoints.value += result.redScore;
if (result.currentRound < 5) {
currentRound.value = result.currentRound + 1;
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
showRoundTip.value = true;
}
}
if (msg.constructor === MESSAGETYPES.FinalShoot) {
currentShooterId.value = 0;
currentRound.value += 1;
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
if (!isFinalShoot.value) {
isFinalShoot.value = true;
showRoundTip.value = true;
tips.value = "准备开始决金箭";
}
}
if (msg.constructor === MESSAGETYPES.MatchOver) {
if (msg.endStatus.noSaved) {
currentRound.value += 1;
currentBluePoint.value = 0;
currentRedPoint.value = 0;
showRoundTip.value = true;
setTimeout(() => {
uni.navigateBack();
}, 3000);
} else {
isEnded.value = true;
uni.setStorageSync("last-battle", msg.endStatus);
setTimeout(() => {
uni.redirectTo({
url: "/pages/battle-result",
});
}, 1000);
}
}
if (msg.constructor === MESSAGETYPES.BackToGame) {
uni.$emit("update-header-loading", false);
if (msg.battleInfo) recoverData(msg.battleInfo);
}
});
}
const onBack = () => {
uni.$showHint(2);
};
onLoad(async (options) => {
if (options.gameMode == 1) title.value = "好友约战 - 1V1";
if (options.gameMode == 2) title.value = "排位赛 - 1V1";
if (options.battleId) {
battleId.value = options.battleId;
redTeam.value = uni.getStorageSync("red-team");
blueTeam.value = uni.getStorageSync("blue-team");
const battleInfo = uni.getStorageSync("current-battle");
if (battleInfo) {
await nextTick(() => {
recoverData(battleInfo);
});
setTimeout(getCurrentGameAPI, 2000);
}
}
});
onMounted(() => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
});
onBeforeUnmount(() => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
});
const refreshTimer = ref(null);
onShow(async () => {
if (battleId.value) {
if (!isEnded.value && (await isGameEnded(battleId.value))) return;
getCurrentGameAPI();
const refreshData = () => {
const lastAwakeTime = uni.getStorageSync("last-awake-time");
if (lastAwakeTime) {
getCurrentGameAPI();
} else {
clearInterval(refreshTimer.value);
}
};
refreshTimer.value = setInterval(refreshData, 2000);
}
});
onHide(() => {
if (refreshTimer.value) clearInterval(refreshTimer.value);
uni.setStorageSync("last-awake-time", Date.now());
});
</script>
<template>
<Container :title="title" :bgType="1" :onBack="onBack">
<view class="container">
<BattleHeader v-if="!start" :redTeam="redTeam" :blueTeam="blueTeam" />
<TestDistance v-if="!start" :guide="false" />
<ShootProgress
:show="start"
:tips="tips"
:total="15"
:currentRound="currentRound"
:battleId="battleId"
/>
<PlayersRow
v-if="start"
:currentShooterId="currentShooterId"
:blueTeam="blueTeam"
:redTeam="redTeam"
/>
<BowTarget
v-if="start"
mode="team"
:power="start ? power : 0"
:currentRound="scores.length"
:totalRound="3"
:scores="scores"
:blueScores="blueScores"
/>
<BattleFooter
v-if="start"
:roundResults="roundResults"
:redPoints="redPoints"
:bluePoints="bluePoints"
/>
<Timer v-if="!start" />
<ScreenHint
:show="showRoundTip"
:onClose="() => (showRoundTip = false)"
:mode="isFinalShoot ? 'tall' : 'normal'"
>
<RoundEndTip
v-if="showRoundTip"
:isFinal="isFinalShoot"
:round="currentRound - 1"
:bluePoint="currentBluePoint"
:redPoint="currentRedPoint"
:roundData="
roundResults[roundResults.length - 2]
? roundResults[roundResults.length - 2]
: []
"
:onAutoClose="() => (showRoundTip = false)"
/>
</ScreenHint>
</view>
</Container>
</template>
<style scoped>
.container {
width: 100%;
height: 100%;
}
</style>

Some files were not shown because too many files have changed in this diff Show More