Compare commits

..
65 Commits
Author SHA1 Message Date
DevOpsandfactory-droid[bot] b67a1e039c feat: 移除项目编码手动输入
- 项目编码由后端自动生成
- 移除表单中的项目编码输入框和验证规则

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 17:19:33 +08:00
DevOpsandfactory-droid[bot] c7e78612bf feat: 添加单位统计API调用
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 16:08:47 +08:00
DevOpsandfactory-droid[bot] 49c1cd81c6 重构项3: 添加表号生成和显示功能
- 在编排页面项目头部添加表号显示
- 实现generateTableNo方法,格式为: 场地(1位)+时段(1位)+序号(2位)
- 时段规则: 上午=1, 下午=2
- 序号在同场地同时段中按id排序确定

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 15:46:48 +08:00
DevOpsandfactory-droid[bot] 420bd29eff 重构项1: 移除项目管理中的场地分配功能
- 移除表格中的所属场地列
- 移除表单中的场地选择字段
- 移除场地相关的API导入和数据定义
- 移除handleCompetitionChangeInForm、loadVenuesForProjects、getVenueName等函数
- 后端已实现动态场地分配,无需前端指定

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-08 15:37:07 +08:00
DevOpsandfactory-droid[bot] a1b26208a4 feat: 赛事管理页面状态根据时间自动计算
- 添加 calculateStatus 方法根据报名时间和比赛时间计算状态
- 状态显示不再依赖数据库字段,而是实时计算

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 12:28:07 +08:00
DevOpsandfactory-droid[bot] 41c67e1ddf fix: 将订单管理改为赛事管理
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-07 11:58:03 +08:00
DevOpsandfactory-droid[bot] e5b028f084 fix: 修复场地类型(venueType)加载和保存问题
- 在loadVenues中添加venueType字段映射,确保从后端加载时正确回显
- 在saveVenues中添加venueType字段,确保保存时正确提交
- 修复附件上传headers认证问题

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 15:17:28 +08:00
DevOps 6385acd43b fix(deduction): 修复扣分项编辑功能
- form对象添加itemName字段解决编辑时名称不显示
- 查询时过滤空字符串参数避免无数据问题
- 字段名deductionPoints改为deductionPoint与后端一致
2026-01-06 14:56:10 +08:00
DevOpsandfactory-droid[bot] c37b6d8f6f fix(competition): 修复附件上传按钮失灵问题
- 添加 getToken 导入
- 为 avue-form 上传组件添加 Blade-Auth headers 认证

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 12:52:28 +08:00
DevOpsandfactory-droid[bot] 98c831eff0 fix: 修复报名详情页面文案和统计逻辑
- 修正错误文案: 单位型号人数 → 报名人数, 剩下显时(公共) → 预计时长(分钟)
- 重新设计参赛人数统计表格: 单位/单人项目/集体项目/男/女/合计
- 修复统计逻辑按项目类型和性别正确统计

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 11:47:56 +08:00
DevOpsandfactory-droid[bot] 586ad7e66e fix: 移除项目列表中的参赛人数限制列
- 从competition/index.vue移除参赛人数限制列
- 从competition/create.vue移除参赛人数限制列
- 该功能已不再使用

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 10:59:51 +08:00
DevOpsandfactory-droid[bot] 8656aa5abc feat(project): add venue selection for project management
- Add venueId field to project form
- Load venue list when competition is selected
- Allow assigning projects to specific venues

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-05 15:59:09 +08:00
DevOps c1f5acb644 Revert "feat(judgeInvite): add project assignment editing feature"
This reverts commit ecd569337d.
2026-01-05 15:20:01 +08:00
DevOpsandfactory-droid[bot] ecd569337d feat(judgeInvite): add project assignment editing feature
- Add edit projects button in judge invite list
- Add edit projects dialog with project multi-select
- Add updateInviteProjects API method
- Fix: load project list before opening edit dialog

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-05 15:08:04 +08:00
DevOps be8b887a1c feat(judgeInvite): 增加场地过滤条件 2025-12-31 16:20:50 +08:00
DevOps 6a5b220f6e fix(project): 将最大参赛人数改为单位容纳人数,用于编排分组计算 2025-12-30 16:55:20 +08:00
DevOps 8f14a165e5 fix(project): 移除项目编辑/新增页面的报名时间字段 2025-12-30 16:48:56 +08:00
DevOps 21fc12b18d feat(schedule): 前端支持动态时间段配置
- 添加 getScheduleConfig API 调用
- 更新 generateTimeSlots 从后端获取时间配置
- 添加 loadScheduleConfig 和 formatTimeForDisplay 方法
- 时间段不再硬编码,从 ScheduleConfig 动态获取
2025-12-30 10:51:28 +08:00
DevOps 21274e9639 fix(schedule): 修复markPlayerAsException重复定义导致的事件绑定问题 2025-12-29 17:28:03 +08:00
DevOps 564374250b fix(api): 修复updateCheckInStatus API路径 2025-12-29 15:42:43 +08:00
DevOps 77c2c51d8a fix(schedule): 修复展开选手详情中的异常状态持久化API调用
- markPlayerAsException方法添加updateCheckInStatus API调用
- removePlayerException方法添加updateCheckInStatus API调用
- 添加编排完成状态检查
2025-12-29 15:33:19 +08:00
DevOps 578b94aa39 feat: add estimated duration field and exception status persistence
- Add estimatedDuration field to project form with validation
- Add estimatedDuration column to project table
- Add updateCheckInStatus API for exception status persistence
- Call backend API when marking/removing exception status
2025-12-29 15:08:29 +08:00
DevOps a9b82d7aae docs: 更新README,简化内容并更新域名 2025-12-29 14:21:48 +08:00
DevOps f412a9c759 fix: 项目管理表格显示所属赛事名称 2025-12-29 13:24:14 +08:00
Developer 0b9f107b2a fix: 修复项目编辑时报名费字段映射问题 2025-12-29 11:48:03 +08:00
DevOps 5bbe374ebf fix: 修复项目管理页面编辑项目API调用错误
- 将updateProject改为submitProject,后端submit接口同时支持新增和修改
2025-12-29 11:21:55 +08:00
DevOps 39ff98e6c0 fix: 修复项目管理页面新增项目API调用错误
- 将addProject改为submitProject,匹配后端API端点/martial/project/submit
2025-12-29 11:16:13 +08:00
DevOpsandfactory-droid[bot] f1c2501afc 裁判邀请表格添加负责场地列
- 显示裁判负责的场地名称
- 总裁显示"全部场地"

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-29 10:32:46 +08:00
DevOpsandfactory-droid[bot] 657c4210a4 添加总裁(裁判长)角色支持到裁判邀请和评委管理页面
- judgeInvite/index.vue: 添加总裁选项和显示
- referee/index.vue: 添加总裁筛选和显示

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 20:06:08 +08:00
DevOps a98b18275f feat: 添加项目类型筛选和显示功能
- 添加项目类型下拉筛选框(套路/散打/器械/对练)
- 在表格中添加项目类型列
- 支持项目类型查询参数
2025-12-28 19:03:48 +08:00
DevOps 6267d87b18 fix: 修复项目管理页面筛选功能
- 将分组类别从下拉框改为文本输入框,支持模糊搜索
- 移除项目类型筛选(后端不支持该字段)
- 修复handleReset中的缩进问题
2025-12-28 17:28:18 +08:00
DevOpsandfactory-droid[bot] 67ffd4fc23 fix: 移除赛事筛选的 status 限制,显示所有赛事
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 17:12:18 +08:00
DevOpsandfactory-droid[bot] 6befd3644a fix: 修复项目管理页面数据显示问题
- 修复 API 响应数据解析 (res.data.data.records)
- 移除后端不支持的 eventType 参数
- 修复报名费字段映射 (registrationFee -> price)
- 修复分组类别显示为文本而非数字

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 17:09:38 +08:00
DevOpsandfactory-droid[bot] a6768c394a fix: change participantType to type to match backend field name
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-28 16:37:31 +08:00
DevOpsandClaude Opus 4.5 ac7587ef7e refactor: 裁判角色名称修改 - 裁判长→主裁判, 普通裁判→裁判员
- 修改referee/index.vue中的UI显示文字
- 修改judgeInvite/index.vue中的UI显示文字
- 更新referee.js中的注释

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 11:37:29 +08:00
DevOpsandClaude Opus 4.5 4f1d0b5888 feat(judgeInvite): 移除项目分配功能,裁判默认负责整个场地所有项目
- 移除导入裁判时的项目选择下拉框
- 移除项目必选验证
- 不传projects参数,后端自动获取场地所有项目

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 16:35:27 +08:00
DevOpsandClaude Opus 4.5 cc6fabe576 feat: 裁判邀请导入功能添加场地和项目选择
- 导入对话框添加场地下拉选择
- 导入对话框添加项目多选
- 调用API时传递venueId和projects参数

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 15:45:10 +08:00
hongjianli 04cd85cbe3 fix bugs 2025-12-26 13:15:26 +08:00
hongjianli c12fb79444 Merge branch 'main' of git.waypeak.work:martial/martial-web 2025-12-26 11:06:52 +08:00
hongjianli 1744adcf92 fix bugs 2025-12-26 11:06:38 +08:00
DevOpsandClaude Opus 4.5 7f8c5c630b 修复导出功能:正确处理blob响应
- axios拦截器跳过blob类型响应的code检查
- 从res.data获取blob数据而非res

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 13:58:21 +08:00
DevOpsandClaude Opus 4.5 694b955cef 实现赛程表导出功能
- 添加 exportSchedule API 调用后端导出接口
- 实现 handleExport 方法,支持下载 Excel 文件
- 文件名格式:赛程表_赛事名称.xlsx

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 13:45:09 +08:00
DevOpsandClaude Opus 4.5 ea4650b912 场地Tab添加场地过滤功能
- 添加场地按钮列表,支持切换不同场地
- 新增 selectedVenueIdForVenueTab 状态变量
- 修改 venueData 计算属性支持按场地过滤
- 初始化时默认选中第一个场地

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 13:34:35 +08:00
DevOpsandClaude Opus 4.5 e035647b51 feat: 竞赛分组页面项目支持折叠,默认收起
- 点击项目头部可展开/收起队伍列表
- 默认所有项目收起,方便管理多个项目
- 添加展开图标指示当前状态

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 12:55:55 +08:00
DevOpsandClaude Opus 4.5 352727b4fb feat: 竞赛分组页面添加队伍展开功能,显示选手签到状态和异常标记
- 点击队伍行可展开显示选手详情
- 显示选手签到状态:未签到/已签到/异常
- 支持标记异常和取消异常操作
- 优化评分页面代码

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 12:49:33 +08:00
DevOpsandClaude Opus 4.5 5e75688e13 feat: 赛程编排表格按队伍分组显示选手
continuous-integration/drone/push Build is pending
- 将同一单位的选手合并为一个队伍行
- 多选手队伍可展开查看具体选手
- 队伍状态根据所有选手状态计算(已签到/未签到/部分签到/部分异常)
- 上移/下移操作移动整个队伍
- 异常标记:单人队伍在主行标记,多人队伍需展开后标记单个选手
- 修复语法错误(转义字符和字符串引号)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 17:17:34 +08:00
DevOpsandClaude 226d92f725 fix: 修复P2和P3问题
continuous-integration/drone/push Build was killed
1. P2: 修复订单管理this.$set不兼容Vue3问题,改为直接赋值
2. P3: 修复选手年龄显示-1问题,改为显示"--"

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-22 13:25:38 +08:00
DevOpsandClaude 3d314fe84f chore: 更新package-lock.json
continuous-integration/drone/push Build is passing
🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 14:44:37 +08:00
hongjianli 179f7ea85d Merge branch 'main' of git.waypeak.work:martial/martial-web
continuous-integration/drone/push Build is passing
2025-12-17 09:31:23 +08:00
hongjianli d8730cc2c2 fix bugs 2025-12-17 09:31:14 +08:00
Developer 96f3b56eff fix: 修复 API 代理路径,去掉多余的 /api 前缀
continuous-integration/drone/push Build is passing
🤖 Generated with Claude Code
2025-12-13 23:11:31 +08:00
hongjianli 1a99a45729 fix bugs
continuous-integration/drone/push Build is passing
2025-12-13 09:34:06 +08:00
hongjianli 669f29878b fix bugs
continuous-integration/drone/push Build is passing
2025-12-12 17:54:40 +08:00
hongjianli 5b806e29b7 fix bugs
continuous-integration/drone/push Build is failing
2025-12-11 16:56:19 +08:00
hongjianli ab69968bda 修改前端页面
continuous-integration/drone/push Build is failing
2025-12-03 13:51:22 +08:00
hongjianli aa6facf13a fix bugs
continuous-integration/drone/push Build is passing
2025-11-30 12:15:21 +08:00
n72595987@gmail.comandClaude e77e73074a docs: 更新前端项目文档
continuous-integration/drone/push Build is passing
- 修复字符编码问题(之前显示乱码)
- 添加完整的项目说明和技术栈介绍
- 记录 Docker 部署方法和 CI/CD 流程
- 添加常见问题解答(登录接口错误、容器启动失败等)
- 完善生产架构图和网络拓扑说明
- 补充开发规范和配置说明

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 00:24:05 +08:00
n72595987@gmail.comandClaude a65daba7a9 fix: 修复 Drone 部署步骤使用正确的 Dockerfile
continuous-integration/drone/push Build is passing
- 使用 Dockerfile.deploy 而非默认 Dockerfile
- 直接使用 docker run 而不是 docker-compose(避免依赖问题)
- 强制删除旧容器后创建新容器(确保使用新镜像)
- 修复镜像构建后容器没有更新的问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 00:05:01 +08:00
n72595987@gmail.comandClaude 134208d510 fix: 修复前端 nginx 配置和 API 基础路径
continuous-integration/drone/push Build is passing
- nginx.conf: 将后端代理地址从 martial-backend 改为 172.21.0.1:8123(宿主机网关)
- .env.production: 移除 VITE_APP_API 的 /api 前缀
- 修复登录时 "No endpoint POST /api/blade-auth/oauth/token" 错误
- 修复容器启动失败 "host not found in upstream martial-backend" 错误

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 00:01:30 +08:00
n72595987@gmail.comandClaude cbf161048c fix: 修复前端 API 路径配置
continuous-integration/drone/push Build is passing
- 移除 VITE_APP_API 的 /api 前缀
- BladeX 框架的端点已包含完整路径(/blade-auth, /blade-system 等)
- 修复登录时 "No endpoint POST /api/blade-auth/oauth/token" 错误

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 23:55:30 +08:00
n72595987@gmail.comandClaude 890218199b 修复前端 CI/CD:改为在部署服务器上构建镜像
continuous-integration/drone/push Build is passing
问题:
- plugins/docker 尝试推送镜像到 Docker Hub
- 没有配置认证导致 'denied: requested access to resource is denied'

解决方案:
- 移除 plugins/docker 步骤
- 添加 drone-scp 步骤传输构建产物(dist、Dockerfile、nginx.conf)
- 在部署服务器上执行 docker build
- 直接使用本地镜像启动容器

优势:
- 不需要配置 Docker Hub 认证
- 不需要推送和拉取镜像,更快
- 镜像只存在于部署服务器本地
- 修正健康检查 URL 为 http://154.30.6.21/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 18:04:56 +08:00
n72595987@gmail.comandClaude 36116e0973 修复前端构建:正确配置 BladeX 私有 npm registry
continuous-integration/drone/push Build is failing
问题:
- Dockerfile 中 npm install 时未复制 .npmrc 文件
- 使用 --registry 参数覆盖了 .npmrc 中的私有 registry 配置
- 导致无法下载 @saber scope 的私有包,报 E401 认证错误

解决方案:
- Dockerfile: 在 npm install 前复制 .npmrc 文件
- Dockerfile: 移除 --registry 参数,使用 .npmrc 中的配置
- .drone.yml: 同样移除 --registry 参数

现在 npm 会正确使用:
- @saber 包 -> https://center.javablade.com (私有 registry)
- 其他包 -> npm 默认源

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 17:57:25 +08:00
n72595987@gmail.comandClaude 1d8d60ccaa 修复 Drone CI 配置:移除 host volumes 以解决 untrusted 仓库问题
continuous-integration/drone/push Build is failing
- 移除 npm-cache volume 挂载
- 同一构建内的步骤仍可共享 node_modules
- 使用国内镜像加速依赖下载

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 17:45:39 +08:00
n72595987@gmail.comandClaude 40b1e48f11 添加 Drone CI/CD 前端自动化部署配置
continuous-integration/drone/push Build encountered an error
1. 新增 .drone.yml 配置文件
   - 自动安装 npm 依赖
   - 自动构建生产版本
   - 构建 Docker 镜像
   - 自动部署到生产服务器
   - 健康检查验证部署成功

2. 新增 Dockerfile
   - 多阶段构建:Node 编译 + Nginx 运行
   - 优化镜像体积,使用 alpine 基础镜像
   - 配置静态资源缓存
   - 添加健康检查

3. 新增 nginx.conf
   - 配置前端路由支持(Vue Router history 模式)
   - 代理 API 请求到后端服务
   - 优化 Gzip 压缩和静态资源缓存
   - 支持 BladeX 系统模块路径代理

现在推送代码后,前端会自动部署到生产环境!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 17:04:57 +08:00
n72595987@gmail.comandClaude dd9927014e 修复Vite代理配置,解决前后端通信问题
1. 修复后端API代理配置
   - 更新代理目标端口从82改为8123(匹配实际后端端口)
   - 区分BladeX系统模块和业务模块的URL路径处理

2. 解决验证码接口404问题
   - 新增 ^/api/blade- 规则处理系统模块(blade-auth、blade-system等)
   - 系统模块需去掉 /api 前缀转发到后端 /blade-auth/* 路径
   - 业务模块保留 /api 前缀转发到后端 /api/martial/* 路径

3. 验证测试结果
   -  /api/blade-auth/oauth/captcha 验证码接口正常
   -  /api/blade-system/tenant/info 租户信息接口正常
   -  /api/martial/competition/list 比赛列表接口正常

现在前端登录页面可以正常获取验证码,所有API接口都能正确访问。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 16:31:05 +08:00
10 changed files with 231 additions and 446 deletions
-37
View File
@@ -1,37 +0,0 @@
# 待处理问题
## Issue 1: 调度页面"状态"列数据应从后端获取
### 问题描述
调度页面中的"状态"列(已完赛/未完赛)目前是前端硬编码为 `false`,并非从后端 API 获取的真实状态。
### 当前行为
- 前端在加载调度数据时,将 `completed` 字段默认设置为 `false`
- 所有参赛者的状态都显示为"未完赛"
### 期望行为
- 从后端 API 获取每个参赛者的真实完赛状态
- 正确显示"已完赛"或"未完赛"
### 相关代码
**文件:** `src/views/martial/dispatch/index.vue`
```javascript
// 第320行 - 当前硬编码
completed: false,
// 第69-70行 - 状态显示逻辑
<el-tag v-if="scope.row.completed" type="success" size="small">已完赛</el-tag>
<el-tag v-else type="warning" size="small">未完赛</el-tag>
```
### 待办
- [ ] 后端 API 返回参赛者的完赛状态字段
- [ ] 前端从 API 响应中读取真实状态
---
+1 -10
View File
@@ -188,15 +188,6 @@ export const saveDispatch = (data) => {
* 导出赛程表
* @param {Number} competitionId - 赛事ID
*/
// 预览赛程表数据
export const getScheduleExportPreview = (competitionId, template, venueId) => {
return request({
url: "/api/martial/export/schedule/preview",
method: "get",
params: { competitionId, template, venueId }
})
}
export const exportSchedule = (competitionId) => {
return request({
url: '/martial/export/schedule',
@@ -206,7 +197,7 @@ export const exportSchedule = (competitionId) => {
})
}
// Export schedule template 2 (competition time format)
export const exportScheduleTemplate2 = (competitionId, venueId, venueName, timeSlot) => {
return request({
url: '/martial/export/schedule2',
+156 -69
View File
@@ -85,10 +85,10 @@
>
<template #default="scope">
<el-tag
:type="getStatusType(calculateStatus(scope.row))"
:type="getStatusType(scope.row.status)"
size="small"
>
{{ getStatusText(calculateStatus(scope.row)) }}
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
@@ -647,50 +647,123 @@
项目列表
</div>
<el-alert
title="项目配置提示"
type="info"
:closable="false"
show-icon
style="margin-bottom: 15px;"
>
<template #default>
<p>请先保存赛事基本信息然后前往 <strong>项目管理</strong> 功能配置比赛项目</p>
<el-button
v-if="competitionId"
type="primary"
size="small"
style="margin-top: 10px;"
@click="goToProjectManagement"
>
前往项目管理
</el-button>
</template>
</el-alert>
<el-form-item label="" class="button-right">
<el-button
v-if="currentView !== 'view'"
type="primary"
icon="el-icon-plus"
size="small"
@click="handleAddProject"
>
添加项目
</el-button>
</el-form-item>
<div v-if="formData.projects && formData.projects.length > 0">
<el-table :data="formData.projects" border size="small" style="width: 100%">
<el-table-column prop="projectCode" label="项目编码" width="120" />
<el-table-column prop="projectName" label="项目名称" min-width="150" />
<el-table-column prop="category" label="组别" width="120" />
<el-table-column label="项目类型" width="100" align="center">
<template #default="scope">
<el-tag v-if="scope.row.eventType === 1" type="primary" size="small">套路</el-tag>
<el-tag v-else-if="scope.row.eventType === 2" type="danger" size="small">散打</el-tag>
<el-tag v-else-if="scope.row.eventType === 3" type="success" size="small">器械</el-tag>
<el-tag v-else-if="scope.row.eventType === 4" type="warning" size="small">对练</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="price" label="报名费(元)" width="100" align="center">
<template #default="scope">
<span style="color: #f56c6c">¥{{ scope.row.price || 0 }}</span>
</template>
</el-table-column>
<el-table-column prop="maxParticipants" label="容纳人数" width="100" align="center" />
</el-table>
</div>
<el-empty v-else description="暂无项目,请前往项目管理添加" :image-size="60" />
<el-table
:data="formData.projects"
border
style="width: 100%"
>
<el-table-column
label="项目名称"
min-width="150"
>
<template #default="scope">
<el-input
v-if="currentView !== 'view'"
v-model="scope.row.projectName"
placeholder="例如:太极拳"
size="small"
/>
<span v-else>{{ scope.row.projectName }}</span>
</template>
</el-table-column>
<el-table-column
label="项目代码"
width="120"
>
<template #default="scope">
<el-input
v-if="currentView !== 'view'"
v-model="scope.row.projectCode"
placeholder="例如:TJQ001"
size="small"
/>
<span v-else>{{ scope.row.projectCode }}</span>
</template>
</el-table-column>
<el-table-column
label="组别"
width="120"
>
<template #default="scope">
<el-input
v-if="currentView !== 'view'"
v-model="scope.row.category"
placeholder="例如:成年男子组"
size="small"
/>
<span v-else>{{ scope.row.category }}</span>
</template>
</el-table-column>
<el-table-column
label="报名费用(元)"
width="130"
align="center"
>
<template #default="scope">
<el-input-number
v-if="currentView !== 'view'"
v-model="scope.row.price"
:min="0"
:precision="2"
:step="10"
size="small"
style="width: 100%"
placeholder="0.00"
/>
<span v-else style="color: #f56c6c">¥{{ scope.row.price || 0 }}</span>
</template>
</el-table-column>
<el-table-column
label="项目说明"
min-width="200"
>
<template #default="scope">
<el-input
v-if="currentView !== 'view'"
v-model="scope.row.description"
placeholder="请输入项目说明"
size="small"
/>
<span v-else>{{ scope.row.description }}</span>
</template>
</el-table-column>
<el-table-column
v-if="currentView !== 'view'"
label="操作"
width="80"
align="center"
>
<template #default="scope">
<el-button
type="danger"
link
size="small"
icon="el-icon-delete"
@click="handleDeleteProject(scope.$index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 场地配置 -->
@@ -1201,7 +1274,18 @@ export default {
console.log('项目列表长度:', projectList.length);
if (projectList.length > 0) {
this.formData.projects = projectList.map(item => ({ ...item }));
this.formData.projects = projectList.map(item => ({
id: item.id,
projectName: item.projectName,
projectCode: item.projectCode,
category: item.category,
type: item.type,
minParticipants: item.minParticipants,
maxParticipants: item.maxParticipants,
estimatedDuration: item.estimatedDuration,
price: item.price,
description: item.description
}));
console.log('✅ 加载的项目列表:', this.formData.projects);
console.log('✅ 项目数量:', this.formData.projects.length);
} else {
@@ -1561,16 +1645,20 @@ export default {
this.formData.schedule.splice(index, 1);
},
// 项目列表管理 - 跳转到项目管理页面
goToProjectManagement() {
if (this.competitionId) {
this.$router.push({
path: '/martial/project/list',
query: { competitionId: this.competitionId }
});
} else {
this.$message.warning('请先保存赛事基本信息');
}
// 项目列表管理
handleAddProject() {
this.formData.projects.push({
projectName: '',
projectCode: '',
category: '',
maxParticipants: null,
price: 0,
description: ''
});
},
handleDeleteProject(index) {
this.formData.projects.splice(index, 1);
},
// 场地配置管理
@@ -1619,10 +1707,10 @@ export default {
savePromises.push(this.saveActivitySchedules(savedCompetitionId));
}
// 2. 项目列表 - 已移至项目管理功能配置
// if (this.formData.projects && this.formData.projects.length > 0) {
// savePromises.push(this.saveProjects(savedCompetitionId));
// }
// 2. 保存项目列表
if (this.formData.projects && this.formData.projects.length > 0) {
savePromises.push(this.saveProjects(savedCompetitionId));
}
// 3. 保存场地配置
if (this.formData.venues && this.formData.venues.length > 0) {
@@ -1706,7 +1794,9 @@ export default {
},
async saveProjects(competitionId) {
const projects = this.formData.projects.filter(item => item.projectName);
const projects = this.formData.projects.filter(item =>
item.projectName && item.projectCode
);
if (projects.length === 0) {
return Promise.resolve();
@@ -1724,19 +1814,16 @@ export default {
const promises = projects.map(project => {
const projectData = {
...project,
competitionId: finalCompetitionId,
projectName: project.projectName,
projectCode: project.projectCode,
category: project.category || '',
eventType: project.eventType || 1,
type: project.type || 1,
type: project.type || 1, // 默认个人项目
minParticipants: project.minParticipants || 1,
maxParticipants: project.maxParticipants || 100,
maxParticipants: project.maxParticipants || 1,
estimatedDuration: project.estimatedDuration || 5,
price: project.price || 0,
sortOrder: project.sortOrder || 0,
description: project.description || '',
rules: project.rules || ''
description: project.description || ''
};
// 如果有 id,说明是编辑已有的项目
@@ -1966,7 +2053,7 @@ export default {
return 3;
}
// 报名中
if (regStart && regEnd && now >= regStart && now <= new Date(regEnd.getFullYear(), regEnd.getMonth(), regEnd.getDate(), 23, 59, 59)) {
if (regStart && regEnd && now >= regStart && now <= regEnd) {
return 2;
}
// 未开始(默认状态)
+2 -2
View File
@@ -60,10 +60,10 @@
>
<template #default="scope">
<el-tag
:type="getStatusType(calculateStatus(scope.row))"
:type="getStatusType(scope.row.status)"
size="small"
>
{{ getStatusText(calculateStatus(scope.row)) }}
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
+13 -50
View File
@@ -130,7 +130,6 @@ export default {
selectedVenueId: null,
venues: [], // 场地列表
timeSlots: [], // 时间段列表
timeSlotIndexMap: {}, // 显示索引到实际时间段索引的映射
dispatchGroups: [], // 调度分组列表
hasChanges: false, // 是否有未保存的更改
originalData: null // 原始数据(用于取消时恢复)
@@ -156,13 +155,12 @@ export default {
async checkScheduleStatus() {
try {
const res = await getScheduleResult(this.competitionId)
const scheduleData = res.data?.data
const isCompleted = scheduleData?.isCompleted || false
const scheduleStatus = res.data?.data?.scheduleStatus || res.data?.scheduleStatus || 0
console.log('编排完成状态:', isCompleted)
console.log('编排状态:', scheduleStatus)
// 如果编排未完成,提示用户并返回
if (!isCompleted) {
// 如果编排未完成(状态不是2,提示用户并返回
if (scheduleStatus !== 2) {
this.$message.warning('请先完成编排并锁定后,才能进行调度操作')
// 延迟返回上一页
setTimeout(() => {
@@ -190,16 +188,8 @@ export default {
const data = res.data?.data
if (data) {
// 获取编排数据以确定有项目的时间段
let scheduleGroups = []
try {
const scheduleRes = await getScheduleResult(this.competitionId)
scheduleGroups = scheduleRes.data?.data?.competitionGroups || scheduleRes.data?.data?.scheduleGroups || []
} catch (e) {
console.warn('获取编排数据失败:', e)
}
// 生成时间段(只显示有项目的时间段)
this.generateTimeSlots(data.competitionStartTime || data.competition_start_time, data.competitionEndTime || data.competition_end_time, scheduleGroups)
// 生成时间段
this.generateTimeSlots(data.competitionStartTime || data.competition_start_time, data.competitionEndTime || data.competition_end_time)
}
} catch (err) {
console.error('加载赛事信息失败', err)
@@ -209,29 +199,17 @@ export default {
}
},
// 生成时间段列表(只显示有项目的时间段)
generateTimeSlots(startTime, endTime, scheduleGroups) {
// 生成时间段列表
generateTimeSlots(startTime, endTime) {
if (!startTime || !endTime) {
this.timeSlots = ['2025年11月6日 上午8:30', '2025年11月6日 下午13:30']
this.timeSlotIndexMap = { 0: 0, 1: 1 }
return
}
// 从编排数据中提取实际有项目的时间段索引
const usedTimeSlotIndices = new Set()
if (scheduleGroups && scheduleGroups.length > 0) {
scheduleGroups.forEach(group => {
if (group.timeSlotIndex !== undefined && group.timeSlotIndex !== null) {
usedTimeSlotIndices.add(group.timeSlotIndex)
}
})
}
const allSlots = []
const slots = []
const start = new Date(startTime)
const end = new Date(endTime)
let currentDate = new Date(start)
let slotIndex = 0
while (currentDate <= end) {
const year = currentDate.getFullYear()
@@ -239,28 +217,13 @@ export default {
const day = currentDate.getDate()
const dateStr = `${year}${month}${day}`
allSlots.push({ label: `${dateStr} 上午8:30`, index: slotIndex++ })
allSlots.push({ label: `${dateStr} 下午13:30`, index: slotIndex++ })
slots.push(`${dateStr} 上午8:30`)
slots.push(`${dateStr} 下午13:30`)
currentDate.setDate(currentDate.getDate() + 1)
}
// 如果有编排数据,只显示有项目的时间段
if (usedTimeSlotIndices.size > 0) {
const filteredSlots = allSlots.filter(slot => usedTimeSlotIndices.has(slot.index))
this.timeSlots = filteredSlots.map(slot => slot.label)
// 保存映射关系:显示索引 -> 实际时间段索引
this.timeSlotIndexMap = {}
filteredSlots.forEach((slot, displayIndex) => {
this.timeSlotIndexMap[displayIndex] = slot.index
})
} else {
this.timeSlots = allSlots.map(slot => slot.label)
this.timeSlotIndexMap = {}
allSlots.forEach((slot, displayIndex) => {
this.timeSlotIndexMap[displayIndex] = slot.index
})
}
this.timeSlots = slots
},
// 加载场地列表
@@ -305,7 +268,7 @@ export default {
const res = await getDispatchData({
competitionId: this.competitionId,
venueId: this.selectedVenueId,
timeSlotIndex: this.timeSlotIndexMap[this.selectedTime] !== undefined ? this.timeSlotIndexMap[this.selectedTime] : this.selectedTime
timeSlotIndex: this.selectedTime
})
if (res.data.success) {
+1 -1
View File
@@ -261,7 +261,7 @@ export default {
const compEnd = row.competitionEndTime ? new Date(row.competitionEndTime) : null
if (compEnd && now > compEnd) return 4
if (compStart && now >= compStart) return 3
if (regStart && regEnd && now >= regStart && now <= new Date(regEnd.getFullYear(), regEnd.getMonth(), regEnd.getDate(), 23, 59, 59)) return 2
if (regStart && regEnd && now >= regStart && now <= regEnd) return 2
return 1
},
+1 -4
View File
@@ -135,10 +135,7 @@
</el-table-column>
<el-table-column prop="category" label="分组类别" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.category == 1" type="primary" size="small">男子</el-tag>
<el-tag v-else-if="row.category == 2" type="danger" size="small">女子</el-tag>
<el-tag v-else-if="row.category == 3" type="success" size="small">混合</el-tag>
<span v-else>-</span>
<span>{{ row.category || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="eventType" label="项目类型" width="100" align="center">
+1 -30
View File
@@ -162,7 +162,6 @@ import {
import { getCompetitionDetail } from '@/api/martial/competition'
import { getProjectDetail } from '@/api/martial/project'
import { getParticipantList } from '@/api/martial/participant'
import { getScheduleResult } from '@/api/martial/activitySchedule'
export default {
name: 'MartialRegistrationDetail',
@@ -392,31 +391,6 @@ export default {
// 预加载项目信息
await this.preloadProjectInfo(participants)
// 获取编排数据,统计每个项目的分组数
let projectGroupCount = new Map()
try {
const scheduleRes = await getScheduleResult(this.competitionId)
const scheduleData = scheduleRes.data?.data
const scheduleGroups = scheduleData?.competitionGroups || scheduleData?.scheduleGroups || []
scheduleGroups.forEach(group => {
const groupProjectId = group.projectId
let key = null
if (groupProjectId && groupProjectId !== -1 && groupProjectId !== '-1') {
key = String(groupProjectId)
} else if (group.title) {
key = group.title.split(' ')[0]
}
if (key) {
projectGroupCount.set(key, (projectGroupCount.get(key) || 0) + 1)
}
})
} catch (scheduleErr) {
console.warn("获取编排数据失败,组数将显示为-:", scheduleErr)
}
// 按项目ID分组统计人数
const projectAthleteCount = new Map()
participants.forEach(athlete => {
@@ -436,10 +410,7 @@ export default {
projectName: project.projectName || project.project_name || '未知项目',
projectType: projectType === 1 ? '单人' : '集体',
athleteCount: count,
groupCount:
projectGroupCount.get(String(projectId)) ||
projectGroupCount.get(project.projectName || project.project_name || '') ||
'-',
groupCount: projectType === 2 ? count : '-', // 集体项目显示组数,单人显示-
estimatedDuration: project.estimatedDuration || project.estimated_duration || 0,
projectCode: project.projectCode || project.project_code || ''
})
@@ -1,145 +0,0 @@
<template>
<el-dialog
title="导出预览"
:model-value="visible"
@update:model-value="handleVisibleChange"
width="90%"
top="5vh"
:before-close="handleClose"
>
<div class="export-options">
<el-form :inline="true" size="small">
<el-form-item label="导出模板">
<el-select v-model="exportTemplate" placeholder="选择模板" style="width: 200px">
<el-option label="模板1-详细赛程表" value="template1" />
<el-option label="模板2-比赛时间表" value="template2" />
</el-select>
</el-form-item>
<el-form-item label="场地" v-if="exportTemplate === 'template2'">
<el-select v-model="selectedVenue" placeholder="选择场地" style="width: 150px">
<el-option label="全部场地" :value="null" />
<el-option v-for="venue in venues" :key="venue.id" :label="venue.venueName" :value="venue.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadPreviewData" :loading="loading">刷新预览</el-button>
</el-form-item>
</el-form>
</div>
<div class="preview-table" v-loading="loading">
<el-table v-if="exportTemplate === 'template1'" :data="previewData" border stripe max-height="500" style="width: 100%">
<el-table-column prop="scheduleDate" label="比赛日期" width="120" />
<el-table-column prop="timeSlot" label="时间段" width="100" />
<el-table-column prop="venueName" label="场地" width="120" />
<el-table-column prop="projectName" label="项目名称" width="180" />
<el-table-column prop="category" label="组别" width="80" />
<el-table-column prop="athleteName" label="运动员姓名" width="120" />
<el-table-column prop="teamName" label="单位/队伍" width="150" />
<el-table-column prop="sortOrder" label="出场顺序" width="90" />
<el-table-column prop="status" label="状态" width="80" />
</el-table>
<el-table v-if="exportTemplate === 'template2'" :data="previewData" border stripe max-height="500" style="width: 100%">
<el-table-column prop="sequenceNo" label="序号" width="80" />
<el-table-column prop="projectName" label="项目" width="200" />
<el-table-column prop="participantCount" label="人数" width="80" />
<el-table-column prop="groupCount" label="组数" width="80" />
<el-table-column prop="durationMinutes" label="时间" width="80" />
<el-table-column prop="tableNo" label="表号" width="100" />
</el-table>
<div v-if="previewData.length === 0 && !loading" class="empty-tip">暂无数据</div>
</div>
<div class="preview-stats" v-if="previewData.length > 0">
<span> {{ previewData.length }} 条记录</span>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleExport" :loading="exporting">确认导出</el-button>
</template>
</el-dialog>
</template>
<script>
import { getScheduleExportPreview, exportSchedule, exportScheduleTemplate2 } from '@/api/martial/activitySchedule'
export default {
name: 'ExportPreview',
props: {
visible: { type: Boolean, default: false },
competitionId: { type: [String, Number], required: true },
competitionName: { type: String, default: '' },
venues: { type: Array, default: () => [] }
},
emits: ['update:visible'],
data() {
return {
exportTemplate: 'template1',
selectedVenue: null,
previewData: [],
loading: false,
exporting: false
}
},
watch: {
visible(val) { if (val) this.loadPreviewData() },
exportTemplate() { this.loadPreviewData() },
selectedVenue() { if (this.exportTemplate === 'template2') this.loadPreviewData() }
},
methods: {
handleVisibleChange(val) { this.$emit('update:visible', val) },
async loadPreviewData() {
this.loading = true
try {
const res = await getScheduleExportPreview(this.competitionId, this.exportTemplate, this.selectedVenue)
this.previewData = res.data?.data || res.data || []
} catch (error) {
console.error('加载预览数据失败:', error)
this.$message.error('加载预览数据失败')
this.previewData = []
} finally {
this.loading = false
}
},
async handleExport() {
this.exporting = true
try {
let res, filename
if (this.exportTemplate === 'template1') {
res = await exportSchedule(this.competitionId)
filename = '赛程表_' + (this.competitionName || this.competitionId) + '.xlsx'
} else {
const venue = this.venues.find(v => v.id === this.selectedVenue)
const venueName = venue ? venue.venueName : null
res = await exportScheduleTemplate2(this.competitionId, this.selectedVenue, venueName, null)
filename = '比赛时间_' + (venueName || '全部场地') + '_' + (this.competitionName || this.competitionId) + '.xlsx'
}
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = filename
link.click()
window.URL.revokeObjectURL(link.href)
this.$message.success('导出成功')
this.handleClose()
} catch (error) {
console.error('导出失败:', error)
this.$message.error('导出失败')
} finally {
this.exporting = false
}
},
handleClose() { this.$emit('update:visible', false) }
}
}
</script>
<style scoped>
.export-options { margin-bottom: 20px; padding: 15px; background: #f5f7fa; border-radius: 4px; }
.preview-table { min-height: 300px; }
.preview-stats { margin-top: 15px; color: #666; font-size: 14px; }
.empty-tip { text-align: center; padding: 60px 0; color: #999; }
</style>
+55 -97
View File
@@ -7,13 +7,12 @@
</div>
<div class="header-right">
<el-button
v-if="!isScheduleCompleted"
size="small"
type="primary"
:loading="autoArrangeLoading"
@click="handleAutoArrange"
>
自动编排
{{ isScheduleCompleted ? '重新编排' : '自动编排' }}
</el-button>
<el-button size="small" type="danger" @click="showExceptionDialog">
异常组 <el-badge :value="exceptionList.length" :hidden="exceptionList.length === 0" />
@@ -86,8 +85,8 @@
<span class="project-title">{{ group.title }}</span>
<span class="project-meta">{{ group.type }}</span>
<span class="project-meta">{{ getTeamCount(group) }}</span>
<span class="project-meta">{{ group.type === '集体' ? getTeamCount(group) : 1 }}</span>
<!-- <span class="project-meta">{{ group.code }}</span> -->
<span class="project-meta">{{ group.items?.length || 0 }}</span>
<span class="project-meta">{{ group.code }}</span>
<span class="project-table-no">表号: {{ generateTableNo(group) }}</span>
</div>
<div class="project-actions" @click.stop>
@@ -104,7 +103,7 @@
</template>
<div class="move-popover">
<div class="move-label">移动到</div>
<el-select v-model="moveTargetVenueId" placeholder="选择场地" size="small" style="width: 100%; margin-bottom: 10px;" :teleported="false">
<el-select v-model="moveTargetVenueId" placeholder="选择场地" size="small" style="width: 100%; margin-bottom: 10px;">
<el-option
v-for="venue in venues"
:key="venue.id"
@@ -186,7 +185,36 @@
>
<span class="player-detail-index">{{ playerIndex + 1 }}</span>
<span class="player-detail-name">{{ player.playerName }}</span>
<!-- 签到状态和异常操作只在调度功能中显示 -->
<span class="player-detail-status">
<el-tag
:type="player.status === '已签到' ? 'success' : player.status === '异常' ? 'danger' : 'info'"
size="small"
>
{{ player.status || '未签到' }}
</el-tag>
</span>
<span class="player-detail-actions">
<el-button
v-if="(player.status || '未签到') === '未签到'"
link
size="small"
@click="markPlayerAsException(group, team, playerIndex)"
:disabled="isScheduleCompleted"
style="color: #f56c6c;"
>
标记异常
</el-button>
<el-button
v-if="player.status === '异常'"
link
size="small"
@click="removePlayerException(group, team, playerIndex)"
:disabled="isScheduleCompleted"
style="color: #67c23a;"
>
取消异常
</el-button>
</span>
</div>
</div>
</div>
@@ -240,7 +268,7 @@
<el-table-column prop="duration" label="合计时间" width="100" align="center"></el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<span :class="['status-tag', scope.row.status === 2 ? 'status-completed' : scope.row.status === 1 ? 'status-progress' : 'status-pending']">{{ scope.row.status === 2 ? '已完成' : scope.row.status === 1 ? '进行中' : '未开始' }}</span>
<span v-if="scope.row.status" class="status-active">{{ scope.row.status }}</span>
</template>
</el-table-column>
</el-table>
@@ -249,7 +277,15 @@
<div class="footer-actions">
<el-button size="small" @click="handleSaveDraft" v-if="!isScheduleCompleted">保存草稿</el-button>
<el-button v-if="isScheduleCompleted" size="small" @click="exportPreviewVisible = true">导出</el-button>
<el-dropdown v-if="isScheduleCompleted" @command="handleExportCommand" trigger="click">
<el-button size="small">导出 <i class="el-icon-arrow-down el-icon--right"></i></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="template1">模板1 - 详细赛程表</el-dropdown-item>
<el-dropdown-item command="template2">模板2 - 比赛时间表</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button size="small" type="primary" @click="handleConfirm" v-if="!isScheduleCompleted">完成编排</el-button>
</div>
@@ -346,13 +382,6 @@
</span>
</template>
</el-dialog>
<!-- 导出预览对话框 -->
<ExportPreview
v-model:visible="exportPreviewVisible"
:competition-id="competitionId"
:competition-name="competitionInfo.competitionName"
:venues="venues"
/>
</div>
</template>
@@ -362,12 +391,10 @@ import { getVenuesByCompetition } from '@/api/martial/venue'
import { getCompetitionDetail } from '@/api/martial/competition'
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule, exportScheduleTemplate2 } from '@/api/martial/activitySchedule'
import { updateCheckInStatus, getScheduleConfig } from '@/api/martial/schedulePlan'
import ExportPreview from './ExportPreview.vue'
export default {
name: 'MartialScheduleList',
components: {
ExportPreview,
ArrowDown,
ArrowRight
},
@@ -381,7 +408,6 @@ export default {
selectedVenueIdForVenueTab: null, // 选中的场地ID(场地Tab
confirmDialogVisible: false,
isScheduleCompleted: false, // 是否已完成编排
exportPreviewVisible: false, // 导出预览对话框
loading: false,
autoArrangeLoading: false, // 自动编排加载状态
venues: [], // 场地列表(从后端加载)
@@ -422,7 +448,7 @@ export default {
const filtered = this.competitionGroups.filter(group => {
console.log(`分组"${group.title}": venueId=${group.venueId}(${typeof group.venueId}), timeSlotIndex=${group.timeSlotIndex}(${typeof group.timeSlotIndex})`)
const venueMatch = String(group.venueId) === String(this.selectedVenueId)
const venueMatch = group.venueId === this.selectedVenueId || Number(group.venueId) === Number(this.selectedVenueId)
const timeMatch = group.timeSlotIndex === this.selectedTime
console.log(` 场地匹配:${venueMatch}, 时间匹配:${timeMatch}`)
return venueMatch && timeMatch
@@ -446,7 +472,8 @@ export default {
// 如果选中了场地,进一步过滤
if (this.selectedVenueIdForVenueTab) {
groupsInTimeSlot = groupsInTimeSlot.filter(group => {
return String(group.venueId) === String(this.selectedVenueIdForVenueTab)
return group.venueId === this.selectedVenueIdForVenueTab ||
Number(group.venueId) === Number(this.selectedVenueIdForVenueTab)
})
}
@@ -456,8 +483,8 @@ export default {
participant: group.type,
team: 1, // 队伍数量,可以从分组中计算
number: group.items?.length || 0, // 参赛人数
duration: group.estimatedDuration || 0, // 合计时间
status: group.status, // 状态: 0-未开始, 1-进行中, 2-已完成
duration: 0, // 合计时间,如果后端有数据可以显示
status: group.venueName, // 显示场地名称
venueName: group.venueName,
hint: '' // 可以根据需要添加提示信息
}))
@@ -500,7 +527,7 @@ export default {
period = hour < 12 ? 1 : 2
}
// 3. 获取序号:在同场地同时段中的顺序(与显示顺序一致)
// 3. 获取序号:在同场地同时段中的顺序
const sameSlotGroups = this.competitionGroups.filter(g => {
const gVenueMatch = String(g.venueId) === String(group.venueId)
if (!gVenueMatch) return false
@@ -508,7 +535,8 @@ export default {
const gPeriod = gHour < 12 ? 1 : 2
return gPeriod === period
})
// 使用原始数组顺序(与显示顺序一致),不再按id排序
// 按id排序保持稳定顺
sameSlotGroups.sort((a, b) => (a.id || 0) - (b.id || 0))
const orderIndex = sameSlotGroups.findIndex(g => g.id === group.id) + 1
// 4. 格式化: 场地(1位) + 时段(1位) + 序号(2位)
@@ -620,22 +648,8 @@ export default {
playerIds: []
})
}
// If item has members array (from API), use it as players
if (item.members && item.members.length > 0) {
item.members.forEach(member => {
teamMap.get(key).players.push({
id: member.id,
playerName: member.name || member.playerName,
organization: member.organization,
status: member.status
})
teamMap.get(key).playerIds.push(member.id)
})
} else {
// Fallback: use item itself as player
teamMap.get(key).players.push(item)
teamMap.get(key).playerIds.push(item.id)
}
teamMap.get(key).players.push(item)
teamMap.get(key).playerIds.push(item.id)
})
return Array.from(teamMap.values())
},
@@ -656,14 +670,6 @@ export default {
const targetVenue = this.venues.find(v => v.id === this.moveTargetVenueId)
// In draft mode (negative groupId), just update frontend state
if (group.id && group.id.toString().startsWith("-")) {
group.venueId = this.moveTargetVenueId
group.venueName = targetVenue ? targetVenue.venueName : ""
this.$message.success(`已移动到 ${group.venueName}`)
return
}
try {
// 调用后端API移动分组
const res = await moveScheduleGroup({
@@ -877,49 +883,6 @@ export default {
console.log('生成的时间段:', this.timeSlots)
},
// 根据实际编排数据更新显示的时间段(只显示有比赛的时段)
updateTimeSlotsFromGroups() {
if (!this.competitionGroups || this.competitionGroups.length === 0) {
return
}
// 从分组数据中提取实际使用的时段索引
const usedTimeSlotIndexes = new Set()
this.competitionGroups.forEach(group => {
if (group.timeSlotIndex !== null && group.timeSlotIndex !== undefined) {
usedTimeSlotIndexes.add(group.timeSlotIndex)
}
})
if (usedTimeSlotIndexes.size === 0) {
return
}
// 保存原始时段用于映射
const allSlots = [...this.timeSlots]
const filteredSlots = []
const indexMapping = {}
let newIndex = 0
allSlots.forEach((slot, oldIndex) => {
if (usedTimeSlotIndexes.has(oldIndex)) {
filteredSlots.push(slot)
indexMapping[oldIndex] = newIndex
newIndex++
}
})
// 更新分组的时段索引
this.competitionGroups.forEach(group => {
if (group.timeSlotIndex !== null && group.timeSlotIndex !== undefined) {
const newIdx = indexMapping[group.timeSlotIndex]
if (newIdx !== undefined) {
group.timeSlotIndex = newIdx
}
}
})
this.timeSlots = filteredSlots
console.log("过滤后的时间段:", this.timeSlots)
if (this.selectedTime >= this.timeSlots.length) {
this.selectedTime = 0
}
},
// 加载场地列表
async loadVenues() {
try {
@@ -981,14 +944,12 @@ export default {
venueName: group.venueName,
timeSlot: group.timeSlot,
timeSlotIndex: group.timeSlotIndex,
estimatedDuration: group.estimatedDuration || 0,
items: (group.participants || []).map(p => ({
id: p.id,
schoolUnit: p.schoolUnit,
playerName: p.playerName,
status: p.status || '未签到',
sortOrder: p.sortOrder,
members: p.members || []
sortOrder: p.sortOrder
}))
}))
@@ -1011,7 +972,6 @@ export default {
console.log('解析后的competitionGroups:', this.competitionGroups)
console.log('当前selectedVenueId:', this.selectedVenueId)
console.log('当前selectedTime:', this.selectedTime)
this.updateTimeSlotsFromGroups()
this.$message.success(data.isDraft ? '已加载草稿数据' : '已加载编排数据')
} else {
// 后端还没有数据,使用默认数据进行展示
@@ -1197,7 +1157,6 @@ export default {
venueName: group.venueName,
timeSlot: group.timeSlot,
timeSlotIndex: group.timeSlotIndex,
estimatedDuration: group.estimatedDuration || 0,
participants: group.items.map((item, index) => ({
id: item.id,
schoolUnit: item.schoolUnit,
@@ -1314,7 +1273,6 @@ export default {
venueName: group.venueName,
timeSlot: group.timeSlot,
timeSlotIndex: group.timeSlotIndex,
estimatedDuration: group.estimatedDuration || 0,
participants: group.items.map((item, index) => ({
id: item.id,
schoolUnit: item.schoolUnit,