From 4a4697c816be8abb16866c15bca08f1245bd65b9 Mon Sep 17 00:00:00 2001 From: liwentao Date: Mon, 20 Jul 2026 17:21:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=95=99=E7=BB=83=E7=AB=AF?= =?UTF-8?q?=EF=BC=8C=E5=AE=9E=E7=8E=B0=E4=B8=9A=E5=8A=A1=E9=97=AD=E7=8E=AF?= =?UTF-8?q?=EF=BC=9A=E4=BC=9A=E5=91=98=E7=99=BB=E5=BD=95=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E2=86=92=E6=9F=A5=E8=AF=A2=E5=9B=A2=E8=AF=BE=E2=86=92=E9=A2=84?= =?UTF-8?q?=E7=BA=A6=E5=9B=A2=E8=AF=BE=E2=86=92=E6=89=AB=E7=A0=81=E7=AD=BE?= =?UTF-8?q?=E5=88=B0=E2=86=92=E6=95=99=E7=BB=83=E7=AB=AF=E5=BC=80=E8=AF=BE?= =?UTF-8?q?=E2=86=92=E8=AE=B0=E5=BD=95=E5=AE=9E=E9=99=85=E5=BC=80=E8=AF=BE?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E2=86=92=E6=95=99=E7=BB=83=E7=AB=AF=E7=BB=93?= =?UTF-8?q?=E8=AF=BE=E2=86=92=E8=AE=B0=E5=BD=95=E5=AE=9E=E9=99=85=E7=BB=93?= =?UTF-8?q?=E8=AF=BE=E6=97=B6=E9=97=B4=E2=86=92=E5=90=8E=E5=8F=B0=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/gym-coach/pom.xml | 62 ++ .../manage/coach/dao/CoachViolationDao.java | 15 + .../coach/entity/CoachViolationEntity.java | 61 ++ .../gym/manage/coach/enums/BookingStatus.java | 42 ++ .../manage/coach/enums/ViolationReason.java | 30 + .../coach/handler/CoachCourseHandler.java | 68 ++ .../manage/coach}/handler/CoachHandler.java | 38 +- .../coach/scheduler/CoachCourseScheduler.java | 178 +++++ .../coach/service/CoachCourseService.java | 357 ++++++++++ .../datacount/dao/DataStatisticsDao.java | 65 ++ .../datacount/domain/CoachStatistics.java | 40 ++ .../datacount/domain/StatisticsSummary.java | 5 + .../impl/DataStatisticsServiceImpl.java | 31 +- .../dao/GroupCourseBookingDao.java | 7 + .../groupcourse/dao/GroupCourseDao.java | 42 +- .../groupcourse/domain/GroupCourse.java | 24 + .../groupcourse/entity/GroupCourseEntity.java | 26 +- .../groupcourse/enums/CourseStatus.java | 5 +- .../scheduler/GroupCourseExpireScheduler.java | 46 -- .../service/impl/GroupCourseService.java | 4 +- .../manage/groupcourse/vo/GroupCourseVO.java | 10 + gym-manage-api/manage-app/pom.xml | 6 + .../gym/manage/app/ManageApplication.java | 3 +- .../gym/manage/app/config/SystemRouter.java | 10 +- .../gym/manage/app/service/CoachService.java | 144 ---- .../V18__Insert_group_course_data.sql | 52 +- .../migration/V23__Create_coach_violation.sql | 30 + .../db/migration/V24__Insert_coach_data.sql | 82 +++ .../V5__Create_group_course_tables.sql | 4 +- .../gym/manage/sys/config/SecurityConfig.java | 3 +- gym-manage-api/pom.xml | 1 + gym-manage-coach-uniapp/.gitignore | 4 + gym-manage-coach-uniapp/App.vue | 25 + gym-manage-coach-uniapp/api/coach.js | 52 ++ gym-manage-coach-uniapp/index.html | 20 + gym-manage-coach-uniapp/main.js | 22 + gym-manage-coach-uniapp/manifest.json | 64 ++ gym-manage-coach-uniapp/package-lock.json | 45 ++ gym-manage-coach-uniapp/package.json | 18 + gym-manage-coach-uniapp/pages.json | 48 ++ .../pages/course-detail/course-detail.vue | 428 +++++++++++ .../pages/course-list/course-list.vue | 292 ++++++++ gym-manage-coach-uniapp/pages/index/index.vue | 282 ++++++++ gym-manage-coach-uniapp/pages/login/login.vue | 217 ++++++ .../pages/profile/profile.vue | 347 +++++++++ gym-manage-coach-uniapp/project.config.json | 25 + .../project.private.config.json | 14 + gym-manage-coach-uniapp/static/logo.png | Bin 0 -> 4023 bytes gym-manage-coach-uniapp/store/index.js | 68 ++ .../uni.promisify.adaptor.js | 13 + gym-manage-coach-uniapp/uni.scss | 76 ++ .../uni_modules/uni-icons/changelog.md | 44 ++ .../components/uni-icons/uni-icons.uvue | 91 +++ .../components/uni-icons/uni-icons.vue | 110 +++ .../components/uni-icons/uniicons.css | 664 ++++++++++++++++++ .../components/uni-icons/uniicons.ttf | Bin 0 -> 35824 bytes .../components/uni-icons/uniicons_file.ts | 664 ++++++++++++++++++ .../components/uni-icons/uniicons_file_vue.js | 649 +++++++++++++++++ .../uni_modules/uni-icons/package.json | 111 +++ .../uni_modules/uni-icons/readme.md | 8 + .../uni_modules/uni-scss/changelog.md | 8 + .../uni_modules/uni-scss/index.scss | 1 + .../uni_modules/uni-scss/package.json | 82 +++ .../uni_modules/uni-scss/readme.md | 4 + .../uni_modules/uni-scss/styles/index.scss | 7 + .../uni-scss/styles/setting/_border.scss | 3 + .../uni-scss/styles/setting/_color.scss | 66 ++ .../uni-scss/styles/setting/_radius.scss | 55 ++ .../uni-scss/styles/setting/_space.scss | 56 ++ .../uni-scss/styles/setting/_styles.scss | 167 +++++ .../uni-scss/styles/setting/_text.scss | 24 + .../uni-scss/styles/setting/_variables.scss | 146 ++++ .../uni-scss/styles/tools/functions.scss | 19 + .../uni_modules/uni-scss/theme.scss | 31 + .../uni_modules/uni-scss/variables.scss | 62 ++ gym-manage-coach-uniapp/utils/request.js | 76 ++ gym-manage-coach-uniapp/utils/signature.js | 59 ++ .../pages/course-detail/course-detail.vue | 24 +- gym-manage-uniapp/utils/request.js | 6 +- gym-manage-web/src/api/coach.api.ts | 23 + gym-manage-web/src/api/statistics.api.ts | 11 + .../groupcourse/GroupCourseManagement.vue | 94 ++- .../views/statistics/StatisticsDashboard.vue | 82 ++- .../src/views/system/CoachManagement.vue | 174 ++++- 84 files changed, 6914 insertions(+), 258 deletions(-) create mode 100644 gym-manage-api/gym-coach/pom.xml create mode 100644 gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/dao/CoachViolationDao.java create mode 100644 gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/entity/CoachViolationEntity.java create mode 100644 gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/enums/BookingStatus.java create mode 100644 gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/enums/ViolationReason.java create mode 100644 gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/handler/CoachCourseHandler.java rename gym-manage-api/{manage-app/src/main/java/cn/novalon/gym/manage/app => gym-coach/src/main/java/cn/novalon/gym/manage/coach}/handler/CoachHandler.java (73%) create mode 100644 gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/scheduler/CoachCourseScheduler.java create mode 100644 gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/service/CoachCourseService.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/CoachStatistics.java delete mode 100644 gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/scheduler/GroupCourseExpireScheduler.java delete mode 100644 gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/service/CoachService.java create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V23__Create_coach_violation.sql create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V24__Insert_coach_data.sql create mode 100644 gym-manage-coach-uniapp/.gitignore create mode 100644 gym-manage-coach-uniapp/App.vue create mode 100644 gym-manage-coach-uniapp/api/coach.js create mode 100644 gym-manage-coach-uniapp/index.html create mode 100644 gym-manage-coach-uniapp/main.js create mode 100644 gym-manage-coach-uniapp/manifest.json create mode 100644 gym-manage-coach-uniapp/package-lock.json create mode 100644 gym-manage-coach-uniapp/package.json create mode 100644 gym-manage-coach-uniapp/pages.json create mode 100644 gym-manage-coach-uniapp/pages/course-detail/course-detail.vue create mode 100644 gym-manage-coach-uniapp/pages/course-list/course-list.vue create mode 100644 gym-manage-coach-uniapp/pages/index/index.vue create mode 100644 gym-manage-coach-uniapp/pages/login/login.vue create mode 100644 gym-manage-coach-uniapp/pages/profile/profile.vue create mode 100644 gym-manage-coach-uniapp/project.config.json create mode 100644 gym-manage-coach-uniapp/project.private.config.json create mode 100644 gym-manage-coach-uniapp/static/logo.png create mode 100644 gym-manage-coach-uniapp/store/index.js create mode 100644 gym-manage-coach-uniapp/uni.promisify.adaptor.js create mode 100644 gym-manage-coach-uniapp/uni.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-icons/changelog.md create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.uvue create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.vue create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.css create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.ttf create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons_file.ts create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons_file_vue.js create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-icons/package.json create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-icons/readme.md create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/changelog.md create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/index.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/package.json create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/readme.md create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/styles/index.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_border.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_color.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_radius.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_space.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_styles.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_text.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_variables.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/styles/tools/functions.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/theme.scss create mode 100644 gym-manage-coach-uniapp/uni_modules/uni-scss/variables.scss create mode 100644 gym-manage-coach-uniapp/utils/request.js create mode 100644 gym-manage-coach-uniapp/utils/signature.js diff --git a/gym-manage-api/gym-coach/pom.xml b/gym-manage-api/gym-coach/pom.xml new file mode 100644 index 0000000..011f5f9 --- /dev/null +++ b/gym-manage-api/gym-coach/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + + cn.novalon.gym.manage + gym-manage-api + 1.0.0 + + + gym-coach + jar + + Gym Coach + Coach Management Module - Course Start/End, Violation Tracking + + + + cn.novalon.gym.manage + manage-common + ${project.version} + + + cn.novalon.gym.manage + manage-db + ${project.version} + + + cn.novalon.gym.manage + manage-sys + ${project.version} + + + cn.novalon.gym.manage + gym-groupCourse + ${project.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.data + spring-data-commons + + + org.springdoc + springdoc-openapi-starter-webflux-ui + + + org.projectlombok + lombok + provided + + + diff --git a/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/dao/CoachViolationDao.java b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/dao/CoachViolationDao.java new file mode 100644 index 0000000..3c39a2e --- /dev/null +++ b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/dao/CoachViolationDao.java @@ -0,0 +1,15 @@ +package cn.novalon.gym.manage.coach.dao; + +import cn.novalon.gym.manage.coach.entity.CoachViolationEntity; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.stereotype.Repository; + +/** + * 教练违规记录 DAO + * + * @author 张翔 + * @date 2026-07-20 + */ +@Repository +public interface CoachViolationDao extends R2dbcRepository { +} diff --git a/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/entity/CoachViolationEntity.java b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/entity/CoachViolationEntity.java new file mode 100644 index 0000000..7d043a5 --- /dev/null +++ b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/entity/CoachViolationEntity.java @@ -0,0 +1,61 @@ +package cn.novalon.gym.manage.coach.entity; + +import cn.novalon.gym.manage.db.entity.BaseEntity; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/** + * 教练违规记录实体类 - 对应 coach_violation 表 + * + * @author 张翔 + * @date 2026-07-20 + */ +@Table("coach_violation") +public class CoachViolationEntity extends BaseEntity { + + @Column("coach_id") + private Long coachId; + + @Column("course_id") + private Long courseId; + + @Column("violation_time") + private LocalDateTime violationTime; + + @Column("violation_reason") + private String violationReason; + + public Long getCoachId() { + return coachId; + } + + public void setCoachId(Long coachId) { + this.coachId = coachId; + } + + public Long getCourseId() { + return courseId; + } + + public void setCourseId(Long courseId) { + this.courseId = courseId; + } + + public LocalDateTime getViolationTime() { + return violationTime; + } + + public void setViolationTime(LocalDateTime violationTime) { + this.violationTime = violationTime; + } + + public String getViolationReason() { + return violationReason; + } + + public void setViolationReason(String violationReason) { + this.violationReason = violationReason; + } +} diff --git a/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/enums/BookingStatus.java b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/enums/BookingStatus.java new file mode 100644 index 0000000..9d6bbcb --- /dev/null +++ b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/enums/BookingStatus.java @@ -0,0 +1,42 @@ +package cn.novalon.gym.manage.coach.enums; + +/** + * 团课预约状态枚举 + * + * @author 张翔 + * @date 2026-07-20 + */ +public enum BookingStatus { + + BOOKED("0", "已预约"), + CANCELLED("1", "已取消"), + ATTENDED("2", "已出席"), + ABSENT("3", "缺席"), + COACH_ABSENT("4", "教练缺席"), + LATE("5", "迟到"); + + private final String value; + private final String desc; + + BookingStatus(String value, String desc) { + this.value = value; + this.desc = desc; + } + + public String getValue() { + return value; + } + + public String getDesc() { + return desc; + } + + public static BookingStatus fromValue(String value) { + for (BookingStatus status : values()) { + if (status.value.equals(value)) { + return status; + } + } + return BOOKED; + } +} diff --git a/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/enums/ViolationReason.java b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/enums/ViolationReason.java new file mode 100644 index 0000000..c845c98 --- /dev/null +++ b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/enums/ViolationReason.java @@ -0,0 +1,30 @@ +package cn.novalon.gym.manage.coach.enums; + +/** + * 教练违规原因枚举 + * + * @author 张翔 + * @date 2026-07-20 + */ +public enum ViolationReason { + + COACH_LATE("COACH_LATE", "教练迟到"), + COACH_ABSENT("COACH_ABSENT", "教练缺席"), + NOT_MANUAL_END("NOT_MANUAL_END", "教练未手动结课"); + + private final String value; + private final String desc; + + ViolationReason(String value, String desc) { + this.value = value; + this.desc = desc; + } + + public String getValue() { + return value; + } + + public String getDesc() { + return desc; + } +} diff --git a/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/handler/CoachCourseHandler.java b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/handler/CoachCourseHandler.java new file mode 100644 index 0000000..09d8784 --- /dev/null +++ b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/handler/CoachCourseHandler.java @@ -0,0 +1,68 @@ +package cn.novalon.gym.manage.coach.handler; + +import cn.novalon.gym.manage.coach.service.CoachCourseService; +import cn.novalon.gym.manage.sys.util.AuthUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +import java.util.Map; + +/** + * 教练开课/结课处理器 + * + * @author 张翔 + * @date 2026-07-20 + */ +@Component +@Tag(name = "教练开课结课", description = "教练开课与结课操作") +public class CoachCourseHandler { + + private static final Logger logger = LoggerFactory.getLogger(CoachCourseHandler.class); + private final CoachCourseService coachCourseService; + private final AuthUtil authUtil; + + public CoachCourseHandler(CoachCourseService coachCourseService, AuthUtil authUtil) { + this.coachCourseService = coachCourseService; + this.authUtil = authUtil; + } + + @Operation(summary = "教练开课", description = "教练手动开课,记录实际开课时间,若超时则判定迟到") + public Mono startCourse(ServerRequest request) { + Long courseId = Long.valueOf(request.pathVariable("courseId")); + Long coachId = authUtil.getMemberIdOrThrow(request); + + return coachCourseService.startCourse(courseId, coachId) + .flatMap(result -> ServerResponse.ok().bodyValue(Map.of( + "message", "开课成功", + "status", result.getStatus(), + "actualStartTime", result.getActualStartTime() != null ? result.getActualStartTime().toString() : "" + ))) + .onErrorResume(e -> { + logger.error("开课失败: {}", e.getMessage()); + return ServerResponse.badRequest().bodyValue(Map.of("error", e.getMessage())); + }); + } + + @Operation(summary = "教练结课", description = "教练手动结课,记录实际结课时间") + public Mono endCourse(ServerRequest request) { + Long courseId = Long.valueOf(request.pathVariable("courseId")); + Long coachId = authUtil.getMemberIdOrThrow(request); + + return coachCourseService.endCourse(courseId, coachId) + .flatMap(result -> ServerResponse.ok().bodyValue(Map.of( + "message", "结课成功", + "status", result.getStatus(), + "actualEndTime", result.getActualEndTime() != null ? result.getActualEndTime().toString() : "" + ))) + .onErrorResume(e -> { + logger.error("结课失败: {}", e.getMessage()); + return ServerResponse.badRequest().bodyValue(Map.of("error", e.getMessage())); + }); + } +} diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/handler/CoachHandler.java b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/handler/CoachHandler.java similarity index 73% rename from gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/handler/CoachHandler.java rename to gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/handler/CoachHandler.java index dddd477..766ea2c 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/handler/CoachHandler.java +++ b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/handler/CoachHandler.java @@ -1,6 +1,6 @@ -package cn.novalon.gym.manage.app.handler; +package cn.novalon.gym.manage.coach.handler; -import cn.novalon.gym.manage.app.service.CoachService; +import cn.novalon.gym.manage.coach.service.CoachCourseService; import cn.novalon.gym.manage.sys.core.domain.SysUser; import cn.novalon.gym.manage.sys.dto.request.CoachCreateRequest; import cn.novalon.gym.manage.sys.dto.request.CoachUpdateRequest; @@ -20,7 +20,7 @@ import java.util.List; import java.util.Map; /** - * 教练处理器(放在 manage-app 中避免模块循环依赖) + * 教练管理处理器(从 manage-app 迁移至 gym-coach 模块) * * @author 张翔 * @date 2026-07-19 @@ -30,18 +30,18 @@ import java.util.Map; public class CoachHandler { private static final Logger logger = LoggerFactory.getLogger(CoachHandler.class); - private final CoachService coachService; + private final CoachCourseService coachCourseService; private final Validator validator; - public CoachHandler(CoachService coachService, Validator validator) { - this.coachService = coachService; + public CoachHandler(CoachCourseService coachCourseService, Validator validator) { + this.coachCourseService = coachCourseService; this.validator = validator; } @Operation(summary = "获取所有教练", description = "获取系统中所有教练列表") public Mono getAllCoaches(ServerRequest request) { return ServerResponse.ok() - .body(coachService.getAllCoaches(), SysUser.class); + .body(coachCourseService.getAllCoaches(), SysUser.class); } @Operation(summary = "创建教练", description = "创建新教练(创建用户并分配教练角色)") @@ -54,7 +54,7 @@ public class CoachHandler { violations.forEach(v -> errors.put(v.getPropertyPath().toString(), v.getMessage())); return ServerResponse.badRequest().bodyValue(errors); } - return coachService.createCoach( + return coachCourseService.createCoach( req.getUsername(), req.getPassword(), req.getNickname(), req.getEmail(), req.getPhone()) .flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user)) @@ -70,7 +70,7 @@ public class CoachHandler { public Mono updateCoach(ServerRequest request) { Long id = Long.valueOf(request.pathVariable("id")); return request.bodyToMono(CoachUpdateRequest.class) - .flatMap(req -> coachService.updateCoach(id, req.getNickname(), req.getEmail(), req.getPhone()) + .flatMap(req -> coachCourseService.updateCoach(id, req.getNickname(), req.getEmail(), req.getPhone()) .flatMap(user -> ServerResponse.ok().bodyValue(user)) .switchIfEmpty(ServerResponse.notFound().build()) .onErrorResume(e -> { @@ -83,7 +83,7 @@ public class CoachHandler { @Operation(summary = "禁用教练", description = "禁用教练账号,自动取消其所有非进行中的团课") public Mono disableCoach(ServerRequest request) { Long id = Long.valueOf(request.pathVariable("id")); - return coachService.disableCoach(id) + return coachCourseService.disableCoach(id) .then(ServerResponse.ok().bodyValue(Map.of("message", "教练已禁用"))) .onErrorResume(e -> { logger.error("禁用教练失败: {}", e.getMessage()); @@ -94,9 +94,25 @@ public class CoachHandler { @Operation(summary = "获取教练的团课", description = "获取指定教练教授的所有团课") public Mono getCoachCourses(ServerRequest request) { Long id = Long.valueOf(request.pathVariable("id")); - return coachService.getCoachCourses(id) + return coachCourseService.getCoachCourses(id) .collectList() .flatMap(courses -> ServerResponse.ok().bodyValue(courses)) .switchIfEmpty(ServerResponse.ok().bodyValue(List.of())); } + + @Operation(summary = "获取教练违规次数统计", description = "获取所有教练的违规次数") + public Mono getViolationCounts(ServerRequest request) { + return coachCourseService.getViolationCounts() + .collectList() + .flatMap(counts -> ServerResponse.ok().bodyValue(counts)); + } + + @Operation(summary = "获取教练违规记录", description = "获取指定教练的违规记录") + public Mono getCoachViolations(ServerRequest request) { + Long id = Long.valueOf(request.pathVariable("id")); + return coachCourseService.getCoachViolations(id) + .collectList() + .flatMap(violations -> ServerResponse.ok().bodyValue(violations)) + .switchIfEmpty(ServerResponse.ok().bodyValue(List.of())); + } } diff --git a/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/scheduler/CoachCourseScheduler.java b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/scheduler/CoachCourseScheduler.java new file mode 100644 index 0000000..f301e68 --- /dev/null +++ b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/scheduler/CoachCourseScheduler.java @@ -0,0 +1,178 @@ +package cn.novalon.gym.manage.coach.scheduler; + +import cn.novalon.gym.manage.coach.enums.ViolationReason; +import cn.novalon.gym.manage.common.util.RedisUtil; +import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao; +import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao; +import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity; +import cn.novalon.gym.manage.groupcourse.enums.CourseStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.r2dbc.core.DatabaseClient; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * 教练课程定时调度器 + * + * 功能: + * 1. 检查未手动开课的课程,超时标记为教练缺席(5)并记录违规 + * 2. 检查未手动结课的课程,超时标记为自动结束(6)并记录违规 + * + * @author 张翔 + * @date 2026-07-20 + */ +@Component +public class CoachCourseScheduler { + + private static final Logger logger = LoggerFactory.getLogger(CoachCourseScheduler.class); + private static final long ONE_HOUR_MINUTES = 60; + private static final long END_GRACE_MINUTES = 10; + + private final GroupCourseDao groupCourseDao; + private final GroupCourseBookingDao groupCourseBookingDao; + private final DatabaseClient databaseClient; + private final RedisUtil redisUtil; + + public CoachCourseScheduler(GroupCourseDao groupCourseDao, + GroupCourseBookingDao groupCourseBookingDao, + DatabaseClient databaseClient, + RedisUtil redisUtil) { + this.groupCourseDao = groupCourseDao; + this.groupCourseBookingDao = groupCourseBookingDao; + this.databaseClient = databaseClient; + this.redisUtil = redisUtil; + } + + /** + * 每分钟检查一次: + * - status=0(NORMAL) 的课程是否已过开课缺席阈值 + * - status=3(IN_PROGRESS) 或 status=7(COACH_LATE) 的课程是否已过结课宽限期 + */ + @Scheduled(fixedRate = 60000) + public void checkAndProcessCourses() { + logger.debug("教练课程调度器开始检查"); + + LocalDateTime now = LocalDateTime.now(); + + // 1. 检查未开课的课程 + processAbsentCourses(now) + .subscribe( + count -> { + if (count > 0) { + logger.info("教练课程调度器:处理了 {} 门教练缺席课程", count); + invalidateCache(); + } + }, + error -> logger.error("教练课程调度器(缺席检查)执行失败:{}", error.getMessage(), error) + ); + + // 2. 检查未结课的课程 + processAutoEndCourses(now) + .subscribe( + count -> { + if (count > 0) { + logger.info("教练课程调度器:处理了 {} 门自动结束课程", count); + invalidateCache(); + } + }, + error -> logger.error("教练课程调度器(自动结课)执行失败:{}", error.getMessage(), error) + ); + } + + /** + * 处理缺席课程:status=0 且已过开课缺席阈值 + */ + private Mono processAbsentCourses(LocalDateTime now) { + return groupCourseDao.findByStatusAndStartTimeBefore(databaseClient, "0", now) + .filter(course -> isAbsentThresholdExceeded(course, now)) + .flatMap(course -> markAsCoachAbsent(course, now)) + .count(); + } + + /** + * 处理自动结课:status IN ('3','7') 且 end_time + 10分钟 已过 + */ + private Mono processAutoEndCourses(LocalDateTime now) { + LocalDateTime endThreshold = now.minusMinutes(END_GRACE_MINUTES); + return groupCourseDao.findByStatusInAndEndTimeBefore(databaseClient, + new String[]{String.valueOf(CourseStatus.IN_PROGRESS.getValue()), + String.valueOf(CourseStatus.COACH_LATE.getValue())}, + endThreshold) + .flatMap(course -> markAsAutoEnded(course, now)) + .count(); + } + + /** + * 判断课程是否已过缺席阈值 + */ + private boolean isAbsentThresholdExceeded(GroupCourseEntity course, LocalDateTime now) { + long courseDurationMinutes = Duration.between(course.getStartTime(), course.getEndTime()).toMinutes(); + long minutesSinceStart = Duration.between(course.getStartTime(), now).toMinutes(); + + if (courseDurationMinutes >= ONE_HOUR_MINUTES) { + return minutesSinceStart > 30; + } else { + long thresholdB = Math.max(1, (long) (courseDurationMinutes * 0.25)); + return minutesSinceStart > thresholdB; + } + } + + /** + * 标记课程为教练缺席(5),更新预约记录为教练缺席(4),记录违规 + */ + private Mono markAsCoachAbsent(GroupCourseEntity course, LocalDateTime now) { + logger.info("课程 {} 教练缺席,标记为 COACH_ABSENT", course.getId()); + + return insertViolation(course.getCoachId(), course.getId(), now, ViolationReason.COACH_ABSENT) + .then(groupCourseDao.updateToCoachAbsent(course.getId(), now, now)) + .then(groupCourseBookingDao.updateStatusByCourseId(course.getId(), "0", "4")) + .thenReturn(course); + } + + /** + * 标记课程为自动结束(6),记录违规 + */ + private Mono markAsAutoEnded(GroupCourseEntity course, LocalDateTime now) { + logger.info("课程 {} 未手动结课,标记为 AUTO_ENDED", course.getId()); + + return insertViolation(course.getCoachId(), course.getId(), now, ViolationReason.NOT_MANUAL_END) + .then(groupCourseDao.updateToAutoEnded(course.getId(), now, now)) + .thenReturn(course); + } + + /** + * 插入违规记录(使用 DatabaseClient 直连) + */ + private Mono insertViolation(Long coachId, Long courseId, LocalDateTime violationTime, + ViolationReason reason) { + return databaseClient.sql(""" + INSERT INTO coach_violation (coach_id, course_id, violation_time, violation_reason, created_at, updated_at) + VALUES (:coachId, :courseId, :violationTime, :reason, :now, :now) + """) + .bind("coachId", coachId) + .bind("courseId", courseId) + .bind("violationTime", violationTime) + .bind("reason", reason.getValue()) + .bind("now", LocalDateTime.now()) + .then(); + } + + /** + * 清除统计缓存和团课缓存 —— 调度器触发时,如有课程状态变更则必须及时失效 + */ + private void invalidateCache() { + redisUtil.deleteByPattern("datacount:statistics:*").subscribe( + deleted -> logger.debug("调度器清除统计缓存,已删除 {} 条", deleted), + error -> logger.warn("调度器清除统计缓存失败: {}", error.getMessage()) + ); + redisUtil.deleteByPattern("group_course:*").subscribe( + deleted -> logger.debug("调度器清除团课缓存,已删除 {} 条", deleted), + error -> logger.warn("调度器清除团课缓存失败: {}", error.getMessage()) + ); + } +} diff --git a/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/service/CoachCourseService.java b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/service/CoachCourseService.java new file mode 100644 index 0000000..18fb3b6 --- /dev/null +++ b/gym-manage-api/gym-coach/src/main/java/cn/novalon/gym/manage/coach/service/CoachCourseService.java @@ -0,0 +1,357 @@ +package cn.novalon.gym.manage.coach.service; + +import cn.novalon.gym.manage.coach.dao.CoachViolationDao; +import cn.novalon.gym.manage.coach.entity.CoachViolationEntity; +import cn.novalon.gym.manage.coach.enums.ViolationReason; +import cn.novalon.gym.manage.common.util.RedisUtil; +import cn.novalon.gym.manage.common.util.StatusConstants; +import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao; +import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao; +import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; +import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity; +import cn.novalon.gym.manage.groupcourse.enums.CourseStatus; +import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository; +import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository; +import cn.novalon.gym.manage.sys.core.domain.SysRole; +import cn.novalon.gym.manage.sys.core.domain.SysUser; +import cn.novalon.gym.manage.sys.core.domain.UserRole; +import cn.novalon.gym.manage.sys.core.repository.ISysRoleRepository; +import cn.novalon.gym.manage.sys.core.repository.ISysUserRepository; +import cn.novalon.gym.manage.sys.core.repository.IUserRoleRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Sort; +import org.springframework.r2dbc.core.DatabaseClient; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 教练课程服务(含教练管理 + 开课/结课逻辑 + 违规记录) + * + * @author 张翔 + * @date 2026-07-20 + */ +@Service +public class CoachCourseService { + + private static final Logger logger = LoggerFactory.getLogger(CoachCourseService.class); + private static final String COACH_ROLE_NAME = "教练"; + private static final long ONE_HOUR_MINUTES = 60; + + private final ISysUserRepository userRepository; + private final ISysRoleRepository roleRepository; + private final IUserRoleRepository userRoleRepository; + private final IGroupCourseRepository groupCourseRepository; + private final IGroupCourseBookingRepository bookingRepository; + private final GroupCourseDao groupCourseDao; + private final GroupCourseBookingDao groupCourseBookingDao; + private final CoachViolationDao violationDao; + private final DatabaseClient databaseClient; + private final PasswordEncoder passwordEncoder; + private final RedisUtil redisUtil; + + public CoachCourseService(ISysUserRepository userRepository, + ISysRoleRepository roleRepository, + IUserRoleRepository userRoleRepository, + IGroupCourseRepository groupCourseRepository, + IGroupCourseBookingRepository bookingRepository, + GroupCourseDao groupCourseDao, + GroupCourseBookingDao groupCourseBookingDao, + CoachViolationDao violationDao, + DatabaseClient databaseClient, + PasswordEncoder passwordEncoder, + RedisUtil redisUtil) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.userRoleRepository = userRoleRepository; + this.groupCourseRepository = groupCourseRepository; + this.bookingRepository = bookingRepository; + this.groupCourseDao = groupCourseDao; + this.groupCourseBookingDao = groupCourseBookingDao; + this.violationDao = violationDao; + this.databaseClient = databaseClient; + this.passwordEncoder = passwordEncoder; + this.redisUtil = redisUtil; + } + + // ==================== 教练管理(从原 CoachService 迁移) ==================== + + public Flux getAllCoaches() { + return getCoachRoleId() + .flatMapMany(roleId -> userRoleRepository.findByRoleId(roleId) + .map(UserRole::getUserId) + .collectList() + .flatMapMany(userIds -> { + if (userIds.isEmpty()) { + return Flux.empty(); + } + return Flux.fromIterable(userIds) + .flatMap(userRepository::findById) + .filter(user -> user.getDeletedAt() == null); + })); + } + + public Mono createCoach(String username, String password, String nickname, String email, String phone) { + return getCoachRoleId().flatMap(coachRoleId -> { + SysUser user = new SysUser(); + user.generateId(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.setNickname(nickname); + user.setEmail(email); + user.setPhone(phone); + user.setStatus(StatusConstants.ENABLED); + + return userRepository.save(user) + .flatMap(saved -> { + UserRole userRole = new UserRole(); + userRole.setUserId(saved.getId()); + userRole.setRoleId(coachRoleId); + return userRoleRepository.save(userRole).thenReturn(saved); + }); + }); + } + + public Mono updateCoach(Long id, String nickname, String email, String phone) { + return userRepository.findById(id) + .switchIfEmpty(Mono.error(new RuntimeException("教练不存在"))) + .flatMap(user -> { + if (nickname != null) user.setNickname(nickname); + if (email != null) user.setEmail(email); + if (phone != null) user.setPhone(phone); + user.setUpdatedAt(LocalDateTime.now()); + return userRepository.update(user); + }); + } + + @Transactional(transactionManager = "connectionFactoryTransactionManager") + public Mono disableCoach(Long id) { + return userRepository.findById(id) + .switchIfEmpty(Mono.error(new RuntimeException("教练不存在"))) + .flatMap(user -> + groupCourseRepository.countByCoachIdAndStatus(id, 3L) + .flatMap(inProgressCount -> { + if (inProgressCount > 0) { + return Mono.error(new RuntimeException( + "该教练有 " + inProgressCount + " 门正在进行中的团课,无法禁用")); + } + return groupCourseRepository.cancelCoursesByCoachIdExceptStatus(id, 3L) + .then(); + }) + .then(Mono.defer(() -> { + user.setStatus(StatusConstants.DISABLED); + user.setUpdatedAt(LocalDateTime.now()); + return userRepository.update(user).then(); + })) + ) + .then(invalidateStatisticsCache()); + } + + public Flux getCoachCourses(Long coachId) { + return groupCourseRepository.findByCoachId(coachId, Sort.by(Sort.Direction.DESC, "startTime")) + .flatMap(course -> + bookingRepository.countValidBookings(course.getId()) + .map(count -> { + course.setCurrentMembers(count.intValue()); + return course; + }) + .defaultIfEmpty(course) + ); + } + + public Mono getCoachRoleId() { + return roleRepository.findByRoleName(COACH_ROLE_NAME) + .map(SysRole::getId) + .switchIfEmpty(Mono.error(new RuntimeException("教练角色未找到,请先执行数据库迁移"))); + } + + // ==================== 开课逻辑 ==================== + + /** + * 教练手动开课 + * 判定逻辑: + * - 长课时(>=1h): 10分钟内正常,10~30分钟迟到,>30分钟拒绝 + * - 短课时(<1h): 10%时长内正常,10%~25%迟到,>25%拒绝 + */ + public Mono startCourse(Long courseId, Long coachId) { + return groupCourseDao.findByIdIsAndDeletedAtIsNull(courseId) + .switchIfEmpty(Mono.error(new RuntimeException("团课不存在"))) + .flatMap(course -> { + // 验证教练身份 + if (!course.getCoachId().equals(coachId)) { + return Mono.error(new RuntimeException("您不是该课程的教练,无权开课")); + } + // 验证课程状态:只有 NORMAL(0) 可以开课 + if (!CourseStatus.NORMAL.getValue().equals(course.getStatus())) { + return Mono.error(new RuntimeException("当前课程状态不允许开课,当前状态: " + course.getStatus())); + } + + LocalDateTime now = LocalDateTime.now(); + long courseDurationMinutes = Duration.between(course.getStartTime(), course.getEndTime()).toMinutes(); + + if (courseDurationMinutes >= ONE_HOUR_MINUTES) { + return handleLongCourseStart(course, now); + } else { + return handleShortCourseStart(course, now, courseDurationMinutes); + } + }); + } + + private Mono handleLongCourseStart(GroupCourseEntity course, LocalDateTime now) { + long minutesSinceStart = Duration.between(course.getStartTime(), now).toMinutes(); + + if (minutesSinceStart < 0) { + return Mono.error(new RuntimeException("课程尚未到开课时间")); + } + if (minutesSinceStart <= 10) { + // 正常开课 + return doStartCourse(course, now, CourseStatus.IN_PROGRESS, null); + } + if (minutesSinceStart <= 30) { + // 教练迟到 + return recordViolation(course.getCoachId(), course.getId(), now, ViolationReason.COACH_LATE) + .then(doStartCourse(course, now, CourseStatus.COACH_LATE, ViolationReason.COACH_LATE)); + } + // >30分钟,拒绝(调度器应已标记为缺席) + return Mono.error(new RuntimeException("已超过开课时间30分钟,无法开课")); + } + + private Mono handleShortCourseStart(GroupCourseEntity course, LocalDateTime now, + long courseDurationMinutes) { + long minutesSinceStart = Duration.between(course.getStartTime(), now).toMinutes(); + long thresholdA = Math.max(1, (long) (courseDurationMinutes * 0.10)); + long thresholdB = Math.max(1, (long) (courseDurationMinutes * 0.25)); + + if (minutesSinceStart < 0) { + return Mono.error(new RuntimeException("课程尚未到开课时间")); + } + if (minutesSinceStart <= thresholdA) { + // 正常开课 + return doStartCourse(course, now, CourseStatus.IN_PROGRESS, null); + } + if (minutesSinceStart <= thresholdB) { + // 教练迟到 + return recordViolation(course.getCoachId(), course.getId(), now, ViolationReason.COACH_LATE) + .then(doStartCourse(course, now, CourseStatus.COACH_LATE, ViolationReason.COACH_LATE)); + } + // >thresholdB,拒绝 + return Mono.error(new RuntimeException("已超过开课时间,无法开课")); + } + + private Mono doStartCourse(GroupCourseEntity course, LocalDateTime now, + CourseStatus newStatus, ViolationReason violationReason) { + course.setStatus(newStatus.getValue()); + course.setActualStartTime(now); + course.setUpdatedAt(now); + return groupCourseDao.updateStartInfo(course.getId(), String.valueOf(newStatus.getValue()), now, now) + .then(groupCourseDao.findByIdIsAndDeletedAtIsNull(course.getId())) + .flatMap(entity -> invalidateStatisticsCache().thenReturn(entity)); + } + + // ==================== 结课逻辑 ==================== + + /** + * 教练手动结课 + * 可在 IN_PROGRESS(3) 或 COACH_LATE(7) 状态下结课 + * 必须在标注结课时间 + 10分钟内 + */ + public Mono endCourse(Long courseId, Long coachId) { + return groupCourseDao.findByIdIsAndDeletedAtIsNull(courseId) + .switchIfEmpty(Mono.error(new RuntimeException("团课不存在"))) + .flatMap(course -> { + // 验证教练身份 + if (!course.getCoachId().equals(coachId)) { + return Mono.error(new RuntimeException("您不是该课程的教练,无权结课")); + } + // 验证状态:IN_PROGRESS(3) 或 COACH_LATE(7) + Long status = course.getStatus(); + if (!CourseStatus.IN_PROGRESS.getValue().equals(status) + && !CourseStatus.COACH_LATE.getValue().equals(status)) { + return Mono.error(new RuntimeException("当前课程状态不允许结课,当前状态: " + status)); + } + + LocalDateTime now = LocalDateTime.now(); + long minutesAfterEnd = Duration.between(course.getEndTime(), now).toMinutes(); + + if (minutesAfterEnd > 10) { + return Mono.error(new RuntimeException("已超过结课时间10分钟,请等待系统自动结课")); + } + + course.setStatus(CourseStatus.ENDED.getValue()); + course.setActualEndTime(now); + course.setUpdatedAt(now); + return groupCourseDao.updateEndInfo(course.getId(), String.valueOf(CourseStatus.ENDED.getValue()), now, now) + .then(groupCourseDao.findByIdIsAndDeletedAtIsNull(course.getId())) + .flatMap(entity -> invalidateStatisticsCache().thenReturn(entity)); + }); + } + + // ==================== 违规记录 ==================== + + /** + * 记录教练违规行为(使用 DatabaseClient 直连,避免 R2DBC Entity 映射问题) + */ + public Mono recordViolation(Long coachId, Long courseId, LocalDateTime violationTime, + ViolationReason reason) { + LocalDateTime now = LocalDateTime.now(); + return databaseClient.sql(""" + INSERT INTO coach_violation (coach_id, course_id, violation_time, violation_reason, created_at, updated_at) + VALUES (:coachId, :courseId, :violationTime, :reason, :now, :now) + """) + .bind("coachId", coachId) + .bind("courseId", courseId) + .bind("violationTime", violationTime) + .bind("reason", reason.getValue()) + .bind("now", now) + .then(); + } + + // ==================== 违规查询 ==================== + + /** + * 获取所有教练的违规次数统计 + */ + public Flux> getViolationCounts() { + return databaseClient.sql(""" + SELECT coach_id, COUNT(*) AS count + FROM coach_violation + WHERE deleted_at IS NULL + GROUP BY coach_id + """) + .fetch() + .all(); + } + + /** + * 获取指定教练的违规记录 + */ + public Flux> getCoachViolations(Long coachId) { + return databaseClient.sql(""" + SELECT v.*, gc.course_name + FROM coach_violation v + LEFT JOIN group_course gc ON v.course_id = gc.id AND gc.deleted_at IS NULL + WHERE v.coach_id = :coachId AND v.deleted_at IS NULL + ORDER BY v.violation_time DESC + """) + .bind("coachId", coachId) + .fetch() + .all(); + } + + /** + * 清除统计缓存和团课缓存 —— 课程状态变更后调用,返回 Mono 确保链式执行 + */ + private Mono invalidateStatisticsCache() { + return redisUtil.deleteByPattern("datacount:statistics:*") + .then(redisUtil.deleteByPattern("group_course:*")) + .doOnNext(count -> {}) + .then(); + } +} diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/DataStatisticsDao.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/DataStatisticsDao.java index c08df57..d26c5f8 100644 --- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/DataStatisticsDao.java +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/DataStatisticsDao.java @@ -6,6 +6,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.LocalDateTime; +import java.util.Map; /** * 数据统计 DAO - 使用 DatabaseClient 执行跨表聚合查询 @@ -180,4 +181,68 @@ public class DataStatisticsDao { this.count = count; } } + + // ========== 教练相关统计 ========== + + /** 统计教练总数(角色为"教练"的用户) */ + public Mono countTotalCoaches() { + return databaseClient.sql(""" + SELECT COUNT(*) FROM sys_user u + INNER JOIN user_role ur ON u.id = ur.user_id + INNER JOIN sys_role sr ON ur.role_id = sr.id + WHERE sr.role_name = '教练' AND u.deleted_at IS NULL + """) + .map(row -> row.get(0, Long.class)) + .one(); + } + + /** 统计违规总数(时间范围) */ + public Mono countTotalViolations(LocalDateTime startTime, LocalDateTime endTime) { + return databaseClient.sql(""" + SELECT COUNT(*) FROM coach_violation + WHERE violation_time >= :startTime AND violation_time < :endTime AND deleted_at IS NULL + """) + .bind("startTime", startTime) + .bind("endTime", endTime) + .map(row -> row.get(0, Long.class)) + .one(); + } + + /** 按违规类型统计次数 */ + public Flux> countViolationsByReason(LocalDateTime startTime, LocalDateTime endTime) { + return databaseClient.sql(""" + SELECT violation_reason, COUNT(*) AS count + FROM coach_violation + WHERE violation_time >= :startTime AND violation_time < :endTime AND deleted_at IS NULL + GROUP BY violation_reason + """) + .bind("startTime", startTime) + .bind("endTime", endTime) + .fetch() + .all(); + } + + /** 统计有违规记录的教练数 */ + public Mono countViolatedCoaches(LocalDateTime startTime, LocalDateTime endTime) { + return databaseClient.sql(""" + SELECT COUNT(DISTINCT coach_id) FROM coach_violation + WHERE violation_time >= :startTime AND violation_time < :endTime AND deleted_at IS NULL + """) + .bind("startTime", startTime) + .bind("endTime", endTime) + .map(row -> row.get(0, Long.class)) + .one(); + } + + /** 统计区间内开课的团课数 */ + public Mono countCourses(LocalDateTime startTime, LocalDateTime endTime) { + return databaseClient.sql(""" + SELECT COUNT(*) FROM group_course + WHERE start_time >= :startTime AND start_time < :endTime AND deleted_at IS NULL + """) + .bind("startTime", startTime) + .bind("endTime", endTime) + .map(row -> row.get(0, Long.class)) + .one(); + } } \ No newline at end of file diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/CoachStatistics.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/CoachStatistics.java new file mode 100644 index 0000000..3c8fbf8 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/CoachStatistics.java @@ -0,0 +1,40 @@ +package cn.novalon.gym.manage.datacount.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 教练统计数据 + * + * @author 张翔 + * @date 2026-07-20 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CoachStatistics { + + /** 教练总数 */ + private Long totalCoaches; + + /** 违规总数 */ + private Long totalViolations; + + /** 迟到次数 */ + private Long lateCount; + + /** 缺席次数 */ + private Long absentCount; + + /** 未手动结课次数 */ + private Long notManualEndCount; + + /** 有违规记录的教练数 */ + private Long violatedCoaches; + + /** 统计区间内开课的团课数 */ + private Long totalCourses; +} diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsSummary.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsSummary.java index 2e7dd12..7d86d1b 100644 --- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsSummary.java +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsSummary.java @@ -37,6 +37,11 @@ public class StatisticsSummary { */ private SignInStatistics signInStatistics; + /** + * 教练统计数据 + */ + private CoachStatistics coachStatistics; + /** * 统计数据生成时间 */ diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataStatisticsServiceImpl.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataStatisticsServiceImpl.java index 074d29f..352d295 100644 --- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataStatisticsServiceImpl.java +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataStatisticsServiceImpl.java @@ -171,18 +171,47 @@ public class DataStatisticsServiceImpl implements IDataStatisticsService { return count != null ? count : 0L; } + private Mono getCoachStatistics(StatisticsQuery query) { + LocalDateTime startTime = getStartTime(query); + LocalDateTime endTime = getEndTime(query); + + Mono totalCoachesMono = dataStatisticsDao.countTotalCoaches(); + Mono totalViolationsMono = dataStatisticsDao.countTotalViolations(startTime, endTime); + Mono violatedCoachesMono = dataStatisticsDao.countViolatedCoaches(startTime, endTime); + Mono totalCoursesMono = dataStatisticsDao.countCourses(startTime, endTime); + Mono> violationByReasonMono = dataStatisticsDao.countViolationsByReason(startTime, endTime) + .collectMap(row -> (String) row.get("violation_reason"), + row -> ((Number) row.get("count")).longValue()); + + return Mono.zip(totalCoachesMono, totalViolationsMono, violatedCoachesMono, totalCoursesMono, violationByReasonMono) + .map(tuple -> { + Map reasonMap = tuple.getT5(); + return CoachStatistics.builder() + .totalCoaches(tuple.getT1()) + .totalViolations(tuple.getT2()) + .lateCount(reasonMap.getOrDefault("COACH_LATE", 0L)) + .absentCount(reasonMap.getOrDefault("COACH_ABSENT", 0L)) + .notManualEndCount(reasonMap.getOrDefault("NOT_MANUAL_END", 0L)) + .violatedCoaches(tuple.getT3()) + .totalCourses(tuple.getT4()) + .build(); + }); + } + @Override public Mono getStatisticsSummary(StatisticsQuery query) { Mono memberStatsMono = getMemberStatistics(query); Mono bookingStatsMono = getBookingStatistics(query); Mono signInStatsMono = getSignInStatistics(query); + Mono coachStatsMono = getCoachStatistics(query); - return Mono.zip(memberStatsMono, bookingStatsMono, signInStatsMono) + return Mono.zip(memberStatsMono, bookingStatsMono, signInStatsMono, coachStatsMono) .map(tuple -> StatisticsSummary.builder() .statDate(LocalDateTime.now().toLocalDate().toString()) .memberStatistics(tuple.getT1()) .bookingStatistics(tuple.getT2()) .signInStatistics(tuple.getT3()) + .coachStatistics(tuple.getT4()) .generatedAt(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) .build()); } diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseBookingDao.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseBookingDao.java index ec0bc5c..9b11e9a 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseBookingDao.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseBookingDao.java @@ -114,4 +114,11 @@ public interface GroupCourseBookingDao extends R2dbcRepository updateToAbsent(Long id, java.time.LocalDateTime updatedAt); + + /** + * 批量更新某课程的预约状态(教练缺席场景) + */ + @org.springframework.data.r2dbc.repository.Modifying + @org.springframework.data.r2dbc.repository.Query("UPDATE group_course_booking SET status = :newStatus, updated_at = NOW() WHERE course_id = :courseId AND status = :oldStatus AND deleted_at IS NULL") + Mono updateStatusByCourseId(Long courseId, String oldStatus, String newStatus); } \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java index 1e3d0d1..2d5c32d 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/dao/GroupCourseDao.java @@ -37,9 +37,45 @@ public interface GroupCourseDao extends R2dbcRepository @Query("UPDATE group_course SET deleted_at = :deletedAt WHERE id = :id") Mono softDelete(Long id, LocalDateTime deletedAt); + // ---------- 教练相关 SQL 方法 ---------- + @Modifying - @Query("UPDATE group_course SET status = '2', updated_at = :updatedAt WHERE status = '0' AND end_time <= NOW() AND deleted_at IS NULL") - Mono completeExpiredCourses(LocalDateTime updatedAt); + @Query("UPDATE group_course SET status = :status, actual_start_time = :actualStartTime, updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL") + Mono updateStartInfo(Long id, String status, LocalDateTime actualStartTime, LocalDateTime updatedAt); + + @Modifying + @Query("UPDATE group_course SET status = :status, actual_end_time = :actualEndTime, updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL") + Mono updateEndInfo(Long id, String status, LocalDateTime actualEndTime, LocalDateTime updatedAt); + + @Modifying + @Query("UPDATE group_course SET status = '5', actual_end_time = :actualEndTime, updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL") + Mono updateToCoachAbsent(Long id, LocalDateTime actualEndTime, LocalDateTime updatedAt); + + @Modifying + @Query("UPDATE group_course SET status = '6', actual_end_time = :actualEndTime, updated_at = :updatedAt WHERE id = :id AND deleted_at IS NULL") + Mono updateToAutoEnded(Long id, LocalDateTime actualEndTime, LocalDateTime updatedAt); + + /** + * 查询指定状态且开始时间早于指定时间的课程(用于缺席检查) + */ + default Flux findByStatusAndStartTimeBefore(DatabaseClient databaseClient, String status, LocalDateTime time) { + return databaseClient.sql("SELECT * FROM group_course WHERE status = :status AND start_time < :time AND deleted_at IS NULL") + .bind("status", status) + .bind("time", time) + .map((row, meta) -> mapRowToEntity(row)) + .all(); + } + + /** + * 查询指定状态集合且结束时间早于指定时间的课程(用于自动结课检查) + */ + default Flux findByStatusInAndEndTimeBefore(DatabaseClient databaseClient, String[] statuses, LocalDateTime time) { + return databaseClient.sql("SELECT * FROM group_course WHERE status = ANY(:statuses::varchar[]) AND end_time < :time AND deleted_at IS NULL") + .bind("statuses", statuses) + .bind("time", time) + .map((row, meta) -> mapRowToEntity(row)) + .all(); + } Flux findByCourseTypeAndDeletedAtIsNull(Long courseType); @@ -328,6 +364,8 @@ public interface GroupCourseDao extends R2dbcRepository entity.setCoverImage(row.get("cover_image", String.class)); entity.setDescription(row.get("description", String.class)); entity.setStoredValueAmount(row.get("stored_value_amount", java.math.BigDecimal.class)); + entity.setActualStartTime(row.get("actual_start_time", LocalDateTime.class)); + entity.setActualEndTime(row.get("actual_end_time", LocalDateTime.class)); entity.setQrCodePath(row.get("qr_code_path", String.class)); entity.setCreateBy(row.get("create_by", String.class)); entity.setUpdateBy(row.get("update_by", String.class)); diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/domain/GroupCourse.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/domain/GroupCourse.java index 2369e2d..66ca574 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/domain/GroupCourse.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/domain/GroupCourse.java @@ -40,6 +40,14 @@ public class GroupCourse extends BaseDomain{ @Schema(description = "课程状态", example = "0") private Long status; + //实际开课时间 + @Schema(description = "实际开课时间") + private LocalDateTime actualStartTime; + + //实际结课时间 + @Schema(description = "实际结课时间") + private LocalDateTime actualEndTime; + //上课地点 @Schema(description = "上课地点", example = "龙泉驿区幸福路") private String location; @@ -124,6 +132,22 @@ public class GroupCourse extends BaseDomain{ this.status = status; } + public LocalDateTime getActualStartTime() { + return actualStartTime; + } + + public void setActualStartTime(LocalDateTime actualStartTime) { + this.actualStartTime = actualStartTime; + } + + public LocalDateTime getActualEndTime() { + return actualEndTime; + } + + public void setActualEndTime(LocalDateTime actualEndTime) { + this.actualEndTime = actualEndTime; + } + public String getLocation() { return location; } diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/entity/GroupCourseEntity.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/entity/GroupCourseEntity.java index 5ef3fa7..4d65dc4 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/entity/GroupCourseEntity.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/entity/GroupCourseEntity.java @@ -38,7 +38,15 @@ public class GroupCourseEntity extends BaseEntity { @Column("current_members") private Integer currentMembers; - //课程状态:0-正常,1-已取消,2-已结束 + //实际开课时间 + @Column("actual_start_time") + private LocalDateTime actualStartTime; + + //实际结课时间 + @Column("actual_end_time") + private LocalDateTime actualEndTime; + + //课程状态:0-正常,1-已取消,2-已结束,3-进行中,5-教练缺席,6-自动结束,7-教练迟到 @Column("status") private Long status; @@ -158,6 +166,22 @@ public class GroupCourseEntity extends BaseEntity { this.storedValueAmount = storedValueAmount; } + public LocalDateTime getActualStartTime() { + return actualStartTime; + } + + public void setActualStartTime(LocalDateTime actualStartTime) { + this.actualStartTime = actualStartTime; + } + + public LocalDateTime getActualEndTime() { + return actualEndTime; + } + + public void setActualEndTime(LocalDateTime actualEndTime) { + this.actualEndTime = actualEndTime; + } + public String getQrCodePath() { return qrCodePath; } diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/enums/CourseStatus.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/enums/CourseStatus.java index ed198e6..5c0b7c0 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/enums/CourseStatus.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/enums/CourseStatus.java @@ -11,7 +11,10 @@ public enum CourseStatus { NORMAL(0L, "正常"), CANCELLED(1L, "已取消"), ENDED(2L, "已结束"), - IN_PROGRESS(3L, "进行中"); + IN_PROGRESS(3L, "进行中"), + COACH_ABSENT(5L, "教练缺席"), + AUTO_ENDED(6L, "自动结束"), + COACH_LATE(7L, "教练迟到"); private final Long value; private final String desc; diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/scheduler/GroupCourseExpireScheduler.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/scheduler/GroupCourseExpireScheduler.java deleted file mode 100644 index da4a5f1..0000000 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/scheduler/GroupCourseExpireScheduler.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.novalon.gym.manage.groupcourse.scheduler; - -import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; - -/** - * 团课过期状态自动更新定时任务 - * - * 功能:定期检查已过期的团课(end_time <= NOW()),自动将 status 从 0 更新为 2(已结束) - * - * @date 2026-06-24 - */ -@Component -public class GroupCourseExpireScheduler { - - private static final Logger logger = LoggerFactory.getLogger(GroupCourseExpireScheduler.class); - - private final GroupCourseDao groupCourseDao; - - public GroupCourseExpireScheduler(GroupCourseDao groupCourseDao) { - this.groupCourseDao = groupCourseDao; - } - - /** - * 每分钟检查一次,将已过期但状态仍为 0 的团课标记为已结束(status = 2) - */ - @Scheduled(fixedRate = 60000) - public void completeExpiredCourses() { - logger.debug("定时任务开始检查已过期团课,更新状态为已结束"); - - groupCourseDao.completeExpiredCourses(LocalDateTime.now()) - .subscribe( - count -> { - if (count > 0) { - logger.info("定时任务完成,更新了 {} 条过期团课状态为已结束", count); - } - }, - error -> logger.error("过期团课状态更新定时任务执行失败:{}", error.getMessage(), error) - ); - } -} diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java index c0d6f04..ad4850e 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/service/impl/GroupCourseService.java @@ -741,7 +741,9 @@ public class GroupCourseService implements IGroupCourseService { private Mono clearCache() { return redisUtil.deleteByPattern(CACHE_KEY_PREFIX + "*") .then(redisUtil.deleteByPattern(CACHE_KEY_ID_PREFIX + "*")) - .then(redisUtil.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*")).then(); + .then(redisUtil.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*")) + .then(redisUtil.deleteByPattern("datacount:statistics:*")) + .then(); } @Override diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/vo/GroupCourseVO.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/vo/GroupCourseVO.java index c76330b..70f713a 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/vo/GroupCourseVO.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/vo/GroupCourseVO.java @@ -21,6 +21,8 @@ public class GroupCourseVO { private Long courseType; private LocalDateTime startTime; private LocalDateTime endTime; + private LocalDateTime actualStartTime; + private LocalDateTime actualEndTime; private Integer maxMembers; private Integer currentMembers; private Long status; @@ -48,6 +50,8 @@ public class GroupCourseVO { vo.setCourseType(course.getCourseType()); vo.setStartTime(course.getStartTime()); vo.setEndTime(course.getEndTime()); + vo.setActualStartTime(course.getActualStartTime()); + vo.setActualEndTime(course.getActualEndTime()); vo.setMaxMembers(course.getMaxMembers()); vo.setCurrentMembers(course.getCurrentMembers()); vo.setStatus(course.getStatus()); @@ -86,6 +90,12 @@ public class GroupCourseVO { public LocalDateTime getEndTime() { return endTime; } public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; } + public LocalDateTime getActualStartTime() { return actualStartTime; } + public void setActualStartTime(LocalDateTime actualStartTime) { this.actualStartTime = actualStartTime; } + + public LocalDateTime getActualEndTime() { return actualEndTime; } + public void setActualEndTime(LocalDateTime actualEndTime) { this.actualEndTime = actualEndTime; } + public Integer getMaxMembers() { return maxMembers; } public void setMaxMembers(Integer maxMembers) { this.maxMembers = maxMembers; } diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index beeccbe..5afc9c2 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -169,6 +169,12 @@ 1.0.0 compile + + cn.novalon.gym.manage + gym-coach + 1.0.0 + compile + diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java index 6ad96f5..620f137 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java @@ -25,7 +25,8 @@ import java.util.List; "cn.novalon.gym.manage.member.repository", "cn.novalon.gym.manage.groupcourse.dao", "cn.novalon.gym.manage.checkIn.repository", - "cn.novalon.gym.manage.payment.repository" + "cn.novalon.gym.manage.payment.repository", + "cn.novalon.gym.manage.coach.dao" }) @EnableReactiveElasticsearchRepositories(basePackages = "cn.novalon.gym.manage.member.es.repository") public class ManageApplication { diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java index d8263ab..5f5c957 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java @@ -20,7 +20,8 @@ import cn.novalon.gym.manage.notify.handler.BannerHandler; import cn.novalon.gym.manage.notify.handler.SysNoticeHandler; import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler; import cn.novalon.gym.manage.payment.handler.PaymentHandler; -import cn.novalon.gym.manage.app.handler.CoachHandler; +import cn.novalon.gym.manage.coach.handler.CoachCourseHandler; +import cn.novalon.gym.manage.coach.handler.CoachHandler; import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler; import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler; import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler; @@ -86,7 +87,8 @@ public class SystemRouter { DataStatisticsHandler dataStatisticsHandler, PhoneAuthHandler phoneAuthHandler, PaymentHandler paymentHandler, - CoachHandler coachHandler) { + CoachHandler coachHandler, + CoachCourseHandler coachCourseHandler) { return route() // ========== 诊断路由 ========== @@ -126,6 +128,10 @@ public class SystemRouter { .PUT("/api/coach/{id}", coachHandler::updateCoach) .POST("/api/coach/{id}/disable", coachHandler::disableCoach) .GET("/api/coach/{id}/courses", coachHandler::getCoachCourses) + .GET("/api/coach/violation-counts", coachHandler::getViolationCounts) + .GET("/api/coach/{id}/violations", coachHandler::getCoachViolations) + .POST("/api/coach/courses/{courseId}/start", coachCourseHandler::startCourse) + .POST("/api/coach/courses/{courseId}/end", coachCourseHandler::endCourse) // ========== 菜单路由 ========== .GET("/api/menus", menuHandler::getAllMenus) diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/service/CoachService.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/service/CoachService.java deleted file mode 100644 index 91bb635..0000000 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/service/CoachService.java +++ /dev/null @@ -1,144 +0,0 @@ -package cn.novalon.gym.manage.app.service; - -import cn.novalon.gym.manage.common.util.StatusConstants; -import cn.novalon.gym.manage.groupcourse.domain.GroupCourse; -import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository; -import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository; -import cn.novalon.gym.manage.sys.core.domain.SysRole; -import cn.novalon.gym.manage.sys.core.domain.SysUser; -import cn.novalon.gym.manage.sys.core.domain.UserRole; -import cn.novalon.gym.manage.sys.core.repository.ISysRoleRepository; -import cn.novalon.gym.manage.sys.core.repository.ISysUserRepository; -import cn.novalon.gym.manage.sys.core.repository.IUserRoleRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Sort; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.time.LocalDateTime; - -/** - * 教练服务(跨模块编排,放在 manage-app 中避免模块循环依赖) - * - * @author 张翔 - * @date 2026-07-19 - */ -@Service -public class CoachService { - - private static final Logger logger = LoggerFactory.getLogger(CoachService.class); - private static final String COACH_ROLE_NAME = "教练"; - - private final ISysUserRepository userRepository; - private final ISysRoleRepository roleRepository; - private final IUserRoleRepository userRoleRepository; - private final IGroupCourseRepository groupCourseRepository; - private final IGroupCourseBookingRepository bookingRepository; - private final PasswordEncoder passwordEncoder; - - public CoachService(ISysUserRepository userRepository, - ISysRoleRepository roleRepository, - IUserRoleRepository userRoleRepository, - IGroupCourseRepository groupCourseRepository, - IGroupCourseBookingRepository bookingRepository, - PasswordEncoder passwordEncoder) { - this.userRepository = userRepository; - this.roleRepository = roleRepository; - this.userRoleRepository = userRoleRepository; - this.groupCourseRepository = groupCourseRepository; - this.bookingRepository = bookingRepository; - this.passwordEncoder = passwordEncoder; - } - - public Flux getAllCoaches() { - return getCoachRoleId() - .flatMapMany(roleId -> userRoleRepository.findByRoleId(roleId) - .map(UserRole::getUserId) - .collectList() - .flatMapMany(userIds -> { - if (userIds.isEmpty()) { - return Flux.empty(); - } - return Flux.fromIterable(userIds) - .flatMap(userRepository::findById) - .filter(user -> user.getDeletedAt() == null); - })); - } - - public Mono createCoach(String username, String password, String nickname, String email, String phone) { - return getCoachRoleId().flatMap(coachRoleId -> { - SysUser user = new SysUser(); - user.generateId(); - user.setUsername(username); - user.setPassword(passwordEncoder.encode(password)); - user.setNickname(nickname); - user.setEmail(email); - user.setPhone(phone); - user.setStatus(StatusConstants.ENABLED); - - return userRepository.save(user) - .flatMap(saved -> { - UserRole userRole = new UserRole(); - userRole.setUserId(saved.getId()); - userRole.setRoleId(coachRoleId); - return userRoleRepository.save(userRole).thenReturn(saved); - }); - }); - } - - public Mono updateCoach(Long id, String nickname, String email, String phone) { - return userRepository.findById(id) - .switchIfEmpty(Mono.error(new RuntimeException("教练不存在"))) - .flatMap(user -> { - if (nickname != null) user.setNickname(nickname); - if (email != null) user.setEmail(email); - if (phone != null) user.setPhone(phone); - user.setUpdatedAt(LocalDateTime.now()); - return userRepository.update(user); - }); - } - - @Transactional(transactionManager = "connectionFactoryTransactionManager") - public Mono disableCoach(Long id) { - return userRepository.findById(id) - .switchIfEmpty(Mono.error(new RuntimeException("教练不存在"))) - .flatMap(user -> - groupCourseRepository.countByCoachIdAndStatus(id, 3L) - .flatMap(inProgressCount -> { - if (inProgressCount > 0) { - return Mono.error(new RuntimeException( - "该教练有 " + inProgressCount + " 门正在进行中的团课,无法禁用")); - } - return groupCourseRepository.cancelCoursesByCoachIdExceptStatus(id, 3L) - .then(); - }) - .then(Mono.defer(() -> { - user.setStatus(StatusConstants.DISABLED); - user.setUpdatedAt(LocalDateTime.now()); - return userRepository.update(user).then(); - })) - ); - } - - public Flux getCoachCourses(Long coachId) { - return groupCourseRepository.findByCoachId(coachId, Sort.by(Sort.Direction.DESC, "startTime")) - .flatMap(course -> - bookingRepository.countValidBookings(course.getId()) - .map(count -> { - course.setCurrentMembers(count.intValue()); - return course; - }) - .defaultIfEmpty(course) - ); - } - - public Mono getCoachRoleId() { - return roleRepository.findByRoleName(COACH_ROLE_NAME) - .map(SysRole::getId) - .switchIfEmpty(Mono.error(new RuntimeException("教练角色未找到,请先执行数据库迁移"))); - } -} diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V18__Insert_group_course_data.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V18__Insert_group_course_data.sql index 4fcc7e9..0075075 100644 --- a/gym-manage-api/manage-db/src/main/resources/db/migration/V18__Insert_group_course_data.sql +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V18__Insert_group_course_data.sql @@ -66,121 +66,121 @@ ON CONFLICT DO NOTHING; -- ============================================ -- 4. 团课课程数据(5个类型 × 5个课程 = 25个) --- start_time / end_time 分布在 2026-07-20 至 2026-08-02 +-- start_time / end_time 分布在 2026-07-22 至 2026-07-28 -- ============================================ -- ---------- 瑜伽课程(5个)---------- INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '哈他瑜伽入门', gt.id, '2026-07-20 08:00:00'::TIMESTAMP, '2026-07-20 09:00:00'::TIMESTAMP, 25, 12, '0', '瑜伽室A', '/static/course/yoga_hatha.jpg', '经典哈他瑜伽,从基础体式入手,配合呼吸引导,帮助初学者建立正确的瑜伽练习基础,感受身心的和谐统一。', 88.00, 'system' +SELECT '哈他瑜伽入门', gt.id, '2026-07-22 08:00:00'::TIMESTAMP, '2026-07-22 09:00:00'::TIMESTAMP, 25, 12, '0', '瑜伽室A', '/static/course/yoga_hatha.jpg', '经典哈他瑜伽,从基础体式入手,配合呼吸引导,帮助初学者建立正确的瑜伽练习基础,感受身心的和谐统一。', 88.00, 'system' FROM group_course_type gt WHERE gt.type_name = '瑜伽'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '流瑜伽', gt.id, '2026-07-22 10:00:00'::TIMESTAMP, '2026-07-22 11:00:00'::TIMESTAMP, 20, 18, '0', '瑜伽室A', '/static/course/yoga_flow.jpg', '体式之间流畅串联,如行云流水般一气呵成。以拜日式为根基,结合站姿、平衡和扭转,提升力量与柔韧的整合能力。', 98.00, 'system' +SELECT '流瑜伽', gt.id, '2026-07-24 10:00:00'::TIMESTAMP, '2026-07-24 11:00:00'::TIMESTAMP, 20, 18, '0', '瑜伽室A', '/static/course/yoga_flow.jpg', '体式之间流畅串联,如行云流水般一气呵成。以拜日式为根基,结合站姿、平衡和扭转,提升力量与柔韧的整合能力。', 98.00, 'system' FROM group_course_type gt WHERE gt.type_name = '瑜伽'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '阴瑜伽', gt.id, '2026-07-24 18:00:00'::TIMESTAMP, '2026-07-24 19:15:00'::TIMESTAMP, 20, 8, '0', '瑜伽室B', '/static/course/yoga_yin.jpg', '阴瑜伽以长时间保持体式为特点,深度伸展筋膜和结缔组织,帮助你释放身体深层紧张,缓解一周的工作疲劳。适合所有级别。', 88.00, 'system' +SELECT '阴瑜伽', gt.id, '2026-07-26 18:00:00'::TIMESTAMP, '2026-07-26 19:15:00'::TIMESTAMP, 20, 8, '0', '瑜伽室B', '/static/course/yoga_yin.jpg', '阴瑜伽以长时间保持体式为特点,深度伸展筋膜和结缔组织,帮助你释放身体深层紧张,缓解一周的工作疲劳。适合所有级别。', 88.00, 'system' FROM group_course_type gt WHERE gt.type_name = '瑜伽'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '阿斯汤加瑜伽', gt.id, '2026-07-21 07:00:00'::TIMESTAMP, '2026-07-21 08:30:00'::TIMESTAMP, 15, 10, '0', '瑜伽室A', '/static/course/yoga_ashtanga.jpg', '阿斯汤加瑜伽以固定的体式序列和独特的呼吸法为核心,节奏紧凑,强度较高,能有效提升力量、柔韧和专注力。建议有一定瑜伽基础者参加。', 128.00, 'system' +SELECT '阿斯汤加瑜伽', gt.id, '2026-07-23 07:00:00'::TIMESTAMP, '2026-07-23 08:30:00'::TIMESTAMP, 15, 10, '0', '瑜伽室A', '/static/course/yoga_ashtanga.jpg', '阿斯汤加瑜伽以固定的体式序列和独特的呼吸法为核心,节奏紧凑,强度较高,能有效提升力量、柔韧和专注力。建议有一定瑜伽基础者参加。', 128.00, 'system' FROM group_course_type gt WHERE gt.type_name = '瑜伽'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '空中瑜伽', gt.id, '2026-07-23 19:00:00'::TIMESTAMP, '2026-07-23 20:00:00'::TIMESTAMP, 12, 12, '0', '瑜伽室B', '/static/course/yoga_aerial.jpg', '利用悬挂的瑜伽吊床完成各种体式,借助重力让脊柱自然延展,体验"飞翔"般的自由感。深受女性学员喜爱,名额有限请提前预约。', 128.00, 'system' +SELECT '空中瑜伽', gt.id, '2026-07-25 19:00:00'::TIMESTAMP, '2026-07-25 20:00:00'::TIMESTAMP, 12, 12, '0', '瑜伽室B', '/static/course/yoga_aerial.jpg', '利用悬挂的瑜伽吊床完成各种体式,借助重力让脊柱自然延展,体验"飞翔"般的自由感。深受女性学员喜爱,名额有限请提前预约。', 128.00, 'system' FROM group_course_type gt WHERE gt.type_name = '瑜伽'; -- ---------- 动感单车课程(5个)---------- INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '初级燃脂骑行', gt.id, '2026-07-20 19:00:00'::TIMESTAMP, '2026-07-20 19:45:00'::TIMESTAMP, 30, 22, '0', '单车房', '/static/course/spin_beginner.jpg', '适合新手的入门级骑行课程,教练将带领你掌握正确的骑行姿势和阻力调节技巧。在动感音乐中轻松燃烧300-400卡路里。', 58.00, 'system' +SELECT '初级燃脂骑行', gt.id, '2026-07-22 19:00:00'::TIMESTAMP, '2026-07-22 19:45:00'::TIMESTAMP, 30, 22, '0', '单车房', '/static/course/spin_beginner.jpg', '适合新手的入门级骑行课程,教练将带领你掌握正确的骑行姿势和阻力调节技巧。在动感音乐中轻松燃烧300-400卡路里。', 58.00, 'system' FROM group_course_type gt WHERE gt.type_name = '动感单车'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '节奏爬坡', gt.id, '2026-07-22 18:30:00'::TIMESTAMP, '2026-07-22 19:15:00'::TIMESTAMP, 30, 15, '0', '单车房', '/static/course/spin_climb.jpg', '模拟山路爬坡骑行,通过逐渐增加阻力来挑战你的耐力和意志力。每一段爬坡后都有短暂的平路冲刺,节奏感十足,大汗淋漓的畅快体验。', 68.00, 'system' +SELECT '节奏爬坡', gt.id, '2026-07-24 18:30:00'::TIMESTAMP, '2026-07-24 19:15:00'::TIMESTAMP, 30, 15, '0', '单车房', '/static/course/spin_climb.jpg', '模拟山路爬坡骑行,通过逐渐增加阻力来挑战你的耐力和意志力。每一段爬坡后都有短暂的平路冲刺,节奏感十足,大汗淋漓的畅快体验。', 68.00, 'system' FROM group_course_type gt WHERE gt.type_name = '动感单车'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '极速冲刺', gt.id, '2026-07-24 20:00:00'::TIMESTAMP, '2026-07-24 20:45:00'::TIMESTAMP, 25, 25, '0', '单车房', '/static/course/spin_sprint.jpg', '高强度冲刺为主题的骑行课,多组30秒极限冲刺搭配短暂恢复,彻底引爆你的心肺极限。周五夜间的爆款课程,约满速度极快!', 68.00, 'system' +SELECT '极速冲刺', gt.id, '2026-07-26 20:00:00'::TIMESTAMP, '2026-07-26 20:45:00'::TIMESTAMP, 25, 25, '0', '单车房', '/static/course/spin_sprint.jpg', '高强度冲刺为主题的骑行课,多组30秒极限冲刺搭配短暂恢复,彻底引爆你的心肺极限。周五夜间的爆款课程,约满速度极快!', 68.00, 'system' FROM group_course_type gt WHERE gt.type_name = '动感单车'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '耐力骑行', gt.id, '2026-07-25 10:00:00'::TIMESTAMP, '2026-07-25 11:00:00'::TIMESTAMP, 30, 18, '0', '单车房', '/static/course/spin_endurance.jpg', '60分钟中低强度耐力骑行,适合周末早晨唤醒身体。在稳定的节奏中持续燃脂,配以舒缓的收尾拉伸,开启元气满满的周末。', 58.00, 'system' +SELECT '耐力骑行', gt.id, '2026-07-27 10:00:00'::TIMESTAMP, '2026-07-27 11:00:00'::TIMESTAMP, 30, 18, '0', '单车房', '/static/course/spin_endurance.jpg', '60分钟中低强度耐力骑行,适合周末早晨唤醒身体。在稳定的节奏中持续燃脂,配以舒缓的收尾拉伸,开启元气满满的周末。', 58.00, 'system' FROM group_course_type gt WHERE gt.type_name = '动感单车'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '音乐主题骑行', gt.id, '2026-07-21 19:30:00'::TIMESTAMP, '2026-07-21 20:15:00'::TIMESTAMP, 30, 20, '0', '单车房', '/static/course/spin_music.jpg', '以热门流行音乐为主题的骑行派对!每首歌曲对应一套骑行组合,跟着节拍加速、爬坡、冲刺,在音乐中忘记疲惫。', 68.00, 'system' +SELECT '音乐主题骑行', gt.id, '2026-07-23 19:30:00'::TIMESTAMP, '2026-07-23 20:15:00'::TIMESTAMP, 30, 20, '0', '单车房', '/static/course/spin_music.jpg', '以热门流行音乐为主题的骑行派对!每首歌曲对应一套骑行组合,跟着节拍加速、爬坡、冲刺,在音乐中忘记疲惫。', 68.00, 'system' FROM group_course_type gt WHERE gt.type_name = '动感单车'; -- ---------- HIIT训练课程(5个)---------- INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT 'Tabata燃脂', gt.id, '2026-07-20 07:00:00'::TIMESTAMP, '2026-07-20 07:30:00'::TIMESTAMP, 15, 8, '0', '多功能训练区', '/static/course/hiit_tabata.jpg', '经典的Tabata模式:20秒全力运动 + 10秒休息,循环8轮共4分钟,搭配热身和拉伸,30分钟全身燃爆。早晨空腹训练燃脂效果加倍!', 98.00, 'system' +SELECT 'Tabata燃脂', gt.id, '2026-07-22 07:00:00'::TIMESTAMP, '2026-07-22 07:30:00'::TIMESTAMP, 15, 8, '0', '多功能训练区', '/static/course/hiit_tabata.jpg', '经典的Tabata模式:20秒全力运动 + 10秒休息,循环8轮共4分钟,搭配热身和拉伸,30分钟全身燃爆。早晨空腹训练燃脂效果加倍!', 98.00, 'system' FROM group_course_type gt WHERE gt.type_name = 'HIIT训练'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '全身循环训练', gt.id, '2026-07-22 12:00:00'::TIMESTAMP, '2026-07-22 12:45:00'::TIMESTAMP, 15, 12, '0', '多功能训练区', '/static/course/hiit_circuit.jpg', '午间燃脂利器!8个动作站点轮流进行,涵盖深蹲跳、波比跳、壶铃摇摆、登山跑等,每个动作45秒,休息15秒,3轮循环让你全身湿透。', 98.00, 'system' +SELECT '全身循环训练', gt.id, '2026-07-24 12:00:00'::TIMESTAMP, '2026-07-24 12:45:00'::TIMESTAMP, 15, 12, '0', '多功能训练区', '/static/course/hiit_circuit.jpg', '午间燃脂利器!8个动作站点轮流进行,涵盖深蹲跳、波比跳、壶铃摇摆、登山跑等,每个动作45秒,休息15秒,3轮循环让你全身湿透。', 98.00, 'system' FROM group_course_type gt WHERE gt.type_name = 'HIIT训练'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '核心爆发力', gt.id, '2026-07-24 17:30:00'::TIMESTAMP, '2026-07-24 18:15:00'::TIMESTAMP, 12, 5, '0', '多功能训练区', '/static/course/hiit_core.jpg', '专注于核心肌群的HIIT训练,结合药球砸地、悬垂举腿、俄罗斯转体和平板支撑变式,打造钢铁般的核心力量,提升所有运动表现。', 108.00, 'system' +SELECT '核心爆发力', gt.id, '2026-07-26 17:30:00'::TIMESTAMP, '2026-07-26 18:15:00'::TIMESTAMP, 12, 5, '0', '多功能训练区', '/static/course/hiit_core.jpg', '专注于核心肌群的HIIT训练,结合药球砸地、悬垂举腿、俄罗斯转体和平板支撑变式,打造钢铁般的核心力量,提升所有运动表现。', 108.00, 'system' FROM group_course_type gt WHERE gt.type_name = 'HIIT训练'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '战绳挑战', gt.id, '2026-07-25 08:00:00'::TIMESTAMP, '2026-07-25 08:45:00'::TIMESTAMP, 10, 10, '0', '户外训练区', '/static/course/hiit_battle.jpg', '风靡全球的战绳训练!通过波浪、猛击、旋转等动作模式激活全身肌群,30秒一组高强度间歇,燃脂同时雕刻上肢线条。名额有限,已约满!', 128.00, 'system' +SELECT '战绳挑战', gt.id, '2026-07-27 08:00:00'::TIMESTAMP, '2026-07-27 08:45:00'::TIMESTAMP, 10, 10, '0', '户外训练区', '/static/course/hiit_battle.jpg', '风靡全球的战绳训练!通过波浪、猛击、旋转等动作模式激活全身肌群,30秒一组高强度间歇,燃脂同时雕刻上肢线条。名额有限,已约满!', 128.00, 'system' FROM group_course_type gt WHERE gt.type_name = 'HIIT训练'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '药球HIIT', gt.id, '2026-07-26 10:00:00'::TIMESTAMP, '2026-07-26 10:45:00'::TIMESTAMP, 12, 6, '0', '多功能训练区', '/static/course/hiit_medball.jpg', '以药球为主要工具的HIIT训练。药球砸地、旋转投掷、过头抛接等动作兼具力量和爆发力训练,动作多样不枯燥,周日上午的爆汗之选。', 108.00, 'system' +SELECT '药球HIIT', gt.id, '2026-07-28 10:00:00'::TIMESTAMP, '2026-07-28 10:45:00'::TIMESTAMP, 12, 6, '0', '多功能训练区', '/static/course/hiit_medball.jpg', '以药球为主要工具的HIIT训练。药球砸地、旋转投掷、过头抛接等动作兼具力量和爆发力训练,动作多样不枯燥,周日上午的爆汗之选。', 108.00, 'system' FROM group_course_type gt WHERE gt.type_name = 'HIIT训练'; -- ---------- 普拉提课程(5个)---------- INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '垫上普拉提', gt.id, '2026-07-21 10:00:00'::TIMESTAMP, '2026-07-21 11:00:00'::TIMESTAMP, 20, 14, '0', '普拉提室', '/static/course/pilates_mat.jpg', '经典垫上普拉提,通过精准的动作控制激活深层核心肌群。从呼吸到骨盆稳定,从脊柱逐节卷动到四肢协调,一节课改善你的身体感知。', 108.00, 'system' +SELECT '垫上普拉提', gt.id, '2026-07-23 10:00:00'::TIMESTAMP, '2026-07-23 11:00:00'::TIMESTAMP, 20, 14, '0', '普拉提室', '/static/course/pilates_mat.jpg', '经典垫上普拉提,通过精准的动作控制激活深层核心肌群。从呼吸到骨盆稳定,从脊柱逐节卷动到四肢协调,一节课改善你的身体感知。', 108.00, 'system' FROM group_course_type gt WHERE gt.type_name = '普拉提'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '器械普拉提', gt.id, '2026-07-23 09:00:00'::TIMESTAMP, '2026-07-23 10:00:00'::TIMESTAMP, 10, 8, '0', '普拉提室', '/static/course/pilates_reformer.jpg', '使用专业普拉提核心床(Reformer)进行训练,弹簧阻力提供精准的负荷控制,适合需要针对性改善体态、康复训练或追求高效塑形的学员。', 158.00, 'system' +SELECT '器械普拉提', gt.id, '2026-07-25 09:00:00'::TIMESTAMP, '2026-07-25 10:00:00'::TIMESTAMP, 10, 8, '0', '普拉提室', '/static/course/pilates_reformer.jpg', '使用专业普拉提核心床(Reformer)进行训练,弹簧阻力提供精准的负荷控制,适合需要针对性改善体态、康复训练或追求高效塑形的学员。', 158.00, 'system' FROM group_course_type gt WHERE gt.type_name = '普拉提'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '核心塑形', gt.id, '2026-07-25 14:00:00'::TIMESTAMP, '2026-07-25 15:00:00'::TIMESTAMP, 15, 9, '0', '普拉提室', '/static/course/pilates_sculpt.jpg', '专注于腹部、腰部和臀部的普拉提塑形课程。百次拍击、剪刀腿、肩桥等经典动作持续刺激目标肌群,打造紧致核心线条。', 128.00, 'system' +SELECT '核心塑形', gt.id, '2026-07-27 14:00:00'::TIMESTAMP, '2026-07-27 15:00:00'::TIMESTAMP, 15, 9, '0', '普拉提室', '/static/course/pilates_sculpt.jpg', '专注于腹部、腰部和臀部的普拉提塑形课程。百次拍击、剪刀腿、肩桥等经典动作持续刺激目标肌群,打造紧致核心线条。', 128.00, 'system' FROM group_course_type gt WHERE gt.type_name = '普拉提'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '脊柱健康', gt.id, '2026-07-20 16:00:00'::TIMESTAMP, '2026-07-20 17:00:00'::TIMESTAMP, 15, 6, '0', '普拉提室', '/static/course/pilates_spine.jpg', '专为久坐办公人群设计,通过普拉提脊柱逐节运动改善驼背、圆肩等不良体态。融合猫牛式、脊柱旋转和游泳式,给你的脊柱一次深度保养。', 108.00, 'system' +SELECT '脊柱健康', gt.id, '2026-07-22 16:00:00'::TIMESTAMP, '2026-07-22 17:00:00'::TIMESTAMP, 15, 6, '0', '普拉提室', '/static/course/pilates_spine.jpg', '专为久坐办公人群设计,通过普拉提脊柱逐节运动改善驼背、圆肩等不良体态。融合猫牛式、脊柱旋转和游泳式,给你的脊柱一次深度保养。', 108.00, 'system' FROM group_course_type gt WHERE gt.type_name = '普拉提'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '产后恢复普拉提', gt.id, '2026-07-22 11:00:00'::TIMESTAMP, '2026-07-22 12:00:00'::TIMESTAMP, 10, 4, '0', '普拉提室', '/static/course/pilates_postnatal.jpg', '专为产后妈妈设计的温和修复课程,重点激活盆底肌和腹横肌,循序渐进恢复核心力量。小班教学,教练一对一关注每位学员的姿势质量。', 158.00, 'system' +SELECT '产后恢复普拉提', gt.id, '2026-07-24 11:00:00'::TIMESTAMP, '2026-07-24 12:00:00'::TIMESTAMP, 10, 4, '0', '普拉提室', '/static/course/pilates_postnatal.jpg', '专为产后妈妈设计的温和修复课程,重点激活盆底肌和腹横肌,循序渐进恢复核心力量。小班教学,教练一对一关注每位学员的姿势质量。', 158.00, 'system' FROM group_course_type gt WHERE gt.type_name = '普拉提'; -- ---------- 搏击操课程(5个)---------- INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '有氧搏击基础', gt.id, '2026-07-21 18:00:00'::TIMESTAMP, '2026-07-21 19:00:00'::TIMESTAMP, 25, 20, '0', '操厅A', '/static/course/boxing_basic.jpg', '零基础友好的搏击操入门课。从直拳、摆拳、勾拳到前踢、侧踹,教练分步讲解每个动作的要领,配合动感音乐串联成完整的搏击组合。', 68.00, 'system' +SELECT '有氧搏击基础', gt.id, '2026-07-23 18:00:00'::TIMESTAMP, '2026-07-23 19:00:00'::TIMESTAMP, 25, 20, '0', '操厅A', '/static/course/boxing_basic.jpg', '零基础友好的搏击操入门课。从直拳、摆拳、勾拳到前踢、侧踹,教练分步讲解每个动作的要领,配合动感音乐串联成完整的搏击组合。', 68.00, 'system' FROM group_course_type gt WHERE gt.type_name = '搏击操'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT 'Kickboxing进阶', gt.id, '2026-07-23 19:00:00'::TIMESTAMP, '2026-07-23 20:00:00'::TIMESTAMP, 20, 15, '0', '操厅A', '/static/course/boxing_kickboxing.jpg', '融合踢拳技术的进阶搏击课程,增加连击组合、闪躲步法和膝肘技术。强度较基础课明显提升,汗如雨下的同时释放工作压力,宣泄效果一流。', 88.00, 'system' +SELECT 'Kickboxing进阶', gt.id, '2026-07-25 19:00:00'::TIMESTAMP, '2026-07-25 20:00:00'::TIMESTAMP, 20, 15, '0', '操厅A', '/static/course/boxing_kickboxing.jpg', '融合踢拳技术的进阶搏击课程,增加连击组合、闪躲步法和膝肘技术。强度较基础课明显提升,汗如雨下的同时释放工作压力,宣泄效果一流。', 88.00, 'system' FROM group_course_type gt WHERE gt.type_name = '搏击操'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '综合格斗体能', gt.id, '2026-07-25 16:00:00'::TIMESTAMP, '2026-07-25 17:00:00'::TIMESTAMP, 20, 12, '0', '操厅A', '/static/course/boxing_mma.jpg', '融合拳击、泰拳、摔跤元素的综合体能训练。沙袋击打、地面对抗和爆发力训练交替进行,全面提升力量、速度和耐力。周六下午的硬核之选!', 98.00, 'system' +SELECT '综合格斗体能', gt.id, '2026-07-27 16:00:00'::TIMESTAMP, '2026-07-27 17:00:00'::TIMESTAMP, 20, 12, '0', '操厅A', '/static/course/boxing_mma.jpg', '融合拳击、泰拳、摔跤元素的综合体能训练。沙袋击打、地面对抗和爆发力训练交替进行,全面提升力量、速度和耐力。周六下午的硬核之选!', 98.00, 'system' FROM group_course_type gt WHERE gt.type_name = '搏击操'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '燃脂拳击', gt.id, '2026-07-20 12:00:00'::TIMESTAMP, '2026-07-20 13:00:00'::TIMESTAMP, 25, 16, '0', '操厅A', '/static/course/boxing_fatburn.jpg', '午间燃脂拳击课。空击组合与沙袋击打间歇进行,心率维持在高燃脂区间。一节课可消耗500-700卡路里,中午来打拳比喝咖啡更提神!', 68.00, 'system' +SELECT '燃脂拳击', gt.id, '2026-07-22 12:00:00'::TIMESTAMP, '2026-07-22 13:00:00'::TIMESTAMP, 25, 16, '0', '操厅A', '/static/course/boxing_fatburn.jpg', '午间燃脂拳击课。空击组合与沙袋击打间歇进行,心率维持在高燃脂区间。一节课可消耗500-700卡路里,中午来打拳比喝咖啡更提神!', 68.00, 'system' FROM group_course_type gt WHERE gt.type_name = '搏击操'; INSERT INTO group_course (course_name, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, stored_value_amount, create_by) -SELECT '泰拳基础', gt.id, '2026-07-26 15:00:00'::TIMESTAMP, '2026-07-26 16:00:00'::TIMESTAMP, 15, 7, '0', '操厅A', '/static/course/boxing_muaythai.jpg', '学习泰拳的八肢艺术(双拳、双腿、双膝、双肘),从基本站架到肘膝组合,感受泰拳的刚猛魅力。小班教学保证动作纠正质量。', 98.00, 'system' +SELECT '泰拳基础', gt.id, '2026-07-28 15:00:00'::TIMESTAMP, '2026-07-28 16:00:00'::TIMESTAMP, 15, 7, '0', '操厅A', '/static/course/boxing_muaythai.jpg', '学习泰拳的八肢艺术(双拳、双腿、双膝、双肘),从基本站架到肘膝组合,感受泰拳的刚猛魅力。小班教学保证动作纠正质量。', 98.00, 'system' FROM group_course_type gt WHERE gt.type_name = '搏击操'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V23__Create_coach_violation.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V23__Create_coach_violation.sql new file mode 100644 index 0000000..ce36fd7 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V23__Create_coach_violation.sql @@ -0,0 +1,30 @@ +-- ============================================ +-- 教练违规记录表 +-- 版本: V23 +-- 描述: 创建教练违规记录表 +-- ============================================ + +CREATE TABLE IF NOT EXISTS coach_violation ( + id BIGSERIAL PRIMARY KEY, + coach_id BIGINT NOT NULL, + course_id BIGINT NOT NULL, + violation_time TIMESTAMP NOT NULL, + violation_reason VARCHAR(50) NOT NULL, + create_by VARCHAR(50), + update_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +COMMENT ON TABLE coach_violation IS '教练违规记录表'; +COMMENT ON COLUMN coach_violation.id IS '主键ID'; +COMMENT ON COLUMN coach_violation.coach_id IS '教练ID(关联sys_user)'; +COMMENT ON COLUMN coach_violation.course_id IS '团课ID(关联group_course)'; +COMMENT ON COLUMN coach_violation.violation_time IS '违规时间'; +COMMENT ON COLUMN coach_violation.violation_reason IS '违规原因(COACH_LATE-教练迟到, COACH_ABSENT-教练缺席, NOT_MANUAL_END-教练未手动结课)'; +COMMENT ON COLUMN coach_violation.create_by IS '创建人'; +COMMENT ON COLUMN coach_violation.update_by IS '更新人'; +COMMENT ON COLUMN coach_violation.created_at IS '创建时间'; +COMMENT ON COLUMN coach_violation.updated_at IS '更新时间'; +COMMENT ON COLUMN coach_violation.deleted_at IS '删除时间(软删除)'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V24__Insert_coach_data.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V24__Insert_coach_data.sql new file mode 100644 index 0000000..8895ea9 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V24__Insert_coach_data.sql @@ -0,0 +1,82 @@ +-- ============================================ +-- V24: 教练测试数据 +-- 描述: 创建5个教练用户并分配团课,每教练负责一种课型 +-- 确保同教练的课程时间不冲突 +-- ============================================ + +-- ============================================ +-- 1. 教练用户数据(密码均为 Test@123) +-- ============================================ +INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by, created_at, updated_at) +VALUES +(11, 'coach_zhang', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'coach_zhang@novalon.com', '13800138011', '张教练(瑜伽)', 1, 'system', 'system', NOW(), NOW()), +(12, 'coach_li', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'coach_li@novalon.com', '13800138012', '李教练(动感单车)', 1, 'system', 'system', NOW(), NOW()), +(13, 'coach_wang', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'coach_wang@novalon.com', '13800138013', '王教练(HIIT)', 1, 'system', 'system', NOW(), NOW()), +(14, 'coach_zhao', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'coach_zhao@novalon.com', '13800138014', '赵教练(普拉提)', 1, 'system', 'system', NOW(), NOW()), +(15, 'coach_chen', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'coach_chen@novalon.com', '13800138015', '陈教练(搏击操)', 1, 'system', 'system', NOW(), NOW()) +ON CONFLICT (username) DO UPDATE SET + password = EXCLUDED.password, + status = EXCLUDED.status; + +SELECT setval('sys_user_id_seq', 15); + +-- ============================================ +-- 2. 分配教练角色(role_id=5) +-- ============================================ +INSERT INTO user_role (user_id, role_id, created_by, created_at) +VALUES +(11, 5, 'system', NOW()), +(12, 5, 'system', NOW()), +(13, 5, 'system', NOW()), +(14, 5, 'system', NOW()), +(15, 5, 'system', NOW()) +ON CONFLICT (user_id, role_id) DO NOTHING; + +-- ============================================ +-- 3. 为团课分配教练(按课型,无时间冲突) +-- coach_id=11 张教练: 瑜伽 5门 +-- coach_id=12 李教练: 动感单车 5门 +-- coach_id=13 王教练: HIIT训练 5门 +-- coach_id=14 赵教练: 普拉提 5门 +-- coach_id=15 陈教练: 搏击操 5门 +-- ============================================ + +-- 瑜伽课程 → 张教练(11) +UPDATE group_course SET coach_id = 11, updated_at = NOW() +WHERE course_type = (SELECT id FROM group_course_type WHERE type_name = '瑜伽') + AND deleted_at IS NULL; + +-- 动感单车课程 → 李教练(12) +UPDATE group_course SET coach_id = 12, updated_at = NOW() +WHERE course_type = (SELECT id FROM group_course_type WHERE type_name = '动感单车') + AND deleted_at IS NULL; + +-- HIIT训练课程 → 王教练(13) +UPDATE group_course SET coach_id = 13, updated_at = NOW() +WHERE course_type = (SELECT id FROM group_course_type WHERE type_name = 'HIIT训练') + AND deleted_at IS NULL; + +-- 普拉提课程 → 赵教练(14) +UPDATE group_course SET coach_id = 14, updated_at = NOW() +WHERE course_type = (SELECT id FROM group_course_type WHERE type_name = '普拉提') + AND deleted_at IS NULL; + +-- 搏击操课程 → 陈教练(15) +UPDATE group_course SET coach_id = 15, updated_at = NOW() +WHERE course_type = (SELECT id FROM group_course_type WHERE type_name = '搏击操') + AND deleted_at IS NULL; + +-- ============================================ +-- 4. 验证:确认每个团课都有教练 +-- ============================================ +DO $$ +DECLARE + null_count INTEGER; +BEGIN + SELECT COUNT(*) INTO null_count FROM group_course WHERE coach_id IS NULL AND deleted_at IS NULL; + IF null_count > 0 THEN + RAISE NOTICE '警告:仍有 % 门团课缺少教练', null_count; + ELSE + RAISE NOTICE '所有团课已成功分配教练'; + END IF; +END $$; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_group_course_tables.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_group_course_tables.sql index a456b63..d56b99c 100644 --- a/gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_group_course_tables.sql +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V5__Create_group_course_tables.sql @@ -53,7 +53,7 @@ COMMENT ON COLUMN group_course.start_time IS '开始时间'; COMMENT ON COLUMN group_course.end_time IS '结束时间'; COMMENT ON COLUMN group_course.max_members IS '最大参与人数'; COMMENT ON COLUMN group_course.current_members IS '当前参与人数'; -COMMENT ON COLUMN group_course.status IS '状态(0正常 1已取消 2已结束 3进行中 4超时)'; +COMMENT ON COLUMN group_course.status IS '状态(0正常 1已取消 2已结束 3进行中 4超时 5教练缺席 6自动结束 7教练迟到)'; COMMENT ON COLUMN group_course.actual_start_time IS '实际开课时间(教练点击开课时记录)'; COMMENT ON COLUMN group_course.actual_end_time IS '实际结课时间(教练点击结课时记录)'; COMMENT ON COLUMN group_course.location IS '上课地点'; @@ -72,7 +72,7 @@ COMMENT ON COLUMN group_course_booking.course_id IS '团课ID'; COMMENT ON COLUMN group_course_booking.member_id IS '用户ID'; COMMENT ON COLUMN group_course_booking.member_card_id IS '会员卡ID'; COMMENT ON COLUMN group_course_booking.booking_time IS '预约时间'; -COMMENT ON COLUMN group_course_booking.status IS '状态(0已预约 1已取消 2已出席 3缺席)'; +COMMENT ON COLUMN group_course_booking.status IS '状态(0已预约 1已取消 2已出席 3缺席 4教练缺席 5迟到)'; COMMENT ON COLUMN group_course_booking.cancel_time IS '取消时间'; COMMENT ON COLUMN group_course_booking.create_by IS '创建人'; COMMENT ON COLUMN group_course_booking.update_by IS '更新人'; diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java index 930814e..b52b481 100644 --- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java +++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java @@ -66,7 +66,8 @@ public class SecurityConfig { .pathMatchers("/api/payment/query").permitAll() .pathMatchers("/api/payment/notify").permitAll() .pathMatchers("/api/payment/refund").permitAll() - .pathMatchers("/api/files/**").permitAll(); + .pathMatchers("/api/files/**").permitAll() + .pathMatchers("/api/coach/**").permitAll(); if (isDevOrTest) { spec.pathMatchers("/swagger-ui.html").permitAll() diff --git a/gym-manage-api/pom.xml b/gym-manage-api/pom.xml index 385258e..9df6ca5 100644 --- a/gym-manage-api/pom.xml +++ b/gym-manage-api/pom.xml @@ -48,6 +48,7 @@ gym-dataCount gym-auth gym-payment + gym-coach diff --git a/gym-manage-coach-uniapp/.gitignore b/gym-manage-coach-uniapp/.gitignore new file mode 100644 index 0000000..2370852 --- /dev/null +++ b/gym-manage-coach-uniapp/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +unpackage/ +.hbuilderx/ +.DS_Store diff --git a/gym-manage-coach-uniapp/App.vue b/gym-manage-coach-uniapp/App.vue new file mode 100644 index 0000000..64dae1b --- /dev/null +++ b/gym-manage-coach-uniapp/App.vue @@ -0,0 +1,25 @@ + + + diff --git a/gym-manage-coach-uniapp/api/coach.js b/gym-manage-coach-uniapp/api/coach.js new file mode 100644 index 0000000..aea4423 --- /dev/null +++ b/gym-manage-coach-uniapp/api/coach.js @@ -0,0 +1,52 @@ +const http = require('../utils/request') + +/** + * 教练登录(账号密码) + */ +function login(username, password) { + return http.post('/auth/login', { username, password }) +} + +/** + * 获取教练负责的团课列表 + */ +function getCoachCourses(coachId) { + return http.get('/coach/' + coachId + '/courses') +} + +/** + * 教练手动开课 + */ +function startCourse(courseId) { + return http.post('/coach/courses/' + courseId + '/start') +} + +/** + * 教练手动结课 + */ +function endCourse(courseId) { + return http.post('/coach/courses/' + courseId + '/end') +} + +/** + * 获取团课详情 + */ +function getCourseDetail(courseId) { + return http.get('/groupCourse/' + courseId + '/detail') +} + +/** + * 获取教练违规记录 + */ +function getCoachViolations(coachId) { + return http.get('/coach/' + coachId + '/violations') +} + +module.exports = { + login, + getCoachCourses, + startCourse, + endCourse, + getCourseDetail, + getCoachViolations +} diff --git a/gym-manage-coach-uniapp/index.html b/gym-manage-coach-uniapp/index.html new file mode 100644 index 0000000..b5d330d --- /dev/null +++ b/gym-manage-coach-uniapp/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ + + diff --git a/gym-manage-coach-uniapp/main.js b/gym-manage-coach-uniapp/main.js new file mode 100644 index 0000000..20ad8f1 --- /dev/null +++ b/gym-manage-coach-uniapp/main.js @@ -0,0 +1,22 @@ +import App from './App' + +// #ifndef VUE3 +import Vue from 'vue' +import './uni.promisify.adaptor' +Vue.config.productionTip = false +App.mpType = 'app' +const app = new Vue({ + ...App +}) +app.$mount() +// #endif + +// #ifdef VUE3 +import { createSSRApp } from 'vue' +export function createApp() { + const app = createSSRApp(App) + return { + app + } +} +// #endif diff --git a/gym-manage-coach-uniapp/manifest.json b/gym-manage-coach-uniapp/manifest.json new file mode 100644 index 0000000..d3a2b2a --- /dev/null +++ b/gym-manage-coach-uniapp/manifest.json @@ -0,0 +1,64 @@ +{ + "name" : "gym-manage-coach-uniapp", + "appid" : "", + "description" : "", + "versionName" : "1.0.0", + "versionCode" : "100", + "transformPx" : false, + "app-plus" : { + "usingComponents" : true, + "nvueStyleCompiler" : "uni-app", + "compilerVersion" : 3, + "splashscreen" : { + "alwaysShowBeforeRender" : true, + "waiting" : true, + "autoclose" : true, + "delay" : 0 + }, + "modules" : {}, + "distribute" : { + "android" : { + "permissions" : [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + "ios" : {}, + "sdkConfigs" : {} + } + }, + "quickapp" : {}, + "mp-weixin" : { + "appid" : "", + "setting" : { + "urlCheck" : false + }, + "usingComponents" : true + }, + "mp-alipay" : { + "usingComponents" : true + }, + "mp-baidu" : { + "usingComponents" : true + }, + "mp-toutiao" : { + "usingComponents" : true + }, + "uniStatistics" : { + "enable" : false + }, + "vueVersion" : "2" +} diff --git a/gym-manage-coach-uniapp/package-lock.json b/gym-manage-coach-uniapp/package-lock.json new file mode 100644 index 0000000..cb205bb --- /dev/null +++ b/gym-manage-coach-uniapp/package-lock.json @@ -0,0 +1,45 @@ +{ + "name": "gym-manage-coach-uniapp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gym-manage-coach-uniapp", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@dcloudio/uni-ui": "^1.5.12", + "crypto-js": "^4.2.0", + "luch-request": "^3.1.1" + } + }, + "node_modules/@dcloudio/types": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@dcloudio/types/-/types-2.6.12.tgz", + "integrity": "sha512-mrCMwcINy1IFjU9VUqLeWBkj404yWs5paLDttBcA+eqUjanuUQbBcTVPqlrGgkyzLXDcV2oDDZRSNxNpXi4kMQ==", + "license": "Apache-2.0" + }, + "node_modules/@dcloudio/uni-ui": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@dcloudio/uni-ui/-/uni-ui-1.5.12.tgz", + "integrity": "sha512-mGDl2OZSz7D8xcUAzJegWDHOqB4MEFBSW9Esb/oJiu2/3Gk9+P/Z4bA4JZ9jv9VWBYbMrYwaTfK1Z728kABdYg==", + "license": "Apache-2.0" + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/luch-request": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/luch-request/-/luch-request-3.1.1.tgz", + "integrity": "sha512-p7+mlcEtgRcd0OfXC4XZbyiwSr1XgCeqNT7LlVUjnk7InYl/8d5Rk7BUqAYNA2WRafI1wRIUQWRWZRpeUwWR0w==", + "license": "MIT", + "dependencies": { + "@dcloudio/types": "^2.0.16" + } + } + } +} diff --git a/gym-manage-coach-uniapp/package.json b/gym-manage-coach-uniapp/package.json new file mode 100644 index 0000000..4bef173 --- /dev/null +++ b/gym-manage-coach-uniapp/package.json @@ -0,0 +1,18 @@ +{ + "name": "gym-manage-coach-uniapp", + "version": "1.0.0", + "description": "", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@dcloudio/uni-ui": "^1.5.12", + "crypto-js": "^4.2.0", + "luch-request": "^3.1.1" + } +} diff --git a/gym-manage-coach-uniapp/pages.json b/gym-manage-coach-uniapp/pages.json new file mode 100644 index 0000000..468189a --- /dev/null +++ b/gym-manage-coach-uniapp/pages.json @@ -0,0 +1,48 @@ +{ + "pages": [ + { + "path": "pages/login/login", + "style": { + "navigationBarTitleText": "教练登录", + "navigationStyle": "custom" + } + }, + { + "path": "pages/index/index", + "style": { + "navigationBarTitleText": "教练首页", + "navigationStyle": "custom", + "enablePullDownRefresh": true + } + }, + { + "path": "pages/course-list/course-list", + "style": { + "navigationBarTitleText": "我的团课", + "navigationStyle": "custom", + "enablePullDownRefresh": true + } + }, + { + "path": "pages/course-detail/course-detail", + "style": { + "navigationBarTitleText": "课程详情", + "navigationStyle": "custom" + } + }, + { + "path": "pages/profile/profile", + "style": { + "navigationBarTitleText": "个人中心", + "navigationStyle": "custom" + } + } + ], + "globalStyle": { + "navigationBarTextStyle": "white", + "navigationBarTitleText": "Novalon教练端", + "navigationBarBackgroundColor": "#1A1A1A", + "backgroundColor": "#F5F7FA" + }, + "uniIdRouter": {} +} diff --git a/gym-manage-coach-uniapp/pages/course-detail/course-detail.vue b/gym-manage-coach-uniapp/pages/course-detail/course-detail.vue new file mode 100644 index 0000000..1a4ae1b --- /dev/null +++ b/gym-manage-coach-uniapp/pages/course-detail/course-detail.vue @@ -0,0 +1,428 @@ + + + + + diff --git a/gym-manage-coach-uniapp/pages/course-list/course-list.vue b/gym-manage-coach-uniapp/pages/course-list/course-list.vue new file mode 100644 index 0000000..c347003 --- /dev/null +++ b/gym-manage-coach-uniapp/pages/course-list/course-list.vue @@ -0,0 +1,292 @@ + + + + + diff --git a/gym-manage-coach-uniapp/pages/index/index.vue b/gym-manage-coach-uniapp/pages/index/index.vue new file mode 100644 index 0000000..b893864 --- /dev/null +++ b/gym-manage-coach-uniapp/pages/index/index.vue @@ -0,0 +1,282 @@ + + + + + diff --git a/gym-manage-coach-uniapp/pages/login/login.vue b/gym-manage-coach-uniapp/pages/login/login.vue new file mode 100644 index 0000000..0e7b9dc --- /dev/null +++ b/gym-manage-coach-uniapp/pages/login/login.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/gym-manage-coach-uniapp/pages/profile/profile.vue b/gym-manage-coach-uniapp/pages/profile/profile.vue new file mode 100644 index 0000000..e21d449 --- /dev/null +++ b/gym-manage-coach-uniapp/pages/profile/profile.vue @@ -0,0 +1,347 @@ + + + + + diff --git a/gym-manage-coach-uniapp/project.config.json b/gym-manage-coach-uniapp/project.config.json new file mode 100644 index 0000000..759ba4e --- /dev/null +++ b/gym-manage-coach-uniapp/project.config.json @@ -0,0 +1,25 @@ +{ + "setting": { + "es6": true, + "postcss": true, + "minified": true, + "uglifyFileName": false, + "enhance": true, + "packNpmRelationList": [], + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "useCompilerPlugins": false, + "minifyWXML": true + }, + "compileType": "miniprogram", + "simulatorPluginLibVersion": {}, + "packOptions": { + "ignore": [], + "include": [] + }, + "appid": "wxe734af0ea3f96437", + "editorSetting": {} +} \ No newline at end of file diff --git a/gym-manage-coach-uniapp/project.private.config.json b/gym-manage-coach-uniapp/project.private.config.json new file mode 100644 index 0000000..214d71a --- /dev/null +++ b/gym-manage-coach-uniapp/project.private.config.json @@ -0,0 +1,14 @@ +{ + "libVersion": "3.17.0", + "projectname": "gym-manage-coach-uniapp", + "setting": { + "urlCheck": true, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "showShadowRootInWxmlPanel": true, + "compileHotReLoad": true + } +} \ No newline at end of file diff --git a/gym-manage-coach-uniapp/static/logo.png b/gym-manage-coach-uniapp/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b5771e209bb677e2ebd5ff766ad5ee11790f305a GIT binary patch literal 4023 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 SYs}WS&%*SCNx89m_ { + res.then((res) => { + if (!res) return resolve(res) + return res[0] ? reject(res[0]) : resolve(res[1]) + }); + }); + }, +}); \ No newline at end of file diff --git a/gym-manage-coach-uniapp/uni.scss b/gym-manage-coach-uniapp/uni.scss new file mode 100644 index 0000000..b9249e9 --- /dev/null +++ b/gym-manage-coach-uniapp/uni.scss @@ -0,0 +1,76 @@ +/** + * 这里是uni-app内置的常用样式变量 + * + * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 + * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App + * + */ + +/** + * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 + * + * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 + */ + +/* 颜色变量 */ + +/* 行为相关颜色 */ +$uni-color-primary: #007aff; +$uni-color-success: #4cd964; +$uni-color-warning: #f0ad4e; +$uni-color-error: #dd524d; + +/* 文字基本颜色 */ +$uni-text-color:#333;//基本色 +$uni-text-color-inverse:#fff;//反色 +$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 +$uni-text-color-placeholder: #808080; +$uni-text-color-disable:#c0c0c0; + +/* 背景颜色 */ +$uni-bg-color:#ffffff; +$uni-bg-color-grey:#f8f8f8; +$uni-bg-color-hover:#f1f1f1;//点击状态颜色 +$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 + +/* 边框颜色 */ +$uni-border-color:#c8c7cc; + +/* 尺寸变量 */ + +/* 文字尺寸 */ +$uni-font-size-sm:12px; +$uni-font-size-base:14px; +$uni-font-size-lg:16px; + +/* 图片尺寸 */ +$uni-img-size-sm:20px; +$uni-img-size-base:26px; +$uni-img-size-lg:40px; + +/* Border Radius */ +$uni-border-radius-sm: 2px; +$uni-border-radius-base: 3px; +$uni-border-radius-lg: 6px; +$uni-border-radius-circle: 50%; + +/* 水平间距 */ +$uni-spacing-row-sm: 5px; +$uni-spacing-row-base: 10px; +$uni-spacing-row-lg: 15px; + +/* 垂直间距 */ +$uni-spacing-col-sm: 4px; +$uni-spacing-col-base: 8px; +$uni-spacing-col-lg: 12px; + +/* 透明度 */ +$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 + +/* 文章场景相关 */ +$uni-color-title: #2C405A; // 文章标题颜色 +$uni-font-size-title:20px; +$uni-color-subtitle: #555555; // 二级标题颜色 +$uni-font-size-subtitle:26px; +$uni-color-paragraph: #3F536E; // 文章段落颜色 +$uni-font-size-paragraph:15px; diff --git a/gym-manage-coach-uniapp/uni_modules/uni-icons/changelog.md b/gym-manage-coach-uniapp/uni_modules/uni-icons/changelog.md new file mode 100644 index 0000000..62e7682 --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-icons/changelog.md @@ -0,0 +1,44 @@ +## 2.0.12(2025-08-26) +- 优化 uni-app x 下 size 类型问题 +## 2.0.11(2025-08-18) +- 修复 图标点击事件返回 +## 2.0.9(2024-01-12) +fix: 修复图标大小默认值错误的问题 +## 2.0.8(2023-12-14) +- 修复 项目未使用 ts 情况下,打包报错的bug +## 2.0.7(2023-12-14) +- 修复 size 属性为 string 时,不加单位导致尺寸异常的bug +## 2.0.6(2023-12-11) +- 优化 兼容老版本icon类型,如 top ,bottom 等 +## 2.0.5(2023-12-11) +- 优化 兼容老版本icon类型,如 top ,bottom 等 +## 2.0.4(2023-12-06) +- 优化 uni-app x 下示例项目图标排序 +## 2.0.3(2023-12-06) +- 修复 nvue下引入组件报错的bug +## 2.0.2(2023-12-05) +-优化 size 属性支持单位 +## 2.0.1(2023-12-05) +- 新增 uni-app x 支持定义图标 +## 1.3.5(2022-01-24) +- 优化 size 属性可以传入不带单位的字符串数值 +## 1.3.4(2022-01-24) +- 优化 size 支持其他单位 +## 1.3.3(2022-01-17) +- 修复 nvue 有些图标不显示的bug,兼容老版本图标 +## 1.3.2(2021-12-01) +- 优化 示例可复制图标名称 +## 1.3.1(2021-11-23) +- 优化 兼容旧组件 type 值 +## 1.3.0(2021-11-19) +- 新增 更多图标 +- 优化 自定义图标使用方式 +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-icons](https://uniapp.dcloud.io/component/uniui/uni-icons) +## 1.1.7(2021-11-08) +## 1.2.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.5(2021-05-12) +- 新增 组件示例地址 +## 1.1.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.uvue b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.uvue new file mode 100644 index 0000000..53eb2ea --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.uvue @@ -0,0 +1,91 @@ + + + + + diff --git a/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.vue b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.vue new file mode 100644 index 0000000..1bd3d5e --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.vue @@ -0,0 +1,110 @@ + + + + + \ No newline at end of file diff --git a/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.css b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.css new file mode 100644 index 0000000..0a6b6fe --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.css @@ -0,0 +1,664 @@ + +.uniui-cart-filled:before { + content: "\e6d0"; +} + +.uniui-gift-filled:before { + content: "\e6c4"; +} + +.uniui-color:before { + content: "\e6cf"; +} + +.uniui-wallet:before { + content: "\e6b1"; +} + +.uniui-settings-filled:before { + content: "\e6ce"; +} + +.uniui-auth-filled:before { + content: "\e6cc"; +} + +.uniui-shop-filled:before { + content: "\e6cd"; +} + +.uniui-staff-filled:before { + content: "\e6cb"; +} + +.uniui-vip-filled:before { + content: "\e6c6"; +} + +.uniui-plus-filled:before { + content: "\e6c7"; +} + +.uniui-folder-add-filled:before { + content: "\e6c8"; +} + +.uniui-color-filled:before { + content: "\e6c9"; +} + +.uniui-tune-filled:before { + content: "\e6ca"; +} + +.uniui-calendar-filled:before { + content: "\e6c0"; +} + +.uniui-notification-filled:before { + content: "\e6c1"; +} + +.uniui-wallet-filled:before { + content: "\e6c2"; +} + +.uniui-medal-filled:before { + content: "\e6c3"; +} + +.uniui-fire-filled:before { + content: "\e6c5"; +} + +.uniui-refreshempty:before { + content: "\e6bf"; +} + +.uniui-location-filled:before { + content: "\e6af"; +} + +.uniui-person-filled:before { + content: "\e69d"; +} + +.uniui-personadd-filled:before { + content: "\e698"; +} + +.uniui-arrowthinleft:before { + content: "\e6d2"; +} + +.uniui-arrowthinup:before { + content: "\e6d3"; +} + +.uniui-arrowthindown:before { + content: "\e6d4"; +} + +.uniui-back:before { + content: "\e6b9"; +} + +.uniui-forward:before { + content: "\e6ba"; +} + +.uniui-arrow-right:before { + content: "\e6bb"; +} + +.uniui-arrow-left:before { + content: "\e6bc"; +} + +.uniui-arrow-up:before { + content: "\e6bd"; +} + +.uniui-arrow-down:before { + content: "\e6be"; +} + +.uniui-arrowthinright:before { + content: "\e6d1"; +} + +.uniui-down:before { + content: "\e6b8"; +} + +.uniui-bottom:before { + content: "\e6b8"; +} + +.uniui-arrowright:before { + content: "\e6d5"; +} + +.uniui-right:before { + content: "\e6b5"; +} + +.uniui-up:before { + content: "\e6b6"; +} + +.uniui-top:before { + content: "\e6b6"; +} + +.uniui-left:before { + content: "\e6b7"; +} + +.uniui-arrowup:before { + content: "\e6d6"; +} + +.uniui-eye:before { + content: "\e651"; +} + +.uniui-eye-filled:before { + content: "\e66a"; +} + +.uniui-eye-slash:before { + content: "\e6b3"; +} + +.uniui-eye-slash-filled:before { + content: "\e6b4"; +} + +.uniui-info-filled:before { + content: "\e649"; +} + +.uniui-reload:before { + content: "\e6b2"; +} + +.uniui-micoff-filled:before { + content: "\e6b0"; +} + +.uniui-map-pin-ellipse:before { + content: "\e6ac"; +} + +.uniui-map-pin:before { + content: "\e6ad"; +} + +.uniui-location:before { + content: "\e6ae"; +} + +.uniui-starhalf:before { + content: "\e683"; +} + +.uniui-star:before { + content: "\e688"; +} + +.uniui-star-filled:before { + content: "\e68f"; +} + +.uniui-calendar:before { + content: "\e6a0"; +} + +.uniui-fire:before { + content: "\e6a1"; +} + +.uniui-medal:before { + content: "\e6a2"; +} + +.uniui-font:before { + content: "\e6a3"; +} + +.uniui-gift:before { + content: "\e6a4"; +} + +.uniui-link:before { + content: "\e6a5"; +} + +.uniui-notification:before { + content: "\e6a6"; +} + +.uniui-staff:before { + content: "\e6a7"; +} + +.uniui-vip:before { + content: "\e6a8"; +} + +.uniui-folder-add:before { + content: "\e6a9"; +} + +.uniui-tune:before { + content: "\e6aa"; +} + +.uniui-auth:before { + content: "\e6ab"; +} + +.uniui-person:before { + content: "\e699"; +} + +.uniui-email-filled:before { + content: "\e69a"; +} + +.uniui-phone-filled:before { + content: "\e69b"; +} + +.uniui-phone:before { + content: "\e69c"; +} + +.uniui-email:before { + content: "\e69e"; +} + +.uniui-personadd:before { + content: "\e69f"; +} + +.uniui-chatboxes-filled:before { + content: "\e692"; +} + +.uniui-contact:before { + content: "\e693"; +} + +.uniui-chatbubble-filled:before { + content: "\e694"; +} + +.uniui-contact-filled:before { + content: "\e695"; +} + +.uniui-chatboxes:before { + content: "\e696"; +} + +.uniui-chatbubble:before { + content: "\e697"; +} + +.uniui-upload-filled:before { + content: "\e68e"; +} + +.uniui-upload:before { + content: "\e690"; +} + +.uniui-weixin:before { + content: "\e691"; +} + +.uniui-compose:before { + content: "\e67f"; +} + +.uniui-qq:before { + content: "\e680"; +} + +.uniui-download-filled:before { + content: "\e681"; +} + +.uniui-pyq:before { + content: "\e682"; +} + +.uniui-sound:before { + content: "\e684"; +} + +.uniui-trash-filled:before { + content: "\e685"; +} + +.uniui-sound-filled:before { + content: "\e686"; +} + +.uniui-trash:before { + content: "\e687"; +} + +.uniui-videocam-filled:before { + content: "\e689"; +} + +.uniui-spinner-cycle:before { + content: "\e68a"; +} + +.uniui-weibo:before { + content: "\e68b"; +} + +.uniui-videocam:before { + content: "\e68c"; +} + +.uniui-download:before { + content: "\e68d"; +} + +.uniui-help:before { + content: "\e679"; +} + +.uniui-navigate-filled:before { + content: "\e67a"; +} + +.uniui-plusempty:before { + content: "\e67b"; +} + +.uniui-smallcircle:before { + content: "\e67c"; +} + +.uniui-minus-filled:before { + content: "\e67d"; +} + +.uniui-micoff:before { + content: "\e67e"; +} + +.uniui-closeempty:before { + content: "\e66c"; +} + +.uniui-clear:before { + content: "\e66d"; +} + +.uniui-navigate:before { + content: "\e66e"; +} + +.uniui-minus:before { + content: "\e66f"; +} + +.uniui-image:before { + content: "\e670"; +} + +.uniui-mic:before { + content: "\e671"; +} + +.uniui-paperplane:before { + content: "\e672"; +} + +.uniui-close:before { + content: "\e673"; +} + +.uniui-help-filled:before { + content: "\e674"; +} + +.uniui-paperplane-filled:before { + content: "\e675"; +} + +.uniui-plus:before { + content: "\e676"; +} + +.uniui-mic-filled:before { + content: "\e677"; +} + +.uniui-image-filled:before { + content: "\e678"; +} + +.uniui-locked-filled:before { + content: "\e668"; +} + +.uniui-info:before { + content: "\e669"; +} + +.uniui-locked:before { + content: "\e66b"; +} + +.uniui-camera-filled:before { + content: "\e658"; +} + +.uniui-chat-filled:before { + content: "\e659"; +} + +.uniui-camera:before { + content: "\e65a"; +} + +.uniui-circle:before { + content: "\e65b"; +} + +.uniui-checkmarkempty:before { + content: "\e65c"; +} + +.uniui-chat:before { + content: "\e65d"; +} + +.uniui-circle-filled:before { + content: "\e65e"; +} + +.uniui-flag:before { + content: "\e65f"; +} + +.uniui-flag-filled:before { + content: "\e660"; +} + +.uniui-gear-filled:before { + content: "\e661"; +} + +.uniui-home:before { + content: "\e662"; +} + +.uniui-home-filled:before { + content: "\e663"; +} + +.uniui-gear:before { + content: "\e664"; +} + +.uniui-smallcircle-filled:before { + content: "\e665"; +} + +.uniui-map-filled:before { + content: "\e666"; +} + +.uniui-map:before { + content: "\e667"; +} + +.uniui-refresh-filled:before { + content: "\e656"; +} + +.uniui-refresh:before { + content: "\e657"; +} + +.uniui-cloud-upload:before { + content: "\e645"; +} + +.uniui-cloud-download-filled:before { + content: "\e646"; +} + +.uniui-cloud-download:before { + content: "\e647"; +} + +.uniui-cloud-upload-filled:before { + content: "\e648"; +} + +.uniui-redo:before { + content: "\e64a"; +} + +.uniui-images-filled:before { + content: "\e64b"; +} + +.uniui-undo-filled:before { + content: "\e64c"; +} + +.uniui-more:before { + content: "\e64d"; +} + +.uniui-more-filled:before { + content: "\e64e"; +} + +.uniui-undo:before { + content: "\e64f"; +} + +.uniui-images:before { + content: "\e650"; +} + +.uniui-paperclip:before { + content: "\e652"; +} + +.uniui-settings:before { + content: "\e653"; +} + +.uniui-search:before { + content: "\e654"; +} + +.uniui-redo-filled:before { + content: "\e655"; +} + +.uniui-list:before { + content: "\e644"; +} + +.uniui-mail-open-filled:before { + content: "\e63a"; +} + +.uniui-hand-down-filled:before { + content: "\e63c"; +} + +.uniui-hand-down:before { + content: "\e63d"; +} + +.uniui-hand-up-filled:before { + content: "\e63e"; +} + +.uniui-hand-up:before { + content: "\e63f"; +} + +.uniui-heart-filled:before { + content: "\e641"; +} + +.uniui-mail-open:before { + content: "\e643"; +} + +.uniui-heart:before { + content: "\e639"; +} + +.uniui-loop:before { + content: "\e633"; +} + +.uniui-pulldown:before { + content: "\e632"; +} + +.uniui-scan:before { + content: "\e62a"; +} + +.uniui-bars:before { + content: "\e627"; +} + +.uniui-checkbox:before { + content: "\e62b"; +} + +.uniui-checkbox-filled:before { + content: "\e62c"; +} + +.uniui-shop:before { + content: "\e62f"; +} + +.uniui-headphones:before { + content: "\e630"; +} + +.uniui-cart:before { + content: "\e631"; +} diff --git a/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.ttf b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..14696d038d828073edac09ea4e5ba1dec2f58115 GIT binary patch literal 35824 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 F{2S@rCxHL} literal 0 HcmV?d00001 diff --git a/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons_file.ts b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons_file.ts new file mode 100644 index 0000000..98e93aa --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons_file.ts @@ -0,0 +1,664 @@ + +export type IconsData = { + id : string + name : string + font_family : string + css_prefix_text : string + description : string + glyphs : Array +} + +export type IconsDataItem = { + font_class : string + unicode : string +} + + +export const fontData = [ + { + "font_class": "arrow-down", + "unicode": "\ue6be" + }, + { + "font_class": "arrow-left", + "unicode": "\ue6bc" + }, + { + "font_class": "arrow-right", + "unicode": "\ue6bb" + }, + { + "font_class": "arrow-up", + "unicode": "\ue6bd" + }, + { + "font_class": "auth", + "unicode": "\ue6ab" + }, + { + "font_class": "auth-filled", + "unicode": "\ue6cc" + }, + { + "font_class": "back", + "unicode": "\ue6b9" + }, + { + "font_class": "bars", + "unicode": "\ue627" + }, + { + "font_class": "calendar", + "unicode": "\ue6a0" + }, + { + "font_class": "calendar-filled", + "unicode": "\ue6c0" + }, + { + "font_class": "camera", + "unicode": "\ue65a" + }, + { + "font_class": "camera-filled", + "unicode": "\ue658" + }, + { + "font_class": "cart", + "unicode": "\ue631" + }, + { + "font_class": "cart-filled", + "unicode": "\ue6d0" + }, + { + "font_class": "chat", + "unicode": "\ue65d" + }, + { + "font_class": "chat-filled", + "unicode": "\ue659" + }, + { + "font_class": "chatboxes", + "unicode": "\ue696" + }, + { + "font_class": "chatboxes-filled", + "unicode": "\ue692" + }, + { + "font_class": "chatbubble", + "unicode": "\ue697" + }, + { + "font_class": "chatbubble-filled", + "unicode": "\ue694" + }, + { + "font_class": "checkbox", + "unicode": "\ue62b" + }, + { + "font_class": "checkbox-filled", + "unicode": "\ue62c" + }, + { + "font_class": "checkmarkempty", + "unicode": "\ue65c" + }, + { + "font_class": "circle", + "unicode": "\ue65b" + }, + { + "font_class": "circle-filled", + "unicode": "\ue65e" + }, + { + "font_class": "clear", + "unicode": "\ue66d" + }, + { + "font_class": "close", + "unicode": "\ue673" + }, + { + "font_class": "closeempty", + "unicode": "\ue66c" + }, + { + "font_class": "cloud-download", + "unicode": "\ue647" + }, + { + "font_class": "cloud-download-filled", + "unicode": "\ue646" + }, + { + "font_class": "cloud-upload", + "unicode": "\ue645" + }, + { + "font_class": "cloud-upload-filled", + "unicode": "\ue648" + }, + { + "font_class": "color", + "unicode": "\ue6cf" + }, + { + "font_class": "color-filled", + "unicode": "\ue6c9" + }, + { + "font_class": "compose", + "unicode": "\ue67f" + }, + { + "font_class": "contact", + "unicode": "\ue693" + }, + { + "font_class": "contact-filled", + "unicode": "\ue695" + }, + { + "font_class": "down", + "unicode": "\ue6b8" + }, + { + "font_class": "bottom", + "unicode": "\ue6b8" + }, + { + "font_class": "download", + "unicode": "\ue68d" + }, + { + "font_class": "download-filled", + "unicode": "\ue681" + }, + { + "font_class": "email", + "unicode": "\ue69e" + }, + { + "font_class": "email-filled", + "unicode": "\ue69a" + }, + { + "font_class": "eye", + "unicode": "\ue651" + }, + { + "font_class": "eye-filled", + "unicode": "\ue66a" + }, + { + "font_class": "eye-slash", + "unicode": "\ue6b3" + }, + { + "font_class": "eye-slash-filled", + "unicode": "\ue6b4" + }, + { + "font_class": "fire", + "unicode": "\ue6a1" + }, + { + "font_class": "fire-filled", + "unicode": "\ue6c5" + }, + { + "font_class": "flag", + "unicode": "\ue65f" + }, + { + "font_class": "flag-filled", + "unicode": "\ue660" + }, + { + "font_class": "folder-add", + "unicode": "\ue6a9" + }, + { + "font_class": "folder-add-filled", + "unicode": "\ue6c8" + }, + { + "font_class": "font", + "unicode": "\ue6a3" + }, + { + "font_class": "forward", + "unicode": "\ue6ba" + }, + { + "font_class": "gear", + "unicode": "\ue664" + }, + { + "font_class": "gear-filled", + "unicode": "\ue661" + }, + { + "font_class": "gift", + "unicode": "\ue6a4" + }, + { + "font_class": "gift-filled", + "unicode": "\ue6c4" + }, + { + "font_class": "hand-down", + "unicode": "\ue63d" + }, + { + "font_class": "hand-down-filled", + "unicode": "\ue63c" + }, + { + "font_class": "hand-up", + "unicode": "\ue63f" + }, + { + "font_class": "hand-up-filled", + "unicode": "\ue63e" + }, + { + "font_class": "headphones", + "unicode": "\ue630" + }, + { + "font_class": "heart", + "unicode": "\ue639" + }, + { + "font_class": "heart-filled", + "unicode": "\ue641" + }, + { + "font_class": "help", + "unicode": "\ue679" + }, + { + "font_class": "help-filled", + "unicode": "\ue674" + }, + { + "font_class": "home", + "unicode": "\ue662" + }, + { + "font_class": "home-filled", + "unicode": "\ue663" + }, + { + "font_class": "image", + "unicode": "\ue670" + }, + { + "font_class": "image-filled", + "unicode": "\ue678" + }, + { + "font_class": "images", + "unicode": "\ue650" + }, + { + "font_class": "images-filled", + "unicode": "\ue64b" + }, + { + "font_class": "info", + "unicode": "\ue669" + }, + { + "font_class": "info-filled", + "unicode": "\ue649" + }, + { + "font_class": "left", + "unicode": "\ue6b7" + }, + { + "font_class": "link", + "unicode": "\ue6a5" + }, + { + "font_class": "list", + "unicode": "\ue644" + }, + { + "font_class": "location", + "unicode": "\ue6ae" + }, + { + "font_class": "location-filled", + "unicode": "\ue6af" + }, + { + "font_class": "locked", + "unicode": "\ue66b" + }, + { + "font_class": "locked-filled", + "unicode": "\ue668" + }, + { + "font_class": "loop", + "unicode": "\ue633" + }, + { + "font_class": "mail-open", + "unicode": "\ue643" + }, + { + "font_class": "mail-open-filled", + "unicode": "\ue63a" + }, + { + "font_class": "map", + "unicode": "\ue667" + }, + { + "font_class": "map-filled", + "unicode": "\ue666" + }, + { + "font_class": "map-pin", + "unicode": "\ue6ad" + }, + { + "font_class": "map-pin-ellipse", + "unicode": "\ue6ac" + }, + { + "font_class": "medal", + "unicode": "\ue6a2" + }, + { + "font_class": "medal-filled", + "unicode": "\ue6c3" + }, + { + "font_class": "mic", + "unicode": "\ue671" + }, + { + "font_class": "mic-filled", + "unicode": "\ue677" + }, + { + "font_class": "micoff", + "unicode": "\ue67e" + }, + { + "font_class": "micoff-filled", + "unicode": "\ue6b0" + }, + { + "font_class": "minus", + "unicode": "\ue66f" + }, + { + "font_class": "minus-filled", + "unicode": "\ue67d" + }, + { + "font_class": "more", + "unicode": "\ue64d" + }, + { + "font_class": "more-filled", + "unicode": "\ue64e" + }, + { + "font_class": "navigate", + "unicode": "\ue66e" + }, + { + "font_class": "navigate-filled", + "unicode": "\ue67a" + }, + { + "font_class": "notification", + "unicode": "\ue6a6" + }, + { + "font_class": "notification-filled", + "unicode": "\ue6c1" + }, + { + "font_class": "paperclip", + "unicode": "\ue652" + }, + { + "font_class": "paperplane", + "unicode": "\ue672" + }, + { + "font_class": "paperplane-filled", + "unicode": "\ue675" + }, + { + "font_class": "person", + "unicode": "\ue699" + }, + { + "font_class": "person-filled", + "unicode": "\ue69d" + }, + { + "font_class": "personadd", + "unicode": "\ue69f" + }, + { + "font_class": "personadd-filled", + "unicode": "\ue698" + }, + { + "font_class": "personadd-filled-copy", + "unicode": "\ue6d1" + }, + { + "font_class": "phone", + "unicode": "\ue69c" + }, + { + "font_class": "phone-filled", + "unicode": "\ue69b" + }, + { + "font_class": "plus", + "unicode": "\ue676" + }, + { + "font_class": "plus-filled", + "unicode": "\ue6c7" + }, + { + "font_class": "plusempty", + "unicode": "\ue67b" + }, + { + "font_class": "pulldown", + "unicode": "\ue632" + }, + { + "font_class": "pyq", + "unicode": "\ue682" + }, + { + "font_class": "qq", + "unicode": "\ue680" + }, + { + "font_class": "redo", + "unicode": "\ue64a" + }, + { + "font_class": "redo-filled", + "unicode": "\ue655" + }, + { + "font_class": "refresh", + "unicode": "\ue657" + }, + { + "font_class": "refresh-filled", + "unicode": "\ue656" + }, + { + "font_class": "refreshempty", + "unicode": "\ue6bf" + }, + { + "font_class": "reload", + "unicode": "\ue6b2" + }, + { + "font_class": "right", + "unicode": "\ue6b5" + }, + { + "font_class": "scan", + "unicode": "\ue62a" + }, + { + "font_class": "search", + "unicode": "\ue654" + }, + { + "font_class": "settings", + "unicode": "\ue653" + }, + { + "font_class": "settings-filled", + "unicode": "\ue6ce" + }, + { + "font_class": "shop", + "unicode": "\ue62f" + }, + { + "font_class": "shop-filled", + "unicode": "\ue6cd" + }, + { + "font_class": "smallcircle", + "unicode": "\ue67c" + }, + { + "font_class": "smallcircle-filled", + "unicode": "\ue665" + }, + { + "font_class": "sound", + "unicode": "\ue684" + }, + { + "font_class": "sound-filled", + "unicode": "\ue686" + }, + { + "font_class": "spinner-cycle", + "unicode": "\ue68a" + }, + { + "font_class": "staff", + "unicode": "\ue6a7" + }, + { + "font_class": "staff-filled", + "unicode": "\ue6cb" + }, + { + "font_class": "star", + "unicode": "\ue688" + }, + { + "font_class": "star-filled", + "unicode": "\ue68f" + }, + { + "font_class": "starhalf", + "unicode": "\ue683" + }, + { + "font_class": "trash", + "unicode": "\ue687" + }, + { + "font_class": "trash-filled", + "unicode": "\ue685" + }, + { + "font_class": "tune", + "unicode": "\ue6aa" + }, + { + "font_class": "tune-filled", + "unicode": "\ue6ca" + }, + { + "font_class": "undo", + "unicode": "\ue64f" + }, + { + "font_class": "undo-filled", + "unicode": "\ue64c" + }, + { + "font_class": "up", + "unicode": "\ue6b6" + }, + { + "font_class": "top", + "unicode": "\ue6b6" + }, + { + "font_class": "upload", + "unicode": "\ue690" + }, + { + "font_class": "upload-filled", + "unicode": "\ue68e" + }, + { + "font_class": "videocam", + "unicode": "\ue68c" + }, + { + "font_class": "videocam-filled", + "unicode": "\ue689" + }, + { + "font_class": "vip", + "unicode": "\ue6a8" + }, + { + "font_class": "vip-filled", + "unicode": "\ue6c6" + }, + { + "font_class": "wallet", + "unicode": "\ue6b1" + }, + { + "font_class": "wallet-filled", + "unicode": "\ue6c2" + }, + { + "font_class": "weibo", + "unicode": "\ue68b" + }, + { + "font_class": "weixin", + "unicode": "\ue691" + } +] as IconsDataItem[] + +// export const fontData = JSON.parse(fontDataJson) diff --git a/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons_file_vue.js b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons_file_vue.js new file mode 100644 index 0000000..1cd11e1 --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons_file_vue.js @@ -0,0 +1,649 @@ + +export const fontData = [ + { + "font_class": "arrow-down", + "unicode": "\ue6be" + }, + { + "font_class": "arrow-left", + "unicode": "\ue6bc" + }, + { + "font_class": "arrow-right", + "unicode": "\ue6bb" + }, + { + "font_class": "arrow-up", + "unicode": "\ue6bd" + }, + { + "font_class": "auth", + "unicode": "\ue6ab" + }, + { + "font_class": "auth-filled", + "unicode": "\ue6cc" + }, + { + "font_class": "back", + "unicode": "\ue6b9" + }, + { + "font_class": "bars", + "unicode": "\ue627" + }, + { + "font_class": "calendar", + "unicode": "\ue6a0" + }, + { + "font_class": "calendar-filled", + "unicode": "\ue6c0" + }, + { + "font_class": "camera", + "unicode": "\ue65a" + }, + { + "font_class": "camera-filled", + "unicode": "\ue658" + }, + { + "font_class": "cart", + "unicode": "\ue631" + }, + { + "font_class": "cart-filled", + "unicode": "\ue6d0" + }, + { + "font_class": "chat", + "unicode": "\ue65d" + }, + { + "font_class": "chat-filled", + "unicode": "\ue659" + }, + { + "font_class": "chatboxes", + "unicode": "\ue696" + }, + { + "font_class": "chatboxes-filled", + "unicode": "\ue692" + }, + { + "font_class": "chatbubble", + "unicode": "\ue697" + }, + { + "font_class": "chatbubble-filled", + "unicode": "\ue694" + }, + { + "font_class": "checkbox", + "unicode": "\ue62b" + }, + { + "font_class": "checkbox-filled", + "unicode": "\ue62c" + }, + { + "font_class": "checkmarkempty", + "unicode": "\ue65c" + }, + { + "font_class": "circle", + "unicode": "\ue65b" + }, + { + "font_class": "circle-filled", + "unicode": "\ue65e" + }, + { + "font_class": "clear", + "unicode": "\ue66d" + }, + { + "font_class": "close", + "unicode": "\ue673" + }, + { + "font_class": "closeempty", + "unicode": "\ue66c" + }, + { + "font_class": "cloud-download", + "unicode": "\ue647" + }, + { + "font_class": "cloud-download-filled", + "unicode": "\ue646" + }, + { + "font_class": "cloud-upload", + "unicode": "\ue645" + }, + { + "font_class": "cloud-upload-filled", + "unicode": "\ue648" + }, + { + "font_class": "color", + "unicode": "\ue6cf" + }, + { + "font_class": "color-filled", + "unicode": "\ue6c9" + }, + { + "font_class": "compose", + "unicode": "\ue67f" + }, + { + "font_class": "contact", + "unicode": "\ue693" + }, + { + "font_class": "contact-filled", + "unicode": "\ue695" + }, + { + "font_class": "down", + "unicode": "\ue6b8" + }, + { + "font_class": "bottom", + "unicode": "\ue6b8" + }, + { + "font_class": "download", + "unicode": "\ue68d" + }, + { + "font_class": "download-filled", + "unicode": "\ue681" + }, + { + "font_class": "email", + "unicode": "\ue69e" + }, + { + "font_class": "email-filled", + "unicode": "\ue69a" + }, + { + "font_class": "eye", + "unicode": "\ue651" + }, + { + "font_class": "eye-filled", + "unicode": "\ue66a" + }, + { + "font_class": "eye-slash", + "unicode": "\ue6b3" + }, + { + "font_class": "eye-slash-filled", + "unicode": "\ue6b4" + }, + { + "font_class": "fire", + "unicode": "\ue6a1" + }, + { + "font_class": "fire-filled", + "unicode": "\ue6c5" + }, + { + "font_class": "flag", + "unicode": "\ue65f" + }, + { + "font_class": "flag-filled", + "unicode": "\ue660" + }, + { + "font_class": "folder-add", + "unicode": "\ue6a9" + }, + { + "font_class": "folder-add-filled", + "unicode": "\ue6c8" + }, + { + "font_class": "font", + "unicode": "\ue6a3" + }, + { + "font_class": "forward", + "unicode": "\ue6ba" + }, + { + "font_class": "gear", + "unicode": "\ue664" + }, + { + "font_class": "gear-filled", + "unicode": "\ue661" + }, + { + "font_class": "gift", + "unicode": "\ue6a4" + }, + { + "font_class": "gift-filled", + "unicode": "\ue6c4" + }, + { + "font_class": "hand-down", + "unicode": "\ue63d" + }, + { + "font_class": "hand-down-filled", + "unicode": "\ue63c" + }, + { + "font_class": "hand-up", + "unicode": "\ue63f" + }, + { + "font_class": "hand-up-filled", + "unicode": "\ue63e" + }, + { + "font_class": "headphones", + "unicode": "\ue630" + }, + { + "font_class": "heart", + "unicode": "\ue639" + }, + { + "font_class": "heart-filled", + "unicode": "\ue641" + }, + { + "font_class": "help", + "unicode": "\ue679" + }, + { + "font_class": "help-filled", + "unicode": "\ue674" + }, + { + "font_class": "home", + "unicode": "\ue662" + }, + { + "font_class": "home-filled", + "unicode": "\ue663" + }, + { + "font_class": "image", + "unicode": "\ue670" + }, + { + "font_class": "image-filled", + "unicode": "\ue678" + }, + { + "font_class": "images", + "unicode": "\ue650" + }, + { + "font_class": "images-filled", + "unicode": "\ue64b" + }, + { + "font_class": "info", + "unicode": "\ue669" + }, + { + "font_class": "info-filled", + "unicode": "\ue649" + }, + { + "font_class": "left", + "unicode": "\ue6b7" + }, + { + "font_class": "link", + "unicode": "\ue6a5" + }, + { + "font_class": "list", + "unicode": "\ue644" + }, + { + "font_class": "location", + "unicode": "\ue6ae" + }, + { + "font_class": "location-filled", + "unicode": "\ue6af" + }, + { + "font_class": "locked", + "unicode": "\ue66b" + }, + { + "font_class": "locked-filled", + "unicode": "\ue668" + }, + { + "font_class": "loop", + "unicode": "\ue633" + }, + { + "font_class": "mail-open", + "unicode": "\ue643" + }, + { + "font_class": "mail-open-filled", + "unicode": "\ue63a" + }, + { + "font_class": "map", + "unicode": "\ue667" + }, + { + "font_class": "map-filled", + "unicode": "\ue666" + }, + { + "font_class": "map-pin", + "unicode": "\ue6ad" + }, + { + "font_class": "map-pin-ellipse", + "unicode": "\ue6ac" + }, + { + "font_class": "medal", + "unicode": "\ue6a2" + }, + { + "font_class": "medal-filled", + "unicode": "\ue6c3" + }, + { + "font_class": "mic", + "unicode": "\ue671" + }, + { + "font_class": "mic-filled", + "unicode": "\ue677" + }, + { + "font_class": "micoff", + "unicode": "\ue67e" + }, + { + "font_class": "micoff-filled", + "unicode": "\ue6b0" + }, + { + "font_class": "minus", + "unicode": "\ue66f" + }, + { + "font_class": "minus-filled", + "unicode": "\ue67d" + }, + { + "font_class": "more", + "unicode": "\ue64d" + }, + { + "font_class": "more-filled", + "unicode": "\ue64e" + }, + { + "font_class": "navigate", + "unicode": "\ue66e" + }, + { + "font_class": "navigate-filled", + "unicode": "\ue67a" + }, + { + "font_class": "notification", + "unicode": "\ue6a6" + }, + { + "font_class": "notification-filled", + "unicode": "\ue6c1" + }, + { + "font_class": "paperclip", + "unicode": "\ue652" + }, + { + "font_class": "paperplane", + "unicode": "\ue672" + }, + { + "font_class": "paperplane-filled", + "unicode": "\ue675" + }, + { + "font_class": "person", + "unicode": "\ue699" + }, + { + "font_class": "person-filled", + "unicode": "\ue69d" + }, + { + "font_class": "personadd", + "unicode": "\ue69f" + }, + { + "font_class": "personadd-filled", + "unicode": "\ue698" + }, + { + "font_class": "personadd-filled-copy", + "unicode": "\ue6d1" + }, + { + "font_class": "phone", + "unicode": "\ue69c" + }, + { + "font_class": "phone-filled", + "unicode": "\ue69b" + }, + { + "font_class": "plus", + "unicode": "\ue676" + }, + { + "font_class": "plus-filled", + "unicode": "\ue6c7" + }, + { + "font_class": "plusempty", + "unicode": "\ue67b" + }, + { + "font_class": "pulldown", + "unicode": "\ue632" + }, + { + "font_class": "pyq", + "unicode": "\ue682" + }, + { + "font_class": "qq", + "unicode": "\ue680" + }, + { + "font_class": "redo", + "unicode": "\ue64a" + }, + { + "font_class": "redo-filled", + "unicode": "\ue655" + }, + { + "font_class": "refresh", + "unicode": "\ue657" + }, + { + "font_class": "refresh-filled", + "unicode": "\ue656" + }, + { + "font_class": "refreshempty", + "unicode": "\ue6bf" + }, + { + "font_class": "reload", + "unicode": "\ue6b2" + }, + { + "font_class": "right", + "unicode": "\ue6b5" + }, + { + "font_class": "scan", + "unicode": "\ue62a" + }, + { + "font_class": "search", + "unicode": "\ue654" + }, + { + "font_class": "settings", + "unicode": "\ue653" + }, + { + "font_class": "settings-filled", + "unicode": "\ue6ce" + }, + { + "font_class": "shop", + "unicode": "\ue62f" + }, + { + "font_class": "shop-filled", + "unicode": "\ue6cd" + }, + { + "font_class": "smallcircle", + "unicode": "\ue67c" + }, + { + "font_class": "smallcircle-filled", + "unicode": "\ue665" + }, + { + "font_class": "sound", + "unicode": "\ue684" + }, + { + "font_class": "sound-filled", + "unicode": "\ue686" + }, + { + "font_class": "spinner-cycle", + "unicode": "\ue68a" + }, + { + "font_class": "staff", + "unicode": "\ue6a7" + }, + { + "font_class": "staff-filled", + "unicode": "\ue6cb" + }, + { + "font_class": "star", + "unicode": "\ue688" + }, + { + "font_class": "star-filled", + "unicode": "\ue68f" + }, + { + "font_class": "starhalf", + "unicode": "\ue683" + }, + { + "font_class": "trash", + "unicode": "\ue687" + }, + { + "font_class": "trash-filled", + "unicode": "\ue685" + }, + { + "font_class": "tune", + "unicode": "\ue6aa" + }, + { + "font_class": "tune-filled", + "unicode": "\ue6ca" + }, + { + "font_class": "undo", + "unicode": "\ue64f" + }, + { + "font_class": "undo-filled", + "unicode": "\ue64c" + }, + { + "font_class": "up", + "unicode": "\ue6b6" + }, + { + "font_class": "top", + "unicode": "\ue6b6" + }, + { + "font_class": "upload", + "unicode": "\ue690" + }, + { + "font_class": "upload-filled", + "unicode": "\ue68e" + }, + { + "font_class": "videocam", + "unicode": "\ue68c" + }, + { + "font_class": "videocam-filled", + "unicode": "\ue689" + }, + { + "font_class": "vip", + "unicode": "\ue6a8" + }, + { + "font_class": "vip-filled", + "unicode": "\ue6c6" + }, + { + "font_class": "wallet", + "unicode": "\ue6b1" + }, + { + "font_class": "wallet-filled", + "unicode": "\ue6c2" + }, + { + "font_class": "weibo", + "unicode": "\ue68b" + }, + { + "font_class": "weixin", + "unicode": "\ue691" + } +] + +// export const fontData = JSON.parse(fontDataJson) diff --git a/gym-manage-coach-uniapp/uni_modules/uni-icons/package.json b/gym-manage-coach-uniapp/uni_modules/uni-icons/package.json new file mode 100644 index 0000000..60e45f0 --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-icons/package.json @@ -0,0 +1,111 @@ +{ + "id": "uni-icons", + "displayName": "uni-icons 图标", + "version": "2.0.12", + "description": "图标组件,用于展示移动端常见的图标,可自定义颜色、大小。", + "keywords": [ + "uni-ui", + "uniui", + "icon", + "图标" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.2.14", + "uni-app": "^4.08", + "uni-app-x": "^4.61" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui", + "type": "component-vue", + "darkmode": "x", + "i18n": "x", + "widescreen": "x" + }, + "uni_modules": { + "dependencies": [ + "uni-scss" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "x", + "aliyun": "x", + "alipay": "x" + }, + "client": { + "uni-app": { + "vue": { + "vue2": "√", + "vue3": "√" + }, + "web": { + "safari": "√", + "chrome": "√" + }, + "app": { + "vue": "√", + "nvue": "-", + "android": { + "extVersion": "", + "minVersion": "29" + }, + "ios": "√", + "harmony": "√" + }, + "mp": { + "weixin": "√", + "alipay": "√", + "toutiao": "√", + "baidu": "√", + "kuaishou": "-", + "jd": "-", + "harmony": "-", + "qq": "√", + "lark": "-" + }, + "quickapp": { + "huawei": "√", + "union": "√" + } + }, + "uni-app-x": { + "web": { + "safari": "√", + "chrome": "√" + }, + "app": { + "android": { + "extVersion": "", + "minVersion": "29" + }, + "ios": "√", + "harmony": "√" + }, + "mp": { + "weixin": "√" + } + } + } + } + } +} \ No newline at end of file diff --git a/gym-manage-coach-uniapp/uni_modules/uni-icons/readme.md b/gym-manage-coach-uniapp/uni_modules/uni-icons/readme.md new file mode 100644 index 0000000..86234ba --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-icons/readme.md @@ -0,0 +1,8 @@ +## Icons 图标 +> **组件名:uni-icons** +> 代码块: `uIcons` + +用于展示 icons 图标 。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-icons) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/changelog.md b/gym-manage-coach-uniapp/uni_modules/uni-scss/changelog.md new file mode 100644 index 0000000..b863bb0 --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/changelog.md @@ -0,0 +1,8 @@ +## 1.0.3(2022-01-21) +- 优化 组件示例 +## 1.0.2(2021-11-22) +- 修复 / 符号在 vue 不同版本兼容问题引起的报错问题 +## 1.0.1(2021-11-22) +- 修复 vue3中scss语法兼容问题 +## 1.0.0(2021-11-18) +- init diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/index.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/index.scss new file mode 100644 index 0000000..1744a5f --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/index.scss @@ -0,0 +1 @@ +@import './styles/index.scss'; diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/package.json b/gym-manage-coach-uniapp/uni_modules/uni-scss/package.json new file mode 100644 index 0000000..7cc0ccb --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/package.json @@ -0,0 +1,82 @@ +{ + "id": "uni-scss", + "displayName": "uni-scss 辅助样式", + "version": "1.0.3", + "description": "uni-sass是uni-ui提供的一套全局样式 ,通过一些简单的类名和sass变量,实现简单的页面布局操作,比如颜色、边距、圆角等。", + "keywords": [ + "uni-scss", + "uni-ui", + "辅助样式" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.1.0" + }, + "dcloudext": { + "category": [ + "JS SDK", + "通用 SDK" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "n", + "联盟": "n" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/readme.md b/gym-manage-coach-uniapp/uni_modules/uni-scss/readme.md new file mode 100644 index 0000000..b7d1c25 --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/readme.md @@ -0,0 +1,4 @@ +`uni-sass` 是 `uni-ui`提供的一套全局样式 ,通过一些简单的类名和`sass`变量,实现简单的页面布局操作,比如颜色、边距、圆角等。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-sass) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/index.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/index.scss new file mode 100644 index 0000000..ffac4fe --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/index.scss @@ -0,0 +1,7 @@ +@import './setting/_variables.scss'; +@import './setting/_border.scss'; +@import './setting/_color.scss'; +@import './setting/_space.scss'; +@import './setting/_radius.scss'; +@import './setting/_text.scss'; +@import './setting/_styles.scss'; diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_border.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_border.scss new file mode 100644 index 0000000..12a11c3 --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_border.scss @@ -0,0 +1,3 @@ +.uni-border { + border: 1px $uni-border-1 solid; +} \ No newline at end of file diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_color.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_color.scss new file mode 100644 index 0000000..1ededd9 --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_color.scss @@ -0,0 +1,66 @@ + +// TODO 暂时不需要 class ,需要用户使用变量实现 ,如果使用类名其实并不推荐 +// @mixin get-styles($k,$c) { +// @if $k == size or $k == weight{ +// font-#{$k}:#{$c} +// }@else{ +// #{$k}:#{$c} +// } +// } +$uni-ui-color:( + // 主色 + primary: $uni-primary, + primary-disable: $uni-primary-disable, + primary-light: $uni-primary-light, + // 辅助色 + success: $uni-success, + success-disable: $uni-success-disable, + success-light: $uni-success-light, + warning: $uni-warning, + warning-disable: $uni-warning-disable, + warning-light: $uni-warning-light, + error: $uni-error, + error-disable: $uni-error-disable, + error-light: $uni-error-light, + info: $uni-info, + info-disable: $uni-info-disable, + info-light: $uni-info-light, + // 中性色 + main-color: $uni-main-color, + base-color: $uni-base-color, + secondary-color: $uni-secondary-color, + extra-color: $uni-extra-color, + // 背景色 + bg-color: $uni-bg-color, + // 边框颜色 + border-1: $uni-border-1, + border-2: $uni-border-2, + border-3: $uni-border-3, + border-4: $uni-border-4, + // 黑色 + black:$uni-black, + // 白色 + white:$uni-white, + // 透明 + transparent:$uni-transparent +) !default; +@each $key, $child in $uni-ui-color { + .uni-#{"" + $key} { + color: $child; + } + .uni-#{"" + $key}-bg { + background-color: $child; + } +} +.uni-shadow-sm { + box-shadow: $uni-shadow-sm; +} +.uni-shadow-base { + box-shadow: $uni-shadow-base; +} +.uni-shadow-lg { + box-shadow: $uni-shadow-lg; +} +.uni-mask { + background-color:$uni-mask; +} diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_radius.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_radius.scss new file mode 100644 index 0000000..9a0428b --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_radius.scss @@ -0,0 +1,55 @@ +@mixin radius($r,$d:null ,$important: false){ + $radius-value:map-get($uni-radius, $r) if($important, !important, null); + // Key exists within the $uni-radius variable + @if (map-has-key($uni-radius, $r) and $d){ + @if $d == t { + border-top-left-radius:$radius-value; + border-top-right-radius:$radius-value; + }@else if $d == r { + border-top-right-radius:$radius-value; + border-bottom-right-radius:$radius-value; + }@else if $d == b { + border-bottom-left-radius:$radius-value; + border-bottom-right-radius:$radius-value; + }@else if $d == l { + border-top-left-radius:$radius-value; + border-bottom-left-radius:$radius-value; + }@else if $d == tl { + border-top-left-radius:$radius-value; + }@else if $d == tr { + border-top-right-radius:$radius-value; + }@else if $d == br { + border-bottom-right-radius:$radius-value; + }@else if $d == bl { + border-bottom-left-radius:$radius-value; + } + }@else{ + border-radius:$radius-value; + } +} + +@each $key, $child in $uni-radius { + @if($key){ + .uni-radius-#{"" + $key} { + @include radius($key) + } + }@else{ + .uni-radius { + @include radius($key) + } + } +} + +@each $direction in t, r, b, l,tl, tr, br, bl { + @each $key, $child in $uni-radius { + @if($key){ + .uni-radius-#{"" + $direction}-#{"" + $key} { + @include radius($key,$direction,false) + } + }@else{ + .uni-radius-#{$direction} { + @include radius($key,$direction,false) + } + } + } +} diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_space.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_space.scss new file mode 100644 index 0000000..3c89528 --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_space.scss @@ -0,0 +1,56 @@ + +@mixin fn($space,$direction,$size,$n) { + @if $n { + #{$space}-#{$direction}: #{$size*$uni-space-root}px + } @else { + #{$space}-#{$direction}: #{-$size*$uni-space-root}px + } +} +@mixin get-styles($direction,$i,$space,$n){ + @if $direction == t { + @include fn($space, top,$i,$n); + } + @if $direction == r { + @include fn($space, right,$i,$n); + } + @if $direction == b { + @include fn($space, bottom,$i,$n); + } + @if $direction == l { + @include fn($space, left,$i,$n); + } + @if $direction == x { + @include fn($space, left,$i,$n); + @include fn($space, right,$i,$n); + } + @if $direction == y { + @include fn($space, top,$i,$n); + @include fn($space, bottom,$i,$n); + } + @if $direction == a { + @if $n { + #{$space}:#{$i*$uni-space-root}px; + } @else { + #{$space}:#{-$i*$uni-space-root}px; + } + } +} + +@each $orientation in m,p { + $space: margin; + @if $orientation == m { + $space: margin; + } @else { + $space: padding; + } + @for $i from 0 through 16 { + @each $direction in t, r, b, l, x, y, a { + .uni-#{$orientation}#{$direction}-#{$i} { + @include get-styles($direction,$i,$space,true); + } + .uni-#{$orientation}#{$direction}-n#{$i} { + @include get-styles($direction,$i,$space,false); + } + } + } +} \ No newline at end of file diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_styles.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_styles.scss new file mode 100644 index 0000000..689afec --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_styles.scss @@ -0,0 +1,167 @@ +/* #ifndef APP-NVUE */ + +$-color-white:#fff; +$-color-black:#000; +@mixin base-style($color) { + color: #fff; + background-color: $color; + border-color: mix($-color-black, $color, 8%); + &:not([hover-class]):active { + background: mix($-color-black, $color, 10%); + border-color: mix($-color-black, $color, 20%); + color: $-color-white; + outline: none; + } +} +@mixin is-color($color) { + @include base-style($color); + &[loading] { + @include base-style($color); + &::before { + margin-right:5px; + } + } + &[disabled] { + &, + &[loading], + &:not([hover-class]):active { + color: $-color-white; + border-color: mix(darken($color,10%), $-color-white); + background-color: mix($color, $-color-white); + } + } + +} +@mixin base-plain-style($color) { + color:$color; + background-color: mix($-color-white, $color, 90%); + border-color: mix($-color-white, $color, 70%); + &:not([hover-class]):active { + background: mix($-color-white, $color, 80%); + color: $color; + outline: none; + border-color: mix($-color-white, $color, 50%); + } +} +@mixin is-plain($color){ + &[plain] { + @include base-plain-style($color); + &[loading] { + @include base-plain-style($color); + &::before { + margin-right:5px; + } + } + &[disabled] { + &, + &:active { + color: mix($-color-white, $color, 40%); + background-color: mix($-color-white, $color, 90%); + border-color: mix($-color-white, $color, 80%); + } + } + } +} + + +.uni-btn { + margin: 5px; + color: #393939; + border:1px solid #ccc; + font-size: 16px; + font-weight: 200; + background-color: #F9F9F9; + // TODO 暂时处理边框隐藏一边的问题 + overflow: visible; + &::after{ + border: none; + } + + &:not([type]),&[type=default] { + color: #999; + &[loading] { + background: none; + &::before { + margin-right:5px; + } + } + + + + &[disabled]{ + color: mix($-color-white, #999, 60%); + &, + &[loading], + &:active { + color: mix($-color-white, #999, 60%); + background-color: mix($-color-white,$-color-black , 98%); + border-color: mix($-color-white, #999, 85%); + } + } + + &[plain] { + color: #999; + background: none; + border-color: $uni-border-1; + &:not([hover-class]):active { + background: none; + color: mix($-color-white, $-color-black, 80%); + border-color: mix($-color-white, $-color-black, 90%); + outline: none; + } + &[disabled]{ + &, + &[loading], + &:active { + background: none; + color: mix($-color-white, #999, 60%); + border-color: mix($-color-white, #999, 85%); + } + } + } + } + + &:not([hover-class]):active { + color: mix($-color-white, $-color-black, 50%); + } + + &[size=mini] { + font-size: 16px; + font-weight: 200; + border-radius: 8px; + } + + + + &.uni-btn-small { + font-size: 14px; + } + &.uni-btn-mini { + font-size: 12px; + } + + &.uni-btn-radius { + border-radius: 999px; + } + &[type=primary] { + @include is-color($uni-primary); + @include is-plain($uni-primary) + } + &[type=success] { + @include is-color($uni-success); + @include is-plain($uni-success) + } + &[type=error] { + @include is-color($uni-error); + @include is-plain($uni-error) + } + &[type=warning] { + @include is-color($uni-warning); + @include is-plain($uni-warning) + } + &[type=info] { + @include is-color($uni-info); + @include is-plain($uni-info) + } +} +/* #endif */ diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_text.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_text.scss new file mode 100644 index 0000000..a34d08f --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_text.scss @@ -0,0 +1,24 @@ +@mixin get-styles($k,$c) { + @if $k == size or $k == weight{ + font-#{$k}:#{$c} + }@else{ + #{$k}:#{$c} + } +} + +@each $key, $child in $uni-headings { + /* #ifndef APP-NVUE */ + .uni-#{$key} { + @each $k, $c in $child { + @include get-styles($k,$c) + } + } + /* #endif */ + /* #ifdef APP-NVUE */ + .container .uni-#{$key} { + @each $k, $c in $child { + @include get-styles($k,$c) + } + } + /* #endif */ +} diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_variables.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_variables.scss new file mode 100644 index 0000000..557d3d7 --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/setting/_variables.scss @@ -0,0 +1,146 @@ +// @use "sass:math"; +@import '../tools/functions.scss'; +// 间距基础倍数 +$uni-space-root: 2 !default; +// 边框半径默认值 +$uni-radius-root:5px !default; +$uni-radius: () !default; +// 边框半径断点 +$uni-radius: map-deep-merge( + ( + 0: 0, + // TODO 当前版本暂时不支持 sm 属性 + // 'sm': math.div($uni-radius-root, 2), + null: $uni-radius-root, + 'lg': $uni-radius-root * 2, + 'xl': $uni-radius-root * 6, + 'pill': 9999px, + 'circle': 50% + ), + $uni-radius +); +// 字体家族 +$body-font-family: 'Roboto', sans-serif !default; +// 文本 +$heading-font-family: $body-font-family !default; +$uni-headings: () !default; +$letterSpacing: -0.01562em; +$uni-headings: map-deep-merge( + ( + 'h1': ( + size: 32px, + weight: 300, + line-height: 50px, + // letter-spacing:-0.01562em + ), + 'h2': ( + size: 28px, + weight: 300, + line-height: 40px, + // letter-spacing: -0.00833em + ), + 'h3': ( + size: 24px, + weight: 400, + line-height: 32px, + // letter-spacing: normal + ), + 'h4': ( + size: 20px, + weight: 400, + line-height: 30px, + // letter-spacing: 0.00735em + ), + 'h5': ( + size: 16px, + weight: 400, + line-height: 24px, + // letter-spacing: normal + ), + 'h6': ( + size: 14px, + weight: 500, + line-height: 18px, + // letter-spacing: 0.0125em + ), + 'subtitle': ( + size: 12px, + weight: 400, + line-height: 20px, + // letter-spacing: 0.00937em + ), + 'body': ( + font-size: 14px, + font-weight: 400, + line-height: 22px, + // letter-spacing: 0.03125em + ), + 'caption': ( + 'size': 12px, + 'weight': 400, + 'line-height': 20px, + // 'letter-spacing': 0.03333em, + // 'text-transform': false + ) + ), + $uni-headings +); + + + +// 主色 +$uni-primary: #2979ff !default; +$uni-primary-disable:lighten($uni-primary,20%) !default; +$uni-primary-light: lighten($uni-primary,25%) !default; + +// 辅助色 +// 除了主色外的场景色,需要在不同的场景中使用(例如危险色表示危险的操作)。 +$uni-success: #18bc37 !default; +$uni-success-disable:lighten($uni-success,20%) !default; +$uni-success-light: lighten($uni-success,25%) !default; + +$uni-warning: #f3a73f !default; +$uni-warning-disable:lighten($uni-warning,20%) !default; +$uni-warning-light: lighten($uni-warning,25%) !default; + +$uni-error: #e43d33 !default; +$uni-error-disable:lighten($uni-error,20%) !default; +$uni-error-light: lighten($uni-error,25%) !default; + +$uni-info: #8f939c !default; +$uni-info-disable:lighten($uni-info,20%) !default; +$uni-info-light: lighten($uni-info,25%) !default; + +// 中性色 +// 中性色用于文本、背景和边框颜色。通过运用不同的中性色,来表现层次结构。 +$uni-main-color: #3a3a3a !default; // 主要文字 +$uni-base-color: #6a6a6a !default; // 常规文字 +$uni-secondary-color: #909399 !default; // 次要文字 +$uni-extra-color: #c7c7c7 !default; // 辅助说明 + +// 边框颜色 +$uni-border-1: #F0F0F0 !default; +$uni-border-2: #EDEDED !default; +$uni-border-3: #DCDCDC !default; +$uni-border-4: #B9B9B9 !default; + +// 常规色 +$uni-black: #000000 !default; +$uni-white: #ffffff !default; +$uni-transparent: rgba($color: #000000, $alpha: 0) !default; + +// 背景色 +$uni-bg-color: #f7f7f7 !default; + +/* 水平间距 */ +$uni-spacing-sm: 8px !default; +$uni-spacing-base: 15px !default; +$uni-spacing-lg: 30px !default; + +// 阴影 +$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5) !default; +$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2) !default; +$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5) !default; + +// 蒙版 +$uni-mask: rgba($color: #000000, $alpha: 0.4) !default; diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/tools/functions.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/tools/functions.scss new file mode 100644 index 0000000..ac6f63e --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/styles/tools/functions.scss @@ -0,0 +1,19 @@ +// 合并 map +@function map-deep-merge($parent-map, $child-map){ + $result: $parent-map; + @each $key, $child in $child-map { + $parent-has-key: map-has-key($result, $key); + $parent-value: map-get($result, $key); + $parent-type: type-of($parent-value); + $child-type: type-of($child); + $parent-is-map: $parent-type == map; + $child-is-map: $child-type == map; + + @if (not $parent-has-key) or ($parent-type != $child-type) or (not ($parent-is-map and $child-is-map)){ + $result: map-merge($result, ( $key: $child )); + }@else { + $result: map-merge($result, ( $key: map-deep-merge($parent-value, $child) )); + } + } + @return $result; +}; diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/theme.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/theme.scss new file mode 100644 index 0000000..80ee62f --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/theme.scss @@ -0,0 +1,31 @@ +// 间距基础倍数 +$uni-space-root: 2; +// 边框半径默认值 +$uni-radius-root:5px; +// 主色 +$uni-primary: #2979ff; +// 辅助色 +$uni-success: #4cd964; +// 警告色 +$uni-warning: #f0ad4e; +// 错误色 +$uni-error: #dd524d; +// 描述色 +$uni-info: #909399; +// 中性色 +$uni-main-color: #303133; +$uni-base-color: #606266; +$uni-secondary-color: #909399; +$uni-extra-color: #C0C4CC; +// 背景色 +$uni-bg-color: #f5f5f5; +// 边框颜色 +$uni-border-1: #DCDFE6; +$uni-border-2: #E4E7ED; +$uni-border-3: #EBEEF5; +$uni-border-4: #F2F6FC; + +// 常规色 +$uni-black: #000000; +$uni-white: #ffffff; +$uni-transparent: rgba($color: #000000, $alpha: 0); diff --git a/gym-manage-coach-uniapp/uni_modules/uni-scss/variables.scss b/gym-manage-coach-uniapp/uni_modules/uni-scss/variables.scss new file mode 100644 index 0000000..1c062d4 --- /dev/null +++ b/gym-manage-coach-uniapp/uni_modules/uni-scss/variables.scss @@ -0,0 +1,62 @@ +@import './styles/setting/_variables.scss'; +// 间距基础倍数 +$uni-space-root: 2; +// 边框半径默认值 +$uni-radius-root:5px; + +// 主色 +$uni-primary: #2979ff; +$uni-primary-disable:mix(#fff,$uni-primary,50%); +$uni-primary-light: mix(#fff,$uni-primary,80%); + +// 辅助色 +// 除了主色外的场景色,需要在不同的场景中使用(例如危险色表示危险的操作)。 +$uni-success: #18bc37; +$uni-success-disable:mix(#fff,$uni-success,50%); +$uni-success-light: mix(#fff,$uni-success,80%); + +$uni-warning: #f3a73f; +$uni-warning-disable:mix(#fff,$uni-warning,50%); +$uni-warning-light: mix(#fff,$uni-warning,80%); + +$uni-error: #e43d33; +$uni-error-disable:mix(#fff,$uni-error,50%); +$uni-error-light: mix(#fff,$uni-error,80%); + +$uni-info: #8f939c; +$uni-info-disable:mix(#fff,$uni-info,50%); +$uni-info-light: mix(#fff,$uni-info,80%); + +// 中性色 +// 中性色用于文本、背景和边框颜色。通过运用不同的中性色,来表现层次结构。 +$uni-main-color: #3a3a3a; // 主要文字 +$uni-base-color: #6a6a6a; // 常规文字 +$uni-secondary-color: #909399; // 次要文字 +$uni-extra-color: #c7c7c7; // 辅助说明 + +// 边框颜色 +$uni-border-1: #F0F0F0; +$uni-border-2: #EDEDED; +$uni-border-3: #DCDCDC; +$uni-border-4: #B9B9B9; + +// 常规色 +$uni-black: #000000; +$uni-white: #ffffff; +$uni-transparent: rgba($color: #000000, $alpha: 0); + +// 背景色 +$uni-bg-color: #f7f7f7; + +/* 水平间距 */ +$uni-spacing-sm: 8px; +$uni-spacing-base: 15px; +$uni-spacing-lg: 30px; + +// 阴影 +$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5); +$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2); +$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5); + +// 蒙版 +$uni-mask: rgba($color: #000000, $alpha: 0.4); diff --git a/gym-manage-coach-uniapp/utils/request.js b/gym-manage-coach-uniapp/utils/request.js new file mode 100644 index 0000000..dbbd73b --- /dev/null +++ b/gym-manage-coach-uniapp/utils/request.js @@ -0,0 +1,76 @@ +const luchRequest = require('luch-request') +const Request = luchRequest.default || luchRequest +const { generateSignatureHeaders } = require('./signature') +const store = require('../store/index') + +const BASE_URL = 'http://192.168.101.5:8084/api' + +const http = new Request({ + baseURL: BASE_URL, + timeout: 15000, + header: { + 'Content-Type': 'application/json' + } +}) + +// 请求拦截器 +http.interceptors.request.use( + (config) => { + // 注入 JWT token + const token = store.getToken() + if (token) { + config.header = config.header || {} + config.header.Authorization = 'Bearer ' + token + } + + // 注入签名头 + const method = (config.method || 'GET').toUpperCase() + const url = config.url || '' + const body = config.data + + const signatureHeaders = generateSignatureHeaders(method, url, body) + config.header = config.header || {} + Object.assign(config.header, signatureHeaders) + + return config + }, + (error) => Promise.reject(error) +) + +// 响应拦截器 +http.interceptors.response.use( + (response) => { + if (response.statusCode === 200) { + return response.data + } + return response + }, + (error) => { + if (error.statusCode === 401) { + store.clearLogin() + const pages = getCurrentPages() + const currentPage = pages[pages.length - 1] + if (currentPage && currentPage.route !== 'pages/login/login') { + uni.reLaunch({ url: '/pages/login/login' }) + } + } + return Promise.reject(error) + } +) + +/** + * 将 coverImage 字段值解析为完整图片 URL + */ +function resolveCoverUrl(coverImage) { + if (!coverImage) return '' + if (/^\/api\/files\/\d+\/preview$/.test(coverImage)) { + return BASE_URL + coverImage.substring(4) + } + if (/^\d+$/.test(coverImage)) { + return BASE_URL + '/files/' + coverImage + '/preview' + } + return coverImage +} + +module.exports = http +module.exports.resolveCoverUrl = resolveCoverUrl diff --git a/gym-manage-coach-uniapp/utils/signature.js b/gym-manage-coach-uniapp/utils/signature.js new file mode 100644 index 0000000..fa87beb --- /dev/null +++ b/gym-manage-coach-uniapp/utils/signature.js @@ -0,0 +1,59 @@ +const CryptoJS = require('crypto-js') + +const SIGNATURE_SECRET = 'NovalonManageSystemSecretKey2026' + +/** + * 生成 HMAC-SHA256 签名 + */ +function generateSignature(method, path, query, body, timestamp, nonce) { + const stringToSign = [method, path, query || '', body || '', String(timestamp), nonce].join('\n') + const signature = CryptoJS.HmacSHA256(stringToSign, SIGNATURE_SECRET) + return CryptoJS.enc.Base64.stringify(signature) +} + +/** + * 生成随机 nonce + */ +function generateNonce() { + const timestamp = Date.now().toString(36) + const randomPart = Math.random().toString(36).substring(2, 15) + return timestamp + '-' + randomPart +} + +/** + * 从 URL 解析 path 和 query + */ +function parseUrl(url) { + if (url.startsWith('http://') || url.startsWith('https://')) { + const withoutProtocol = url.substring(url.indexOf('://') + 3) + const pathStart = withoutProtocol.indexOf('/') + if (pathStart === -1) return { path: '/', query: '' } + const pathAndQuery = withoutProtocol.substring(pathStart) + const queryIndex = pathAndQuery.indexOf('?') + if (queryIndex === -1) return { path: pathAndQuery, query: '' } + return { path: pathAndQuery.substring(0, queryIndex), query: pathAndQuery.substring(queryIndex + 1) } + } + const queryIndex = url.indexOf('?') + if (queryIndex === -1) return { path: url, query: '' } + return { path: url.substring(0, queryIndex), query: url.substring(queryIndex + 1) } +} + +/** + * 生成签名请求头 + */ +function generateSignatureHeaders(method, url, body) { + const timestamp = Date.now() + const nonce = generateNonce() + const { path, query } = parseUrl(url) + const bodyStr = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : '' + + const signature = generateSignature(method.toUpperCase(), path, query, bodyStr, timestamp, nonce) + + return { + 'X-Signature': signature, + 'X-Timestamp': String(timestamp), + 'X-Nonce': nonce + } +} + +module.exports = { generateSignatureHeaders } diff --git a/gym-manage-uniapp/pages/course-detail/course-detail.vue b/gym-manage-uniapp/pages/course-detail/course-detail.vue index 72c8f65..f8ee2de 100644 --- a/gym-manage-uniapp/pages/course-detail/course-detail.vue +++ b/gym-manage-uniapp/pages/course-detail/course-detail.vue @@ -233,10 +233,28 @@ let canBook = false let btnText = '立即预约' + // 检查是否距离课程开始至少30分钟 + const nowTime = new Date() + const minutesUntilStart = dt && !isNaN(dt) ? Math.round((dt - nowTime) / 60000) : 0 + const isTooLate = minutesUntilStart < 30 + const isPastStart = minutesUntilStart <= 0 + if (statusStr === 0) { - canBook = !isFull - btnText = isFull ? '已约满' : '立即预约' - statusText = isFull ? '已约满' : '' + if (isPastStart) { + canBook = false + btnText = '课程已开始' + statusText = '课程已开始' + } else if (isTooLate) { + canBook = false + btnText = '需提前30分钟预约' + } else if (isFull) { + canBook = false + btnText = '已约满' + statusText = '已约满' + } else { + canBook = true + btnText = '立即预约' + } } else if (statusStr === 1) { statusText = '已取消' statusClass = 'status-cancel' diff --git a/gym-manage-uniapp/utils/request.js b/gym-manage-uniapp/utils/request.js index d5e2fd2..b545527 100644 --- a/gym-manage-uniapp/utils/request.js +++ b/gym-manage-uniapp/utils/request.js @@ -44,7 +44,11 @@ http.interceptors.response.use( if (response.statusCode === 200) { return response.data } - return response + // 非200状态码:构造错误对象,确保 catch 块能正确读取 message + const error = new Error(response.data?.message || '请求失败') + error.statusCode = response.statusCode + error.data = response.data + return Promise.reject(error) }, (error) => { // 401 处理:清除登录态,跳转登录页 diff --git a/gym-manage-web/src/api/coach.api.ts b/gym-manage-web/src/api/coach.api.ts index 865f78a..2a293da 100644 --- a/gym-manage-web/src/api/coach.api.ts +++ b/gym-manage-web/src/api/coach.api.ts @@ -27,6 +27,21 @@ export interface CoachUpdateRequest { phone?: string } +export interface ViolationCountItem { + coach_id: number + count: number +} + +export interface ViolationRecord { + id: number + coach_id: number + course_id: number + violation_time: string + violation_reason: string + course_name?: string + created_at: string +} + export const coachApi = { /** 获取所有教练列表 */ getAll: () => @@ -47,4 +62,12 @@ export const coachApi = { /** 获取教练教授的团课列表 */ getCourses: (id: string) => request.get(`/coach/${id}/courses`), + + /** 获取所有教练的违规次数统计 */ + getViolationCounts: () => + request.get('/coach/violation-counts'), + + /** 获取指定教练的违规记录 */ + getViolations: (id: string) => + request.get(`/coach/${id}/violations`), } diff --git a/gym-manage-web/src/api/statistics.api.ts b/gym-manage-web/src/api/statistics.api.ts index 53f36e4..e19ffd5 100644 --- a/gym-manage-web/src/api/statistics.api.ts +++ b/gym-manage-web/src/api/statistics.api.ts @@ -36,11 +36,22 @@ export interface SignInStatistics { faceSignIns: number } +export interface CoachStatistics { + totalCoaches: number + totalViolations: number + lateCount: number + absentCount: number + notManualEndCount: number + violatedCoaches: number + totalCourses: number +} + export interface StatisticsSummary { statDate: string memberStatistics: MemberStatistics bookingStatistics: BookingStatistics signInStatistics: SignInStatistics + coachStatistics: CoachStatistics generatedAt: string } diff --git a/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue b/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue index 9edc3b1..e479e0e 100644 --- a/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue +++ b/gym-manage-web/src/views/groupcourse/GroupCourseManagement.vue @@ -27,11 +27,16 @@ - + + + 搜索 + + + 新增团课 @@ -82,12 +87,12 @@ @@ -96,11 +101,14 @@ {{ row.currentMembers || 0 }}/{{ row.maxMembers || 0 }} - + + + + + + {{ detailRow.id }} + {{ detailRow.courseName }} + {{ getCourseTypeName(detailRow.courseType) }} + {{ getCoachDisplayName(detailRow.coachId) || detailRow.coachName || '-' }} + + + {{ statusMap[detailRow.status || '']?.label || detailRow.status }} + + + {{ detailRow.location || '-' }} + {{ formatDateTime(detailRow.startTime) }} + {{ formatDateTime(detailRow.actualStartTime) || '-' }} + {{ formatDateTime(detailRow.endTime) }} + {{ formatDateTime(detailRow.actualEndTime) || '-' }} + {{ detailRow.currentMembers || 0 }} / {{ detailRow.maxMembers || 0 }} + {{ detailRow.storedValueAmount ?? '-' }} + + + - + + {{ detailRow.description || '-' }} + {{ detailRow.createBy || '-' }} + {{ detailRow.updateBy || '-' }} + {{ formatDateTime(detailRow.createdAt) }} + {{ formatDateTime(detailRow.updatedAt) }} + + + @@ -837,4 +910,11 @@ onMounted(async () => { color: #909399; font-size: 14px; } + +.detail-cover { + width: 120px; + height: 80px; + object-fit: cover; + border-radius: 4px; +} diff --git a/gym-manage-web/src/views/statistics/StatisticsDashboard.vue b/gym-manage-web/src/views/statistics/StatisticsDashboard.vue index 42efa28..6cb96ef 100644 --- a/gym-manage-web/src/views/statistics/StatisticsDashboard.vue +++ b/gym-manage-web/src/views/statistics/StatisticsDashboard.vue @@ -50,9 +50,9 @@ - -
签到总数
-
{{ summary?.signInStatistics?.totalSignIns ?? '-' }}
+ +
教练违规
+
{{ summary?.coachStatistics?.totalViolations ?? '-' }}
@@ -192,12 +192,65 @@ + + + + + + +
+ 教练总数 + {{ summary?.coachStatistics?.totalCoaches ?? '-' }} +
+
+ +
+ 违规教练 + + {{ summary?.coachStatistics?.violatedCoaches ?? '-' }} + +
+
+ +
+ 开课总数 + {{ summary?.coachStatistics?.totalCourses ?? '-' }} +
+
+ +
+ 迟到 + + {{ summary?.coachStatistics?.lateCount ?? '-' }} + +
+
+ +
+ 缺席 + + {{ summary?.coachStatistics?.absentCount ?? '-' }} + +
+
+ +
+ 未手动结课 + + {{ summary?.coachStatistics?.notManualEndCount ?? '-' }} + +
+
+
+
@@ -350,6 +415,7 @@ onMounted(() => { .stat-card--success .stat-card__value { color: #67c23a; } .stat-card--warning .stat-card__value { color: #e6a23c; } .stat-card--info .stat-card__value { color: #909399; } +.stat-card--danger .stat-card__value { color: #f56c6c; } /* ---- 明细分区 ---- */ .section-card { @@ -380,6 +446,14 @@ onMounted(() => { color: #303133; } +.metric-value--warn { + color: #e6a23c; +} + +.metric-value--danger { + color: #f56c6c; +} + .rate-group { display: flex; flex-direction: column; diff --git a/gym-manage-web/src/views/system/CoachManagement.vue b/gym-manage-web/src/views/system/CoachManagement.vue index f864160..d809efa 100644 --- a/gym-manage-web/src/views/system/CoachManagement.vue +++ b/gym-manage-web/src/views/system/CoachManagement.vue @@ -26,7 +26,7 @@ - + @@ -41,12 +41,19 @@ + + + - + + + + + + + @@ -145,13 +237,14 @@ import { ref, reactive, computed, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { Search } from '@element-plus/icons-vue' -import { coachApi, type Coach, type CoachCreateRequest, type CoachUpdateRequest, type GroupCourse } from '@/api/coach.api' +import { coachApi, type Coach, type CoachCreateRequest, type CoachUpdateRequest, type GroupCourse, type ViolationCountItem, type ViolationRecord } from '@/api/coach.api' import { handleApiError } from '@/utils/errorHandler' import { formatDateTime } from '@/utils/dateFormat' const loading = ref(false) const dataSource = ref([]) const searchKeyword = ref('') +const violationCounts = reactive>({}) const filteredData = computed(() => { if (!searchKeyword.value) return dataSource.value @@ -209,7 +302,24 @@ const courseStatusMap: Record = { 1: { text: '已取消', tag: 'info' }, 2: { text: '已结束', tag: 'warning' }, 3: { text: '进行中', tag: 'danger' }, - 4: { text: '超时', tag: 'info' }, + 5: { text: '教练缺席', tag: 'danger' }, + 6: { text: '自动结束', tag: 'info' }, + 7: { text: '教练迟到', tag: 'warning' }, +} + +// 违规类型映射 +const violationTypeMap: Record = { + COACH_LATE: { text: '教练迟到', tag: 'warning' }, + COACH_ABSENT: { text: '教练缺席', tag: 'danger' }, + NOT_MANUAL_END: { text: '未手动结课', tag: 'info' }, +} + +function getViolationTypeText(reason: string) { + return violationTypeMap[reason]?.text || reason +} + +function getViolationTypeTag(reason: string) { + return violationTypeMap[reason]?.tag || 'info' } function getCourseStatusText(status: number) { @@ -224,6 +334,13 @@ const fetchData = async () => { loading.value = true try { dataSource.value = await coachApi.getAll() + // 同步获取违规次数统计 + const counts = await coachApi.getViolationCounts() + // 清空旧数据 + Object.keys(violationCounts).forEach(key => delete violationCounts[Number(key)]) + counts.forEach(item => { + violationCounts[item.coach_id] = item.count + }) } catch (error) { handleApiError(error) } finally { @@ -333,6 +450,49 @@ const handleViewCourses = async (row: Coach) => { } } +// 违规次数标签颜色 +function getViolationTag(coachId: number) { + const count = violationCounts[coachId] ?? 0 + if (count === 0) return 'success' + if (count <= 2) return 'warning' + return 'danger' +} + +// 违规行高亮 +function getRowClass({ row }: { row: Coach }) { + const count = violationCounts[Number(row.id)] ?? 0 + return count > 0 ? 'row--has-violation' : '' +} + +// 详情弹窗 +const detailDialogVisible = ref(false) +const detailCoach = ref(null) +const detailViolations = ref([]) +const detailViolationsLoading = ref(false) +const detailCourses = ref([]) +const detailCoursesLoading = ref(false) + +const handleViewDetail = async (row: Coach) => { + detailCoach.value = row + detailDialogVisible.value = true + // 并行加载违规记录和团课记录 + detailViolationsLoading.value = true + detailCoursesLoading.value = true + try { + const [violations, courses] = await Promise.all([ + coachApi.getViolations(row.id), + coachApi.getCourses(row.id), + ]) + detailViolations.value = violations + detailCourses.value = courses + } catch (error) { + handleApiError(error) + } finally { + detailViolationsLoading.value = false + detailCoursesLoading.value = false + } +} + onMounted(() => { fetchData() }) @@ -353,4 +513,10 @@ onMounted(() => { } } } + +/* 有违规记录的教练行:左侧红色边框 + 浅色背景 */ +:deep(.row--has-violation) { + border-left: 3px solid #F56C6C; + background-color: #fef0f0 !important; +}