Compare commits
38 Commits
0f3cfee622
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
412069524e | ||
|
|
53cc4600a8 | ||
|
|
3b05e7fc28 | ||
|
|
0ab8312bbb | ||
|
|
13eb311575 | ||
|
|
4eddc5a194 | ||
|
|
c249c9e213 | ||
|
|
5911a55432 | ||
|
|
adbafedb5b | ||
|
|
a56ca34cc3 | ||
|
|
1e053f62c0 | ||
|
|
bb0f620d12 | ||
|
|
ecb9fcba6b | ||
|
|
3baa14a6b6 | ||
|
|
b5a8c811aa | ||
|
|
c6c9f9a5d1 | ||
|
|
d22944e575 | ||
|
|
5a09cceab0 | ||
|
|
541c770f27 | ||
| 9c063a9779 | |||
|
|
d0b39a0319 | ||
| 47fc5544ca | |||
|
|
012f641daa | ||
|
|
641e398aae | ||
| 0ccf8d7c6f | |||
| 1c79d5ea95 | |||
| 78291bb76b | |||
|
|
0222a04dea | ||
|
|
8f0b8d5536 | ||
|
|
cdee00b503 | ||
|
|
59f3b9ff97 | ||
|
|
3ebcb44853 | ||
|
|
5dacd05cbb | ||
|
|
4ccc095b05 | ||
|
|
f2e5fed3b7 | ||
|
|
e9ed4e8e44 | ||
|
|
307653f3af | ||
|
|
198d9edf12 |
@@ -19,7 +19,20 @@
|
||||
"Bash(\"D:\\Program Files\\mysql-8.0.32-winx64\\bin\\mysql.exe\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"SHOW TABLES;\")",
|
||||
"Bash(\"D:\\Program Files\\mysql-8.0.32-winx64\\bin\\mysql.exe\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"DESCRIBE athlete;\")",
|
||||
"Bash(tree:*)",
|
||||
"Bash(find:*)"
|
||||
"Bash(find:*)",
|
||||
"Bash(\"D:\\\\Program Files\\\\mysql-8.0.32-winx64\\\\bin\\\\mysql.exe\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"SHOW TABLES LIKE ''%attachment%'';\")",
|
||||
"Bash(\"D:\\\\Program Files\\\\mysql-8.0.32-winx64\\\\bin\\\\mysql.exe\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"DESCRIBE martial_competition_rules_attachment;\")",
|
||||
"Bash(ls -la \"d:\\\\workspace\\\\31.比赛项目\\\\project\\\\martial-mini\\\\src\\\\pages\\\\attachment-view\"\" 2>/dev/null && ls -la \"d:workspace31.比赛项目projectmartial-minisrcpagesevent-photos\"\")",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(npm uninstall:*)",
|
||||
"Bash(\"D:\\\\Program Files\\\\mysql-8.0.32-winx64\\\\bin\\\\mysql.exe\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"SELECT id, name FROM martial_competition LIMIT 5;\")",
|
||||
"Bash(\"D:\\\\Program Files\\\\mysql-8.0.32-winx64\\\\bin\\\\mysql.exe\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"DESCRIBE martial_competition;\")",
|
||||
"Bash(\"D:\\\\Program Files\\\\mysql-8.0.32-winx64\\\\bin\\\\mysql.exe\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"SELECT id, competition_name, total_participants FROM martial_competition WHERE is_deleted = 0 LIMIT 5;\")",
|
||||
"Bash(\"D:\\\\Program Files\\\\mysql-8.0.32-winx64\\\\bin\\\\mysql.exe\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"DESCRIBE martial_registration_order;\")",
|
||||
"Bash(\"D:\\\\Program Files\\\\mysql-8.0.32-winx64\\\\bin\\\\mysql.exe\":*)",
|
||||
"Bash(\"D:\\\\Program Files\\\\mysql-8.0.32-winx64\\\\bin\\\\mysql.exe\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"SELECT id, competition_name, total_participants FROM martial_competition WHERE id = 200;\")",
|
||||
"Bash(\"D:\\\\Program Files\\\\mysql-8.0.32-winx64\\\\bin\\\\mysql.exe\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"SELECT competition_id, COUNT\\(*\\) as order_count, SUM\\(total_participants\\) as total_people, is_deleted FROM martial_registration_order WHERE competition_id = 200 GROUP BY competition_id, is_deleted;\")",
|
||||
"Bash(\"D:\\\\Program Files\\\\mysql-8.0.32-winx64\\\\bin\\\\mysql.exe\" -h localhost -P 3306 -u root -p123456 -D martial_db -e \"SELECT id, competition_name, total_participants, is_deleted FROM martial_competition WHERE id = 200;\")"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -10,9 +10,12 @@ trigger:
|
||||
steps:
|
||||
- name: build
|
||||
image: node:16-alpine
|
||||
environment:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
commands:
|
||||
- npm install
|
||||
- ./node_modules/.bin/cross-env NODE_ENV=production UNI_PLATFORM=h5 ./node_modules/.bin/vue-cli-service build
|
||||
- npm install --legacy-peer-deps
|
||||
- npm run build:h5
|
||||
- ls -la dist/dev/h5/
|
||||
|
||||
- name: deploy
|
||||
image: appleboy/drone-scp
|
||||
@@ -21,7 +24,7 @@ steps:
|
||||
username: root
|
||||
key:
|
||||
from_secret: ssh_key
|
||||
source: dist/build/h5/*
|
||||
source: dist/dev/h5/*
|
||||
target: /var/www/martial-mini
|
||||
strip_components: 3
|
||||
|
||||
|
||||
11
Dockerfile.fixed
Normal file
11
Dockerfile.fixed
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM node:16-alpine as builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build:h5
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist/dev/h5 /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
290
README.md
290
README.md
@@ -1,251 +1,71 @@
|
||||
# 武术赛事报名系统
|
||||
# 武术赛事管理系统 - 用户端
|
||||
|
||||
基于uniapp框架开发的武术赛事报名系统,支持H5和微信小程序。
|
||||
基于 UniApp 开发的武术赛事报名小程序,支持 H5 和微信小程序。
|
||||
|
||||
## 项目概述
|
||||
## 在线访问
|
||||
|
||||
本项目是一个完整的武术赛事报名管理系统,实现了从赛事浏览、报名到信息管理的完整流程。
|
||||
| 服务 | 地址 |
|
||||
|------|------|
|
||||
| 用户端 H5 | https://martial.aitisai.com |
|
||||
| 后端 API | https://martial-api.aitisai.com |
|
||||
|
||||
### 技术栈
|
||||
## 功能特点
|
||||
|
||||
- **框架**: uniapp
|
||||
- 赛事浏览与搜索
|
||||
- 在线报名与支付
|
||||
- 选手信息管理
|
||||
- 报名记录查询
|
||||
- 赛事详情查看
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: UniApp
|
||||
- **平台**: H5、微信小程序
|
||||
- **样式**: SCSS、rpx响应式单位
|
||||
- **版本**: 2.0
|
||||
- **样式**: SCSS
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# H5 开发
|
||||
npm run dev:h5
|
||||
|
||||
# 微信小程序开发
|
||||
npm run dev:mp-weixin
|
||||
|
||||
# 构建 H5
|
||||
npm run build:h5
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
martial-mini/
|
||||
├── pages/ # 页面目录
|
||||
│ ├── home/ # 首页(武术赛事通)
|
||||
│ ├── profile/ # 个人中心
|
||||
│ ├── common-info/ # 常用信息
|
||||
│ ├── add-player/ # 新增选手
|
||||
│ ├── edit-player/ # 编辑选手
|
||||
│ ├── add-contact/ # 新增联系人
|
||||
│ ├── my-registration/ # 我的报名
|
||||
│ ├── event-list/ # 全部赛事列表
|
||||
│ ├── event-detail/ # 赛事详情
|
||||
│ ├── register-type/ # 选择报名类型
|
||||
│ ├── select-event/ # 选择报名项目
|
||||
│ └── event-register/ # 赛事报名流程
|
||||
├── components/ # 公共组件目录
|
||||
│ ├── custom-navbar/ # 自定义导航栏
|
||||
│ ├── custom-tabs/ # Tab切换组件
|
||||
│ └── confirm-modal/ # 确认弹窗组件
|
||||
├── static/ # 静态资源目录
|
||||
│ └── images/ # 图片资源
|
||||
├── image/ # 设计稿图片
|
||||
├── App.vue # 应用主组件
|
||||
├── main.js # 应用入口文件
|
||||
├── pages.json # 页面配置
|
||||
├── manifest.json # 应用配置
|
||||
├── uni.scss # 全局样式变量
|
||||
├── index.html # H5入口页面
|
||||
├── package.json # 项目依赖配置
|
||||
├── check-pages.js # 页面完成度检查工具
|
||||
└── README.md # 项目说明文档
|
||||
├── pages/
|
||||
│ ├── home/ # 首页
|
||||
│ ├── profile/ # 个人中心
|
||||
│ ├── event-list/ # 赛事列表
|
||||
│ ├── event-detail/ # 赛事详情
|
||||
│ ├── event-register/ # 赛事报名
|
||||
│ ├── my-registration/ # 我的报名
|
||||
│ ├── add-player/ # 新增选手
|
||||
│ └── common-info/ # 常用信息
|
||||
├── components/ # 公共组件
|
||||
├── static/ # 静态资源
|
||||
├── pages.json # 页面配置
|
||||
└── manifest.json # 应用配置
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
## 相关仓库
|
||||
|
||||
### 1. 首页(武术赛事通)
|
||||
- 轮播图展示
|
||||
- 精品赛事列表
|
||||
- 赛事状态显示(报名中/已结束)
|
||||
- 快速报名入口
|
||||
| 仓库 | 说明 |
|
||||
|------|------|
|
||||
| [martial-master](https://git.waypeak.work/martial/martial-master) | 后端 API |
|
||||
| [martial-web](https://git.waypeak.work/martial/martial-web) | 管理后台 |
|
||||
| [martial-admin-mini](https://git.waypeak.work/martial/martial-admin-mini) | 裁判端小程序 |
|
||||
|
||||
### 2. 个人中心
|
||||
- 用户信息展示
|
||||
- 我的报名入口
|
||||
- 常用信息管理
|
||||
- 修改密码
|
||||
- 联系我们
|
||||
- 退出登录
|
||||
---
|
||||
|
||||
### 3. 常用信息管理
|
||||
- 选手信息管理(新增/编辑/删除)
|
||||
- 集体信息管理
|
||||
- 联系人信息管理
|
||||
- Tab切换界面
|
||||
|
||||
### 4. 选手管理
|
||||
- 证件类型选择(身份证)
|
||||
- 姓名输入
|
||||
- 证件号码输入(18位身份证号验证)
|
||||
- 队伍名称输入
|
||||
- 表单验证提示
|
||||
|
||||
### 5. 联系人管理
|
||||
- 证件信息
|
||||
- 手机号码(格式验证)
|
||||
- 邮箱地址
|
||||
- 联系地址
|
||||
- 设置默认联系人开关
|
||||
|
||||
### 6. 我的报名
|
||||
- 全部/待开始/进行中/已结束 Tab切换
|
||||
- 赛事状态标签
|
||||
- 赛事详细信息展示
|
||||
- 参赛选手列表
|
||||
- 查看证件功能
|
||||
|
||||
### 7. 赛事列表
|
||||
- 搜索功能
|
||||
- 日期筛选
|
||||
- 地区筛选
|
||||
- 赛事卡片展示
|
||||
- 报名状态显示
|
||||
|
||||
### 8. 赛事详情
|
||||
- 赛事基本信息
|
||||
- 功能网格入口:
|
||||
- 信息发布
|
||||
- 赛事规程
|
||||
- 活动日程
|
||||
- 参赛选手
|
||||
- 比赛实况
|
||||
- 出场顺序
|
||||
- 成绩
|
||||
- 奖牌榜
|
||||
- 图片直播
|
||||
- 报名按钮
|
||||
|
||||
### 9. 报名流程
|
||||
- **选择报名类型**:单人赛/集体赛
|
||||
- **选择报名项目**:多选项目,显示价格
|
||||
- **报名三步骤**:
|
||||
1. 选择选手信息(可新增/编辑/删除选手)
|
||||
2. 订单支付(显示赛事信息、联系人、参赛选手、总价)
|
||||
3. 报名成功(显示选手编号信息)
|
||||
|
||||
## 设计特点
|
||||
|
||||
### 配色方案
|
||||
- 主题色:#C93639(中国红)
|
||||
- 文字色:#333333(深灰)、#666666(中灰)、#999999(浅灰)
|
||||
- 背景色:#f5f5f5(浅灰背景)、#ffffff(白色)
|
||||
|
||||
### 样式规范
|
||||
- 使用rpx响应式单位,自适应不同屏幕
|
||||
- 圆角统一:16rpx(常规)、24rpx(大圆角)
|
||||
- 间距统一:30rpx(页面内边距)
|
||||
- 字体大小:
|
||||
- 标题:36-38rpx
|
||||
- 正文:28-32rpx
|
||||
- 辅助文字:24-26rpx
|
||||
|
||||
### UI组件
|
||||
- 自定义导航栏(支持返回按钮、标题、右侧操作区)
|
||||
- Tab切换组件(带下划线指示器)
|
||||
- 确认弹窗组件(取消/确定)
|
||||
- 表单输入组件
|
||||
- 按钮组件(主按钮/禁用按钮)
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 环境要求
|
||||
- Node.js 12+
|
||||
- HBuilderX(推荐)或 VSCode + uni-app插件
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 运行项目
|
||||
|
||||
#### H5开发
|
||||
```bash
|
||||
npm run dev:h5
|
||||
```
|
||||
|
||||
#### 微信小程序开发
|
||||
```bash
|
||||
npm run dev:mp-weixin
|
||||
```
|
||||
|
||||
### 构建项目
|
||||
|
||||
#### 构建H5
|
||||
```bash
|
||||
npm run build:h5
|
||||
```
|
||||
|
||||
#### 构建微信小程序
|
||||
```bash
|
||||
npm run build:mp-weixin
|
||||
```
|
||||
|
||||
### 检查页面完成度
|
||||
```bash
|
||||
node check-pages.js
|
||||
```
|
||||
|
||||
## 页面列表
|
||||
|
||||
1. **首页** - `/pages/home/home`
|
||||
2. **个人中心** - `/pages/profile/profile`
|
||||
3. **常用信息** - `/pages/common-info/common-info`
|
||||
4. **新增选手** - `/pages/add-player/add-player`
|
||||
5. **编辑选手** - `/pages/edit-player/edit-player`
|
||||
6. **新增联系人** - `/pages/add-contact/add-contact`
|
||||
7. **我的报名** - `/pages/my-registration/my-registration`
|
||||
8. **全部赛事列表** - `/pages/event-list/event-list`
|
||||
9. **赛事详情** - `/pages/event-detail/event-detail`
|
||||
10. **选择报名类型** - `/pages/register-type/register-type`
|
||||
11. **选择报名项目** - `/pages/select-event/select-event`
|
||||
12. **赛事报名流程** - `/pages/event-register/event-register`
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 所有静态页面已完成,数据为模拟数据
|
||||
2. 实际开发时需要对接后端API接口
|
||||
3. 图片资源需要替换为实际的设计图
|
||||
4. 部分功能(如支付)需要集成第三方SDK
|
||||
5. 微信小程序需要配置appid和相关权限
|
||||
|
||||
## 后续开发建议
|
||||
|
||||
1. **接口对接**:
|
||||
- 用户登录/注册
|
||||
- 赛事列表获取
|
||||
- 报名信息提交
|
||||
- 支付接口对接
|
||||
|
||||
2. **功能完善**:
|
||||
- 图片上传功能
|
||||
- 消息通知
|
||||
- 数据缓存优化
|
||||
- 错误处理机制
|
||||
|
||||
3. **性能优化**:
|
||||
- 图片懒加载
|
||||
- 列表虚拟滚动
|
||||
- 页面预加载
|
||||
|
||||
4. **用户体验**:
|
||||
- 加载动画
|
||||
- 骨架屏
|
||||
- 下拉刷新
|
||||
- 上拉加载更多
|
||||
|
||||
## 检查报告
|
||||
|
||||
运行 `node check-pages.js` 可查看详细的页面完成度报告。
|
||||
|
||||
当前完成度:**100%** ✓
|
||||
|
||||
- 配置文件:7/7 完成
|
||||
- 公共组件:3/3 完成
|
||||
- 页面文件:12/12 完成
|
||||
|
||||
## 版本信息
|
||||
|
||||
- 版本号:V 2.0
|
||||
- 开发时间:2025年1月
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
**最后更新**: 2024-12-29
|
||||
|
||||
@@ -61,5 +61,28 @@ export default {
|
||||
*/
|
||||
getCompetitionRules(competitionId) {
|
||||
return request.get('/martial/competition/rules', { competitionId })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取赛事附件列表
|
||||
* @param {Object} params { competitionId, type }
|
||||
* type: info-信息发布, rules-赛事规程, schedule-活动日程,
|
||||
* results-成绩, medals-奖牌榜, photos-图片直播
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAttachments(params = {}) {
|
||||
return request.get('/martial/competition/attachment/getByType', {
|
||||
competitionId: params.competitionId,
|
||||
attachmentType: params.type
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取赛事所有附件
|
||||
* @param {String|Number} competitionId 赛事ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAllAttachments(competitionId) {
|
||||
return request.get('/martial/competition/attachment/getByCompetition', { competitionId })
|
||||
}
|
||||
}
|
||||
|
||||
2432
package-lock.json
generated
2432
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@
|
||||
"@dcloudio/uni-migration": "^2.0.2-4080720251210002",
|
||||
"cache-loader": "^4.1.0",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"pdfjs-dist": "^2.16.105",
|
||||
"vue": "^2.6.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -94,15 +94,6 @@
|
||||
<text class="toast-text">{{ toastMessage }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 提示文字 -->
|
||||
<view class="info-text">
|
||||
<text class="info-hint">默认关闭,可切换开关</text>
|
||||
</view>
|
||||
|
||||
<view class="warning-text">
|
||||
<text>联系人用于接收比赛信息,成绩和证书。</text>
|
||||
</view>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<view class="btn-wrapper">
|
||||
<view class="btn save-btn disabled" v-if="!isFormValid">保存</view>
|
||||
@@ -219,7 +210,7 @@ export default {
|
||||
}
|
||||
|
||||
.form-label {
|
||||
width: 180rpx;
|
||||
width: 280rpx;
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,8 @@ export default {
|
||||
organization: '',
|
||||
phone: ''
|
||||
},
|
||||
competitionId: '',
|
||||
projectIds: [],
|
||||
errors: [],
|
||||
showHint: false,
|
||||
showToast: false,
|
||||
@@ -133,6 +135,19 @@ export default {
|
||||
showIdTypePicker: false
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
// 接收赛事ID和项目ID
|
||||
if (options.competitionId) {
|
||||
this.competitionId = options.competitionId
|
||||
}
|
||||
if (options.projectIds) {
|
||||
this.projectIds = options.projectIds.split(',').map(id => parseInt(id))
|
||||
}
|
||||
console.log('新增选手页面接收参数:', {
|
||||
competitionId: this.competitionId,
|
||||
projectIds: this.projectIds
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
isFormValid() {
|
||||
return (
|
||||
@@ -252,7 +267,7 @@ export default {
|
||||
const info = this.extractInfoFromIdCard(this.formData.idCard)
|
||||
|
||||
// 调用API保存选手信息(使用后端实体类的字段名)
|
||||
await athleteAPI.submitAthlete({
|
||||
const submitData = {
|
||||
playerName: this.formData.name,
|
||||
idCard: this.formData.idCard,
|
||||
teamName: this.formData.team,
|
||||
@@ -262,7 +277,19 @@ export default {
|
||||
gender: info.gender,
|
||||
age: info.age,
|
||||
birthDate: info.birthDate
|
||||
})
|
||||
}
|
||||
|
||||
// 如果有赛事ID和项目ID,一起提交
|
||||
if (this.competitionId) {
|
||||
submitData.competitionId = parseInt(this.competitionId)
|
||||
}
|
||||
if (this.projectIds && this.projectIds.length > 0) {
|
||||
// 如果有多个项目,取第一个项目ID
|
||||
submitData.projectId = this.projectIds[0]
|
||||
}
|
||||
|
||||
console.log('提交选手数据:', submitData)
|
||||
await athleteAPI.submitAthlete(submitData)
|
||||
|
||||
// 保存成功
|
||||
uni.showToast({
|
||||
@@ -415,9 +442,9 @@ export default {
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
880
pages/attachment-view/attachment-view.vue
Normal file
880
pages/attachment-view/attachment-view.vue
Normal file
@@ -0,0 +1,880 @@
|
||||
<template>
|
||||
<view class="attachment-page">
|
||||
<!-- 赛事信息卡片 -->
|
||||
<view class="event-info-card">
|
||||
<view class="event-title">{{ competitionName || '赛事名称' }}</view>
|
||||
<view class="event-time-row">
|
||||
<view class="time-item">
|
||||
<text class="time-label">开始时间</text>
|
||||
<text class="time-value">{{ startTime || '待定' }}</text>
|
||||
</view>
|
||||
<view class="time-divider"></view>
|
||||
<view class="time-item">
|
||||
<text class="time-label">结束时间</text>
|
||||
<text class="time-value">{{ endTime || '待定' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" v-if="loading">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 附件列表 -->
|
||||
<view class="attachments-section" v-if="!loading && attachments.length > 0">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ pageTitle }}文件</text>
|
||||
<text class="section-count">共{{ attachments.length }}个文件</text>
|
||||
</view>
|
||||
|
||||
<view class="attachments-list">
|
||||
<view
|
||||
class="attachment-item"
|
||||
v-for="(file, index) in attachments"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 文件图标 -->
|
||||
<view class="file-icon" :class="'icon-' + file.fileType">
|
||||
<text class="icon-text">{{ getFileIconText(file.fileType) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 文件信息 -->
|
||||
<view class="file-content">
|
||||
<text class="file-name">{{ file.fileName }}</text>
|
||||
<view class="file-meta">
|
||||
<text class="meta-item">{{ file.fileSize }}</text>
|
||||
<text class="meta-dot" v-if="file.uploadTime">·</text>
|
||||
<text class="meta-item" v-if="file.uploadTime">{{ file.uploadTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="file-actions">
|
||||
<view class="action-btn preview-btn" @click="previewFile(file)">
|
||||
<text class="action-text">预览</text>
|
||||
</view>
|
||||
<view class="action-btn download-btn" @click="downloadFile(file)">
|
||||
<text class="action-text">下载</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="!loading && attachments.length === 0">
|
||||
<image class="empty-image" src="/static/images/empty.png" mode="aspectFit" />
|
||||
<text class="empty-title">暂无{{ pageTitle }}文件</text>
|
||||
<text class="empty-desc">相关文件正在整理中,请稍后查看</text>
|
||||
</view>
|
||||
|
||||
<!-- PDF预览弹窗 (仅H5) -->
|
||||
<!-- #ifdef H5 -->
|
||||
<view class="preview-modal" v-if="showPreview" @click="closePreview">
|
||||
<view class="preview-container" @click.stop>
|
||||
<view class="preview-header">
|
||||
<text class="preview-title">{{ previewFileName }}</text>
|
||||
<view class="preview-close" @click="closePreview">
|
||||
<text class="close-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="preview-body">
|
||||
<iframe
|
||||
:src="previewUrl"
|
||||
class="preview-iframe"
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import competitionAPI from '@/api/competition.js'
|
||||
|
||||
// 页面类型配置
|
||||
const PAGE_CONFIG = {
|
||||
'info': { title: '信息发布', type: 'info' },
|
||||
'rules': { title: '赛事规程', type: 'rules' },
|
||||
'schedule': { title: '活动日程', type: 'schedule' },
|
||||
'score': { title: '成绩公告', type: 'results' },
|
||||
'results': { title: '成绩公告', type: 'results' },
|
||||
'awards': { title: '奖牌榜', type: 'medals' },
|
||||
'medals': { title: '奖牌榜', type: 'medals' },
|
||||
'photos': { title: '图片直播', type: 'photos' }
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
pageType: 'rules',
|
||||
pageTitle: '赛事规程',
|
||||
competitionId: '',
|
||||
competitionName: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
attachments: [],
|
||||
// H5预览相关
|
||||
showPreview: false,
|
||||
previewUrl: '',
|
||||
previewFileName: ''
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 获取页面类型
|
||||
if (options.type && PAGE_CONFIG[options.type]) {
|
||||
this.pageType = options.type
|
||||
this.pageTitle = PAGE_CONFIG[options.type].title
|
||||
}
|
||||
|
||||
// 获取赛事ID
|
||||
if (options.competitionId || options.eventId) {
|
||||
this.competitionId = options.competitionId || options.eventId
|
||||
}
|
||||
|
||||
// 获取赛事名称
|
||||
if (options.name) {
|
||||
this.competitionName = decodeURIComponent(options.name)
|
||||
}
|
||||
|
||||
// 获取比赛时间
|
||||
if (options.startTime) {
|
||||
this.startTime = decodeURIComponent(options.startTime)
|
||||
}
|
||||
if (options.endTime) {
|
||||
this.endTime = decodeURIComponent(options.endTime)
|
||||
}
|
||||
|
||||
// 设置导航栏标题
|
||||
uni.setNavigationBarTitle({
|
||||
title: this.pageTitle
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载数据
|
||||
*/
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
try {
|
||||
// 如果没有赛事信息,先获取赛事详情
|
||||
if (!this.startTime || !this.endTime) {
|
||||
await this.loadCompetitionInfo()
|
||||
}
|
||||
// 加载附件列表
|
||||
await this.loadAttachments()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载赛事信息
|
||||
*/
|
||||
async loadCompetitionInfo() {
|
||||
try {
|
||||
const res = await competitionAPI.getCompetitionDetail(this.competitionId)
|
||||
if (res) {
|
||||
this.competitionName = res.name || res.title || this.competitionName
|
||||
this.startTime = this.formatDate(res.startTime || res.competitionStartTime)
|
||||
this.endTime = this.formatDate(res.endTime || res.competitionEndTime)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载赛事信息失败:', err)
|
||||
// 使用模拟数据
|
||||
this.startTime = '2025.12.12'
|
||||
this.endTime = '2025.12.14'
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载附件列表
|
||||
*/
|
||||
async loadAttachments() {
|
||||
try {
|
||||
// 使用 PAGE_CONFIG 映射的 type 值
|
||||
const attachmentType = PAGE_CONFIG[this.pageType]?.type || this.pageType
|
||||
console.log('=== 加载附件 ===')
|
||||
console.log('competitionId:', this.competitionId)
|
||||
console.log('pageType:', this.pageType)
|
||||
console.log('attachmentType (发送给后端):', attachmentType)
|
||||
|
||||
const res = await competitionAPI.getAttachments({
|
||||
competitionId: this.competitionId,
|
||||
type: attachmentType
|
||||
})
|
||||
|
||||
console.log('API返回结果:', res)
|
||||
console.log('API返回结果类型:', typeof res)
|
||||
console.log('是否为数组:', Array.isArray(res))
|
||||
|
||||
// 兼容不同的返回格式
|
||||
let attachmentList = []
|
||||
if (Array.isArray(res)) {
|
||||
attachmentList = res
|
||||
} else if (res && res.records && Array.isArray(res.records)) {
|
||||
// 分页格式
|
||||
attachmentList = res.records
|
||||
} else if (res && typeof res === 'object') {
|
||||
// 可能是单个对象,转为数组
|
||||
attachmentList = [res]
|
||||
}
|
||||
|
||||
console.log('处理后的附件列表:', attachmentList)
|
||||
|
||||
if (attachmentList.length > 0) {
|
||||
this.attachments = attachmentList.map(file => ({
|
||||
id: file.id,
|
||||
fileName: file.fileName || file.name,
|
||||
fileUrl: file.fileUrl || file.url,
|
||||
fileSize: this.formatFileSize(file.fileSize || file.size),
|
||||
fileType: this.getFileType(file.fileName || file.name),
|
||||
uploadTime: this.formatDate(file.uploadTime || file.createTime)
|
||||
}))
|
||||
console.log('附件加载成功,共', this.attachments.length, '个文件')
|
||||
} else {
|
||||
console.log('没有附件数据,显示空状态')
|
||||
this.attachments = []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('=== 加载附件失败 ===')
|
||||
console.error('错误详情:', err)
|
||||
console.error('错误消息:', err.message)
|
||||
console.error('错误代码:', err.code)
|
||||
// 显示空状态,不使用模拟数据,方便调试
|
||||
this.attachments = []
|
||||
// 如果需要模拟数据,取消下面的注释
|
||||
// this.loadMockData()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载模拟数据
|
||||
*/
|
||||
loadMockData() {
|
||||
const mockDataMap = {
|
||||
'info': [
|
||||
{ id: '1', fileName: '2025年郑州武术大赛通知.pdf', fileUrl: '', fileSize: '1.2 MB', fileType: 'pdf', uploadTime: '2025-12-20' }
|
||||
],
|
||||
'rules': [
|
||||
{ id: '1', fileName: '2025年郑州武术大赛竞赛规程.pdf', fileUrl: '', fileSize: '2.5 MB', fileType: 'pdf', uploadTime: '2025-12-18' },
|
||||
{ id: '2', fileName: '参赛报名表.pdf', fileUrl: '', fileSize: '156 KB', fileType: 'pdf', uploadTime: '2025-12-18' }
|
||||
],
|
||||
'schedule': [
|
||||
{ id: '1', fileName: '比赛日程安排表.pdf', fileUrl: '', fileSize: '890 KB', fileType: 'pdf', uploadTime: '2025-12-19' }
|
||||
],
|
||||
'score': [
|
||||
{ id: '1', fileName: '比赛成绩公告.pdf', fileUrl: '', fileSize: '1.8 MB', fileType: 'pdf', uploadTime: '2025-12-25' }
|
||||
],
|
||||
'results': [
|
||||
{ id: '1', fileName: '比赛成绩公告.pdf', fileUrl: '', fileSize: '1.8 MB', fileType: 'pdf', uploadTime: '2025-12-25' }
|
||||
],
|
||||
'awards': [
|
||||
{ id: '1', fileName: '奖牌榜统计.pdf', fileUrl: '', fileSize: '520 KB', fileType: 'pdf', uploadTime: '2025-12-25' }
|
||||
],
|
||||
'medals': [
|
||||
{ id: '1', fileName: '奖牌榜统计.pdf', fileUrl: '', fileSize: '520 KB', fileType: 'pdf', uploadTime: '2025-12-25' }
|
||||
],
|
||||
'photos': [
|
||||
{ id: '1', fileName: '比赛精彩瞬间.pdf', fileUrl: '', fileSize: '15.6 MB', fileType: 'pdf', uploadTime: '2025-12-25' }
|
||||
]
|
||||
}
|
||||
|
||||
this.attachments = mockDataMap[this.pageType] || []
|
||||
},
|
||||
|
||||
/**
|
||||
* 预览文件
|
||||
*/
|
||||
previewFile(file) {
|
||||
if (!file.fileUrl) {
|
||||
uni.showToast({
|
||||
title: '文件暂不可用',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
// H5端:根据文件类型选择预览方式
|
||||
if (file.fileType === 'pdf') {
|
||||
// 方案1: 直接在新标签页打开PDF(浏览器内置PDF阅读器)
|
||||
window.open(file.fileUrl, '_blank')
|
||||
|
||||
// 方案2: 使用微软 Office Online Viewer(备选,需要公网可访问)
|
||||
// const msViewerUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(file.fileUrl)}`
|
||||
// window.open(msViewerUrl, '_blank')
|
||||
|
||||
// 方案3: 使用内嵌弹窗(如果服务器支持)
|
||||
// this.previewFileName = file.fileName
|
||||
// this.previewUrl = file.fileUrl
|
||||
// this.showPreview = true
|
||||
} else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.fileType)) {
|
||||
// 图片使用弹窗显示
|
||||
this.previewFileName = file.fileName
|
||||
this.previewUrl = file.fileUrl
|
||||
this.showPreview = true
|
||||
} else if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(file.fileType)) {
|
||||
// Office 文档使用微软在线预览
|
||||
const msViewerUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(file.fileUrl)}`
|
||||
window.open(msViewerUrl, '_blank')
|
||||
} else {
|
||||
// 其他文件类型,在新窗口打开
|
||||
window.open(file.fileUrl, '_blank')
|
||||
}
|
||||
return
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
// 非H5端使用下载+打开文档的方式
|
||||
uni.showLoading({
|
||||
title: '加载中...'
|
||||
})
|
||||
|
||||
uni.downloadFile({
|
||||
url: file.fileUrl,
|
||||
success: (res) => {
|
||||
uni.hideLoading()
|
||||
if (res.statusCode === 200) {
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
fileType: file.fileType,
|
||||
showMenu: true,
|
||||
success: () => {
|
||||
console.log('打开文档成功')
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('打开文档失败:', err)
|
||||
uni.showToast({
|
||||
title: '无法预览此文件',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '文件加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('下载失败:', err)
|
||||
uni.showToast({
|
||||
title: '下载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭预览弹窗
|
||||
*/
|
||||
closePreview() {
|
||||
this.showPreview = false
|
||||
this.previewUrl = ''
|
||||
this.previewFileName = ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
downloadFile(file) {
|
||||
if (!file.fileUrl) {
|
||||
uni.showToast({
|
||||
title: '文件暂不可用',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({
|
||||
title: '下载中...'
|
||||
})
|
||||
|
||||
uni.downloadFile({
|
||||
url: file.fileUrl,
|
||||
success: (res) => {
|
||||
uni.hideLoading()
|
||||
if (res.statusCode === 200) {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序保存文件
|
||||
uni.saveFile({
|
||||
tempFilePath: res.tempFilePath,
|
||||
success: (saveRes) => {
|
||||
uni.showToast({
|
||||
title: '下载成功',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
// 保存失败则打开文档
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
fileType: file.fileType,
|
||||
showMenu: true
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
// H5端打开新窗口下载
|
||||
window.open(file.fileUrl)
|
||||
uni.showToast({
|
||||
title: '开始下载',
|
||||
icon: 'success'
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// APP端保存到相册或文件
|
||||
uni.saveFile({
|
||||
tempFilePath: res.tempFilePath,
|
||||
success: (saveRes) => {
|
||||
uni.showToast({
|
||||
title: '下载成功',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
fileType: file.fileType,
|
||||
showMenu: true
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '下载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('下载失败:', err)
|
||||
uni.showToast({
|
||||
title: '下载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件类型
|
||||
*/
|
||||
getFileType(fileName) {
|
||||
if (!fileName) return 'pdf'
|
||||
const ext = fileName.split('.').pop().toLowerCase()
|
||||
return ext
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件图标文字
|
||||
*/
|
||||
getFileIconText(fileType) {
|
||||
const iconMap = {
|
||||
'pdf': 'PDF',
|
||||
'doc': 'DOC',
|
||||
'docx': 'DOC',
|
||||
'xls': 'XLS',
|
||||
'xlsx': 'XLS',
|
||||
'ppt': 'PPT',
|
||||
'pptx': 'PPT',
|
||||
'txt': 'TXT',
|
||||
'jpg': 'IMG',
|
||||
'jpeg': 'IMG',
|
||||
'png': 'IMG',
|
||||
'zip': 'ZIP',
|
||||
'rar': 'RAR'
|
||||
}
|
||||
return iconMap[fileType] || 'FILE'
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B'
|
||||
if (typeof bytes === 'string') return bytes
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
if (isNaN(date.getTime())) return dateStr
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}.${month}.${day}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.attachment-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
// 赛事信息卡片
|
||||
.event-info-card {
|
||||
background: #fff;
|
||||
margin: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 24rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.event-time-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.time-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time-divider {
|
||||
width: 1rpx;
|
||||
height: 60rpx;
|
||||
background: #e0e0e0;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-top-color: #C93639;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
// 附件区域
|
||||
.attachments-section {
|
||||
margin: 20rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.attachments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
// 附件项
|
||||
.attachment-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
// 文件图标
|
||||
.file-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: #C93639;
|
||||
}
|
||||
|
||||
.file-icon.icon-pdf {
|
||||
background: #E74C3C;
|
||||
}
|
||||
|
||||
.file-icon.icon-doc,
|
||||
.file-icon.icon-docx {
|
||||
background: #3498DB;
|
||||
}
|
||||
|
||||
.file-icon.icon-xls,
|
||||
.file-icon.icon-xlsx {
|
||||
background: #27AE60;
|
||||
}
|
||||
|
||||
.file-icon.icon-ppt,
|
||||
.file-icon.icon-pptx {
|
||||
background: #E67E22;
|
||||
}
|
||||
|
||||
.file-icon.icon-jpg,
|
||||
.file-icon.icon-jpeg,
|
||||
.file-icon.icon-png {
|
||||
background: #9B59B6;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// 文件内容
|
||||
.file-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.meta-dot {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
// 操作按钮
|
||||
.file-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 30rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
background: #C93639;
|
||||
|
||||
.action-text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
background: #fff;
|
||||
border: 1rpx solid #C93639;
|
||||
|
||||
.action-text {
|
||||
color: #C93639;
|
||||
}
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 60rpx;
|
||||
}
|
||||
|
||||
.empty-image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// PDF预览弹窗样式
|
||||
.preview-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
width: 95%;
|
||||
height: 90%;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 30rpx;
|
||||
background: #C93639;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 20rpx;
|
||||
}
|
||||
|
||||
.preview-close {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 40rpx;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@@ -60,6 +60,7 @@
|
||||
import CustomTabs from '../../components/custom-tabs/custom-tabs.vue';
|
||||
import ConfirmModal from '../../components/confirm-modal/confirm-modal.vue';
|
||||
import athleteAPI from '@/api/athlete.js';
|
||||
import { getUserInfo } from '@/utils/auth.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -89,9 +90,21 @@ export default {
|
||||
*/
|
||||
async loadPlayerList() {
|
||||
try {
|
||||
// 获取当前用户信息
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo || !userInfo.userId) {
|
||||
console.error('未获取到用户信息')
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const res = await athleteAPI.getAthleteList({
|
||||
current: 1,
|
||||
size: 100
|
||||
size: 100,
|
||||
createUser: userInfo.userId // 只查询当前用户创建的选手
|
||||
})
|
||||
|
||||
let list = []
|
||||
@@ -104,11 +117,11 @@ export default {
|
||||
// 数据映射
|
||||
this.playerList = list.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
name: item.name || item.playerName,
|
||||
idCard: item.idCard || item.idCardNumber,
|
||||
gender: item.gender,
|
||||
team: item.team,
|
||||
phone: item.phone
|
||||
team: item.team || item.teamName,
|
||||
phone: item.phone || item.contactPhone
|
||||
}))
|
||||
} catch (err) {
|
||||
console.error('加载选手列表失败:', err)
|
||||
|
||||
@@ -125,7 +125,7 @@ export default {
|
||||
res.registerTime || res.registrationPeriod || '待定',
|
||||
matchTime: this.formatTimeRange(startTime, endTime) ||
|
||||
res.matchTime || res.competitionTime || '待定',
|
||||
registerCount: res.registrationCount || res.registerCount || res.signUpCount || '0',
|
||||
registerCount: res.registrationCount || res.registerCount || res.signUpCount || res.totalParticipants || '0',
|
||||
status: this.getStatus(res.status)
|
||||
}
|
||||
|
||||
@@ -169,16 +169,23 @@ export default {
|
||||
},
|
||||
|
||||
handleFunction(type) {
|
||||
// 需要跳转到附件展示页面的类型
|
||||
const attachmentTypes = ['info', 'rules', 'schedule', 'score', 'awards', 'photos']
|
||||
|
||||
if (attachmentTypes.includes(type)) {
|
||||
// 跳转到通用附件展示页面
|
||||
const name = encodeURIComponent(this.eventInfo.title)
|
||||
uni.navigateTo({
|
||||
url: `/pages/attachment-view/attachment-view?type=${type}&competitionId=${this.eventId}&name=${name}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 其他功能页面的路由映射
|
||||
const routeMap = {
|
||||
'info': '/pages/event-info/event-info',
|
||||
'rules': '/pages/event-rules/event-rules',
|
||||
'schedule': '/pages/event-schedule/event-schedule',
|
||||
'players': '/pages/event-players/event-players',
|
||||
'match': '/pages/event-live/event-live',
|
||||
'lineup': '/pages/event-lineup/event-lineup',
|
||||
'score': '/pages/event-score/event-score',
|
||||
'awards': '/pages/event-medals/event-medals',
|
||||
'photos': '' // 图片直播暂未实现
|
||||
'lineup': '/pages/event-lineup/event-lineup'
|
||||
};
|
||||
|
||||
const url = routeMap[type];
|
||||
|
||||
@@ -228,7 +228,7 @@ export default {
|
||||
item.registerTime || item.registrationPeriod || '待定',
|
||||
matchTime: this.formatTimeRange(startTime, endTime) ||
|
||||
item.matchTime || item.competitionTime || '待定',
|
||||
registerCount: item.registrationCount || item.registerCount || item.signUpCount || '0',
|
||||
registerCount: item.registrationCount || item.registerCount || item.signUpCount || item.totalParticipants || '0',
|
||||
status: this.getStatus(item.status)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
import competitionAPI from '@/api/competition.js'
|
||||
import athleteAPI from '@/api/athlete.js'
|
||||
import registrationAPI from '@/api/registration.js'
|
||||
import { getUserInfo } from '@/utils/auth.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -286,9 +287,21 @@ export default {
|
||||
*/
|
||||
async loadPlayerList() {
|
||||
try {
|
||||
// 获取当前用户信息
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo || !userInfo.userId) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 只查询当前用户创建的选手
|
||||
const res = await athleteAPI.getAthleteList({
|
||||
current: 1,
|
||||
size: 100
|
||||
size: 100,
|
||||
createUser: userInfo.userId
|
||||
})
|
||||
|
||||
let list = []
|
||||
@@ -363,8 +376,10 @@ export default {
|
||||
}
|
||||
},
|
||||
goToAddPlayer() {
|
||||
// 传递赛事ID和项目ID到新增选手页面
|
||||
const projectIds = this.selectedProjects.map(p => p.id).join(',')
|
||||
uni.navigateTo({
|
||||
url: '/pages/add-player/add-player'
|
||||
url: `/pages/add-player/add-player?competitionId=${this.eventId}&projectIds=${projectIds}`
|
||||
});
|
||||
},
|
||||
handleEdit(item) {
|
||||
|
||||
@@ -137,7 +137,7 @@ export default {
|
||||
item.registerTime || item.registrationPeriod || '待定',
|
||||
matchTime: this.formatTimeRange(startTime, endTime) ||
|
||||
item.matchTime || item.competitionTime || '待定',
|
||||
registerCount: item.registrationCount || item.registerCount || item.signUpCount || '0',
|
||||
registerCount: item.registrationCount || item.registerCount || item.signUpCount || item.totalParticipants || '0',
|
||||
status: this.getStatus(item.status)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
import CustomTabs from '../../components/custom-tabs/custom-tabs.vue';
|
||||
import registrationAPI from '@/api/registration.js'
|
||||
import competitionAPI from '@/api/competition.js'
|
||||
import { getUserInfo } from '@/utils/auth.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -119,9 +120,29 @@ export default {
|
||||
*/
|
||||
async loadRegistrationList(refresh = false, loadMore = false) {
|
||||
try {
|
||||
// 获取当前用户信息
|
||||
const userInfo = getUserInfo()
|
||||
console.log('=== 用户信息调试 ===')
|
||||
console.log('获取到的用户信息:', userInfo)
|
||||
console.log('用户信息类型:', typeof userInfo)
|
||||
|
||||
if (!userInfo || (!userInfo.userId && !userInfo.user_id)) {
|
||||
console.error('未获取到用户信息或缺少用户ID字段')
|
||||
console.log('原始存储数据:', uni.getStorageSync('userInfo'))
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 兼容 userId 和 user_id 两种字段名
|
||||
const userId = userInfo.userId || userInfo.user_id
|
||||
|
||||
const params = {
|
||||
current: this.pageParams.current,
|
||||
size: this.pageParams.size
|
||||
size: this.pageParams.size,
|
||||
createUser: userId // 只查询当前用户创建的报名记录
|
||||
}
|
||||
|
||||
// 添加状态筛选
|
||||
@@ -222,7 +243,7 @@ export default {
|
||||
matchTime: '',
|
||||
projects: '',
|
||||
contact: orderItem.contactPhone || '',
|
||||
participants: `${orderItem.totalParticipants || 0}人`
|
||||
participants: `${orderItem.registerCount || 0}人`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -66,8 +66,9 @@ export default {
|
||||
.type-btn {
|
||||
background-color: #C93639;
|
||||
color: #fff;
|
||||
padding: 20rpx 60rpx;
|
||||
padding: 20rpx 50rpx;
|
||||
border-radius: 50rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</view>
|
||||
<view class="status-item">
|
||||
<text class="label">参赛人数:</text>
|
||||
<text class="value">{{ scheduleData.totalParticipants || 0 }}</text>
|
||||
<text class="value">{{ scheduleData.registerCount || 0 }}</text>
|
||||
</view>
|
||||
<view class="status-item" v-if="scheduleData.lastAutoScheduleTime">
|
||||
<text class="label">最后编排时间:</text>
|
||||
|
||||
14
public/index.html
Normal file
14
public/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
|
||||
<title>武术赛事报名系统</title>
|
||||
<link rel="stylesheet" href="./static/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,8 +7,6 @@ import request from '@/utils/request.js'
|
||||
export default {
|
||||
/**
|
||||
* 获取选手列表
|
||||
* @param {Object} params 查询参数 { current, size, competitionId, name }
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAthleteList(params = {}) {
|
||||
return request.get('/martial/athlete/list', {
|
||||
@@ -20,8 +18,6 @@ export default {
|
||||
|
||||
/**
|
||||
* 获取选手详情
|
||||
* @param {String|Number} id 选手ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAthleteDetail(id) {
|
||||
return request.get('/martial/athlete/detail', { id })
|
||||
@@ -29,8 +25,6 @@ export default {
|
||||
|
||||
/**
|
||||
* 新增或修改选手
|
||||
* @param {Object} data 选手数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
submitAthlete(data) {
|
||||
return request.post('/martial/athlete/submit', data)
|
||||
@@ -38,12 +32,54 @@ export default {
|
||||
|
||||
/**
|
||||
* 删除选手
|
||||
* @param {String|Array} ids 选手ID或ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
removeAthlete(ids) {
|
||||
return request.post('/martial/athlete/remove', {
|
||||
ids: Array.isArray(ids) ? ids.join(',') : ids
|
||||
})
|
||||
},
|
||||
|
||||
// ========== 集体/团队相关 API ==========
|
||||
|
||||
getTeamList(params = {}) {
|
||||
return request.get('/martial/team/list', {
|
||||
current: params.current || 1,
|
||||
size: params.size || 100,
|
||||
...params
|
||||
})
|
||||
},
|
||||
|
||||
getTeamDetail(id) {
|
||||
return request.get('/martial/team/detail', { id })
|
||||
},
|
||||
|
||||
saveTeam(data) {
|
||||
return request.post('/martial/team/submit', data)
|
||||
},
|
||||
|
||||
removeTeam(id) {
|
||||
return request.post('/martial/team/remove?id=' + id, {})
|
||||
},
|
||||
|
||||
// ========== 联系人相关 API ==========
|
||||
|
||||
getContactList(params = {}) {
|
||||
return request.get('/martial/contact/list', {
|
||||
current: params.current || 1,
|
||||
size: params.size || 100,
|
||||
...params
|
||||
})
|
||||
},
|
||||
|
||||
getContactDetail(id) {
|
||||
return request.get('/martial/contact/detail', { id })
|
||||
},
|
||||
|
||||
saveContact(data) {
|
||||
return request.post('/martial/contact/submit', data)
|
||||
},
|
||||
|
||||
removeContact(id) {
|
||||
return request.post('/martial/contact/remove?ids=' + id, {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,5 +61,28 @@ export default {
|
||||
*/
|
||||
getCompetitionRules(competitionId) {
|
||||
return request.get('/martial/competition/rules', { competitionId })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取赛事附件列表
|
||||
* @param {Object} params { competitionId, type }
|
||||
* type: info-信息发布, rules-赛事规程, schedule-活动日程,
|
||||
* results-成绩, medals-奖牌榜, photos-图片直播
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAttachments(params = {}) {
|
||||
return request.get('/martial/competition/attachment/getByType', {
|
||||
competitionId: params.competitionId,
|
||||
attachmentType: params.type
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取赛事所有附件
|
||||
* @param {String|Number} competitionId 赛事ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getAllAttachments(competitionId) {
|
||||
return request.get('/martial/competition/attachment/getByCompetition', { competitionId })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,26 +7,23 @@ import request from '@/utils/request.js'
|
||||
export default {
|
||||
/**
|
||||
* 提交报名订单
|
||||
* @param {Object} data 报名数据 { competitionId, projectIds, athleteIds, contactPhone, totalAmount }
|
||||
* @returns {Promise}
|
||||
*/
|
||||
submitRegistration(data) {
|
||||
// 处理数组参数:将数组转换为逗号分隔的字符串
|
||||
const formattedData = {
|
||||
orderNo: data.orderNo,
|
||||
competitionId: data.competitionId,
|
||||
projectIds: Array.isArray(data.projectIds) ? data.projectIds.join(',') : data.projectIds,
|
||||
athleteIds: Array.isArray(data.athleteIds) ? data.athleteIds.join(',') : data.athleteIds,
|
||||
athleteIds: Array.isArray(data.athleteIds) ? data.athleteIds.join(',') : (data.athleteIds || ''),
|
||||
teamIds: Array.isArray(data.teamIds) ? data.teamIds.join(',') : (data.teamIds || ''),
|
||||
contactPhone: data.contactPhone,
|
||||
totalAmount: data.totalAmount
|
||||
}
|
||||
|
||||
console.log('=== API层转换后的数据 ===')
|
||||
console.log('订单号:', formattedData.orderNo)
|
||||
console.log('转换前 projectIds:', data.projectIds)
|
||||
console.log('转换后 projectIds:', formattedData.projectIds)
|
||||
console.log('转换前 athleteIds:', data.athleteIds)
|
||||
console.log('转换后 athleteIds:', formattedData.athleteIds)
|
||||
console.log('projectIds:', formattedData.projectIds)
|
||||
console.log('athleteIds:', formattedData.athleteIds)
|
||||
console.log('teamIds:', formattedData.teamIds)
|
||||
console.log('最终发送到后端的完整数据:', formattedData)
|
||||
|
||||
return request.post('/martial/registrationOrder/submit', formattedData)
|
||||
@@ -34,8 +31,6 @@ export default {
|
||||
|
||||
/**
|
||||
* 获取报名订单列表
|
||||
* @param {Object} params 查询参数 { current, size, status }
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getRegistrationList(params = {}) {
|
||||
return request.get('/martial/registrationOrder/list', {
|
||||
@@ -47,8 +42,6 @@ export default {
|
||||
|
||||
/**
|
||||
* 获取报名订单详情
|
||||
* @param {String|Number} id 订单ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getRegistrationDetail(id) {
|
||||
return request.get('/martial/registrationOrder/detail', { id })
|
||||
@@ -56,8 +49,6 @@ export default {
|
||||
|
||||
/**
|
||||
* 取消报名
|
||||
* @param {String|Array} ids 订单ID或ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
cancelRegistration(ids) {
|
||||
return request.post('/martial/registrationOrder/remove', {
|
||||
|
||||
@@ -91,6 +91,27 @@ export function saveDispatch(data) {
|
||||
return request.post('/martial/schedule/save-dispatch', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取编排状态(小程序用)
|
||||
* @param {Number} competitionId - 赛事ID
|
||||
*/
|
||||
export function getScheduleStatus(competitionId) {
|
||||
return request.get('/mini/schedule/status', {
|
||||
params: { competitionId }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取出场顺序(小程序用)
|
||||
* @param {Number} competitionId - 赛事ID
|
||||
* @param {Number} projectId - 项目ID (可选)
|
||||
*/
|
||||
export function getLineup(competitionId, projectId) {
|
||||
return request.get('/mini/schedule/lineup', {
|
||||
params: { competitionId, projectId }
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
getScheduleResult,
|
||||
triggerAutoArrange,
|
||||
@@ -99,5 +120,7 @@ export default {
|
||||
moveScheduleGroup,
|
||||
getDispatchData,
|
||||
adjustOrder,
|
||||
saveDispatch
|
||||
saveDispatch,
|
||||
getScheduleStatus,
|
||||
getLineup
|
||||
}
|
||||
|
||||
508
src/components/pdf-viewer/pdf-viewer.vue
Normal file
508
src/components/pdf-viewer/pdf-viewer.vue
Normal file
@@ -0,0 +1,508 @@
|
||||
<template>
|
||||
<view class="pdf-viewer-modal" v-if="visible" @click="handleClose">
|
||||
<view class="pdf-container" @click.stop>
|
||||
<!-- 头部 -->
|
||||
<view class="pdf-header">
|
||||
<text class="pdf-title">{{ fileName }}</text>
|
||||
<view class="pdf-actions">
|
||||
<text class="page-info">{{ currentPage }} / {{ totalPages }}</text>
|
||||
<view class="close-btn" @click="handleClose">
|
||||
<text class="close-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- PDF 内容区域 -->
|
||||
<view class="pdf-content" ref="pdfContent">
|
||||
<!-- 加载中 -->
|
||||
<view class="loading-wrapper" v-if="loading">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中... {{ loadingProgress }}%</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<view class="error-wrapper" v-if="error">
|
||||
<text class="error-text">{{ error }}</text>
|
||||
<view class="retry-btn" @click="loadPdf">
|
||||
<text>重试</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- PDF 画布容器 - 使用原生 div -->
|
||||
<view
|
||||
class="pdf-scroll-wrapper"
|
||||
v-show="!loading && !error"
|
||||
ref="scrollWrapper"
|
||||
>
|
||||
<view class="canvas-container" ref="canvasContainer">
|
||||
<!-- canvas 将通过 JS 动态创建 -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部工具栏 -->
|
||||
<view class="pdf-toolbar">
|
||||
<view class="toolbar-btn" :class="{ disabled: currentPage <= 1 }" @click="prevPage">
|
||||
<text>上一页</text>
|
||||
</view>
|
||||
<view class="toolbar-btn" :class="{ disabled: currentPage >= totalPages }" @click="nextPage">
|
||||
<text>下一页</text>
|
||||
</view>
|
||||
<view class="toolbar-btn" @click="zoomOut">
|
||||
<text>缩小</text>
|
||||
</view>
|
||||
<view class="toolbar-btn" @click="zoomIn">
|
||||
<text>放大</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// #ifdef H5
|
||||
let pdfjsLib = null
|
||||
// #endif
|
||||
|
||||
export default {
|
||||
name: 'PdfViewer',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
default: 'PDF文档'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
loadingProgress: 0,
|
||||
error: '',
|
||||
pdfDoc: null,
|
||||
currentPage: 1,
|
||||
totalPages: 0,
|
||||
scale: 1.2,
|
||||
canvasElements: {},
|
||||
pdfjsLoaded: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (val && this.url) {
|
||||
this.$nextTick(() => {
|
||||
this.initPdfjs()
|
||||
})
|
||||
} else if (!val) {
|
||||
this.cleanup()
|
||||
}
|
||||
},
|
||||
url(val) {
|
||||
if (this.visible && val) {
|
||||
this.$nextTick(() => {
|
||||
this.initPdfjs()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async initPdfjs() {
|
||||
// #ifdef H5
|
||||
if (!pdfjsLib) {
|
||||
try {
|
||||
// 动态导入 pdfjs-dist
|
||||
const pdfjs = await import('pdfjs-dist/legacy/build/pdf.js')
|
||||
pdfjsLib = pdfjs
|
||||
|
||||
// 设置 worker
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js'
|
||||
|
||||
this.pdfjsLoaded = true
|
||||
console.log('PDF.js 加载成功')
|
||||
} catch (err) {
|
||||
console.error('加载 PDF.js 失败:', err)
|
||||
this.error = '加载 PDF 组件失败'
|
||||
return
|
||||
}
|
||||
}
|
||||
this.loadPdf()
|
||||
// #endif
|
||||
},
|
||||
|
||||
async loadPdf() {
|
||||
// #ifdef H5
|
||||
if (!this.url) {
|
||||
this.error = '文件地址无效'
|
||||
return
|
||||
}
|
||||
|
||||
if (!pdfjsLib) {
|
||||
this.error = 'PDF组件未加载'
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.loadingProgress = 0
|
||||
this.error = ''
|
||||
this.clearCanvases()
|
||||
|
||||
try {
|
||||
console.log('开始加载 PDF:', this.url)
|
||||
|
||||
// 加载 PDF 文档
|
||||
const loadingTask = pdfjsLib.getDocument({
|
||||
url: this.url,
|
||||
withCredentials: false
|
||||
})
|
||||
|
||||
loadingTask.onProgress = (progress) => {
|
||||
if (progress.total > 0) {
|
||||
this.loadingProgress = Math.round((progress.loaded / progress.total) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
this.pdfDoc = await loadingTask.promise
|
||||
this.totalPages = this.pdfDoc.numPages
|
||||
this.currentPage = 1
|
||||
|
||||
console.log('PDF 加载成功,总页数:', this.totalPages)
|
||||
|
||||
// 渲染第一页
|
||||
await this.renderPage(1)
|
||||
|
||||
this.loading = false
|
||||
} catch (err) {
|
||||
console.error('PDF加载失败:', err)
|
||||
this.loading = false
|
||||
|
||||
if (err.name === 'MissingPDFException') {
|
||||
this.error = '文件不存在或已被删除'
|
||||
} else if (err.message && err.message.includes('CORS')) {
|
||||
this.error = '跨域访问被拒绝,请联系管理员'
|
||||
} else if (err.message && err.message.includes('Invalid PDF')) {
|
||||
this.error = '无效的PDF文件'
|
||||
} else {
|
||||
this.error = '文件加载失败: ' + (err.message || '未知错误')
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
this.error = '当前平台不支持PDF预览'
|
||||
// #endif
|
||||
},
|
||||
|
||||
async renderPage(pageNum) {
|
||||
// #ifdef H5
|
||||
if (!this.pdfDoc) {
|
||||
console.error('pdfDoc 不存在')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('开始渲染页面:', pageNum)
|
||||
|
||||
const page = await this.pdfDoc.getPage(pageNum)
|
||||
const viewport = page.getViewport({ scale: this.scale })
|
||||
|
||||
console.log('页面尺寸:', viewport.width, 'x', viewport.height)
|
||||
|
||||
// 获取或创建 canvas
|
||||
let canvas = this.canvasElements[pageNum]
|
||||
if (!canvas) {
|
||||
canvas = document.createElement('canvas')
|
||||
canvas.id = 'pdf-canvas-' + pageNum
|
||||
canvas.className = 'pdf-canvas-element'
|
||||
canvas.style.display = 'block'
|
||||
canvas.style.margin = '10px auto'
|
||||
canvas.style.backgroundColor = '#fff'
|
||||
canvas.style.boxShadow = '0 2px 10px rgba(0,0,0,0.1)'
|
||||
|
||||
this.canvasElements[pageNum] = canvas
|
||||
|
||||
// 添加到容器
|
||||
const container = this.$refs.canvasContainer?.$el || this.$refs.canvasContainer
|
||||
if (container) {
|
||||
container.appendChild(canvas)
|
||||
console.log('Canvas 已添加到容器')
|
||||
} else {
|
||||
console.error('找不到 canvas 容器')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 canvas 尺寸
|
||||
canvas.width = viewport.width
|
||||
canvas.height = viewport.height
|
||||
canvas.style.width = viewport.width + 'px'
|
||||
canvas.style.height = viewport.height + 'px'
|
||||
|
||||
const context = canvas.getContext('2d')
|
||||
|
||||
// 清除之前的内容
|
||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 渲染 PDF 页面到 canvas
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: viewport
|
||||
}
|
||||
|
||||
await page.render(renderContext).promise
|
||||
console.log('页面渲染完成:', pageNum)
|
||||
|
||||
} catch (err) {
|
||||
console.error('渲染页面失败:', err)
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
clearCanvases() {
|
||||
// 清除所有 canvas
|
||||
const container = this.$refs.canvasContainer?.$el || this.$refs.canvasContainer
|
||||
if (container) {
|
||||
container.innerHTML = ''
|
||||
}
|
||||
this.canvasElements = {}
|
||||
},
|
||||
|
||||
async prevPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--
|
||||
this.clearCanvases()
|
||||
await this.renderPage(this.currentPage)
|
||||
}
|
||||
},
|
||||
|
||||
async nextPage() {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.currentPage++
|
||||
this.clearCanvases()
|
||||
await this.renderPage(this.currentPage)
|
||||
}
|
||||
},
|
||||
|
||||
async zoomIn() {
|
||||
if (this.scale < 3) {
|
||||
this.scale += 0.2
|
||||
this.clearCanvases()
|
||||
await this.renderPage(this.currentPage)
|
||||
}
|
||||
},
|
||||
|
||||
async zoomOut() {
|
||||
if (this.scale > 0.5) {
|
||||
this.scale -= 0.2
|
||||
this.clearCanvases()
|
||||
await this.renderPage(this.currentPage)
|
||||
}
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.$emit('close')
|
||||
this.$emit('update:visible', false)
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
this.clearCanvases()
|
||||
if (this.pdfDoc) {
|
||||
this.pdfDoc.destroy()
|
||||
this.pdfDoc = null
|
||||
}
|
||||
this.currentPage = 1
|
||||
this.totalPages = 0
|
||||
this.error = ''
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.cleanup()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pdf-viewer-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-container {
|
||||
width: 95%;
|
||||
height: 90%;
|
||||
max-width: 900px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pdf-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 15px;
|
||||
background: #C93639;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.pdf-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 13px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 4px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pdf-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.loading-wrapper,
|
||||
.error-wrapper {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 2px solid #f0f0f0;
|
||||
border-top-color: #C93639;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
padding: 8px 20px;
|
||||
background: #C93639;
|
||||
color: #fff;
|
||||
border-radius: 15px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pdf-scroll-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
min-height: 100%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pdf-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 8px 16px;
|
||||
background: #C93639;
|
||||
color: #fff;
|
||||
border-radius: 15px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
background: #ccc;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 全局样式,确保 canvas 正确显示 */
|
||||
.pdf-canvas-element {
|
||||
display: block !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -12,13 +12,13 @@ const development = {
|
||||
|
||||
// 测试环境配置
|
||||
const test = {
|
||||
baseURL: 'http://test-api.yourdomain.com',
|
||||
baseURL: 'https://martial-api.aitisai.com',
|
||||
timeout: 30000
|
||||
}
|
||||
|
||||
// 生产环境配置
|
||||
const production = {
|
||||
baseURL: 'https://api.yourdomain.com',
|
||||
baseURL: 'https://martial-api.aitisai.com',
|
||||
timeout: 30000
|
||||
}
|
||||
|
||||
|
||||
247
src/pages.json
247
src/pages.json
@@ -1,220 +1,35 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/register/register",
|
||||
"style": {
|
||||
"navigationBarTitleText": "注册",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/home/home",
|
||||
"style": {
|
||||
"navigationBarTitleText": "武术赛事通",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/profile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人中心",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/change-password/change-password",
|
||||
"style": {
|
||||
"navigationBarTitleText": "修改密码",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/common-info/common-info",
|
||||
"style": {
|
||||
"navigationBarTitleText": "常用信息",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/add-player/add-player",
|
||||
"style": {
|
||||
"navigationBarTitleText": "新增选手",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/edit-player/edit-player",
|
||||
"style": {
|
||||
"navigationBarTitleText": "编辑选手",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/add-contact/add-contact",
|
||||
"style": {
|
||||
"navigationBarTitleText": "新增联系人",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my-registration/my-registration",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的报名",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-list/event-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "全部赛事列表",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-detail/event-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "赛事详情",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/select-event/select-event",
|
||||
"style": {
|
||||
"navigationBarTitleText": "选择报名项目",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-register/event-register",
|
||||
"style": {
|
||||
"navigationBarTitleText": "赛事报名",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/register-type/register-type",
|
||||
"style": {
|
||||
"navigationBarTitleText": "选择报名",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-info/event-info",
|
||||
"style": {
|
||||
"navigationBarTitleText": "信息发布",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-info-detail/event-info-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "信息详情",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-rules/event-rules",
|
||||
"style": {
|
||||
"navigationBarTitleText": "赛事规程",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-schedule/event-schedule",
|
||||
"style": {
|
||||
"navigationBarTitleText": "活动日程",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-players/event-players",
|
||||
"style": {
|
||||
"navigationBarTitleText": "参赛选手",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-live/event-live",
|
||||
"style": {
|
||||
"navigationBarTitleText": "比赛实况",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-lineup/event-lineup",
|
||||
"style": {
|
||||
"navigationBarTitleText": "出场顺序",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-score/event-score",
|
||||
"style": {
|
||||
"navigationBarTitleText": "成绩",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/event-medals/event-medals",
|
||||
"style": {
|
||||
"navigationBarTitleText": "奖牌榜",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
}
|
||||
{"path": "pages/login/login", "style": {"navigationBarTitleText": "登录", "navigationStyle": "custom"}},
|
||||
{"path": "pages/register/register", "style": {"navigationBarTitleText": "注册", "navigationStyle": "custom"}},
|
||||
{"path": "pages/home/home", "style": {"navigationBarTitleText": "武术赛事通", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/profile/profile", "style": {"navigationBarTitleText": "个人中心", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/change-password/change-password", "style": {"navigationBarTitleText": "修改密码", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/common-info/common-info", "style": {"navigationBarTitleText": "常用信息", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/add-player/add-player", "style": {"navigationBarTitleText": "新增选手", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/add-team/add-team", "style": {"navigationBarTitleText": "新增集体", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/edit-team/edit-team", "style": {"navigationBarTitleText": "编辑集体", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/edit-player/edit-player", "style": {"navigationBarTitleText": "编辑选手", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/add-contact/add-contact", "style": {"navigationBarTitleText": "新增联系人", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/edit-contact/edit-contact", "style": {"navigationBarTitleText": "编辑联系人", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/my-registration/my-registration", "style": {"navigationBarTitleText": "我的报名", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-list/event-list", "style": {"navigationBarTitleText": "全部赛事列表", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-detail/event-detail", "style": {"navigationBarTitleText": "赛事详情", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/select-event/select-event", "style": {"navigationBarTitleText": "选择报名项目", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-register/event-register", "style": {"navigationBarTitleText": "赛事报名", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/register-type/register-type", "style": {"navigationBarTitleText": "选择报名", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-info/event-info", "style": {"navigationBarTitleText": "信息发布", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-info-detail/event-info-detail", "style": {"navigationBarTitleText": "信息详情", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-rules/event-rules", "style": {"navigationBarTitleText": "赛事规程", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-schedule/event-schedule", "style": {"navigationBarTitleText": "活动日程", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-players/event-players", "style": {"navigationBarTitleText": "参赛选手", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-live/event-live", "style": {"navigationBarTitleText": "比赛实况", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-lineup/event-lineup", "style": {"navigationBarTitleText": "出场顺序", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-score/event-score", "style": {"navigationBarTitleText": "成绩", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-medals/event-medals", "style": {"navigationBarTitleText": "奖牌榜", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/attachment-view/attachment-view", "style": {"navigationBarTitleText": "附件查看", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}},
|
||||
{"path": "pages/event-photos/event-photos", "style": {"navigationBarTitleText": "图片直播", "navigationBarBackgroundColor": "#C93639", "navigationBarTextStyle": "white"}}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "武术赛事",
|
||||
"navigationBarBackgroundColor": "#C93639",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#999999",
|
||||
"selectedColor": "#C93639",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/home/home",
|
||||
"text": "首页",
|
||||
"iconPath": "static/images/首页灰@3x.png",
|
||||
"selectedIconPath": "static/images/首页亮@3x.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"text": "个人中心",
|
||||
"iconPath": "static/images/个人中心灰@3x.png",
|
||||
"selectedIconPath": "static/images/个人中心亮@3x.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
"globalStyle": {"navigationBarTextStyle": "white", "navigationBarTitleText": "武术赛事", "navigationBarBackgroundColor": "#C93639", "backgroundColor": "#F8F8F8"},
|
||||
"tabBar": {"color": "#999999", "selectedColor": "#C93639", "backgroundColor": "#ffffff", "borderStyle": "black", "list": [{"pagePath": "pages/home/home", "text": "首页", "iconPath": "static/images/首页灰@3x.png", "selectedIconPath": "static/images/首页亮@3x.png"}, {"pagePath": "pages/profile/profile", "text": "个人中心", "iconPath": "static/images/个人中心灰@3x.png", "selectedIconPath": "static/images/个人中心亮@3x.png"}]}
|
||||
}
|
||||
|
||||
@@ -13,105 +13,63 @@
|
||||
<view class="form-item">
|
||||
<view class="form-label">姓名</view>
|
||||
<view class="form-value">
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="formData.name"
|
||||
placeholder="请输入姓名"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
<input class="form-input" v-model="formData.name" placeholder="请输入姓名" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">证件号码</view>
|
||||
<view class="form-value">
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="formData.idCard"
|
||||
placeholder="请输入身份证号码"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
<input class="form-input" v-model="formData.idCard" placeholder="请输入身份证号码" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">手机号码</view>
|
||||
<view class="form-value">
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="formData.phone"
|
||||
placeholder="请输入手机号码"
|
||||
placeholder-class="placeholder"
|
||||
type="number"
|
||||
/>
|
||||
<input class="form-input" v-model="formData.phone" placeholder="请输入手机号码" placeholder-class="placeholder" type="number" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">邮箱</view>
|
||||
<view class="form-value">
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="formData.email"
|
||||
placeholder="请输入邮箱"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
<input class="form-input" v-model="formData.email" placeholder="请输入邮箱" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">地址</view>
|
||||
<view class="form-value">
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="formData.address"
|
||||
placeholder="请输入地址"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
<input class="form-input" v-model="formData.address" placeholder="请输入地址" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item switch-item">
|
||||
<view class="form-label">设置为默认联系人</view>
|
||||
<view class="form-value">
|
||||
<switch
|
||||
:checked="formData.isDefault"
|
||||
@change="handleSwitchChange"
|
||||
color="#C93639"
|
||||
/>
|
||||
<switch :checked="formData.isDefault" @change="handleSwitchChange" color="#C93639" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<view class="hint-message" v-if="showHint">
|
||||
<text class="hint-icon">ℹ</text>
|
||||
<text class="hint-text">{{ hintText }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Toast提示 -->
|
||||
<view class="toast-message" v-if="showToast">
|
||||
<text class="toast-text">{{ toastMessage }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 提示文字 -->
|
||||
<view class="info-text">
|
||||
<text class="info-hint">默认关闭,可切换开关</text>
|
||||
</view>
|
||||
|
||||
<view class="warning-text">
|
||||
<text>联系人用于接收比赛信息,成绩和证书。</text>
|
||||
</view>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<view class="btn-wrapper">
|
||||
<view class="btn save-btn disabled" v-if="!isFormValid">保存</view>
|
||||
<view class="btn save-btn" v-else @click="handleSave">保存</view>
|
||||
<view class="btn save-btn" :class="{ disabled: !isFormValid || saving }" @click="handleSave">
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import athleteAPI from '@/api/athlete.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -124,27 +82,20 @@ export default {
|
||||
address: '',
|
||||
isDefault: false
|
||||
},
|
||||
showHint: false,
|
||||
hintText: '',
|
||||
showToast: false,
|
||||
toastMessage: '',
|
||||
showIdTypePicker: false
|
||||
saving: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isFormValid() {
|
||||
return (
|
||||
this.formData.name &&
|
||||
this.formData.idCard &&
|
||||
this.formData.phone &&
|
||||
this.validateIdCard(this.formData.idCard) &&
|
||||
this.validatePhone(this.formData.phone)
|
||||
);
|
||||
return this.formData.name && this.formData.idCard && this.formData.phone &&
|
||||
this.validateIdCard(this.formData.idCard) && this.validatePhone(this.formData.phone);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateIdCard(idCard) {
|
||||
return /^\d{18}$/.test(idCard);
|
||||
return /^\d{17}[\dXx]$/.test(idCard);
|
||||
},
|
||||
validatePhone(phone) {
|
||||
return /^1\d{10}$/.test(phone);
|
||||
@@ -152,38 +103,34 @@ export default {
|
||||
handleSwitchChange(e) {
|
||||
this.formData.isDefault = e.detail.value;
|
||||
},
|
||||
handleSave() {
|
||||
if (!this.isFormValid) {
|
||||
if (this.formData.phone && !this.validatePhone(this.formData.phone)) {
|
||||
this.toastMessage = '手机号码格式不正确';
|
||||
this.showToast = true;
|
||||
setTimeout(() => {
|
||||
this.showToast = false;
|
||||
}, 2000);
|
||||
} else if (this.formData.idCard && !this.validateIdCard(this.formData.idCard)) {
|
||||
this.toastMessage = '身份证号码格式不正确';
|
||||
this.showToast = true;
|
||||
setTimeout(() => {
|
||||
this.showToast = false;
|
||||
}, 2000);
|
||||
} else {
|
||||
this.hintText = '请完善信息';
|
||||
this.showHint = true;
|
||||
setTimeout(() => {
|
||||
this.showHint = false;
|
||||
}, 2000);
|
||||
}
|
||||
showToastMsg(msg) {
|
||||
this.toastMessage = msg;
|
||||
this.showToast = true;
|
||||
setTimeout(() => { this.showToast = false; }, 2000);
|
||||
},
|
||||
async handleSave() {
|
||||
if (!this.isFormValid || this.saving) return;
|
||||
|
||||
if (!this.validatePhone(this.formData.phone)) {
|
||||
this.showToastMsg('手机号码格式不正确');
|
||||
return;
|
||||
}
|
||||
if (!this.validateIdCard(this.formData.idCard)) {
|
||||
this.showToastMsg('身份证号码格式不正确');
|
||||
return;
|
||||
}
|
||||
|
||||
// 实际保存逻辑
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
this.saving = true;
|
||||
try {
|
||||
await athleteAPI.saveContact(this.formData);
|
||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||
setTimeout(() => { uni.navigateBack(); }, 1500);
|
||||
} catch (err) {
|
||||
console.error('保存联系人失败:', err);
|
||||
this.showToastMsg('保存失败,请重试');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -210,13 +157,8 @@ export default {
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.switch-item {
|
||||
padding: 30rpx;
|
||||
}
|
||||
.form-item:last-child { border-bottom: none; }
|
||||
.switch-item { padding: 30rpx; }
|
||||
|
||||
.form-label {
|
||||
width: 180rpx;
|
||||
@@ -237,38 +179,8 @@ export default {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 40rpx;
|
||||
color: #cccccc;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.hint-message {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
padding: 30rpx 50rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15rpx;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.hint-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.placeholder { color: #cccccc; }
|
||||
.arrow { font-size: 40rpx; color: #cccccc; margin-left: 10rpx; }
|
||||
|
||||
.toast-message {
|
||||
position: fixed;
|
||||
@@ -282,26 +194,7 @@ export default {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.toast-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
margin: 20rpx 30rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-hint {
|
||||
font-size: 24rpx;
|
||||
color: #C93639;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
margin: 20rpx 30rpx;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #C93639;
|
||||
}
|
||||
.toast-text { font-size: 28rpx; }
|
||||
|
||||
.btn-wrapper {
|
||||
position: fixed;
|
||||
|
||||
@@ -72,29 +72,16 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<view class="error-hints" v-if="errors.length > 0">
|
||||
<view class="error-item" v-for="(error, index) in errors" :key="index">
|
||||
<text class="error-label">{{ error.label }}</text>
|
||||
<text class="error-msg">{{ error.message }}</text>
|
||||
<!-- 错误提示 - 只在有具体错误时显示 -->
|
||||
<view class="error-hints" v-if="validationErrors.length > 0">
|
||||
<view class="error-item" v-for="(error, index) in validationErrors" :key="index">
|
||||
<text class="error-text">{{ error }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<view class="hint-message" v-if="showHint">
|
||||
<text class="hint-icon">ℹ</text>
|
||||
<text class="hint-text">请完善信息</text>
|
||||
</view>
|
||||
|
||||
<!-- Toast提示 -->
|
||||
<view class="toast-message" v-if="showToast">
|
||||
<text class="toast-text">{{ toastMessage }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<view class="btn-wrapper">
|
||||
<view class="btn save-btn disabled" v-if="!isFormValid">保存</view>
|
||||
<view class="btn save-btn" v-else @click="handleSave">保存</view>
|
||||
<view class="btn save-btn" :class="{ disabled: !isFormValid }" @click="handleSave">保存</view>
|
||||
</view>
|
||||
|
||||
<!-- 证件类型选择器 -->
|
||||
@@ -114,6 +101,7 @@
|
||||
|
||||
<script>
|
||||
import athleteAPI from '@/api/athlete.js'
|
||||
import { getUserInfo, getToken } from '@/utils/auth.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -126,13 +114,20 @@ export default {
|
||||
organization: '',
|
||||
phone: ''
|
||||
},
|
||||
errors: [],
|
||||
showHint: false,
|
||||
showToast: false,
|
||||
toastMessage: '',
|
||||
competitionId: '',
|
||||
projectIds: [],
|
||||
validationErrors: [],
|
||||
showIdTypePicker: false
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.competitionId) {
|
||||
this.competitionId = options.competitionId
|
||||
}
|
||||
if (options.projectIds) {
|
||||
this.projectIds = options.projectIds.split(',')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isFormValid() {
|
||||
return (
|
||||
@@ -147,141 +142,138 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'formData.name'(val) {
|
||||
this.validateForm();
|
||||
},
|
||||
'formData.idCard'(val) {
|
||||
this.validateForm();
|
||||
},
|
||||
'formData.team'(val) {
|
||||
this.validateForm();
|
||||
},
|
||||
'formData.organization'(val) {
|
||||
this.validateForm();
|
||||
this.updateValidationErrors();
|
||||
},
|
||||
'formData.phone'(val) {
|
||||
this.validateForm();
|
||||
this.updateValidationErrors();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateIdCard(idCard) {
|
||||
// 身份证号验证:18位,最后一位可以是数字或字母X
|
||||
return /^\d{17}[\dXx]$/.test(idCard);
|
||||
if (!idCard) return false;
|
||||
if (!/^\d{17}[\dXx]$/.test(idCard)) return false;
|
||||
|
||||
const year = parseInt(idCard.substring(6, 10))
|
||||
const month = parseInt(idCard.substring(10, 12))
|
||||
const day = parseInt(idCard.substring(12, 14))
|
||||
|
||||
if (month < 1 || month > 12) return false;
|
||||
if (day < 1 || day > 31) return false;
|
||||
|
||||
const currentYear = new Date().getFullYear()
|
||||
if (year < 1900 || year > currentYear) return false;
|
||||
|
||||
const date = new Date(year, month - 1, day)
|
||||
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
validatePhone(phone) {
|
||||
// 手机号验证:11位数字
|
||||
return /^1[3-9]\d{9}$/.test(phone);
|
||||
},
|
||||
validateForm() {
|
||||
this.errors = [];
|
||||
this.showHint = false;
|
||||
|
||||
if (!this.formData.name || !this.formData.idCard || !this.formData.team || !this.formData.organization || !this.formData.phone) {
|
||||
this.showHint = true;
|
||||
if (!this.formData.name || !this.formData.idCard || !this.formData.team || !this.formData.organization || !this.formData.phone) {
|
||||
this.errors.push({
|
||||
label: '有空文本时弹出:',
|
||||
message: '按钮置灰'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateValidationErrors() {
|
||||
this.validationErrors = [];
|
||||
|
||||
if (this.formData.idCard && !this.validateIdCard(this.formData.idCard)) {
|
||||
this.errors.push({
|
||||
label: '身份证不足18位:',
|
||||
message: '按钮置灰'
|
||||
});
|
||||
this.errors.push({
|
||||
label: '输入不合法:',
|
||||
message: '按钮置灰'
|
||||
});
|
||||
this.validationErrors.push('身份证号码格式不正确');
|
||||
}
|
||||
|
||||
|
||||
if (this.formData.phone && !this.validatePhone(this.formData.phone)) {
|
||||
this.errors.push({
|
||||
label: '手机号格式不正确:',
|
||||
message: '按钮置灰'
|
||||
});
|
||||
this.validationErrors.push('手机号格式不正确');
|
||||
}
|
||||
},
|
||||
handleIdTypeChange(e) {
|
||||
this.formData.idType = '身份证';
|
||||
this.showIdTypePicker = false;
|
||||
},
|
||||
/**
|
||||
* 从身份证号中提取信息
|
||||
*/
|
||||
extractInfoFromIdCard(idCard) {
|
||||
if (!idCard || idCard.length !== 18) {
|
||||
return {
|
||||
gender: null,
|
||||
age: null,
|
||||
birthDate: null
|
||||
}
|
||||
return { gender: null, age: null, birthDate: null }
|
||||
}
|
||||
|
||||
// 提取出生日期
|
||||
const year = idCard.substring(6, 10)
|
||||
const month = idCard.substring(10, 12)
|
||||
const day = idCard.substring(12, 14)
|
||||
const birthDate = `${year}-${month}-${day}`
|
||||
|
||||
// 计算年龄
|
||||
const birthYear = parseInt(year)
|
||||
const currentYear = new Date().getFullYear()
|
||||
const age = currentYear - birthYear
|
||||
|
||||
// 提取性别(倒数第二位,奇数为男,偶数为女)
|
||||
const genderCode = parseInt(idCard.substring(16, 17))
|
||||
const gender = genderCode % 2 === 1 ? 1 : 2
|
||||
|
||||
return {
|
||||
gender,
|
||||
age,
|
||||
birthDate
|
||||
}
|
||||
return { gender, age, birthDate }
|
||||
},
|
||||
|
||||
async handleSave() {
|
||||
if (!this.isFormValid) {
|
||||
uni.showToast({
|
||||
title: '请完善信息',
|
||||
icon: 'none'
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
// Check login status
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 从身份证号中提取信息
|
||||
const info = this.extractInfoFromIdCard(this.formData.idCard)
|
||||
|
||||
// 调用API保存选手信息(使用后端实体类的字段名)
|
||||
await athleteAPI.submitAthlete({
|
||||
const userInfo = getUserInfo()
|
||||
const submitData = {
|
||||
playerName: this.formData.name,
|
||||
idCard: this.formData.idCard,
|
||||
teamName: this.formData.team,
|
||||
organization: this.formData.organization,
|
||||
contactPhone: this.formData.phone,
|
||||
idCardType: 1, // 身份证类型固定为1
|
||||
idCardType: 1,
|
||||
gender: info.gender,
|
||||
age: info.age,
|
||||
birthDate: info.birthDate
|
||||
})
|
||||
birthDate: info.birthDate,
|
||||
createUser: userInfo ? userInfo.userId : null
|
||||
}
|
||||
|
||||
if (this.competitionId) {
|
||||
submitData.competitionId = this.competitionId
|
||||
}
|
||||
if (this.projectIds && this.projectIds.length > 0) {
|
||||
submitData.projectId = this.projectIds[0]
|
||||
}
|
||||
|
||||
console.log('提交选手数据:', submitData)
|
||||
await athleteAPI.submitAthlete(submitData)
|
||||
|
||||
// 保存成功
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
console.error('保存选手失败:', err)
|
||||
// 显示错误提示
|
||||
this.toastMessage = '保存失败,请重试'
|
||||
this.showToast = true
|
||||
setTimeout(() => {
|
||||
this.showToast = false
|
||||
}, 2000)
|
||||
uni.showToast({
|
||||
title: err.message || '保存失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,64 +335,23 @@ export default {
|
||||
}
|
||||
|
||||
.error-hints {
|
||||
margin: 30rpx;
|
||||
padding: 30rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin: 0 30rpx;
|
||||
padding: 20rpx;
|
||||
background-color: #fff3f3;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.error-item {
|
||||
margin-bottom: 15rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.error-label {
|
||||
.error-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 26rpx;
|
||||
color: #C93639;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
font-size: 26rpx;
|
||||
color: #C93639;
|
||||
}
|
||||
|
||||
.hint-message {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
padding: 30rpx 50rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15rpx;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.hint-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
padding: 30rpx 50rpx;
|
||||
border-radius: 16rpx;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.toast-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn-wrapper {
|
||||
@@ -415,9 +366,9 @@ export default {
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
374
src/pages/add-team/add-team.vue
Normal file
374
src/pages/add-team/add-team.vue
Normal file
@@ -0,0 +1,374 @@
|
||||
<template>
|
||||
<view class="add-team-page">
|
||||
<view class="form-section">
|
||||
<view class="form-item">
|
||||
<view class="form-label required">集体名称</view>
|
||||
<input
|
||||
class="form-input"
|
||||
type="text"
|
||||
v-model="formData.name"
|
||||
placeholder="请输入集体名称"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">备注</view>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="formData.remark"
|
||||
placeholder="请输入备注信息"
|
||||
></textarea>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成员列表 -->
|
||||
<view class="member-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">成员列表</text>
|
||||
<view class="add-member-btn" @click="showMemberPicker = true">
|
||||
<text>+ 添加成员</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="member-list" v-if="formData.members.length > 0">
|
||||
<view class="member-item" v-for="(member, index) in formData.members" :key="index">
|
||||
<view class="member-info">
|
||||
<text class="member-name">{{ member.name }}</text>
|
||||
</view>
|
||||
<view class="remove-btn" @click="removeMember(index)">移除</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-members" v-else>
|
||||
<text>暂无成员,请添加</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-btn-wrapper">
|
||||
<view class="submit-btn" @click="handleSubmit">保存</view>
|
||||
</view>
|
||||
|
||||
<!-- 成员选择弹窗 -->
|
||||
<view class="member-picker-mask" v-if="showMemberPicker" @click="showMemberPicker = false">
|
||||
<view class="member-picker" @click.stop>
|
||||
<view class="picker-header">
|
||||
<text class="picker-title">选择成员</text>
|
||||
<text class="picker-close" @click="showMemberPicker = false">×</text>
|
||||
</view>
|
||||
<view class="picker-list">
|
||||
<view
|
||||
class="picker-item"
|
||||
v-for="(player, index) in availablePlayers"
|
||||
:key="index"
|
||||
@click="addMember(player)"
|
||||
>
|
||||
<text>{{ player.name }}</text>
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
<view class="empty-picker" v-if="availablePlayers.length === 0">
|
||||
<text>暂无可选成员</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import athleteAPI from '@/api/athlete.js';
|
||||
import { getUserInfo } from '@/utils/auth.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
name: '',
|
||||
remark: '',
|
||||
members: []
|
||||
},
|
||||
playerList: [],
|
||||
showMemberPicker: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
availablePlayers() {
|
||||
const memberIds = this.formData.members.map(m => m.id)
|
||||
return this.playerList.filter(p => !memberIds.includes(p.id))
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadPlayerList()
|
||||
},
|
||||
methods: {
|
||||
async loadPlayerList() {
|
||||
try {
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo || !userInfo.userId) return
|
||||
|
||||
const res = await athleteAPI.getAthleteList({
|
||||
current: 1,
|
||||
size: 100,
|
||||
createUser: userInfo.userId
|
||||
})
|
||||
|
||||
let list = []
|
||||
if (res.records) {
|
||||
list = res.records
|
||||
} else if (Array.isArray(res)) {
|
||||
list = res
|
||||
}
|
||||
|
||||
this.playerList = list.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name || item.playerName
|
||||
}))
|
||||
} catch (err) {
|
||||
console.error('加载选手列表失败:', err)
|
||||
}
|
||||
},
|
||||
|
||||
addMember(player) {
|
||||
this.formData.members.push({
|
||||
id: player.id,
|
||||
name: player.name
|
||||
})
|
||||
this.showMemberPicker = false
|
||||
},
|
||||
|
||||
removeMember(index) {
|
||||
this.formData.members.splice(index, 1)
|
||||
},
|
||||
|
||||
async handleSubmit() {
|
||||
if (!this.formData.name) {
|
||||
uni.showToast({
|
||||
title: '请输入集体名称',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (this.formData.members.length === 0) {
|
||||
uni.showToast({
|
||||
title: '请添加至少一名成员',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = getUserInfo()
|
||||
|
||||
const data = {
|
||||
teamName: this.formData.name,
|
||||
remark: this.formData.remark,
|
||||
memberIds: this.formData.members.map(m => m.id),
|
||||
createUser: userInfo?.userId
|
||||
}
|
||||
|
||||
if (athleteAPI.saveTeam) {
|
||||
await athleteAPI.saveTeam(data)
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
console.error('保存失败:', err)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.add-team-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 150rpx;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.form-label.required::before {
|
||||
content: '*';
|
||||
color: #C93639;
|
||||
margin-right: 5rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.member-section {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.add-member-btn {
|
||||
color: #C93639;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
color: #C93639;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.empty-members {
|
||||
text-align: center;
|
||||
padding: 50rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.submit-btn-wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 30rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #C93639;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.member-picker-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.member-picker {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.picker-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.picker-close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.picker-list {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.picker-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
color: #C93639;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.empty-picker {
|
||||
text-align: center;
|
||||
padding: 50rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
903
src/pages/attachment-view/attachment-view.vue
Normal file
903
src/pages/attachment-view/attachment-view.vue
Normal file
@@ -0,0 +1,903 @@
|
||||
<template>
|
||||
<view class="attachment-page">
|
||||
<!-- 赛事信息卡片 -->
|
||||
<view class="event-info-card">
|
||||
<view class="event-title">{{ competitionName || '赛事名称' }}</view>
|
||||
<view class="event-time-row">
|
||||
<view class="time-item">
|
||||
<text class="time-label">开始时间</text>
|
||||
<text class="time-value">{{ startTime || '待定' }}</text>
|
||||
</view>
|
||||
<view class="time-divider"></view>
|
||||
<view class="time-item">
|
||||
<text class="time-label">结束时间</text>
|
||||
<text class="time-value">{{ endTime || '待定' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" v-if="loading">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 附件列表 -->
|
||||
<view class="attachments-section" v-if="!loading && attachments.length > 0">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ pageTitle }}文件</text>
|
||||
<text class="section-count">共{{ attachments.length }}个文件</text>
|
||||
</view>
|
||||
|
||||
<view class="attachments-list">
|
||||
<view
|
||||
class="attachment-item"
|
||||
v-for="(file, index) in attachments"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 文件图标 -->
|
||||
<view class="file-icon" :class="'icon-' + file.fileType">
|
||||
<text class="icon-text">{{ getFileIconText(file.fileType) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 文件信息 -->
|
||||
<view class="file-content">
|
||||
<text class="file-name">{{ file.fileName }}</text>
|
||||
<view class="file-meta">
|
||||
<text class="meta-item">{{ file.fileSize }}</text>
|
||||
<text class="meta-dot" v-if="file.uploadTime">·</text>
|
||||
<text class="meta-item" v-if="file.uploadTime">{{ file.uploadTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="file-actions">
|
||||
<view class="action-btn preview-btn" @click="previewFile(file)">
|
||||
<text class="action-text">预览</text>
|
||||
</view>
|
||||
<view class="action-btn download-btn" @click="downloadFile(file)">
|
||||
<text class="action-text">下载</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="!loading && attachments.length === 0">
|
||||
<image class="empty-image" src="/static/images/empty.png" mode="aspectFit" />
|
||||
<text class="empty-title">暂无{{ pageTitle }}文件</text>
|
||||
<text class="empty-desc">相关文件正在整理中,请稍后查看</text>
|
||||
</view>
|
||||
|
||||
<!-- PDF预览组件 (仅H5) -->
|
||||
<!-- #ifdef H5 -->
|
||||
<pdf-viewer
|
||||
:visible="showPdfViewer"
|
||||
:url="pdfUrl"
|
||||
:file-name="pdfFileName"
|
||||
@close="closePdfViewer"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 图片预览弹窗 (仅H5) -->
|
||||
<!-- #ifdef H5 -->
|
||||
<view class="image-preview-modal" v-if="showImagePreview" @click="closeImagePreview">
|
||||
<view class="image-preview-container" @click.stop>
|
||||
<view class="image-preview-header">
|
||||
<text class="image-preview-title">{{ previewImageName }}</text>
|
||||
<view class="image-preview-close" @click="closeImagePreview">
|
||||
<text class="close-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="image-preview-body">
|
||||
<image :src="previewImageUrl" mode="aspectFit" class="preview-image" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import competitionAPI from '@/api/competition.js'
|
||||
// #ifdef H5
|
||||
import PdfViewer from '@/components/pdf-viewer/pdf-viewer.vue'
|
||||
// #endif
|
||||
|
||||
// 页面类型配置
|
||||
const PAGE_CONFIG = {
|
||||
'info': { title: '信息发布', type: 'info' },
|
||||
'rules': { title: '赛事规程', type: 'rules' },
|
||||
'schedule': { title: '活动日程', type: 'schedule' },
|
||||
'score': { title: '成绩公告', type: 'results' },
|
||||
'results': { title: '成绩公告', type: 'results' },
|
||||
'awards': { title: '奖牌榜', type: 'medals' },
|
||||
'medals': { title: '奖牌榜', type: 'medals' },
|
||||
'photos': { title: '图片直播', type: 'photos' }
|
||||
}
|
||||
|
||||
export default {
|
||||
// #ifdef H5
|
||||
components: {
|
||||
PdfViewer
|
||||
},
|
||||
// #endif
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
pageType: 'rules',
|
||||
pageTitle: '赛事规程',
|
||||
competitionId: '',
|
||||
competitionName: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
attachments: [],
|
||||
// PDF预览相关
|
||||
showPdfViewer: false,
|
||||
pdfUrl: '',
|
||||
pdfFileName: '',
|
||||
// 图片预览相关
|
||||
showImagePreview: false,
|
||||
previewImageUrl: '',
|
||||
previewImageName: ''
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 获取页面类型
|
||||
if (options.type && PAGE_CONFIG[options.type]) {
|
||||
this.pageType = options.type
|
||||
this.pageTitle = PAGE_CONFIG[options.type].title
|
||||
}
|
||||
|
||||
// 获取赛事ID
|
||||
if (options.competitionId || options.eventId) {
|
||||
this.competitionId = options.competitionId || options.eventId
|
||||
}
|
||||
|
||||
// 获取赛事名称
|
||||
if (options.name) {
|
||||
this.competitionName = decodeURIComponent(options.name)
|
||||
}
|
||||
|
||||
// 获取比赛时间
|
||||
if (options.startTime) {
|
||||
this.startTime = decodeURIComponent(options.startTime)
|
||||
}
|
||||
if (options.endTime) {
|
||||
this.endTime = decodeURIComponent(options.endTime)
|
||||
}
|
||||
|
||||
// 设置导航栏标题
|
||||
uni.setNavigationBarTitle({
|
||||
title: this.pageTitle
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载数据
|
||||
*/
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
try {
|
||||
// 如果没有赛事信息,先获取赛事详情
|
||||
if (!this.startTime || !this.endTime) {
|
||||
await this.loadCompetitionInfo()
|
||||
}
|
||||
// 加载附件列表
|
||||
await this.loadAttachments()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载赛事信息
|
||||
*/
|
||||
async loadCompetitionInfo() {
|
||||
try {
|
||||
const res = await competitionAPI.getCompetitionDetail(this.competitionId)
|
||||
if (res) {
|
||||
this.competitionName = res.name || res.title || this.competitionName
|
||||
this.startTime = this.formatDate(res.startTime || res.competitionStartTime)
|
||||
this.endTime = this.formatDate(res.endTime || res.competitionEndTime)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载赛事信息失败:', err)
|
||||
// 使用模拟数据
|
||||
this.startTime = '2025.12.12'
|
||||
this.endTime = '2025.12.14'
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载附件列表
|
||||
*/
|
||||
async loadAttachments() {
|
||||
try {
|
||||
// 使用 PAGE_CONFIG 映射的 type 值,而不是直接使用 pageType
|
||||
const attachmentType = PAGE_CONFIG[this.pageType]?.type || this.pageType
|
||||
console.log('=== 加载附件 ===')
|
||||
console.log('competitionId:', this.competitionId)
|
||||
console.log('pageType:', this.pageType)
|
||||
console.log('attachmentType:', attachmentType)
|
||||
|
||||
const res = await competitionAPI.getAttachments({
|
||||
competitionId: this.competitionId,
|
||||
type: attachmentType
|
||||
})
|
||||
|
||||
console.log('API返回结果:', res)
|
||||
|
||||
// 处理返回结果 - res 可能是数组或者 null/undefined
|
||||
if (res && Array.isArray(res) && res.length > 0) {
|
||||
this.attachments = res.map(file => ({
|
||||
id: file.id,
|
||||
fileName: file.fileName || file.name,
|
||||
fileUrl: file.fileUrl || file.url,
|
||||
fileSize: this.formatFileSize(file.fileSize || file.size),
|
||||
fileType: this.getFileType(file.fileName || file.name),
|
||||
uploadTime: this.formatDate(file.uploadTime || file.createTime)
|
||||
}))
|
||||
console.log('附件加载成功,共', this.attachments.length, '个文件')
|
||||
} else {
|
||||
// 空数据,显示空状态
|
||||
console.log('没有附件数据')
|
||||
this.attachments = []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('=== 加载附件失败 ===')
|
||||
console.error('错误详情:', err)
|
||||
console.error('错误消息:', err.message)
|
||||
console.error('错误代码:', err.code)
|
||||
|
||||
// 只有在开发环境或者网络错误时才使用模拟数据
|
||||
// 生产环境应该显示空状态
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('开发环境,使用模拟数据')
|
||||
this.loadMockData()
|
||||
} else {
|
||||
this.attachments = []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载模拟数据
|
||||
*/
|
||||
loadMockData() {
|
||||
const mockDataMap = {
|
||||
'info': [
|
||||
{ id: '1', fileName: '2025年郑州武术大赛通知.pdf', fileUrl: '', fileSize: '1.2 MB', fileType: 'pdf', uploadTime: '2025-12-20' }
|
||||
],
|
||||
'rules': [
|
||||
{ id: '1', fileName: '2025年郑州武术大赛竞赛规程.pdf', fileUrl: '', fileSize: '2.5 MB', fileType: 'pdf', uploadTime: '2025-12-18' },
|
||||
{ id: '2', fileName: '参赛报名表.pdf', fileUrl: '', fileSize: '156 KB', fileType: 'pdf', uploadTime: '2025-12-18' }
|
||||
],
|
||||
'schedule': [
|
||||
{ id: '1', fileName: '比赛日程安排表.pdf', fileUrl: '', fileSize: '890 KB', fileType: 'pdf', uploadTime: '2025-12-19' }
|
||||
],
|
||||
'score': [
|
||||
{ id: '1', fileName: '比赛成绩公告.pdf', fileUrl: '', fileSize: '1.8 MB', fileType: 'pdf', uploadTime: '2025-12-25' }
|
||||
],
|
||||
'results': [
|
||||
{ id: '1', fileName: '比赛成绩公告.pdf', fileUrl: '', fileSize: '1.8 MB', fileType: 'pdf', uploadTime: '2025-12-25' }
|
||||
],
|
||||
'awards': [
|
||||
{ id: '1', fileName: '奖牌榜统计.pdf', fileUrl: '', fileSize: '520 KB', fileType: 'pdf', uploadTime: '2025-12-25' }
|
||||
],
|
||||
'medals': [
|
||||
{ id: '1', fileName: '奖牌榜统计.pdf', fileUrl: '', fileSize: '520 KB', fileType: 'pdf', uploadTime: '2025-12-25' }
|
||||
],
|
||||
'photos': [
|
||||
{ id: '1', fileName: '比赛精彩瞬间.pdf', fileUrl: '', fileSize: '15.6 MB', fileType: 'pdf', uploadTime: '2025-12-25' }
|
||||
]
|
||||
}
|
||||
|
||||
this.attachments = mockDataMap[this.pageType] || []
|
||||
},
|
||||
|
||||
/**
|
||||
* 预览文件
|
||||
*/
|
||||
previewFile(file) {
|
||||
if (!file.fileUrl) {
|
||||
uni.showToast({
|
||||
title: '文件暂不可用',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
// H5端:根据文件类型选择预览方式
|
||||
if (file.fileType === 'pdf') {
|
||||
// 使用 PDF.js 组件预览
|
||||
this.pdfUrl = file.fileUrl
|
||||
this.pdfFileName = file.fileName
|
||||
this.showPdfViewer = true
|
||||
} else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.fileType)) {
|
||||
// 图片使用弹窗显示
|
||||
this.previewImageUrl = file.fileUrl
|
||||
this.previewImageName = file.fileName
|
||||
this.showImagePreview = true
|
||||
} else if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(file.fileType)) {
|
||||
// Office 文档使用微软在线预览
|
||||
const msViewerUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(file.fileUrl)}`
|
||||
window.open(msViewerUrl, '_blank')
|
||||
} else {
|
||||
// 其他文件类型,在新窗口打开
|
||||
window.open(file.fileUrl, '_blank')
|
||||
}
|
||||
return
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
// 非H5端使用下载+打开文档的方式
|
||||
uni.showLoading({
|
||||
title: '加载中...'
|
||||
})
|
||||
|
||||
uni.downloadFile({
|
||||
url: file.fileUrl,
|
||||
success: (res) => {
|
||||
uni.hideLoading()
|
||||
if (res.statusCode === 200) {
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
fileType: file.fileType,
|
||||
showMenu: true,
|
||||
success: () => {
|
||||
console.log('打开文档成功')
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('打开文档失败:', err)
|
||||
uni.showToast({
|
||||
title: '无法预览此文件',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '文件加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('下载失败:', err)
|
||||
uni.showToast({
|
||||
title: '下载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭PDF预览
|
||||
*/
|
||||
closePdfViewer() {
|
||||
this.showPdfViewer = false
|
||||
this.pdfUrl = ''
|
||||
this.pdfFileName = ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭图片预览
|
||||
*/
|
||||
closeImagePreview() {
|
||||
this.showImagePreview = false
|
||||
this.previewImageUrl = ''
|
||||
this.previewImageName = ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
downloadFile(file) {
|
||||
if (!file.fileUrl) {
|
||||
uni.showToast({
|
||||
title: '文件暂不可用',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
// H5端:创建下载链接
|
||||
const link = document.createElement('a')
|
||||
link.href = file.fileUrl
|
||||
link.download = file.fileName
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
uni.showToast({
|
||||
title: '开始下载',
|
||||
icon: 'success'
|
||||
})
|
||||
return
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
uni.showLoading({
|
||||
title: '下载中...'
|
||||
})
|
||||
|
||||
uni.downloadFile({
|
||||
url: file.fileUrl,
|
||||
success: (res) => {
|
||||
uni.hideLoading()
|
||||
if (res.statusCode === 200) {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序保存文件
|
||||
uni.saveFile({
|
||||
tempFilePath: res.tempFilePath,
|
||||
success: (saveRes) => {
|
||||
uni.showToast({
|
||||
title: '下载成功',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
// 保存失败则打开文档
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
fileType: file.fileType,
|
||||
showMenu: true
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// APP端保存到相册或文件
|
||||
uni.saveFile({
|
||||
tempFilePath: res.tempFilePath,
|
||||
success: (saveRes) => {
|
||||
uni.showToast({
|
||||
title: '下载成功',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
fileType: file.fileType,
|
||||
showMenu: true
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '下载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('下载失败:', err)
|
||||
uni.showToast({
|
||||
title: '下载失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件类型
|
||||
*/
|
||||
getFileType(fileName) {
|
||||
if (!fileName) return 'pdf'
|
||||
const ext = fileName.split('.').pop().toLowerCase()
|
||||
return ext
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件图标文字
|
||||
*/
|
||||
getFileIconText(fileType) {
|
||||
const iconMap = {
|
||||
'pdf': 'PDF',
|
||||
'doc': 'DOC',
|
||||
'docx': 'DOC',
|
||||
'xls': 'XLS',
|
||||
'xlsx': 'XLS',
|
||||
'ppt': 'PPT',
|
||||
'pptx': 'PPT',
|
||||
'txt': 'TXT',
|
||||
'jpg': 'IMG',
|
||||
'jpeg': 'IMG',
|
||||
'png': 'IMG',
|
||||
'zip': 'ZIP',
|
||||
'rar': 'RAR'
|
||||
}
|
||||
return iconMap[fileType] || 'FILE'
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B'
|
||||
if (typeof bytes === 'string') return bytes
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
if (isNaN(date.getTime())) return dateStr
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}.${month}.${day}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.attachment-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
// 赛事信息卡片
|
||||
.event-info-card {
|
||||
background: #fff;
|
||||
margin: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 24rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.event-time-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.time-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time-divider {
|
||||
width: 1rpx;
|
||||
height: 60rpx;
|
||||
background: #e0e0e0;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-top-color: #C93639;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
// 附件区域
|
||||
.attachments-section {
|
||||
margin: 20rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.attachments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
// 附件项
|
||||
.attachment-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
// 文件图标
|
||||
.file-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: #C93639;
|
||||
}
|
||||
|
||||
.file-icon.icon-pdf {
|
||||
background: #E74C3C;
|
||||
}
|
||||
|
||||
.file-icon.icon-doc,
|
||||
.file-icon.icon-docx {
|
||||
background: #3498DB;
|
||||
}
|
||||
|
||||
.file-icon.icon-xls,
|
||||
.file-icon.icon-xlsx {
|
||||
background: #27AE60;
|
||||
}
|
||||
|
||||
.file-icon.icon-ppt,
|
||||
.file-icon.icon-pptx {
|
||||
background: #E67E22;
|
||||
}
|
||||
|
||||
.file-icon.icon-jpg,
|
||||
.file-icon.icon-jpeg,
|
||||
.file-icon.icon-png {
|
||||
background: #9B59B6;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// 文件内容
|
||||
.file-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.meta-dot {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
// 操作按钮
|
||||
.file-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 30rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
background: #C93639;
|
||||
|
||||
.action-text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
background: #fff;
|
||||
border: 1rpx solid #C93639;
|
||||
|
||||
.action-text {
|
||||
color: #C93639;
|
||||
}
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 60rpx;
|
||||
}
|
||||
|
||||
.empty-image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// 图片预览弹窗样式
|
||||
.image-preview-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-preview-container {
|
||||
width: 95%;
|
||||
height: 90%;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 30rpx;
|
||||
background: #C93639;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.image-preview-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 20rpx;
|
||||
}
|
||||
|
||||
.image-preview-close {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 40rpx;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.image-preview-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -6,10 +6,10 @@
|
||||
<!-- 新增按钮 -->
|
||||
<view class="add-btn-wrapper" @click="handleAdd">
|
||||
<text class="add-icon">⊕</text>
|
||||
<text class="add-text">{{ currentTab === 0 ? '新增选手' : '新增联系人' }}</text>
|
||||
<text class="add-text">{{ addButtonText }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 选手列表 -->
|
||||
<!-- 选手列表 (Tab 0) -->
|
||||
<view class="player-list" v-if="currentTab === 0 && playerList.length > 0">
|
||||
<view class="player-item" v-for="(item, index) in playerList" :key="index">
|
||||
<view class="player-info">
|
||||
@@ -21,7 +21,7 @@
|
||||
<image class="action-icon" src="/static/images/编辑@3x.png" mode="aspectFit"></image>
|
||||
<text>编辑</text>
|
||||
</view>
|
||||
<view class="action-btn delete-btn" @click.stop="handleDelete(item)">
|
||||
<view class="action-btn delete-btn" @click.stop="handleDelete(item, 'player')">
|
||||
<image class="action-icon" src="/static/images/删除@3x.png" mode="aspectFit"></image>
|
||||
<text>删除</text>
|
||||
</view>
|
||||
@@ -29,21 +29,69 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<!-- 选手空状态 -->
|
||||
<view class="empty-state" v-if="currentTab === 0 && playerList.length === 0">
|
||||
<text class="empty-text">暂无选手信息</text>
|
||||
</view>
|
||||
|
||||
<!-- 联系人Tab内容 -->
|
||||
<view class="empty-state" v-if="currentTab === 1">
|
||||
<!-- 集体列表 (Tab 1) -->
|
||||
<view class="player-list" v-if="currentTab === 1 && teamList.length > 0">
|
||||
<view class="player-item" v-for="(item, index) in teamList" :key="index">
|
||||
<view class="player-info">
|
||||
<view class="player-name">{{ item.name }}</view>
|
||||
<view class="player-id">成员数:{{ item.memberCount || 0 }}人</view>
|
||||
</view>
|
||||
<view class="player-actions">
|
||||
<view class="action-btn edit-btn" @click.stop="handleEditTeam(item)">
|
||||
<image class="action-icon" src="/static/images/编辑@3x.png" mode="aspectFit"></image>
|
||||
<text>编辑</text>
|
||||
</view>
|
||||
<view class="action-btn delete-btn" @click.stop="handleDelete(item, 'team')">
|
||||
<image class="action-icon" src="/static/images/删除@3x.png" mode="aspectFit"></image>
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 集体空状态 -->
|
||||
<view class="empty-state" v-if="currentTab === 1 && teamList.length === 0">
|
||||
<text class="empty-text">暂无集体信息</text>
|
||||
</view>
|
||||
|
||||
<!-- 联系人列表 (Tab 2) -->
|
||||
<view class="player-list" v-if="currentTab === 2 && contactList.length > 0">
|
||||
<view class="player-item" v-for="(item, index) in contactList" :key="index">
|
||||
<view class="player-info">
|
||||
<view class="player-name">
|
||||
{{ item.name }}
|
||||
<text class="default-tag" v-if="item.isDefault">默认</text>
|
||||
</view>
|
||||
<view class="player-id">手机:{{ item.phone }}</view>
|
||||
</view>
|
||||
<view class="player-actions">
|
||||
<view class="action-btn edit-btn" @click.stop="handleEditContact(item)">
|
||||
<image class="action-icon" src="/static/images/编辑@3x.png" mode="aspectFit"></image>
|
||||
<text>编辑</text>
|
||||
</view>
|
||||
<view class="action-btn delete-btn" @click.stop="handleDelete(item, 'contact')">
|
||||
<image class="action-icon" src="/static/images/删除@3x.png" mode="aspectFit"></image>
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系人空状态 -->
|
||||
<view class="empty-state" v-if="currentTab === 2 && contactList.length === 0">
|
||||
<text class="empty-text">暂无联系人信息</text>
|
||||
</view>
|
||||
|
||||
<!-- 删除确认弹窗 -->
|
||||
<confirm-modal
|
||||
:show="showDeleteModal"
|
||||
title="删除选手"
|
||||
content="确定要删除该选手吗?"
|
||||
:title="deleteModalTitle"
|
||||
:content="deleteModalContent"
|
||||
@cancel="showDeleteModal = false"
|
||||
@confirm="confirmDelete"
|
||||
></confirm-modal>
|
||||
@@ -60,6 +108,7 @@
|
||||
import CustomTabs from '../../components/custom-tabs/custom-tabs.vue';
|
||||
import ConfirmModal from '../../components/confirm-modal/confirm-modal.vue';
|
||||
import athleteAPI from '@/api/athlete.js';
|
||||
import { getUserInfo } from '@/utils/auth.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -68,100 +117,177 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabs: ['选手', '联系人'],
|
||||
tabs: ['选手', '集体', '联系人'],
|
||||
currentTab: 0,
|
||||
playerList: [],
|
||||
teamList: [],
|
||||
contactList: [],
|
||||
showDeleteModal: false,
|
||||
showSuccessToast: false,
|
||||
currentItem: null
|
||||
currentItem: null,
|
||||
deleteType: 'player'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
addButtonText() {
|
||||
if (this.currentTab === 0) return '新增选手'
|
||||
if (this.currentTab === 1) return '新增集体'
|
||||
return '新增联系人'
|
||||
},
|
||||
deleteModalTitle() {
|
||||
if (this.deleteType === 'team') return '删除集体'
|
||||
if (this.deleteType === 'contact') return '删除联系人'
|
||||
return '删除选手'
|
||||
},
|
||||
deleteModalContent() {
|
||||
if (this.deleteType === 'team') return '确定要删除该集体吗?'
|
||||
if (this.deleteType === 'contact') return '确定要删除该联系人吗?'
|
||||
return '确定要删除该选手吗?'
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadPlayerList()
|
||||
this.loadTeamList()
|
||||
this.loadContactList()
|
||||
},
|
||||
onShow() {
|
||||
// 从新增/编辑页面返回时重新加载列表
|
||||
this.loadPlayerList()
|
||||
this.loadTeamList()
|
||||
this.loadContactList()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载选手列表
|
||||
*/
|
||||
async loadPlayerList() {
|
||||
try {
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo || !userInfo.userId) return
|
||||
|
||||
const res = await athleteAPI.getAthleteList({
|
||||
current: 1,
|
||||
size: 100
|
||||
size: 100,
|
||||
createUser: userInfo.userId
|
||||
})
|
||||
|
||||
let list = []
|
||||
if (res.records) {
|
||||
list = res.records
|
||||
} else if (Array.isArray(res)) {
|
||||
list = res
|
||||
}
|
||||
|
||||
// 数据映射
|
||||
let list = res.records || (Array.isArray(res) ? res : [])
|
||||
this.playerList = list.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
name: item.name || item.playerName,
|
||||
idCard: item.idCard || item.idCardNumber,
|
||||
gender: item.gender,
|
||||
team: item.team,
|
||||
phone: item.phone
|
||||
team: item.team || item.teamName,
|
||||
phone: item.phone || item.contactPhone
|
||||
}))
|
||||
} catch (err) {
|
||||
console.error('加载选手列表失败:', err)
|
||||
}
|
||||
},
|
||||
|
||||
async loadTeamList() {
|
||||
try {
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo || !userInfo.userId) return
|
||||
|
||||
if (athleteAPI.getTeamList) {
|
||||
const res = await athleteAPI.getTeamList({
|
||||
current: 1,
|
||||
size: 100,
|
||||
createUser: userInfo.userId
|
||||
})
|
||||
|
||||
let list = res.records || (Array.isArray(res) ? res : [])
|
||||
this.teamList = list.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name || item.teamName,
|
||||
memberCount: item.memberCount || item.members?.length || 0
|
||||
}))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载集体列表失败:', err)
|
||||
this.teamList = []
|
||||
}
|
||||
},
|
||||
|
||||
async loadContactList() {
|
||||
try {
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo || !userInfo.userId) return
|
||||
|
||||
if (athleteAPI.getContactList) {
|
||||
const res = await athleteAPI.getContactList({
|
||||
current: 1,
|
||||
size: 100,
|
||||
createUser: userInfo.userId
|
||||
})
|
||||
|
||||
let list = res.records || (Array.isArray(res) ? res : [])
|
||||
this.contactList = list.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
phone: item.phone,
|
||||
idCard: item.idCard,
|
||||
email: item.email,
|
||||
address: item.address,
|
||||
isDefault: item.isDefault
|
||||
}))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载联系人列表失败:', err)
|
||||
this.contactList = []
|
||||
}
|
||||
},
|
||||
|
||||
handleTabChange(index) {
|
||||
this.currentTab = index;
|
||||
},
|
||||
handleAdd() {
|
||||
if (this.currentTab === 0) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/add-player/add-player'
|
||||
});
|
||||
uni.navigateTo({ url: '/pages/add-player/add-player' });
|
||||
} else if (this.currentTab === 1) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/add-contact/add-contact'
|
||||
});
|
||||
uni.navigateTo({ url: '/pages/add-team/add-team' });
|
||||
} else if (this.currentTab === 2) {
|
||||
uni.navigateTo({ url: '/pages/add-contact/add-contact' });
|
||||
}
|
||||
},
|
||||
handleEdit(item) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/edit-player/edit-player?id=' + item.id
|
||||
});
|
||||
uni.navigateTo({ url: '/pages/edit-player/edit-player?id=' + item.id });
|
||||
},
|
||||
handleDelete(item) {
|
||||
handleEditTeam(item) {
|
||||
uni.navigateTo({ url: '/pages/edit-team/edit-team?id=' + item.id });
|
||||
},
|
||||
handleEditContact(item) {
|
||||
uni.navigateTo({ url: '/pages/edit-contact/edit-contact?id=' + item.id });
|
||||
},
|
||||
handleDelete(item, type) {
|
||||
this.currentItem = item;
|
||||
this.deleteType = type;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
async confirmDelete() {
|
||||
this.showDeleteModal = false;
|
||||
|
||||
try {
|
||||
// 调用删除API
|
||||
await athleteAPI.removeAthlete(this.currentItem.id)
|
||||
|
||||
// 从列表中移除
|
||||
const index = this.playerList.findIndex(item => item.id === this.currentItem.id);
|
||||
if (index > -1) {
|
||||
this.playerList.splice(index, 1);
|
||||
if (this.deleteType === 'team') {
|
||||
if (athleteAPI.removeTeam) {
|
||||
await athleteAPI.removeTeam(this.currentItem.id)
|
||||
}
|
||||
const index = this.teamList.findIndex(item => item.id === this.currentItem.id);
|
||||
if (index > -1) this.teamList.splice(index, 1);
|
||||
} else if (this.deleteType === 'contact') {
|
||||
if (athleteAPI.removeContact) {
|
||||
await athleteAPI.removeContact(this.currentItem.id)
|
||||
}
|
||||
const index = this.contactList.findIndex(item => item.id === this.currentItem.id);
|
||||
if (index > -1) this.contactList.splice(index, 1);
|
||||
} else {
|
||||
await athleteAPI.removeAthlete(this.currentItem.id)
|
||||
const index = this.playerList.findIndex(item => item.id === this.currentItem.id);
|
||||
if (index > -1) this.playerList.splice(index, 1);
|
||||
}
|
||||
|
||||
// 显示成功提示
|
||||
this.showSuccessToast = true;
|
||||
setTimeout(() => {
|
||||
this.showSuccessToast = false;
|
||||
}, 2000);
|
||||
setTimeout(() => { this.showSuccessToast = false; }, 2000);
|
||||
} catch (err) {
|
||||
console.error('删除选手失败:', err)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
console.error('删除失败:', err)
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,6 +340,18 @@ export default {
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.default-tag {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
background-color: #C93639;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.player-id {
|
||||
@@ -247,10 +385,6 @@ export default {
|
||||
border-color: #C93639;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
|
||||
249
src/pages/edit-contact/edit-contact.vue
Normal file
249
src/pages/edit-contact/edit-contact.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<view class="add-contact-page">
|
||||
<view class="form-container">
|
||||
<view class="form-item" @click="showIdTypePicker = true">
|
||||
<view class="form-label">证件类型</view>
|
||||
<view class="form-value">
|
||||
<text>{{ formData.idType }}</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">姓名</view>
|
||||
<view class="form-value">
|
||||
<input class="form-input" v-model="formData.name" placeholder="请输入姓名" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">证件号码</view>
|
||||
<view class="form-value">
|
||||
<input class="form-input" v-model="formData.idCard" placeholder="请输入身份证号码" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">手机号码</view>
|
||||
<view class="form-value">
|
||||
<input class="form-input" v-model="formData.phone" placeholder="请输入手机号码" placeholder-class="placeholder" type="number" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">邮箱</view>
|
||||
<view class="form-value">
|
||||
<input class="form-input" v-model="formData.email" placeholder="请输入邮箱" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">地址</view>
|
||||
<view class="form-value">
|
||||
<input class="form-input" v-model="formData.address" placeholder="请输入地址" placeholder-class="placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item switch-item">
|
||||
<view class="form-label">设置为默认联系人</view>
|
||||
<view class="form-value">
|
||||
<switch :checked="formData.isDefault" @change="handleSwitchChange" color="#C93639" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="toast-message" v-if="showToast">
|
||||
<text class="toast-text">{{ toastMessage }}</text>
|
||||
</view>
|
||||
|
||||
<view class="btn-wrapper">
|
||||
<view class="btn save-btn" :class="{ disabled: !isFormValid || saving }" @click="handleSave">
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import athleteAPI from '@/api/athlete.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
contactId: null,
|
||||
formData: {
|
||||
idType: '身份证',
|
||||
name: '',
|
||||
idCard: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
isDefault: false
|
||||
},
|
||||
showToast: false,
|
||||
toastMessage: '',
|
||||
saving: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isFormValid() {
|
||||
return this.formData.name && this.formData.idCard && this.formData.phone &&
|
||||
this.validateIdCard(this.formData.idCard) && this.validatePhone(this.formData.phone);
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.id) {
|
||||
this.contactId = options.id;
|
||||
this.loadContactDetail();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateIdCard(idCard) {
|
||||
return /^\d{17}[\dXx]$/.test(idCard);
|
||||
},
|
||||
validatePhone(phone) {
|
||||
return /^1\d{10}$/.test(phone);
|
||||
},
|
||||
handleSwitchChange(e) {
|
||||
this.formData.isDefault = e.detail.value;
|
||||
},
|
||||
showToastMsg(msg) {
|
||||
this.toastMessage = msg;
|
||||
this.showToast = true;
|
||||
setTimeout(() => { this.showToast = false; }, 2000);
|
||||
},
|
||||
async loadContactDetail() {
|
||||
try {
|
||||
const res = await athleteAPI.getContactDetail(this.contactId);
|
||||
if (res) {
|
||||
this.formData = {
|
||||
idType: res.idType || '身份证',
|
||||
name: res.name || '',
|
||||
idCard: res.idCard || '',
|
||||
phone: res.phone || '',
|
||||
email: res.email || '',
|
||||
address: res.address || '',
|
||||
isDefault: res.isDefault || false
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载联系人详情失败:', err);
|
||||
this.showToastMsg('加载失败');
|
||||
}
|
||||
},
|
||||
async handleSave() {
|
||||
if (!this.isFormValid || this.saving) return;
|
||||
|
||||
if (!this.validatePhone(this.formData.phone)) {
|
||||
this.showToastMsg('手机号码格式不正确');
|
||||
return;
|
||||
}
|
||||
if (!this.validateIdCard(this.formData.idCard)) {
|
||||
this.showToastMsg('身份证号码格式不正确');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const data = { ...this.formData, id: this.contactId };
|
||||
await athleteAPI.saveContact(data);
|
||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||
setTimeout(() => { uni.navigateBack(); }, 1500);
|
||||
} catch (err) {
|
||||
console.error('保存联系人失败:', err);
|
||||
this.showToastMsg('保存失败,请重试');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.add-contact-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 200rpx;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: #fff;
|
||||
margin: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 35rpx 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.form-item:last-child { border-bottom: none; }
|
||||
.switch-item { padding: 30rpx; }
|
||||
|
||||
.form-label {
|
||||
width: 180rpx;
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.form-value {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.placeholder { color: #cccccc; }
|
||||
.arrow { font-size: 40rpx; color: #cccccc; margin-left: 10rpx; }
|
||||
|
||||
.toast-message {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
padding: 30rpx 50rpx;
|
||||
border-radius: 16rpx;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.toast-text { font-size: 28rpx; }
|
||||
|
||||
.btn-wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 30rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background-color: #C93639;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.save-btn.disabled {
|
||||
background-color: rgba(201, 54, 57, 0.5);
|
||||
}
|
||||
</style>
|
||||
401
src/pages/edit-team/edit-team.vue
Normal file
401
src/pages/edit-team/edit-team.vue
Normal file
@@ -0,0 +1,401 @@
|
||||
<template>
|
||||
<view class="add-team-page">
|
||||
<view class="form-section">
|
||||
<view class="form-item">
|
||||
<view class="form-label required">集体名称</view>
|
||||
<input
|
||||
class="form-input"
|
||||
type="text"
|
||||
v-model="formData.name"
|
||||
placeholder="请输入集体名称"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">备注</view>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="formData.remark"
|
||||
placeholder="请输入备注信息"
|
||||
></textarea>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成员列表 -->
|
||||
<view class="member-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">成员列表</text>
|
||||
<view class="add-member-btn" @click="showMemberPicker = true">
|
||||
<text>+ 添加成员</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="member-list" v-if="formData.members.length > 0">
|
||||
<view class="member-item" v-for="(member, index) in formData.members" :key="index">
|
||||
<view class="member-info">
|
||||
<text class="member-name">{{ member.name }}</text>
|
||||
</view>
|
||||
<view class="remove-btn" @click="removeMember(index)">移除</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-members" v-else>
|
||||
<text>暂无成员,请添加</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-btn-wrapper">
|
||||
<view class="submit-btn" @click="handleSubmit">保存</view>
|
||||
</view>
|
||||
|
||||
<!-- 成员选择弹窗 -->
|
||||
<view class="member-picker-mask" v-if="showMemberPicker" @click="showMemberPicker = false">
|
||||
<view class="member-picker" @click.stop>
|
||||
<view class="picker-header">
|
||||
<text class="picker-title">选择成员</text>
|
||||
<text class="picker-close" @click="showMemberPicker = false">×</text>
|
||||
</view>
|
||||
<view class="picker-list">
|
||||
<view
|
||||
class="picker-item"
|
||||
v-for="(player, index) in availablePlayers"
|
||||
:key="index"
|
||||
@click="addMember(player)"
|
||||
>
|
||||
<text>{{ player.name }}</text>
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
<view class="empty-picker" v-if="availablePlayers.length === 0">
|
||||
<text>暂无可选成员</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import athleteAPI from '@/api/athlete.js';
|
||||
import { getUserInfo } from '@/utils/auth.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
teamId: null,
|
||||
formData: {
|
||||
name: '',
|
||||
remark: '',
|
||||
members: []
|
||||
},
|
||||
playerList: [],
|
||||
showMemberPicker: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
availablePlayers() {
|
||||
const memberIds = this.formData.members.map(m => m.id)
|
||||
return this.playerList.filter(p => !memberIds.includes(p.id))
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.id) {
|
||||
this.teamId = options.id
|
||||
this.loadTeamDetail()
|
||||
}
|
||||
this.loadPlayerList()
|
||||
},
|
||||
methods: {
|
||||
async loadTeamDetail() {
|
||||
try {
|
||||
const res = await athleteAPI.getTeamDetail(this.teamId)
|
||||
if (res) {
|
||||
this.formData.name = res.teamName || res.name || ''
|
||||
this.formData.remark = res.remark || ''
|
||||
if (res.members && Array.isArray(res.members)) {
|
||||
this.formData.members = res.members.map(m => ({
|
||||
id: m.id,
|
||||
name: m.name || m.playerName
|
||||
}))
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载集体详情失败:', err)
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async loadPlayerList() {
|
||||
try {
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo || !userInfo.userId) return
|
||||
|
||||
const res = await athleteAPI.getAthleteList({
|
||||
current: 1,
|
||||
size: 100,
|
||||
createUser: userInfo.userId
|
||||
})
|
||||
|
||||
let list = []
|
||||
if (res.records) {
|
||||
list = res.records
|
||||
} else if (Array.isArray(res)) {
|
||||
list = res
|
||||
}
|
||||
|
||||
this.playerList = list.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name || item.playerName
|
||||
}))
|
||||
} catch (err) {
|
||||
console.error('加载选手列表失败:', err)
|
||||
}
|
||||
},
|
||||
|
||||
addMember(player) {
|
||||
this.formData.members.push({
|
||||
id: player.id,
|
||||
name: player.name
|
||||
})
|
||||
this.showMemberPicker = false
|
||||
},
|
||||
|
||||
removeMember(index) {
|
||||
this.formData.members.splice(index, 1)
|
||||
},
|
||||
|
||||
async handleSubmit() {
|
||||
if (!this.formData.name) {
|
||||
uni.showToast({
|
||||
title: '请输入集体名称',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (this.formData.members.length === 0) {
|
||||
uni.showToast({
|
||||
title: '请添加至少一名成员',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = getUserInfo()
|
||||
|
||||
const data = {
|
||||
teamId: this.teamId ? String(this.teamId) : null,
|
||||
teamName: this.formData.name,
|
||||
remark: this.formData.remark,
|
||||
memberIds: this.formData.members.map(m => String(m.id)),
|
||||
createUser: userInfo?.userId
|
||||
}
|
||||
|
||||
console.log('Submitting team data:', JSON.stringify(data))
|
||||
await athleteAPI.saveTeam(data)
|
||||
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
console.error('保存失败:', err)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.add-team-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 150rpx;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.form-label.required::before {
|
||||
content: '*';
|
||||
color: #C93639;
|
||||
margin-right: 5rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.member-section {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.add-member-btn {
|
||||
color: #C93639;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
color: #C93639;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.empty-members {
|
||||
text-align: center;
|
||||
padding: 50rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.submit-btn-wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 30rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #C93639;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.member-picker-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.member-picker {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.picker-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.picker-close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.picker-list {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.picker-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
color: #C93639;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.empty-picker {
|
||||
text-align: center;
|
||||
padding: 50rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -125,7 +125,7 @@ export default {
|
||||
res.registerTime || res.registrationPeriod || '待定',
|
||||
matchTime: this.formatTimeRange(startTime, endTime) ||
|
||||
res.matchTime || res.competitionTime || '待定',
|
||||
registerCount: res.registrationCount || res.registerCount || res.signUpCount || '0',
|
||||
registerCount: res.registrationCount || res.registerCount || res.signUpCount || res.totalParticipants || '0',
|
||||
status: this.getStatus(res.status)
|
||||
}
|
||||
|
||||
@@ -169,16 +169,23 @@ export default {
|
||||
},
|
||||
|
||||
handleFunction(type) {
|
||||
// 需要跳转到附件展示页面的类型
|
||||
const attachmentTypes = ['info', 'rules', 'schedule', 'score', 'awards', 'photos']
|
||||
|
||||
if (attachmentTypes.includes(type)) {
|
||||
// 跳转到通用附件展示页面
|
||||
const name = encodeURIComponent(this.eventInfo.title)
|
||||
uni.navigateTo({
|
||||
url: `/pages/attachment-view/attachment-view?type=${type}&competitionId=${this.eventId}&name=${name}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 其他功能页面的路由映射
|
||||
const routeMap = {
|
||||
'info': '/pages/event-info/event-info',
|
||||
'rules': '/pages/event-rules/event-rules',
|
||||
'schedule': '/pages/event-schedule/event-schedule',
|
||||
'players': '/pages/event-players/event-players',
|
||||
'match': '/pages/event-live/event-live',
|
||||
'lineup': '/pages/event-lineup/event-lineup',
|
||||
'score': '/pages/event-score/event-score',
|
||||
'awards': '/pages/event-medals/event-medals',
|
||||
'photos': '' // 图片直播暂未实现
|
||||
'lineup': '/pages/event-lineup/event-lineup'
|
||||
};
|
||||
|
||||
const url = routeMap[type];
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
<template>
|
||||
<view class="event-info-page">
|
||||
<!-- 附件下载区 -->
|
||||
<view class="attachments-section" v-if="attachments.length > 0">
|
||||
<view class="section-title">
|
||||
<text class="title-icon">📎</text>
|
||||
<text class="title-text">相关附件</text>
|
||||
</view>
|
||||
<view class="attachments-list">
|
||||
<view class="attachment-item" v-for="(file, index) in attachments" :key="index" @click="downloadFile(file)">
|
||||
<view class="file-info">
|
||||
<text class="file-icon">{{ getFileIcon(file.fileType) }}</text>
|
||||
<view class="file-details">
|
||||
<text class="file-name">{{ file.fileName }}</text>
|
||||
<text class="file-size">{{ file.fileSize }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="download-btn">
|
||||
<text class="download-icon">⬇</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 信息列表 -->
|
||||
<view class="info-list">
|
||||
<view class="info-item" v-for="(item, index) in infoList" :key="index" @click="handleItemClick(item)">
|
||||
@@ -13,7 +35,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="infoList.length === 0">
|
||||
<view class="empty-state" v-if="infoList.length === 0 && attachments.length === 0">
|
||||
<text class="empty-text">暂无信息发布</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -21,21 +43,57 @@
|
||||
|
||||
<script>
|
||||
import infoAPI from '@/api/info.js'
|
||||
import competitionAPI from '@/api/competition.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
eventId: '',
|
||||
infoList: []
|
||||
infoList: [],
|
||||
attachments: []
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.eventId) {
|
||||
this.eventId = options.eventId
|
||||
this.loadInfoList(options.eventId)
|
||||
this.loadAttachments(options.eventId)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载附件列表
|
||||
*/
|
||||
async loadAttachments(eventId) {
|
||||
try {
|
||||
const res = await competitionAPI.getAttachments({
|
||||
competitionId: eventId,
|
||||
type: 'info'
|
||||
})
|
||||
|
||||
let list = []
|
||||
if (res && Array.isArray(res)) {
|
||||
list = res
|
||||
} else if (res && res.records) {
|
||||
list = res.records
|
||||
} else if (res && res.data && Array.isArray(res.data)) {
|
||||
list = res.data
|
||||
}
|
||||
|
||||
if (list.length > 0) {
|
||||
this.attachments = list.map(file => ({
|
||||
id: file.id,
|
||||
fileName: file.fileName || file.name,
|
||||
fileUrl: file.fileUrl || file.url,
|
||||
fileSize: this.formatFileSize(file.fileSize || file.size),
|
||||
fileType: file.fileType || this.getFileType(file.fileName || file.name)
|
||||
}))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载附件失败:', err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载信息发布列表
|
||||
*/
|
||||
@@ -50,11 +108,6 @@ export default {
|
||||
list = res
|
||||
}
|
||||
|
||||
// 如果后端没有数据,使用模拟数据
|
||||
if (list.length === 0) {
|
||||
list = this.getMockData()
|
||||
}
|
||||
|
||||
// 数据映射
|
||||
this.infoList = list.map(item => ({
|
||||
id: item.id,
|
||||
@@ -66,81 +119,100 @@ export default {
|
||||
}))
|
||||
} catch (err) {
|
||||
console.error('加载信息列表失败:', err)
|
||||
// 加载失败时使用模拟数据
|
||||
const list = this.getMockData()
|
||||
this.infoList = list.map(item => ({
|
||||
id: item.id,
|
||||
type: this.getInfoType(item.info_type || item.type),
|
||||
typeText: this.getInfoTypeText(item.info_type || item.type),
|
||||
title: item.title,
|
||||
desc: item.content,
|
||||
time: this.formatTime(item.publishTime)
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取模拟数据
|
||||
* 下载文件
|
||||
*/
|
||||
getMockData() {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
info_type: 3,
|
||||
title: '重要通知:赛事报名截止时间变更',
|
||||
content: '由于场馆调整,本次赛事报名截止时间延长至2025年12月20日,请各位选手抓紧时间报名。如有疑问,请联系赛事组委会。',
|
||||
publishTime: '2025-01-10 09:00:00'
|
||||
downloadFile(file) {
|
||||
const fileExt = file.fileType || this.getFileType(file.fileName) || 'pdf'
|
||||
|
||||
// #ifdef H5
|
||||
const link = document.createElement('a')
|
||||
link.href = file.fileUrl
|
||||
link.download = file.fileName
|
||||
link.target = '_blank'
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
uni.showToast({
|
||||
title: '开始下载',
|
||||
icon: 'success'
|
||||
})
|
||||
return
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
uni.showLoading({ title: '准备下载' })
|
||||
uni.downloadFile({
|
||||
url: file.fileUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
fileType: fileExt,
|
||||
success: () => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '打开成功', icon: 'success' })
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('打开文件失败:', err)
|
||||
uni.showToast({ title: '打开失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
info_type: 1,
|
||||
title: '参赛选手须知',
|
||||
content: '请各位参赛选手提前1小时到达比赛场地进行检录,携带身份证原件及复印件。比赛期间请遵守赛场纪律,服从裁判判决。',
|
||||
publishTime: '2025-01-09 14:30:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
info_type: 2,
|
||||
title: '比赛场地及交通指引',
|
||||
content: '本次赛事在市体育中心举行,地址:XX市XX区XX路100号。可乘坐地铁2号线至体育中心站下车,或乘坐公交车88路、99路至体育中心站。场馆提供免费停车位。',
|
||||
publishTime: '2025-01-08 16:00:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
info_type: 1,
|
||||
title: '赛前训练安排通知',
|
||||
content: '为方便各位选手熟悉场地,组委会安排在比赛前一天(12月24日)下午14:00-17:00开放场地供选手训练。请需要训练的选手提前联系组委会预约。',
|
||||
publishTime: '2025-01-07 10:20:00'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
info_type: 2,
|
||||
title: '比赛流程及注意事项',
|
||||
content: '比赛采用淘汰赛制,分为预赛、半决赛和决赛三个阶段。每场比赛时长为5分钟,选手需提前做好热身准备。比赛过程中严禁使用违禁器材。',
|
||||
publishTime: '2025-01-06 11:45:00'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
info_type: 1,
|
||||
title: '医疗保障及安全提示',
|
||||
content: '赛事现场配备专业医疗团队和救护车,设有医疗服务点。建议选手自备常用药品,如有特殊疾病请提前告知组委会。比赛前请充分热身,避免受伤。',
|
||||
publishTime: '2025-01-05 15:10:00'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
info_type: 3,
|
||||
title: '关于赛事直播安排的通知',
|
||||
content: '本次赛事将进行全程网络直播,届时可通过官方网站和APP观看。精彩瞬间将在赛后剪辑发布,敬请期待!',
|
||||
publishTime: '2025-01-04 13:00:00'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
info_type: 2,
|
||||
title: '志愿者招募公告',
|
||||
content: '赛事组委会现招募志愿者50名,负责现场引导、秩序维护、后勤保障等工作。有意者请扫描海报二维码报名,报名截止时间为12月15日。',
|
||||
publishTime: '2025-01-03 09:30:00'
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('下载失败:', err)
|
||||
uni.showToast({ title: '下载失败', icon: 'none' })
|
||||
}
|
||||
]
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件类型
|
||||
*/
|
||||
getFileType(fileName) {
|
||||
if (!fileName) return 'pdf'
|
||||
const ext = fileName.split('.').pop().toLowerCase()
|
||||
return ext
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件图标
|
||||
*/
|
||||
getFileIcon(fileType) {
|
||||
const iconMap = {
|
||||
'pdf': '📕',
|
||||
'doc': '📘',
|
||||
'docx': '📘',
|
||||
'xls': '📗',
|
||||
'xlsx': '📗',
|
||||
'ppt': '📙',
|
||||
'pptx': '📙',
|
||||
'txt': '📄',
|
||||
'zip': '📦',
|
||||
'rar': '📦'
|
||||
}
|
||||
return iconMap[fileType] || '📄'
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B'
|
||||
if (typeof bytes === 'string') return bytes
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -190,7 +262,6 @@ export default {
|
||||
},
|
||||
|
||||
handleItemClick(item) {
|
||||
// 跳转到信息详情页
|
||||
uni.navigateTo({
|
||||
url: `/pages/event-info-detail/event-info-detail?id=${item.id}&type=${item.type}&typeText=${encodeURIComponent(item.typeText)}&title=${encodeURIComponent(item.title)}&content=${encodeURIComponent(item.desc)}&time=${encodeURIComponent(item.time)}`
|
||||
})
|
||||
@@ -206,6 +277,103 @@ export default {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
// 区块标题
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
// 附件下载区
|
||||
.attachments-section {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.attachments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 25rpx 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
background-color: #f8f8f8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 48rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
background-color: #C93639;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.download-icon {
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,135 +1,127 @@
|
||||
<template>
|
||||
<view class="event-lineup-page">
|
||||
<!-- 组别选择 -->
|
||||
<view class="group-tabs">
|
||||
<view
|
||||
class="group-tab"
|
||||
v-for="(group, index) in groups"
|
||||
:key="index"
|
||||
:class="{ active: currentGroup === index }"
|
||||
@click="currentGroup = index"
|
||||
>
|
||||
{{ group }}
|
||||
</view>
|
||||
<view v-if="loading" class="loading-container">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 出场顺序列表 -->
|
||||
<view class="lineup-list">
|
||||
<view class="lineup-item" v-for="(item, index) in currentLineup" :key="index">
|
||||
<view class="lineup-order">
|
||||
<view class="order-number">{{ item.order }}</view>
|
||||
<text class="order-text">号</text>
|
||||
</view>
|
||||
<view class="lineup-info">
|
||||
<view class="lineup-name">{{ item.name }}</view>
|
||||
<view class="lineup-detail">
|
||||
<text class="detail-item">{{ item.team }}</text>
|
||||
<text class="divider">|</text>
|
||||
<text class="detail-item">{{ item.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="lineup-status" :class="item.status">
|
||||
{{ item.statusText }}
|
||||
<view v-else-if="!isScheduleCompleted" class="not-ready">
|
||||
<view class="not-ready-icon">📋</view>
|
||||
<text class="not-ready-text">赛程编排尚未完成</text>
|
||||
<text class="not-ready-hint">请等待赛事组织方完成编排后查看</text>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<!-- 组别选择 -->
|
||||
<view class="group-tabs">
|
||||
<view
|
||||
class="group-tab"
|
||||
v-for="(group, index) in groups"
|
||||
:key="index"
|
||||
:class="{ active: currentGroupIndex === index }"
|
||||
@click="currentGroupIndex = index"
|
||||
>
|
||||
{{ group.groupName }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 出场顺序列表 -->
|
||||
<view class="lineup-list" v-if="currentGroup">
|
||||
<view class="group-header">
|
||||
<text class="group-venue">{{ currentGroup.venueName }}</text>
|
||||
<text class="group-time">{{ currentGroup.timeSlot }}</text>
|
||||
<text class="group-table">表号: {{ currentGroup.tableNo }}</text>
|
||||
</view>
|
||||
|
||||
<view class="lineup-item" v-for="(item, index) in currentGroup.participants" :key="index">
|
||||
<view class="lineup-order">
|
||||
<view class="order-number">{{ item.order }}</view>
|
||||
<text class="order-text">号</text>
|
||||
</view>
|
||||
<view class="lineup-info">
|
||||
<view class="lineup-name">{{ item.playerName }}</view>
|
||||
<view class="lineup-detail">
|
||||
<text class="detail-item">{{ item.organization }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="lineup-status" :class="item.status">
|
||||
{{ getStatusText(item.status) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!currentGroup.participants || currentGroup.participants.length === 0" class="empty-list">
|
||||
<text>暂无参赛者信息</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getScheduleStatus, getLineup } from '@/api/schedule.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentGroup: 0,
|
||||
groups: ['男子A组', '男子B组', '女子A组'],
|
||||
lineups: {
|
||||
0: [
|
||||
{
|
||||
order: 1,
|
||||
name: '张三',
|
||||
team: '北京队',
|
||||
time: '09:00',
|
||||
status: 'finished',
|
||||
statusText: '已完成'
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
name: '李四',
|
||||
team: '上海队',
|
||||
time: '09:15',
|
||||
status: 'finished',
|
||||
statusText: '已完成'
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
name: '王五',
|
||||
team: '广东队',
|
||||
time: '09:30',
|
||||
status: 'ongoing',
|
||||
statusText: '进行中'
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
name: '赵六',
|
||||
team: '天津队',
|
||||
time: '09:45',
|
||||
status: 'waiting',
|
||||
statusText: '待出场'
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
name: '刘七',
|
||||
team: '江苏队',
|
||||
time: '10:00',
|
||||
status: 'waiting',
|
||||
statusText: '待出场'
|
||||
}
|
||||
],
|
||||
1: [
|
||||
{
|
||||
order: 1,
|
||||
name: '孙八',
|
||||
team: '浙江队',
|
||||
time: '10:30',
|
||||
status: 'waiting',
|
||||
statusText: '待出场'
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
name: '周九',
|
||||
team: '湖北队',
|
||||
time: '10:45',
|
||||
status: 'waiting',
|
||||
statusText: '待出场'
|
||||
}
|
||||
],
|
||||
2: [
|
||||
{
|
||||
order: 1,
|
||||
name: '小红',
|
||||
team: '四川队',
|
||||
time: '14:00',
|
||||
status: 'waiting',
|
||||
statusText: '待出场'
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
name: '小芳',
|
||||
team: '河南队',
|
||||
time: '14:15',
|
||||
status: 'waiting',
|
||||
statusText: '待出场'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
eventId: '',
|
||||
loading: true,
|
||||
isScheduleCompleted: false,
|
||||
currentGroupIndex: 0,
|
||||
groups: []
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentLineup() {
|
||||
return this.lineups[this.currentGroup] || [];
|
||||
currentGroup() {
|
||||
return this.groups[this.currentGroupIndex] || null
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
if (options.eventId) {
|
||||
this.eventId = options.eventId
|
||||
this.checkScheduleStatus()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async checkScheduleStatus() {
|
||||
try {
|
||||
const res = await getScheduleStatus(this.eventId)
|
||||
if (res.data) {
|
||||
this.isScheduleCompleted = res.data.isCompleted
|
||||
if (this.isScheduleCompleted) {
|
||||
await this.loadLineup()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查编排状态失败:', error)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async loadLineup() {
|
||||
try {
|
||||
const res = await getLineup(this.eventId)
|
||||
if (res.data && res.data.groups) {
|
||||
this.groups = res.data.groups
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载出场顺序失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
getStatusText(status) {
|
||||
const map = {
|
||||
'waiting': '待出场',
|
||||
'ongoing': '进行中',
|
||||
'finished': '已完成'
|
||||
}
|
||||
return map[status] || '待出场'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -138,6 +130,38 @@ export default {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 300rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.not-ready {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 40rpx;
|
||||
}
|
||||
|
||||
.not-ready-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.not-ready-text {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.not-ready-hint {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.group-tabs {
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
@@ -166,6 +190,21 @@ export default {
|
||||
padding: 0 30rpx 20rpx;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.group-venue, .group-time, .group-table {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.lineup-item {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
@@ -216,10 +255,6 @@ export default {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 0 10rpx;
|
||||
}
|
||||
|
||||
.lineup-status {
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 8rpx;
|
||||
@@ -241,4 +276,11 @@ export default {
|
||||
background-color: #F5F5F5;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
text-align: center;
|
||||
padding: 60rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -228,7 +228,7 @@ export default {
|
||||
item.registerTime || item.registrationPeriod || '待定',
|
||||
matchTime: this.formatTimeRange(startTime, endTime) ||
|
||||
item.matchTime || item.competitionTime || '待定',
|
||||
registerCount: item.registrationCount || item.registerCount || item.signUpCount || '0',
|
||||
registerCount: item.registrationCount || item.registerCount || item.signUpCount || item.totalParticipants || '0',
|
||||
status: this.getStatus(item.status)
|
||||
}
|
||||
})
|
||||
|
||||
43
src/pages/event-photos/event-photos.vue
Normal file
43
src/pages/event-photos/event-photos.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<view class="event-photos-page">
|
||||
<!-- 页面内容由 attachment-view 处理 -->
|
||||
<!-- 此页面作为备用,实际跳转到 attachment-view -->
|
||||
<view class="redirect-notice">
|
||||
<text>正在跳转...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
eventId: ''
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 重定向到通用附件页面
|
||||
if (options.eventId || options.competitionId) {
|
||||
const id = options.eventId || options.competitionId
|
||||
uni.redirectTo({
|
||||
url: `/pages/attachment-view/attachment-view?type=photos&competitionId=${id}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.event-photos-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.redirect-notice {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -133,23 +133,24 @@ export default {
|
||||
}
|
||||
|
||||
const res = await athleteAPI.getAthleteList(params)
|
||||
|
||||
console.log('=== 选手列表响应 ===', res)
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
const records = res.data.records || []
|
||||
// request.js already extracts data.data, so res is the actual data
|
||||
const records = res.records || res || []
|
||||
|
||||
if (isLoadMore) {
|
||||
this.playersList = [...this.playersList, ...records]
|
||||
} else {
|
||||
this.playersList = records
|
||||
this.page = 1
|
||||
}
|
||||
|
||||
this.totalCount = res.data.total || 0
|
||||
this.hasMore = this.playersList.length < this.totalCount
|
||||
|
||||
// 统计已确认人数
|
||||
this.confirmedCount = this.playersList.filter(p => p.registrationStatus === 1).length
|
||||
if (isLoadMore) {
|
||||
this.playersList = [...this.playersList, ...records]
|
||||
} else {
|
||||
this.playersList = Array.isArray(records) ? records : []
|
||||
this.page = 1
|
||||
}
|
||||
|
||||
this.totalCount = res.total || this.playersList.length
|
||||
this.hasMore = this.playersList.length < this.totalCount
|
||||
|
||||
// 统计已确认人数
|
||||
this.confirmedCount = this.playersList.filter(p => p.registrationStatus === 1).length
|
||||
} catch (error) {
|
||||
console.error('加载选手列表失败:', error)
|
||||
uni.showToast({
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<view class="steps-indicator">
|
||||
<view class="step-item" :class="{ active: currentStep >= 1 }">
|
||||
<image class="step-icon-img" :src="currentStep >= 1 ? '/static/images/选择选手信息@3x.png' : '/static/images/选择选手信息@3x.png'" mode="aspectFit"></image>
|
||||
<text class="step-text">选择选手信息</text>
|
||||
<text class="step-text">{{ isTeamProject ? '选择集体' : '选择选手信息' }}</text>
|
||||
</view>
|
||||
<view class="step-line" :class="{ active: currentStep >= 2 }"></view>
|
||||
<view class="step-item" :class="{ active: currentStep >= 2 }">
|
||||
@@ -18,37 +18,73 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 步骤1:选择选手信息 -->
|
||||
<!-- 步骤1:选择选手/集体 -->
|
||||
<view class="step-content" v-if="currentStep === 1">
|
||||
<view class="selected-count">已选:<text class="count">{{ selectedCount }}</text> 人</view>
|
||||
<!-- 个人项目:选择选手 -->
|
||||
<template v-if="!isTeamProject">
|
||||
<view class="selected-count">已选:<text class="count">{{ selectedCount }}</text> 人</view>
|
||||
|
||||
<view class="add-player-btn" @click="goToAddPlayer">
|
||||
<text class="add-icon">⊕</text>
|
||||
<text>新增选手</text>
|
||||
</view>
|
||||
<view class="add-player-btn" @click="goToAddPlayer">
|
||||
<text class="add-icon">⊕</text>
|
||||
<text>新增选手</text>
|
||||
</view>
|
||||
|
||||
<view class="player-list">
|
||||
<view class="player-item" v-for="(item, index) in playerList" :key="index">
|
||||
<view class="player-checkbox" @click="togglePlayer(item)">
|
||||
<image v-if="item.selected" class="checkbox-img" src="/static/images/选中@3x.png" mode="aspectFit"></image>
|
||||
<image v-else class="checkbox-img" src="/static/images/未选中@3x.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="player-info">
|
||||
<view class="player-name">{{ item.name }}</view>
|
||||
<view class="player-id">身份证:{{ item.idCard }}</view>
|
||||
</view>
|
||||
<view class="player-actions">
|
||||
<view class="action-btn edit-btn" @click.stop="handleEdit(item)">
|
||||
<image class="action-icon" src="/static/images/编辑@3x.png" mode="aspectFit"></image>
|
||||
<text>编辑</text>
|
||||
<view class="player-list">
|
||||
<view class="player-item" v-for="(item, index) in playerList" :key="index">
|
||||
<view class="player-checkbox" @click="togglePlayer(item)">
|
||||
<image v-if="item.selected" class="checkbox-img" src="/static/images/选中@3x.png" mode="aspectFit"></image>
|
||||
<image v-else class="checkbox-img" src="/static/images/未选中@3x.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="action-btn delete-btn" @click.stop="handleDelete(item)">
|
||||
<image class="action-icon" src="/static/images/删除@3x.png" mode="aspectFit"></image>
|
||||
<text>删除</text>
|
||||
<view class="player-info">
|
||||
<view class="player-name">{{ item.name }}</view>
|
||||
<view class="player-id">身份证:{{ item.idCard }}</view>
|
||||
</view>
|
||||
<view class="player-actions">
|
||||
<view class="action-btn edit-btn" @click.stop="handleEdit(item)">
|
||||
<image class="action-icon" src="/static/images/编辑@3x.png" mode="aspectFit"></image>
|
||||
<text>编辑</text>
|
||||
</view>
|
||||
<view class="action-btn delete-btn" @click.stop="handleDelete(item)">
|
||||
<image class="action-icon" src="/static/images/删除@3x.png" mode="aspectFit"></image>
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 集体项目:选择集体 -->
|
||||
<template v-else>
|
||||
<view class="selected-count">已选:<text class="count">{{ selectedTeamCount }}</text> 个集体</view>
|
||||
|
||||
<view class="add-player-btn" @click="goToAddTeam">
|
||||
<text class="add-icon">⊕</text>
|
||||
<text>新增集体</text>
|
||||
</view>
|
||||
|
||||
<view class="player-list">
|
||||
<view class="player-item" v-for="(item, index) in teamList" :key="index">
|
||||
<view class="player-checkbox" @click="toggleTeam(item)">
|
||||
<image v-if="item.selected" class="checkbox-img" src="/static/images/选中@3x.png" mode="aspectFit"></image>
|
||||
<image v-else class="checkbox-img" src="/static/images/未选中@3x.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="player-info">
|
||||
<view class="player-name">{{ item.name }}</view>
|
||||
<view class="player-id">成员数:{{ item.memberCount || 0 }}人</view>
|
||||
</view>
|
||||
<view class="player-actions">
|
||||
<view class="action-btn delete-btn" @click.stop="handleDeleteTeam(item)">
|
||||
<image class="action-icon" src="/static/images/删除@3x.png" mode="aspectFit"></image>
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-state" v-if="teamList.length === 0">
|
||||
<text class="empty-text">暂无集体,请先新增</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="next-btn-wrapper">
|
||||
<view class="next-btn" @click="goToStep2">下一步</view>
|
||||
@@ -62,30 +98,30 @@
|
||||
<view class="event-info-card">
|
||||
<view class="event-title">{{ eventInfo.title }}</view>
|
||||
<view class="divider"></view>
|
||||
<view class="info-item">
|
||||
<view class="info-item contact-item" @click="showContactPicker = true">
|
||||
<text class="label">地点:</text>
|
||||
<text class="value">{{ eventInfo.location }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-item contact-item" @click="showContactPicker = true">
|
||||
<text class="label">比赛时间:</text>
|
||||
<text class="value">{{ eventInfo.matchTime }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-item contact-item" @click="showContactPicker = true">
|
||||
<text class="label">报名项目:</text>
|
||||
<text class="value">{{ eventInfo.projects }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-item contact-item" @click="showContactPicker = true">
|
||||
<text class="label">联系人:</text>
|
||||
<text class="value">{{ eventInfo.contact }}</text>
|
||||
<text class="edit-icon">📋</text>
|
||||
<text class="value" :class="{ placeholder: !selectedContact }">{{ selectedContact ? selectedContact.name + " " + selectedContact.phone : "请选择联系人" }}</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
<view class="info-hint">(注意是否用此号码接收信息)</view>
|
||||
<view class="info-item participants-item">
|
||||
<text class="label">参赛选手:</text>
|
||||
<text class="label">{{ isTeamProject ? '参赛集体:' : '参赛选手:' }}</text>
|
||||
<text class="value participants" style="color: #C93639; font-weight: bold;">
|
||||
{{ eventInfo.participants || '未选择选手' }}
|
||||
{{ eventInfo.participants || (isTeamProject ? '未选择集体' : '未选择选手') }}
|
||||
</text>
|
||||
<view class="view-cert-btn" @click="showPlayers">
|
||||
<view class="view-cert-btn" @click="showPlayers" v-if="!isTeamProject">
|
||||
<text>查看证件</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
@@ -94,8 +130,8 @@
|
||||
|
||||
<view class="payment-info">
|
||||
<view class="payment-row">
|
||||
<text class="label">人数:</text>
|
||||
<text class="value">{{ selectedCount }}</text>
|
||||
<text class="label">{{ isTeamProject ? '集体数:' : '人数:' }}</text>
|
||||
<text class="value">{{ isTeamProject ? selectedTeamCount : selectedCount }}</text>
|
||||
</view>
|
||||
<view class="payment-row total">
|
||||
<text class="label">合计:</text>
|
||||
@@ -115,31 +151,42 @@
|
||||
<view class="event-info-card">
|
||||
<view class="event-title">{{ eventInfo.title }}</view>
|
||||
<view class="divider"></view>
|
||||
<view class="info-item">
|
||||
<view class="info-item contact-item" @click="showContactPicker = true">
|
||||
<text class="label">地点:</text>
|
||||
<text class="value">{{ eventInfo.location }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-item contact-item" @click="showContactPicker = true">
|
||||
<text class="label">比赛时间:</text>
|
||||
<text class="value">{{ eventInfo.matchTime }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-item contact-item" @click="showContactPicker = true">
|
||||
<text class="label">报名项目:</text>
|
||||
<text class="value">{{ eventInfo.projects }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-item contact-item" @click="showContactPicker = true">
|
||||
<text class="label">联系人:</text>
|
||||
<text class="value">{{ eventInfo.contact }}</text>
|
||||
<text class="value" :class="{ placeholder: !selectedContact }">{{ selectedContact ? selectedContact.name + " " + selectedContact.phone : "请选择联系人" }}</text>
|
||||
</view>
|
||||
|
||||
<view class="participants-title">参赛选手:{{ selectedPlayers.length }}人</view>
|
||||
<view class="participants-detail">
|
||||
<view class="participant-item" v-for="(item, index) in selectedPlayers" :key="index">
|
||||
<view class="participant-name">{{ item.name }}</view>
|
||||
<view class="participant-id">身份证:{{ item.idCard }}</view>
|
||||
<view class="participant-number">编号:{{ item.number }}</view>
|
||||
<template v-if="!isTeamProject">
|
||||
<view class="participants-title">参赛选手:{{ selectedPlayers.length }}人</view>
|
||||
<view class="participants-detail">
|
||||
<view class="participant-item" v-for="(item, index) in selectedPlayers" :key="index">
|
||||
<view class="participant-name">{{ item.name }}</view>
|
||||
<view class="participant-id">身份证:{{ item.idCard }}</view>
|
||||
<view class="participant-number">编号:{{ item.number }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view class="participants-title">参赛集体:{{ selectedTeams.length }}个</view>
|
||||
<view class="participants-detail">
|
||||
<view class="participant-item" v-for="(item, index) in selectedTeams" :key="index">
|
||||
<view class="participant-name">{{ item.name }}</view>
|
||||
<view class="participant-id">成员数:{{ item.memberCount }}人</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view class="close-btn-wrapper">
|
||||
@@ -164,6 +211,32 @@
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系人选择弹窗 -->
|
||||
<view class="contact-modal" v-if="showContactPicker" @click="showContactPicker = false">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">选择联系人</text>
|
||||
<text class="close-icon" @click="showContactPicker = false">✕</text>
|
||||
</view>
|
||||
<view class="add-contact-btn" @click="goToAddContact">
|
||||
<text class="add-icon">⊕</text>
|
||||
<text>新增联系人</text>
|
||||
</view>
|
||||
<scroll-view class="modal-body contact-list" scroll-y>
|
||||
<view class="contact-item" v-for="(item, index) in contactList" :key="index" @click="selectContact(item)">
|
||||
<view class="contact-info">
|
||||
<view class="contact-name">{{ item.name }}</view>
|
||||
<view class="contact-phone">{{ item.phone }}</view>
|
||||
</view>
|
||||
<view class="contact-check" v-if="selectedContact && selectedContact.id === item.id">✓</view>
|
||||
</view>
|
||||
<view class="empty-state" v-if="contactList.length === 0">
|
||||
<text class="empty-text">暂无联系人,请先新增</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -171,6 +244,7 @@
|
||||
import competitionAPI from '@/api/competition.js'
|
||||
import athleteAPI from '@/api/athlete.js'
|
||||
import registrationAPI from '@/api/registration.js'
|
||||
import { getUserInfo } from '@/utils/auth.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -178,6 +252,7 @@ export default {
|
||||
currentStep: 1,
|
||||
eventId: '',
|
||||
selectedProjects: [],
|
||||
isTeamProject: false,
|
||||
eventInfo: {
|
||||
title: '',
|
||||
location: '',
|
||||
@@ -187,8 +262,13 @@ export default {
|
||||
participants: ''
|
||||
},
|
||||
playerList: [],
|
||||
teamList: [],
|
||||
selectedPlayers: [],
|
||||
selectedTeams: [],
|
||||
showPlayerModal: false,
|
||||
showContactPicker: false,
|
||||
contactList: [],
|
||||
selectedContact: null,
|
||||
totalPrice: 0,
|
||||
registrationId: ''
|
||||
};
|
||||
@@ -197,11 +277,8 @@ export default {
|
||||
selectedCount() {
|
||||
return this.playerList.filter(item => item.selected).length
|
||||
},
|
||||
participantsText() {
|
||||
return this.playerList
|
||||
.filter(item => item.selected)
|
||||
.map(item => item.name)
|
||||
.join('、')
|
||||
selectedTeamCount() {
|
||||
return this.teamList.filter(item => item.selected).length
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
@@ -212,47 +289,78 @@ export default {
|
||||
|
||||
if (options.projects) {
|
||||
try {
|
||||
// 尝试解码(可能被双重编码)
|
||||
let projectsStr = decodeURIComponent(options.projects)
|
||||
|
||||
// 如果还包含 %,说明被双重编码了,再解码一次
|
||||
if (projectsStr.includes('%')) {
|
||||
projectsStr = decodeURIComponent(projectsStr)
|
||||
}
|
||||
|
||||
this.selectedProjects = JSON.parse(projectsStr)
|
||||
|
||||
// Check if any project is team type (type === 2)
|
||||
this.isTeamProject = this.selectedProjects.some(p => p.type === 2 || p.type === '2')
|
||||
} catch (err) {
|
||||
console.error('解析项目数据失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载选手列表
|
||||
this.loadPlayerList()
|
||||
},
|
||||
onShow() {
|
||||
// 从新增/编辑页面返回时重新加载列表
|
||||
if (this.currentStep === 1) {
|
||||
// Load list based on project type
|
||||
if (this.isTeamProject) {
|
||||
this.loadTeamList()
|
||||
} else {
|
||||
this.loadPlayerList()
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
// Refresh contact list when returning from add-contact page
|
||||
this.loadContactList()
|
||||
if (this.currentStep === 1) {
|
||||
if (this.isTeamProject) {
|
||||
this.loadTeamList()
|
||||
} else {
|
||||
this.loadPlayerList()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载赛事详情
|
||||
*/
|
||||
// Contact methods
|
||||
async loadContactList() {
|
||||
try {
|
||||
const res = await athleteAPI.getContactList({ current: 1, size: 100 })
|
||||
this.contactList = (res.records || []).map(item => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
phone: item.phone,
|
||||
idCard: item.idCard
|
||||
}))
|
||||
// Auto select default contact if exists
|
||||
if (!this.selectedContact && this.contactList.length > 0) {
|
||||
const defaultContact = this.contactList.find(c => c.isDefault) || this.contactList[0]
|
||||
this.selectedContact = defaultContact
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载联系人列表失败:', err)
|
||||
}
|
||||
},
|
||||
|
||||
selectContact(contact) {
|
||||
this.selectedContact = contact
|
||||
this.showContactPicker = false
|
||||
},
|
||||
|
||||
goToAddContact() {
|
||||
this.showContactPicker = false
|
||||
uni.navigateTo({
|
||||
url: '/pages/add-contact/add-contact'
|
||||
})
|
||||
},
|
||||
|
||||
async loadEventDetail(id) {
|
||||
try {
|
||||
const res = await competitionAPI.getCompetitionDetail(id)
|
||||
|
||||
// 尝试多个可能的时间字段名
|
||||
const startTime = res.startTime || res.competitionStartTime || res.beginTime || res.startDate
|
||||
const endTime = res.endTime || res.competitionEndTime || res.finishTime || res.endDate
|
||||
|
||||
// 如果没有时间字段,尝试使用其他字段
|
||||
let matchTime = this.formatTimeRange(startTime, endTime)
|
||||
if (!matchTime && res.matchTime) {
|
||||
matchTime = res.matchTime
|
||||
} else if (!matchTime && res.competitionTime) {
|
||||
matchTime = res.competitionTime
|
||||
} else if (!matchTime) {
|
||||
matchTime = '待定'
|
||||
}
|
||||
@@ -269,7 +377,6 @@ export default {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载赛事详情失败:', err)
|
||||
// 设置默认值,防止页面显示空白
|
||||
this.eventInfo = {
|
||||
title: '未命名赛事',
|
||||
location: '待定',
|
||||
@@ -281,97 +388,105 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载选手列表
|
||||
*/
|
||||
async loadPlayerList() {
|
||||
try {
|
||||
const res = await athleteAPI.getAthleteList({
|
||||
current: 1,
|
||||
size: 100
|
||||
})
|
||||
|
||||
let list = []
|
||||
if (res.records) {
|
||||
list = res.records
|
||||
} else if (Array.isArray(res)) {
|
||||
list = res
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo || !userInfo.userId) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 数据映射 - 尝试多个可能的字段名
|
||||
const res = await athleteAPI.getAthleteList({
|
||||
current: 1,
|
||||
size: 100,
|
||||
createUser: userInfo.userId
|
||||
})
|
||||
|
||||
let list = res.records || (Array.isArray(res) ? res : [])
|
||||
this.playerList = list.map(item => ({
|
||||
id: item.id,
|
||||
// 尝试多个可能的姓名字段
|
||||
name: item.name || item.athleteName || item.playerName || item.realName || item.userName || '未命名',
|
||||
// 尝试多个可能的身份证字段
|
||||
idCard: item.idCard || item.idCardNumber || item.idCardNo || item.identityCard || '',
|
||||
name: item.name || item.athleteName || item.playerName || '未命名',
|
||||
idCard: item.idCard || item.idCardNumber || '',
|
||||
selected: false
|
||||
}))
|
||||
} catch (err) {
|
||||
console.error('加载选手列表失败:', err)
|
||||
uni.showToast({
|
||||
title: '加载选手列表失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化时间范围
|
||||
*/
|
||||
async loadTeamList() {
|
||||
try {
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo || !userInfo.userId) return
|
||||
|
||||
const res = await athleteAPI.getTeamList({
|
||||
current: 1,
|
||||
size: 100
|
||||
})
|
||||
|
||||
let list = res.records || (Array.isArray(res) ? res : [])
|
||||
this.teamList = list.map(item => ({
|
||||
id: item.id,
|
||||
name: item.teamName || item.name,
|
||||
memberCount: item.memberCount || 0,
|
||||
selected: false
|
||||
}))
|
||||
} catch (err) {
|
||||
console.error('加载集体列表失败:', err)
|
||||
this.teamList = []
|
||||
}
|
||||
},
|
||||
|
||||
formatTimeRange(startTime, endTime) {
|
||||
if (!startTime || !endTime) return ''
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}.${month}.${day}`
|
||||
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
return `${formatDate(startTime)}-${formatDate(endTime)}`
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算总价
|
||||
*/
|
||||
calculateTotalPrice() {
|
||||
const count = this.selectedCount
|
||||
|
||||
if (!this.selectedProjects || this.selectedProjects.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 计算所有项目的总价(将字符串转换为数字)
|
||||
const pricePerProject = this.selectedProjects.reduce((sum, p) => {
|
||||
const price = parseFloat(p.price || 0)
|
||||
return sum + price
|
||||
}, 0)
|
||||
|
||||
const total = count * pricePerProject
|
||||
|
||||
return total.toFixed(2)
|
||||
const count = this.isTeamProject ? this.selectedTeamCount : this.selectedCount
|
||||
if (!this.selectedProjects || this.selectedProjects.length === 0) return 0
|
||||
const pricePerProject = this.selectedProjects.reduce((sum, p) => sum + parseFloat(p.price || 0), 0)
|
||||
return (count * pricePerProject).toFixed(2)
|
||||
},
|
||||
|
||||
togglePlayer(item) {
|
||||
const index = this.playerList.findIndex(p => p.id === item.id)
|
||||
if (index !== -1) {
|
||||
const newValue = !this.playerList[index].selected
|
||||
this.$set(this.playerList[index], 'selected', newValue)
|
||||
this.$set(this.playerList[index], 'selected', !this.playerList[index].selected)
|
||||
}
|
||||
},
|
||||
goToAddPlayer() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/add-player/add-player'
|
||||
});
|
||||
|
||||
toggleTeam(item) {
|
||||
const index = this.teamList.findIndex(t => t.id === item.id)
|
||||
if (index !== -1) {
|
||||
this.$set(this.teamList[index], 'selected', !this.teamList[index].selected)
|
||||
}
|
||||
},
|
||||
|
||||
goToAddPlayer() {
|
||||
const projectIds = this.selectedProjects.map(p => p.id).join(',')
|
||||
uni.navigateTo({
|
||||
url: `/pages/add-player/add-player?competitionId=${this.eventId}&projectIds=${projectIds}`
|
||||
})
|
||||
},
|
||||
|
||||
goToAddTeam() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/add-team/add-team'
|
||||
})
|
||||
},
|
||||
|
||||
handleEdit(item) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/edit-player/edit-player?id=' + item.id
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
async handleDelete(item) {
|
||||
try {
|
||||
const confirmRes = await new Promise((resolve) => {
|
||||
@@ -384,167 +499,147 @@ export default {
|
||||
|
||||
if (confirmRes.confirm) {
|
||||
await athleteAPI.removeAthlete(item.id)
|
||||
|
||||
const index = this.playerList.findIndex(p => p.id === item.id);
|
||||
if (index > -1) {
|
||||
this.playerList.splice(index, 1);
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
const index = this.playerList.findIndex(p => p.id === item.id)
|
||||
if (index > -1) this.playerList.splice(index, 1)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('删除选手失败:', err)
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
async handleDeleteTeam(item) {
|
||||
try {
|
||||
const confirmRes = await new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title: '删除集体',
|
||||
content: '确定要删除该集体吗?',
|
||||
success: (res) => resolve(res)
|
||||
})
|
||||
})
|
||||
|
||||
if (confirmRes.confirm) {
|
||||
await athleteAPI.removeTeam(item.id)
|
||||
const index = this.teamList.findIndex(t => t.id === item.id)
|
||||
if (index > -1) this.teamList.splice(index, 1)
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('删除集体失败:', err)
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
goToStep2() {
|
||||
const selected = this.playerList.filter(item => item.selected)
|
||||
|
||||
if (selected.length === 0) {
|
||||
uni.showToast({
|
||||
title: '请至少选择一名选手',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
if (this.isTeamProject) {
|
||||
const selected = this.teamList.filter(item => item.selected)
|
||||
if (selected.length === 0) {
|
||||
uni.showToast({ title: '请至少选择一个集体', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.$set(this.eventInfo, 'participants', selected.map(t => t.name).join('、'))
|
||||
this.selectedTeams = selected
|
||||
} else {
|
||||
const selected = this.playerList.filter(item => item.selected)
|
||||
if (selected.length === 0) {
|
||||
uni.showToast({ title: '请至少选择一名选手', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.$set(this.eventInfo, 'participants', selected.map(p => p.name).join('、'))
|
||||
}
|
||||
|
||||
// 更新参赛选手信息
|
||||
const participantsText = selected.map(p => p.name).join('、')
|
||||
|
||||
// 使用 $set 确保响应式更新
|
||||
this.$set(this.eventInfo, 'participants', participantsText)
|
||||
|
||||
this.totalPrice = this.calculateTotalPrice()
|
||||
|
||||
// 延迟切换步骤,确保数据更新完成
|
||||
this.$nextTick(() => {
|
||||
this.currentStep = 2
|
||||
})
|
||||
this.loadContactList()
|
||||
this.$nextTick(() => { this.currentStep = 2 })
|
||||
},
|
||||
|
||||
async goToStep3() {
|
||||
try {
|
||||
// 获取选中的选手
|
||||
const selected = this.playerList.filter(item => item.selected)
|
||||
|
||||
// 检查必填字段
|
||||
if (!this.eventId) {
|
||||
uni.showToast({
|
||||
title: '赛事ID缺失',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '赛事ID缺失', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.selectedProjects || this.selectedProjects.length === 0) {
|
||||
uni.showToast({
|
||||
title: '请选择报名项目',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '请选择报名项目', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (selected.length === 0) {
|
||||
uni.showToast({
|
||||
title: '请选择参赛选手',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成订单号:格式 BMyyyyMMddHHmmss + 随机4位数
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const hours = String(now.getHours()).padStart(2, '0')
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||
const random = String(Math.floor(Math.random() * 10000)).padStart(4, '0')
|
||||
const orderNo = `BM${year}${month}${day}${hours}${minutes}${seconds}${random}`
|
||||
const orderNo = `BM${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}${String(Math.floor(Math.random() * 10000)).padStart(4, '0')}`
|
||||
|
||||
// 构建提交数据 - 确保ID都是数字类型
|
||||
const submitData = {
|
||||
orderNo: orderNo,
|
||||
competitionId: parseInt(this.eventId),
|
||||
projectIds: this.selectedProjects.map(p => parseInt(p.id)),
|
||||
athleteIds: selected.map(p => parseInt(p.id)),
|
||||
contactPhone: this.eventInfo.contact || '',
|
||||
totalAmount: parseFloat(this.totalPrice) || 0
|
||||
let submitData
|
||||
if (this.isTeamProject) {
|
||||
const selected = this.teamList.filter(item => item.selected)
|
||||
if (selected.length === 0) {
|
||||
uni.showToast({ title: '请选择参赛集体', icon: 'none' })
|
||||
return
|
||||
}
|
||||
submitData = {
|
||||
orderNo: orderNo,
|
||||
competitionId: String(this.eventId),
|
||||
projectIds: this.selectedProjects.map(p => String(p.id)),
|
||||
teamIds: selected.map(t => String(t.id)),
|
||||
athleteIds: [],
|
||||
contactPhone: this.selectedContact ? this.selectedContact.phone : '',
|
||||
totalAmount: parseFloat(this.totalPrice) || 0
|
||||
}
|
||||
} else {
|
||||
const selected = this.playerList.filter(item => item.selected)
|
||||
if (selected.length === 0) {
|
||||
uni.showToast({ title: '请选择参赛选手', icon: 'none' })
|
||||
return
|
||||
}
|
||||
submitData = {
|
||||
orderNo: orderNo,
|
||||
competitionId: String(this.eventId),
|
||||
projectIds: this.selectedProjects.map(p => String(p.id)),
|
||||
athleteIds: selected.map(p => String(p.id)),
|
||||
contactPhone: this.selectedContact ? this.selectedContact.phone : '',
|
||||
totalAmount: parseFloat(this.totalPrice) || 0
|
||||
}
|
||||
}
|
||||
|
||||
console.log('=== 提交报名数据 ===')
|
||||
console.log('订单号:', submitData.orderNo)
|
||||
console.log('完整提交数据:', submitData)
|
||||
console.log('赛事ID:', submitData.competitionId, typeof submitData.competitionId)
|
||||
console.log('项目IDs:', submitData.projectIds)
|
||||
console.log('选手IDs:', submitData.athleteIds)
|
||||
console.log('联系电话:', submitData.contactPhone)
|
||||
console.log('总金额:', submitData.totalAmount, typeof submitData.totalAmount)
|
||||
|
||||
// 提交报名订单
|
||||
console.log('提交报名数据:', submitData)
|
||||
const res = await registrationAPI.submitRegistration(submitData)
|
||||
this.registrationId = res.id || res.registrationId || orderNo
|
||||
|
||||
console.log('=== 报名响应数据 ===')
|
||||
console.log('完整响应:', res)
|
||||
console.log('响应数据:', res.data)
|
||||
|
||||
// 保存报名ID - 尝试多个可能的字段
|
||||
this.registrationId = res.id || res.registrationId || res.data?.id || res.data?.registrationId || orderNo
|
||||
|
||||
console.log('报名ID:', this.registrationId)
|
||||
|
||||
// 更新选中的选手列表(包含编号)
|
||||
this.selectedPlayers = selected.map((item, index) => {
|
||||
// 生成编号:使用报名ID或订单号 + 选手索引
|
||||
const playerNumber = item.playerNo || item.number || `${this.registrationId}-${String(index + 1).padStart(6, '0')}`
|
||||
|
||||
return {
|
||||
if (this.isTeamProject) {
|
||||
this.selectedTeams = this.teamList.filter(item => item.selected).map(item => ({
|
||||
name: item.name,
|
||||
memberCount: item.memberCount
|
||||
}))
|
||||
} else {
|
||||
const selected = this.playerList.filter(item => item.selected)
|
||||
this.selectedPlayers = selected.map((item, index) => ({
|
||||
name: item.name,
|
||||
idCard: item.idCard,
|
||||
number: playerNumber
|
||||
}
|
||||
})
|
||||
number: `${this.registrationId}-${String(index + 1).padStart(6, '0')}`
|
||||
}))
|
||||
}
|
||||
|
||||
this.currentStep = 3;
|
||||
|
||||
uni.showToast({
|
||||
title: '报名成功',
|
||||
icon: 'success'
|
||||
})
|
||||
this.currentStep = 3
|
||||
uni.showToast({ title: '报名成功', icon: 'success' })
|
||||
} catch (err) {
|
||||
console.error('提交报名失败:', err)
|
||||
uni.showToast({
|
||||
title: '报名失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '报名失败,请重试', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
showPlayers() {
|
||||
// 更新选中的选手列表
|
||||
this.selectedPlayers = this.playerList
|
||||
.filter(item => item.selected)
|
||||
.map(item => ({
|
||||
name: item.name,
|
||||
idCard: item.idCard,
|
||||
number: ''
|
||||
}))
|
||||
|
||||
this.showPlayerModal = true;
|
||||
.map(item => ({ name: item.name, idCard: item.idCard, number: '' }))
|
||||
this.showPlayerModal = true
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
uni.navigateBack({
|
||||
delta: 3
|
||||
});
|
||||
uni.navigateBack({ delta: 3 })
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.event-register-page {
|
||||
min-height: 100vh;
|
||||
@@ -983,4 +1078,92 @@ export default {
|
||||
color: #C93639;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
/* Contact Modal Styles */
|
||||
.contact-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.contact-modal .modal-content {
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
width: 100%;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.contact-modal .add-contact-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
color: #C93639;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.contact-modal .add-contact-btn .add-icon {
|
||||
margin-right: 10rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.contact-modal .contact-list {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.contact-modal .contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.contact-modal .contact-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contact-modal .contact-name {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.contact-modal .contact-phone {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.contact-modal .contact-check {
|
||||
color: #C93639;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.contact-item.clickable,
|
||||
.info-item.contact-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.info-item .placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
.empty-state {
|
||||
padding: 100rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
@@ -71,40 +71,60 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
eventId: '',
|
||||
// 附件列表
|
||||
attachments: [],
|
||||
// 规程章节列表
|
||||
rulesList: []
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.eventId) {
|
||||
this.eventId = options.eventId
|
||||
this.loadRulesData()
|
||||
this.loadAttachments(options.eventId)
|
||||
this.loadRulesData(options.eventId)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载规程数据
|
||||
* 加载附件列表
|
||||
*/
|
||||
async loadRulesData() {
|
||||
async loadAttachments(eventId) {
|
||||
try {
|
||||
// 调用API获取规程数据
|
||||
const res = await competitionAPI.getCompetitionRules(this.eventId)
|
||||
const res = await competitionAPI.getAttachments({
|
||||
competitionId: eventId,
|
||||
type: 'rules'
|
||||
})
|
||||
|
||||
// 处理附件数据
|
||||
if (res.attachments && res.attachments.length > 0) {
|
||||
this.attachments = res.attachments.map(file => ({
|
||||
id: file.id,
|
||||
fileName: file.name || file.fileName,
|
||||
fileUrl: file.url || file.fileUrl,
|
||||
fileSize: this.formatFileSize(file.size || file.fileSize),
|
||||
fileType: this.getFileType(file.name || file.fileName)
|
||||
}))
|
||||
let list = []
|
||||
if (res && Array.isArray(res)) {
|
||||
list = res
|
||||
} else if (res && res.records) {
|
||||
list = res.records
|
||||
} else if (res && res.data && Array.isArray(res.data)) {
|
||||
list = res.data
|
||||
}
|
||||
|
||||
if (list.length > 0) {
|
||||
this.attachments = list.map(file => ({
|
||||
id: file.id,
|
||||
fileName: file.fileName || file.name,
|
||||
fileUrl: file.fileUrl || file.url,
|
||||
fileSize: this.formatFileSize(file.fileSize || file.size),
|
||||
fileType: file.fileType || this.getFileType(file.fileName || file.name)
|
||||
}))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载附件失败:', err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载规程数据
|
||||
*/
|
||||
async loadRulesData(eventId) {
|
||||
try {
|
||||
const res = await competitionAPI.getCompetitionRules(eventId)
|
||||
|
||||
// 处理规程内容数据
|
||||
if (res.chapters && res.chapters.length > 0) {
|
||||
if (res && res.chapters && res.chapters.length > 0) {
|
||||
this.rulesList = res.chapters.map(chapter => ({
|
||||
chapter: chapter.chapterNumber || chapter.number,
|
||||
title: chapter.title || chapter.name,
|
||||
@@ -114,76 +134,9 @@ export default {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载规程数据失败:', err)
|
||||
// 如果API失败,使用模拟数据
|
||||
this.loadMockData()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载模拟数据(用于开发测试)
|
||||
*/
|
||||
loadMockData() {
|
||||
this.attachments = [
|
||||
{
|
||||
id: '1',
|
||||
fileName: '2025年郑州武术大赛规程.pdf',
|
||||
fileUrl: 'https://example.com/rules.pdf',
|
||||
fileSize: '2.5 MB',
|
||||
fileType: 'pdf'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
fileName: '参赛报名表.docx',
|
||||
fileUrl: 'https://example.com/form.docx',
|
||||
fileSize: '156 KB',
|
||||
fileType: 'docx'
|
||||
}
|
||||
]
|
||||
|
||||
this.rulesList = [
|
||||
{
|
||||
chapter: '第一章',
|
||||
title: '总则',
|
||||
expanded: false,
|
||||
contents: [
|
||||
'1.1 本次比赛遵循国际武术联合会竞赛规则。',
|
||||
'1.2 所有参赛选手必须持有效证件参赛。',
|
||||
'1.3 参赛选手须服从裁判判决,不得有违规行为。'
|
||||
]
|
||||
},
|
||||
{
|
||||
chapter: '第二章',
|
||||
title: '参赛资格',
|
||||
expanded: false,
|
||||
contents: [
|
||||
'2.1 参赛选手年龄须在18-45周岁之间。',
|
||||
'2.2 参赛选手须持有武术等级证书或相关证明。',
|
||||
'2.3 参赛选手须通过健康检查,身体状况良好。'
|
||||
]
|
||||
},
|
||||
{
|
||||
chapter: '第三章',
|
||||
title: '比赛规则',
|
||||
expanded: false,
|
||||
contents: [
|
||||
'3.1 比赛采用单败淘汰制。',
|
||||
'3.2 每场比赛时间为3分钟,分3局进行。',
|
||||
'3.3 得分规则按照国际标准执行。'
|
||||
]
|
||||
},
|
||||
{
|
||||
chapter: '第四章',
|
||||
title: '奖项设置',
|
||||
expanded: false,
|
||||
contents: [
|
||||
'4.1 各组别设金、银、铜牌各一枚。',
|
||||
'4.2 设最佳表现奖、体育道德风尚奖等特别奖项。',
|
||||
'4.3 所有参赛选手均可获得参赛证书。'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换章节展开/收起
|
||||
*/
|
||||
@@ -195,36 +148,41 @@ export default {
|
||||
* 下载文件
|
||||
*/
|
||||
downloadFile(file) {
|
||||
uni.showLoading({
|
||||
title: '准备下载'
|
||||
})
|
||||
const fileExt = file.fileType || this.getFileType(file.fileName) || 'pdf'
|
||||
|
||||
// 下载文件
|
||||
// #ifdef H5
|
||||
const link = document.createElement('a')
|
||||
link.href = file.fileUrl
|
||||
link.download = file.fileName
|
||||
link.target = '_blank'
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
uni.showToast({
|
||||
title: '开始下载',
|
||||
icon: 'success'
|
||||
})
|
||||
return
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
uni.showLoading({ title: '准备下载' })
|
||||
uni.downloadFile({
|
||||
url: file.fileUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
// 保存文件到本地
|
||||
const filePath = res.tempFilePath
|
||||
|
||||
// 打开文档
|
||||
uni.openDocument({
|
||||
filePath: filePath,
|
||||
fileType: file.fileType,
|
||||
filePath: res.tempFilePath,
|
||||
fileType: fileExt,
|
||||
success: () => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '打开成功',
|
||||
icon: 'success'
|
||||
})
|
||||
uni.showToast({ title: '打开成功', icon: 'success' })
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('打开文件失败:', err)
|
||||
uni.showToast({
|
||||
title: '打开失败',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '打开失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -232,18 +190,17 @@ export default {
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('下载失败:', err)
|
||||
uni.showToast({
|
||||
title: '下载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
uni.showToast({ title: '下载失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件类型
|
||||
*/
|
||||
getFileType(fileName) {
|
||||
if (!fileName) return 'pdf'
|
||||
const ext = fileName.split('.').pop().toLowerCase()
|
||||
return ext
|
||||
},
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
<template>
|
||||
<view class="event-schedule-page">
|
||||
<!-- 附件下载区 -->
|
||||
<view class="attachments-section" v-if="attachments.length > 0">
|
||||
<view class="section-title">
|
||||
<text class="title-icon">📎</text>
|
||||
<text class="title-text">日程附件</text>
|
||||
</view>
|
||||
<view class="attachments-list">
|
||||
<view class="attachment-item" v-for="(file, index) in attachments" :key="index" @click="downloadFile(file)">
|
||||
<view class="file-info">
|
||||
<text class="file-icon">{{ getFileIcon(file.fileType) }}</text>
|
||||
<view class="file-details">
|
||||
<text class="file-name">{{ file.fileName }}</text>
|
||||
<text class="file-size">{{ file.fileSize }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="download-btn">
|
||||
<text class="download-icon">⬇</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期选择器 -->
|
||||
<view class="date-tabs">
|
||||
<view class="date-tabs" v-if="dates.length > 0">
|
||||
<view
|
||||
class="date-tab"
|
||||
v-for="(date, index) in dates"
|
||||
@@ -15,7 +37,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 日程时间线 -->
|
||||
<view class="schedule-timeline">
|
||||
<view class="schedule-timeline" v-if="currentSchedule.length > 0">
|
||||
<view class="timeline-item" v-for="(item, index) in currentSchedule" :key="index">
|
||||
<view class="time-dot"></view>
|
||||
<view class="time-line" v-if="index < currentSchedule.length - 1"></view>
|
||||
@@ -29,11 +51,18 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="attachments.length === 0 && dates.length === 0">
|
||||
<text class="empty-icon">📅</text>
|
||||
<text class="empty-text">暂无日程安排</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import infoAPI from '@/api/info.js'
|
||||
import competitionAPI from '@/api/competition.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -41,7 +70,8 @@ export default {
|
||||
eventId: '',
|
||||
currentDate: 0,
|
||||
dates: [],
|
||||
schedules: {}
|
||||
schedules: {},
|
||||
attachments: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -52,6 +82,7 @@ export default {
|
||||
onLoad(options) {
|
||||
if (options.eventId) {
|
||||
this.eventId = options.eventId
|
||||
this.loadAttachments(options.eventId)
|
||||
this.loadScheduleDates(options.eventId)
|
||||
}
|
||||
},
|
||||
@@ -63,6 +94,132 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载附件列表
|
||||
*/
|
||||
async loadAttachments(eventId) {
|
||||
try {
|
||||
const res = await competitionAPI.getAttachments({
|
||||
competitionId: eventId,
|
||||
type: 'schedule'
|
||||
})
|
||||
|
||||
let list = []
|
||||
if (res && Array.isArray(res)) {
|
||||
list = res
|
||||
} else if (res && res.records) {
|
||||
list = res.records
|
||||
} else if (res && res.data && Array.isArray(res.data)) {
|
||||
list = res.data
|
||||
}
|
||||
|
||||
if (list.length > 0) {
|
||||
this.attachments = list.map(file => ({
|
||||
id: file.id,
|
||||
fileName: file.fileName || file.name,
|
||||
fileUrl: file.fileUrl || file.url,
|
||||
fileSize: this.formatFileSize(file.fileSize || file.size),
|
||||
fileType: file.fileType || this.getFileType(file.fileName || file.name)
|
||||
}))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载附件失败:', err)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
downloadFile(file) {
|
||||
const fileExt = file.fileType || this.getFileType(file.fileName) || 'pdf'
|
||||
|
||||
// #ifdef H5
|
||||
const link = document.createElement('a')
|
||||
link.href = file.fileUrl
|
||||
link.download = file.fileName
|
||||
link.target = '_blank'
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
uni.showToast({
|
||||
title: '开始下载',
|
||||
icon: 'success'
|
||||
})
|
||||
return
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
uni.showLoading({ title: '准备下载' })
|
||||
uni.downloadFile({
|
||||
url: file.fileUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
fileType: fileExt,
|
||||
success: () => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '打开成功', icon: 'success' })
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('打开文件失败:', err)
|
||||
uni.showToast({ title: '打开失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('下载失败:', err)
|
||||
uni.showToast({ title: '下载失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件类型
|
||||
*/
|
||||
getFileType(fileName) {
|
||||
if (!fileName) return 'pdf'
|
||||
const ext = fileName.split('.').pop().toLowerCase()
|
||||
return ext
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件图标
|
||||
*/
|
||||
getFileIcon(fileType) {
|
||||
const iconMap = {
|
||||
'pdf': '📕',
|
||||
'doc': '📘',
|
||||
'docx': '📘',
|
||||
'xls': '📗',
|
||||
'xlsx': '📗',
|
||||
'ppt': '📙',
|
||||
'pptx': '📙',
|
||||
'txt': '📄',
|
||||
'zip': '📦',
|
||||
'rar': '📦'
|
||||
}
|
||||
return iconMap[fileType] || '📄'
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B'
|
||||
if (typeof bytes === 'string') return bytes
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载日程日期列表
|
||||
*/
|
||||
@@ -77,9 +234,8 @@ export default {
|
||||
list = res
|
||||
}
|
||||
|
||||
// 如果后端没有数据,使用模拟数据
|
||||
if (list.length === 0) {
|
||||
list = this.getMockScheduleData()
|
||||
return
|
||||
}
|
||||
|
||||
// 提取唯一日期
|
||||
@@ -93,7 +249,7 @@ export default {
|
||||
|
||||
// 格式化日期选项卡并排序
|
||||
this.dates = Array.from(dateSet)
|
||||
.sort((a, b) => new Date(a) - new Date(b)) // 按日期升序排序
|
||||
.sort((a, b) => new Date(a) - new Date(b))
|
||||
.map(date => {
|
||||
const d = new Date(date)
|
||||
const month = d.getMonth() + 1
|
||||
@@ -119,7 +275,7 @@ export default {
|
||||
|
||||
this.schedules[dateIndex].push({
|
||||
time: this.formatTime(item.scheduleTime || item.schedule_time || item.time),
|
||||
timeRaw: item.scheduleTime || item.schedule_time || item.time, // 保存原始时间用于排序
|
||||
timeRaw: item.scheduleTime || item.schedule_time || item.time,
|
||||
title: item.eventName || item.event_name || item.title || item.activityName || item.scheduleName,
|
||||
location: item.venue || item.location || ''
|
||||
})
|
||||
@@ -141,59 +297,6 @@ export default {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载日程日期失败:', err)
|
||||
// 加载失败时使用模拟数据
|
||||
const list = this.getMockScheduleData()
|
||||
|
||||
// 提取唯一日期
|
||||
const dateSet = new Set()
|
||||
list.forEach(item => {
|
||||
if (item.scheduleDate) {
|
||||
dateSet.add(item.scheduleDate)
|
||||
}
|
||||
})
|
||||
|
||||
// 格式化日期选项卡并排序
|
||||
this.dates = Array.from(dateSet)
|
||||
.sort((a, b) => new Date(a) - new Date(b)) // 按日期升序排序
|
||||
.map(date => {
|
||||
const d = new Date(date)
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
const weekDay = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][d.getDay()]
|
||||
|
||||
return {
|
||||
date: date,
|
||||
day: `${month}月${day}日`,
|
||||
text: weekDay
|
||||
}
|
||||
})
|
||||
|
||||
// 按日期分组日程
|
||||
list.forEach(item => {
|
||||
const dateIndex = this.dates.findIndex(d => d.date === item.scheduleDate)
|
||||
|
||||
if (dateIndex >= 0) {
|
||||
if (!this.schedules[dateIndex]) {
|
||||
this.schedules[dateIndex] = []
|
||||
}
|
||||
|
||||
this.schedules[dateIndex].push({
|
||||
time: this.formatTime(item.scheduleTime),
|
||||
timeRaw: item.scheduleTime, // 保存原始时间用于排序
|
||||
title: item.eventName,
|
||||
location: item.venue || ''
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 对每个日期内的日程按时间排序
|
||||
Object.keys(this.schedules).forEach(dateIndex => {
|
||||
this.schedules[dateIndex].sort((a, b) => {
|
||||
const timeA = a.timeRaw || a.time
|
||||
const timeB = b.timeRaw || b.time
|
||||
return timeA.localeCompare(timeB)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -226,7 +329,6 @@ export default {
|
||||
return timeA.localeCompare(timeB)
|
||||
})
|
||||
|
||||
// 触发视图更新
|
||||
this.$forceUpdate()
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -240,17 +342,14 @@ export default {
|
||||
formatTime(timeStr) {
|
||||
if (!timeStr) return ''
|
||||
|
||||
// 如果已经是 HH:MM 格式
|
||||
if (/^\d{2}:\d{2}$/.test(timeStr)) {
|
||||
return timeStr
|
||||
}
|
||||
|
||||
// 如果是 HH:MM:SS 格式,直接截取前5位
|
||||
if (/^\d{2}:\d{2}:\d{2}$/.test(timeStr)) {
|
||||
return timeStr.substring(0, 5)
|
||||
}
|
||||
|
||||
// 尝试解析完整的日期时间字符串
|
||||
const date = new Date(timeStr)
|
||||
if (!isNaN(date.getTime())) {
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
@@ -258,235 +357,7 @@ export default {
|
||||
return `${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 如果无法解析,返回原字符串
|
||||
return timeStr
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取模拟日程数据
|
||||
*/
|
||||
getMockScheduleData() {
|
||||
return [
|
||||
// 第一天:2025-12-25 (报到日)
|
||||
{
|
||||
id: 2001,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-25',
|
||||
scheduleTime: '08:00',
|
||||
eventName: '运动员报到',
|
||||
venue: '赛事组委会接待处',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2002,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-25',
|
||||
scheduleTime: '09:00',
|
||||
eventName: '领取参赛证件及装备',
|
||||
venue: '赛事组委会接待处',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2003,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-25',
|
||||
scheduleTime: '10:00',
|
||||
eventName: '赛前技术会议',
|
||||
venue: '会议室A',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2004,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-25',
|
||||
scheduleTime: '14:00',
|
||||
eventName: '场地开放训练',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2005,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-25',
|
||||
scheduleTime: '16:00',
|
||||
eventName: '裁判员培训会',
|
||||
venue: '会议室B',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2006,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-25',
|
||||
scheduleTime: '18:00',
|
||||
eventName: '开幕式彩排',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
// 第二天:2025-12-26 (正式比赛第一天)
|
||||
{
|
||||
id: 2007,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-26',
|
||||
scheduleTime: '07:30',
|
||||
eventName: '运动员检录',
|
||||
venue: '检录处',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2008,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-26',
|
||||
scheduleTime: '08:30',
|
||||
eventName: '开幕式',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2009,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-26',
|
||||
scheduleTime: '09:00',
|
||||
eventName: '男子长拳预赛',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2010,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-26',
|
||||
scheduleTime: '10:30',
|
||||
eventName: '女子长拳预赛',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2011,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-26',
|
||||
scheduleTime: '12:00',
|
||||
eventName: '午休',
|
||||
venue: '',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2012,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-26',
|
||||
scheduleTime: '14:00',
|
||||
eventName: '男子太极拳预赛',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2013,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-26',
|
||||
scheduleTime: '15:30',
|
||||
eventName: '女子太极拳预赛',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2014,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-26',
|
||||
scheduleTime: '17:00',
|
||||
eventName: '当日赛事总结会',
|
||||
venue: '会议室A',
|
||||
status: 1
|
||||
},
|
||||
// 第三天:2025-12-27 (正式比赛第二天 - 决赛日)
|
||||
{
|
||||
id: 2015,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-27',
|
||||
scheduleTime: '07:30',
|
||||
eventName: '运动员检录',
|
||||
venue: '检录处',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2016,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-27',
|
||||
scheduleTime: '08:30',
|
||||
eventName: '男子长拳半决赛',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2017,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-27',
|
||||
scheduleTime: '10:00',
|
||||
eventName: '女子长拳半决赛',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2018,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-27',
|
||||
scheduleTime: '12:00',
|
||||
eventName: '午休',
|
||||
venue: '',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2019,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-27',
|
||||
scheduleTime: '14:00',
|
||||
eventName: '男子长拳决赛',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2020,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-27',
|
||||
scheduleTime: '15:00',
|
||||
eventName: '女子长拳决赛',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2021,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-27',
|
||||
scheduleTime: '16:00',
|
||||
eventName: '男子太极拳决赛',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2022,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-27',
|
||||
scheduleTime: '17:00',
|
||||
eventName: '女子太极拳决赛',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2023,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-27',
|
||||
scheduleTime: '18:00',
|
||||
eventName: '颁奖典礼',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2024,
|
||||
competitionId: 200,
|
||||
scheduleDate: '2025-12-27',
|
||||
scheduleTime: '19:00',
|
||||
eventName: '闭幕式',
|
||||
venue: '主赛场',
|
||||
status: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -498,6 +369,104 @@ export default {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
// 区块标题
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
// 附件下载区
|
||||
.attachments-section {
|
||||
padding: 20rpx 30rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.attachments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 25rpx 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
background-color: #f8f8f8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 48rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
background-color: #C93639;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.download-icon {
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.date-tabs {
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
@@ -594,4 +563,24 @@ export default {
|
||||
.location-icon {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -137,8 +137,8 @@ export default {
|
||||
item.registerTime || item.registrationPeriod || '待定',
|
||||
matchTime: this.formatTimeRange(startTime, endTime) ||
|
||||
item.matchTime || item.competitionTime || '待定',
|
||||
registerCount: item.registrationCount || item.registerCount || item.signUpCount || '0',
|
||||
status: this.getStatus(item.status)
|
||||
registerCount: item.registrationCount || item.registerCount || item.signUpCount || item.totalParticipants || '0',
|
||||
status: this.getStatus(item.status, regEndTime, endTime)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -174,12 +174,28 @@ export default {
|
||||
* @param {Number|String} status 状态码
|
||||
* @returns {String}
|
||||
*/
|
||||
getStatus(status) {
|
||||
getStatus(status, regEndTime, competitionEndTime) {
|
||||
// 根据后端状态码映射为前端需要的状态
|
||||
// 1: 报名中, 2: 进行中, 3: 已结束
|
||||
if (status === 3 || status === '3' || status === 'finished') {
|
||||
return 'finished'
|
||||
}
|
||||
// 根据比赛结束时间判断是否已结束
|
||||
if (competitionEndTime) {
|
||||
const compEndDate = new Date(competitionEndTime)
|
||||
const now = new Date()
|
||||
if (now > compEndDate) {
|
||||
return 'finished'
|
||||
}
|
||||
}
|
||||
// 根据报名结束时间判断是否已结束
|
||||
if (regEndTime) {
|
||||
const endDate = new Date(regEndTime)
|
||||
const now = new Date()
|
||||
if (now > endDate) {
|
||||
return 'finished'
|
||||
}
|
||||
}
|
||||
return 'open'
|
||||
},
|
||||
|
||||
|
||||
@@ -19,22 +19,22 @@
|
||||
|
||||
<view class="event-info">
|
||||
<text class="label">地点:</text>
|
||||
<text class="value">{{ item.location }}</text>
|
||||
<text class="value location-text">{{ item.location || "暂无" }}</text>
|
||||
</view>
|
||||
|
||||
<view class="event-info">
|
||||
<text class="label">比赛时间:</text>
|
||||
<text class="value">{{ item.matchTime }}</text>
|
||||
<text class="value">{{ item.matchTime || "暂无" }}</text>
|
||||
</view>
|
||||
|
||||
<view class="event-info">
|
||||
<text class="label">报名项目:</text>
|
||||
<text class="value">{{ item.projects }}</text>
|
||||
<text class="value">{{ item.projects || "暂无" }}</text>
|
||||
</view>
|
||||
|
||||
<view class="event-info">
|
||||
<text class="label">联系人:</text>
|
||||
<text class="value">{{ item.contact }}</text>
|
||||
<text class="value">{{ item.contact || "暂无" }}</text>
|
||||
</view>
|
||||
|
||||
<view class="event-info">
|
||||
@@ -42,8 +42,8 @@
|
||||
<text class="value participants">{{ item.participants }}</text>
|
||||
</view>
|
||||
|
||||
<view class="event-footer" v-if="item.status === 'ongoing'">
|
||||
<view class="register-note">
|
||||
<view class="event-footer">
|
||||
<view class="register-note" v-if="item.status === 'ongoing'">
|
||||
<text>点击后进入【赛事详情】页面</text>
|
||||
</view>
|
||||
<view class="view-cert-btn" @click.stop="handleViewCert(item)">
|
||||
@@ -58,6 +58,26 @@
|
||||
<view class="empty-state" v-if="filteredList.length === 0">
|
||||
<text class="empty-text">暂无报名记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 证件弹窗 -->
|
||||
<view class="cert-modal" v-if="showCertModal" @click="closeCertModal">
|
||||
<view class="cert-modal-content" @click.stop>
|
||||
<view class="cert-modal-header">
|
||||
<text class="cert-modal-title">参赛选手证件</text>
|
||||
<text class="cert-close-icon" @click="closeCertModal">✕</text>
|
||||
</view>
|
||||
<scroll-view class="cert-modal-body" scroll-y>
|
||||
<view class="cert-athlete-item" v-for="(athlete, index) in certAthleteList" :key="index">
|
||||
<view class="cert-athlete-name">{{ athlete.playerName }}</view>
|
||||
<view class="cert-athlete-info">性别:{{ athlete.gender === 1 ? '男' : '女' }}</view>
|
||||
<view class="cert-athlete-info">身份证:{{ athlete.idCard || '暂无' }}</view>
|
||||
</view>
|
||||
<view class="cert-empty" v-if="certAthleteList.length === 0">
|
||||
<text>暂无选手信息</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -80,7 +100,9 @@ export default {
|
||||
current: 1,
|
||||
size: 20
|
||||
},
|
||||
hasMore: true
|
||||
hasMore: true,
|
||||
showCertModal: false,
|
||||
certAthleteList: []
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
@@ -131,8 +153,7 @@ export default {
|
||||
|
||||
const res = await registrationAPI.getRegistrationList(params)
|
||||
|
||||
console.log('=== 我的报名列表 - 后端返回的原始数据 ===')
|
||||
console.log('完整响应:', res)
|
||||
console.log('=== 我的报名列表 ===', res)
|
||||
|
||||
let list = []
|
||||
let total = 0
|
||||
@@ -146,18 +167,18 @@ export default {
|
||||
total = res.length
|
||||
}
|
||||
|
||||
// 为每条报名记录获取详情(包含关联数据)
|
||||
const detailPromises = list.map(item => this.getRegistrationDetailData(item))
|
||||
const mappedList = await Promise.all(detailPromises)
|
||||
// 优化:先收集所有不重复的赛事ID,批量获取赛事信息(使用缓存)
|
||||
const competitionIds = [...new Set(list.map(item => item.competitionId).filter(Boolean))]
|
||||
const competitionMap = await this.batchGetCompetitionInfo(competitionIds)
|
||||
|
||||
// 过滤掉获取失败的记录
|
||||
const validList = mappedList.filter(item => item !== null)
|
||||
// 映射数据(不再单独请求赛事详情)
|
||||
const mappedList = list.map(item => this.mapRegistrationItem(item, competitionMap))
|
||||
|
||||
// 刷新或加载更多
|
||||
if (refresh || !loadMore) {
|
||||
this.eventList = validList
|
||||
this.eventList = mappedList
|
||||
} else {
|
||||
this.eventList = [...this.eventList, ...validList]
|
||||
this.eventList = [...this.eventList, ...mappedList]
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
@@ -174,56 +195,70 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取单条报名记录的详细信息
|
||||
* @param {Object} orderItem 订单基本信息
|
||||
* @returns {Promise<Object>} 包含完整信息的记录
|
||||
* 批量获取赛事信息(带缓存,避免重复请求)
|
||||
* @param {Array} competitionIds 赛事ID数组
|
||||
* @returns {Promise<Object>} 赛事信息Map {id: info}
|
||||
*/
|
||||
async getRegistrationDetailData(orderItem) {
|
||||
try {
|
||||
console.log('=== 获取报名详情 ===', orderItem.id)
|
||||
async batchGetCompetitionInfo(competitionIds) {
|
||||
const competitionMap = {}
|
||||
|
||||
// 获取报名详情
|
||||
const detail = await registrationAPI.getRegistrationDetail(orderItem.id)
|
||||
console.log('报名详情:', detail)
|
||||
// 初始化缓存
|
||||
if (!this.competitionCache) {
|
||||
this.competitionCache = {}
|
||||
}
|
||||
|
||||
// 获取赛事详情
|
||||
let competitionInfo = null
|
||||
if (orderItem.competitionId || detail.competitionId) {
|
||||
const competitionId = orderItem.competitionId || detail.competitionId
|
||||
competitionInfo = await competitionAPI.getCompetitionDetail(competitionId)
|
||||
console.log('赛事详情:', competitionInfo)
|
||||
}
|
||||
// 过滤出未缓存的赛事ID
|
||||
const uncachedIds = competitionIds.filter(id => !this.competitionCache[id])
|
||||
|
||||
// 构建映射数据
|
||||
const mapped = {
|
||||
id: orderItem.id,
|
||||
status: this.getStatus(orderItem.status),
|
||||
title: competitionInfo?.name || detail.competitionName || '未知赛事',
|
||||
location: competitionInfo?.location || competitionInfo?.address || detail.location || '',
|
||||
matchTime: this.formatTimeRange(
|
||||
competitionInfo?.startTime || detail.startTime,
|
||||
competitionInfo?.endTime || detail.endTime
|
||||
) || '',
|
||||
projects: detail.projectNames || this.formatProjects(detail.projects || detail.projectList) || '',
|
||||
contact: orderItem.contactPhone || detail.contactPhone || '',
|
||||
participants: detail.athleteNames || this.formatParticipants(detail.athletes || detail.athleteList) || ''
|
||||
}
|
||||
// 只请求未缓存的赛事(去重后通常只有1-2个)
|
||||
if (uncachedIds.length > 0) {
|
||||
console.log('需要请求的赛事ID(去重后):', uncachedIds)
|
||||
|
||||
console.log('映射后的数据:', mapped)
|
||||
return mapped
|
||||
} catch (err) {
|
||||
console.error('获取报名详情失败:', err, orderItem.id)
|
||||
// 返回基本信息,避免整个记录丢失
|
||||
return {
|
||||
id: orderItem.id,
|
||||
status: this.getStatus(orderItem.status),
|
||||
title: '获取详情失败',
|
||||
location: '',
|
||||
matchTime: '',
|
||||
projects: '',
|
||||
contact: orderItem.contactPhone || '',
|
||||
participants: `${orderItem.totalParticipants || 0}人`
|
||||
}
|
||||
// 并行请求所有未缓存的赛事详情
|
||||
const promises = uncachedIds.map(async (id) => {
|
||||
try {
|
||||
const info = await competitionAPI.getCompetitionDetail(id)
|
||||
this.competitionCache[id] = info
|
||||
} catch (err) {
|
||||
console.error('获取赛事详情失败:', id, err)
|
||||
this.competitionCache[id] = null
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
// 从缓存中获取所有赛事信息
|
||||
competitionIds.forEach(id => {
|
||||
competitionMap[id] = this.competitionCache[id] || null
|
||||
})
|
||||
|
||||
return competitionMap
|
||||
},
|
||||
|
||||
/**
|
||||
* 映射单条报名记录(使用已缓存的赛事信息)
|
||||
* @param {Object} item 订单信息
|
||||
* @param {Object} competitionMap 赛事信息Map
|
||||
* @returns {Object} 映射后的数据
|
||||
*/
|
||||
mapRegistrationItem(item, competitionMap) {
|
||||
const competitionInfo = competitionMap[item.competitionId] || {}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
competitionId: item.competitionId,
|
||||
status: this.getStatus(item.status),
|
||||
title: competitionInfo.competitionName || competitionInfo.name || item.competitionName || '未知赛事',
|
||||
location: competitionInfo.location || competitionInfo.address || item.location || '',
|
||||
matchTime: this.formatTimeRange(
|
||||
competitionInfo.competitionStartTime || competitionInfo.startTime,
|
||||
competitionInfo.competitionEndTime || competitionInfo.endTime
|
||||
) || '',
|
||||
projects: item.projectNames || this.formatProjects(item.projects) || '',
|
||||
contact: item.contactPhone || item.contactPerson || '',
|
||||
participants: item.athleteNames || `${item.totalParticipants || 0}人`,
|
||||
athleteList: item.athleteList || []
|
||||
}
|
||||
},
|
||||
|
||||
@@ -306,14 +341,24 @@ export default {
|
||||
},
|
||||
goToEventDetail(item) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/event-detail/event-detail?id=' + item.id
|
||||
url: '/pages/event-detail/event-detail?id=' + item.competitionId
|
||||
});
|
||||
},
|
||||
handleViewCert(item) {
|
||||
uni.showToast({
|
||||
title: '查看证件',
|
||||
icon: 'none'
|
||||
});
|
||||
// Get athlete list from the item
|
||||
if (item.athleteList && item.athleteList.length > 0) {
|
||||
this.certAthleteList = item.athleteList;
|
||||
this.showCertModal = true;
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '暂无选手证件信息',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
closeCertModal() {
|
||||
this.showCertModal = false;
|
||||
this.certAthleteList = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -384,6 +429,14 @@ export default {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.location-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
max-width: 480rpx;
|
||||
}
|
||||
|
||||
.participants {
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -426,4 +479,77 @@ export default {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
/* Cert Modal Styles */
|
||||
.cert-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.cert-modal-content {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
width: 85%;
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cert-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.cert-modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cert-close-icon {
|
||||
font-size: 36rpx;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cert-modal-body {
|
||||
max-height: 50vh;
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.cert-athlete-item {
|
||||
padding: 20rpx;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.cert-athlete-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.cert-athlete-info {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.cert-empty {
|
||||
text-align: center;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -66,8 +66,9 @@ export default {
|
||||
.type-btn {
|
||||
background-color: #C93639;
|
||||
color: #fff;
|
||||
padding: 20rpx 60rpx;
|
||||
padding: 20rpx 50rpx;
|
||||
border-radius: 50rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</view>
|
||||
<view class="status-item">
|
||||
<text class="label">参赛人数:</text>
|
||||
<text class="value">{{ scheduleData.totalParticipants || 0 }}</text>
|
||||
<text class="value">{{ scheduleData.registerCount || 0 }}</text>
|
||||
</view>
|
||||
<view class="status-item" v-if="scheduleData.lastAutoScheduleTime">
|
||||
<text class="label">最后编排时间:</text>
|
||||
|
||||
@@ -39,19 +39,27 @@ export default {
|
||||
onLoad(options) {
|
||||
if (options.eventId) {
|
||||
this.eventId = options.eventId;
|
||||
this.loadProjectList(options.eventId)
|
||||
}
|
||||
if (options.type) {
|
||||
this.type = options.type;
|
||||
}
|
||||
if (this.eventId) {
|
||||
this.loadProjectList(this.eventId)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 加载报名项目列表
|
||||
*/
|
||||
async loadProjectList(eventId) {
|
||||
try {
|
||||
const res = await competitionAPI.getProjectList({ competitionId: eventId })
|
||||
const params = { competitionId: eventId }
|
||||
|
||||
// Filter by participation type: single=1, team=2
|
||||
if (this.type === 'single') {
|
||||
params.type = 1
|
||||
} else if (this.type === 'team') {
|
||||
params.type = 2
|
||||
}
|
||||
|
||||
const res = await competitionAPI.getProjectList(params)
|
||||
|
||||
let list = []
|
||||
if (res.records) {
|
||||
@@ -60,11 +68,11 @@ export default {
|
||||
list = res
|
||||
}
|
||||
|
||||
// 数据映射
|
||||
this.projectList = list.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name || item.projectName,
|
||||
price: item.price || item.registrationFee || 0,
|
||||
type: item.type || (this.type === 'team' ? 2 : 1),
|
||||
selected: false
|
||||
}))
|
||||
} catch (err) {
|
||||
@@ -80,6 +88,7 @@ export default {
|
||||
item.selected = !item.selected;
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
||||
handleRegister() {
|
||||
const selectedProjects = this.projectList.filter(item => item.selected);
|
||||
if (selectedProjects.length === 0) {
|
||||
@@ -90,8 +99,16 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// Include type in selected projects data
|
||||
const projectsData = selectedProjects.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
price: p.price,
|
||||
type: p.type
|
||||
}))
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/event-register/event-register?eventId=${this.eventId}&projects=${encodeURIComponent(JSON.stringify(selectedProjects))}`
|
||||
url: `/pages/event-register/event-register?eventId=${this.eventId}&projects=${encodeURIComponent(JSON.stringify(projectsData))}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,30 @@ export function setRefreshToken(token) {
|
||||
* 获取用户信息
|
||||
*/
|
||||
export function getUserInfo() {
|
||||
const userInfo = uni.getStorageSync(USER_INFO_KEY)
|
||||
return userInfo ? JSON.parse(userInfo) : null
|
||||
try {
|
||||
const userInfo = uni.getStorageSync(USER_INFO_KEY)
|
||||
|
||||
// 如果没有数据,返回 null
|
||||
if (!userInfo) {
|
||||
console.log('本地存储中没有用户信息')
|
||||
return null
|
||||
}
|
||||
|
||||
// 如果已经是对象,直接返回
|
||||
if (typeof userInfo === 'object') {
|
||||
return userInfo
|
||||
}
|
||||
|
||||
// 如果是字符串,尝试解析
|
||||
if (typeof userInfo === 'string') {
|
||||
return JSON.parse(userInfo)
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
/**
|
||||
* Vue CLI 配置文件
|
||||
* 用于配置开发服务器代理,解决跨域问题
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// 输出目录
|
||||
outputDir: 'dist/build/h5',
|
||||
|
||||
// 静态资源目录
|
||||
assetsDir: 'static',
|
||||
|
||||
publicPath: './',
|
||||
productionSourceMap: false,
|
||||
css: {
|
||||
extract: true
|
||||
},
|
||||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
port: 8084,
|
||||
disableHostCheck: true,
|
||||
proxy: {
|
||||
// 代理所有 /api 开头的请求
|
||||
'/api': {
|
||||
target: 'http://localhost:8123', // 后端服务地址
|
||||
changeOrigin: true, // 改变请求源
|
||||
ws: true, // 支持websocket
|
||||
target: 'http://localhost:8123',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
pathRewrite: {
|
||||
// 将 /api 重写为空,因为后端没有 /api 前缀
|
||||
'^/api': ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 确保 transpileDependencies 配置正确
|
||||
transpileDependencies: [],
|
||||
|
||||
// 生产环境配置
|
||||
productionSourceMap: false
|
||||
chainWebpack: config => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
config.performance.hints(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user