171 Commits
ios-ui ... main

Author SHA1 Message Date
4dcfdeda68 feat: 添加分支管理说明 2026-05-07 21:04:10 +08:00
a6becf67ff fix: 个人练习报环完成之后再弹练习结果弹窗 2026-05-07 18:30:46 +08:00
8c66ef78c6 fix:房间号更新改为pinia 2026-05-07 17:15:28 +08:00
29a6f46a0d Merge remote-tracking branch 'origin/new-race-mode' into new-race-mode 2026-05-07 16:54:32 +08:00
895be17f7c pref: 进度条倒计时 2026-05-07 16:49:09 +08:00
7c8fd9395b fix:踢出功能二次确认弹窗完成 2026-05-07 14:31:17 +08:00
972d817629 bug:修复房间号模块显示问题 2026-05-07 14:03:53 +08:00
31140c7ae2 fix:好友约战,我的战绩按钮跳转完成 2026-05-07 13:53:51 +08:00
542ee5f031 fix:好友约战首页调整完成 2026-05-07 11:13:18 +08:00
e28424456f Merge branch 'new-race-mode' of https://git.shelingxingqiu.com/laoma/shoot-miniprograms into new-race-mode 2026-05-06 18:11:02 +08:00
e037c02888 fix:好友约战新头部完成 2026-05-06 18:10:49 +08:00
9a5d64cc9e pref: 进度条变化 2026-05-06 17:57:02 +08:00
1beb1009b3 pref: 比赛结束 2026-05-06 16:15:39 +08:00
9cd32a7aa6 Merge remote-tracking branch 'origin/new-race-mode' into new-race-mode 2026-05-06 14:20:41 +08:00
473e6df77b pref: 匹配成功展示准备页面 2026-05-06 14:20:06 +08:00
088cd33b0a fix:解决后端返回null数据报错问题 2026-05-06 11:55:46 +08:00
51fd4acd8b fix:添加git忽略提交配置2 2026-05-06 10:15:13 +08:00
691e33a84e fix:添加git忽略提交配置 2026-05-06 10:12:41 +08:00
e60d24d56c fix:配置文件提交 2026-05-06 10:03:20 +08:00
e1a9d97596 pref: 小程序添加静默登录 2026-04-28 10:31:54 +08:00
f07facd98b pref: 绑定设备、匹配pk 2026-04-28 08:59:16 +08:00
8c48216a75 feat: 好友约战选择20、40厘米靶 2026-04-20 17:43:38 +08:00
c1ff0cedad feat:选择20cm、40cm全环靶 2026-04-20 16:00:10 +08:00
56650793e8 pref: 更新音频 2026-04-20 11:13:59 +08:00
e8568ee6a8 feat: 添加射箭声音 2026-04-16 18:06:59 +08:00
kron
1181a2133a 更换图片地址 2026-04-07 16:27:49 +08:00
kron
b9bb1e6653 BUG修复 2026-02-10 18:13:11 +08:00
kron
608de34dd3 细节完善 2026-02-10 17:03:25 +08:00
kron
88f1ef5d95 细节完善 2026-02-10 14:48:07 +08:00
kron
b0bf1880e4 完善个人练习分享 2026-02-10 11:47:20 +08:00
kron
812879d252 完善我的成长 2026-02-10 11:47:09 +08:00
kron
303e1830d3 BUG修复 2026-02-10 11:32:53 +08:00
kron
61ff1af4c3 细节完善 2026-02-10 09:20:01 +08:00
kron
a3a9f7b351 细节完善 2026-02-09 18:16:48 +08:00
kron
4801833fa9 删除无用文件 2026-02-09 18:16:39 +08:00
kron
a3fea0bb1f 添加X环显示,比赛过程完善 2026-02-09 17:27:44 +08:00
kron
b355f4e009 添加踢人接口 2026-02-09 15:10:23 +08:00
kron
b37f181c0f 添加X环显示 2026-02-09 15:09:50 +08:00
kron
1a5a488776 完成排位匹配调试 2026-02-09 10:40:40 +08:00
kron
2044821e4d 更换新手引导图片 2026-02-09 09:16:26 +08:00
kron
d9a2e53faf 完成新的个人练习流程调试 2026-02-07 18:30:16 +08:00
kron
d35ff9335f 逻辑完善 2026-02-07 15:12:20 +08:00
kron
715e614f9d bug修复 2026-02-07 11:59:21 +08:00
kron
6101cd80ce 完成打乱斗数据调试 2026-02-07 10:52:56 +08:00
kron
09d8e7b3da 添加新通知处理 2026-02-06 14:00:18 +08:00
kron
0a9f398ef4 添加移除玩家UI 2026-02-06 13:59:43 +08:00
kron
4cfa097cc8 调试多v多房间功能 2026-02-05 18:06:55 +08:00
kron
93c549109f 显示已准备状态 2026-02-05 10:35:12 +08:00
kron
7f73f3ebb3 新版房间1v1对战数据调试完成 2026-02-04 17:45:57 +08:00
kron
a2674aae5b 代码优化 2026-01-12 15:03:20 +08:00
kron
4771f53d71 数据显示修改 2026-01-12 10:55:20 +08:00
kron
121d0e36f3 UI修改 2026-01-12 10:09:36 +08:00
kron
88fbc93d97 添加swiper 2026-01-09 18:18:33 +08:00
kron
155f2986c7 页面布局优化 2026-01-09 18:12:27 +08:00
kron
937fce1a35 业务逻辑完善 2026-01-09 14:10:23 +08:00
kron
ddf0dafe08 代码优化 2026-01-09 13:44:29 +08:00
kron
71b25144a4 添加分享房间链接和对战结束返回房间 2026-01-09 11:50:21 +08:00
kron
4aa14c6a4c canvas代码优化 2026-01-08 10:30:41 +08:00
kron
c9a7c1ae8b 修复显示问题 2026-01-08 10:03:00 +08:00
kron
6f464a9f71 删除无用代码 2026-01-07 18:19:12 +08:00
kron
23cd5bd835 练习流程修改 2026-01-07 15:12:30 +08:00
kron
1f75045db4 细节优化 2026-01-07 15:12:18 +08:00
kron
494f83392e 细节完善 2026-01-07 10:07:25 +08:00
kron
199bf3160e 文字修改 2026-01-06 18:18:22 +08:00
kron
7cb203a08f 返回游戏时添加声音加载 2026-01-06 18:13:55 +08:00
kron
ab3537e35d 修改文案 2026-01-06 15:34:53 +08:00
kron
4275f608fe 细节调整 2026-01-06 14:41:45 +08:00
kron
3d13f7c880 内容更新 2026-01-06 10:00:57 +08:00
kron
60f4b3370c 样式更新 2026-01-06 09:08:56 +08:00
kron
f7c24680cb 细节优化 2026-01-05 18:03:47 +08:00
kron
3364aac93d 计分本添加排行榜 2026-01-05 15:59:23 +08:00
kron
fd026efc85 微信合规调整 2026-01-05 09:23:26 +08:00
kron
9f7523839d 仅正式环境判断设备在线 2026-01-04 11:36:31 +08:00
kron
60b1006447 添加新年主题 2025-12-31 16:42:53 +08:00
kron
af852d9b59 添加设备在线离线处理 2025-12-31 13:39:16 +08:00
kron
25f51ad53a 用户中心逻辑完善 2025-12-31 11:56:45 +08:00
kron
910530748d 细节修改 2025-12-30 18:10:31 +08:00
kron
44913a6f2e UI修改 2025-12-30 14:25:20 +08:00
kron
e120ec8e7e 添加音频重新加载功能 2025-12-29 11:53:19 +08:00
kron
919b06bba0 点击拖拽创建的点可以继续拖拽 2025-12-29 10:10:21 +08:00
kron
08c4ef0625 小修改 2025-12-26 16:16:05 +08:00
kron
e3752e6fe2 细节修改 2025-12-08 18:34:38 +08:00
kron
a5c70db8ad 修复放大1.4倍的三连靶 2025-12-05 15:37:02 +08:00
kron
8c22eb0877 不放大的3连靶 2025-12-05 15:24:42 +08:00
kron
19902d5bed 小修改 2025-12-05 15:24:14 +08:00
kron
e6eda5ce9b 更换图片 2025-12-04 14:13:51 +08:00
kron
b33f5263f6 压线精度调整 2025-12-04 14:11:56 +08:00
kron
fa394bdd87 审核被拒修改 2025-12-04 09:35:44 +08:00
kron
8cb4a26263 版本1.0.14 2025-12-03 17:24:55 +08:00
kron
a9156e57ca 添加后台清缓存的处理 2025-12-03 16:46:07 +08:00
kron
d9ac803902 登录添加手机号 2025-12-03 15:19:17 +08:00
kron
f9548f1373 为指定用户添加客服弹窗 2025-12-03 13:56:01 +08:00
kron
efa16c64a6 修改音频加载策略 2025-11-27 18:16:32 +08:00
kron
82a0ee83b2 添加计分本草稿功能 2025-11-27 12:02:57 +08:00
kron
c697a7edd0 修改靶点颜色和字体 2025-11-26 17:49:09 +08:00
kron
da7816bb88 修改音频加载提示的交互 2025-11-26 17:12:55 +08:00
kron
d02ecf6a7e 更换葵花码 2025-11-26 16:29:19 +08:00
kron
15ee4e7afa 添加音频loading页面 2025-11-25 15:02:50 +08:00
kron
1320519e90 流程完善 2025-11-17 17:14:51 +08:00
kron
e50532e2de 细节完善 2025-11-17 16:28:57 +08:00
kron
0a08955caa BUG修复 2025-11-17 16:03:41 +08:00
kron
115f270ed1 细节修改 2025-11-17 15:49:53 +08:00
kron
77f0460dd3 BUG修复 2025-11-17 14:30:54 +08:00
kron
bb8f1c715e 接口参数修改 2025-11-17 14:15:06 +08:00
kron
d9563a25c6 分享图兼容性修复 2025-11-17 12:02:07 +08:00
kron
046d1a7c9e 压缩图片 2025-11-17 11:50:58 +08:00
kron
3e362241cc 完成分享计分详情的靶子和点的渲染 2025-11-16 17:09:50 +08:00
kron
8a4b44666f 完成分享计分详情的柱状图 2025-11-14 18:13:55 +08:00
kron
52bccd25fc 更换语音 2025-11-14 15:51:45 +08:00
kron
0745c4ba9f 完成生成靶子数据图片的头部 2025-11-14 11:59:21 +08:00
kron
7871544f01 BUG修复 2025-11-14 11:05:47 +08:00
kron
daee337e59 添加兼容性处理 2025-11-14 09:46:14 +08:00
kron
c11a108f5d 添加播放相同声音的防抖 2025-11-14 09:25:24 +08:00
kron
2fb4740752 逻辑完善 2025-11-13 21:23:45 +08:00
kron
bd01b179a6 添加轮数的声音 2025-11-13 21:04:48 +08:00
kron
66705ae9b1 个人12支练习流程修改 2025-11-13 19:57:44 +08:00
kron
be0d3a9e3c BUG修复 2025-11-13 16:48:24 +08:00
kron
e25d91f025 加载策略调整 2025-11-13 16:29:21 +08:00
kron
eba65a4fbd 细节调整 2025-11-13 14:51:44 +08:00
kron
24996a4b35 添加退出当前页面关闭声音 2025-11-13 11:58:16 +08:00
kron
cfc9912a4a 方向调整效果修改 2025-11-13 09:59:35 +08:00
kron
9f33610f20 计分本细节调整 2025-11-12 20:40:00 +08:00
kron
f41a3d7a3a 添加为上靶的方向指示 2025-11-12 16:14:48 +08:00
kron
caadb5ea99 细节调整 2025-11-12 15:54:03 +08:00
kron
6b96087b68 添加声音连续播放 2025-11-12 11:39:17 +08:00
kron
59a2b173a6 细节调整 2025-11-11 16:31:47 +08:00
kron
6df7986c47 细节修改 2025-11-11 10:56:53 +08:00
kron
dfc62c7e3f 修复练习不能分享的问题 2025-11-11 10:13:14 +08:00
kron
81ab085e48 样式优化 2025-11-10 17:47:32 +08:00
kron
cd8814189f 排名显示错误修复 2025-11-10 14:49:35 +08:00
kron
7e1a3be510 细节优化 2025-11-10 13:43:25 +08:00
kron
2cf55dcdde 细节调整 2025-11-10 09:26:51 +08:00
kron
571073d7ec fix bug 2025-11-08 12:18:34 +08:00
kron
b0a4c7bea5 优化轮到你了声音播放 2025-11-08 11:33:03 +08:00
kron
339d12b7b8 样式优化 2025-11-08 10:57:37 +08:00
kron
92cb614c50 BUG修复 2025-11-07 16:42:07 +08:00
kron
738614d724 更新成数据覆盖同步 2025-11-07 16:28:52 +08:00
kron
28bcfbb00a BUG修复 2025-11-06 17:38:34 +08:00
kron
2ab601fef5 细节调整 2025-11-06 17:10:24 +08:00
kron
e942c63885 细节优化 2025-11-06 16:19:52 +08:00
kron
5cefd8b36d 细节优化 2025-11-06 16:08:02 +08:00
kron
cf95e8c046 细节优化 2025-11-06 14:15:53 +08:00
kron
d637a0fa72 添加兼容性处理 2025-11-06 14:06:37 +08:00
kron
035171290c 修复BUG 2025-11-06 11:29:08 +08:00
kron
ae9ec4a7f7 添加音频测试页面 2025-11-06 10:52:08 +08:00
kron
6b30eedcc2 样式优化 2025-11-06 09:38:19 +08:00
kron
534450a629 添加防止ios加载时误播放 2025-11-05 18:30:33 +08:00
kron
b3ec164475 样式优化 2025-11-05 17:53:48 +08:00
kron
49c1447942 BUG修复 2025-11-05 17:48:38 +08:00
kron
3ac184f8cd 样式优化 2025-11-05 17:28:11 +08:00
kron
4e10f3228e 优化点击放大的方向 2025-11-05 16:58:27 +08:00
kron
20ae92729f 细节优化 2025-11-05 16:39:31 +08:00
kron
42a7d49603 细节调整 2025-11-05 14:27:58 +08:00
kron
2e9d257faa BUG修改 2025-11-05 13:38:37 +08:00
kron
ecdf4a76f4 新增分享页,以及BUG修复 2025-11-05 09:36:37 +08:00
kron
802d21a896 细节调整 2025-11-04 17:41:50 +08:00
kron
c7ebeaac36 处理分享后的兼容问题 2025-11-04 16:49:56 +08:00
kron
3f6d8cb821 改成左滑删除 2025-11-04 14:38:16 +08:00
kron
aa4fe1babd 细节完善 2025-11-04 14:18:44 +08:00
kron
3498bc5027 计分本细节更新 2025-11-04 11:54:22 +08:00
kron
1d086c83d4 细节优化 2025-11-03 10:49:13 +08:00
kron
f95759f3e9 积分本靶点大小调整 2025-10-31 18:29:53 +08:00
kron
d9d1efa316 细节完善 2025-10-31 10:22:02 +08:00
kron
a8ee1f0be3 细节调整 2025-10-30 10:11:15 +08:00
kron
1dcbb7cf2f 添加射箭前校准提示 2025-10-30 09:19:34 +08:00
kron
2c0982bd27 在比赛开始时关闭激光 2025-10-30 09:09:03 +08:00
kron
84cdbb94db 添加校准声音 2025-10-29 18:07:42 +08:00
kron
f5d5475ee4 电量显示走接口请求 2025-10-29 17:26:27 +08:00
kron
7036135d9c 细节调整 2025-10-29 17:18:25 +08:00
kron
a6b0d7f28c 添加激光校准 2025-10-29 15:17:22 +08:00
kron
4ce366864e 添加匹配环节的名字 2025-10-28 16:44:15 +08:00
172 changed files with 19185 additions and 7469 deletions

5
.gitignore vendored
View File

@@ -8,6 +8,11 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
.history
.github
openspec
CLAUDE.md
dosc
.DS_Store
dist
*.local

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"i18n-ally.localesPaths": []
}

116
doc.md Normal file
View File

@@ -0,0 +1,116 @@
# 微信小程序多人协作分支管理规范
## 一、分支结构
```
main (主分支/生产环境)
└── test (测试分支)
└── feature/xxx (个人开发分支)
```
| 分支 | 用途 | 稳定性 |
|------|------|--------|
| main | 生产环境代码 | 最高,仅接受测试通过的代码合并 |
| test | 测试环境,用于体验版发布 | 中,需验证后合并到 main |
| feature/xxx | 个人开发分支 | 低,按需命名,如 `feature/user-center` |
---
## 二、开发流程
### 1. 开始开发
```bash
# 确保本地 main 最新
git checkout main
git pull origin main
# 从 main 创建自己的开发分支
git checkout -b feature/your-name-work
```
### 2. 开发阶段
- 在个人分支上开发功能
- 频繁提交,保持原子性提交
- 定期 `git pull origin main` 同步主线变更,避免合并冲突累积
```bash
git add .
git commit -m "feat: 完成xxx功能"
```
### 3. 合并到 test 分支
```bash
# 切换到 test
git checkout test
git pull origin test
# 合并个人分支
git merge feature/your-name-work
# 推送 test 分支
git push origin test
```
### 4. 打包上传体验版
```bash
# 执行打包
npm run build
```
打包完成后:
1. 打开 **微信开发者工具**
2. 导入项目,选择 `dist/build/mp-weixin` 目录
3. 在开发者工具中点击 **上传**
4. 登录 [微信公众平台](https://mp.weixin.qq.com)
5. 进入 **管理->版本管理**
6. 找到刚上传的版本,点击 **选为体验版**
---
## 三、合并到 main 分支
当 test 分支验证通过后,将其合并到 main
```bash
git checkout main
git pull origin main
git merge origin/test
git push origin main
```
---
## 四、冲突处理
合并时如有冲突,在个人分支解决后再合并:
```bash
git checkout feature/your-name-work
git merge main
# 解决冲突后
git add .
git commit -m "merge: 解决与main的冲突"
git push origin feature/your-name-work
# 重新合并到 test
git checkout test
git merge feature/your-name-work
git push origin test
```
---
## 五、注意事项
1. **禁止直接向 main 和 test 分支提交代码**,必须通过合并
2. **每次合并前先拉取最新代码**,避免覆盖他人改动
3. **体验版发布前确认代码已提交**,避免遗漏
4. **开发分支命名建议**`feature/姓名-功能名`,如 `feature/zhangsan-login`
5. **删除已合并的开发分支**`git branch -d feature/your-name-work`

10532
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,231 +1,292 @@
<script setup>
import { watch } from "vue";
import { onShow, onHide } from "@dcloudio/uni-app";
import websocket from "@/websocket";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const { updateUser } = store;
import {
watch
} from "vue";
import {
onShow,
onHide
} from "@dcloudio/uni-app";
import websocket from "@/websocket";
import {
getDeviceBatteryAPI
} from "@/apis";
import useStore from "@/store";
import {
storeToRefs
} from "pinia";
import audioManager from "./audioManager";
const store = useStore();
const {
user
} = storeToRefs(store);
const {
updateUser,
updateOnline
} = store;
watch(
() => user.value.id,
(newVal) => {
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
if (newVal && token) {
websocket.createWebSocket(token, (content) => {
uni.$emit("socket-inbox", content);
});
}
if (!newVal) {
websocket.closeWebSocket();
}
},
{
deep: false, // 如果 user 是一个对象或数组,建议开启
immediate: false, // 若想在初始化时立即执行一次回调,可开启。
}
);
watch(
() => user.value.id,
(newVal) => {
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
if (newVal && token) {
websocket.createWebSocket(token, onShootWsMsg);
}
if (!newVal) {
websocket.closeWebSocket();
}
}, {
deep: false, // 如果 user 是一个对象或数组,建议开启
immediate: false, // 若想在初始化时立即执行一次回调,可开启。
}
);
onShow(() => {
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
if (user.value.id && token) {
console.log("回到前台,重新连接 websocket");
websocket.createWebSocket(token, (content) => {
uni.$emit("socket-inbox", content);
});
}
});
function emitUpdateUser(value) {
updateUser(value);
}
onHide(() => {
websocket.closeWebSocket();
});
async function emitUpdateOnline() {
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
}
function onDeviceShoot() {
// audioManager.play("射箭声音")
}
function onShootWsMsg(content) {
if(content.type === 'shoot-trigger'){
onDeviceShoot()
}
uni.$emit("socket-inbox", content);
}
onShow(() => {
uni.$on("update-user", emitUpdateUser);
uni.$on("update-online", emitUpdateOnline);
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
if (user.value.id && token) {
console.log("回到前台,重新连接 websocket");
websocket.createWebSocket(token, onShootWsMsg);
}
});
onHide(() => {
uni.$off("update-user", emitUpdateUser);
uni.$off("update-online", emitUpdateOnline);
websocket.closeWebSocket();
});
</script>
<style>
page {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
background-color: #000;
}
page {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
background-color: #000;
}
button {
margin: 0;
padding: 0;
border: none;
background: none;
line-height: 1;
outline: none;
box-sizing: border-box;
}
button {
margin: 0;
padding: 0;
border: none;
background: none;
line-height: 1;
outline: none;
box-sizing: border-box;
}
view::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
view::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
button::after {
border: none;
}
button::after {
border: none;
}
.guide-tips {
display: flex;
flex-direction: column;
}
.guide-tips > text:first-child {
color: #fed847;
}
.guide-tips {
display: flex;
flex-direction: column;
font-size: 28rpx;
}
@keyframes fadeInOut {
0% {
transform: translateY(20px);
opacity: 0;
}
30% {
transform: translateY(0);
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.guide-tips>text:first-child {
color: #fed847;
}
.fade-in-out {
animation: fadeInOut 1.2s ease forwards;
}
.guide-tips>text:nth-child(2) {
font-size: 24rpx;
}
@keyframes fadeOut {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(20px);
opacity: 0;
}
}
@keyframes fadeInOut {
0% {
transform: translateY(20px);
opacity: 0;
}
.fade-out {
animation: fadeOut 0.3s ease forwards;
}
30% {
transform: translateY(0);
opacity: 1;
}
@keyframes scaleIn {
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
80% {
opacity: 1;
}
.scale-in {
animation: scaleIn 0.3s ease-out forwards;
transform-origin: center center;
}
100% {
opacity: 0;
}
}
@keyframes scaleOut {
from {
transform: scale(1);
opacity: 1;
}
to {
transform: scale(0);
opacity: 0;
}
}
.fade-in-out {
animation: fadeInOut 1.2s ease forwards;
}
.scale-out {
animation: scaleOut 0.3s ease-out forwards;
transform-origin: center center;
}
@keyframes fadeOut {
from {
transform: translateY(0);
opacity: 1;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
to {
transform: translateY(20px);
opacity: 0;
}
}
@keyframes pumpIn {
from {
transform: scale(2);
}
to {
transform: scale(1);
}
}
.fade-out {
animation: fadeOut 0.3s ease forwards;
}
.pump-in {
animation: pumpIn 0.3s ease-out forwards;
transform-origin: center center;
}
@keyframes scaleIn {
from {
transform: scale(0);
opacity: 0;
}
.share-canvas {
width: 300px;
height: 534px;
position: absolute;
top: -1000px;
left: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.scale-in {
animation: scaleIn 0.3s ease-out forwards;
transform-origin: center center;
}
.modal {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.user-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
padding-top: 7px;
position: relative;
}
.half-time-tip {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.half-time-tip > text:last-child {
margin-top: 20px;
color: #fff9;
}
.see-more {
display: flex;
align-items: center;
justify-content: center;
margin-top: 5px;
}
.see-more > text {
color: #39a8ff;
margin-top: 2px;
font-size: 13px;
}
.see-more > image {
width: 15px;
margin-top: 2px;
}
</style>
@keyframes scaleOut {
from {
transform: scale(1);
opacity: 1;
}
to {
transform: scale(0);
opacity: 0;
}
}
.scale-out {
animation: scaleOut 0.3s ease-out forwards;
transform-origin: center center;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pumpIn {
from {
transform: scale(2);
}
to {
transform: scale(1);
}
}
.pump-in {
animation: pumpIn 0.3s ease-out forwards;
transform-origin: center center;
}
.share-canvas {
width: 300px;
height: 530px;
position: absolute;
top: -1000px;
left: 0;
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.modal {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.user-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
padding-top: 7px;
position: relative;
}
.half-time-tip {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.half-time-tip>text:last-child {
margin-top: 20px;
color: #fff9;
}
.see-more {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
}
.see-more>text {
color: #39a8ff;
font-size: 13px;
}
.see-more>image {
width: 15px;
}
@font-face {
font-family: "DINCondensed";
src: url("https://static.shelingxingqiu.com/font/DIN-Condensed-Bold-2.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
</style>

View File

@@ -1,520 +1,498 @@
let BASE_URL = "https://api.shelingxingqiu.com/api/shoot"; // 默认正式版
try {
const accountInfo = uni.getAccountInfoSync();
const envVersion = accountInfo.miniProgram.envVersion;
const accountInfo = uni.getAccountInfoSync();
const envVersion = accountInfo.miniProgram.envVersion;
switch (envVersion) {
case "develop": // 开发版
// BASE_URL = "http://192.168.1.242:8000/api/shoot";
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
break;
case "trial": // 体验版
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
break;
case "release": // 正式版
BASE_URL = "https://api.shelingxingqiu.com/api/shoot";
break;
default:
// 保持默认值
break;
}
switch (envVersion) {
case "develop": // 开发版
BASE_URL = "http://localhost:8000/api/shoot";
// BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
break;
case "trial": // 体验版
BASE_URL = "https://apitest.shelingxingqiu.com/api/shoot";
break;
case "release": // 正式版
BASE_URL = "https://api.shelingxingqiu.com/api/shoot";
break;
default:
// 保持默认值
break;
}
} catch (e) {
console.error("获取环境信息失败,使用默认正式环境", e);
console.error("获取环境信息失败,使用默认正式环境", e);
}
function request(method, url, data = {}) {
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
const header = {};
if (token) header.Authorization = `Bearer ${token || ""}`;
return new Promise((resolve, reject) => {
uni.request({
url: `${BASE_URL}${url}`,
method,
header,
data,
timeout: 10000,
success: (res) => {
if (res.data) {
const { code, data, message } = res.data;
if (code === 0) resolve(data);
else if (message) {
if (message.indexOf("登录身份已失效") !== -1) {
uni.removeStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
}
if (message === "ROOM_FULL") {
resolve({ full: true });
return;
}
if (message === "ERROR_ROOM_GAME_START") {
resolve({ started: true });
return;
}
if (url.indexOf("/user/room") !== -1 && method === "GET") {
resolve({});
return;
}
if (message === "ERROR_BATTLE_GAMING") {
resolve({});
return;
}
if (message === "BIND_DEVICE") {
resolve({ binded: true });
return;
}
if (message === "ERROR_ORDER_UNPAY") {
uni.showToast({
title: "当前有未支付订单",
icon: "none",
});
resolve({});
return;
}
if (message === "ROOM_EMPTY") {
return uni.showToast({
title: "房间已过期",
icon: "none",
});
}
uni.showToast({
title: message,
icon: "none",
});
}
reject("");
}
},
fail: (err) => {
handleRequestError(err, url);
reject(err);
},
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
const header = {};
if (token) header.Authorization = `Bearer ${token || ""}`;
return new Promise((resolve, reject) => {
uni.request({
url: `${BASE_URL}${url}`,
method,
header,
data,
timeout: 10000,
success: (res) => {
if (res.data) {
const {code, data, message} = res.data;
if (code === 0) resolve(data);
else if (message) {
if (message.indexOf("登录身份已失效") !== -1) {
uni.removeStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
uni.$emit("update-user");
}
if (message === "ROOM_FULL") {
resolve({full: true});
return;
}
if (message === "ERROR_ROOM_GAME_START") {
resolve({started: true});
return;
}
if (url.indexOf("/user/room") !== -1 && method === "GET") {
resolve({});
return;
}
if (message === "ERROR_BATTLE_GAMING") {
resolve({});
return;
}
if (message === "BIND_DEVICE") {
resolve({binded: true});
return;
}
if (message === "ERROR_ORDER_UNPAY") {
uni.showToast({
title: "当前有未支付订单",
icon: "none",
});
resolve({});
return;
}
if (message === "ROOM_EMPTY") {
return uni.showToast({
title: "房间已过期",
icon: "none",
});
}
uni.showToast({
title: message,
icon: "none",
});
}
reject("");
}
},
fail: (err) => {
handleRequestError(err, url);
reject(err);
},
});
});
});
}
// 统一的错误处理函数
function handleRequestError(err, url) {
console.log("请求失败:", { err, url });
console.log("请求失败:", {err, url});
// 根据错误类型显示不同提示
if (err.errMsg) {
if (err.errMsg.includes("timeout")) {
showCustomToast("请求超时,请稍后重试", "timeout");
} else if (err.errMsg.includes("fail")) {
// 检查网络状态
uni.getNetworkType({
success: (res) => {
if (res.networkType === "none") {
showCustomToast("网络连接已断开,请检查网络设置", "network");
} else {
showCustomToast("服务器连接失败,请稍后重试", "server");
}
},
fail: () => {
showCustomToast("网络异常,请检查网络连接", "unknown");
},
});
// 根据错误类型显示不同提示
if (err.errMsg) {
if (err.errMsg.includes("timeout")) {
showCustomToast("请求超时,请稍后重试", "timeout");
} else if (err.errMsg.includes("fail")) {
// 检查网络状态
uni.getNetworkType({
success: (res) => {
if (res.networkType === "none") {
showCustomToast("网络连接已断开,请检查网络设置", "network");
} else {
showCustomToast("服务器连接失败,请稍后重试", "server");
}
},
fail: () => {
showCustomToast("网络异常,请检查网络连接", "unknown");
},
});
} else {
showCustomToast("请求失败,请稍后重试", "general");
}
} else {
showCustomToast("请求失败,请稍后重试", "general");
showCustomToast("网络异常,请稍后重试", "unknown");
}
} else {
showCustomToast("网络异常,请稍后重试", "unknown");
}
}
// 自定义提示函数
function showCustomToast(message, type) {
const config = {
title: message,
icon: "none",
duration: 3000,
};
const config = {
title: message,
icon: "none",
duration: 3000,
};
// 根据错误类型可以添加不同的处理逻辑
switch (type) {
case "timeout":
config.duration = 4000; // 超时提示显示更久
break;
case "network":
config.duration = 5000; // 网络问题提示显示更久
break;
default:
break;
}
// 根据错误类型可以添加不同的处理逻辑
switch (type) {
case "timeout":
config.duration = 4000; // 超时提示显示更久
break;
case "network":
config.duration = 5000; // 网络问题提示显示更久
break;
default:
break;
}
uni.showToast(config);
uni.showToast(config);
}
// 获取全局配置
export const getAppConfig = () => {
return request("GET", "/index/appConfig");
return request("GET", "/index/appConfig");
};
export const getHomeData = (seasonId) => {
return request("GET", `/user/myHome?seasonId=${seasonId}`);
return request("GET", `/user/myHome?seasonId=${seasonId}`);
};
export const getProvinceData = () => {
return request("GET", "/index/provinces/list");
return request("GET", "/index/provinces/list");
};
export const loginAPI = async (nickName, avatarData, code) => {
const result = await request("POST", "/index/code", {
appName: "shoot",
appId: "wxa8f5989dcd45cc23",
nickName,
avatarData,
code,
});
uni.setStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`,
result.token
);
return result;
export const loginAPI = async (phone, nickName, avatarData, code) => {
const result = await request("POST", "/index/code", {
appName: "shoot",
appId: "wxa8f5989dcd45cc23",
nickName,
avatarData,
code,
phone,
});
uni.setStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`,
result.token
);
return result;
};
export const silentLoginAPI = async (code) => {
const result = await request("POST", "/index/code", {
appName: "shoot",
appId: "wxa8f5989dcd45cc23",
code,
});
uni.setStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`,
result.token
);
return result;
};
export const checkUserBindAPI = async (code) => {
return request("POST", "/index/checkBind", {
appName: "shoot",
appId: "wxa8f5989dcd45cc23",
code,
});
};
export const bindDeviceAPI = (device) => {
return request("POST", "/user/device/bindDevice", {
device,
});
return request("POST", "/user/device/bindDevice", {
device,
});
};
export const bindDeviceAPIV2 = (token) => {
return request("POST", "/user/device/bindDevice/v2", {
token: token,
});
};
export const unbindDeviceAPI = (deviceId) => {
return request("POST", "/user/device/unbindDevice", {
deviceId,
});
return request("POST", "/user/device/unbindDevice", {
deviceId,
});
};
export const getMyDevicesAPI = () => {
// "/user/device/getBinding?deviceId=9ZF9oVXs"
return request("GET", "/user/device/getBindings");
// "/user/device/getBinding?deviceId=9ZF9oVXs"
return request("GET", "/user/device/getBindings");
};
export const createPractiseAPI = (arrows) => {
return request("POST", "/user/practice/create", {
arrows,
});
export const createPractiseAPI = (arrows, time, target) => {
return request("POST", "/user/practice/create", {
shootNumber: arrows,
shootTime: time,
targetType: target * 20,
});
};
export const startPractiseAPI = () => {
return request("POST", "/user/practice/begin");
};
export const endPractiseAPI = () => {
return request("POST", "/user/practice/stop");
};
export const getPractiseAPI = async (id) => {
const result = await request("GET", `/user/practice/get?id=${id}`);
const data = { ...(result.UserPracticeRound || {}) };
if (data.arrows) data.arrows = JSON.parse(data.arrows);
return data;
return request("GET", `/user/practice/get?id=${id}`);
};
export const createRoomAPI = (gameType, teamSize) => {
return request("POST", "/user/createroom", {
gameType,
teamSize,
});
export const createRoomAPI = (gameType, teamSize, targetType) => {
return request("POST", "/user/createroom", {
gameType,
teamSize,
targetType,
});
};
export const getRoomAPI = (number) => {
return request("GET", `/user/room?number=${number}`);
return request("GET", `/user/room?number=${number}`);
};
export const joinRoomAPI = (number) => {
return request("POST", `/user/room/join`, { number });
return request("POST", `/user/room/join`, {number});
};
export const destroyRoomAPI = (roomNumber) => {
return request("POST", "/user/room/destroyRoom", {
roomNumber,
});
return request("POST", "/user/room/destroyRoom", {
roomNumber,
});
};
export const exitRoomAPI = (number) => {
return request("POST", "/user/room/exitRoom", {
number,
});
export const exitRoomAPI = (number, userId) => {
return request("POST", "/user/room/exitRoom", {
number,
userId,
});
};
export const startRoomAPI = (number) => {
return request("POST", "/user/room/start", { number });
return request("POST", "/user/room/start", {number});
};
export const getPractiseResultListAPI = async (page = 1, page_size = 15) => {
const reuslt = await request(
"GET",
`/user/practice/list?page=${page}&page_size=${page_size}`
);
return reuslt.list;
const reuslt = await request(
"GET",
`/user/practice/list?page=${page}&page_size=${page_size}`
);
return reuslt.list;
};
export const matchGameAPI = (match, gameType, teamSize) => {
return request("POST", "/user/game/match", {
match,
gameType,
teamSize,
});
return request("POST", "/user/game/match", {
match,
gameType,
teamSize,
readyTime: 15,
targetType: 20,
});
};
export const readyGameAPI = (battleId) => {
return request("POST", "/user/game/prepare", {
battleId,
});
};
export const getGameAPI = async (battleId) => {
const result = await request("POST", "/user/battle/detail", {
id: battleId,
});
if (!result.battleStats) return {};
const {
battleStats = {},
playerStats = {},
goldenRoundRecords = [],
} = result;
const data = {
id: battleId,
mode: battleStats.mode, // 1.几V几 2.大乱斗
gameMode: battleStats.gameMode, // 1.约战 2.排位
teamSize: battleStats.teamSize,
};
if (battleStats && battleStats.mode === 1) {
data.winner = battleStats.winner;
data.roundsData = {};
data.redPlayers = {};
data.bluePlayers = {};
data.mvps = [];
data.goldenRounds =
goldenRoundRecords && goldenRoundRecords.length ? goldenRoundRecords : [];
playerStats.forEach((item) => {
const { playerBattleStats = {}, roundRecords = [] } = item;
if (playerBattleStats.team === 0) {
data.redPlayers[playerBattleStats.playerId] = playerBattleStats;
}
if (playerBattleStats.team === 1) {
data.bluePlayers[playerBattleStats.playerId] = playerBattleStats;
}
if (playerBattleStats.mvp) {
data.mvps.push(playerBattleStats);
}
roundRecords.forEach((round) => {
data.roundsData[round.roundNumber] = {
...data.roundsData[round.roundNumber],
[round.playerId]: round.arrowHistory,
};
});
return request("POST", "/user/game/prepare", {
battleId,
});
const totalRounds = Object.keys(data.roundsData).length;
(goldenRoundRecords || []).forEach((item, index) => {
item.arrowHistory.forEach((arrow) => {
if (!data.roundsData[totalRounds + index + 1]) {
data.roundsData[totalRounds + index + 1] = {};
}
if (!data.roundsData[totalRounds + index + 1][arrow.playerId]) {
data.roundsData[totalRounds + index + 1][arrow.playerId] = [];
}
data.roundsData[totalRounds + index + 1][arrow.playerId].push(arrow);
});
});
data.mvps.sort((a, b) => b.totalRings - a.totalRings);
}
if (battleStats && battleStats.mode === 2) {
data.players = [];
playerStats.forEach((item) => {
data.players.push({
...item.playerBattleStats,
arrowHistory: item.roundRecords[0].arrowHistory,
});
});
data.players = data.players.sort((a, b) => b.totalScore - a.totalScore);
}
// console.log("game result:", result);
// console.log("format data:", data);
return data;
};
export const simulShootAPI = (device_id, x, y) => {
const data = {
device_id,
};
if (x !== undefined && y !== undefined) {
data.x = x;
data.y = y;
}
return request("POST", "/index/arrow", data);
const data = {
device_id,
};
if (x !== undefined && y !== undefined) {
data.x = x;
data.y = y;
}
return request("POST", "/index/arrow", data);
};
export const getBattleListAPI = async (page, battleType) => {
const data = [];
const result = await request("POST", "/user/battle/details/list", {
page,
battleType,
modeType: 0,
});
(result.Battles || []).forEach((item) => {
let name = "";
if (item.battleStats.mode === 1) {
name = `${item.playerStats.length / 2}V${item.playerStats.length / 2}`;
}
if (item.battleStats.mode === 2) {
name = `${item.playerStats.length}人大乱斗`;
}
data.push({
name,
battleId: item.battleStats.battleId,
mode: item.battleStats.mode,
createdAt: item.battleStats.createdAt,
gameEndAt: item.battleStats.gameEndAt,
winner: item.battleStats.winner,
players: item.playerStats
.map((p) => p.playerBattleStats)
.sort((a, b) => b.totalScore - a.totalScore),
redPlayers: item.playerStats
.filter((p) => p.playerBattleStats.team === 0)
.map((p) => p.playerBattleStats),
bluePlayers: item.playerStats
.filter((p) => p.playerBattleStats.team === 1)
.map((p) => p.playerBattleStats),
const result = await request("POST", "/user/battle/details/list", {
page,
pageSize: 10,
battleType,
});
});
return data;
return result.list;
};
export const getRankListAPI = () => {
return request("GET", "/index/ranklist");
return request("GET", "/index/ranklist");
};
export const createOrderAPI = (vipId) => {
return request("POST", "/user/order/create", {
vipId,
quanity: 1,
tradeType: "mini",
payType: "wxpay",
});
return request("POST", "/user/order/create", {
vipId,
quanity: 1,
tradeType: "mini",
payType: "wxpay",
});
};
export const payOrderAPI = (id) => {
return request("POST", "/user/order/pay", {
id,
tradeType: "mini",
payType: "wxpay",
});
return request("POST", "/user/order/pay", {
id,
tradeType: "mini",
payType: "wxpay",
});
};
export const getOrderListAPI = async (page) => {
const reuslt = await request("GET", `/user/order/list?page=${page}`);
return reuslt.items || [];
const reuslt = await request("GET", `/user/order/list?page=${page}`);
return reuslt.items || [];
};
export const cancelOrderListAPI = async (id) => {
return request("POST", "/user/order/cancelOrder", { id });
return request("POST", "/user/order/cancelOrder", {id});
};
export const isGamingAPI = async () => {
const result = await request("GET", "/user/isGaming");
return result.gaming || false;
};
export const getCurrentGameAPI = async () => {
uni.$emit("update-header-loading", true);
const result = await request("GET", "/user/join/battle");
return result.currentGame || {};
export const getUserGameState = () => {
return request("GET", "/user/state");
};
export const getPointBookConfigAPI = async () => {
return request("GET", "/user/score/sheet/option");
return request("GET", "/user/score/sheet/option");
};
export const savePointBookAPI = async (
bowType,
distance,
targetType,
groups,
arrows,
data = []
) => {
return request("POST", "/user/score/sheet/report", {
bowType,
distance,
targetType,
groups,
arrows,
group_data: data.map((item) =>
item.map((i) => ({
...i,
ring: i.ring === "M" ? -1 : i.ring === "X" ? 0 : Number(i.ring),
}))
),
});
data = []
) => {
return request("POST", "/user/score/sheet/report", {
bowType,
distance,
targetType,
groups,
arrows,
group_data: data.map((item) =>
item.map((i) => ({
...i,
ring: i.ring === "M" ? -1 : i.ring === "X" ? 0 : Number(i.ring),
}))
),
});
};
export const getPointBookListAPI = async (
page = 1,
bowType,
distance,
targetType
page = 1,
bowType,
distance,
targetType
) => {
let url = `/user/score/sheet/list?pageNum=${page}&pageSize=10`;
if (bowType) url += `&bowType=${bowType}`;
if (distance) url += `&distance=${distance}`;
if (targetType) url += `&targetType=${targetType}`;
const result = await request("GET", url);
return result.list || [];
let url = `/user/score/sheet/list?pageNum=${page}&pageSize=10`;
if (bowType) url += `&bowType=${bowType}`;
if (distance) url += `&distance=${distance}`;
if (targetType) url += `&targetType=${targetType}`;
const result = await request("GET", url);
return result.list || [];
};
export const getPointBookDetailAPI = async (id) => {
return request("GET", `/user/score/sheet/detail?id=${id}`);
return request("GET", `/user/score/sheet/detail?id=${id}`);
};
export const getPointBookDataAPI = async () => {
return request("GET", "/user/score/sheet/statistics");
return request("GET", "/user/score/sheet/statistics");
};
export const getPractiseDataAPI = async () => {
return request("GET", "/user/practice/statistics");
return request("GET", "/user/practice/statistics");
};
export const getBattleDataAPI = async () => {
return request("GET", "/user/fight/statistics");
return request("GET", "/user/fight/statistics");
};
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");
return request("GET", "/index/memberVipDescribe");
};
export const getPointBookStatisticsAPI = async () => {
return request("GET", `/v2/user/score/sheet/statistics`);
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,
});
return request("POST", `/user/donate`, {
amount,
name,
phone,
organizer,
advice,
});
};
export const laserAimAPI = async () => {
return request("POST", "/user/device/laserAim");
return request("POST", "/user/device/laserAim");
};
export const laserCloseAPI = async () => {
return request("POST", "/user/device/closeAim");
return request("POST", "/user/device/closeAim");
};
export const getDeviceBatteryAPI = async () => {
return request("GET", "/user/device/battery");
return request("GET", "/user/device/battery");
};
export const addNoteAPI = async (id, remark) => {
return request("POST", "/user/score/sheet/remark", { 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}`);
return request("DELETE", `/user/score/sheet/delete?id=${id}`);
};
export const getPhoneNumberAPI = (data) => {
return request("POST", "/index/getPhone", data);
};
export const getPointBookRankListAPI = (page = 1) => {
return request(
"GET",
`/user/score/sheet/week/shoot/rank/list?pageNum=${page}&pageSize=100`
);
};
export const clickLikeAPI = (userId, ifLike) => {
return request("POST", "/user/score/sheet/week/shoot/rank/like", {
userId,
ifLike,
});
};
export const getMyLikeList = (page = 1, pageSize = 10) => {
return request(
"GET",
`/user/score/sheet/week/shoot/rank/like/list?pageNum=${page}&pageSize=${pageSize}`
);
};
export const getReadyAPI = (roomId) => {
return request("POST", `/user/room/ready`, {
roomId,
});
};
export const getBattleAPI = async (battleId) => {
return request("POST", "/user/match/info", {
id: battleId,
});
};
export const kickPlayerAPI = (number, userId) => {
return request("POST", "/user/room/kicking", {
number,
userId,
});
};

View File

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

1012
src/canvas.js Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -73,7 +73,7 @@ defineProps({
<text class="player-name">{{ player.name }}</text>
</view>
<image
v-if="winner === 0"
v-if="winner === 2"
src="../static/winner-badge.png"
mode="widthFix"
class="right-winner-badge"

View File

@@ -21,6 +21,10 @@ const props = defineProps({
type: Array,
default: () => [],
},
total: {
type: Number,
default: 0,
},
});
</script>
@@ -45,16 +49,16 @@ const props = defineProps({
<view class="desc">
<text>{{ arrows.length }}</text>
<text>支箭</text>
<text>{{ arrows.reduce((a, b) => a + b.ring, 0) }}</text>
<text>{{ arrows.reduce((a, b) => a + (b.ring || 0), 0) }}</text>
<text></text>
</view>
<ScorePanel
:completeEffect="false"
:rowCount="arrows.length === 12 ? 6 : 9"
:total="arrows.length"
:scores="arrows.map((a) => a.ring)"
:margin="arrows.length === 12 ? 4 : 1"
:fontSize="arrows.length === 12 ? 25 : 22"
:rowCount="total === 12 ? 6 : 9"
:total="total"
:arrows="arrows"
:margin="total === 12 ? 4 : 1"
:fontSize="total === 12 ? 25 : 22"
/>
</view>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,14 @@
<script setup>
import { ref } from "vue";
import SButton from "@/components/SButton.vue";
import { joinRoomAPI, createRoomAPI, isGamingAPI } from "@/apis";
import { joinRoomAPI, createRoomAPI } from "@/apis";
import { debounce } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, game } = storeToRefs(store);
const props = defineProps({
onConfirm: {
@@ -11,13 +18,12 @@ const props = defineProps({
});
const battleMode = ref(1);
const step = ref(1);
const targetMode = ref(1);
const loading = ref(false);
const roomNumber = ref("");
const createRoom = async () => {
const isGaming = await isGamingAPI();
if (isGaming) {
const createRoom = debounce(async () => {
if (game.value.inBattle) {
uni.$showHint(1);
return;
}
@@ -27,79 +33,79 @@ const createRoom = async () => {
if (battleMode.value === 2) size = 10;
if (battleMode.value === 3) size = 4;
if (battleMode.value === 4) size = 6;
const result = await createRoomAPI(
battleMode.value === 2 ? 2 : 1,
battleMode.value === 2 ? 10 : size
);
if (result.number) roomNumber.value = result.number;
step.value = 2;
loading.value = false;
};
const enterRoom = async () => {
step.value = 1;
props.onConfirm();
await joinRoomAPI(roomNumber.value);
uni.navigateTo({
url: `/pages/battle-room?roomNumber=${roomNumber.value}`,
});
};
const setClipboardData = () => {
uni.setClipboardData({
data: roomNumber.value,
success() {
uni.showToast({ title: "复制成功" });
},
});
};
try {
const result = await createRoomAPI(
battleMode.value === 2 ? 2 : 1,
battleMode.value === 2 ? 10 : size,
targetMode.value*20,
);
if (result.number) {
props.onConfirm();
await joinRoomAPI(result.number);
uni.navigateTo({
url: "/pages/battle-room?roomNumber=" + result.number + "&target=" + targetMode.value,
});
}
} catch (error) {
console.log(error);
} finally {
loading.value = false;
}
});
</script>
<template>
<view class="container">
<image
v-if="step === 1"
src="../static/choose-battle-mode.png"
mode="widthFix"
/>
<view v-if="step === 1" class="create-options">
<view class="target-options-header">
<view class="target-options-header-line-left"></view>
<image class="target-options-header-title-img" src="../static/choose-battle-mode.png" mode="widthFix" />
<view class="target-options-header-line-right"></view>
</view>
<view class="create-options">
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 1 }"
@click="() => (battleMode = 1)"
>
<text>对抗模式1V1</text>
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 2 }"
@click="() => (battleMode = 2)"
>
<text>乱斗模式3-10</text>
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 3 }"
@click="() => (battleMode = 3)"
>
<text>对抗模式2V2</text>
<!-- <text>敬请期待</text> -->
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 4 }"
@click="() => (battleMode = 4)"
>
<text>对抗模式3V3</text>
<!-- <text>敬请期待</text> -->
</view>
</view>
<SButton v-if="step === 1" :onClick="createRoom">下一步</SButton>
<view v-if="step === 2" class="room-info">
<view>
<text>房间号</text>
<text>{{ roomNumber }}</text>
</view>
<view class="copy-room-number" @click="setClipboardData"
>复制房间信息邀请朋友进入</view
<view
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 2 }"
@click="() => (battleMode = 2)"
>
<SButton width="70vw" :onClick="enterRoom">进入房间</SButton>
<text>30分钟无人进入则房间无效</text>
<text>乱斗模式3-10</text>
</view>
</view>
<view class="target-options-header">
<view class="target-options-header-line-left"></view>
<view class="target-options-header-title">选择靶纸</view>
<view class="target-options-header-line-right"></view>
</view>
<view class="target-options">
<view
:class="{ 'battle-btn': true, 'battle-choosen': targetMode === 1 }"
@click="() => (targetMode = 1)"
>
<text>20厘米全环靶</text>
</view>
<view
:class="{ 'battle-btn': true, 'battle-choosen': targetMode === 2 }"
@click="() => (targetMode = 2)"
>
<text>40厘米全环靶</text>
</view>
</view>
<SButton :onClick="createRoom">创建房间</SButton>
</view>
</template>
@@ -110,6 +116,7 @@ const setClipboardData = () => {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 44rpx;
}
.container > image:first-child {
width: 45%;
@@ -124,6 +131,50 @@ const setClipboardData = () => {
justify-content: center;
margin-bottom: 15px;
}
.target-options-header{
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
}
.target-options-header-title-img{
width: 196rpx;
height: 40rpx;
}
.target-options-header-title{
width: 112rpx;
height: 40rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 28rpx;
text-align: center;
font-style: normal;
text-transform: none;
color: #FFEFBA;
margin: 0 18rpx;
}
.target-options-header-line-left{
width: 214rpx;
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 1rpx solid;
border-image: linear-gradient(90deg, rgba(133, 119, 96, 0), rgba(133, 119, 96, 1)) 1 1;
}
.target-options-header-line-right{
width: 214rpx;
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 1rpx solid;
border-image: linear-gradient(90deg, rgba(133, 119, 96, 1), rgba(133, 119, 96, 0)) 1 1;
}
.target-options {
width: 100%;
padding: 0 10px;
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 15px;
}
.battle-btn {
width: 45%;
height: 55px;
@@ -142,42 +193,4 @@ const setClipboardData = () => {
border: 4rpx solid #fff3;
border-color: #fed847;
}
/* .battle-close {
background-color: #8889;
color: #b3b3b3;
}
.battle-close > text:last-child {
font-size: 12px;
} */
.room-info {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding-top: 40px;
}
.room-info > view:first-child {
font-size: 22px;
color: #fff;
margin-bottom: 20px;
}
.room-info > text {
color: #888686;
font-size: 14px;
margin: 10px 0;
}
.room-info > view:last-child {
color: #287fff;
margin: 20px 0;
font-size: 14px;
}
.copy-room-number {
width: calc(70vw - 20px);
color: #fed847;
border: 1px solid #fed847;
padding: 10px;
text-align: center;
border-radius: 10px;
margin-bottom: 20px;
}
</style>

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
<script setup>
defineProps({
noBg: {
type: Boolean,
default: false,
}
});
</script>
<template>
<view class="container">
<image class="shooter2" src="../static/shooter2.png" mode="widthFix" />
<view class="bg-box">
<image
class="bg"
v-if="!noBg"
src="../static/long-bubble-border.png"
mode="widthFix"
/>
<slot />
</view>
</view>
</template>
<style scoped>
.container {
display: flex;
align-items: center;
padding: 0 15px;
margin-bottom: 14rpx;
width: clac(100% - 30px);
}
.container .shooter2 {
width: 150rpx;
height: 162rpx;
}
.container .bg-box {
color: #fff;
font-size: 28rpx;
position: relative;
flex: 1;
min-height: 55px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.container .bg-box .bg {
position: absolute;
left: 0;
right: 0;
width: 100%;
}
</style>

View File

@@ -6,7 +6,7 @@ import Avatar from "@/components/Avatar.vue";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const { user, game } = storeToRefs(store);
const currentPage = computed(() => {
const pages = getCurrentPages();
@@ -51,17 +51,15 @@ const toUserPage = () => {
const signin = () => {
if (!user.value.id) {
uni.navigateTo({
url: "/pages/sign-in",
});
uni.$emit("point-book-signin");
}
};
const loading = ref(false);
const showLoader = ref(false);
const pointBook = ref(null);
const showProgress = ref(false);
const heat = ref(0);
const updateLoading = (value) => {
loading.value = value;
};
@@ -73,23 +71,21 @@ const updateHot = (value) => {
onMounted(() => {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
if (currentPage.route === "pages/point-book-edit") {
pointBook.value = uni.getStorageSync("point-book");
}
if (
currentPage.route === "pages/team-battle" ||
currentPage.route === "pages/melee-match"
currentPage.route === "pages/point-book-edit" ||
currentPage.route === "pages/point-book-detail"
) {
showLoader.value = true;
pointBook.value = uni.getStorageSync("point-book");
if (!pointBook.value) {
pointBook.value = uni.getStorageSync("last-point-book");
}
}
if (currentPage.route === "pages/team-battle") {
showProgress.value = true;
}
uni.$on("update-header-loading", updateLoading);
uni.$on("update-hot", updateHot);
});
onBeforeUnmount(() => {
uni.$off("update-header-loading", updateLoading);
uni.$off("update-hot", updateHot);
});
</script>
@@ -157,12 +153,6 @@ onBeforeUnmount(() => {
</view>
</block>
</view>
<image
:style="{ opacity: showLoader && loading ? 1 : 0 }"
src="../static/btn-loading.png"
mode="widthFix"
class="loading"
/>
<view v-if="pointBook" class="point-book-info">
<text>{{ pointBook.bowType.name }}</text>
<text>{{ pointBook.distance }} 米</text>
@@ -183,6 +173,16 @@ onBeforeUnmount(() => {
<view v-if="showProgress" class="battle-progress">
<HeaderProgress />
</view>
<!-- 对战房间:整个胶囊为分享按钮,房号从 Store 读取 -->
<button
v-if="currentPage === 'pages/battle-room' && game.roomNumber"
open-type="share"
hover-class="none"
class="battle-room-number"
>
<text class="battle-room-number__text">房号: {{ game.roomNumber }}</text>
<image src="../static/share2.png" mode="widthFix" class="battle-room-number__icon" />
</button>
</view>
</template>
@@ -226,14 +226,6 @@ onBeforeUnmount(() => {
font-size: 16px;
color: #fff;
}
.loading {
width: 20px;
height: 20px;
margin-left: 10px;
transition: all 0.3s ease;
background-blend-mode: darken;
animation: rotate 2s linear infinite;
}
.point-book-info {
color: #333;
position: fixed;
@@ -269,6 +261,7 @@ onBeforeUnmount(() => {
}
.user-header > image:last-child {
width: 36rpx;
height: 36rpx;
}
.user-header > text:nth-child(2) {
font-weight: 500;
@@ -277,4 +270,37 @@ onBeforeUnmount(() => {
margin: 0 20rpx;
max-width: 300rpx;
}
/* 对战房间:整个胶囊作为分享按钮,靠右对齐 */
.battle-room-number {
margin-left: auto;
margin-right: 10rpx;
display: flex;
align-items: center;
justify-content: center;
width: 240rpx;
height: 64rpx;
background: rgba(0, 0, 0, 0.15);
border-radius: 96rpx;
border: 1rpx solid #5b5758;
flex-shrink: 0;
padding: 0;
line-height: normal;
}
/* 重置 button 默认边框 */
.battle-room-number::after {
border: none;
}
.battle-room-number__text {
width: 156rpx;
height: 28rpx;
font-weight: 400;
font-size: 20rpx;
color: #ffffff;
text-align: center;
line-height: 28rpx;
}
.battle-room-number__icon {
width: 25rpx;
height: 26rpx;
}
</style>

View File

@@ -1,7 +1,9 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import audioManager from "@/audioManager";
import { MESSAGETYPES } from "@/constants";
import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
@@ -11,107 +13,78 @@ const tips = ref("");
const melee = ref(false);
const timer = ref(null);
const sound = ref(true);
const currentSound = ref("");
const currentRound = ref(0);
const currentRoundEnded = ref(false);
const ended = ref(false);
const halfTime = ref(false);
const currentShot = ref(0);
const totalShot = ref(0);
const yourTurn = ref(false);
watch(
() => tips.value,
(newVal) => {
let key = "";
if (newVal.includes("红队")) key = "请红方射箭";
if (newVal.includes("蓝队")) key = "请蓝方射箭";
if (!sound.value) return;
let key = [];
if (newVal.includes("重回")) return;
if (currentRoundEnded.value) {
currentRound.value += 1;
// 播放当前轮次语音
audioManager.play(
`${["一", "二", "三", "四", "五"][currentRound.value - 1]}`
);
key.push(`${["一", "二", "三", "四", "五"][currentRound.value]}`);
}
// 延迟播放队伍提示音
setTimeout(
() => {
if (key && !yourTurn.value) audioManager.play(key);
currentRoundEnded.value = false;
yourTurn.value = false;
},
currentRoundEnded.value ? 1000 : 0
key.push(
newVal.includes("你")
? "轮到你了"
: newVal.includes("红队")
? "请红方射箭"
: "请蓝方射箭"
);
audioManager.play(key, false);
currentRoundEnded.value = false;
}
);
const updateSound = () => {
sound.value = !sound.value;
if (!sound.value) audioManager.stop(currentSound.value);
audioManager.setMuted(!sound.value);
};
async function onReceiveMessage(messages = []) {
if (!sound.value || ended.value) return;
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootResult) {
if (melee.value && msg.userId !== user.value.id) return;
if (msg.userId === user.value.id) currentShot.value++;
if (!halfTime.value && msg.target) {
currentSound.value = msg.target.ring
? `${msg.target.ring}`
: "未上靶";
audioManager.play(currentSound.value);
}
} else if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) {
yourTurn.value = user.value.id === msg.userId;
} else if (msg.constructor === MESSAGETYPES.InvalidShot) {
if (msg.userId === user.value.id) {
uni.showToast({
title: "距离不足,无效",
icon: "none",
});
audioManager.play("射击无效");
}
} 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;
audioManager.play("比赛开始");
} else if (msg.constructor === MESSAGETYPES.MeleeAllReady) {
melee.value = true;
halfTime.value = false;
audioManager.play("比赛开始");
} else if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
currentShot.value = 0;
if (msg.preRoundResult && msg.preRoundResult.currentRound) {
currentRound.value = msg.preRoundResult.currentRound;
currentRoundEnded.value = true;
}
} else if (msg.constructor === MESSAGETYPES.HalfTimeOver) {
halfTime.value = true;
audioManager.play("中场休息");
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
audioManager.play("比赛结束");
} else if (msg.constructor === MESSAGETYPES.FinalShoot) {
totalShot.value = 0;
audioManager.play("决金箭轮");
tips.value = "即将开始...";
currentRoundEnded.value = false;
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
ended.value = true;
} else if (msg.constructor === MESSAGETYPES.BackToGame) {
if (msg.battleInfo) {
melee.value = msg.battleInfo.config.mode === 2;
}
async function onReceiveMessage(message) {
if (ended.value) return;
if (Array.isArray(message)) return;
const { type, mode, current, shootData } = message;
if (type === MESSAGETYPESV2.BattleStart) {
melee.value = Boolean(mode > 3);
totalShot.value = mode === 1 ? 3 : 2;
currentRoundEnded.value = true;
audioManager.play("比赛开始");
} else if (type === MESSAGETYPESV2.BattleEnd) {
audioManager.play("比赛结束", false);
} else if (type === MESSAGETYPESV2.ShootResult) {
if (melee.value && current.playerId !== user.value.id) return;
if (current.playerId === user.value.id) currentShot.value++;
if (message.shootData) {
let key = [];
key.push(
shootData.ring
? `${shootData.ringX ? "X" : shootData.ring}`
: "未上靶"
);
if (shootData.angle !== null)
key.push(`${getDirectionText(shootData.angle)}调整`);
audioManager.play(key, false);
}
});
} else if (type === MESSAGETYPESV2.NewRound) {
currentShot.value = 0;
currentRound.value = current.round;
currentRoundEnded.value = true;
} else if (type === MESSAGETYPESV2.InvalidShot) {
uni.showToast({
title: "距离不足,无效",
icon: "none",
});
audioManager.play("射击无效");
}
}
const playSound = (key) => {
currentSound.value = key;
audioManager.play(key);
};
@@ -141,7 +114,7 @@ onBeforeUnmount(() => {
<template>
<view class="container">
<text>{{ tips }}</text>
<text>{{ (tips || "").replace(/你/g, "").replace(/重回/g, "") }}</text>
<text v-if="totalShot > 0"> ({{ currentShot }}/{{ totalShot }}) </text>
<button v-if="!!tips" hover-class="none" @click="updateSound">
<image
@@ -159,6 +132,7 @@ onBeforeUnmount(() => {
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
}
.container > button:last-child {
width: 36px;

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ defineProps({
type: String,
default: "",
},
scores: {
arrows: {
type: Array,
default: () => [],
},
@@ -21,10 +21,6 @@ defineProps({
type: Number,
default: 0,
},
totalRing: {
type: Number,
default: 0,
},
});
const rowCount = new Array(6).fill(0);
</script>
@@ -60,19 +56,19 @@ const rowCount = new Array(6).fill(0);
<view>
<view>
<view v-for="(_, index) in rowCount" :key="index">
<text>{{ scores[index] ? `${scores[index].ring}` : "-" }}</text>
<text>{{ arrows[index] ? `${arrows[index].ring}` : "-" }}</text>
</view>
</view>
<view>
<view v-for="(_, index) in rowCount" :key="index">
<text>{{
scores[index + 6] ? `${scores[index + 6].ring}` : "-"
arrows[index + 6] ? `${arrows[index + 6].ring}` : "-"
}}</text>
</view>
</view>
</view>
<view>
<text>{{ totalRing }}</text>
<text>{{ arrows.reduce((last, next) => last + next.ring, 0) }}</text>
<text>积分{{ totalScore }}</text>
</view>
</view>

View File

@@ -1,4 +1,6 @@
<script setup>
import Avatar from "@/components/Avatar.vue";
const props = defineProps({
total: {
type: Number,
@@ -8,6 +10,10 @@ const props = defineProps({
type: Array,
default: () => [],
},
removePlayer: {
type: Function,
default: () => {},
},
});
const seats = new Array(props.total).fill(1);
</script>
@@ -16,11 +22,16 @@ const seats = new Array(props.total).fill(1);
<view class="players">
<view v-for="(_, index) in seats" :key="index">
<image src="../static/player-bg.png" mode="widthFix" />
<image
v-if="players[index] && players[index].name"
:src="players[index].avatar || '../static/user-icon.png'"
mode="widthFix"
/>
<view v-if="players[index] && players[index].name" class="avatar">
<Avatar
:src="players[index].avatar || '../static/user-icon.png'"
:size="40"
/>
<text
:style="{ opacity: players[index] && !!players[index].state ? 1 : 0 }"
>已准备</text
>
</view>
<view v-else class="player-unknow">
<image src="../static/question-mark.png" mode="widthFix" />
</view>
@@ -28,48 +39,68 @@ const seats = new Array(props.total).fill(1);
players[index].name
}}</text>
<text v-else :style="{ color: '#fff9' }">虚位以待</text>
<view v-if="index === 0" class="founder">创建者</view>
<image
<view v-if="index === 0" class="founder">管理员</view>
<!-- <image
:src="`../static/player-${index + 1}.png`"
mode="widthFix"
class="player-bg"
/>
/> -->
<button
v-if="index > 0 && players[index]"
hover-class="none"
class="remove-player"
@click="() => removePlayer(players[index])"
>
<image src="../static/close-white.png" mode="widthFix" />
</button>
</view>
</view>
</template>
<style scoped>
.players {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
-moz-column-gap: 20px;
column-gap: 14px;
display: grid;
grid-template-columns: repeat(2, 1fr);
row-gap: 20rpx;
column-gap: 25rpx;
margin-bottom: 20px;
font-size: 14px;
padding: 0 14px;
}
.players > view {
width: calc(50% - 7px);
display: flex;
align-items: center;
position: relative;
color: #fff;
height: 100px;
height: 176rpx;
overflow: hidden;
}
.players > view > image:first-child {
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
top: 0;
}
.players > view > image:nth-child(2) {
width: 40px;
height: 40px;
min-height: 40px;
margin: 0 10px;
border: 1px solid #fff;
border-radius: 50%;
.avatar {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24rpx;
margin-top: 16rpx;
}
.avatar > text {
background-color: #2c261fb3;
border: 1rpx solid #a3793f66;
color: #fed847;
font-size: 16rpx;
border-radius: 20rpx;
width: 70rpx;
text-align: center;
margin-top: -16rpx;
position: relative;
height: 28rpx;
line-height: 28rpx;
}
.players > view > text:nth-child(3) {
width: 20vw;
@@ -80,30 +111,48 @@ const seats = new Array(props.total).fill(1);
.founder {
position: absolute;
background-color: #fed847;
top: 6px;
top: 0;
left: 0;
color: #000;
font-size: 10px;
padding: 2px 5px;
border-top-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.player-bg {
/* .player-bg {
position: absolute;
width: 52px;
right: 0;
}
} */
.player-unknow {
width: 40px;
height: 40px;
margin: 0 10px;
border: 1px solid #fff3;
width: 84rpx;
height: 84rpx;
margin: 0 24rpx;
border: 1rpx solid #fff3;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #69686866;
box-sizing: border-box;
}
.player-unknow > image {
width: 40%;
}
.remove-player {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 10rpx;
right: 0;
}
.remove-player > image {
width: 100%;
height: 100%;
opacity: 0.6;
}
</style>

View File

@@ -1,137 +0,0 @@
<script setup>
defineProps({
avatar: {
type: String,
default: "",
},
blueTeam: {
type: Array,
default: () => [],
},
redTeam: {
type: Array,
default: () => [],
},
currentShooterId: {
type: Number,
default: 0,
},
});
</script>
<template>
<view class="container">
<image v-if="avatar" class="avatar" :src="avatar" mode="widthFix" />
<view
v-if="blueTeam.length && redTeam.length"
:style="{ height: 20 + blueTeam.length * 20 + 'px' }"
>
<view
v-for="(player, index) in blueTeam"
:key="index"
:style="{
top: index * 20 + 'px',
zIndex: blueTeam.length - index,
left: 0,
}"
>
<image
class="avatar"
:src="player.avatar || '../static/user-icon.png'"
mode="widthFix"
:style="{
borderColor: currentShooterId === player.id ? '#5fadff' : '#fff',
}"
/>
<text
:style="{
color: currentShooterId === player.id ? '#5fadff' : '#fff',
fontSize: currentShooterId === player.id ? 16 : 12 + 'px',
}"
>
{{ player.name }}
</text>
</view>
</view>
<view
v-if="!avatar"
:style="{
height: 20 + redTeam.length * 20 + 'px',
}"
>
<view
v-for="(player, index) in redTeam"
:key="index"
:style="{
top: index * 20 + 'px',
zIndex: redTeam.length - index,
right: 0,
}"
>
<text
:style="{
color: currentShooterId === player.id ? '#ff6060' : '#fff',
fontSize: currentShooterId === player.id ? 16 : 12 + 'px',
textAlign: 'right',
}"
>
{{ player.name }}
</text>
<image
class="avatar"
:src="player.avatar || '../static/user-icon.png'"
mode="widthFix"
:style="{
borderColor: currentShooterId === player.id ? '#ff6060' : '#fff',
}"
/>
</view>
</view>
</view>
</template>
<style scoped>
.container {
width: calc(100% - 30px);
margin: 0 15px;
margin-top: 5px;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.container > view {
width: 50%;
position: relative;
}
.container > view > view {
position: absolute;
top: -20px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s linear;
}
.container > view > view > text {
margin: 0 10px;
overflow: hidden;
width: 120px;
transition: all 0.3s linear;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.avatar {
width: 40px;
height: 40px;
min-width: 40px;
min-height: 40px;
border: 1px solid #fff;
border-radius: 50%;
}
.red-avatar {
border: 1px solid #ff6060;
}
.blue-avatar {
border: 1px solid #5fadff;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,14 +52,14 @@ onBeforeUnmount(() => {
<view class="point-view1" v-if="bluePoint !== 0 || redPoint !== 0">
<text>本轮蓝队</text>
<text>{{
(roundData.blueArrows || []).reduce(
(roundData.shoots[1] || []).reduce(
(last, next) => last + next.ring,
0
)
}}</text>
<text>红队</text>
<text>{{
(roundData.redArrows || []).reduce(
(roundData.shoots[2] || []).reduce(
(last, next) => last + next.ring,
0
)
@@ -117,10 +117,10 @@ onBeforeUnmount(() => {
display: flex;
flex-direction: column;
align-items: center;
font-size: 16px;
font-size: 32rpx;
}
.round-end-tip > text:first-child {
font-size: 18px;
font-size: 36rpx;
color: #fff;
}
.point-view1 {
@@ -137,7 +137,7 @@ onBeforeUnmount(() => {
}
.point-view2 {
margin: 12px 0;
font-size: 24px;
font-size: 48rpx;
display: flex;
align-items: center;
justify-content: center;
@@ -155,7 +155,8 @@ onBeforeUnmount(() => {
align-items: center;
justify-content: center;
margin-top: 10px;
font-size: 14px;
font-size: 28rpx;
word-break: keep-all;
}
.final-shoot > text:nth-child(1) {
width: 20px;
@@ -163,7 +164,7 @@ onBeforeUnmount(() => {
}
.final-shoot > text:nth-child(1),
.final-shoot > text:nth-child(3) {
font-size: 18px;
font-size: 32rpx;
color: #fed847;
margin-left: 10px;
margin-right: 5px;

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ const props = defineProps({
type: Number,
default: 0,
},
scores: {
arrows: {
type: Array,
default: () => [],
},
@@ -51,7 +51,7 @@ onBeforeUnmount(() => {
<template>
<view class="container">
<image
v-if="total > 0 && scores.length === total && completeEffect"
v-if="total > 0 && arrows.length === total && completeEffect"
:src="bgImages[bgIndex]"
class="complete-light"
:style="{
@@ -79,8 +79,10 @@ onBeforeUnmount(() => {
>
<image src="../static/score-bg.png" mode="widthFix" />
<text
:style="{ fontWeight: scores[index] !== undefined ? 'bold' : 'normal' }"
>{{ scores[index] !== undefined ? scores[index] : "-" }}</text
:style="{ fontWeight: arrows[index] !== undefined ? 'bold' : 'normal' }"
>{{
!arrows[index] ? "-" : arrows[index].ringX ? "X" : arrows[index].ring
}}</text
>
</view>
</view>

View File

@@ -1,6 +1,6 @@
<script setup>
const props = defineProps({
scores: {
arrows: {
type: Array,
default: () => [],
},
@@ -10,37 +10,34 @@ const getSum = (a, b, c) => {
return sum > 0 ? sum + "环" : "-";
};
const roundsName = ["第一轮", "第二轮", "第三轮", "第四轮"];
const getShowText = (arrow = {}) => {
return arrow.ring ? (arrow.ringX ? "X" : arrow.ring + "环") : "-";
};
</script>
<template>
<view class="container">
<view>
<text :style="{ transform: 'translateX(-10%)' }">总成绩</text>
<text>{{ scores.reduce((last, next) => last + next, 0) }}</text>
<text>{{ arrows.reduce((last, next) => last + next.ring, 0) }}</text>
</view>
<view
v-for="(_, index) in new Array(
Math.min(
Math.ceil(scores.length / 3) + (scores.length % 3 === 0 ? 1 : 0),
Math.ceil(arrows.length / 3) + (arrows.length % 3 === 0 ? 1 : 0),
4
)
).fill(1)"
:key="index"
>
<text>{{ roundsName[index] }}</text>
<text>{{
scores[index * 3 + 0] ? scores[index * 3 + 0] + "环" : "-"
}}</text>
<text>{{
scores[index * 3 + 1] ? scores[index * 3 + 1] + "环" : "-"
}}</text>
<text>{{
scores[index * 3 + 2] ? scores[index * 3 + 2] + "环" : "-"
}}</text>
<text>{{ getShowText(arrows[index * 3 + 0]) }}</text>
<text>{{ getShowText(arrows[index * 3 + 1]) }}</text>
<text>{{ getShowText(arrows[index * 3 + 2]) }}</text>
<text :style="{ width: '40%', transform: 'translateX(20%)' }">{{
getSum(
scores[index * 3 + 0],
scores[index * 3 + 1],
scores[index * 3 + 2]
arrows[index * 3 + 0],
arrows[index * 3 + 1],
arrows[index * 3 + 2]
)
}}</text>
</view>

View File

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

View File

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

View File

@@ -1,11 +1,14 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
import audioManager from "@/audioManager";
import { MESSAGETYPES } from "@/constants";
import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const props = defineProps({
show: {
type: Boolean,
@@ -35,17 +38,22 @@ const props = defineProps({
type: Boolean,
default: false,
},
onStop: {
type: Function,
default: () => {},
},
});
const barColor = ref("#fed847");
const remain = ref(props.total);
const timer = ref(null);
const sound = ref(true);
const currentSound = ref("");
const currentRound = ref(props.currentRound);
const currentRoundEnded = ref(false);
const ended = ref(false);
const halfTime = ref(false);
const wait = ref(0);
const transitionStyle = ref("all 1s linear");
watch(
() => props.tips,
@@ -53,7 +61,7 @@ watch(
let key = "";
if (newVal.includes("红队")) key = "请红方射箭";
if (newVal.includes("蓝队")) key = "请蓝方射箭";
if (key && sound.value) {
if (key) {
if (currentRoundEnded.value) {
currentRound.value += 1;
currentRoundEnded.value = false;
@@ -72,110 +80,103 @@ watch(
}
);
watch(
() => props.tips,
(newVal) => {
if (newVal.includes("红队")) barColor.value = "#FF6060";
if (newVal.includes("蓝队")) barColor.value = "#5FADFF";
if (newVal.includes("红队") || newVal.includes("蓝队")) {
if (timer.value) clearInterval(timer.value);
remain.value = props.total;
timer.value = setInterval(() => {
if (remain.value > 0) remain.value--;
}, 1000);
}
},
{
immediate: true,
}
);
watch(
() => props.start,
(newVal) => {
if (timer.value) clearInterval(timer.value);
if (newVal) {
remain.value = props.total;
timer.value = setInterval(() => {
if (remain.value > 0) remain.value--;
}, 1000);
}
},
{
immediate: true,
}
);
const updateRemain = (value) => {
const resetTimer = (count) => {
if (timer.value) clearInterval(timer.value);
remain.value = Math.round(value);
const newVal = Math.round(count);
// 如果剩余时间增加(如重置),瞬间变化无动画
if (newVal >= remain.value) {
transitionStyle.value = "none";
remain.value = newVal;
setTimeout(() => {
transitionStyle.value = "all 1s linear";
}, 50);
} else {
remain.value = newVal;
}
if (remain.value > 0) {
timer.value = setInterval(() => {
if (remain.value === 0) {
clearInterval(timer.value);
props.onStop();
}
if (remain.value > 0) remain.value--;
}, 1000);
}
};
watch(
() => props.start,
(newVal) => {
if (newVal) {
resetTimer(props.total);
} else {
remain.value = 0;
clearInterval(timer.value);
}
},
{
immediate: true,
}
);
const tipContent = computed(() => {
if (halfTime.value) {
return props.battleId ? "中场休息" : `中场休息(${wait.value}秒)`;
}
return props.start && remain.value === 0 ? "时间到!" : props.tips;
});
const updateSound = () => {
sound.value = !sound.value;
if (!sound.value) audioManager.stop(currentSound.value);
audioManager.setMuted(!sound.value);
};
async function onReceiveMessage(messages = []) {
if (!sound.value || ended.value) return;
messages.forEach((msg) => {
if (
(props.battleId && msg.constructor === MESSAGETYPES.ShootResult) ||
(!props.battleId && msg.constructor === MESSAGETYPES.ShootSyncMeArrowID)
) {
if (props.melee && msg.userId !== user.value.id) return;
if (!halfTime.value && msg.target) {
currentSound.value = msg.target.ring
? `${msg.target.ring}`
: "未上靶";
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) {
audioManager.play("比赛开始");
} else if (msg.constructor === MESSAGETYPES.MeleeAllReady) {
halfTime.value = false;
audioManager.play("比赛开始");
} else if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
currentRoundEnded.value = true;
} else if (msg.constructor === MESSAGETYPES.HalfTimeOver) {
halfTime.value = true;
audioManager.play("中场休息");
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
audioManager.play("比赛结束");
} else if (msg.constructor === MESSAGETYPES.FinalShoot) {
audioManager.play("决金箭轮");
} else if (msg.constructor === MESSAGETYPES.MatchOver) {
ended.value = true;
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.BattleStart) {
halfTime.value = false;
audioManager.play("比赛开始");
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
audioManager.play("比赛结束", false);
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
let arrow = {};
if (msg.details && Array.isArray(msg.details)) {
arrow = msg.details[msg.details.length - 1];
} else {
if (msg.shootData.playerId !== user.value.id) return;
if (msg.shootData) arrow = msg.shootData;
}
});
let key = [];
key.push(arrow.ring ? `${arrow.ringX ? "X" : arrow.ring}` : "未上靶");
if (arrow.angle !== null)
key.push(`${getDirectionText(arrow.angle)}调整`);
audioManager.play(key, false);
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
halfTime.value = true;
audioManager.play("中场休息");
} else if (msg.type === MESSAGETYPESV2.InvalidShot) {
uni.showToast({
title: "距离不足,无效",
icon: "none",
});
audioManager.play("射击无效");
}
}
const playSound = (key) => {
currentSound.value = key;
audioManager.play(key);
};
onMounted(() => {
uni.$on("update-ramain", updateRemain);
uni.$on("update-remain", resetTimer);
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("play-sound", playSound);
});
onBeforeUnmount(() => {
uni.$off("update-ramain", updateRemain);
uni.$off("update-remain", resetTimer);
uni.$off("socket-inbox", onReceiveMessage);
uni.$off("play-sound", playSound);
if (timer.value) clearInterval(timer.value);
@@ -186,7 +187,7 @@ onBeforeUnmount(() => {
<view class="container" :style="{ display: show ? 'block' : 'none' }">
<view>
<image src="../static/shooter.png" mode="widthFix" />
<text>{{ start && remain === 0 ? "时间到!" : tips }}</text>
<text>{{ tipContent }}</text>
<button hover-class="none" @click="updateSound">
<image
:src="`../static/sound${sound ? '' : '-off'}-yellow.png`"
@@ -200,6 +201,7 @@ onBeforeUnmount(() => {
width: `${(remain / total) * 100}%`,
backgroundColor: barColor,
right: tips.includes('红队') ? 0 : 'unset',
transition: transitionStyle,
}"
/>
<text>剩余{{ remain }}</text>
@@ -254,7 +256,6 @@ onBeforeUnmount(() => {
height: 15px;
border-radius: 15px;
z-index: -1;
transition: all 1s linear;
}
.container > view:last-child > text {
font-size: 10px;

View File

@@ -1,6 +1,7 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import { RoundGoldImages } from "@/constants";
import {ref, watch, onMounted, onBeforeUnmount} from "vue";
import {RoundGoldImages} from "@/constants";
const props = defineProps({
tips: {
type: String,
@@ -19,57 +20,81 @@ const props = defineProps({
const barColor = ref("");
const remain = ref(15);
const timer = ref(null);
watch(
() => props.tips,
(newVal) => {
if (newVal.includes("红队"))
barColor.value = "linear-gradient( 180deg, #FFA0A0 0%, #FF6060 100%)";
if (newVal.includes("蓝队"))
barColor.value = "linear-gradient( 180deg, #9AB3FF 0%, #4288FF 100%)";
if (newVal.includes("红队") || newVal.includes("蓝队")) {
if (timer.value) clearInterval(timer.value);
remain.value = props.total;
timer.value = setInterval(() => {
if (remain.value > 0) remain.value--;
}, 1000);
}
},
{
immediate: true,
}
);
const loading = ref(false);
const transitionStyle = ref("all 1s linear");
const currentTeam = ref(null);
const updateRemain = (value) => {
if (timer.value) clearInterval(timer.value);
remain.value = Math.round(value);
if (value.stop) {
if (timer.value) clearInterval(timer.value);
return
}
loading.value = false;
currentTeam.value = value.team
if (value.team === 'red')
barColor.value = "linear-gradient( 180deg, #FFA0A0 0%, #FF6060 100%)";
if (value.team === 'blue')
barColor.value = "linear-gradient( 180deg, #9AB3FF 0%, #4288FF 100%)";
if (value.reset) {
remain.value = value.value;
return;
}
const newVal = Math.round(value.value);
// 如果剩余时间增加(如轮次切换重置),瞬间变化无动画;否则保持动画
if (newVal >= remain.value) {
transitionStyle.value = "none";
remain.value = newVal;
setTimeout(() => {
transitionStyle.value = "all 1s linear";
}, 50);
} else {
remain.value = newVal;
}
timer.value = setInterval(() => {
loading.value = remain.value === 0;
if (remain.value > 0) remain.value--;
}, 1000);
};
watch(
() => props.tips,
(newVal) => {
},
{
immediate: true,
}
);
onMounted(() => {
uni.$on("update-ramain", updateRemain);
uni.$on("update-remain", updateRemain);
});
onBeforeUnmount(() => {
uni.$off("update-ramain", updateRemain);
uni.$off("update-remain", updateRemain);
if (timer.value) clearInterval(timer.value);
});
</script>
<template>
<view class="container">
<image :src="RoundGoldImages[props.currentRound]" mode="widthFix" />
<view>
<view
<image :src="RoundGoldImages[props.currentRound]" mode="widthFix"/>
<view
:style="{
justifyContent: currentTeam==='red' ? 'flex-end' : 'flex-start',
}"
>
<view
:style="{
width: `${(remain / total) * 100}%`,
background: barColor,
right: tips.includes('红队') ? 0 : 'unset',
right: currentTeam==='red' ? 0 : 'unset',
transition: transitionStyle,
}"
/>
<text>剩余{{ remain }}</text>
<text v-if="!loading">剩余{{ remain }}</text>
<text v-else>···</text>
</view>
</view>
</template>
@@ -80,33 +105,37 @@ onBeforeUnmount(() => {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 12vw;
}
.container > image {
width: 100%;
transform: translateY(7px);
width: 380rpx;
height: 80rpx;
transform: translateY(18rpx);
}
.container > view:last-child {
width: 100%;
text-align: center;
background-color: #444444;
border-radius: 20px;
font-size: 12px;
height: 15px;
line-height: 15px;
height: 24rpx;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
}
.container > view:last-child > view {
position: absolute;
height: 15px;
height: 24rpx;
border-radius: 15px;
transition: all 1s linear;
}
.container > view:last-child > text {
font-size: 10px;
line-height: 15px;
font-size: 18rpx;
color: #fff;
position: relative;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

View File

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

View File

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

View File

@@ -75,7 +75,7 @@ const handleChange = (e) => {
.dots {
position: absolute;
bottom: 15%;
bottom: 5%;
left: 50%;
transform: translateX(-50%);
display: flex;
@@ -90,6 +90,6 @@ const handleChange = (e) => {
}
.dot.active {
background-color: #000;
background-color: #fed847;
}
</style>

View File

@@ -0,0 +1,197 @@
<script setup>
import { ref, watch } from "vue";
import SButton from "@/components/SButton.vue";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
onClose: {
type: Function,
default: () => {},
},
onConfirm: {
type: Function,
default: () => {},
},
});
const selectedTarget = ref(2);
const showContainer = ref(false);
const showContent = ref(false);
watch(
() => props.show,
(newValue) => {
if (newValue) {
showContainer.value = true;
setTimeout(() => {
showContent.value = true;
}, 100);
} else {
showContent.value = false;
setTimeout(() => {
showContainer.value = false;
}, 100);
}
},
{}
);
const handleConfirm = () => {
props.onConfirm(selectedTarget.value);
props.onClose();
};
</script>
<template>
<view
class="container"
v-if="showContainer"
:class="{ 'container-show': showContent }"
@click="onClose"
>
<view
class="modal-content"
:class="{ 'modal-show': showContent }"
@click.stop=""
>
<view class="header">
<view class="header-title">
<view class="header-title-line-left"></view>
<text>选择靶型</text>
<view class="header-title-line-right"></view>
</view>
<view class="close-btn" @click="onClose">
<image src="../static/close-yellow.png" mode="widthFix" />
</view>
</view>
<view class="target-options">
<view
:class="{ 'target-btn': true, 'target-choosen': selectedTarget === 1 }"
@click="() => (selectedTarget = 1)"
>
<text>20厘米全环靶</text>
</view>
<view style="width: 30rpx"></view>
<view
:class="{ 'target-btn': true, 'target-choosen': selectedTarget === 2 }"
@click="() => (selectedTarget = 2)"
>
<text>40厘米全环靶</text>
</view>
</view>
<SButton width="694rpx" :onClick="handleConfirm">确定</SButton>
</view>
</view>
</template>
<style scoped>
.container {
position: fixed;
top: 0;
left: 0;
background-color: #00000099;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
opacity: 0;
transition: all 0.3s ease;
z-index: 999;
}
.container-show {
opacity: 1;
}
.modal-content {
width: 100%;
transform: translateY(100%);
transition: all 0.3s ease;
background: url("https://static.shelingxingqiu.com/attachment/2025-12-04/dep11770wzxg6o2alo.png")
no-repeat center top;
background-size: 100% auto;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
padding-bottom: 68rpx;
padding-top: 44rpx;
}
.modal-show {
transform: translateY(0%);
}
.header {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
margin-bottom: 44rpx;
}
.header-title{
display: flex;
align-items: center;
justify-content: center;
}
.header-title text{
width: 196rpx;
height: 40rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 28rpx;
text-align: center;
font-style: normal;
text-transform: none;
color: #FFEFBA;
}
.header-title-line-left{
width: 214rpx;
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 1rpx solid;
border-image: linear-gradient(90deg, rgba(133, 119, 96, 1), rgba(133, 119, 96, 0)) 1 1;
}
.header-title-line-right{
width: 214rpx;
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 1rpx solid;
border-image: linear-gradient(90deg, rgba(133, 119, 96, 1), rgba(133, 119, 96, 0)) 1 1;
}
.close-btn {
position: absolute;
right: 0;
top: -10px;
}
.close-btn > image {
width: 40px;
height: 40px;
}
.target-options {
width: 750rpx;
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 38rpx;
}
.target-btn {
width: 332rpx;
height: 92rpx;
text-align: center;
border-radius: 10px;
border: 2rpx solid #fff3;
box-sizing: border-box;
color: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.target-choosen {
color: #fed847;
border: 4rpx solid #fed847;
}
</style>

View File

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

View File

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

View File

@@ -5,8 +5,7 @@ import BowPower from "@/components/BowPower.vue";
import Avatar from "@/components/Avatar.vue";
import audioManager from "@/audioManager";
import { simulShootAPI } from "@/apis";
import { checkConnection } from "@/util";
import { MESSAGETYPES } from "@/constants";
import { MESSAGETYPESV2 } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
@@ -20,13 +19,15 @@ const props = defineProps({
type: Boolean,
default: false,
},
count: {
type: Number,
default: 15,
},
});
const arrow = ref({});
const power = ref(0);
const distance = ref(0);
const debugInfo = ref("");
const showsimul = ref(false);
const count = ref(15);
const count = ref(props.count);
const timer = ref(null);
const updateTimer = (value) => {
@@ -34,10 +35,12 @@ const updateTimer = (value) => {
};
onMounted(() => {
audioManager.play("请射箭测试距离");
timer.value = setInterval(() => {
if (count.value > 0) count.value -= 1;
else clearInterval(timer.value);
}, 1000);
if (props.isBattle) {
timer.value = setInterval(() => {
count.value -= 1;
if (count.value < 0) clearInterval(timer.value);
}, 1000);
}
uni.$on("update-timer", updateTimer);
});
onBeforeUnmount(() => {
@@ -45,19 +48,13 @@ onBeforeUnmount(() => {
uni.$off("update-timer", updateTimer);
});
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
arrow.value = msg.target;
power.value = msg.target.battery;
distance.value = Number((msg.target.dst / 100).toFixed(2));
debugInfo.value = msg.target;
audioManager.play("距离合格");
} else if (msg.constructor === MESSAGETYPES.InvalidShot) {
distance.value = Number((msg.target.dst / 100).toFixed(2));
audioManager.play("距离不足");
}
});
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.TestDistance) {
distance.value = Number((msg.shootData.distance / 100).toFixed(2));
if (distance.value >= 5) audioManager.play("距离合格");
else audioManager.play("距离不足");
}
}
const simulShoot = async () => {
@@ -65,7 +62,6 @@ const simulShoot = async () => {
};
onMounted(() => {
checkConnection();
uni.$on("socket-inbox", onReceiveMessage);
const accountInfo = uni.getAccountInfoSync();
const envVersion = accountInfo.miniProgram.envVersion;
@@ -80,18 +76,9 @@ onBeforeUnmount(() => {
<template>
<view class="container">
<Guide v-show="guide">
<view
:style="{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: '10px',
}"
>
<view :style="{ display: 'flex', flexDirection: 'column' }">
<text :style="{ color: '#fed847' }">请确保站距达到5米</text>
<text>低于5米的射箭无效</text>
</view>
<view class="guide-tips">
<text>请确保站距达到5米</text>
<text>低于5米的射箭无效</text>
</view>
</Guide>
<view class="test-area">
@@ -120,16 +107,17 @@ onBeforeUnmount(() => {
</view>
<view class="user-row">
<Avatar :src="user.avatar" :size="35" />
<BowPower :power="power" />
<BowPower />
</view>
</view>
<view v-if="isBattle" class="ready-timer">
<image src="../static/test-tip.png" mode="widthFix" />
<view>
<view v-if="count >= 0">
<text>具体正式比赛还有</text>
<text>{{ count }}</text>
<text></text>
</view>
<view v-else> 进入中... </view>
</view>
</view>
</template>

View File

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

View File

@@ -24,17 +24,35 @@ export const MESSAGETYPES = {
LvlUpdate: 3958625354,
TeamUpdate: 4168086616,
InvalidShot: 4168086617,
Calibration: 4168086625,
DeviceOnline: 4168086626,
DeviceOffline: 4168086627,
SomeoneIsReady: 4168086628,
};
export const MESSAGETYPESV2 = {
AboutToStart: 1,
BattleStart: 2,
ToSomeoneShoot: 3,
ShootResult: 4,
NewRound: 5,
BattleEnd: 6,
HalfRest: 7,
TestDistance: 8,
MatchSuccess: 9,
InvalidShot: 10,
};
export const topThreeColors = ["#FFD947", "#D2D2D2", "#FFA515"];
export const getMessageTypeName = (id) => {
for (let key in MESSAGETYPES) {
if (MESSAGETYPES[key] === id) {
return key;
}
if (MESSAGETYPES[key] === id) return key;
}
return null;
for (let key in MESSAGETYPESV2) {
if (MESSAGETYPESV2[key] === id) return key;
}
return id;
};
export const roundsName = {
@@ -100,7 +118,7 @@ export const getBattleResultTips = (
) => {
const getRandomIndex = (len) => Math.floor(Math.random() * len);
if (gameMode === 1) {
if (mode === 1) {
if (mode <= 3) {
if (win) {
const tests = [
"https://static.shelingxingqiu.com/attachment/2025-08-01/dbqq1fglywucyoh9zn.png",
@@ -122,7 +140,7 @@ export const getBattleResultTips = (
];
return tests[getRandomIndex(3)];
}
} else if (mode === 2) {
} else {
if (rank <= 3) {
const tests = [
"好成绩!全国排位赛等着你!",
@@ -134,7 +152,7 @@ export const getBattleResultTips = (
}
}
} else if (gameMode === 2) {
if (mode === 1) {
if (mode <= 3) {
if (win) {
const tests = [
"https://static.shelingxingqiu.com/attachment/2025-08-01/dbqq1fgtb29jbdus4g.png",
@@ -156,7 +174,7 @@ export const getBattleResultTips = (
];
return tests[getRandomIndex(3)];
}
} else if (mode === 2) {
} else {
if (score > 0) {
const tests = [
"王者一定属于你!",

View File

@@ -1,68 +1,70 @@
{
"name": "shoot-miniprograms",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"uniStatistics": {
"enable": false
},
"app-plus": {
"bounce": "none",
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
"name" : "shoot-miniprograms",
"appid" : "__UNI__B03E251",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"uniStatistics" : {
"enable" : false
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios": {},
"sdkConfigs": {}
"app-plus" : {
"bounce" : "none",
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {},
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {
"dSYMs" : false
},
"sdkConfigs" : {}
}
},
"h5" : {
"darkmode" : true,
"themeLocation" : "theme.json"
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "wxa8f5989dcd45cc23",
"setting" : {
"urlCheck" : false,
"minified" : true,
"uglifyFileName" : true,
"useCompilerModule" : true,
"useIsolateContext" : true
},
"lazyCodeLoading" : "requiredComponents",
"usingComponents" : true,
"darkmode" : true,
"themeLocation" : "theme.json",
"permission" : {},
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
}
},
"h5": {
"darkmode": true,
"themeLocation": "theme.json"
},
"quickapp": {},
"mp-weixin": {
"appid": "wxa8f5989dcd45cc23",
"setting": {
"urlCheck": false,
"minified": true,
"uglifyFileName": true,
"useCompilerModule": true,
"useIsolateContext": true
},
"lazyCodeLoading": "requiredComponents",
"usingComponents": true,
"darkmode": true,
"themeLocation": "theme.json",
"permission": {},
"requiredPrivateInfos": ["getLocation", "chooseLocation"]
}
}

View File

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

View File

@@ -1,24 +1,7 @@
<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";
});
const isIOS = uni.getDeviceInfo().osName === "ios";
</script>
<template>
@@ -34,8 +17,7 @@ onMounted(() => {
<view
class="copyright"
:style="{ paddingBottom: isIos ? '30rpx' : '20rpx' }"
@click="openLink"
:style="{ paddingBottom: isIOS ? '40rpx' : '20rpx' }"
>
<text>粤ICP备2025421150号-2X</text>
</view>
@@ -46,7 +28,7 @@ onMounted(() => {
<style scoped>
.container {
width: calc(100% - 50rpx);
height: 100%;
height: calc(100% - 50rpx);
padding: 25rpx;
background-color: #ffffff;
position: relative;

68
src/pages/audio-test.vue Normal file
View File

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

View File

@@ -1,9 +1,9 @@
<script setup>
import { ref, onMounted } from "vue";
import { ref, computed, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Avatar from "@/components/Avatar.vue";
import UserUpgrade from "@/components/UserUpgrade.vue";
import { getGameAPI } from "@/apis";
import { getBattleAPI } from "@/apis";
import { topThreeColors, getBattleResultTips } from "@/constants";
import audioManager from "@/audioManager";
import useStore from "@/store";
@@ -16,134 +16,108 @@ const ifWin = ref(false);
const data = ref({});
const totalPoints = ref(0);
const rank = ref(0);
const players = ref([]);
function exit() {
uni.navigateBack();
if (data.value.roomId) {
uni.redirectTo({
url: `/pages/battle-room?roomNumber=${data.value.roomId}`,
});
} else {
uni.navigateBack();
}
}
onLoad(async (options) => {
if (!options.battleId) return;
const myId = user.value.id;
if (options.battleId) {
const result = await getGameAPI(
options.battleId || "BATTLE-1758270367040321900-868"
const result = await getBattleAPI(options.battleId || "60049406950510592");
data.value = result;
if (result.winTeam) {
ifWin.value = result.teams[result.winTeam].players.some(
(p) => p.id === myId
);
data.value = {
...result,
battleMode: result.gameMode,
};
if (result.mode === 1) {
data.value.redPlayers = Object.values(result.redPlayers);
data.value.bluePlayers = Object.values(result.bluePlayers);
if (result.redPlayers[myId]) {
totalPoints.value = result.redPlayers[myId].totalScore;
data.value.myTeam = result.redPlayers[myId].team;
ifWin.value = result.winner === 0;
}
if (result.bluePlayers[myId]) {
totalPoints.value = result.bluePlayers[myId].totalScore;
data.value.myTeam = result.bluePlayers[myId].team;
ifWin.value = result.winner === 1;
}
}
if (result.mode === 2) {
data.value.playerStats = result.players.map((p) => ({
...p,
id: p.playerId,
}));
const mine = result.players.find((p) => p.playerId === myId);
if (mine) totalPoints.value = mine.totalScore;
rank.value = result.players.findIndex((p) => p.playerId === myId) + 1;
}
} else {
const battleInfo = uni.getStorageSync("last-battle");
if (!battleInfo) return;
data.value = {
mvps: [],
...battleInfo,
};
if (battleInfo.mode === 1) {
battleInfo.playerStats.forEach((p) => {
if (p.team === 1) data.value.bluePlayers = [p];
if (p.team === 0) data.value.redPlayers = [p];
if (p.mvp) data.value.mvps.push(p);
});
data.value.mvps.sort((a, b) => b.totalRings - a.totalRings);
}
rank.value = 0;
const mine = battleInfo.playerStats.find((p, index) => {
rank.value = index + 1;
return p.id === myId;
});
if (mine) {
data.value.myTeam = mine.team;
totalPoints.value = mine.totalScore;
if (battleInfo.mode === 1) {
ifWin.value = mine.team === battleInfo.winner;
}
}
}
if (data.value.mode === 1) {
if (result.mode <= 3) {
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("失败");
} else {
players.value = result.resultList.map((item, index) => {
const plist = result.teams[0] ? result.teams[0].players : [];
const p = plist.find((p) => p.id === item.userId);
if (p.id === user.value.id) {
totalPoints.value = p.score;
rank.value = index + 1;
}
return {
...item,
rank: index + 1,
name: p.name,
avatar: p.avatar || "",
};
});
if (rank.value <= players.value * 0.3) {
audioManager.play("胜利");
} else {
audioManager.play("胜利");
}
}
});
const myTeam = computed(() => {
const teams = data.value.teams;
if (teams && teams.length) {
if (teams[1].players.some((p) => p.id === user.value.id)) return 1;
}
return 2;
});
const checkBowData = () => {
uni.navigateTo({
url: `/pages/match-detail?id=${data.value.id}`,
url: `/pages/match-detail?battleId=${data.value.matchId}`,
});
};
</script>
<template>
<view class="container">
<block v-if="data.mode === 1">
<block v-if="data.mode <= 3">
<view class="header-team" :style="{ marginTop: '25%' }">
<image src="../static/battle-result.png" mode="widthFix" />
<view class="header-solo" v-if="data.teamSize === 2">
<view class="header-solo" v-if="data.mode === 1">
<text
:style="{
background:
data.winner === 1
data.winTeam === 1
? 'linear-gradient(270deg, #3597ff 0%, rgba(0,0,0,0) 100%);'
: 'linear-gradient(270deg, #fd4444 0%, rgba(0, 0, 0, 0) 100%)',
}"
>{{ data.winner === 1 ? "蓝队" : "红队" }}获胜</text
>{{ data.winTeam === 1 ? "蓝队" : "红队" }}获胜</text
>
<Avatar
:size="32"
:src="
data.winner === 1
? data.bluePlayers[0].avatar
: data.redPlayers[0].avatar
data.winTeam === 1
? data.teams[1].players[0].avatar
: data.teams[2].players[0].avatar
"
:borderColor="data.winner === 1 ? '#5FADFF' : '#FF5656'"
:borderColor="data.winTeam === 1 ? '#5FADFF' : '#FF5656'"
mode="widthFix"
/>
</view>
</view>
<view class="header-mvp" v-if="data.teamSize !== 2">
<view class="header-mvp" v-if="data.mode === 2 || data.mode === 3">
<image
:src="`../static/${data.winner === 1 ? 'blue' : 'red'}-team-win.png`"
:src="`../static/${data.winTeam === 1 ? 'blue' : 'red'}-team-win.png`"
mode="widthFix"
/>
<view
:style="{
transform: `translateY(50px) rotate(-${5 + data.mvps.length}deg)`,
transform: `translateY(50px) rotate(-${
5 + (data.mvp || []).length
}deg)`,
}"
>
<view v-if="data.mvps && data.mvps[0].totalRings">
<view v-if="data.mvp && data.mvp[0].totalRings">
<image src="../static/title-mvp.png" mode="widthFix" />
<text
>斩获<text
@@ -153,22 +127,22 @@ const checkBowData = () => {
margin: '0 3px',
fontWeight: '600',
}"
>{{ data.mvps[0].totalRings }}</text
>{{ data.mvp[0].totalRings }}</text
></text
>
</view>
<view v-if="data.mvps && data.mvps.length">
<view v-for="(player, index) in data.mvps" :key="index">
<view v-if="data.mvp && data.mvp.length">
<view v-for="(player, index) in data.mvp" :key="index">
<view class="team-avatar">
<Avatar
:src="player.avatar"
:size="40"
:borderColor="data.myTeam === 1 ? '#5fadff' : '#ff6060'"
:borderColor="myTeam === 1 ? '#5fadff' : '#ff6060'"
/>
<text
v-if="player.id === user.id"
:style="{
backgroundColor: data.myTeam === 1 ? '#5fadff' : '#ff6060',
backgroundColor: myTeam === 1 ? '#5fadff' : '#ff6060',
}"
>自己</text
>
@@ -187,7 +161,7 @@ const checkBowData = () => {
/>
<image
:src="
getBattleResultTips(data.battleMode, data.mode, {
getBattleResultTips(data.way, data.mode, {
win: ifWin,
})
"
@@ -196,7 +170,7 @@ const checkBowData = () => {
/>
</view>
</block>
<block v-if="data.mode === 2">
<block v-else>
<view class="header-melee">
<view />
<image src="../static/battle-result.png" mode="widthFix" />
@@ -205,11 +179,11 @@ const checkBowData = () => {
<view
class="players"
:style="{
height: `${Math.max(data.playerStats.length > 5 ? '330' : '300')}px`,
height: `${Math.max(players.length > 5 ? '330' : '300')}px`,
}"
>
<view
v-for="(player, index) in data.playerStats"
v-for="(player, index) in players"
:key="index"
:style="{
border: player.id === user.id ? '1px solid #B04630' : 'none',
@@ -251,7 +225,9 @@ const checkBowData = () => {
src="../static/champ3.png"
mode="widthFix"
/>
<view v-if="index > 2" class="view-crown">{{ index + 1 }}</view>
<view v-if="player.rank > 3" class="view-crown">{{
player.rank
}}</view>
<Avatar
:src="player.avatar"
:size="36"
@@ -259,10 +235,10 @@ const checkBowData = () => {
/>
<view class="player-title">
<text class="truncate">{{ player.name }}</text>
<text>{{ getLvlName(player.totalScore) }}</text>
<text>{{ getLvlName(player.rank_lvl) }}</text>
</view>
<text
><text :style="{ color: '#fff' }">{{ player.totalRings }}</text>
><text :style="{ color: '#fff' }">{{ player.totalRing }}</text>
</text
>
</view>
@@ -270,36 +246,36 @@ const checkBowData = () => {
</block>
<view
class="battle-e"
:style="{ marginTop: data.mode === 2 ? '20px' : '20vw' }"
:style="{ marginTop: data.mode > 3 ? '20px' : '20vw' }"
>
<image src="../static/row-yellow-bg.png" mode="widthFix" />
<view class="team-avatar">
<Avatar
:src="user.avatar"
:size="40"
:borderColor="data.myTeam === 1 ? '#5fadff' : '#ff6060'"
:borderColor="myTeam === 1 ? '#5fadff' : '#ff6060'"
/>
<text
:style="{ backgroundColor: '#5fadff' }"
v-if="data.mode === 1 && data.myTeam === 1"
v-if="data.mode <= 3 && myTeam === 1"
>蓝队</text
>
<text
:style="{ backgroundColor: '#ff6060' }"
v-if="data.mode === 1 && data.myTeam === 0"
v-if="data.mode <= 3 && myTeam === 2"
>红队</text
>
</view>
<text v-if="data.battleMode === 1">
<text v-if="data.way === 1">
你的经验 {{ totalPoints > 0 ? "+" + totalPoints : totalPoints }}
</text>
<text v-if="data.battleMode === 2">
<text v-if="data.way === 2">
你的积分 {{ totalPoints > 0 ? "+" + totalPoints : totalPoints }}
</text>
</view>
<text v-if="data.mode === 2" class="description">
<text v-if="data.mode > 3" class="description">
{{
getBattleResultTips(data.battleMode, data.mode, {
getBattleResultTips(data.way, data.mode, {
win: ifWin,
score: totalPoints,
rank,
@@ -308,7 +284,7 @@ const checkBowData = () => {
</text>
<view class="op-btn">
<view @click="checkBowData">查看成绩</view>
<view @click="exit">退出</view>
<view @click="exit">返回</view>
</view>
<UserUpgrade />
</view>
@@ -418,6 +394,7 @@ const checkBowData = () => {
border-radius: 20px;
padding: 10px 0;
text-align: center;
color: #000;
}
.op-btn > view:last-child {
color: #fff;

File diff suppressed because it is too large Load Diff

View File

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

116
src/pages/calibration.vue Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,86 +1,111 @@
<script setup>
import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { onLoad, onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Guide from "@/components/Guide.vue";
import GuideTwo from "@/components/GuideTwo.vue";
import SButton from "@/components/SButton.vue";
import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue";
import CreateRoom from "@/components/CreateRoom.vue";
import Avatar from "@/components/Avatar.vue";
import { getRoomAPI, joinRoomAPI, isGamingAPI, getBattleDataAPI } from "@/apis";
import { getRoomAPI, joinRoomAPI, getBattleDataAPI } from "@/apis";
import { debounce, canEenter } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
import { debounce } from "@/util";
const { user, device, online, game } = storeToRefs(store);
const showModal = ref(false);
const showSignin = ref(false);
const warnning = ref("");
const roomNumber = ref("");
const data = ref({});
const roomID = ref("");
const loading = ref(false);
const enterRoom = debounce(async () => {
const isGaming = await isGamingAPI();
if (isGaming) {
const enterRoom = debounce(async (number) => {
if (loading.value) return;
if (!canEenter(user.value, device.value, online.value)) return;
if (game.value.inBattle) {
uni.$showHint(1);
return;
}
if (!roomNumber.value) {
if (!number) {
warnning.value = "请输入房间号";
showModal.value = true;
} else {
const room = await getRoomAPI(roomNumber.value);
if (room.number) {
const alreadyIn = room.members.find(
(item) => item.userInfo.id === user.value.id
);
if (!alreadyIn) {
const result = await joinRoomAPI(roomNumber.value);
if (result.full) {
warnning.value = "房间已满员";
showModal.value = true;
return;
}
}
roomNumber.value = "";
showModal.value = false;
uni.navigateTo({
url: `/pages/battle-room?roomNumber=${room.number}`,
});
} else {
return;
}
try {
const room = await getRoomAPI(number);
if (!room.number) {
warnning.value = room.started ? "该房间对战已开始,无法加入" : "查无此房";
showModal.value = true;
return;
}
const alreadyIn = room.members.find(
(item) => item.userInfo.id === user.value.id
);
if (!alreadyIn) {
const result = await joinRoomAPI(number);
if (result.full) {
warnning.value = "房间已满员";
showModal.value = true;
return;
}
}
loading.value = true;
uni.navigateTo({
url: "/pages/battle-room?roomNumber=" + number,
});
} finally {
loading.value = false;
}
});
const onCreateRoom = async () => {
const isGaming = await isGamingAPI();
if (isGaming) {
uni.$showHint(1);
return;
}
if (!canEenter(user.value, device.value, online.value)) return;
warnning.value = "";
showModal.value = true;
};
const onSignin = () => {
if (roomID.value && user.value.id) enterRoom(roomID.value);
showSignin.value = false;
};
/** 跳转到我的战绩页面默认展示「好友约战」tab */
const goMyRecord = () => {
uni.navigateTo({
url: '/pages/my-growth?tab=1',
});
};
onShow(async () => {
const result = await getBattleDataAPI();
data.value = result;
if (user.value.id) {
const result = await getBattleDataAPI();
data.value = result;
}
});
onLoad(async (options) => {
if (options.roomID) {
roomID.value = options.roomID;
if (user.value.id) enterRoom(options.roomID);
else showSignin.value = true;
}
});
</script>
<template>
<Container title="好友约战" :showBackToGame="true">
<view :style="{ width: '100%' }">
<Guide>
<view class="guide-tips">
<text>约上朋友开几局欢乐多不寂寞</text>
<text>一起练升级更快早日加入全国排位赛</text>
</view>
</Guide>
<view :style="{ width: '100%', height: '100%' }">
<GuideTwo>
<text :style="{color: 'rgba(255,217,71,0.8)'}">约上朋友开几局欢乐多不寂寞</text>
<text>一起练升级更快早日加入全国排位赛</text>
</GuideTwo>
<view class="my-data">
<view>
<Avatar :rankLvl="user.rankLvl" :src="user.avatar" :size="30" />
<text class="truncate">{{ user.nickName }}</text>
<text class="my-record-btn" @click="goMyRecord">我的战绩</text>
</view>
<view>
<view>
@@ -100,16 +125,9 @@ onShow(async () => {
<view>
<view class="stars">
<block v-for="i in 5" :key="i">
<image
v-if="data.totalWinningRate >= i * 0.2"
src="../static/star-full.png"
mode="widthFix"
/>
<image
v-else-if="data.totalWinningRate >= (i - 1) * 0.2 + 0.1"
src="../static/star-half.png"
mode="widthFix"
/>
<image v-if="data.totalWinningRate >= i * 0.2" src="../static/star-full.png" mode="widthFix" />
<image v-else-if="data.totalWinningRate >= (i - 1) * 0.2 + 0.1" src="../static/star-half.png"
mode="widthFix" />
<image v-else src="../static/star-empty.png" mode="widthFix" />
</block>
</view>
@@ -120,19 +138,12 @@ onShow(async () => {
<view class="founded-room">
<image src="../static/founded-room.png" mode="widthFix" />
<view>
<input
placeholder="输入房间号"
v-model="roomNumber"
placeholder-style="color: #ccc"
/>
<view @click="enterRoom">进入房间</view>
<input placeholder="输入房间号" v-model="roomNumber" placeholder-style="color: #ccc" />
<view @click="enterRoom(roomNumber)">进入房间</view>
</view>
</view>
<view class="create-room">
<image
src="https://static.shelingxingqiu.com/attachment/2025-07-15/dbcejys872iyun92h6.png"
mode="widthFix"
/>
<image src="https://static.shelingxingqiu.com/attachment/2025-07-15/dbcejys872iyun92h6.png" mode="widthFix" />
<image src="../static/room-notfound-title.png" mode="widthFix" />
<view>
<image :src="user.avatar" mode="widthFix" />
@@ -147,12 +158,13 @@ onShow(async () => {
</SButton>
</view>
</view>
<SModal :show="showModal" :onClose="() => (showModal = false)">
<SModal :show="showModal" :onClose="() => (showModal = false)" height="716rpx">
<view v-if="warnning" class="warnning">
{{ warnning }}
</view>
<CreateRoom v-if="!warnning" :onConfirm="() => (showModal = false)" />
</SModal>
<Signin :show="showSignin" :onClose="onSignin" />
</view>
</Container>
</template>
@@ -168,10 +180,12 @@ onShow(async () => {
border-radius: 10px;
padding: 15px;
}
.founded-room > image {
.founded-room>image {
width: 16vw;
}
.founded-room > view {
.founded-room>view {
display: flex;
justify-content: space-between;
align-items: center;
@@ -181,14 +195,16 @@ onShow(async () => {
width: 100%;
overflow: hidden;
}
.founded-room > view > input {
.founded-room>view>input {
width: 70%;
text-align: center;
font-size: 14px;
height: 40px;
color: #fff;
color: #000;
}
.founded-room > view > view {
.founded-room>view>view {
background-color: #fed847;
width: 30%;
line-height: 40px;
@@ -199,38 +215,45 @@ onShow(async () => {
color: #000;
text-align: center;
}
.create-room {
position: relative;
margin: 15px;
height: 50vw;
}
.create-room > image:first-of-type {
.create-room>image:first-of-type {
position: absolute;
width: 100%;
}
.create-room > image:nth-of-type(2) {
.create-room>image:nth-of-type(2) {
padding: 15px;
width: 25vw;
position: relative;
}
.create-room > view:nth-child(3) {
.create-room>view:nth-child(3) {
margin: 12vw auto;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.create-room > view > image:first-child {
.create-room>view>image:first-child {
width: 19vw;
transform: translateY(-60%);
border-radius: 50%;
position: relative;
}
.create-room > view > image:nth-child(2) {
.create-room>view>image:nth-child(2) {
width: 37vw;
position: relative;
}
.create-room > view > view:nth-child(3) {
.create-room>view>view:nth-child(3) {
position: relative;
width: 19vw;
height: 19vw;
@@ -241,10 +264,12 @@ onShow(async () => {
align-items: center;
transform: translateY(60%);
}
.create-room > view > view:nth-child(3) > image {
.create-room>view>view:nth-child(3)>image {
width: 20px;
margin-right: 2px;
}
.warnning {
width: 100%;
height: 100%;
@@ -253,6 +278,7 @@ onShow(async () => {
align-items: center;
color: #fff9;
}
.my-data {
width: calc(100% - 30px);
margin: 15px;
@@ -262,12 +288,14 @@ onShow(async () => {
overflow: hidden;
background-color: #54431d33;
}
.my-data > view {
.my-data>view {
width: 100%;
display: flex;
color: #fff9;
}
.my-data > view:first-child {
.my-data>view:first-child {
width: calc(100% - 30px);
align-items: flex-end;
padding-bottom: 15px;
@@ -275,16 +303,29 @@ onShow(async () => {
margin: 15px;
margin-bottom: 0;
}
.my-data > view:first-child > text {
.my-data>view:first-child>.my-record-btn {
font-weight: 400;
font-size: 24rpx;
color: #76D4FF;
text-align: center;
font-style: normal;
width: auto;
margin-left: auto;
}
.my-data>view:first-child>text {
color: #fff;
font-size: 17px;
margin-left: 10px;
width: 120px;
}
.my-data > view:last-child {
.my-data>view:last-child {
margin-bottom: 15px;
}
.my-data > view:last-child > view {
.my-data>view:last-child>view {
width: 33%;
margin-top: 15px;
display: flex;
@@ -292,26 +333,32 @@ onShow(async () => {
align-items: center;
font-size: 12px;
}
.my-data > view:last-child > view > view {
.my-data>view:last-child>view>view {
margin-bottom: 5px;
}
.my-data > view:last-child > view > view > text:first-child {
.my-data>view:last-child>view>view>text:first-child {
color: #fff;
font-size: 20px;
margin-right: 5px;
transform: translateY(4px);
}
.my-data > view:last-child > view:nth-child(2) {
.my-data>view:last-child>view:nth-child(2) {
border-left: 1px solid #48494e;
border-right: 1px solid #48494e;
}
.my-data > view:last-child > view > view {
.my-data>view:last-child>view>view {
display: flex;
align-items: flex-end;
height: 20px;
}
.stars > image {
.stars>image {
width: 4vw;
height: 4vw;
margin: 0 1px;
}
</style>

View File

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

View File

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

View File

@@ -1,54 +1,52 @@
<script setup>
import { ref, onMounted } from "vue";
import { onShow, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import {onMounted, ref} from "vue";
import {onShareAppMessage, onShareTimeline, onShow} from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import AppFooter from "@/components/AppFooter.vue";
import AppBackground from "@/components/AppBackground.vue";
import UserHeader from "@/components/UserHeader.vue";
import SModal from "@/components/SModal.vue";
import Signin from "@/components/Signin.vue";
import BubbleTip from "@/components/BubbleTip.vue";
import {
checkUserBindAPI,
getAppConfig,
getRankListAPI,
getDeviceBatteryAPI,
getHomeData,
getMyDevicesAPI,
getRankListAPI,
silentLoginAPI,
} from "@/apis";
import { topThreeColors } from "@/constants";
import {topThreeColors} from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
import {storeToRefs} from "pinia";
const store = useStore();
const { updateConfig, updateUser, updateDevice, updateRank, getLvlName } =
store;
// 使用storeToRefs用于UI里显示保持响应性
const { user, device, rankData } = storeToRefs(store);
const {
updateConfig,
updateUser,
updateDevice,
updateRank,
getLvlName,
getLvlNameByScore,
updateOnline,
} = store;
const {user, device, rankData, online, game} = storeToRefs(store);
const showModal = ref(false);
const showGuide = ref(false);
const toPage = (path) => {
const toPage = async (path) => {
if (!user.value.id) {
showModal.value = true;
return;
}
if (
"/pages/first-try,/pages/practise,/pages/friend-battle".indexOf(path) !== -1
) {
if (!device.value.deviceId) {
return uni.showToast({
title: "请先绑定设备",
icon: "none",
});
}
if ("/pages/first-try".indexOf(path) === -1 && !user.value.trio) {
return uni.showToast({
title: "请先完成新手试炼",
icon: "none",
});
}
}
uni.navigateTo({
url: path,
});
// if (path === "/pages/first-try") {
// if (canEenter(user.value, device.value, online.value, path)) {
// await uni.$checkAudio();
// }
// }
uni.navigateTo({url: path});
};
const toRankListPage = () => {
@@ -58,12 +56,36 @@ const toRankListPage = () => {
};
onShow(async () => {
const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
);
const env = uni.getAccountInfoSync().miniProgram.envVersion;
const token = uni.getStorageSync(`${env}_token`);
if (!user.value.id && !token) {
try {
const wxResult = await uni.login({provider: "weixin"});
const bindResult = await checkUserBindAPI(wxResult.code);
if (bindResult.binded) {
const newResult = await uni.login({provider: "weixin"});
const silentResult = await silentLoginAPI(newResult.code);
if (silentResult.user) updateUser(silentResult.user);
const devices = await getMyDevicesAPI();
if (devices.bindings && devices.bindings.length) {
updateDevice(
devices.bindings[0].deviceId,
devices.bindings[0].deviceName
);
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
}
} else {
showModal.value = true;
}
} catch (e) {
console.log("检查绑定状态失败", e);
}
}
const promises = [getRankListAPI()];
if (token) {
if (token || user.value.id) {
promises.push(getHomeData());
}
@@ -76,6 +98,12 @@ onShow(async () => {
console.log("首页数据:", homeData);
if (homeData.user) {
updateUser(homeData.user);
if ("823,209,293,257".indexOf(homeData.user.id) !== -1) {
const show = uni.getStorageSync("show-the-user");
if (!show) {
uni.setStorageSync("show-the-user", true);
}
}
if (homeData.user.trio <= 0) {
showGuide.value = true;
setTimeout(() => {
@@ -85,9 +113,11 @@ onShow(async () => {
const devices = await getMyDevicesAPI();
if (devices.bindings && devices.bindings.length) {
updateDevice(
devices.bindings[0].deviceId,
devices.bindings[0].deviceName
devices.bindings[0].deviceId,
devices.bindings[0].deviceName
);
const data = await getDeviceBatteryAPI();
updateOnline(data.online);
}
}
}
@@ -99,19 +129,12 @@ onMounted(async () => {
console.log("全局配置:", config);
});
const comingSoon = () => {
uni.showToast({
title: "敬请期待",
icon: "none",
});
};
onShareAppMessage(() => {
return {
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享卡片的标题
path: "/pages/index", // 用户点击分享卡片后跳转的页面路径
imageUrl:
"https://static.shelingxingqiu.com/attachment/2025-09-12/dcqoz26q0268wxmzjg.png", // 分享卡片的配图,可以是本地或网络图片
"https://static.shelingxingqiu.com/attachment/2025-09-12/dcqoz26q0268wxmzjg.png", // 分享卡片的配图,可以是本地或网络图片
};
});
onShareTimeline(() => {
@@ -119,7 +142,7 @@ onShareTimeline(() => {
title: "智能真弓:实时捕捉+毫秒级同步,弓箭选手全球竞技!", // 分享到朋友圈的标题
query: "from=timeline", // 用户通过朋友圈点击后,在页面 onShow 的 options 中可以获取到的参数
imageUrl:
"https://static.shelingxingqiu.com/attachment/2025-09-12/dcqoz26q0268wxmzjg.png", // 分享到朋友圈的配图
"https://static.shelingxingqiu.com/attachment/2025-09-12/dcqoz26q0268wxmzjg.png", // 分享到朋友圈的配图
};
});
</script>
@@ -127,81 +150,88 @@ onShareTimeline(() => {
<template>
<Container :isHome="true" :showBackToGame="true">
<view class="container">
<UserHeader showRank :onSignin="() => (showModal = true)" />
<view class="top-theme">
<image
src="https://static.shelingxingqiu.com/attachment/2025-12-31/dfc9dxrq4xn7e6y2pp.png"
mode="widthFix"
/>
</view>
<UserHeader showRank :onSignin="() => (showModal = true)"/>
<view :style="{ padding: '12px 10px' }">
<view class="feature-grid">
<view class="bow-card">
<image
src="https://static.shelingxingqiu.com/attachment/2025-08-07/dbvt1o6dvhr2rop3kn.webp"
mode="widthFix"
@click="() => toPage('/pages/my-device')"
v-if="online"
src="https://static.shelingxingqiu.com/attachment/2025-08-07/dbvt1o6dvhr2rop3kn.webp"
mode="widthFix"
@click="() => toPage('/pages/my-device')"
/>
<text v-if="!user.id">我的弓箭</text>
<text v-if="user.id && !device.deviceId">连接智能弓箭</text>
<text
v-if="user.id && device.deviceId"
class="truncate"
:style="{ width: '90%', textAlign: 'center' }"
>{{ device.deviceName }}</text
>
<image
src="../static/first-try.png"
mode="widthFix"
@click="() => toPage('/pages/first-try')"
v-else
src="https://static.shelingxingqiu.com/attachment/2026-01-04/dffohwtk1gwh0xfa6h.png"
mode="widthFix"
@click="() => toPage('/pages/my-device')"
/>
<BubbleTip
v-if="showGuide"
:location="{ top: '60%', left: '40%', fontSize: '14px' }"
>
<block v-if="user.id">
<text v-if="!device.deviceId">绑定我的智能弓</text>
<text v-else-if="!online">设备离线</text>
<text v-else-if="online">设备在线</text>
</block>
<image
src="../static/first-try.png"
mode="widthFix"
@click="() => toPage('/pages/first-try')"
/>
<BubbleTip v-if="showGuide" :location="{ top: '60%', left: '47%' }">
<text>新人必刷</text>
<text>快来报到吧~</text>
</BubbleTip>
</view>
<view class="play-card">
<view @click="() => toPage('/pages/practise')">
<image src="../static/my-practise.png" mode="widthFix" />
<image src="../static/my-practise.png" mode="widthFix"/>
</view>
<view @click="() => toPage('/pages/friend-battle')">
<image src="../static/friend-battle.png" mode="widthFix" />
<image src="../static/friend-battle.png" mode="widthFix"/>
</view>
</view>
</view>
<view class="ranking-section">
<image
src="https://static.shelingxingqiu.com/attachment/2025-09-25/dd1p9ci9v7frcrsxhj.png"
mode="widthFix"
src="https://static.shelingxingqiu.com/attachment/2025-09-25/dd1p9ci9v7frcrsxhj.png"
mode="widthFix"
/>
<button
class="into-btn"
@click="() => toPage('/pages/ranking')"
hover-class="none"
class="into-btn"
@click="() => toPage('/pages/ranking')"
hover-class="none"
></button>
<view class="ranking-players" @click="toRankListPage">
<img src="../static/juezhanbang.png" mode="widthFix" />
<img src="../static/juezhanbang.png" mode="widthFix"/>
<view class="divide-line"></view>
<view class="player-avatars">
<view
v-for="i in 6"
:key="i"
class="player-avatar"
:style="{
v-for="i in 6"
:key="i"
class="player-avatar"
:style="{
zIndex: 8 - i,
borderColor: rankData.rank[i - 1]
? topThreeColors[i - 1] || '#000'
: '#000',
}"
>
<image v-if="i === 1" src="../static/champ1.png" />
<image v-if="i === 2" src="../static/champ2.png" />
<image v-if="i === 3" src="../static/champ3.png" />
<image v-if="i === 1" src="../static/champ1.png"/>
<image v-if="i === 2" src="../static/champ2.png"/>
<image v-if="i === 3" src="../static/champ3.png"/>
<view v-if="i > 3">{{ i }}</view>
<image
:src="
:src="
rankData.rank[i - 1]
? rankData.rank[i - 1].avatar
: '../static/user-icon-dark.png'
"
mode="aspectFill"
mode="aspectFill"
/>
</view>
<view class="more-players">
@@ -211,14 +241,15 @@ onShareTimeline(() => {
</view>
<view class="my-data">
<view @click="() => toPage('/pages/my-growth')">
<image src="../static/my-growth.png" mode="widthFix" />
<image src="../static/my-growth.png" mode="widthFix"/>
</view>
<view @click="() => toPage('/pages/ranking')">
<view>
<text>段位</text>
<text>{{
user.scores ? getLvlName(user.scores) : "暂无"
}}</text>
user.lvlName || "暂无"
}}
</text>
</view>
<view>
<text>赛季平均环数</text>
@@ -227,79 +258,26 @@ onShareTimeline(() => {
<view>
<text>赛季胜率</text>
<text>{{
user.avg_win
? Number((user.avg_win * 100).toFixed(2)) + "%"
: "暂无"
}}</text>
user.avg_win
? Number((user.avg_win * 100).toFixed(2)) + "%"
: "暂无"
}}
</text>
</view>
</view>
</view>
<!-- <view class="region-stats">
<view
v-for="(region, index) in [
{ name: '广东', score: 4291 },
{ name: '湖南', score: 3095 },
{ name: '内蒙', score: 2342 },
{ name: '海南', score: 1812 },
{ name: '四川', score: 1293 },
]"
:key="index"
class="region-item"
@click="comingSoon"
>
<image src="../static/region-bg.png" mode="widthFix" />
<image
v-if="index === 0"
src="../static/region-1.png"
mode="widthFix"
/>
<image
v-if="index === 1"
src="../static/region-2.png"
mode="widthFix"
/>
<image
v-if="index === 2"
src="../static/region-3.png"
mode="widthFix"
/>
<image
v-if="index === 3"
src="../static/region-4.png"
mode="widthFix"
/>
<image
v-if="index === 4"
src="../static/region-5.png"
mode="widthFix"
/>
<text>{{ region.name }}</text>
<view>
<text :style="{ color: '#fff', marginRight: '2px' }">{{
region.score
}}</text>
<text>分</text>
</view>
</view>
<view class="region-more" @click="comingSoon">
<image src="../static/region-more.png" mode="widthFix" />
<text>...</text>
<text>更多</text>
</view>
</view> -->
</view>
</view>
<SModal :show="showModal" :onClose="() => (showModal = false)">
<Signin :onClose="() => (showModal = false)" />
</SModal>
<Signin :show="showModal" :onClose="() => (showModal = false)"/>
</view>
<AppFooter />
<AppFooter/>
</Container>
</template>
<style scoped>
.container {
width: 100%;
height: calc(100% - 120px);
}
.feature-grid {
@@ -316,6 +294,8 @@ onShareTimeline(() => {
.bow-card {
width: 50%;
border-radius: 25rpx;
overflow: hidden;
}
.feature-grid > view > image {
@@ -324,7 +304,7 @@ onShareTimeline(() => {
.bow-card > text {
position: absolute;
top: 65%;
top: 66%;
left: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
@@ -411,6 +391,7 @@ onShareTimeline(() => {
width: 32rpx;
height: 32rpx;
}
.player-avatar > view:first-child {
border-radius: 50%;
background: #777777;
@@ -421,6 +402,7 @@ onShareTimeline(() => {
height: 18px;
color: #fff;
}
.player-avatar > image:last-child {
width: 100%;
height: 100%;
@@ -440,71 +422,21 @@ onShareTimeline(() => {
color: #fff;
}
.region-stats {
display: flex;
grid-template-columns: repeat(6, 1fr);
margin-top: 20px;
justify-content: space-between;
}
.region-item,
.region-more {
border-radius: 10px;
text-align: center;
position: relative;
width: 13vw;
height: 13vw;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #c5c5c5;
font-size: 12px;
}
.region-item > text {
margin-top: 10px;
}
.region-more {
width: 8vw;
height: 13vw;
}
.region-item > image:first-child,
.region-more > image:first-child {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: -1;
}
.region-item > image:nth-of-type(2) {
position: absolute;
top: 0;
left: 0;
width: 18px;
}
.region-item > view:last-child {
display: flex;
justify-content: center;
font-size: 10px;
}
.region-more > text:first-of-type {
font-size: 30px;
line-height: 20px;
margin-bottom: 5px;
}
.my-data {
display: flex;
margin-top: 20px;
justify-content: space-between;
}
.my-data > view:first-child {
width: 28%;
}
.my-data > view:first-child > image {
width: 100%;
transform: translateX(-8px);
}
.my-data > view:nth-child(2) {
width: 68%;
font-size: 12px;
@@ -512,9 +444,11 @@ onShareTimeline(() => {
display: flex;
justify-content: space-between;
}
.my-data > view:nth-child(2) > view:nth-child(2) {
width: 38%;
}
.my-data > view:nth-child(2) > view {
width: 28%;
border-radius: 10px;
@@ -524,9 +458,25 @@ onShareTimeline(() => {
align-items: center;
justify-content: center;
}
.my-data > view:nth-child(2) > view > text:last-child {
color: #fff;
line-height: 25px;
font-weight: 500;
}
.top-theme {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 60px;
z-index: -1;
}
.top-theme > image {
width: 300rpx;
transform: translate(-4%, -14%);
}
</style>

View File

@@ -4,91 +4,50 @@ import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import BattleHeader from "@/components/BattleHeader.vue";
import Avatar from "@/components/Avatar.vue";
// import TeamResult from "@/components/TeamResult.vue";
// import MeleeResult from "@/components/MeleeResult.vue";
import PlayerScore2 from "@/components/PlayerScore2.vue";
import { getGameAPI } from "@/apis";
import { getBattleAPI } from "@/apis";
const blueTeam = ref([]);
const redTeam = ref([]);
const roundsData = ref([]);
const goldenRoundsData = ref([]);
const battleId = ref("");
const data = ref({
players: [],
teams: [],
rounds: [],
});
// const show = ref(false);
const players = ref([]);
onLoad(async (options) => {
if (options.id) {
battleId.value = options.id || "BATTLE-1755484626207409508-955";
const result = await getGameAPI(battleId.value);
data.value = result;
if (result.mode === 1) {
blueTeam.value = Object.values(result.bluePlayers || {});
redTeam.value = Object.values(result.redPlayers || {});
Object.values(result.roundsData).forEach((item) => {
let bluePoint = 1;
let redPoint = 1;
let blueTotalRings = 0;
let redTotalRings = 0;
let blueArrows = [];
let redArrows = [];
blueTeam.value.forEach((p) => {
if (!item[p.playerId]) return;
blueTotalRings += item[p.playerId].reduce((a, b) => a + b.ring, 0);
blueArrows = [...blueArrows, ...item[p.playerId]];
});
redTeam.value.forEach((p) => {
if (!item[p.playerId]) return;
redTotalRings += item[p.playerId].reduce((a, b) => a + b.ring, 0);
redArrows = [...redArrows, ...item[p.playerId]];
});
if (blueTotalRings > redTotalRings) {
bluePoint = 2;
redPoint = 0;
} else if (blueTotalRings < redTotalRings) {
bluePoint = 0;
redPoint = 2;
if (!options.battleId) return;
battleId.value = options.battleId || "60510101693403136";
const result = await getBattleAPI(battleId.value);
data.value = result;
if (result.mode > 3) {
players.value = result.resultList.map((item, index) => {
const plist = result.teams[0] ? result.teams[0].players : [];
const p = plist.find((p) => p.id === item.userId);
const arrows = new Array(12);
result.rounds.forEach((r, index) => {
if (r.shoots[item.userId]) {
r.shoots[item.userId].forEach((s, index2) => {
arrows[index2 + index * 6] = s;
});
}
roundsData.value.push({
blue: {
avatars: blueTeam.value.map((p) => p.avatar),
arrows: blueArrows,
totalRing: blueTotalRings,
totalScore: bluePoint,
},
red: {
avatars: redTeam.value.map((p) => p.avatar),
arrows: redArrows,
totalRing: redTotalRings,
totalScore: redPoint,
},
});
});
result.goldenRounds.forEach((round) => {
goldenRoundsData.value.push({
blue: {
avatars: blueTeam.value.map((p) => p.avatar),
arrows: round.arrowHistory.filter((a) => a.team === 1),
},
red: {
avatars: redTeam.value.map((p) => p.avatar),
arrows: round.arrowHistory.filter((a) => a.team === 0),
},
winner: round.winner,
});
});
}
return {
...item,
rank: index + 1,
name: p.name,
avatar: p.avatar || "",
arrows,
};
});
}
});
const checkBowData = () => {
if (data.value.mode === 1) {
const checkBowData = (selected) => {
if (data.value.mode <= 3) {
uni.navigateTo({
url: `/pages/team-bow-data?battleId=${battleId.value}`,
url: `/pages/team-bow-data?battleId=${battleId.value}&selected=${selected}`,
});
} else if (data.value.mode === 2) {
} else {
uni.navigateTo({
url: `/pages/melee-bow-data?battleId=${battleId.value}`,
});
@@ -100,13 +59,13 @@ const checkBowData = () => {
<Container title="详情">
<view class="container">
<BattleHeader
:winner="data.winner"
:blueTeam="blueTeam"
:redTeam="redTeam"
:players="data.players"
:winner="data.winTeam"
:blueTeam="data.teams[1] ? data.teams[1].players : []"
:redTeam="data.teams[2] ? data.teams[2].players : []"
:players="players"
/>
<view
v-if="data.players && data.players.length"
v-if="data.mode >= 3"
class="score-header"
:style="{ border: 'none', padding: '5px 15px' }"
>
@@ -117,156 +76,64 @@ const checkBowData = () => {
</view>
</view>
<PlayerScore2
v-if="data.players && data.players.length"
v-for="(player, index) in data.players"
v-if="data.mode >= 3"
v-for="(player, index) in players"
:key="index"
:name="player.name"
:avatar="player.avatar"
:scores="player.arrowHistory"
:arrows="player.arrows"
:totalScore="player.totalScore"
:totalRing="player.totalRings"
:rank="index + 1"
/>
<block v-for="(round, index) in goldenRoundsData" :key="index">
<view
v-if="data.mode <= 3"
v-for="(round, index) in data.rounds"
:key="index"
:style="{ marginBottom: '5px' }"
>
<view class="score-header">
<text>决金箭轮环数</text>
<view @click="checkBowData">
<text>{{ round.ifGold ? "决金箭" : `${index + 1}` }}</text>
<view @click="() => checkBowData(index)">
<text>查看靶纸</text>
<image src="../static/back.png" mode="widthFix" />
</view>
</view>
<view class="score-row">
<view
class="score-row"
v-for="team in Object.keys(round.shoots)"
:key="team"
>
<view>
<view>
<image
v-for="(src, index) in round.blue.avatars"
v-for="(p, index) in data.teams[team].players"
:style="{
borderColor: '#64BAFF',
transform: `translateX(-${index * 15}px)`,
}"
:src="src"
:src="p.avatar || '../static/user-icon.png'"
:key="index"
mode="widthFix"
/>
</view>
<text v-for="(arrow, index) in round.blue.arrows" :key="index">
{{ arrow.ring }}
<text
v-for="(arrow, index2) in round.shoots[team]"
:key="index2"
:style="{ color: arrow.ringX ? '#fed847' : '#ccc' }"
>
{{ arrow.ringX ? "X" : `${arrow.ring}` }}
</text>
</view>
<image
v-if="round.winner === 1"
src="../static/winner-badge.png"
mode="widthFix"
/>
</view>
<view class="score-row" :style="{ marginBottom: '5px' }">
<view>
<view>
<image
v-for="(src, index) in round.red.avatars"
:style="{
borderColor: '#FF6767',
transform: `translateX(-${index * 15}px)`,
}"
:src="src || '../static/user-icon.png'"
:key="index"
mode="widthFix"
/>
</view>
<text v-for="(arrow, index) in round.red.arrows" :key="index">
{{ arrow.ring }}
<text :style="{ color: team == 1 ? '#64BAFF' : '#FF6767' }">
{{ round.shoots[team].reduce((acc, cur) => acc + cur.ring, 0) }}
</text>
<text>得分 {{ round.scores[team].score }}</text>
</view>
<image
v-if="round.winner === 0"
src="../static/winner-badge.png"
mode="widthFix"
/>
</view>
</block>
<view
v-for="(round, index) in roundsData"
:key="index"
:style="{ marginBottom: '5px' }"
>
<block
v-if="
index < Object.keys(roundsData).length - goldenRoundsData.length
"
>
<view class="score-header">
<text>第{{ index + 1 }}轮</text>
<view @click="checkBowData">
<text>查看靶纸</text>
<image src="../static/back.png" mode="widthFix" />
</view>
</view>
<view class="score-row">
<view>
<view>
<image
v-for="(src, index) in round.blue.avatars"
:style="{
borderColor: '#64BAFF',
transform: `translateX(-${index * 15}px)`,
}"
:src="src || '../static/user-icon.png'"
:key="index"
mode="widthFix"
/>
</view>
<text v-for="(arrow, index2) in round.blue.arrows" :key="index2">
{{ arrow.ring }}环
</text>
</view>
<view>
<text :style="{ color: '#64BAFF' }">
{{ round.blue.totalRing }}环
</text>
<text>得分 {{ round.blue.totalScore }}</text>
</view>
</view>
<view class="score-row">
<view>
<view>
<image
v-for="(src, index) in round.red.avatars"
:style="{
borderColor: '#FF6767',
transform: `translateX(-${index * 15}px)`,
}"
:src="src"
:key="index"
mode="widthFix"
/>
</view>
<text v-for="(arrow, index2) in round.red.arrows" :key="index2">
{{ arrow.ring }}环
</text>
</view>
<view>
<text :style="{ color: '#FF6767' }">
{{ round.red.totalRing }}环
</text>
<text>得分 {{ round.red.totalScore }}</text>
</view>
</view>
</block>
</view>
<view :style="{ height: '20px' }"></view>
</view>
<!-- <TeamResult
v-if="data.mode === 1"
:show="show"
:onClose="() => (show = false)"
:data="data"
/>
<MeleeResult
v-if="data.mode === 2"
:show="show"
:onClose="() => (show = false)"
:data="data"
/> -->
</Container>
</template>

View File

@@ -3,10 +3,8 @@ import { ref, onMounted, onBeforeUnmount } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Matching from "@/components/Matching.vue";
import RoundEndTip from "@/components/RoundEndTip.vue";
import TestDistance from "@/components/TestDistance.vue";
import { matchGameAPI } from "@/apis";
import { MESSAGETYPES } from "@/constants";
import { MESSAGETYPESV2 } from "@/constants";
const gameType = ref(0);
const teamSize = ref(0);
@@ -16,33 +14,28 @@ async function stopMatch() {
uni.$showHint(3);
}
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.WaitForAllReady) {
if (!onComplete.value) {
onComplete.value = () => {
if (msg.groupUserStatus) {
uni.setStorageSync("red-team", msg.groupUserStatus.redTeam);
uni.setStorageSync("blue-team", msg.groupUserStatus.blueTeam);
uni.setStorageSync("melee-players", [
...msg.groupUserStatus.redTeam,
...msg.groupUserStatus.blueTeam,
]);
}
uni.removeStorageSync("current-battle");
if (gameType.value == 1) {
uni.redirectTo({
url: `/pages/team-battle?battleId=${msg.id}&gameMode=2`,
});
} else if (gameType.value == 2) {
uni.redirectTo({
url: `/pages/melee-match?battleId=${msg.id}&gameMode=2`,
});
}
};
}
async function cancelMatch() {
if (gameType.value && teamSize.value) {
await matchGameAPI(false, gameType.value, teamSize.value);
}
uni.navigateBack()
}
async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.MatchSuccess) {
onComplete.value = () => {}
}
if (msg.type === MESSAGETYPESV2.AboutToStart) {
if (gameType.value == 1) {
uni.redirectTo({
url: `/pages/team-battle?battleId=${msg.id}&gameMode=2`,
});
} else if (gameType.value == 2) {
uni.redirectTo({
url: `/pages/melee-battle?battleId=${msg.id}&gameMode=2`,
});
}
});
}
}
onLoad(async (options) => {
@@ -57,6 +50,7 @@ onMounted(() => {
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("cancelMatching", cancelMatch);
});
onBeforeUnmount(() => {
@@ -64,9 +58,7 @@ onBeforeUnmount(() => {
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
if (gameType.value && teamSize.value) {
matchGameAPI(false, gameType.value, teamSize.value);
}
uni.$off("cancelMatching", cancelMatch);
});
onShow(async () => {
@@ -76,9 +68,7 @@ onShow(async () => {
});
onHide(() => {
if (gameType.value && teamSize.value) {
matchGameAPI(false, gameType.value, teamSize.value);
}
});
</script>

219
src/pages/melee-battle.vue Normal file
View File

@@ -0,0 +1,219 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import BowTarget from "@/components/BowTarget.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import BattleHeader from "@/components/BattleHeader.vue";
import PlayerScore from "@/components/PlayerScore.vue";
import SButton from "@/components/SButton.vue";
import Avatar from "@/components/Avatar.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import TestDistance from "@/components/TestDistance.vue";
import audioManager from "@/audioManager";
import { getBattleAPI, laserCloseAPI } from "@/apis";
import { MESSAGETYPESV2 } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const title = ref("");
const start = ref(null);
const battleId = ref("");
const currentRound = ref(1);
const tips = ref("即将开始...");
const players = ref([]);
const playersSorted = ref([]);
const playersScores = ref([]);
const halfTimeTip = ref(false);
const halfRest = ref(false);
const navigateToResult = () => {
uni.redirectTo({ url: `/pages/battle-result?battleId=${battleId.value}` });
};
function recoverData(battleInfo, { force = false } = {}) {
if (!battleInfo) return;
try {
if (battleInfo.way === 1) title.value = "好友约战 - 大乱斗";
if (battleInfo.way === 2) title.value = "排位赛 - 大乱斗";
// 优先使用接口数据,否则使用缓存
if (battleInfo.teams?.[0]?.players) {
players.value = [...battleInfo.teams[0].players];
} else {
// 大乱斗可能存的是 players 列表
// 这里的缓存逻辑根据 AboutToStart 消息结构可能不同,假设也是 teams[0]
// 如果是从 match-page 过来的match-page 只存了 teams[1] 和 [2] 给对抗模式
// 大乱斗的匹配逻辑可能不同,暂时保持原样,只做安全保护
players.value = [];
}
start.value = battleInfo.status !== 0;
if (battleInfo.status === 0) {
const readyRemain = (Date.now() - (battleInfo.createTime || Date.now())) / 1000;
if (readyRemain > 0 && readyRemain < 15) {
setTimeout(() => uni.$emit("update-timer", 15 - readyRemain - 0.2), 200);
}
return;
}
tips.value =
(battleInfo.rounds.length !== 2 ? "上" : "下") + "半场请先射6箭";
playersScores.value = battleInfo.rounds.map((r) => ({ ...r.shoots }));
const totals = {};
players.value.forEach((p) => {
const total = playersScores.value.reduce((acc, round) => {
const arr = round[p.id] || [];
return acc + arr.length;
}, 0);
totals[p.id] = total;
});
playersSorted.value = players.value.slice().sort((a, b) => {
return totals[b.id] - totals[a.id];
});
if (battleInfo.status === 3) {
halfTimeTip.value = true;
halfRest.value = true;
tips.value = "准备下半场";
// 剩余休息时间
// const remain = (Date.now() - battleInfo.timeoutTime) / 1000;
setTimeout(() => {
uni.$emit("update-remain", 0);
}, 200);
return;
}
if (force) {
const remain = (Date.now() - (battleInfo.current?.startTime || Date.now())) / 1000;
console.log(`当前轮已进行${remain}`);
if (remain > 0 && remain < 90) {
setTimeout(() => {
uni.$emit("update-remain", 90 - remain - 0.2);
}, 200);
}
}
} catch (err) {
console.error("recoverData error:", err);
}
}
onLoad(async (options) => {
if (options.battleId) battleId.value = options.battleId;
// uni.enableAlertBeforeUnload({
// message: "离开比赛可能导致比赛失败,是否继续?",
// success: (res) => {
// console.log("已启用离开提示");
// },
// });
});
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.BattleStart) {
halfTimeTip.value = false;
halfRest.value = false;
recoverData(msg);
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
recoverData(msg);
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
halfTimeTip.value = true;
halfRest.value = true;
tips.value = "准备下半场";
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(() => {
uni.redirectTo({
url: "/pages/battle-result?battleId=" + msg.matchId,
});
}, 1000);
}
}
onMounted(async () => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
await laserCloseAPI();
});
onBeforeUnmount(() => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
audioManager.stopAll();
});
onShow(async () => {
if (battleId.value) {
const result = await getBattleAPI(battleId.value);
if (!result) return;
if (result.status === 2) {
uni.showToast({
title: "比赛已结束",
icon: "none",
});
uni.navigateBack({
delta: 2,
});
} else {
recoverData(result, { force: true });
}
}
});
</script>
<template>
<Container :title="title" :bgType="1">
<view class="container">
<BattleHeader v-if="!start" :players="players" />
<TestDistance v-if="start === false" :guide="false" :isBattle="true" />
<ShootProgress
:show="start"
:start="start && !halfRest"
:tips="tips"
:total="90"
:melee="true"
:battleId="battleId"
/>
<view v-if="start" class="user-row">
<Avatar :src="user.avatar" :size="35" />
<BowPower />
</view>
<BowTarget
v-if="start"
:currentRound="
playersScores.map((s) => s[user.id].length).reduce((a, b) => a + b, 0)
"
:totalRound="12"
:scores="playersScores.map((r) => r[user.id]).flat()"
:stop="halfRest"
/>
<view :style="{ paddingBottom: '20px' }">
<PlayerScore
v-if="start"
v-for="(player, index) in playersSorted"
:key="index"
:player="player"
:scores="playersScores.map((s) => s[player.id])"
/>
</view>
<ScreenHint
:show="halfTimeTip"
mode="small"
:onClose="() => (halfTimeTip = false)"
>
<view class="half-time-tip">
<text>上半场结束休息一下吧:</text>
<text>20秒后开始下半场</text>
</view>
</ScreenHint>
</view>
</Container>
</template>
<style scoped>
.container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -4,28 +4,39 @@ import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import BowTarget from "@/components/BowTarget.vue";
import Avatar from "@/components/Avatar.vue";
import { getGameAPI } from "@/apis";
import { getBattleAPI } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const scores = ref([]);
const currentUser = ref({});
const data = ref({});
const onSelect = (userId) => {
const user = data.value.players.find((p) => p.playerId === userId);
currentUser.value = user;
if (user && user.arrowHistory) {
scores.value = user.arrowHistory;
}
};
const { user } = storeToRefs(useStore());
const currentUser = ref({
arrows: [],
});
const players = ref([]);
onLoad(async (options) => {
if (options.battleId) {
const result = await getGameAPI(options.battleId);
data.value = result;
if (result.players && result.players[0]) {
onSelect(result.players[0].playerId);
}
if (!options.battleId) return;
const result = await getBattleAPI(options.battleId || "59348111700660224");
players.value = result.resultList.map((item, index) => {
const plist = result.teams[0] ? result.teams[0].players : [];
const p = plist.find((p) => p.id === item.userId);
const arrows = new Array(12);
result.rounds.forEach((r, index) => {
if (r.shoots[item.userId]) {
r.shoots[item.userId].forEach((s, index2) => {
arrows[index2 + index * 6] = s;
});
}
});
return {
...item,
rank: index + 1,
name: p.name,
avatar: p.avatar || "",
arrows,
};
});
if (players.value[0]) {
currentUser.value = players.value[0];
}
});
</script>
@@ -33,22 +44,26 @@ onLoad(async (options) => {
<template>
<Container title="靶纸">
<view class="container">
<image src="../static/battle-header-melee.png" mode="widthFix" />
<view class="players" v-if="data.players">
<image
src="../static/battle-header-melee.png"
mode="widthFix"
:style="{ top: '-50rpx' }"
/>
<view class="players">
<view
v-for="(player, index) in data.players"
v-for="(player, index) in players"
:key="index"
:style="{
width: `${Math.max(100 / data.players.length, 18)}vw`,
color: player.playerId === currentUser.playerId ? '#000' : '#fff9',
width: `${Math.max(100 / players.length, 18)}vw`,
color: player.userId === currentUser.userId ? '#000' : '#fff9',
}"
@click="() => onSelect(player.playerId)"
@click="currentUser = player"
>
<image
v-if="player.playerId === currentUser.playerId"
v-if="player.userId === currentUser.userId"
src="../static/player-bg2.png"
:style="{
width: `${Math.max(100 / data.players.length, 18)}vw`,
width: `${Math.max(100 / players.length, 18)}vw`,
}"
class="player-bg"
/>
@@ -57,23 +72,25 @@ onLoad(async (options) => {
</view>
</view>
<view :style="{ marginTop: '10px' }">
<BowTarget :scores="scores" />
<BowTarget :scores="currentUser.arrows" />
</view>
<view class="score-text"
><text :style="{ color: '#fed847' }">{{ scores.length }}</text
><text :style="{ color: '#fed847' }">{{
currentUser.arrows.length
}}</text
>支箭<text :style="{ color: '#fed847' }">{{
scores.reduce((last, next) => last + next.ring, 0)
currentUser.arrows.reduce((last, next) => last + next.ring, 0)
}}</text
></view
>
<view class="score-row">
<view class="score-row" v-if="currentUser.arrows">
<view
v-for="(score, index) in scores"
v-for="(score, index) in currentUser.arrows"
:key="index"
class="score-item"
:style="{ width: '13vw', height: '13vw' }"
>
{{ score.ring }}
{{ score.ringX ? "X" : score.ring }}
</view>
</view>
</view>
@@ -97,7 +114,7 @@ onLoad(async (options) => {
display: flex;
width: 100%;
overflow-x: auto;
margin-top: 25px;
margin-top: 50rpx;
}
.players::-webkit-scrollbar {
width: 0;

View File

@@ -1,244 +0,0 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import BowTarget from "@/components/BowTarget.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import BattleHeader from "@/components/BattleHeader.vue";
import PlayerScore from "@/components/PlayerScore.vue";
import SButton from "@/components/SButton.vue";
import Avatar from "@/components/Avatar.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import TestDistance from "@/components/TestDistance.vue";
import { getCurrentGameAPI } from "@/apis";
import { isGameEnded } from "@/util";
import { MESSAGETYPES } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const title = ref("大乱斗");
const start = ref(false);
const startCount = ref(true);
const battleId = ref("");
const currentRound = ref(1);
const power = ref(0);
const scores = ref([]);
const tips = ref("即将开始...");
const players = ref([]);
const playersSorted = ref([]);
const playersScores = ref({});
const halfTimeTip = ref(false);
const isEnded = ref(false);
watch(
() => [players.value, playersScores.value],
([n_players, n_scores]) => {
if (n_players.length) {
playersSorted.value = Object.keys(n_scores)
.sort((a, b) => n_scores[b].length - n_scores[a].length)
.map((pid) => n_players.find((p) => p.id == pid));
}
},
{
deep: true, // 添加深度监听
immediate: true,
}
);
function recoverData(battleInfo) {
uni.removeStorageSync("last-awake-time");
battleId.value = battleInfo.id;
players.value = [...battleInfo.blueTeam, ...battleInfo.redTeam];
players.value.forEach((p) => {
playersScores.value[p.id] = [...p.arrows];
if (p.id === user.value.id) scores.value = [...p.arrows];
});
const remain = Date.now() / 1000 - battleInfo.startTime;
console.log(`当前局已进行${remain}`);
if (battleInfo.status === 0) {
if (remain > 0) {
setTimeout(() => {
uni.$emit("update-timer", 15 - remain);
}, 200);
}
} else {
start.value = true;
}
if (battleInfo.status === 2) {
const elapsedTime = (Date.now() - Date.parse(battleInfo.createdAt)) / 1000;
console.log("elapsedTime:", elapsedTime);
startCount.value = true;
// 这里的开始时间不是游戏开始时间,而是上半场或者下半场或者中场的开始时间,还要根据状态来判断
tips.value = battleInfo.halfGame
? "下半场请再射6箭"
: "上半场请先射6箭";
setTimeout(() => {
uni.$emit("update-ramain", 90 - remain);
}, 200);
} else if (battleInfo.status === 9) {
startCount.value = false;
tips.value = "准备下半场";
setTimeout(() => {
uni.$emit("update-ramain", 0);
}, 200);
}
}
onLoad(async (options) => {
if (options.gameMode == 1) title.value = "好友约战 - 大乱斗";
if (options.gameMode == 2) title.value = "排位赛 - 大乱斗";
if (options.battleId) {
battleId.value = options.battleId;
const players = uni.getStorageSync("melee-players");
if (players) {
players.value = players;
players.value.forEach((p) => {
playersScores.value[p.id] = [];
});
}
const battleInfo = uni.getStorageSync("current-battle");
if (battleInfo) {
recoverData(battleInfo);
setTimeout(getCurrentGameAPI, 2000);
}
}
});
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.id !== battleId.value) return;
if (msg.constructor === MESSAGETYPES.MeleeAllReady) {
start.value = true;
startCount.value = true;
tips.value = scores.value.length
? "下半场请再射6箭"
: "上半场请先射6箭";
halfTimeTip.value = false;
}
if (msg.constructor === MESSAGETYPES.ShootResult) {
if (!start.value) getCurrentGameAPI();
if (msg.userId === user.value.id) {
scores.value.push({ ...msg.target });
power.value = msg.target.battery;
}
playersScores.value[msg.userId].push({ ...msg.target });
}
if (msg.constructor === MESSAGETYPES.HalfTimeOver) {
uni.$emit("update-ramain", 0);
[...msg.groupUserStatus.redTeam, ...msg.groupUserStatus.blueTeam].forEach(
(player) => {
playersScores.value[player.id] = [...player.arrows];
if (player.id === user.value.id) scores.value = [...player.arrows];
}
);
startCount.value = false;
halfTimeTip.value = true;
tips.value = "准备下半场";
}
if (msg.constructor === MESSAGETYPES.MatchOver) {
isEnded.value = true;
uni.setStorageSync("last-battle", msg.endStatus);
setTimeout(() => {
uni.redirectTo({
url: "/pages/battle-result",
});
}, 1000);
}
if (msg.constructor === MESSAGETYPES.BackToGame) {
uni.$emit("update-header-loading", false);
if (msg.battleInfo) recoverData(msg.battleInfo);
}
});
}
const onBack = () => {
uni.$showHint(2);
};
onMounted(() => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
});
onBeforeUnmount(() => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
});
const refreshTimer = ref(null);
onShow(async () => {
if (battleId.value) {
if (!isEnded.value && (await isGameEnded(battleId.value))) return;
getCurrentGameAPI();
const refreshData = () => {
const lastAwakeTime = uni.getStorageSync("last-awake-time");
if (lastAwakeTime) {
getCurrentGameAPI();
} else {
clearInterval(refreshTimer.value);
}
};
refreshTimer.value = setInterval(refreshData, 2000);
}
});
onHide(() => {
if (refreshTimer.value) clearInterval(refreshTimer.value);
uni.setStorageSync("last-awake-time", Date.now());
});
</script>
<template>
<Container :title="title" :bgType="1" :onBack="onBack">
<view class="container">
<BattleHeader v-if="!start" :players="players" />
<TestDistance v-if="!start" :guide="false" :isBattle="true" />
<ShootProgress
:show="start"
:start="start && startCount"
:tips="tips"
:total="90"
:melee="true"
:battleId="battleId"
/>
<view v-if="start" class="user-row">
<Avatar :src="user.avatar" :size="35" />
<BowPower :power="power" />
</view>
<BowTarget
v-if="start"
:currentRound="scores.length"
:totalRound="12"
:scores="scores"
:stop="!startCount"
/>
<view :style="{ paddingBottom: '20px' }">
<PlayerScore
v-if="start"
v-for="(player, index) in playersSorted"
:key="index"
:name="player.name"
:avatar="player.avatar"
:scores="playersScores[player.id] || []"
/>
</view>
<ScreenHint
:show="halfTimeTip"
mode="small"
:onClose="() => (halfTimeTip = false)"
>
<view class="half-time-tip">
<text>上半场结束休息一下吧:</text>
<text>20秒后开始下半场</text>
</view>
</ScreenHint>
</view>
</Container>
</template>
<style scoped>
.container {
width: 100%;
height: 100%;
}
</style>

View File

@@ -14,11 +14,10 @@ const arrows = ref([]);
const total = ref(0);
onLoad(async (options) => {
if (options.id) {
const result = await getPractiseAPI(options.id);
arrows.value = result.arrows;
total.value = result.completed_arrows;
}
if (!options.id) return;
const result = await getPractiseAPI(options.id || 176);
arrows.value = result.details;
total.value = result.details.length;
});
</script>
@@ -47,7 +46,7 @@ onLoad(async (options) => {
:completeEffect="false"
:rowCount="total === 12 ? 6 : 9"
:total="total"
:scores="arrows.map((a) => a.ring)"
:arrows="arrows"
:margin="arrows.length === 12 ? 4 : 1"
:fontSize="arrows.length === 12 ? 25 : 22"
/>

View File

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

View File

@@ -1,5 +1,6 @@
<script setup>
import { onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Avatar from "@/components/Avatar.vue";
import BowData from "@/components/BowData.vue";
@@ -17,7 +18,7 @@ const practiseList = ref([]);
const toMatchDetail = (id) => {
uni.navigateTo({
url: `/pages/match-detail?id=${id}`,
url: `/pages/match-detail?battleId=${id}`,
});
};
const getPractiseDetail = async (id) => {
@@ -52,10 +53,25 @@ const onPractiseLoading = async (page) => {
}
return result.length;
};
const getName = (battle) => {
if (battle.mode <= 3) return `${battle.mode}V${battle.mode}`;
return battle.mode + "人大乱斗";
};
/**
* 支持通过 URL 参数指定初始 tab
* @example /pages/my-growth?tab=1 跳转到「好友约战」tab
*/
onLoad((options) => {
if (options && options.tab !== undefined) {
const tabIndex = parseInt(options.tab, 10);
if (!isNaN(tabIndex)) selectedIndex.value = tabIndex;
}
});
</script>
<template>
<Container title="我的成长脚印" overflow="hidden">
<Container title="我的成长脚印" :scroll="false">
<view class="tabs">
<view
v-for="(rankType, index) in ['排位赛', '好友约战', '个人练习']"
@@ -70,66 +86,74 @@ const onPractiseLoading = async (page) => {
</view>
</view>
<view class="contents">
<ScrollList :show="selectedIndex === 0" :onLoading="onMatchLoading">
<view
v-for="(item, index) in matchList"
:key="index"
@click="() => toMatchDetail(item.battleId)"
>
<view class="contest-header">
<text>{{ item.name }}</text>
<text>{{ item.createdAt }}</text>
<image src="../static/back.png" mode="widthFix" />
</view>
<BattleHeader
:players="item.mode === 1 ? [] : item.players"
:blueTeam="item.bluePlayers"
:redTeam="item.redPlayers"
:winner="item.winner"
:showRank="item.mode === 2"
:showHeader="false"
/>
</view>
</ScrollList>
<ScrollList :show="selectedIndex === 1" :onLoading="onBattleLoading">
<view
v-for="(item, index) in battleList"
:key="index"
@click="() => toMatchDetail(item.battleId)"
>
<view class="contest-header">
<text>{{ item.name }}</text>
<text>{{ item.createdAt }}</text>
<image src="../static/back.png" mode="widthFix" />
</view>
<BattleHeader
:players="item.mode === 1 ? [] : item.players"
:blueTeam="item.bluePlayers"
:redTeam="item.redPlayers"
:winner="item.winner"
:showRank="item.mode === 2"
:showHeader="false"
/>
</view>
</ScrollList>
<ScrollList
:show="selectedIndex === 2"
:onLoading="onPractiseLoading"
:pageSize="15"
<swiper
:current="selectedIndex"
@change="(e) => (selectedIndex = e.detail.current)"
:style="{ height: '100%' }"
>
<view
v-for="(item, index) in practiseList"
:key="index"
class="practice-record"
@click="() => getPractiseDetail(item.id)"
>
<text
>{{ item.completed_arrows === 36 ? "耐力挑战" : "单组练习" }}
{{ item.createdAt }}</text
>
<image src="../static/back.png" mode="widthFix" />
</view>
</ScrollList>
<swiper-item>
<ScrollList :onLoading="onMatchLoading">
<view
v-for="(item, index) in matchList"
:key="index"
@click="() => toMatchDetail(item.id)"
>
<view class="contest-header">
<text>{{ getName(item) }}</text>
<text>{{ item.createTime }}</text>
<image src="../static/back.png" mode="widthFix" />
</view>
<BattleHeader
:players="item.teams[0] ? item.teams[0].players : []"
:blueTeam="item.teams[1] ? item.teams[1].players : []"
:redTeam="item.teams[2] ? item.teams[2].players : []"
:winner="item.winTeam"
:showRank="item.teams[0]"
:showHeader="false"
/>
</view>
</ScrollList>
</swiper-item>
<swiper-item>
<ScrollList :onLoading="onBattleLoading">
<view
v-for="(item, index) in battleList"
:key="index"
@click="() => toMatchDetail(item.id)"
>
<view class="contest-header">
<text>{{ getName(item) }}</text>
<text>{{ item.createTime }}</text>
<image src="../static/back.png" mode="widthFix" />
</view>
<BattleHeader
:players="item.teams[0] ? item.teams[0].players : []"
:blueTeam="item.teams[1] ? item.teams[1].players : []"
:redTeam="item.teams[2] ? item.teams[2].players : []"
:winner="item.winTeam"
:showRank="item.teams[0]"
:showHeader="false"
/>
</view>
</ScrollList>
</swiper-item>
<swiper-item>
<ScrollList :onLoading="onPractiseLoading" :pageSize="15">
<view
v-for="(item, index) in practiseList"
:key="index"
class="practice-record"
@click="() => getPractiseDetail(item.id)"
>
<text
>{{ item.completed_arrows === 36 ? "耐力挑战" : "单组练习" }}
{{ item.createTime }}</text
>
<image src="../static/back.png" mode="widthFix" />
</view>
</ScrollList>
</swiper-item>
</swiper>
</view>
</Container>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import BowTarget from "@/components/BowTarget.vue";
@@ -10,92 +11,95 @@ import Avatar from "@/components/Avatar.vue";
import BowPower from "@/components/BowPower.vue";
import TestDistance from "@/components/TestDistance.vue";
import BubbleTip from "@/components/BubbleTip.vue";
import { createPractiseAPI } from "@/apis";
import { generateCanvasImage } from "@/util";
import { MESSAGETYPES, roundsName } from "@/constants";
import audioManager from "@/audioManager";
import {
createPractiseAPI,
startPractiseAPI,
endPractiseAPI,
getPractiseAPI,
} from "@/apis";
import { sharePractiseData } from "@/canvas";
import { wxShare, debounce } from "@/util";
import { MESSAGETYPESV2, roundsName } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const start = ref(false);
const scores = ref([]);
const total = 12;
const currentRound = ref(0);
const practiseResult = ref({});
const power = ref(0);
const practiseId = ref("");
const showGuide = ref(false);
const tips = ref("");
const targetType = ref(1);
onLoad((options) => {
if (options.target) {
targetType.value = Number(options.target);
}
});
const onReady = async () => {
const result = await createPractiseAPI(total);
if (result) practiseId.value = result.id;
currentRound.value = 0;
await startPractiseAPI();
scores.value = [];
start.value = true;
setTimeout(() => {
uni.$emit("play-sound", "请开始射击");
}, 300);
audioManager.play("练习开始");
};
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
if (scores.value.length < total) {
scores.value.push(msg.target);
currentRound.value += 1;
if (currentRound.value === 4) {
currentRound.value = 1;
}
if (practiseId && scores.value.length === total / 2) {
showGuide.value = true;
setTimeout(() => {
showGuide.value = false;
}, 3000);
}
}
power.value = msg.target.battery;
}
if (msg.constructor === MESSAGETYPES.ShootSyncMePracticeID) {
if (practiseId.value && practiseId.value === msg.practice.id) {
setTimeout(() => {
start.value = false;
practiseResult.value = {
...msg.practice,
arrows: JSON.parse(msg.practice.arrows),
lvl: msg.lvl,
};
generateCanvasImage(
"shareCanvas",
2,
user.value,
practiseResult.value
);
}, 1500);
}
}
});
const onOver = async () => {
practiseResult.value = await getPractiseAPI(practiseId.value);
start.value = false;
};
async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult) {
scores.value = msg.details;
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
// setTimeout(onOver, 1500);
}
}
async function onComplete() {
if (
practiseResult.value.arrows &&
practiseResult.value.arrows.length === total
) {
const validArrows = (practiseResult.value.details || []).filter(
(a) => a.x !== -30 && a.y !== -30
);
if (validArrows.length === total) {
uni.navigateBack();
} else {
practiseId.value = "";
practiseResult.value = {};
start.value = false;
scores.value = [];
currentRound.value = 0;
const result = await createPractiseAPI(total, 120);
if (result) practiseId.value = result.id;
}
}
onMounted(() => {
const onClickShare = debounce(async () => {
await sharePractiseData("shareCanvas", 2, user.value, practiseResult.value);
await wxShare("shareCanvas");
});
function onAudioEnded(s) {
if (s.indexOf("比赛结束") >= 0) {
onOver()
}
}
onMounted(async () => {
// audioManager.play("第一轮");
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("share-image", onClickShare);
uni.$on("audioEnded", onAudioEnded);
const result = await createPractiseAPI(total, 120, targetType.value);
if (result) practiseId.value = result.id;
});
onBeforeUnmount(() => {
@@ -103,14 +107,22 @@ onBeforeUnmount(() => {
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
uni.$off("share-image", onClickShare);
uni.$off("audioEnded", onAudioEnded);
audioManager.stopAll();
endPractiseAPI();
});
</script>
<template>
<Container :bgType="1" title="个人单组练习">
<Container
:bgType="1"
title="个人单组练习"
:showBottom="!start && !scores.length"
>
<view>
<TestDistance v-if="!practiseId" />
<block v-if="practiseId">
<TestDistance v-if="!start && !practiseResult.id" />
<block v-else>
<ShootProgress
:tips="`${
!start || scores.length === 12
@@ -120,7 +132,7 @@ onBeforeUnmount(() => {
}轮`
}`"
:start="start"
:total="120"
:onStop="onOver"
/>
<view class="user-row">
<Avatar :src="user.avatar" :size="35" />
@@ -128,30 +140,34 @@ onBeforeUnmount(() => {
<text>还有两场坚持</text>
<text>就是胜利💪</text>
</BubbleTip>
<BowPower :power="power" />
<BowPower />
</view>
<BowTarget
:totalRound="start ? total / 4 : 0"
:currentRound="currentRound"
:currentRound="scores.length % 3"
:scores="scores"
/>
<ScorePanel2 :scores="scores.map((s) => s.ring)" />
<ScorePanel2 :arrows="scores" />
<ScoreResult
v-if="practiseResult.arrows"
v-if="practiseResult.details"
:rowCount="6"
:total="total"
:onClose="onComplete"
:result="practiseResult"
:tipSrc="`../static/${
practiseResult.arrows.length < total ? 'un' : ''
practiseResult.details.filter(
(arrow) => arrow.x !== -30 && arrow.y !== -30
).length < total
? 'un'
: ''
}finish-tip.png`"
/>
<canvas class="share-canvas" canvas-id="shareCanvas"></canvas>
<canvas class="share-canvas" id="shareCanvas" type="2d"></canvas>
</block>
</view>
<view :style="{ marginBottom: '20px' }">
<SButton v-if="!start" :onClick="onReady">准备好了直接开始</SButton>
</view>
<template #bottom>
<SButton :onClick="onReady">准备好了直接开始</SButton>
</template>
</Container>
</template>

View File

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

View File

@@ -4,23 +4,29 @@ import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import Guide from "@/components/Guide.vue";
import Avatar from "@/components/Avatar.vue";
import TargetPicker from "@/components/TargetPicker.vue";
import { getPractiseDataAPI } from "@/apis";
import { canEenter } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const { user, device, online } = storeToRefs(store);
const data = ref({});
const showTargetPicker = ref(false);
const pendingPractiseType = ref("");
const toPractiseOne = () => {
uni.navigateTo({
url: "/pages/practise-one",
});
const goPractise = async (type) => {
if (!canEenter(user.value, device.value, online.value)) return;
pendingPractiseType.value = type;
showTargetPicker.value = true;
};
const toPractiseTwo = () => {
const handleTargetConfirm = (target) => {
showTargetPicker.value = false;
const type = pendingPractiseType.value;
uni.navigateTo({
url: "/pages/practise-two",
url: `/pages/practise-${type}?target=${target}`,
});
};
@@ -32,14 +38,12 @@ onShow(async () => {
<template>
<Container title="个人练习">
<view :style="{ width: '100%' }">
<view :style="{ width: '100%', height: '100%' }">
<Guide>
<text :style="{ color: '#fed847' }"
>师傅领进门修行靠自身赶紧练起来吧</text
>
<text :style="{ fontSize: '12px' }"
>坚持练习就能你快速升级早日加入全国排位赛</text
>
<view class="guide-tips">
<text>师傅领进门修行靠自身赶紧练起来吧</text>
<text>坚持练习就能你快速升级早日加入全国排位赛</text>
</view>
</Guide>
<view class="practise-data">
<view>
@@ -78,14 +82,14 @@ onShow(async () => {
</view>
</view>
</view>
<view class="practise-btn" @click="toPractiseOne">
<view class="practise-btn" @click="() => goPractise('one')">
<image
src="https://static.shelingxingqiu.com/attachment/2025-07-12/db9x668e2vdtqh0otq.png"
class="practise1"
mode="widthFix"
/>
</view>
<view class="practise-btn" @click="toPractiseTwo">
<view class="practise-btn" @click="() => goPractise('two')">
<image
src="https://static.shelingxingqiu.com/attachment/2025-07-12/db9x668eehkvyicc08.png"
class="practise2"
@@ -93,6 +97,11 @@ onShow(async () => {
/>
</view>
</view>
<TargetPicker
:show="showTargetPicker"
:onClose="() => (showTargetPicker = false)"
:onConfirm="handleTargetConfirm"
/>
</Container>
</template>

View File

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

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
<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>

View File

@@ -1,145 +0,0 @@
<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>

View File

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

View File

@@ -1,11 +1,9 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import {ref, onMounted, onBeforeUnmount, nextTick} from "vue";
import {onLoad, onShow, onHide} from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import BattleHeader from "@/components/BattleHeader.vue";
import BowTarget from "@/components/BowTarget.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import PlayersRow from "@/components/PlayersRow.vue";
import BattleFooter from "@/components/BattleFooter.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import SButton from "@/components/SButton.vue";
@@ -13,23 +11,21 @@ import RoundEndTip from "@/components/RoundEndTip.vue";
import TestDistance from "@/components/TestDistance.vue";
import TeamAvatars from "@/components/TeamAvatars.vue";
import ShootProgress2 from "@/components/ShootProgress2.vue";
import { getCurrentGameAPI } from "@/apis";
import { isGameEnded } from "@/util";
import { MESSAGETYPES, roundsName } from "@/constants";
import {laserCloseAPI, getBattleAPI} from "@/apis";
import {MESSAGETYPESV2} from "@/constants";
import audioManager from "@/audioManager";
import useStore from "@/store";
import { storeToRefs } from "pinia";
import {storeToRefs} from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const start = ref(false);
const {user} = storeToRefs(store);
const start = ref(null);
const tips = ref("");
const battleId = ref("");
const currentRound = ref(1);
const currentRound = ref(0);
const goldenRound = ref(0);
const currentRedPoint = ref(0);
const currentBluePoint = ref(0);
const totalRounds = ref(0);
const power = ref(0);
const scores = ref([]);
const blueScores = ref([]);
const redTeam = ref([]);
@@ -40,330 +36,237 @@ const redPoints = ref(0);
const bluePoints = ref(0);
const showRoundTip = ref(false);
const isFinalShoot = ref(false);
const isEnded = ref(false);
const matchStatus = ref(undefined);
const updateRemainSecond = ref(0);
const onBack = () => {
uni.$showHint(2);
};
const recoverData = (battleInfo, {force = false, arrowOnly = false} = {}) => {
try {
battleId.value = battleInfo.matchId;
function recoverData(battleInfo) {
uni.removeStorageSync("last-awake-time");
battleId.value = battleInfo.id;
redTeam.value = battleInfo.redTeam;
blueTeam.value = battleInfo.blueTeam;
if (battleInfo.status === 0) {
const readyRemain = Date.now() / 1000 - battleInfo.startTime;
console.log(`当前局已进行${readyRemain}`);
if (readyRemain > 0) {
setTimeout(() => {
uni.$emit("update-timer", 15 - readyRemain);
}, 200);
// 优先使用接口返回的队伍数据,如果没有则尝试从缓存读取(应对匹配刚完成接口未就绪的情况)
const t1 = battleInfo.teams?.[1] || {};
const t2 = battleInfo.teams?.[2] || {};
if (t1.players) blueTeam.value = [...t1.players];
else {
const cached = uni.getStorageSync("blue-team");
if (cached && cached.length) blueTeam.value = cached;
}
} else {
start.value = true;
bluePoints.value = 0;
redPoints.value = 0;
currentRound.value = battleInfo.currentRound;
totalRounds.value = battleInfo.maxRound;
roundResults.value = [...battleInfo.roundResults];
battleInfo.roundResults.forEach((round) => {
const blueTotal = round.blueArrows.reduce(
(last, next) => last + next.ring,
0
);
const redTotal = round.redArrows.reduce(
(last, next) => last + next.ring,
0
);
if (blueTotal === redTotal) {
bluePoints.value += 1;
redPoints.value += 1;
} else if (blueTotal > redTotal) {
bluePoints.value += 2;
} else {
redPoints.value += 2;
if (t2.players) redTeam.value = [...t2.players];
else {
const cached = uni.getStorageSync("red-team");
if (cached && cached.length) redTeam.value = cached;
}
start.value = battleInfo.status !== 0;
if (battleInfo.status === 0) {
const readyRemain = (Date.now() - (battleInfo.createTime || Date.now())) / 1000;
if (readyRemain > 0 && readyRemain < 15) {
setTimeout(() => {
uni.$emit("update-timer", 15 - readyRemain - 0.2);
}, 200);
}
});
const hasCurrentRoundData =
battleInfo.redTeam.some(
(item) => !!item.shotHistory[battleInfo.currentRound]
) ||
battleInfo.blueTeam.some(
(item) => !!item.shotHistory[battleInfo.currentRound]
);
if (
battleInfo.currentRound > battleInfo.roundResults.length &&
hasCurrentRoundData
) {
const blueArrows = [];
const redArrows = [];
battleInfo.redTeam.forEach((item) =>
item.shotHistory[battleInfo.currentRound]
.filter((item) => !!item.playerId)
.forEach((item) => redArrows.push(item))
);
battleInfo.blueTeam.forEach((item) =>
item.shotHistory[battleInfo.currentRound]
.filter((item) => !!item.playerId)
.forEach((item) => blueArrows.push(item))
);
roundResults.value.push({
redArrows,
blueArrows,
});
return;
}
if (battleInfo.goldenRound) {
const { ShotCount, RedRecords, BlueRecords } = battleInfo.goldenRound;
currentRound.value += ShotCount;
goldenRound.value += ShotCount;
isFinalShoot.value = true;
for (let i = 0; i < ShotCount; i++) {
const roundData = {
redArrows:
RedRecords && RedRecords[i] ? RedRecords[i].Arrows || [] : [],
blueArrows:
BlueRecords && BlueRecords[i] ? BlueRecords[i].Arrows || [] : [],
gold: true,
};
roundResults.value.push(roundData);
if (!arrowOnly) {
currentShooterId.value = battleInfo.current.playerId;
const redPlayer = battleInfo.teams[2].players.find(
(item) => item.id === battleInfo.current.playerId
);
let nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭";
if (force) nextTips += "重回";
if (
battleInfo.current.playerId === user.value.id &&
redTeam.value.length > 1
) {
nextTips += "你";
}
tips.value = nextTips;
uni.$emit("update-tips", nextTips);
uni.$emit("update-remain", {reset: true, value: 15, team: redTeam?'red':'blue'});
if (force) {
const remain = (Date.now() - battleInfo.current.startTime) / 1000;
console.log(`当前轮已进行${remain}`);
if (remain > 0 && remain < 15) {
updateRemainSecond.value = 15 - remain - 0.2
}
} else {
updateRemainSecond.value = battleInfo.readyTime
}
} 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;
if (roundResults.value[lastIndex]) {
const redArrows = roundResults.value[lastIndex].redArrows;
scores.value = [...redArrows].filter((item) => !!item.playerId);
const blueArrows = roundResults.value[lastIndex].blueArrows;
blueScores.value = [...blueArrows].filter((item) => !!item.playerId);
}
// if (battleInfo.status !== 11) return;
if (battleInfo.firePlayerIndex) {
currentShooterId.value = battleInfo.firePlayerIndex;
const redPlayer = redTeam.value.find(
(item) => item.id === currentShooterId.value
);
tips.value = redPlayer ? "请红队射箭" : "请蓝队射箭";
uni.$emit("update-tips", tips.value);
}
if (battleInfo.fireTime > 0) {
const remain = Date.now() / 1000 - battleInfo.fireTime;
console.log(`当前箭已过${remain}`);
if (remain > 0 && remain <= 15) {
// 等渲染好再通知
setTimeout(() => {
uni.$emit("update-ramain", 15 - remain);
}, 300);
currentRound.value = battleInfo.current.round || 1;
const latestRound = battleInfo.rounds[currentRound.value - 1];
if (latestRound) {
blueScores.value = latestRound.shoots[1];
scores.value = latestRound.shoots[2];
}
}
roundResults.value = battleInfo.rounds || [];
isFinalShoot.value = battleInfo.current.goldRound;
bluePoints.value = battleInfo.teams[1].score;
redPoints.value = battleInfo.teams[2].score;
} catch (err) {
console.log(err);
}
};
function onAudioEnded(s) {
if (s.indexOf('请红方射箭') >= 0 || s.indexOf('请蓝方射箭') >= 0) {
let team = s.indexOf('请红方射箭') >= 0 ? 'red' : 'blue';
uni.$emit("update-remain", {stop: false, value: updateRemainSecond.value, team: team});
}
if (s.indexOf("比赛结束") >= 0) {
onBattleEnd()
}
}
function onBattleEnd() {
if (matchStatus.value === 2) {
uni.redirectTo({
url: `/pages/battle-result?battleId=${battleId.value}`,
});
}
}
function onNewRound(msg) {
showRoundTip.value = true;
isFinalShoot.value = msg.current.goldRound;
const latestRound = msg.rounds[currentRound.value - 1];
if (latestRound) {
if (isFinalShoot.value) {
currentBluePoint.value = msg.teams[1].score;
currentRedPoint.value = msg.teams[2].score;
} else {
currentBluePoint.value = latestRound.scores[1].score;
currentRedPoint.value = latestRound.scores[2].score;
}
}
}
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (msg.constructor === MESSAGETYPES.AllReady) {
start.value = true;
totalRounds.value = msg.groupUserStatus.config.maxRounds;
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.BattleStart) {
start.value = true;
} else if (msg.type === MESSAGETYPESV2.ToSomeoneShoot) {
recoverData(msg);
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
uni.$emit("update-remain", {stop: true})
showRoundTip.value = false;
recoverData(msg, {arrowOnly: true});
} else if (msg.type === MESSAGETYPESV2.NewRound) {
setTimeout(() => {
onNewRound(msg)
}, 800)
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
matchStatus.value = msg.status;
if (msg.status === 4) {
showRoundTip.value = true;
currentBluePoint.value = 0;
currentRedPoint.value = 0;
setTimeout(() => {
uni.navigateBack();
}, 2000);
}
if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) {
if (currentShooterId.value !== msg.userId) {
currentShooterId.value = msg.userId;
const redPlayer = redTeam.value.find(
(item) => item.id === currentShooterId.value
);
if (msg.userId === user.value.id) audioManager.play("轮到你了");
const nextTips = redPlayer ? "请红队射箭" : "请蓝队射箭";
if (nextTips !== tips.value) {
tips.value = nextTips;
uni.$emit("update-tips", tips.value);
} else {
uni.$emit("update-ramain", 15);
}
}
}
if (msg.constructor === MESSAGETYPES.ShootResult) {
if (currentShooterId.value !== msg.userId) return;
const isRed = redTeam.value.find((item) => item.id === msg.userId);
if (isRed) scores.value.push({ ...msg.target });
else blueScores.value.push({ ...msg.target });
// 下标从0开始的要减1
if (!roundResults.value[currentRound.value - 1]) {
roundResults.value.push({
redArrows: [],
blueArrows: [],
gold: goldenRound.value > 0,
});
}
roundResults.value[currentRound.value - 1][
isRed ? "redArrows" : "blueArrows"
].push({ ...msg.target });
}
if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
const result = msg.preRoundResult;
scores.value = [];
blueScores.value = [];
currentShooterId.value = 0;
currentBluePoint.value = result.blueScore;
currentRedPoint.value = result.redScore;
bluePoints.value += result.blueScore;
redPoints.value += result.redScore;
currentRound.value = result.currentRound + 1;
if (!result.goldenRound) {
showRoundTip.value = true;
}
}
if (msg.constructor === MESSAGETYPES.FinalShoot) {
currentShooterId.value = 0;
currentRound.value = msg.groupUserStatus.currentRound + 1;
goldenRound.value += 1;
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
currentBluePoint.value = bluePoints.value;
currentRedPoint.value = redPoints.value;
if (!isFinalShoot.value) {
isFinalShoot.value = true;
showRoundTip.value = true;
tips.value = "准备开始决金箭";
}
}
if (msg.constructor === MESSAGETYPES.MatchOver) {
if (msg.endStatus.noSaved) {
currentRound.value += 1;
currentBluePoint.value = 0;
currentRedPoint.value = 0;
showRoundTip.value = true;
isFinalShoot.value = false;
setTimeout(() => {
uni.navigateBack();
}, 3000);
} else {
isEnded.value = true;
uni.setStorageSync("last-battle", msg.endStatus);
setTimeout(() => {
uni.redirectTo({
url: "/pages/battle-result",
});
}, 1000);
}
}
if (msg.constructor === MESSAGETYPES.BackToGame) {
uni.$emit("update-header-loading", false);
if (msg.battleInfo) recoverData(msg.battleInfo);
}
});
}
}
onLoad(async (options) => {
if (options.battleId) {
battleId.value = options.battleId;
redTeam.value = uni.getStorageSync("red-team");
blueTeam.value = uni.getStorageSync("blue-team");
const battleInfo = uni.getStorageSync("current-battle");
if (battleInfo) {
await nextTick(() => {
recoverData(battleInfo);
});
setTimeout(getCurrentGameAPI, 2000);
}
}
if (options.battleId) battleId.value = options.battleId;
// uni.enableAlertBeforeUnload({
// message: "离开比赛可能导致比赛失败,是否继续?",
// success: (res) => {
// console.log("已启用离开提示");
// },
// });
});
onMounted(() => {
onMounted(async () => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
uni.$on("audioEnded", onAudioEnded);
await laserCloseAPI();
});
onBeforeUnmount(() => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
uni.$off("audioEnded", onAudioEnded);
audioManager.stopAll();
});
const refreshTimer = ref(null);
onShow(async () => {
if (battleId.value) {
if (!isEnded.value && (await isGameEnded(battleId.value))) return;
getCurrentGameAPI();
const refreshData = () => {
const lastAwakeTime = uni.getStorageSync("last-awake-time");
if (lastAwakeTime) {
getCurrentGameAPI();
} else {
clearInterval(refreshTimer.value);
}
};
refreshTimer.value = setInterval(refreshData, 2000);
const result = await getBattleAPI(battleId.value);
if (!result) return;
if (result.status === 2) {
uni.showToast({
title: "比赛已结束",
icon: "none",
});
uni.navigateBack({
delta: 2,
});
} else {
recoverData(result, {force: true});
}
}
});
onHide(() => {
if (refreshTimer.value) clearInterval(refreshTimer.value);
uni.setStorageSync("last-awake-time", Date.now());
});
</script>
<template>
<Container :bgType="start ? 3 : 1" :onBack="onBack">
<Container :bgType="start ? 3 : 1">
<view class="container">
<BattleHeader v-if="!start" :redTeam="redTeam" :blueTeam="blueTeam" />
<TestDistance v-if="!start" :guide="false" :isBattle="true" />
<BattleHeader
v-if="start === false"
:redTeam="redTeam"
:blueTeam="blueTeam"
/>
<TestDistance v-if="start === false" :guide="false" :isBattle="true"/>
<view v-if="start" class="players-row">
<TeamAvatars
:team="blueTeam"
:isRed="false"
:currentShooterId="currentShooterId"
:team="blueTeam"
:isRed="false"
:currentShooterId="currentShooterId"
/>
<ShootProgress2
:tips="tips"
:currentRound="
:tips="tips"
:currentRound="
goldenRound > 0 ? 'gold' + goldenRound : 'round' + currentRound
"
/>
<TeamAvatars :team="redTeam" :currentShooterId="currentShooterId" />
<TeamAvatars :team="redTeam" :currentShooterId="currentShooterId"/>
</view>
<BowTarget
v-if="start"
mode="team"
:power="start ? power : 0"
:scores="scores"
:blueScores="blueScores"
v-if="start"
mode="team"
:scores="scores"
:blueScores="blueScores"
/>
<BattleFooter
v-if="start"
:roundResults="roundResults"
:redPoints="redPoints"
:bluePoints="bluePoints"
:goldenRound="goldenRound"
:power="power"
v-if="start"
:roundResults="roundResults"
:redPoints="redPoints"
:bluePoints="bluePoints"
:goldenRound="goldenRound"
/>
<ScreenHint
:show="showRoundTip"
:onClose="() => (showRoundTip = false)"
:mode="isFinalShoot ? 'tall' : 'normal'"
:show="showRoundTip"
:onClose="() => (showRoundTip = false)"
:mode="isFinalShoot ? 'tall' : 'normal'"
>
<RoundEndTip
v-if="showRoundTip"
:isFinal="isFinalShoot"
:round="currentRound - 1"
:bluePoint="currentBluePoint"
:redPoint="currentRedPoint"
:roundData="
roundResults[currentRound - 2] ? roundResults[currentRound - 2] : []
v-if="showRoundTip"
:isFinal="isFinalShoot"
:round="currentRound"
:bluePoint="currentBluePoint"
:redPoint="currentRedPoint"
:roundData="
roundResults[currentRound - 1] ? roundResults[currentRound - 1] : []
"
:onAutoClose="() => (showRoundTip = false)"
:onAutoClose="()=>{ showRoundTip = false}"
/>
</ScreenHint>
</view>
@@ -375,11 +278,12 @@ onHide(() => {
width: 100%;
height: 100%;
}
.players-row {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: -7vw;
margin-top: -3vw;
margin-top: -2%;
margin-bottom: 6%;
}
</style>

View File

@@ -5,105 +5,42 @@ import Container from "@/components/Container.vue";
import BowTarget from "@/components/BowTarget.vue";
import Avatar from "@/components/Avatar.vue";
import { roundsName } from "@/constants";
import { getGameAPI } from "@/apis";
import { getBattleAPI } from "@/apis";
const selected = ref(0);
const redScores = ref([]);
const blueScores = ref([]);
const tabs = ref(["所有轮次"]);
const tabs = ref([]);
const players = ref([]);
const allRoundsScore = ref({});
const data = ref({
goldenRounds: [],
});
const data = ref({});
const loadArrows = (round) => {
round.shoots[1].forEach((arrow) => {
blueScores.value.push(arrow);
});
round.shoots[2].forEach((arrow) => {
redScores.value.push(arrow);
});
};
onLoad(async (options) => {
if (options.battleId) {
const result = await getGameAPI(
options.battleId || "BATTLE-1756453741433684760-512"
);
data.value = result;
Object.values(result.bluePlayers).forEach((p, index) => {
players.value.push(p);
if (
Object.values(result.redPlayers) &&
Object.values(result.redPlayers)[index]
) {
players.value.push(Object.values(result.redPlayers)[index]);
}
});
if (result.goldenRounds) {
result.goldenRounds.forEach(() => {
tabs.value.push("决金箭");
});
}
Object.keys(result.roundsData).forEach((key, index) => {
if (
index <
Object.keys(result.roundsData).length - result.goldenRounds.length
) {
tabs.value.push(`${roundsName[key]}`);
}
});
onClickTab(0);
}
if (!options.battleId) return;
const result = await getBattleAPI(options.battleId || "57943107462893568");
data.value = result;
data.value.teams[1].players.forEach((p, index) => {
players.value.push(p);
players.value.push(data.value.teams[2].players[index]);
});
Object.values(data.value.rounds).forEach((round, index) => {
if (round.ifGold) tabs.value.push(`决金箭`);
else tabs.value.push(`${roundsName[index + 1]}`);
});
selected.value = Number(options.selected || 0);
onClickTab(selected.value);
});
const onClickTab = (index) => {
selected.value = index;
redScores.value = [];
blueScores.value = [];
const { bluePlayers, redPlayers, roundsData, goldenRounds } = data.value;
let maxArrowLength = 0;
if (index === 0) {
Object.keys(bluePlayers).forEach((p) => {
allRoundsScore.value[p] = [];
Object.values(roundsData).forEach((round) => {
if (!round[p]) return;
allRoundsScore.value[p].push(
round[p].reduce((last, next) => last + next.ring, 0)
);
round[p].forEach((arrow) => {
blueScores.value.push(arrow);
});
});
});
Object.keys(redPlayers).forEach((p) => {
allRoundsScore.value[p] = [];
Object.values(roundsData).forEach((round) => {
if (!round[p]) return;
allRoundsScore.value[p].push(
round[p].reduce((last, next) => last + next.ring, 0)
);
round[p].forEach((arrow) => {
redScores.value.push(arrow);
});
});
});
} else if (index <= goldenRounds.length) {
const dataIndex =
Object.keys(roundsData).length - goldenRounds.length + index;
Object.keys(bluePlayers).forEach((p) => {
if (!roundsData[dataIndex][p]) return;
roundsData[dataIndex][p].forEach((arrow) => {
blueScores.value.push(arrow);
});
});
Object.keys(redPlayers).forEach((p) => {
if (!roundsData[dataIndex][p]) return;
roundsData[dataIndex][p].forEach((arrow) => {
redScores.value.push(arrow);
});
});
} else {
Object.keys(bluePlayers).forEach((p) => {
roundsData[index - goldenRounds.length][p].forEach((arrow) => {
blueScores.value.push(arrow);
});
});
Object.keys(redPlayers).forEach((p) => {
roundsData[index - goldenRounds.length][p].forEach((arrow) => {
redScores.value.push(arrow);
});
});
}
loadArrows(data.value.rounds[index]);
};
</script>
@@ -121,7 +58,7 @@ const onClickTab = (index) => {
</view>
</view>
<view :style="{ margin: '20px 0' }">
<BowTarget :scores="redScores" :blueScores="blueScores" />
<BowTarget :scores="redScores" :blueScores="blueScores" mode="team" />
</view>
<view class="score-container">
<view
@@ -134,45 +71,18 @@ const onClickTab = (index) => {
>
<Avatar
:src="player.avatar"
:borderColor="
data.bluePlayers[player.playerId] ? '#64BAFF' : '#FF6767'
"
:borderColor="index % 2 === 0 ? '#64BAFF' : '#FF6767'"
:size="36"
/>
<view>
<view
v-if="selected === 0"
v-for="(ring, index) in allRoundsScore[player.playerId]"
v-for="(score, index) in data.rounds[selected].shoots[
index % 2 === 0 ? 1 : 2
]"
:key="index"
class="score-item"
>
{{ ring }}
</view>
<view
v-if="
selected > 0 &&
selected >= data.goldenRounds.length &&
selected <= data.goldenRounds.length
"
v-for="(score, index) in data.roundsData[
Object.keys(data.roundsData).length -
data.goldenRounds.length +
selected
][player.playerId]"
:key="index"
class="score-item"
>
{{ score.ring }}
</view>
<view
v-if="selected > data.goldenRounds.length"
v-for="(score, index) in data.roundsData[
selected - data.goldenRounds.length
][player.playerId]"
:key="index"
class="score-item"
>
{{ score.ring }}
{{ score.ringX ? "X" : score.ring }}
</view>
</view>
</view>

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