Browse Source

临时提交

master
王泽彦 8 months ago
parent
commit
fa30f04be4
  1. 4
      .gitignore
  2. BIN
      doc/xx校区周&月综合报表.xls
  3. BIN
      doc/xx校区周&月转化表.xls
  4. BIN
      doc/副本xx校区周&月综合报表.xlsx
  5. BIN
      doc/副本xx校区周&月转化表.xlsx
  6. BIN
      doc/副本各校区月&年综合报表.xlsx
  7. BIN
      doc/副本各校区月&年转化汇总表.xlsx
  8. BIN
      doc/副本月卡体能课学员课程协议(1).docx
  9. BIN
      doc/副本私教学员课程协议(1).docx
  10. BIN
      doc/副本续费月卡体能课学员课程协议.docx
  11. BIN
      doc/副本课程协议—月卡篮球(1).docx
  12. BIN
      doc/副本课程协议—月卡篮球(2).docx
  13. BIN
      doc/副本(时间卡)体能课学员课程协议.docx
  14. BIN
      doc/各校区月&年综合报表.xls
  15. BIN
      doc/各校区月&年转化汇总表.xls
  16. 393
      doc/各校区综合数据管理系统需求文档.md
  17. 251
      doc/数据统计系统需求文档.md
  18. BIN
      doc/月卡体能课学员课程协议(1).doc
  19. BIN
      doc/续费月卡体能课学员课程协议.doc
  20. BIN
      doc/课程协议—月卡篮球(2).doc
  21. 406
      niucloud/PLANNING.md
  22. 0
      niucloud/TASK.md
  23. 97
      niucloud/app/api/controller/apiController/PhysicalTest.php
  24. 179
      niucloud/app/api/controller/apiController/StudyPlan.php
  25. 22
      niucloud/app/api/controller/login/Login.php
  26. 21
      niucloud/app/api/route/route.php
  27. 15
      niucloud/app/service/admin/pay/PayService.php
  28. 1
      niucloud/app/service/api/apiService/CourseService.php
  29. 254
      niucloud/app/service/api/apiService/PhysicalTestService.php
  30. 326
      niucloud/app/service/api/apiService/StudyPlanService.php
  31. 92
      uniapp/PLANNING.md
  32. BIN
      uniapp/TASK.md
  33. 37
      uniapp/api/apiRoute.js
  34. 2
      uniapp/common/config.js
  35. 50
      uniapp/components/bottom-popup/index.vue
  36. 101
      uniapp/components/call-record-card/call-record-card.vue
  37. 34
      uniapp/components/course-info-card/index.vue
  38. 93
      uniapp/components/fitness-record-card/fitness-record-card.vue
  39. 100
      uniapp/components/fitness-record-popup/fitness-record-popup.vue
  40. 72
      uniapp/components/order-form-popup/index.vue
  41. 6
      uniapp/components/order-list-card/index.vue
  42. 38
      uniapp/components/service-list-card/index.vue
  43. 27
      uniapp/components/study-plan-card/index.vue
  44. 424
      uniapp/components/study-plan-popup/study-plan-popup.vue
  45. 215
      uniapp/mock/index.js
  46. 3
      uniapp/pages/coach/student/student_list.vue
  47. 8
      uniapp/pages/common/profile/index.vue
  48. 1254
      uniapp/pages/common/profile/personal_info.vue
  49. 34
      uniapp/pages/market/clue/class_arrangement_detail.vue
  50. 47
      uniapp/pages/market/clue/clue_info.less
  51. 228
      uniapp/pages/market/clue/clue_info.vue
  52. 132
      各校区转化数据统计需求文档.md

4
.gitignore

@ -15,5 +15,9 @@ examples
PRPs
INITIAL.md
CLAUDE.local.md
uniapp/TASK.md
uniapp/PLANNING.md
niucloud/TASK.md
niucloud/PLANNING.md

BIN
doc/xx校区周&月综合报表.xls

Binary file not shown.

BIN
doc/xx校区周&月转化表.xls

Binary file not shown.

BIN
doc/副本xx校区周&月综合报表.xlsx

Binary file not shown.

BIN
doc/副本xx校区周&月转化表.xlsx

Binary file not shown.

BIN
doc/副本各校区月&年综合报表.xlsx

Binary file not shown.

BIN
doc/副本各校区月&年转化汇总表.xlsx

Binary file not shown.

BIN
doc/私教学员课程协议(1).doc → doc/副本月卡体能课学员课程协议(1).docx

Binary file not shown.

BIN
doc/课程协议—月卡篮球(1).doc → doc/副本私教学员课程协议(1).docx

Binary file not shown.

BIN
doc/(时间卡)体能课学员课程协议.doc → doc/副本续费月卡体能课学员课程协议.docx

Binary file not shown.

BIN
doc/副本课程协议—月卡篮球(1).docx

Binary file not shown.

BIN
doc/副本课程协议—月卡篮球(2).docx

Binary file not shown.

BIN
doc/副本(时间卡)体能课学员课程协议.docx

Binary file not shown.

BIN
doc/各校区月&年综合报表.xls

Binary file not shown.

BIN
doc/各校区月&年转化汇总表.xls

Binary file not shown.

393
doc/各校区综合数据管理系统需求文档.md

@ -0,0 +1,393 @@
# 各校区综合数据管理系统需求文档
## 文档信息
- **生成时间**: 2025-07-29 00:19:30
- **数据来源**: 副本各校区月&年综合报表.xlsx
- **文档版本**: v1.0
- **项目类型**: 教育培训机构多校区数据管理系统
## 1. 项目背景与现状
### 1.1 业务背景
该教育培训机构拥有多个校区,需要对各校区的经营数据进行统一管理和分析。当前使用Excel进行手工统计,存在数据分散、统计效率低、易出错等问题。
### 1.2 校区分布
目前包含以下校区:
- **吾悦校区**
- **国兴校区**
- **国贸校区**
- **远大校区**
- **龙湖校区**
### 1.3 现状问题
- 数据录入依赖手工Excel操作,效率低下
- 各校区数据分散,难以进行横向对比分析
- 缺乏实时数据监控和预警机制
- 报表生成耗时,影响决策效率
## 2. 业务需求分析
### 2.1 数据维度分析
#### 2.1.1 时间维度
系统需要支持以下时间维度的数据统计:
- 1月
- 2月
- 3月
- 4月
- 5月
- 6月
- 7月
- 8月
- 9月
- 10月
- 11月
- 12月
- 年度合计
#### 2.1.2 校区维度
支持多校区数据管理:
- 吾悦校区
- 国兴校区
- 国贸校区
- 远大校区
- 龙湖校区
### 2.2 核心业务模块
#### 2.2.1 资源管理模块
**功能描述**: 管理资源管理相关业务数据
**核心指标**:
- 线下资源
- 邀约数
- 一访到访
- 一访成交
- 二访到访
- 二访成交
- 成交率
- 线上资源
**业务流程**:
1. 数据录入:相关指标数据录入
2. 数据统计:自动计算汇总数据
3. 数据分析:趋势分析和对比分析
4. 报表生成:生成相关业务报表
#### 2.2.2 销售业绩模块
**功能描述**: 管理销售业绩相关业务数据
**核心指标**:
- 成交总课时
- 成交总金额
- 均单价
- (净)成交总单数
- (净)成交总课时
- (净)成交总金额
**业务流程**:
1. 数据录入:相关指标数据录入
2. 数据统计:自动计算汇总数据
3. 数据分析:趋势分析和对比分析
4. 报表生成:生成相关业务报表
#### 2.2.3 转介绍管理模块
**功能描述**: 管理转介绍管理相关业务数据
**核心指标**:
- 转介绍
- 转介绍资源数
**业务流程**:
1. 数据录入:相关指标数据录入
2. 数据统计:自动计算汇总数据
3. 数据分析:趋势分析和对比分析
4. 报表生成:生成相关业务报表
#### 2.2.4 续费管理模块
**功能描述**: 管理续费管理相关业务数据
**核心指标**:
- 续费
- 月应续费人数
- 续费人数(本月)
- 续费人数(次月)
- 老卡续费人数
- 总续费数
- 月卡续费率
- 续费总课时
- 续费总金额
**业务流程**:
1. 数据录入:相关指标数据录入
2. 数据统计:自动计算汇总数据
3. 数据分析:趋势分析和对比分析
4. 报表生成:生成相关业务报表
#### 2.2.5 学员异动模块
**功能描述**: 管理学员异动相关业务数据
**核心指标**:
- 异动
- 转出人数
- 转出金额
- 转入人数
- 转入金额
- 退费人数
- 退费金额
**业务流程**:
1. 数据录入:相关指标数据录入
2. 数据统计:自动计算汇总数据
3. 数据分析:趋势分析和对比分析
4. 报表生成:生成相关业务报表
#### 2.2.6 经营分析模块
**功能描述**: 管理经营分析相关业务数据
**核心指标**:
- 经营汇总
- 任务(月)
- 成本(月)
- (净)任务完成率
- (净)成本完成率
**业务流程**:
1. 数据录入:相关指标数据录入
2. 数据统计:自动计算汇总数据
3. 数据分析:趋势分析和对比分析
4. 报表生成:生成相关业务报表
## 3. 功能需求
### 3.1 数据录入功能
- **多校区数据录入**: 支持各校区独立录入数据
- **批量导入**: 支持Excel文件批量导入
- **数据校验**: 自动校验数据合理性和完整性
- **权限控制**: 校区管理员只能录入本校区数据
### 3.2 数据统计功能
- **实时统计**: 数据录入后自动更新统计结果
- **多维度统计**: 按时间、校区、业务类型等维度统计
- **自动计算**: 自动计算转化率、完成率等衍生指标
- **数据汇总**: 自动生成各校区汇总数据
### 3.3 数据分析功能
- **趋势分析**: 各指标的时间趋势分析
- **对比分析**: 校区间横向对比、同期对比
- **转化漏斗**: 从资源到成交的完整转化漏斗
- **异常预警**: 数据异常自动预警
### 3.4 报表展示功能
- **综合仪表盘**: 核心指标实时展示
- **校区报表**: 单校区详细报表
- **对比报表**: 多校区对比报表
- **导出功能**: 支持PDF、Excel格式导出
## 4. 技术实现方案
### 4.1 系统架构
- **前端**: Vue.js 3 + Element Plus + ECharts
- **后端**: Node.js + Express + TypeScript
- **数据库**: MySQL 8.0
- **缓存**: Redis
- **部署**: Docker + Nginx
### 4.2 数据库设计
#### 4.2.1 核心数据表设计
**校区表 (campus)**
- id: 主键
- name: 校区名称
- code: 校区编码
- status: 状态
- created_at: 创建时间
- updated_at: 更新时间
**业务数据表 (business_data)**
- id: 主键
- campus_id: 校区ID
- year: 年份
- month: 月份
- metric_category: 指标分类
- metric_name: 指标名称
- metric_value: 指标值
- created_at: 创建时间
- updated_at: 更新时间
**计算指标表 (calculated_metrics)**
- id: 主键
- campus_id: 校区ID
- year: 年份
- month: 月份
- conversion_rate: 成交率
- renewal_rate: 续费率
- avg_price: 均单价
- task_completion_rate: 任务完成率
- cost_completion_rate: 成本完成率
### 4.3 关键算法
#### 4.3.1 转化率计算
- 成交率 = (一访成交 + 二访成交) / (一访到访 + 二访到访) × 100%
- 续费率 = 续费人数(本月) / 月应续费人数 × 100%
- 均单价 = 成交总金额 / 成交总单数
#### 4.3.2 数据聚合规则
- 年度数据 = 各月度数据累加或平均
- 校区对比 = 同期数据横向对比
- 趋势分析 = 时间序列数据分析
## 5. 界面设计要求
### 5.1 主要页面
1. **数据录入页面**: 表格式录入,支持快速录入和批量导入
2. **综合仪表盘**: 核心指标卡片式展示,图表可视化
3. **校区对比页面**: 多校区数据对比分析
4. **报表管理页面**: 报表生成、导出、历史查看
5. **系统管理页面**: 用户管理、权限管理、校区管理
### 5.2 用户体验要求
- **响应式设计**: 支持PC端和平板端使用
- **操作便捷**: 减少点击次数,支持键盘快捷操作
- **数据可视化**: 图表展示直观清晰
- **加载性能**: 页面加载时间 < 3秒
## 6. 权限与安全
### 6.1 角色权限设计
- **超级管理员**: 全系统权限,可管理所有校区数据
- **校区管理员**: 只能管理本校区数据,查看对比报表
- **数据录入员**: 只能录入本校区数据,无查看权限
- **数据分析师**: 只读权限,可查看所有数据和报表
### 6.2 数据安全要求
- **数据加密**: 敏感数据加密存储
- **操作日志**: 记录所有数据变更操作
- **数据备份**: 每日自动备份,保留30天
- **访问控制**: IP白名单,异常登录预警
## 7. 性能要求
### 7.1 响应时间要求
- 页面加载时间 ≤ 3秒
- 数据查询响应时间 ≤ 2秒
- 报表生成时间 ≤ 5秒
- 数据导出时间 ≤ 10秒
### 7.2 并发性能要求
- 支持50个并发用户同时使用
- 数据库连接池优化
- 缓存策略优化关键查询
## 8. 验收标准
### 8.1 功能验收
- [ ] 各校区数据录入功能正常
- [ ] 数据统计计算准确无误
- [ ] 报表展示美观清晰
- [ ] 权限控制有效
- [ ] 数据导入导出功能正常
### 8.2 性能验收
- [ ] 响应时间满足要求
- [ ] 并发性能达标
- [ ] 数据安全可靠
- [ ] 系统稳定性良好
## 9. 风险评估与应对
### 9.1 技术风险
**风险**: Excel数据迁移可能存在数据丢失
**应对**: 制定详细的数据迁移方案,多次测试验证
**风险**: 多校区数据同步可能存在延迟
**应对**: 采用事务机制保证数据一致性
### 9.2 业务风险
**风险**: 用户可能不适应新系统操作
**应对**: 提供详细培训和操作手册
**风险**: 数据录入错误可能影响决策
**应对**: 增加数据校验规则和审核流程
## 10. 实施计划
### 10.1 开发阶段 (6周)
- **第1周**: 需求确认、系统设计
- **第2周**: 数据库设计、后端架构搭建
- **第3-4周**: 后端API开发、前端页面开发
- **第5周**: 功能集成、测试
- **第6周**: 用户培训、上线部署
### 10.2 验收测试
- 单元测试覆盖率 > 80%
- 集成测试通过率 100%
- 用户验收测试通过
- 性能测试达标
## 11. 后续优化建议
### 11.1 功能扩展
- **移动端应用**: 开发移动端APP,支持移动办公
- **智能分析**: 引入AI算法,提供数据预测和建议
- **第三方集成**: 与财务系统、CRM系统集成
- **自动化报表**: 定时自动生成和发送报表
### 11.2 技术优化
- **微服务架构**: 系统模块化,提高可维护性
- **大数据分析**: 引入大数据技术,支持更复杂的分析
- **实时数据流**: 实现数据实时同步和分析
- **云原生部署**: 采用云原生技术,提高系统弹性
---
**备注**:
1. 本需求文档基于Excel数据结构分析生成,具体业务规则需与业务方进一步确认
2. 技术方案需根据实际技术栈和团队能力调整
3. 项目实施过程中需重点关注数据准确性和业务连续性
## 附录:业务指标详细说明
### A.1 资源管理指标
- **线下资源**: 通过线下渠道获得的潜在客户数量
- **线上资源**: 通过线上渠道获得的潜在客户数量
- **邀约数**: 成功邀约到访的客户数量
- **一访到访**: 首次到访的客户数量
- **一访成交**: 首次到访即成交的客户数量
- **二访到访**: 二次到访的客户数量
- **二访成交**: 二次到访成交的客户数量
- **成交率**: 总成交数/总到访数的比例
### A.2 销售业绩指标
- **成交总课时**: 所有成交订单的课时总和
- **成交总金额**: 所有成交订单的金额总和
- **均单价**: 平均每单成交金额
- **(净)成交总单数**: 扣除退费后的实际成交单数
- **(净)成交总课时**: 扣除退费后的实际成交课时
- **(净)成交总金额**: 扣除退费后的实际成交金额
### A.3 续费管理指标
- **月应续费人数**: 当月应该续费的学员数量
- **续费人数(本月)**: 当月实际续费的学员数量
- **续费人数(次月)**: 延期到次月续费的学员数量
- **老卡续费人数**: 使用老卡续费的学员数量
- **总续费数**: 所有续费的总数量
- **月卡续费率**: 当月续费率
- **续费总课时**: 续费的总课时数
- **续费总金额**: 续费的总金额
### A.4 学员异动指标
- **转出人数**: 转出到其他校区的学员数量
- **转出金额**: 转出学员对应的金额
- **转入人数**: 从其他校区转入的学员数量
- **转入金额**: 转入学员对应的金额
- **退费人数**: 申请退费的学员数量
- **退费金额**: 退费的总金额
### A.5 经营分析指标
- **任务(月)**: 月度业绩任务目标
- **成本(月)**: 月度运营成本
- **(净)任务完成率**: 净业绩/任务目标的完成比例
- **(净)成本完成率**: 实际成本/预算成本的比例

251
doc/数据统计系统需求文档.md

@ -0,0 +1,251 @@
# 教育培训机构数据统计分析系统需求文档
## 文档信息
- **生成时间**: 2025-07-28 23:03:52
- **数据来源**: 各校区月&年转化汇总表.xlsx
- **文档版本**: v1.0
## 1. 项目背景
基于现有Excel报表数据分析,该教育培训机构需要一个数字化的数据统计分析系统,用于替代手工Excel报表,实现数据的自动化统计、分析和可视化展示。
### 1.1 现状分析
- 当前使用Excel手工统计各校区数据
- 包含13个工作表:1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月等
- 数据维度复杂,包含时间、校区、业务指标等多个维度
## 2. 业务需求分析
### 2.1 数据维度分析
#### 时间维度
- 1月
- 2月
- 3月
- 4月
- 5月
- 6月
- 7月
- 8月
- 9月
- 10月
- 11月
- 12月
- 年度合计
#### 业务分类
- 资源
#### 核心指标
- 线下资源
- 邀约数
- 一访到访
- 一访成交
- 二访到访
- 二访成交
- 成交率
- 线上资源
### 2.2 核心业务流程
#### 2.2.1 资源管理流程
1. **线下资源管理**
- 邀约数统计
- 一访到访率跟踪
- 一访成交率分析
- 二访到访率跟踪
- 二访成交率分析
2. **线上资源管理**
- 线上资源数量统计
- 线上转化率分析
#### 2.2.2 销售转化流程
1. **成交管理**
- 成交总课时统计
- 成交总金额统计
- 均单价计算
- 成交率分析
2. **转介绍管理**
- 转介绍资源数统计
- 转介绍转化率分析
#### 2.2.3 续费管理流程
1. **续费统计**
- 月应续费人数
- 续费人数(本月/次月)
- 老卡续费人数
- 总续费数
2. **续费分析**
- 月卡续费率
- 续费总课时
- 续费总金额
## 3. 功能需求
### 3.1 数据录入模块
- **手工录入**: 支持按日、周、月维度录入数据
- **批量导入**: 支持Excel文件批量导入
- **数据校验**: 自动校验数据完整性和合理性
### 3.2 数据统计模块
- **实时统计**: 自动计算各项指标
- **多维度统计**: 支持按时间、校区、业务类型等维度统计
- **自动汇总**: 自动生成周报、月报、年报
### 3.3 数据分析模块
- **趋势分析**: 各指标的时间趋势分析
- **对比分析**: 校区间、时期间对比分析
- **转化漏斗**: 从邀约到成交的转化漏斗分析
### 3.4 报表展示模块
- **仪表盘**: 核心指标实时展示
- **图表展示**: 柱状图、折线图、饼图等多种图表
- **报表导出**: 支持PDF、Excel格式导出
## 4. 技术实现方案
### 4.1 系统架构
- **前端**: Vue.js + Element UI
- **后端**: Node.js/Python + Express/FastAPI
- **数据库**: MySQL/PostgreSQL
- **缓存**: Redis
### 4.2 数据模型设计
#### 4.2.1 核心实体
```sql
-- 校区表
CREATE TABLE campus (
id INT PRIMARY KEY,
name VARCHAR(100),
code VARCHAR(50),
status TINYINT
);
-- 统计数据表
CREATE TABLE statistics_data (
id INT PRIMARY KEY,
campus_id INT,
date DATE,
period_type ENUM('daily', 'weekly', 'monthly', 'yearly'),
metric_type VARCHAR(50),
metric_value DECIMAL(10,2),
created_at TIMESTAMP
);
```
### 4.3 关键算法
#### 4.3.1 转化率计算
```
成交率 = 成交人数 / 到访人数 * 100%
续费率 = 续费人数 / 应续费人数 * 100%
```
#### 4.3.2 数据聚合规则
- 日数据 → 周数据:按自然周聚合
- 周数据 → 月数据:按自然月聚合
- 月数据 → 年数据:按自然年聚合
## 5. 界面设计要求
### 5.1 主要页面
1. **数据录入页面**: 表格式录入界面,支持快速录入
2. **统计分析页面**: 多维度查询和分析界面
3. **报表展示页面**: 图表和表格混合展示
4. **系统管理页面**: 用户、权限、校区管理
### 5.2 用户体验要求
- 响应式设计,支持PC和移动端
- 操作简单直观,减少学习成本
- 数据加载快速,支持分页和懒加载
## 6. 数据安全要求
### 6.1 权限控制
- 角色权限管理:超级管理员、校区管理员、数据录入员
- 数据权限隔离:校区间数据隔离
- 操作日志记录:记录所有数据变更操作
### 6.2 数据备份
- 定期数据备份
- 数据恢复机制
- 异地备份策略
## 7. 性能要求
### 7.1 响应时间
- 页面加载时间 < 3秒
- 数据查询响应时间 < 2秒
- 报表生成时间 < 5秒
### 7.2 并发要求
- 支持100个并发用户
- 数据库连接池优化
- 缓存策略优化
## 8. 验收标准
### 8.1 功能验收
- [ ] 数据录入功能完整可用
- [ ] 统计计算准确无误
- [ ] 报表展示美观清晰
- [ ] 权限控制有效
### 8.2 性能验收
- [ ] 响应时间满足要求
- [ ] 并发性能达标
- [ ] 数据安全可靠
## 9. 风险评估
### 9.1 技术风险
- **数据迁移风险**: Excel数据迁移可能存在数据丢失
- **性能风险**: 大量历史数据可能影响查询性能
- **兼容性风险**: 不同浏览器兼容性问题
### 9.2 业务风险
- **用户接受度**: 用户可能不适应新系统
- **数据准确性**: 自动计算可能与手工计算存在差异
- **业务连续性**: 系统切换期间业务连续性保障
## 10. 实施计划
### 10.1 开发阶段
1. **需求确认** (1周)
2. **系统设计** (1周)
3. **数据库设计** (3天)
4. **后端开发** (2周)
5. **前端开发** (2周)
6. **集成测试** (1周)
7. **用户培训** (3天)
8. **上线部署** (2天)
### 10.2 验收测试
- 单元测试覆盖率 > 80%
- 集成测试通过率 100%
- 用户验收测试通过
## 11. 后续优化建议
### 11.1 功能扩展
- 移动端APP开发
- 数据预测分析功能
- 智能报表推荐
- 第三方系统集成
### 11.2 技术优化
- 微服务架构改造
- 大数据分析平台集成
- AI智能分析功能
- 实时数据流处理
---
**注意事项**:
1. 本需求文档基于Excel数据结构分析生成,具体业务规则需要与业务方进一步确认
2. 技术实现方案需要根据实际技术栈和团队能力进行调整
3. 项目实施过程中需要密切关注数据准确性和业务连续性

BIN
doc/月卡体能课学员课程协议(1).doc

Binary file not shown.

BIN
doc/续费月卡体能课学员课程协议.doc

Binary file not shown.

BIN
doc/课程协议—月卡篮球(2).doc

Binary file not shown.

406
niucloud/PLANNING.md

@ -0,0 +1,406 @@
# PHP后端开发规划
## 已完成接口
### 1. 学员课程信息接口
**描述**: 获取学员课程信息,用于前端CourseInfoCard组件使用
**接口**: `GET /api/getStudentCourseInfo`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| resource_id | int | 是 | 资源ID |
| member_id | string | 否 | 会员ID |
| student_id | int | 否 | 学员ID(优先级高于member_id) |
**响应示例**:
```json
{
"code": 1,
"message": "success",
"data": [
{
"id": 1,
"course_name": "篮球课",
"total_count": 24,
"used_count": 8,
"formal_hours": 20,
"gift_hours": 4,
"used_formal_hours": 6,
"used_gift_hours": 2,
"leave_count": 1,
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"expiry_date": "2024-06-30",
"status": "active",
"course_type": "常规课",
"teacher_name": "张老师",
"course_price": 2880.00,
"class_duration": 90,
"create_time": "2024-01-01 10:00:00",
"remark": "备注信息"
}
]
}
```
**涉及数据表**:
- `school_customer_resources` - 资源表
- `school_student` - 学员表
- `school_course` - 课程表
- `school_student_course` - 学员课程关联表
- `school_course_schedule` - 课程排期表
- `school_attendance` - 考勤表
**SQL查询逻辑参考**:
```sql
SELECT
sc.id,
sc.course_name,
ssc.total_count,
ssc.used_count,
ssc.formal_hours,
ssc.gift_hours,
ssc.used_formal_hours,
ssc.used_gift_hours,
ssc.leave_count,
ssc.start_date,
ssc.end_date,
ssc.expiry_date,
ssc.status,
sc.course_type,
sp.name as teacher_name,
ssc.course_price,
sc.class_duration,
ssc.create_time,
ssc.remark
FROM school_student_course ssc
LEFT JOIN school_course sc ON ssc.course_id = sc.id
LEFT JOIN school_personnel sp ON sc.teacher_id = sp.id
WHERE ssc.student_id = ?
AND ssc.resource_id = ?
[AND ssc.member_id = ?]
ORDER BY ssc.create_time DESC
```
**状态说明**:
- `active`: 正常使用
- `completed`: 课程完结
- `expired`: 已过期
- `pending`: 待激活
**业务逻辑**:
1. 优先使用student_id查询,其次使用member_id
2. 计算剩余课时:remaining_count = total_count - used_count
3. 检查课程是否过期,基于expiry_date
4. 返回完整的课程信息及使用情况
5. 支持多个课程返回
**优化建议**:
- 使用Redis缓存常用查询结果
- 对学员课程查询添加索引优化
- 考虑分页处理大量数据
## 已新增接口
### 2. 服务列表接口
**描述**: 获取学员服务记录列表
**接口**: `GET /api/xy/service/list`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| student_id | int | 是 | 学员ID |
**响应示例**:
```json
{
"code": 1,
"message": "操作成功",
"data": [
{
"id": 1,
"service_name": "测试服务",
"preview_image_url": "图片URL",
"description": "服务描述",
"service_type": "服务类型",
"status": "active",
"logs": [
{
"id": 1,
"status": 1,
"service_content": "服务内容",
"service_staff": "教练姓名",
"service_time": "服务时间",
"duration": "持续时间",
"customer_feedback": "客户反馈",
"service_rating": 5,
"remark": "备注",
"course_name": "课程名称",
"updated_at": "更新时间"
}
],
"total_count": 3,
"completed_count": 3
}
]
}
```
### 3. 体测记录接口(增删改查)
**描述**: 体测记录的完整CRUD操作
**接口列表**:
- `GET /api/xy/physicalTest` - 获取体测记录列表
- `GET /api/xy/physicalTest/info` - 获取体测记录详情
- `POST /api/xy/physicalTest/add` - 添加体测记录
- `POST /api/xy/physicalTest/edit` - 编辑体测记录
- `POST /api/xy/physicalTest/delete` - 删除体测记录
**数据表结构**:
```sql
CREATE TABLE `school_physical_test` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '体测编号',
`resource_id` int NOT NULL COMMENT '资源ID',
`student_id` int DEFAULT NULL COMMENT '学员ID',
`age` int NOT NULL DEFAULT '0' COMMENT '学员年龄',
`height` decimal(5,2) NOT NULL COMMENT '身高',
`weight` decimal(5,2) NOT NULL COMMENT '体重',
`coach_id` int DEFAULT NULL COMMENT '教练ID',
`seated_forward_bend` decimal(5,2) DEFAULT NULL COMMENT '坐位体前屈',
`sit_ups` decimal(5,2) DEFAULT NULL COMMENT '仰卧起坐',
`push_ups` decimal(5,2) DEFAULT NULL COMMENT '俯卧撑',
`flamingo_balance` decimal(5,2) DEFAULT NULL COMMENT '单脚站立',
`thirty_sec_jump` decimal(5,2) DEFAULT NULL COMMENT '30秒跳绳',
`standing_long_jump` decimal(5,2) DEFAULT NULL COMMENT '立定跳远',
`agility_run` decimal(5,2) DEFAULT NULL COMMENT '敏捷跑',
`balance_beam` decimal(5,2) DEFAULT NULL COMMENT '平衡木',
`tennis_throw` decimal(5,2) DEFAULT NULL COMMENT '网球掷远',
`ten_meter_shuttle_run` decimal(5,2) DEFAULT NULL COMMENT '10米折返跑',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`physical_test_report` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '体测报告附件(多文件)',
PRIMARY KEY (`id`) USING BTREE
)
```
### 4. 学习计划接口(基于自定义表单)
**描述**: 基于diy_form自定义表单的学习计划管理系统
**接口列表**:
- `GET /api/xy/studyPlan` - 获取学习计划列表
- `GET /api/xy/studyPlan/info` - 获取学习计划详情
- `POST /api/xy/studyPlan/add` - 添加学习计划
- `POST /api/xy/studyPlan/edit` - 编辑学习计划
- `POST /api/xy/studyPlan/delete` - 删除学习计划
- `POST /api/xy/studyPlan/updateProgress` - 更新学习计划进度
**数据表**: 使用diy_form自定义表单系统
- `school_diy_form` - 表单定义 (type='study_plan')
- `school_diy_form_fields` - 字段定义
- `school_diy_form_records` - 记录数据
**响应数据结构**:
```json
{
"code": 1,
"message": "操作成功",
"data": [
{
"id": 1,
"student_id": 1,
"plan_name": "基础体能训练计划",
"plan_content": "针对学员的基础体能进行系统性训练",
"plan_type": "体能训练",
"status": "active",
"progress": 65,
"start_date": "2024-01-15",
"end_date": "2024-03-15",
"target_goals": "提升学员整体体能水平",
"learning_materials": "体能训练器材、训练计划表",
"evaluation_criteria": "体能测试成绩、训练完成度",
"remark": "备注信息",
"create_time": "2024-01-10 14:30:00",
"update_time": "2024-01-10 14:30:00"
}
]
}
```
## 前端集成状态
### ✅ 已完成联调的接口:
1. **课程信息** - `getStudentCourseInfo` (完整联调,Mock与API一致)
2. **服务列表** - `getStudentServiceList` (API正常工作)
3. **体测记录** - `xy_physicalTest` (列表API + 增删改查API完整)
4. **学习计划** - `getStudyPlanList` (基于自定义表单的完整CRUD)
### 🔧 技术实现特点:
- **统一响应格式**: 所有接口使用统一的 `{code: 1, data: [], msg: "操作成功"}` 格式
- **JWT认证**: 所有接口通过token头进行身份验证
- **参数验证**: 完整的参数校验和错误处理
- **数据库事务**: 确保数据一致性
- **错误处理**: 完善的异常处理和日志记录
### 5. 个人资料接口(员工信息管理)
**描述**: 员工个人资料的查看和编辑功能,涉及基础信息和详细信息两个数据表
**接口列表**:
- `GET /api/getPersonnelInfo` - 获取员工基础信息
- `POST /api/updatePersonnelInfo` - 更新员工基础信息
- `GET /api/getPersonnelDetailInfo` - 获取员工详细信息
- `POST /api/updatePersonnelDetailInfo` - 更新员工详细信息
#### 基础信息接口
**接口**: `GET /api/getPersonnelInfo`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | int | 否 | 员工ID,不传则获取当前登录用户信息 |
**响应示例**:
```json
{
"code": 1,
"message": "操作成功",
"data": {
"id": 1,
"name": "张三",
"head_img": "/uploads/avatar/20240101/avatar.jpg",
"gender": 1,
"birthday": "1990-05-15",
"phone": "13800138000",
"email": "zhangsan@example.com",
"wx": "zhangsan_wx",
"address": "北京市朝阳区XXX小区",
"native_place": "山东济南",
"education": "本科",
"profile": "个人简介内容",
"emergency_contact_phone": "13900139000",
"id_card_front": "/uploads/idcard/front.jpg",
"id_card_back": "/uploads/idcard/back.jpg",
"employee_number": "EMP001",
"status": 2,
"account_type": "teacher",
"join_time": "2024-01-01 09:00:00",
"create_time": "2024-01-01 09:00:00",
"update_time": "2024-01-01 09:00:00"
}
}
```
**接口**: `POST /api/updatePersonnelInfo`
**请求参数**:
```json
{
"id": 1,
"name": "张三",
"head_img": "/uploads/avatar/20240101/avatar.jpg",
"gender": 1,
"birthday": "1990-05-15",
"phone": "13800138000",
"email": "zhangsan@example.com",
"wx": "zhangsan_wx",
"address": "北京市朝阳区XXX小区",
"native_place": "山东济南",
"education": "本科",
"profile": "个人简介内容",
"emergency_contact_phone": "13900139000",
"id_card_front": "/uploads/idcard/front.jpg",
"id_card_back": "/uploads/idcard/back.jpg"
}
```
#### 详细信息接口
**接口**: `GET /api/getPersonnelDetailInfo`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| person_id | int | 是 | 员工ID |
**响应示例**:
```json
{
"code": 1,
"message": "操作成功",
"data": {
"id": 1,
"person_id": 1,
"name": "张三花名",
"store": "朝阳校区",
"ethnicity": "汉族",
"age": 30,
"tenure": "3年2个月",
"regular_date": "2024-07-01",
"is_regular": "是",
"politics": "群众",
"university": "北京大学",
"major": "体育教育",
"graduation_date": "2015-06-30",
"household_place": "山东省济南市",
"household_type": "城镇户口",
"household_address": "山东省济南市历下区XXX街道",
"current_address": "北京市朝阳区XXX小区",
"emergency_contact": "张父",
"emergency_phone": "13900139000",
"marital_status": "已婚",
"bank_card": "6222021234567890123",
"bank_name": "中国银行北京分行",
"contract_expire": "2025-12-31",
"is_rehired": "否",
"remark": "备注信息",
"created_at": "2024-01-01 09:00:00",
"updated_at": "2024-01-01 09:00:00"
}
}
```
**接口**: `POST /api/updatePersonnelDetailInfo`
**请求参数**: 与详细信息响应数据格式相同
#### 涉及数据表:
- `school_personnel` - 员工基础信息表
- `school_personnel_info` - 员工详细信息表
#### 字段重复处理:
基于数据库分析,需要删除以下重复字段:
```sql
-- 删除重复字段
ALTER TABLE school_personnel_info
DROP COLUMN birthday,
DROP COLUMN education,
DROP COLUMN native_place;
```
#### 业务逻辑:
1. **只读字段**: `employee_number`(员工编号)、`tenure`(司龄)只能查看不能修改
2. **司龄计算**: 根据`join_time`自动计算并更新
3. **图片上传**: 支持头像、身份证正反面图片上传
4. **数据验证**:
- 手机号格式验证
- 邮箱格式验证
- 身份证号码验证
5. **权限控制**: 员工只能编辑自己的信息
#### 前端页面:
- 路径: `/pages/common/profile/personal_info.vue`
- 功能: 查看、编辑员工个人资料
- 特性: 响应式设计、表单验证、图片上传、分段展示
---
*最后更新:2024-12-29*
*备注:后续接口开发请及时更新此文档*

0
niucloud/TASK.md

97
niucloud/app/api/controller/apiController/PhysicalTest.php

@ -62,4 +62,101 @@ class PhysicalTest extends BaseApiService
return success($res['data']);
}
//添加体测记录
public function add(Request $request)
{
$data = $request->param();
// 验证必填字段
$required_fields = ['resource_id', 'student_id', 'age', 'height', 'weight'];
foreach ($required_fields as $field) {
if (empty($data[$field])) {
return fail("缺少参数:{$field}");
}
}
try {
$res = (new PhysicalTestService())->add($data);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data'], '添加成功');
} catch (\Exception $e) {
return fail('添加失败:' . $e->getMessage());
}
}
//编辑体测记录
public function edit(Request $request)
{
$data = $request->param();
if (empty($data['id'])) {
return fail('缺少参数:id');
}
try {
$res = (new PhysicalTestService())->edit($data);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data'], '修改成功');
} catch (\Exception $e) {
return fail('修改失败:' . $e->getMessage());
}
}
//删除体测记录
public function delete(Request $request)
{
$id = $request->param('id', '');
if (empty($id)) {
return fail('缺少参数:id');
}
try {
$res = (new PhysicalTestService())->delete($id);
if (!$res['code']) {
return fail($res['msg']);
}
return success([], '删除成功');
} catch (\Exception $e) {
return fail('删除失败:' . $e->getMessage());
}
}
// 上传PDF文件
public function uploadPdf(Request $request)
{
try {
$file = $request->file('file');
if (empty($file)) {
return fail('未找到上传文件');
}
// 验证文件类型
$allowedTypes = ['pdf'];
$extension = strtolower($file->getOriginalExtension());
if (!in_array($extension, $allowedTypes)) {
return fail('只允许上传PDF文件');
}
// 验证文件大小 (最大10MB)
$maxSize = 10 * 1024 * 1024; // 10MB
if ($file->getSize() > $maxSize) {
return fail('文件大小不能超过10MB');
}
$res = (new PhysicalTestService())->uploadPdf($file);
if ($res['code']) {
return success($res['data'], '文件上传成功');
} else {
return fail($res['msg']);
}
} catch (\Exception $e) {
return fail('上传失败:' . $e->getMessage());
}
}
}

179
niucloud/app/api/controller/apiController/StudyPlan.php

@ -0,0 +1,179 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\api\controller\apiController;
use app\Request;
use app\service\api\apiService\StudyPlanService;
use core\base\BaseApiService;
/**
* 学习计划相关接口
* Class StudyPlan
* @package app\api\controller\apiController
*/
class StudyPlan extends BaseApiService
{
/**
* 获取学习计划列表
* @param Request $request
* @return \think\Response
*/
public function index(Request $request)
{
$student_id = $request->param('student_id', '');
if (empty($student_id)) {
return fail('学员ID不能为空');
}
try {
$res = (new StudyPlanService())->getList($student_id);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data']);
} catch (\Exception $e) {
return fail('获取学习计划失败:' . $e->getMessage());
}
}
/**
* 获取学习计划详情
* @param Request $request
* @return \think\Response
*/
public function info(Request $request)
{
$record_id = $request->param('record_id', '');
if (empty($record_id)) {
return fail('记录ID不能为空');
}
try {
$res = (new StudyPlanService())->getInfo($record_id);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data']);
} catch (\Exception $e) {
return fail('获取学习计划详情失败:' . $e->getMessage());
}
}
/**
* 添加学习计划
* @param Request $request
* @return \think\Response
*/
public function add(Request $request)
{
$data = $request->param();
// 验证必填字段
$required_fields = ['student_id', 'plan_name', 'plan_content'];
foreach ($required_fields as $field) {
if (empty($data[$field])) {
return fail("缺少参数:{$field}");
}
}
try {
$res = (new StudyPlanService())->add($data);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data'], '添加成功');
} catch (\Exception $e) {
return fail('添加失败:' . $e->getMessage());
}
}
/**
* 编辑学习计划
* @param Request $request
* @return \think\Response
*/
public function edit(Request $request)
{
$data = $request->param();
if (empty($data['record_id'])) {
return fail('缺少参数:record_id');
}
try {
$res = (new StudyPlanService())->edit($data);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data'], '修改成功');
} catch (\Exception $e) {
return fail('修改失败:' . $e->getMessage());
}
}
/**
* 删除学习计划
* @param Request $request
* @return \think\Response
*/
public function delete(Request $request)
{
$record_id = $request->param('record_id', '');
if (empty($record_id)) {
return fail('缺少参数:record_id');
}
try {
$res = (new StudyPlanService())->delete($record_id);
if (!$res['code']) {
return fail($res['msg']);
}
return success([], '删除成功');
} catch (\Exception $e) {
return fail('删除失败:' . $e->getMessage());
}
}
/**
* 更新学习计划进度
* @param Request $request
* @return \think\Response
*/
public function updateProgress(Request $request)
{
$record_id = $request->param('record_id', '');
$progress = $request->param('progress', 0);
if (empty($record_id)) {
return fail('缺少参数:record_id');
}
if ($progress < 0 || $progress > 100) {
return fail('进度值必须在0-100之间');
}
try {
$res = (new StudyPlanService())->updateProgress($record_id, $progress);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data'], '更新成功');
} catch (\Exception $e) {
return fail('更新失败:' . $e->getMessage());
}
}
}

22
niucloud/app/api/controller/login/Login.php

@ -117,17 +117,17 @@ class Login extends BaseController
}
//销售教师人员登陆
public function personnelLogin()
{
$data = $this->request->params([
['phone', ''],
['password', ''],
['login_type', ''],//登陆类型|1=教练,2=销售
]);
//验证码验证
$result = (new LoginService())->loginByPersonnel($data);
return success($result);//code|1正确
}
// public function personnelLogin()
// {
// $data = $this->request->params([
// ['phone', ''],
// ['password', ''],
// ['login_type', ''],//登陆类型|1=教练,2=销售
// ]);
// //验证码验证
// $result = (new LoginService())->loginByPersonnel($data);
// return success($result);//code|1正确
// }
public function test(){
$order = new OrderTable();

21
niucloud/app/api/route/route.php

@ -484,12 +484,33 @@ Route::group(function () {
Route::get('xy/physicalTest', 'apiController.PhysicalTest/index');
//学生端-体测报告-详情
Route::get('xy/physicalTest/info', 'apiController.PhysicalTest/info');
//学生端-体测报告-添加
Route::post('xy/physicalTest/add', 'apiController.PhysicalTest/add');
//学生端-体测报告-编辑
Route::post('xy/physicalTest/edit', 'apiController.PhysicalTest/edit');
//学生端-体测报告-删除
Route::post('xy/physicalTest/delete', 'apiController.PhysicalTest/delete');
//学生端-体测报告-PDF上传
Route::post('xy/physicalTest/uploadPdf', 'apiController.PhysicalTest/uploadPdf');
//学生端-学生课程安排-列表
Route::get('xy/personCourseSchedule', 'apiController.PersonCourseSchedule/index');
//学生端-学生课程安排-详情
Route::get('xy/personCourseSchedule/info', 'apiController.PersonCourseSchedule/info');
//学生端-学习计划-列表
Route::get('xy/studyPlan', 'apiController.StudyPlan/index');
//学生端-学习计划-详情
Route::get('xy/studyPlan/info', 'apiController.StudyPlan/info');
//学生端-学习计划-添加
Route::post('xy/studyPlan/add', 'apiController.StudyPlan/add');
//学生端-学习计划-编辑
Route::post('xy/studyPlan/edit', 'apiController.StudyPlan/edit');
//学生端-学习计划-删除
Route::post('xy/studyPlan/delete', 'apiController.StudyPlan/delete');
//学生端-学习计划-更新进度
Route::post('xy/studyPlan/updateProgress', 'apiController.StudyPlan/updateProgress');
//学生端-学生课程安排-修改请假状态
Route::post('xy/personCourseSchedule/editStatus', 'apiController.PersonCourseSchedule/editStatus');
//学生端-学生课程安排-获取排课日历

15
niucloud/app/service/admin/pay/PayService.php

@ -268,7 +268,20 @@ class PayService extends BaseAdminService
'channel' => '微信扫码支付'
]);
return ['qrcode_url' => getCurrentDomain().$path,'out_trade_no'=>$out_trade_no,'code_url'=> $url['code_url']];
// 开发环境:读取二维码文件并转换为base64
$qrcode_base64 = '';
$full_path = public_path() . $path;
if (file_exists($full_path)) {
$qrcode_content = file_get_contents($full_path);
$qrcode_base64 = 'data:image/png;base64,' . base64_encode($qrcode_content);
}
return [
'qrcode_url' => getCurrentDomain() . $path, // 保留原URL
'qrcode_base64' => $qrcode_base64, // 新增base64字段
'out_trade_no' => $out_trade_no,
'code_url' => $url['code_url']
];
}
public function check_payment_status($data)

1
niucloud/app/service/api/apiService/CourseService.php

@ -447,7 +447,6 @@ class CourseService extends BaseApiService
'time_slot' => $data['time_slot'],
'schedule_type' => $data['schedule_type'] ?? 1, // 1=正式位, 2=等待位
'course_type' => $data['course_type'] ?? 1, // 1=正式课, 2=体验课, 3=等待位
'position' => $data['position'] ?? '', // 位置信息
'remark' => $data['remark'] ?? '' // 备注
]);
$CourseSchedule->where(['id' => $data['schedule_id']])->dec("available_capacity")->update();

254
niucloud/app/service/api/apiService/PhysicalTestService.php

@ -112,4 +112,258 @@ class PhysicalTestService extends BaseApiService
return $res;
}
}
/**
* 添加体测记录
* @param array $data
* @return array
*/
public function add(array $data)
{
try {
$model = new PhysicalTest();
// 设置基础数据
$physicalTestData = [
'resource_id' => $data['resource_id'],
'student_id' => $data['student_id'],
'age' => $data['age'],
'height' => $data['height'],
'weight' => $data['weight'],
'coach_id' => $data['coach_id'] ?? null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
// 设置体测项目数据
$test_items = [
'seated_forward_bend', 'sit_ups', 'push_ups', 'flamingo_balance',
'thirty_sec_jump', 'standing_long_jump', 'agility_run', 'balance_beam',
'tennis_throw', 'ten_meter_shuttle_run'
];
foreach ($test_items as $item) {
if (isset($data[$item])) {
$physicalTestData[$item] = $data[$item];
}
}
// 处理体测报告附件
if (!empty($data['physical_test_report'])) {
$physicalTestData['physical_test_report'] = is_array($data['physical_test_report'])
? implode(',', $data['physical_test_report'])
: $data['physical_test_report'];
}
$result = $model->create($physicalTestData);
if ($result) {
return [
'code' => 1,
'msg' => '添加成功',
'data' => $result->toArray()
];
} else {
return [
'code' => 0,
'msg' => '添加失败',
'data' => []
];
}
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '添加失败:' . $e->getMessage(),
'data' => []
];
}
}
/**
* 编辑体测记录
* @param array $data
* @return array
*/
public function edit(array $data)
{
try {
$model = new PhysicalTest();
$record = $model->find($data['id']);
if (!$record) {
return [
'code' => 0,
'msg' => '记录不存在',
'data' => []
];
}
// 更新基础数据
$updateData = [
'updated_at' => date('Y-m-d H:i:s')
];
// 允许更新的字段
$allowed_fields = [
'age', 'height', 'weight', 'coach_id',
'seated_forward_bend', 'sit_ups', 'push_ups', 'flamingo_balance',
'thirty_sec_jump', 'standing_long_jump', 'agility_run', 'balance_beam',
'tennis_throw', 'ten_meter_shuttle_run'
];
foreach ($allowed_fields as $field) {
if (isset($data[$field])) {
$updateData[$field] = $data[$field];
}
}
// 处理体测报告附件
if (isset($data['physical_test_report'])) {
$updateData['physical_test_report'] = is_array($data['physical_test_report'])
? implode(',', $data['physical_test_report'])
: $data['physical_test_report'];
}
$result = $record->save($updateData);
if ($result !== false) {
return [
'code' => 1,
'msg' => '修改成功',
'data' => $record->toArray()
];
} else {
return [
'code' => 0,
'msg' => '修改失败',
'data' => []
];
}
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '修改失败:' . $e->getMessage(),
'data' => []
];
}
}
/**
* 删除体测记录
* @param int $id
* @return array
*/
public function delete($id)
{
try {
$model = new PhysicalTest();
$record = $model->find($id);
if (!$record) {
return [
'code' => 0,
'msg' => '记录不存在',
'data' => []
];
}
$result = $record->delete();
if ($result) {
return [
'code' => 1,
'msg' => '删除成功',
'data' => []
];
} else {
return [
'code' => 0,
'msg' => '删除失败',
'data' => []
];
}
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '删除失败:' . $e->getMessage(),
'data' => []
];
}
}
/**
* 上传PDF文件
* @param $file
* @return array
*/
public function uploadPdf($file)
{
try {
if (!$file || !$file->isValid()) {
return [
'code' => 0,
'msg' => '上传的文件无效',
'data' => []
];
}
// 验证文件类型
$allowedExtensions = ['pdf'];
$fileExtension = strtolower($file->getOriginalExtension());
if (!in_array($fileExtension, $allowedExtensions)) {
return [
'code' => 0,
'msg' => '只支持PDF文件格式',
'data' => []
];
}
// 验证文件大小(限制10MB)
$maxSize = 10 * 1024 * 1024; // 10MB
if ($file->getSize() > $maxSize) {
return [
'code' => 0,
'msg' => '文件大小不能超过10MB',
'data' => []
];
}
// 设置上传目录
$uploadDir = './uploads/physical_test/pdf/' . date('Y/m/');
// 如果目录不存在,创建目录
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// 生成唯一文件名
$fileName = date('YmdHis') . '_' . uniqid() . '.pdf';
// 移动文件
$file->move($uploadDir, $fileName);
// 构建完整路径
$fullPath = $uploadDir . $fileName;
$webPath = '/uploads/physical_test/pdf/' . date('Y/m/') . $fileName;
// 返回文件信息
return [
'code' => 1,
'msg' => '上传成功',
'data' => [
'file_name' => $fileName,
'file_path' => $fullPath,
'file_url' => $webPath,
'file_size' => filesize($fullPath),
'upload_time' => date('Y-m-d H:i:s')
]
];
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '上传失败:' . $e->getMessage(),
'data' => []
];
}
}
}

326
niucloud/app/service/api/apiService/StudyPlanService.php

@ -0,0 +1,326 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\service\api\apiService;
use app\model\diy_form\DiyForm;
use app\model\diy_form\DiyFormRecords;
use core\base\BaseApiService;
/**
* 学习计划API服务层
* Class StudyPlanService
* @package app\service\api\apiService
*/
class StudyPlanService extends BaseApiService
{
public function __construct()
{
parent::__construct();
}
/**
* 获取学习计划列表
* @param int $studentId 学员ID
* @return array
*/
public function getList($studentId)
{
try {
// 获取学习计划表单ID
$studyPlanForm = DiyForm::where('type', 'study_plan')->find();
if (!$studyPlanForm) {
return ['code' => 0, 'data' => [], 'msg' => '学习计划表单不存在'];
}
// 查询学习计划记录
$records = DiyFormRecords::where('form_id', $studyPlanForm->form_id)
->where('student_id', $studentId)
->order('create_time desc')
->select()
->toArray();
$result = [];
foreach ($records as $record) {
$value = is_string($record['value']) ? (json_decode($record['value'], true) ?: []) : (is_array($record['value']) ? $record['value'] : []);
$result[] = [
'id' => $record['record_id'],
'student_id' => $record['student_id'],
'plan_name' => $value['plan_name'] ?? '',
'plan_content' => $value['plan_content'] ?? '',
'plan_type' => $value['plan_type'] ?? '常规计划',
'status' => $value['status'] ?? 'active',
'progress' => (int)($value['progress'] ?? 0),
'start_date' => $value['start_date'] ?? '',
'end_date' => $value['end_date'] ?? '',
'target_goals' => $value['target_goals'] ?? '',
'learning_materials' => $value['learning_materials'] ?? '',
'evaluation_criteria' => $value['evaluation_criteria'] ?? '',
'remark' => $value['remark'] ?? '',
'create_time' => is_numeric($record['create_time']) ? date('Y-m-d H:i:s', $record['create_time']) : $record['create_time'],
'update_time' => is_numeric($record['create_time']) ? date('Y-m-d H:i:s', $record['create_time']) : $record['create_time']
];
}
return ['code' => 1, 'data' => $result, 'msg' => '获取成功'];
} catch (\Exception $e) {
return ['code' => 0, 'data' => [], 'msg' => '获取失败:' . $e->getMessage()];
}
}
/**
* 获取学习计划详情
* @param int $recordId 记录ID
* @return array
*/
public function getInfo($recordId)
{
try {
$record = DiyFormRecords::find($recordId);
if (!$record) {
return ['code' => 0, 'data' => [], 'msg' => '记录不存在'];
}
$value = is_string($record->value) ? (json_decode($record->value, true) ?: []) : (is_array($record->value) ? $record->value : []);
$result = [
'id' => $record->record_id,
'student_id' => $record->student_id,
'plan_name' => $value['plan_name'] ?? '',
'plan_content' => $value['plan_content'] ?? '',
'plan_type' => $value['plan_type'] ?? '常规计划',
'status' => $value['status'] ?? 'active',
'progress' => (int)($value['progress'] ?? 0),
'start_date' => $value['start_date'] ?? '',
'end_date' => $value['end_date'] ?? '',
'target_goals' => $value['target_goals'] ?? '',
'learning_materials' => $value['learning_materials'] ?? '',
'evaluation_criteria' => $value['evaluation_criteria'] ?? '',
'remark' => $value['remark'] ?? '',
'create_time' => is_numeric($record->create_time) ? date('Y-m-d H:i:s', $record->create_time) : $record->create_time,
'update_time' => is_numeric($record->create_time) ? date('Y-m-d H:i:s', $record->create_time) : $record->create_time
];
return ['code' => 1, 'data' => $result, 'msg' => '获取成功'];
} catch (\Exception $e) {
return ['code' => 0, 'data' => [], 'msg' => '获取失败:' . $e->getMessage()];
}
}
/**
* 添加学习计划
* @param array $data
* @return array
*/
public function add(array $data)
{
try {
// 获取学习计划表单ID
$studyPlanForm = DiyForm::where('type', 'study_plan')->find();
if (!$studyPlanForm) {
return ['code' => 0, 'data' => [], 'msg' => '学习计划表单不存在'];
}
// 构建表单数据
$formValue = [
'plan_name' => $data['plan_name'],
'plan_content' => $data['plan_content'],
'plan_type' => $data['plan_type'] ?? '常规计划',
'status' => $data['status'] ?? 'active',
'progress' => (int)($data['progress'] ?? 0),
'start_date' => $data['start_date'] ?? '',
'end_date' => $data['end_date'] ?? '',
'target_goals' => $data['target_goals'] ?? '',
'learning_materials' => $data['learning_materials'] ?? '',
'evaluation_criteria' => $data['evaluation_criteria'] ?? '',
'remark' => $data['remark'] ?? ''
];
// 创建记录
$record = DiyFormRecords::create([
'form_id' => $studyPlanForm->form_id,
'value' => json_encode($formValue),
'member_id' => $this->member_id ?? 0,
'relate_id' => 0,
'student_id' => $data['student_id'],
'create_time' => time()
]);
if ($record) {
$result = [
'id' => $record->record_id,
'student_id' => $record->student_id,
'plan_name' => $formValue['plan_name'],
'plan_content' => $formValue['plan_content'],
'plan_type' => $formValue['plan_type'],
'status' => $formValue['status'],
'progress' => $formValue['progress'],
'start_date' => $formValue['start_date'],
'end_date' => $formValue['end_date'],
'target_goals' => $formValue['target_goals'],
'learning_materials' => $formValue['learning_materials'],
'evaluation_criteria' => $formValue['evaluation_criteria'],
'remark' => $formValue['remark'],
'create_time' => is_numeric($record->create_time) ? date('Y-m-d H:i:s', $record->create_time) : $record->create_time
];
return ['code' => 1, 'data' => $result, 'msg' => '添加成功'];
} else {
return ['code' => 0, 'data' => [], 'msg' => '添加失败'];
}
} catch (\Exception $e) {
return ['code' => 0, 'data' => [], 'msg' => '添加失败:' . $e->getMessage()];
}
}
/**
* 编辑学习计划
* @param array $data
* @return array
*/
public function edit(array $data)
{
try {
$record = DiyFormRecords::find($data['record_id']);
if (!$record) {
return ['code' => 0, 'data' => [], 'msg' => '记录不存在'];
}
// 获取现有数据
$existingValue = is_string($record->value) ? (json_decode($record->value, true) ?: []) : (is_array($record->value) ? $record->value : []);
// 更新字段
$formValue = $existingValue;
$updateFields = [
'plan_name', 'plan_content', 'plan_type', 'status', 'progress',
'start_date', 'end_date', 'target_goals', 'learning_materials',
'evaluation_criteria', 'remark'
];
foreach ($updateFields as $field) {
if (isset($data[$field])) {
$formValue[$field] = ($field === 'progress') ? (int)$data[$field] : $data[$field];
}
}
// 更新记录
$result = $record->save([
'value' => json_encode($formValue)
]);
if ($result !== false) {
$resultData = [
'id' => $record->record_id,
'student_id' => $record->student_id,
'plan_name' => $formValue['plan_name'] ?? '',
'plan_content' => $formValue['plan_content'] ?? '',
'plan_type' => $formValue['plan_type'] ?? '常规计划',
'status' => $formValue['status'] ?? 'active',
'progress' => (int)($formValue['progress'] ?? 0),
'start_date' => $formValue['start_date'] ?? '',
'end_date' => $formValue['end_date'] ?? '',
'target_goals' => $formValue['target_goals'] ?? '',
'learning_materials' => $formValue['learning_materials'] ?? '',
'evaluation_criteria' => $formValue['evaluation_criteria'] ?? '',
'remark' => $formValue['remark'] ?? '',
'create_time' => is_numeric($record->create_time) ? date('Y-m-d H:i:s', $record->create_time) : $record->create_time
];
return ['code' => 1, 'data' => $resultData, 'msg' => '修改成功'];
} else {
return ['code' => 0, 'data' => [], 'msg' => '修改失败'];
}
} catch (\Exception $e) {
return ['code' => 0, 'data' => [], 'msg' => '修改失败:' . $e->getMessage()];
}
}
/**
* 删除学习计划
* @param int $recordId
* @return array
*/
public function delete($recordId)
{
try {
$record = DiyFormRecords::find($recordId);
if (!$record) {
return ['code' => 0, 'data' => [], 'msg' => '记录不存在'];
}
$result = $record->delete();
if ($result) {
return ['code' => 1, 'data' => [], 'msg' => '删除成功'];
} else {
return ['code' => 0, 'data' => [], 'msg' => '删除失败'];
}
} catch (\Exception $e) {
return ['code' => 0, 'data' => [], 'msg' => '删除失败:' . $e->getMessage()];
}
}
/**
* 更新学习计划进度
* @param int $recordId
* @param int $progress
* @return array
*/
public function updateProgress($recordId, $progress)
{
try {
$record = DiyFormRecords::find($recordId);
if (!$record) {
return ['code' => 0, 'data' => [], 'msg' => '记录不存在'];
}
// 获取现有数据
$existingValue = is_string($record->value) ? (json_decode($record->value, true) ?: []) : (is_array($record->value) ? $record->value : []);
$existingValue['progress'] = (int)$progress;
// 根据进度自动更新状态
if ($progress >= 100) {
$existingValue['status'] = 'completed';
} elseif ($progress > 0) {
$existingValue['status'] = 'in_progress';
} else {
$existingValue['status'] = 'active';
}
// 更新记录
$result = $record->save([
'value' => json_encode($existingValue)
]);
if ($result !== false) {
$resultData = [
'id' => $record->record_id,
'progress' => (int)$progress,
'status' => $existingValue['status']
];
return ['code' => 1, 'data' => $resultData, 'msg' => '更新成功'];
} else {
return ['code' => 0, 'data' => [], 'msg' => '更新失败'];
}
} catch (\Exception $e) {
return ['code' => 0, 'data' => [], 'msg' => '更新失败:' . $e->getMessage()];
}
}
}

92
uniapp/PLANNING.md

@ -0,0 +1,92 @@
✅ 已完成:uniapp/components/service-list-card/index.vue数据问题
- 前端:已添加完整的Mock数据支持,组件可正常显示测试数据
- 后端:PHP后端工程师已成功补充测试数据到数据库
- API测试:接口 `/xy/service/list?student_id=1` 已验证返回完整数据
- 最终状态:前端组件可以正常获取和显示真实的服务列表数据
API返回数据包含:
- 3种不同类型的服务(体测服务、测试服务、1V1+情感沟通)
- 每个服务包含完整的日志记录、评分、反馈信息
- 服务状态、教练信息、时间等完整字段
- 数据结构完全符合前端组件期望格式
✅ 已完成:uniapp/components/service-list-card/index.vue弹窗无法关闭问题
- 问题原因:事件绑定和微信小程序兼容性问题
- 修复内容:添加了事件防冒泡处理,优化微信小程序环境下的事件响应
- 解决方案:增强了响应式更新机制,添加了调试功能
- 验证状态:已修复弹窗关闭功能,支持微信小程序环境
✅ 已处理:components/course-info-card/index.vue页面的接口需求分析
- 已完成接口需求分析,包含完整的数据结构和字段定义
- 已将详细的接口规格写入 niucloud/PLANNING.md 中
- 等待PHP后端工程师按照规格开发接口
- 开发完成后需要进行联调测试
✅ 已修复:课程安排、订单列表、服务列表按钮点击无法打开弹窗问题
- 问题原因:事件冒泡导致按钮点击事件冒泡到遮罩层,立即触发关闭逻辑
- 修复方案:为所有触发弹窗的按钮添加 `.stop` 事件修饰符阻止冒泡
- 涉及组件:主页面、OrderListCard、ServiceListCard等所有弹窗相关按钮
- 验证状态:弹窗打开和关闭功能均正常,微信小程序环境兼容
✅ 已修复:pages/market/clue/clue_info.vue页面所有点击事件失灵问题
- 问题原因:底部弹窗组件的遮罩层一直存在并覆盖整个视口,拦截所有页面点击
- 核心问题:遮罩层即使弹窗隐藏时仍然监听点击事件,导致所有操作被误判为遮罩点击
- 修复方案:
- 为BottomPopup组件添加 `v-if="visible"` 条件渲染
- 关键元素添加 `@click.stop` 阻止事件冒泡
- 优化弹窗关闭时的数据重置逻辑
- 影响范围:修复了页面上所有按钮、标签切换、操作项的点击响应
- 验证状态:页面交互功能完全恢复正常,微信小程序环境兼容
📋 体测记录PDF附件预览功能需求
**背景**:体测记录弹窗需要支持PDF附件预览功能,允许用户查看体测报告
**需求描述**:
1. 体测记录数据中需要包含 `pdf_files` 字段,包含附件信息
2. 需要提供PDF文件预览接口,支持在线预览或下载
3. 前端需要实现PDF预览功能,兼容微信小程序环境
**接口需求**:
1. 获取体测记录接口需要返回PDF附件信息
2. 新增PDF文件预览/下载接口
- 接口路径:`/api/fitness/record/pdf/preview`
- 请求方式:GET
- 请求参数:
```json
{
"file_id": "文件ID",
"record_id": "体测记录ID"
}
```
- 响应数据:
```json
{
"code": 1,
"msg": "success",
"data": {
"file_url": "PDF文件访问URL",
"file_name": "文件名称",
"file_size": 1024000,
"preview_url": "在线预览URL(可选)",
"download_url": "下载URL"
}
}
```
**数据结构扩展**:
体测记录数据中的 `pdf_files` 字段结构:
```json
{
"pdf_files": [
{
"id": "文件ID",
"name": "体测报告.pdf",
"size": 1024000,
"upload_time": "2024-01-15 10:30:00",
"file_path": "/uploads/fitness/2024/01/xxx.pdf"
}
]
}
```
**前端实现**:
- 在 FitnessRecordCard 组件中实现PDF预览按钮
- 支持微信小程序环境下的PDF预览
- 提供下载功能作为备选方案

BIN
uniapp/TASK.md

Binary file not shown.

37
uniapp/api/apiRoute.js

@ -584,6 +584,43 @@ export default {
async xy_physicalTestInfo(data = {}) {
return await http.get('/xy/physicalTest/info', data);
},
//学生端-体测报告-添加
async xy_physicalTestAdd(data = {}) {
return await http.post('/xy/physicalTest/add', data);
},
//学生端-体测报告-编辑
async xy_physicalTestEdit(data = {}) {
return await http.post('/xy/physicalTest/edit', data);
},
//学生端-体测报告-删除
async xy_physicalTestDelete(data = {}) {
return await http.post('/xy/physicalTest/delete', data);
},
//学生端-学习计划-列表
async getStudyPlanList(data = {}) {
return await http.get('/xy/studyPlan', data);
},
//学生端-学习计划-详情
async getStudyPlanInfo(data = {}) {
return await http.get('/xy/studyPlan/info', data);
},
//学生端-学习计划-添加
async addStudyPlan(data = {}) {
return await http.post('/xy/studyPlan/add', data);
},
//学生端-学习计划-编辑
async editStudyPlan(data = {}) {
return await http.post('/xy/studyPlan/edit', data);
},
//学生端-学习计划-删除
async deleteStudyPlan(data = {}) {
return await http.post('/xy/studyPlan/delete', data);
},
//学生端-学习计划-更新进度
async updateStudyPlanProgress(data = {}) {
return await http.post('/xy/studyPlan/updateProgress', data);
},
//学生端-学生课程安排-列表
async xy_personCourseSchedule(data = {}) {
return await http.get('/xy/personCourseSchedule', data);

2
uniapp/common/config.js

@ -1,6 +1,6 @@
// 环境变量配置
const env = process.env.VUE_APP_ENV || 'development'
const isMockEnabled = process.env.VUE_APP_MOCK_ENABLED === 'true' || true // 默认启用Mock数据
const isMockEnabled = process.env.VUE_APP_MOCK_ENABLED === 'true' || false // 默认禁用Mock优先模式,仅作为回退
const isDebug = process.env.VUE_APP_DEBUG === 'true' || true // 默认启用调试模式
// API配置 - 支持环境变量

50
uniapp/components/bottom-popup/index.vue

@ -1,15 +1,20 @@
<!--通用底部弹窗组件-->
<template>
<view class="bottom-popup" :class="{ 'show': visible }" @touchmove.stop.prevent>
<view
class="bottom-popup"
:class="{ 'show': visible }"
v-if="visible"
@touchmove.stop.prevent
>
<!-- 遮罩层 -->
<view
class="mask"
:class="{ 'show': visible }"
@click="handleMaskClick"
@click.stop="handleMaskClick"
></view>
<!-- 弹窗内容 -->
<view class="popup-content" :class="{ 'show': visible }">
<view class="popup-content" :class="{ 'show': visible }" @click.stop="stopPropagation">
<!-- 顶部拖拽条 -->
<view class="drag-handle">
<view class="drag-line"></view>
@ -18,7 +23,7 @@
<!-- 标题栏 -->
<view class="popup-header" v-if="title">
<view class="popup-title">{{ title }}</view>
<view class="close-btn" @click="close">
<view class="close-btn" @click.stop="close">
<text class="close-icon">×</text>
</view>
</view>
@ -29,12 +34,13 @@
:class="{ 'has-title': title, 'has-footer': hasFooter }"
:scroll-y="true"
:enable-passive="true"
@click.stop="stopPropagation"
>
<slot></slot>
</scroll-view>
<!-- 底部操作按钮区域 -->
<view class="popup-footer" v-if="hasFooter">
<view class="popup-footer" v-if="hasFooter" @click.stop="stopPropagation">
<slot name="footer"></slot>
</view>
</view>
@ -76,13 +82,20 @@ export default {
//
handleMaskClick() {
if (this.maskClosable) {
console.log('遮罩点击,准备关闭弹窗')
this.close()
}
},
//
close() {
console.log('弹窗关闭事件触发')
this.$emit('close')
},
//
stopPropagation(event) {
event.stopPropagation()
}
}
}
@ -96,13 +109,17 @@ export default {
width: 100vw;
height: 100vh;
z-index: 1000;
pointer-events: none;
&.show {
pointer-events: auto;
}
}
/* 微信小程序兼容性修复 */
/* #ifdef MP-WEIXIN */
.bottom-popup {
position: fixed !important;
z-index: 1000 !important;
}
/* #endif */
.mask {
position: absolute;
top: 0;
@ -112,12 +129,20 @@ export default {
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: auto;
&.show {
opacity: 1;
}
}
/* 微信小程序遮罩兼容性 */
/* #ifdef MP-WEIXIN */
.mask {
pointer-events: auto !important;
}
/* #endif */
.popup-content {
position: absolute;
bottom: 0;
@ -175,6 +200,13 @@ export default {
justify-content: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
transition: all 0.2s ease;
cursor: pointer;
&:active {
background: rgba(255, 255, 255, 0.2);
transform: scale(0.95);
}
}
.close-icon {

101
uniapp/components/call-record-card/call-record-card.vue

@ -2,28 +2,30 @@
<template>
<view class="call-record-card">
<view class="call-header">
<view class="call-time">{{ formatCallTime(record.call_time) }}</view>
<view class="call-duration">{{ formatDuration(record.duration) }}</view>
<view class="call-time">{{ formatCallTime(record.communication_time) }}</view>
<view class="call-result">{{ getCallResult(record.communication_result) }}</view>
</view>
<view class="call-info">
<view class="info-row">
<text class="info-label">通话类型</text>
<text class="info-value">{{ getCallType(record.call_type) }}</text>
<text class="info-value">{{ getCallType(record.communication_type) }}</text>
</view>
<view class="info-row" v-if="record.caller_name">
<text class="info-label">拨打人员</text>
<text class="info-value">{{ record.caller_name }}</text>
</view>
<view class="info-row" v-if="record.phone_number">
<text class="info-label">通话号码</text>
<text class="info-value">{{ record.phone_number }}</text>
<view class="info-row">
<text class="info-label">通话时间</text>
<text class="info-value">{{ formatCallTime(record.communication_time) }}</text>
</view>
</view>
<view class="call-notes" v-if="record.notes">
<view class="call-notes">
<view class="notes-header">
<text class="notes-label">通话备注</text>
<text class="notes-content">{{ record.notes }}</text>
<view class="edit-btn" @click="handleEdit">
<text class="edit-text">编辑</text>
</view>
</view>
<text class="notes-content" v-if="record.remarks">{{ record.remarks }}</text>
<text class="notes-placeholder" v-else>暂无备注信息</text>
</view>
</view>
</template>
@ -40,28 +42,43 @@ export default {
methods: {
formatCallTime(time) {
if (!time) return '未知时间'
// 使使
if (this.$util && this.$util.formatToDateTime) {
return this.$util.formatToDateTime(time, 'Y-m-d H:i')
},
formatDuration(duration) {
if (!duration || duration <= 0) return '0秒'
const minutes = Math.floor(duration / 60)
const seconds = duration % 60
if (minutes > 0) {
return `${minutes}${seconds}`
}
return `${seconds}`
//
const date = new Date(time)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
},
getCallType(type) {
const typeMap = {
1: '呼出',
2: '呼入',
3: '未接'
'phone': '电话',
'message': '消息',
'wechat': '微信',
'qq': 'QQ'
}
return typeMap[type] || '未知'
return typeMap[type] || type || '电话'
},
getCallResult(result) {
const resultMap = {
'success': '成功',
'failed': '失败',
'busy': '忙线',
'no_answer': '未接听'
}
return resultMap[result] || result || '未知'
},
handleEdit() {
//
this.$emit('remark', this.record)
}
}
}
@ -88,7 +105,7 @@ export default {
font-weight: bold;
}
.call-duration {
.call-result {
color: #29d3b4;
font-size: 22rpx;
background-color: rgba(41, 211, 180, 0.2);
@ -123,11 +140,29 @@ export default {
padding-top: 15rpx;
border-top: 1rpx solid #333;
.notes-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
.notes-label {
color: #999;
font-size: 22rpx;
display: block;
margin-bottom: 8rpx;
}
.edit-btn {
background-color: #29d3b4;
color: white;
padding: 6rpx 15rpx;
border-radius: 15rpx;
font-size: 20rpx;
.edit-text {
color: white;
font-size: 20rpx;
}
}
}
.notes-content {
@ -136,5 +171,11 @@ export default {
line-height: 1.5;
word-break: break-all;
}
.notes-placeholder {
color: #666;
font-size: 22rpx;
font-style: italic;
}
}
</style>

34
uniapp/components/course-info-card/index.vue

@ -36,9 +36,9 @@
<text class="detail-label">课程类型</text>
<text class="detail-value">{{ course.course_type }}</text>
</view>
<view class="detail-item" v-if="course.teacher_name">
<view class="detail-item" v-if="course.teacher_name || course.main_coach_name">
<text class="detail-label">授课教练</text>
<text class="detail-value">{{ course.teacher_name }}</text>
<text class="detail-value">{{ course.main_coach_name || course.teacher_name }}</text>
</view>
<view class="detail-item">
<text class="detail-label">剩余课时</text>
@ -64,9 +64,9 @@
<text class="detail-label">课程价格</text>
<text class="detail-value price">¥{{ course.course_price }}</text>
</view>
<view class="detail-item" v-if="course.class_duration">
<view class="detail-item" v-if="course.class_duration || course.single_session_count">
<text class="detail-label">单节时长</text>
<text class="detail-value">{{ course.class_duration }}分钟</text>
<text class="detail-value">{{ course.single_session_count || course.class_duration }}分钟</text>
</view>
<view class="detail-item" v-if="course.create_time">
<text class="detail-label">创建时间</text>
@ -175,12 +175,16 @@ export default {
<style lang="scss" scoped>
.course-info-card {
padding: 0;
max-height: 60vh;
overflow-y: auto;
}
.course-list {
display: flex;
flex-direction: column;
gap: 24rpx;
max-height: 50vh;
overflow-y: auto;
}
.course-item {
@ -352,4 +356,26 @@ export default {
color: #999999;
line-height: 1.4;
}
/* 滚动条样式 */
.course-info-card::-webkit-scrollbar,
.course-list::-webkit-scrollbar {
width: 6rpx;
}
.course-info-card::-webkit-scrollbar-track,
.course-list::-webkit-scrollbar-track {
background: transparent;
}
.course-info-card::-webkit-scrollbar-thumb,
.course-list::-webkit-scrollbar-thumb {
background: #29D3B4;
border-radius: 3rpx;
}
.course-info-card::-webkit-scrollbar-thumb:hover,
.course-list::-webkit-scrollbar-thumb:hover {
background: #24B89E;
}
</style>

93
uniapp/components/fitness-record-card/fitness-record-card.vue

@ -1,9 +1,10 @@
<!--体测记录卡片组件-->
<template>
<view class="fitness-record-card">
<view class="fitness-record-card" @click="handleCardClick">
<view class="record-header">
<view class="record-date">{{ record.test_date }}</view>
<view class="record-status">已完成</view>
<view class="edit-btn" @click.stop="handleEditClick">编辑</view>
</view>
<view class="record-data">
@ -24,7 +25,7 @@
class="file-item"
v-for="pdf in record.pdf_files"
:key="pdf.id"
@click="handleFileClick(pdf)"
@click.stop="handleFileClick(pdf)"
>
<view class="file-icon">📄</view>
<view class="file-info">
@ -50,7 +51,69 @@ export default {
}
},
methods: {
handleFileClick(file) {
//
handleCardClick() {
this.handleEditClick()
},
//
handleEditClick() {
this.$emit('edit', this.record)
},
async handleFileClick(file) {
try {
// PDF
const response = await this.$api.get('/fitness/record/pdf/preview', {
file_id: file.id,
record_id: this.record.id
})
if (response.code === 1 && response.data) {
// PDF
uni.downloadFile({
url: response.data.file_url,
success: (res) => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
fileType: 'pdf',
success: () => {
console.log('PDF预览成功')
},
fail: (err) => {
console.error('PDF预览失败:', err)
uni.showToast({
title: 'PDF预览失败',
icon: 'none'
})
}
})
}
},
fail: (err) => {
console.error('PDF下载失败:', err)
uni.showToast({
title: 'PDF下载失败',
icon: 'none'
})
}
})
} else {
uni.showToast({
title: response.msg || 'PDF预览失败',
icon: 'none'
})
}
} catch (error) {
console.error('PDF预览异常:', error)
uni.showToast({
title: 'PDF预览异常',
icon: 'none'
})
}
//
this.$emit('file-click', { file, record: this.record })
}
}
@ -64,6 +127,17 @@ export default {
padding: 25rpx;
margin-bottom: 20rpx;
border: 1rpx solid #333;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: rgba(41, 211, 180, 0.3);
background-color: #1f1f1f;
}
&:active {
background-color: #252525;
}
}
.record-header {
@ -85,6 +159,19 @@ export default {
padding: 6rpx 15rpx;
border-radius: 15rpx;
}
.edit-btn {
color: #29d3b4;
font-size: 22rpx;
background-color: rgba(41, 211, 180, 0.1);
padding: 6rpx 15rpx;
border-radius: 15rpx;
border: 1px solid rgba(41, 211, 180, 0.3);
&:active {
background-color: rgba(41, 211, 180, 0.2);
}
}
}
.record-data {

100
uniapp/components/fitness-record-popup/fitness-record-popup.vue

@ -159,12 +159,29 @@ export default {
mask: true
})
// resource_id
console.log('当前resourceId:', this.resourceId)
console.log('父组件传递的resourceId:', this.$props.resourceId)
if (!this.resourceId) {
uni.showToast({
title: '缺少学生资源ID,请稍后重试',
icon: 'none'
})
uni.hideLoading()
return
}
const params = {
resource_id: this.resourceId,
student_id: this.resourceId, // student_id
age: 18, //
test_date: this.recordData.test_date,
height: this.recordData.height,
weight: this.recordData.weight,
pdf_files: this.recordData.pdf_files
pdf_files: this.recordData.pdf_files,
// PDF
physical_test_report: this.recordData.pdf_files.map(file => file.server_path || file.url).filter(path => path)
}
if (this.isEditing) {
@ -198,20 +215,39 @@ export default {
count: 5,
type: 'file',
extension: ['pdf'],
success: (res) => {
success: async (res) => {
console.log('选择的文件:', res.tempFiles)
res.tempFiles.forEach(file => {
for (let file of res.tempFiles) {
if (file.type === 'application/pdf') {
try {
// PDF
const uploadResult = await this.uploadPdfFile(file)
if (uploadResult && uploadResult.code === 1) {
const pdfFile = {
id: Date.now() + Math.random(),
name: file.name,
size: file.size,
url: file.path
url: uploadResult.data.file_url, // 使URL
server_path: uploadResult.data.file_path, //
upload_time: uploadResult.data.upload_time
}
this.recordData.pdf_files.push(pdfFile)
} else {
uni.showToast({
title: uploadResult.msg || '文件上传失败',
icon: 'none'
})
}
} catch (error) {
console.error('上传PDF文件失败:', error)
uni.showToast({
title: '文件上传失败',
icon: 'none'
})
}
}
}
},
fail: (err) => {
console.error('选择文件失败:', err)
@ -223,6 +259,62 @@ export default {
})
},
// PDF
async uploadPdfFile(file) {
const { Api_url } = require('@/common/config.js')
const token = uni.getStorageSync('token') || ''
return new Promise((resolve, reject) => {
uni.uploadFile({
url: Api_url + '/xy/physicalTest/uploadPdf', // 使PDF
filePath: file.path,
name: 'file',
header: {
'token': token
},
success: (res) => {
let response
try {
// BOM JSON
response = JSON.parse(res.data.replace(/\ufeff/g, '') || '{}')
} catch (e) {
console.error('PDF上传响应解析失败:', e)
reject(e)
return
}
if (response.code === 1) {
resolve({
code: 1,
msg: '上传成功',
data: {
file_name: response.data.file_name || file.name,
file_path: response.data.file_path,
file_url: response.data.file_url || response.data.url,
file_size: file.size,
upload_time: new Date().toLocaleString()
}
})
} else if (response.code === 401) {
uni.showToast({ title: response.msg, icon: 'none' })
setTimeout(() => {
uni.navigateTo({ url: '/pages/student/login/login' })
}, 1000)
reject(response)
} else {
uni.showToast({ title: response.msg || 'PDF上传失败', icon: 'none' })
reject(response)
}
},
fail: (err) => {
console.error('PDF上传网络失败:', err)
uni.showToast({ title: err.errMsg || '网络异常', icon: 'none' })
reject(err)
}
})
})
},
// PDF
removePDFFile(index) {
this.recordData.pdf_files.splice(index, 1)

72
uniapp/components/order-form-popup/index.vue

@ -7,7 +7,7 @@
</view>
<view class="popup-content">
<view class="form-section">
<scroll-view class="form-section" scroll-y="true" enable-passive="true" show-scrollbar="false">
<view class="form-item">
<text class="label">学生信息</text>
<view class="student-info">
@ -57,29 +57,29 @@
/>
</view>
<view class="form-item">
<text class="label">课时数</text>
<input
class="form-input readonly"
type="number"
v-model="formData.total_hours"
placeholder="请先选择课程"
readonly
disabled
/>
</view>
<view class="form-item">
<text class="label">赠送课时</text>
<input
class="form-input readonly"
type="number"
v-model="formData.gift_hours"
placeholder="请先选择课程"
readonly
disabled
/>
</view>
<!-- <view class="form-item">-->
<!-- <text class="label">课时数</text>-->
<!-- <input -->
<!-- class="form-input readonly"-->
<!-- type="number"-->
<!-- v-model="formData.total_hours"-->
<!-- placeholder="请先选择课程"-->
<!-- readonly-->
<!-- disabled-->
<!-- />-->
<!-- </view>-->
<!-- <view class="form-item">-->
<!-- <text class="label">赠送课时</text>-->
<!-- <input -->
<!-- class="form-input readonly"-->
<!-- type="number"-->
<!-- v-model="formData.gift_hours"-->
<!-- placeholder="请先选择课程"-->
<!-- readonly-->
<!-- disabled-->
<!-- />-->
<!-- </view>-->
<view class="form-item">
<text class="label">备注</text>
@ -90,7 +90,7 @@
maxlength="200"
></textarea>
</view>
</view>
</scroll-view>
</view>
<view class="popup-footer">
@ -384,7 +384,7 @@ export default {
background: #1a1a1a;
border-radius: 20rpx 20rpx 0 0;
color: #ffffff;
max-height: 80vh;
height: 80vh;
display: flex;
flex-direction: column;
}
@ -417,11 +417,19 @@ export default {
.popup-content {
flex: 1;
overflow-y: auto;
padding: 40rpx;
overflow: hidden;
padding: 0;
min-height: 0; /* 确保flex子项可以收缩 */
height: 0; /* 在微信小程序中确保flex子项正确计算高度 */
}
.form-section {
height: 100%;
width: 100%;
padding: 40rpx 40rpx 60rpx 40rpx; //
/* 确保滚动区域正确计算 */
box-sizing: border-box;
.form-item {
margin-bottom: 40rpx;
@ -489,7 +497,7 @@ export default {
background: #1a1a1a;
border-color: #333;
color: #888;
height: 100rpx;
&::placeholder {
color: #666;
}
@ -506,8 +514,11 @@ export default {
.popup-footer {
display: flex;
padding: 30rpx 40rpx;
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
border-top: 1px solid #333;
gap: 30rpx;
flex-shrink: 0; /* 确保底部按钮区域不被压缩 */
background: #1a1a1a; /* 确保背景色一致 */
.footer-btn {
flex: 1;
@ -520,8 +531,7 @@ export default {
font-weight: 500;
&.cancel-btn {
background: #444;
color: #cccccc;
background: #cccccc;
}
&.confirm-btn {

6
uniapp/components/order-list-card/index.vue

@ -3,7 +3,7 @@
<view class="order-list-card">
<!-- 操作按钮区域 -->
<view class="action-header" v-if="orderList && orderList.length > 0">
<view class="add-order-btn" @click="handleAddOrder">
<view class="add-order-btn" @click.stop="handleAddOrder">
<text class="add-icon">+</text>
<text class="add-text">新增订单</text>
</view>
@ -22,7 +22,7 @@
v-for="(order, index) in orderList"
:key="order && order.id ? order.id : `order-${index}`"
:class="{ 'pending-payment': order && order.status === 'pending' }"
@click="order ? handleOrderClick(order) : null"
@click.stop="order ? handleOrderClick(order) : null"
v-if="order"
>
<view class="order-header">
@ -94,7 +94,7 @@
<view class="empty-icon">📋</view>
<view class="empty-text">暂无订单记录</view>
<view class="empty-tip">客户还未产生任何订单</view>
<view class="empty-add-btn" @click="handleAddOrder">
<view class="empty-add-btn" @click.stop="handleAddOrder">
<text>新增订单</text>
</view>
</view>

38
uniapp/components/service-list-card/index.vue

@ -11,7 +11,7 @@
<view class="error-state" v-else-if="error">
<view class="error-icon"></view>
<view class="error-text">{{ error }}</view>
<view class="retry-button" @click="fetchServiceList">重试</view>
<view class="retry-button" @click.stop="fetchServiceList">重试</view>
</view>
<!-- 服务列表 -->
@ -20,7 +20,7 @@
class="service-item"
v-for="(service, index) in actualServiceList"
:key="service.id || index"
@click="viewServiceDetail(service)"
@click.stop="viewServiceDetail(service)"
>
<!-- 服务头部 -->
<view class="service-header">
@ -67,17 +67,17 @@
</view>
<view class="log-details">
<view class="log-detail-item" v-if="log.service_content">
<view class="log-detail-item">
<text class="detail-label">服务内容</text>
<text class="detail-value">{{ log.service_content }}</text>
</view>
<view class="log-detail-item" v-if="log.service_staff">
<view class="log-detail-item">
<text class="detail-label">服务人员</text>
<text class="detail-value">{{ log.service_staff }}</text>
</view>
<view class="log-detail-item" v-if="log.duration">
<view class="log-detail-item">
<text class="detail-label">服务时间</text>
<text class="detail-value">{{ log.updated_at }}</text>
</view>
@ -100,7 +100,7 @@
</view>
</view>
<view class="log-detail-item" v-if="log.remark">
<view class="log-detail-item">
<text class="detail-label">备注</text>
<text class="detail-value remark">{{ log.remark }}</text>
</view>
@ -292,6 +292,8 @@ export default {
<style lang="scss" scoped>
.service-list-card {
padding: 0;
max-height: 60vh;
overflow-y: auto;
}
//
@ -361,6 +363,8 @@ export default {
display: flex;
flex-direction: column;
gap: 32rpx;
max-height: 50vh;
overflow-y: auto;
}
.service-item {
@ -633,4 +637,26 @@ export default {
color: #999999;
line-height: 1.4;
}
/* 滚动条样式 */
.service-list-card::-webkit-scrollbar,
.service-list::-webkit-scrollbar {
width: 6rpx;
}
.service-list-card::-webkit-scrollbar-track,
.service-list::-webkit-scrollbar-track {
background: transparent;
}
.service-list-card::-webkit-scrollbar-thumb,
.service-list::-webkit-scrollbar-thumb {
background: #29D3B4;
border-radius: 3rpx;
}
.service-list-card::-webkit-scrollbar-thumb:hover,
.service-list::-webkit-scrollbar-thumb:hover {
background: #24B89E;
}
</style>

27
uniapp/components/study-plan-card/index.vue

@ -11,9 +11,12 @@
>
<view class="plan-header">
<view class="plan-title">{{ plan.plan_name || '未命名计划' }}</view>
<view class="plan-actions">
<view :class="['plan-status',getStatusClass(plan.status)]">
{{ getStatusText(plan.status) }}
</view>
<view class="edit-btn" @click.stop="editPlan(plan)">编辑</view>
</view>
</view>
<view class="plan-content">
@ -80,6 +83,11 @@ export default {
this.$emit('view-detail', plan)
},
//
editPlan(plan) {
this.$emit('edit', plan)
},
//
getStatusClass(status) {
const statusMap = {
@ -165,6 +173,12 @@ export default {
margin-right: 20rpx;
}
.plan-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
.plan-status {
padding: 8rpx 16rpx;
border-radius: 20rpx;
@ -197,6 +211,19 @@ export default {
}
}
.edit-btn {
color: #29d3b4;
font-size: 22rpx;
background-color: rgba(41, 211, 180, 0.1);
padding: 6rpx 15rpx;
border-radius: 15rpx;
border: 1px solid rgba(41, 211, 180, 0.3);
&:active {
background-color: rgba(41, 211, 180, 0.2);
}
}
.plan-content {
margin-bottom: 20rpx;
}

424
uniapp/components/study-plan-popup/study-plan-popup.vue

@ -0,0 +1,424 @@
<template>
<uni-popup ref="popup" type="center">
<view class="popup-container">
<view class="popup-header">
<view class="popup-title">{{ isEditing ? '编辑学习计划' : '新增学习计划' }}</view>
<view class="popup-close" @click="close"></view>
</view>
<view class="study-plan-form">
<view class="form-section">
<view class="form-item">
<view class="form-label">计划名称</view>
<view class="form-input">
<input v-model="planData.plan_name" placeholder="请输入计划名称" />
</view>
</view>
<view class="form-item">
<view class="form-label">计划类型</view>
<view class="form-input">
<picker :value="typeIndex" :range="planTypes" @change="onTypeChange">
<view class="picker-display">
{{ planData.plan_type || '请选择计划类型' }}
</view>
</picker>
</view>
</view>
<view class="form-item">
<view class="form-label">开始日期</view>
<view class="form-input">
<input type="date" v-model="planData.start_date" placeholder="请选择开始日期" />
</view>
</view>
<view class="form-item">
<view class="form-label">结束日期</view>
<view class="form-input">
<input type="date" v-model="planData.end_date" placeholder="请选择结束日期" />
</view>
</view>
<view class="form-item">
<view class="form-label">计划内容</view>
<view class="form-input">
<textarea
v-model="planData.plan_content"
placeholder="请输入计划详细内容"
maxlength="500"
:show-confirm-bar="false"
></textarea>
</view>
</view>
<view class="form-item">
<view class="form-label">状态</view>
<view class="form-input">
<picker :value="statusIndex" :range="statusOptions" @change="onStatusChange">
<view class="picker-display">
{{ getStatusText(planData.status) || '请选择状态' }}
</view>
</picker>
</view>
</view>
</view>
</view>
<view class="popup-footer">
<view class="popup-btn cancel-btn" @click="close">取消</view>
<view class="popup-btn confirm-btn" @click="confirm">确认</view>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
name: 'StudyPlanPopup',
props: {
studentId: {
type: [String, Number],
default: ''
}
},
data() {
return {
isVisible: false,
isEditing: false,
planData: {
id: null,
plan_name: '',
plan_type: '',
start_date: '',
end_date: '',
plan_content: '',
status: 'pending'
},
planTypes: ['学习计划', '训练计划', '康复计划', '体能提升', '技能培养', '其他'],
typeIndex: 0,
statusOptions: ['pending', 'active', 'completed', 'expired'],
statusIndex: 0
}
},
methods: {
//
openAdd() {
this.isEditing = false
this.resetData()
this.isVisible = true
this.$refs.popup.open()
},
//
openEdit(plan) {
this.isEditing = true
this.planData = {
id: plan.id,
plan_name: plan.plan_name || '',
plan_type: plan.plan_type || '',
start_date: plan.start_date || '',
end_date: plan.end_date || '',
plan_content: plan.plan_content || '',
status: plan.status || 'pending'
}
//
this.typeIndex = this.planTypes.indexOf(this.planData.plan_type)
this.statusIndex = this.statusOptions.indexOf(this.planData.status)
this.isVisible = true
this.$refs.popup.open()
},
//
close() {
this.isVisible = false
this.$refs.popup.close()
this.resetData()
this.$emit('close')
},
//
resetData() {
this.planData = {
id: null,
plan_name: '',
plan_type: '',
start_date: '',
end_date: '',
plan_content: '',
status: 'pending'
}
this.typeIndex = 0
this.statusIndex = 0
},
//
async confirm() {
try {
//
if (!this.planData.plan_name) {
uni.showToast({
title: '请输入计划名称',
icon: 'none'
})
return
}
if (!this.planData.plan_type) {
uni.showToast({
title: '请选择计划类型',
icon: 'none'
})
return
}
if (!this.planData.start_date) {
uni.showToast({
title: '请选择开始日期',
icon: 'none'
})
return
}
if (!this.planData.plan_content) {
uni.showToast({
title: '请输入计划内容',
icon: 'none'
})
return
}
//
if (this.planData.end_date && this.planData.start_date > this.planData.end_date) {
uni.showToast({
title: '结束日期不能早于开始日期',
icon: 'none'
})
return
}
uni.showLoading({
title: '保存中...',
mask: true
})
const params = {
student_id: this.studentId,
plan_name: this.planData.plan_name,
plan_type: this.planData.plan_type,
start_date: this.planData.start_date,
end_date: this.planData.end_date,
plan_content: this.planData.plan_content,
status: this.planData.status
}
if (this.isEditing) {
params.id = this.planData.id
}
console.log('保存学习计划参数:', params)
//
this.$emit('confirm', {
isEditing: this.isEditing,
data: params
})
uni.hideLoading()
this.close()
} catch (error) {
console.error('保存学习计划失败:', error)
uni.showToast({
title: '保存失败,请重试',
icon: 'none'
})
uni.hideLoading()
}
},
//
onTypeChange(e) {
this.typeIndex = e.detail.value
this.planData.plan_type = this.planTypes[this.typeIndex]
},
//
onStatusChange(e) {
this.statusIndex = e.detail.value
this.planData.status = this.statusOptions[this.statusIndex]
},
//
getStatusText(status) {
const statusMap = {
'pending': '待开始',
'active': '进行中',
'completed': '已完成',
'expired': '已过期'
}
return statusMap[status] || ''
}
}
}
</script>
<style lang="less" scoped>
.popup-container {
width: 90vw;
max-width: 500rpx;
background-color: #1a1a1a;
border-radius: 20rpx;
border: 1px solid #333;
overflow: hidden;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1px solid #333;
background-color: #2a2a2a;
}
.popup-title {
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
}
.popup-close {
color: #999999;
font-size: 36rpx;
cursor: pointer;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: #ffffff;
}
}
.study-plan-form {
padding: 30rpx;
max-height: 60vh;
overflow-y: auto;
}
.form-section {
display: flex;
flex-direction: column;
gap: 30rpx;
}
.form-item {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.form-label {
color: #ffffff;
font-size: 28rpx;
font-weight: 500;
}
.form-input {
background-color: #333333;
border-radius: 12rpx;
border: 1px solid #404040;
overflow: hidden;
input, textarea {
width: 100%;
padding: 20rpx;
background-color: transparent;
color: #ffffff;
font-size: 28rpx;
border: none;
outline: none;
&::placeholder {
color: #999999;
}
}
textarea {
min-height: 120rpx;
resize: none;
}
}
.picker-display {
padding: 20rpx;
color: #ffffff;
font-size: 28rpx;
&:empty::before {
content: attr(placeholder);
color: #999999;
}
}
.popup-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 1px solid #333;
background-color: #1a1a1a;
}
.popup-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.cancel-btn {
background-color: #333333;
color: #ffffff;
border: 1px solid #404040;
&:active {
background-color: #404040;
}
}
.confirm-btn {
background-color: #29D3B4;
color: #ffffff;
&:active {
background-color: #24B89E;
}
}
/* 滚动条样式 */
.study-plan-form::-webkit-scrollbar {
width: 6rpx;
}
.study-plan-form::-webkit-scrollbar-track {
background: transparent;
}
.study-plan-form::-webkit-scrollbar-thumb {
background: #29D3B4;
border-radius: 3rpx;
}
.study-plan-form::-webkit-scrollbar-thumb:hover {
background: #24B89E;
}
</style>

215
uniapp/mock/index.js

@ -238,15 +238,49 @@ const mockData = {
weight: 55,
calculateChildHealthScore: 85,
created_at: '2024-01-10 14:30:00',
test_date: '2024-01-10',
bmi: 20.2,
test_items: {
flexibility: 78,
strength: 85,
endurance: 82
},
pdf_files: [
{
id: 'pdf_001',
name: '体测报告_2024-01-10.pdf',
size: 1024000,
upload_time: '2024-01-10 14:35:00',
file_path: '/uploads/fitness/2024/01/fitness_report_001.pdf'
}
]
},
{
id: 2,
resource_id: 1001,
height: 166,
weight: 56,
calculateChildHealthScore: 88,
created_at: '2024-02-15 09:20:00',
test_date: '2024-02-15',
bmi: 20.3,
test_items: {
flexibility: 80,
strength: 88,
endurance: 85
},
pdf_files: [
{
id: 'pdf_002',
name: '体测报告_2024-02-15.pdf',
size: 1150000,
upload_time: '2024-02-15 09:25:00',
file_path: '/uploads/fitness/2024/02/fitness_report_002.pdf'
}
]
}
],
total: 1,
total: 2,
page: 1,
pages: 1
},
@ -556,47 +590,65 @@ class MockService {
// 获取学员课程信息
if (checkEndpoint(['/getStudentCourseInfo', 'getStudentCourseInfo'])) {
const resourceId = params.resource_id || params.id
// 模拟课程信息数据
// 模拟课程信息数据 - 匹配后端API实际返回结构
const courseInfoData = [
{
id: 1,
course_name: '少儿篮球初级班',
total_count: 24, // 总课时
used_count: 8, // 已使用课时
remaining_count: 16, // 剩余课时
formal_hours: 20, // 正式课时
course_name: '篮球课',
total_count: 28, // 总课时
used_count: 10, // 已使用课时
remaining_count: 18, // 剩余课时
formal_hours: 24, // 正式课时
gift_hours: 4, // 赠送课时
used_formal_hours: 6, // 已使用正式课时
used_formal_hours: 8, // 已使用正式课时
used_gift_hours: 2, // 已使用赠送课时
leave_count: 1, // 请假次数
leave_count: 0, // 请假次数
start_date: '2024-01-01', // 开始日期
expiry_date: '2024-06-30', // 结束日期
expiry_date: '2024-12-31', // 到期日期
status: 'active', // 课程状态
course_type: '正式课',
teacher_name: '王教练',
db_status: 1, // 数据库状态
single_session_count: 90, // 单节时长(分钟)
main_coach_id: 1, // 主教练ID
main_coach_name: '张老师', // 主教练姓名
education_id: null, // 教务ID
education_name: '未分配', // 教务姓名
assistant_ids: '', // 助教ID列表
assistant_names: '无', // 助教姓名列表
course_type: '常规课',
teacher_name: '张老师', // 兼容字段
course_price: 2880.00,
class_duration: 90, // 单节时长(分钟)
create_time: '2024-01-01 10:00:00'
class_duration: 90, // 兼容字段
create_time: '2024-01-01 10:00:00',
remark: '学员表现优秀,建议继续加强基础练习'
},
{
id: 2,
course_name: '体能训练课',
total_count: 12,
used_count: 3,
remaining_count: 9,
formal_hours: 10,
total_count: 14,
used_count: 4,
remaining_count: 10,
formal_hours: 12,
gift_hours: 2,
used_formal_hours: 2,
used_formal_hours: 3,
used_gift_hours: 1,
leave_count: 0,
start_date: '2024-01-15',
expiry_date: '2024-04-15',
expiry_date: '2024-07-15', // 到期日期
status: 'active',
course_type: '正式课',
teacher_name: '李教练',
db_status: 1,
single_session_count: 60,
main_coach_id: 2,
main_coach_name: '李教练',
education_id: null,
education_name: '未分配',
assistant_ids: '',
assistant_names: '无',
course_type: '体能课',
teacher_name: '李教练', // 兼容字段
course_price: 1680.00,
class_duration: 60,
create_time: '2024-01-15 14:00:00'
class_duration: 60, // 兼容字段
create_time: '2024-01-15 14:00:00',
remark: '需要加强核心力量训练'
}
]
@ -682,6 +734,108 @@ class MockService {
return this.createResponse({}, 1, '作业提交成功')
}
// 学员服务记录列表
if (checkEndpoint(['/xy/service/list', 'getStudentServiceList'])) {
const studentId = params.student_id || params.id || 1
// 模拟学员服务记录数据
const serviceListData = [
{
id: 1,
service_name: '游泳技能训练',
preview_image_url: 'https://via.placeholder.com/200x120?text=游泳训练',
description: '专业游泳技能指导,提升水中运动能力和安全意识',
service_type: '技能训练',
status: 'active',
logs: [
{
id: 101,
status: 1, // completed
service_content: '学员表现优秀,掌握了蛙泳基本动作\n• 蛙泳姿势:90%完成度\n• 呼吸节奏:85%完成度\n• 水中平衡:95%完成度',
service_staff: '张教练',
service_time: '2024-01-21 09:00:00',
customer_feedback: '孩子很喜欢游泳课,教练很耐心',
service_rating: 5,
remark: '学员进步明显,建议继续巩固基础动作',
updated_at: '2024-01-21 10:30:00'
},
{
id: 102,
status: 1, // completed
service_content: '继续巩固蛙泳技术,开始学习自由泳\n• 自由泳入门:良好\n• 换气技巧:需要练习\n• 游泳距离:100米连续',
service_staff: '张教练',
service_time: '2024-01-25 09:00:00',
customer_feedback: '孩子进步很快,很有天赋',
service_rating: 5,
remark: '可以开始进阶训练',
updated_at: '2024-01-25 10:30:00'
}
],
total_count: 2,
completed_count: 2
},
{
id: 2,
service_name: '体能综合测评',
preview_image_url: 'https://via.placeholder.com/200x120?text=体能测评',
description: '全面体能测试与评估,制定个性化训练方案',
service_type: '体能测评',
status: 'active',
logs: [
{
id: 201,
status: 1, // completed
service_content: '体能测试结果良好,建议加强核心力量训练\n• 柔韧性:优秀\n• 平衡能力:良好\n• 耐力水平:需提升',
service_staff: '李教练',
service_time: '2024-01-14 14:00:00',
customer_feedback: '测试很全面,建议很有用',
service_rating: 4,
remark: '整体素质不错,有提升空间',
updated_at: '2024-01-14 15:30:00'
}
],
total_count: 1,
completed_count: 1
},
{
id: 3,
service_name: '篮球技巧训练',
preview_image_url: 'https://via.placeholder.com/200x120?text=篮球训练',
description: '篮球基础技巧和战术训练,提升球感和协调性',
service_type: '球类运动',
status: 'active',
logs: [
{
id: 301,
status: 0, // pending
service_content: '本次训练重点练习运球和投篮\n• 运球技巧:有进步\n• 投篮姿势:需要调整\n• 团队配合:积极主动',
service_staff: '王教练',
service_time: '2024-01-26 16:00:00',
customer_feedback: '',
service_rating: null,
remark: '训练进行中...',
updated_at: '2024-01-26 16:00:00'
},
{
id: 302,
status: 1, // completed
service_content: '基础运球和传球练习\n• 基本运球:掌握良好\n• 双手传球:需要加强\n• 移动中运球:有待提高',
service_staff: '王教练',
service_time: '2024-01-19 16:00:00',
customer_feedback: '教练很专业,孩子很喜欢',
service_rating: 4,
remark: '基础扎实,继续练习',
updated_at: '2024-01-19 17:30:00'
}
],
total_count: 2,
completed_count: 1
}
]
return this.createResponse(serviceListData, 1, '获取成功')
}
// 学生登录
if (checkEndpoint(['/xy/login', 'xy_login'])) {
return this.createResponse({
@ -716,6 +870,16 @@ class MockService {
params.size || 10
)
case '/fitness/record/pdf/preview':
// PDF预览Mock数据
return this.createResponse({
file_url: 'https://example.com/fitness-report.pdf',
file_name: '体测报告_' + (params.record_id || '123') + '.pdf',
file_size: 1024000,
preview_url: 'https://example.com/preview/fitness-report.pdf',
download_url: 'https://example.com/download/fitness-report.pdf'
}, 1, 'PDF预览链接获取成功')
default:
return this.createResponse(null, 404, '接口未找到')
}
@ -732,11 +896,13 @@ class MockService {
'/student/exam/results',
'/courses',
'/students',
'/fitness/record/pdf/preview',
// 学员端专用API - URL匹配
'/customerResourcesAuth/info', // xy_memberInfo
'/xy/physicalTest', // xy_physicalTest
'/xy/personCourseSchedule', // xy_personCourseSchedule相关
'/xy/assignment', // xy_assignment相关
'/xy/service/list', // xy/service/list - 学员服务记录
'/xy/login', // xy_login
'/getStudentCourseInfo', // 获取学员课程信息
// 家长端专用API - URL匹配
@ -758,6 +924,7 @@ class MockService {
'xy_assignmentSubmitObj',
'xy_personCourseScheduleGetCalendar',
'xy_personCourseScheduleGetMyCoach',
'getStudentServiceList', // 学员服务记录列表
'xy_login',
'getStudentCourseInfo',
// 家长端专用API - 方法名匹配(用于开发调试)

3
uniapp/pages/coach/student/student_list.vue

@ -108,17 +108,14 @@
</view>
</view>
</fui-drawer>
<AQTabber />
</view>
</template>
<script>
import memberApi from '@/api/member.js';
import AQTabber from "@/components/AQ/AQTabber.vue"
import SinglePicker from "@/components/custom-picker/single-picker.vue"
export default {
components: {
AQTabber,
SinglePicker
},
data() {

8
uniapp/pages/common/profile/index.vue

@ -110,11 +110,9 @@
}
},
viewPersonalProfile() {
//
uni.showModal({
title: '个人资料',
content: '个人资料页面开发中,将包含employee_number、name、phone、email、address等字段的查看和编辑功能',
showCancel: false
//
uni.navigateTo({
url: '/pages/common/profile/personal_info'
});
},
viewSalaryInfo() {

1254
uniapp/pages/common/profile/personal_info.vue

File diff suppressed because it is too large

34
uniapp/pages/market/clue/class_arrangement_detail.vue

@ -29,7 +29,7 @@
<view class="student-name">{{ stu.name }}</view>
<view class="student-age">年龄{{ stu.age || '未知' }}</view>
<view class="course-status">课程状态{{ stu.courseStatus }}</view>
<view class="course-progress">上课情况{{ stu.course_progress.used }}/{{ stu.course_progress.total }} ({{ stu.course_progress.percentage }}%)</view>
<view class="course-status">上课情况{{ stu.course_progress.used }}/{{ stu.course_progress.total }}</view>
<view class="expiry-date" v-if="stu.student_course_info">到期时间{{ stu.student_course_info.end_date || '未设置' }}</view>
</view>
</view>
@ -138,7 +138,7 @@
</view>
<!-- 预设学生信息显示 -->
<view v-if="presetStudent" class="form-section">
<view v-if="presetStudent.name && presetStudent.phone" class="form-section">
<text class="form-label">选中学员</text>
<view class="preset-student">
<view class="student-avatar">{{ presetStudent.name ? presetStudent.name.charAt(0) : '?' }}</view>
@ -480,11 +480,22 @@
}
} catch (error) {
uni.hideLoading();
console.error('添加学员失败:', error);
//
let errorMsg = '添加失败';
if (error && error.data && error.data.msg) {
errorMsg = error.data.msg;
} else if (error && error.message) {
errorMsg = error.message;
} else if (typeof error === 'string') {
errorMsg = error;
}
uni.showToast({
title: '添加失败',
title: errorMsg,
icon: 'none'
});
console.error('添加学员失败:', error);
}
},
@ -749,11 +760,22 @@
} catch (error) {
uni.hideLoading();
console.error('添加学员失败:', error);
//
let errorMsg = '添加失败';
if (error && error.data && error.data.msg) {
errorMsg = error.data.msg;
} else if (error && error.message) {
errorMsg = error.message;
} else if (typeof error === 'string') {
errorMsg = error;
}
uni.showToast({
title: '添加失败',
title: errorMsg,
icon: 'none'
});
console.error('添加学员失败:', error);
}
},
//

47
uniapp/pages/market/clue/clue_info.less

@ -73,6 +73,43 @@
}
}
.popup-footer {
display: flex;
padding: 30rpx 40rpx;
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
border-top: 1px solid #333;
gap: 30rpx;
flex-shrink: 0; /* 确保底部按钮区域不被压缩 */
background: #1a1a1a; /* 确保背景色一致 */
.popup-footer-btns {
display: flex;
justify-content: space-between;
.footer-btn {
flex: 1;
height: 88rpx;
display: flex;
width: 45%;
align-items: center;
justify-content: center;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: 500;
&.cancel-btn {
background: #cccccc;
}
&.confirm-btn {
background: linear-gradient(45deg, #29D3B4, #1DB584);
color: #ffffff;
}
}
}
}
// 课程、通话、体测记录等区域
.course-section, .call-section, .fitness-section, .study-plan-section {
background-color: #434544;
@ -212,6 +249,8 @@
.remark-dialog {
width: 600rpx;
padding: 30rpx;
background: #fff;
border-radius: 16rpx;
textarea {
width: 100%;
@ -220,11 +259,19 @@
border: 2rpx solid #e9ecef;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
background-color: #fff;
box-sizing: border-box;
resize: none;
&::placeholder {
color: #999;
}
&:focus {
border-color: #29d3b4;
outline: none;
box-shadow: 0 0 0 2rpx rgba(41, 211, 180, 0.1);
}
}

228
uniapp/pages/market/clue/clue_info.vue

@ -24,7 +24,7 @@
<view class="student-section" v-if="switch_tags_type == 1">
<view class="section-header">
<text class="section-title">学生信息</text>
<view class="add-student-btn" @click="openAddStudentDialog">
<view class="add-student-btn" @click.stop="openAddStudentDialog">
<text class="add-icon">+</text>
<text class="add-text">添加学生</text>
</view>
@ -60,7 +60,7 @@
class="action-item"
v-for="action in actionButtons"
:key="action.key"
@click="handleStudentActionClick(action, currentStudent)"
@click.stop="handleStudentActionClick(action, currentStudent)"
>
<view class="action-icon">
<text>{{ action.icon }}</text>
@ -73,7 +73,7 @@
<view v-if="studentList.length === 0" class="empty-state">
<text class="empty-icon">👤</text>
<text class="empty-text">暂无学生信息</text>
<view class="empty-add-btn" @click="openAddStudentDialog">
<view class="empty-add-btn" @click.stop="openAddStudentDialog">
<text>添加第一个学生</text>
</view>
</view>
@ -110,7 +110,7 @@
<view class="fitness-section" v-if="switch_tags_type == 4">
<view class="section-header" v-if="currentStudent">
<text class="context-title">{{ currentStudent.name }}的体测记录</text>
<view class="add-record-btn" @click="openAddFitnessRecord">
<view class="add-record-btn" @click.stop="openAddFitnessRecord">
<text class="add-icon">+</text>
<text class="add-text">新增记录</text>
</view>
@ -143,6 +143,7 @@
:title="popupTitle"
:has-footer="needsFooter"
@close="closePopup"
@click.stop
>
<!-- 课程信息弹窗 -->
<CourseInfoCard
@ -152,18 +153,31 @@
/>
<!-- 体测记录弹窗 -->
<FitnessRecordCard
<view
class="fitness-records-container"
v-if="currentPopup === 'fitness_record'"
>
<!-- 空状态提示 -->
<view v-if="currentStudentFitnessRecords.length === 0" class="empty-state">
<view class="empty-icon">📊</view>
<view class="empty-text">暂无体测记录</view>
<view class="empty-tip">点击下方"新增"按钮添加体测记录</view>
</view>
<!-- 体测记录列表 -->
<FitnessRecordCard
v-for="record in currentStudentFitnessRecords"
:key="record.id"
:record="record"
@edit="openEditFitnessRecord"
/>
</view>
<!-- 学习计划弹窗 -->
<StudyPlanCard
v-if="currentPopup === 'study_plan'"
:plan-list="studyPlanList"
@edit="openEditStudyPlan"
/>
<!-- 订单列表弹窗 -->
@ -182,13 +196,13 @@
/>
<!-- 底部操作按钮 -->
<template #footer v-if="needsFooter">
<template #footer>
<view class="popup-footer-btns">
<view class="footer-btn secondary" @click="closePopup">关闭</view>
<view class="footer-btn cancel-btn" @click.stop="closePopup">关闭</view>
<view
class="footer-btn primary"
class="footer-btn confirm-btn"
v-if="showAddButton"
@click="handleAddAction"
@click.stop="handleAddAction"
>新增</view>
</view>
</template>
@ -209,9 +223,10 @@
</view>
</uni-popup>
<FitnessRecordPopup ref="fitnessRecordPopup" @confirm="handleFitnessRecordConfirm" />
<FitnessRecordPopup ref="fitnessRecordPopup" :resource-id="clientInfo.resource_id" @confirm="handleFitnessRecordConfirm" />
<CourseEditPopup ref="courseEditPopup" @confirm="handleCourseEditConfirm" />
<StudentEditPopup ref="studentEditPopup" :resource-id="clientInfo.resource_id" @confirm="handleStudentEditConfirm" />
<StudyPlanPopup ref="studyPlanPopup" :student-id="currentStudent && currentStudent.id" @confirm="handleStudyPlanConfirm" />
<!-- 新增订单弹窗 -->
<uni-popup ref="orderFormPopup" type="bottom">
@ -225,7 +240,7 @@
</uni-popup>
<!-- 二维码支付弹窗 -->
<uni-popup ref="qrCodePopup" type="center" :show="showQRCodeModal" @close="closeQRCodeModal">
<uni-popup ref="qrCodePopup" type="center" @close="closeQRCodeModal">
<view class="qrcode-payment-modal" v-if="qrCodePaymentData">
<!-- 弹窗头部 -->
<view class="modal-header">
@ -262,7 +277,7 @@
<!-- 操作按钮 -->
<view class="modal-buttons">
<view class="btn secondary" @click="closeQRCodeModal">取消支付</view>
<view class="btn primary" @click="confirmQRCodePayment">已完成支付</view>
<view class="btn primary" @click="confirmQRCodePayment">发送二维码给客户</view>
</view>
</view>
</uni-popup>
@ -288,6 +303,7 @@ import OrderFormPopup from '@/components/order-form-popup/index.vue'
import CourseEditPopup from '@/components/course-edit-popup/course-edit-popup.vue'
import StudentEditPopup from '@/components/student-edit-popup/student-edit-popup.vue'
import FitnessRecordPopup from '@/components/fitness-record-popup/fitness-record-popup.vue'
import StudyPlanPopup from '@/components/study-plan-popup/study-plan-popup.vue'
export default {
components: {
@ -304,7 +320,8 @@ export default {
OrderFormPopup,
CourseEditPopup,
StudentEditPopup,
FitnessRecordPopup
FitnessRecordPopup,
StudyPlanPopup
},
data() {
return {
@ -367,7 +384,16 @@ export default {
currentStudentFitnessRecords() {
if (!this.currentStudent) return []
return this.fitnessRecords.filter(record => record.student_id === this.currentStudent.id)
// 使resource_idresource_id
// Mock
const filtered = this.fitnessRecords.filter(record =>
record.student_id === this.currentStudent.id ||
record.resource_id === this.currentStudent.id ||
record.resource_id === this.clientInfo.resource_id
)
//
return filtered.length > 0 ? filtered : this.fitnessRecords
},
popupTitle() {
@ -628,15 +654,21 @@ export default {
},
closePopup() {
console.log('关闭弹窗,当前弹窗类型:', this.currentPopup)
this.currentPopup = null
//
this.studyPlanList = []
this.orderList = []
this.serviceList = []
this.courseInfo = []
this.fitnessRecords = []
},
handleAddAction() {
if (this.currentPopup === 'fitness_record') {
this.openAddFitnessRecord()
} else if (this.currentPopup === 'study_plan') {
// TODO:
uni.showToast({ title: '添加学习计划功能待实现', icon: 'none' })
this.openAddStudyPlan()
}
},
@ -701,18 +733,27 @@ export default {
async getFitnessRecords(studentId = null) {
if (!this.clientInfo?.resource_id) return
try {
// ID
const targetStudentId = studentId || this.currentStudent?.id
// 使
this.fitnessRecords = [
{
id: 1,
student_id: targetStudentId || 1,
test_date: '2024-01-15',
height: '165',
weight: '55'
// API
const res = await apiRoute.xy_physicalTest({
resource_id: this.clientInfo.resource_id,
student_id: targetStudentId
})
if (res.code === 1 && res.data) {
this.fitnessRecords = res.data.data || []
} else {
console.warn('获取体测记录失败:', res.msg)
this.fitnessRecords = []
}
} catch (error) {
console.error('获取体测记录异常:', error)
this.fitnessRecords = []
}
]
},
@ -724,8 +765,79 @@ export default {
this.$refs.fitnessRecordPopup.openEdit(record)
},
handleFitnessRecordConfirm(result) {
uni.showToast({ title: '保存成功', icon: 'success' })
async handleFitnessRecordConfirm(result) {
try {
const { isEditing, data } = result
if (isEditing) {
//
const response = await apiRoute.xy_physicalTestEdit(data)
if (response.code === 1) {
uni.showToast({ title: '编辑成功', icon: 'success' })
//
await this.getFitnessRecords()
} else {
uni.showToast({ title: response.msg || '编辑失败', icon: 'none' })
}
} else {
//
const response = await apiRoute.xy_physicalTestAdd(data)
if (response.code === 1) {
uni.showToast({ title: '新增成功', icon: 'success' })
//
await this.getFitnessRecords()
} else {
uni.showToast({ title: response.msg || '新增失败', icon: 'none' })
}
}
} catch (error) {
console.error('保存体测记录失败:', error)
uni.showToast({ title: '保存失败,请重试', icon: 'none' })
}
},
//
openAddStudyPlan() {
if (!this.currentStudent) {
uni.showToast({ title: '请先选择学生', icon: 'none' })
return
}
this.$refs.studyPlanPopup.openAdd()
},
openEditStudyPlan(plan) {
this.$refs.studyPlanPopup.openEdit(plan)
},
async handleStudyPlanConfirm(result) {
try {
const { isEditing, data } = result
if (isEditing) {
//
const response = await apiRoute.editStudyPlan(data)
if (response.code === 1) {
uni.showToast({ title: '编辑成功', icon: 'success' })
//
await this.getStudyPlanList()
} else {
uni.showToast({ title: response.msg || '编辑失败', icon: 'none' })
}
} else {
//
const response = await apiRoute.addStudyPlan(data)
if (response.code === 1) {
uni.showToast({ title: '新增成功', icon: 'success' })
//
await this.getStudyPlanList()
} else {
uni.showToast({ title: response.msg || '新增失败', icon: 'none' })
}
}
} catch (error) {
console.error('保存学习计划失败:', error)
uni.showToast({ title: '保存失败,请重试', icon: 'none' })
}
},
@ -867,21 +979,28 @@ export default {
async getStudyPlanList(studentId = null) {
if (!this.clientInfo?.resource_id) return
const targetStudentId = studentId || this.currentStudent?.id
// 使
this.studyPlanList = [
{
id: 1,
student_id: targetStudentId || 1,
plan_name: '基础体能训练计划',
plan_content: '针对学员的基础体能进行系统性训练',
plan_type: '体能训练',
status: 'active',
progress: 65,
start_date: '2024-01-15',
end_date: '2024-03-15',
create_time: '2024-01-10 14:30:00'
if (!targetStudentId) {
console.warn('学习计划列表:缺少学生ID')
this.studyPlanList = []
return
}
try {
const response = await apiRoute.getStudyPlanList({
student_id: targetStudentId
})
if (response.code === 1) {
this.studyPlanList = response.data || []
} else {
console.error('获取学习计划失败:', response.msg)
this.studyPlanList = []
}
} catch (error) {
console.error('获取学习计划异常:', error)
this.studyPlanList = []
}
]
},
//
@ -1128,7 +1247,8 @@ export default {
this.openQRCodeModal({
order: order,
qrcode: res.data.code_url,
qrcodeImage: res.data.qrcode_url
// 使base64访localhost
qrcodeImage: res.data.qrcode_base64 || res.data.qrcode_url
})
} else {
uni.showToast({
@ -1150,12 +1270,14 @@ export default {
openQRCodeModal(paymentData) {
this.qrCodePaymentData = paymentData
this.showQRCodeModal = true
this.$refs.qrCodePopup.open()
},
//
closeQRCodeModal() {
this.showQRCodeModal = false
this.qrCodePaymentData = null
this.$refs.qrCodePopup.close()
},
//
@ -1320,5 +1442,27 @@ ${orderInfo.paid_at ? '支付时间:' + this.formatOrderTime(orderInfo.paid_at
</script>
<style lang="less" scoped>
.fitness-records-container {
max-height: 60vh;
overflow-y: auto;
}
/* 滚动条样式 */
.fitness-records-container::-webkit-scrollbar {
width: 6rpx;
}
.fitness-records-container::-webkit-scrollbar-track {
background: transparent;
}
.fitness-records-container::-webkit-scrollbar-thumb {
background: #29D3B4;
border-radius: 3rpx;
}
.fitness-records-container::-webkit-scrollbar-thumb:hover {
background: #24B89E;
}
@import './clue_info.less';
</style>

132
各校区转化数据统计需求文档.md

@ -0,0 +1,132 @@
# 各校区月&年转化汇总表数据统计需求文档
## 1. 项目背景
基于智慧教务系统,需要建立完善的数据统计分析功能,为各校区运营决策提供数据支撑。
## 2. 业务目标
- 实现各校区数据的统一汇总和对比分析
- 提供月度、年度的转化率趋势分析
- 为校区运营优化提供数据依据
## 3. 功能需求
### 3.1 数据统计维度
#### 3.1.1 校区维度
- **数据范围**:所有已开设校区
- **统计指标**
- 学员总数
- 新增学员数
- 流失学员数
- 收入金额
- 转化率
#### 3.1.2 时间维度
- **月度统计**:按自然月统计各项指标
- **年度统计**:按自然年统计各项指标
- **对比分析**:同比、环比增长率
#### 3.1.3 课程维度
- **课程类型**:体能课、篮球课、私教课等
- **统计指标**
- 各课程类型学员数
- 各课程类型收入
- 课程转化率
### 3.2 转化率计算规则
#### 3.2.1 试听转化率
```
试听转化率 = (试听后报名学员数 / 试听学员总数) × 100%
```
#### 3.2.2 续费转化率
```
续费转化率 = (续费学员数 / 到期学员总数) × 100%
```
#### 3.2.3 整体转化率
```
整体转化率 = (正式学员数 / 潜在客户总数) × 100%
```
### 3.3 报表功能需求
#### 3.3.1 数据展示
- **表格形式**:支持Excel导出
- **图表形式**:柱状图、折线图、饼图
- **数据筛选**:按校区、时间、课程类型筛选
#### 3.3.2 数据权限
- **超级管理员**:查看所有校区数据
- **校区管理员**:仅查看本校区数据
- **教练**:查看相关课程数据
## 4. 技术实现方案
### 4.1 数据源
- **学员信息表**:基础学员数据
- **课程安排表**:课程相关数据
- **收费记录表**:财务数据
- **考勤记录表**:出勤数据
### 4.2 技术架构
- **后端**:PHP (ThinkPHP框架)
- **前端**:Vue.js + Element UI
- **移动端**:uni-app
- **图表库**:uCharts/ECharts
### 4.3 数据处理流程
1. **数据采集**:从各业务表采集原始数据
2. **数据清洗**:处理异常数据和重复数据
3. **数据计算**:按统计规则计算各项指标
4. **数据存储**:存储到统计结果表
5. **数据展示**:通过API提供给前端展示
## 5. 界面设计要求
### 5.1 PC端管理后台
- **统计概览页**:关键指标卡片展示
- **详细报表页**:可筛选的数据表格
- **图表分析页**:多维度图表展示
### 5.2 移动端
- **数据看板**:关键指标移动端展示
- **简化报表**:适配移动端的数据表格
## 6. 数据安全要求
- **数据加密**:敏感数据加密存储
- **访问控制**:基于角色的权限控制
- **操作日志**:记录数据查看和导出日志
## 7. 性能要求
- **响应时间**:页面加载时间 < 3秒
- **并发支持**:支持100个用户同时访问
- **数据更新**:支持实时或定时更新
## 8. 验收标准
- [ ] 数据统计准确性验证
- [ ] 各维度筛选功能正常
- [ ] 图表展示效果符合要求
- [ ] 权限控制功能正常
- [ ] 导出功能正常
- [ ] 移动端适配正常
## 9. 风险评估
- **数据一致性风险**:需要确保统计数据与业务数据一致
- **性能风险**:大数据量统计可能影响系统性能
- **权限风险**:需要严格控制数据访问权限
## 10. 后续优化建议
- **数据挖掘**:基于历史数据进行趋势预测
- **智能分析**:提供数据异常预警功能
- **自动化报告**:定期自动生成运营报告
---
**注意**:本需求文档基于项目结构分析生成,具体的数据字段和计算规则需要根据实际Excel表格内容进行调整。
**下一步行动**:
1. 请提供Excel表格的具体内容截图或数据样例
2. 确认具体的业务规则和计算公式
3. 明确优先级和开发时间节点
Loading…
Cancel
Save