Compare commits
89 Commits
feature-po
...
ios-ui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08e91a8c18 | ||
|
|
00a52f60b5 | ||
|
|
b853d52a26 | ||
|
|
ea0c54b767 | ||
|
|
2bbe9f1aab | ||
|
|
3af68d968c | ||
|
|
63c002ed56 | ||
|
|
14f43e929f | ||
|
|
a9168201b3 | ||
|
|
6087e1bf94 | ||
|
|
5a7605e9d2 | ||
|
|
3b1fb5b270 | ||
|
|
eef113902e | ||
|
|
5bf3bbccdb | ||
|
|
fb0cf62ca0 | ||
|
|
d2ce9f1026 | ||
|
|
ce34ca93d1 | ||
|
|
9f6e1b1e97 | ||
|
|
0ffca23dbf | ||
|
|
22429bda52 | ||
|
|
e91ba88b9f | ||
|
|
96fc942d02 | ||
|
|
0875297819 | ||
|
|
10afe737f6 | ||
|
|
1daa830ed0 | ||
|
|
e636d02657 | ||
|
|
c0aa6e8058 | ||
|
|
ca399ffc19 | ||
|
|
301b7a67a0 | ||
|
|
9c6824b82f | ||
|
|
889e87d3e9 | ||
|
|
caa70b16f4 | ||
|
|
9af2f5b887 | ||
|
|
b75ab93af9 | ||
|
|
f8bc5d094e | ||
|
|
91535abfd7 | ||
|
|
b8d0c6c567 | ||
|
|
2a1bc1e3bc | ||
|
|
8c45e7f4eb | ||
|
|
ef96f90470 | ||
|
|
867d4d0090 | ||
|
|
67be4ad7d6 | ||
|
|
94edc3d6c9 | ||
|
|
59016fe54f | ||
|
|
0de4dc8e6d | ||
|
|
d5acc639b3 | ||
|
|
5748dfdfde | ||
|
|
890867586b | ||
|
|
72ab9c3757 | ||
|
|
b952ea9fd0 | ||
|
|
aa6bbf6fd6 | ||
|
|
0a151de3c9 | ||
|
|
24b776f327 | ||
|
|
1c79ed6183 | ||
|
|
eca11715d5 | ||
|
|
65548e6c6a | ||
|
|
c9e575a81e | ||
|
|
71a79defe7 | ||
|
|
c9eaeedc0d | ||
|
|
9b2ba22b97 | ||
|
|
1f15183fc4 | ||
|
|
bc17a3a584 | ||
|
|
a1942697e7 | ||
|
|
3c414afd82 | ||
|
|
91ee2a714c | ||
|
|
677d280a4e | ||
|
|
f076065550 | ||
|
|
01f05f4824 | ||
|
|
eb076df7d5 | ||
|
|
448df06daf | ||
|
|
7a9439567f | ||
|
|
73e35a5506 | ||
|
|
3be6a5ef04 | ||
|
|
17e463a884 | ||
|
|
4347dea41e | ||
|
|
0d1e0737ff | ||
|
|
c0736e1285 | ||
|
|
e20cb3b272 | ||
|
|
fca4a138d7 | ||
|
|
529a09da3e | ||
|
|
2dd3ea05a4 | ||
|
|
d9c9319d24 | ||
|
|
70b3a25369 | ||
|
|
f19b9b1f9d | ||
|
|
22a9fe56c0 | ||
|
|
12dbe2d05b | ||
|
|
f0edb2a57f | ||
|
|
7162490ef7 | ||
|
|
f03adb5ea0 |
14
src/App.vue
@@ -11,7 +11,9 @@ const { updateUser } = store;
|
|||||||
watch(
|
watch(
|
||||||
() => user.value.id,
|
() => user.value.id,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
const token = uni.getStorageSync("token");
|
const token = uni.getStorageSync(
|
||||||
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
|
);
|
||||||
if (newVal && token) {
|
if (newVal && token) {
|
||||||
websocket.createWebSocket(token, (content) => {
|
websocket.createWebSocket(token, (content) => {
|
||||||
uni.$emit("socket-inbox", content);
|
uni.$emit("socket-inbox", content);
|
||||||
@@ -28,7 +30,9 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
const token = uni.getStorageSync("token");
|
const token = uni.getStorageSync(
|
||||||
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
|
);
|
||||||
if (user.value.id && token) {
|
if (user.value.id && token) {
|
||||||
console.log("回到前台,重新连接 websocket");
|
console.log("回到前台,重新连接 websocket");
|
||||||
websocket.createWebSocket(token, (content) => {
|
websocket.createWebSocket(token, (content) => {
|
||||||
@@ -60,6 +64,12 @@ button {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
view::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
button::after {
|
button::after {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|||||||
71
src/apis.js
@@ -24,7 +24,9 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function request(method, url, data = {}) {
|
function request(method, url, data = {}) {
|
||||||
const token = uni.getStorageSync("token");
|
const token = uni.getStorageSync(
|
||||||
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
|
);
|
||||||
const header = {};
|
const header = {};
|
||||||
if (token) header.Authorization = `Bearer ${token || ""}`;
|
if (token) header.Authorization = `Bearer ${token || ""}`;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -40,7 +42,9 @@ function request(method, url, data = {}) {
|
|||||||
if (code === 0) resolve(data);
|
if (code === 0) resolve(data);
|
||||||
else if (message) {
|
else if (message) {
|
||||||
if (message.indexOf("登录身份已失效") !== -1) {
|
if (message.indexOf("登录身份已失效") !== -1) {
|
||||||
uni.removeStorageSync("token");
|
uni.removeStorageSync(
|
||||||
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (message === "ROOM_FULL") {
|
if (message === "ROOM_FULL") {
|
||||||
resolve({ full: true });
|
resolve({ full: true });
|
||||||
@@ -70,11 +74,18 @@ function request(method, url, data = {}) {
|
|||||||
resolve({});
|
resolve({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (message === "ROOM_EMPTY") {
|
||||||
|
return uni.showToast({
|
||||||
|
title: "房间已过期",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
}
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: message,
|
title: message,
|
||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
reject("");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
@@ -159,7 +170,10 @@ export const loginAPI = async (nickName, avatarData, code) => {
|
|||||||
avatarData,
|
avatarData,
|
||||||
code,
|
code,
|
||||||
});
|
});
|
||||||
uni.setStorageSync("token", result.token);
|
uni.setStorageSync(
|
||||||
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`,
|
||||||
|
result.token
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,17 +302,16 @@ export const getGameAPI = async (battleId) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const totalRounds = Object.keys(data.roundsData).length;
|
||||||
(goldenRoundRecords || []).forEach((item, index) => {
|
(goldenRoundRecords || []).forEach((item, index) => {
|
||||||
item.arrowHistory.forEach((arrow) => {
|
item.arrowHistory.forEach((arrow) => {
|
||||||
if (!data.roundsData[playerStats.length + index + 1]) {
|
if (!data.roundsData[totalRounds + index + 1]) {
|
||||||
data.roundsData[playerStats.length + index + 1] = {};
|
data.roundsData[totalRounds + index + 1] = {};
|
||||||
}
|
}
|
||||||
if (!data.roundsData[playerStats.length + index + 1][arrow.playerId]) {
|
if (!data.roundsData[totalRounds + index + 1][arrow.playerId]) {
|
||||||
data.roundsData[playerStats.length + index + 1][arrow.playerId] = [];
|
data.roundsData[totalRounds + index + 1][arrow.playerId] = [];
|
||||||
}
|
}
|
||||||
data.roundsData[playerStats.length + index + 1][arrow.playerId].push(
|
data.roundsData[totalRounds + index + 1][arrow.playerId].push(arrow);
|
||||||
arrow
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -467,3 +480,41 @@ export const getBattleDataAPI = async () => {
|
|||||||
export const chooseTeamAPI = async (number, group) => {
|
export const chooseTeamAPI = async (number, group) => {
|
||||||
return request("POST", "/user/room/group", { number, group });
|
return request("POST", "/user/room/group", { number, group });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getVIPDescAPI = async () => {
|
||||||
|
return request("GET", "/index/memberVipDescribe");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPointBookStatisticsAPI = async () => {
|
||||||
|
return request("GET", `/v2/user/score/sheet/statistics`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const donateAPI = async (amount, name, phone, organizer, advice) => {
|
||||||
|
return request("POST", `/user/donate`, {
|
||||||
|
amount,
|
||||||
|
name,
|
||||||
|
phone,
|
||||||
|
organizer,
|
||||||
|
advice,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const laserAimAPI = async () => {
|
||||||
|
return request("POST", "/user/device/laserAim");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const laserCloseAPI = async () => {
|
||||||
|
return request("POST", "/user/device/closeAim");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDeviceBatteryAPI = async () => {
|
||||||
|
return request("GET", "/user/device/battery");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addNoteAPI = async (id, remark) => {
|
||||||
|
return request("POST", "/user/score/sheet/remark", { id, remark });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removePointRecord = async (id) => {
|
||||||
|
return request("DELETE", `/user/score/sheet/delete?id=${id}`);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,91 +1,245 @@
|
|||||||
const audioFils = {
|
const audioFils = {
|
||||||
|
胜利: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcuo9yjp0kt5msvmvd.mp3",
|
||||||
|
失败: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcuo9yht2sdwhuqygy.mp3",
|
||||||
|
请射箭测试距离:
|
||||||
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcuvj8avzqyw4hpq7t.mp3",
|
||||||
|
距离合格:
|
||||||
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutwrda0amn5kqr4j.mp3",
|
||||||
|
距离不足:
|
||||||
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya57qurnsj6pg4.mp3",
|
||||||
|
轮到你了:
|
||||||
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrn4lxcpv8aqr.mp3",
|
||||||
第一轮:
|
第一轮:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbblu6fl26qrspvy79.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9a7m1vz2w13.mp3",
|
||||||
第二轮:
|
第二轮:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbblu6fehshrpe5ook.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9ldnfexjxtw.mp3",
|
||||||
第三轮:
|
第三轮:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbblu6fgbz3iimk7yy.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr97m4ipxaze4.mp3",
|
||||||
第四轮:
|
第四轮:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbblu6fjwf50tlxxbi.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9x5addohlzf.mp3",
|
||||||
第五轮:
|
第五轮:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbblu6fg63lqrslhm7.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9d7lw2gebpv.mp3",
|
||||||
决金箭轮:
|
决金箭轮:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbblu6fhjycwubbwil.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutwrd9zs4oi2kujv.mp3",
|
||||||
请蓝方射击:
|
请蓝方射箭:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbblu6fr0zpluiabph.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrxcbe5ll46as.mp3",
|
||||||
请红方射击:
|
请红方射箭:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbblu6fu169yerpwey.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrl3re3dhlfjd.mp3",
|
||||||
中场休息:
|
中场休息:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbblu6fug8faqrbtwd.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutwrd9zdk1xyolst.mp3",
|
||||||
比赛结束:
|
比赛结束:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbblsdl717ilr0b3o0.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya59b6pu0ur4um.mp3",
|
||||||
比赛开始:
|
比赛开始:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbblsdl7qlkqgvthfr.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcuu5z3a3lumkutske.mp3",
|
||||||
请开始射击:
|
请开始射击:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbljrfx5guqt5oulk.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrl5u0iromqhf.mp3",
|
||||||
|
射击无效:
|
||||||
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya55ufiiw8oo55.mp3",
|
||||||
未上靶:
|
未上靶:
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbkxm60bul0khcoqq.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcuuznjc78ljhzuw1o.mp3",
|
||||||
"1环":
|
"1环":
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbklufj59qmdo96ha.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin1aq7gxjih5l.mp3",
|
||||||
"2环":
|
"2环":
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbklufogy49ousbv4.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin64tdgx2s4at.mp3",
|
||||||
"3环":
|
"3环":
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbklufl3hhijeasck.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinlmf87vt8z65.mp3",
|
||||||
"4环":
|
"4环":
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbklufo8vo7k6jxdz.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinniv97sx0q9u.mp3",
|
||||||
"5环":
|
"5环":
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbklkzq7lrbfpr6ij.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin7j01kknpb7k.mp3",
|
||||||
"6环":
|
"6环":
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbkll0fw7hbmmhxkl.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin4syy1015rtq.mp3",
|
||||||
"7环":
|
"7环":
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbkll0fkirkanghmf.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin3iz3dvmjdai.mp3",
|
||||||
"8环":
|
"8环":
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbkll0cly2noykieg.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinnjd42lhpfiw.mp3",
|
||||||
"9环":
|
"9环":
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbkll0gsuumekhpkn.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin69nj1xh7yfz.mp3",
|
||||||
"10环":
|
"10环":
|
||||||
"https://static.shelingxingqiu.com/attachment/2025-07-14/dbbklgw2dk22ek7qha.mp3",
|
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinnvsx0tt7ksa.mp3",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 版本控制日志函数
|
||||||
|
function debugLog(...args) {
|
||||||
|
// 获取当前环境信息
|
||||||
|
const accountInfo = uni.getAccountInfoSync();
|
||||||
|
const envVersion = accountInfo.miniProgram.envVersion;
|
||||||
|
|
||||||
|
// 只在体验版打印日志,正式版(release)和开发版(develop)不打印
|
||||||
|
if (envVersion === 'trial') {
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AudioManager {
|
class AudioManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.audioMap = new Map();
|
this.audioMap = new Map();
|
||||||
Object.keys(audioFils).forEach((key) => {
|
this.currentPlayingKey = null;
|
||||||
|
this.retryCount = new Map();
|
||||||
|
this.maxRetries = 3;
|
||||||
|
|
||||||
|
// 串行加载相关属性
|
||||||
|
this.audioKeys = [];
|
||||||
|
this.currentLoadingIndex = 0;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.loadingPromise = null;
|
||||||
|
|
||||||
|
this.initAudios();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化音频
|
||||||
|
initAudios() {
|
||||||
|
if (this.isLoading) {
|
||||||
|
debugLog("音频正在加载中,跳过重复初始化");
|
||||||
|
return this.loadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("开始串行加载音频...");
|
||||||
|
this.isLoading = true;
|
||||||
|
this.audioKeys = Object.keys(audioFils);
|
||||||
|
this.currentLoadingIndex = 0;
|
||||||
|
|
||||||
|
this.loadingPromise = new Promise((resolve) => {
|
||||||
|
this.loadNextAudio(resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.loadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 串行加载下一个音频
|
||||||
|
loadNextAudio(onComplete) {
|
||||||
|
if (this.currentLoadingIndex >= this.audioKeys.length) {
|
||||||
|
debugLog("所有音频加载完成");
|
||||||
|
this.isLoading = false;
|
||||||
|
if (onComplete) onComplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = this.audioKeys[this.currentLoadingIndex];
|
||||||
|
debugLog(`开始加载音频 ${this.currentLoadingIndex + 1}/${this.audioKeys.length}: ${key}`);
|
||||||
|
|
||||||
|
this.createAudio(key, () => {
|
||||||
|
this.currentLoadingIndex++;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadNextAudio(onComplete);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建单个音频实例
|
||||||
|
createAudio(key, callback) {
|
||||||
|
const src = audioFils[key];
|
||||||
const audio = uni.createInnerAudioContext();
|
const audio = uni.createInnerAudioContext();
|
||||||
audio.src = audioFils[key];
|
audio.src = src;
|
||||||
audio.autoplay = false;
|
audio.autoplay = false;
|
||||||
|
|
||||||
|
// 设置加载超时
|
||||||
|
const loadTimeout = setTimeout(() => {
|
||||||
|
debugLog(`音频 ${key} 加载超时`);
|
||||||
|
audio.destroy();
|
||||||
|
if (callback) callback();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
// 监听加载状态
|
// 监听加载状态
|
||||||
audio.onCanplay(() => {
|
audio.onCanplay(() => {
|
||||||
// console.log(`音频 ${key} 已加载完成`);
|
clearTimeout(loadTimeout);
|
||||||
|
debugLog(`音频 ${key} 已加载完成`);
|
||||||
|
this.retryCount.set(key, 0);
|
||||||
|
if (callback) callback();
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.onError((res) => {
|
audio.onError((res) => {
|
||||||
console.log(`音频 ${key} 加载失败:`, res.errMsg);
|
clearTimeout(loadTimeout);
|
||||||
|
debugLog(`音频 ${key} 加载失败:`, res.errMsg);
|
||||||
|
this.handleAudioError(key);
|
||||||
|
if (callback) callback();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听播放结束事件
|
||||||
|
audio.onEnded(() => {
|
||||||
|
if (this.currentPlayingKey === key) {
|
||||||
|
this.currentPlayingKey = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听播放停止事件
|
||||||
|
audio.onStop(() => {
|
||||||
|
if (this.currentPlayingKey === key) {
|
||||||
|
this.currentPlayingKey = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.audioMap.set(key, audio);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载音频
|
||||||
|
retryLoadAudio(key) {
|
||||||
|
const oldAudio = this.audioMap.get(key);
|
||||||
|
if (oldAudio) {
|
||||||
|
oldAudio.destroy();
|
||||||
|
}
|
||||||
|
this.createAudio(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放指定音频
|
// 播放指定音频
|
||||||
play(key) {
|
play(key) {
|
||||||
|
// 如果有正在播放的音频,先停止
|
||||||
|
if (this.currentPlayingKey) {
|
||||||
|
this.stop(this.currentPlayingKey);
|
||||||
|
}
|
||||||
|
|
||||||
const audio = this.audioMap.get(key);
|
const audio = this.audioMap.get(key);
|
||||||
if (audio) audio.play();
|
if (audio) {
|
||||||
|
audio.play();
|
||||||
|
this.currentPlayingKey = key;
|
||||||
|
} else {
|
||||||
|
debugLog(`音频 ${key} 不存在,尝试重新加载...`);
|
||||||
|
this.reloadAudio(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止指定音频
|
// 停止指定音频
|
||||||
stop(key) {
|
stop(key) {
|
||||||
const audio = this.audioMap.get(key);
|
const audio = this.audioMap.get(key);
|
||||||
if (audio) audio.stop();
|
if (audio) {
|
||||||
|
audio.stop();
|
||||||
|
if (this.currentPlayingKey === key) {
|
||||||
|
this.currentPlayingKey = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 销毁所有音频实例
|
// 手动重新加载指定音频
|
||||||
destroyAll() {
|
reloadAudio(key) {
|
||||||
this.audioMap.forEach((audio) => {
|
if (audioFils[key]) {
|
||||||
audio.destroy();
|
debugLog(`手动重新加载音频: ${key}`);
|
||||||
});
|
this.retryCount.set(key, 0);
|
||||||
this.audioMap.clear();
|
this.retryLoadAudio(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ onMounted(() => {
|
|||||||
src="../static/app-bg4.png"
|
src="../static/app-bg4.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
|
<image
|
||||||
|
class="bg-image"
|
||||||
|
v-if="type === 4"
|
||||||
|
src="../static/app-bg5.png"
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
<view class="bg-overlay" v-if="type === 0"></view>
|
<view class="bg-overlay" v-if="type === 0"></view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,15 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import useStore from "@/store";
|
|
||||||
import { storeToRefs } from "pinia";
|
|
||||||
const store = useStore();
|
|
||||||
const { user } = storeToRefs(store);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
signin: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ image: "../static/tab-vip.png" },
|
{ image: "../static/tab-vip.png" },
|
||||||
@@ -18,7 +7,6 @@ const tabs = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function handleTabClick(index) {
|
function handleTabClick(index) {
|
||||||
if (index === 1 && !user.value.id) return props.signin();
|
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/be-vip",
|
url: "/pages/be-vip",
|
||||||
@@ -26,7 +14,7 @@ function handleTabClick(index) {
|
|||||||
}
|
}
|
||||||
if (index === 1) {
|
if (index === 1) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/point-book-create",
|
url: "/pages/point-book",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (index === 2) {
|
if (index === 2) {
|
||||||
@@ -56,7 +44,7 @@ function handleTabClick(index) {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.footer {
|
.footer {
|
||||||
height: 120px;
|
height: 117px;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -72,17 +60,22 @@ function handleTabClick(index) {
|
|||||||
}
|
}
|
||||||
.tab-item {
|
.tab-item {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.tab-item > image {
|
.tab-item > image {
|
||||||
width: 86%;
|
width: 86%;
|
||||||
}
|
}
|
||||||
.tab-item:nth-child(2) {
|
.tab-item:nth-child(2) {
|
||||||
transform: translate(25%, 20%);
|
transform: translate(25%, 30%);
|
||||||
}
|
}
|
||||||
.tab-item:nth-child(3) {
|
.tab-item:nth-child(3) {
|
||||||
transform: translate(6%, -10%);
|
margin-bottom: 25rpx;
|
||||||
|
}
|
||||||
|
.tab-item:nth-child(3) > image {
|
||||||
|
width: 140rpx;
|
||||||
}
|
}
|
||||||
.tab-item:nth-child(4) {
|
.tab-item:nth-child(4) {
|
||||||
transform: translate(-25%, 20%);
|
transform: translate(-25%, 30%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
import { onShow } from "@dcloudio/uni-app";
|
||||||
import { isGamingAPI, getCurrentGameAPI } from "@/apis";
|
import { isGamingAPI, getCurrentGameAPI } from "@/apis";
|
||||||
import { debounce } from "@/util";
|
import { debounce } from "@/util";
|
||||||
@@ -49,7 +49,7 @@ const gameOver = () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
uni.$on("game-over", gameOver);
|
uni.$on("game-over", gameOver);
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.$off("game-over", gameOver);
|
uni.$off("game-over", gameOver);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ defineProps({
|
|||||||
}
|
}
|
||||||
.players > view > view > image:first-child {
|
.players > view > view > image:first-child {
|
||||||
width: 72px;
|
width: 72px;
|
||||||
|
height: 20px;
|
||||||
}
|
}
|
||||||
.players > view > view > view:last-child {
|
.players > view > view > view:last-child {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|||||||
@@ -46,17 +46,18 @@ defineProps({
|
|||||||
v-for="(player, index) in blueTeam"
|
v-for="(player, index) in blueTeam"
|
||||||
:key="index"
|
:key="index"
|
||||||
:style="{
|
:style="{
|
||||||
margin: blueTeam.length === 2 ? '0 12px' : '0 6px',
|
margin: blueTeam.length === 2 ? '0 -5px' : '0 6px',
|
||||||
width: `${100 / blueTeam.length - blueTeam.length * 3}%`,
|
width: `${100 / blueTeam.length - blueTeam.length * 3}%`,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Avatar :src="player.avatar" :rankLvl="player.rankLvl" />
|
<Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" />
|
||||||
<text class="player-name">{{ player.name }}</text>
|
<text class="player-name">{{ player.name }}</text>
|
||||||
</view>
|
</view>
|
||||||
<image
|
<image
|
||||||
v-if="winner === 1"
|
v-if="winner === 1"
|
||||||
src="../static/winner-badge.png"
|
src="../static/winner-badge.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
|
class="left-winner-badge"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
@@ -64,17 +65,18 @@ defineProps({
|
|||||||
v-for="(player, index) in redTeam"
|
v-for="(player, index) in redTeam"
|
||||||
:key="index"
|
:key="index"
|
||||||
:style="{
|
:style="{
|
||||||
margin: redTeam.length === 2 ? '0 12px' : '0 6px',
|
margin: redTeam.length === 2 ? '0 -5px' : '0 6px',
|
||||||
width: `${100 / redTeam.length - redTeam.length * 3}%`,
|
width: `${100 / redTeam.length - redTeam.length * 3}%`,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Avatar :src="player.avatar" :rankLvl="player.rankLvl" />
|
<Avatar :src="player.avatar" :rankLvl="player.rankLvl" :size="40" />
|
||||||
<text class="player-name">{{ player.name }}</text>
|
<text class="player-name">{{ player.name }}</text>
|
||||||
</view>
|
</view>
|
||||||
<image
|
<image
|
||||||
v-if="winner === 0"
|
v-if="winner === 0"
|
||||||
src="../static/winner-badge.png"
|
src="../static/winner-badge.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
|
class="right-winner-badge"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -93,7 +95,7 @@ defineProps({
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="player.avatar"
|
:src="player.avatar"
|
||||||
:rankLvl="player.rankLvl"
|
:rankLvl="showRank ? undefined : player.rankLvl"
|
||||||
:size="40"
|
:size="40"
|
||||||
:rank="showRank ? index + 1 : 0"
|
:rank="showRank ? index + 1 : 0"
|
||||||
/>
|
/>
|
||||||
@@ -120,12 +122,12 @@ defineProps({
|
|||||||
}
|
}
|
||||||
.players > view {
|
.players > view {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
height: 80px;
|
height: 75px;
|
||||||
color: #fff9;
|
color: #fff9;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-top: 7px;
|
padding-top: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@@ -135,13 +137,6 @@ defineProps({
|
|||||||
.players > view:last-child {
|
.players > view:last-child {
|
||||||
background-color: #692735;
|
background-color: #692735;
|
||||||
}
|
}
|
||||||
.players > view > image:last-child {
|
|
||||||
position: absolute;
|
|
||||||
width: 50px;
|
|
||||||
top: -10%;
|
|
||||||
left: -5%;
|
|
||||||
transform: rotate(-12deg);
|
|
||||||
}
|
|
||||||
.players > view > view {
|
.players > view > view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -170,11 +165,25 @@ defineProps({
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
.player-name {
|
.player-name {
|
||||||
margin-top: 5px;
|
margin-top: 3px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.left-winner-badge {
|
||||||
|
position: absolute;
|
||||||
|
width: 50px;
|
||||||
|
top: -12%;
|
||||||
|
left: -5%;
|
||||||
|
transform: rotate(-12deg);
|
||||||
|
}
|
||||||
|
.right-winner-badge {
|
||||||
|
position: absolute;
|
||||||
|
width: 50px;
|
||||||
|
top: -12%;
|
||||||
|
right: -5%;
|
||||||
|
transform: rotate(36deg);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -49,21 +49,11 @@ const props = defineProps({
|
|||||||
|
|
||||||
const showsimul = ref(false);
|
const showsimul = ref(false);
|
||||||
const latestOne = ref(null);
|
const latestOne = ref(null);
|
||||||
|
const bluelatestOne = ref(null);
|
||||||
const prevScores = ref([]);
|
const prevScores = ref([]);
|
||||||
const prevBlueScores = ref([]);
|
const prevBlueScores = ref([]);
|
||||||
// const startCount = ref(false);
|
|
||||||
const timer = ref(null);
|
const timer = ref(null);
|
||||||
|
|
||||||
// watch(
|
|
||||||
// () => props.start,
|
|
||||||
// (newVal) => {
|
|
||||||
// startCount.value = newVal;
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// immediate: true,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.scores,
|
() => props.scores,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
@@ -85,10 +75,10 @@ watch(
|
|||||||
() => props.blueScores,
|
() => props.blueScores,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
if (newVal.length - prevBlueScores.value.length === 1) {
|
if (newVal.length - prevBlueScores.value.length === 1) {
|
||||||
latestOne.value = newVal[newVal.length - 1];
|
bluelatestOne.value = newVal[newVal.length - 1];
|
||||||
if (timer.value) clearTimeout(timer.value);
|
if (timer.value) clearTimeout(timer.value);
|
||||||
timer.value = setTimeout(() => {
|
timer.value = setTimeout(() => {
|
||||||
latestOne.value = null;
|
bluelatestOne.value = null;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
prevBlueScores.value = [...newVal];
|
prevBlueScores.value = [...newVal];
|
||||||
@@ -98,13 +88,13 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function calcRealX(num, offset = 12) {
|
function calcRealX(num, offset = 3.4) {
|
||||||
const len = 20.4 + num;
|
const len = 20.4 + num;
|
||||||
return `calc(${(len / 40.8) * 100}% - ${offset / 2}px)`;
|
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
|
||||||
}
|
}
|
||||||
function calcRealY(num, offset = 12) {
|
function calcRealY(num, offset = 3.4) {
|
||||||
const len = num < 0 ? Math.abs(num) + 20.4 : 20.4 - num;
|
const len = num < 0 ? Math.abs(num) + 20.4 : 20.4 - num;
|
||||||
return `calc(${(len / 40.8) * 100}% - ${offset / 2}px)`;
|
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
|
||||||
}
|
}
|
||||||
const simulShoot = async () => {
|
const simulShoot = async () => {
|
||||||
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
||||||
@@ -136,8 +126,8 @@ onMounted(() => {
|
|||||||
v-if="latestOne && user.id === latestOne.playerId"
|
v-if="latestOne && user.id === latestOne.playerId"
|
||||||
class="e-value fade-in-out"
|
class="e-value fade-in-out"
|
||||||
:style="{
|
:style="{
|
||||||
left: calcRealX(latestOne.ring ? latestOne.x : 0, 66),
|
left: calcRealX(latestOne.ring ? latestOne.x : 0, 20),
|
||||||
top: calcRealY(latestOne.ring ? latestOne.y : 0, 150),
|
top: calcRealY(latestOne.ring ? latestOne.y : 0, 40),
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
经验 +1
|
经验 +1
|
||||||
@@ -146,8 +136,8 @@ onMounted(() => {
|
|||||||
v-if="latestOne"
|
v-if="latestOne"
|
||||||
class="round-tip fade-in-out"
|
class="round-tip fade-in-out"
|
||||||
:style="{
|
:style="{
|
||||||
left: calcRealX(latestOne.ring ? latestOne.x : 0, 100),
|
left: calcRealX(latestOne.ring ? latestOne.x : 0, 28),
|
||||||
top: calcRealY(latestOne.ring ? latestOne.y : 0, 100),
|
top: calcRealY(latestOne.ring ? latestOne.y : 0, 28),
|
||||||
}"
|
}"
|
||||||
>{{ latestOne.ring || "未上靶"
|
>{{ latestOne.ring || "未上靶"
|
||||||
}}<text v-if="latestOne.ring">环</text></view
|
}}<text v-if="latestOne.ring">环</text></view
|
||||||
@@ -176,7 +166,7 @@ onMounted(() => {
|
|||||||
<view
|
<view
|
||||||
v-if="bow.ring > 0"
|
v-if="bow.ring > 0"
|
||||||
:class="`hit ${
|
:class="`hit ${
|
||||||
index === blueScores.length - 1 && latestOne ? 'pump-in' : ''
|
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
|
||||||
}`"
|
}`"
|
||||||
:style="{
|
:style="{
|
||||||
left: calcRealX(bow.x),
|
left: calcRealX(bow.x),
|
||||||
@@ -205,13 +195,16 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
.container {
|
||||||
width: calc(100% - 30px);
|
width: calc(100vw - 30px);
|
||||||
|
height: calc(100vw - 30px);
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.target {
|
.target {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
height: calc(100% - 20px);
|
||||||
}
|
}
|
||||||
.e-value {
|
.e-value {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -243,25 +236,32 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
.target > image:last-child {
|
.target > image:last-child {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
.hit {
|
.hit {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 12px;
|
width: 3.4%;
|
||||||
height: 12px;
|
height: 3.4%;
|
||||||
|
min-width: 3.4%;
|
||||||
|
min-height: 3.4%;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 8px;
|
font-size: 2.1vw;
|
||||||
text-align: center;
|
|
||||||
line-height: 10px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
/* transform: translate(-50%, -50%); */
|
||||||
}
|
}
|
||||||
.hit > text {
|
.hit > text {
|
||||||
transform: scaleX(0.7);
|
transform: scaleX(0.7) translateY(-0.5px);
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
editMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const rect = ref({});
|
const rect = ref({});
|
||||||
@@ -27,8 +31,6 @@ const isDragging = ref(false);
|
|||||||
const dragStartPos = ref({ x: 0, y: 0 });
|
const dragStartPos = ref({ x: 0, y: 0 });
|
||||||
const capsuleHeight = ref(0);
|
const capsuleHeight = ref(0);
|
||||||
const scale = ref(1);
|
const scale = ref(1);
|
||||||
const zoomPos = ref({ x: 0, y: 0 });
|
|
||||||
const targetPos = ref({ x: 0, y: 0 });
|
|
||||||
let lastMoveTime = 0;
|
let lastMoveTime = 0;
|
||||||
|
|
||||||
// 点击靶纸创建新的点
|
// 点击靶纸创建新的点
|
||||||
@@ -40,21 +42,14 @@ const onClick = async (e) => {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (props.id === 7 || props.id === 9) {
|
||||||
|
scale.value = 1.5;
|
||||||
|
}
|
||||||
const newArrow = {
|
const newArrow = {
|
||||||
x: e.detail.x - zoomPos.value.x - 6 / scale.value,
|
x: (e.detail.x - 6) * scale.value,
|
||||||
y:
|
y: (e.detail.y - rect.value.top - capsuleHeight.value - 6) * scale.value,
|
||||||
e.detail.y -
|
|
||||||
rect.value.top -
|
|
||||||
capsuleHeight.value -
|
|
||||||
zoomPos.value.y -
|
|
||||||
6 / scale.value,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
targetPos.value = {
|
|
||||||
x: zoomPos.value.x,
|
|
||||||
y: zoomPos.value.y,
|
|
||||||
};
|
|
||||||
const side = rect.value.width;
|
const side = rect.value.width;
|
||||||
newArrow.ring = calcRing(
|
newArrow.ring = calcRing(
|
||||||
props.id,
|
props.id,
|
||||||
@@ -72,10 +67,6 @@ const onClick = async (e) => {
|
|||||||
// 确认添加箭矢
|
// 确认添加箭矢
|
||||||
const confirmAdd = () => {
|
const confirmAdd = () => {
|
||||||
if (props.onChange) {
|
if (props.onChange) {
|
||||||
targetPos.value = {
|
|
||||||
x: zoomPos.value.x,
|
|
||||||
y: zoomPos.value.y,
|
|
||||||
};
|
|
||||||
props.onChange({
|
props.onChange({
|
||||||
x: arrow.value.x / scale.value,
|
x: arrow.value.x / scale.value,
|
||||||
y: arrow.value.y / scale.value,
|
y: arrow.value.y / scale.value,
|
||||||
@@ -83,15 +74,13 @@ const confirmAdd = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
arrow.value = null;
|
arrow.value = null;
|
||||||
|
scale.value = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除箭矢
|
// 删除箭矢
|
||||||
const deleteArrow = () => {
|
const deleteArrow = () => {
|
||||||
arrow.value = null;
|
arrow.value = null;
|
||||||
targetPos.value = {
|
scale.value = 1;
|
||||||
x: zoomPos.value.x,
|
|
||||||
y: zoomPos.value.y,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 开始拖拽 - 同样修复坐标获取
|
// 开始拖拽 - 同样修复坐标获取
|
||||||
@@ -142,21 +131,17 @@ const onDrag = async (e) => {
|
|||||||
const endDrag = (e) => {
|
const endDrag = (e) => {
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
};
|
};
|
||||||
const onScale = (e) => {
|
|
||||||
lastMoveTime = Date.now();
|
|
||||||
const lastScale = scale.value;
|
|
||||||
scale.value = e.detail.scale;
|
|
||||||
zoomPos.value = { x: e.detail.x, y: e.detail.y };
|
|
||||||
if (arrow.value) {
|
|
||||||
arrow.value.x = arrow.value.x * (scale.value / lastScale);
|
|
||||||
arrow.value.y = arrow.value.y * (scale.value / lastScale);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMove = (e) => {
|
const getNewPos = () => {
|
||||||
if (e.detail.source) {
|
if (props.id === 7 || props.id === 9) {
|
||||||
zoomPos.value = { x: e.detail.x, y: e.detail.y };
|
if (arrow.value.y > 1.4)
|
||||||
|
return { left: "-12px", bottom: "calc(50% - 12px)" };
|
||||||
|
} else {
|
||||||
|
if (arrow.value.y > 0.88) {
|
||||||
|
return { left: "-12px", bottom: "calc(50% - 12px)" };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return { left: "calc(50% - 12px)", bottom: "-12px" };
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -169,25 +154,21 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view
|
<view
|
||||||
|
:style="{ overflowY: editMode ? 'auto' : 'hidden' }"
|
||||||
class="container"
|
class="container"
|
||||||
@tap="onClick"
|
@tap="onClick"
|
||||||
@touchmove="onDrag"
|
@touchmove="onDrag"
|
||||||
@touchend="endDrag"
|
@touchend="endDrag"
|
||||||
>
|
>
|
||||||
<movable-area class="move-area" scale-area>
|
<movable-area
|
||||||
<movable-view
|
class="move-area"
|
||||||
class="move-view"
|
:style="{
|
||||||
direction="all"
|
width: scale * 100 + 'vw',
|
||||||
scale
|
height: scale * 100 + 'vw',
|
||||||
:x="targetPos.x"
|
transform: `translate(${(100 - scale * 100) / 2}vw,${
|
||||||
:y="targetPos.y"
|
(100 - scale * 100) / 2
|
||||||
:scale-min="1"
|
}vw) translateY(${scale > 1 ? 16.7 : 0}%)`,
|
||||||
:scale-max="2"
|
}"
|
||||||
:scale-value="scale"
|
|
||||||
:animation="false"
|
|
||||||
@scale="onScale"
|
|
||||||
@change="onMove"
|
|
||||||
:out-of-bounds="true"
|
|
||||||
>
|
>
|
||||||
<image :src="src" mode="widthFix" />
|
<image :src="src" mode="widthFix" />
|
||||||
<view
|
<view
|
||||||
@@ -202,6 +183,9 @@ onMounted(async () => {
|
|||||||
<view
|
<view
|
||||||
v-if="arrow.x !== undefined && arrow.y !== undefined"
|
v-if="arrow.x !== undefined && arrow.y !== undefined"
|
||||||
class="point"
|
class="point"
|
||||||
|
:style="{
|
||||||
|
transform: props.id === 7 || props.id === 9 ? 'scale(0.7)' : '',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<text>{{ index + 1 }}</text>
|
<text>{{ index + 1 }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -212,45 +196,37 @@ onMounted(async () => {
|
|||||||
direction="all"
|
direction="all"
|
||||||
:animation="false"
|
:animation="false"
|
||||||
:out-of-bounds="true"
|
:out-of-bounds="true"
|
||||||
:x="arrow ? (rect.width * arrow.x) / scale : 0"
|
:x="arrow ? rect.width * arrow.x : 0"
|
||||||
:y="arrow ? (rect.width * arrow.y) / scale : 0"
|
:y="arrow ? rect.width * arrow.y : 0"
|
||||||
>
|
>
|
||||||
<view class="point"> </view>
|
<view class="point"> </view>
|
||||||
<view
|
<view v-if="arrow" class="edit-buttons" @touchstart.stop>
|
||||||
v-if="arrow"
|
|
||||||
class="edit-buttons"
|
|
||||||
@touchstart.stop
|
|
||||||
:style="{ transform: `scale(${1 / scale})` }"
|
|
||||||
>
|
|
||||||
<view class="edit-btn-text">
|
<view class="edit-btn-text">
|
||||||
<!-- <text v-if="arrow.ring === 0" :style="{ width: '100%' }"
|
|
||||||
>未上靶</text
|
|
||||||
> -->
|
|
||||||
<text>{{ arrow.ring === 0 ? "M" : arrow.ring }}</text>
|
<text>{{ arrow.ring === 0 ? "M" : arrow.ring }}</text>
|
||||||
<!-- <text
|
<text
|
||||||
v-if="arrow.ring > 0"
|
v-if="arrow.ring > 0"
|
||||||
:style="{
|
:style="{
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
marginLeft: '2px',
|
marginLeft: '2px',
|
||||||
}"
|
}"
|
||||||
>环</text
|
>points</text
|
||||||
> -->
|
>
|
||||||
</view>
|
</view>
|
||||||
<view class="edit-btn confirm-btn" @touchstart.stop="confirmAdd">
|
<view
|
||||||
|
class="edit-btn confirm-btn"
|
||||||
|
@touchstart.stop="confirmAdd"
|
||||||
|
:style="{ ...getNewPos() }"
|
||||||
|
>
|
||||||
<image src="../static/arrow-edit-save.png" mode="widthFix" />
|
<image src="../static/arrow-edit-save.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
<view class="edit-btn delete-btn" @touchstart.stop="deleteArrow">
|
<view class="edit-btn delete-btn" @touchstart.stop="deleteArrow">
|
||||||
<image src="../static/arrow-edit-delete.png" mode="widthFix" />
|
<image src="../static/arrow-edit-delete.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view class="edit-btn drag-btn" @touchstart.stop="startDrag($event)">
|
||||||
class="edit-btn drag-btn"
|
|
||||||
@touchstart.stop="startDrag($event)"
|
|
||||||
>
|
|
||||||
<image src="../static/arrow-edit-move.png" mode="widthFix" />
|
<image src="../static/arrow-edit-move.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</movable-view>
|
</movable-view>
|
||||||
</movable-view>
|
|
||||||
</movable-area>
|
</movable-area>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -259,14 +235,24 @@ onMounted(async () => {
|
|||||||
.container {
|
.container {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vw;
|
height: 100vw;
|
||||||
overflow: hidden;
|
overflow-x: hidden;
|
||||||
transform: translateY(-10px);
|
}
|
||||||
|
.container::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.move-area {
|
.move-area {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-area > image {
|
||||||
|
width: 90%;
|
||||||
|
height: 90%;
|
||||||
|
margin: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.move-view {
|
.move-view {
|
||||||
@@ -300,6 +286,7 @@ onMounted(async () => {
|
|||||||
background-color: #ff4444;
|
background-color: #ff4444;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
transition: all 0.1s linear;
|
transition: all 0.1s linear;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.point > text {
|
.point > text {
|
||||||
@@ -323,8 +310,8 @@ onMounted(async () => {
|
|||||||
.edit-btn-text {
|
.edit-btn-text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
/* justify-content: center; */
|
justify-content: center;
|
||||||
margin-left: 10px;
|
/* margin-left: 10px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-btn-text > text {
|
.edit-btn-text > text {
|
||||||
@@ -350,8 +337,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.confirm-btn {
|
.confirm-btn {
|
||||||
left: calc(50% - 12px);
|
transition: all 0.3s ease;
|
||||||
bottom: -12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-btn {
|
.delete-btn {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
import { onShow } from "@dcloudio/uni-app";
|
||||||
import AppBackground from "@/components/AppBackground.vue";
|
import AppBackground from "@/components/AppBackground.vue";
|
||||||
import Header from "@/components/Header.vue";
|
import Header from "@/components/Header.vue";
|
||||||
@@ -44,30 +44,69 @@ const props = defineProps({
|
|||||||
const showHint = ref(false);
|
const showHint = ref(false);
|
||||||
const hintType = ref(0);
|
const hintType = ref(0);
|
||||||
const capsuleHeight = ref(0);
|
const capsuleHeight = ref(0);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
const showGlobalHint = (type) => {
|
const showGlobalHint = (type) => {
|
||||||
hintType.value = type;
|
hintType.value = type;
|
||||||
showHint.value = true;
|
showHint.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideGlobalHint = () => {
|
const hideGlobalHint = () => {
|
||||||
showHint.value = false;
|
showHint.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
|
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
|
||||||
capsuleHeight.value = menuBtnInfo.top - 9;
|
capsuleHeight.value = menuBtnInfo.top - 9;
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
|
||||||
// const pages = getCurrentPages();
|
|
||||||
// const currentPage = pages[pages.length - 1];
|
|
||||||
// uni.setStorageSync("last-route", currentPage.route);
|
|
||||||
});
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
uni.$showHint = showGlobalHint;
|
uni.$showHint = showGlobalHint;
|
||||||
uni.$hideHint = hideGlobalHint;
|
uni.$hideHint = hideGlobalHint;
|
||||||
showHint.value = false;
|
showHint.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const backToGame = debounce(async () => {
|
const backToGame = debounce(async () => {
|
||||||
const result = await getCurrentGameAPI();
|
if (isLoading.value) return; // 防止重复点击
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// 设置请求超时
|
||||||
|
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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showHint.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取当前游戏失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: error.message || '网络请求失败,请重试',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
};
|
};
|
||||||
@@ -100,7 +139,13 @@ const goBack = () => {
|
|||||||
<button hover-class="none" @click="() => (showHint = false)">
|
<button hover-class="none" @click="() => (showHint = false)">
|
||||||
不进入
|
不进入
|
||||||
</button>
|
</button>
|
||||||
<button hover-class="none" @click="backToGame">进入</button>
|
<button
|
||||||
|
hover-class="none"
|
||||||
|
@click="backToGame"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
{{ isLoading ? '加载中...' : '进入' }}
|
||||||
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="hintType === 2" class="tip-content">
|
<view v-if="hintType === 2" class="tip-content">
|
||||||
@@ -168,4 +213,10 @@ const goBack = () => {
|
|||||||
background-color: #fed847;
|
background-color: #fed847;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tip-content > view > button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
color: #666;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ const createRoom = async () => {
|
|||||||
if (battleMode.value === 2) size = 10;
|
if (battleMode.value === 2) size = 10;
|
||||||
if (battleMode.value === 3) size = 4;
|
if (battleMode.value === 3) size = 4;
|
||||||
if (battleMode.value === 4) size = 6;
|
if (battleMode.value === 4) size = 6;
|
||||||
const result = await createRoomAPI(battleMode.value === 2 ? 2 : 1, size);
|
const result = await createRoomAPI(
|
||||||
|
battleMode.value === 2 ? 2 : 1,
|
||||||
|
battleMode.value === 2 ? 10 : size
|
||||||
|
);
|
||||||
if (result.number) roomNumber.value = result.number;
|
if (result.number) roomNumber.value = result.number;
|
||||||
step.value = 2;
|
step.value = 2;
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
import { ref, watch, onMounted, computed } from "vue";
|
||||||
import { getPointBookConfigAPI } from "@/apis";
|
import { getPointBookConfigAPI } from "@/apis";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
itemIndex: {
|
itemIndex: {
|
||||||
@@ -27,13 +27,18 @@ const props = defineProps({
|
|||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const itemTexts = ["选择弓种", "选择练习距离", "选择靶纸", "选择组/箭数"];
|
const itemTexts = ["Select Bow", "Select Distance", "Select Target", "Select Sets/Arrows"];
|
||||||
const distances = [5, 8, 10, 18, 25, 30, 50, 60, 70];
|
const distances = [5, 8, 10, 18, 25, 30, 50, 60, 70];
|
||||||
const groupArrows = [3, 6, 12, 18];
|
const groupArrows = [3, 6, 12, 18];
|
||||||
|
|
||||||
const data = ref([]);
|
const data = ref([]);
|
||||||
const selectedIndex = ref(-1);
|
const selectedIndex = ref(-1);
|
||||||
const secondSelectIndex = ref(-1);
|
const secondSelectIndex = ref(-1);
|
||||||
|
|
||||||
|
const meter = ref("");
|
||||||
|
const sets = ref("");
|
||||||
|
const arrowAmount = ref("");
|
||||||
|
|
||||||
const onSelectItem = (index) => {
|
const onSelectItem = (index) => {
|
||||||
selectedIndex.value = index;
|
selectedIndex.value = index;
|
||||||
if (props.itemIndex === 0) {
|
if (props.itemIndex === 0) {
|
||||||
@@ -42,27 +47,58 @@ const onSelectItem = (index) => {
|
|||||||
props.onSelect(props.itemIndex, distances[index]);
|
props.onSelect(props.itemIndex, distances[index]);
|
||||||
} else if (props.itemIndex === 2) {
|
} else if (props.itemIndex === 2) {
|
||||||
props.onSelect(props.itemIndex, data.value[index]);
|
props.onSelect(props.itemIndex, data.value[index]);
|
||||||
} else if (props.itemIndex === 3 && secondSelectIndex.value !== -1) {
|
} else if (props.itemIndex === 3) {
|
||||||
|
if (secondSelectIndex.value !== -1) {
|
||||||
props.onSelect(
|
props.onSelect(
|
||||||
props.itemIndex,
|
props.itemIndex,
|
||||||
`${selectedIndex.value}/${groupArrows[secondSelectIndex.value]}`
|
`${selectedIndex.value}/${groupArrows[secondSelectIndex.value]}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const onSelectSecondItem = (index) => {
|
const onSelectSecondItem = (index) => {
|
||||||
secondSelectIndex.value = index;
|
secondSelectIndex.value = index;
|
||||||
if (selectedIndex.value !== -1) {
|
if (selectedIndex.value !== -1) {
|
||||||
props.onSelect(
|
props.onSelect(
|
||||||
props.itemIndex,
|
props.itemIndex,
|
||||||
`${selectedIndex.value}/${groupArrows[secondSelectIndex.value]}`
|
`${selectedIndex.value < 5 ? selectedIndex.value : sets.value}/${
|
||||||
|
groupArrows[secondSelectIndex.value]
|
||||||
|
}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const meter = ref("");
|
|
||||||
const onMeterChange = (e) => {
|
const onMeterChange = (e) => {
|
||||||
meter.value = e.detail.value;
|
meter.value = e.detail.value;
|
||||||
props.onSelect(props.itemIndex, e.detail.value);
|
props.onSelect(props.itemIndex, e.detail.value);
|
||||||
};
|
};
|
||||||
|
const onSetsChange = (e) => {
|
||||||
|
if (!e.detail.value) return;
|
||||||
|
sets.value = Math.min(30, Number(e.detail.value));
|
||||||
|
if (!sets.value) return;
|
||||||
|
if (secondSelectIndex.value !== -1) {
|
||||||
|
props.onSelect(
|
||||||
|
props.itemIndex,
|
||||||
|
`${sets.value}/${
|
||||||
|
secondSelectIndex.value === 99
|
||||||
|
? arrowAmount.value
|
||||||
|
: groupArrows[secondSelectIndex.value]
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onArrowAmountChange = (e) => {
|
||||||
|
if (!e.detail.value) return;
|
||||||
|
arrowAmount.value = Math.min(60, Number(e.detail.value));
|
||||||
|
if (!arrowAmount.value) return;
|
||||||
|
if (selectedIndex.value !== -1) {
|
||||||
|
props.onSelect(
|
||||||
|
props.itemIndex,
|
||||||
|
`${selectedIndex.value === 99 ? sets.value : selectedIndex.value}/${
|
||||||
|
arrowAmount.value
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
watch(
|
watch(
|
||||||
() => props.value,
|
() => props.value,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
@@ -114,6 +150,17 @@ 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} sets/${
|
||||||
|
secondSelectIndex.value === 99
|
||||||
|
? arrowAmount.value
|
||||||
|
: groupArrows[secondSelectIndex.value]
|
||||||
|
} arrows`;
|
||||||
|
});
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const config = uni.getStorageSync("point-book-config");
|
const config = uni.getStorageSync("point-book-config");
|
||||||
if (config) {
|
if (config) {
|
||||||
@@ -135,24 +182,21 @@ onMounted(async () => {
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<view @click="() => onExpand(itemIndex, !expand)">
|
<view @click="() => onExpand(itemIndex, !expand)">
|
||||||
<text :style="{ opacity: expand ? 1 : 0 }">{{
|
<view></view>
|
||||||
itemIndex !== 3 ? itemTexts[itemIndex] : "选择组"
|
|
||||||
}}</text>
|
|
||||||
<block>
|
<block>
|
||||||
<text :style="{ opacity: expand ? 0 : 1 }" v-if="itemIndex === 0">{{
|
<text v-if="expand" :style="{ color: '#999', fontWeight: 'normal' }">{{
|
||||||
|
itemIndex !== 3 ? itemTexts[itemIndex] : "Select Sets"
|
||||||
|
}}</text>
|
||||||
|
<text v-if="!expand && itemIndex === 0">{{
|
||||||
value || itemTexts[itemIndex]
|
value || itemTexts[itemIndex]
|
||||||
}}</text>
|
}}</text>
|
||||||
<text :style="{ opacity: expand ? 0 : 1 }" v-if="itemIndex === 1">{{
|
<text v-if="!expand && itemIndex === 1">{{
|
||||||
value && value > 0 ? value + "米" : itemTexts[itemIndex]
|
value && value > 0 ? value + " m" : itemTexts[itemIndex]
|
||||||
}}</text>
|
}}</text>
|
||||||
<text :style="{ opacity: expand ? 0 : 1 }" v-if="itemIndex === 2">{{
|
<text v-if="!expand && itemIndex === 2">{{
|
||||||
value || itemTexts[itemIndex]
|
value || itemTexts[itemIndex]
|
||||||
}}</text>
|
}}</text>
|
||||||
<text :style="{ opacity: expand ? 0 : 1 }" v-if="itemIndex === 3">{{
|
<text v-if="!expand && itemIndex === 3">{{ formatSetAndAmount }}</text>
|
||||||
selectedIndex !== -1 && secondSelectIndex !== -1
|
|
||||||
? `${selectedIndex + 1}组/${groupArrows[secondSelectIndex]}箭`
|
|
||||||
: itemTexts[itemIndex]
|
|
||||||
}}</text>
|
|
||||||
</block>
|
</block>
|
||||||
<button hover-class="none">
|
<button hover-class="none">
|
||||||
<image
|
<image
|
||||||
@@ -186,7 +230,7 @@ onMounted(async () => {
|
|||||||
@click="onSelectItem(index)"
|
@click="onSelectItem(index)"
|
||||||
>
|
>
|
||||||
<text>{{ item }}</text>
|
<text>{{ item }}</text>
|
||||||
<text>米</text>
|
<text>m</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
:style="{
|
:style="{
|
||||||
@@ -195,12 +239,13 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-model="meter"
|
v-model="meter"
|
||||||
placeholder="自定义"
|
type="number"
|
||||||
|
placeholder="Custom"
|
||||||
placeholder-style="color: #DDDDDD"
|
placeholder-style="color: #DDDDDD"
|
||||||
@focus="() => (selectedIndex = 9)"
|
@focus="() => (selectedIndex = 9)"
|
||||||
@change="onMeterChange"
|
@blur="onMeterChange"
|
||||||
/>
|
/>
|
||||||
<text>米</text>
|
<text>m</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="itemIndex === 2" class="bowtarget-items">
|
<view v-if="itemIndex === 2" class="bowtarget-items">
|
||||||
@@ -219,7 +264,7 @@ onMounted(async () => {
|
|||||||
<view v-if="itemIndex === 3">
|
<view v-if="itemIndex === 3">
|
||||||
<view class="amount-items">
|
<view class="amount-items">
|
||||||
<view
|
<view
|
||||||
v-for="i in 12"
|
v-for="i in 4"
|
||||||
:key="i"
|
:key="i"
|
||||||
:style="{
|
:style="{
|
||||||
borderColor: selectedIndex === i ? '#fed847' : '#eeeeee',
|
borderColor: selectedIndex === i ? '#fed847' : '#eeeeee',
|
||||||
@@ -227,12 +272,32 @@ onMounted(async () => {
|
|||||||
@click="onSelectItem(i)"
|
@click="onSelectItem(i)"
|
||||||
>
|
>
|
||||||
<text>{{ i }}</text>
|
<text>{{ i }}</text>
|
||||||
<text>组</text>
|
<text>sets</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
:style="{
|
||||||
|
borderColor: selectedIndex === 99 ? '#fed847' : '#eeeeee',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
placeholder="1 ~ 30"
|
||||||
|
type="number"
|
||||||
|
placeholder-style="color: #DDDDDD"
|
||||||
|
v-model="sets"
|
||||||
|
@focus="() => (selectedIndex = 99)"
|
||||||
|
@blur="onSetsChange"
|
||||||
|
/>
|
||||||
|
<text>sets</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
:style="{ marginTop: '5px', marginBottom: '10px', color: '#999999' }"
|
:style="{
|
||||||
>选择每组的箭数</view
|
marginTop: '5px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
color: '#999999',
|
||||||
|
textAlign: 'center',
|
||||||
|
}"
|
||||||
|
>Select arrows per set</view
|
||||||
>
|
>
|
||||||
<view class="amount-items">
|
<view class="amount-items">
|
||||||
<view
|
<view
|
||||||
@@ -244,7 +309,23 @@ onMounted(async () => {
|
|||||||
@click="onSelectSecondItem(index)"
|
@click="onSelectSecondItem(index)"
|
||||||
>
|
>
|
||||||
<text>{{ item }}</text>
|
<text>{{ item }}</text>
|
||||||
<text>箭</text>
|
<text>arrows</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
:style="{
|
||||||
|
borderColor: secondSelectIndex === 99 ? '#fed847' : '#eeeeee',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
placeholder="1 ~ 60"
|
||||||
|
type="number"
|
||||||
|
placeholder-style="color: #DDDDDD"
|
||||||
|
v-model="arrowAmount"
|
||||||
|
maxlength="99"
|
||||||
|
@focus="() => (secondSelectIndex = 99)"
|
||||||
|
@blur="onArrowAmountChange"
|
||||||
|
/>
|
||||||
|
<text>arrows</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -269,9 +350,8 @@ onMounted(async () => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
.container > view:first-child > text:first-child {
|
.container > view:first-child > view:first-child {
|
||||||
width: 85px;
|
width: 85px;
|
||||||
color: #999999;
|
|
||||||
}
|
}
|
||||||
.container > view:first-child > text:nth-child(2) {
|
.container > view:first-child > text:nth-child(2) {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -352,4 +432,12 @@ onMounted(async () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.amount-items > view:last-child {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.amount-items > view:last-child > input {
|
||||||
|
width: 85%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||||
import HeaderProgress from "@/components/HeaderProgress.vue";
|
import HeaderProgress from "@/components/HeaderProgress.vue";
|
||||||
|
import Avatar from "@/components/Avatar.vue";
|
||||||
|
|
||||||
|
import useStore from "@/store";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
const store = useStore();
|
||||||
|
const { user } = storeToRefs(store);
|
||||||
|
|
||||||
|
const currentPage = computed(() => {
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
return pages[pages.length - 1].route;
|
||||||
|
});
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@@ -18,18 +29,47 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
if (props.onBack) props.onBack();
|
if (props.onBack) {
|
||||||
else uni.navigateBack();
|
props.onBack();
|
||||||
|
} else {
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
if (pages.length > 1) {
|
||||||
|
uni.navigateBack();
|
||||||
|
} else {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: "/pages/index",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toUserPage = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: "/pages/user",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const signin = () => {
|
||||||
|
if (!user.value.id) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: "/pages/sign-in",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const showLoader = ref(false);
|
const showLoader = ref(false);
|
||||||
const pointBook = ref(null);
|
const pointBook = ref(null);
|
||||||
const showProgress = ref(false);
|
const showProgress = ref(false);
|
||||||
|
const heat = ref(0);
|
||||||
const updateLoading = (value) => {
|
const updateLoading = (value) => {
|
||||||
loading.value = value;
|
loading.value = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateHot = (value) => {
|
||||||
|
heat.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const pages = getCurrentPages();
|
const pages = getCurrentPages();
|
||||||
const currentPage = pages[pages.length - 1];
|
const currentPage = pages[pages.length - 1];
|
||||||
@@ -37,19 +77,20 @@ onMounted(() => {
|
|||||||
pointBook.value = uni.getStorageSync("point-book");
|
pointBook.value = uni.getStorageSync("point-book");
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
currentPage.route === "pages/battle-room" ||
|
|
||||||
currentPage.route === "pages/team-battle" ||
|
currentPage.route === "pages/team-battle" ||
|
||||||
currentPage.route === "pages/melee-match"
|
currentPage.route === "pages/melee-match"
|
||||||
) {
|
) {
|
||||||
showLoader.value = true;
|
showLoader.value = true;
|
||||||
}
|
}
|
||||||
uni.$on("update-header-loading", updateLoading);
|
|
||||||
if (currentPage.route === "pages/team-battle") {
|
if (currentPage.route === "pages/team-battle") {
|
||||||
showProgress.value = true;
|
showProgress.value = true;
|
||||||
}
|
}
|
||||||
|
uni.$on("update-header-loading", updateLoading);
|
||||||
|
uni.$on("update-hot", updateHot);
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.$off("update-header-loading", updateLoading);
|
uni.$off("update-header-loading", updateLoading);
|
||||||
|
uni.$off("update-hot", updateHot);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -64,6 +105,30 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view :style="{ color: whiteBackArrow ? '#fff' : '#000' }">
|
<view :style="{ color: whiteBackArrow ? '#fff' : '#000' }">
|
||||||
|
<view
|
||||||
|
v-if="currentPage === 'pages/point-book'"
|
||||||
|
class="user-header"
|
||||||
|
@click="signin"
|
||||||
|
>
|
||||||
|
<block v-if="user.id">
|
||||||
|
<Avatar
|
||||||
|
:src="user.avatar"
|
||||||
|
:onClick="toUserPage"
|
||||||
|
:size="40"
|
||||||
|
borderColor="#333"
|
||||||
|
/>
|
||||||
|
<text class="truncate">{{ user.nickName }}</text>
|
||||||
|
<image
|
||||||
|
v-if="heat"
|
||||||
|
:src="`../static/hot${heat}.png`"
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
|
</block>
|
||||||
|
<block v-else>
|
||||||
|
<image src="../static/user-icon.png" mode="widthFix" />
|
||||||
|
<text>新来的弓箭手你好呀~</text>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
<block
|
<block
|
||||||
v-if="
|
v-if="
|
||||||
'-凹造型-感知距离-小试牛刀'.indexOf(title) === -1 ||
|
'-凹造型-感知距离-小试牛刀'.indexOf(title) === -1 ||
|
||||||
@@ -191,4 +256,25 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.user-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.user-header > image:first-child {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2rpx solid #333;
|
||||||
|
}
|
||||||
|
.user-header > image:last-child {
|
||||||
|
width: 36rpx;
|
||||||
|
}
|
||||||
|
.user-header > text:nth-child(2) {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333333;
|
||||||
|
margin: 0 20rpx;
|
||||||
|
max-width: 300rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||||
import audioManager from "@/audioManager";
|
import audioManager from "@/audioManager";
|
||||||
import { MESSAGETYPES } from "@/constants";
|
import { MESSAGETYPES } from "@/constants";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
@@ -12,33 +12,37 @@ const melee = ref(false);
|
|||||||
const timer = ref(null);
|
const timer = ref(null);
|
||||||
const sound = ref(true);
|
const sound = ref(true);
|
||||||
const currentSound = ref("");
|
const currentSound = ref("");
|
||||||
const currentRound = ref(1);
|
const currentRound = ref(0);
|
||||||
const totalRound = ref(1);
|
|
||||||
const currentRoundEnded = ref(false);
|
const currentRoundEnded = ref(false);
|
||||||
const ended = ref(false);
|
const ended = ref(false);
|
||||||
const halfTime = ref(false);
|
const halfTime = ref(false);
|
||||||
|
const currentShot = ref(0);
|
||||||
|
const totalShot = ref(0);
|
||||||
|
const yourTurn = ref(false);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => tips.value,
|
() => tips.value,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
let key = "";
|
let key = "";
|
||||||
if (newVal.includes("红队")) key = "请红方射击";
|
if (newVal.includes("红队")) key = "请红方射箭";
|
||||||
if (newVal.includes("蓝队")) key = "请蓝方射击";
|
if (newVal.includes("蓝队")) key = "请蓝方射箭";
|
||||||
if (key && sound.value) {
|
if (!sound.value) return;
|
||||||
if (currentRoundEnded.value) {
|
if (currentRoundEnded.value) {
|
||||||
|
currentRound.value += 1;
|
||||||
|
// 播放当前轮次语音
|
||||||
|
audioManager.play(
|
||||||
|
`第${["一", "二", "三", "四", "五"][currentRound.value - 1]}轮`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 延迟播放队伍提示音
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
if (key && !yourTurn.value) audioManager.play(key);
|
||||||
currentRoundEnded.value = false;
|
currentRoundEnded.value = false;
|
||||||
if (currentRound.value === 1) audioManager.play("第一轮");
|
yourTurn.value = false;
|
||||||
if (currentRound.value === 2) audioManager.play("第二轮");
|
},
|
||||||
if (currentRound.value === 3) audioManager.play("第三轮");
|
currentRoundEnded.value ? 1000 : 0
|
||||||
if (currentRound.value === 4) audioManager.play("第四轮");
|
);
|
||||||
if (currentRound.value === 5) audioManager.play("第五轮");
|
|
||||||
setTimeout(() => {
|
|
||||||
audioManager.play(key);
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
audioManager.play(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -52,14 +56,28 @@ async function onReceiveMessage(messages = []) {
|
|||||||
messages.forEach((msg) => {
|
messages.forEach((msg) => {
|
||||||
if (msg.constructor === MESSAGETYPES.ShootResult) {
|
if (msg.constructor === MESSAGETYPES.ShootResult) {
|
||||||
if (melee.value && msg.userId !== user.value.id) return;
|
if (melee.value && msg.userId !== user.value.id) return;
|
||||||
|
if (msg.userId === user.value.id) currentShot.value++;
|
||||||
if (!halfTime.value && msg.target) {
|
if (!halfTime.value && msg.target) {
|
||||||
currentSound.value = msg.target.ring
|
currentSound.value = msg.target.ring
|
||||||
? `${msg.target.ring}环`
|
? `${msg.target.ring}环`
|
||||||
: "未上靶";
|
: "未上靶";
|
||||||
console.log(currentSound.value);
|
|
||||||
audioManager.play(currentSound.value);
|
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({
|
||||||
|
title: "距离不足,无效",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
audioManager.play("射击无效");
|
||||||
|
}
|
||||||
} else if (msg.constructor === MESSAGETYPES.AllReady) {
|
} else if (msg.constructor === MESSAGETYPES.AllReady) {
|
||||||
|
const { config } = msg.groupUserStatus;
|
||||||
|
if (config && config.mode === 1) {
|
||||||
|
totalShot.value = config.teamSize === 2 ? 3 : 2;
|
||||||
|
}
|
||||||
currentRoundEnded.value = true;
|
currentRoundEnded.value = true;
|
||||||
audioManager.play("比赛开始");
|
audioManager.play("比赛开始");
|
||||||
} else if (msg.constructor === MESSAGETYPES.MeleeAllReady) {
|
} else if (msg.constructor === MESSAGETYPES.MeleeAllReady) {
|
||||||
@@ -67,8 +85,9 @@ async function onReceiveMessage(messages = []) {
|
|||||||
halfTime.value = false;
|
halfTime.value = false;
|
||||||
audioManager.play("比赛开始");
|
audioManager.play("比赛开始");
|
||||||
} else if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
|
} else if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
|
||||||
|
currentShot.value = 0;
|
||||||
if (msg.preRoundResult && msg.preRoundResult.currentRound) {
|
if (msg.preRoundResult && msg.preRoundResult.currentRound) {
|
||||||
currentRound.value = msg.preRoundResult.currentRound + 1;
|
currentRound.value = msg.preRoundResult.currentRound;
|
||||||
currentRoundEnded.value = true;
|
currentRoundEnded.value = true;
|
||||||
}
|
}
|
||||||
} else if (msg.constructor === MESSAGETYPES.HalfTimeOver) {
|
} else if (msg.constructor === MESSAGETYPES.HalfTimeOver) {
|
||||||
@@ -77,14 +96,15 @@ async function onReceiveMessage(messages = []) {
|
|||||||
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
|
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
|
||||||
audioManager.play("比赛结束");
|
audioManager.play("比赛结束");
|
||||||
} else if (msg.constructor === MESSAGETYPES.FinalShoot) {
|
} else if (msg.constructor === MESSAGETYPES.FinalShoot) {
|
||||||
|
totalShot.value = 0;
|
||||||
audioManager.play("决金箭轮");
|
audioManager.play("决金箭轮");
|
||||||
} else if (msg.constructor === MESSAGETYPES.ShootSyncMePracticeID) {
|
tips.value = "即将开始...";
|
||||||
ended.value = true;
|
currentRoundEnded.value = false;
|
||||||
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
|
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
|
||||||
ended.value = true;
|
ended.value = true;
|
||||||
} else if (msg.constructor === MESSAGETYPES.BackToGame) {
|
} else if (msg.constructor === MESSAGETYPES.BackToGame) {
|
||||||
if (msg.battleInfo) {
|
if (msg.battleInfo) {
|
||||||
melee.value = msg.battleInfo.config.battleMode === 2;
|
melee.value = msg.battleInfo.config.mode === 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -99,14 +119,20 @@ const onUpdateTips = (newVal) => {
|
|||||||
tips.value = newVal;
|
tips.value = newVal;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onUpdateTotalShot = (newVal) => {
|
||||||
|
currentShot.value = newVal.currentShot;
|
||||||
|
totalShot.value = newVal.totalShot;
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
uni.$on("update-shot", onUpdateTotalShot);
|
||||||
uni.$on("update-tips", onUpdateTips);
|
uni.$on("update-tips", onUpdateTips);
|
||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
uni.$on("play-sound", playSound);
|
uni.$on("play-sound", playSound);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.$off("update-tips", onUpdateTips);
|
uni.$off("update-shot", onUpdateTotalShot);
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
uni.$off("socket-inbox", onReceiveMessage);
|
||||||
uni.$off("play-sound", playSound);
|
uni.$off("play-sound", playSound);
|
||||||
if (timer.value) clearInterval(timer.value);
|
if (timer.value) clearInterval(timer.value);
|
||||||
@@ -116,7 +142,7 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<text>{{ tips }}</text>
|
<text>{{ tips }}</text>
|
||||||
<!-- <text> ({{ currentRound }}/{{ totalRound }}) </text> -->
|
<text v-if="totalShot > 0"> ({{ currentShot }}/{{ totalShot }}) </text>
|
||||||
<button v-if="!!tips" hover-class="none" @click="updateSound">
|
<button v-if="!!tips" hover-class="none" @click="updateSound">
|
||||||
<image
|
<image
|
||||||
:src="`../static/sound${sound ? '' : '-off'}-yellow.png`"
|
:src="`../static/sound${sound ? '' : '-off'}-yellow.png`"
|
||||||
|
|||||||
84
src/components/InputRow.vue
Normal 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>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, watch } from "vue";
|
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
stopMatch: {
|
stopMatch: {
|
||||||
type: Function,
|
type: Function,
|
||||||
@@ -94,7 +94,7 @@ onMounted(() => {
|
|||||||
textStyles.value = getTextStyle(totalTop.value);
|
textStyles.value = getTextStyle(totalTop.value);
|
||||||
}, 40);
|
}, 40);
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timer.value) clearInterval(timer.value);
|
if (timer.value) clearInterval(timer.value);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ const seats = new Array(props.total).fill(1);
|
|||||||
.players > view > image:nth-child(2) {
|
.players > view > image:nth-child(2) {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
min-height: 40px;
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ const props = defineProps({
|
|||||||
const bowOptions = ref({});
|
const bowOptions = ref({});
|
||||||
const targetOptions = ref({});
|
const targetOptions = ref({});
|
||||||
|
|
||||||
|
const toDetailPage = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/point-book-detail?id=${props.data.id}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const result = uni.getStorageSync("point-book-config");
|
const result = uni.getStorageSync("point-book-config");
|
||||||
(result.bowOption || []).forEach((item) => {
|
(result.bowOption || []).forEach((item) => {
|
||||||
@@ -22,9 +28,10 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container" @click="toDetailPage">
|
||||||
<view>
|
<view>
|
||||||
<view class="labels">
|
<view class="labels">
|
||||||
|
<view></view>
|
||||||
<text>{{
|
<text>{{
|
||||||
bowOptions[data.bowType] ? bowOptions[data.bowType].name : ""
|
bowOptions[data.bowType] ? bowOptions[data.bowType].name : ""
|
||||||
}}</text>
|
}}</text>
|
||||||
@@ -38,10 +45,15 @@ onMounted(() => {
|
|||||||
<view>
|
<view>
|
||||||
<text>{{ data.createAt }}</text>
|
<text>{{ data.createAt }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view>
|
||||||
|
<text>黄心率:{{ Number((data.yellowRate * 100).toFixed(2)) }}%</text>
|
||||||
|
<text>10环数:{{ data.tenRings }}</text>
|
||||||
|
<text>平均:{{ data.averageRing }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<image src="../static/bow-target.png" mode="widthFix" />
|
<image src="../static/bow-target.png" mode="widthFix" />
|
||||||
<view class="aroow-amount">
|
<view class="arrow-amount">
|
||||||
<text>共</text>
|
<text>共</text>
|
||||||
<text>{{ data.arrows * data.groups }}</text>
|
<text>{{ data.arrows * data.groups }}</text>
|
||||||
<text>箭</text>
|
<text>箭</text>
|
||||||
@@ -53,37 +65,54 @@ onMounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
.container {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 15px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 15px;
|
align-items: center;
|
||||||
height: 24vw;
|
border-radius: 25rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border: 2rpx solid #fed848;
|
||||||
}
|
}
|
||||||
.container > view {
|
.container > view {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
.container > view:first-child {
|
.container > view:first-child {
|
||||||
width: calc(100% - 5vw);
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: calc(100% - 50rpx);
|
||||||
|
color: #333333;
|
||||||
}
|
}
|
||||||
.container > view:first-child > view {
|
.container > view:first-child > view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50%;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.container > view:first-child > view:nth-child(3) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #666;
|
||||||
}
|
}
|
||||||
.container > view:first-child > view:last-child {
|
.container > view:first-child > view:nth-child(3) > text {
|
||||||
font-weight: 500;
|
margin-right: 10rpx;
|
||||||
}
|
}
|
||||||
.labels {
|
.labels {
|
||||||
align-items: flex-end !important;
|
align-items: flex-end !important;
|
||||||
}
|
}
|
||||||
|
.labels > view:first-child {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
height: 10rpx;
|
||||||
|
background: #fee947;
|
||||||
|
border-radius: 5rpx;
|
||||||
|
width: 300rpx;
|
||||||
|
}
|
||||||
.labels > text {
|
.labels > text {
|
||||||
font-size: 12px;
|
font-size: 26rpx;
|
||||||
color: #333333;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
border-radius: 10px;
|
position: relative;
|
||||||
padding: 5px 10px;
|
color: #333;
|
||||||
}
|
}
|
||||||
.container > view:last-child {
|
.container > view:last-child {
|
||||||
margin-right: 1vw;
|
margin-right: 1vw;
|
||||||
@@ -91,7 +120,7 @@ onMounted(() => {
|
|||||||
.container > view:last-child > image {
|
.container > view:last-child > image {
|
||||||
width: 24vw;
|
width: 24vw;
|
||||||
}
|
}
|
||||||
.aroow-amount {
|
.arrow-amount {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #0009;
|
background-color: #0009;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -101,10 +130,10 @@ onMounted(() => {
|
|||||||
width: 60px;
|
width: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
top: calc(50% - 11px);
|
top: calc(50% - 13px);
|
||||||
left: calc(50% - 30px);
|
left: calc(50% - 30px);
|
||||||
}
|
}
|
||||||
.aroow-amount > text:nth-child(2) {
|
.arrow-amount > text:nth-child(2) {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0 3px;
|
margin: 0 3px;
|
||||||
|
|||||||
246
src/components/RewardUs.vue
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch, onMounted } from "vue";
|
||||||
|
import { getAppConfig, donateAPI } from "@/apis";
|
||||||
|
import useStore from "@/store";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
const store = useStore();
|
||||||
|
const { config } = storeToRefs(store);
|
||||||
|
const { updateConfig } = store;
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
onClose: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const amounts = ref([]);
|
||||||
|
const selected = ref(null);
|
||||||
|
const checked = ref(false);
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
name: "",
|
||||||
|
account: "",
|
||||||
|
organization: "",
|
||||||
|
suggestion: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const onPay = async (index) => {
|
||||||
|
selected.value = index;
|
||||||
|
const result = await donateAPI(
|
||||||
|
amounts.value[index],
|
||||||
|
formData.name,
|
||||||
|
formData.account,
|
||||||
|
formData.organization,
|
||||||
|
formData.suggestion
|
||||||
|
);
|
||||||
|
const params = result.order.jsApi.params;
|
||||||
|
if (params) {
|
||||||
|
wx.requestPayment({
|
||||||
|
timeStamp: params.timeStamp,
|
||||||
|
nonceStr: params.nonceStr,
|
||||||
|
package: params.package,
|
||||||
|
paySign: params.paySign,
|
||||||
|
signType: "RSA",
|
||||||
|
async success(res) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "感谢您的支持!",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
props.onClose();
|
||||||
|
},
|
||||||
|
fail(res) {
|
||||||
|
console.log("pay error", res);
|
||||||
|
uni.showToast({
|
||||||
|
title: "取消支付",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
props.onClose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => config.value.donateAmount,
|
||||||
|
(value) => {
|
||||||
|
amounts.value = value || [];
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
() => {
|
||||||
|
selected.value = null;
|
||||||
|
formData.name = "";
|
||||||
|
formData.account = "";
|
||||||
|
formData.organization = "";
|
||||||
|
formData.suggestion = "";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!config.value.donateAmount) {
|
||||||
|
const config = await getAppConfig();
|
||||||
|
updateConfig(config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<image src="../static/donate.png" mode="widthFix" />
|
||||||
|
<text>感谢您对我们公益项目的支持!</text>
|
||||||
|
<view class="amounts">
|
||||||
|
<button
|
||||||
|
v-for="(item, index) in amounts"
|
||||||
|
:key="index"
|
||||||
|
hover-class="none"
|
||||||
|
@click="onPay(index)"
|
||||||
|
:style="{
|
||||||
|
background: selected === index ? '#fed848' : 'white',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<text>¥</text>
|
||||||
|
<text>{{ item }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
@click="checked = !checked"
|
||||||
|
:style="{ marginBottom: !checked ? '20rpx' : '0' }"
|
||||||
|
>
|
||||||
|
<image v-if="checked" src="../static/checked.png" mode="widthFix" />
|
||||||
|
<view v-else></view>
|
||||||
|
<text>我想给建议(选填)</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="checked">
|
||||||
|
<view>
|
||||||
|
<text>您的姓名</text>
|
||||||
|
<input v-model="formData.name" />
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<text>手机/微信号</text>
|
||||||
|
<input v-model="formData.account" />
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<text>所在机构</text>
|
||||||
|
<input v-model="formData.organization" />
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<text>建议</text>
|
||||||
|
<textarea v-model="formData.suggestion" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.container > image:first-child {
|
||||||
|
width: 200rpx;
|
||||||
|
position: absolute;
|
||||||
|
top: -114rpx;
|
||||||
|
}
|
||||||
|
.container > text:nth-child(2) {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
margin: 40rpx 0;
|
||||||
|
}
|
||||||
|
.amounts {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
row-gap: 20rpx;
|
||||||
|
column-gap: 20rpx;
|
||||||
|
}
|
||||||
|
.amounts > button {
|
||||||
|
width: 150rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
border: 1rpx solid #fed848;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.amounts > button > text:first-child {
|
||||||
|
font-size: 20rpx;
|
||||||
|
margin-right: 3rpx;
|
||||||
|
}
|
||||||
|
.amounts > button > text:last-child {
|
||||||
|
font-size: 34rpx;
|
||||||
|
}
|
||||||
|
.container > view:nth-child(4) {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
.container > view:nth-child(4) > view {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
margin: 0 10rpx;
|
||||||
|
border: 1rpx solid #e3e3e3;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.container > view:nth-child(4) > image {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
margin: 0 10rpx;
|
||||||
|
}
|
||||||
|
.container > view:nth-child(4) > text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #333333;
|
||||||
|
margin: 20rpx 0;
|
||||||
|
}
|
||||||
|
.container > view:nth-child(5) {
|
||||||
|
margin-bottom: 25rpx;
|
||||||
|
}
|
||||||
|
.container > view:nth-child(5) > view {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
.container > view:nth-child(5) > view > text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #333333;
|
||||||
|
width: 30%;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 20rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
line-height: 60rpx;
|
||||||
|
}
|
||||||
|
.container > view:nth-child(5) > view > input {
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 1rpx solid #fed848;
|
||||||
|
height: 40rpx;
|
||||||
|
line-height: 40rpx;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
width: 55%;
|
||||||
|
}
|
||||||
|
.container > view:nth-child(5) > view > textarea {
|
||||||
|
width: 55%;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 1rpx solid #fed848;
|
||||||
|
height: 100rpx;
|
||||||
|
line-height: 40rpx;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
105
src/components/RingBarChart.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: Array,
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const barColor = (rate) => {
|
||||||
|
if (rate >= 0.4) return "#FDC540";
|
||||||
|
if (rate >= 0.2) return "#FED847";
|
||||||
|
return "#ffe88f";
|
||||||
|
};
|
||||||
|
|
||||||
|
const bars = computed(() => {
|
||||||
|
const newList = new Array(12).fill({ ring: 0, rate: 0 }).map((_, index) => {
|
||||||
|
let ring = index;
|
||||||
|
if (ring === 11) ring = "M";
|
||||||
|
if (ring === 0) ring = "X";
|
||||||
|
return {
|
||||||
|
ring: ring,
|
||||||
|
rate: props.data[index] || 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
[newList[0], newList[11]] = [newList[11], newList[0]];
|
||||||
|
return newList.reverse();
|
||||||
|
});
|
||||||
|
|
||||||
|
const ringText = (ring) => {
|
||||||
|
if (ring === 11) return "X";
|
||||||
|
if (ring === 0) return "M";
|
||||||
|
return ring;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view>
|
||||||
|
<view v-for="(b, index) in bars" :key="index">
|
||||||
|
<text v-if="b && b.rate">
|
||||||
|
{{ total === 0 ? `${Number((b.rate * 100).toFixed(1))}%` : b.rate }}
|
||||||
|
</text>
|
||||||
|
<view
|
||||||
|
:style="{
|
||||||
|
background: barColor(total === 0 ? b.rate : b.rate / total),
|
||||||
|
height: (total === 0 ? b.rate : b.rate / total) * 300 + 'rpx',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<text v-for="(b, index) in bars" :key="index">
|
||||||
|
{{ b && b.ring !== undefined ? b.ring : "" }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 150rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.container > view {
|
||||||
|
padding: 0 10rpx;
|
||||||
|
}
|
||||||
|
.container > view:first-child {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-around;
|
||||||
|
min-height: 50rpx;
|
||||||
|
}
|
||||||
|
.container > view:first-child > view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 18rpx;
|
||||||
|
color: #333;
|
||||||
|
width: 5vw;
|
||||||
|
}
|
||||||
|
.container > view:first-child > view > view {
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
.container > view:last-child {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(12, 1fr);
|
||||||
|
border-top: 1rpx solid #333;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
.container > view:last-child > text {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isFinal: {
|
isFinal: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -26,7 +26,7 @@ const props = defineProps({
|
|||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const count = ref(3);
|
const count = ref(props.isFinal ? 10 : 3);
|
||||||
const tiemr = ref(null);
|
const tiemr = ref(null);
|
||||||
function startCount() {
|
function startCount() {
|
||||||
if (tiemr.value) clearInterval(tiemr.value);
|
if (tiemr.value) clearInterval(tiemr.value);
|
||||||
@@ -37,24 +37,17 @@ function startCount() {
|
|||||||
} else count.value -= 1;
|
} else count.value -= 1;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
watch(
|
|
||||||
() => [props.isFinal, props.roundData],
|
|
||||||
([n_isFinal, n_roundData]) => {
|
|
||||||
count.value = n_isFinal ? 10 : 3;
|
|
||||||
startCount();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
startCount();
|
startCount();
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
if (tiemr.value) clearInterval(tiemr.value);
|
if (tiemr.value) clearInterval(tiemr.value);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="round-end-tip">
|
<view class="round-end-tip">
|
||||||
<text>第{{ round }}轮射击结束</text>
|
<text>第{{ round }}轮射箭结束</text>
|
||||||
<block v-if="!isFinal">
|
<block v-if="!isFinal">
|
||||||
<view class="point-view1" v-if="bluePoint !== 0 || redPoint !== 0">
|
<view class="point-view1" v-if="bluePoint !== 0 || redPoint !== 0">
|
||||||
<text>本轮蓝队</text>
|
<text>本轮蓝队</text>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
rounded: {
|
rounded: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 10,
|
default: 45,
|
||||||
},
|
},
|
||||||
onClick: {
|
onClick: {
|
||||||
type: Function,
|
type: Function,
|
||||||
@@ -58,7 +58,7 @@ const onBtnClick = debounce(async () => {
|
|||||||
hover-class="none"
|
hover-class="none"
|
||||||
:style="{
|
:style="{
|
||||||
width: width,
|
width: width,
|
||||||
borderRadius: rounded + 'px',
|
borderRadius: rounded + 'rpx',
|
||||||
backgroundColor: disabled ? disabledColor : backgroundColor,
|
backgroundColor: disabled ? disabledColor : backgroundColor,
|
||||||
color,
|
color,
|
||||||
}"
|
}"
|
||||||
@@ -77,10 +77,10 @@ const onBtnClick = debounce(async () => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.sbtn {
|
.sbtn {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
height: 44px;
|
height: 88rpx;
|
||||||
line-height: 44px;
|
line-height: 44px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 15px;
|
font-size: 42rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: "260px",
|
||||||
|
},
|
||||||
onClose: {
|
onClose: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
@@ -46,7 +50,7 @@ watch(
|
|||||||
class="modal-content"
|
class="modal-content"
|
||||||
:style="{
|
:style="{
|
||||||
transform: `translateY(${showContent ? '0%' : '100%'})`,
|
transform: `translateY(${showContent ? '0%' : '100%'})`,
|
||||||
height: !noBg ? '260px' : 'auto',
|
height,
|
||||||
}"
|
}"
|
||||||
@click.stop=""
|
@click.stop=""
|
||||||
>
|
>
|
||||||
@@ -55,7 +59,7 @@ watch(
|
|||||||
src="https://static.shelingxingqiu.com/attachment/2025-08-05/dbuaf19pf7qd8ps0uh.png"
|
src="https://static.shelingxingqiu.com/attachment/2025-08-05/dbuaf19pf7qd8ps0uh.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<view class="close-btn" @click="onClose">
|
<view class="close-btn" @click="onClose" v-if="!noBg">
|
||||||
<image src="../static/close-yellow.png" mode="widthFix" />
|
<image src="../static/close-yellow.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rowCount: {
|
rowCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -42,7 +42,7 @@ onMounted(() => {
|
|||||||
bgIndex.value = bgIndex.value === 0 ? 1 : 0;
|
bgIndex.value = bgIndex.value === 0 ? 1 : 0;
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timer.value) {
|
if (timer.value) {
|
||||||
clearInterval(timer.value);
|
clearInterval(timer.value);
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,10 @@ onUnmounted(() => {
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<image src="../static/score-bg.png" mode="widthFix" />
|
<image src="../static/score-bg.png" mode="widthFix" />
|
||||||
<text>{{ scores[index] }}</text>
|
<text
|
||||||
|
:style="{ fontWeight: scores[index] !== undefined ? 'bold' : 'normal' }"
|
||||||
|
>{{ scores[index] !== undefined ? scores[index] : "-" }}</text
|
||||||
|
>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -111,7 +114,6 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
.score-item > text {
|
.score-item > text {
|
||||||
position: relative;
|
position: relative;
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
.complete-light {
|
.complete-light {
|
||||||
|
|||||||
@@ -53,8 +53,7 @@ const props = defineProps({
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 75vw;
|
width: 75vw;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
border-radius: 20px;
|
border-radius: 30rpx;
|
||||||
overflow: hidden;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.container > view:first-child > image {
|
.container > view:first-child > image {
|
||||||
@@ -62,5 +61,7 @@ const props = defineProps({
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
border-top-left-radius: 30rpx;
|
||||||
|
border-top-right-radius: 30rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from "vue";
|
import { ref } from "vue";
|
||||||
|
import { onShow } from "@dcloudio/uni-app";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -14,9 +15,10 @@ const props = defineProps({
|
|||||||
default: 10,
|
default: 10,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const refreshing = ref(false);
|
const refreshing = ref(true);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const noMore = ref(false);
|
const noMore = ref(false);
|
||||||
|
const count = ref(0);
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const refresherrefresh = async () => {
|
const refresherrefresh = async () => {
|
||||||
if (refreshing.value) return;
|
if (refreshing.value) return;
|
||||||
@@ -24,6 +26,7 @@ const refresherrefresh = async () => {
|
|||||||
refreshing.value = true;
|
refreshing.value = true;
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
const length = await props.onLoading(page.value);
|
const length = await props.onLoading(page.value);
|
||||||
|
count.value = length;
|
||||||
if (length < props.pageSize) noMore.value = true;
|
if (length < props.pageSize) noMore.value = true;
|
||||||
} finally {
|
} finally {
|
||||||
refreshing.value = false;
|
refreshing.value = false;
|
||||||
@@ -35,26 +38,28 @@ const scrolltolower = async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
page.value += 1;
|
page.value += 1;
|
||||||
const length = await props.onLoading(page.value);
|
const length = await props.onLoading(page.value);
|
||||||
|
count.value += length;
|
||||||
if (length < props.pageSize) noMore.value = true;
|
if (length < props.pageSize) noMore.value = true;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
watch(
|
onShow(async () => {
|
||||||
() => props.show,
|
try {
|
||||||
async (newVal) => {
|
const length = await props.onLoading(page.value);
|
||||||
if (newVal) await props.onLoading(1);
|
count.value = length;
|
||||||
},
|
if (length < props.pageSize) noMore.value = true;
|
||||||
{
|
} finally {
|
||||||
immediate: true,
|
refreshing.value = false;
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<scroll-view
|
<scroll-view
|
||||||
class="scroll-list"
|
class="scroll-list"
|
||||||
scroll-y
|
scroll-y
|
||||||
|
enable-flex="true"
|
||||||
:show-scrollbar="false"
|
:show-scrollbar="false"
|
||||||
enhanced="true"
|
enhanced="true"
|
||||||
:bounces="false"
|
:bounces="false"
|
||||||
@@ -68,8 +73,10 @@ watch(
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<text class="tips" v-if="loading">加载中...</text>
|
<view class="tips">
|
||||||
<text class="tips" v-if="noMore">我是有底线的</text>
|
<text v-if="loading">Loading...</text>
|
||||||
|
<text v-if="noMore">{{ count === 0 ? "No data" : "That‘s all" }}</text>
|
||||||
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -77,9 +84,10 @@ watch(
|
|||||||
.scroll-list {
|
.scroll-list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.tips {
|
.tips > text {
|
||||||
color: #fff9;
|
color: #d0d0d0;
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||||
import audioManager from "@/audioManager";
|
import audioManager from "@/audioManager";
|
||||||
import { MESSAGETYPES } from "@/constants";
|
import { MESSAGETYPES } from "@/constants";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
@@ -51,8 +51,8 @@ watch(
|
|||||||
() => props.tips,
|
() => props.tips,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
let key = "";
|
let key = "";
|
||||||
if (newVal.includes("红队")) key = "请红方射击";
|
if (newVal.includes("红队")) key = "请红方射箭";
|
||||||
if (newVal.includes("蓝队")) key = "请蓝方射击";
|
if (newVal.includes("蓝队")) key = "请蓝方射箭";
|
||||||
if (key && sound.value) {
|
if (key && sound.value) {
|
||||||
if (currentRoundEnded.value) {
|
if (currentRoundEnded.value) {
|
||||||
currentRound.value += 1;
|
currentRound.value += 1;
|
||||||
@@ -135,6 +135,14 @@ async function onReceiveMessage(messages = []) {
|
|||||||
: "未上靶";
|
: "未上靶";
|
||||||
audioManager.play(currentSound.value);
|
audioManager.play(currentSound.value);
|
||||||
}
|
}
|
||||||
|
} else if (msg.constructor === MESSAGETYPES.InvalidShot) {
|
||||||
|
if (msg.userId === user.value.id) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "距离不足,无效",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
audioManager.play("射击无效");
|
||||||
|
}
|
||||||
} else if (msg.constructor === MESSAGETYPES.AllReady) {
|
} else if (msg.constructor === MESSAGETYPES.AllReady) {
|
||||||
audioManager.play("比赛开始");
|
audioManager.play("比赛开始");
|
||||||
} else if (msg.constructor === MESSAGETYPES.MeleeAllReady) {
|
} else if (msg.constructor === MESSAGETYPES.MeleeAllReady) {
|
||||||
@@ -149,8 +157,6 @@ async function onReceiveMessage(messages = []) {
|
|||||||
audioManager.play("比赛结束");
|
audioManager.play("比赛结束");
|
||||||
} else if (msg.constructor === MESSAGETYPES.FinalShoot) {
|
} else if (msg.constructor === MESSAGETYPES.FinalShoot) {
|
||||||
audioManager.play("决金箭轮");
|
audioManager.play("决金箭轮");
|
||||||
} else if (msg.constructor === MESSAGETYPES.ShootSyncMePracticeID) {
|
|
||||||
ended.value = true;
|
|
||||||
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
|
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
|
||||||
ended.value = true;
|
ended.value = true;
|
||||||
}
|
}
|
||||||
@@ -168,7 +174,7 @@ onMounted(() => {
|
|||||||
uni.$on("play-sound", playSound);
|
uni.$on("play-sound", playSound);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.$off("update-ramain", updateRemain);
|
uni.$off("update-ramain", updateRemain);
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
uni.$off("socket-inbox", onReceiveMessage);
|
||||||
uni.$off("play-sound", playSound);
|
uni.$off("play-sound", playSound);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { RoundGoldImages } from "@/constants";
|
import { RoundGoldImages } from "@/constants";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tips: {
|
tips: {
|
||||||
@@ -52,7 +52,7 @@ onMounted(() => {
|
|||||||
uni.$on("update-ramain", updateRemain);
|
uni.$on("update-ramain", updateRemain);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.$off("update-ramain", updateRemain);
|
uni.$off("update-ramain", updateRemain);
|
||||||
if (timer.value) clearInterval(timer.value);
|
if (timer.value) clearInterval(timer.value);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import useStore from "@/store";
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { updateUser, updateDevice } = store;
|
const { updateUser, updateDevice } = store;
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
noBg: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
onClose: {
|
onClose: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
@@ -79,15 +83,36 @@ const handleLogin = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openServiceLink = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url:
|
||||||
|
"/pages/webview?url=" +
|
||||||
|
encodeURIComponent(
|
||||||
|
"https://static.shelingxingqiu.com/shootServiceAgreement.html"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPrivacyLink = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url:
|
||||||
|
"/pages/webview?url=" +
|
||||||
|
encodeURIComponent(
|
||||||
|
"https://static.shelingxingqiu.com/shootPrivacyPolicy.html"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container" :style="{ background: noBg ? '#fff' : 'none' }">
|
||||||
<view class="avatar">
|
<view class="avatar" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff' }">
|
||||||
<text>头像:</text>
|
<text :style="{ color: noBg ? '#666' : '#fff' }">头像:</text>
|
||||||
<button
|
<button
|
||||||
open-type="chooseAvatar"
|
open-type="chooseAvatar"
|
||||||
@chooseavatar="onChooseAvatar"
|
@chooseavatar="onChooseAvatar"
|
||||||
@@ -95,17 +120,19 @@ onShow(() => {
|
|||||||
hover-class="none"
|
hover-class="none"
|
||||||
>
|
>
|
||||||
<Avatar v-if="avatarUrl" :src="avatarUrl" :size="30" />
|
<Avatar v-if="avatarUrl" :src="avatarUrl" :size="30" />
|
||||||
<text v-else>点击获取</text>
|
<text v-else :style="{ color: noBg ? '#666' : '#fff9' }">点击获取</text>
|
||||||
|
<image src="../static/enter.png" mode="widthFix" />
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
<view class="nickname">
|
<view class="nickname" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff' }">
|
||||||
<text>昵称:</text>
|
<text :style="{ color: noBg ? '#666' : '#fff' }">昵称:</text>
|
||||||
<input
|
<input
|
||||||
type="nickname"
|
type="nickname"
|
||||||
placeholder="请输入昵称"
|
placeholder="请输入昵称"
|
||||||
placeholder-style="color: #fff9"
|
:placeholder-style="{ color: noBg ? '#666' : '#fff9' }"
|
||||||
@change="onNicknameChange"
|
@change="onNicknameChange"
|
||||||
@blur="onNicknameBlur"
|
@blur="onNicknameBlur"
|
||||||
|
:style="{ color: noBg ? '#333' : '#fff' }"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<SButton :rounded="20" width="80vw" :onClick="handleLogin">
|
<SButton :rounded="20" width="80vw" :onClick="handleLogin">
|
||||||
@@ -126,14 +153,23 @@ onShow(() => {
|
|||||||
</block>
|
</block>
|
||||||
</SButton>
|
</SButton>
|
||||||
<view class="protocol" @click="handleAgree">
|
<view class="protocol" @click="handleAgree">
|
||||||
<view v-if="!agree" />
|
<view v-if="!agree" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff' }" />
|
||||||
<image v-if="agree" src="../static/checked.png" mode="widthFix" />
|
<image v-if="agree" src="../static/checked.png" mode="widthFix" />
|
||||||
<text
|
<view>
|
||||||
>已同意并阅读<text :style="{ color: '#fff' }">《用户协议》</text>及<text
|
<text>已同意并阅读</text>
|
||||||
:style="{ color: '#fff' }"
|
<view
|
||||||
>《隐私协议》</text
|
@click.stop="openServiceLink"
|
||||||
>内容</text
|
:style="{ color: noBg ? '#333' : '#fff' }"
|
||||||
|
>《用户协议》</view
|
||||||
>
|
>
|
||||||
|
<text>及</text>
|
||||||
|
<view
|
||||||
|
@click.stop="openPrivacyLink"
|
||||||
|
:style="{ color: noBg ? '#333' : '#fff' }"
|
||||||
|
>《隐私协议》</view
|
||||||
|
>
|
||||||
|
<text>内容</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -146,6 +182,8 @@ onShow(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-top-left-radius: 24rpx;
|
||||||
|
border-top-right-radius: 24rpx;
|
||||||
}
|
}
|
||||||
.avatar,
|
.avatar,
|
||||||
.nickname {
|
.nickname {
|
||||||
@@ -153,7 +191,7 @@ onShow(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
border-bottom: 1px solid #fff3;
|
border-bottom: 1rpx solid #fff3;
|
||||||
}
|
}
|
||||||
.avatar {
|
.avatar {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -161,7 +199,6 @@ onShow(() => {
|
|||||||
.avatar > text,
|
.avatar > text,
|
||||||
.nickname > text {
|
.nickname > text {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
color: #fff9;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 55px;
|
line-height: 55px;
|
||||||
}
|
}
|
||||||
@@ -172,7 +209,6 @@ onShow(() => {
|
|||||||
.nickname > input {
|
.nickname > input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #fff;
|
|
||||||
line-height: 55px;
|
line-height: 55px;
|
||||||
}
|
}
|
||||||
.wechat-icon {
|
.wechat-icon {
|
||||||
@@ -198,12 +234,22 @@ onShow(() => {
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
border: 1px solid #fff;
|
border: 1rpx solid #fff;
|
||||||
|
}
|
||||||
|
.protocol > view:last-child {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.login-btn {
|
.login-btn {
|
||||||
line-height: 55px;
|
line-height: 55px;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.login-btn > image {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
}
|
}
|
||||||
.loading {
|
.loading {
|
||||||
width: 25px;
|
width: 25px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
start: {
|
start: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -32,7 +32,7 @@ onMounted(() => {
|
|||||||
const deviceInfo = uni.getDeviceInfo();
|
const deviceInfo = uni.getDeviceInfo();
|
||||||
isIos.value = deviceInfo.osName === "ios";
|
isIos.value = deviceInfo.osName === "ios";
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timer.value) clearInterval(timer.value);
|
if (timer.value) clearInterval(timer.value);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
|
<image
|
||||||
|
:src="isRed ? '../static/flag-red.png' : '../static/flag-blue.png'"
|
||||||
|
class="flag"
|
||||||
|
:style="{ [isRed ? 'left' : 'right']: '10rpx' }"
|
||||||
|
/>
|
||||||
<view
|
<view
|
||||||
v-for="(item, index) in team"
|
v-for="(item, index) in team"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -67,7 +72,7 @@ watch(
|
|||||||
left:
|
left:
|
||||||
(isRed
|
(isRed
|
||||||
? ((players[item.id] || {}).sort || 0) * 20
|
? ((players[item.id] || {}).sort || 0) * 20
|
||||||
: 35 - ((players[item.id] || {}).sort || 0) * 15) + 'px',
|
: 40 - ((players[item.id] || {}).sort || 0) * 20) + 'px',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<image :src="item.avatar || '../static/user-icon.png'" mode="widthFix" />
|
<image :src="item.avatar || '../static/user-icon.png'" mode="widthFix" />
|
||||||
@@ -96,6 +101,7 @@ watch(
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 20vw;
|
width: 20vw;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
|
margin: 0 20rpx;
|
||||||
}
|
}
|
||||||
.container > text {
|
.container > text {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -113,6 +119,7 @@ watch(
|
|||||||
}
|
}
|
||||||
.player > image {
|
.player > image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
.player > text {
|
.player > text {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -123,4 +130,10 @@ watch(
|
|||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
.flag {
|
||||||
|
position: absolute;
|
||||||
|
width: 45rpx;
|
||||||
|
height: 45rpx;
|
||||||
|
top: -30rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import Guide from "@/components/Guide.vue";
|
import Guide from "@/components/Guide.vue";
|
||||||
import BowPower from "@/components/BowPower.vue";
|
import BowPower from "@/components/BowPower.vue";
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import Avatar from "@/components/Avatar.vue";
|
||||||
|
import audioManager from "@/audioManager";
|
||||||
import { simulShootAPI } from "@/apis";
|
import { simulShootAPI } from "@/apis";
|
||||||
import { checkConnection } from "@/util";
|
import { checkConnection } from "@/util";
|
||||||
import { MESSAGETYPES } from "@/constants";
|
import { MESSAGETYPES } from "@/constants";
|
||||||
@@ -15,20 +16,46 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
isBattle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const arrow = ref({});
|
const arrow = ref({});
|
||||||
const power = ref(0);
|
const power = ref(0);
|
||||||
const distance = ref(0);
|
const distance = ref(0);
|
||||||
const debugInfo = ref("");
|
const debugInfo = ref("");
|
||||||
const showsimul = ref(false);
|
const showsimul = ref(false);
|
||||||
|
const count = ref(15);
|
||||||
|
const timer = ref(null);
|
||||||
|
|
||||||
|
const updateTimer = (value) => {
|
||||||
|
count.value = Math.round(value);
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
audioManager.play("请射箭测试距离");
|
||||||
|
timer.value = setInterval(() => {
|
||||||
|
if (count.value > 0) count.value -= 1;
|
||||||
|
else clearInterval(timer.value);
|
||||||
|
}, 1000);
|
||||||
|
uni.$on("update-timer", updateTimer);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timer.value) clearInterval(timer.value);
|
||||||
|
uni.$off("update-timer", updateTimer);
|
||||||
|
});
|
||||||
|
|
||||||
async function onReceiveMessage(messages = []) {
|
async function onReceiveMessage(messages = []) {
|
||||||
messages.forEach((msg) => {
|
messages.forEach((msg) => {
|
||||||
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
|
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
|
||||||
arrow.value = msg.target;
|
arrow.value = msg.target;
|
||||||
power.value = msg.target.battery;
|
power.value = msg.target.battery;
|
||||||
distance.value = msg.target.dst / 100;
|
distance.value = Number((msg.target.dst / 100).toFixed(2));
|
||||||
debugInfo.value = msg.target;
|
debugInfo.value = msg.target;
|
||||||
|
audioManager.play("距离合格");
|
||||||
|
} else if (msg.constructor === MESSAGETYPES.InvalidShot) {
|
||||||
|
distance.value = Number((msg.target.dst / 100).toFixed(2));
|
||||||
|
audioManager.play("距离不足");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -45,14 +72,14 @@ onMounted(() => {
|
|||||||
if (envVersion !== "release") showsimul.value = true;
|
if (envVersion !== "release") showsimul.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
uni.$off("socket-inbox", onReceiveMessage);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<Guide v-if="guide">
|
<Guide v-show="guide">
|
||||||
<view
|
<view
|
||||||
:style="{
|
:style="{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -62,20 +89,25 @@ onUnmounted(() => {
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<view :style="{ display: 'flex', flexDirection: 'column' }">
|
<view :style="{ display: 'flex', flexDirection: 'column' }">
|
||||||
<text :style="{ color: '#fed847' }">请预先射几箭测试</text>
|
<text :style="{ color: '#fed847' }">请确保站距达到5米</text>
|
||||||
<text>请确保射击距离有5米</text>
|
<text>低于5米的射箭无效</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</Guide>
|
</Guide>
|
||||||
<view class="user-row">
|
<view class="test-area">
|
||||||
<Avatar :src="user.avatar" :size="35" />
|
|
||||||
<BowPower :power="power" />
|
|
||||||
</view>
|
|
||||||
<image
|
<image
|
||||||
class="text-bg"
|
class="text-bg"
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-07-05/db3skuq1n9rj4fmld4.png"
|
src="https://static.shelingxingqiu.com/attachment/2025-07-05/db3skuq1n9rj4fmld4.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
class="simul"
|
||||||
|
@click="simulShoot"
|
||||||
|
hover-class="none"
|
||||||
|
v-if="showsimul"
|
||||||
|
>
|
||||||
|
模拟射箭
|
||||||
|
</button>
|
||||||
<view class="warnning-text">
|
<view class="warnning-text">
|
||||||
<block v-if="distance > 0">
|
<block v-if="distance > 0">
|
||||||
<text>当前距离{{ distance }}米</text>
|
<text>当前距离{{ distance }}米</text>
|
||||||
@@ -83,18 +115,20 @@ onUnmounted(() => {
|
|||||||
<text v-else>请调整站位</text>
|
<text v-else>请调整站位</text>
|
||||||
</block>
|
</block>
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<text>大人,请射箭</text>
|
<text>请射箭,测试站距</text>
|
||||||
</block>
|
</block>
|
||||||
</view>
|
</view>
|
||||||
<!-- <view class="debug-text">{{ debugInfo }}</view> -->
|
<view class="user-row">
|
||||||
|
<Avatar :src="user.avatar" :size="35" />
|
||||||
|
<BowPower :power="power" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="isBattle" class="ready-timer">
|
||||||
|
<image src="../static/test-tip.png" mode="widthFix" />
|
||||||
<view>
|
<view>
|
||||||
<view
|
<text>具体正式比赛还有</text>
|
||||||
class="simul"
|
<text>{{ count }}</text>
|
||||||
@click="simulShoot"
|
<text>秒</text>
|
||||||
:style="{ color: '#fff' }"
|
|
||||||
v-if="showsimul"
|
|
||||||
>
|
|
||||||
模拟射箭
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -103,41 +137,69 @@ onUnmounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
.container {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
.ready-timer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
transform: translateY(-10vw);
|
||||||
|
}
|
||||||
|
.ready-timer > image:first-child {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
.ready-timer > view {
|
||||||
|
width: 80%;
|
||||||
|
height: 45px;
|
||||||
|
background-color: #545454;
|
||||||
|
border-radius: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transform: translateY(-8vw);
|
||||||
|
color: #bebebe;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.ready-timer > view > text:nth-child(2) {
|
||||||
|
color: #fed847;
|
||||||
|
font-size: 20px;
|
||||||
|
width: 22px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.test-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
height: 112vw;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.test-area > view:last-child {
|
||||||
|
padding: 15px;
|
||||||
|
width: calc(100% - 30px);
|
||||||
}
|
}
|
||||||
.text-bg {
|
.text-bg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transform: translateY(-50px);
|
position: absolute;
|
||||||
|
top: -14.4%;
|
||||||
|
left: 0;
|
||||||
}
|
}
|
||||||
.warnning-text {
|
.warnning-text {
|
||||||
position: absolute;
|
|
||||||
color: #fed847;
|
color: #fed847;
|
||||||
font-size: 30px;
|
font-size: 27px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
width: 54vw;
|
height: 40%;
|
||||||
left: calc(50% - 27vw);
|
|
||||||
top: 34%;
|
|
||||||
}
|
}
|
||||||
.warnning-text > text {
|
.warnning-text > text {
|
||||||
width: 60vw;
|
width: 70vw;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.container > view:last-child {
|
.simul {
|
||||||
display: flex;
|
position: absolute;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 15px;
|
|
||||||
margin-top: -40vw;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.debug-text {
|
|
||||||
position: fixed;
|
|
||||||
left: calc(50% - 45vw);
|
|
||||||
top: 66%;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 14px;
|
right: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
countdown: {
|
countdown: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -26,7 +26,7 @@ onMounted(() => {
|
|||||||
}, 300);
|
}, 300);
|
||||||
uni.$on("update-timer", updateTimer);
|
uni.$on("update-timer", updateTimer);
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timer.value) clearInterval(timer.value);
|
if (timer.value) clearInterval(timer.value);
|
||||||
uni.$off("update-timer", updateTimer);
|
uni.$off("update-timer", updateTimer);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -44,7 +44,7 @@ onMounted(async () => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timer.value) clearTimeout(timer.value);
|
if (timer.value) clearTimeout(timer.value);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const MESSAGETYPES = {
|
|||||||
RankUpdate: 1121669910,
|
RankUpdate: 1121669910,
|
||||||
LvlUpdate: 3958625354,
|
LvlUpdate: 3958625354,
|
||||||
TeamUpdate: 4168086616,
|
TeamUpdate: 4168086616,
|
||||||
|
InvalidShot: 4168086617,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const topThreeColors = ["#FFD947", "#D2D2D2", "#FFA515"];
|
export const topThreeColors = ["#FFD947", "#D2D2D2", "#FFA515"];
|
||||||
|
|||||||
105
src/heatmap.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* 在 uni-app 小程序里画弓箭热力图
|
||||||
|
* @param {String} canvasId 画布 id
|
||||||
|
* @param {Number} width 画布宽(px)
|
||||||
|
* @param {Number} height 画布高(px)
|
||||||
|
* @param {Array} arrowData [{x, y, count}, ...]
|
||||||
|
*/
|
||||||
|
export function generateHeatmapImage(canvasId, width, height, arrowData) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 1. 创建绘图上下文
|
||||||
|
const ctx = uni.createCanvasContext(canvasId);
|
||||||
|
|
||||||
|
// 3. 计算最大 count,用于归一化
|
||||||
|
const maxCount = Math.max(...arrowData.map((p) => p.count), 1);
|
||||||
|
// 4. 热点半径:可按实际靶子大小调,这里取画布短边的 6%
|
||||||
|
const radius = Math.min(width, height) * 0.12;
|
||||||
|
|
||||||
|
// 5. 按count从小到大排序,count越大越后面
|
||||||
|
arrowData.sort((a, b) => a.count - b.count);
|
||||||
|
|
||||||
|
// 6. 画每个点
|
||||||
|
arrowData.forEach((item) => {
|
||||||
|
const intensity = item.count / maxCount; // 0-1
|
||||||
|
// console.log(item.count, maxCount, intensity);
|
||||||
|
const r = radius * (1.2 - intensity * 0.8);
|
||||||
|
// 创建径向渐变
|
||||||
|
const grd = ctx.createCircularGradient(
|
||||||
|
item.x * width,
|
||||||
|
item.y * height,
|
||||||
|
r
|
||||||
|
);
|
||||||
|
grd.addColorStop(0, heatColor(intensity, 1));
|
||||||
|
grd.addColorStop(0.5, heatColor(intensity, 0.6));
|
||||||
|
grd.addColorStop(1, "rgba(0,0,0,0)");
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = grd;
|
||||||
|
ctx.globalCompositeOperation = "screen"; // 叠加变亮
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(item.x * width, item.y * height, r, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. 可选:整体蒙版,让非热点区域暗下去
|
||||||
|
// ctx.save()
|
||||||
|
// ctx.fillStyle = 'rgba(0,0,0,0.35)'
|
||||||
|
// ctx.fillRect(0, 0, width, height)
|
||||||
|
// ctx.restore()
|
||||||
|
|
||||||
|
// 7. 把指令一次性推送到 canvas
|
||||||
|
ctx.draw(false, () => {
|
||||||
|
// Canvas绘制完成后生成图片
|
||||||
|
uni.canvasToTempFilePath({
|
||||||
|
canvasId: "heatMapCanvas",
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
destWidth: width * 2, // 提高图片质量
|
||||||
|
destHeight: height * 2,
|
||||||
|
success: (res) => {
|
||||||
|
console.log("热力图图片生成成功:", res.tempFilePath);
|
||||||
|
resolve(res.tempFilePath);
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error("热力图图片生成失败:", error);
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把强度 0-1 映射成红-黄-绿渐变,返回 rgba 字符串
|
||||||
|
* @param {Number} v 0-1
|
||||||
|
* @param {Number} a 透明度 0-1
|
||||||
|
*/
|
||||||
|
function heatColor(v, a) {
|
||||||
|
// v 从 0→1,重新映射:极低值绿色,低值黄色,中到高值红色
|
||||||
|
let red, green;
|
||||||
|
|
||||||
|
if (v < 0.2) {
|
||||||
|
// 极低值:纯绿色
|
||||||
|
red = 0;
|
||||||
|
green = 200; // 柔和的绿
|
||||||
|
} else if (v < 0.4) {
|
||||||
|
// 低值:绿色到黄色
|
||||||
|
const t = (v - 0.2) / 0.2;
|
||||||
|
red = Math.round(255 * t);
|
||||||
|
green = 255;
|
||||||
|
} else if (v < 0.6) {
|
||||||
|
// 中低值:黄色到橙色
|
||||||
|
const t = (v - 0.4) / 0.2;
|
||||||
|
red = 255;
|
||||||
|
green = Math.round(255 * (1 - t * 0.5));
|
||||||
|
} else {
|
||||||
|
// 中到高值:橙色到红色
|
||||||
|
const t = (v - 0.6) / 0.4;
|
||||||
|
red = 255;
|
||||||
|
green = Math.round(128 * (1 - t));
|
||||||
|
}
|
||||||
|
|
||||||
|
const blue = 0;
|
||||||
|
return `rgba(${red}, ${green}, ${blue}, ${a})`;
|
||||||
|
}
|
||||||
277
src/kde-heatmap.js
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
/**
|
||||||
|
* 基于小程序Canvas API的核密度估计热力图
|
||||||
|
* 实现类似test.html中的效果,但适配uni-app小程序环境
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Epanechnikov核函数
|
||||||
|
* @param {Number} bandwidth 带宽参数
|
||||||
|
* @returns {Function} 核函数
|
||||||
|
*/
|
||||||
|
function kernelEpanechnikov(bandwidth) {
|
||||||
|
return function (v) {
|
||||||
|
const r = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
|
||||||
|
return r <= bandwidth
|
||||||
|
? (3 / (Math.PI * bandwidth * bandwidth)) *
|
||||||
|
(1 - (r * r) / (bandwidth * bandwidth))
|
||||||
|
: 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核密度估计器
|
||||||
|
* @param {Function} kernel 核函数
|
||||||
|
* @param {Array} range 范围[xmin, xmax]
|
||||||
|
* @param {Number} samples 采样点数
|
||||||
|
* @returns {Function} 密度估计函数
|
||||||
|
*/
|
||||||
|
function kernelDensityEstimator(kernel, range, samples) {
|
||||||
|
return function (data) {
|
||||||
|
const gridSize = (range[1] - range[0]) / samples;
|
||||||
|
const densityData = [];
|
||||||
|
|
||||||
|
for (let x = range[0]; x <= range[1]; x += gridSize) {
|
||||||
|
for (let y = range[0]; y <= range[1]; y += gridSize) {
|
||||||
|
let sum = 0;
|
||||||
|
for (const point of data) {
|
||||||
|
sum += kernel([x - point[0], y - point[1]]);
|
||||||
|
}
|
||||||
|
densityData.push([x, y, sum / data.length]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 归一化
|
||||||
|
const maxDensity = Math.max(...densityData.map((d) => d[2]));
|
||||||
|
densityData.forEach((d) => {
|
||||||
|
if (maxDensity > 0) d[2] /= maxDensity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return densityData;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机射箭数据点
|
||||||
|
* @param {Number} centerCount 中心点数量
|
||||||
|
* @param {Number} pointsPerCenter 每个中心点的箭数
|
||||||
|
* @returns {Array} 箭矢坐标数组
|
||||||
|
*/
|
||||||
|
export function generateArcheryPoints(centerCount = 2, pointsPerCenter = 100) {
|
||||||
|
const points = [];
|
||||||
|
const range = 8; // 坐标范围 -4 到 4
|
||||||
|
const spread = 3; // 分散度
|
||||||
|
|
||||||
|
for (let i = 0; i < centerCount; i++) {
|
||||||
|
const centerX = Math.random() * range - range / 2;
|
||||||
|
const centerY = Math.random() * range - range / 2;
|
||||||
|
|
||||||
|
for (let j = 0; j < pointsPerCenter; j++) {
|
||||||
|
points.push([
|
||||||
|
centerX + (Math.random() - 0.5) * spread,
|
||||||
|
centerY + (Math.random() - 0.5) * spread,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 颜色映射函数 - 将密度值映射到颜色
|
||||||
|
* @param {Number} density 密度值 0-1
|
||||||
|
* @returns {String} RGBA颜色字符串
|
||||||
|
*/
|
||||||
|
function getHeatColor(density) {
|
||||||
|
// 绿色系热力图:从浅绿到深绿
|
||||||
|
if (density < 0.1) return "rgba(0, 255, 0, 0)";
|
||||||
|
|
||||||
|
const alpha = Math.min(density * 1.2, 1); // 增强透明度
|
||||||
|
const intensity = density;
|
||||||
|
|
||||||
|
if (intensity < 0.5) {
|
||||||
|
// 低密度:浅绿色
|
||||||
|
const green = Math.round(200 + 55 * intensity);
|
||||||
|
const blue = Math.round(50 + 100 * intensity);
|
||||||
|
return `rgba(${Math.round(50 * intensity)}, ${green}, ${blue}, ${
|
||||||
|
alpha * 0.7
|
||||||
|
})`;
|
||||||
|
} else {
|
||||||
|
// 高密度:深绿色
|
||||||
|
const red = Math.round(50 * (intensity - 0.5) * 2);
|
||||||
|
const green = Math.round(180 + 75 * (1 - intensity));
|
||||||
|
const blue = Math.round(30 * (1 - intensity));
|
||||||
|
return `rgba(${red}, ${green}, ${blue}, ${alpha * 0.7})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于小程序Canvas API绘制核密度估计热力图
|
||||||
|
* @param {String} canvasId 画布ID
|
||||||
|
* @param {Number} width 画布宽度
|
||||||
|
* @param {Number} height 画布高度
|
||||||
|
* @param {Array} points 箭矢坐标数组 [[x, y], ...]
|
||||||
|
* @param {Object} options 可选参数
|
||||||
|
* @returns {Promise} 绘制完成的Promise
|
||||||
|
*/
|
||||||
|
export function drawKDEHeatmap(canvasId, width, height, points, options = {}) {
|
||||||
|
const {
|
||||||
|
bandwidth = 0.8,
|
||||||
|
gridSize = 100,
|
||||||
|
range = [-4, 4],
|
||||||
|
showPoints = true,
|
||||||
|
pointColor = "rgba(255, 255, 255, 0.9)",
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// 微信小程序使用 Canvas 2D
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
wx.createSelectorQuery()
|
||||||
|
.select(`#${canvasId}`)
|
||||||
|
.fields({ node: true, size: true })
|
||||||
|
.exec((res) => {
|
||||||
|
try {
|
||||||
|
const { node: canvas, width: w, height: h } = res[0] || {};
|
||||||
|
if (!canvas) return resolve();
|
||||||
|
|
||||||
|
// 设置画布尺寸
|
||||||
|
const cw = width || w || 300;
|
||||||
|
const ch = height || h || 300;
|
||||||
|
canvas.width = cw;
|
||||||
|
canvas.height = ch;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
ctx.clearRect(0, 0, cw, ch);
|
||||||
|
|
||||||
|
if (!points || points.length === 0) return resolve();
|
||||||
|
|
||||||
|
// 计算核密度估计
|
||||||
|
const kernel = kernelEpanechnikov(bandwidth);
|
||||||
|
const kde = kernelDensityEstimator(kernel, range, gridSize);
|
||||||
|
const densityData = kde(points);
|
||||||
|
|
||||||
|
// 计算网格大小
|
||||||
|
const cellWidth = cw / gridSize;
|
||||||
|
const cellHeight = ch / gridSize;
|
||||||
|
const xRange = range[1] - range[0];
|
||||||
|
const yRange = range[1] - range[0];
|
||||||
|
|
||||||
|
// 绘制热力图网格
|
||||||
|
densityData.forEach(([x, y, density]) => {
|
||||||
|
const normalizedX = (x - range[0]) / xRange;
|
||||||
|
const normalizedY = (y - range[0]) / yRange;
|
||||||
|
const canvasX = normalizedX * cw;
|
||||||
|
const canvasY = normalizedY * ch;
|
||||||
|
|
||||||
|
const color = getHeatColor(density);
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(
|
||||||
|
canvasX,
|
||||||
|
canvasY,
|
||||||
|
Math.min(cellWidth, cellHeight) * 0.6,
|
||||||
|
0,
|
||||||
|
2 * Math.PI
|
||||||
|
);
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 绘制原始数据点
|
||||||
|
if (showPoints) {
|
||||||
|
ctx.fillStyle = pointColor;
|
||||||
|
points.forEach(([x, y]) => {
|
||||||
|
const normalizedX = (x - range[0]) / xRange;
|
||||||
|
const normalizedY = (y - range[0]) / yRange;
|
||||||
|
const canvasX = normalizedX * cw;
|
||||||
|
const canvasY = normalizedY * ch;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(canvasX, canvasY, 2.5, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成热力图图片(类似原有的generateHeatmapImage函数)
|
||||||
|
* 但使用核密度估计算法
|
||||||
|
*/
|
||||||
|
export function generateKDEHeatmapImage(
|
||||||
|
canvasId,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
points,
|
||||||
|
options = {}
|
||||||
|
) {
|
||||||
|
// Canvas 2D 导出(传入 canvas 对象)
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
drawKDEHeatmap(canvasId, width, height, points, options)
|
||||||
|
.then(() => {
|
||||||
|
try {
|
||||||
|
wx.createSelectorQuery()
|
||||||
|
.select(`#${canvasId}`)
|
||||||
|
.fields({ node: true, size: true })
|
||||||
|
.exec((res) => {
|
||||||
|
const { node: canvas, width: w, height: h } = res[0] || {};
|
||||||
|
if (!canvas) return reject(new Error("canvas 为空"));
|
||||||
|
const cw = width || w || 300;
|
||||||
|
const ch = height || h || 300;
|
||||||
|
uni.canvasToTempFilePath({
|
||||||
|
canvas,
|
||||||
|
width: cw,
|
||||||
|
height: ch,
|
||||||
|
destWidth: cw * 3,
|
||||||
|
destHeight: ch * 3,
|
||||||
|
success: (r) => resolve(r.tempFilePath),
|
||||||
|
fail: reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateHeatMapData = (width, height, amount = 100) => {
|
||||||
|
const data = [];
|
||||||
|
const centerX = 0.5; // 中心点X坐标
|
||||||
|
const centerY = 0.5; // 中心点Y坐标
|
||||||
|
|
||||||
|
for (let i = 0; i < amount; i++) {
|
||||||
|
let x, y;
|
||||||
|
|
||||||
|
// 30%的数据集中在中心区域(高斯分布)
|
||||||
|
if (Math.random() < 0.3) {
|
||||||
|
// 使用正态分布生成中心区域的数据
|
||||||
|
const angle = Math.random() * 2 * Math.PI;
|
||||||
|
const radius = Math.sqrt(-2 * Math.log(Math.random())) * 0.15; // 标准差0.15
|
||||||
|
x = centerX + radius * Math.cos(angle);
|
||||||
|
y = centerY + radius * Math.sin(angle);
|
||||||
|
} else {
|
||||||
|
x = Math.random() * 0.8 + 0.1; // 0.1-0.9范围
|
||||||
|
y = Math.random() * 0.8 + 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保坐标在0-1范围内
|
||||||
|
x = Math.max(0.05, Math.min(0.95, x));
|
||||||
|
y = Math.max(0.05, Math.min(0.95, y));
|
||||||
|
|
||||||
|
data.push({
|
||||||
|
x: parseFloat(x.toFixed(3)),
|
||||||
|
y: parseFloat(y.toFixed(3)),
|
||||||
|
ring: Math.floor(Math.random() * 5) + 6, // 6-10环
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -3,6 +3,30 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/index"
|
"path": "pages/index"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/reset-password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/point-book"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/edit-profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/sign-in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/sign-up"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/about-us"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/webview",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/battle-result"
|
"path": "pages/battle-result"
|
||||||
},
|
},
|
||||||
@@ -79,10 +103,7 @@
|
|||||||
"path": "pages/ranking"
|
"path": "pages/ranking"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/rank-list",
|
"path": "pages/rank-list"
|
||||||
"style": {
|
|
||||||
"enablePullDownRefresh": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/team-match"
|
"path": "pages/team-match"
|
||||||
@@ -104,14 +125,15 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
"backgroundColor": "@bgColor",
|
"backgroundColor": "#fff",
|
||||||
"backgroundColorBottom": "@bgColorBottom",
|
"backgroundColorBottom": "@bgColorBottom",
|
||||||
"backgroundColorTop": "@bgColorTop",
|
"backgroundColorTop": "@bgColorTop",
|
||||||
"backgroundTextStyle": "@bgTxtStyle",
|
"backgroundTextStyle": "@bgTxtStyle",
|
||||||
"navigationBarBackgroundColor": "#000000",
|
"navigationBarBackgroundColor": "#000000",
|
||||||
"navigationBarTextStyle": "@navTxtStyle",
|
"navigationBarTextStyle": "@navTxtStyle",
|
||||||
"navigationBarTitleText": "Uni Creator",
|
"navigationBarTitleText": "Uni Creator",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
},
|
},
|
||||||
"subPackages": []
|
"subPackages": []
|
||||||
}
|
}
|
||||||
|
|||||||
87
src/pages/about-us.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<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 = 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>
|
||||||
|
<Container title="关于我们">
|
||||||
|
<view class="container">
|
||||||
|
<view class="text">
|
||||||
|
《射灵星球》是以智能和物联网技术驱动的全球射箭专业选手及射箭爱好者互动交流平台,由广州光点飞舞网络有限公司研发并提供线上服务。
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="text">
|
||||||
|
我们专注于智能射箭技术的探索和应用,通过物联网技术、激光系统、人工智能、嵌入式AI及射箭在线互娱模式的创新与研发,提供专业的智能体育设备和有趣的在线物联游戏,以此推动射箭运动及更多专业体育运动走入大众家庭。
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
class="copyright"
|
||||||
|
:style="{ paddingBottom: isIos ? '30rpx' : '20rpx' }"
|
||||||
|
@click="openLink"
|
||||||
|
>
|
||||||
|
<text>粤ICP备2025421150号-2X</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
width: calc(100% - 50rpx);
|
||||||
|
height: 100%;
|
||||||
|
padding: 25rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: calc(100% - 50rpx);
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #afafaf;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import Avatar from "@/components/Avatar.vue";
|
||||||
import UserUpgrade from "@/components/UserUpgrade.vue";
|
import UserUpgrade from "@/components/UserUpgrade.vue";
|
||||||
import { getGameAPI } from "@/apis";
|
import { getGameAPI } from "@/apis";
|
||||||
import { topThreeColors, getBattleResultTips } from "@/constants";
|
import { topThreeColors, getBattleResultTips } from "@/constants";
|
||||||
|
import audioManager from "@/audioManager";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -24,7 +25,7 @@ onLoad(async (options) => {
|
|||||||
const myId = user.value.id;
|
const myId = user.value.id;
|
||||||
if (options.battleId) {
|
if (options.battleId) {
|
||||||
const result = await getGameAPI(
|
const result = await getGameAPI(
|
||||||
options.battleId || "BATTLE-1755230224461209000-539"
|
options.battleId || "BATTLE-1758270367040321900-868"
|
||||||
);
|
);
|
||||||
data.value = {
|
data.value = {
|
||||||
...result,
|
...result,
|
||||||
@@ -76,7 +77,24 @@ onLoad(async (options) => {
|
|||||||
if (mine) {
|
if (mine) {
|
||||||
data.value.myTeam = mine.team;
|
data.value.myTeam = mine.team;
|
||||||
totalPoints.value = mine.totalScore;
|
totalPoints.value = mine.totalScore;
|
||||||
ifWin.value = battleInfo.mode === 1 && mine.team === battleInfo.winner;
|
if (battleInfo.mode === 1) {
|
||||||
|
ifWin.value = mine.team === battleInfo.winner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.value.mode === 1) {
|
||||||
|
audioManager.play(ifWin.value ? "胜利" : "失败");
|
||||||
|
} else if (data.value.mode === 2) {
|
||||||
|
if (data.value.battleMode === 1) {
|
||||||
|
if (rank.value <= data.value.playerStats.length * 0.3) {
|
||||||
|
audioManager.play("胜利");
|
||||||
|
}
|
||||||
|
} else if (data.value.battleMode === 2) {
|
||||||
|
if (totalPoints.value > 0) {
|
||||||
|
audioManager.play("胜利");
|
||||||
|
} else if (totalPoints.value < 0) {
|
||||||
|
audioManager.play("失败");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -129,14 +147,19 @@ const checkBowData = () => {
|
|||||||
<image src="../static/title-mvp.png" mode="widthFix" />
|
<image src="../static/title-mvp.png" mode="widthFix" />
|
||||||
<text
|
<text
|
||||||
>斩获<text
|
>斩获<text
|
||||||
:style="{ color: '#fed847', fontSize: '18px', margin: '0 2px' }"
|
:style="{
|
||||||
|
color: '#fed847',
|
||||||
|
fontSize: '18px',
|
||||||
|
margin: '0 3px',
|
||||||
|
fontWeight: '600',
|
||||||
|
}"
|
||||||
>{{ data.mvps[0].totalRings }}</text
|
>{{ data.mvps[0].totalRings }}</text
|
||||||
>环</text
|
>环</text
|
||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="data.mvps && data.mvps.length">
|
<view v-if="data.mvps && data.mvps.length">
|
||||||
<view v-for="(player, index) in data.mvps" :key="index">
|
<view v-for="(player, index) in data.mvps" :key="index">
|
||||||
<view class="team-avatar" :style="{ transform: 'rotate(10deg)' }">
|
<view class="team-avatar">
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="player.avatar"
|
:src="player.avatar"
|
||||||
:size="40"
|
:size="40"
|
||||||
@@ -193,37 +216,37 @@ const checkBowData = () => {
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<image
|
<image
|
||||||
v-if="index === 0"
|
v-if="player.rank === 1"
|
||||||
class="player-bg"
|
class="player-bg"
|
||||||
src="../static/melee-player-bg1.png"
|
src="../static/melee-player-bg1.png"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
<image
|
<image
|
||||||
v-if="index === 1"
|
v-if="player.rank === 2"
|
||||||
class="player-bg"
|
class="player-bg"
|
||||||
src="../static/melee-player-bg2.png"
|
src="../static/melee-player-bg2.png"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
<image
|
<image
|
||||||
v-if="index === 2"
|
v-if="player.rank === 3"
|
||||||
class="player-bg"
|
class="player-bg"
|
||||||
src="../static/melee-player-bg3.png"
|
src="../static/melee-player-bg3.png"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
<image
|
<image
|
||||||
v-if="index === 0"
|
v-if="player.rank === 1"
|
||||||
class="player-crown"
|
class="player-crown"
|
||||||
src="../static/champ1.png"
|
src="../static/champ1.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<image
|
<image
|
||||||
v-if="index === 1"
|
v-if="player.rank === 2"
|
||||||
class="player-crown"
|
class="player-crown"
|
||||||
src="../static/champ2.png"
|
src="../static/champ2.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<image
|
<image
|
||||||
v-if="index === 2"
|
v-if="player.rank === 3"
|
||||||
class="player-crown"
|
class="player-crown"
|
||||||
src="../static/champ3.png"
|
src="../static/champ3.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
@@ -256,10 +279,14 @@ const checkBowData = () => {
|
|||||||
:size="40"
|
:size="40"
|
||||||
:borderColor="data.myTeam === 1 ? '#5fadff' : '#ff6060'"
|
:borderColor="data.myTeam === 1 ? '#5fadff' : '#ff6060'"
|
||||||
/>
|
/>
|
||||||
<text :style="{ backgroundColor: '#5fadff' }" v-if="data.myTeam === 1"
|
<text
|
||||||
|
:style="{ backgroundColor: '#5fadff' }"
|
||||||
|
v-if="data.mode === 1 && data.myTeam === 1"
|
||||||
>蓝队</text
|
>蓝队</text
|
||||||
>
|
>
|
||||||
<text :style="{ backgroundColor: '#ff6060' }" v-if="data.myTeam === 0"
|
<text
|
||||||
|
:style="{ backgroundColor: '#ff6060' }"
|
||||||
|
v-if="data.mode === 1 && data.myTeam === 0"
|
||||||
>红队</text
|
>红队</text
|
||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
@@ -442,6 +469,7 @@ const checkBowData = () => {
|
|||||||
.player-crown {
|
.player-crown {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 27px;
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
margin: 0 15px;
|
margin: 0 15px;
|
||||||
}
|
}
|
||||||
.view-crown {
|
.view-crown {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted, nextTick } from "vue";
|
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import PlayerSeats from "@/components/PlayerSeats.vue";
|
import PlayerSeats from "@/components/PlayerSeats.vue";
|
||||||
@@ -34,6 +34,7 @@ const refreshRoomTimer = ref(null);
|
|||||||
async function refreshRoomData() {
|
async function refreshRoomData() {
|
||||||
if (!roomNumber.value) return;
|
if (!roomNumber.value) return;
|
||||||
const result = await getRoomAPI(roomNumber.value);
|
const result = await getRoomAPI(roomNumber.value);
|
||||||
|
if (result.started) return;
|
||||||
room.value = result;
|
room.value = result;
|
||||||
battleType.value = result.battleType;
|
battleType.value = result.battleType;
|
||||||
(result.members || []).some((m) => {
|
(result.members || []).some((m) => {
|
||||||
@@ -42,31 +43,23 @@ async function refreshRoomData() {
|
|||||||
id: m.userInfo.id,
|
id: m.userInfo.id,
|
||||||
name: m.userInfo.name,
|
name: m.userInfo.name,
|
||||||
avatar: m.userInfo.avatar,
|
avatar: m.userInfo.avatar,
|
||||||
|
rankLvl: m.userInfo.rankLvl,
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
if (result.battleType === 1 && result.count === 1) {
|
if (result.battleType === 1 && result.count === 2) {
|
||||||
if (user.value.id !== owner.value.id) {
|
result.members.forEach((m) => {
|
||||||
opponent.value = {
|
|
||||||
id: user.value.id,
|
|
||||||
name: user.value.nickName,
|
|
||||||
avatar: user.value.avatar,
|
|
||||||
};
|
|
||||||
} else if (result.members.length > 1) {
|
|
||||||
result.members.some((m) => {
|
|
||||||
if (m.userInfo.id !== owner.value.id) {
|
if (m.userInfo.id !== owner.value.id) {
|
||||||
opponent.value = {
|
opponent.value = {
|
||||||
id: m.userInfo.id,
|
id: m.userInfo.id,
|
||||||
name: m.userInfo.name,
|
name: m.userInfo.name,
|
||||||
avatar: m.userInfo.avatar,
|
avatar: m.userInfo.avatar,
|
||||||
|
rankLvl: m.userInfo.rankLvl,
|
||||||
};
|
};
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} else if (result.battleType === 2) {
|
} else if (result.battleType === 2) {
|
||||||
players.value = [];
|
players.value = [];
|
||||||
const ownerIndex = result.members.findIndex(
|
const ownerIndex = result.members.findIndex(
|
||||||
@@ -117,12 +110,14 @@ async function onReceiveMessage(messages = []) {
|
|||||||
id: msg.userId,
|
id: msg.userId,
|
||||||
name: msg.name,
|
name: msg.name,
|
||||||
avatar: msg.avatar,
|
avatar: msg.avatar,
|
||||||
|
rankLvl: msg.rankLvl,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
opponent.value = {
|
opponent.value = {
|
||||||
id: msg.userId,
|
id: msg.userId,
|
||||||
name: msg.name,
|
name: msg.name,
|
||||||
avatar: msg.avatar,
|
avatar: msg.avatar,
|
||||||
|
rankLvl: msg.rankLvl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,12 +127,14 @@ async function onReceiveMessage(messages = []) {
|
|||||||
id: msg.userId,
|
id: msg.userId,
|
||||||
name: msg.name,
|
name: msg.name,
|
||||||
avatar: msg.avatar,
|
avatar: msg.avatar,
|
||||||
|
rankLvl: msg.rankLvl,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
players.value.push({
|
players.value.push({
|
||||||
id: msg.userId,
|
id: msg.userId,
|
||||||
name: msg.name,
|
name: msg.name,
|
||||||
avatar: msg.avatar,
|
avatar: msg.avatar,
|
||||||
|
rankLvl: msg.rankLvl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,6 +175,7 @@ async function onReceiveMessage(messages = []) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (msg.constructor === MESSAGETYPES.WaitForAllReady) {
|
if (msg.constructor === MESSAGETYPES.WaitForAllReady) {
|
||||||
|
roomNumber.value = "";
|
||||||
if (msg.groupUserStatus) {
|
if (msg.groupUserStatus) {
|
||||||
uni.setStorageSync("red-team", msg.groupUserStatus.redTeam);
|
uni.setStorageSync("red-team", msg.groupUserStatus.redTeam);
|
||||||
uni.setStorageSync("blue-team", msg.groupUserStatus.blueTeam);
|
uni.setStorageSync("blue-team", msg.groupUserStatus.blueTeam);
|
||||||
@@ -188,7 +186,7 @@ async function onReceiveMessage(messages = []) {
|
|||||||
uni.removeStorageSync("current-battle");
|
uni.removeStorageSync("current-battle");
|
||||||
if (msg.groupUserStatus.config.mode == 1) {
|
if (msg.groupUserStatus.config.mode == 1) {
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
url: `/pages/team-match?battleId=${msg.id}&gameMode=1`,
|
url: `/pages/team-battle?battleId=${msg.id}&gameMode=1`,
|
||||||
});
|
});
|
||||||
} else if (msg.groupUserStatus.config.mode == 2) {
|
} else if (msg.groupUserStatus.config.mode == 2) {
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
@@ -247,7 +245,7 @@ onMounted(() => {
|
|||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
if (refreshRoomTimer.value) clearInterval(refreshRoomTimer.value);
|
if (refreshRoomTimer.value) clearInterval(refreshRoomTimer.value);
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
@@ -285,26 +283,38 @@ onHide(() => {});
|
|||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<view>
|
<view>
|
||||||
<view class="player" :style="{ transform: 'translateY(-60px)' }">
|
<view
|
||||||
<Avatar :rankLvl="user.rankLvl" :src="user.avatar" :size="60" />
|
v-if="owner.id"
|
||||||
|
class="player"
|
||||||
|
:style="{ transform: 'translateY(-60px)' }"
|
||||||
|
>
|
||||||
|
<Avatar :rankLvl="owner.rankLvl" :src="owner.avatar" :size="60" />
|
||||||
<text>{{ owner.name }}</text>
|
<text>{{ owner.name }}</text>
|
||||||
<text>创建者</text>
|
<text>创建者</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view
|
||||||
|
v-else
|
||||||
|
class="no-player"
|
||||||
|
:style="{ transform: 'translateY(-60px)' }"
|
||||||
|
>
|
||||||
|
<image src="../static/question-mark.png" mode="widthFix" />
|
||||||
|
</view>
|
||||||
<image src="../static/versus.png" mode="widthFix" />
|
<image src="../static/versus.png" mode="widthFix" />
|
||||||
<block v-if="opponent.id">
|
<view
|
||||||
<view class="player" :style="{ transform: 'translateY(60px)' }">
|
v-if="opponent.id"
|
||||||
<image
|
class="player"
|
||||||
:src="opponent.avatar || '../static/user-icon.png'"
|
:style="{ transform: 'translateY(60px)' }"
|
||||||
mode="widthFix"
|
>
|
||||||
|
<Avatar
|
||||||
|
:rankLvl="opponent.rankLvl"
|
||||||
|
:src="opponent.avatar"
|
||||||
|
:size="60"
|
||||||
/>
|
/>
|
||||||
<text v-if="opponent.name">{{ opponent.name }}</text>
|
<text v-if="opponent.name">{{ opponent.name }}</text>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
<view class="no-player" v-else>
|
||||||
<block v-else>
|
|
||||||
<view class="no-player">
|
|
||||||
<image src="../static/question-mark.png" mode="widthFix" />
|
<image src="../static/question-mark.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
</block>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<PlayerSeats
|
<PlayerSeats
|
||||||
@@ -318,7 +328,8 @@ onHide(() => {});
|
|||||||
src="https://static.shelingxingqiu.com/attachment/2025-08-13/dc0x1p59iab6cvbhqc.png"
|
src="https://static.shelingxingqiu.com/attachment/2025-08-13/dc0x1p59iab6cvbhqc.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<image src="../static/title-2v2.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>
|
||||||
<view v-for="(item, index) in players" :key="index">
|
<view v-for="(item, index) in players" :key="index">
|
||||||
<Avatar v-if="item.id" :src="item.avatar" :size="36" />
|
<Avatar v-if="item.id" :src="item.avatar" :size="36" />
|
||||||
@@ -478,7 +489,7 @@ onHide(() => {});
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.player > text:last-child {
|
.player > text:nth-child(3) {
|
||||||
color: #000;
|
color: #000;
|
||||||
background-color: #fed847;
|
background-color: #fed847;
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import Avatar from "@/components/Avatar.vue";
|
||||||
import SButton from "@/components/SButton.vue";
|
import SButton from "@/components/SButton.vue";
|
||||||
import SModal from "@/components/SModal.vue";
|
import SModal from "@/components/SModal.vue";
|
||||||
import Signin from "@/components/Signin.vue";
|
import Signin from "@/components/Signin.vue";
|
||||||
import UserHeader from "@/components/UserHeader.vue";
|
import UserHeader from "@/components/UserHeader.vue";
|
||||||
import { createOrderAPI, getHomeData } from "@/apis";
|
import { createOrderAPI, getHomeData, getVIPDescAPI } from "@/apis";
|
||||||
import { formatTimestamp } from "@/util";
|
import { formatTimestamp } from "@/util";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
@@ -19,6 +19,7 @@ const showModal = ref(false);
|
|||||||
const lastDate = ref(user.value.expiredAt);
|
const lastDate = ref(user.value.expiredAt);
|
||||||
const refreshing = ref(false);
|
const refreshing = ref(false);
|
||||||
const timer = ref(null);
|
const timer = ref(null);
|
||||||
|
const richContent = ref("");
|
||||||
|
|
||||||
const onPay = async () => {
|
const onPay = async () => {
|
||||||
if (!user.value.id) {
|
if (!user.value.id) {
|
||||||
@@ -61,13 +62,18 @@ const onPay = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const result = await getVIPDescAPI();
|
||||||
|
richContent.value = result.describe;
|
||||||
|
});
|
||||||
|
|
||||||
const toOrderPage = () => {
|
const toOrderPage = () => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: "/pages/orders",
|
url: "/pages/orders",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
if (timer.value) clearInterval(timer.value);
|
if (timer.value) clearInterval(timer.value);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -107,8 +113,9 @@ onUnmounted(() => {
|
|||||||
<view />
|
<view />
|
||||||
<text>VIP 介绍</text>
|
<text>VIP 介绍</text>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view :style="{ marginTop: '10rpx' }">
|
||||||
<text
|
<rich-text :nodes="richContent" />
|
||||||
|
<!-- <text
|
||||||
>射灵星球VIP服务,为全球弓箭手提供约战、段位评级、实时排位赛、智能教练点评等专属特权。会员可在酷帅的真实射箭运动中,同步享受在线竞技的乐趣,还能找到志同道合的伙伴,并获得新鲜的功能体验和持续升级的系统。
|
>射灵星球VIP服务,为全球弓箭手提供约战、段位评级、实时排位赛、智能教练点评等专属特权。会员可在酷帅的真实射箭运动中,同步享受在线竞技的乐趣,还能找到志同道合的伙伴,并获得新鲜的功能体验和持续升级的系统。
|
||||||
</text>
|
</text>
|
||||||
<text
|
<text
|
||||||
@@ -117,7 +124,7 @@ onUnmounted(() => {
|
|||||||
<text
|
<text
|
||||||
>VIP会员还将获得专属客服支持。当您在游戏中遇到任何问题,无论是技术故障、规则疑问还是其他需要帮助的情况,都可联系我们的VIP专属客服团队。他们将提供全年不间断的优质服务,确保您的对战体验不受影响。
|
>VIP会员还将获得专属客服支持。当您在游戏中遇到任何问题,无论是技术故障、规则疑问还是其他需要帮助的情况,都可联系我们的VIP专属客服团队。他们将提供全年不间断的优质服务,确保您的对战体验不受影响。
|
||||||
</text>
|
</text>
|
||||||
<text>期待您的加入!</text>
|
<text>期待您的加入!</text> -->
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="content">
|
<view class="content">
|
||||||
@@ -202,6 +209,7 @@ onUnmounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.title-bar > view:first-child {
|
.title-bar > view:first-child {
|
||||||
width: 5px;
|
width: 5px;
|
||||||
@@ -214,11 +222,6 @@ onUnmounted(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
.content > view:nth-child(2) > text {
|
|
||||||
display: block;
|
|
||||||
margin-top: 10px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.vip-items {
|
.vip-items {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -232,6 +235,7 @@ onUnmounted(() => {
|
|||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-size: 27rpx;
|
||||||
}
|
}
|
||||||
.vip-content {
|
.vip-content {
|
||||||
max-height: 62%;
|
max-height: 62%;
|
||||||
|
|||||||
@@ -1,46 +1,101 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Container from "@/components/Container.vue";
|
import { ref, onMounted } from "vue";
|
||||||
import SButton from "@/components/SButton.vue";
|
import SButton from "@/components/SButton.vue";
|
||||||
|
|
||||||
|
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-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("");
|
||||||
|
const capsuleHeight = ref(0);
|
||||||
|
onMounted(async () => {
|
||||||
|
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
|
||||||
|
capsuleHeight.value = menuBtnInfo.top - 9;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onScrollView = (e) => {
|
||||||
|
addBg.value = e.detail.scrollTop > 100;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container title="智能弓箭">
|
|
||||||
<view class="container">
|
<view class="container">
|
||||||
|
<view
|
||||||
|
class="header"
|
||||||
|
:style="{
|
||||||
|
paddingTop: capsuleHeight + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
<image
|
<image
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-08-07/dbw5tq93d6n7xgtgvp.png"
|
v-if="addBg"
|
||||||
|
class="bg-image"
|
||||||
|
src="../static/app-bg.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<text>商品形象图及配图标题</text>
|
<navigator open-type="navigateBack">
|
||||||
|
<image class="header-back" src="../static/back.png" mode="widthFix" />
|
||||||
|
</navigator>
|
||||||
<text
|
<text
|
||||||
>在射灵世界中,等级是衡量您射箭技能的重要指标,而点数则是您提升等级的关键。具体的要求如下
|
:style="{ opacity: addBg ? 1 : 0, color: '#fff', fontWeight: 'bold' }"
|
||||||
每射出一支箭并上靶:无论您射出的箭命中哪个环数,只要箭成功上靶,您将获得1点基础点数。这是您积累点数的基本方式,每一次射击都是您向更高目标迈进的一步。
|
|
||||||
射出的箭命中7-9环:当您的箭命中7环、8环或9环时,除了获得1点基础点数外,还将额外获得0.5点基础点数;
|
|
||||||
射出的箭命中10环:命中10环是射箭中的最高成就,因此,当您的箭命中10环时,除了获得1点基础点数外,还将额外获得1点基础点数.即每次命中10环将总共获得?</text
|
|
||||||
>
|
>
|
||||||
<SButton>加官方企业微信订购(有优惠)</SButton>
|
本赛季排行榜
|
||||||
|
</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 />
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.header-bg {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
.bg-image {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.images {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
.container > image {
|
.images > image {
|
||||||
width: 85%;
|
width: 100vw;
|
||||||
border-radius: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.container > text {
|
|
||||||
color: #fff;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
.container > text:nth-child(3) {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #fff9;
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0 15px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
102
src/pages/edit-profile.vue
Normal 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>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import Guide from "@/components/Guide.vue";
|
import Guide from "@/components/Guide.vue";
|
||||||
import SButton from "@/components/SButton.vue";
|
import SButton from "@/components/SButton.vue";
|
||||||
import Swiper from "@/components/Swiper.vue";
|
import Swiper from "@/components/Swiper.vue";
|
||||||
@@ -100,7 +100,7 @@ onMounted(() => {
|
|||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ onShow(async () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
.founded-room > view > view {
|
.founded-room > view > view {
|
||||||
background-color: #fed847;
|
background-color: #fed847;
|
||||||
|
|||||||
@@ -30,32 +30,26 @@ const { user } = storeToRefs(store);
|
|||||||
<!-- 说明文本 -->
|
<!-- 说明文本 -->
|
||||||
<view class="content">
|
<view class="content">
|
||||||
<view class="intro-text">
|
<view class="intro-text">
|
||||||
在射灵世界中,等级是衡量您射箭技能的重要指标,而点数则是您提升等级的关键。具体的要求如下:
|
在射灵世界中,等级是衡量您射箭技能的重要指标,而经验则是您提升等级的关键。具体的要求如下:
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="section">
|
<view class="section">
|
||||||
<view class="title">一、点数的获取规则</view>
|
<view class="title">一、经验的获取规则</view>
|
||||||
<view class="text">
|
<view class="text">
|
||||||
每射出一支箭并上靶,无论您射出的箭命中哪个环数,只要箭成功上靶,您将获得1点基础点数。这是您积累点数的基本方式,每一次射击都是您向更高目标迈进的一步。
|
每射出一支箭,无论是否中靶、环数高低,您将获得1点经验。这是您提升射灵等级的基本方式,每一次射箭都是您向更高目标迈进的一步。
|
||||||
</view>
|
|
||||||
<view class="text">
|
|
||||||
射出的箭中7-9环:当您的箭中7环、8环或9环时,除了获得1点基础点数外,还将额外获得0.5点基础点数;
|
|
||||||
</view>
|
|
||||||
<view class="text">
|
|
||||||
射出的箭中10环:命中10环是射箭中的最高成就,因此,当您的箭中10环时,除了获得1点基础点数外,还将额外获得1点基础点数。即每次命中10环将总共获得2点基础点数。
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="section">
|
<view class="section">
|
||||||
<view class="title">二、解锁特权与玩法</view>
|
<view class="title">二、解锁特权与玩法</view>
|
||||||
<view class="text">
|
<view class="text">
|
||||||
当您等级达到9级时,将解锁"约战模式"。在这个模式中,您可以邀请您的好友进行切磋,与他们展开一场精彩的射箭对决。通过与好友的对抗,您不仅可以收获友谊和欢乐,还能在交流中学习到更多的技巧和经验。
|
当您等级达到9级时,将解锁“约战模式”。在这个模式中,您可以邀请您的好友进行切磋,与他们展开一场精彩的射箭对决。通过与好友的对抗,您不仅可以收获友谊和欢乐,还能在交流中学习到更多的技巧和经验。
|
||||||
</view>
|
</view>
|
||||||
<view class="text">
|
<view class="text">
|
||||||
当您等级达到16级时,将解锁对战模式,每次对战都是一次难得的学习机会。您可以借此机会提升自己的水平,同时也为您的好友提供帮助和建议。此外,约战模式还为您提供了展示自己技艺的平台,让您在与好友的互动中感受到射箭的乐趣和成就感。
|
当您等级达到16级时,将解锁“对战模式”,每次对战都是一次难得的学习机会。您可以借此机会提升自己的水平,同时也为您的好友提供帮助和建议。此外,约战模式还为您提供了展示自己技艺的平台,让您在与好友的互动中感受到射箭的乐趣和成就感。
|
||||||
</view>
|
</view>
|
||||||
<view class="text">
|
<view class="text">
|
||||||
当您等级达到22级时,将解锁"押豆模式"。在这个模式中,您可以与相同段位的玩家进行对抗,赢家将收获所有的灵豆。这不仅增加了游戏的趣味性和挑战性,还为您提供了赢取更多灵豆的机会。通过与更多玩家的对战,您可以不断提升自己的技术水平。
|
未来我们将推出“押豆模式”,敬请期待!当您等级达到22级时,您可以与相同段位的玩家进行对抗,赢家将收获所有的灵豆。这不仅增加了游戏的趣味性和挑战性,还为您提供了赢取更多灵豆的机会。通过与更多玩家的对战,您可以不断提升自己的技术水平。
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -71,6 +65,7 @@ const { user } = storeToRefs(store);
|
|||||||
|
|
||||||
.level-progress {
|
.level-progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 32rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
@@ -95,9 +90,9 @@ const { user } = storeToRefs(store);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 0 10px;
|
height: calc(100% - 148rpx);
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
padding: 15px;
|
padding: 30rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro-text {
|
.intro-text {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
import { onShow, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import AppFooter from "@/components/AppFooter.vue";
|
import AppFooter from "@/components/AppFooter.vue";
|
||||||
import AppBackground from "@/components/AppBackground.vue";
|
import AppBackground from "@/components/AppBackground.vue";
|
||||||
@@ -58,15 +58,25 @@ const toRankListPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
const rankList = await getRankListAPI();
|
const token = uni.getStorageSync(
|
||||||
updateRank(rankList);
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
const token = uni.getStorageSync("token");
|
);
|
||||||
|
|
||||||
|
const promises = [getRankListAPI()];
|
||||||
if (token) {
|
if (token) {
|
||||||
const result = await getHomeData();
|
promises.push(getHomeData());
|
||||||
console.log("首页数据:", result);
|
}
|
||||||
if (result.user) {
|
|
||||||
updateUser(result.user);
|
const [rankList, homeData] = await Promise.all(promises);
|
||||||
if (result.user.trio <= 0) {
|
|
||||||
|
console.log("排行数据", rankList);
|
||||||
|
updateRank(rankList);
|
||||||
|
|
||||||
|
if (homeData) {
|
||||||
|
console.log("首页数据:", homeData);
|
||||||
|
if (homeData.user) {
|
||||||
|
updateUser(homeData.user);
|
||||||
|
if (homeData.user.trio <= 0) {
|
||||||
showGuide.value = true;
|
showGuide.value = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showGuide.value = false;
|
showGuide.value = false;
|
||||||
@@ -84,7 +94,6 @@ onShow(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
uni.removeStorageSync("point-book-config");
|
|
||||||
const config = await getAppConfig();
|
const config = await getAppConfig();
|
||||||
updateConfig(config);
|
updateConfig(config);
|
||||||
console.log("全局配置:", config);
|
console.log("全局配置:", config);
|
||||||
@@ -96,6 +105,23 @@ const comingSoon = () => {
|
|||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onShareAppMessage(() => {
|
||||||
|
return {
|
||||||
|
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
|
||||||
|
path: "/pages/index", // 用户点击分享卡片后跳转的页面路径
|
||||||
|
imageUrl:
|
||||||
|
"https://static.shelingxingqiu.com/attachment/2025-09-12/dcqoz26q0268wxmzjg.png", // 分享卡片的配图,可以是本地或网络图片
|
||||||
|
};
|
||||||
|
});
|
||||||
|
onShareTimeline(() => {
|
||||||
|
return {
|
||||||
|
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享到朋友圈的标题
|
||||||
|
query: "from=timeline", // 用户通过朋友圈点击后,在页面 onShow 的 options 中可以获取到的参数
|
||||||
|
imageUrl:
|
||||||
|
"https://static.shelingxingqiu.com/attachment/2025-09-12/dcqoz26q0268wxmzjg.png", // 分享到朋友圈的配图
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -111,7 +137,7 @@ const comingSoon = () => {
|
|||||||
@click="() => toPage('/pages/my-device')"
|
@click="() => toPage('/pages/my-device')"
|
||||||
/>
|
/>
|
||||||
<text v-if="!user.id">我的弓箭</text>
|
<text v-if="!user.id">我的弓箭</text>
|
||||||
<text v-if="user.id && !device.deviceId">请绑定设备</text>
|
<text v-if="user.id && !device.deviceId">连接智能弓箭</text>
|
||||||
<text
|
<text
|
||||||
v-if="user.id && device.deviceId"
|
v-if="user.id && device.deviceId"
|
||||||
class="truncate"
|
class="truncate"
|
||||||
@@ -141,7 +167,10 @@ const comingSoon = () => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="ranking-section">
|
<view class="ranking-section">
|
||||||
<image src="../static/rank-bg.png" mode="widthFix" />
|
<image
|
||||||
|
src="https://static.shelingxingqiu.com/attachment/2025-09-25/dd1p9ci9v7frcrsxhj.png"
|
||||||
|
mode="widthFix"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
class="into-btn"
|
class="into-btn"
|
||||||
@click="() => toPage('/pages/ranking')"
|
@click="() => toPage('/pages/ranking')"
|
||||||
@@ -151,13 +180,15 @@ const comingSoon = () => {
|
|||||||
<img src="../static/juezhanbang.png" mode="widthFix" />
|
<img src="../static/juezhanbang.png" mode="widthFix" />
|
||||||
<view class="divide-line"></view>
|
<view class="divide-line"></view>
|
||||||
<view class="player-avatars">
|
<view class="player-avatars">
|
||||||
<block v-for="i in 6" :key="i">
|
|
||||||
<block v-if="rankData.rank && rankData.rank[i - 1]">
|
|
||||||
<view
|
<view
|
||||||
|
v-for="i in 6"
|
||||||
|
:key="i"
|
||||||
class="player-avatar"
|
class="player-avatar"
|
||||||
:style="{
|
:style="{
|
||||||
zIndex: 8 - i,
|
zIndex: 8 - i,
|
||||||
borderColor: topThreeColors[i - 1] || '#000',
|
borderColor: rankData.rank[i - 1]
|
||||||
|
? topThreeColors[i - 1] || '#000'
|
||||||
|
: '#000',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<image v-if="i === 1" src="../static/champ1.png" />
|
<image v-if="i === 1" src="../static/champ1.png" />
|
||||||
@@ -166,13 +197,13 @@ const comingSoon = () => {
|
|||||||
<view v-if="i > 3">{{ i }}</view>
|
<view v-if="i > 3">{{ i }}</view>
|
||||||
<image
|
<image
|
||||||
:src="
|
:src="
|
||||||
rankData.rank[i - 1].avatar || '../static/user-icon.png'
|
rankData.rank[i - 1]
|
||||||
|
? rankData.rank[i - 1].avatar
|
||||||
|
: '../static/user-icon-dark.png'
|
||||||
"
|
"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
|
||||||
</block>
|
|
||||||
<view class="more-players">
|
<view class="more-players">
|
||||||
<text>{{ rankData.rank.length }}</text>
|
<text>{{ rankData.rank.length }}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -182,7 +213,7 @@ const comingSoon = () => {
|
|||||||
<view @click="() => toPage('/pages/my-growth')">
|
<view @click="() => toPage('/pages/my-growth')">
|
||||||
<image src="../static/my-growth.png" mode="widthFix" />
|
<image src="../static/my-growth.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view @click="() => toPage('/pages/ranking')">
|
||||||
<view>
|
<view>
|
||||||
<text>段位</text>
|
<text>段位</text>
|
||||||
<text>{{
|
<text>{{
|
||||||
@@ -262,7 +293,7 @@ const comingSoon = () => {
|
|||||||
<Signin :onClose="() => (showModal = false)" />
|
<Signin :onClose="() => (showModal = false)" />
|
||||||
</SModal>
|
</SModal>
|
||||||
</view>
|
</view>
|
||||||
<AppFooter :signin="() => (showModal = true)" />
|
<AppFooter />
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -341,7 +372,7 @@ const comingSoon = () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
margin-top: 42%;
|
margin-top: 42%;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
border-bottom: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ranking-players > image:first-child {
|
.ranking-players > image:first-child {
|
||||||
@@ -366,8 +397,8 @@ const comingSoon = () => {
|
|||||||
width: 82rpx;
|
width: 82rpx;
|
||||||
height: 82rpx;
|
height: 82rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-right: -10px;
|
margin-right: -20rpx;
|
||||||
border: 1px solid #312f35;
|
border: 1rpx solid #312f35;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -375,10 +406,10 @@ const comingSoon = () => {
|
|||||||
.player-avatar > image:first-child,
|
.player-avatar > image:first-child,
|
||||||
.player-avatar > view:first-child {
|
.player-avatar > view:first-child {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -10px;
|
top: -24rpx;
|
||||||
left: 12px;
|
left: 22rpx;
|
||||||
width: 16px;
|
width: 32rpx;
|
||||||
height: 16px;
|
height: 32rpx;
|
||||||
}
|
}
|
||||||
.player-avatar > view:first-child {
|
.player-avatar > view:first-child {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ const checkBowData = () => {
|
|||||||
borderColor: '#FF6767',
|
borderColor: '#FF6767',
|
||||||
transform: `translateX(-${index * 15}px)`,
|
transform: `translateX(-${index * 15}px)`,
|
||||||
}"
|
}"
|
||||||
:src="src"
|
:src="src || '../static/user-icon.png'"
|
||||||
:key="index"
|
:key="index"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
@@ -210,7 +210,7 @@ const checkBowData = () => {
|
|||||||
borderColor: '#64BAFF',
|
borderColor: '#64BAFF',
|
||||||
transform: `translateX(-${index * 15}px)`,
|
transform: `translateX(-${index * 15}px)`,
|
||||||
}"
|
}"
|
||||||
:src="src"
|
:src="src || '../static/user-icon.png'"
|
||||||
:key="index"
|
:key="index"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import Matching from "@/components/Matching.vue";
|
import Matching from "@/components/Matching.vue";
|
||||||
@@ -59,7 +59,7 @@ onMounted(() => {
|
|||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import BowTarget from "@/components/BowTarget.vue";
|
import BowTarget from "@/components/BowTarget.vue";
|
||||||
import ShootProgress from "@/components/ShootProgress.vue";
|
import ShootProgress from "@/components/ShootProgress.vue";
|
||||||
import BattleHeader from "@/components/BattleHeader.vue";
|
import BattleHeader from "@/components/BattleHeader.vue";
|
||||||
import Timer from "@/components/Timer.vue";
|
|
||||||
import PlayerScore from "@/components/PlayerScore.vue";
|
import PlayerScore from "@/components/PlayerScore.vue";
|
||||||
import SButton from "@/components/SButton.vue";
|
import SButton from "@/components/SButton.vue";
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import Avatar from "@/components/Avatar.vue";
|
||||||
@@ -161,7 +160,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
@@ -193,7 +192,7 @@ onHide(() => {
|
|||||||
<Container :title="title" :bgType="1" :onBack="onBack">
|
<Container :title="title" :bgType="1" :onBack="onBack">
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<BattleHeader v-if="!start" :players="players" />
|
<BattleHeader v-if="!start" :players="players" />
|
||||||
<TestDistance v-if="!start" :guide="false" />
|
<TestDistance v-if="!start" :guide="false" :isBattle="true" />
|
||||||
<ShootProgress
|
<ShootProgress
|
||||||
:show="start"
|
:show="start"
|
||||||
:start="start && startCount"
|
:start="start && startCount"
|
||||||
@@ -223,7 +222,6 @@ onHide(() => {
|
|||||||
:scores="playersScores[player.id] || []"
|
:scores="playersScores[player.id] || []"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<Timer v-if="!start" />
|
|
||||||
<ScreenHint
|
<ScreenHint
|
||||||
:show="halfTimeTip"
|
:show="halfTimeTip"
|
||||||
mode="small"
|
mode="small"
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import { storeToRefs } from "pinia";
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { user } = storeToRefs(store);
|
const { user } = storeToRefs(store);
|
||||||
const arrows = ref([]);
|
const arrows = ref([]);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
onLoad(async (options) => {
|
onLoad(async (options) => {
|
||||||
if (options.id) {
|
if (options.id) {
|
||||||
const result = await getPractiseAPI(options.id);
|
const result = await getPractiseAPI(options.id);
|
||||||
arrows.value = result.arrows;
|
arrows.value = result.arrows;
|
||||||
|
total.value = result.completed_arrows;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -43,8 +45,8 @@ onLoad(async (options) => {
|
|||||||
</view>
|
</view>
|
||||||
<ScorePanel
|
<ScorePanel
|
||||||
:completeEffect="false"
|
:completeEffect="false"
|
||||||
:rowCount="arrows.length === 12 ? 6 : 9"
|
:rowCount="total === 12 ? 6 : 9"
|
||||||
:total="arrows.length"
|
:total="total"
|
||||||
:scores="arrows.map((a) => a.ring)"
|
:scores="arrows.map((a) => a.ring)"
|
||||||
:margin="arrows.length === 12 ? 4 : 1"
|
:margin="arrows.length === 12 ? 4 : 1"
|
||||||
:fontSize="arrows.length === 12 ? 25 : 22"
|
:fontSize="arrows.length === 12 ? 25 : 22"
|
||||||
|
|||||||
@@ -92,35 +92,53 @@ const toDeviceIntroPage = () => {
|
|||||||
const backToHome = () => {
|
const backToHome = () => {
|
||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyEmail = () => {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: "shelingxingqiu@163.com",
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: "邮箱已复制",
|
||||||
|
icon: "success",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container title="弓箭绑定">
|
<Container title="弓箭绑定">
|
||||||
<view v-if="!device.deviceId" class="scan-code">
|
<view v-if="!device.deviceId" class="scan-code">
|
||||||
<view @click="handleScan">
|
<button hover-class="none" @click="handleScan">
|
||||||
<image src="../static/scan.png" mode="widthFix" />
|
<image src="../static/scan.png" mode="widthFix" />
|
||||||
<text>扫码绑定弓箭</text>
|
</button>
|
||||||
</view>
|
<button hover-class="none" @click="showTip = true">
|
||||||
<view>
|
<text>扫</text>
|
||||||
<view @click="() => (showTip = true)">找不到我的弓箭?</view>
|
<text :style="{ color: '#fed847' }">射灵弓箭</text>
|
||||||
<view @click="toDeviceIntroPage">我还没有弓箭</view>
|
<text>上的二维码</text>
|
||||||
</view>
|
<image src="../static/s-question-mark-white.png" mode="widthFix" />
|
||||||
|
</button>
|
||||||
|
<text>射灵智能弓箭,三模传感系统与独创靶环算法,</text>
|
||||||
|
<text>毫秒级在线实时对战,让你拥有全球约战的乐趣!</text>
|
||||||
|
<button hover-class="none" @click="toDeviceIntroPage">
|
||||||
|
<image src="../static/have-no-device.png" mode="widthFix" />
|
||||||
|
</button>
|
||||||
<ScreenHint
|
<ScreenHint
|
||||||
mode="square"
|
mode="square"
|
||||||
:show="showTip"
|
:show="showTip"
|
||||||
:onClose="() => (showTip = false)"
|
:onClose="() => (showTip = false)"
|
||||||
>
|
>
|
||||||
<view class="scan-tips">
|
<view class="scan-tips">
|
||||||
<text>找不到我的弓箭 </text>
|
<text>扫码绑定设灵弓箭</text>
|
||||||
<text>1.确认弓箭是智能弓箭 </text>
|
|
||||||
<text>2.确认弓箭有电且电源已开启 </text>
|
|
||||||
<text>3.进入弓箭绑定功能,扫描弓箭上的二维码 </text>
|
|
||||||
<image
|
<image
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-08-05/dbuacrelri7jr3axiy.png"
|
src="https://static.shelingxingqiu.com/attachment/2025-08-05/dbuacrelri7jr3axiy.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<text>4.连接成功后</text>
|
<text>【注】已被绑定的弓箭无法再次绑定。</text>
|
||||||
<view>联系在线客服</view>
|
<view>
|
||||||
|
<text>如有任何疑问,请随时联系:</text>
|
||||||
|
<button hover-class="none" @click="copyEmail">shelingxingqiu@163.com</button>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</ScreenHint>
|
</ScreenHint>
|
||||||
<ScreenHint
|
<ScreenHint
|
||||||
@@ -202,44 +220,66 @@ const backToHome = () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.scan-code > view:first-child {
|
.scan-code {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.scan-code > button:first-child {
|
||||||
|
margin-top: 22%;
|
||||||
|
}
|
||||||
|
.scan-code > button:first-child > image {
|
||||||
|
width: 300rpx;
|
||||||
|
}
|
||||||
|
.scan-code > button:nth-child(2) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 40%;
|
justify-content: center;
|
||||||
color: #fff;
|
font-size: 26rpx;
|
||||||
font-size: 14px;
|
color: #ffffff;
|
||||||
margin: 35% 0;
|
margin: 50rpx;
|
||||||
}
|
}
|
||||||
.scan-code > view:first-child > image {
|
.scan-code > button:nth-child(2) > image {
|
||||||
width: 100%;
|
width: 28rpx;
|
||||||
margin-bottom: 20px;
|
margin-left: 10rpx;
|
||||||
}
|
}
|
||||||
.scan-code > view:nth-child(2) {
|
.scan-code > text {
|
||||||
display: flex;
|
font-size: 24rpx;
|
||||||
flex-direction: column;
|
color: #fff9;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
.scan-code > view:nth-child(2) > view {
|
.scan-code > button:nth-child(5) {
|
||||||
color: #39a8ff;
|
margin-top: 25%;
|
||||||
font-size: 14px;
|
}
|
||||||
margin: 10px;
|
.scan-code > button:nth-child(5) > image {
|
||||||
|
width: 380rpx;
|
||||||
}
|
}
|
||||||
.scan-tips {
|
.scan-tips {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
width: 90%;
|
||||||
|
margin-top: 20%;
|
||||||
}
|
}
|
||||||
.scan-tips > text {
|
.scan-tips > text {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
}
|
}
|
||||||
.scan-tips > text:first-child {
|
.scan-tips > text:first-child {
|
||||||
color: #fed847;
|
color: #fed847;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
.scan-tips > view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
.scan-tips > view:last-child {
|
.scan-tips > view:last-child {
|
||||||
color: #39a8ff;
|
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
.scan-tips > view:last-child > button {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #39a8ff;
|
||||||
}
|
}
|
||||||
.scan-tips > image {
|
.scan-tips > image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ const onPractiseLoading = async (page) => {
|
|||||||
:blueTeam="item.bluePlayers"
|
:blueTeam="item.bluePlayers"
|
||||||
:redTeam="item.redPlayers"
|
:redTeam="item.redPlayers"
|
||||||
:winner="item.winner"
|
:winner="item.winner"
|
||||||
|
:showRank="item.mode === 2"
|
||||||
:showHeader="false"
|
:showHeader="false"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
@@ -101,50 +102,14 @@ const onPractiseLoading = async (page) => {
|
|||||||
<text>{{ item.createdAt }}</text>
|
<text>{{ item.createdAt }}</text>
|
||||||
<image src="../static/back.png" mode="widthFix" />
|
<image src="../static/back.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
<view v-if="item.mode === 1" class="contest-team">
|
<BattleHeader
|
||||||
<block v-if="item.bluePlayers[0]">
|
:players="item.mode === 1 ? [] : item.players"
|
||||||
<view class="player">
|
:blueTeam="item.bluePlayers"
|
||||||
<Avatar
|
:redTeam="item.redPlayers"
|
||||||
:rankLvl="item.bluePlayers[0].rankLvl"
|
:winner="item.winner"
|
||||||
:src="item.bluePlayers[0].avatar"
|
:showRank="item.mode === 2"
|
||||||
|
:showHeader="false"
|
||||||
/>
|
/>
|
||||||
<text>{{ item.bluePlayers[0].name }}</text>
|
|
||||||
<image
|
|
||||||
v-if="item.winner === 1"
|
|
||||||
src="../static/winner-badge.png"
|
|
||||||
mode="widthFix"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
<block v-if="item.redPlayers[0]">
|
|
||||||
<view class="player">
|
|
||||||
<Avatar
|
|
||||||
:rankLvl="item.redPlayers[0].rankLvl"
|
|
||||||
:src="item.redPlayers[0].avatar"
|
|
||||||
/>
|
|
||||||
<text>{{ item.redPlayers[0].name }}</text>
|
|
||||||
<image
|
|
||||||
v-if="item.winner === 0"
|
|
||||||
src="../static/winner-badge.png"
|
|
||||||
mode="widthFix"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
</view>
|
|
||||||
<view v-if="item.mode === 2" class="contest-melee">
|
|
||||||
<view
|
|
||||||
class="player"
|
|
||||||
v-for="(p, index2) in item.players"
|
|
||||||
:key="index2"
|
|
||||||
:style="{
|
|
||||||
width: `${Math.max(100 / item.players.length, 18)}vw`,
|
|
||||||
backgroundColor: meleeAvatarColors[index2],
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<Avatar :rank="index2 + 1" :src="p.avatar" />
|
|
||||||
<text>{{ p.name }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</ScrollList>
|
</ScrollList>
|
||||||
<ScrollList
|
<ScrollList
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import SButton from "@/components/SButton.vue";
|
import SButton from "@/components/SButton.vue";
|
||||||
import { payOrderAPI, cancelOrderListAPI, getHomeData } from "@/apis";
|
import { payOrderAPI, cancelOrderListAPI, getHomeData } from "@/apis";
|
||||||
@@ -30,7 +30,7 @@ onMounted(() => {
|
|||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.$off("socket-inbox", onReceiveMessage);
|
uni.$off("socket-inbox", onReceiveMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { onShow } from "@dcloudio/uni-app";
|
import { onShow } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import EditOption from "@/components/EditOption.vue";
|
import EditOption from "@/components/EditOption.vue";
|
||||||
import SButton from "@/components/SButton.vue";
|
import SButton from "@/components/SButton.vue";
|
||||||
import { getPointBookDataAPI } from "@/apis";
|
|
||||||
|
|
||||||
const expandIndex = ref(0);
|
const expandIndex = ref(0);
|
||||||
const bowType = ref({});
|
const bowType = ref("");
|
||||||
const distance = ref(0);
|
const distance = ref(0);
|
||||||
const bowtargetType = ref("");
|
const bowtargetType = ref("");
|
||||||
const amountGroup = ref("");
|
const amountGroup = ref("");
|
||||||
@@ -43,7 +42,7 @@ const toEditPage = () => {
|
|||||||
bowtargetType.value &&
|
bowtargetType.value &&
|
||||||
amountGroup.value
|
amountGroup.value
|
||||||
) {
|
) {
|
||||||
uni.setStorageSync("point-book", {
|
uni.setStorageSync("last-point-book", {
|
||||||
bowType: bowType.value,
|
bowType: bowType.value,
|
||||||
distance: distance.value,
|
distance: distance.value,
|
||||||
bowtargetType: bowtargetType.value,
|
bowtargetType: bowtargetType.value,
|
||||||
@@ -54,20 +53,13 @@ const toEditPage = () => {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: "请完善信息",
|
title: "Please complete the information",
|
||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
onShow(async () => {
|
|
||||||
const result = await getPointBookDataAPI();
|
|
||||||
if (result) {
|
|
||||||
days.value = result.total_day || 0;
|
|
||||||
arrows.value = result.total_arrow || 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const pointBook = uni.getStorageSync("point-book");
|
const pointBook = uni.getStorageSync("last-point-book");
|
||||||
if (pointBook) {
|
if (pointBook) {
|
||||||
bowType.value = pointBook.bowType;
|
bowType.value = pointBook.bowType;
|
||||||
distance.value = pointBook.distance;
|
distance.value = pointBook.distance;
|
||||||
@@ -82,29 +74,9 @@ onMounted(async () => {
|
|||||||
:bgType="2"
|
:bgType="2"
|
||||||
bgColor="#F5F5F5"
|
bgColor="#F5F5F5"
|
||||||
:whiteBackArrow="false"
|
:whiteBackArrow="false"
|
||||||
title="计分与技术分析"
|
title="选择参数"
|
||||||
>
|
>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<view class="header">
|
|
||||||
<image
|
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-08-06/dbv8w5ak76hozbfpy2.png"
|
|
||||||
mode="widthFix"
|
|
||||||
/>
|
|
||||||
<view>
|
|
||||||
<view>
|
|
||||||
<text>{{ days }}</text>
|
|
||||||
<text>天</text>
|
|
||||||
</view>
|
|
||||||
<text>训练天数</text>
|
|
||||||
</view>
|
|
||||||
<view>
|
|
||||||
<view>
|
|
||||||
<text>{{ arrows }}</text>
|
|
||||||
<text>箭</text>
|
|
||||||
</view>
|
|
||||||
<text>训练箭数</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view>
|
<view>
|
||||||
<EditOption
|
<EditOption
|
||||||
:itemIndex="0"
|
:itemIndex="0"
|
||||||
@@ -136,11 +108,7 @@ onMounted(async () => {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view :style="{ marginBottom: '20px' }">
|
<view :style="{ marginBottom: '20px' }">
|
||||||
<SButton :rounded="50" :onClick="toEditPage">开始计分</SButton>
|
<SButton :rounded="50" :onClick="toEditPage">Next</SButton>
|
||||||
<view class="see-more" @click="toListPage">
|
|
||||||
<text>历史计分记录</text>
|
|
||||||
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
434
src/pages/point-book-detail-share.vue
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
|
import SButton from "@/components/SButton.vue";
|
||||||
|
import BowTargetEdit from "@/components/BowTargetEdit.vue";
|
||||||
|
import ScreenHint2 from "@/components/ScreenHint2.vue";
|
||||||
|
import RingBarChart from "@/components/RingBarChart.vue";
|
||||||
|
import { getPointBookDetailAPI, getPointBookConfigAPI } from "@/apis";
|
||||||
|
|
||||||
|
const selectedIndex = ref(0);
|
||||||
|
const showTip = ref(false);
|
||||||
|
const showTip2 = ref(false);
|
||||||
|
const data = ref({});
|
||||||
|
const targetId = ref(0);
|
||||||
|
const targetSrc = ref("");
|
||||||
|
const arrows = ref([]);
|
||||||
|
const record = ref({
|
||||||
|
groups: [],
|
||||||
|
user: {},
|
||||||
|
});
|
||||||
|
const bowConfig = ref({});
|
||||||
|
|
||||||
|
const paddingTop = computed(() => {
|
||||||
|
const menuBtnInfo = uni.getMenuButtonBoundingClientRect();
|
||||||
|
return menuBtnInfo.top - 9 - 9;
|
||||||
|
});
|
||||||
|
|
||||||
|
const openTip = (index) => {
|
||||||
|
if (index === 1) showTip.value = true;
|
||||||
|
else if (index === 2) showTip2.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeTip = () => {
|
||||||
|
showTip.value = false;
|
||||||
|
showTip2.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
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 goHome = () => {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: "/pages/point-book",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const ringRates = computed(() => {
|
||||||
|
const rates = new Array(12).fill(0);
|
||||||
|
arrows.value.forEach((item) => {
|
||||||
|
if (item.ring === -1) rates[11] += 1;
|
||||||
|
else rates[item.ring] += 1;
|
||||||
|
});
|
||||||
|
return rates.map((r) => r / arrows.value.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
onLoad(async (options) => {
|
||||||
|
if (options.id) {
|
||||||
|
const result = await getPointBookDetailAPI(options.id || 183);
|
||||||
|
record.value = result;
|
||||||
|
const config = await getPointBookConfigAPI();
|
||||||
|
bowConfig.value = config;
|
||||||
|
config.targetOption.some((item) => {
|
||||||
|
if (item.id === result.targetType) {
|
||||||
|
targetId.value = item.id;
|
||||||
|
targetSrc.value = item.icon;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (result.groups) {
|
||||||
|
data.value = result.groups[0];
|
||||||
|
arrows.value = result.groups[0].list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const bowOptionName = computed(() => {
|
||||||
|
if (bowConfig.value.bowOption && record.value.bowType) {
|
||||||
|
const data = bowConfig.value.bowOption.find(
|
||||||
|
(b) => b.id === record.value.bowType
|
||||||
|
);
|
||||||
|
if (data) return data.name || "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetTypeName = computed(() => {
|
||||||
|
if (bowConfig.value.targetOption && record.value.targetType) {
|
||||||
|
const data = bowConfig.value.targetOption.find(
|
||||||
|
(b) => b.id === record.value.targetType
|
||||||
|
);
|
||||||
|
if (data) return data.name || "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="container" :style="{ paddingTop: paddingTop + 'px' }">
|
||||||
|
<image
|
||||||
|
src="../static/app-bg5.png"
|
||||||
|
class="bg-image"
|
||||||
|
mode="aspectFill"
|
||||||
|
:style="{ height: paddingTop + 60 + 'px' }"
|
||||||
|
/>
|
||||||
|
<view class="header">
|
||||||
|
<image
|
||||||
|
:src="record.user.avatar || '../static/user-icon.png'"
|
||||||
|
mode="widthFix"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
|
<view>
|
||||||
|
<text>{{ record.user.name }}</text>
|
||||||
|
<view class="point-book-info">
|
||||||
|
<text v-if="bowOptionName">{{ bowOptionName }}</text>
|
||||||
|
<text>{{ record.distance }} 米</text>
|
||||||
|
<text v-if="targetTypeName">{{ targetTypeName }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="detail-data">
|
||||||
|
<view>
|
||||||
|
<view
|
||||||
|
:style="{ display: 'flex', alignItems: 'center' }"
|
||||||
|
@click="() => openTip(1)"
|
||||||
|
>
|
||||||
|
<text>落点稳定性</text>
|
||||||
|
<image
|
||||||
|
src="../static/s-question-mark.png"
|
||||||
|
mode="widthFix"
|
||||||
|
class="question-mark"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<text>{{ Number((data.stability || 0).toFixed(2)) }}</text>
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<view>黄心率</view>
|
||||||
|
<text>{{ Number((data.yellowRate * 100).toFixed(2)) }}%</text>
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<view>10环数</view>
|
||||||
|
<text>{{ data.tenRings }}</text>
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<view>平均环数</view>
|
||||||
|
<text>{{ Number((data.averageRing || 0).toFixed(2)) }}</text>
|
||||||
|
</view>
|
||||||
|
<view>
|
||||||
|
<view>总环数</view>
|
||||||
|
<text>{{ data.userTotalRing }}/{{ data.totalRing }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="title-bar">
|
||||||
|
<view />
|
||||||
|
<text>落点分布</text>
|
||||||
|
<!-- <button hover-class="none" @click="() => openTip(2)">
|
||||||
|
<image
|
||||||
|
src="../static/s-question-mark.png"
|
||||||
|
mode="widthFix"
|
||||||
|
class="question-mark"
|
||||||
|
/>
|
||||||
|
</button> -->
|
||||||
|
</view>
|
||||||
|
<view :style="{ transform: 'translateY(-45rpx)' }">
|
||||||
|
<BowTargetEdit
|
||||||
|
:id="targetId"
|
||||||
|
:src="targetSrc"
|
||||||
|
:arrows="arrows.filter((item) => item.x && item.y)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view :style="{ transform: 'translateY(-60rpx)' }">
|
||||||
|
<!-- <view class="title-bar">
|
||||||
|
<view />
|
||||||
|
<text>环值分布</text>
|
||||||
|
</view> -->
|
||||||
|
<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.list.reduce((acc, cur) => acc + cur.ring, 0) }}</text>
|
||||||
|
<text>环</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="
|
||||||
|
(selectedIndex === 0 && index !== 0) ||
|
||||||
|
(selectedIndex !== 0 && index === selectedIndex)
|
||||||
|
"
|
||||||
|
:style="{
|
||||||
|
marginLeft: selectedIndex === 0 && index !== 0 ? '20rpx' : '0',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
v-for="(arrow, index2) in item.list"
|
||||||
|
:key="index2"
|
||||||
|
:style="{
|
||||||
|
color:
|
||||||
|
arrow.ring === 0 || arrow.ring === 10 ? '#FFA118' : '#666',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
arrow.ring === 0 ? "X" : arrow.ring === -1 ? "M" : arrow.ring
|
||||||
|
}}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<SButton :onClick="goHome" :rounded="40">开启我的弓箭记录</SButton>
|
||||||
|
</view>
|
||||||
|
<ScreenHint2 :show="showTip || showTip2" :onClose="closeTip">
|
||||||
|
<view class="tip-content">
|
||||||
|
<block v-if="showTip">
|
||||||
|
<text>落点稳定性说明</text>
|
||||||
|
<text
|
||||||
|
>通过计算每支箭与其他箭的平均距离衡量射箭的稳定性,数字越小则说明射箭越稳定。该数据只能在用户标记落点的情况下生成。</text
|
||||||
|
>
|
||||||
|
</block>
|
||||||
|
<block v-if="showTip2">
|
||||||
|
<text>落点分布说明</text>
|
||||||
|
<text>展示用户某次练习中射箭的点位</text>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
</ScreenHint2>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.header > image {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 90rpx;
|
||||||
|
height: 90rpx;
|
||||||
|
border: 2rpx solid #000;
|
||||||
|
margin: 0 25rpx;
|
||||||
|
}
|
||||||
|
.header > view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.header > view > text {
|
||||||
|
color: #000;
|
||||||
|
margin-bottom: 7rpx;
|
||||||
|
}
|
||||||
|
.bg-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.detail-data {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
column-gap: 3vw;
|
||||||
|
margin: 10rpx 30rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
.detail-data > view,
|
||||||
|
.detail-data > button {
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding: 15rpx 24rpx;
|
||||||
|
}
|
||||||
|
.detail-data > view > view {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
.detail-data > view > view > text {
|
||||||
|
word-break: keep-all;
|
||||||
|
}
|
||||||
|
.detail-data > view > text {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.detail-data > button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
.detail-data > button > image {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
}
|
||||||
|
.question-mark {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
.title-bar {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.title-bar > view:first-child {
|
||||||
|
width: 8rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #fed847;
|
||||||
|
margin-right: 7px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
.title-bar > button {
|
||||||
|
height: 34rpx;
|
||||||
|
}
|
||||||
|
.tip-content {
|
||||||
|
width: 100%;
|
||||||
|
padding: 50rpx 44rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.tip-content > text {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.tip-content > text:first-child {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.tip-content > text:last-child {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 20px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.ring-text-groups {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20rpx;
|
||||||
|
padding-top: 40rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
.ring-text-groups > view {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.ring-text-groups > view > view:first-child:nth-last-child(2) {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
margin-left: 30rpx;
|
||||||
|
width: 90rpx;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-size: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.ring-text-groups > view > view:first-child:nth-last-child(2) > text {
|
||||||
|
line-height: 30rpx;
|
||||||
|
}
|
||||||
|
.ring-text-groups
|
||||||
|
> view
|
||||||
|
> view:first-child:nth-last-child(2)
|
||||||
|
> text:nth-child(2) {
|
||||||
|
font-size: 40rpx;
|
||||||
|
/* min-width: 45rpx; */
|
||||||
|
color: #666;
|
||||||
|
margin-right: 6rpx;
|
||||||
|
margin-top: -5rpx;
|
||||||
|
}
|
||||||
|
.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%;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
.notes-input {
|
||||||
|
width: calc(100% - 40rpx);
|
||||||
|
margin: 25rpx 0;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #000;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
.point-book-info {
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.point-book-info > text {
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,63 +1,153 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, computed } from "vue";
|
||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onLoad, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import BowTargetEdit from "@/components/BowTargetEdit.vue";
|
import BowTargetEdit from "@/components/BowTargetEdit.vue";
|
||||||
import ScreenHint2 from "@/components/ScreenHint2.vue";
|
import ScreenHint2 from "@/components/ScreenHint2.vue";
|
||||||
import SButton from "@/components/SButton.vue";
|
import RingBarChart from "@/components/RingBarChart.vue";
|
||||||
|
|
||||||
import { getPointBookDetailAPI } from "@/apis";
|
import { getPointBookDetailAPI, addNoteAPI } from "@/apis";
|
||||||
|
import { wxShare, generateShareCard, generateShareImage } from "@/util";
|
||||||
|
|
||||||
|
import useStore from "@/store";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
const store = useStore();
|
||||||
|
const { user, device } = storeToRefs(store);
|
||||||
|
|
||||||
const selectedIndex = ref(0);
|
const selectedIndex = ref(0);
|
||||||
const showTip = ref(false);
|
const showTip = ref(false);
|
||||||
const showTip2 = ref(false);
|
const showTip2 = ref(false);
|
||||||
const groups = ref([]);
|
const showTip3 = ref(false);
|
||||||
const data = ref({});
|
const data = ref({});
|
||||||
|
const targetId = ref(0);
|
||||||
const targetSrc = ref("");
|
const targetSrc = ref("");
|
||||||
const arrows = ref([]);
|
const arrows = ref([]);
|
||||||
|
const notes = ref("");
|
||||||
|
const draftNotes = ref("");
|
||||||
|
const record = ref({
|
||||||
|
groups: [],
|
||||||
|
user: {},
|
||||||
|
});
|
||||||
|
const shareType = ref(1);
|
||||||
|
|
||||||
const openTip = (index) => {
|
const openTip = (index) => {
|
||||||
if (index === 1) showTip.value = true;
|
if (index === 1) showTip.value = true;
|
||||||
else if (index === 2) showTip2.value = true;
|
else if (index === 2) showTip2.value = true;
|
||||||
|
else if (index === 3) showTip3.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeTip = () => {
|
const closeTip = () => {
|
||||||
showTip.value = false;
|
showTip.value = false;
|
||||||
showTip2.value = false;
|
showTip2.value = false;
|
||||||
|
showTip3.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveNote = async () => {
|
||||||
|
notes.value = draftNotes.value;
|
||||||
|
draftNotes.value = "";
|
||||||
|
showTip3.value = false;
|
||||||
|
if (record.value.id) {
|
||||||
|
await addNoteAPI(record.value.id, notes.value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = (index) => {
|
const onSelect = (index) => {
|
||||||
selectedIndex.value = index;
|
selectedIndex.value = index;
|
||||||
data.value = groups.value[index];
|
data.value = record.value.groups[index];
|
||||||
arrows.value = groups.value[index].list.filter((item) => item.x && item.y);
|
arrows.value = record.value.groups[index].list.filter(
|
||||||
|
(item) => item.x && item.y
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
uni.navigateBack();
|
const pages = getCurrentPages();
|
||||||
|
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(() => {
|
||||||
|
const rates = new Array(12).fill(0);
|
||||||
|
arrows.value.forEach((item) => {
|
||||||
|
if (item.ring === -1) rates[11] += 1;
|
||||||
|
else rates[item.ring] += 1;
|
||||||
|
});
|
||||||
|
return rates.map((r) => r / arrows.value.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const shareImage = async () => {
|
||||||
|
if (loading.value) return;
|
||||||
|
loading.value = true;
|
||||||
|
await generateShareImage("shareImageCanvas");
|
||||||
|
await wxShare("shareImageCanvas");
|
||||||
|
loading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
onLoad(async (options) => {
|
onLoad(async (options) => {
|
||||||
if (options.id) {
|
if (options.id) {
|
||||||
const result = await getPointBookDetailAPI(options.id);
|
const result = await getPointBookDetailAPI(options.id || 209);
|
||||||
|
record.value = result;
|
||||||
|
notes.value = result.remark || "";
|
||||||
const config = uni.getStorageSync("point-book-config");
|
const config = uni.getStorageSync("point-book-config");
|
||||||
config.targetOption.some((item) => {
|
config.targetOption.some((item) => {
|
||||||
if (item.id === result.targetType) {
|
if (item.id === result.targetType) {
|
||||||
|
targetId.value = item.id;
|
||||||
targetSrc.value = item.icon;
|
targetSrc.value = item.icon;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (result.groups) {
|
if (result.groups) {
|
||||||
groups.value = result.groups;
|
|
||||||
data.value = result.groups[0];
|
data.value = result.groups[0];
|
||||||
arrows.value = result.groups[0].list.filter((item) => item.x && item.y);
|
arrows.value = result.groups[0].list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onShareAppMessage(async () => {
|
||||||
|
const imageUrl = await generateShareCard(
|
||||||
|
"shareCardCanvas",
|
||||||
|
record.value.recordDate,
|
||||||
|
data.value.userTotalRing,
|
||||||
|
data.value.totalRing
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
title: "射箭打卡,今日又精进了一些~",
|
||||||
|
path: "/pages/point-book-detail-share?id=" + record.value.id,
|
||||||
|
imageUrl,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
onShareTimeline(async () => {
|
||||||
|
const imageUrl = await generateShareCard(
|
||||||
|
"shareCardCanvas",
|
||||||
|
record.value.recordDate,
|
||||||
|
data.value.userTotalRing,
|
||||||
|
data.value.totalRing
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
title: "射箭打卡,今日又精进了一些~",
|
||||||
|
query: "id=" + record.value.id,
|
||||||
|
imageUrl,
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container :bgType="2" bgColor="#F5F5F5" :whiteBackArrow="false" title="分析">
|
<Container
|
||||||
|
:bgType="2"
|
||||||
|
bgColor="#F5F5F5"
|
||||||
|
:whiteBackArrow="false"
|
||||||
|
title=""
|
||||||
|
:onBack="goBack"
|
||||||
|
>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<view class="tab-bar">
|
<!-- <view class="tab-bar">
|
||||||
<view
|
<view
|
||||||
v-for="(_, index) in groups"
|
v-for="(_, index) in groups"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -72,20 +162,25 @@ onLoad(async (options) => {
|
|||||||
}"
|
}"
|
||||||
>{{ index === 0 ? "全部" : `第${index}组` }}</text
|
>{{ index === 0 ? "全部" : `第${index}组` }}</text
|
||||||
>
|
>
|
||||||
<!-- <image
|
|
||||||
src="../static/s-triangle.png"
|
|
||||||
mode="widthFix"
|
|
||||||
:style="{ bottom: selectedIndex !== index ? '0' : '-5px' }"
|
|
||||||
/> -->
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
</view> -->
|
||||||
|
<canvas
|
||||||
|
class="share-canvas"
|
||||||
|
canvas-id="shareCardCanvas"
|
||||||
|
style="width: 375px; height: 300px"
|
||||||
|
></canvas>
|
||||||
|
<canvas
|
||||||
|
class="share-canvas"
|
||||||
|
canvas-id="shareImageCanvas"
|
||||||
|
style="width: 375px; height: 860px"
|
||||||
|
></canvas>
|
||||||
<view class="detail-data">
|
<view class="detail-data">
|
||||||
<view>
|
<view>
|
||||||
<view
|
<view
|
||||||
:style="{ display: 'flex', alignItems: 'center' }"
|
:style="{ display: 'flex', alignItems: 'center' }"
|
||||||
@click="() => openTip(1)"
|
@click="() => openTip(1)"
|
||||||
>
|
>
|
||||||
<text>落点稳定性</text>
|
<text>Stability</text>
|
||||||
<image
|
<image
|
||||||
src="../static/s-question-mark.png"
|
src="../static/s-question-mark.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
@@ -95,48 +190,134 @@ onLoad(async (options) => {
|
|||||||
<text>{{ Number((data.stability || 0).toFixed(2)) }}</text>
|
<text>{{ Number((data.stability || 0).toFixed(2)) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<view>黄心率</view>
|
<view>Yellow Rate</view>
|
||||||
<text>{{ Number((data.yellowRate * 100).toFixed(2)) }}%</text>
|
<text>{{ Number((data.yellowRate * 100).toFixed(2)) }}%</text>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<view>10环数</view>
|
<view>Gold Rings</view>
|
||||||
<text>{{ data.tenRings }}</text>
|
<text>{{ data.tenRings }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<view>平均环数</view>
|
<view>Avg Rings</view>
|
||||||
<text>{{ Number((data.averageRing || 0).toFixed(2)) }}</text>
|
<text>{{ Number((data.averageRing || 0).toFixed(2)) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view>
|
<view>
|
||||||
<view>总环数</view>
|
<view>Total Rings</view>
|
||||||
<text>{{ data.userTotalRing }}/{{ data.totalRing }}</text>
|
<text>{{ data.userTotalRing }}/{{ data.totalRing }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
<button
|
||||||
|
hover-class="none"
|
||||||
|
@click="() => openTip(3)"
|
||||||
|
v-if="user.id === record.user.id"
|
||||||
|
>
|
||||||
|
<image src="../static/edit.png" mode="widthFix" />
|
||||||
|
<text>Notes</text>
|
||||||
|
</button>
|
||||||
</view>
|
</view>
|
||||||
<view class="title-bar">
|
<view class="title-bar">
|
||||||
<view />
|
<view />
|
||||||
<text>落点分布</text>
|
<text>Distribution</text>
|
||||||
<button hover-class="none" @click="() => openTip(2)">
|
<!-- <button hover-class="none" @click="() => openTip(2)">
|
||||||
<image
|
<image
|
||||||
src="../static/s-question-mark.png"
|
src="../static/s-question-mark.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
class="question-mark"
|
class="question-mark"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button> -->
|
||||||
</view>
|
</view>
|
||||||
<BowTargetEdit :src="targetSrc" :arrows="arrows" />
|
<view :style="{ transform: 'translateY(-45rpx)' }">
|
||||||
<view :style="{ marginTop: '20px' }">
|
<BowTargetEdit
|
||||||
<SButton :onClick="goBack" :rounded="50">关闭</SButton>
|
:id="targetId"
|
||||||
|
:src="targetSrc"
|
||||||
|
:arrows="arrows.filter((item) => item.x && item.y)"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
<ScreenHint2 :show="showTip || showTip2" :onClose="closeTip">
|
<view :style="{ transform: 'translateY(-60rpx)' }">
|
||||||
|
<view :style="{ padding: '0 30rpx' }">
|
||||||
|
<RingBarChart :data="ringRates" />
|
||||||
|
</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>Ring</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="
|
||||||
|
(selectedIndex === 0 && index !== 0) ||
|
||||||
|
(selectedIndex !== 0 && index === selectedIndex)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
v-for="(arrow, index2) in item.list"
|
||||||
|
:key="index2"
|
||||||
|
:style="{
|
||||||
|
color:
|
||||||
|
arrow.ring === 0 || arrow.ring === 10 ? '#FFA118' : '#666',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
arrow.ring === 0 ? "X" : arrow.ring === -1 ? "M" : arrow.ring
|
||||||
|
}}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="btns"
|
||||||
|
:style="{
|
||||||
|
gridTemplateColumns: `repeat(${
|
||||||
|
user.id === record.user.id ? 1 : 1
|
||||||
|
}, 1fr)`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<button hover-class="none" @click="goBack">Close</button>
|
||||||
|
<!-- <button
|
||||||
|
hover-class="none"
|
||||||
|
@click="shareImage"
|
||||||
|
v-if="user.id === record.user.id"
|
||||||
|
>
|
||||||
|
分享
|
||||||
|
</button> -->
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<ScreenHint2
|
||||||
|
:show="showTip || showTip2 || showTip3"
|
||||||
|
:onClose="!notes && showTip3 ? null : closeTip"
|
||||||
|
>
|
||||||
<view class="tip-content">
|
<view class="tip-content">
|
||||||
<block v-if="showTip">
|
<block v-if="showTip">
|
||||||
<text>落点稳定性说明</text>
|
<text>Stability Description</text>
|
||||||
<text
|
<text
|
||||||
>通过计算每支箭与其他箭的平均距离衡一量射击的稳定性,数字越小则说明射击越稳定。该数据只能在用户标记落点的情况下生成。</text
|
>The stability of archery is measured by calculating the average
|
||||||
|
distance of each arrow to other arrows. The smaller the number,
|
||||||
|
the more stable the archery. This data can only be generated when
|
||||||
|
the user marks the landing point.</text
|
||||||
>
|
>
|
||||||
</block>
|
</block>
|
||||||
<block v-if="showTip2">
|
<block v-if="showTip2">
|
||||||
<text>落点分布说明</text>
|
<text>Distribution Description</text>
|
||||||
<text>展示用户某次练习中射击的点位</text>
|
<text>Show the user's archery points in a practice session</text>
|
||||||
|
</block>
|
||||||
|
<block v-if="showTip3">
|
||||||
|
<text>Notes</text>
|
||||||
|
<text v-if="notes">{{ notes }}</text>
|
||||||
|
<textarea
|
||||||
|
v-if="!notes"
|
||||||
|
v-model="draftNotes"
|
||||||
|
maxlength="300"
|
||||||
|
rows="4"
|
||||||
|
class="notes-input"
|
||||||
|
placeholder="写下本次射箭的补充信息与心得"
|
||||||
|
placeholder-style="color: #ccc;"
|
||||||
|
/>
|
||||||
|
<view v-if="!notes">
|
||||||
|
<button hover-class="none" @click="showTip3 = false">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button hover-class="none" @click="saveNote">Save Notes</button>
|
||||||
|
</view>
|
||||||
</block>
|
</block>
|
||||||
</view>
|
</view>
|
||||||
</ScreenHint2>
|
</ScreenHint2>
|
||||||
@@ -166,8 +347,7 @@ onLoad(async (options) => {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
width: 24vw;
|
width: 24vw;
|
||||||
height: 13vw;
|
height: 80rpx;
|
||||||
line-height: 13vw;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@@ -176,6 +356,7 @@ onLoad(async (options) => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.tab-bar > view > text {
|
.tab-bar > view > text {
|
||||||
|
line-height: 80rpx;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
.tab-bar > view > image {
|
.tab-bar > view > image {
|
||||||
@@ -189,25 +370,43 @@ onLoad(async (options) => {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
column-gap: 3vw;
|
column-gap: 3vw;
|
||||||
margin: 10px 15px;
|
margin: 10rpx 30rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
}
|
}
|
||||||
.detail-data > view {
|
.detail-data > view,
|
||||||
|
.detail-data > button {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 20rpx;
|
||||||
padding: 12px;
|
padding: 15rpx 24rpx;
|
||||||
}
|
}
|
||||||
.detail-data > view > view {
|
.detail-data > view > view {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #999;
|
color: #999;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
.detail-data > view > view > text {
|
||||||
|
word-break: keep-all;
|
||||||
}
|
}
|
||||||
.detail-data > view > text {
|
.detail-data > view > text {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.detail-data > button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
.detail-data > button > image {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
margin-left: 20rpx;
|
||||||
}
|
}
|
||||||
.question-mark {
|
.question-mark {
|
||||||
width: 15px;
|
width: 28rpx;
|
||||||
height: 15px;
|
height: 28rpx;
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
}
|
}
|
||||||
.title-bar {
|
.title-bar {
|
||||||
@@ -216,23 +415,27 @@ onLoad(async (options) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
.title-bar > view:first-child {
|
.title-bar > view:first-child {
|
||||||
width: 5px;
|
width: 8rpx;
|
||||||
height: 15px;
|
height: 28rpx;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: #fed847;
|
background-color: #fed847;
|
||||||
margin-right: 7px;
|
margin-right: 7px;
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
.title-bar > text {
|
.title-bar > button {
|
||||||
margin-bottom: 2px;
|
height: 34rpx;
|
||||||
}
|
}
|
||||||
.tip-content {
|
.tip-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 25px;
|
padding: 50rpx 44rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
color: #000;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.tip-content > text {
|
.tip-content > text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -245,4 +448,113 @@ onLoad(async (options) => {
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
.tip-content > view {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.tip-content > view > input {
|
||||||
|
width: 80%;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 22px;
|
||||||
|
border: 1px solid #eeeeee;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.tip-content > view > button {
|
||||||
|
width: 48%;
|
||||||
|
background: linear-gradient(180deg, #fbfbfb 0%, #f5f5f5 100%);
|
||||||
|
border-radius: 22px;
|
||||||
|
border: 1px solid #eeeeee;
|
||||||
|
padding: 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.tip-content > view > button:last-child {
|
||||||
|
background: #fed847;
|
||||||
|
}
|
||||||
|
.ring-text-groups {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20rpx;
|
||||||
|
padding-top: 40rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
.ring-text-groups > view {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.ring-text-groups > view > view:first-child:nth-last-child(2) {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
margin-left: 30rpx;
|
||||||
|
width: 90rpx;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-size: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.ring-text-groups > view > view:first-child:nth-last-child(2) > text {
|
||||||
|
line-height: 30rpx;
|
||||||
|
}
|
||||||
|
.ring-text-groups
|
||||||
|
> view
|
||||||
|
> view:first-child:nth-last-child(2)
|
||||||
|
> text:nth-child(2) {
|
||||||
|
font-size: 40rpx;
|
||||||
|
/* min-width: 45rpx; */
|
||||||
|
color: #666;
|
||||||
|
margin-right: 6rpx;
|
||||||
|
margin-top: -5rpx;
|
||||||
|
}
|
||||||
|
.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%;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
.notes-input {
|
||||||
|
width: calc(100% - 40rpx);
|
||||||
|
min-width: calc(100% - 40rpx);
|
||||||
|
margin: 25rpx 0;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
color: #000;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
.btns {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
column-gap: 20rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
}
|
||||||
|
.btns > button {
|
||||||
|
height: 84rpx;
|
||||||
|
line-height: 84rpx;
|
||||||
|
background: linear-gradient(180deg, #fbfbfb 0%, #f5f5f5 100%), #ffffff;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
border: 2rpx solid #eeeeee;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.btns > button:nth-child(2) {
|
||||||
|
background: #fed847;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const onSubmit = async () => {
|
|||||||
);
|
);
|
||||||
if (!isComplete) {
|
if (!isComplete) {
|
||||||
return uni.showToast({
|
return uni.showToast({
|
||||||
title: "请完善信息",
|
title: "Please complete the information",
|
||||||
icon: "none",
|
icon: "none",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ const onSubmit = async () => {
|
|||||||
currentGroup.value++;
|
currentGroup.value++;
|
||||||
currentArrow.value = 0;
|
currentArrow.value = 0;
|
||||||
} else {
|
} else {
|
||||||
const pointBook = uni.getStorageSync("point-book");
|
const pointBook = uni.getStorageSync("last-point-book");
|
||||||
const res = await savePointBookAPI(
|
const res = await savePointBookAPI(
|
||||||
pointBook.bowType.id,
|
pointBook.bowType.id,
|
||||||
pointBook.distance,
|
pointBook.distance,
|
||||||
@@ -75,7 +75,7 @@ const onEditDone = (arrow) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const pointBook = uni.getStorageSync("point-book");
|
const pointBook = uni.getStorageSync("last-point-book");
|
||||||
if (pointBook.bowtargetType) {
|
if (pointBook.bowtargetType) {
|
||||||
bowtarget.value = pointBook.bowtargetType;
|
bowtarget.value = pointBook.bowtargetType;
|
||||||
if (bowtarget.value.id > 3) {
|
if (bowtarget.value.id > 3) {
|
||||||
@@ -112,11 +112,11 @@ onMounted(() => {
|
|||||||
<view class="title-bar">
|
<view class="title-bar">
|
||||||
<view>
|
<view>
|
||||||
<view />
|
<view />
|
||||||
<text>第 {{ currentGroup }} 组</text>
|
<text>Set {{ currentGroup }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view @click="deleteArrow">
|
<view @click="deleteArrow">
|
||||||
<image src="../static/delete.png" />
|
<image src="../static/delete.png" />
|
||||||
<text>删除</text>
|
<text>Delete</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="bow-arrows">
|
<view class="bow-arrows">
|
||||||
@@ -133,12 +133,15 @@ onMounted(() => {
|
|||||||
isNaN(arrow.ring)
|
isNaN(arrow.ring)
|
||||||
? arrow.ring
|
? arrow.ring
|
||||||
: arrow.ring
|
: arrow.ring
|
||||||
? arrow.ring + " 环"
|
? arrow.ring + " points"
|
||||||
: ""
|
: ""
|
||||||
}}</view
|
}}</view
|
||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
<text>推荐在靶纸上落点计分,这样可获得稳定性分析</text>
|
<text
|
||||||
|
>It is recommended to score on the target face to obtain stability
|
||||||
|
analysis</text
|
||||||
|
>
|
||||||
<view class="bow-rings">
|
<view class="bow-rings">
|
||||||
<view
|
<view
|
||||||
v-for="(item, index) in ringTypes"
|
v-for="(item, index) in ringTypes"
|
||||||
@@ -155,12 +158,12 @@ onMounted(() => {
|
|||||||
</view>
|
</view>
|
||||||
<ScreenHint2 :show="showTip">
|
<ScreenHint2 :show="showTip">
|
||||||
<view class="tip-content">
|
<view class="tip-content">
|
||||||
<text>现在离开会导致</text>
|
<text>Leaving now will result in the loss of unsaved data.</text>
|
||||||
<text>未提交的数据丢失,是否继续?</text>
|
<text>Are you sure you want to continue?</text>
|
||||||
<view>
|
<view>
|
||||||
<button hover-class="none" @click="onBack">退出</button>
|
<button hover-class="none" @click="onBack">Exit</button>
|
||||||
<button hover-class="none" @click="showTip = false">
|
<button hover-class="none" @click="showTip = false">
|
||||||
继续记录
|
Continue
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -168,7 +171,7 @@ onMounted(() => {
|
|||||||
</view>
|
</view>
|
||||||
<view :style="{ marginBottom: '20px' }">
|
<view :style="{ marginBottom: '20px' }">
|
||||||
<SButton :rounded="50" :onClick="onSubmit">
|
<SButton :rounded="50" :onClick="onSubmit">
|
||||||
{{ currentGroup === groups ? "记完了,提交看分析" : "下一组" }}
|
{{ currentGroup === groups ? "Submit for analysis" : "Next set" }}
|
||||||
</SButton>
|
</SButton>
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
</Container>
|
||||||
@@ -218,6 +221,7 @@ onMounted(() => {
|
|||||||
padding: 25px;
|
padding: 25px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.tip-content > text {
|
.tip-content > text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -237,6 +241,7 @@ onMounted(() => {
|
|||||||
border: 1px solid #eeeeee;
|
border: 1px solid #eeeeee;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.tip-content > view > button:last-child {
|
.tip-content > view > button:last-child {
|
||||||
background: #fed847;
|
background: #fed847;
|
||||||
|
|||||||
381
src/pages/point-book-home.vue
Normal 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>
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import SModal from "@/components/SModal.vue";
|
import SModal from "@/components/SModal.vue";
|
||||||
import EditOption from "@/components/EditOption.vue";
|
import EditOption from "@/components/EditOption.vue";
|
||||||
import PointRecord from "@/components/PointRecord.vue";
|
import PointRecord from "@/components/PointRecord.vue";
|
||||||
import ScrollList from "@/components/ScrollList.vue";
|
import ScrollList from "@/components/ScrollList.vue";
|
||||||
import { getPointBookListAPI } from "@/apis";
|
import ScreenHint2 from "@/components/ScreenHint2.vue";
|
||||||
|
import { getPointBookListAPI, removePointRecord } from "@/apis";
|
||||||
|
|
||||||
|
const showTip = ref(false);
|
||||||
const bowType = ref({});
|
const bowType = ref({});
|
||||||
const distance = ref(0);
|
const distance = ref(0);
|
||||||
const bowtargetType = ref({});
|
const bowtargetType = ref({});
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
const selectorIndex = ref(0);
|
const selectorIndex = ref(0);
|
||||||
const list = ref([]);
|
const list = ref([]);
|
||||||
|
const removeId = ref("");
|
||||||
|
|
||||||
const onListLoading = async (page) => {
|
const onListLoading = async (page) => {
|
||||||
const result = await getPointBookListAPI(
|
const result = await getPointBookListAPI(
|
||||||
@@ -34,6 +37,22 @@ const openSelector = (index) => {
|
|||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRemoveRecord = (item) => {
|
||||||
|
removeId.value = item.id;
|
||||||
|
showTip.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmRemove = async () => {
|
||||||
|
try {
|
||||||
|
showTip.value = false;
|
||||||
|
await removePointRecord(removeId.value);
|
||||||
|
list.value = list.value.filter((it) => it.id !== removeId.value);
|
||||||
|
uni.showToast({ title: "Deleted", icon: "none" });
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({ title: "Delete failed, please retry", icon: "none" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSelectOption = (itemIndex, value) => {
|
const onSelectOption = (itemIndex, value) => {
|
||||||
if (itemIndex === 0) {
|
if (itemIndex === 0) {
|
||||||
bowType.value = value.name === bowType.value.name ? {} : value;
|
bowType.value = value.name === bowType.value.name ? {} : value;
|
||||||
@@ -45,11 +64,6 @@ const onSelectOption = (itemIndex, value) => {
|
|||||||
showModal.value = false;
|
showModal.value = false;
|
||||||
onListLoading(1);
|
onListLoading(1);
|
||||||
};
|
};
|
||||||
const toDetailPage = (id) => {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/point-book-detail?id=${id}`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -57,44 +71,42 @@ const toDetailPage = (id) => {
|
|||||||
:bgType="2"
|
:bgType="2"
|
||||||
bgColor="#F5F5F5"
|
bgColor="#F5F5F5"
|
||||||
:whiteBackArrow="false"
|
:whiteBackArrow="false"
|
||||||
title="计分记录"
|
title="Point Records"
|
||||||
>
|
>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<view class="selectors">
|
<view class="selectors">
|
||||||
<view @click="() => openSelector(0)">
|
<view @click="() => openSelector(0)">
|
||||||
<text :style="{ color: bowType.name ? '#000' : '#999' }">{{
|
<text :style="{ color: bowType.name ? '#000' : '#999' }">{{
|
||||||
bowType.name || "请选择"
|
bowType.name || "Please select"
|
||||||
}}</text>
|
}}</text>
|
||||||
<image src="../static/arrow-grey.png" mode="widthFix" />
|
<image src="../static/arrow-grey.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
<view @click="() => openSelector(1)">
|
<view @click="() => openSelector(1)">
|
||||||
<text :style="{ color: distance ? '#000' : '#999' }">{{
|
<text :style="{ color: distance ? '#000' : '#999' }">{{
|
||||||
distance ? distance + " 米" : "请选择"
|
distance ? distance + " m" : "Please select"
|
||||||
}}</text>
|
}}</text>
|
||||||
<image src="../static/arrow-grey.png" mode="widthFix" />
|
<image src="../static/arrow-grey.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
<view @click="() => openSelector(2)">
|
<view @click="() => openSelector(2)">
|
||||||
<text :style="{ color: bowtargetType.name ? '#000' : '#999' }">{{
|
<text :style="{ color: bowtargetType.name ? '#000' : '#999' }">{{
|
||||||
bowtargetType.name || "请选择"
|
bowtargetType.name || "Please select"
|
||||||
}}</text>
|
}}</text>
|
||||||
<image src="../static/arrow-grey.png" mode="widthFix" />
|
<image src="../static/arrow-grey.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="point-records">
|
<view class="point-records">
|
||||||
<ScrollList :onLoading="onListLoading">
|
<ScrollList :onLoading="onListLoading">
|
||||||
<view
|
<view v-for="(item, index) in list" :key="item.id">
|
||||||
v-for="(item, index) in list"
|
<PointRecord :data="item" :onRemove="onRemoveRecord" />
|
||||||
:key="index"
|
<view v-if="index < list.length - 1" :style="{ height: '25rpx' }"></view>
|
||||||
@click="() => toDetailPage(item.id)"
|
|
||||||
>
|
|
||||||
<PointRecord :data="item" />
|
|
||||||
</view>
|
</view>
|
||||||
<view class="no-data" v-if="list.length === 0">暂无数据</view>
|
<view class="no-data" v-if="list.length === 0">No data</view>
|
||||||
</ScrollList>
|
</ScrollList>
|
||||||
</view>
|
</view>
|
||||||
<SModal
|
<SModal
|
||||||
:show="showModal"
|
:show="showModal"
|
||||||
:noBg="true"
|
:noBg="true"
|
||||||
|
height="auto"
|
||||||
:onClose="() => (showModal = false)"
|
:onClose="() => (showModal = false)"
|
||||||
>
|
>
|
||||||
<view class="selector">
|
<view class="selector">
|
||||||
@@ -127,6 +139,15 @@ const toDetailPage = (id) => {
|
|||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</SModal>
|
</SModal>
|
||||||
|
<ScreenHint2 :show="showTip">
|
||||||
|
<view class="tip-content">
|
||||||
|
<text>Are you sure to delete this record?</text>
|
||||||
|
<view>
|
||||||
|
<button hover-class="none" @click="showTip = false">Cancel</button>
|
||||||
|
<button hover-class="none" @click="confirmRemove">Confirm</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ScreenHint2>
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
</Container>
|
||||||
</template>
|
</template>
|
||||||
@@ -194,4 +215,34 @@ const toDetailPage = (id) => {
|
|||||||
color: #999999;
|
color: #999999;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
.tip-content {
|
||||||
|
width: 100%;
|
||||||
|
padding: 25px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.tip-content > text {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.tip-content > view {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.tip-content > view > button {
|
||||||
|
width: 48%;
|
||||||
|
background: linear-gradient(180deg, #fbfbfb 0%, #f5f5f5 100%);
|
||||||
|
border-radius: 22px;
|
||||||
|
border: 1px solid #eeeeee;
|
||||||
|
padding: 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.tip-content > view > button:last-child {
|
||||||
|
background: #fed847;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
201
src/pages/point-book-profile.vue
Normal 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>
|
||||||
551
src/pages/point-book.vue
Normal file
@@ -0,0 +1,551 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
|
||||||
|
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 {
|
||||||
|
getHomeData,
|
||||||
|
getPointBookConfigAPI,
|
||||||
|
getPointBookListAPI,
|
||||||
|
getPointBookStatisticsAPI,
|
||||||
|
removePointRecord,
|
||||||
|
} from "@/apis";
|
||||||
|
|
||||||
|
import { getElementRect } from "@/util";
|
||||||
|
|
||||||
|
import { generateKDEHeatmapImage } from "@/kde-heatmap";
|
||||||
|
|
||||||
|
import useStore from "@/store";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
const store = useStore();
|
||||||
|
const { updateUser } = store;
|
||||||
|
const { user } = storeToRefs(store);
|
||||||
|
|
||||||
|
const isIOS = computed(() => {
|
||||||
|
const systemInfo = uni.getDeviceInfo();
|
||||||
|
return systemInfo.osName === "ios";
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadImage = ref(false);
|
||||||
|
const showModal = ref(false);
|
||||||
|
const showTip = ref(false);
|
||||||
|
const showTip2 = ref(false);
|
||||||
|
const data = ref({
|
||||||
|
weeksCheckIn: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = ref([]);
|
||||||
|
const bowTargetSrc = ref("");
|
||||||
|
const heatMapImageSrc = ref(""); // 存储热力图图片地址
|
||||||
|
const canvasVisible = ref(false); // 控制canvas显示状态
|
||||||
|
const removeId = ref("");
|
||||||
|
|
||||||
|
const toListPage = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: "/pages/point-book-list",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSignin = () => {
|
||||||
|
showModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startScoring = () => {
|
||||||
|
if (user.value.id) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: "/pages/point-book-create",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showModal.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveRecord = (item) => {
|
||||||
|
removeId.value = item.id;
|
||||||
|
showTip2.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const rect = await getElementRect(".heat-map");
|
||||||
|
let hot = 0;
|
||||||
|
if (result2.checkInCount > -3 && result2.checkInCount < 3) hot = 1;
|
||||||
|
else if (result2.checkInCount >= 3) hot = 2;
|
||||||
|
else if (result2.checkInCount >= 5) hot = 3;
|
||||||
|
else if (result2.checkInCount === 7) hot = 4;
|
||||||
|
uni.$emit("update-hot", hot);
|
||||||
|
loadImage.value = true;
|
||||||
|
const generateHeatmapAsync = async () => {
|
||||||
|
const weekArrows = result2.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("Heatmap image path:", finalPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to generate heatmap image:", error);
|
||||||
|
loadImage.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 异步生成热力图,不阻塞UI
|
||||||
|
generateHeatmapAsync();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => user.value.id,
|
||||||
|
(id) => {
|
||||||
|
if (id) loadData();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onShow(async () => {
|
||||||
|
uni.removeStorageSync("point-book");
|
||||||
|
if (user.value.id) loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
uni.$on("point-book-signin", onSignin);
|
||||||
|
const token = uni.getStorageSync(
|
||||||
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
|
);
|
||||||
|
if (!user.value.id && token) {
|
||||||
|
const data = await getHomeData();
|
||||||
|
if (data.user) updateUser(data.user);
|
||||||
|
}
|
||||||
|
const config = await getPointBookConfigAPI();
|
||||||
|
uni.setStorageSync("point-book-config", config);
|
||||||
|
if (config.targetOption && config.targetOption[0]) {
|
||||||
|
bowTargetSrc.value = config.targetOption[0].icon;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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",
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Container :bgType="4" bgColor="#F5F5F5" :whiteBackArrow="false" title="">
|
||||||
|
<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>Arrows Today</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>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>
|
||||||
|
<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>
|
||||||
|
<view class="reward" v-if="data.totalArrow">
|
||||||
|
<button hover-class="none" @click="showTip = true">
|
||||||
|
<image src="../static/reward-us.png" mode="widthFix" />
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<RingBarChart :data="data.ringRate" v-if="user.id" />
|
||||||
|
<view class="title" v-if="user.id">
|
||||||
|
<image src="../static/point-book-title2.png" mode="widthFix" />
|
||||||
|
</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="toListPage"
|
||||||
|
v-if="list.length"
|
||||||
|
:style="{ marginBottom: isIOS ? '10rpx' : 0 }"
|
||||||
|
>
|
||||||
|
<text>View all records</text>
|
||||||
|
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<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>Are you sure to delete this record?</text>
|
||||||
|
<view>
|
||||||
|
<button hover-class="none" @click="showTip2 = false">Cancel</button>
|
||||||
|
<button hover-class="none" @click="confirmRemove">Confirm</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</ScreenHint2>
|
||||||
|
</Container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
width: calc(100% - 50rpx);
|
||||||
|
padding: 25rpx;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.reward {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: -120rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.reward > button {
|
||||||
|
width: 100rpx;
|
||||||
|
}
|
||||||
|
.reward > button > image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.tip-content {
|
||||||
|
width: 100%;
|
||||||
|
padding: 25px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.tip-content > text {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.tip-content > view {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.tip-content > view > button {
|
||||||
|
width: 48%;
|
||||||
|
background: linear-gradient(180deg, #fbfbfb 0%, #f5f5f5 100%);
|
||||||
|
border-radius: 22px;
|
||||||
|
border: 1px solid #eeeeee;
|
||||||
|
padding: 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.tip-content > view > button:last-child {
|
||||||
|
background: #fed847;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import ShootProgress from "@/components/ShootProgress.vue";
|
import ShootProgress from "@/components/ShootProgress.vue";
|
||||||
import BowTarget from "@/components/BowTarget.vue";
|
import BowTarget from "@/components/BowTarget.vue";
|
||||||
@@ -98,7 +98,7 @@ onMounted(() => {
|
|||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import ShootProgress from "@/components/ShootProgress.vue";
|
import ShootProgress from "@/components/ShootProgress.vue";
|
||||||
import BowTarget from "@/components/BowTarget.vue";
|
import BowTarget from "@/components/BowTarget.vue";
|
||||||
@@ -91,7 +91,7 @@ onMounted(() => {
|
|||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
@@ -106,7 +106,7 @@ onUnmounted(() => {
|
|||||||
<block v-if="practiseId">
|
<block v-if="practiseId">
|
||||||
<ShootProgress
|
<ShootProgress
|
||||||
:start="start"
|
:start="start"
|
||||||
:tips="`请连续射击${total}支箭`"
|
:tips="`请连续射${total}支箭`"
|
||||||
:total="120"
|
:total="120"
|
||||||
/>
|
/>
|
||||||
<view class="user-row">
|
<view class="user-row">
|
||||||
|
|||||||
@@ -38,6 +38,15 @@ const handleSelect = (index) => {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
if (!myData.value.userId) {
|
||||||
|
myData.value = {
|
||||||
|
userId: user.value.id,
|
||||||
|
TotalGames: 0,
|
||||||
|
totalScore: 0,
|
||||||
|
mvpCount: 0,
|
||||||
|
TenRings: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,11 +54,11 @@ const onScrollView = (e) => {
|
|||||||
addBg.value = e.detail.scrollTop > 100;
|
addBg.value = e.detail.scrollTop > 100;
|
||||||
};
|
};
|
||||||
|
|
||||||
const subTitles = ["排位赛积分", "本周MVP次数", "本周十环次数"];
|
const subTitles = ["排位赛积分", "MVP次数", "十环次数"];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<scroll-view class="container" scroll-y @scroll="onScrollView">
|
<view class="container">
|
||||||
<view
|
<view
|
||||||
class="header"
|
class="header"
|
||||||
:style="{
|
:style="{
|
||||||
@@ -71,7 +80,16 @@ const subTitles = ["排位赛积分", "本周MVP次数", "本周十环次数"];
|
|||||||
本赛季排行榜
|
本赛季排行榜
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<image src="../static/rankbg.png" mode="widthFix" class="header-bg" />
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
@scroll="onScrollView"
|
||||||
|
:style="{ height: myData.userId ? '90vh' : '100vh' }"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
src="https://static.shelingxingqiu.com/attachment/2025-09-25/dd1p9b3wcrwnlnghiq.png"
|
||||||
|
mode="widthFix"
|
||||||
|
class="header-bg"
|
||||||
|
/>
|
||||||
<view class="rank-tabs">
|
<view class="rank-tabs">
|
||||||
<view
|
<view
|
||||||
v-for="(rankType, index) in ['积分榜', 'MVP榜', '十环榜']"
|
v-for="(rankType, index) in ['积分榜', 'MVP榜', '十环榜']"
|
||||||
@@ -145,17 +163,20 @@ const subTitles = ["排位赛积分", "本周MVP次数", "本周十环次数"];
|
|||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
<text class="rank-item-integral" v-if="selectedIndex === 0">
|
<text class="rank-item-integral" v-if="selectedIndex === 0">
|
||||||
<text :style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
<text
|
||||||
|
:style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
||||||
>{{ item.totalScore }} </text
|
>{{ item.totalScore }} </text
|
||||||
>分
|
>分
|
||||||
</text>
|
</text>
|
||||||
<text class="rank-item-integral" v-if="selectedIndex === 1">
|
<text class="rank-item-integral" v-if="selectedIndex === 1">
|
||||||
<text :style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
<text
|
||||||
|
:style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
||||||
>{{ item.mvpCount }} </text
|
>{{ item.mvpCount }} </text
|
||||||
>次
|
>次
|
||||||
</text>
|
</text>
|
||||||
<text class="rank-item-integral" v-if="selectedIndex === 2">
|
<text class="rank-item-integral" v-if="selectedIndex === 2">
|
||||||
<text :style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
<text
|
||||||
|
:style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
||||||
>{{ item.TenRings }} </text
|
>{{ item.TenRings }} </text
|
||||||
>次
|
>次
|
||||||
</text>
|
</text>
|
||||||
@@ -164,6 +185,7 @@ const subTitles = ["排位赛积分", "本周MVP次数", "本周十环次数"];
|
|||||||
<text>筹备中...</text>
|
<text>筹备中...</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</scroll-view>
|
||||||
<view class="my-rank-data" v-if="myData.userId">
|
<view class="my-rank-data" v-if="myData.userId">
|
||||||
<image
|
<image
|
||||||
src="https://static.shelingxingqiu.com/attachment/2025-08-05/dbuaf19pf7qd8ps0uh.png"
|
src="https://static.shelingxingqiu.com/attachment/2025-08-05/dbuaf19pf7qd8ps0uh.png"
|
||||||
@@ -178,30 +200,28 @@ const subTitles = ["排位赛积分", "本周MVP次数", "本周十环次数"];
|
|||||||
<text class="rank-item-integral" v-if="selectedIndex === 0">
|
<text class="rank-item-integral" v-if="selectedIndex === 0">
|
||||||
<text
|
<text
|
||||||
:style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
:style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
||||||
>{{ myData.totalScore }}</text
|
>{{ myData.totalScore || 0 }}</text
|
||||||
>次</text
|
>分</text
|
||||||
>
|
>
|
||||||
<text class="rank-item-integral" v-if="selectedIndex === 1">
|
<text class="rank-item-integral" v-if="selectedIndex === 1">
|
||||||
<text
|
<text
|
||||||
:style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
:style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
||||||
>{{ myData.TotalGames }}</text
|
>{{ myData.mvpCount || 0 }}</text
|
||||||
>次</text
|
>次</text
|
||||||
>
|
>
|
||||||
<text class="rank-item-integral" v-if="selectedIndex === 2">
|
<text class="rank-item-integral" v-if="selectedIndex === 2">
|
||||||
<text
|
<text
|
||||||
:style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
:style="{ fontSize: '14px', color: '#fff', marginRight: '5px' }"
|
||||||
>{{ myData.TenRings }}</text
|
>{{ myData.TenRings || 0 }}</text
|
||||||
>次</text
|
>次</text
|
||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
|
||||||
padding-bottom: 100px;
|
|
||||||
}
|
}
|
||||||
.header-bg {
|
.header-bg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -279,6 +299,7 @@ const subTitles = ["排位赛积分", "本周MVP次数", "本周十环次数"];
|
|||||||
.player-crown {
|
.player-crown {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 27px;
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
margin: 0 15px;
|
margin: 0 15px;
|
||||||
}
|
}
|
||||||
.view-crown {
|
.view-crown {
|
||||||
@@ -313,17 +334,17 @@ const subTitles = ["排位赛积分", "本周MVP次数", "本周十环次数"];
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
.my-rank-data {
|
.my-rank-data {
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: calc(100% - 30px);
|
width: calc(100% - 30px);
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
padding-bottom: 30px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
color: #fff9;
|
color: #fff9;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
height: calc(10vh - 30px);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
.my-rank-data > image:first-child {
|
.my-rank-data > image:first-child {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -369,6 +390,7 @@ const subTitles = ["排位赛积分", "本周MVP次数", "本周十环次数"];
|
|||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
margin: 0px 15px;
|
margin: 0px 15px;
|
||||||
|
margin-top: 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.bg-image {
|
.bg-image {
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ const updateData = () => {
|
|||||||
}
|
}
|
||||||
let keyName = "";
|
let keyName = "";
|
||||||
if (item.gameType === 1 && item.teamSize === 2) keyName = "1v1";
|
if (item.gameType === 1 && item.teamSize === 2) keyName = "1v1";
|
||||||
if (item.gameType === 1 && item.teamSize === 3) keyName = "2v2";
|
if (item.gameType === 1 && item.teamSize === 4) keyName = "2v2";
|
||||||
if (item.gameType === 1 && item.teamSize === 5) keyName = "3v3";
|
if (item.gameType === 1 && item.teamSize === 6) keyName = "3v3";
|
||||||
if (item.gameType === 2 && item.teamSize === 5) keyName = "5m";
|
if (item.gameType === 2 && item.teamSize === 5) keyName = "5m";
|
||||||
if (item.gameType === 2 && item.teamSize === 10) keyName = "10m";
|
if (item.gameType === 2 && item.teamSize === 10) keyName = "10m";
|
||||||
if (keyName) {
|
if (keyName) {
|
||||||
@@ -121,6 +121,9 @@ onShow(async () => {
|
|||||||
rankData.value = result;
|
rankData.value = result;
|
||||||
handleSelect(selectedIndex.value);
|
handleSelect(selectedIndex.value);
|
||||||
seasonData.value = result.seasonList;
|
seasonData.value = result.seasonList;
|
||||||
|
if (seasonData.value[0]) {
|
||||||
|
seasonName.value = seasonData.value[0].seasonName;
|
||||||
|
}
|
||||||
updateData();
|
updateData();
|
||||||
showSeasonList.value = false;
|
showSeasonList.value = false;
|
||||||
});
|
});
|
||||||
@@ -191,7 +194,7 @@ onShow(async () => {
|
|||||||
</view>
|
</view>
|
||||||
<view class="my-rank-score">
|
<view class="my-rank-score">
|
||||||
<image src="../static/bubble-tip5.png" mode="widthFix" />
|
<image src="../static/bubble-tip5.png" mode="widthFix" />
|
||||||
<text>积分:{{ rankData.user.scores }}</text>
|
<text>积分:{{ Math.max(0, rankData.user.scores) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="battle-types">
|
<view class="battle-types">
|
||||||
@@ -261,7 +264,7 @@ onShow(async () => {
|
|||||||
<view>
|
<view>
|
||||||
<view
|
<view
|
||||||
:style="{
|
:style="{
|
||||||
width: `${currentSeasonData['1v1'].winRate}%`,
|
width: `${currentSeasonData['3v3'].winRate}%`,
|
||||||
backgroundColor: '#FF8C8C',
|
backgroundColor: '#FF8C8C',
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@@ -511,6 +514,7 @@ onShow(async () => {
|
|||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
font-size: 30rpx;
|
||||||
}
|
}
|
||||||
.rank-item {
|
.rank-item {
|
||||||
width: calc(100% - 30px);
|
width: calc(100% - 30px);
|
||||||
@@ -595,11 +599,13 @@ onShow(async () => {
|
|||||||
top: -44rpx;
|
top: -44rpx;
|
||||||
right: -30rpx;
|
right: -30rpx;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
.season-list > view {
|
.season-list > view {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
|
word-break: keep-all;
|
||||||
}
|
}
|
||||||
.season-list > view > image {
|
.season-list > view > image {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
|
|||||||
52
src/pages/reset-password.vue
Normal 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
@@ -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
@@ -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>
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
import { ref, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||||||
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import BattleHeader from "@/components/BattleHeader.vue";
|
import BattleHeader from "@/components/BattleHeader.vue";
|
||||||
import BowTarget from "@/components/BowTarget.vue";
|
import BowTarget from "@/components/BowTarget.vue";
|
||||||
import ShootProgress from "@/components/ShootProgress.vue";
|
import ShootProgress from "@/components/ShootProgress.vue";
|
||||||
import PlayersRow from "@/components/PlayersRow.vue";
|
import PlayersRow from "@/components/PlayersRow.vue";
|
||||||
import Timer from "@/components/Timer.vue";
|
|
||||||
import BattleFooter from "@/components/BattleFooter.vue";
|
import BattleFooter from "@/components/BattleFooter.vue";
|
||||||
import ScreenHint from "@/components/ScreenHint.vue";
|
import ScreenHint from "@/components/ScreenHint.vue";
|
||||||
import SButton from "@/components/SButton.vue";
|
import SButton from "@/components/SButton.vue";
|
||||||
@@ -17,6 +16,7 @@ import ShootProgress2 from "@/components/ShootProgress2.vue";
|
|||||||
import { getCurrentGameAPI } from "@/apis";
|
import { getCurrentGameAPI } from "@/apis";
|
||||||
import { isGameEnded } from "@/util";
|
import { isGameEnded } from "@/util";
|
||||||
import { MESSAGETYPES, roundsName } from "@/constants";
|
import { MESSAGETYPES, roundsName } from "@/constants";
|
||||||
|
import audioManager from "@/audioManager";
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -65,7 +65,7 @@ function recoverData(battleInfo) {
|
|||||||
redPoints.value = 0;
|
redPoints.value = 0;
|
||||||
currentRound.value = battleInfo.currentRound;
|
currentRound.value = battleInfo.currentRound;
|
||||||
totalRounds.value = battleInfo.maxRound;
|
totalRounds.value = battleInfo.maxRound;
|
||||||
roundResults.value = battleInfo.roundResults;
|
roundResults.value = [...battleInfo.roundResults];
|
||||||
battleInfo.roundResults.forEach((round) => {
|
battleInfo.roundResults.forEach((round) => {
|
||||||
const blueTotal = round.blueArrows.reduce(
|
const blueTotal = round.blueArrows.reduce(
|
||||||
(last, next) => last + next.ring,
|
(last, next) => last + next.ring,
|
||||||
@@ -84,31 +84,40 @@ function recoverData(battleInfo) {
|
|||||||
redPoints.value += 2;
|
redPoints.value += 2;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const hasCurrentRoundData =
|
||||||
|
battleInfo.redTeam.some(
|
||||||
|
(item) => !!item.shotHistory[battleInfo.currentRound]
|
||||||
|
) ||
|
||||||
|
battleInfo.blueTeam.some(
|
||||||
|
(item) => !!item.shotHistory[battleInfo.currentRound]
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
battleInfo.redTeam[0].shotHistory[battleInfo.currentRound] ||
|
battleInfo.currentRound > battleInfo.roundResults.length &&
|
||||||
battleInfo.blueTeam[0].shotHistory[battleInfo.currentRound]
|
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({
|
roundResults.value.push({
|
||||||
redArrows: battleInfo.redTeam[0].shotHistory[
|
redArrows,
|
||||||
battleInfo.currentRound
|
blueArrows,
|
||||||
].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) {
|
if (battleInfo.goldenRound) {
|
||||||
const { ShotCount, RedRecords, BlueRecords } = battleInfo.goldenRound;
|
const { ShotCount, RedRecords, BlueRecords } = battleInfo.goldenRound;
|
||||||
const roundCount = Math.max(RedRecords.length, BlueRecords.length);
|
currentRound.value += ShotCount;
|
||||||
currentRound.value += roundCount;
|
goldenRound.value += ShotCount;
|
||||||
goldenRound.value += roundCount;
|
|
||||||
isFinalShoot.value = true;
|
isFinalShoot.value = true;
|
||||||
for (let i = 0; i < roundCount; i++) {
|
for (let i = 0; i < ShotCount; i++) {
|
||||||
const roundData = {
|
const roundData = {
|
||||||
redArrows:
|
redArrows:
|
||||||
RedRecords && RedRecords[i] ? RedRecords[i].Arrows || [] : [],
|
RedRecords && RedRecords[i] ? RedRecords[i].Arrows || [] : [],
|
||||||
@@ -116,12 +125,22 @@ function recoverData(battleInfo) {
|
|||||||
BlueRecords && BlueRecords[i] ? BlueRecords[i].Arrows || [] : [],
|
BlueRecords && BlueRecords[i] ? BlueRecords[i].Arrows || [] : [],
|
||||||
gold: true,
|
gold: true,
|
||||||
};
|
};
|
||||||
if (roundResults.value[5 + i]) {
|
|
||||||
roundResults.value[5 + i] = roundData;
|
|
||||||
} else {
|
|
||||||
roundResults.value.push(roundData);
|
roundResults.value.push(roundData);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
[...battleInfo.redTeam, ...battleInfo.blueTeam].some((p) => {
|
||||||
|
if (p.id === user.value.id) {
|
||||||
|
const roundArrows = Object.values(p.shotHistory);
|
||||||
|
if (roundArrows.length) {
|
||||||
|
uni.$emit("update-shot", {
|
||||||
|
currentShot: roundArrows[roundArrows.length - 1].length,
|
||||||
|
totalShot: battleInfo.config.teamSize === 2 ? 3 : 2,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const lastIndex = roundResults.value.length - 1;
|
const lastIndex = roundResults.value.length - 1;
|
||||||
if (roundResults.value[lastIndex]) {
|
if (roundResults.value[lastIndex]) {
|
||||||
@@ -157,12 +176,6 @@ async function onReceiveMessage(messages = []) {
|
|||||||
if (msg.constructor === MESSAGETYPES.AllReady) {
|
if (msg.constructor === MESSAGETYPES.AllReady) {
|
||||||
start.value = true;
|
start.value = true;
|
||||||
totalRounds.value = msg.groupUserStatus.config.maxRounds;
|
totalRounds.value = msg.groupUserStatus.config.maxRounds;
|
||||||
roundResults.value = [
|
|
||||||
{
|
|
||||||
redArrows: [],
|
|
||||||
blueArrows: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) {
|
if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) {
|
||||||
if (currentShooterId.value !== msg.userId) {
|
if (currentShooterId.value !== msg.userId) {
|
||||||
@@ -170,6 +183,7 @@ async function onReceiveMessage(messages = []) {
|
|||||||
const redPlayer = redTeam.value.find(
|
const redPlayer = redTeam.value.find(
|
||||||
(item) => item.id === currentShooterId.value
|
(item) => item.id === currentShooterId.value
|
||||||
);
|
);
|
||||||
|
if (msg.userId === user.value.id) audioManager.play("轮到你了");
|
||||||
const nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭";
|
const nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭";
|
||||||
if (nextTips !== tips.value) {
|
if (nextTips !== tips.value) {
|
||||||
tips.value = nextTips;
|
tips.value = nextTips;
|
||||||
@@ -184,6 +198,7 @@ async function onReceiveMessage(messages = []) {
|
|||||||
const isRed = redTeam.value.find((item) => item.id === msg.userId);
|
const isRed = redTeam.value.find((item) => item.id === msg.userId);
|
||||||
if (isRed) scores.value.push({ ...msg.target });
|
if (isRed) scores.value.push({ ...msg.target });
|
||||||
else blueScores.value.push({ ...msg.target });
|
else blueScores.value.push({ ...msg.target });
|
||||||
|
// 下标从0开始的,要减1
|
||||||
if (!roundResults.value[currentRound.value - 1]) {
|
if (!roundResults.value[currentRound.value - 1]) {
|
||||||
roundResults.value.push({
|
roundResults.value.push({
|
||||||
redArrows: [],
|
redArrows: [],
|
||||||
@@ -204,17 +219,14 @@ async function onReceiveMessage(messages = []) {
|
|||||||
currentRedPoint.value = result.redScore;
|
currentRedPoint.value = result.redScore;
|
||||||
bluePoints.value += result.blueScore;
|
bluePoints.value += result.blueScore;
|
||||||
redPoints.value += result.redScore;
|
redPoints.value += result.redScore;
|
||||||
if (result.currentRound < 5) {
|
|
||||||
currentRound.value = result.currentRound + 1;
|
currentRound.value = result.currentRound + 1;
|
||||||
roundResults.value.push({
|
if (!result.goldenRound) {
|
||||||
redArrows: [],
|
|
||||||
blueArrows: [],
|
|
||||||
});
|
|
||||||
showRoundTip.value = true;
|
showRoundTip.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (msg.constructor === MESSAGETYPES.FinalShoot) {
|
if (msg.constructor === MESSAGETYPES.FinalShoot) {
|
||||||
currentShooterId.value = 0;
|
currentShooterId.value = 0;
|
||||||
|
currentRound.value = msg.groupUserStatus.currentRound + 1;
|
||||||
goldenRound.value += 1;
|
goldenRound.value += 1;
|
||||||
roundResults.value.push({
|
roundResults.value.push({
|
||||||
redArrows: [],
|
redArrows: [],
|
||||||
@@ -234,6 +246,7 @@ async function onReceiveMessage(messages = []) {
|
|||||||
currentBluePoint.value = 0;
|
currentBluePoint.value = 0;
|
||||||
currentRedPoint.value = 0;
|
currentRedPoint.value = 0;
|
||||||
showRoundTip.value = true;
|
showRoundTip.value = true;
|
||||||
|
isFinalShoot.value = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
@@ -274,7 +287,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
@@ -303,10 +316,10 @@ onHide(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Container :bgType="3" :onBack="onBack">
|
<Container :bgType="start ? 3 : 1" :onBack="onBack">
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<BattleHeader v-if="!start" :redTeam="redTeam" :blueTeam="blueTeam" />
|
<BattleHeader v-if="!start" :redTeam="redTeam" :blueTeam="blueTeam" />
|
||||||
<TestDistance v-if="!start" :guide="false" />
|
<TestDistance v-if="!start" :guide="false" :isBattle="true" />
|
||||||
<view v-if="start" class="players-row">
|
<view v-if="start" class="players-row">
|
||||||
<TeamAvatars
|
<TeamAvatars
|
||||||
:team="blueTeam"
|
:team="blueTeam"
|
||||||
@@ -336,7 +349,6 @@ onHide(() => {
|
|||||||
:goldenRound="goldenRound"
|
:goldenRound="goldenRound"
|
||||||
:power="power"
|
:power="power"
|
||||||
/>
|
/>
|
||||||
<Timer v-if="!start" />
|
|
||||||
<ScreenHint
|
<ScreenHint
|
||||||
:show="showRoundTip"
|
:show="showRoundTip"
|
||||||
:onClose="() => (showRoundTip = false)"
|
:onClose="() => (showRoundTip = false)"
|
||||||
@@ -349,9 +361,7 @@ onHide(() => {
|
|||||||
:bluePoint="currentBluePoint"
|
:bluePoint="currentBluePoint"
|
||||||
:redPoint="currentRedPoint"
|
:redPoint="currentRedPoint"
|
||||||
:roundData="
|
:roundData="
|
||||||
roundResults[roundResults.length - 2]
|
roundResults[currentRound - 2] ? roundResults[currentRound - 2] : []
|
||||||
? roundResults[roundResults.length - 2]
|
|
||||||
: []
|
|
||||||
"
|
"
|
||||||
:onAutoClose="() => (showRoundTip = false)"
|
:onAutoClose="() => (showRoundTip = false)"
|
||||||
/>
|
/>
|
||||||
@@ -368,7 +378,7 @@ onHide(() => {
|
|||||||
.players-row {
|
.players-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: center;
|
||||||
margin-bottom: -7vw;
|
margin-bottom: -7vw;
|
||||||
margin-top: -3vw;
|
margin-top: -3vw;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const data = ref({
|
|||||||
onLoad(async (options) => {
|
onLoad(async (options) => {
|
||||||
if (options.battleId) {
|
if (options.battleId) {
|
||||||
const result = await getGameAPI(
|
const result = await getGameAPI(
|
||||||
options.battleId || "BATTLE-1755484626207409508-955"
|
options.battleId || "BATTLE-1756453741433684760-512"
|
||||||
);
|
);
|
||||||
data.value = result;
|
data.value = result;
|
||||||
Object.values(result.bluePlayers).forEach((p, index) => {
|
Object.values(result.bluePlayers).forEach((p, index) => {
|
||||||
@@ -150,6 +150,7 @@ const onClickTab = (index) => {
|
|||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
v-if="
|
v-if="
|
||||||
|
selected > 0 &&
|
||||||
selected >= data.goldenRounds.length &&
|
selected >= data.goldenRounds.length &&
|
||||||
selected <= data.goldenRounds.length
|
selected <= data.goldenRounds.length
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
import { ref, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||||||
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import BattleHeader from "@/components/BattleHeader.vue";
|
import BattleHeader from "@/components/BattleHeader.vue";
|
||||||
@@ -270,7 +270,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
uni.$on("socket-inbox", onReceiveMessage);
|
uni.$on("socket-inbox", onReceiveMessage);
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onBeforeUnmount(() => {
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
keepScreenOn: false,
|
keepScreenOn: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
import Container from "@/components/Container.vue";
|
import Container from "@/components/Container.vue";
|
||||||
import UserHeader from "@/components/UserHeader.vue";
|
import UserHeader from "@/components/UserHeader.vue";
|
||||||
import UserItem from "@/components/UserItem.vue";
|
import UserItem from "@/components/UserItem.vue";
|
||||||
@@ -46,11 +47,25 @@ const toRankIntroPage = () => {
|
|||||||
url: "/pages/rank-intro",
|
url: "/pages/rank-intro",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const toAboutUsPage = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: "/pages/about-us",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showLogout = ref(false);
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
uni.removeStorageSync("token");
|
uni.removeStorageSync(
|
||||||
|
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||||
|
);
|
||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
updateUser();
|
updateUser();
|
||||||
};
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
const accountInfo = uni.getAccountInfoSync();
|
||||||
|
const envVersion = accountInfo.miniProgram.envVersion;
|
||||||
|
if (envVersion !== "release") showLogout.value = true;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -110,7 +125,12 @@ const logout = () => {
|
|||||||
<view class="my-grow" @click="toMyGrowthPage">
|
<view class="my-grow" @click="toMyGrowthPage">
|
||||||
<image src="../static/my-grow.png" mode="widthFix" />
|
<image src="../static/my-grow.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
<UserItem title="退出登录(仅用于测试)" :onClick="logout" />
|
<UserItem title="关于我们" :onClick="toAboutUsPage" />
|
||||||
|
<UserItem
|
||||||
|
title="退出登录(仅用于测试)"
|
||||||
|
:onClick="logout"
|
||||||
|
v-if="showLogout"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
15
src/pages/webview.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { onLoad } from "@dcloudio/uni-app";
|
||||||
|
|
||||||
|
const url = ref("");
|
||||||
|
onLoad((options) => {
|
||||||
|
url.value = decodeURIComponent(options.url);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<web-view :src="url"></web-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
Before Width: | Height: | Size: 189 B After Width: | Height: | Size: 171 B |
BIN
src/static/app-bg5.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/static/app-bg6.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
src/static/apple-icon.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/static/back-grey.png
Normal file
|
After Width: | Height: | Size: 273 B |
BIN
src/static/checked-green2.png
Normal file
|
After Width: | Height: | Size: 372 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.2 KiB |
BIN
src/static/donate.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/static/edit.png
Normal file
|
After Width: | Height: | Size: 363 B |
BIN
src/static/email-yellow.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/static/eye-close.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/static/eye-open.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
src/static/flag-blue.png
Normal file
|
After Width: | Height: | Size: 522 B |
BIN
src/static/flag-red.png
Normal file
|
After Width: | Height: | Size: 540 B |
BIN
src/static/google-icon.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/static/has-note.png
Normal file
|
After Width: | Height: | Size: 709 B |
BIN
src/static/have-no-device.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/static/hot1.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/static/hot2.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/static/hot3.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/static/hot4.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/static/logo.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/static/password-yellow.png
Normal file
|
After Width: | Height: | Size: 687 B |