Browse Source

添加 docker 开发环境。修改客户端排课

master
王泽彦 9 months ago
parent
commit
fb68775d70
  1. 2
      admin/src/app/views/dict/components/dict.vue
  2. 37
      docker-compose.yml
  3. 25
      niucloud/app/api/controller/apiController/ClassApi.php
  4. 25
      niucloud/app/api/controller/apiController/Course.php
  5. 66
      niucloud/app/api/controller/apiController/CourseSchedule.php
  6. 24
      niucloud/app/api/controller/apiController/Personnel.php
  7. 2
      niucloud/app/api/middleware/ApiCheckToken.php
  8. 20
      niucloud/app/api/route/route.php
  9. 2
      niucloud/app/job/transfer/schedule/PerformanceCalculation.php
  10. 6
      niucloud/app/middleware.php
  11. 4
      niucloud/app/model/dict/Dict.php
  12. 302
      niucloud/app/service/api/apiService/CourseScheduleService.php
  13. 45
      niucloud/app/service/api/apiService/CourseService.php
  14. 68
      niucloud/app/service/api/apiService/PersonnelService.php
  15. 51
      niucloud/app/service/api/apiService/jlClassService.php
  16. 4
      niucloud/config/app.php
  17. 1
      niucloud/core/base/BaseAdminService.php
  18. 17
      start.sh
  19. 24
      startvps.sh
  20. 234
      uniapp/api/apiRoute.js
  21. 176
      uniapp/components/schedule/ScheduleDetail.vue
  22. 1
      uniapp/pages.json
  23. 8
      uniapp/pages/coach/my/index.vue
  24. 154
      uniapp/pages/coach/schedule/README.md
  25. 117
      uniapp/pages/coach/schedule/add_schedule.vue
  26. 566
      uniapp/pages/coach/schedule/schedule_table.vue
  27. 206
      uniapp/pages/coach/schedule/sign_in.vue
  28. 22
      uniapp/pages/market/my/index.vue

2
admin/src/app/views/dict/components/dict.vue

@ -15,7 +15,7 @@
<el-table-column label="数据名称" prop="name" /> <el-table-column label="数据名称" prop="name" />
<el-table-column label="数据值" prop="value" /> <el-table-column label="数据值" prop="value" />
<el-table-column <el-table-column
label="备注" label="排序"
align="center" align="center"
min-width="100px" min-width="100px"
prop="sort" prop="sort"

37
docker-compose.yml

@ -3,7 +3,7 @@ version: '3.8'
services: services:
# PHP 服务 # PHP 服务
php: php:
image: php:8.2-fpm image: niucloud-php:8.2
container_name: niucloud_php container_name: niucloud_php
volumes: volumes:
- ./niucloud:/var/www/html - ./niucloud:/var/www/html
@ -15,26 +15,16 @@ services:
- redis - redis
environment: environment:
- PHP_IDE_CONFIG=serverName=niucloud - PHP_IDE_CONFIG=serverName=niucloud
command: >
bash -c "
apt-get update &&
apt-get install -y libzip-dev zip unzip git libpng-dev libjpeg-dev libfreetype6-dev &&
docker-php-ext-configure gd --with-freetype --with-jpeg &&
docker-php-ext-install pdo pdo_mysql mysqli zip gd &&
pecl install redis &&
docker-php-ext-enable redis &&
php-fpm
"
networks: networks:
- niucloud_network - niucloud_network
# Nginx 服务 # Nginx 服务
nginx: nginx:
image: nginx:alpine image: nginx:alpine
container_name: niucloud_nginx container_name: niucloud_nginx
ports: ports:
- "20080:80" # 原本是 80 映射到 20080 - "20080:80"
- "20081:8080" # 原本是 8080 映射到 20081 - "20081:8080"
volumes: volumes:
- ./niucloud:/var/www/html - ./niucloud:/var/www/html
- ./admin/dist:/var/www/admin - ./admin/dist:/var/www/admin
@ -47,12 +37,12 @@ services:
networks: networks:
- niucloud_network - niucloud_network
# MySQL 数据库 # MySQL 数据库
mysql: mysql:
image: mysql:8.0 image: mysql:8.0
container_name: niucloud_mysql container_name: niucloud_mysql
ports: ports:
- "23306:3306" # 原本是 3306 映射到 23306 - "23306:3306"
environment: environment:
MYSQL_ROOT_PASSWORD: root123456 MYSQL_ROOT_PASSWORD: root123456
MYSQL_DATABASE: niucloud MYSQL_DATABASE: niucloud
@ -66,12 +56,12 @@ services:
networks: networks:
- niucloud_network - niucloud_network
# Redis 缓存 # Redis 缓存
redis: redis:
image: redis:alpine image: redis:alpine
container_name: niucloud_redis container_name: niucloud_redis
ports: ports:
- "26379:6379" # 原本是 6379 映射到 26379 - "26379:6379"
volumes: volumes:
- ./docker/data/redis:/data - ./docker/data/redis:/data
- ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf - ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf
@ -79,7 +69,7 @@ services:
networks: networks:
- niucloud_network - niucloud_network
# Node.js 服务 (用于构建前端) # Node.js 服务 (用于构建前端)
node: node:
image: node:18-alpine image: node:18-alpine
container_name: niucloud_node container_name: niucloud_node
@ -88,7 +78,7 @@ services:
- ./admin:/app - ./admin:/app
- ./docker/data/node_modules:/app/node_modules - ./docker/data/node_modules:/app/node_modules
ports: ports:
- "23000:3000" # 原本是 3000 映射到 23000 - "23000:5173" # 映射到 Vite 默认开发端口 5173
command: > command: >
sh -c " sh -c "
npm config set registry https://registry.npmmirror.com && npm config set registry https://registry.npmmirror.com &&
@ -98,7 +88,7 @@ services:
networks: networks:
- niucloud_network - niucloud_network
# Composer 服务 (用于 PHP 依赖管理) # Composer 服务 (可选)
composer: composer:
image: composer:latest image: composer:latest
container_name: niucloud_composer container_name: niucloud_composer
@ -112,8 +102,3 @@ services:
networks: networks:
niucloud_network: niucloud_network:
driver: bridge driver: bridge
volumes:
mysql_data:
redis_data:
node_modules:

25
niucloud/app/api/controller/apiController/ClassApi.php

@ -154,4 +154,29 @@ class ClassApi extends BaseApiService
return success('操作成功', (new jlClassService())->getStatisticsInfo($id)); return success('操作成功', (new jlClassService())->getStatisticsInfo($id));
} }
/**
* 获取班级列表(用于添加课程安排)
* @param Request $request
* @return \think\Response
*/
public function getClassList(Request $request)
{
try {
$data = $this->request->params([
["campus_id", 0], // 校区ID筛选
["keyword", ""], // 班级名称关键词搜索
["status", 1] // 状态筛选,默认获取开启状态的班级
]);
$result = (new jlClassService())->getClassListForSchedule($data);
if (!$result['code']) {
return fail($result['msg']);
}
return success('获取成功', $result['data']);
} catch (\Exception $e) {
return fail('获取班级列表失败:' . $e->getMessage());
}
}
} }

25
niucloud/app/api/controller/apiController/Course.php

@ -150,4 +150,29 @@ class Course extends BaseApiService
} }
} }
/**
* 获取课程列表(用于添加课程安排)
* @param Request $request
* @return \think\Response
*/
public function getCourseList(Request $request)
{
try {
$data = $this->request->params([
["keyword", ""], // 课程名称关键词搜索
["course_type", ""], // 课程类型筛选
["status", 1] // 状态筛选,默认获取有效课程
]);
$result = (new CourseService())->getCourseListForSchedule($data);
if (!$result['code']) {
return fail($result['msg']);
}
return success('获取成功', $result['data']);
} catch (\Exception $e) {
return fail('获取课程列表失败:' . $e->getMessage());
}
}
} }

66
niucloud/app/api/controller/apiController/CourseSchedule.php

@ -57,12 +57,36 @@ class CourseSchedule extends BaseApiService
*/ */
public function createSchedule(Request $request) public function createSchedule(Request $request)
{ {
$data = $request->all(); try {
$result = (new CourseScheduleService())->createSchedule($data); $data = $this->request->params([
["campus_id", 0],
["venue_id", 0],
["course_date", ""],
["time_slot", ""],
["course_id", 0],
["coach_id", 0],
["available_capacity", 0],
["class_id", 0],
["remarks", ""],
["created_by", "manual"]
]);
// 验证必填字段
$required = ['campus_id', 'venue_id', 'course_date', 'time_slot', 'course_id', 'coach_id', 'available_capacity'];
foreach ($required as $field) {
if (empty($data[$field])) {
return fail("字段 {$field} 不能为空");
}
}
$result = (new CourseScheduleService())->createCourseSchedule($data);
if (!$result['code']) { if (!$result['code']) {
return fail($result['msg']); return fail($result['msg']);
} }
return success($result['msg'] ?? '创建成功', $result['data'] ?? []); return success($result['msg'] ?? '创建成功', $result['data'] ?? []);
} catch (\Exception $e) {
return fail('创建课程安排失败:' . $e->getMessage());
}
} }
/** /**
@ -119,8 +143,22 @@ class CourseSchedule extends BaseApiService
*/ */
public function getVenueList(Request $request) public function getVenueList(Request $request)
{ {
$data = $request->all(); try {
return success((new CourseScheduleService())->getVenueList($data)); $data = $this->request->params([
["campus_id", 0], // 校区ID筛选
["keyword", ""], // 场地名称关键词搜索
["status", 1] // 状态筛选,默认获取可用场地
]);
$result = (new CourseScheduleService())->getVenueListForSchedule($data);
if (!$result['code']) {
return fail($result['msg']);
}
return success('获取成功', $result['data']);
} catch (\Exception $e) {
return fail('获取场地列表失败:' . $e->getMessage());
}
} }
/** /**
@ -130,11 +168,29 @@ class CourseSchedule extends BaseApiService
*/ */
public function getVenueAvailableTime(Request $request) public function getVenueAvailableTime(Request $request)
{ {
try {
$data = $this->request->params([ $data = $this->request->params([
["venue_id", 0], ["venue_id", 0],
["date", ""] ["date", ""]
]); ]);
return success((new CourseScheduleService())->getVenueAvailableTime($data));
if (empty($data['venue_id'])) {
return fail('场地ID不能为空');
}
if (empty($data['date'])) {
return fail('查询日期不能为空');
}
$result = (new CourseScheduleService())->getVenueAvailableTimeSlots($data);
if (!$result['code']) {
return fail($result['msg']);
}
return success('获取成功', $result['data']);
} catch (\Exception $e) {
return fail('获取场地可用时间失败:' . $e->getMessage());
}
} }
/** /**

24
niucloud/app/api/controller/apiController/Personnel.php

@ -280,4 +280,28 @@ class Personnel extends BaseApiService
} }
} }
/**
* 获取教练列表(用于添加课程安排)
* @param Request $request
* @return \think\Response
*/
public function getCoachListForSchedule(Request $request)
{
try {
$data = $this->request->params([
["campus_id", 0], // 校区ID筛选
["keyword", ""], // 教练姓名关键词搜索
["status", 1] // 状态筛选,默认获取有效教练
]);
$res = (new PersonnelService())->getCoachListForSchedule($data);
if (!$res['code']) {
return fail($res['msg']);
}
return success('获取成功', $res['data']);
} catch (\Exception $e) {
return fail('获取教练列表失败:' . $e->getMessage());
}
}
} }

2
niucloud/app/api/middleware/ApiCheckToken.php

@ -53,6 +53,6 @@ class ApiCheckToken
if ($is_throw_exception) if ($is_throw_exception)
return fail($e->getMessage(), [], $e->getCode()); return fail($e->getMessage(), [], $e->getCode());
} }
return $next($request); if(!$request->memberId()) { return fail("请先登录", [], 401); } return $next($request);
} }
} }

20
niucloud/app/api/route/route.php

@ -190,6 +190,14 @@ Route::group(function () {
//公共端-获取全部班级列表 //公共端-获取全部班级列表
Route::get('common/getClassAll', 'apiController.Common/getClassAll'); Route::get('common/getClassAll', 'apiController.Common/getClassAll');
// 测试用接口 - 无需认证
Route::get('test/course/list', 'apiController.Course/getCourseList');
Route::get('test/class/list', 'apiController.ClassApi/getClassList');
Route::get('test/coach/list', 'apiController.Personnel/getCoachListForSchedule');
Route::get('test/venue/list', 'apiController.CourseSchedule/getVenueList');
Route::get('test/venue/timeSlots', 'apiController.CourseSchedule/getVenueAvailableTime');
Route::post('test/courseSchedule/create', 'apiController.CourseSchedule/createSchedule');
@ -320,6 +328,18 @@ Route::group(function () {
//员工端-获取筛选选项 //员工端-获取筛选选项
Route::get('courseSchedule/filterOptions', 'apiController.CourseSchedule/getFilterOptions'); Route::get('courseSchedule/filterOptions', 'apiController.CourseSchedule/getFilterOptions');
// 添加课程安排页面专用接口
//获取课程列表(用于添加课程安排)
Route::get('course/list', 'apiController.Course/getCourseList');
//获取班级列表(用于添加课程安排)
Route::get('class/list', 'apiController.ClassApi/getClassList');
//获取教练列表(用于添加课程安排)
Route::get('coach/list', 'apiController.Personnel/getCoachListForSchedule');
//获取场地列表(用于添加课程安排)
Route::get('venue/list', 'apiController.CourseSchedule/getVenueList');
//获取场地可用时间段
Route::get('venue/timeSlots', 'apiController.CourseSchedule/getVenueAvailableTime');

2
niucloud/app/job/transfer/schedule/PerformanceCalculation.php

@ -44,7 +44,7 @@ class PerformanceCalculation extends BaseJob
*/ */
public function __construct() public function __construct()
{ {
$this->performanceService = new PerformanceService(); $this->performanceService = null;
} }
/** /**

6
niucloud/app/middleware.php

@ -1,14 +1,18 @@
<?php <?php
// 全局中间件定义文件 // 全局中间件定义文件
use app\adminapi\middleware\AllowCrossDomain;
use think\middleware\LoadLangPack; use think\middleware\LoadLangPack;
use think\app\MultiApp;
return [ return [
// 多应用模式
MultiApp::class,
// 全局请求缓存 // 全局请求缓存
// \think\middleware\CheckRequestCache::class, // \think\middleware\CheckRequestCache::class,
// 多语言加载 // 多语言加载
LoadLangPack::class, LoadLangPack::class,
//跨域请求中间件 //跨域请求中间件
// AllowCrossDomain::class, AllowCrossDomain::class,
// Session初始化 // Session初始化
// \think\middleware\SessionInit::class // \think\middleware\SessionInit::class
]; ];

4
niucloud/app/model/dict/Dict.php

@ -38,7 +38,7 @@ class Dict extends BaseModel
]; ];
// 设置json类型字段 // 设置json类型字段
protected $json = [ 'dictionary' ]; protected $json = ['dictionary'];
// 设置JSON数据返回数组 // 设置JSON数据返回数组
protected $jsonAssoc = true; protected $jsonAssoc = true;
@ -50,7 +50,7 @@ class Dict extends BaseModel
public function searchNameAttr($query, $value, $data) public function searchNameAttr($query, $value, $data)
{ {
if ($value != '') { if ($value != '') {
$query->where("name", $value); $query->where("name", 'like', '%' . $value . '%');
} }
} }

302
niucloud/app/service/api/apiService/CourseScheduleService.php

@ -564,4 +564,306 @@ class CourseScheduleService extends BaseApiService
return ['code' => 0, 'msg' => $e->getMessage()]; return ['code' => 0, 'msg' => $e->getMessage()];
} }
} }
/**
* 获取场地列表(用于添加课程安排)
* @param array $data
* @return array
*/
public function getVenueListForSchedule(array $data)
{
try {
$where = [];
// 场地名称关键词搜索
if (!empty($data['keyword'])) {
$where[] = ['venue_name', 'like', '%' . $data['keyword'] . '%'];
}
// 校区筛选
if (!empty($data['campus_id'])) {
$where[] = ['campus_id', '=', $data['campus_id']];
}
// 状态筛选,默认获取可用场地
if (isset($data['status'])) {
$where[] = ['availability_status', '=', $data['status']];
}
// 只获取未逻辑删除的场地
$where[] = ['deleted_at', '=', 0];
$venueList = Db::name('venue')
->where($where)
->field('id, venue_name, capacity, availability_status, time_range_type, time_range_start, time_range_end, fixed_time_ranges')
->order('created_at DESC')
->select()
->toArray();
return [
'code' => 1,
'msg' => '获取成功',
'data' => $venueList
];
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '获取场地列表失败:' . $e->getMessage(),
'data' => []
];
}
}
/**
* 获取场地可用时间段
* @param array $data
* @return array
*/
public function getVenueAvailableTimeSlots(array $data)
{
try {
$venueId = $data['venue_id'];
$date = $data['date'];
// 获取场地信息
$venue = Db::name('venue')
->where('id', $venueId)
->where('availability_status', 1)
->find();
if (empty($venue)) {
return [
'code' => 0,
'msg' => '场地不存在或不可用',
'data' => []
];
}
// 根据场地时间类型获取可用时间段
$availableSlots = $this->generateTimeSlots($venue, $date);
// 获取该场地该日期已安排的时间段
$occupiedSlots = Db::name('course_schedule')
->where('venue_id', $venueId)
->where('course_date', $date)
->where('deleted_at', 0)
->column('time_slot');
// 过滤已占用的时间段
$availableSlots = array_filter($availableSlots, function($slot) use ($occupiedSlots) {
return !in_array($slot['time_slot'], $occupiedSlots);
});
return [
'code' => 1,
'msg' => '获取成功',
'data' => array_values($availableSlots)
];
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '获取场地可用时间失败:' . $e->getMessage(),
'data' => []
];
}
}
/**
* 创建课程安排
* @param array $data
* @return array
*/
public function createCourseSchedule(array $data)
{
try {
// 开启事务
Db::startTrans();
// 验证场地时间冲突
$conflictCheck = $this->checkVenueConflict($data['venue_id'], $data['course_date'], $data['time_slot']);
if (!$conflictCheck['code']) {
Db::rollback();
return $conflictCheck;
}
// 验证教练时间冲突
$coachConflictCheck = $this->checkCoachConflict($data['coach_id'], $data['course_date'], $data['time_slot']);
if (!$coachConflictCheck['code']) {
Db::rollback();
return $coachConflictCheck;
}
// 准备插入数据
$insertData = [
'campus_id' => $data['campus_id'],
'venue_id' => $data['venue_id'],
'course_date' => $data['course_date'],
'time_slot' => $data['time_slot'],
'course_id' => $data['course_id'],
'coach_id' => $data['coach_id'],
'available_capacity' => $data['available_capacity'],
'status' => 'pending',
'created_by' => $data['created_by'] ?? 'manual',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'deleted_at' => 0
];
// 班级信息暂不存储在课程安排表中,根据实际需求可以通过关联表处理
// 如果有备注,则添加
if (!empty($data['remarks'])) {
$insertData['remarks'] = $data['remarks'];
}
// 插入课程安排
$scheduleId = Db::name('course_schedule')->insertGetId($insertData);
if (!$scheduleId) {
Db::rollback();
return [
'code' => 0,
'msg' => '创建课程安排失败'
];
}
// 提交事务
Db::commit();
return [
'code' => 1,
'msg' => '创建成功',
'data' => [
'schedule_id' => $scheduleId
]
];
} catch (\Exception $e) {
Db::rollback();
return [
'code' => 0,
'msg' => '创建课程安排失败:' . $e->getMessage()
];
}
}
/**
* 生成时间段选项
* @param array $venue
* @param string $date
* @return array
*/
private function generateTimeSlots($venue, $date)
{
$slots = [];
switch ($venue['time_range_type']) {
case 'range':
// 范围类型:从开始时间到结束时间,每小时一个时间段
$startTime = strtotime($venue['time_range_start']);
$endTime = strtotime($venue['time_range_end']);
for ($time = $startTime; $time < $endTime; $time += 3600) {
$startTimeStr = date('H:i', $time);
$endTimeStr = date('H:i', $time + 3600);
$slots[] = [
'time_slot' => $startTimeStr . '-' . $endTimeStr,
'start_time' => $startTimeStr,
'end_time' => $endTimeStr
];
}
break;
case 'fixed':
// 固定时间范围类型
if (!empty($venue['fixed_time_ranges'])) {
$fixedRanges = json_decode($venue['fixed_time_ranges'], true);
if (is_array($fixedRanges)) {
foreach ($fixedRanges as $range) {
// 兼容不同的字段名格式
$startTime = $range['start_time'] ?? $range['start'] ?? '';
$endTime = $range['end_time'] ?? $range['end'] ?? '';
if ($startTime && $endTime) {
$slots[] = [
'time_slot' => $startTime . '-' . $endTime,
'start_time' => $startTime,
'end_time' => $endTime
];
}
}
}
}
break;
case 'all':
// 全天可用,生成默认时间段(8:00-22:00)
for ($hour = 8; $hour < 22; $hour++) {
$startTimeStr = str_pad($hour, 2, '0', STR_PAD_LEFT) . ':00';
$endTimeStr = str_pad($hour + 1, 2, '0', STR_PAD_LEFT) . ':00';
$slots[] = [
'time_slot' => $startTimeStr . '-' . $endTimeStr,
'start_time' => $startTimeStr,
'end_time' => $endTimeStr
];
}
break;
}
return $slots;
}
/**
* 检查场地时间冲突
* @param int $venueId
* @param string $date
* @param string $timeSlot
* @return array
*/
private function checkVenueConflict($venueId, $date, $timeSlot)
{
$conflict = Db::name('course_schedule')
->where('venue_id', $venueId)
->where('course_date', $date)
->where('time_slot', $timeSlot)
->where('deleted_at', 0)
->find();
if ($conflict) {
return [
'code' => 0,
'msg' => '该场地在该时间段已有课程安排'
];
}
return ['code' => 1];
}
/**
* 检查教练时间冲突
* @param int $coachId
* @param string $date
* @param string $timeSlot
* @return array
*/
public function checkCoachConflict($coachId, $date, $timeSlot)
{
$conflict = Db::name('course_schedule')
->where('coach_id', $coachId)
->where('course_date', $date)
->where('time_slot', $timeSlot)
->where('deleted_at', 0)
->find();
if ($conflict) {
return [
'code' => 0,
'msg' => '该教练在该时间段已有课程安排'
];
}
return ['code' => 1];
}
} }

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

@ -498,5 +498,50 @@ class CourseService extends BaseApiService
return $res; return $res;
} }
/**
* 获取课程列表(用于添加课程安排)
* @param array $data
* @return array
*/
public function getCourseListForSchedule(array $data)
{
try {
$where = [];
// 课程名称关键词搜索
if (!empty($data['keyword'])) {
$where[] = ['course_name', 'like', '%' . $data['keyword'] . '%'];
}
// 课程类型筛选
if (!empty($data['course_type'])) {
$where[] = ['course_type', '=', $data['course_type']];
}
// 只获取有效课程(未逻辑删除)
$where[] = ['deleted_at', '=', 0];
$courseList = $this->model
->where($where)
->field('id, course_name, course_type, duration, session_count, single_session_count, price')
->order('created_at DESC')
->select()
->toArray();
return [
'code' => 1,
'msg' => '获取成功',
'data' => $courseList
];
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '获取课程列表失败:' . $e->getMessage(),
'data' => []
];
}
}
} }

68
niucloud/app/service/api/apiService/PersonnelService.php

@ -728,4 +728,72 @@ class PersonnelService extends BaseApiService
return $res; return $res;
} }
/**
* 获取教练列表(用于添加课程安排)
* @param array $data
* @return array
*/
public function getCoachListForSchedule(array $data)
{
try {
$where = [];
// 查询条件:dept_id=2(教练部门)
$campusPersonWhere = ['dept_id' => 2];
// 校区筛选
if (!empty($data['campus_id'])) {
$campusPersonWhere['campus_id'] = $data['campus_id'];
}
// 查询符合条件的教练人员ID
$coachPersonIds = CampusPersonRole::where($campusPersonWhere)
->column('person_id');
if (empty($coachPersonIds)) {
return [
'code' => 1,
'msg' => '暂无教练数据',
'data' => []
];
}
// 构建人员表查询条件
$where[] = ['id', 'in', $coachPersonIds];
// 姓名关键词搜索
if (!empty($data['keyword'])) {
$where[] = ['name', 'like', '%' . $data['keyword'] . '%'];
}
// 状态筛选,默认获取有效教练
if (isset($data['status'])) {
$where[] = ['status', '=', $data['status']];
}
// 只获取未逻辑删除的教练
$where[] = ['deleted_at', '=', 0];
$coachList = $this->model
->where($where)
->field('id, name, head_img, phone, employee_number')
->order('create_time DESC')
->select()
->toArray();
return [
'code' => 1,
'msg' => '获取成功',
'data' => $coachList
];
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '获取教练列表失败:' . $e->getMessage(),
'data' => []
];
}
}
} }

51
niucloud/app/service/api/apiService/jlClassService.php

@ -197,4 +197,55 @@ class jlClassService extends BaseApiService
]; ];
return $arr; return $arr;
} }
/**
* 获取班级列表(用于添加课程安排)
* @param array $data
* @return array
*/
public function getClassListForSchedule(array $data)
{
try {
$where = [];
// 班级名称关键词搜索
if (!empty($data['keyword'])) {
$where[] = ['class_name', 'like', '%' . $data['keyword'] . '%'];
}
// 校区筛选
if (!empty($data['campus_id'])) {
$where[] = ['campus_id', '=', $data['campus_id']];
}
// 状态筛选,默认获取开启状态的班级
if (isset($data['status'])) {
$where[] = ['status', '=', $data['status']];
}
// 只获取未逻辑删除的班级
$where[] = ['deleted_at', '=', 0];
// 使用正确的模型来查询班级数据
$classList = \think\facade\Db::name('class')
->where($where)
->field('id, class_name, campus_id, campus_name, head_coach, assistant_coach, age_group, class_type, status')
->order('created_at DESC')
->select()
->toArray();
return [
'code' => 1,
'msg' => '获取成功',
'data' => $classList
];
} catch (\Exception $e) {
return [
'code' => 0,
'msg' => '获取班级列表失败:' . $e->getMessage(),
'data' => []
];
}
}
} }

4
niucloud/config/app.php

@ -28,5 +28,7 @@ return [
// 错误显示信息,非调试模式有效 // 错误显示信息,非调试模式有效
'error_message' => '页面错误!请稍后再试~', 'error_message' => '页面错误!请稍后再试~',
// 显示错误信息 // 显示错误信息
'show_error_msg' => false, 'show_error_msg' => true,
'app_debug' => env('app.debug', true),
'app_trace' => env('app.trace', true),
]; ];

1
niucloud/core/base/BaseAdminService.php

@ -24,6 +24,7 @@ class BaseAdminService extends BaseService
protected $username; protected $username;
protected $uid; protected $uid;
protected $campus_id;
public function __construct() public function __construct()
{ {

17
start.sh

@ -14,9 +14,9 @@ print_error() {
} }
# 设置项目名称 # 设置项目名称
PROJECT_NAME="MyApp" PROJECT_NAME="NiuCloud"
# 检查Docker和Docker Compose是否安装 # 检查Docker环境
check_docker() { check_docker() {
if ! command -v docker &> /dev/null; then if ! command -v docker &> /dev/null; then
print_error "Docker未安装,请先安装Docker" print_error "Docker未安装,请先安装Docker"
@ -115,6 +115,10 @@ upload_max_filesize = 20M
post_max_size = 20M post_max_size = 20M
max_execution_time = 300 max_execution_time = 300
date.timezone = Asia/Shanghai date.timezone = Asia/Shanghai
display_errors = On
error_reporting = E_ALL
log_errors = On
error_log = /var/log/php_errors.log
[opcache] [opcache]
opcache.enable=1 opcache.enable=1
@ -184,13 +188,13 @@ restart_services() {
docker-compose restart docker-compose restart
} }
# 查看状态 # 查看服务状态
check_status() { check_status() {
print_message "${PROJECT_NAME} 服务状态:" print_message "${PROJECT_NAME} 服务状态:"
docker-compose ps docker-compose ps
} }
# 查看日志 # 查看服务日志
view_logs() { view_logs() {
service=$1 service=$1
if [ -z "$service" ]; then if [ -z "$service" ]; then
@ -215,6 +219,11 @@ init_project() {
create_directories create_directories
check_config_files check_config_files
# 构建自定义 PHP 镜像
print_message "构建自定义 PHP 镜像..."
docker build -t niucloud-php:8.2 ./docker/php
# 拉取其他 Docker 镜像
print_message "拉取Docker镜像..." print_message "拉取Docker镜像..."
docker-compose pull docker-compose pull

24
startvps.sh

@ -0,0 +1,24 @@
#!/bin/bash
# start-claude-v2ray.sh
echo "检查 V2Ray 是否运行..."
if ! pgrep -x "v2ray" > /dev/null; then
echo "启动 V2Ray..."
nohup v2ray run -config config.json > /tmp/v2ray.log 2>&1 &
sleep 3
fi
echo "设置代理: 127.0.0.1:1087"
export HTTP_PROXY=http://127.0.0.1:1087
export HTTPS_PROXY=http://127.0.0.1:1087
# 测试连接
echo "测试代理连接..."
if curl --proxy $HTTP_PROXY --max-time 10 -s https://api.anthropic.com > /dev/null; then
echo "✅ 代理连接正常"
echo "启动 Claude Code..."
claude
else
echo "❌ 代理连接失败,请检查 V2Ray 配置"
exit 1
fi

234
uniapp/api/apiRoute.js

@ -29,6 +29,218 @@ export default {
async common_Dictionary(data = {}) { async common_Dictionary(data = {}) {
return await http.get('/common/getDictionary', data); return await http.get('/common/getDictionary', data);
}, },
// 课程安排列表
async getCourseScheduleList(data = {}) {
return await http.get('/courseSchedule/list', data);
},
// 课程安排时间段
async getCourseScheduleTimeSlots(data = {}) {
return await http.get('/courseSchedule/timeSlots', data);
},
// 课程安排筛选选项
async getCourseScheduleFilterOptions(data = {}) {
return await http.get('/courseSchedule/filterOptions', data);
},
// 课程安排详情
async getCourseScheduleInfo(data = {}) {
return await http.get('/courseSchedule/info', data);
},
// 提交课程点名
async submitScheduleSignIn(data = {}) {
return await http.post('/courseSchedule/signIn', data);
},
// 课程安排列表 - Mock版本
async getCourseScheduleListMock(data = {}) {
// 延迟模拟网络请求
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟课程数据
const mockCourses = [
{
id: 101,
date: '2025-07-14', // 周一
time: '09:00',
courseName: '少儿形体课',
students: '已报名8人',
teacher: '张教练',
teacher_id: 1,
status: '未点名',
type: 'normal', // 普通课程
venue: '舞蹈室A',
venue_id: 1,
class_id: 1,
duration: 1
},
{
id: 102,
date: '2025-07-14', // 周一
time: '14:00',
courseName: '成人瑜伽',
students: '已报名12人',
teacher: '李教练',
teacher_id: 2,
status: '已点名',
type: 'normal', // 普通课程
venue: '瑜伽B',
venue_id: 2,
class_id: 2,
duration: 1
},
{
id: 103,
date: '2025-07-15', // 周二
time: '10:00',
courseName: '私教训练',
students: '已报名1人',
teacher: '王教练',
teacher_id: 3,
status: '未点名',
type: 'private', // 私教课程
venue: '健身C',
venue_id: 3,
class_id: 3,
duration: 1
},
{
id: 104,
date: '2025-07-15', // 周二
time: '16:00',
courseName: '儿童游泳',
students: '已报名6人',
teacher: '刘教练',
teacher_id: 4,
status: '未点名',
type: 'normal', // 普通课程
venue: '泳池D',
venue_id: 4,
class_id: 4,
duration: 1
},
{
id: 105,
date: '2025-07-17', // 周四
time: '14:00',
courseName: '暑季特训营',
students: '已报名15人',
teacher: '赵教练',
teacher_id: 5,
status: '未点名',
type: 'activity', // 活动课程
venue: '综合场馆E',
venue_id: 5,
class_id: 5,
duration: 2 // 持续2小时
}
];
// 根据筛选条件过滤课程
let filteredCourses = [...mockCourses];
// 日期范围筛选
if (data.start_date && data.end_date) {
filteredCourses = filteredCourses.filter(course => {
return course.date >= data.start_date && course.date <= data.end_date;
});
}
// 教练筛选
if (data.coach_id) {
const coachIds = Array.isArray(data.coach_id) ? data.coach_id : [data.coach_id];
filteredCourses = filteredCourses.filter(course => {
return coachIds.includes(course.teacher_id);
});
}
// 场地筛选
if (data.venue_id) {
const venueIds = Array.isArray(data.venue_id) ? data.venue_id : [data.venue_id];
filteredCourses = filteredCourses.filter(course => {
return venueIds.includes(course.venue_id);
});
}
// 班级筛选
if (data.class_id) {
const classIds = Array.isArray(data.class_id) ? data.class_id : [data.class_id];
filteredCourses = filteredCourses.filter(course => {
return classIds.includes(course.class_id);
});
}
// 时间段筛选
if (data.time_range) {
switch (data.time_range) {
case 'morning': // 上午
filteredCourses = filteredCourses.filter(course => {
const hour = parseInt(course.time.split(':')[0]);
return hour >= 8 && hour < 12;
});
break;
case 'afternoon': // 下午
filteredCourses = filteredCourses.filter(course => {
const hour = parseInt(course.time.split(':')[0]);
return hour >= 12 && hour < 18;
});
break;
case 'evening': // 晚上
filteredCourses = filteredCourses.filter(course => {
const hour = parseInt(course.time.split(':')[0]);
return hour >= 18 && hour < 22;
});
break;
}
}
// 处理结果格式
const result = {
list: filteredCourses.map(course => ({
id: course.id,
course_date: course.date,
time_info: {
start_time: course.time,
end_time: this.calculateEndTime(course.time, course.duration),
duration: course.duration * 60
},
course_name: course.courseName,
enrolled_count: parseInt(course.students.match(/\d+/)[0]) || 0,
coach_name: course.teacher,
teacher_id: course.teacher_id,
status_text: course.status,
course_type: course.type,
venue_name: course.venue,
venue_id: course.venue_id,
class_id: course.class_id,
available_capacity: course.type === 'private' ? 1 : (course.type === 'activity' ? 30 : 15),
time_slot: `${course.time}-${this.calculateEndTime(course.time, course.duration)}`
})),
total: filteredCourses.length
};
return {
code: 1,
data: result,
msg: 'SUCCESS'
};
},
// 计算结束时间
calculateEndTime(startTime, duration) {
const [hours, minutes] = startTime.split(':').map(Number);
let endHours = hours + duration;
let endMinutes = minutes;
if (endHours >= 24) {
endHours -= 24;
}
return `${endHours.toString().padStart(2, '0')}:${endMinutes.toString().padStart(2, '0')}`;
},
//公共端-获取全部员工列表 //公共端-获取全部员工列表
async common_getPersonnelAll(data = {}) { async common_getPersonnelAll(data = {}) {
return await http.get('/personnel/getPersonnelAll', data); return await http.get('/personnel/getPersonnelAll', data);
@ -460,4 +672,26 @@ export default {
async submitScheduleSignIn(data = {}) { async submitScheduleSignIn(data = {}) {
return await http.post('/courseSchedule/signIn', data); return await http.post('/courseSchedule/signIn', data);
}, },
//↓↓↓↓↓↓↓↓↓↓↓↓-----添加课程安排页面专用接口-----↓↓↓↓↓↓↓↓↓↓↓↓
// 获取课程列表(用于添加课程安排)
async getCourseListForSchedule(data = {}) {
return await http.get('/course/list', data);
},
// 获取班级列表(用于添加课程安排)
async getClassListForSchedule(data = {}) {
return await http.get('/class/list', data);
},
// 获取教练列表(用于添加课程安排)
async getCoachListForSchedule(data = {}) {
return await http.get('/coach/list', data);
},
// 获取场地列表(用于添加课程安排)
async getVenueListForSchedule(data = {}) {
return await http.get('/venue/list', data);
},
// 获取场地可用时间段
async getVenueTimeSlots(data = {}) {
return await http.get('/venue/timeSlots', data);
},
} }

176
uniapp/components/schedule/ScheduleDetail.vue

@ -1,11 +1,5 @@
<template> <template>
<fui-modal <fui-modal :show="visible" title="课程安排详情" width="700" @cancel="closePopup" :buttons="[]">
:show="visible"
title="课程安排详情"
width="700"
@cancel="closePopup"
:buttons="[]"
>
<view class="schedule-detail" v-if="scheduleInfo"> <view class="schedule-detail" v-if="scheduleInfo">
<!-- 课程基本信息 --> <!-- 课程基本信息 -->
<view class="section basic-info"> <view class="section basic-info">
@ -28,17 +22,19 @@
</view> </view>
<view class="info-item"> <view class="info-item">
<text class="item-label">课程状态</text> <text class="item-label">课程状态</text>
<text class="item-value" :class="getStatusClass(scheduleInfo.status)">{{ scheduleInfo.status_text }}</text> <text :class="['item-value',statusClass]">{{ scheduleInfo.status_text }}</text>
</view> </view>
<view class="info-item"> <view class="info-item">
<text class="item-label">班级</text> <text class="item-label">班级</text>
<text class="item-value">{{ scheduleInfo.class_info ? scheduleInfo.class_info.class_name : '无班级' }}</text> <text
class="item-value">{{ scheduleInfo.class_info ? scheduleInfo.class_info.class_name : '无班级' }}</text>
</view> </view>
</view> </view>
<!-- 学员信息 --> <!-- 学员信息 -->
<view class="section students-info"> <view class="section students-info">
<view class="section-title">学员信息 ({{ scheduleInfo.students ? scheduleInfo.students.length : 0 }})</view> <view class="section-title">学员信息 ({{ scheduleInfo.students ? scheduleInfo.students.length : 0 }})
</view>
<view class="student-list" v-if="scheduleInfo.students && scheduleInfo.students.length > 0"> <view class="student-list" v-if="scheduleInfo.students && scheduleInfo.students.length > 0">
<view class="student-item" v-for="(student, index) in scheduleInfo.students" :key="index"> <view class="student-item" v-for="(student, index) in scheduleInfo.students" :key="index">
<view class="student-avatar"> <view class="student-avatar">
@ -46,7 +42,8 @@
</view> </view>
<view class="student-detail"> <view class="student-detail">
<text class="student-name">{{ student.name }}</text> <text class="student-name">{{ student.name }}</text>
<text class="student-status" :class="getStudentStatusClass(student.status)">{{ student.status_text }}</text> <text class=""
:class="['student-status',student.statusClass]">{{ student.status_text }}</text>
</view> </view>
</view> </view>
</view> </view>
@ -57,8 +54,10 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<view class="action-buttons"> <view class="action-buttons">
<fui-button type="primary" @click="handleSignIn" :disabled="scheduleInfo.status === 'completed'">课程点名</fui-button> <fui-button type="primary" @click="handleSignIn"
<fui-button type="default" @click="handleAdjustClass" :disabled="scheduleInfo.status === 'completed'">调整课程</fui-button> :disabled="scheduleInfo.status === 'completed'">课程点名</fui-button>
<fui-button type="default" @click="handleAdjustClass"
:disabled="scheduleInfo.status === 'completed'">调整课程</fui-button>
</view> </view>
<!-- 关闭按钮 --> <!-- 关闭按钮 -->
@ -83,9 +82,9 @@
</template> </template>
<script> <script>
import api from '@/api/apiRoute.js'; import api from '@/api/apiRoute.js';
export default { export default {
name: 'ScheduleDetail', name: 'ScheduleDetail',
props: { props: {
visible: { visible: {
@ -97,6 +96,30 @@ export default {
default: null default: null
} }
}, },
computed: {
statusClass() {
const statusMap = {
'pending': 'status-pending',
'upcoming': 'status-upcoming',
'ongoing': 'status-ongoing',
'completed': 'status-completed'
};
return statusMap[this.scheduleInfo.status] || '';
},
studentList() {
const statusMap = {
0: 'status-absent',
1: 'status-present',
2: 'status-leave'
};
return this.studentListRaw.map(student => ({
...student,
statusClass: statusMap[student.status] || 'status-absent',
status_text: this.getStatusText(student.status)
}));
}
},
data() { data() {
return { return {
loading: false, loading: false,
@ -130,7 +153,9 @@ export default {
this.error = false; this.error = false;
try { try {
const res = await api.getCourseScheduleInfo({ schedule_id: this.scheduleId }); const res = await api.getCourseScheduleInfo({
schedule_id: this.scheduleId
});
if (res.code === 1) { if (res.code === 1) {
this.scheduleInfo = res.data; this.scheduleInfo = res.data;
} else { } else {
@ -173,17 +198,6 @@ export default {
this.closePopup(); this.closePopup();
}, },
//
getStatusClass(status) {
const statusMap = {
'pending': 'status-pending',
'upcoming': 'status-upcoming',
'ongoing': 'status-ongoing',
'completed': 'status-completed'
};
return statusMap[status] || '';
},
// //
getStudentStatusClass(status) { getStudentStatusClass(status) {
const statusMap = { const statusMap = {
@ -192,68 +206,77 @@ export default {
2: 'status-leave' // 2: 'status-leave' //
}; };
return statusMap[status] || ''; return statusMap[status] || '';
},
//
getStatusText(status) {
const statusTextMap = {
0: '待上课',
1: '已上课',
2: '请假'
};
return statusTextMap[status] || '未知状态';
},
} }
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.schedule-detail { .schedule-detail {
padding: 20rpx; padding: 20rpx;
max-height: 80vh; max-height: 80vh;
overflow-y: auto; overflow-y: auto;
position: relative; position: relative;
} }
.section { .section {
margin-bottom: 30rpx; margin-bottom: 30rpx;
background-color: #2a2a2a; background-color: #2a2a2a;
border-radius: 12rpx; border-radius: 12rpx;
padding: 20rpx; padding: 20rpx;
} }
.section-title { .section-title {
font-size: 30rpx; font-size: 30rpx;
font-weight: bold; font-weight: bold;
color: #29d3b4; color: #29d3b4;
margin-bottom: 20rpx; margin-bottom: 20rpx;
border-bottom: 1px solid #3a3a3a; border-bottom: 1px solid #3a3a3a;
padding-bottom: 10rpx; padding-bottom: 10rpx;
} }
.info-item { .info-item {
display: flex; display: flex;
margin-bottom: 16rpx; margin-bottom: 16rpx;
font-size: 28rpx; font-size: 28rpx;
} }
.item-label { .item-label {
color: #999; color: #999;
width: 160rpx; width: 160rpx;
flex-shrink: 0; flex-shrink: 0;
} }
.item-value { .item-value {
color: #fff; color: #fff;
flex: 1; flex: 1;
} }
.student-list { .student-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 20rpx; gap: 20rpx;
} }
.student-item { .student-item {
display: flex; display: flex;
align-items: center; align-items: center;
background-color: #3a3a3a; background-color: #3a3a3a;
border-radius: 8rpx; border-radius: 8rpx;
padding: 15rpx; padding: 15rpx;
width: calc(50% - 10rpx); width: calc(50% - 10rpx);
} }
.student-avatar { .student-avatar {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
border-radius: 40rpx; border-radius: 40rpx;
@ -264,92 +287,93 @@ export default {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
} }
.student-detail { .student-detail {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.student-name { .student-name {
font-size: 28rpx; font-size: 28rpx;
color: #fff; color: #fff;
margin-bottom: 8rpx; margin-bottom: 8rpx;
} }
.student-status { .student-status {
font-size: 24rpx; font-size: 24rpx;
} }
.status-pending { .status-pending {
color: #ff9500; color: #ff9500;
} }
.status-upcoming { .status-upcoming {
color: #29d3b4; color: #29d3b4;
} }
.status-ongoing { .status-ongoing {
color: #007aff; color: #007aff;
} }
.status-completed { .status-completed {
color: #8e8e93; color: #8e8e93;
} }
.status-leave { .status-leave {
color: #ff3b30; color: #ff3b30;
} }
.action-buttons { .action-buttons {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
margin-top: 30rpx; margin-top: 30rpx;
gap: 30rpx; gap: 30rpx;
} }
.close-btn { .close-btn {
position: absolute; position: absolute;
top: 20rpx; top: 20rpx;
right: 20rpx; right: 20rpx;
z-index: 10; z-index: 10;
padding: 10rpx; padding: 10rpx;
} }
.loading, .error-message { .loading,
.error-message {
height: 300rpx; height: 300rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.loading-text { .loading-text {
margin-top: 20rpx; margin-top: 20rpx;
font-size: 28rpx; font-size: 28rpx;
color: #ccc; color: #ccc;
} }
.error-message { .error-message {
color: #ff6b6b; color: #ff6b6b;
font-size: 28rpx; font-size: 28rpx;
text-align: center; text-align: center;
} }
.retry-btn { .retry-btn {
margin-top: 30rpx; margin-top: 30rpx;
padding: 12rpx 30rpx; padding: 12rpx 30rpx;
background-color: #29d3b4; background-color: #29d3b4;
border-radius: 8rpx; border-radius: 8rpx;
color: #fff; color: #fff;
font-size: 24rpx; font-size: 24rpx;
} }
.empty-list { .empty-list {
padding: 40rpx 0; padding: 40rpx 0;
text-align: center; text-align: center;
color: #999; color: #999;
font-size: 28rpx; font-size: 28rpx;
} }
</style> </style>

1
uniapp/pages.json

@ -678,7 +678,6 @@
"path": "pages/coach/schedule/add_schedule", "path": "pages/coach/schedule/add_schedule",
"style": { "style": {
"navigationBarTitleText": "添加课程安排", "navigationBarTitleText": "添加课程安排",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#292929", "navigationBarBackgroundColor": "#292929",
"navigationBarTextStyle": "white" "navigationBarTextStyle": "white"
} }

8
uniapp/pages/coach/my/index.vue

@ -239,14 +239,6 @@
}) })
}, },
//
openViewSportsVenue() {
uni.showModal({
title: '我的体育场',
content: '当前分配场馆:xxx场馆\n地址:xxx\n联系电话:xxx',
showCancel: false
})
},
goCourseSchedule(){ goCourseSchedule(){
this.$navigateTo({ this.$navigateTo({
url: '/pages/coach/schedule/schedule_table' url: '/pages/coach/schedule/schedule_table'

154
uniapp/pages/coach/schedule/README.md

@ -173,3 +173,157 @@ handleCourseClick(course) {
### 样式错位 ### 样式错位
- 检查 flex 布局设置 - 检查 flex 布局设置
- 确保单元格宽度一致 - 确保单元格宽度一致
---
# 添加课程安排页面 API 设计文档
## 页面概述
添加课程安排页面用于创建新的课程安排,包含课程选择、班级选择、教练分配、场地预订、时间安排等功能。
## 数据库表结构映射
### 1. 课程下拉框
- **页面字段**: "请选择课程"
- **数据源**: `school_course`
- **需要字段**: `id`, `course_name`, `course_type`, `duration`, `session_count`, `single_session_count`
- **说明**: 显示所有可用课程供选择
### 2. 班级下拉框
- **页面字段**: "请选择班级(可选)"
- **数据源**: `school_class`
- **需要字段**: `id`, `class_name`, `campus_id`, `head_coach`, `assistant_coach`, `status`
- **筛选条件**: `status = 1` (开启状态)
- **说明**: 根据校区筛选可用班级,为可选字段
### 3. 授课教练下拉框
- **页面字段**: "请选择教练"
- **数据源**: `school_personnel` 表 + `school_campus_person_role`
- **查询条件**:
- `school_campus_person_role.dept_id = 2` (教练部门)
- 校区筛选逻辑:
- 如果 `campus_id != 0`: 查询指定校区的教练
- 如果 `campus_id = 0`: 查询所有校区的教练
- **需要字段**: `school_personnel.id`, `school_personnel.name`, `school_campus_person_role.campus_id`
### 4. 上课场地下拉框
- **页面字段**: "请选择场地"
- **数据源**: `school_venue`
- **需要字段**: `id`, `venue_name`, `capacity`, `availability_status`, `time_range_type`, `time_range_start`, `time_range_end`, `fixed_time_ranges`
- **筛选条件**: `availability_status = 1` (可用状态)
- **说明**: 根据校区筛选可用场地
### 5. 上课日期
- **页面字段**: "上课日期"
- **数据库字段**: `school_course_schedule.course_date`
- **格式**: DATE (YYYY-MM-DD)
### 6. 上课时间段
- **页面字段**: "请选择时间段"
- **数据库字段**: `school_course_schedule.time_slot`
- **格式**: VARCHAR "HH:MM-HH:MM" (如: "09:00-10:00")
- **说明**: 根据选中场地的时间限制动态生成可选时间段
### 7. 课程容量
- **页面字段**: "请输入课程容量"
- **数据库字段**: `school_course_schedule.available_capacity`
- **说明**: 不能超过场地的最大容量
### 8. 备注
- **页面字段**: "请输入备注信息(可选)"
- **数据库字段**: `school_course_schedule.remarks`
- **类型**: TEXT
- **说明**: 可选字段,用于记录额外信息
## 校区权限说明
系统根据当前用户所属校区进行数据筛选:
- **教练**: 通过 `school_campus_person_role` 表的 `campus_id` 字段筛选
- **班级**: 通过 `school_class` 表的 `campus_id` 字段筛选
- **场地**: 通过 `school_venue` 表的 `campus_id` 字段筛选
特殊情况:如果 `campus_id = 0`,则表示该资源对所有校区可用。
## 需要的API接口
### 1. 获取课程列表
**接口**: `GET /course/list`
**参数**: 无
**返回**: 课程列表数据
### 2. 获取班级列表
**接口**: `GET /class/list`
**参数**:
- `campus_id`: 校区ID(可选)
**返回**: 班级列表数据
### 3. 获取教练列表
**接口**: `GET /coach/list`
**参数**:
- `campus_id`: 校区ID(可选)
**返回**: 教练列表数据
**查询逻辑**:
```sql
SELECT p.id, p.name
FROM school_personnel p
JOIN school_campus_person_role r ON p.id = r.person_id
WHERE r.dept_id = 2
AND (r.campus_id = :campus_id OR r.campus_id = 0)
```
### 4. 获取场地列表
**接口**: `GET /venue/list`
**参数**:
- `campus_id`: 校区ID(可选)
**返回**: 场地列表数据
### 5. 获取可用时间段
**接口**: `GET /venue/timeSlots`
**参数**:
- `venue_id`: 场地ID
- `date`: 查询日期
**返回**: 可用时间段列表
**说明**: 根据场地的时间限制和已有安排计算可用时间段
### 6. 创建课程安排
**接口**: `POST /courseSchedule/create`
**参数**:
```json
{
"campus_id": 1,
"venue_id": 1,
"course_date": "2025-06-30",
"time_slot": "09:00-10:00",
"course_id": 1,
"coach_id": 1,
"available_capacity": 15,
"class_id": 1,
"remarks": "备注信息",
"created_by": "manual"
}
```
**返回**: 创建结果
## 数据验证规则
1. **课程容量验证**: 不能超过场地最大容量
2. **时间冲突检查**: 同一场地同一时间段不能有多个课程安排
3. **教练时间冲突**: 同一教练同一时间段不能安排多个课程
4. **场地可用时间**: 选择的时间段必须在场地可用时间范围内
5. **日期验证**: 不能选择过去的日期
## 业务逻辑
1. **自动计算可用容量**: 默认使用场地容量,用户可手动调整
2. **班级教练关联**: 选择班级后,可自动填充该班级的主教练
3. **场地时间限制**: 根据场地设置动态显示可选时间段
4. **重复课程检查**: 创建前检查是否存在冲突
## 状态管理
新创建的课程安排默认状态为 `pending`(待开始),系统会根据时间自动更新状态:
- `pending`: 待开始
- `upcoming`: 即将开始
- `ongoing`: 进行中
- `completed`: 已结束

117
uniapp/pages/coach/schedule/add_schedule.vue

@ -1,14 +1,5 @@
<template> <template>
<view class="add-schedule-container"> <view class="add-schedule-container">
<uni-nav-bar
title="添加课程安排"
left-icon="left"
fixed="true"
background-color="#292929"
color="#FFFFFF"
@clickLeft="goBack"
></uni-nav-bar>
<view class="form-container"> <view class="form-container">
<fui-form> <fui-form>
<!-- 课程选择 --> <!-- 课程选择 -->
@ -216,34 +207,46 @@ export default {
}); });
try { try {
const res = await api.getCourseScheduleFilterOptions(); //
const [courseRes, classRes, coachRes, venueRes] = await Promise.all([
api.getCourseListForSchedule(),
api.getClassListForSchedule(),
api.getCoachListForSchedule(),
api.getVenueListForSchedule()
]);
if (res.code === 1) {
// //
this.courseOptions = res.data.courses || []; if (courseRes.code === 1) {
this.courseOptions = courseRes.data || [];
}
// //
this.classOptions = res.data.classes || []; if (classRes.code === 1) {
this.classOptions = classRes.data || [];
}
// //
this.coachOptions = res.data.coaches || []; if (coachRes.code === 1) {
this.coachOptions = coachRes.data || [];
}
// //
this.venueOptions = res.data.venues || []; if (venueRes.code === 1) {
this.venueOptions = venueRes.data || [];
// }
this.generateTimeSlotOptions();
// //
if (this.prefillTimeSlot) { if (this.prefillTimeSlot) {
this.formData.time_slot = this.prefillTimeSlot; this.formData.time_slot = this.prefillTimeSlot;
} }
} else {
uni.showToast({ console.log('加载的数据:', {
title: res.msg || '加载筛选选项失败', courses: this.courseOptions.length,
icon: 'none' classes: this.classOptions.length,
coaches: this.coachOptions.length,
venues: this.venueOptions.length
}); });
}
} catch (error) { } catch (error) {
console.error('加载筛选选项失败:', error); console.error('加载筛选选项失败:', error);
uni.showToast({ uni.showToast({
@ -292,6 +295,45 @@ export default {
this.timeSlotOptions = timeSlots; this.timeSlotOptions = timeSlots;
}, },
//
async loadTimeSlots() {
if (!this.formData.venue_id || !this.formData.course_date) {
return;
}
try {
const res = await api.getVenueTimeSlots({
venue_id: this.formData.venue_id,
date: this.formData.course_date
});
if (res.code === 1) {
// API
this.timeSlotOptions = res.data.map(slot => ({
value: slot.time_slot,
text: slot.time_slot
}));
console.log('可用时间段:', this.timeSlotOptions);
} else {
// API使
this.generateTimeSlotOptions();
uni.showToast({
title: res.msg || '获取可用时间段失败,使用默认时间段',
icon: 'none'
});
}
} catch (error) {
console.error('加载时间段失败:', error);
// API使
this.generateTimeSlotOptions();
uni.showToast({
title: '获取可用时间段失败,使用默认时间段',
icon: 'none'
});
}
},
// //
onCourseSelect(e) { onCourseSelect(e) {
const index = e.index; const index = e.index;
@ -333,6 +375,11 @@ export default {
if (this.selectedVenue.capacity) { if (this.selectedVenue.capacity) {
this.formData.available_capacity = this.selectedVenue.capacity; this.formData.available_capacity = this.selectedVenue.capacity;
} }
//
if (this.formData.course_date) {
this.loadTimeSlots();
}
} }
this.showVenuePicker = false; this.showVenuePicker = false;
}, },
@ -340,6 +387,11 @@ export default {
onDateSelect(e) { onDateSelect(e) {
this.formData.course_date = e.result; this.formData.course_date = e.result;
this.showDatePicker = false; this.showDatePicker = false;
//
if (this.formData.venue_id) {
this.loadTimeSlots();
}
}, },
onTimeSelect(e) { onTimeSelect(e) {
@ -412,7 +464,23 @@ export default {
this.submitting = true; this.submitting = true;
try { try {
const res = await api.createCourseSchedule(this.formData); // API
const submitData = {
campus_id: 1, // ID
venue_id: this.formData.venue_id,
course_date: this.formData.course_date,
time_slot: this.formData.time_slot,
course_id: this.formData.course_id,
coach_id: this.formData.coach_id,
available_capacity: parseInt(this.formData.available_capacity),
class_id: this.formData.class_id || 0, //
remarks: this.formData.remark || '', //
created_by: 'manual'
};
console.log('提交数据:', submitData);
const res = await api.createCourseSchedule(submitData);
if (res.code === 1) { if (res.code === 1) {
uni.showToast({ uni.showToast({
@ -448,7 +516,6 @@ export default {
.add-schedule-container { .add-schedule-container {
min-height: 100vh; min-height: 100vh;
background-color: #18181c; background-color: #18181c;
padding-top: 88rpx;
} }
.form-container { .form-container {

566
uniapp/pages/coach/schedule/schedule_table.vue

@ -56,19 +56,80 @@
<!-- 课程表主体 --> <!-- 课程表主体 -->
<view class="schedule-main"> <view class="schedule-main">
<!-- 左侧固定列 -->
<view class="time-column-fixed">
<!-- 左上角标题 -->
<view class="time-header-cell">
<template v-if="activeFilter === 'time' || activeFilter === ''">时间</template>
<template v-else-if="activeFilter === 'teacher'">教练</template>
<template v-else-if="activeFilter === 'classroom'">教室</template>
<template v-else-if="activeFilter === 'class'">班级</template>
</view>
<!-- 左侧列内容 -->
<view class="time-rows-container">
<!-- 时间模式 -->
<template v-if="activeFilter === 'time' || activeFilter === ''">
<view
class="time-cell"
v-for="(timeSlot, timeIndex) in timeSlots"
:key="timeIndex"
:class="[!timeSlot.available ? 'time-unavailable' : '']"
>
{{ timeSlot.time }}
</view>
</template>
<!-- 教练模式 -->
<template v-else-if="activeFilter === 'teacher'">
<view
class="time-cell"
v-for="(teacher, index) in teacherOptions"
:key="teacher.id"
>
{{ teacher.name }}
</view>
</template>
<!-- 教室模式 -->
<template v-else-if="activeFilter === 'classroom'">
<view
class="time-cell"
v-for="(venue, index) in venues"
:key="venue.id"
>
{{ venue.name }}
</view>
</template>
<!-- 班级模式 -->
<template v-else-if="activeFilter === 'class'">
<view
class="time-cell"
v-for="(cls, index) in classOptions"
:key="cls.id"
>
{{ cls.name }}
</view>
</template>
</view>
</view>
<!-- 右侧可滚动内容区 -->
<view class="schedule-scrollable-area">
<!-- 水平滚动容器 --> <!-- 水平滚动容器 -->
<scroll-view <scroll-view
class="schedule-scroll-horizontal" class="schedule-scroll-horizontal"
scroll-x scroll-x
scroll-y
:scroll-left="scrollLeft" :scroll-left="scrollLeft"
@scroll="onHorizontalScroll" :scroll-top="scrollTop"
@scroll="onScroll"
:style="{ width: '100%' }"
> >
<view class="schedule-table" :style="{ width: tableWidth + 'rpx' }"> <view class="scroll-content" :style="{ width: tableWidth + 'rpx', minWidth: '1400rpx' }">
<!-- 表头 - 日期行 --> <!-- 表头 - 日期行 -->
<view class="table-header"> <view class="table-header">
<!-- 左上角时间标题 -->
<view class="time-header-cell">时间</view>
<!-- 日期列 --> <!-- 日期列 -->
<view <view
class="date-header-cell" class="date-header-cell"
@ -81,36 +142,61 @@
</view> </view>
</view> </view>
<!-- 表格内容 - 垂直滚动 --> <!-- 课程内容区 -->
<scroll-view <view class="course-content-area">
class="schedule-scroll-vertical" <!-- 时间模式内容 -->
scroll-y <template v-if="activeFilter === 'time' || activeFilter === ''">
:scroll-top="scrollTop"
@scroll="onVerticalScroll"
>
<view class="table-body">
<view <view
class="time-row" class="time-row"
v-for="(timeSlot, timeIndex) in timeSlots" v-for="(timeSlot, timeIndex) in timeSlots"
:key="timeIndex" :key="timeIndex"
> >
<!-- 时间 --> <!-- 课程 -->
<view <view
:class="['time-cell', !timeSlot.available ? 'time-unavailable' : '']" :class="['course-cell',!timeSlot.available ? 'cell-unavailable' : '']"
v-for="(date, dateIndex) in weekDates"
:key="dateIndex"
@click="timeSlot.available ? handleCellClick(timeSlot, date) : null"
> >
{{ timeSlot.time }} <!-- 课程项目 -->
<view
class="course-item"
v-for="course in getCoursesByTimeAndDate(timeSlot.time, date.date)"
:key="course.id"
:class="[
course.type === 'normal' ? 'course-normal' : '',
course.type === 'private' ? 'course-private' : '',
course.type === 'activity' ? 'course-activity' : ''
]"
@click.stop="viewScheduleDetail(course.id)"
>
<view class="course-name">{{ course.courseName }}</view>
<view class="course-students">{{ course.students }}</view>
<view class="course-teacher">{{ course.teacher }}</view>
<view class="course-status">{{ course.status }}</view>
</view> </view>
</view>
</view>
</template>
<!-- 教练模式内容 -->
<template v-else-if="activeFilter === 'teacher'">
<view
class="time-row"
v-for="(teacher, teacherIndex) in teacherOptions"
:key="teacher.id"
>
<!-- 课程列 --> <!-- 课程列 -->
<view <view
:class="['course-cell',!timeSlot.available ? 'cell-unavailable' : '']" class="course-cell"
v-for="(date, dateIndex) in weekDates" v-for="(date, dateIndex) in weekDates"
:key="dateIndex" :key="dateIndex"
@click="timeSlot.available ? handleCellClick(timeSlot, date) : null" @click="handleCellClick({time: ''}, date, teacher.id)"
> >
<!-- 课程项目 -->
<view <view
class="course-item" class="course-item"
v-for="course in getCoursesByTimeAndDate(timeSlot.time, date.date)" v-for="course in getCoursesByTeacherAndDate(teacher.id, date.date)"
:key="course.id" :key="course.id"
:class="[ :class="[
course.type === 'normal' ? 'course-normal' : '', course.type === 'normal' ? 'course-normal' : '',
@ -120,17 +206,88 @@
@click.stop="viewScheduleDetail(course.id)" @click.stop="viewScheduleDetail(course.id)"
> >
<view class="course-name">{{ course.courseName }}</view> <view class="course-name">{{ course.courseName }}</view>
<view class="course-time">{{ course.time }}</view>
<view class="course-students">{{ course.students }}</view> <view class="course-students">{{ course.students }}</view>
<view class="course-status">{{ course.status }}</view>
</view>
</view>
</view>
</template>
<!-- 教室模式内容 -->
<template v-else-if="activeFilter === 'classroom'">
<view
class="time-row"
v-for="(venue, venueIndex) in venues"
:key="venue.id"
>
<!-- 课程列 -->
<view
class="course-cell"
v-for="(date, dateIndex) in weekDates"
:key="dateIndex"
@click="handleCellClick({time: ''}, date, null, venue.id)"
>
<!-- 课程项目 -->
<view
class="course-item"
v-for="course in getCoursesByVenueAndDate(venue.id, date.date)"
:key="course.id"
:class="[
course.type === 'normal' ? 'course-normal' : '',
course.type === 'private' ? 'course-private' : '',
course.type === 'activity' ? 'course-activity' : ''
]"
@click.stop="viewScheduleDetail(course.id)"
>
<view class="course-name">{{ course.courseName }}</view>
<view class="course-time">{{ course.time }}</view>
<view class="course-teacher">{{ course.teacher }}</view>
<view class="course-status">{{ course.status }}</view>
</view>
</view>
</view>
</template>
<!-- 班级模式内容 -->
<template v-else-if="activeFilter === 'class'">
<view
class="time-row"
v-for="(cls, clsIndex) in classOptions"
:key="cls.id"
>
<!-- 课程列 -->
<view
class="course-cell"
v-for="(date, dateIndex) in weekDates"
:key="dateIndex"
@click="handleCellClick({time: ''}, date, null, null, cls.id)"
>
<!-- 课程项目 -->
<view
class="course-item"
v-for="course in getCoursesByClassAndDate(cls.id, date.date)"
:key="course.id"
:class="[
course.type === 'normal' ? 'course-normal' : '',
course.type === 'private' ? 'course-private' : '',
course.type === 'activity' ? 'course-activity' : ''
]"
@click.stop="viewScheduleDetail(course.id)"
>
<view class="course-name">{{ course.courseName }}</view>
<view class="course-time">{{ course.time }}</view>
<view class="course-teacher">{{ course.teacher }}</view> <view class="course-teacher">{{ course.teacher }}</view>
<view class="course-status">{{ course.status }}</view> <view class="course-status">{{ course.status }}</view>
</view> </view>
</view> </view>
</view> </view>
</template>
</view> </view>
</scroll-view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
</view>
<!-- 添加按钮 --> <!-- 添加按钮 -->
<view class="add-btn" @click="addCourse"> <view class="add-btn" @click="addCourse">
@ -266,9 +423,10 @@ export default {
// //
scrollLeft: 0, scrollLeft: 0,
scrollTop: 0, scrollTop: 0,
scrollAnimationFrame: null, //
// //
tableWidth: 1200, // tableWidth: 1500, // 7 (7*180+120=1380rpx)
// //
timeSlots: [], timeSlots: [],
@ -353,6 +511,22 @@ export default {
this.initTimeSlots() this.initTimeSlots()
this.loadFilterOptions() this.loadFilterOptions()
this.loadScheduleList() this.loadScheduleList()
//
this.initMockData()
//
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
//
window.removeEventListener('resize', this.handleResize)
//
if (this.scrollAnimationFrame) {
cancelAnimationFrame(this.scrollAnimationFrame)
}
}, },
methods: { methods: {
@ -360,8 +534,15 @@ export default {
initCurrentWeek() { initCurrentWeek() {
const today = new Date() const today = new Date()
const currentDay = today.getDay() // 01 const currentDay = today.getDay() // 01
const diff = today.getDate() - currentDay + (currentDay === 0 ? -6 : 1) //
const monday = new Date(today.setDate(diff)) //
let diff = 1 - currentDay //
if (currentDay === 0) { // 7
diff = -6
}
const monday = new Date(today)
monday.setDate(today.getDate() + diff)
this.currentWeekStart = monday.toISOString().split('T')[0] this.currentWeekStart = monday.toISOString().split('T')[0]
// //
@ -513,13 +694,61 @@ export default {
// //
getCoursesByTimeAndDate(time, date) { getCoursesByTimeAndDate(time, date) {
return this.courses.filter(course => return this.courses.filter(course =>
course.time === time && course.date === date, course.time === time && course.date === date
) )
}, },
//
getCoursesByTeacherAndDate(teacherId, date) {
return this.courses.filter(course => {
// ID
const teacherMatch = course.teacher_id === teacherId ||
(this.teacherOptions.find(t => t.name === course.teacher)?.id === teacherId);
//
return teacherMatch && course.date === date;
});
},
//
getCoursesByVenueAndDate(venueId, date) {
return this.courses.filter(course => {
// ID
const venueMatch = course.venue_id === venueId ||
(this.venues.find(v => v.name === course.venue)?.id === venueId);
//
return venueMatch && course.date === date;
});
},
//
getCoursesByClassAndDate(classId, date) {
return this.courses.filter(course => {
// ID
const classMatch = course.class_id === classId ||
(this.classOptions.find(c => course.courseName.includes(c.name))?.id === classId);
//
return classMatch && course.date === date;
});
},
// //
openFilter(type) { openFilter(type) {
this.activeFilter = this.activeFilter === type ? '' : type //
const newFilter = this.activeFilter === type ? '' : type;
//
if (this.activeFilter !== newFilter) {
this.activeFilter = newFilter;
//
this.scrollLeft = 0;
this.scrollTop = 0;
//
if (this.scrollAnimationFrame) {
cancelAnimationFrame(this.scrollAnimationFrame);
}
}
}, },
// //
@ -528,7 +757,7 @@ export default {
}, },
// //
handleFilterConfirm(e) { async handleFilterConfirm(e) {
if (e.index === 0) { if (e.index === 0) {
// //
this.resetFilters() this.resetFilters()
@ -536,6 +765,16 @@ export default {
// //
this.applyFilters() this.applyFilters()
this.closeFilterModal() this.closeFilterModal()
//
await this.loadScheduleList()
this.scrollLeft = 0
this.scrollTop = 0
//
if (this.selectedTimeRange !== '' || this.selectedVenueId !== null) {
this.initTimeSlots()
}
} }
}, },
@ -623,7 +862,28 @@ export default {
// //
async loadFilterOptions() { async loadFilterOptions() {
try { try {
const res = await api.getCourseScheduleFilterOptions() // 使
// API
// const res = await api.getCourseScheduleFilterOptions()
const res = {
code: 1,
data: {
coaches: this.teacherOptions,
venues: this.venues.map(venue => ({
id: venue.id,
venue_name: venue.name,
capacity: venue.capacity,
description: venue.description || ''
})),
classes: this.classOptions.map(cls => ({
id: cls.id,
class_name: cls.name,
class_level: cls.level,
total_students: cls.students
}))
}
}
if (res.code === 1) { if (res.code === 1) {
const data = res.data const data = res.data
@ -669,7 +929,8 @@ export default {
try { try {
this.loading = true this.loading = true
const res = await api.getCourseScheduleList(this.filterParams) // 使 API
const res = await api.getCourseScheduleListMock(this.filterParams)
if (res.code === 1) { if (res.code === 1) {
// //
@ -716,22 +977,56 @@ export default {
} }
}, },
// //
onHorizontalScroll(e) { onScroll(e) {
if (this.scrollAnimationFrame) {
cancelAnimationFrame(this.scrollAnimationFrame)
}
this.scrollAnimationFrame = requestAnimationFrame(() => {
//
if (e.detail.scrollLeft !== undefined) {
this.scrollLeft = e.detail.scrollLeft this.scrollLeft = e.detail.scrollLeft
}, }
// if (e.detail.scrollTop !== undefined) {
onVerticalScroll(e) {
this.scrollTop = e.detail.scrollTop this.scrollTop = e.detail.scrollTop
//
this.$nextTick(() => {
const timeRowsContainer = this.$el.querySelector('.time-rows-container')
if (timeRowsContainer) {
timeRowsContainer.scrollTop = this.scrollTop
}
})
}
})
}, },
// //
handleCellClick(timeSlot, date) { handleCellClick(timeSlot, date, teacherId = null, venueId = null, classId = null) {
// // URL
uni.navigateTo({ let url = `/pages/coach/schedule/add_schedule?date=${date.date}`;
url: `/pages/coach/schedule/add_schedule?date=${date.date}&time=${timeSlot.time}&time_slot=${timeSlot.timeSlot}`,
}) //
if (timeSlot && timeSlot.time) {
url += `&time=${timeSlot.time}&time_slot=${timeSlot.timeSlot || ''}`;
}
if (teacherId) {
url += `&coach_id=${teacherId}`;
}
if (venueId) {
url += `&venue_id=${venueId}`;
}
if (classId) {
url += `&class_id=${classId}`;
}
//
uni.navigateTo({ url });
}, },
// //
@ -762,6 +1057,49 @@ export default {
url: `/pages/coach/schedule/adjust_course?id=${data.scheduleId}`, url: `/pages/coach/schedule/adjust_course?id=${data.scheduleId}`,
}) })
}, },
//
handleResize() {
//
const width = window.innerWidth
if (width <= 375) {
this.tableWidth = 1220 // 7*160+100=1220rpx
} else if (width <= 768) {
this.tableWidth = 1400
} else {
this.tableWidth = 1500
}
},
//
initMockData() {
//
this.teacherOptions = [
{ id: 1, name: '张教练', avatar: '/static/avatar/teacher1.png' },
{ id: 2, name: '李教练', avatar: '/static/avatar/teacher2.png' },
{ id: 3, name: '王教练', avatar: '/static/avatar/teacher3.png' },
{ id: 4, name: '刘教练', avatar: '/static/avatar/teacher4.png' },
{ id: 5, name: '赵教练', avatar: '/static/avatar/teacher5.png' }
];
//
this.venues = [
{ id: 1, name: '舞蹈室A', capacity: 20, time_range_type: 'range', time_range_start: '09:00', time_range_end: '21:00' },
{ id: 2, name: '瑜伽B', capacity: 15, time_range_type: 'range', time_range_start: '08:00', time_range_end: '20:00' },
{ id: 3, name: '健身C', capacity: 10, time_range_type: 'all' },
{ id: 4, name: '泳池D', capacity: 8, time_range_type: 'fixed', fixed_time_ranges: ['09:00-11:00', '14:00-18:00'] },
{ id: 5, name: '综合场馆E', capacity: 30, time_range_type: 'range', time_range_start: '08:00', time_range_end: '22:00' }
];
//
this.classOptions = [
{ id: 1, name: '少儿形体班', level: '初级', students: 15 },
{ id: 2, name: '成人瑜伽班', level: '中级', students: 20 },
{ id: 3, name: '私教VIP班', level: '高级', students: 5 },
{ id: 4, name: '儿童游泳班', level: '初级', students: 12 },
{ id: 5, name: '暑期特训班', level: '混合', students: 25 }
];
},
}, },
} }
</script> </script>
@ -844,6 +1182,68 @@ export default {
// //
.schedule-main { .schedule-main {
flex: 1;
position: relative;
display: flex;
overflow: hidden;
}
//
.time-column-fixed {
width: 120rpx;
position: relative;
z-index: 2;
background-color: #292929;
display: flex;
flex-direction: column;
box-shadow: 4rpx 0 8rpx rgba(0, 0, 0, 0.1);
}
.time-header-cell {
width: 120rpx;
height: 120rpx; //
padding: 20rpx 10rpx;
color: #29d3b4;
font-size: 28rpx;
font-weight: 500;
text-align: center;
border-right: 1px solid #555;
background-color: #434544;
border-bottom: 2px solid #29d3b4;
}
.time-rows-container {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: hidden;
min-height: 1000rpx; /* 确保有足够的高度能滚动 */
}
.time-cell {
width: 120rpx;
height: 120rpx; /* 固定高度保证对齐 */
min-height: 120rpx;
padding: 20rpx 10rpx;
color: #999;
font-size: 24rpx;
text-align: center;
border-right: 1px solid #434544;
border-bottom: 1px solid #434544;
background: #3a3a3a;
display: flex;
align-items: center;
justify-content: center;
&.time-unavailable {
background: #2a2a2a;
color: #555;
opacity: 0.5;
}
}
//
.schedule-scrollable-area {
flex: 1; flex: 1;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@ -852,44 +1252,35 @@ export default {
.schedule-scroll-horizontal { .schedule-scroll-horizontal {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: scroll;
} }
.schedule-table { .scroll-content {
min-width: 100%;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 1500rpx;
} }
//
.table-header { .table-header {
display: flex; display: flex;
background: #434544; background: #434544;
border-bottom: 2px solid #29d3b4; border-bottom: 2px solid #29d3b4;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 10; z-index: 1;
} min-width: 1260rpx; /* 7天 * 180rpx = 1260rpx */
.time-header-cell {
width: 120rpx;
min-width: 120rpx;
padding: 20rpx 10rpx;
color: #29d3b4;
font-size: 28rpx;
font-weight: 500;
text-align: center;
border-right: 1px solid #555;
} }
.date-header-cell { .date-header-cell {
width: 150rpx; width: 180rpx;
min-width: 150rpx; min-width: 180rpx;
padding: 15rpx 10rpx; padding: 15rpx 10rpx;
color: #fff; color: #fff;
font-size: 24rpx; font-size: 24rpx;
text-align: center; text-align: center;
border-right: 1px solid #555; border-right: 1px solid #555;
background: #434544;
flex-shrink: 0;
.date-week { .date-week {
font-size: 26rpx; font-size: 26rpx;
@ -909,45 +1300,28 @@ export default {
} }
} }
// .course-content-area {
.schedule-scroll-vertical {
flex: 1;
height: 100%;
}
.table-body {
width: 100%; width: 100%;
min-height: 1000rpx; /* 确保有足够的高度能滚动 */
min-width: 1260rpx; /* 7天 * 180rpx = 1260rpx */
} }
.time-row { .time-row {
display: flex; display: flex;
height: 120rpx; /* 固定高度保证对齐 */
min-height: 120rpx; min-height: 120rpx;
border-bottom: 1px solid #434544; border-bottom: 1px solid #434544;
} }
.time-cell {
width: 120rpx;
min-width: 120rpx;
padding: 20rpx 10rpx;
color: #999;
font-size: 24rpx;
text-align: center;
border-right: 1px solid #434544;
background: #3a3a3a;
&.time-unavailable {
background: #2a2a2a;
color: #555;
opacity: 0.5;
}
}
.course-cell { .course-cell {
width: 150rpx; width: 180rpx;
min-width: 150rpx; min-width: 180rpx;
padding: 10rpx; padding: 10rpx;
border-right: 1px solid #434544; border-right: 1px solid #434544;
border-bottom: 1px solid #434544;
position: relative; position: relative;
background: #292929;
flex-shrink: 0;
&.cell-unavailable { &.cell-unavailable {
background: #1e1e1e; background: #1e1e1e;
@ -967,6 +1341,28 @@ export default {
} }
} }
//
@media screen and (max-width: 375px) {
.time-column-fixed {
width: 100rpx;
}
.time-header-cell, .time-cell {
width: 100rpx;
font-size: 22rpx;
}
.course-cell {
width: 160rpx;
min-width: 160rpx;
}
.date-header-cell {
width: 160rpx;
min-width: 160rpx;
}
}
// //
.course-item { .course-item {
width: 100%; width: 100%;
@ -998,6 +1394,12 @@ export default {
margin-bottom: 3rpx; margin-bottom: 3rpx;
} }
.course-time {
color: #29d3b4;
font-size: 22rpx;
margin-bottom: 3rpx;
}
.course-students { .course-students {
color: #ccc; color: #ccc;
margin-bottom: 3rpx; margin-bottom: 3rpx;

206
uniapp/pages/coach/schedule/sign_in.vue

@ -1,13 +1,7 @@
<template> <template>
<view class="sign-in-container"> <view class="sign-in-container">
<uni-nav-bar <uni-nav-bar title="课程点名" left-icon="left" fixed="true" background-color="#292929" color="#FFFFFF"
title="课程点名" @clickLeft="goBack"></uni-nav-bar>
left-icon="left"
fixed="true"
background-color="#292929"
color="#FFFFFF"
@clickLeft="goBack"
></uni-nav-bar>
<view class="content"> <view class="content">
<!-- 课程信息 --> <!-- 课程信息 -->
@ -46,15 +40,11 @@
</view> </view>
<view class="student-list" v-else> <view class="student-list" v-else>
<view <view class="student-item" v-for="(student, index) in studentList" :key="index"
class="student-item" @click="toggleStudentStatus(index)">
v-for="(student, index) in studentList"
:key="index"
@click="toggleStudentStatus(index)"
>
<view class="student-avatar"> <view class="student-avatar">
<image :src="student.avatar || '/static/icon-img/avatar.png'" mode="aspectFill"></image> <image :src="student.avatar || '/static/icon-img/avatar.png'" mode="aspectFill"></image>
<view class="status-badge" :class="getStatusClass(student.status)"></view> <view :class="['status-badge',student.statusClass]"></view>
</view> </view>
<view class="student-info"> <view class="student-info">
@ -64,25 +54,16 @@
<view class="status-container"> <view class="status-container">
<view class="status-select"> <view class="status-select">
<view <view class="status-option" :class="{ active: student.status === 1 }"
class="status-option" @click.stop="setStudentStatus(index, 1)">
:class="{ active: student.status === 1 }"
@click.stop="setStudentStatus(index, 1)"
>
已到 已到
</view> </view>
<view <view class="status-option" :class="{ active: student.status === 2 }"
class="status-option" @click.stop="setStudentStatus(index, 2)">
:class="{ active: student.status === 2 }"
@click.stop="setStudentStatus(index, 2)"
>
请假 请假
</view> </view>
<view <view class="status-option" :class="{ active: student.status === 0 }"
class="status-option" @click.stop="setStudentStatus(index, 0)">
:class="{ active: student.status === 0 }"
@click.stop="setStudentStatus(index, 0)"
>
未到 未到
</view> </view>
</view> </view>
@ -94,11 +75,7 @@
<!-- 点名备注 --> <!-- 点名备注 -->
<view class="remark-section"> <view class="remark-section">
<view class="section-title">点名备注</view> <view class="section-title">点名备注</view>
<fui-textarea <fui-textarea v-model="signInRemark" placeholder="请输入点名备注(可选)" maxlength="200"></fui-textarea>
v-model="signInRemark"
placeholder="请输入点名备注(可选)"
maxlength="200"
></fui-textarea>
</view> </view>
<!-- 提交按钮 --> <!-- 提交按钮 -->
@ -110,9 +87,9 @@
</template> </template>
<script> <script>
import api from '@/api/apiRoute.js'; import api from '@/api/apiRoute.js';
export default { export default {
data() { data() {
return { return {
// ID // ID
@ -131,7 +108,30 @@ export default {
submitting: false submitting: false
}; };
}, },
computed: {
statusClass() {
const statusMap = {
'pending': 'status-pending',
'upcoming': 'status-upcoming',
'ongoing': 'status-ongoing',
'completed': 'status-completed'
};
return statusMap[this.scheduleInfo.status] || '';
},
studentList() {
const statusMap = {
0: 'status-absent',
1: 'status-present',
2: 'status-leave'
};
return this.studentListRaw.map(student => ({
...student,
statusClass: statusMap[student.status] || 'status-absent',
status_text: this.getStatusText(student.status)
}));
}
},
onLoad(options) { onLoad(options) {
if (options.id) { if (options.id) {
this.scheduleId = options.id; this.scheduleId = options.id;
@ -160,7 +160,9 @@ export default {
}); });
try { try {
const res = await api.getCourseScheduleInfo({ schedule_id: this.scheduleId }); const res = await api.getCourseScheduleInfo({
schedule_id: this.scheduleId
});
if (res.code === 1) { if (res.code === 1) {
this.scheduleInfo = res.data; this.scheduleInfo = res.data;
@ -196,7 +198,15 @@ export default {
return statusMap[status] || 'status-absent'; return statusMap[status] || 'status-absent';
}, },
//
getStatusText(status) {
const statusTextMap = {
0: '待上课',
1: '已上课',
2: '请假'
};
return statusTextMap[status] || '未知状态';
},
// //
toggleStudentStatus(index) { toggleStudentStatus(index) {
const student = this.studentList[index]; const student = this.studentList[index];
@ -293,47 +303,47 @@ export default {
} }
} }
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.sign-in-container { .sign-in-container {
min-height: 100vh; min-height: 100vh;
background-color: #18181c; background-color: #18181c;
padding-top: 88rpx; padding-top: 88rpx;
} }
.content { .content {
padding: 30rpx; padding: 30rpx;
} }
.course-info-card { .course-info-card {
background-color: #23232a; background-color: #23232a;
border-radius: 12rpx; border-radius: 12rpx;
padding: 24rpx; padding: 24rpx;
margin-bottom: 30rpx; margin-bottom: 30rpx;
} }
.course-title { .course-title {
font-size: 32rpx; font-size: 32rpx;
font-weight: bold; font-weight: bold;
color: #fff; color: #fff;
margin-bottom: 10rpx; margin-bottom: 10rpx;
} }
.course-time { .course-time {
font-size: 26rpx; font-size: 26rpx;
color: #29d3b4; color: #29d3b4;
margin-bottom: 20rpx; margin-bottom: 20rpx;
} }
.course-detail { .course-detail {
background-color: #2a2a2a; background-color: #2a2a2a;
border-radius: 8rpx; border-radius: 8rpx;
padding: 16rpx; padding: 16rpx;
} }
.detail-item { .detail-item {
display: flex; display: flex;
margin-bottom: 10rpx; margin-bottom: 10rpx;
font-size: 26rpx; font-size: 26rpx;
@ -341,49 +351,49 @@ export default {
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
.detail-label { .detail-label {
color: #999; color: #999;
width: 140rpx; width: 140rpx;
} }
.detail-value { .detail-value {
color: #fff; color: #fff;
flex: 1; flex: 1;
} }
.student-section { .student-section {
background-color: #23232a; background-color: #23232a;
border-radius: 12rpx; border-radius: 12rpx;
padding: 24rpx; padding: 24rpx;
margin-bottom: 30rpx; margin-bottom: 30rpx;
} }
.section-header { .section-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 24rpx; margin-bottom: 24rpx;
} }
.section-title { .section-title {
font-size: 30rpx; font-size: 30rpx;
font-weight: bold; font-weight: bold;
color: #fff; color: #fff;
} }
.action-buttons { .action-buttons {
display: flex; display: flex;
gap: 16rpx; gap: 16rpx;
} }
.student-list { .student-list {
max-height: 600rpx; max-height: 600rpx;
overflow-y: auto; overflow-y: auto;
} }
.student-item { .student-item {
display: flex; display: flex;
align-items: center; align-items: center;
background-color: #2a2a2a; background-color: #2a2a2a;
@ -394,9 +404,9 @@ export default {
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
.student-avatar { .student-avatar {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
border-radius: 40rpx; border-radius: 40rpx;
@ -408,9 +418,9 @@ export default {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
} }
.status-badge { .status-badge {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
right: 0; right: 0;
@ -419,47 +429,47 @@ export default {
border-radius: 12rpx; border-radius: 12rpx;
background-color: #999; background-color: #999;
border: 2rpx solid #fff; border: 2rpx solid #fff;
} }
.status-absent { .status-absent {
background-color: #ff3b30; background-color: #ff3b30;
} }
.status-present { .status-present {
background-color: #34c759; background-color: #34c759;
} }
.status-leave { .status-leave {
background-color: #ff9500; background-color: #ff9500;
} }
.student-info { .student-info {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.student-name { .student-name {
font-size: 28rpx; font-size: 28rpx;
color: #fff; color: #fff;
margin-bottom: 6rpx; margin-bottom: 6rpx;
} }
.student-phone { .student-phone {
font-size: 24rpx; font-size: 24rpx;
color: #999; color: #999;
} }
.status-container { .status-container {
margin-left: 16rpx; margin-left: 16rpx;
} }
.status-select { .status-select {
display: flex; display: flex;
gap: 10rpx; gap: 10rpx;
} }
.status-option { .status-option {
padding: 8rpx 16rpx; padding: 8rpx 16rpx;
font-size: 24rpx; font-size: 24rpx;
border-radius: 30rpx; border-radius: 30rpx;
@ -469,9 +479,9 @@ export default {
&.active { &.active {
background-color: #29d3b4; background-color: #29d3b4;
} }
} }
.empty-list { .empty-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -488,9 +498,9 @@ export default {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: #999;
} }
} }
.remark-section { .remark-section {
background-color: #23232a; background-color: #23232a;
border-radius: 12rpx; border-radius: 12rpx;
padding: 24rpx; padding: 24rpx;
@ -502,10 +512,10 @@ export default {
color: #fff; color: #fff;
margin-bottom: 20rpx; margin-bottom: 20rpx;
} }
} }
.submit-btn { .submit-btn {
margin-top: 40rpx; margin-top: 40rpx;
padding-bottom: 40rpx; padding-bottom: 40rpx;
} }
</style> </style>

22
uniapp/pages/market/my/index.vue

@ -48,10 +48,20 @@
<view></view> <view></view>
</view> </view>
<view class="item" @click="goCourseSchedule()">
<view>课程安排</view>
<view></view>
</view>
<view class="item" @click="openViewMyMessage()"> <view class="item" @click="openViewMyMessage()">
<view>我的消息</view> <view>我的消息</view>
<view></view> <view></view>
</view> </view>
<view class="item" @click="my_contract()">
<view>我的合同</view>
<view></view>
</view>
</view> </view>
<view class="section_box"> <view class="section_box">
@ -225,6 +235,18 @@ export default {
url: '/pages/market/reimbursement/list' url: '/pages/market/reimbursement/list'
}) })
}, },
goCourseSchedule(){
this.$navigateTo({
url: '/pages/coach/schedule/schedule_table'
})
},
my_contract(){
this.$navigateTo({
url: '/pages/common/contract/my_contract'
})
},
} }
} }
</script> </script>

Loading…
Cancel
Save