完成单组练习接口调试
This commit is contained in:
82
src/apis.js
82
src/apis.js
@@ -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",
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -14,7 +14,7 @@ const props = defineProps({
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: "22",
|
||||
default: 22,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
3
src/constants.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const MESSAGETYPES = {
|
||||
ShootSyncMeArrowID: parseInt("0x789b6b0d"),
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
BIN
src/static/hit-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 524 B |
12
src/store.js
12
src/store.js
@@ -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"], // 只持久化用户信息
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user