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
162 changed files with 5586 additions and 19347 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-xhs": "3.0.0-4060620250520001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
"@dcloudio/uni-ui": "^1.5.11",
"pinia": "2.0.36",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"

View File

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

View File

@@ -45,7 +45,6 @@ function request(method, url, data = {}) {
uni.removeStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
uni.$emit("update-user");
}
if (message === "ROOM_FULL") {
resolve({ full: true });
@@ -163,14 +162,13 @@ export const getProvinceData = () => {
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", {
appName: "shoot",
appId: "wxa8f5989dcd45cc23",
nickName,
avatarData,
code,
phone,
});
uni.setStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`,
@@ -196,10 +194,9 @@ export const getMyDevicesAPI = () => {
return request("GET", "/user/device/getBindings");
};
export const createPractiseAPI = (arrows, mode) => {
export const createPractiseAPI = (arrows) => {
return request("POST", "/user/practice/create", {
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", {
number,
userId,
});
};
@@ -413,8 +409,9 @@ export const cancelOrderListAPI = async (id) => {
return request("POST", "/user/order/cancelOrder", { id });
};
export const getUserGameState = () => {
return request("GET", "/user/state");
export const isGamingAPI = async () => {
const result = await request("GET", "/user/isGaming");
return result.gaming || false;
};
export const getCurrentGameAPI = async () => {
@@ -521,34 +518,3 @@ export const addNoteAPI = async (id, remark) => {
export const removePointRecord = async (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 = {
// 激光已校准:
// "https://static.shelingxingqiu.com/attachment/2025-10-29/ddupaur1vdkyhzaqdc.mp3",
const audioFils = {
胜利: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcuo9yjp0kt5msvmvd.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-11-12/de6hr2faw28t0ianh0.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya57qurnsj6pg4.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-11-12/de6n45o3tsm1v4unam.mp3",
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcuuznjc78ljhzuw1o.mp3",
"1环":
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin1aq7gxjih5l.mp3",
"2环":
@@ -59,26 +57,6 @@ export const audioFils = {
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin69nj1xh7yfz.mp3",
"10环":
"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 envVersion = accountInfo.miniProgram.envVersion;
// 只在体验版打印日志,正式版(release)和开发版(develop)不打印
if (envVersion === "trial") {
if (envVersion === 'trial') {
console.log(...args);
}
}
@@ -97,497 +75,150 @@ class AudioManager {
constructor() {
this.audioMap = new Map();
this.currentPlayingKey = null;
this.retryCount = new Map();
this.maxRetries = 3;
// 多轮统一重试:最多重试的轮次与每轮间隔
this.maxRetryRounds = 10;
this.retryRoundIntervalMs = 1500;
// 显式授权播放标记,防止 iOS 在设置 src 后误播
this.allowPlayMap = new Map();
// 串行加载相关属性
this.audioKeys = [];
this.currentLoadingIndex = 0;
this.isLoading = false;
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();
}
// 清理不再使用的缓存文件
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() {
if (this.isLoading) {
debugLog("音频正在加载中,跳过重复初始化");
return this.loadingPromise;
}
debugLog("开始串行加载音频...");
this.isLoading = true;
this.audioKeys = Object.keys(audioFils);
this.currentLoadingIndex = 0;
this.failedLoadKeys.clear();
// 增加代数,使得旧的加载循环失效
this.loadGeneration = (this.loadGeneration || 0) + 1;
const currentGen = this.loadGeneration;
this.loadingPromise = new Promise((resolve) => {
const finalize = () => {
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);
this.loadNextAudio(resolve);
});
return this.loadingPromise;
}
// 按自定义列表串行加载音频(避免并发过多)
loadKeysSequentially(keys, onComplete, gen) {
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;
// 串行加载下一个音频
loadNextAudio(onComplete) {
if (this.currentLoadingIndex >= this.audioKeys.length) {
debugLog("首轮加载遍历完成", this.currentLoadingIndex);
debugLog("所有音频加载完成");
this.isLoading = false;
if (onComplete) onComplete();
return;
}
const key = this.audioKeys[this.currentLoadingIndex];
debugLog(
`开始加载音频 ${this.currentLoadingIndex + 1}/${
this.audioKeys.length
}: ${key}`
);
debugLog(`开始加载音频 ${this.currentLoadingIndex + 1}/${this.audioKeys.length}: ${key}`);
this.createAudio(key, () => {
this.currentLoadingIndex++;
setTimeout(() => {
this.loadNextAudio(onComplete, gen);
this.loadNextAudio(onComplete);
}, 100);
});
}
// 创建单个音频实例(支持本地缓存)
// 创建单个音频实例
createAudio(key, callback) {
this.currentLoadingIndex++;
const src = audioFils[key];
const audio = uni.createInnerAudioContext();
audio.src = src;
audio.autoplay = false;
const setupAudio = (realSrc) => {
const audio = uni.createInnerAudioContext();
audio.autoplay = false;
audio.src = realSrc;
try {
if (typeof audio.volume === "number") {
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.destroy();
if (callback) callback();
}, 10000);
const loadTimeout = setTimeout(() => {
debugLog(`音频 ${key} 加载超时`);
this.recordLoadFailure(key);
try {
audio.destroy();
} catch (_) {}
if (callback) callback();
}, 10000);
// 监听加载状态
audio.onCanplay(() => {
clearTimeout(loadTimeout);
debugLog(`音频 ${key} 已加载完成`);
this.retryCount.set(key, 0);
if (callback) callback();
});
audio.onCanplay(() => {
if (!this.allowPlayMap.get(key)) {
try {
audio.pause();
} catch (_) {}
}
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);
debugLog(`音频 ${key} 加载失败:`, res.errMsg);
this.handleAudioError(key);
if (callback) callback();
});
audio.onError((res) => {
clearTimeout(loadTimeout);
debugLog(`音频 ${key} 加载失败:`, res.errMsg);
// 如果是本地文件加载失败,可能是文件损坏,清除缓存以便下次重新下载
if (realSrc !== src && this.localFileCache[src] === realSrc) {
debugLog(`本地缓存失效,移除记录: ${key}`);
delete this.localFileCache[src];
uni.setStorageSync("audio_local_files", this.localFileCache);
// 移除文件
uni.removeSavedFile({ filePath: realSrc });
}
this.recordLoadFailure(key);
// 监听播放结束事件
audio.onEnded(() => {
if (this.currentPlayingKey === key) {
this.currentPlayingKey = null;
}
});
// 监听播放停止事件
audio.onStop(() => {
if (this.currentPlayingKey === key) {
this.currentPlayingKey = null;
}
});
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);
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) {
const oldAudio = this.audioMap.get(key);
if (oldAudio) oldAudio.destroy();
if (oldAudio) {
oldAudio.destroy();
}
this.createAudio(key);
}
// 播放指定音频或音频数组(数组则按顺序连续播放)
play(input, interrupt = true) {
// 统一规范化为队列
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;
}
// 不打断当前播放:把新的队列加入到序列中,等待当前播放结束后衔接
// 播放指定音频
play(key) {
// 如果有正在播放的音频,先停止
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);
} else if (this.currentPlayingKey === key) {
// 同一音频正在播放:不重启,避免听到重复开头
return;
}
const audio = this.audioMap.get(key);
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();
this.currentPlayingKey = key;
this.lastPlayKey = key;
this.lastPlayAt = Date.now();
} else {
debugLog(`音频 ${key} 不存在,尝试重新加载...`);
this.retryLoadAudio(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;
this.reloadAudio(key);
}
}
@@ -596,104 +227,19 @@ class AudioManager {
const audio = this.audioMap.get(key);
if (audio) {
audio.stop();
this.allowPlayMap.set(key, false);
if (this.currentPlayingKey === key) {
this.currentPlayingKey = null;
}
}
}
// 停止所有音频
stopAll() {
for (const [k, audio] of this.audioMap.entries()) {
try {
audio.stop();
} catch (_) {}
this.allowPlayMap.set(k, false);
// 手动重新加载指定音频
reloadAudio(key) {
if (audioFils[key]) {
debugLog(`手动重新加载音频: ${key}`);
this.retryCount.set(key, 0);
this.retryLoadAudio(key);
}
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>
import { capsuleHeight } from "@/util";
import { ref, onMounted } from "vue";
const props = defineProps({
type: {
type: Number,
@@ -11,6 +10,11 @@ const props = defineProps({
default: "#050b19",
},
});
const capsuleHeight = ref(0);
onMounted(() => {
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
capsuleHeight.value = menuBtnInfo.top + 50 - 9;
});
</script>
<template>
@@ -31,7 +35,7 @@ const props = defineProps({
class="bg-image"
v-if="type === 2"
src="../static/app-bg3.png"
:style="{ height: capsuleHeight + 50 + 'px' }"
:style="{ height: capsuleHeight + 'px' }"
/>
<image
class="bg-image"
@@ -45,12 +49,6 @@ const props = defineProps({
src="../static/app-bg5.png"
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>
</template>

View File

@@ -1,122 +1,81 @@
<script setup>
import { ref } from "vue";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const { user } = storeToRefs(useStore());
const tabs = [
{ image: "../static/tab-vip.png" },
{ image: "../static/tab-point-book.png" },
{ image: "../static/tab-mall.png" },
];
const props = defineProps({
selected: {
type: Number,
default: 0,
},
onSignin: {
type: Function,
default: () => {},
},
onChange: {
type: Function,
default: () => {},
},
});
const isIOS = uni.getDeviceInfo().osName === "ios";
const tabs = {
VIP: "vip",
智能弓: "mall",
计分本: "tab-point-book",
勋章: "medal",
个人中心: "user",
};
const onTabChange = (index) => {
function handleTabClick(index) {
if (index === 0) {
return uni.navigateTo({
uni.navigateTo({
url: "/pages/be-vip",
});
}
if (index === 1) {
return uni.navigateTo({
url: "/pages/device-intro",
});
}
if (index === 2) {
return uni.navigateTo({
uni.navigateTo({
url: "/pages/point-book",
});
}
if (index === 4) {
if (!user.value.id) {
props.onSignin();
return;
}
return uni.navigateTo({
url: "/pages/user",
if (index === 2) {
uni.navigateTo({
url: "/pages/device-intro",
});
}
// props.onChange(index);
};
}
</script>
<template>
<view class="footer" :style="{ paddingBottom: isIOS ? '30rpx' : '0' }">
<button
hover-class="none"
v-for="(key, index) in Object.keys(tabs)"
:key="key"
@click="onTabChange(index)"
:class="index === 2 ? 'point-book-tab' : ''"
<view class="footer">
<image class="footer-bg" src="../static/tab-bg.png" mode="widthFix" />
<view
v-for="(tab, index) in tabs"
:key="index"
class="tab-item"
@click="handleTabClick(index)"
:style="{
width: index === 1 ? '36%' : '10%',
}"
>
<image
v-if="index !== 2"
:src="`../static/tab-${tabs[key]}.png`"
mode="widthFix"
/>
<image v-else src="../static/tab-point-book.png" mode="widthFix" />
<text v-if="index !== 2">{{ key }}</text>
</button>
<image :src="tab.image" mode="widthFix" />
</view>
</view>
</template>
<style scoped>
.footer {
height: 140rpx;
height: 117px;
width: 100vw;
position: relative;
display: flex;
justify-content: center;
justify-content: space-around;
align-items: center;
background: linear-gradient(
180deg,
rgba(70, 55, 34, 0.75) 0%,
rgba(5, 11, 25, 0.58) 77%
),
#000000;
/* box-shadow: 0rpx 0rpx 20rpx 0rpx rgba(0, 0, 0, 0.06); */
overflow-x: hidden;
}
.footer > button {
.footer-bg {
width: 100%;
height: 100%;
position: absolute;
z-index: 0;
}
.tab-item {
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 19%;
color: #8e7d5c;
}
.footer > button > image {
width: 46rpx;
height: 46rpx;
.tab-item > image {
width: 86%;
}
.footer > button > text {
font-weight: 500;
font-size: 20rpx;
margin-top: 10rpx;
.tab-item:nth-child(2) {
transform: translate(25%, 30%);
}
.point-book-tab {
overflow: unset;
margin: 0 1%;
.tab-item:nth-child(3) {
margin-bottom: 25rpx;
}
.point-book-tab > image {
width: 144rpx !important;
height: 144rpx !important;
transform: translateY(-20rpx);
.tab-item:nth-child(3) > image {
width: 140rpx;
}
.tab-item:nth-child(4) {
transform: translate(-25%, 30%);
}
</style>

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,16 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { getDeviceBatteryAPI } from "@/apis";
const power = ref(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);
defineProps({
power: {
type: Number,
default: 0,
},
});
</script>
<template>
<view class="container">
<view class="container" :style="{ opacity: power > 0 ? 1 : 0 }">
<image src="../static/b-power.png" mode="widthFix" />
<view>电量{{ power || 1 }}%</view>
<view>电量{{ power }}%</view>
</view>
</template>

View File

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

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>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { getElementRect, calcRing, capsuleHeight } from "@/util";
import { ref, onMounted } from "vue";
import { getElementRect, calcRing } from "@/util";
const props = defineProps({
id: {
@@ -19,15 +19,18 @@ const props = defineProps({
type: Function,
default: null,
},
editMode: {
type: Boolean,
default: true,
},
});
const rect = ref({});
const arrow = ref(null);
const isDragging = ref(false);
const dragStartPos = ref({ x: 0, y: 0 });
const capsuleHeight = ref(0);
const scale = ref(1);
const scrollTop = ref(0);
const selected = ref(null);
let lastMoveTime = 0;
// 点击靶纸创建新的点
@@ -40,33 +43,19 @@ const onClick = async (e) => {
return;
}
if (props.id === 7 || props.id === 9) {
if (
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);
scale.value = 1.5;
}
const newArrow = {
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;
newArrow.ring = calcRing(
props.id,
newArrow.x / scale.value - side * 0.05,
newArrow.y / scale.value - side * 0.05,
side * 0.9
newArrow.x / scale.value - rect.value.width * 0.05,
newArrow.y / scale.value - rect.value.width * 0.05,
rect.value.width * 0.9
);
arrow.value = {
...newArrow,
@@ -86,14 +75,12 @@ const confirmAdd = () => {
}
arrow.value = null;
scale.value = 1;
scrollTop.value = 0;
};
// 删除箭矢
const deleteArrow = () => {
arrow.value = null;
scale.value = 1;
scrollTop.value = 0;
};
// 开始拖拽 - 同样修复坐标获取
@@ -128,9 +115,9 @@ const onDrag = async (e) => {
);
arrow.value.ring = calcRing(
props.id,
arrow.value.x / scale.value - side * 0.05,
arrow.value.y / scale.value - side * 0.05,
side * 0.9
arrow.value.x / scale.value - rect.value.width * 0.05,
arrow.value.y / scale.value - rect.value.width * 0.05,
rect.value.width * 0.9
);
arrow.value.x = arrow.value.x / side;
@@ -147,63 +134,27 @@ const endDrag = (e) => {
const getNewPos = () => {
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)" };
} else {
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: "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 () => {
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
capsuleHeight.value = menuBtnInfo.top - 9;
const result = await getElementRect(".container");
rect.value = result;
uni.$on("set-edit-arrow", setEditArrow);
});
onBeforeUnmount(() => {
uni.$off("set-edit-arrow", setEditArrow);
});
</script>
<template>
<scroll-view
:scroll-y="scale > 1"
scroll-with-animation
:scroll-top="scrollTop"
:show-scrollbar="false"
:enhanced="true"
<view
:style="{ overflowY: editMode ? 'auto' : 'hidden' }"
class="container"
@tap="onClick"
@touchmove="onDrag"
@@ -214,16 +165,16 @@ onBeforeUnmount(() => {
:style="{
width: 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" />
<view
v-for="(arrow, index) in arrows"
:key="index"
:class="`arrow-point ${
selected !== null && index === selected ? 'selected-arrow-point' : ''
}`"
class="arrow-point"
:style="{
left: (arrow.x !== undefined ? arrow.x : 0) * 100 + '%',
top: (arrow.y !== undefined ? arrow.y : 0) * 100 + '%',
@@ -232,6 +183,9 @@ onBeforeUnmount(() => {
<view
v-if="arrow.x !== undefined && arrow.y !== undefined"
class="point"
:style="{
transform: props.id === 7 || props.id === 9 ? 'scale(0.7)' : '',
}"
>
<text>{{ index + 1 }}</text>
</view>
@@ -245,44 +199,36 @@ onBeforeUnmount(() => {
:x="arrow ? rect.width * arrow.x : 0"
:y="arrow ? rect.width * arrow.y : 0"
>
<view
class="point"
:style="{ minWidth: 10 * scale + 'px', minHeight: 10 * scale + 'px' }"
>
<view v-if="arrow" class="edit-buttons" @touchstart.stop>
<view class="edit-btn-text">
<text>{{ arrow.ring === 0 ? "M" : arrow.ring }}</text>
<text
v-if="arrow.ring > 0"
:style="{
fontSize: '16px',
marginLeft: '2px',
}"
>环</text
>
</view>
<view
class="edit-btn confirm-btn"
@touchstart.stop="confirmAdd"
:style="{ ...getNewPos() }"
<view class="point"> </view>
<view v-if="arrow" class="edit-buttons" @touchstart.stop>
<view class="edit-btn-text">
<text>{{ arrow.ring === 0 ? "M" : arrow.ring }}</text>
<text
v-if="arrow.ring > 0"
:style="{
fontSize: '16px',
marginLeft: '2px',
}"
>points</text
>
<image src="../static/arrow-edit-save.png" mode="widthFix" />
</view>
<view class="edit-btn delete-btn" @touchstart.stop="deleteArrow">
<image src="../static/arrow-edit-delete.png" mode="widthFix" />
</view>
<view
class="edit-btn drag-btn"
@touchstart.stop="startDrag($event)"
>
<image src="../static/arrow-edit-move.png" mode="widthFix" />
</view>
</view>
<view
class="edit-btn confirm-btn"
@touchstart.stop="confirmAdd"
:style="{ ...getNewPos() }"
>
<image src="../static/arrow-edit-save.png" mode="widthFix" />
</view>
<view class="edit-btn delete-btn" @touchstart.stop="deleteArrow">
<image src="../static/arrow-edit-delete.png" mode="widthFix" />
</view>
<view class="edit-btn drag-btn" @touchstart.stop="startDrag($event)">
<image src="../static/arrow-edit-move.png" mode="widthFix" />
</view>
</view>
</movable-view>
<!-- <view class="test-view"></view> -->
</movable-area>
</scroll-view>
</view>
</template>
<style scoped>
@@ -322,35 +268,31 @@ onBeforeUnmount(() => {
.arrow-point {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
.point {
min-width: 10px;
min-height: 10px;
min-width: 12px;
min-height: 12px;
border-radius: 50%;
border: 1px solid #fff;
color: #fff;
font-size: 8px;
text-align: center;
line-height: 10px;
box-sizing: border-box;
background-color: #00bf04;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
background-color: #ff4444;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
transition: all 0.1s linear;
position: relative;
transform: translate(-50%, -50%);
}
.point > text {
transform: scaleX(0.7);
display: block;
font-size: 16rpx;
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;
font-weight: bold;
}
.edit-buttons {
@@ -369,6 +311,7 @@ onBeforeUnmount(() => {
width: 100%;
display: flex;
justify-content: center;
/* margin-left: 10px; */
}
.edit-btn-text > text {
@@ -406,31 +349,4 @@ onBeforeUnmount(() => {
right: -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>

View File

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

View File

@@ -1,13 +1,12 @@
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { ref, onMounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import AppBackground from "@/components/AppBackground.vue";
import Header from "@/components/Header.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import BackToGame from "@/components/BackToGame.vue";
import { getCurrentGameAPI, laserAimAPI } from "@/apis";
import { capsuleHeight, debounce } from "@/util";
import AudioManager from "@/audioManager";
import { getCurrentGameAPI } from "@/apis";
import { debounce } from "@/util";
const props = defineProps({
title: {
type: String,
@@ -21,9 +20,13 @@ const props = defineProps({
type: Function,
default: null,
},
scroll: {
overflow: {
type: String,
default: "auto",
},
isHome: {
type: Boolean,
default: true,
default: false,
},
showBackToGame: {
type: Boolean,
@@ -37,18 +40,11 @@ const props = defineProps({
type: Boolean,
default: true,
},
showBottom: {
type: Boolean,
default: true,
},
});
const isIOS = uni.getDeviceInfo().osName === "ios";
const showHint = ref(false);
const hintType = ref(0);
const capsuleHeight = ref(0);
const isLoading = ref(false);
const audioInitProgress = ref(1);
const audioProgress = ref(0);
const audioTimer = ref(null);
const showGlobalHint = (type) => {
hintType.value = type;
@@ -59,65 +55,53 @@ const hideGlobalHint = () => {
showHint.value = false;
};
const restart = () => {
uni.restartMiniProgram({
path: "/pages/index",
});
};
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);
onMounted(() => {
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
capsuleHeight.value = menuBtnInfo.top - 9;
});
onShow(() => {
uni.$showHint = showGlobalHint;
uni.$hideHint = hideGlobalHint;
uni.$checkAudio = checkAudioProgress;
showHint.value = false;
});
const backToGame = debounce(async () => {
if (isLoading.value) return; // 防止重复点击
try {
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({
title: "没有进行中的对局",
icon: "none",
title: '没有进行中的对局',
icon: 'none'
});
}
showHint.value = false;
} catch (error) {
console.error("获取当前游戏失败:", error);
console.error('获取当前游戏失败:', error);
uni.showToast({
title: error.message || '网络请求失败,请重试',
icon: 'none'
});
} finally {
isLoading.value = false;
}
@@ -126,39 +110,26 @@ const backToGame = debounce(async () => {
const goBack = () => {
uni.navigateBack();
};
const goCalibration = async () => {
await laserAimAPI();
uni.navigateTo({
url: "/pages/calibration",
});
};
</script>
<template>
<view :style="{ paddingTop: capsuleHeight + 'px' }">
<AppBackground :type="bgType" :bgColor="bgColor" />
<Header :title="title" :onBack="onBack" :whiteBackArrow="whiteBackArrow" />
<Header
v-if="!isHome"
:title="title"
:onBack="onBack"
:whiteBackArrow="whiteBackArrow"
/>
<BackToGame v-if="showBackToGame" />
<scroll-view
:scroll-y="scroll"
:enhanced="true"
:bounces="false"
:show-scrollbar="false"
<view
class="content"
:style="{
height: `calc(100vh - ${capsuleHeight + 50}px - ${
$slots.bottom && showBottom ? (isIOS ? '75px' : '65px') : '0px'
})`,
height: `calc(100vh - ${capsuleHeight + (isHome ? 0 : 50)}px)`,
overflow,
}"
>
<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>
<ScreenHint :show="showHint">
<view v-if="hintType === 1" class="tip-content">
@@ -168,8 +139,12 @@ const goCalibration = async () => {
<button hover-class="none" @click="() => (showHint = false)">
不进入
</button>
<button hover-class="none" @click="backToGame" :disabled="isLoading">
{{ isLoading ? "加载中..." : "进入" }}
<button
hover-class="none"
@click="backToGame"
:disabled="isLoading"
>
{{ isLoading ? '加载中...' : '进入' }}
</button>
</view>
</view>
@@ -192,38 +167,21 @@ const goCalibration = async () => {
<button hover-class="none" @click="goBack">确认</button>
</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>
<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>
</template>
<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 {
flex-direction: column;
display: flex;
@@ -261,62 +219,4 @@ const goCalibration = async () => {
color: #666;
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>

View File

@@ -1,14 +1,7 @@
<script setup>
import { ref } from "vue";
import SButton from "@/components/SButton.vue";
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);
import { joinRoomAPI, createRoomAPI, isGamingAPI } from "@/apis";
const props = defineProps({
onConfirm: {
@@ -18,11 +11,13 @@ const props = defineProps({
});
const battleMode = ref(1);
const step = ref(1);
const loading = ref(false);
const roomNumber = ref("");
const createRoom = debounce(async () => {
if (game.value.inBattle) {
const createRoom = async () => {
const isGaming = await isGamingAPI();
if (isGaming) {
uni.$showHint(1);
return;
}
@@ -36,46 +31,75 @@ const createRoom = debounce(async () => {
battleMode.value === 2 ? 2 : 1,
battleMode.value === 2 ? 10 : size
);
if (result.number) {
props.onConfirm();
await joinRoomAPI(result.number);
uni.navigateTo({
url: "/pages/battle-room?roomNumber=" + result.number,
});
}
if (result.number) roomNumber.value = result.number;
step.value = 2;
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>
<template>
<view class="container">
<image src="../static/choose-battle-mode.png" mode="widthFix" />
<view class="create-options">
<image
v-if="step === 1"
src="../static/choose-battle-mode.png"
mode="widthFix"
/>
<view v-if="step === 1" class="create-options">
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 1 }"
@click="() => (battleMode = 1)"
>
<text>对抗模式1V1</text>
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 2 }"
@click="() => (battleMode = 2)"
>
<text>乱斗模式3-10</text>
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 3 }"
@click="() => (battleMode = 3)"
>
<text>对抗模式2V2</text>
<!-- <text>敬请期待</text> -->
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 4 }"
@click="() => (battleMode = 4)"
>
<text>对抗模式3V3</text>
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 2 }"
@click="() => (battleMode = 2)"
>
<text>乱斗模式3-10</text>
<!-- <text>敬请期待</text> -->
</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>
</template>
@@ -118,4 +142,42 @@ const createRoom = debounce(async () => {
border: 4rpx solid #fff3;
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>

View File

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

View File

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

View File

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

View File

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

@@ -1,142 +0,0 @@
<script setup>
import { ref, onMounted } from "vue";
const props = defineProps({});
onMounted(async () => {});
</script>
<template>
<view class="live-item">
<view>
<image src="../static/user-icon-dark.png" mode="widthFix" />
</view>
<view>
<text>陈百强姓陈</text>
<view>
<text>节奏迅猛</text>
<text>节奏迅猛</text>
</view>
<view class="live-item-avatars">
<view :style="{ width: `${25 * 5 + 25}rpx` }">
<image
v-for="(avatar, i) in [1, 2, 3, 4, 5]"
:key="avatar"
:src="'../static/user-icon.png'"
:style="{
zIndex: i,
transform: `translateX(-${10 * i}rpx)`,
}"
/>
</view>
<text>观战中...</text>
</view>
</view>
<view>
<view>
<image src="../static/race-title-bg.png" mode="widthFix" />
<text>1v1对抗</text>
</view>
<view>
<text>本轮环数</text>
<text>62</text>
</view>
</view>
</view>
</template>
<style scoped>
.live-item {
display: flex;
align-items: center;
background: #252831;
padding: 25rpx;
border-radius: 25rpx;
}
.live-item > view:nth-child(1) {
width: 150rpx;
height: 150rpx;
}
.live-item > view:nth-child(1) > image {
width: 100%;
height: 100%;
border-radius: 25rpx;
}
.live-item > view:nth-child(2) {
flex: 1;
display: flex;
flex-direction: column;
margin: 0 20rpx;
}
.live-item > view:nth-child(2) > text {
font-weight: 500;
font-size: 28rpx;
color: #ffffff;
}
.live-item > view:nth-child(2) > view:nth-child(2) {
font-weight: 500;
font-size: 20rpx;
color: #ffa61b;
margin-top: 10rpx;
margin-bottom: 20rpx;
}
.live-item > view:nth-child(2) > view:nth-child(2) > text {
line-height: 28rpx;
border-radius: 15rpx;
border: 2rpx solid #ffc05d;
padding: 0 10rpx;
margin-right: 10rpx;
}
.live-item-avatars {
display: flex;
align-items: center;
font-size: 20rpx;
color: #999999;
}
.live-item-avatars > view {
display: flex;
align-items: center;
}
.live-item-avatars > view > image {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
flex-shrink: 0;
border: 1rpx solid #000;
box-sizing: border-box;
}
.live-item > view:nth-child(3) {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: space-around;
height: 150rpx;
}
.live-item > view:nth-child(3) > view:first-child {
display: flex;
align-items: center;
font-size: 24rpx;
color: #c46e36;
transform: translateX(100rpx);
}
.live-item > view:nth-child(3) > view:first-child > text {
transform: translateX(-90rpx);
}
.live-item > view:nth-child(3) > view:first-child > image {
width: 160rpx;
height: 50rpx;
}
.live-item > view:nth-child(3) > view:last-child {
display: flex;
align-items: flex-end;
font-size: 20rpx;
color: #999999;
}
.live-item > view:nth-child(3) > view:last-child > text:last-child {
font-weight: 600;
font-size: 48rpx;
color: #ffd299;
margin-left: 10rpx;
line-height: 40rpx;
}
</style>

View File

@@ -11,94 +11,91 @@ const props = defineProps({
},
});
const playerNames = [
"乐正青山",
"宇文玉兰",
"岑思宇",
"邬梓瑜",
"范子衿",
"彭妮·希利",
"埃琳娜·奥西波娃",
"凯西·考夫霍尔德",
"旗鼓相当的对手",
"乐子睿",
"时春晓",
"柏孤鸿",
"东宫锦瑟",
"段干流云",
];
const textStyles = [
{
color: "#fff9",
fontSize: "18px",
},
{
color: "#fff",
fontSize: "22px",
},
{
color: "#fed847",
fontSize: "30px",
},
"马乌罗·内斯波利",
"埃琳娜·奥西波娃",
"凯西·考夫霍尔德",
];
const rowHeight = 100 / 7;
const totalHeight = (playerNames.length / 7) * 100 + 7;
const currentTop = ref(-totalHeight + rowHeight * 0);
const totalTop = ref(0);
const timer = ref(null);
const textStyles = ref([]);
const getTextStyle = (top, index) => {
const count = Math.floor(
((totalHeight + (top + rowHeight / 3)) / rowHeight).toFixed(1)
);
if (index === 12 - count) return textStyles[0];
else if (index === 13 - count) return textStyles[1];
else if (index === 14 - count) return textStyles[2];
else if (index === 15 - count) return textStyles[1];
else if (index === 16 - count) return textStyles[0];
return {
const getTextStyle = (top) => {
const styles = [
{
color: "#fff9",
fontSize: "20px",
},
{
color: "#fff",
fontSize: "24px",
},
{
color: "#fed847",
fontSize: "30px",
},
];
const data = new Array(14).fill({
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(
() => props.onComplete,
(newVal) => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
(newVal, oldVal) => {
if (newVal && !oldVal) {
if (timer.value) clearInterval(timer.value);
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(() => {
timer.value = setInterval(() => {
if (currentTop.value >= -4) {
currentTop.value = -totalHeight;
if (totalTop.value === 100) {
totalTop.value = 0;
} else {
currentTop.value += 2;
totalTop.value += 2;
}
textStyles.value = getTextStyle(totalTop.value);
}, 40);
});
onBeforeUnmount(() => {
if (timer.value) clearInterval(timer.value);
timer.value = null;
});
</script>
@@ -110,13 +107,30 @@ onBeforeUnmount(() => {
class="matching-bg"
/>
<view>
<view class="player-names" :style="{ top: `${currentTop}%` }">
<view
class="player-names"
:style="{
top: `${totalTop - 100}%`,
}"
>
<text
v-for="(name, index) in [...playerNames, ...playerNames]"
v-for="(name, index) in playerNames"
:key="index"
:style="{
lineHeight: `${rowHeight}vw`,
...getTextStyle(currentTop, index),
lineHeight: `${95 / 7}vw`,
...(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 }}
@@ -142,7 +156,7 @@ onBeforeUnmount(() => {
height: 95vw;
overflow: hidden;
position: absolute;
top: 30vw;
top: 30.5vw;
}
.matching-bg {
position: absolute;
@@ -162,6 +176,7 @@ onBeforeUnmount(() => {
}
.player-names {
width: 100%;
height: 95vw;
display: flex;
flex-direction: column;
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,204 +0,0 @@
<script setup>
import { ref, watch } from "vue";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { getLvlName } = store;
const { config, user } = storeToRefs(store);
const props = defineProps({});
const nextLvlPoints = ref(0);
watch(
() => [config.value, user.value],
([n_config, n_user]) => {
const rankInfos = n_config.randInfos || [];
if (n_user.id && rankInfos.length) {
rankInfos.some((r, index) => {
if (r.upgrade_scores && r.upgrade_scores > n_user.scores) {
nextLvlPoints.value = r.upgrade_scores;
return true;
}
return false;
});
}
},
{
immediate: true,
deep: true,
}
);
</script>
<template>
<view class="my-rank">
<view>
<image :src="user.avatar || '../static/user-icon.png'" mode="widthFix" />
<view>
<text class="score color1">积分榜</text>
<text class="score color2">Mvp榜</text>
<text class="score color3">十环榜</text>
</view>
</view>
<view>
<text class="truncate">{{ user.nickName }}</text>
<image class="user-name-image" src="../static/vip1.png" mode="widthFix" />
</view>
<view>
<text>lv{{ user.lvl }}</text>
<view>
<view
:style="{
width: `${(Math.max(user.scores, 0) / nextLvlPoints) * 100}%`,
}"
></view>
<text>{{ Math.max(user.scores, 0) }}/{{ nextLvlPoints }}</text>
</view>
</view>
<view>
<view>
<text>段位</text>
<text>{{ user.rankLvl ? getLvlName(user.rankLvl) : "暂无" }}</text>
</view>
<view>
<text>平均环数</text>
<text>{{ user.avg_ring ? user.avg_ring + "环" : "暂无" }}</text>
</view>
<view>
<text>胜率</text>
<text>{{
user.avg_win ? Number((user.avg_win * 100).toFixed(2)) + "%" : "暂无"
}}</text>
</view>
</view>
</view>
</template>
<style scoped>
.my-rank {
display: flex;
flex-direction: column;
padding-left: 40rpx;
}
.my-rank > view:first-child {
display: flex;
align-items: center;
}
.my-rank > view:first-child > image {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
margin-right: 30rpx;
}
.my-rank > view:first-child > view {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.score {
line-height: 40rpx;
font-size: 20rpx;
color: #fff;
position: relative;
z-index: 1;
}
.score::before {
content: "";
width: calc(100% + 20rpx);
height: 10rpx;
border-radius: 10rpx;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
z-index: -1;
}
.color1::before {
background-color: #c9ff7e;
}
.color2::before {
background-color: #fff14b;
}
.color3::before {
background-color: #7efff7;
}
.my-rank > view:nth-child(2) {
display: flex;
align-items: center;
justify-content: flex-start;
margin-top: 10rpx;
}
.my-rank > view:nth-child(2) > text {
font-weight: 500;
font-size: 26rpx;
color: #ffffff;
margin-right: 20rpx;
max-width: 80%;
}
.my-rank > view:nth-child(2) > image {
width: 40rpx;
height: 40rpx;
}
.my-rank > view:nth-child(3) {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10rpx;
font-weight: 500;
font-size: 16rpx;
color: #ffffff;
}
.my-rank > view:nth-child(3) > text:first-child {
width: 80rpx;
height: 20rpx;
background: #978eff;
border-radius: 16rpx;
line-height: 22rpx;
text-align: center;
text-align: center;
}
.my-rank > view:nth-child(3) > view {
position: relative;
width: calc(100% - 100rpx);
height: 20rpx;
background: #c8c8c8;
border-radius: 16rpx;
}
.my-rank > view:nth-child(3) > view > view {
background-color: #c7a670;
height: 20rpx;
border-radius: 16rpx;
}
.my-rank > view:nth-child(3) > view > text {
width: 100%;
text-align: center;
display: block;
position: absolute;
top: 0;
left: 0;
}
.my-rank > view:nth-child(4) {
display: grid;
grid-template-columns: repeat(3, 1fr);
column-gap: 20rpx;
margin-top: 20rpx;
}
.my-rank > view:nth-child(4) > view {
height: 112rpx;
background: #f6f6f61a;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 20rpx;
color: #999999;
line-height: 40rpx;
}
.my-rank > view:nth-child(4) > view > text:last-child {
color: #fff;
font-size: 22rpx;
}
</style>

View File

@@ -1,98 +0,0 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const { user } = storeToRefs(useStore());
const props = defineProps({});
const list = [1, 2, 3, 4, 5];
const timer = ref(null);
const offset = ref(0);
onMounted(async () => {
timer.value = setInterval(() => {
if (offset.value <= -100 * list.length) {
offset.value = 100;
} else {
offset.value -= 0.2;
}
}, 10);
});
onBeforeUnmount(() => {
clearInterval(timer.value);
});
</script>
<template>
<view class="notice-bar">
<image src="../static/announce.png" mode="heightFix" />
<view>
<view
v-for="(item, index) in list"
:key="item"
:style="{
transform: `translateX(${offset}%)`,
}"
>
<image src="../static/user-icon.png" mode="widthFix" />
<text class="truncate">{{ index + 1 }}毛毛丛</text>
<text>成功晋升</text>
<text>荣耀王者</text>
<text>段位从此横着走</text>
</view>
</view>
<image src="../static/enter.png" mode="widthFix" />
</view>
</template>
<style scoped>
.notice-bar {
margin-top: 10rpx;
background: linear-gradient(180deg, #2f2d2b 0%, #252831 100%);
border-radius: 25rpx;
height: 80rpx;
display: flex;
align-items: center;
}
.notice-bar > image:first-child {
height: 100%;
}
.notice-bar > view {
flex: 1;
overflow: hidden;
display: flex;
align-items: center;
}
.notice-bar > view > view {
flex-shrink: 0;
font-size: 24rpx;
padding-left: 15rpx;
color: #999;
display: flex;
align-items: center;
}
.notice-bar > view > view > image {
width: 48rpx;
height: 48rpx;
margin-right: 10rpx;
border-radius: 50%;
}
.notice-bar > view > view > text {
word-break: keep-all;
display: inline-block;
}
.notice-bar > view > view > text:nth-child(2) {
color: #fff;
width: 100rpx;
display: block;
}
.notice-bar > view > view > text:nth-child(4) {
color: #e7ba80;
}
.notice-bar > image:last-child {
width: 30rpx;
height: 30rpx;
margin: 0 20rpx;
}
</style>

View File

@@ -1,34 +1,30 @@
<script setup>
import useStore from "@/store";
import { storeToRefs } from "pinia";
const { user } = storeToRefs(useStore());
defineProps({
player: {
type: Object,
default: () => ({}),
avatar: {
type: String,
default: "",
},
name: {
type: String,
default: "",
},
scores: {
type: Array,
default: () => [],
},
});
const rowCount = new Array(6).fill(0);
</script>
<template>
<view
class="container"
:style="{ borderColor: player.id === user.id ? '#FED847' : '#fff3' }"
>
<view class="container">
<image
:style="{ opacity: scores.length === 12 ? 1 : 0 }"
src="../static/checked-green.png"
mode="widthFix"
/>
<image :src="player.avatar || '../static/user-icon.png'" mode="widthFix" />
<text>{{ player.name }}</text>
<image :src="avatar || '../static/user-icon.png'" mode="widthFix" />
<text>{{ name }}</text>
<view>
<view>
<view v-for="(_, index) in rowCount" :key="index">
@@ -108,6 +104,5 @@ const rowCount = new Array(6).fill(0);
.container > text:nth-child(5) {
width: 40px;
text-align: right;
word-break: keep-all;
}
</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>
import { ref, onMounted, computed } from "vue";
import { ref, onMounted } from "vue";
const props = defineProps({
data: {
@@ -11,19 +11,6 @@ const bowOptions = ref({});
const targetOptions = ref({});
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({
url: `/pages/point-book-detail?id=${props.data.id}`,
});
@@ -42,7 +29,7 @@ onMounted(() => {
<template>
<view class="container" @click="toDetailPage">
<view class="left-part">
<view>
<view class="labels">
<view></view>
<text>{{
@@ -64,12 +51,12 @@ onMounted(() => {
<text>平均{{ data.averageRing }}</text>
</view>
</view>
<view class="right-part">
<view>
<image src="../static/bow-target.png" mode="widthFix" />
<view class="arrow-amount">
<text>{{ data.actualTotalRing }}</text>
<text>/</text>
<text>{{ data.totalRing }}</text>
<text></text>
<text>{{ data.arrows * data.groups }}</text>
<text></text>
</view>
</view>
</view>
@@ -83,13 +70,12 @@ onMounted(() => {
border-radius: 25rpx;
height: 200rpx;
border: 2rpx solid #fed848;
padding-left: 30rpx;
padding-right: 10rpx;
}
.container > view {
position: relative;
margin-left: 15px;
}
.left-part {
.container > view:first-child {
flex: 1;
display: flex;
flex-direction: column;
@@ -97,24 +83,20 @@ onMounted(() => {
height: calc(100% - 50rpx);
color: #333333;
}
.left-part > view {
.container > view:first-child > view {
width: 100%;
display: flex;
position: relative;
}
.left-part > view:nth-child(3) {
.container > view:first-child > view:nth-child(3) {
display: flex;
align-items: center;
font-size: 20rpx;
color: #666;
}
.left-part > view:nth-child(3) > text {
.container > view:first-child > view:nth-child(3) > text {
margin-right: 10rpx;
}
.right-part > image {
width: 180rpx;
height: 180rpx;
}
.labels {
align-items: flex-end !important;
}
@@ -132,21 +114,28 @@ onMounted(() => {
position: relative;
color: #333;
}
.container > view:last-child {
margin-right: 1vw;
}
.container > view:last-child > image {
width: 24vw;
}
.arrow-amount {
position: absolute;
background-color: #0009;
border-radius: 12px;
border-radius: 10px;
color: #fffc;
font-size: 24rpx;
line-height: 26px;
width: 64px;
font-size: 12px;
line-height: 22px;
width: 60px;
display: flex;
justify-content: center;
top: calc(50% - 15px);
left: calc(50% - 32px);
top: calc(50% - 13px);
left: calc(50% - 30px);
}
.arrow-amount > text:nth-child(1) {
font-size: 30rpx;
.arrow-amount > text:nth-child(2) {
color: #fff;
font-size: 14px;
margin: 0 3px;
}
</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 {
width: 200rpx;
position: absolute;
top: -112rpx;
top: -114rpx;
}
.container > text:nth-child(2) {
font-weight: 500;

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ const props = defineProps({
},
height: {
type: String,
default: "650rpx",
default: "260px",
},
onClose: {
type: Function,
@@ -56,7 +56,7 @@ watch(
>
<image
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"
/>
<view class="close-btn" @click="onClose" v-if="!noBg">
@@ -81,7 +81,7 @@ watch(
align-items: center;
opacity: 0;
transition: all 0.3s ease;
z-index: 999;
z-index: 99;
}
.modal-content {
width: 100%;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,13 @@
<script setup>
import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import SModal from "@/components/SModal.vue";
import Avatar from "@/components/Avatar.vue";
import SButton from "@/components/SButton.vue";
import { wxLogin } from "@/util";
import {
getMyDevicesAPI,
loginAPI,
getHomeData,
getPhoneNumberAPI,
getDeviceBatteryAPI,
} from "@/apis";
import { getMyDevicesAPI, loginAPI, getHomeData } from "@/apis";
import useStore from "@/store";
const store = useStore();
const { updateUser, updateDevice, updateOnline } = store;
const { updateUser, updateDevice } = store;
const props = defineProps({
show: {
type: Boolean,
default: false,
},
noBg: {
type: Boolean,
default: false,
@@ -33,7 +18,6 @@ const props = defineProps({
},
});
const agree = ref(false);
const phone = ref("");
const avatarUrl = ref("");
const nickName = ref("");
const loading = ref(false);
@@ -41,17 +25,6 @@ const handleAgree = () => {
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) {
avatarUrl.value = e.detail.avatarUrl;
}
@@ -60,14 +33,8 @@ function onNicknameChange(e) {
nickName.value = e.detail.value;
}
const handleLogin = async () => {
const handleLogin = () => {
if (loading.value) return;
if (!phone.value) {
return uni.showToast({
title: "请获取手机号",
icon: "none",
});
}
if (!avatarUrl.value) {
return uni.showToast({
title: "请选择头像",
@@ -87,28 +54,34 @@ const handleLogin = async () => {
});
}
loading.value = true;
const wxResult = await wxLogin();
const fileManager = uni.getFileSystemManager();
const avatarBase64 = fileManager.readFileSync(avatarUrl.value, "base64");
const base64Url = `data:image/png;base64,${avatarBase64}`;
const result = await loginAPI(
phone.value,
nickName.value,
base64Url,
wxResult.code
);
const data = await getHomeData();
if (data.user) updateUser(data.user);
const devices = await getMyDevicesAPI();
if (devices.bindings && devices.bindings.length) {
updateDevice(devices.bindings[0].deviceId, devices.bindings[0].deviceName);
try {
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
} catch (error) {}
}
loading.value = false;
props.onClose();
uni.login({
provider: "weixin",
success: async (loginRes) => {
const { code } = loginRes;
const fileManager = uni.getFileSystemManager();
const avatarBase64 = fileManager.readFileSync(avatarUrl.value, "base64");
const base64Url = `data:image/png;base64,${avatarBase64}`;
const result = await loginAPI(nickName.value, base64Url, code);
const data = await getHomeData();
if (data.user) updateUser(data.user);
const devices = await getMyDevicesAPI();
if (devices.bindings && devices.bindings.length) {
updateDevice(
devices.bindings[0].deviceId,
devices.bindings[0].deviceName
);
}
props.onClose();
},
fail: (err) => {
loading.value = false;
uni.showToast({
title: "登录失败",
icon: "none",
});
console.error("登录失败:", err);
},
});
};
const openServiceLink = () => {
@@ -137,90 +110,68 @@ onShow(() => {
</script>
<template>
<SModal :show="show" :onClose="onClose" :noBg="noBg">
<view class="container" :style="{ background: noBg ? '#fff' : 'none' }">
<view class="avatar" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff3' }">
<text :style="{ color: noBg ? '#666' : '#fff' }">手机:</text>
<button
:open-type="!phone ? 'getPhoneNumber' : ''"
@getphonenumber="getphonenumber"
class="login-btn"
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' }"
<view class="container" :style="{ background: noBg ? '#fff' : 'none' }">
<view class="avatar" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff' }">
<text :style="{ color: noBg ? '#666' : '#fff' }">头像:</text>
<button
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
class="login-btn"
hover-class="none"
>
<text :style="{ color: noBg ? '#666' : '#fff' }">昵称:</text>
<input
type="nickname"
placeholder="请输入昵称"
:placeholder-style="`color: ${noBg ? '#666' : '#fff9'} `"
@change="onNicknameChange"
@blur="onNicknameBlur"
:style="{ color: noBg ? '#333' : '#fff' }"
<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' : '#fff' }">
<text :style="{ color: noBg ? '#666' : '#fff' }">昵称:</text>
<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>
<SButton :rounded="20" width="80vw" :onClick="handleLogin">
<block v-if="!loading">
<text :style="{ color: '#000' }">手机号快捷登录</text>
</block>
<block v-else>
<image
src="../static/btn-loading.png"
mode="widthFix"
class="loading"
/>
</block>
</SButton>
<view class="protocol" @click="handleAgree">
<text :style="{ color: '#000' }">登录/注册</text>
</block>
<block v-else>
<image
src="../static/btn-loading.png"
mode="widthFix"
class="loading"
/>
</block>
</SButton>
<view class="protocol" @click="handleAgree">
<view v-if="!agree" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff' }" />
<image v-if="agree" src="../static/checked.png" mode="widthFix" />
<view>
<text>已同意并阅读</text>
<view
v-if="!agree"
:style="{ borderColor: noBg ? '#E3E3E3' : '#fff' }"
/>
<image v-if="agree" src="../static/checked.png" mode="widthFix" />
<view>
<text>已同意并阅读</text>
<view
@click.stop="openServiceLink"
:style="{ color: noBg ? '#333' : '#ffffff99' }"
>用户协议</view
>
<text></text>
<view
@click.stop="openPrivacyLink"
:style="{ color: noBg ? '#333' : '#ffffff99' }"
>隐私协议</view
>
<text>内容</text>
</view>
@click.stop="openServiceLink"
:style="{ color: noBg ? '#333' : '#fff' }"
>用户协议</view
>
<text></text>
<view
@click.stop="openPrivacyLink"
:style="{ color: noBg ? '#333' : '#fff' }"
>隐私协议</view
>
<text>内容</text>
</view>
</view>
</SModal>
</view>
</template>
<style scoped>
@@ -240,7 +191,7 @@ onShow(() => {
display: flex;
align-items: center;
margin-bottom: 20px;
border-bottom: 1rpx solid #ffffff1a;
border-bottom: 1rpx solid #fff3;
}
.avatar {
margin: 0;
@@ -249,7 +200,7 @@ onShow(() => {
.nickname > text {
width: 20%;
font-size: 14px;
line-height: 120rpx;
line-height: 55px;
}
.avatar > button > text {
color: #fff9;
@@ -258,7 +209,7 @@ onShow(() => {
.nickname > input {
flex: 1;
font-size: 14px;
line-height: 120rpx;
line-height: 55px;
}
.wechat-icon {
width: 24px;
@@ -269,8 +220,8 @@ onShow(() => {
display: flex;
justify-content: center;
align-items: center;
font-size: 22rpx;
margin: 30rpx 0;
font-size: 13px;
margin-top: 15px;
color: #8a8a8a;
}
.protocol > image {
@@ -283,7 +234,7 @@ onShow(() => {
height: 14px;
border-radius: 50%;
margin-right: 10px;
border: 1px solid #fff;
border: 1rpx solid #fff;
}
.protocol > view:last-child {
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>
import { ref, watch, onMounted, computed } from "vue";
import { ref, watch, onMounted } from "vue";
const props = defineProps({
isRed: {
type: Boolean,
@@ -13,23 +13,15 @@ const props = defineProps({
type: Number,
default: "",
},
youTurn: {
type: Boolean,
default: false,
},
});
const players = ref({});
const currentTeam = ref(false);
const youTurn = ref(false);
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(() => {
props.team.forEach((p, index) => {
players.value[p.id] = { sort: index, ...p };
@@ -41,7 +33,7 @@ watch(
(newVal) => {
if (!newVal) return;
const index = props.team.findIndex((p) => p.id === newVal);
currentTeam.value = index >= 0;
youTurn.value = index >= 0;
if (index >= 0) {
const newPlayers = [...props.team];
const target = newPlayers.splice(index, 1)[0];
@@ -63,38 +55,39 @@ watch(
<image
:src="isRed ? '../static/flag-red.png' : '../static/flag-blue.png'"
class="flag"
:style="{
[isRed ? 'left' : 'right']: '10rpx',
top: currentTeam ? '-36rpx' : '-24rpx',
}"
:style="{ [isRed ? 'left' : 'right']: '10rpx' }"
/>
<view
v-for="(item, index) in team"
:key="index"
class="player"
:style="{
width: (isFirst(item.id) ? 80 : 60) + 'rpx',
height: (isFirst(item.id) ? 80 : 60) + 'rpx',
zIndex: team.length - ((players[item.id] || {}).sort || 0),
border: isFirst(item.id) ? '3.5rpx solid' : '2rpx solid',
width:
(youTurn ? 40 - ((players[item.id] || {}).sort || 0) * 5 : 35) + 'px',
height:
(youTurn ? 40 - ((players[item.id] || {}).sort || 0) * 5 : 35) + 'px',
borderColor: isRed ? '#ff6060' : '#5fadff',
top: isFirst(item.id) ? '0rpx' : '12rpx',
[isRed ? 'left' : 'right']: getPos(item.id) + 'rpx',
zIndex: team.length - ((players[item.id] || {}).sort || 0),
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" />
<text
v-if="isFirst(item.id)"
v-if="youTurn && ((players[item.id] || {}).sort || 0) === 0"
:style="{ backgroundColor: isRed ? '#ff6060' : '#5fadff' }"
>{{ isRed ? "红队" : "蓝队" }}</text
>
</view>
<text
v-if="currentTeam"
v-if="youTurn"
class="truncate"
:style="{
color: isRed ? '#ff6060' : '#5fadff',
[isRed ? 'left' : 'right']: '-4rpx',
[isRed ? 'left' : 'right']: 0,
}"
>{{ firstName }}</text
>
@@ -107,22 +100,22 @@ watch(
align-items: center;
position: relative;
width: 20vw;
height: 10rpx;
height: 45px;
margin: 0 20rpx;
}
.container > text {
position: absolute;
font-size: 20rpx;
font-size: 10px;
text-align: center;
width: 80rpx;
bottom: -100rpx;
width: 40px;
bottom: -12px;
}
.player {
transition: all 0.3s ease;
position: absolute;
border-radius: 50%;
overflow: hidden;
box-sizing: border-box;
border: 1px solid;
}
.player > image {
width: 100%;
@@ -130,17 +123,17 @@ watch(
}
.player > text {
position: absolute;
font-size: 15rpx;
font-size: 8px;
text-align: center;
width: 76rpx;
left: 0;
bottom: 0;
width: 40px;
left: 0px;
bottom: 0px;
color: #fff;
}
.flag {
position: absolute;
width: 45rpx;
height: 45rpx;
transition: all 0.3s ease;
top: -30rpx;
}
</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 audioManager from "@/audioManager";
import { simulShootAPI } from "@/apis";
import { checkConnection } from "@/util";
import { MESSAGETYPES } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
@@ -21,6 +22,7 @@ const props = defineProps({
},
});
const arrow = ref({});
const power = ref(0);
const distance = ref(0);
const debugInfo = ref("");
const showsimul = ref(false);
@@ -47,6 +49,7 @@ async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
arrow.value = msg.target;
power.value = msg.target.battery;
distance.value = Number((msg.target.dst / 100).toFixed(2));
debugInfo.value = msg.target;
audioManager.play("距离合格");
@@ -62,6 +65,7 @@ const simulShoot = async () => {
};
onMounted(() => {
checkConnection();
uni.$on("socket-inbox", onReceiveMessage);
const accountInfo = uni.getAccountInfoSync();
const envVersion = accountInfo.miniProgram.envVersion;
@@ -76,9 +80,18 @@ onBeforeUnmount(() => {
<template>
<view class="container">
<Guide v-show="guide">
<view class="guide-tips">
<text>请确保站距达到5米</text>
<text>低于5米的射箭无效</text>
<view
:style="{
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>
</Guide>
<view class="test-area">
@@ -107,7 +120,7 @@ onBeforeUnmount(() => {
</view>
<view class="user-row">
<Avatar :src="user.avatar" :size="35" />
<BowPower />
<BowPower :power="power" />
</view>
</view>
<view v-if="isBattle" class="ready-timer">

View File

@@ -1,162 +0,0 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
const props = defineProps({
data: {
type: Array,
default: () => [],
},
});
const titles = ["积分榜", "MVP榜", "十环榜"];
const selected = ref(0);
const timer = ref(0);
const onClick = (index) => {
selected.value = index;
clearInterval(timer.value);
timer.value = setInterval(() => {
selected.value = (selected.value + 1) % titles.length;
}, 3000);
};
onMounted(async () => {
timer.value = setInterval(() => {
selected.value = (selected.value + 1) % titles.length;
}, 3000);
});
onBeforeUnmount(() => {
clearInterval(timer.value);
});
</script>
<template>
<view class="top-rank">
<view>
<button
hover-class="none"
v-for="(name, index) in titles"
:key="index"
@click="onClick(index)"
:style="{ color: selected === index ? '#E7BA80' : '#999' }"
>
{{ name }}
</button>
</view>
<swiper
:current="selected"
@change="(e) => (selected = e.detail.current)"
:style="{ width: '100%' }"
>
<swiper-item v-for="(list, index1) in data" :key="index1">
<view v-for="(item, index2) in list" :key="index2" class="rank-item">
<view>
<text>{{ index2 + 1 }}</text>
<image
:src="item.avatar || '../static/user-icon-dark.png'"
mode="widthFix"
/>
</view>
<view>
<text>{{ item.name }}</text>
<text>积分{{ item.totalScore }}</text>
</view>
</view>
</swiper-item>
</swiper>
<view>
<view
v-for="(item, index) in data"
:key="index"
:style="{
width: selected === index ? '20rpx' : '6rpx',
backgroundColor: selected === index ? '#656565;' : '#e4e4e4',
}"
/>
</view>
</view>
</template>
<style scoped>
.top-rank {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.top-rank > view:first-child {
display: flex;
align-items: center;
}
.top-rank > view:first-child > button {
margin-right: 20rpx;
font-weight: 600;
font-size: 24rpx;
}
.rank-item {
display: flex;
align-items: center;
height: 80rpx;
margin-top: 20rpx;
margin-left: 1rpx;
}
.rank-item > view:first-child {
position: relative;
margin-right: 20rpx;
}
.rank-item > view:first-child > text {
position: absolute;
top: 0;
left: 0;
width: 28rpx;
height: 28rpx;
line-height: 28rpx;
text-align: center;
background: #ffa711;
border-radius: 14rpx 0rpx 14rpx 0rpx;
color: #fff;
font-weight: 600;
font-size: 20rpx;
}
.rank-item > view:first-child > image {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
}
.rank-item > view:last-child {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: flex-start;
height: 100%;
}
.rank-item > view:last-child > text:first-child {
font-weight: 500;
font-size: 24rpx;
color: #ffffff;
line-height: 34rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180rpx;
}
.rank-item > view:last-child > text:last-child {
background: rgb(255 250 234 / 0.1);
border-radius: 16rpx;
font-size: 20rpx;
color: #e7ba80;
line-height: 28rpx;
padding: 5rpx 10rpx;
}
.top-rank > view:last-child {
width: 100%;
display: flex;
justify-content: center;
margin-top: 20rpx;
}
.top-rank > view:last-child > view {
height: 6rpx;
border-radius: 3rpx;
background: #e4e4e4;
margin: 0 5rpx;
transition: all 0.3s ease;
}
</style>

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,24 @@
<script setup>
import { ref, onMounted } from "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>
<template>
@@ -17,7 +34,8 @@ const isIOS = uni.getDeviceInfo().osName === "ios";
<view
class="copyright"
:style="{ paddingBottom: isIOS ? '40rpx' : '20rpx' }"
:style="{ paddingBottom: isIos ? '30rpx' : '20rpx' }"
@click="openLink"
>
<text>粤ICP备2025421150号-2X</text>
</view>
@@ -28,7 +46,7 @@ const isIOS = uni.getDeviceInfo().osName === "ios";
<style scoped>
.container {
width: calc(100% - 50rpx);
height: calc(100% - 50rpx);
height: 100%;
padding: 25rpx;
background-color: #ffffff;
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);
function exit() {
const battleInfo = uni.getStorageSync("last-battle");
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();
}
uni.navigateBack();
}
onLoad(async (options) => {
@@ -262,9 +251,7 @@ const checkBowData = () => {
src="../static/champ3.png"
mode="widthFix"
/>
<view v-if="player.rank > 3" class="view-crown">{{
player.rank
}}</view>
<view v-if="index > 2" class="view-crown">{{ index + 1 }}</view>
<Avatar
:src="player.avatar"
:size="36"
@@ -272,7 +259,7 @@ const checkBowData = () => {
/>
<view class="player-title">
<text class="truncate">{{ player.name }}</text>
<text>{{ getLvlName(player.rank_lvl) }}</text>
<text>{{ getLvlName(player.totalScore) }}</text>
</view>
<text
><text :style="{ color: '#fff' }">{{ player.totalRings }}</text>
@@ -321,7 +308,7 @@ const checkBowData = () => {
</text>
<view class="op-btn">
<view @click="checkBowData">查看成绩</view>
<view @click="exit">返回</view>
<view @click="exit">退出</view>
</view>
<UserUpgrade />
</view>
@@ -431,7 +418,6 @@ const checkBowData = () => {
border-radius: 20px;
padding: 10px 0;
text-align: center;
color: #000;
}
.op-btn > view:last-child {
color: #fff;

View File

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

View File

@@ -3,6 +3,7 @@ import { ref, onMounted, onBeforeUnmount } from "vue";
import Container from "@/components/Container.vue";
import Avatar from "@/components/Avatar.vue";
import SButton from "@/components/SButton.vue";
import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue";
import UserHeader from "@/components/UserHeader.vue";
import { createOrderAPI, getHomeData, getVIPDescAPI } from "@/apis";
@@ -79,73 +80,87 @@ onBeforeUnmount(() => {
<template>
<Container title="会员说明">
<view v-if="user.id" class="header">
<view>
<Avatar :src="user.avatar" :size="35" />
<text class="truncate">{{ user.nickName }}</text>
<image
class="user-name-image"
src="../static/vip1.png"
mode="widthFix"
/>
<view :style="{ width: '100%', height: '100%' }">
<view v-if="user.id" class="header">
<view>
<Avatar :src="user.avatar" :size="35" />
<text class="truncate">{{ user.nickName }}</text>
<image
class="user-name-image"
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>
<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
class="container"
:style="{ height: !user.id ? 'calc(100% - 10px)' : 'calc(100% - 62px)' }"
>
<view class="content vip-content">
<view class="title-bar">
<view />
<text>VIP 介绍</text>
<view
class="container"
:style="{ height: !user.id ? '100%' : 'calc(100% - 62px)' }"
>
<view class="content vip-content">
<view class="title-bar">
<view />
<text>VIP 介绍</text>
</view>
<view :style="{ marginTop: '10rpx' }">
<rich-text :nodes="richContent" />
<!-- <text
>射灵星球VIP服务为全球弓箭手提供约战段位评级实时排位赛智能教练点评等专属特权会员可在酷帅的真实射箭运动中同步享受在线竞技的乐趣还能找到志同道合的伙伴并获得新鲜的功能体验和持续升级的系统
</text>
<text
>所有新注册用户我们都会默认赠送6个月超长会员到期之后可续费单月10元年度VIP100元我们鼓励每一位弓箭手长期坚持练习这项运动在对战的世界中尽情驰骋不断挑战自我创造属于自己的辉煌战绩
</text>
<text
>VIP会员还将获得专属客服支持当您在游戏中遇到任何问题无论是技术故障规则疑问还是其他需要帮助的情况都可联系我们的VIP专属客服团队他们将提供全年不间断的优质服务确保您的对战体验不受影响
</text>
<text>期待您的加入</text> -->
</view>
</view>
<view :style="{ marginTop: '10rpx' }">
<rich-text :nodes="richContent" />
<view class="content">
<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 class="content">
<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 }}
<SButton :onClick="onPay">支付</SButton>
<SModal :show="showModal" :onClose="() => (showModal = false)">
<Signin :onClose="() => (showModal = false)" />
</SModal>
<view class="my-orders" v-if="user.id">
<view @click="toOrderPage">
<text>我的订单</text>
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
</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>
</Container>
</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 SButton from "@/components/SButton.vue";
import { capsuleHeight } from "@/util";
const images = [
"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/dcjmxsmgy8ej5wuap5.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-12-04/depguhlfr041aedqmb.jpg",
"https://static.shelingxingqiu.com/attachment/2025-12-04/depguhlpnlyxndnor5.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmfhqew0xhy6i.jpg",
"https://static.shelingxingqiu.com/attachment/2025-09-04/dcjmxsmhs38abrqfyp.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-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) => {
addBg.value = e.detail.scrollTop > 100;
@@ -32,7 +35,8 @@ const onScrollView = (e) => {
}"
>
<image
:style="{ opacity: addBg ? 1 : 0 }"
v-if="addBg"
class="bg-image"
src="../static/app-bg.png"
mode="widthFix"
/>
@@ -42,17 +46,12 @@ const onScrollView = (e) => {
<text
:style="{ opacity: addBg ? 1 : 0, color: '#fff', fontWeight: 'bold' }"
>
本赛季排行榜
</text>
</view>
<scroll-view scroll-y @scroll="onScrollView" :style="{ height: '100vh' }">
<view class="images">
<image
v-for="src in images"
:key="src"
:src="src"
mode="widthFix"
show-menu-by-longpress
/>
<image v-for="src in images" :key="src" :src="src" mode="widthFix" show-menu-by-longpress />
</view>
</scroll-view>
</view>
@@ -72,6 +71,7 @@ const onScrollView = (e) => {
align-items: center;
position: fixed;
top: 0;
transition: all 0.3s ease;
z-index: 10;
overflow: hidden;
}
@@ -82,19 +82,12 @@ const onScrollView = (e) => {
margin-top: 5px;
position: relative;
}
.header > image:first-child {
.bg-image {
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;
}
.images {
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 TestDistance from "@/components/TestDistance.vue";
import BubbleTip from "@/components/BubbleTip.vue";
import audioManager from "@/audioManager";
import { createPractiseAPI, getPractiseAPI } from "@/apis";
import { sharePractiseData } from "@/canvas";
import { wxShare, debounce } from "@/util";
import { createPractiseAPI } from "@/apis";
import { generateCanvasImage } from "@/util";
import { MESSAGETYPES } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
@@ -35,6 +33,7 @@ const stepButtonTexts = [
const title = ref("新手试炼场");
const start = ref(false);
const practiseResult = ref({});
const power = ref(0);
const btnDisabled = ref(false);
const practiseId = ref("");
const showGuide = ref(false);
@@ -57,42 +56,48 @@ const onSwiperIndexChange = (index) => {
};
const createPractise = async (arrows) => {
const result = await createPractiseAPI(arrows, 1);
const result = await createPractiseAPI(arrows);
if (result) practiseId.value = result.id;
};
const onOver = async () => {
start.value = false;
practiseResult.value = await getPractiseAPI(practiseId.value);
};
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
if (step.value === 2 && msg.target.dst / 100 >= 5) {
btnDisabled.value = false;
showGuide.value = true;
} else if (scores.value.length < total) {
if (scores.value.length < total) {
scores.value.push(msg.target);
}
if (scores.value.length === total) {
setTimeout(onOver, 1500);
power.value = msg.target.battery;
// 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(() => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("share-image", onClickShare);
});
onBeforeUnmount(() => {
@@ -100,8 +105,6 @@ onBeforeUnmount(() => {
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
uni.$off("share-image", onClickShare);
audioManager.stopAll();
});
const nextStep = async () => {
@@ -134,10 +137,10 @@ const nextStep = async () => {
};
const onClose = () => {
const validArrows = (practiseResult.value.arrows || []).filter(
(a) => a.x !== -30 && a.y !== -30
);
if (validArrows.length === total) {
if (
practiseResult.value.arrows &&
practiseResult.value.arrows.length === total
) {
setTimeout(() => {
practiseResult.value = {};
showGuide.value = false;
@@ -153,7 +156,7 @@ const onClose = () => {
</script>
<template>
<Container :bgType="1" :title="title" :showBottom="step !== 4">
<Container :bgType="1" :title="title">
<view class="container">
<Guide
v-if="step !== 4"
@@ -165,40 +168,37 @@ const onClose = () => {
: 0
"
>
<text
v-if="step === 0"
:style="{
fontSize: '28rpx',
marginTop: user.nickName.length > 6 ? '-10rpx' : '0',
}"
>
<text v-if="step === 0">
hi<text :style="{ color: '#fed847' }">{{ user.nickName }}</text>
这是新人必刷小任务0基础小白也能快速掌握弓箭技巧和游戏规则哦~
</text>
<text v-if="step === 1" :style="{ fontSize: '28rpx' }"
<text v-if="step === 1"
>这是我们人帅技高的高教练首先请按教练示范尝试自己去做这些动作和手势吧</text
>
<view
class="guide-tips"
:style="{ marginTop: '8rpx' }"
v-if="step === 2"
>
<text>你知道5米射程有多远吗</text>
<text>
在我们的排位赛中射程小于5米的成绩无效建议平时练习距离至少5米现在来边射箭边调整你的站位点吧
</text>
<view v-if="step === 2">
<view :style="{ display: 'flex', flexDirection: 'column' }">
<text :style="{ color: '#fed847' }">你知道5米射程有多远吗</text>
<text>
在我们的排位赛中射程小于5米的成绩无效建议平时练习距离至少5米现在来边射箭边调整你的站位点吧
</text>
</view>
</view>
<view class="guide-tips" v-if="step === 3">
<text>一切准备就绪</text>
<text :style="{ fontSize: '28rpx' }"
>试着完成一个真正的弓箭手任务吧</text
>
<view v-if="step === 3">
<view :style="{ display: 'flex', flexDirection: 'column' }">
<text :style="{ color: '#fed847' }">一切准备就绪</text>
<text>试着完成一个真正的弓箭手任务吧</text>
</view>
</view>
<view class="guide-tips" v-if="step === 5">
<text>新手试炼场通关啦优秀</text>
<text :style="{ fontSize: '28rpx' }"
>反曲弓运动基本知识和射灵世界系统规则你已Get是不是挺容易呀</text
<view v-if="step === 5">
<view
:style="{ display: 'flex', flexDirection: 'column', marginTop: 20 }"
>
<text :style="{ color: '#fed847' }">新手试炼场通关啦优秀</text>
<text
>反曲弓运动基本知识和射灵世界系统规则你已Get是不是挺容易呀</text
>
<text :style="{ opacity: 0 }">新手试炼场通关啦优秀</text>
</view>
</view>
</Guide>
<image
@@ -208,7 +208,7 @@ const onClose = () => {
v-if="step === 0"
/>
<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"
mode="widthFix"
v-if="step === 3"
@@ -230,7 +230,7 @@ const onClose = () => {
:style="{ marginBottom: step === 2 ? '40px' : '0' }"
>
<Avatar :src="user.avatar" :size="35" />
<BowPower />
<BowPower :power="power" />
</view>
<BowTarget
v-if="step === 4"
@@ -251,17 +251,13 @@ const onClose = () => {
:onClose="onClose"
:result="practiseResult"
:tipSrc="`../static/${
practiseResult.arrows.filter(
(arrow) => arrow.x !== -30 && arrow.y !== -30
).length < total
? 'un'
: ''
practiseResult.arrows.length < total ? 'un' : ''
}finish-tip.png`"
/>
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
<canvas class="share-canvas" canvas-id="shareCanvas"></canvas>
</view>
<template #bottom>
<SButton :onClick="nextStep" :disabled="btnDisabled">
<view :style="{ marginBottom: '20px' }">
<SButton v-if="step !== 4" :onClick="nextStep" :disabled="btnDisabled">
<BubbleTip v-if="showGuide" :type="step === 1 ? 'long' : 'short'">
<text :style="{ transform: 'translateY(-18rpx)' }">{{
step === 1 ? "学会了,我摆得比教练还帅" : "我找到合适的点位了"
@@ -269,7 +265,7 @@ const onClose = () => {
</BubbleTip>
{{ stepButtonTexts[step] }}
</SButton>
</template>
</view>
</Container>
</template>

View File

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

View File

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

@@ -1,52 +1,54 @@
<script setup>
import { ref, onMounted } from "vue";
import { onShow, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import AppFooter from "@/components/AppFooter.vue";
import AppBackground from "@/components/AppBackground.vue";
import UserHeader from "@/components/UserHeader.vue";
import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue";
import NoticeBar from "@/components/NoticeBar.vue";
import TopRank from "@/components/TopRank.vue";
import MyRank from "@/components/MyRank.vue";
import LiveItem from "@/components/LiveItem.vue";
import BubbleTip from "@/components/BubbleTip.vue";
import {
getAppConfig,
getRankListAPI,
getHomeData,
getMyDevicesAPI,
getDeviceBatteryAPI,
} from "@/apis";
import { canEenter, capsuleHeight } from "@/util";
import { topThreeColors } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { updateConfig, updateUser, updateDevice, updateRank, updateOnline } =
const { updateConfig, updateUser, updateDevice, updateRank, getLvlName } =
store;
const { user, device, rankData, online, game } = storeToRefs(store);
// 使用storeToRefs用于UI里显示保持响应性
const { user, device, rankData } = storeToRefs(store);
const showModal = ref(false);
const selected = ref(0);
const liveType = ref(0);
const liveTypes = {
大神赛: "../static/dashen.png",
新人王: "../static/xinren.png",
女神赛: "../static/nvshen.png",
};
const ranks = ref([]);
const showGuide = ref(false);
const toPage = async (path) => {
const toPage = (path) => {
if (!user.value.id) {
showModal.value = true;
return;
}
if (path === "/pages/first-try") {
if (canEenter(user.value, device.value, online.value, path)) {
await uni.$checkAudio();
} else {
return;
if (
"/pages/first-try,/pages/practise,/pages/friend-battle".indexOf(path) !== -1
) {
if (!device.value.deviceId) {
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 = () => {
@@ -68,30 +70,24 @@ onShow(async () => {
const [rankList, homeData] = await Promise.all(promises);
console.log("排行数据", rankList);
ranks.value = [];
ranks.value.push((rankList.rank || []).slice(0, 3));
ranks.value.push((rankList.mvpRank || []).slice(0, 3));
ranks.value.push((rankList.ringRank || []).slice(0, 3));
updateRank(rankList);
if (homeData) {
console.log("首页数据:", homeData);
if (homeData.user) {
updateUser(homeData.user);
// if (homeData.user.trio <= 0) {
// showGuide.value = true;
// setTimeout(() => {
// showGuide.value = false;
// }, 3000);
// }
if (homeData.user.trio <= 0) {
showGuide.value = true;
setTimeout(() => {
showGuide.value = false;
}, 3000);
}
const devices = await getMyDevicesAPI();
if (devices.bindings && devices.bindings.length) {
updateDevice(
devices.bindings[0].deviceId,
devices.bindings[0].deviceName
);
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
}
}
}
@@ -103,295 +99,434 @@ onMounted(async () => {
console.log("全局配置:", config);
});
const comingSoon = () => {
uni.showToast({
title: "敬请期待",
icon: "none",
});
};
onShareAppMessage(() => {
return {
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!",
path: "/pages/index",
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
path: "/pages/index", // 用户点击分享卡片后跳转的页面路径
imageUrl:
"https://static.shelingxingqiu.com/attachment/2025-09-12/dcqoz26q0268wxmzjg.png",
"https://static.shelingxingqiu.com/attachment/2025-09-12/dcqoz26q0268wxmzjg.png", // 分享卡片的配图,可以是本地或网络图片
};
});
onShareTimeline(() => {
return {
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!",
query: "from=timeline",
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享到朋友圈的标题
query: "from=timeline", // 用户通过朋友圈点击后,在页面 onShow 的 options 中可以获取到的参数
imageUrl:
"https://static.shelingxingqiu.com/attachment/2025-09-12/dcqoz26q0268wxmzjg.png",
"https://static.shelingxingqiu.com/attachment/2025-09-12/dcqoz26q0268wxmzjg.png", // 分享到朋友圈的配图
};
});
</script>
<template>
<view
class="container"
:style="{
paddingTop: capsuleHeight + 'px',
height: 'calc(100vh - ' + capsuleHeight + 'px)',
}"
>
<view :style="{ flex: 1, overflow: 'hidden' }">
<image
src="https://static.shelingxingqiu.com/attachment/2026-01-12/dfmg11wd20o1bagd4k.png"
mode="widthFix"
class="top-bg"
/>
<view class="header">
<image
src="https://static.shelingxingqiu.com/attachment/2026-01-12/dfmf4cjlds7oxd0tqd.png"
mode="widthFix"
/>
</view>
<scroll-view
class="body"
scroll-y
:show-scrollbar="false"
:enhanced="true"
:bounces="false"
>
<view class="main-btns" :style="{ marginBottom: '10rpx' }">
<view @click="() => toPage('/pages/my-device')">
<Container :isHome="true" :showBackToGame="true">
<view class="container">
<UserHeader showRank :onSignin="() => (showModal = true)" />
<view :style="{ padding: '12px 10px' }">
<view class="feature-grid">
<view class="bow-card">
<image
src="https://static.shelingxingqiu.com/attachment/2026-01-12/dfmgwjhsjc19tl0t7c.png"
src="https://static.shelingxingqiu.com/attachment/2025-08-07/dbvt1o6dvhr2rop3kn.webp"
mode="widthFix"
@click="() => toPage('/pages/my-device')"
/>
<view v-if="user.id" class="device-info">
<text v-if="!device.deviceId">绑定我的智能弓</text>
<text v-else-if="!online">设备离线</text>
<text v-else-if="online">设备在线</text>
</view>
<text v-if="!user.id">我的弓箭</text>
<text v-if="user.id && !device.deviceId">连接智能弓</text>
<text
v-if="user.id && device.deviceId"
class="truncate"
:style="{ width: '90%', textAlign: 'center' }"
>{{ device.deviceName }}</text
>
<image
src="../static/first-try.png"
mode="widthFix"
@click="() => toPage('/pages/first-try')"
/>
<BubbleTip
v-if="showGuide"
:location="{ top: '60%', left: '40%', fontSize: '14px' }"
>
<text>新人必刷</text>
<text>快来报到吧~</text>
</BubbleTip>
</view>
<view class="main-sub-btns">
<view @click="() => toPage('/pages/first-try')">
<image
src="https://static.shelingxingqiu.com/attachment/2026-01-12/dfmgwjhren1tfefi7k.png"
mode="widthFix"
/>
</view>
<view class="play-card">
<view @click="() => toPage('/pages/practise')">
<image src="../static/my-practise.png" mode="widthFix" />
</view>
<view @click="() => toPage('/pages/friend-battle')">
<image src="../static/friend-battle.png" mode="widthFix" />
</view>
</view>
</view>
<view class="ranking-section">
<image
src="https://static.shelingxingqiu.com/attachment/2025-09-25/dd1p9ci9v7frcrsxhj.png"
mode="widthFix"
/>
<button
class="into-btn"
@click="() => toPage('/pages/ranking')"
hover-class="none"
></button>
<view class="ranking-players" @click="toRankListPage">
<img src="../static/juezhanbang.png" mode="widthFix" />
<view class="divide-line"></view>
<view class="player-avatars">
<view
v-for="i in 6"
:key="i"
class="player-avatar"
:style="{
zIndex: 8 - i,
borderColor: rankData.rank[i - 1]
? topThreeColors[i - 1] || '#000'
: '#000',
}"
>
<image v-if="i === 1" src="../static/champ1.png" />
<image v-if="i === 2" src="../static/champ2.png" />
<image v-if="i === 3" src="../static/champ3.png" />
<view v-if="i > 3">{{ i }}</view>
<image
:src="
rankData.rank[i - 1]
? rankData.rank[i - 1].avatar
: '../static/user-icon-dark.png'
"
mode="aspectFill"
/>
</view>
<view class="more-players">
<text>{{ rankData.rank.length }}</text>
</view>
</view>
</view>
<view class="my-data">
<view @click="() => toPage('/pages/my-growth')">
<image src="../static/my-growth.png" mode="widthFix" />
</view>
<view @click="() => toPage('/pages/ranking')">
<view>
<text>段位</text>
<text>{{
user.scores ? getLvlName(user.scores) : "暂无"
}}</text>
</view>
<view>
<text>赛季平均环数</text>
<text>{{ user.avg_ring ? user.avg_ring + "环" : "暂无" }}</text>
</view>
<view>
<text>赛季胜率</text>
<text>{{
user.avg_win
? Number((user.avg_win * 100).toFixed(2)) + "%"
: "暂无"
}}</text>
</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
src="https://static.shelingxingqiu.com/attachment/2026-01-12/dfmgwjht6q8l8zcduq.png"
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>
</view>
<view class="main-btns">
<view @click="() => toPage('/pages/ranking')">
<image
src="https://static.shelingxingqiu.com/attachment/2026-01-12/dfmgwjhsi4s5qa7hch.png"
mode="widthFix"
/>
</view>
<view @click="() => toPage('/pages/friend-battle')">
<image
src="https://static.shelingxingqiu.com/attachment/2026-01-12/dfmgwjht17th6vl9a6.png"
mode="widthFix"
/>
</view>
</view>
<!-- <NoticeBar /> -->
<view class="rank-info">
<image
src="../static/trophy-bg.png"
mode="widthFix"
class="trophy-bg"
/>
<view class="rank-info-header">
<view>
<image src="../static/rank-title.png" mode="widthFix" />
<text>星球榜</text>
<view class="region-more" @click="comingSoon">
<image src="../static/region-more.png" mode="widthFix" />
<text>...</text>
<text>更多</text>
</view>
<button hover-class="none" @click="toRankListPage">
<text>更多榜单</text>
<image src="../static/enter.png" mode="widthFix" />
</button>
</view>
<view class="rank-info-body">
<view :style="{ width: '45%' }">
<TopRank :data="ranks" />
</view>
<view :style="{ width: '55%' }">
<MyRank />
</view>
</view>
</view> -->
</view>
<!-- <view class="live-bar">
<button
hover-class="none"
v-for="(item, index) in Object.keys(liveTypes)"
:key="index"
@click="liveType = index"
:style="{
color: liveType === index ? '#fff' : '#fff9',
background:
liveType === index
? 'linear-gradient( 133deg, #FFD19A 0%, #A17636 100%)'
: '#252831',
}"
>
<image :src="liveTypes[item]" mode="widthFix" />
<text>{{ item }}</text>
</button>
</view>
<swiper
:current="liveType"
@change="(e) => (liveType = e.detail.current)"
>
<swiper-item>
<LiveItem />
</swiper-item>
<swiper-item>
<LiveItem />
</swiper-item>
<swiper-item>
<LiveItem />
</swiper-item>
</swiper> -->
</scroll-view>
<Signin :show="showModal" :onClose="() => (showModal = false)" />
</view>
<SModal :show="showModal" :onClose="() => (showModal = false)">
<Signin :onClose="() => (showModal = false)" />
</SModal>
</view>
<AppFooter
:selected="selected"
:onChange="(index) => (selected = index)"
:onSignin="() => (showModal = true)"
/>
</view>
<AppFooter />
</Container>
</template>
<style scoped lang="scss">
<style scoped>
.container {
width: 100vw;
height: 100vh;
width: 100%;
}
.feature-grid {
width: 100%;
display: flex;
margin-bottom: 5px;
}
.feature-grid > view {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #000;
}
.top-bg {
.bow-card {
width: 50%;
}
.feature-grid > view > image {
width: 100%;
position: fixed;
}
.bow-card > text {
position: absolute;
top: 65%;
left: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
font-size: 13px;
color: #b3b3b3;
}
.bow-card > image:nth-child(3) {
transform: translateY(-1px);
}
.play-card {
width: 48%;
margin-left: 2%;
}
.play-card > view > image {
width: 100%;
}
.ranking-section {
border-radius: 15px;
padding: 15px;
position: relative;
}
.ranking-section > image {
position: absolute;
top: 0;
left: 0;
}
.header {
height: 50px;
position: relative;
display: flex;
align-items: center;
padding-left: 30rpx;
}
.header > image {
width: 200rpx;
height: 50rpx;
}
.body {
width: calc(100vw - 50rpx);
height: calc(100% - 25rpx);
padding: 0 25rpx;
position: relative;
margin-bottom: 25rpx;
}
.main-btns {
width: 100%;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
z-index: -1;
}
.main-btns > view {
position: relative;
}
.main-btns image {
width: 100%;
}
.main-sub-btns {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.device-info {
.into-btn {
position: absolute;
width: 100%;
bottom: 50rpx;
font-size: 24rpx;
color: #fff9;
display: flex;
justify-content: center;
top: 40px;
left: calc(50% - 100px);
width: 200px;
height: 100px;
}
.rank-info {
margin-top: 10rpx;
background: linear-gradient(180deg, #2f2d2b 0%, #252831 100%);
border-radius: 20rpx;
.ranking-players {
display: flex;
align-items: center;
padding-bottom: 20px;
margin-top: 42%;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.2);
}
.ranking-players > image:first-child {
width: 28%;
transform: translateX(-10px) translateY(-8px);
}
.player-avatars {
display: flex;
align-items: center;
}
.divide-line {
width: 1px;
height: 35px;
background-color: #80808033;
margin-right: 8px;
}
.player-avatar,
.more-players {
width: 82rpx;
height: 82rpx;
border-radius: 50%;
margin-right: -20rpx;
border: 1rpx solid #312f35;
position: relative;
padding: 20rpx;
display: flex;
flex-direction: column;
margin-top: 20rpx;
}
.trophy-bg {
position: absolute;
top: 0;
right: 0;
width: 188rpx;
height: 192rpx;
}
.rank-info-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
.rank-info-header > view {
width: 186rpx;
height: 56rpx;
font-weight: 500;
font-size: 28rpx;
color: #ffffff;
}
.rank-info-header > view > image {
width: 100%;
height: 100%;
}
.rank-info-header > view > text {
line-height: 56rpx;
display: block;
transform: translate(75rpx, -65rpx);
}
.rank-info-header > button {
font-size: 20rpx;
color: #999999;
display: flex;
align-items: center;
}
.rank-info-header > button > image {
width: 30rpx;
height: 30rpx;
}
.rank-info-body {
display: flex;
align-items: center;
}
.rank-info-body > view:first-child {
border-right: 1rpx solid #e8e8e81a;
box-sizing: border-box;
}
.live-bar {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
margin: 20rpx 0;
.player-avatar > image:first-child,
.player-avatar > view:first-child {
position: absolute;
top: -24rpx;
left: 22rpx;
width: 32rpx;
height: 32rpx;
}
.live-bar > button {
.player-avatar > view:first-child {
border-radius: 50%;
background: #777777;
text-align: center;
font-size: 10px;
line-height: 18px;
width: 18px;
height: 18px;
color: #fff;
}
.player-avatar > image:last-child {
width: 100%;
height: 100%;
border-radius: 50%;
}
.more-players {
background: #3c445a;
font-size: 9px;
line-height: 80rpx;
text-align: center;
z-index: 1;
}
.more-players > text {
margin-left: 2px;
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 {
display: flex;
margin-top: 20px;
justify-content: space-between;
}
.my-data > view:first-child {
width: 28%;
}
.my-data > view:first-child > image {
width: 100%;
transform: translateX(-8px);
}
.my-data > view:nth-child(2) {
width: 68%;
font-size: 12px;
color: #fff6;
display: flex;
justify-content: space-between;
}
.my-data > view:nth-child(2) > view:nth-child(2) {
width: 38%;
}
.my-data > view:nth-child(2) > view {
width: 28%;
border-radius: 10px;
background: linear-gradient(180deg, #303b4c 30%, #2c384a 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 68rpx;
border-radius: 34rpx;
font-weight: 600;
font-size: 28rpx;
}
.live-bar > button > image {
width: 46rpx;
height: 40rpx;
margin-right: 15rpx;
.my-data > view:nth-child(2) > view > text:last-child {
color: #fff;
line-height: 25px;
font-weight: 500;
}
</style>

View File

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

View File

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

View File

@@ -1,15 +1,9 @@
<script setup>
import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { ref, onMounted } from "vue";
import Container from "@/components/Container.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import SButton from "@/components/SButton.vue";
import {
bindDeviceAPI,
getMyDevicesAPI,
unbindDeviceAPI,
laserAimAPI,
} from "@/apis";
import { bindDeviceAPI, getMyDevicesAPI, unbindDeviceAPI } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const showTip = ref(false);
@@ -19,7 +13,6 @@ const store = useStore();
const { updateDevice } = store;
const { user, device } = storeToRefs(store);
const justBind = ref(false);
const calibration = ref(false);
// 扫描二维码方法
const handleScan = () => {
@@ -83,7 +76,6 @@ const toFristTryPage = () => {
const unbindDevice = async () => {
await unbindDeviceAPI(device.value.deviceId);
uni.setStorageSync("calibration", false);
uni.showToast({
title: "解绑成功",
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>
<template>
@@ -156,9 +137,7 @@ onShow(() => {
<text>已被绑定的弓箭无法再次绑定</text>
<view>
<text>如有任何疑问请随时联系</text>
<button hover-class="none" @click="copyEmail">
shelingxingqiu@163.com
</button>
<button hover-class="none" @click="copyEmail">shelingxingqiu@163.com</button>
</view>
</view>
</ScreenHint>
@@ -182,23 +161,10 @@ onShow(() => {
</ScreenHint>
</view>
<view v-if="justBind" class="just-bind">
<view
class="device-binded"
:style="{ marginBottom: calibration ? '250rpx' : '100rpx' }"
>
<view>
<view class="device-binded">
<view @click="toDeviceIntroPage">
<image src="../static/device-icon.png" mode="widthFix" />
<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>
<image src="../static/bind-success.png" mode="widthFix" />
<view>
@@ -210,51 +176,23 @@ onShow(() => {
<text>{{ user.nickName }}</text>
</view>
</view>
<block v-if="calibration">
<SButton :onClick="toFristTryPage" width="60vw" :rounded="40"
>进入新手试炼</SButton
<view>
<text>恭喜你的弓箭和账号已成功绑定</text>
<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' }">
<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 class="device-binded">
<view>
<view @click="toDeviceIntroPage">
<image src="../static/device-icon.png" mode="widthFix" />
<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>
<image src="../static/bind.png" mode="widthFix" />
<view>
@@ -266,11 +204,7 @@ onShow(() => {
<text>{{ user.nickName }}</text>
</view>
</view>
<view :style="{ marginTop: '240rpx' }">
<SButton :onClick="unbindDevice" width="80vw" :rounded="40"
>解绑</SButton
>
</view>
<SButton :onClick="unbindDevice">解绑</SButton>
</view>
</Container>
</template>
@@ -379,18 +313,16 @@ onShow(() => {
justify-content: center;
color: #fff;
font-size: 14px;
margin-top: 200rpx;
margin: 100px 0;
}
.device-binded > view {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
font-size: 26rpx;
}
.device-binded > view > image {
width: 140rpx;
height: 140rpx;
width: 24vw;
height: 24vw;
margin-bottom: 5px;
border-radius: 10px;
}
@@ -402,7 +334,7 @@ onShow(() => {
text-align: center;
}
.device-binded > image {
width: 100rpx;
width: 16vw;
margin: 0 20px;
}
.has-device,
@@ -415,42 +347,11 @@ onShow(() => {
display: flex;
flex-direction: column;
align-items: center;
font-size: 28rpx;
margin-bottom: 100rpx;
font-size: 14px;
margin: 75px 0;
}
.has-device > view:nth-child(2) > text,
.just-bind > view:nth-child(2) > text {
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>

View File

@@ -55,7 +55,7 @@ const onPractiseLoading = async (page) => {
</script>
<template>
<Container title="我的成长脚印" :scroll="false">
<Container title="我的成长脚印" overflow="hidden">
<view class="tabs">
<view
v-for="(rankType, index) in ['排位赛', '好友约战', '个人练习']"
@@ -70,74 +70,66 @@ const onPractiseLoading = async (page) => {
</view>
</view>
<view class="contents">
<swiper
:current="selectedIndex"
@change="(e) => (selectedIndex = e.detail.current)"
:style="{ height: '100%' }"
<ScrollList :show="selectedIndex === 0" :onLoading="onMatchLoading">
<view
v-for="(item, index) in matchList"
: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>
<ScrollList :onLoading="onMatchLoading">
<view
v-for="(item, index) in matchList"
: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="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
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>
</view>
</Container>
</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 EditOption from "@/components/EditOption.vue";
import SButton from "@/components/SButton.vue";
import { getPointBookDataAPI } from "@/apis";
const expandIndex = ref(0);
const bowType = ref("");
@@ -49,23 +48,16 @@ const toEditPage = () => {
bowtargetType: bowtargetType.value,
amountGroup: amountGroup.value,
});
uni.redirectTo({
uni.navigateTo({
url: "/pages/point-book-edit",
});
} else {
uni.showToast({
title: "请完善信息",
title: "Please complete the information",
icon: "none",
});
}
};
// onShow(async () => {
// const result = await getPointBookDataAPI();
// if (result) {
// days.value = result.total_day || 0;
// arrows.value = result.total_arrow || 0;
// }
// });
onMounted(async () => {
const pointBook = uni.getStorageSync("last-point-book");
if (pointBook) {
@@ -85,37 +77,39 @@ onMounted(async () => {
title="选择参数"
>
<view class="container">
<EditOption
:itemIndex="0"
:expand="expandIndex === 0"
:onExpand="onExpandChange"
:onSelect="onSelect"
:value="bowType.name"
/>
<EditOption
:itemIndex="1"
:expand="expandIndex === 1"
:onExpand="onExpandChange"
:onSelect="onSelect"
:value="distance + ''"
/>
<EditOption
:itemIndex="2"
:expand="expandIndex === 2"
:onExpand="onExpandChange"
:onSelect="onSelect"
:value="bowtargetType.name"
/>
<EditOption
:itemIndex="3"
:expand="expandIndex === 3"
:onExpand="onExpandChange"
:onSelect="onSelect"
/>
<view>
<EditOption
:itemIndex="0"
:expand="expandIndex === 0"
:onExpand="onExpandChange"
:onSelect="onSelect"
:value="bowType.name"
/>
<EditOption
:itemIndex="1"
:expand="expandIndex === 1"
:onExpand="onExpandChange"
:onSelect="onSelect"
:value="distance + ''"
/>
<EditOption
:itemIndex="2"
:expand="expandIndex === 2"
:onExpand="onExpandChange"
:onSelect="onSelect"
:value="bowtargetType.name"
/>
<EditOption
:itemIndex="3"
:expand="expandIndex === 3"
:onExpand="onExpandChange"
:onSelect="onSelect"
/>
</view>
</view>
<view :style="{ marginBottom: '20px' }">
<SButton :rounded="50" :onClick="toEditPage">Next</SButton>
</view>
<template #bottom>
<SButton :rounded="50" :onClick="toEditPage">下一步</SButton>
</template>
</Container>
</template>

View File

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

View File

@@ -1,6 +1,5 @@
<script setup>
import { ref, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ScreenHint2 from "@/components/ScreenHint2.vue";
import SButton from "@/components/SButton.vue";
@@ -37,7 +36,7 @@ const onSubmit = async () => {
);
if (!isComplete) {
return uni.showToast({
title: "请完善信息",
title: "Please complete the information",
icon: "none",
});
}
@@ -55,7 +54,6 @@ const onSubmit = async () => {
Object.values(arrowGroups.value)
);
if (res.record_id) {
uni.removeStorageSync("last-point-record");
uni.redirectTo({
url: `/pages/point-book-detail?id=${res.record_id}`,
});
@@ -66,35 +64,17 @@ const onClickRing = (ring) => {
if (arrowGroups.value[currentGroup.value]) {
arrowGroups.value[currentGroup.value][currentArrow.value] = { ring };
if (currentArrow.value < amount.value - 1) currentArrow.value++;
uni.setStorageSync("last-point-record", arrowGroups.value);
}
};
const deleteArrow = () => {
const arrow = 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);
arrowGroups.value[currentGroup.value][currentArrow.value] = {};
};
const onEditDone = (arrow) => {
arrowGroups.value[currentGroup.value][currentArrow.value] = arrow;
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");
if (pointBook.bowtargetType) {
bowtarget.value = pointBook.bowtargetType;
@@ -112,27 +92,16 @@ onLoad((options) => {
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>
<template>
<Container :bgType="2" bgColor="#F5F5F5" :whiteBackArrow="false">
<Container
:bgType="2"
bgColor="#F5F5F5"
:whiteBackArrow="false"
:onBack="() => (showTip = true)"
>
<view class="container">
<BowTargetEdit
:onChange="onEditDone"
@@ -143,11 +112,11 @@ onLoad((options) => {
<view class="title-bar">
<view>
<view />
<text> {{ currentGroup }} </text>
<text>Set {{ currentGroup }}</text>
</view>
<view @click="deleteArrow">
<image src="../static/delete.png" />
<text>删除</text>
<text>Delete</text>
</view>
</view>
<view class="bow-arrows">
@@ -155,7 +124,7 @@ onLoad((options) => {
v-if="arrowGroups[currentGroup]"
v-for="(arrow, index) in arrowGroups[currentGroup]"
:key="index"
@click="onSelectArrow(index)"
@click="currentArrow = index"
:style="{
borderColor: currentArrow === index ? '#FED847' : '#eeeeee',
borderWidth: currentArrow === index ? '2px' : '1px',
@@ -164,12 +133,15 @@ onLoad((options) => {
isNaN(arrow.ring)
? arrow.ring
: arrow.ring
? arrow.ring + " "
? arrow.ring + " points"
: ""
}}
</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
v-for="(item, index) in ringTypes"
@@ -186,22 +158,22 @@ onLoad((options) => {
</view>
<ScreenHint2 :show="showTip">
<view class="tip-content">
<text>现在离开会导致</text>
<text>未提交的数据丢失是否继续</text>
<text>Leaving now will result in the loss of unsaved data.</text>
<text>Are you sure you want to continue?</text>
<view>
<button hover-class="none" @click="onBack">退出</button>
<button hover-class="none" @click="onBack">Exit</button>
<button hover-class="none" @click="showTip = false">
继续记录
Continue
</button>
</view>
</view>
</ScreenHint2>
</view>
<template #bottom>
<view :style="{ marginBottom: '20px' }">
<SButton :rounded="50" :onClick="onSubmit">
{{ currentGroup === groups ? "保存并查看分析" : "下一组" }}
{{ currentGroup === groups ? "Submit for analysis" : "Next set" }}
</SButton>
</template>
</view>
</Container>
</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>
import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { ref, onMounted } from "vue";
import Container from "@/components/Container.vue";
import SModal from "@/components/SModal.vue";
import EditOption from "@/components/EditOption.vue";
@@ -17,7 +16,6 @@ const showModal = ref(false);
const selectorIndex = ref(0);
const list = ref([]);
const removeId = ref("");
const pointDraft = ref(null);
const onListLoading = async (page) => {
const result = await getPointBookListAPI(
@@ -49,9 +47,9 @@ const confirmRemove = async () => {
showTip.value = false;
await removePointRecord(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) {
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;
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>
<template>
@@ -89,79 +71,36 @@ onShow(() => {
:bgType="2"
bgColor="#F5F5F5"
:whiteBackArrow="false"
title="计分记录"
title="Point Records"
>
<view class="container">
<view class="selectors">
<view @click="() => openSelector(0)">
<text :style="{ color: bowType.name ? '#000' : '#999' }">{{
bowType.name || "请选择"
bowType.name || "Please select"
}}</text>
<image src="../static/arrow-grey.png" mode="widthFix" />
</view>
<view @click="() => openSelector(1)">
<text :style="{ color: distance ? '#000' : '#999' }">{{
distance ? distance + " " : "请选择"
distance ? distance + " m" : "Please select"
}}</text>
<image src="../static/arrow-grey.png" mode="widthFix" />
</view>
<view @click="() => openSelector(2)">
<text :style="{ color: bowtargetType.name ? '#000' : '#999' }">{{
bowtargetType.name || "请选择"
bowtargetType.name || "Please select"
}}</text>
<image src="../static/arrow-grey.png" mode="widthFix" />
</view>
</view>
<view class="point-records">
<ScrollList :onLoading="onListLoading">
<uni-swipe-action>
<block v-if="pointDraft">
<uni-swipe-action-item>
<template v-slot:right>
<view class="swipe-right" @click="onRemoveDraft">
<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>
<view v-for="(item, index) in list" :key="item.id">
<PointRecord :data="item" :onRemove="onRemoveRecord" />
<view v-if="index < list.length - 1" :style="{ height: '25rpx' }"></view>
</view>
<view class="no-data" v-if="list.length === 0">No data</view>
</ScrollList>
</view>
<SModal
@@ -202,10 +141,10 @@ onShow(() => {
</SModal>
<ScreenHint2 :show="showTip">
<view class="tip-content">
<text>确认删除该记录吗?</text>
<text>Are you sure to delete this record?</text>
<view>
<button hover-class="none" @click="showTip = false">取消</button>
<button hover-class="none" @click="confirmRemove">确认</button>
<button hover-class="none" @click="showTip = false">Cancel</button>
<button hover-class="none" @click="confirmRemove">Confirm</button>
</view>
</view>
</ScreenHint2>
@@ -306,67 +245,4 @@ onShow(() => {
.tip-content > view > button:last-child {
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>

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,12 +4,11 @@ import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Avatar from "@/components/Avatar.vue";
import { topThreeColors } from "@/constants";
import { getHomeData } from "@/apis";
import { canEenter } from "@/util";
import { isGamingAPI, getHomeData } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, device, online, game } = storeToRefs(store);
const { user, device } = storeToRefs(store);
const { getLvlName } = store;
const defaultSeasonData = {
@@ -42,12 +41,23 @@ const handleSelect = (index) => {
};
const toMatchPage = async (gameType, teamSize) => {
if (!canEenter(user.value, device.value, online.value)) return;
if (game.value.inBattle) {
if (!device.value.deviceId) {
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);
return;
}
await uni.$checkAudio();
uni.navigateTo({
url: `/pages/match-page?gameType=${gameType}&teamSize=${teamSize}`,
});
@@ -63,14 +73,14 @@ const toRankListPage = () => {
});
};
const onChangeSeason = async (seasonId, name) => {
showSeasonList.value = false;
if (name !== seasonName.value) {
handleSelect(selectedIndex.value);
const result = await getHomeData(seasonId);
rankData.value = result;
seasonName.value = name;
handleSelect(selectedIndex.value);
updateData();
}
showSeasonList.value = false;
};
const updateData = () => {
const { userGameStats, seasonList } = rankData.value;
@@ -165,7 +175,7 @@ onShow(async () => {
<view>
<text>段位</text>
<text :style="{ color: '#83CDFF' }">{{
getLvlName(rankData.user.rankLvl) || "-"
getLvlName(rankData.user.scores) || "-"
}}</text>
</view>
<view>
@@ -344,7 +354,7 @@ onShow(async () => {
<view>
<text class="truncate">{{ item.name }}</text>
<text>
{{ getLvlName(item.rankLvl) }}{{ item.TotalGames }}场
{{ getLvlName(item.totalScore) }}{{ item.TotalGames }}场
</text>
</view>
<text v-if="selectedIndex === 0">
@@ -501,11 +511,10 @@ onShow(async () => {
}
.ranking-data > view:first-of-type > view {
width: 25%;
padding: 7px 10px;
text-align: center;
border-radius: 20px;
font-size: 30rpx;
word-break: keep-all;
line-height: 70rpx;
}
.rank-item {
width: calc(100% - 30px);
@@ -595,19 +604,13 @@ onShow(async () => {
.season-list > view {
display: flex;
align-items: center;
padding: 10px 20px;
word-break: keep-all;
padding: 20rpx 0;
}
.season-list > view > text {
width: 140rpx;
text-align: right;
}
.season-list > view > image {
width: 24rpx;
height: 24rpx;
min-width: 24rpx;
min-height: 24rpx;
margin-left: 20rpx;
width: 12px;
height: 12px;
margin-left: 10px;
}
.my-rank-score {
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 BattleHeader from "@/components/BattleHeader.vue";
import BowTarget from "@/components/BowTarget.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import PlayersRow from "@/components/PlayersRow.vue";
import BattleFooter from "@/components/BattleFooter.vue";
import ScreenHint from "@/components/ScreenHint.vue";
@@ -12,7 +13,7 @@ import RoundEndTip from "@/components/RoundEndTip.vue";
import TestDistance from "@/components/TestDistance.vue";
import TeamAvatars from "@/components/TeamAvatars.vue";
import ShootProgress2 from "@/components/ShootProgress2.vue";
import { getCurrentGameAPI, laserCloseAPI } from "@/apis";
import { getCurrentGameAPI } from "@/apis";
import { isGameEnded } from "@/util";
import { MESSAGETYPES, roundsName } from "@/constants";
import audioManager from "@/audioManager";
@@ -27,6 +28,8 @@ const currentRound = ref(1);
const goldenRound = ref(0);
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([]);
@@ -39,6 +42,10 @@ const showRoundTip = ref(false);
const isFinalShoot = ref(false);
const isEnded = ref(false);
const onBack = () => {
uni.$showHint(2);
};
function recoverData(battleInfo) {
uni.removeStorageSync("last-awake-time");
battleId.value = battleInfo.id;
@@ -57,8 +64,8 @@ function recoverData(battleInfo) {
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,
@@ -77,64 +84,50 @@ function recoverData(battleInfo) {
redPoints.value += 2;
}
});
if (battleInfo.goldenRoundNumber) {
currentRound.value += battleInfo.goldenRoundNumber;
goldenRound.value = battleInfo.goldenRoundNumber;
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,
});
}
if (battleInfo.goldenRound) {
const { ShotCount, RedRecords, BlueRecords } = battleInfo.goldenRound;
currentRound.value += ShotCount;
goldenRound.value += ShotCount;
isFinalShoot.value = true;
for (let i = 1; i <= battleInfo.goldenRoundNumber; i++) {
const redArrows = [];
battleInfo.redTeam.forEach((item) => {
if (item.shotHistory[roundResults.value.length + 1]) {
item.shotHistory[roundResults.value.length + 1]
.filter((item) => !!item.playerId)
.forEach((item) => redArrows.push(item));
}
});
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),
});
for (let i = 0; i < ShotCount; i++) {
const roundData = {
redArrows:
RedRecords && RedRecords[i] ? RedRecords[i].Arrows || [] : [],
blueArrows:
BlueRecords && BlueRecords[i] ? BlueRecords[i].Arrows || [] : [],
gold: true,
};
roundResults.value.push(roundData);
}
} 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) => {
if (p.id === user.value.id) {
const roundArrows = Object.values(p.shotHistory);
@@ -152,24 +145,18 @@ function recoverData(battleInfo) {
const lastIndex = roundResults.value.length - 1;
if (roundResults.value[lastIndex]) {
const redArrows = roundResults.value[lastIndex].redArrows;
scores.value = [...redArrows]
.filter((item) => !!item.playerId)
.sort((a, b) => a.shotTimeUnix - b.shotTimeUnix);
scores.value = [...redArrows].filter((item) => !!item.playerId);
const blueArrows = roundResults.value[lastIndex].blueArrows;
blueScores.value = [...blueArrows]
.filter((item) => !!item.playerId)
.sort((a, b) => a.shotTimeUnix - b.shotTimeUnix);
blueScores.value = [...blueArrows].filter((item) => !!item.playerId);
}
// if (battleInfo.status !== 11) return;
if (battleInfo.firePlayerIndex) {
currentShooterId.value = battleInfo.firePlayerIndex;
const redPlayer = redTeam.value.find(
(item) => item.id === currentShooterId.value
);
let nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭";
nextTips += "重回";
// if (battleInfo.firePlayerIndex === user.value.id) nextTips += "你";
tips.value = nextTips;
uni.$emit("update-tips", nextTips);
tips.value = redPlayer ? "请红队射箭" : "请蓝队射箭";
uni.$emit("update-tips", tips.value);
}
if (battleInfo.fireTime > 0) {
const remain = Date.now() / 1000 - battleInfo.fireTime;
@@ -186,12 +173,9 @@ function recoverData(battleInfo) {
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.WaitForAllReady) {
redTeam.value = msg.groupUserStatus.redTeam;
blueTeam.value = msg.groupUserStatus.blueTeam;
}
if (msg.constructor === MESSAGETYPES.AllReady) {
start.value = true;
totalRounds.value = msg.groupUserStatus.config.maxRounds;
}
if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) {
if (currentShooterId.value !== msg.userId) {
@@ -199,11 +183,8 @@ async function onReceiveMessage(messages = []) {
const redPlayer = redTeam.value.find(
(item) => item.id === currentShooterId.value
);
let nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭";
if (msg.userId === user.value.id && redTeam.value.length > 1) {
nextTips += "你";
}
if (msg.userId === user.value.id) audioManager.play("轮到你了");
const nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭";
if (nextTips !== tips.value) {
tips.value = nextTips;
uni.$emit("update-tips", tips.value);
@@ -213,7 +194,21 @@ async function onReceiveMessage(messages = []) {
}
}
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) {
const result = msg.preRoundResult;
@@ -224,12 +219,19 @@ async function onReceiveMessage(messages = []) {
currentRedPoint.value = result.redScore;
bluePoints.value += result.blueScore;
redPoints.value += result.redScore;
currentRound.value = result.currentRound + 1;
if (!result.goldenRound) {
showRoundTip.value = true;
}
}
if (msg.constructor === MESSAGETYPES.FinalShoot) {
currentShooterId.value = 0;
currentRound.value = msg.groupUserStatus.currentRound + 1;
goldenRound.value += 1;
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
currentBluePoint.value = bluePoints.value;
currentRedPoint.value = redPoints.value;
if (!isFinalShoot.value) {
@@ -240,6 +242,7 @@ async function onReceiveMessage(messages = []) {
}
if (msg.constructor === MESSAGETYPES.MatchOver) {
if (msg.endStatus.noSaved) {
currentRound.value += 1;
currentBluePoint.value = 0;
currentRedPoint.value = 0;
showRoundTip.value = true;
@@ -277,26 +280,18 @@ onLoad(async (options) => {
setTimeout(getCurrentGameAPI, 2000);
}
}
uni.enableAlertBeforeUnload({
message: "离开比赛可能导致比赛失败,是否继续?",
success: (res) => {
console.log("已启用离开提示");
},
});
});
onMounted(async () => {
onMounted(() => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
await laserCloseAPI();
});
onBeforeUnmount(() => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
audioManager.stopAll();
});
const refreshTimer = ref(null);
onShow(async () => {
@@ -321,7 +316,7 @@ onHide(() => {
</script>
<template>
<Container :bgType="start ? 3 : 1">
<Container :bgType="start ? 3 : 1" :onBack="onBack">
<view class="container">
<BattleHeader v-if="!start" :redTeam="redTeam" :blueTeam="blueTeam" />
<TestDistance v-if="!start" :guide="false" :isBattle="true" />
@@ -342,6 +337,7 @@ onHide(() => {
<BowTarget
v-if="start"
mode="team"
:power="start ? power : 0"
:scores="scores"
:blueScores="blueScores"
/>
@@ -351,6 +347,7 @@ onHide(() => {
:redPoints="redPoints"
:bluePoints="bluePoints"
:goldenRound="goldenRound"
:power="power"
/>
<ScreenHint
:show="showRoundTip"
@@ -360,11 +357,11 @@ onHide(() => {
<RoundEndTip
v-if="showRoundTip"
:isFinal="isFinalShoot"
:round="currentRound"
:round="currentRound - 1"
:bluePoint="currentBluePoint"
:redPoint="currentRedPoint"
:roundData="
roundResults[currentRound - 1] ? roundResults[currentRound - 1] : []
roundResults[currentRound - 2] ? roundResults[currentRound - 2] : []
"
:onAutoClose="() => (showRoundTip = false)"
/>
@@ -382,7 +379,7 @@ onHide(() => {
display: flex;
align-items: center;
justify-content: center;
margin-top: -2%;
margin-bottom: 6%;
margin-bottom: -7vw;
margin-top: -3vw;
}
</style>

View File

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

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

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