完成单组练习接口调试

This commit is contained in:
kron
2025-05-29 23:45:44 +08:00
parent 6466c65b66
commit 9db31ce664
14 changed files with 282 additions and 122 deletions

View File

@@ -1,5 +1,12 @@
const BASE_URL = "http://120.79.241.5:8000/api/shoot"; const BASE_URL = "http://120.79.241.5:8000/api/shoot";
function getAuthHeader() {
const token = uni.getStorageSync("token");
return {
Authorization: `Bearer ${token}`,
};
}
// 获取全局配置 // 获取全局配置
export const getAppConfig = () => { export const getAppConfig = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -72,6 +79,81 @@ export const loginAPI = (nickName, avatarUrl, code) => {
avatarUrl, avatarUrl,
code, code,
}, },
success: (res) => {
const { code, data } = res.data;
if (code === 0) {
uni.setStorageSync("token", data.token);
uni.setStorageSync("tokenExpire", data.expires + Date.now());
resolve(data);
}
},
fail: (err) => {
reject(err);
uni.showToast({
title: "获取数据失败",
icon: "none",
});
},
});
});
};
export const bindDeviceAPI = (device) => {
return new Promise((resolve, reject) => {
uni.request({
url: `${BASE_URL}/user/device/bindDevice`,
method: "POST",
header: getAuthHeader(),
data: {
device,
},
success: (res) => {
const { code, data } = res.data;
if (code === 0) {
resolve(data);
}
},
fail: (err) => {
reject(err);
uni.showToast({
title: "获取数据失败",
icon: "none",
});
},
});
});
};
export const getMyDeviceAPI = () => {
return new Promise((resolve, reject) => {
uni.request({
url: `${BASE_URL}/user/device/getBinding?deviceId=9ZF9oVXs`,
// url: `${BASE_URL}/user/device/getBindings`,
method: "GET",
header: getAuthHeader(),
success: (res) => {
resolve(res.data);
},
fail: (err) => {
reject(err);
uni.showToast({
title: "获取数据失败",
icon: "none",
});
},
});
});
};
export const createPractiseAPI = (arrows) => {
return new Promise((resolve, reject) => {
uni.request({
url: `${BASE_URL}/user/practice/create`,
method: "POST",
header: getAuthHeader(),
data: {
arrows,
},
success: (res) => { success: (res) => {
const { code, data } = res.data; const { code, data } = res.data;
if (code === 0) { if (code === 0) {

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import BowPower from "@/components/BowPower.vue"; import BowPower from "@/components/BowPower.vue";
defineProps({ const props = defineProps({
totalRound: { totalRound: {
type: Number, type: Number,
default: 0, default: 0,
@@ -25,7 +25,20 @@ defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
scores: {
type: Array,
default: () => [],
},
}); });
function calcRealX(num) {
const len = 20 + num;
return `calc(${(len / 40) * 100}% - 10px)`;
}
function calcRealY(num) {
const len = num < 0 ? Math.abs(num) + 20 : 20 - num;
return `calc(${(len / 40) * 100}% - 10px)`;
}
</script> </script>
<template> <template>
@@ -37,7 +50,19 @@ defineProps({
}}</text> }}</text>
<BowPower v-if="power > 0" :power="power" /> <BowPower v-if="power > 0" :power="power" />
</view> </view>
<view class="target">
<image
v-for="(bow, index) in scores"
:key="index"
src="../static/hit-icon.png"
:class="`hit ${index + 1 === scores.length ? 'pump-in' : ''}`"
:style="{
left: calcRealX(bow.x),
top: calcRealY(bow.y),
}"
/>
<image src="../static/bow-target.png" mode="widthFix" /> <image src="../static/bow-target.png" mode="widthFix" />
</view>
<view v-if="avatar" class="footer"> <view v-if="avatar" class="footer">
<image :src="avatar" mode="widthFix" /> <image :src="avatar" mode="widthFix" />
</view> </view>
@@ -50,9 +75,29 @@ defineProps({
width: calc(100% - 30px); width: calc(100% - 30px);
margin: 15px; margin: 15px;
} }
.container > image { .target {
position: relative;
}
.target > image:last-child {
width: 100%; width: 100%;
} }
@keyframes pumpIn {
from {
transform: scale(2);
}
to {
transform: scale(1);
}
}
.pump-in {
animation: pumpIn 0.3s ease-out forwards;
transform-origin: center center;
}
.hit {
position: absolute;
width: 20px;
height: 20px;
}
.header { .header {
width: 100%; width: 100%;
display: flex; display: flex;

View File

@@ -6,10 +6,6 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
content: {
type: String,
default: false,
},
onClose: { onClose: {
type: Function, type: Function,
default: () => {}, default: () => {},
@@ -38,7 +34,7 @@ const props = defineProps({
</view> </view>
<IconButton <IconButton
src="../static/close-gold-outline.png" src="../static/close-gold-outline.png"
width="30" :width="30"
:onClick="onClose" :onClick="onClose"
/> />
</view> </view>

View File

@@ -14,7 +14,7 @@ const props = defineProps({
}, },
width: { width: {
type: Number, type: Number,
default: "22", default: 22,
}, },
}); });
</script> </script>

View File

@@ -1,5 +1,4 @@
<script setup> <script setup>
import { ref } from "vue";
const props = defineProps({ const props = defineProps({
scores: { scores: {
type: Array, type: Array,
@@ -15,7 +14,7 @@ const getSum = (a, b, c) => {
<view class="container"> <view class="container">
<view> <view>
<text>总成绩</text> <text>总成绩</text>
<text>23</text> <text>{{ scores.reduce((last, next) => last + next, 0) }}</text>
</view> </view>
<view <view
v-for="(title, index) in ['第一轮', '第二轮', '第三轮']" v-for="(title, index) in ['第一轮', '第二轮', '第三轮']"
@@ -24,23 +23,25 @@ const getSum = (a, b, c) => {
> >
<text>{{ title }}</text> <text>{{ title }}</text>
<text>{{ <text>{{
scores[index * 3 + 0] ? scores[index * 3 + 0] + "环" : "-" scores[index * 4 + 0] ? scores[index * 4 + 0] + "环" : "-"
}}</text> }}</text>
<text>{{ <text>{{
scores[index * 3 + 1] ? scores[index * 3 + 1] + "环" : "-" scores[index * 4 + 1] ? scores[index * 4 + 1] + "环" : "-"
}}</text> }}</text>
<text>{{ <text>{{
scores[index * 3 + 2] ? scores[index * 3 + 2] + "环" : "-" scores[index * 4 + 2] ? scores[index * 4 + 2] + "环" : "-"
}}</text> }}</text>
<text <text>{{
>{{ scores[index * 4 + 3] ? scores[index * 4 + 3] + "环" : "-"
}}</text>
<text>{{
getSum( getSum(
scores[index * 3 + 0], scores[index * 4 + 0],
scores[index * 3 + 1], scores[index * 4 + 1],
scores[index * 3 + 2] scores[index * 4 + 2],
scores[index * 4 + 3]
) )
}}</text }}</text>
>
</view> </view>
</view> </view>
</template> </template>

View File

@@ -20,8 +20,11 @@ const props = defineProps({
type: Number, type: Number,
default: 0, default: 0,
}, },
scores: {
type: Array,
default: () => [],
},
}); });
const items = ref(new Array(props.total).fill(9));
const showPanel = ref(true); const showPanel = ref(true);
const showComment = ref(false); const showComment = ref(false);
const closePanel = () => { const closePanel = () => {
@@ -40,14 +43,22 @@ setTimeout(() => {
<view :class="['container-header', showPanel ? 'scale-in' : 'scale-out']"> <view :class="['container-header', showPanel ? 'scale-in' : 'scale-out']">
<image src="../static/finish-tip.png" mode="widthFix" /> <image src="../static/finish-tip.png" mode="widthFix" />
<image src="../static/finish-frame.png" mode="widthFix" /> <image src="../static/finish-frame.png" mode="widthFix" />
<text>完成36箭获得36点经验</text> <text
>完成{{ total }}获得{{
scores.reduce((last, next) => last + next, 0)
}}点经验</text
>
</view> </view>
<view <view
class="container-content" class="container-content"
:style="{ transform: `translateY(${showPanel ? '0%' : '100%'})` }" :style="{ transform: `translateY(${showPanel ? '0%' : '100%'})` }"
> >
<view> <view>
<text>本剧成绩{{ total }}</text> <text
>本剧成绩{{
scores.reduce((last, next) => last + next, 0)
}}</text
>
<button> <button>
<text>查看靶纸</text> <text>查看靶纸</text>
<image <image
@@ -58,7 +69,7 @@ setTimeout(() => {
</button> </button>
</view> </view>
<view :style="{ gridTemplateColumns: `repeat(${rowCount}, 1fr)` }"> <view :style="{ gridTemplateColumns: `repeat(${rowCount}, 1fr)` }">
<view v-for="(score, index) in items" :key="index"> <view v-for="(score, index) in scores" :key="index">
{{ score }}<text></text> {{ score }}<text></text>
</view> </view>
</view> </view>

View File

@@ -1,6 +1,10 @@
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref, watch } from "vue";
const props = defineProps({ const props = defineProps({
start: {
type: Boolean,
default: false,
},
tips: { tips: {
type: String, type: String,
default: "", default: "",
@@ -10,18 +14,25 @@ const props = defineProps({
default: 90, default: 90,
}, },
}); });
let barColor = "#fed847"; let barColor = "#fed847";
if (props.tips.includes("红队")) barColor = "#FF6060"; if (props.tips.includes("红队")) barColor = "#FF6060";
if (props.tips.includes("蓝队")) barColor = "#5FADFF"; if (props.tips.includes("蓝队")) barColor = "#5FADFF";
const remain = ref(0); const remain = ref(props.total);
onMounted(() => {
watch(
() => props.start,
(newVal, oldVal) => {
if (oldVal === false && newVal === true) {
remain.value = props.total; remain.value = props.total;
setInterval(() => { setInterval(() => {
if (remain.value > 0) { if (remain.value > 0) {
remain.value--; remain.value--;
} }
}, 1000); }, 1000);
}); }
}
);
</script> </script>
<template> <template>

3
src/constants.js Normal file
View File

@@ -0,0 +1,3 @@
export const MESSAGETYPES = {
ShootSyncMeArrowID: parseInt("0x789b6b0d"),
};

View File

@@ -3,38 +3,17 @@ import Guide from "@/components/Guide.vue";
import BowTarget from "@/components/BowTarget.vue"; import BowTarget from "@/components/BowTarget.vue";
import SButton from "@/components/SButton.vue"; import SButton from "@/components/SButton.vue";
import Container from "@/components/Container.vue"; import Container from "@/components/Container.vue";
import { getMyDeviceAPI } from "@/apis";
// 扫描二维码方法 const getMyDevice = async () => {
const handleScan = () => { const result = await getMyDeviceAPI();
console.log('开始扫码'); console.log("我的设备:", result);
// 调用扫码API
uni.scanCode({
// 只支持扫码二维码
onlyFromCamera: true,
scanType: ['qrCode'],
success: (res) => {
// res.result 为二维码内容
console.log('扫码结果:', res.result);
uni.showToast({
title: '扫码成功',
icon: 'success'
});
// 这里可以处理扫码后的业务逻辑
},
fail: (err) => {
console.error('扫码失败:', err);
uni.showToast({
title: '扫码失败',
icon: 'error'
});
}
});
}; };
</script> </script>
<template> <template>
<Container bgType="1" title="弓箭调试"> <Container bgType="1" title="弓箭调试">
<Guide> <!-- <Guide>
<view class="guide-tips"> <view class="guide-tips">
<text>请预先射几箭测试</text> <text>请预先射几箭测试</text>
<text>确保射击距离有5米</text> <text>确保射击距离有5米</text>
@@ -44,10 +23,10 @@ const handleScan = () => {
avatar="../static/avatar.png" avatar="../static/avatar.png"
:power="45" :power="45"
tips="本次射程5.2米,已达距离要求" tips="本次射程5.2米,已达距离要求"
/> /> -->
<view> <view>
<SButton>准备好了直接开始</SButton> <!-- <SButton>准备好了直接开始</SButton> -->
<SButton :onClick="handleScan">扫码</SButton> <SButton :onClick="getMyDevice">获取我的设备</SButton>
</view> </view>
</Container> </Container>
</template> </template>

View File

@@ -1,36 +1,66 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref, onMounted, onUnmounted } from "vue";
import AppBackground from "@/components/AppBackground.vue"; import AppBackground from "@/components/AppBackground.vue";
import Header from "@/components/Header.vue"; import Header from "@/components/Header.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";
import ScorePanel2 from "@/components/ScorePanel2.vue"; import ScorePanel2 from "@/components/ScorePanel2.vue";
import ScoreResult from "@/components/ScoreResult.vue"; import ScoreResult from "@/components/ScoreResult.vue";
import SButton from "@/components/SButton.vue";
import { createPractiseAPI } from "@/apis";
import { MESSAGETYPES } from "@/constants";
import websocket from "@/websocket";
const start = ref(false);
const showScore = ref(false); const showScore = ref(false);
const scores = ref([]);
setTimeout(() => { const onReady = async () => {
const result = await createPractiseAPI(12);
console.log("result", result);
start.value = true;
const token = uni.getStorageSync("token");
websocket.createWebSocket(token, (result) => {
const messages = JSON.parse(result).data.updates || [];
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
scores.value.push(msg.target);
console.log("msg:", msg.target);
if (scores.value.length === 12) {
showScore.value = true; showScore.value = true;
}, 2000); websocket.closeWebSocket();
}
}
});
});
};
onUnmounted(() => {
websocket.closeWebSocket();
});
</script> </script>
<template> <template>
<view class="container"> <view class="container">
<AppBackground type="1" /> <AppBackground :type="1" />
<Header title="个人单组练习" /> <Header title="个人单组练习" />
<ShootProgress tips="请开始射箭第一轮" total="120" /> <ShootProgress tips="请开始射箭第一轮" :start="start" :total="120" />
<BowTarget <BowTarget
totalRound="10" :totalRound="12"
currentRound="4" :currentRound="scores.length + 1"
avatar="../static/avatar.png" avatar="../static/avatar.png"
power="45" :power="45"
:scores="scores"
/> />
<ScorePanel2 :scores="[1, 2, 3, 4, 5, 6]" /> <ScorePanel2 v-if="start" :scores="scores.map((s) => s.ring)" />
<ScoreResult <ScoreResult
:total="12" :total="12"
:rowCount="6" :rowCount="6"
:show="showScore" :show="showScore"
:onClose="() => (showScore = false)" :onClose="() => (showScore = false)"
:scores="scores.map((s) => s.ring)"
/> />
<SButton v-if="!start" :onClick="onReady">准备好了直接开始</SButton>
</view> </view>
</template> </template>

View File

@@ -60,7 +60,7 @@ const toRankIntroPage = () => {
:onClick="toOrderPage" :onClick="toOrderPage"
/> />
<UserItem title="新手试炼场" :onClick="toFristTryPage"> <UserItem title="新手试炼场" :onClick="toFristTryPage">
<text v-if="user.trio" :style="{ color: '#259249' }">已完成</text> <text v-if="user.trio > 1" :style="{ color: '#259249' }">已完成</text>
<text v-else :style="{ color: '#CC311F' }">未完成</text> <text v-else :style="{ color: '#CC311F' }">未完成</text>
</UserItem> </UserItem>
<UserItem title="会员" :onClick="toBeVipPage"> 已赠送6个月会员 </UserItem> <UserItem title="会员" :onClick="toBeVipPage"> 已赠送6个月会员 </UserItem>

BIN
src/static/hit-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

View File

@@ -8,7 +8,11 @@ export default defineStore("store", {
id: "", id: "",
nickName: "游客", nickName: "游客",
avatarUrl: "../static/avatar.png", avatarUrl: "../static/avatar.png",
trio: false, // 是否完成新手试炼 trio: 0, // 大于1表示完成新手引导
},
device: {
id: "",
deviceName: "",
}, },
}), }),
@@ -24,6 +28,10 @@ export default defineStore("store", {
updateUser(user) { updateUser(user) {
this.user = user; this.user = user;
}, },
updateDevice(deviceId, deviceName) {
this.device.id = deviceId;
this.device.deviceName = deviceName;
},
}, },
// 开启数据持久化 // 开启数据持久化
@@ -32,7 +40,7 @@ export default defineStore("store", {
strategies: [ strategies: [
{ {
storage: uni.getStorageSync, storage: uni.getStorageSync,
paths: ["user"], // 只持久化用户信息 paths: ["user", "device"], // 只持久化用户信息
}, },
], ],
}, },

View File

@@ -1,31 +1,18 @@
// utils/websocket.js
let socket = null; let socket = null;
let messageCallback = null; let heartbeatInterval = null;
/** /**
* 连接 WebSocket * 建立 WebSocket 连接
* @param {String} url WebSocket 地址
* @param {Function} onMessage 消息回调
*/ */
function connectWebSocket(token, onMessage) { function createWebSocket(token, onMessage) {
messageCallback = onMessage;
socket = uni.connectSocket({ socket = uni.connectSocket({
url: `ws://120.79.241.5:8000/socket?authorization=${token}`, url: `ws://120.79.241.5:8000/socket?authorization=${token}`,
success: () => { success: () => console.log("websocket 连接成功"),
console.log("连接建立成功");
},
fail: (err) => {
console.error("连接失败", err);
},
}); });
// 接收消息 // 接收消息
uni.onSocketMessage((res) => { uni.onSocketMessage((res) => {
if (messageCallback) { if (onMessage) onMessage(res.data);
messageCallback(res.data);
}
}); });
// 错误处理 // 错误处理
@@ -36,39 +23,46 @@ function connectWebSocket(token, onMessage) {
// 关闭处理 // 关闭处理
uni.onSocketClose(() => { uni.onSocketClose(() => {
console.log("WebSocket 已关闭"); console.log("WebSocket 已关闭");
socket = null;
}); });
// 启动心跳
startHeartbeat();
}
function closeWebSocket() {
if (socket) socket.close();
} }
/** /**
* 发送消息 * 启动心跳
* @param {String} msg 要发送的消息内容
*/ */
function sendWebSocketMessage(msg) { function startHeartbeat() {
stopHeartbeat(); // 防止重复启动
heartbeatInterval = setInterval(() => {
if (socket && uni.sendSocketMessage) { if (socket && uni.sendSocketMessage) {
console.log("ping");
uni.sendSocketMessage({ uni.sendSocketMessage({
data: msg, data: "ping",
fail: (err) => { fail: (err) => {
console.error("发送消息失败", err); console.error("发送心跳失败", err);
}, },
}); });
} else {
console.warn("WebSocket 未连接");
} }
}, 5000);
} }
/** /**
* 关闭连接 * 停止心跳
*/ */
function closeWebSocket() { function stopHeartbeat() {
if (socket) { if (heartbeatInterval) {
uni.closeSocket(); clearInterval(heartbeatInterval);
socket = null; heartbeatInterval = null;
} }
} }
export default { export default {
connectWebSocket, createWebSocket,
sendWebSocketMessage,
closeWebSocket, closeWebSocket,
}; };