9 Commits

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

10532
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -2,12 +2,11 @@
import { watch } from "vue"; import { watch } from "vue";
import { onShow, onHide } from "@dcloudio/uni-app"; import { onShow, onHide } from "@dcloudio/uni-app";
import websocket from "@/websocket"; import websocket from "@/websocket";
import { getDeviceBatteryAPI } from "@/apis";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user } = storeToRefs(store); const { user } = storeToRefs(store);
const { updateUser, updateOnline } = store; const { updateUser } = store;
watch( watch(
() => user.value.id, () => user.value.id,
@@ -30,18 +29,7 @@ watch(
} }
); );
function emitUpdateUser(value) {
updateUser(value);
}
async function emitUpdateOnline() {
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
}
onShow(() => { onShow(() => {
uni.$on("update-user", emitUpdateUser);
uni.$on("update-online", emitUpdateOnline);
const token = uni.getStorageSync( const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token` `${uni.getAccountInfoSync().miniProgram.envVersion}_token`
); );
@@ -54,8 +42,6 @@ onShow(() => {
}); });
onHide(() => { onHide(() => {
uni.$off("update-user", emitUpdateUser);
uni.$off("update-online", emitUpdateOnline);
websocket.closeWebSocket(); websocket.closeWebSocket();
}); });
</script> </script>
@@ -91,14 +77,10 @@ button::after {
.guide-tips { .guide-tips {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-size: 28rpx;
} }
.guide-tips > text:first-child { .guide-tips > text:first-child {
color: #fed847; color: #fed847;
} }
.guide-tips > text:nth-child(2) {
font-size: 24rpx;
}
@keyframes fadeInOut { @keyframes fadeInOut {
0% { 0% {
@@ -193,7 +175,7 @@ button::after {
.share-canvas { .share-canvas {
width: 300px; width: 300px;
height: 530px; height: 534px;
position: absolute; position: absolute;
top: -1000px; top: -1000px;
left: 0; left: 0;
@@ -235,22 +217,15 @@ button::after {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 20rpx; margin-top: 5px;
} }
.see-more > text { .see-more > text {
color: #39a8ff; color: #39a8ff;
margin-top: 2px;
font-size: 13px; font-size: 13px;
} }
.see-more > image { .see-more > image {
width: 15px; width: 15px;
} margin-top: 2px;
@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> </style>

View File

@@ -45,7 +45,6 @@ function request(method, url, data = {}) {
uni.removeStorageSync( uni.removeStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token` `${uni.getAccountInfoSync().miniProgram.envVersion}_token`
); );
uni.$emit("update-user");
} }
if (message === "ROOM_FULL") { if (message === "ROOM_FULL") {
resolve({ full: true }); resolve({ full: true });
@@ -163,14 +162,13 @@ export const getProvinceData = () => {
return request("GET", "/index/provinces/list"); return request("GET", "/index/provinces/list");
}; };
export const loginAPI = async (phone, nickName, avatarData, code) => { export const loginAPI = async (nickName, avatarData, code) => {
const result = await request("POST", "/index/code", { const result = await request("POST", "/index/code", {
appName: "shoot", appName: "shoot",
appId: "wxa8f5989dcd45cc23", appId: "wxa8f5989dcd45cc23",
nickName, nickName,
avatarData, avatarData,
code, code,
phone,
}); });
uni.setStorageSync( uni.setStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`, `${uni.getAccountInfoSync().miniProgram.envVersion}_token`,
@@ -196,10 +194,9 @@ export const getMyDevicesAPI = () => {
return request("GET", "/user/device/getBindings"); return request("GET", "/user/device/getBindings");
}; };
export const createPractiseAPI = (arrows, mode) => { export const createPractiseAPI = (arrows) => {
return request("POST", "/user/practice/create", { return request("POST", "/user/practice/create", {
arrows, arrows,
mode,
}); });
}; };
@@ -231,10 +228,9 @@ export const destroyRoomAPI = (roomNumber) => {
}); });
}; };
export const exitRoomAPI = (number, userId) => { export const exitRoomAPI = (number) => {
return request("POST", "/user/room/exitRoom", { return request("POST", "/user/room/exitRoom", {
number, number,
userId,
}); });
}; };
@@ -413,8 +409,9 @@ export const cancelOrderListAPI = async (id) => {
return request("POST", "/user/order/cancelOrder", { id }); return request("POST", "/user/order/cancelOrder", { id });
}; };
export const getUserGameState = () => { export const isGamingAPI = async () => {
return request("GET", "/user/state"); const result = await request("GET", "/user/isGaming");
return result.gaming || false;
}; };
export const getCurrentGameAPI = async () => { export const getCurrentGameAPI = async () => {
@@ -521,34 +518,3 @@ export const addNoteAPI = async (id, remark) => {
export const removePointRecord = async (id) => { export const removePointRecord = async (id) => {
return request("DELETE", `/user/score/sheet/delete?id=${id}`); return request("DELETE", `/user/score/sheet/delete?id=${id}`);
}; };
export const getPhoneNumberAPI = (data) => {
return request("POST", "/index/getPhone", data);
};
export const getPointBookRankListAPI = (page = 1) => {
return request(
"GET",
`/user/score/sheet/week/shoot/rank/list?pageNum=${page}&pageSize=100`
);
};
export const clickLikeAPI = (userId, ifLike) => {
return request("POST", "/user/score/sheet/week/shoot/rank/like", {
userId,
ifLike,
});
};
export const getMyLikeList = (page = 1, pageSize = 10) => {
return request(
"GET",
`/user/score/sheet/week/shoot/rank/like/list?pageNum=${page}&pageSize=${pageSize}`
);
};
export const getReadyAPI = (roomId) => {
return request("POST", `/user/room/ready`, {
roomId,
});
};

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,10 +21,6 @@ const props = defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
total: {
type: Number,
default: 0,
},
}); });
</script> </script>
@@ -55,7 +51,7 @@ const props = defineProps({
<ScorePanel <ScorePanel
:completeEffect="false" :completeEffect="false"
:rowCount="arrows.length === 12 ? 6 : 9" :rowCount="arrows.length === 12 ? 6 : 9"
:total="total" :total="arrows.length"
:scores="arrows.map((a) => a.ring)" :scores="arrows.map((a) => a.ring)"
:margin="arrows.length === 12 ? 4 : 1" :margin="arrows.length === 12 ? 4 : 1"
:fontSize="arrows.length === 12 ? 25 : 22" :fontSize="arrows.length === 12 ? 25 : 22"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,7 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import SButton from "@/components/SButton.vue"; import SButton from "@/components/SButton.vue";
import { joinRoomAPI, createRoomAPI, isGamingAPI } from "@/apis";
import { joinRoomAPI, createRoomAPI } from "@/apis";
import { debounce } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, game } = storeToRefs(store);
const props = defineProps({ const props = defineProps({
onConfirm: { onConfirm: {
@@ -18,11 +11,13 @@ const props = defineProps({
}); });
const battleMode = ref(1); const battleMode = ref(1);
const step = ref(1);
const loading = ref(false); const loading = ref(false);
const roomNumber = ref(""); const roomNumber = ref("");
const createRoom = debounce(async () => { const createRoom = async () => {
if (game.value.inBattle) { const isGaming = await isGamingAPI();
if (isGaming) {
uni.$showHint(1); uni.$showHint(1);
return; return;
} }
@@ -36,46 +31,75 @@ const createRoom = debounce(async () => {
battleMode.value === 2 ? 2 : 1, battleMode.value === 2 ? 2 : 1,
battleMode.value === 2 ? 10 : size battleMode.value === 2 ? 10 : size
); );
if (result.number) { if (result.number) roomNumber.value = result.number;
props.onConfirm(); step.value = 2;
await joinRoomAPI(result.number);
uni.navigateTo({
url: "/pages/battle-room?roomNumber=" + result.number,
});
}
loading.value = false; loading.value = false;
}); };
const enterRoom = async () => {
step.value = 1;
props.onConfirm();
await joinRoomAPI(roomNumber.value);
uni.navigateTo({
url: `/pages/battle-room?roomNumber=${roomNumber.value}`,
});
};
const setClipboardData = () => {
uni.setClipboardData({
data: roomNumber.value,
success() {
uni.showToast({ title: "复制成功" });
},
});
};
</script> </script>
<template> <template>
<view class="container"> <view class="container">
<image src="../static/choose-battle-mode.png" mode="widthFix" /> <image
<view class="create-options"> v-if="step === 1"
src="../static/choose-battle-mode.png"
mode="widthFix"
/>
<view v-if="step === 1" class="create-options">
<view <view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 1 }" :class="{ 'battle-btn': true, 'battle-choosen': battleMode === 1 }"
@click="() => (battleMode = 1)" @click="() => (battleMode = 1)"
> >
<text>对抗模式1V1</text> <text>对抗模式1V1</text>
</view> </view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 2 }"
@click="() => (battleMode = 2)"
>
<text>乱斗模式3-10</text>
</view>
<view <view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 3 }" :class="{ 'battle-btn': true, 'battle-choosen': battleMode === 3 }"
@click="() => (battleMode = 3)" @click="() => (battleMode = 3)"
> >
<text>对抗模式2V2</text> <text>对抗模式2V2</text>
<!-- <text>敬请期待</text> -->
</view> </view>
<view <view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 4 }" :class="{ 'battle-btn': true, 'battle-choosen': battleMode === 4 }"
@click="() => (battleMode = 4)" @click="() => (battleMode = 4)"
> >
<text>对抗模式3V3</text> <text>对抗模式3V3</text>
</view> <!-- <text>敬请期待</text> -->
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 2 }"
@click="() => (battleMode = 2)"
>
<text>乱斗模式3-10</text>
</view> </view>
</view> </view>
<SButton :onClick="createRoom">创建房间</SButton> <SButton v-if="step === 1" :onClick="createRoom">下一步</SButton>
<view v-if="step === 2" class="room-info">
<view>
<text>房间号</text>
<text>{{ roomNumber }}</text>
</view>
<view class="copy-room-number" @click="setClipboardData"
>复制房间信息邀请朋友进入</view
>
<SButton width="70vw" :onClick="enterRoom">进入房间</SButton>
<text>30分钟无人进入则房间无效</text>
</view>
</view> </view>
</template> </template>
@@ -118,4 +142,42 @@ const createRoom = debounce(async () => {
border: 4rpx solid #fff3; border: 4rpx solid #fff3;
border-color: #fed847; border-color: #fed847;
} }
/* .battle-close {
background-color: #8889;
color: #b3b3b3;
}
.battle-close > text:last-child {
font-size: 12px;
} */
.room-info {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding-top: 40px;
}
.room-info > view:first-child {
font-size: 22px;
color: #fff;
margin-bottom: 20px;
}
.room-info > text {
color: #888686;
font-size: 14px;
margin: 10px 0;
}
.room-info > view:last-child {
color: #287fff;
margin: 20px 0;
font-size: 14px;
}
.copy-room-number {
width: calc(70vw - 20px);
color: #fed847;
border: 1px solid #fed847;
padding: 10px;
text-align: center;
border-radius: 10px;
margin-bottom: 20px;
}
</style> </style>

View File

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

View File

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

View File

@@ -51,7 +51,9 @@ const toUserPage = () => {
const signin = () => { const signin = () => {
if (!user.value.id) { if (!user.value.id) {
uni.$emit("point-book-signin"); uni.navigateTo({
url: "/pages/sign-in",
});
} }
}; };
@@ -71,14 +73,8 @@ const updateHot = (value) => {
onMounted(() => { onMounted(() => {
const pages = getCurrentPages(); const pages = getCurrentPages();
const currentPage = pages[pages.length - 1]; const currentPage = pages[pages.length - 1];
if ( if (currentPage.route === "pages/point-book-edit") {
currentPage.route === "pages/point-book-edit" ||
currentPage.route === "pages/point-book-detail"
) {
pointBook.value = uni.getStorageSync("point-book"); pointBook.value = uni.getStorageSync("point-book");
if (!pointBook.value) {
pointBook.value = uni.getStorageSync("last-point-book");
}
} }
if ( if (
currentPage.route === "pages/team-battle" || currentPage.route === "pages/team-battle" ||
@@ -162,7 +158,7 @@ onBeforeUnmount(() => {
</block> </block>
</view> </view>
<image <image
:style="{ opacity: showLoader && loading ? 0 : 0 }" :style="{ opacity: showLoader && loading ? 1 : 0 }"
src="../static/btn-loading.png" src="../static/btn-loading.png"
mode="widthFix" mode="widthFix"
class="loading" class="loading"
@@ -273,7 +269,6 @@ onBeforeUnmount(() => {
} }
.user-header > image:last-child { .user-header > image:last-child {
width: 36rpx; width: 36rpx;
height: 36rpx;
} }
.user-header > text:nth-child(2) { .user-header > text:nth-child(2) {
font-weight: 500; font-weight: 500;

View File

@@ -2,8 +2,6 @@
import { ref, watch, onMounted, onBeforeUnmount } from "vue"; import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import audioManager from "@/audioManager"; import audioManager from "@/audioManager";
import { MESSAGETYPES } from "@/constants"; import { MESSAGETYPES } from "@/constants";
import { getDirectionText } from "@/util";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
@@ -13,71 +11,60 @@ const tips = ref("");
const melee = ref(false); const melee = ref(false);
const timer = ref(null); const timer = ref(null);
const sound = ref(true); const sound = ref(true);
const currentSound = ref("");
const currentRound = ref(0); const currentRound = ref(0);
const currentRoundEnded = ref(false); const currentRoundEnded = ref(false);
const ended = ref(false); const ended = ref(false);
const halfTime = ref(false); const halfTime = ref(false);
const currentShot = ref(0); const currentShot = ref(0);
const totalShot = ref(0); const totalShot = ref(0);
const yourTurn = ref(false);
watch( watch(
() => tips.value, () => tips.value,
(newVal) => { (newVal) => {
let key = []; let key = "";
if (newVal.includes("重回")) return; if (newVal.includes("红队")) key = "请红方射箭";
if (newVal.includes("蓝队")) key = "请蓝方射箭";
if (!sound.value) return;
if (currentRoundEnded.value) { if (currentRoundEnded.value) {
currentRound.value += 1; currentRound.value += 1;
// 播放当前轮次语音 // 播放当前轮次语音
key.push(`${["一", "二", "三", "四", "五"][currentRound.value - 1]}`); audioManager.play(
`${["一", "二", "三", "四", "五"][currentRound.value - 1]}`
);
} }
key.push( // 延迟播放队伍提示音
newVal.includes("你") setTimeout(
? "轮到你了" () => {
: newVal.includes("红队") if (key && !yourTurn.value) audioManager.play(key);
? "请红方射箭" currentRoundEnded.value = false;
: "请蓝方射箭" yourTurn.value = false;
},
currentRoundEnded.value ? 1000 : 0
); );
audioManager.play(key, false);
currentRoundEnded.value = false;
} }
); );
const updateSound = () => { const updateSound = () => {
sound.value = !sound.value; sound.value = !sound.value;
audioManager.setMuted(!sound.value); if (!sound.value) audioManager.stop(currentSound.value);
}; };
async function onReceiveMessage(messages = []) { async function onReceiveMessage(messages = []) {
if (ended.value) return; if (!sound.value || ended.value) return;
messages.forEach((msg) => { messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootResult) { if (msg.constructor === MESSAGETYPES.ShootResult) {
if (melee.value && msg.userId !== user.value.id) return; if (melee.value && msg.userId !== user.value.id) return;
if (msg.userId === user.value.id) currentShot.value++; if (msg.userId === user.value.id) currentShot.value++;
if (msg.battleInfo && msg.userId === user.value.id) {
const players = [
...(msg.battleInfo.blueTeam || []),
...(msg.battleInfo.redTeam || []),
];
const currentPlayer = players.find((p) => p.id === msg.userId);
currentShot.value = 0;
try {
if (
currentPlayer &&
currentPlayer.shotHistory &&
currentPlayer.shotHistory[msg.battleInfo.currentRound]
) {
currentShot.value =
currentPlayer.shotHistory[msg.battleInfo.currentRound].length;
}
} catch (_) {}
}
if (!halfTime.value && msg.target) { if (!halfTime.value && msg.target) {
let key = []; currentSound.value = msg.target.ring
key.push(msg.target.ring ? `${msg.target.ring}` : "未上靶"); ? `${msg.target.ring}`
if (!msg.target.ring) : "未上靶";
key.push(`${getDirectionText(msg.target.angle)}调整`); audioManager.play(currentSound.value);
audioManager.play(key);
} }
} else if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) {
yourTurn.value = user.value.id === msg.userId;
} else if (msg.constructor === MESSAGETYPES.InvalidShot) { } else if (msg.constructor === MESSAGETYPES.InvalidShot) {
if (msg.userId === user.value.id) { if (msg.userId === user.value.id) {
uni.showToast({ uni.showToast({
@@ -124,6 +111,7 @@ async function onReceiveMessage(messages = []) {
} }
const playSound = (key) => { const playSound = (key) => {
currentSound.value = key;
audioManager.play(key); audioManager.play(key);
}; };
@@ -153,7 +141,7 @@ onBeforeUnmount(() => {
<template> <template>
<view class="container"> <view class="container">
<text>{{ (tips || "").replace(/你/g, "").replace(/重回/g, "") }}</text> <text>{{ tips }}</text>
<text v-if="totalShot > 0"> ({{ currentShot }}/{{ totalShot }}) </text> <text v-if="totalShot > 0"> ({{ currentShot }}/{{ totalShot }}) </text>
<button v-if="!!tips" hover-class="none" @click="updateSound"> <button v-if="!!tips" hover-class="none" @click="updateSound">
<image <image
@@ -171,7 +159,6 @@ onBeforeUnmount(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-weight: 500;
} }
.container > button:last-child { .container > button:last-child {
width: 36px; width: 36px;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -117,10 +117,10 @@ onBeforeUnmount(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
font-size: 32rpx; font-size: 16px;
} }
.round-end-tip > text:first-child { .round-end-tip > text:first-child {
font-size: 36rpx; font-size: 18px;
color: #fff; color: #fff;
} }
.point-view1 { .point-view1 {
@@ -137,7 +137,7 @@ onBeforeUnmount(() => {
} }
.point-view2 { .point-view2 {
margin: 12px 0; margin: 12px 0;
font-size: 48rpx; font-size: 24px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -155,8 +155,7 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 10px; margin-top: 10px;
font-size: 28rpx; font-size: 14px;
word-break: keep-all;
} }
.final-shoot > text:nth-child(1) { .final-shoot > text:nth-child(1) {
width: 20px; width: 20px;
@@ -164,7 +163,7 @@ onBeforeUnmount(() => {
} }
.final-shoot > text:nth-child(1), .final-shoot > text:nth-child(1),
.final-shoot > text:nth-child(3) { .final-shoot > text:nth-child(3) {
font-size: 32rpx; font-size: 18px;
color: #fed847; color: #fed847;
margin-left: 10px; margin-left: 10px;
margin-right: 5px; margin-right: 5px;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,11 @@
<script setup> <script setup>
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue"; import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import audioManager from "@/audioManager"; import audioManager from "@/audioManager";
import { MESSAGETYPES } from "@/constants"; import { MESSAGETYPES } from "@/constants";
import { getDirectionText } from "@/util";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user } = storeToRefs(store); const { user } = storeToRefs(store);
const props = defineProps({ const props = defineProps({
show: { show: {
type: Boolean, type: Boolean,
@@ -38,21 +35,17 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
onStop: {
type: Function,
default: () => {},
},
}); });
const barColor = ref("#fed847"); const barColor = ref("#fed847");
const remain = ref(props.total); const remain = ref(props.total);
const timer = ref(null); const timer = ref(null);
const sound = ref(true); const sound = ref(true);
const currentSound = ref("");
const currentRound = ref(props.currentRound); const currentRound = ref(props.currentRound);
const currentRoundEnded = ref(false); const currentRoundEnded = ref(false);
const ended = ref(false); const ended = ref(false);
const halfTime = ref(false); const halfTime = ref(false);
const wait = ref(0);
watch( watch(
() => props.tips, () => props.tips,
@@ -60,7 +53,7 @@ watch(
let key = ""; let key = "";
if (newVal.includes("红队")) key = "请红方射箭"; if (newVal.includes("红队")) key = "请红方射箭";
if (newVal.includes("蓝队")) key = "请蓝方射箭"; if (newVal.includes("蓝队")) key = "请蓝方射箭";
if (key) { if (key && sound.value) {
if (currentRoundEnded.value) { if (currentRoundEnded.value) {
currentRound.value += 1; currentRound.value += 1;
currentRoundEnded.value = false; currentRoundEnded.value = false;
@@ -79,45 +72,57 @@ watch(
} }
); );
const resetTimer = (count) => {
if (timer.value) clearInterval(timer.value);
remain.value = Math.round(count);
if (remain.value > 0) {
timer.value = setInterval(() => {
if (remain.value === 0) {
clearInterval(timer.value);
props.onStop();
}
if (remain.value > 0) remain.value--;
}, 1000);
}
};
watch( watch(
() => props.start, () => props.tips,
(newVal) => { (newVal) => {
if (newVal) resetTimer(props.total); if (newVal.includes("红队")) barColor.value = "#FF6060";
else if (timer.value) clearInterval(timer.value); if (newVal.includes("蓝队")) barColor.value = "#5FADFF";
if (newVal.includes("红队") || newVal.includes("蓝队")) {
if (timer.value) clearInterval(timer.value);
remain.value = props.total;
timer.value = setInterval(() => {
if (remain.value > 0) remain.value--;
}, 1000);
}
}, },
{ {
immediate: true, immediate: true,
} }
); );
const tipContent = computed(() => { watch(
if (halfTime.value) { () => props.start,
return props.battleId ? "中场休息" : `中场休息(${wait.value}秒)`; (newVal) => {
if (timer.value) clearInterval(timer.value);
if (newVal) {
remain.value = props.total;
timer.value = setInterval(() => {
if (remain.value > 0) remain.value--;
}, 1000);
}
},
{
immediate: true,
} }
return props.start && remain.value === 0 ? "时间到!" : props.tips; );
});
const updateRemain = (value) => {
if (timer.value) clearInterval(timer.value);
remain.value = Math.round(value);
if (remain.value > 0) {
timer.value = setInterval(() => {
if (remain.value > 0) remain.value--;
}, 1000);
}
};
const updateSound = () => { const updateSound = () => {
sound.value = !sound.value; sound.value = !sound.value;
audioManager.setMuted(!sound.value); if (!sound.value) audioManager.stop(currentSound.value);
}; };
async function onReceiveMessage(messages = []) { async function onReceiveMessage(messages = []) {
if (ended.value) return; if (!sound.value || ended.value) return;
messages.forEach((msg) => { messages.forEach((msg) => {
if ( if (
(props.battleId && msg.constructor === MESSAGETYPES.ShootResult) || (props.battleId && msg.constructor === MESSAGETYPES.ShootResult) ||
@@ -125,11 +130,10 @@ async function onReceiveMessage(messages = []) {
) { ) {
if (props.melee && msg.userId !== user.value.id) return; if (props.melee && msg.userId !== user.value.id) return;
if (!halfTime.value && msg.target) { if (!halfTime.value && msg.target) {
let key = []; currentSound.value = msg.target.ring
key.push(msg.target.ring ? `${msg.target.ring}` : "未上靶"); ? `${msg.target.ring}`
if (!msg.target.ring) : "未上靶";
key.push(`${getDirectionText(msg.target.angle)}调整`); audioManager.play(currentSound.value);
audioManager.play(key);
} }
} else if (msg.constructor === MESSAGETYPES.InvalidShot) { } else if (msg.constructor === MESSAGETYPES.InvalidShot) {
if (msg.userId === user.value.id) { if (msg.userId === user.value.id) {
@@ -147,23 +151,8 @@ async function onReceiveMessage(messages = []) {
} else if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) { } else if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
currentRoundEnded.value = true; currentRoundEnded.value = true;
} else if (msg.constructor === MESSAGETYPES.HalfTimeOver) { } else if (msg.constructor === MESSAGETYPES.HalfTimeOver) {
if (props.battleId) { halfTime.value = true;
halfTime.value = true; audioManager.play("中场休息");
audioManager.play("中场休息");
return;
}
if (wait.value !== msg.wait) {
setTimeout(() => {
wait.value = msg.wait;
if (msg.wait === 20) {
halfTime.value = true;
audioManager.play("中场休息", false);
}
if (msg.wait === 0) {
halfTime.value = false;
}
}, 200);
}
} else if (msg.constructor === MESSAGETYPES.MatchOver) { } else if (msg.constructor === MESSAGETYPES.MatchOver) {
audioManager.play("比赛结束"); audioManager.play("比赛结束");
} else if (msg.constructor === MESSAGETYPES.FinalShoot) { } else if (msg.constructor === MESSAGETYPES.FinalShoot) {
@@ -175,17 +164,18 @@ async function onReceiveMessage(messages = []) {
} }
const playSound = (key) => { const playSound = (key) => {
currentSound.value = key;
audioManager.play(key); audioManager.play(key);
}; };
onMounted(() => { onMounted(() => {
uni.$on("update-ramain", resetTimer); uni.$on("update-ramain", updateRemain);
uni.$on("socket-inbox", onReceiveMessage); uni.$on("socket-inbox", onReceiveMessage);
uni.$on("play-sound", playSound); uni.$on("play-sound", playSound);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
uni.$off("update-ramain", resetTimer); uni.$off("update-ramain", updateRemain);
uni.$off("socket-inbox", onReceiveMessage); uni.$off("socket-inbox", onReceiveMessage);
uni.$off("play-sound", playSound); uni.$off("play-sound", playSound);
if (timer.value) clearInterval(timer.value); if (timer.value) clearInterval(timer.value);
@@ -196,7 +186,7 @@ onBeforeUnmount(() => {
<view class="container" :style="{ display: show ? 'block' : 'none' }"> <view class="container" :style="{ display: show ? 'block' : 'none' }">
<view> <view>
<image src="../static/shooter.png" mode="widthFix" /> <image src="../static/shooter.png" mode="widthFix" />
<text>{{ tipContent }}</text> <text>{{ start && remain === 0 ? "时间到!" : tips }}</text>
<button hover-class="none" @click="updateSound"> <button hover-class="none" @click="updateSound">
<image <image
:src="`../static/sound${sound ? '' : '-off'}-yellow.png`" :src="`../static/sound${sound ? '' : '-off'}-yellow.png`"

View File

@@ -27,7 +27,6 @@ watch(
barColor.value = "linear-gradient( 180deg, #FFA0A0 0%, #FF6060 100%)"; barColor.value = "linear-gradient( 180deg, #FFA0A0 0%, #FF6060 100%)";
if (newVal.includes("蓝队")) if (newVal.includes("蓝队"))
barColor.value = "linear-gradient( 180deg, #9AB3FF 0%, #4288FF 100%)"; barColor.value = "linear-gradient( 180deg, #9AB3FF 0%, #4288FF 100%)";
if (newVal.includes("重回")) return;
if (newVal.includes("红队") || newVal.includes("蓝队")) { if (newVal.includes("红队") || newVal.includes("蓝队")) {
if (timer.value) clearInterval(timer.value); if (timer.value) clearInterval(timer.value);
remain.value = props.total; remain.value = props.total;
@@ -42,8 +41,6 @@ watch(
); );
const updateRemain = (value) => { const updateRemain = (value) => {
if (Math.ceil(value) === remain.value || Math.floor(value) === remain.value)
return;
if (timer.value) clearInterval(timer.value); if (timer.value) clearInterval(timer.value);
remain.value = Math.round(value); remain.value = Math.round(value);
timer.value = setInterval(() => { timer.value = setInterval(() => {
@@ -64,11 +61,7 @@ onBeforeUnmount(() => {
<template> <template>
<view class="container"> <view class="container">
<image :src="RoundGoldImages[props.currentRound]" mode="widthFix" /> <image :src="RoundGoldImages[props.currentRound]" mode="widthFix" />
<view <view>
:style="{
justifyContent: tips.includes('红队') ? 'flex-end' : 'flex-start',
}"
>
<view <view
:style="{ :style="{
width: `${(remain / total) * 100}%`, width: `${(remain / total) * 100}%`,
@@ -87,34 +80,33 @@ onBeforeUnmount(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-bottom: 12vw;
} }
.container > image { .container > image {
width: 380rpx; width: 100%;
height: 80rpx; transform: translateY(7px);
transform: translateY(18rpx);
} }
.container > view:last-child { .container > view:last-child {
width: 100%; width: 100%;
text-align: center; text-align: center;
background-color: #444444; background-color: #444444;
border-radius: 20px; border-radius: 20px;
height: 24rpx; font-size: 12px;
height: 15px;
line-height: 15px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: flex;
align-items: center;
} }
.container > view:last-child > view { .container > view:last-child > view {
height: 24rpx; position: absolute;
height: 15px;
border-radius: 15px; border-radius: 15px;
transition: all 1s linear; transition: all 1s linear;
} }
.container > view:last-child > text { .container > view:last-child > text {
font-size: 18rpx; font-size: 10px;
line-height: 15px;
color: #fff; color: #fff;
position: absolute; position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
} }
</style> </style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import BowPower from "@/components/BowPower.vue";
import Avatar from "@/components/Avatar.vue"; import Avatar from "@/components/Avatar.vue";
import audioManager from "@/audioManager"; import audioManager from "@/audioManager";
import { simulShootAPI } from "@/apis"; import { simulShootAPI } from "@/apis";
import { checkConnection } from "@/util";
import { MESSAGETYPES } from "@/constants"; import { MESSAGETYPES } from "@/constants";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
@@ -21,6 +22,7 @@ const props = defineProps({
}, },
}); });
const arrow = ref({}); const arrow = ref({});
const power = ref(0);
const distance = ref(0); const distance = ref(0);
const debugInfo = ref(""); const debugInfo = ref("");
const showsimul = ref(false); const showsimul = ref(false);
@@ -47,6 +49,7 @@ async function onReceiveMessage(messages = []) {
messages.forEach((msg) => { messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) { if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
arrow.value = msg.target; arrow.value = msg.target;
power.value = msg.target.battery;
distance.value = Number((msg.target.dst / 100).toFixed(2)); distance.value = Number((msg.target.dst / 100).toFixed(2));
debugInfo.value = msg.target; debugInfo.value = msg.target;
audioManager.play("距离合格"); audioManager.play("距离合格");
@@ -62,6 +65,7 @@ const simulShoot = async () => {
}; };
onMounted(() => { onMounted(() => {
checkConnection();
uni.$on("socket-inbox", onReceiveMessage); uni.$on("socket-inbox", onReceiveMessage);
const accountInfo = uni.getAccountInfoSync(); const accountInfo = uni.getAccountInfoSync();
const envVersion = accountInfo.miniProgram.envVersion; const envVersion = accountInfo.miniProgram.envVersion;
@@ -76,9 +80,18 @@ onBeforeUnmount(() => {
<template> <template>
<view class="container"> <view class="container">
<Guide v-show="guide"> <Guide v-show="guide">
<view class="guide-tips"> <view
<text>请确保站距达到5米</text> :style="{
<text>低于5米的射箭无效</text> display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: '10px',
}"
>
<view :style="{ display: 'flex', flexDirection: 'column' }">
<text :style="{ color: '#fed847' }">请确保站距达到5米</text>
<text>低于5米的射箭无效</text>
</view>
</view> </view>
</Guide> </Guide>
<view class="test-area"> <view class="test-area">
@@ -107,7 +120,7 @@ onBeforeUnmount(() => {
</view> </view>
<view class="user-row"> <view class="user-row">
<Avatar :src="user.avatar" :size="35" /> <Avatar :src="user.avatar" :size="35" />
<BowPower /> <BowPower :power="power" />
</view> </view>
</view> </view>
<view v-if="isBattle" class="ready-timer"> <view v-if="isBattle" class="ready-timer">

View File

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

View File

@@ -24,17 +24,15 @@ export const MESSAGETYPES = {
LvlUpdate: 3958625354, LvlUpdate: 3958625354,
TeamUpdate: 4168086616, TeamUpdate: 4168086616,
InvalidShot: 4168086617, InvalidShot: 4168086617,
Calibration: 4168086625,
DeviceOnline: 4168086626,
DeviceOffline: 4168086627,
SomeoneIsReady: 4168086628,
}; };
export const topThreeColors = ["#FFD947", "#D2D2D2", "#FFA515"]; export const topThreeColors = ["#FFD947", "#D2D2D2", "#FFA515"];
export const getMessageTypeName = (id) => { export const getMessageTypeName = (id) => {
for (let key in MESSAGETYPES) { for (let key in MESSAGETYPES) {
if (MESSAGETYPES[key] === id) return key; if (MESSAGETYPES[key] === id) {
return key;
}
} }
return null; return null;
}; };

View File

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

View File

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

View File

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

View File

@@ -18,18 +18,7 @@ const totalPoints = ref(0);
const rank = ref(0); const rank = ref(0);
function exit() { function exit() {
const battleInfo = uni.getStorageSync("last-battle"); uni.navigateBack();
if (battleInfo && battleInfo.roomId) {
uni.redirectTo({
url: `/pages/battle-room?roomNumber=${battleInfo.roomId}`,
});
} else if (data.value.roomId) {
uni.redirectTo({
url: `/pages/battle-room?roomNumber=${data.value.roomId}`,
});
} else {
uni.navigateBack();
}
} }
onLoad(async (options) => { onLoad(async (options) => {
@@ -262,9 +251,7 @@ const checkBowData = () => {
src="../static/champ3.png" src="../static/champ3.png"
mode="widthFix" mode="widthFix"
/> />
<view v-if="player.rank > 3" class="view-crown">{{ <view v-if="index > 2" class="view-crown">{{ index + 1 }}</view>
player.rank
}}</view>
<Avatar <Avatar
:src="player.avatar" :src="player.avatar"
:size="36" :size="36"
@@ -272,7 +259,7 @@ const checkBowData = () => {
/> />
<view class="player-title"> <view class="player-title">
<text class="truncate">{{ player.name }}</text> <text class="truncate">{{ player.name }}</text>
<text>{{ getLvlName(player.rank_lvl) }}</text> <text>{{ getLvlName(player.totalScore) }}</text>
</view> </view>
<text <text
><text :style="{ color: '#fff' }">{{ player.totalRings }}</text> ><text :style="{ color: '#fff' }">{{ player.totalRings }}</text>
@@ -321,7 +308,7 @@ const checkBowData = () => {
</text> </text>
<view class="op-btn"> <view class="op-btn">
<view @click="checkBowData">查看成绩</view> <view @click="checkBowData">查看成绩</view>
<view @click="exit">返回</view> <view @click="exit">退出</view>
</view> </view>
<UserUpgrade /> <UserUpgrade />
</view> </view>
@@ -431,7 +418,6 @@ const checkBowData = () => {
border-radius: 20px; border-radius: 20px;
padding: 10px 0; padding: 10px 0;
text-align: center; text-align: center;
color: #000;
} }
.op-btn > view:last-child { .op-btn > view:last-child {
color: #fff; color: #fff;

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue"; import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import { onLoad, onShareAppMessage } from "@dcloudio/uni-app"; import { onLoad, onShow, onHide } 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";
import Guide from "@/components/Guide.vue"; import Guide from "@/components/Guide.vue";
@@ -13,15 +13,12 @@ import {
exitRoomAPI, exitRoomAPI,
startRoomAPI, startRoomAPI,
chooseTeamAPI, chooseTeamAPI,
getReadyAPI,
} from "@/apis"; } from "@/apis";
import { MESSAGETYPES } from "@/constants"; import { MESSAGETYPES } from "@/constants";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user } = storeToRefs(store); const { user } = storeToRefs(store);
const room = ref({}); const room = ref({});
const roomNumber = ref(""); const roomNumber = ref("");
const owner = ref({}); const owner = ref({});
@@ -33,27 +30,14 @@ const redTeam = ref([]);
const showModal = ref(false); const showModal = ref(false);
const battleType = ref(0); const battleType = ref(0);
const refreshRoomTimer = ref(null); const refreshRoomTimer = ref(null);
const ready = ref(false);
const allReady = ref(false);
const timer = ref(null);
async function refreshRoomData() { async function refreshRoomData() {
if (!roomNumber.value) return;
const result = await getRoomAPI(roomNumber.value); const result = await getRoomAPI(roomNumber.value);
if (result.started) return; if (result.started) return;
room.value = result; room.value = result;
battleType.value = result.battleType; battleType.value = result.battleType;
const members = result.members || []; (result.members || []).some((m) => {
if (members.length === result.count) {
allReady.value = members.every((m) => !!m.userInfo.state);
}
members.some((m) => {
if (m.userInfo.id === user.value.id) {
ready.value = !!m.userInfo.state;
return true;
}
return false;
});
members.some((m) => {
if (m.userInfo.id === result.creator) { if (m.userInfo.id === result.creator) {
owner.value = { owner.value = {
id: m.userInfo.id, id: m.userInfo.id,
@@ -93,19 +77,13 @@ async function refreshRoomData() {
players.value = new Array(result.count).fill({}); players.value = new Array(result.count).fill({});
refreshMembers(result.members); refreshMembers(result.members);
} }
if (timer.value) clearInterval(timer.value);
timer.value = setTimeout(refreshRoomData, 1000);
} }
const startGame = async () => { const startGame = async () => {
const result = await startRoomAPI(room.value.number); const result = await startRoomAPI(room.value.number);
}; };
const getReady = async () => { const refreshMembers = (members) => {
await getReadyAPI(roomNumber.value);
};
const refreshMembers = (members = []) => {
blueTeam.value = []; blueTeam.value = [];
redTeam.value = []; redTeam.value = [];
members.forEach((m, index) => { members.forEach((m, index) => {
@@ -160,7 +138,8 @@ async function onReceiveMessage(messages = []) {
}); });
} }
} }
} else if (msg.constructor === MESSAGETYPES.UserExitRoom) { }
if (msg.constructor === MESSAGETYPES.UserExitRoom) {
if (battleType.value === 1) { if (battleType.value === 1) {
if (msg.userId === room.value.creator) { if (msg.userId === room.value.creator) {
owner.value = { owner.value = {
@@ -178,22 +157,25 @@ async function onReceiveMessage(messages = []) {
if (msg.room && msg.room.members) { if (msg.room && msg.room.members) {
refreshMembers(msg.room.members); refreshMembers(msg.room.members);
} }
} else if (msg.constructor === MESSAGETYPES.TeamUpdate) { }
if (msg.constructor === MESSAGETYPES.TeamUpdate) {
if (msg.room && msg.room.members) { if (msg.room && msg.room.members) {
refreshMembers(msg.room.members); refreshMembers(msg.room.members);
} }
} else if (msg.constructor === MESSAGETYPES.RoomDestroy) { }
if (msg.constructor === MESSAGETYPES.RoomDestroy) {
uni.showToast({ uni.showToast({
title: "房间已解散", title: "房间已解散",
icon: "none", icon: "none",
}); });
roomNumber.value = "";
setTimeout(() => { setTimeout(() => {
uni.navigateBack(); uni.navigateBack();
}, 1000); }, 1000);
} else if (msg.constructor === MESSAGETYPES.SomeoneIsReady) {
refreshRoomData();
} }
} else if (msg.constructor === MESSAGETYPES.WaitForAllReady) { }
if (msg.constructor === MESSAGETYPES.WaitForAllReady) {
roomNumber.value = "";
if (msg.groupUserStatus) { if (msg.groupUserStatus) {
uni.setStorageSync("red-team", msg.groupUserStatus.redTeam); uni.setStorageSync("red-team", msg.groupUserStatus.redTeam);
uni.setStorageSync("blue-team", msg.groupUserStatus.blueTeam); uni.setStorageSync("blue-team", msg.groupUserStatus.blueTeam);
@@ -202,8 +184,6 @@ async function onReceiveMessage(messages = []) {
...msg.groupUserStatus.blueTeam, ...msg.groupUserStatus.blueTeam,
]); ]);
uni.removeStorageSync("current-battle"); uni.removeStorageSync("current-battle");
// 避免离开页面,触发退出房间
roomNumber.value = "";
if (msg.groupUserStatus.config.mode == 1) { if (msg.groupUserStatus.config.mode == 1) {
uni.redirectTo({ uni.redirectTo({
url: `/pages/team-battle?battleId=${msg.id}&gameMode=1`, url: `/pages/team-battle?battleId=${msg.id}&gameMode=1`,
@@ -246,18 +226,15 @@ const setClipboardData = () => {
}); });
}; };
onShareAppMessage(() => { const onBack = () => {
return { showModal.value = true;
title: "邀请您进入房间对战", };
path: "/pages/friend-battle?roomID=" + roomNumber.value,
imageUrl: "",
};
});
onLoad(async (options) => { onLoad(async (options) => {
if (options.roomNumber) { if (options.roomNumber) {
roomNumber.value = options.roomNumber; roomNumber.value = options.roomNumber;
refreshRoomData(); refreshRoomData();
refreshRoomTimer.value = setInterval(refreshRoomData, 2000);
} }
}); });
@@ -274,25 +251,30 @@ onBeforeUnmount(() => {
keepScreenOn: false, keepScreenOn: false,
}); });
uni.$off("socket-inbox", onReceiveMessage); uni.$off("socket-inbox", onReceiveMessage);
if (roomNumber.value) exitRoomAPI(roomNumber.value); if (roomNumber.value && owner.value.id !== user.value.id) {
if (timer.value) clearInterval(timer.value); exitRoomAPI(roomNumber.value);
timer.value = null; }
}); });
onShow(async () => {
refreshRoomData();
});
onHide(() => {});
</script> </script>
<template> <template>
<Container :title="`好友约战 - ${roomNumber}`"> <Container :title="`好友约战 - ${roomNumber}`" :onBack="onBack">
<view class="standby-phase"> <view class="standby-phase">
<Guide> <Guide>
<view class="battle-guide"> <view class="battle-guide">
<view class="guide-tips"> <view :style="{ display: 'flex', flexDirection: 'column' }">
<text>弓箭手们人都到齐了吗?</text> <text :style="{ color: '#fed847' }">弓箭手们人都到齐了吗?</text>
<text v-if="battleType === 1">{{ <text v-if="battleType === 1">1v1比赛即将开始! </text>
`${room.count / 2}v${room.count / 2}比赛即将开始!` <text v-if="battleType === 3">2v2比赛即将开始! </text>
}}</text> <text v-if="battleType === 4">3v3比赛即将开始! </text>
<text v-if="battleType === 2">大乱斗即将开始! </text> <text v-if="battleType === 2">大乱斗即将开始! </text>
</view> </view>
<button hover-class="none" open-type="share">邀请</button> <view @click="setClipboardData">复制房间号</view>
</view> </view>
</Guide> </Guide>
<view v-if="battleType === 1 && room.count === 2" class="team-mode"> <view v-if="battleType === 1 && room.count === 2" class="team-mode">
@@ -346,16 +328,8 @@ onBeforeUnmount(() => {
src="https://static.shelingxingqiu.com/attachment/2025-08-13/dc0x1p59iab6cvbhqc.png" src="https://static.shelingxingqiu.com/attachment/2025-08-13/dc0x1p59iab6cvbhqc.png"
mode="widthFix" mode="widthFix"
/> />
<image <image v-if="room.count === 4" src="../static/title-2v2.png" mode="widthFix" />
v-if="room.count === 4" <image v-if="room.count === 6" src="../static/title-3v3.png" mode="widthFix" />
src="../static/title-2v2.png"
mode="widthFix"
/>
<image
v-if="room.count === 6"
src="../static/title-3v3.png"
mode="widthFix"
/>
<view> <view>
<view v-for="(item, index) in players" :key="index"> <view v-for="(item, index) in players" :key="index">
<Avatar v-if="item.id" :src="item.avatar" :size="36" /> <Avatar v-if="item.id" :src="item.avatar" :size="36" />
@@ -407,7 +381,7 @@ onBeforeUnmount(() => {
</view> </view>
</block> </block>
<view> <view>
<!-- <SButton <SButton
v-if="user.id === owner.id && battleType === 1 && room.count === 2" v-if="user.id === owner.id && battleType === 1 && room.count === 2"
:disabled="!opponent.id" :disabled="!opponent.id"
:onClick="startGame" :onClick="startGame"
@@ -427,18 +401,11 @@ onBeforeUnmount(() => {
:onClick="startGame" :onClick="startGame"
>开启对局</SButton >开启对局</SButton
> >
<SButton v-if="user.id !== owner.id" disabled>等待房主开启对战</SButton> --> <SButton v-if="user.id !== owner.id" disabled>等待房主开启对战</SButton>
<SButton :disabled="ready" :onClick="getReady">{{ <text class="tips">创建者点击下一步所有人即可进入游戏</text>
allReady.value ? "即将进入对局..." : "我准备好了"
}}</SButton>
<!-- <text class="tips">创建者点击下一步所有人即可进入游戏</text> -->
</view> </view>
</view> </view>
<!-- <SModal <SModal :show="showModal" :onClose="() => (showModal = false)">
:show="showModal"
:onClose="() => (showModal = false)"
height="520rpx"
>
<view class="btns"> <view class="btns">
<SButton :onClick="exitRoom" width="200px" :rounded="20"> <SButton :onClick="exitRoom" width="200px" :rounded="20">
暂时离开 暂时离开
@@ -450,7 +417,7 @@ onBeforeUnmount(() => {
</SButton> </SButton>
</block> </block>
</view> </view>
</SModal> --> </SModal>
</Container> </Container>
</template> </template>
@@ -527,7 +494,7 @@ onBeforeUnmount(() => {
background-color: #fed847; background-color: #fed847;
font-size: 8px; font-size: 8px;
border-radius: 10px; border-radius: 10px;
padding: 2rpx 10rpx; padding: 2px 5px;
} }
.team-mode > view > image:nth-child(2) { .team-mode > view > image:nth-child(2) {
width: 120px; width: 120px;
@@ -558,14 +525,13 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.battle-guide > button:last-child { .battle-guide > view:last-child {
color: #fed847; color: #fed847;
border: 1px solid #fed847; border: 1px solid #fed847;
margin-right: 10px; margin-right: 10px;
padding: 5px 12px; padding: 5px 12px;
border-radius: 20px; border-radius: 20px;
position: relative; position: relative;
font-size: 28rpx;
} }
.all-players { .all-players {
position: relative; position: relative;

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -1,54 +1,51 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app"; 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 SButton from "@/components/SButton.vue"; import SButton from "@/components/SButton.vue";
import SModal from "@/components/SModal.vue"; import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue";
import CreateRoom from "@/components/CreateRoom.vue"; import CreateRoom from "@/components/CreateRoom.vue";
import Avatar from "@/components/Avatar.vue"; import Avatar from "@/components/Avatar.vue";
import { getRoomAPI, joinRoomAPI, isGamingAPI, getBattleDataAPI } from "@/apis";
import { getRoomAPI, joinRoomAPI, getBattleDataAPI } from "@/apis";
import { debounce, canEenter } from "@/util";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user, device, online, game } = storeToRefs(store); const { user } = storeToRefs(store);
import { debounce } from "@/util";
const showModal = ref(false); const showModal = ref(false);
const showSignin = ref(false);
const warnning = ref(""); const warnning = ref("");
const roomNumber = ref(""); const roomNumber = ref("");
const data = ref({}); const data = ref({});
const roomID = ref("");
const enterRoom = debounce(async (number) => { const enterRoom = debounce(async () => {
if (!canEenter(user.value, device.value, online.value)) return; const isGaming = await isGamingAPI();
if (game.value.inBattle) { if (isGaming) {
uni.$showHint(1); uni.$showHint(1);
return; return;
} }
if (!number) { if (!roomNumber.value) {
warnning.value = "请输入房间号"; warnning.value = "请输入房间号";
showModal.value = true; showModal.value = true;
} else { } else {
const room = await getRoomAPI(number); const room = await getRoomAPI(roomNumber.value);
if (room.number) { if (room.number) {
const alreadyIn = room.members.find( const alreadyIn = room.members.find(
(item) => item.userInfo.id === user.value.id (item) => item.userInfo.id === user.value.id
); );
if (!alreadyIn) { if (!alreadyIn) {
const result = await joinRoomAPI(number); const result = await joinRoomAPI(roomNumber.value);
if (result.full) { if (result.full) {
warnning.value = "房间已满员"; warnning.value = "房间已满员";
showModal.value = true; showModal.value = true;
return; return;
} }
} }
roomNumber.value = "";
showModal.value = false;
uni.navigateTo({ uni.navigateTo({
url: "/pages/battle-room?roomNumber=" + number, url: `/pages/battle-room?roomNumber=${room.number}`,
}); });
} else { } else {
warnning.value = room.started ? "该房间对战已开始,无法加入" : "查无此房"; warnning.value = room.started ? "该房间对战已开始,无法加入" : "查无此房";
@@ -57,32 +54,23 @@ const enterRoom = debounce(async (number) => {
} }
}); });
const onCreateRoom = async () => { const onCreateRoom = async () => {
if (!canEenter(user.value, device.value, online.value)) return; const isGaming = await isGamingAPI();
if (isGaming) {
uni.$showHint(1);
return;
}
warnning.value = ""; warnning.value = "";
showModal.value = true; showModal.value = true;
}; };
const onSignin = () => {
if (roomID.value && user.value.id) enterRoom(roomID.value);
showSignin.value = false;
};
onShow(async () => { onShow(async () => {
if (user.value.id) { const result = await getBattleDataAPI();
const result = await getBattleDataAPI(); data.value = result;
data.value = result;
}
});
onLoad(async (options) => {
if (options.roomID) {
roomID.value = options.roomID;
if (user.value.id) enterRoom(options.roomID);
else showSignin.value = true;
}
}); });
</script> </script>
<template> <template>
<Container title="好友约战" :showBackToGame="true"> <Container title="好友约战" :showBackToGame="true">
<view :style="{ width: '100%', height: '100%' }"> <view :style="{ width: '100%' }">
<Guide> <Guide>
<view class="guide-tips"> <view class="guide-tips">
<text>约上朋友开几局欢乐多不寂寞</text> <text>约上朋友开几局欢乐多不寂寞</text>
@@ -137,7 +125,7 @@ onLoad(async (options) => {
v-model="roomNumber" v-model="roomNumber"
placeholder-style="color: #ccc" placeholder-style="color: #ccc"
/> />
<view @click="enterRoom(roomNumber)">进入房间</view> <view @click="enterRoom">进入房间</view>
</view> </view>
</view> </view>
<view class="create-room"> <view class="create-room">
@@ -159,17 +147,12 @@ onLoad(async (options) => {
</SButton> </SButton>
</view> </view>
</view> </view>
<SModal <SModal :show="showModal" :onClose="() => (showModal = false)">
:show="showModal"
:onClose="() => (showModal = false)"
height="520rpx"
>
<view v-if="warnning" class="warnning"> <view v-if="warnning" class="warnning">
{{ warnning }} {{ warnning }}
</view> </view>
<CreateRoom v-if="!warnning" :onConfirm="() => (showModal = false)" /> <CreateRoom v-if="!warnning" :onConfirm="() => (showModal = false)" />
</SModal> </SModal>
<Signin :show="showSignin" :onClose="onSignin" />
</view> </view>
</Container> </Container>
</template> </template>
@@ -203,7 +186,7 @@ onLoad(async (options) => {
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
height: 40px; height: 40px;
color: #000; color: #fff;
} }
.founded-room > view > view { .founded-room > view > view {
background-color: #fed847; background-color: #fed847;
@@ -329,7 +312,6 @@ onLoad(async (options) => {
} }
.stars > image { .stars > image {
width: 4vw; width: 4vw;
height: 4vw;
margin: 0 1px; margin: 0 1px;
} }
</style> </style>

View File

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

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

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

View File

@@ -5,48 +5,50 @@ import Container from "@/components/Container.vue";
import AppFooter from "@/components/AppFooter.vue"; import AppFooter from "@/components/AppFooter.vue";
import AppBackground from "@/components/AppBackground.vue"; import AppBackground from "@/components/AppBackground.vue";
import UserHeader from "@/components/UserHeader.vue"; import UserHeader from "@/components/UserHeader.vue";
import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue"; import Signin from "@/components/Signin.vue";
import BubbleTip from "@/components/BubbleTip.vue"; import BubbleTip from "@/components/BubbleTip.vue";
import { import {
getAppConfig, getAppConfig,
getRankListAPI, getRankListAPI,
getHomeData, getHomeData,
getMyDevicesAPI, getMyDevicesAPI,
getDeviceBatteryAPI,
} from "@/apis"; } from "@/apis";
import { topThreeColors } from "@/constants"; import { topThreeColors } from "@/constants";
import { canEenter } from "@/util";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { const { updateConfig, updateUser, updateDevice, updateRank, getLvlName } =
updateConfig, store;
updateUser, // 使用storeToRefs用于UI里显示保持响应性
updateDevice, const { user, device, rankData } = storeToRefs(store);
updateRank,
getLvlName,
updateOnline,
} = store;
const { user, device, rankData, online, game } = storeToRefs(store);
const showModal = ref(false); const showModal = ref(false);
const showGuide = ref(false); const showGuide = ref(false);
const toPage = async (path) => { const toPage = (path) => {
if (!user.value.id) { if (!user.value.id) {
showModal.value = true; showModal.value = true;
return; return;
} }
if (path === "/pages/first-try") { if (
if (canEenter(user.value, device.value, online.value, path)) { "/pages/first-try,/pages/practise,/pages/friend-battle".indexOf(path) !== -1
await uni.$checkAudio(); ) {
} else { if (!device.value.deviceId) {
return; return uni.showToast({
title: "请先绑定设备",
icon: "none",
});
}
if ("/pages/first-try".indexOf(path) === -1 && !user.value.trio) {
return uni.showToast({
title: "请先完成新手试炼",
icon: "none",
});
} }
} }
uni.navigateTo({ url: path }); uni.navigateTo({
url: path,
});
}; };
const toRankListPage = () => { const toRankListPage = () => {
@@ -74,13 +76,6 @@ onShow(async () => {
console.log("首页数据:", homeData); console.log("首页数据:", homeData);
if (homeData.user) { if (homeData.user) {
updateUser(homeData.user); updateUser(homeData.user);
if ("823,209,293,257".indexOf(homeData.user.id) !== -1) {
const show = uni.getStorageSync("show-the-user");
if (!show) {
showTheUser.value = true;
uni.setStorageSync("show-the-user", true);
}
}
if (homeData.user.trio <= 0) { if (homeData.user.trio <= 0) {
showGuide.value = true; showGuide.value = true;
setTimeout(() => { setTimeout(() => {
@@ -93,8 +88,6 @@ onShow(async () => {
devices.bindings[0].deviceId, devices.bindings[0].deviceId,
devices.bindings[0].deviceName devices.bindings[0].deviceName
); );
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
} }
} }
} }
@@ -106,6 +99,13 @@ onMounted(async () => {
console.log("全局配置:", config); console.log("全局配置:", config);
}); });
const comingSoon = () => {
uni.showToast({
title: "敬请期待",
icon: "none",
});
};
onShareAppMessage(() => { onShareAppMessage(() => {
return { return {
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题 title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
@@ -127,39 +127,32 @@ onShareTimeline(() => {
<template> <template>
<Container :isHome="true" :showBackToGame="true"> <Container :isHome="true" :showBackToGame="true">
<view class="container"> <view class="container">
<view class="top-theme">
<image
src="https://static.shelingxingqiu.com/attachment/2025-12-31/dfc9dxrq4xn7e6y2pp.png"
mode="widthFix"
/>
</view>
<UserHeader showRank :onSignin="() => (showModal = true)" /> <UserHeader showRank :onSignin="() => (showModal = true)" />
<view :style="{ padding: '12px 10px' }"> <view :style="{ padding: '12px 10px' }">
<view class="feature-grid"> <view class="feature-grid">
<view class="bow-card"> <view class="bow-card">
<image <image
v-if="online"
src="https://static.shelingxingqiu.com/attachment/2025-08-07/dbvt1o6dvhr2rop3kn.webp" src="https://static.shelingxingqiu.com/attachment/2025-08-07/dbvt1o6dvhr2rop3kn.webp"
mode="widthFix" mode="widthFix"
@click="() => toPage('/pages/my-device')" @click="() => toPage('/pages/my-device')"
/> />
<image <text v-if="!user.id">我的弓箭</text>
v-else <text v-if="user.id && !device.deviceId">连接智能弓箭</text>
src="https://static.shelingxingqiu.com/attachment/2026-01-04/dffohwtk1gwh0xfa6h.png" <text
mode="widthFix" v-if="user.id && device.deviceId"
@click="() => toPage('/pages/my-device')" class="truncate"
/> :style="{ width: '90%', textAlign: 'center' }"
<block v-if="user.id"> >{{ device.deviceName }}</text
<text v-if="!device.deviceId">绑定我的智能弓</text> >
<text v-else-if="!online">设备离线</text>
<text v-else-if="online">设备在线</text>
</block>
<image <image
src="../static/first-try.png" src="../static/first-try.png"
mode="widthFix" mode="widthFix"
@click="() => toPage('/pages/first-try')" @click="() => toPage('/pages/first-try')"
/> />
<BubbleTip v-if="showGuide" :location="{ top: '60%', left: '47%' }"> <BubbleTip
v-if="showGuide"
:location="{ top: '60%', left: '40%', fontSize: '14px' }"
>
<text>新人必刷</text> <text>新人必刷</text>
<text>快来报到吧~</text> <text>快来报到吧~</text>
</BubbleTip> </BubbleTip>
@@ -224,7 +217,7 @@ onShareTimeline(() => {
<view> <view>
<text>段位</text> <text>段位</text>
<text>{{ <text>{{
user.rankLvl ? getLvlName(user.rankLvl) : "暂无" user.scores ? getLvlName(user.scores) : "暂无"
}}</text> }}</text>
</view> </view>
<view> <view>
@@ -241,9 +234,64 @@ onShareTimeline(() => {
</view> </view>
</view> </view>
</view> </view>
<!-- <view class="region-stats">
<view
v-for="(region, index) in [
{ name: '广东', score: 4291 },
{ name: '湖南', score: 3095 },
{ name: '内蒙', score: 2342 },
{ name: '海南', score: 1812 },
{ name: '四川', score: 1293 },
]"
:key="index"
class="region-item"
@click="comingSoon"
>
<image src="../static/region-bg.png" mode="widthFix" />
<image
v-if="index === 0"
src="../static/region-1.png"
mode="widthFix"
/>
<image
v-if="index === 1"
src="../static/region-2.png"
mode="widthFix"
/>
<image
v-if="index === 2"
src="../static/region-3.png"
mode="widthFix"
/>
<image
v-if="index === 3"
src="../static/region-4.png"
mode="widthFix"
/>
<image
v-if="index === 4"
src="../static/region-5.png"
mode="widthFix"
/>
<text>{{ region.name }}</text>
<view>
<text :style="{ color: '#fff', marginRight: '2px' }">{{
region.score
}}</text>
<text>分</text>
</view>
</view>
<view class="region-more" @click="comingSoon">
<image src="../static/region-more.png" mode="widthFix" />
<text>...</text>
<text>更多</text>
</view>
</view> -->
</view> </view>
</view> </view>
<Signin :show="showModal" :onClose="() => (showModal = false)" /> <SModal :show="showModal" :onClose="() => (showModal = false)">
<Signin :onClose="() => (showModal = false)" />
</SModal>
</view> </view>
<AppFooter /> <AppFooter />
</Container> </Container>
@@ -252,7 +300,6 @@ onShareTimeline(() => {
<style scoped> <style scoped>
.container { .container {
width: 100%; width: 100%;
height: calc(100% - 120px);
} }
.feature-grid { .feature-grid {
@@ -269,8 +316,6 @@ onShareTimeline(() => {
.bow-card { .bow-card {
width: 50%; width: 50%;
border-radius: 25rpx;
overflow: hidden;
} }
.feature-grid > view > image { .feature-grid > view > image {
@@ -279,7 +324,7 @@ onShareTimeline(() => {
.bow-card > text { .bow-card > text {
position: absolute; position: absolute;
top: 66%; top: 65%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
white-space: nowrap; white-space: nowrap;
@@ -394,6 +439,60 @@ onShareTimeline(() => {
margin-left: 2px; margin-left: 2px;
color: #fff; color: #fff;
} }
.region-stats {
display: flex;
grid-template-columns: repeat(6, 1fr);
margin-top: 20px;
justify-content: space-between;
}
.region-item,
.region-more {
border-radius: 10px;
text-align: center;
position: relative;
width: 13vw;
height: 13vw;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #c5c5c5;
font-size: 12px;
}
.region-item > text {
margin-top: 10px;
}
.region-more {
width: 8vw;
height: 13vw;
}
.region-item > image:first-child,
.region-more > image:first-child {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: -1;
}
.region-item > image:nth-of-type(2) {
position: absolute;
top: 0;
left: 0;
width: 18px;
}
.region-item > view:last-child {
display: flex;
justify-content: center;
font-size: 10px;
}
.region-more > text:first-of-type {
font-size: 30px;
line-height: 20px;
margin-bottom: 5px;
}
.my-data { .my-data {
display: flex; display: flex;
margin-top: 20px; margin-top: 20px;
@@ -430,17 +529,4 @@ onShareTimeline(() => {
line-height: 25px; line-height: 25px;
font-weight: 500; font-weight: 500;
} }
.top-theme {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 60px;
z-index: -1;
}
.top-theme > image {
width: 300rpx;
transform: translate(-4%, -14%);
}
</style> </style>

View File

@@ -4,6 +4,8 @@ import { onLoad } 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 Avatar from "@/components/Avatar.vue"; import Avatar from "@/components/Avatar.vue";
// import TeamResult from "@/components/TeamResult.vue";
// import MeleeResult from "@/components/MeleeResult.vue";
import PlayerScore2 from "@/components/PlayerScore2.vue"; import PlayerScore2 from "@/components/PlayerScore2.vue";
import { getGameAPI } from "@/apis"; import { getGameAPI } from "@/apis";
@@ -15,6 +17,7 @@ const battleId = ref("");
const data = ref({ const data = ref({
players: [], players: [],
}); });
// const show = ref(false);
onLoad(async (options) => { onLoad(async (options) => {
if (options.id) { if (options.id) {
@@ -232,7 +235,7 @@ const checkBowData = () => {
borderColor: '#FF6767', borderColor: '#FF6767',
transform: `translateX(-${index * 15}px)`, transform: `translateX(-${index * 15}px)`,
}" }"
:src="src || '../static/user-icon.png'" :src="src"
:key="index" :key="index"
mode="widthFix" mode="widthFix"
/> />
@@ -252,6 +255,18 @@ const checkBowData = () => {
</view> </view>
<view :style="{ height: '20px' }"></view> <view :style="{ height: '20px' }"></view>
</view> </view>
<!-- <TeamResult
v-if="data.mode === 1"
:show="show"
:onClose="() => (show = false)"
:data="data"
/>
<MeleeResult
v-if="data.mode === 2"
:show="show"
:onClose="() => (show = false)"
:data="data"
/> -->
</Container> </Container>
</template> </template>

View File

@@ -10,8 +10,7 @@ import SButton from "@/components/SButton.vue";
import Avatar from "@/components/Avatar.vue"; import Avatar from "@/components/Avatar.vue";
import ScreenHint from "@/components/ScreenHint.vue"; import ScreenHint from "@/components/ScreenHint.vue";
import TestDistance from "@/components/TestDistance.vue"; import TestDistance from "@/components/TestDistance.vue";
import audioManager from "@/audioManager"; import { getCurrentGameAPI } from "@/apis";
import { getCurrentGameAPI, laserCloseAPI } from "@/apis";
import { isGameEnded } from "@/util"; import { isGameEnded } from "@/util";
import { MESSAGETYPES } from "@/constants"; import { MESSAGETYPES } from "@/constants";
import useStore from "@/store"; import useStore from "@/store";
@@ -23,6 +22,7 @@ const start = ref(false);
const startCount = ref(true); const startCount = ref(true);
const battleId = ref(""); const battleId = ref("");
const currentRound = ref(1); const currentRound = ref(1);
const power = ref(0);
const scores = ref([]); const scores = ref([]);
const tips = ref("即将开始..."); const tips = ref("即将开始...");
const players = ref([]); const players = ref([]);
@@ -48,7 +48,6 @@ watch(
function recoverData(battleInfo) { function recoverData(battleInfo) {
uni.removeStorageSync("last-awake-time"); uni.removeStorageSync("last-awake-time");
// 注释用于测试
battleId.value = battleInfo.id; battleId.value = battleInfo.id;
players.value = [...battleInfo.blueTeam, ...battleInfo.redTeam]; players.value = [...battleInfo.blueTeam, ...battleInfo.redTeam];
players.value.forEach((p) => { players.value.forEach((p) => {
@@ -104,12 +103,6 @@ onLoad(async (options) => {
setTimeout(getCurrentGameAPI, 2000); setTimeout(getCurrentGameAPI, 2000);
} }
} }
uni.enableAlertBeforeUnload({
message: "离开比赛可能导致比赛失败,是否继续?",
success: (res) => {
console.log("已启用离开提示");
},
});
}); });
async function onReceiveMessage(messages = []) { async function onReceiveMessage(messages = []) {
@@ -127,6 +120,7 @@ async function onReceiveMessage(messages = []) {
if (!start.value) getCurrentGameAPI(); if (!start.value) getCurrentGameAPI();
if (msg.userId === user.value.id) { if (msg.userId === user.value.id) {
scores.value.push({ ...msg.target }); scores.value.push({ ...msg.target });
power.value = msg.target.battery;
} }
playersScores.value[msg.userId].push({ ...msg.target }); playersScores.value[msg.userId].push({ ...msg.target });
} }
@@ -157,19 +151,20 @@ async function onReceiveMessage(messages = []) {
} }
}); });
} }
onMounted(async () => { const onBack = () => {
uni.$showHint(2);
};
onMounted(() => {
uni.setKeepScreenOn({ uni.setKeepScreenOn({
keepScreenOn: true, keepScreenOn: true,
}); });
uni.$on("socket-inbox", onReceiveMessage); uni.$on("socket-inbox", onReceiveMessage);
await laserCloseAPI();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
uni.setKeepScreenOn({ uni.setKeepScreenOn({
keepScreenOn: false, keepScreenOn: false,
}); });
uni.$off("socket-inbox", onReceiveMessage); uni.$off("socket-inbox", onReceiveMessage);
audioManager.stopAll();
}); });
const refreshTimer = ref(null); const refreshTimer = ref(null);
onShow(async () => { onShow(async () => {
@@ -194,7 +189,7 @@ onHide(() => {
</script> </script>
<template> <template>
<Container :title="title" :bgType="1"> <Container :title="title" :bgType="1" :onBack="onBack">
<view class="container"> <view class="container">
<BattleHeader v-if="!start" :players="players" /> <BattleHeader v-if="!start" :players="players" />
<TestDistance v-if="!start" :guide="false" :isBattle="true" /> <TestDistance v-if="!start" :guide="false" :isBattle="true" />
@@ -208,7 +203,7 @@ onHide(() => {
/> />
<view v-if="start" class="user-row"> <view v-if="start" class="user-row">
<Avatar :src="user.avatar" :size="35" /> <Avatar :src="user.avatar" :size="35" />
<BowPower /> <BowPower :power="power" />
</view> </view>
<BowTarget <BowTarget
v-if="start" v-if="start"
@@ -222,7 +217,8 @@ onHide(() => {
v-if="start" v-if="start"
v-for="(player, index) in playersSorted" v-for="(player, index) in playersSorted"
:key="index" :key="index"
:player="player" :name="player.name"
:avatar="player.avatar"
:scores="playersScores[player.id] || []" :scores="playersScores[player.id] || []"
/> />
</view> </view>

View File

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

View File

@@ -55,7 +55,7 @@ const onPractiseLoading = async (page) => {
</script> </script>
<template> <template>
<Container title="我的成长脚印" :scroll="false"> <Container title="我的成长脚印" overflow="hidden">
<view class="tabs"> <view class="tabs">
<view <view
v-for="(rankType, index) in ['排位赛', '好友约战', '个人练习']" v-for="(rankType, index) in ['排位赛', '好友约战', '个人练习']"
@@ -70,74 +70,66 @@ const onPractiseLoading = async (page) => {
</view> </view>
</view> </view>
<view class="contents"> <view class="contents">
<swiper <ScrollList :show="selectedIndex === 0" :onLoading="onMatchLoading">
:current="selectedIndex" <view
@change="(e) => (selectedIndex = e.detail.current)" v-for="(item, index) in matchList"
:style="{ height: '100%' }" :key="index"
@click="() => toMatchDetail(item.battleId)"
>
<view class="contest-header">
<text>{{ item.name }}</text>
<text>{{ item.createdAt }}</text>
<image src="../static/back.png" mode="widthFix" />
</view>
<BattleHeader
:players="item.mode === 1 ? [] : item.players"
:blueTeam="item.bluePlayers"
:redTeam="item.redPlayers"
:winner="item.winner"
:showRank="item.mode === 2"
:showHeader="false"
/>
</view>
</ScrollList>
<ScrollList :show="selectedIndex === 1" :onLoading="onBattleLoading">
<view
v-for="(item, index) in battleList"
:key="index"
@click="() => toMatchDetail(item.battleId)"
>
<view class="contest-header">
<text>{{ item.name }}</text>
<text>{{ item.createdAt }}</text>
<image src="../static/back.png" mode="widthFix" />
</view>
<BattleHeader
:players="item.mode === 1 ? [] : item.players"
:blueTeam="item.bluePlayers"
:redTeam="item.redPlayers"
:winner="item.winner"
:showRank="item.mode === 2"
:showHeader="false"
/>
</view>
</ScrollList>
<ScrollList
:show="selectedIndex === 2"
:onLoading="onPractiseLoading"
:pageSize="15"
> >
<swiper-item> <view
<ScrollList :onLoading="onMatchLoading"> v-for="(item, index) in practiseList"
<view :key="index"
v-for="(item, index) in matchList" class="practice-record"
:key="index" @click="() => getPractiseDetail(item.id)"
@click="() => toMatchDetail(item.battleId)" >
> <text
<view class="contest-header"> >{{ item.completed_arrows === 36 ? "耐力挑战" : "单组练习" }}
<text>{{ item.name }}</text> {{ item.createdAt }}</text
<text>{{ item.createdAt }}</text> >
<image src="../static/back.png" mode="widthFix" /> <image src="../static/back.png" mode="widthFix" />
</view> </view>
<BattleHeader </ScrollList>
:players="item.mode === 1 ? [] : item.players"
:blueTeam="item.bluePlayers"
:redTeam="item.redPlayers"
:winner="item.winner"
:showRank="item.mode === 2"
:showHeader="false"
/>
</view>
</ScrollList>
</swiper-item>
<swiper-item>
<ScrollList :onLoading="onBattleLoading">
<view
v-for="(item, index) in battleList"
:key="index"
@click="() => toMatchDetail(item.battleId)"
>
<view class="contest-header">
<text>{{ item.name }}</text>
<text>{{ item.createdAt }}</text>
<image src="../static/back.png" mode="widthFix" />
</view>
<BattleHeader
:players="item.mode === 1 ? [] : item.players"
:blueTeam="item.bluePlayers"
:redTeam="item.redPlayers"
:winner="item.winner"
:showRank="item.mode === 2"
:showHeader="false"
/>
</view>
</ScrollList>
</swiper-item>
<swiper-item>
<ScrollList :onLoading="onPractiseLoading" :pageSize="15">
<view
v-for="(item, index) in practiseList"
:key="index"
class="practice-record"
@click="() => getPractiseDetail(item.id)"
>
<text
>{{ item.completed_arrows === 36 ? "耐力挑战" : "单组练习" }}
{{ item.createdAt }}</text
>
<image src="../static/back.png" mode="widthFix" />
</view>
</ScrollList>
</swiper-item>
</swiper>
</view> </view>
</Container> </Container>
</template> </template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,19 +5,22 @@ 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 { getPractiseDataAPI } from "@/apis"; import { getPractiseDataAPI } from "@/apis";
import { canEenter } from "@/util";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user, device, online } = storeToRefs(store); const { user } = storeToRefs(store);
const data = ref({}); const data = ref({});
const goPractise = async (type) => { const toPractiseOne = () => {
if (!canEenter(user.value, device.value, online.value)) return;
await uni.$checkAudio();
uni.navigateTo({ uni.navigateTo({
url: `/pages/practise-${type}`, url: "/pages/practise-one",
});
};
const toPractiseTwo = () => {
uni.navigateTo({
url: "/pages/practise-two",
}); });
}; };
@@ -29,12 +32,14 @@ onShow(async () => {
<template> <template>
<Container title="个人练习"> <Container title="个人练习">
<view :style="{ width: '100%', height: '100%' }"> <view :style="{ width: '100%' }">
<Guide> <Guide>
<view class="guide-tips"> <text :style="{ color: '#fed847' }"
<text>师傅领进门修行靠自身赶紧练起来吧</text> >师傅领进门修行靠自身赶紧练起来吧</text
<text>坚持练习就能你快速升级早日加入全国排位赛</text> >
</view> <text :style="{ fontSize: '12px' }"
>坚持练习就能你快速升级早日加入全国排位赛</text
>
</Guide> </Guide>
<view class="practise-data"> <view class="practise-data">
<view> <view>
@@ -73,14 +78,14 @@ onShow(async () => {
</view> </view>
</view> </view>
</view> </view>
<view class="practise-btn" @click="() => goPractise('one')"> <view class="practise-btn" @click="toPractiseOne">
<image <image
src="https://static.shelingxingqiu.com/attachment/2025-07-12/db9x668e2vdtqh0otq.png" src="https://static.shelingxingqiu.com/attachment/2025-07-12/db9x668e2vdtqh0otq.png"
class="practise1" class="practise1"
mode="widthFix" mode="widthFix"
/> />
</view> </view>
<view class="practise-btn" @click="() => goPractise('two')"> <view class="practise-btn" @click="toPractiseTwo">
<image <image
src="https://static.shelingxingqiu.com/attachment/2025-07-12/db9x668eehkvyicc08.png" src="https://static.shelingxingqiu.com/attachment/2025-07-12/db9x668eehkvyicc08.png"
class="practise2" class="practise2"

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

@@ -4,6 +4,7 @@ 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 ShootProgress from "@/components/ShootProgress.vue";
import PlayersRow from "@/components/PlayersRow.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";
@@ -12,7 +13,7 @@ import RoundEndTip from "@/components/RoundEndTip.vue";
import TestDistance from "@/components/TestDistance.vue"; 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 { getCurrentGameAPI, laserCloseAPI } from "@/apis"; import { getCurrentGameAPI } from "@/apis";
import { isGameEnded } from "@/util"; import { isGameEnded } from "@/util";
import { MESSAGETYPES, roundsName } from "@/constants"; import { MESSAGETYPES, roundsName } from "@/constants";
import audioManager from "@/audioManager"; import audioManager from "@/audioManager";
@@ -27,6 +28,8 @@ const currentRound = ref(1);
const goldenRound = ref(0); const goldenRound = ref(0);
const currentRedPoint = ref(0); const currentRedPoint = ref(0);
const currentBluePoint = ref(0); const currentBluePoint = ref(0);
const totalRounds = ref(0);
const power = ref(0);
const scores = ref([]); const scores = ref([]);
const blueScores = ref([]); const blueScores = ref([]);
const redTeam = ref([]); const redTeam = ref([]);
@@ -39,6 +42,10 @@ const showRoundTip = ref(false);
const isFinalShoot = ref(false); const isFinalShoot = ref(false);
const isEnded = ref(false); const isEnded = ref(false);
const onBack = () => {
uni.$showHint(2);
};
function recoverData(battleInfo) { function recoverData(battleInfo) {
uni.removeStorageSync("last-awake-time"); uni.removeStorageSync("last-awake-time");
battleId.value = battleInfo.id; battleId.value = battleInfo.id;
@@ -57,8 +64,8 @@ function recoverData(battleInfo) {
bluePoints.value = 0; bluePoints.value = 0;
redPoints.value = 0; redPoints.value = 0;
currentRound.value = battleInfo.currentRound; currentRound.value = battleInfo.currentRound;
totalRounds.value = battleInfo.maxRound;
roundResults.value = [...battleInfo.roundResults]; roundResults.value = [...battleInfo.roundResults];
// 算得分
battleInfo.roundResults.forEach((round) => { battleInfo.roundResults.forEach((round) => {
const blueTotal = round.blueArrows.reduce( const blueTotal = round.blueArrows.reduce(
(last, next) => last + next.ring, (last, next) => last + next.ring,
@@ -77,64 +84,50 @@ function recoverData(battleInfo) {
redPoints.value += 2; redPoints.value += 2;
} }
}); });
if (battleInfo.goldenRoundNumber) { const hasCurrentRoundData =
currentRound.value += battleInfo.goldenRoundNumber; battleInfo.redTeam.some(
goldenRound.value = battleInfo.goldenRoundNumber; (item) => !!item.shotHistory[battleInfo.currentRound]
) ||
battleInfo.blueTeam.some(
(item) => !!item.shotHistory[battleInfo.currentRound]
);
if (
battleInfo.currentRound > battleInfo.roundResults.length &&
hasCurrentRoundData
) {
const blueArrows = [];
const redArrows = [];
battleInfo.redTeam.forEach((item) =>
item.shotHistory[battleInfo.currentRound]
.filter((item) => !!item.playerId)
.forEach((item) => redArrows.push(item))
);
battleInfo.blueTeam.forEach((item) =>
item.shotHistory[battleInfo.currentRound]
.filter((item) => !!item.playerId)
.forEach((item) => blueArrows.push(item))
);
roundResults.value.push({
redArrows,
blueArrows,
});
}
if (battleInfo.goldenRound) {
const { ShotCount, RedRecords, BlueRecords } = battleInfo.goldenRound;
currentRound.value += ShotCount;
goldenRound.value += ShotCount;
isFinalShoot.value = true; isFinalShoot.value = true;
for (let i = 1; i <= battleInfo.goldenRoundNumber; i++) { for (let i = 0; i < ShotCount; i++) {
const redArrows = []; const roundData = {
battleInfo.redTeam.forEach((item) => { redArrows:
if (item.shotHistory[roundResults.value.length + 1]) { RedRecords && RedRecords[i] ? RedRecords[i].Arrows || [] : [],
item.shotHistory[roundResults.value.length + 1] blueArrows:
.filter((item) => !!item.playerId) BlueRecords && BlueRecords[i] ? BlueRecords[i].Arrows || [] : [],
.forEach((item) => redArrows.push(item)); gold: true,
} };
}); roundResults.value.push(roundData);
const blueArrows = [];
battleInfo.blueTeam.forEach((item) => {
if (item.shotHistory[roundResults.value.length + 1]) {
item.shotHistory[roundResults.value.length + 1]
.filter((item) => !!item.playerId)
.forEach((item) => blueArrows.push(item));
}
});
roundResults.value.push({
number: roundResults.value.length + 1,
blueArrows,
redArrows,
blueTotal: blueArrows.reduce((last, next) => last + next.ring, 0),
redTotal: redArrows.reduce((last, next) => last + next.ring, 0),
});
} }
} else { } else {
const hasCurrentRoundData =
battleInfo.redTeam.some(
(item) => !!item.shotHistory[battleInfo.currentRound]
) ||
battleInfo.blueTeam.some(
(item) => !!item.shotHistory[battleInfo.currentRound]
);
if (
battleInfo.currentRound > battleInfo.roundResults.length &&
hasCurrentRoundData
) {
const blueArrows = [];
const redArrows = [];
battleInfo.redTeam.forEach((item) =>
item.shotHistory[battleInfo.currentRound]
.filter((item) => !!item.playerId)
.forEach((item) => redArrows.push(item))
);
battleInfo.blueTeam.forEach((item) =>
item.shotHistory[battleInfo.currentRound]
.filter((item) => !!item.playerId)
.forEach((item) => blueArrows.push(item))
);
roundResults.value.push({
redArrows,
blueArrows,
});
}
[...battleInfo.redTeam, ...battleInfo.blueTeam].some((p) => { [...battleInfo.redTeam, ...battleInfo.blueTeam].some((p) => {
if (p.id === user.value.id) { if (p.id === user.value.id) {
const roundArrows = Object.values(p.shotHistory); const roundArrows = Object.values(p.shotHistory);
@@ -152,24 +145,18 @@ function recoverData(battleInfo) {
const lastIndex = roundResults.value.length - 1; const lastIndex = roundResults.value.length - 1;
if (roundResults.value[lastIndex]) { if (roundResults.value[lastIndex]) {
const redArrows = roundResults.value[lastIndex].redArrows; const redArrows = roundResults.value[lastIndex].redArrows;
scores.value = [...redArrows] scores.value = [...redArrows].filter((item) => !!item.playerId);
.filter((item) => !!item.playerId)
.sort((a, b) => a.shotTimeUnix - b.shotTimeUnix);
const blueArrows = roundResults.value[lastIndex].blueArrows; const blueArrows = roundResults.value[lastIndex].blueArrows;
blueScores.value = [...blueArrows] blueScores.value = [...blueArrows].filter((item) => !!item.playerId);
.filter((item) => !!item.playerId)
.sort((a, b) => a.shotTimeUnix - b.shotTimeUnix);
} }
// if (battleInfo.status !== 11) return;
if (battleInfo.firePlayerIndex) { if (battleInfo.firePlayerIndex) {
currentShooterId.value = battleInfo.firePlayerIndex; currentShooterId.value = battleInfo.firePlayerIndex;
const redPlayer = redTeam.value.find( const redPlayer = redTeam.value.find(
(item) => item.id === currentShooterId.value (item) => item.id === currentShooterId.value
); );
let nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭"; tips.value = redPlayer ? "请红队射箭" : "请蓝队射箭";
nextTips += "重回"; uni.$emit("update-tips", tips.value);
// if (battleInfo.firePlayerIndex === user.value.id) nextTips += "你";
tips.value = nextTips;
uni.$emit("update-tips", nextTips);
} }
if (battleInfo.fireTime > 0) { if (battleInfo.fireTime > 0) {
const remain = Date.now() / 1000 - battleInfo.fireTime; const remain = Date.now() / 1000 - battleInfo.fireTime;
@@ -186,12 +173,9 @@ function recoverData(battleInfo) {
async function onReceiveMessage(messages = []) { async function onReceiveMessage(messages = []) {
messages.forEach((msg) => { messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.WaitForAllReady) {
redTeam.value = msg.groupUserStatus.redTeam;
blueTeam.value = msg.groupUserStatus.blueTeam;
}
if (msg.constructor === MESSAGETYPES.AllReady) { if (msg.constructor === MESSAGETYPES.AllReady) {
start.value = true; start.value = true;
totalRounds.value = msg.groupUserStatus.config.maxRounds;
} }
if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) { if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) {
if (currentShooterId.value !== msg.userId) { if (currentShooterId.value !== msg.userId) {
@@ -199,11 +183,8 @@ async function onReceiveMessage(messages = []) {
const redPlayer = redTeam.value.find( const redPlayer = redTeam.value.find(
(item) => item.id === currentShooterId.value (item) => item.id === currentShooterId.value
); );
if (msg.userId === user.value.id) audioManager.play("轮到你了");
let nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭"; const nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭";
if (msg.userId === user.value.id && redTeam.value.length > 1) {
nextTips += "你";
}
if (nextTips !== tips.value) { if (nextTips !== tips.value) {
tips.value = nextTips; tips.value = nextTips;
uni.$emit("update-tips", tips.value); uni.$emit("update-tips", tips.value);
@@ -213,7 +194,21 @@ async function onReceiveMessage(messages = []) {
} }
} }
if (msg.constructor === MESSAGETYPES.ShootResult) { if (msg.constructor === MESSAGETYPES.ShootResult) {
if (msg.battleInfo) recoverData(msg.battleInfo); if (currentShooterId.value !== msg.userId) return;
const isRed = redTeam.value.find((item) => item.id === msg.userId);
if (isRed) scores.value.push({ ...msg.target });
else blueScores.value.push({ ...msg.target });
// 下标从0开始的要减1
if (!roundResults.value[currentRound.value - 1]) {
roundResults.value.push({
redArrows: [],
blueArrows: [],
gold: goldenRound.value > 0,
});
}
roundResults.value[currentRound.value - 1][
isRed ? "redArrows" : "blueArrows"
].push({ ...msg.target });
} }
if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) { if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
const result = msg.preRoundResult; const result = msg.preRoundResult;
@@ -224,12 +219,19 @@ async function onReceiveMessage(messages = []) {
currentRedPoint.value = result.redScore; currentRedPoint.value = result.redScore;
bluePoints.value += result.blueScore; bluePoints.value += result.blueScore;
redPoints.value += result.redScore; redPoints.value += result.redScore;
currentRound.value = result.currentRound + 1;
if (!result.goldenRound) { if (!result.goldenRound) {
showRoundTip.value = true; showRoundTip.value = true;
} }
} }
if (msg.constructor === MESSAGETYPES.FinalShoot) { if (msg.constructor === MESSAGETYPES.FinalShoot) {
currentShooterId.value = 0; currentShooterId.value = 0;
currentRound.value = msg.groupUserStatus.currentRound + 1;
goldenRound.value += 1;
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
currentBluePoint.value = bluePoints.value; currentBluePoint.value = bluePoints.value;
currentRedPoint.value = redPoints.value; currentRedPoint.value = redPoints.value;
if (!isFinalShoot.value) { if (!isFinalShoot.value) {
@@ -240,6 +242,7 @@ async function onReceiveMessage(messages = []) {
} }
if (msg.constructor === MESSAGETYPES.MatchOver) { if (msg.constructor === MESSAGETYPES.MatchOver) {
if (msg.endStatus.noSaved) { if (msg.endStatus.noSaved) {
currentRound.value += 1;
currentBluePoint.value = 0; currentBluePoint.value = 0;
currentRedPoint.value = 0; currentRedPoint.value = 0;
showRoundTip.value = true; showRoundTip.value = true;
@@ -277,26 +280,18 @@ onLoad(async (options) => {
setTimeout(getCurrentGameAPI, 2000); setTimeout(getCurrentGameAPI, 2000);
} }
} }
uni.enableAlertBeforeUnload({
message: "离开比赛可能导致比赛失败,是否继续?",
success: (res) => {
console.log("已启用离开提示");
},
});
}); });
onMounted(async () => { onMounted(() => {
uni.setKeepScreenOn({ uni.setKeepScreenOn({
keepScreenOn: true, keepScreenOn: true,
}); });
uni.$on("socket-inbox", onReceiveMessage); uni.$on("socket-inbox", onReceiveMessage);
await laserCloseAPI();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
uni.setKeepScreenOn({ uni.setKeepScreenOn({
keepScreenOn: false, keepScreenOn: false,
}); });
uni.$off("socket-inbox", onReceiveMessage); uni.$off("socket-inbox", onReceiveMessage);
audioManager.stopAll();
}); });
const refreshTimer = ref(null); const refreshTimer = ref(null);
onShow(async () => { onShow(async () => {
@@ -321,7 +316,7 @@ onHide(() => {
</script> </script>
<template> <template>
<Container :bgType="start ? 3 : 1"> <Container :bgType="start ? 3 : 1" :onBack="onBack">
<view class="container"> <view class="container">
<BattleHeader v-if="!start" :redTeam="redTeam" :blueTeam="blueTeam" /> <BattleHeader v-if="!start" :redTeam="redTeam" :blueTeam="blueTeam" />
<TestDistance v-if="!start" :guide="false" :isBattle="true" /> <TestDistance v-if="!start" :guide="false" :isBattle="true" />
@@ -342,6 +337,7 @@ onHide(() => {
<BowTarget <BowTarget
v-if="start" v-if="start"
mode="team" mode="team"
:power="start ? power : 0"
:scores="scores" :scores="scores"
:blueScores="blueScores" :blueScores="blueScores"
/> />
@@ -351,6 +347,7 @@ onHide(() => {
:redPoints="redPoints" :redPoints="redPoints"
:bluePoints="bluePoints" :bluePoints="bluePoints"
:goldenRound="goldenRound" :goldenRound="goldenRound"
:power="power"
/> />
<ScreenHint <ScreenHint
:show="showRoundTip" :show="showRoundTip"
@@ -360,11 +357,11 @@ onHide(() => {
<RoundEndTip <RoundEndTip
v-if="showRoundTip" v-if="showRoundTip"
:isFinal="isFinalShoot" :isFinal="isFinalShoot"
:round="currentRound" :round="currentRound - 1"
:bluePoint="currentBluePoint" :bluePoint="currentBluePoint"
:redPoint="currentRedPoint" :redPoint="currentRedPoint"
:roundData=" :roundData="
roundResults[currentRound - 1] ? roundResults[currentRound - 1] : [] roundResults[currentRound - 2] ? roundResults[currentRound - 2] : []
" "
:onAutoClose="() => (showRoundTip = false)" :onAutoClose="() => (showRoundTip = false)"
/> />
@@ -382,7 +379,7 @@ onHide(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: -2%; margin-bottom: -7vw;
margin-bottom: 6%; margin-top: -3vw;
} }
</style> </style>

View File

@@ -121,7 +121,7 @@ const onClickTab = (index) => {
</view> </view>
</view> </view>
<view :style="{ margin: '20px 0' }"> <view :style="{ margin: '20px 0' }">
<BowTarget :scores="redScores" :blueScores="blueScores" mode="team" /> <BowTarget :scores="redScores" :blueScores="blueScores" />
</view> </view>
<view class="score-container"> <view class="score-container">
<view <view

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

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

View File

@@ -4,11 +4,10 @@ import Container from "@/components/Container.vue";
import UserHeader from "@/components/UserHeader.vue"; import UserHeader from "@/components/UserHeader.vue";
import UserItem from "@/components/UserItem.vue"; import UserItem from "@/components/UserItem.vue";
import Avatar from "@/components/Avatar.vue"; import Avatar from "@/components/Avatar.vue";
import { canEenter } from "@/util";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user, device, online } = storeToRefs(store); const { user, device } = storeToRefs(store);
const { updateUser } = store; const { updateUser } = store;
const toOrderPage = () => { const toOrderPage = () => {
@@ -17,13 +16,16 @@ const toOrderPage = () => {
}); });
}; };
const toFristTryPage = async () => { const toFristTryPage = () => {
if (canEenter(user.value, device.value, online.value, "/pages/first-try")) { if (!device.value.deviceId) {
await uni.$checkAudio(); return uni.showToast({
uni.navigateTo({ title: "请先绑定设备",
url: "/pages/first-try", icon: "none",
}); });
} }
uni.navigateTo({
url: "/pages/first-try",
});
}; };
const toBeVipPage = () => { const toBeVipPage = () => {
uni.navigateTo({ uni.navigateTo({
@@ -50,11 +52,6 @@ const toAboutUsPage = () => {
url: "/pages/about-us", url: "/pages/about-us",
}); });
}; };
const toAudioTestPage = () => {
uni.navigateTo({
url: "/pages/audio-test",
});
};
const showLogout = ref(false); const showLogout = ref(false);
const logout = () => { const logout = () => {
@@ -73,15 +70,9 @@ onMounted(() => {
<template> <template>
<Container title="用户信息"> <Container title="用户信息">
<view :style="{ width: '100%', height: '100%' }"> <view :style="{ width: '100%' }">
<UserHeader /> <UserHeader />
<scroll-view <view class="container">
scroll-y
:show-scrollbar="false"
:enhanced="true"
class="container"
>
<view :style="{ height: '10px' }"></view>
<UserItem title="用户名">{{ user.nickName }}</UserItem> <UserItem title="用户名">{{ user.nickName }}</UserItem>
<UserItem title="头像"> <UserItem title="头像">
<Avatar :src="user.avatar" :size="35" /> <Avatar :src="user.avatar" :size="35" />
@@ -135,18 +126,12 @@ onMounted(() => {
<image src="../static/my-grow.png" mode="widthFix" /> <image src="../static/my-grow.png" mode="widthFix" />
</view> </view>
<UserItem title="关于我们" :onClick="toAboutUsPage" /> <UserItem title="关于我们" :onClick="toAboutUsPage" />
<UserItem
title="音频测试"
:onClick="toAudioTestPage"
v-if="showLogout"
/>
<UserItem <UserItem
title="退出登录(仅用于测试)" title="退出登录(仅用于测试)"
:onClick="logout" :onClick="logout"
v-if="showLogout" v-if="showLogout"
/> />
<view :style="{ height: '10px' }"></view> </view>
</scroll-view>
</view> </view>
</Container> </Container>
</template> </template>
@@ -154,8 +139,9 @@ onMounted(() => {
<style scoped> <style scoped>
.container { .container {
background-color: #f5f5f5; background-color: #f5f5f5;
height: calc(100% - 65px); height: calc(100vh - 161px);
margin-top: 15px; margin-top: 20px;
padding-top: 10px;
} }
.my-grow { .my-grow {
width: 100%; width: 100%;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 B

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/static/app-bg6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
src/static/back-grey.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 B

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