完成单组练习接口调试

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";
function getAuthHeader() {
const token = uni.getStorageSync("token");
return {
Authorization: `Bearer ${token}`,
};
}
// 获取全局配置
export const getAppConfig = () => {
return new Promise((resolve, reject) => {
@@ -75,6 +82,8 @@ export const loginAPI = (nickName, avatarUrl, 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);
}
},
@@ -88,3 +97,76 @@ export const loginAPI = (nickName, avatarUrl, code) => {
});
});
};
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) => {
const { code, data } = res.data;
if (code === 0) {
resolve(data);
}
},
fail: (err) => {
reject(err);
uni.showToast({
title: "获取数据失败",
icon: "none",
});
},
});
});
};

View File

@@ -1,6 +1,6 @@
<script setup>
import BowPower from "@/components/BowPower.vue";
defineProps({
const props = defineProps({
totalRound: {
type: Number,
default: 0,
@@ -25,7 +25,20 @@ defineProps({
type: Boolean,
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>
<template>
@@ -37,7 +50,19 @@ defineProps({
}}</text>
<BowPower v-if="power > 0" :power="power" />
</view>
<image src="../static/bow-target.png" mode="widthFix" />
<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" />
</view>
<view v-if="avatar" class="footer">
<image :src="avatar" mode="widthFix" />
</view>
@@ -50,9 +75,29 @@ defineProps({
width: calc(100% - 30px);
margin: 15px;
}
.container > image {
.target {
position: relative;
}
.target > image:last-child {
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 {
width: 100%;
display: flex;

View File

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

View File

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

View File

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

View File

@@ -20,8 +20,11 @@ const props = defineProps({
type: Number,
default: 0,
},
scores: {
type: Array,
default: () => [],
},
});
const items = ref(new Array(props.total).fill(9));
const showPanel = ref(true);
const showComment = ref(false);
const closePanel = () => {
@@ -40,14 +43,22 @@ setTimeout(() => {
<view :class="['container-header', showPanel ? 'scale-in' : 'scale-out']">
<image src="../static/finish-tip.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
class="container-content"
:style="{ transform: `translateY(${showPanel ? '0%' : '100%'})` }"
>
<view>
<text>本剧成绩{{ total }}</text>
<text
>本剧成绩{{
scores.reduce((last, next) => last + next, 0)
}}</text
>
<button>
<text>查看靶纸</text>
<image
@@ -58,7 +69,7 @@ setTimeout(() => {
</button>
</view>
<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>
</view>
</view>

View File

@@ -1,6 +1,10 @@
<script setup>
import { ref, onMounted } from "vue";
import { ref, watch } from "vue";
const props = defineProps({
start: {
type: Boolean,
default: false,
},
tips: {
type: String,
default: "",
@@ -10,18 +14,25 @@ const props = defineProps({
default: 90,
},
});
let barColor = "#fed847";
if (props.tips.includes("红队")) barColor = "#FF6060";
if (props.tips.includes("蓝队")) barColor = "#5FADFF";
const remain = ref(0);
onMounted(() => {
remain.value = props.total;
setInterval(() => {
if (remain.value > 0) {
remain.value--;
const remain = ref(props.total);
watch(
() => props.start,
(newVal, oldVal) => {
if (oldVal === false && newVal === true) {
remain.value = props.total;
setInterval(() => {
if (remain.value > 0) {
remain.value--;
}
}, 1000);
}
}, 1000);
});
}
);
</script>
<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 SButton from "@/components/SButton.vue";
import Container from "@/components/Container.vue";
import { getMyDeviceAPI } from "@/apis";
// 扫描二维码方法
const handleScan = () => {
console.log('开始扫码');
// 调用扫码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'
});
}
});
const getMyDevice = async () => {
const result = await getMyDeviceAPI();
console.log("我的设备:", result);
};
</script>
<template>
<Container bgType="1" title="弓箭调试">
<Guide>
<!-- <Guide>
<view class="guide-tips">
<text>请预先射几箭测试</text>
<text>确保射击距离有5米</text>
@@ -44,10 +23,10 @@ const handleScan = () => {
avatar="../static/avatar.png"
:power="45"
tips="本次射程5.2米,已达距离要求"
/>
/> -->
<view>
<SButton>准备好了直接开始</SButton>
<SButton :onClick="handleScan">扫码</SButton>
<!-- <SButton>准备好了直接开始</SButton> -->
<SButton :onClick="getMyDevice">获取我的设备</SButton>
</view>
</Container>
</template>

View File

@@ -1,36 +1,66 @@
<script setup>
import { ref } from "vue";
import { ref, onMounted, onUnmounted } from "vue";
import AppBackground from "@/components/AppBackground.vue";
import Header from "@/components/Header.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import BowTarget from "@/components/BowTarget.vue";
import ScorePanel2 from "@/components/ScorePanel2.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 scores = ref([]);
setTimeout(() => {
showScore.value = true;
}, 2000);
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;
websocket.closeWebSocket();
}
}
});
});
};
onUnmounted(() => {
websocket.closeWebSocket();
});
</script>
<template>
<view class="container">
<AppBackground type="1" />
<AppBackground :type="1" />
<Header title="个人单组练习" />
<ShootProgress tips="请开始射箭第一轮" total="120" />
<ShootProgress tips="请开始射箭第一轮" :start="start" :total="120" />
<BowTarget
totalRound="10"
currentRound="4"
:totalRound="12"
:currentRound="scores.length + 1"
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
:total="12"
:rowCount="6"
:show="showScore"
:onClose="() => (showScore = false)"
:scores="scores.map((s) => s.ring)"
/>
<SButton v-if="!start" :onClick="onReady">准备好了直接开始</SButton>
</view>
</template>

View File

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

View File

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