14 Commits

Author SHA1 Message Date
8c48216a75 feat: 好友约战选择20、40厘米靶 2026-04-20 17:43:38 +08:00
c1ff0cedad feat:选择20cm、40cm全环靶 2026-04-20 16:00:10 +08:00
56650793e8 pref: 更新音频 2026-04-20 11:13:59 +08:00
e8568ee6a8 feat: 添加射箭声音 2026-04-16 18:06:59 +08:00
kron
1181a2133a 更换图片地址 2026-04-07 16:27:49 +08:00
kron
b9bb1e6653 BUG修复 2026-02-10 18:13:11 +08:00
kron
608de34dd3 细节完善 2026-02-10 17:03:25 +08:00
kron
88f1ef5d95 细节完善 2026-02-10 14:48:07 +08:00
kron
b0bf1880e4 完善个人练习分享 2026-02-10 11:47:20 +08:00
kron
812879d252 完善我的成长 2026-02-10 11:47:09 +08:00
kron
303e1830d3 BUG修复 2026-02-10 11:32:53 +08:00
kron
61ff1af4c3 细节完善 2026-02-10 09:20:01 +08:00
kron
a3a9f7b351 细节完善 2026-02-09 18:16:48 +08:00
kron
4801833fa9 删除无用文件 2026-02-09 18:16:39 +08:00
31 changed files with 749 additions and 652 deletions

3
.vscode/settings.json vendored Normal file
View File

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

View File

@@ -1,256 +1,292 @@
<script setup> <script setup>
import { watch } from "vue"; import {
import { onShow, onHide } from "@dcloudio/uni-app"; watch
import websocket from "@/websocket"; } from "vue";
import { getDeviceBatteryAPI } from "@/apis"; import {
import useStore from "@/store"; onShow,
import { storeToRefs } from "pinia"; onHide
const store = useStore(); } from "@dcloudio/uni-app";
const { user } = storeToRefs(store); import websocket from "@/websocket";
const { updateUser, updateOnline } = store; import {
getDeviceBatteryAPI
} from "@/apis";
import useStore from "@/store";
import {
storeToRefs
} from "pinia";
import audioManager from "./audioManager";
const store = useStore();
const {
user
} = storeToRefs(store);
const {
updateUser,
updateOnline
} = store;
watch( watch(
() => user.value.id, () => user.value.id,
(newVal) => { (newVal) => {
const token = uni.getStorageSync( const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token` `${uni.getAccountInfoSync().miniProgram.envVersion}_token`
); );
if (newVal && token) { if (newVal && token) {
websocket.createWebSocket(token, (content) => { websocket.createWebSocket(token, onShootWsMsg);
uni.$emit("socket-inbox", content); }
}); if (!newVal) {
} websocket.closeWebSocket();
if (!newVal) { }
websocket.closeWebSocket(); }, {
} deep: false, // 如果 user 是一个对象或数组,建议开启
}, immediate: false, // 若想在初始化时立即执行一次回调,可开启。
{ }
deep: false, // 如果 user 是一个对象或数组,建议开启 );
immediate: false, // 若想在初始化时立即执行一次回调,可开启。
}
);
function emitUpdateUser(value) { function emitUpdateUser(value) {
updateUser(value); updateUser(value);
} }
async function emitUpdateOnline() { async function emitUpdateOnline() {
const data = await getDeviceBatteryAPI(); const data = await getDeviceBatteryAPI();
updateOnline(data.online); updateOnline(data.online);
} }
onShow(() => { function onDeviceShoot() {
uni.$on("update-user", emitUpdateUser); audioManager.play("射箭声音")
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, (content) => {
uni.$emit("socket-inbox", content);
});
}
});
onHide(() => { function onShootWsMsg(content) {
uni.$off("update-user", emitUpdateUser); if(content.type === 'shoot-trigger'){
uni.$off("update-online", emitUpdateOnline); onDeviceShoot()
websocket.closeWebSocket(); }
}); 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);
}
});
onHide(() => {
uni.$off("update-user", emitUpdateUser);
uni.$off("update-online", emitUpdateOnline);
websocket.closeWebSocket();
});
</script> </script>
<style> <style>
page { page {
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
background-color: #000; background-color: #000;
} }
button { button {
margin: 0; margin: 0;
padding: 0; padding: 0;
border: none; border: none;
background: none; background: none;
line-height: 1; line-height: 1;
outline: none; outline: none;
box-sizing: border-box; box-sizing: border-box;
} }
view::-webkit-scrollbar { view::-webkit-scrollbar {
width: 0; width: 0;
height: 0; height: 0;
color: transparent; color: transparent;
} }
button::after { button::after {
border: none; border: none;
} }
.guide-tips { .guide-tips {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-size: 28rpx; font-size: 28rpx;
} }
.guide-tips > text:first-child {
color: #fed847;
}
.guide-tips > text:nth-child(2) {
font-size: 24rpx;
}
@keyframes fadeInOut { .guide-tips>text:first-child {
0% { color: #fed847;
transform: translateY(20px); }
opacity: 0;
}
30% {
transform: translateY(0);
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.fade-in-out { .guide-tips>text:nth-child(2) {
animation: fadeInOut 1.2s ease forwards; font-size: 24rpx;
} }
@keyframes fadeOut { @keyframes fadeInOut {
from { 0% {
transform: translateY(0); transform: translateY(20px);
opacity: 1; opacity: 0;
} }
to {
transform: translateY(20px);
opacity: 0;
}
}
.fade-out { 30% {
animation: fadeOut 0.3s ease forwards; transform: translateY(0);
} opacity: 1;
}
@keyframes scaleIn { 80% {
from { opacity: 1;
transform: scale(0); }
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.scale-in { 100% {
animation: scaleIn 0.3s ease-out forwards; opacity: 0;
transform-origin: center center; }
} }
@keyframes scaleOut { .fade-in-out {
from { animation: fadeInOut 1.2s ease forwards;
transform: scale(1); }
opacity: 1;
}
to {
transform: scale(0);
opacity: 0;
}
}
.scale-out { @keyframes fadeOut {
animation: scaleOut 0.3s ease-out forwards; from {
transform-origin: center center; transform: translateY(0);
} opacity: 1;
}
@keyframes rotate { to {
from { transform: translateY(20px);
transform: rotate(0deg); opacity: 0;
} }
to { }
transform: rotate(360deg);
}
}
@keyframes pumpIn { .fade-out {
from { animation: fadeOut 0.3s ease forwards;
transform: scale(2); }
}
to {
transform: scale(1);
}
}
.pump-in { @keyframes scaleIn {
animation: pumpIn 0.3s ease-out forwards; from {
transform-origin: center center; transform: scale(0);
} opacity: 0;
}
.share-canvas { to {
width: 300px; transform: scale(1);
height: 530px; opacity: 1;
position: absolute; }
top: -1000px; }
left: 0;
}
.truncate { .scale-in {
white-space: nowrap; animation: scaleIn 0.3s ease-out forwards;
overflow: hidden; transform-origin: center center;
text-overflow: ellipsis; }
}
.modal { @keyframes scaleOut {
height: 100%; from {
display: flex; transform: scale(1);
flex-direction: column; opacity: 1;
align-items: center; }
justify-content: center;
}
.user-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
padding-top: 7px;
position: relative;
}
.half-time-tip {
width: 100%;
display: flex;
flex-direction: column;
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;
}
.see-more > text {
color: #39a8ff;
font-size: 13px;
}
.see-more > image {
width: 15px;
}
@font-face { to {
font-family: "DINCondensed"; transform: scale(0);
src: url("https://static.shelingxingqiu.com/font/DIN-Condensed-Bold-2.ttf") opacity: 0;
format("truetype"); }
font-weight: 700; }
font-style: normal;
font-display: swap; .scale-out {
} animation: scaleOut 0.3s ease-out forwards;
</style> transform-origin: center center;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pumpIn {
from {
transform: scale(2);
}
to {
transform: scale(1);
}
}
.pump-in {
animation: pumpIn 0.3s ease-out forwards;
transform-origin: center center;
}
.share-canvas {
width: 300px;
height: 530px;
position: absolute;
top: -1000px;
left: 0;
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.modal {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.user-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
padding-top: 7px;
position: relative;
}
.half-time-tip {
width: 100%;
display: flex;
flex-direction: column;
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;
}
.see-more>text {
color: #39a8ff;
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;
}
</style>

View File

@@ -6,8 +6,8 @@ try {
switch (envVersion) { switch (envVersion) {
case "develop": // 开发版 case "develop": // 开发版
// BASE_URL = "http://192.168.1.30:8000/api/shoot"; BASE_URL = "http://localhost:8000/api/shoot";
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot"; // BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
break; break;
case "trial": // 体验版 case "trial": // 体验版
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot"; BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
@@ -196,10 +196,11 @@ export const getMyDevicesAPI = () => {
return request("GET", "/user/device/getBindings"); return request("GET", "/user/device/getBindings");
}; };
export const createPractiseAPI = (arrows, time) => { export const createPractiseAPI = (arrows, time, target) => {
return request("POST", "/user/practice/create", { return request("POST", "/user/practice/create", {
shootNumber: arrows, shootNumber: arrows,
shootTime: time, shootTime: time,
targetType: target*20,
}); });
}; };
@@ -215,10 +216,11 @@ export const getPractiseAPI = async (id) => {
return request("GET", `/user/practice/get?id=${id}`); return request("GET", `/user/practice/get?id=${id}`);
}; };
export const createRoomAPI = (gameType, teamSize) => { export const createRoomAPI = (gameType, teamSize, targetType) => {
return request("POST", "/user/createroom", { return request("POST", "/user/createroom", {
gameType, gameType,
teamSize, teamSize,
targetType,
}); });
}; };
@@ -270,78 +272,6 @@ 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) => { export const simulShootAPI = (device_id, x, y) => {
const data = { const data = {
device_id, device_id,
@@ -354,39 +284,12 @@ export const simulShootAPI = (device_id, x, y) => {
}; };
export const getBattleListAPI = async (page, battleType) => { export const getBattleListAPI = async (page, battleType) => {
const data = [];
const result = await request("POST", "/user/battle/details/list", { const result = await request("POST", "/user/battle/details/list", {
page, page,
pageSize: 10,
battleType, battleType,
modeType: 0,
}); });
(result.Battles || []).forEach((item) => { return result.list;
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 = () => { export const getRankListAPI = () => {

View File

@@ -40,26 +40,26 @@ export const audioFils = {
未上靶: 未上靶:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6n45o3tsm1v4unam.mp3", "https://static.shelingxingqiu.com/attachment/2025-11-12/de6n45o3tsm1v4unam.mp3",
"1环": "1环":
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin1aq7gxjih5l.mp3", "https://static.shelingxingqiu.com/shootaudio/v3/1.mp3",
"2环": "2环":
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin64tdgx2s4at.mp3", "https://static.shelingxingqiu.com/shootaudio/v3/2.mp3",
"3环": "3环":
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinlmf87vt8z65.mp3", "https://static.shelingxingqiu.com/shootaudio/v3/3.mp3",
"4环": "4环":
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinniv97sx0q9u.mp3", "https://static.shelingxingqiu.com/shootaudio/v3/4.mp3",
"5环": "5环":
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin7j01kknpb7k.mp3", "https://static.shelingxingqiu.com/shootaudio/v3/5.mp3",
"6环": "6环":
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin4syy1015rtq.mp3", "https://static.shelingxingqiu.com/shootaudio/v3/6.mp3",
"7环": "7环":
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin3iz3dvmjdai.mp3", "https://static.shelingxingqiu.com/shootaudio/v3/7.mp3",
"8环": "8环":
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinnjd42lhpfiw.mp3", "https://static.shelingxingqiu.com/shootaudio/v3/8.mp3",
"9环": "9环":
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin69nj1xh7yfz.mp3", "https://static.shelingxingqiu.com/shootaudio/v3/9.mp3",
"10环": "10环":
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinnvsx0tt7ksa.mp3", "https://static.shelingxingqiu.com/shootaudio/v3/10.mp3",
X环: "https://static.shelingxingqiu.com/attachment/2026-02-09/dga8puwekpe2gmtbu4.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/de6ellf5pfvu3l8dhr.mp3",
向右上调整: 向右上调整:
@@ -80,6 +80,10 @@ export const audioFils = {
"https://static.shelingxingqiu.com/attachment/2025-11-13/de7kzzllq0futwynso.mp3", "https://static.shelingxingqiu.com/attachment/2025-11-13/de7kzzllq0futwynso.mp3",
练习开始: 练习开始:
"https://static.shelingxingqiu.com/attachment/2025-11-14/de88w0lmmt43nnfmoi.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"
}; };
// 版本控制日志函数 // 版本控制日志函数

View File

@@ -1,3 +1,5 @@
import { formatTimestamp } from "@/util";
const loadImage = (src) => const loadImage = (src) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
try { try {
@@ -635,7 +637,7 @@ export function renderScores(ctx, arrows = [], bgImg) {
} }
renderText( renderText(
ctx, ctx,
item.ring, item.ringX ? "X" : item.ring,
18, 18,
"#fed847", "#fed847",
29.5 + (i % 9) * 30, 29.5 + (i % 9) * 30,
@@ -657,7 +659,7 @@ export function renderScores(ctx, arrows = [], bgImg) {
} }
renderText( renderText(
ctx, ctx,
item.ring, item.ringX ? "X" : item.ring,
23, 23,
"#fed847", "#fed847",
43 + rowIndex * 42, 43 + rowIndex * 42,
@@ -737,9 +739,7 @@ export async function sharePractiseData(canvasId, type, user, data) {
let subTitle = "正式开启弓箭手之路"; let subTitle = "正式开启弓箭手之路";
if (type > 1) { if (type > 1) {
subTitle = `今日弓箭练习打卡 ${data.createdAt subTitle = `今日弓箭练习打卡 ${formatTimestamp(data.startTime)}`;
.split(" ")[0]
.replaceAll("-", ".")}`;
} }
ctx.drawImage(titleImg, (width - 160) / 2, 160, 160, 40); ctx.drawImage(titleImg, (width - 160) / 2, 160, 160, 40);
@@ -748,14 +748,14 @@ export async function sharePractiseData(canvasId, type, user, data) {
renderText(ctx, subTitle, 18, "#fff", width / 2, 224, "center"); renderText(ctx, subTitle, 18, "#fff", width / 2, 224, "center");
renderText(ctx, "共", 14, "#fff", 122, 300); renderText(ctx, "共", 14, "#fff", 122, 300);
const totalRing = data.arrows.reduce((last, next) => last + next.ring, 0); const totalRing = data.details.reduce((last, next) => last + next.ring, 0);
renderText(ctx, totalRing, 14, "#fed847", 148, 300, "center"); renderText(ctx, totalRing, 14, "#fed847", 148, 300, "center");
renderText(ctx, "环", 14, "#fff", 161, 300); renderText(ctx, "环", 14, "#fff", 161, 300);
renderLine(ctx, 77); renderLine(ctx, 77);
renderLine(ctx, 185); renderLine(ctx, 185);
renderScores(ctx, data.arrows, scoreBgImg); renderScores(ctx, data.details, scoreBgImg);
ctx.drawImage(qrCodeImg, width * 0.06, height * 0.87, 52, 52); ctx.drawImage(qrCodeImg, width * 0.06, height * 0.87, 52, 52);
renderText(ctx, "射灵平台", 12, "#fff", width * 0.26, height * 0.9); renderText(ctx, "射灵平台", 12, "#fff", width * 0.26, height * 0.9);

View File

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

View File

@@ -49,16 +49,16 @@ const props = defineProps({
<view class="desc"> <view class="desc">
<text>{{ arrows.length }}</text> <text>{{ arrows.length }}</text>
<text>支箭</text> <text>支箭</text>
<text>{{ arrows.reduce((a, b) => a + b.ring, 0) }}</text> <text>{{ arrows.reduce((a, b) => a + (b.ring || 0), 0) }}</text>
<text></text> <text></text>
</view> </view>
<ScorePanel <ScorePanel
:completeEffect="false" :completeEffect="false"
:rowCount="arrows.length === 12 ? 6 : 9" :rowCount="total === 12 ? 6 : 9"
:total="total" :total="total"
:arrows="arrows" :arrows="arrows"
:margin="arrows.length === 12 ? 4 : 1" :margin="total === 12 ? 4 : 1"
:fontSize="arrows.length === 12 ? 25 : 22" :fontSize="total === 12 ? 25 : 22"
/> />
</view> </view>
</template> </template>

View File

@@ -18,6 +18,7 @@ const props = defineProps({
}); });
const battleMode = ref(1); const battleMode = ref(1);
const targetMode = ref(1);
const loading = ref(false); const loading = ref(false);
const roomNumber = ref(""); const roomNumber = ref("");
@@ -35,13 +36,14 @@ const createRoom = debounce(async () => {
try { try {
const result = await createRoomAPI( const result = await createRoomAPI(
battleMode.value === 2 ? 2 : 1, battleMode.value === 2 ? 2 : 1,
battleMode.value === 2 ? 10 : size battleMode.value === 2 ? 10 : size,
targetMode.value*20,
); );
if (result.number) { if (result.number) {
props.onConfirm(); props.onConfirm();
await joinRoomAPI(result.number); await joinRoomAPI(result.number);
uni.navigateTo({ uni.navigateTo({
url: "/pages/battle-room?roomNumber=" + result.number, url: "/pages/battle-room?roomNumber=" + result.number + "&target=" + targetMode.value,
}); });
} }
} catch (error) { } catch (error) {
@@ -53,7 +55,11 @@ const createRoom = debounce(async () => {
</script> </script>
<template> <template>
<view class="container"> <view class="container">
<image src="../static/choose-battle-mode.png" mode="widthFix" /> <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"> <view class="create-options">
<view <view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 1 }" :class="{ 'battle-btn': true, 'battle-choosen': battleMode === 1 }"
@@ -80,6 +86,25 @@ const createRoom = debounce(async () => {
<text>乱斗模式3-10</text> <text>乱斗模式3-10</text>
</view> </view>
</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> <SButton :onClick="createRoom">创建房间</SButton>
</view> </view>
</template> </template>
@@ -91,6 +116,7 @@ const createRoom = debounce(async () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding-top: 44rpx;
} }
.container > image:first-child { .container > image:first-child {
width: 45%; width: 45%;
@@ -105,6 +131,50 @@ const createRoom = debounce(async () => {
justify-content: center; justify-content: center;
margin-bottom: 15px; 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 { .battle-btn {
width: 45%; width: 45%;
height: 55px; height: 55px;

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue"; import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import audioManager from "@/audioManager"; import audioManager from "@/audioManager";
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants"; import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText } from "@/util"; import { getDirectionText } from "@/util";
import useStore from "@/store"; import useStore from "@/store";
@@ -55,11 +55,9 @@ async function onReceiveMessage(message) {
totalShot.value = mode === 1 ? 3 : 2; totalShot.value = mode === 1 ? 3 : 2;
currentRoundEnded.value = true; currentRoundEnded.value = true;
audioManager.play("比赛开始"); audioManager.play("比赛开始");
} } else if (type === MESSAGETYPESV2.BattleEnd) {
if (type === MESSAGETYPESV2.BattleEnd) {
audioManager.play("比赛结束"); audioManager.play("比赛结束");
} } else if (type === MESSAGETYPESV2.ShootResult) {
if (type === MESSAGETYPESV2.ShootResult) {
if (melee.value && current.playerId !== user.value.id) return; if (melee.value && current.playerId !== user.value.id) return;
if (current.playerId === user.value.id) currentShot.value++; if (current.playerId === user.value.id) currentShot.value++;
if (message.shootData) { if (message.shootData) {
@@ -73,11 +71,16 @@ async function onReceiveMessage(message) {
key.push(`${getDirectionText(shootData.angle)}调整`); key.push(`${getDirectionText(shootData.angle)}调整`);
audioManager.play(key, false); audioManager.play(key, false);
} }
} } else if (type === MESSAGETYPESV2.NewRound) {
if (type === MESSAGETYPESV2.NewRound) {
currentShot.value = 0; currentShot.value = 0;
currentRound.value = current.round; currentRound.value = current.round;
currentRoundEnded.value = true; currentRoundEnded.value = true;
} else if (type === MESSAGETYPESV2.InvalidShot) {
uni.showToast({
title: "距离不足,无效",
icon: "none",
});
audioManager.play("射击无效");
} }
} }

View File

@@ -1,137 +0,0 @@
<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

@@ -88,6 +88,7 @@ watch(
transform: translateY(100%); transform: translateY(100%);
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative; position: relative;
background-color: #372E1D;
} }
.modal-content > image:first-child { .modal-content > image:first-child {
width: 100%; width: 100%;

View File

@@ -56,16 +56,20 @@ onMounted(() => {
); );
}); });
const validArrows = computed(() => {
return (props.result.details || []).filter(
(arrow) => arrow.x !== -30 && arrow.y !== -30
).length;
});
const getRing = (arrow) => { const getRing = (arrow) => {
if (arrow.ringX) return "X"; if (arrow.ringX) return "X";
return arrow.ring ? arrow.ring + "环" : "-"; 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);
</script> </script>
<template> <template>
@@ -96,8 +100,8 @@ const getRing = (arrow) => {
</view> </view>
<view :style="{ gridTemplateColumns: `repeat(${rowCount}, 1fr)` }"> <view :style="{ gridTemplateColumns: `repeat(${rowCount}, 1fr)` }">
<view v-for="(_, index) in new Array(total).fill(0)" :key="index"> <view v-for="(_, index) in new Array(total).fill(0)" :key="index">
{{ getRing(result.details[index]) {{ getRing(arrows[index])
}}<text v-if="getRing(result.details[index]) !== '-'"></text> }}<text v-if="getRing(arrows[index]) !== '-'"></text>
</view> </view>
</view> </view>
<view> <view>
@@ -160,7 +164,7 @@ const getRing = (arrow) => {
</view> </view>
</ScreenHint> </ScreenHint>
<BowData <BowData
:total="result.details.length" :total="arrows.length"
:arrows="result.details" :arrows="result.details"
:show="showBowData" :show="showBowData"
:onClose="() => (showBowData = false)" :onClose="() => (showBowData = false)"

View File

@@ -143,6 +143,12 @@ async function onReceiveMessage(msg) {
} else if (msg.type === MESSAGETYPESV2.HalfRest) { } else if (msg.type === MESSAGETYPESV2.HalfRest) {
halfTime.value = true; halfTime.value = true;
audioManager.play("中场休息"); audioManager.play("中场休息");
} else if (msg.type === MESSAGETYPESV2.InvalidShot) {
uni.showToast({
title: "距离不足,无效",
icon: "none",
});
audioManager.play("射击无效");
} }
} }

View File

@@ -87,28 +87,34 @@ const handleLogin = async () => {
}); });
} }
loading.value = true; loading.value = true;
const wxResult = await wxLogin(); try {
const fileManager = uni.getFileSystemManager(); const wxResult = await wxLogin();
const avatarBase64 = fileManager.readFileSync(avatarUrl.value, "base64"); const fileManager = uni.getFileSystemManager();
const base64Url = `data:image/png;base64,${avatarBase64}`; const avatarBase64 = fileManager.readFileSync(avatarUrl.value, "base64");
const result = await loginAPI( const base64Url = `data:image/png;base64,${avatarBase64}`;
phone.value, const result = await loginAPI(
nickName.value, phone.value,
base64Url, nickName.value,
wxResult.code base64Url,
); wxResult.code
const data = await getHomeData(); );
if (data.user) updateUser(data.user); const data = await getHomeData();
const devices = await getMyDevicesAPI(); if (data.user) updateUser(data.user);
if (devices.bindings && devices.bindings.length) { const devices = await getMyDevicesAPI();
updateDevice(devices.bindings[0].deviceId, devices.bindings[0].deviceName); if (devices.bindings && devices.bindings.length) {
try { updateDevice(
devices.bindings[0].deviceId,
devices.bindings[0].deviceName
);
const data = await getDeviceBatteryAPI(); const data = await getDeviceBatteryAPI();
updateOnline(data.online); updateOnline(data.online);
} catch (error) {} }
props.onClose();
} catch (error) {
console.log("login error", error);
} finally {
loading.value = false;
} }
loading.value = false;
props.onClose();
}; };
const openServiceLink = () => { const openServiceLink = () => {

View File

@@ -0,0 +1,197 @@
<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

@@ -40,6 +40,7 @@ export const MESSAGETYPESV2 = {
HalfRest: 7, HalfRest: 7,
TestDistance: 8, TestDistance: 8,
MatchSuccess: 9, MatchSuccess: 9,
InvalidShot: 10,
}; };
export const topThreeColors = ["#FFD947", "#D2D2D2", "#FFA515"]; export const topThreeColors = ["#FFD947", "#D2D2D2", "#FFA515"];

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount } from "vue"; import { ref, onMounted, computed, onBeforeUnmount } from "vue";
import { onShow, onLoad, onShareAppMessage } from "@dcloudio/uni-app"; import { onShow, onLoad, onShareAppMessage } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue"; import Container from "@/components/Container.vue";
import PlayerSeats from "@/components/PlayerSeats.vue"; import PlayerSeats from "@/components/PlayerSeats.vue";
@@ -188,6 +188,18 @@ const removePlayer = async (player) => {
await kickPlayerAPI(roomNumber.value, player.id); await kickPlayerAPI(roomNumber.value, player.id);
}; };
const canClick = computed(() => {
if (ready.value) return false;
const { members = [] } = room.value;
if (members.length < 2) return false;
if (
owner.value.id === user.value.id &&
members.some((m) => !m.userInfo.state && m.userInfo.id !== owner.value.id)
)
return false;
return true;
});
onShareAppMessage(() => { onShareAppMessage(() => {
return { return {
title: "邀请您进入房间对战", title: "邀请您进入房间对战",
@@ -373,14 +385,16 @@ onBeforeUnmount(() => {
:removePlayer="removePlayer" :removePlayer="removePlayer"
/> />
<view> <view>
<SButton :disabled="ready" :onClick="getReady">{{ <SButton :disabled="!canClick" :onClick="getReady">
allReady.value {{
? "即将进入对局..." allReady.value
: owner.id === user.id && (room.members || []).length > 2 ? "即将进入对局..."
? "开始对局" : owner.id === user.id
: "我准备好了" ? "开始对局"
}}</SButton> : "我准备好了"
<text class="tips">所有人准备后自动开始游戏</text> }}
</SButton>
<text class="tips">所有人准备好后由房主点击开始</text>
</view> </view>
</view> </view>
</Container> </Container>

View File

@@ -5,15 +5,15 @@ import SButton from "@/components/SButton.vue";
import { capsuleHeight } from "@/util"; import { capsuleHeight } from "@/util";
const images = [ const images = [
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmf6yitekatwe.jpg", "https://static.shelingxingqiu.com/mall/images/mall_01.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmi475gqdtrvx.jpg", "https://static.shelingxingqiu.com/mall/images/mall_02.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmgy8ej5wuap5.jpg", "https://static.shelingxingqiu.com/mall/images/mall_03.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmg6y7nveaadv.jpg", "https://static.shelingxingqiu.com/mall/images/mall_04.jpg",
"https://static.shelingxingqiu.com/attachment/2025-12-04/depguhlqg9zxastyn3.jpg", "https://static.shelingxingqiu.com/mall/images/mall_05.jpg",
"https://static.shelingxingqiu.com/attachment/2025-12-04/depguhlfr041aedqmb.jpg", "https://static.shelingxingqiu.com/mall/images/mall_06.jpg",
"https://static.shelingxingqiu.com/attachment/2025-12-04/depguhlpnlyxndnor5.jpg", "https://static.shelingxingqiu.com/mall/images/mall_07.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmg68a8mezgzx.jpg", "https://static.shelingxingqiu.com/mall/images/mall_08.jpg",
"https://static.shelingxingqiu.com/attachment/2025-10-14/ddht51a3hiyw7ueli4.jpg", "https://static.shelingxingqiu.com/mall/images/mall_09.jpg",
]; ];
const addBg = ref(false); const addBg = ref(false);

View File

@@ -129,7 +129,7 @@ const nextStep = async () => {
btnDisabled.value = true; btnDisabled.value = true;
step.value = 2; step.value = 2;
title.value = "-感知距离"; title.value = "-感知距离";
const result = await createPractiseAPI(total, 360); const result = await createPractiseAPI(total, 120);
if (result) practiseId.value = result.id; if (result) practiseId.value = result.id;
} else if (step.value === 2) { } else if (step.value === 2) {
showGuide.value = false; showGuide.value = false;
@@ -166,7 +166,7 @@ const onClose = async () => {
start.value = false; start.value = false;
scores.value = []; scores.value = [];
step.value = 3; step.value = 3;
const result = await createPractiseAPI(total, 360); const result = await createPractiseAPI(total, 120);
if (result) practiseId.value = result.id; if (result) practiseId.value = result.id;
} }
}; };

View File

@@ -169,7 +169,7 @@ onLoad(async (options) => {
<SModal <SModal
:show="showModal" :show="showModal"
:onClose="() => (showModal = false)" :onClose="() => (showModal = false)"
height="520rpx" height="716rpx"
> >
<view v-if="warnning" class="warnning"> <view v-if="warnning" class="warnning">
{{ warnning }} {{ warnning }}

View File

@@ -39,11 +39,11 @@ const toPage = async (path) => {
showModal.value = true; showModal.value = true;
return; return;
} }
if (path === "/pages/first-try") { // if (path === "/pages/first-try") {
// if (canEenter(user.value, device.value, online.value, path)) { // if (canEenter(user.value, device.value, online.value, path)) {
// await uni.$checkAudio(); // await uni.$checkAudio();
// } // }
} // }
uni.navigateTo({ url: path }); uni.navigateTo({ url: path });
}; };
@@ -75,7 +75,6 @@ onShow(async () => {
if ("823,209,293,257".indexOf(homeData.user.id) !== -1) { if ("823,209,293,257".indexOf(homeData.user.id) !== -1) {
const show = uni.getStorageSync("show-the-user"); const show = uni.getStorageSync("show-the-user");
if (!show) { if (!show) {
showTheUser.value = true;
uni.setStorageSync("show-the-user", true); uni.setStorageSync("show-the-user", true);
} }
} }

View File

@@ -16,7 +16,7 @@ const players = ref([]);
onLoad(async (options) => { onLoad(async (options) => {
if (!options.battleId) return; if (!options.battleId) return;
battleId.value = options.battleId || "60143330377469952"; battleId.value = options.battleId || "60510101693403136";
const result = await getBattleAPI(battleId.value); const result = await getBattleAPI(battleId.value);
data.value = result; data.value = result;
if (result.mode > 3) { if (result.mode > 3) {
@@ -128,7 +128,7 @@ const checkBowData = (selected) => {
<text :style="{ color: team == 1 ? '#64BAFF' : '#FF6767' }"> <text :style="{ color: team == 1 ? '#64BAFF' : '#FF6767' }">
{{ round.shoots[team].reduce((acc, cur) => acc + cur.ring, 0) }} {{ round.shoots[team].reduce((acc, cur) => acc + cur.ring, 0) }}
</text> </text>
<text>得分 {{ round.scores[team].totalRing }}</text> <text>得分 {{ round.scores[team].score }}</text>
</view> </view>
</view> </view>
</view> </view>

View File

@@ -12,7 +12,6 @@ import ScreenHint from "@/components/ScreenHint.vue";
import TestDistance from "@/components/TestDistance.vue"; import TestDistance from "@/components/TestDistance.vue";
import audioManager from "@/audioManager"; import audioManager from "@/audioManager";
import { getBattleAPI, laserCloseAPI } from "@/apis"; import { getBattleAPI, laserCloseAPI } from "@/apis";
import { isGameEnded } from "@/util";
import { MESSAGETYPESV2 } from "@/constants"; import { MESSAGETYPESV2 } from "@/constants";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
@@ -103,9 +102,11 @@ async function onReceiveMessage(msg) {
halfRest.value = true; halfRest.value = true;
tips.value = "准备下半场"; tips.value = "准备下半场";
} else if (msg.type === MESSAGETYPESV2.BattleEnd) { } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
uni.redirectTo({ setTimeout(() => {
url: "/pages/battle-result?battleId=" + msg.matchId, uni.redirectTo({
}); url: "/pages/battle-result?battleId=" + msg.matchId,
});
}, 1000);
} }
} }
onMounted(async () => { onMounted(async () => {

View File

@@ -14,11 +14,10 @@ const arrows = ref([]);
const total = ref(0); const total = ref(0);
onLoad(async (options) => { onLoad(async (options) => {
if (options.id) { if (!options.id) return;
const result = await getPractiseAPI(options.id); const result = await getPractiseAPI(options.id || 176);
arrows.value = result.arrows; arrows.value = result.details;
total.value = result.completed_arrows; total.value = result.details.length;
}
}); });
</script> </script>

View File

@@ -17,7 +17,7 @@ const practiseList = ref([]);
const toMatchDetail = (id) => { const toMatchDetail = (id) => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/match-detail?id=${id}`, url: `/pages/match-detail?battleId=${id}`,
}); });
}; };
const getPractiseDetail = async (id) => { const getPractiseDetail = async (id) => {
@@ -52,6 +52,10 @@ const onPractiseLoading = async (page) => {
} }
return result.length; return result.length;
}; };
const getName = (battle) => {
if (battle.mode <= 3) return `${battle.mode}V${battle.mode}`;
return battle.mode + "人大乱斗";
};
</script> </script>
<template> <template>
@@ -80,19 +84,19 @@ const onPractiseLoading = async (page) => {
<view <view
v-for="(item, index) in matchList" v-for="(item, index) in matchList"
:key="index" :key="index"
@click="() => toMatchDetail(item.battleId)" @click="() => toMatchDetail(item.id)"
> >
<view class="contest-header"> <view class="contest-header">
<text>{{ item.name }}</text> <text>{{ getName(item) }}</text>
<text>{{ item.createdAt }}</text> <text>{{ item.createTime }}</text>
<image src="../static/back.png" mode="widthFix" /> <image src="../static/back.png" mode="widthFix" />
</view> </view>
<BattleHeader <BattleHeader
:players="item.mode === 1 ? [] : item.players" :players="item.teams[0] ? item.teams[0].players : []"
:blueTeam="item.bluePlayers" :blueTeam="item.teams[1] ? item.teams[1].players : []"
:redTeam="item.redPlayers" :redTeam="item.teams[2] ? item.teams[2].players : []"
:winner="item.winner" :winner="item.winTeam"
:showRank="item.mode === 2" :showRank="item.teams[0]"
:showHeader="false" :showHeader="false"
/> />
</view> </view>
@@ -103,19 +107,19 @@ const onPractiseLoading = async (page) => {
<view <view
v-for="(item, index) in battleList" v-for="(item, index) in battleList"
:key="index" :key="index"
@click="() => toMatchDetail(item.battleId)" @click="() => toMatchDetail(item.id)"
> >
<view class="contest-header"> <view class="contest-header">
<text>{{ item.name }}</text> <text>{{ getName(item) }}</text>
<text>{{ item.createdAt }}</text> <text>{{ item.createTime }}</text>
<image src="../static/back.png" mode="widthFix" /> <image src="../static/back.png" mode="widthFix" />
</view> </view>
<BattleHeader <BattleHeader
:players="item.mode === 1 ? [] : item.players" :players="item.teams[0] ? item.teams[0].players : []"
:blueTeam="item.bluePlayers" :blueTeam="item.teams[1] ? item.teams[1].players : []"
:redTeam="item.redPlayers" :redTeam="item.teams[2] ? item.teams[2].players : []"
:winner="item.winner" :winner="item.winTeam"
:showRank="item.mode === 2" :showRank="item.teams[0]"
:showHeader="false" :showHeader="false"
/> />
</view> </view>
@@ -131,7 +135,7 @@ const onPractiseLoading = async (page) => {
> >
<text <text
>{{ item.completed_arrows === 36 ? "耐力挑战" : "单组练习" }} >{{ item.completed_arrows === 36 ? "耐力挑战" : "单组练习" }}
{{ item.createdAt }}</text {{ item.createTime }}</text
> >
<image src="../static/back.png" mode="widthFix" /> <image src="../static/back.png" mode="widthFix" />
</view> </view>

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount } from "vue"; import { ref, onMounted, onBeforeUnmount } from "vue";
import { onShow } from "@dcloudio/uni-app"; import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue"; import Container from "@/components/Container.vue";
import ShootProgress from "@/components/ShootProgress.vue"; import ShootProgress from "@/components/ShootProgress.vue";
import BowTarget from "@/components/BowTarget.vue"; import BowTarget from "@/components/BowTarget.vue";
@@ -21,7 +21,7 @@ import {
} from "@/apis"; } from "@/apis";
import { sharePractiseData } from "@/canvas"; import { sharePractiseData } from "@/canvas";
import { wxShare, debounce } from "@/util"; import { wxShare, debounce } from "@/util";
import { MESSAGETYPESV2, MESSAGETYPES, roundsName } from "@/constants"; import { MESSAGETYPESV2, roundsName } from "@/constants";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
@@ -31,15 +31,20 @@ const { user } = storeToRefs(store);
const start = ref(false); const start = ref(false);
const scores = ref([]); const scores = ref([]);
const total = 12; const total = 12;
const currentRound = ref(0);
const practiseResult = ref({}); const practiseResult = ref({});
const practiseId = ref(""); const practiseId = ref("");
const showGuide = ref(false); const showGuide = ref(false);
const tips = ref(""); const tips = ref("");
const targetType = ref(1);
onLoad((options) => {
if (options.target) {
targetType.value = Number(options.target);
}
});
const onReady = async () => { const onReady = async () => {
await startPractiseAPI(); await startPractiseAPI();
currentRound.value = 0;
scores.value = []; scores.value = [];
start.value = true; start.value = true;
audioManager.play("练习开始"); audioManager.play("练习开始");
@@ -56,26 +61,6 @@ async function onReceiveMessage(msg) {
} else if (msg.type === MESSAGETYPESV2.BattleEnd) { } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(onOver, 1500); setTimeout(onOver, 1500);
} }
// 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);
// }
// if (scores.value.length === total) {
// setTimeout(onOver, 1500);
// }
// }
// }
// });
} }
async function onComplete() { async function onComplete() {
@@ -89,8 +74,7 @@ async function onComplete() {
practiseResult.value = {}; practiseResult.value = {};
start.value = false; start.value = false;
scores.value = []; scores.value = [];
currentRound.value = 0; const result = await createPractiseAPI(total, 120);
const result = await createPractiseAPI(total, 360);
if (result) practiseId.value = result.id; if (result) practiseId.value = result.id;
} }
} }
@@ -107,7 +91,7 @@ onMounted(async () => {
}); });
uni.$on("socket-inbox", onReceiveMessage); uni.$on("socket-inbox", onReceiveMessage);
uni.$on("share-image", onClickShare); uni.$on("share-image", onClickShare);
const result = await createPractiseAPI(total, 120); const result = await createPractiseAPI(total, 120, targetType.value);
if (result) practiseId.value = result.id; if (result) practiseId.value = result.id;
}); });
@@ -152,7 +136,7 @@ onBeforeUnmount(() => {
</view> </view>
<BowTarget <BowTarget
:totalRound="start ? total / 4 : 0" :totalRound="start ? total / 4 : 0"
:currentRound="currentRound" :currentRound="scores.length % 3"
:scores="scores" :scores="scores"
/> />
<ScorePanel2 :arrows="scores" /> <ScorePanel2 :arrows="scores" />

View File

@@ -24,6 +24,7 @@ import { MESSAGETYPESV2 } from "@/constants";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import {onLoad} from "@dcloudio/uni-app";
const store = useStore(); const store = useStore();
const { user } = storeToRefs(store); const { user } = storeToRefs(store);
@@ -33,6 +34,13 @@ const total = 36;
const practiseResult = ref({}); const practiseResult = ref({});
const practiseId = ref(""); const practiseId = ref("");
const showGuide = ref(false); const showGuide = ref(false);
const targetType = ref(1);
onLoad((options) => {
if (options.target) {
targetType.value = Number(options.target);
}
});
const onReady = async () => { const onReady = async () => {
await startPractiseAPI(); await startPractiseAPI();
@@ -97,7 +105,7 @@ onMounted(async () => {
}); });
uni.$on("socket-inbox", onReceiveMessage); uni.$on("socket-inbox", onReceiveMessage);
uni.$on("share-image", onClickShare); uni.$on("share-image", onClickShare);
const result = await createPractiseAPI(total, 360); const result = await createPractiseAPI(total, 360, targetType.value);
if (result) practiseId.value = result.id; if (result) practiseId.value = result.id;
}); });

View File

@@ -4,6 +4,7 @@ import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue"; import Container from "@/components/Container.vue";
import Guide from "@/components/Guide.vue"; import Guide from "@/components/Guide.vue";
import Avatar from "@/components/Avatar.vue"; import Avatar from "@/components/Avatar.vue";
import TargetPicker from "@/components/TargetPicker.vue";
import { getPractiseDataAPI } from "@/apis"; import { getPractiseDataAPI } from "@/apis";
import { canEenter } from "@/util"; import { canEenter } from "@/util";
@@ -12,12 +13,20 @@ import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user, device, online } = storeToRefs(store); const { user, device, online } = storeToRefs(store);
const data = ref({}); const data = ref({});
const showTargetPicker = ref(false);
const pendingPractiseType = ref("");
const goPractise = async (type) => { const goPractise = async (type) => {
if (!canEenter(user.value, device.value, online.value)) return; if (!canEenter(user.value, device.value, online.value)) return;
// await uni.$checkAudio(); pendingPractiseType.value = type;
showTargetPicker.value = true;
};
const handleTargetConfirm = (target) => {
showTargetPicker.value = false;
const type = pendingPractiseType.value;
uni.navigateTo({ uni.navigateTo({
url: `/pages/practise-${type}`, url: `/pages/practise-${type}?target=${target}`,
}); });
}; };
@@ -88,6 +97,11 @@ onShow(async () => {
/> />
</view> </view>
</view> </view>
<TargetPicker
:show="showTargetPicker"
:onClose="() => (showTargetPicker = false)"
:onConfirm="handleTargetConfirm"
/>
</Container> </Container>
</template> </template>

View File

@@ -4,7 +4,6 @@ import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue"; import Container from "@/components/Container.vue";
import BattleHeader from "@/components/BattleHeader.vue"; import BattleHeader from "@/components/BattleHeader.vue";
import BowTarget from "@/components/BowTarget.vue"; import BowTarget from "@/components/BowTarget.vue";
import PlayersRow from "@/components/PlayersRow.vue";
import BattleFooter from "@/components/BattleFooter.vue"; import BattleFooter from "@/components/BattleFooter.vue";
import ScreenHint from "@/components/ScreenHint.vue"; import ScreenHint from "@/components/ScreenHint.vue";
import SButton from "@/components/SButton.vue"; import SButton from "@/components/SButton.vue";
@@ -13,8 +12,7 @@ import TestDistance from "@/components/TestDistance.vue";
import TeamAvatars from "@/components/TeamAvatars.vue"; import TeamAvatars from "@/components/TeamAvatars.vue";
import ShootProgress2 from "@/components/ShootProgress2.vue"; import ShootProgress2 from "@/components/ShootProgress2.vue";
import { laserCloseAPI, getBattleAPI } from "@/apis"; import { laserCloseAPI, getBattleAPI } from "@/apis";
import { isGameEnded, formatTimestamp } from "@/util"; import { MESSAGETYPESV2 } from "@/constants";
import { MESSAGETYPES, MESSAGETYPESV2, roundsName } from "@/constants";
import audioManager from "@/audioManager"; import audioManager from "@/audioManager";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
@@ -129,9 +127,11 @@ async function onReceiveMessage(msg) {
}, 2000); }, 2000);
return; return;
} }
uni.redirectTo({ setTimeout(() => {
url: "/pages/battle-result?battleId=" + msg.matchId, uni.redirectTo({
}); url: "/pages/battle-result?battleId=" + msg.matchId,
});
}, 1000);
} }
} }

View File

@@ -1,5 +1,3 @@
import { getUserGameState, getGameAPI } from "@/apis";
export const formatTimestamp = (timestamp) => { export const formatTimestamp = (timestamp) => {
const date = new Date(timestamp * 1000); const date = new Date(timestamp * 1000);
const year = date.getFullYear(); const year = date.getFullYear();
@@ -89,27 +87,6 @@ export const wxShare = async (canvasId = "shareCanvas") => {
} }
}; };
export const isGameEnded = async (battleId) => {
const state = await getUserGameState();
if (!state.gaming) {
const result = await getGameAPI(battleId);
if (result.mode) {
uni.redirectTo({
url: `/pages/battle-result?battleId=${battleId}`,
});
} else {
uni.showToast({
title: "比赛已结束",
icon: "none",
});
setTimeout(() => {
uni.navigateBack();
}, 1000);
}
}
return !state.gaming;
};
// 获取元素尺寸和位置信息 // 获取元素尺寸和位置信息
export const getElementRect = (classname) => { export const getElementRect = (classname) => {
return new Promise((resolve) => { return new Promise((resolve) => {

View File

@@ -14,8 +14,8 @@ function createWebSocket(token, onMessage) {
switch (envVersion) { switch (envVersion) {
case "develop": // 开发版 case "develop": // 开发版
// url = "ws://192.168.1.30:8000/socket"; url = "ws://localhost:8000/socket";
url = "wss://apitest.shelingxingqiu.com/socket"; // url = "wss://apitest.shelingxingqiu.com/socket";
break; break;
case "trial": // 体验版 case "trial": // 体验版
url = "wss://apitest.shelingxingqiu.com/socket"; url = "wss://apitest.shelingxingqiu.com/socket";
@@ -47,9 +47,9 @@ function createWebSocket(token, onMessage) {
uni.onSocketMessage((res) => { uni.onSocketMessage((res) => {
const { data, event } = JSON.parse(res.data); const { data, event } = JSON.parse(res.data);
if (event === "pong") return; if (event === "pong") return;
if (data.type && data.data) { if (data.type) {
console.log("收到消息:", getMessageTypeName(data.type), data.data); console.log("收到消息:", getMessageTypeName(data.type), data.data);
if (onMessage) onMessage({ ...data.data, type: data.type }); if (onMessage) onMessage({ ...(data.data || {}), type: data.type });
return; return;
} }
if (onMessage && data.updates) onMessage(data.updates); if (onMessage && data.updates) onMessage(data.updates);