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 0000000..b5771e2 Binary files /dev/null and b/gym-manage-coach-uniapp/static/logo.png differ diff --git a/gym-manage-coach-uniapp/store/index.js b/gym-manage-coach-uniapp/store/index.js new file mode 100644 index 0000000..f837f10 --- /dev/null +++ b/gym-manage-coach-uniapp/store/index.js @@ -0,0 +1,68 @@ +/** + * 教练端全局状态管理 — 以 uni.Storage 为唯一数据源 + */ +const TOKEN_KEY = 'coach_token' +const COACH_KEY = 'coach_info' + +const store = { + /** 是否已登录 */ + get isLoggedIn() { + try { + return !!uni.getStorageSync(TOKEN_KEY) + } catch (e) { + return false + } + }, + + /** 获取 token */ + getToken() { + try { + return uni.getStorageSync(TOKEN_KEY) || null + } catch (e) { + return null + } + }, + + /** 获取教练信息 */ + getCoachInfo() { + try { + return uni.getStorageSync(COACH_KEY) || null + } catch (e) { + return null + } + }, + + /** 获取教练 ID */ + getCoachId() { + const info = this.getCoachInfo() + return info ? info.coachId : null + }, + + /** 获取教练用户名 */ + getUsername() { + const info = this.getCoachInfo() + return info ? info.username : '' + }, + + /** 设置登录态 */ + setLogin(token, coachInfo) { + try { + uni.setStorageSync(TOKEN_KEY, token) + if (coachInfo) uni.setStorageSync(COACH_KEY, coachInfo) + } catch (e) { + console.error('存储登录态失败:', e) + } + }, + + /** 清除登录态 */ + clearLogin() { + try { + uni.removeStorageSync(TOKEN_KEY) + uni.removeStorageSync(COACH_KEY) + } catch (e) { + console.error('清除登录态失败:', e) + } + } +} + +module.exports = store diff --git a/gym-manage-coach-uniapp/uni.promisify.adaptor.js b/gym-manage-coach-uniapp/uni.promisify.adaptor.js new file mode 100644 index 0000000..5fec4f3 --- /dev/null +++ b/gym-manage-coach-uniapp/uni.promisify.adaptor.js @@ -0,0 +1,13 @@ +uni.addInterceptor({ + returnValue (res) { + if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) { + return res; + } + return new Promise((resolve, reject) => { + 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 0000000..14696d0 Binary files /dev/null and b/gym-manage-coach-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.ttf differ 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; +}