Browse Source

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

master
王泽彦 9 months ago
parent
commit
fb68775d70
  1. 2
      admin/src/app/views/dict/components/dict.vue
  2. 29
      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. 2
      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. 78
      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. 84
      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="value" />
<el-table-column
label="备注"
label="排序"
align="center"
min-width="100px"
prop="sort"

29
docker-compose.yml

@ -3,7 +3,7 @@ version: '3.8'
services:
# PHP 服务
php:
image: php:8.2-fpm
image: niucloud-php:8.2
container_name: niucloud_php
volumes:
- ./niucloud:/var/www/html
@ -15,16 +15,6 @@ services:
- redis
environment:
- 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:
- niucloud_network
@ -33,8 +23,8 @@ services:
image: nginx:alpine
container_name: niucloud_nginx
ports:
- "20080:80" # 原本是 80 映射到 20080
- "20081:8080" # 原本是 8080 映射到 20081
- "20080:80"
- "20081:8080"
volumes:
- ./niucloud:/var/www/html
- ./admin/dist:/var/www/admin
@ -52,7 +42,7 @@ services:
image: mysql:8.0
container_name: niucloud_mysql
ports:
- "23306:3306" # 原本是 3306 映射到 23306
- "23306:3306"
environment:
MYSQL_ROOT_PASSWORD: root123456
MYSQL_DATABASE: niucloud
@ -71,7 +61,7 @@ services:
image: redis:alpine
container_name: niucloud_redis
ports:
- "26379:6379" # 原本是 6379 映射到 26379
- "26379:6379"
volumes:
- ./docker/data/redis:/data
- ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf
@ -88,7 +78,7 @@ services:
- ./admin:/app
- ./docker/data/node_modules:/app/node_modules
ports:
- "23000:3000" # 原本是 3000 映射到 23000
- "23000:5173" # 映射到 Vite 默认开发端口 5173
command: >
sh -c "
npm config set registry https://registry.npmmirror.com &&
@ -98,7 +88,7 @@ services:
networks:
- niucloud_network
# Composer 服务 (用于 PHP 依赖管理)
# Composer 服务 (可选)
composer:
image: composer:latest
container_name: niucloud_composer
@ -112,8 +102,3 @@ services:
networks:
niucloud_network:
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));
}
/**
* 获取班级列表(用于添加课程安排)
* @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)
{
$data = $request->all();
$result = (new CourseScheduleService())->createSchedule($data);
try {
$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']) {
return fail($result['msg']);
}
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)
{
$data = $request->all();
return success((new CourseScheduleService())->getVenueList($data));
try {
$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)
{
try {
$data = $this->request->params([
["venue_id", 0],
["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)
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('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('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()
{
$this->performanceService = new PerformanceService();
$this->performanceService = null;
}
/**

6
niucloud/app/middleware.php

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

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

@ -50,7 +50,7 @@ class Dict extends BaseModel
public function searchNameAttr($query, $value, $data)
{
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()];
}
}
/**
* 获取场地列表(用于添加课程安排)
* @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;
}
/**
* 获取课程列表(用于添加课程安排)
* @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;
}
/**
* 获取教练列表(用于添加课程安排)
* @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;
}
/**
* 获取班级列表(用于添加课程安排)
* @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' => '页面错误!请稍后再试~',
// 显示错误信息
'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 $uid;
protected $campus_id;
public function __construct()
{

17
start.sh

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

78
uniapp/components/schedule/ScheduleDetail.vue

@ -1,11 +1,5 @@
<template>
<fui-modal
:show="visible"
title="课程安排详情"
width="700"
@cancel="closePopup"
:buttons="[]"
>
<fui-modal :show="visible" title="课程安排详情" width="700" @cancel="closePopup" :buttons="[]">
<view class="schedule-detail" v-if="scheduleInfo">
<!-- 课程基本信息 -->
<view class="section basic-info">
@ -28,17 +22,19 @@
</view>
<view class="info-item">
<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 class="info-item">
<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 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-item" v-for="(student, index) in scheduleInfo.students" :key="index">
<view class="student-avatar">
@ -46,7 +42,8 @@
</view>
<view class="student-detail">
<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>
@ -57,8 +54,10 @@
<!-- 操作按钮 -->
<view class="action-buttons">
<fui-button type="primary" @click="handleSignIn" :disabled="scheduleInfo.status === 'completed'">课程点名</fui-button>
<fui-button type="default" @click="handleAdjustClass" :disabled="scheduleInfo.status === 'completed'">调整课程</fui-button>
<fui-button type="primary" @click="handleSignIn"
:disabled="scheduleInfo.status === 'completed'">课程点名</fui-button>
<fui-button type="default" @click="handleAdjustClass"
:disabled="scheduleInfo.status === 'completed'">调整课程</fui-button>
</view>
<!-- 关闭按钮 -->
@ -97,6 +96,30 @@ export default {
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() {
return {
loading: false,
@ -130,7 +153,9 @@ export default {
this.error = false;
try {
const res = await api.getCourseScheduleInfo({ schedule_id: this.scheduleId });
const res = await api.getCourseScheduleInfo({
schedule_id: this.scheduleId
});
if (res.code === 1) {
this.scheduleInfo = res.data;
} else {
@ -173,17 +198,6 @@ export default {
this.closePopup();
},
//
getStatusClass(status) {
const statusMap = {
'pending': 'status-pending',
'upcoming': 'status-upcoming',
'ongoing': 'status-ongoing',
'completed': 'status-completed'
};
return statusMap[status] || '';
},
//
getStudentStatusClass(status) {
const statusMap = {
@ -192,7 +206,16 @@ export default {
2: 'status-leave' //
};
return statusMap[status] || '';
}
},
//
getStatusText(status) {
const statusTextMap = {
0: '待上课',
1: '已上课',
2: '请假'
};
return statusTextMap[status] || '未知状态';
},
}
}
</script>
@ -317,7 +340,8 @@ export default {
padding: 10rpx;
}
.loading, .error-message {
.loading,
.error-message {
height: 300rpx;
display: flex;
flex-direction: column;

1
uniapp/pages.json

@ -678,7 +678,6 @@
"path": "pages/coach/schedule/add_schedule",
"style": {
"navigationBarTitleText": "添加课程安排",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#292929",
"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(){
this.$navigateTo({
url: '/pages/coach/schedule/schedule_table'

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

@ -173,3 +173,157 @@ handleCourseClick(course) {
### 样式错位
- 检查 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>
<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">
<fui-form>
<!-- 课程选择 -->
@ -216,34 +207,46 @@ export default {
});
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 || [];
//
this.generateTimeSlotOptions();
if (venueRes.code === 1) {
this.venueOptions = venueRes.data || [];
}
//
if (this.prefillTimeSlot) {
this.formData.time_slot = this.prefillTimeSlot;
}
} else {
uni.showToast({
title: res.msg || '加载筛选选项失败',
icon: 'none'
console.log('加载的数据:', {
courses: this.courseOptions.length,
classes: this.classOptions.length,
coaches: this.coachOptions.length,
venues: this.venueOptions.length
});
}
} catch (error) {
console.error('加载筛选选项失败:', error);
uni.showToast({
@ -292,6 +295,45 @@ export default {
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) {
const index = e.index;
@ -333,6 +375,11 @@ export default {
if (this.selectedVenue.capacity) {
this.formData.available_capacity = this.selectedVenue.capacity;
}
//
if (this.formData.course_date) {
this.loadTimeSlots();
}
}
this.showVenuePicker = false;
},
@ -340,6 +387,11 @@ export default {
onDateSelect(e) {
this.formData.course_date = e.result;
this.showDatePicker = false;
//
if (this.formData.venue_id) {
this.loadTimeSlots();
}
},
onTimeSelect(e) {
@ -412,7 +464,23 @@ export default {
this.submitting = true;
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) {
uni.showToast({
@ -448,7 +516,6 @@ export default {
.add-schedule-container {
min-height: 100vh;
background-color: #18181c;
padding-top: 88rpx;
}
.form-container {

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

@ -56,19 +56,80 @@
<!-- 课程表主体 -->
<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
class="schedule-scroll-horizontal"
scroll-x
scroll-y
: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="time-header-cell">时间</view>
<!-- 日期列 -->
<view
class="date-header-cell"
@ -81,36 +142,61 @@
</view>
</view>
<!-- 表格内容 - 垂直滚动 -->
<scroll-view
class="schedule-scroll-vertical"
scroll-y
:scroll-top="scrollTop"
@scroll="onVerticalScroll"
>
<view class="table-body">
<!-- 课程内容区 -->
<view class="course-content-area">
<!-- 时间模式内容 -->
<template v-if="activeFilter === 'time' || activeFilter === ''">
<view
class="time-row"
v-for="(timeSlot, timeIndex) in timeSlots"
:key="timeIndex"
>
<!-- 时间 -->
<!-- 课程 -->
<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>
</template>
<!-- 教练模式内容 -->
<template v-else-if="activeFilter === 'teacher'">
<view
class="time-row"
v-for="(teacher, teacherIndex) in teacherOptions"
:key="teacher.id"
>
<!-- 课程列 -->
<view
:class="['course-cell',!timeSlot.available ? 'cell-unavailable' : '']"
class="course-cell"
v-for="(date, dateIndex) in weekDates"
:key="dateIndex"
@click="timeSlot.available ? handleCellClick(timeSlot, date) : null"
@click="handleCellClick({time: ''}, date, teacher.id)"
>
<!-- 课程项目 -->
<view
class="course-item"
v-for="course in getCoursesByTimeAndDate(timeSlot.time, date.date)"
v-for="course in getCoursesByTeacherAndDate(teacher.id, date.date)"
:key="course.id"
:class="[
course.type === 'normal' ? 'course-normal' : '',
@ -120,17 +206,88 @@
@click.stop="viewScheduleDetail(course.id)"
>
<view class="course-name">{{ course.courseName }}</view>
<view class="course-time">{{ course.time }}</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-status">{{ course.status }}</view>
</view>
</view>
</view>
</template>
</view>
</scroll-view>
</view>
</scroll-view>
</view>
</view>
<!-- 添加按钮 -->
<view class="add-btn" @click="addCourse">
@ -266,9 +423,10 @@ export default {
//
scrollLeft: 0,
scrollTop: 0,
scrollAnimationFrame: null, //
//
tableWidth: 1200, //
tableWidth: 1500, // 7 (7*180+120=1380rpx)
//
timeSlots: [],
@ -353,6 +511,22 @@ export default {
this.initTimeSlots()
this.loadFilterOptions()
this.loadScheduleList()
//
this.initMockData()
//
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
//
window.removeEventListener('resize', this.handleResize)
//
if (this.scrollAnimationFrame) {
cancelAnimationFrame(this.scrollAnimationFrame)
}
},
methods: {
@ -360,8 +534,15 @@ export default {
initCurrentWeek() {
const today = new Date()
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]
//
@ -513,13 +694,61 @@ export default {
//
getCoursesByTimeAndDate(time, date) {
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) {
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) {
//
this.resetFilters()
@ -536,6 +765,16 @@ export default {
//
this.applyFilters()
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() {
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) {
const data = res.data
@ -669,7 +929,8 @@ export default {
try {
this.loading = true
const res = await api.getCourseScheduleList(this.filterParams)
// 使 API
const res = await api.getCourseScheduleListMock(this.filterParams)
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
},
}
//
onVerticalScroll(e) {
if (e.detail.scrollTop !== undefined) {
this.scrollTop = e.detail.scrollTop
//
this.$nextTick(() => {
const timeRowsContainer = this.$el.querySelector('.time-rows-container')
if (timeRowsContainer) {
timeRowsContainer.scrollTop = this.scrollTop
}
})
}
})
},
//
handleCellClick(timeSlot, date) {
//
uni.navigateTo({
url: `/pages/coach/schedule/add_schedule?date=${date.date}&time=${timeSlot.time}&time_slot=${timeSlot.timeSlot}`,
})
handleCellClick(timeSlot, date, teacherId = null, venueId = null, classId = null) {
// URL
let url = `/pages/coach/schedule/add_schedule?date=${date.date}`;
//
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}`,
})
},
//
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>
@ -844,6 +1182,68 @@ export default {
//
.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;
position: relative;
overflow: hidden;
@ -852,44 +1252,35 @@ export default {
.schedule-scroll-horizontal {
width: 100%;
height: 100%;
overflow: scroll;
}
.schedule-table {
min-width: 100%;
height: 100%;
.scroll-content {
display: flex;
flex-direction: column;
min-width: 1500rpx;
}
//
.table-header {
display: flex;
background: #434544;
border-bottom: 2px solid #29d3b4;
position: sticky;
top: 0;
z-index: 10;
}
.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;
z-index: 1;
min-width: 1260rpx; /* 7天 * 180rpx = 1260rpx */
}
.date-header-cell {
width: 150rpx;
min-width: 150rpx;
width: 180rpx;
min-width: 180rpx;
padding: 15rpx 10rpx;
color: #fff;
font-size: 24rpx;
text-align: center;
border-right: 1px solid #555;
background: #434544;
flex-shrink: 0;
.date-week {
font-size: 26rpx;
@ -909,45 +1300,28 @@ export default {
}
}
//
.schedule-scroll-vertical {
flex: 1;
height: 100%;
}
.table-body {
.course-content-area {
width: 100%;
min-height: 1000rpx; /* 确保有足够的高度能滚动 */
min-width: 1260rpx; /* 7天 * 180rpx = 1260rpx */
}
.time-row {
display: flex;
height: 120rpx; /* 固定高度保证对齐 */
min-height: 120rpx;
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 {
width: 150rpx;
min-width: 150rpx;
width: 180rpx;
min-width: 180rpx;
padding: 10rpx;
border-right: 1px solid #434544;
border-bottom: 1px solid #434544;
position: relative;
background: #292929;
flex-shrink: 0;
&.cell-unavailable {
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 {
width: 100%;
@ -998,6 +1394,12 @@ export default {
margin-bottom: 3rpx;
}
.course-time {
color: #29d3b4;
font-size: 22rpx;
margin-bottom: 3rpx;
}
.course-students {
color: #ccc;
margin-bottom: 3rpx;

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

@ -1,13 +1,7 @@
<template>
<view class="sign-in-container">
<uni-nav-bar
title="课程点名"
left-icon="left"
fixed="true"
background-color="#292929"
color="#FFFFFF"
@clickLeft="goBack"
></uni-nav-bar>
<uni-nav-bar title="课程点名" left-icon="left" fixed="true" background-color="#292929" color="#FFFFFF"
@clickLeft="goBack"></uni-nav-bar>
<view class="content">
<!-- 课程信息 -->
@ -46,15 +40,11 @@
</view>
<view class="student-list" v-else>
<view
class="student-item"
v-for="(student, index) in studentList"
:key="index"
@click="toggleStudentStatus(index)"
>
<view class="student-item" v-for="(student, index) in studentList" :key="index"
@click="toggleStudentStatus(index)">
<view class="student-avatar">
<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 class="student-info">
@ -64,25 +54,16 @@
<view class="status-container">
<view class="status-select">
<view
class="status-option"
:class="{ active: student.status === 1 }"
@click.stop="setStudentStatus(index, 1)"
>
<view class="status-option" :class="{ active: student.status === 1 }"
@click.stop="setStudentStatus(index, 1)">
已到
</view>
<view
class="status-option"
:class="{ active: student.status === 2 }"
@click.stop="setStudentStatus(index, 2)"
>
<view class="status-option" :class="{ active: student.status === 2 }"
@click.stop="setStudentStatus(index, 2)">
请假
</view>
<view
class="status-option"
:class="{ active: student.status === 0 }"
@click.stop="setStudentStatus(index, 0)"
>
<view class="status-option" :class="{ active: student.status === 0 }"
@click.stop="setStudentStatus(index, 0)">
未到
</view>
</view>
@ -94,11 +75,7 @@
<!-- 点名备注 -->
<view class="remark-section">
<view class="section-title">点名备注</view>
<fui-textarea
v-model="signInRemark"
placeholder="请输入点名备注(可选)"
maxlength="200"
></fui-textarea>
<fui-textarea v-model="signInRemark" placeholder="请输入点名备注(可选)" maxlength="200"></fui-textarea>
</view>
<!-- 提交按钮 -->
@ -131,7 +108,30 @@ export default {
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) {
if (options.id) {
this.scheduleId = options.id;
@ -160,7 +160,9 @@ export default {
});
try {
const res = await api.getCourseScheduleInfo({ schedule_id: this.scheduleId });
const res = await api.getCourseScheduleInfo({
schedule_id: this.scheduleId
});
if (res.code === 1) {
this.scheduleInfo = res.data;
@ -196,7 +198,15 @@ export default {
return statusMap[status] || 'status-absent';
},
//
getStatusText(status) {
const statusTextMap = {
0: '待上课',
1: '已上课',
2: '请假'
};
return statusTextMap[status] || '未知状态';
},
//
toggleStudentStatus(index) {
const student = this.studentList[index];

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

@ -48,10 +48,20 @@
<view></view>
</view>
<view class="item" @click="goCourseSchedule()">
<view>课程安排</view>
<view></view>
</view>
<view class="item" @click="openViewMyMessage()">
<view>我的消息</view>
<view></view>
</view>
<view class="item" @click="my_contract()">
<view>我的合同</view>
<view></view>
</view>
</view>
<view class="section_box">
@ -225,6 +235,18 @@ export default {
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>

Loading…
Cancel
Save