Browse Source

修改 bug

master
王泽彦 8 months ago
parent
commit
e2cd720267
  1. 434
      admin/src/app/views/campus_pay/components/campus-pay-edit.vue
  2. 2
      niucloud/app/adminapi/controller/campus_pay/CampusPay.php
  3. 2
      niucloud/app/api/controller/apiController/CoachStudent.php
  4. 44
      niucloud/app/job/schedule/HandleCourseSchedule.php
  5. 7
      niucloud/app/service/admin/upload/UploadService.php
  6. 40
      niucloud/app/service/api/apiService/CoachStudentService.php
  7. 34
      niucloud/app/service/api/apiService/PersonnelService.php
  8. 78
      niucloud/test_upload.php
  9. 71
      test-student-orders-api.js
  10. 125
      uniapp/pages-coach/coach/schedule/schedule_table.vue
  11. 256
      uniapp/pages-coach/coach/student/student_list.vue

434
admin/src/app/views/campus_pay/components/campus-pay-edit.vue

@ -1,216 +1,218 @@
<template> <template>
<el-dialog v-model="showDialog" :title="formData.id ? t('updateCampusPay') : t('addCampusPay')" width="50%" class="diy-dialog-wrap" :destroy-on-close="true"> <el-dialog v-model="showDialog" :title="formData.id ? t('updateCampusPay') : t('addCampusPay')" width="50%" class="diy-dialog-wrap" :destroy-on-close="true">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading"> <el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('campusId')" prop="campus_id"> <el-form-item :label="t('campusId')" prop="campus_id">
<el-select class="input-width" v-model="formData.campus_id" clearable :placeholder="t('campusIdPlaceholder')"> <el-select class="input-width" v-model="formData.campus_id" clearable :placeholder="t('campusIdPlaceholder')">
<el-option label="请选择" value=""></el-option> <el-option label="请选择" value=""></el-option>
<el-option <el-option
v-for="(item, index) in campusIdList" v-for="(item, index) in campusIdList"
:key="index" :key="index"
:label="item['campus_name']" :label="item['campus_name']"
:value="item['id']" :value="item['id']"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item :label="t('mchid')" prop="mchid"> <el-form-item :label="t('mchid')" prop="mchid">
<el-input v-model="formData.mchid" clearable :placeholder="t('mchidPlaceholder')" class="input-width" /> <el-input v-model="formData.mchid" clearable :placeholder="t('mchidPlaceholder')" class="input-width" />
</el-form-item> </el-form-item>
<el-form-item :label="t('paySignKey')" prop="pay_sign_key"> <el-form-item :label="t('paySignKey')" prop="pay_sign_key">
<el-input v-model="formData.pay_sign_key" clearable :placeholder="t('paySignKeyPlaceholder')" class="input-width" /> <el-input v-model="formData.pay_sign_key" clearable :placeholder="t('paySignKeyPlaceholder')" class="input-width" />
</el-form-item> </el-form-item>
<el-form-item :label="t('apiclientKey')"> <el-form-item :label="t('apiclientKey')">
<upload-file v-model="formData.apiclient_key" /> <upload-file v-model="formData.apiclient_key" api="sys/document/cert"/>
</el-form-item> </el-form-item>
<el-form-item :label="t('apiclientCert')"> <el-form-item :label="t('apiclientCert')">
<upload-file v-model="formData.apiclient_cert" /> <upload-file v-model="formData.apiclient_cert" api="sys/document/cert"/>
</el-form-item> </el-form-item>
<el-form-item :label="t('wxPayKey')"> <el-form-item :label="t('wxPayKey')">
<upload-file v-model="formData.wx_pay_key" /> <upload-file v-model="formData.wx_pay_key" api="sys/document/cert"/>
</el-form-item> </el-form-item>
<el-form-item :label="t('wxPayKeyId')" prop="wx_pay_key_id"> <el-form-item :label="t('wxPayKeyId')" prop="wx_pay_key_id">
<el-input v-model="formData.wx_pay_key_id" clearable :placeholder="t('wxPayKeyIdPlaceholder')" class="input-width" /> <el-input v-model="formData.wx_pay_key_id" clearable :placeholder="t('wxPayKeyIdPlaceholder')" class="input-width" />
</el-form-item> </el-form-item>
</el-form>
</el-form>
<template #footer>
<span class="dialog-footer"> <template #footer>
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button> <span class="dialog-footer">
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ <el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
t('confirm') <el-button type="primary" :loading="loading" @click="confirm(formRef)">{{
}}</el-button> t('confirm')
</span> }}</el-button>
</template> </span>
</el-dialog> </template>
</template> </el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue' <script lang="ts" setup>
import { useDictionary } from '@/app/api/dict' import { ref, reactive, computed, watch } from 'vue'
import { t } from '@/lang' import { useDictionary } from '@/app/api/dict'
import type { FormInstance } from 'element-plus' import { t } from '@/lang'
import { addCampusPay, editCampusPay, getCampusPayInfo, getWithCampusList } from '@/app/api/campus_pay' import type { FormInstance } from 'element-plus'
import { addCampusPay, editCampusPay, getCampusPayInfo, getWithCampusList } from '@/app/api/campus_pay'
let showDialog = ref(false)
const loading = ref(false) let showDialog = ref(false)
const loading = ref(false)
/**
* 表单数据 /**
*/ * 表单数据
const initialFormData = { */
id: '', const initialFormData = {
campus_id: '', id: '',
mchid: '', campus_id: '',
pay_sign_key: '', mchid: '',
apiclient_key: '', pay_sign_key: '',
apiclient_cert: '', apiclient_key: '',
wx_pay_key: '', apiclient_cert: '',
wx_pay_key_id: '', wx_pay_key: '',
} wx_pay_key_id: '',
const formData: Record<string, any> = reactive({ ...initialFormData })
}
const formRef = ref<FormInstance>() const formData: Record<string, any> = reactive({ ...initialFormData })
// const formRef = ref<FormInstance>()
const formRules = computed(() => {
return { //
campus_id: [ const formRules = computed(() => {
{ required: true, message: t('campusIdPlaceholder'), trigger: 'blur' }, return {
campus_id: [
] { required: true, message: t('campusIdPlaceholder'), trigger: 'blur' },
,
mchid: [ ]
{ required: true, message: t('mchidPlaceholder'), trigger: 'blur' }, ,
mchid: [
] { required: true, message: t('mchidPlaceholder'), trigger: 'blur' },
,
pay_sign_key: [ ]
{ required: true, message: t('paySignKeyPlaceholder'), trigger: 'blur' }, ,
pay_sign_key: [
] { required: true, message: t('paySignKeyPlaceholder'), trigger: 'blur' },
,
apiclient_key: [ ]
{ required: true, message: t('apiclientKeyPlaceholder'), trigger: 'blur' }, ,
apiclient_key: [
] { required: true, message: t('apiclientKeyPlaceholder'), trigger: 'blur' },
,
apiclient_cert: [ ]
{ required: true, message: t('apiclientCertPlaceholder'), trigger: 'blur' }, ,
apiclient_cert: [
] { required: true, message: t('apiclientCertPlaceholder'), trigger: 'blur' },
,
wx_pay_key: [ ]
{ required: true, message: t('wxPayKeyPlaceholder'), trigger: 'blur' }, ,
wx_pay_key: [
] { required: true, message: t('wxPayKeyPlaceholder'), trigger: 'blur' },
,
wx_pay_key_id: [ ]
{ required: true, message: t('wxPayKeyIdPlaceholder'), trigger: 'blur' }, ,
wx_pay_key_id: [
] { required: true, message: t('wxPayKeyIdPlaceholder'), trigger: 'blur' },
,
} ]
}) ,
}
const emit = defineEmits(['complete']) })
/** const emit = defineEmits(['complete'])
* 确认
* @param formEl /**
*/ * 确认
const confirm = async (formEl: FormInstance | undefined) => { * @param formEl
if (loading.value || !formEl) return */
let save = formData.id ? editCampusPay : addCampusPay const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => { let save = formData.id ? editCampusPay : addCampusPay
if (valid) {
loading.value = true await formEl.validate(async (valid) => {
if (valid) {
let data = formData loading.value = true
save(data).then(res => { let data = formData
loading.value = false
showDialog.value = false save(data).then(res => {
emit('complete') loading.value = false
}).catch(err => { showDialog.value = false
loading.value = false emit('complete')
}) }).catch(err => {
} loading.value = false
}) })
} }
})
// }
//
const campusIdList = ref([] as any[])
const setCampusIdList = async () => {
campusIdList.value = await (await getWithCampusList({})).data const campusIdList = ref([] as any[])
} const setCampusIdList = async () => {
setCampusIdList() campusIdList.value = await (await getWithCampusList({})).data
const setFormData = async (row: any = null) => { }
Object.assign(formData, initialFormData) setCampusIdList()
loading.value = true const setFormData = async (row: any = null) => {
if(row){ Object.assign(formData, initialFormData)
const data = await (await getCampusPayInfo(row.id)).data loading.value = true
if (data) Object.keys(formData).forEach((key: string) => { if(row){
if (data[key] != undefined) formData[key] = data[key] const data = await (await getCampusPayInfo(row.id)).data
}) if (data) Object.keys(formData).forEach((key: string) => {
} if (data[key] != undefined) formData[key] = data[key]
loading.value = false })
} }
loading.value = false
// }
const mobileVerify = (rule: any, value: any, callback: any) => {
if (value && !/^1[3-9]\d{9}$/.test(value)) { //
callback(new Error(t('generateMobile'))) const mobileVerify = (rule: any, value: any, callback: any) => {
} else { if (value && !/^1[3-9]\d{9}$/.test(value)) {
callback() callback(new Error(t('generateMobile')))
} } else {
} callback()
}
// }
const idCardVerify = (rule: any, value: any, callback: any) => {
if (value && !/^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(value)) { //
callback(new Error(t('generateIdCard'))) const idCardVerify = (rule: any, value: any, callback: any) => {
} else { if (value && !/^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(value)) {
callback() callback(new Error(t('generateIdCard')))
} } else {
} callback()
}
// }
const emailVerify = (rule: any, value: any, callback: any) => {
if (value && !/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(value)) { //
callback(new Error(t('generateEmail'))) const emailVerify = (rule: any, value: any, callback: any) => {
} else { if (value && !/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(value)) {
callback() callback(new Error(t('generateEmail')))
} } else {
} callback()
}
// }
const numberVerify = (rule: any, value: any, callback: any) => {
if (!Number.isInteger(value)) { //
callback(new Error(t('generateNumber'))) const numberVerify = (rule: any, value: any, callback: any) => {
} else { if (!Number.isInteger(value)) {
callback() callback(new Error(t('generateNumber')))
} } else {
} callback()
}
defineExpose({ }
showDialog,
setFormData defineExpose({
}) showDialog,
</script> setFormData
})
<style lang="scss" scoped></style> </script>
<style lang="scss">
.diy-dialog-wrap .el-form-item__label{ <style lang="scss" scoped></style>
height: auto !important; <style lang="scss">
} .diy-dialog-wrap .el-form-item__label{
</style> height: auto !important;
}
</style>

2
niucloud/app/adminapi/controller/campus_pay/CampusPay.php

@ -58,7 +58,6 @@ class CampusPay extends BaseAdminController
["wx_pay_key_id",""], ["wx_pay_key_id",""],
]); ]);
$this->validate($data, 'app\validate\campus_pay\CampusPay.add');
$id = (new CampusPayService())->add($data); $id = (new CampusPayService())->add($data);
return success('ADD_SUCCESS', ['id' => $id]); return success('ADD_SUCCESS', ['id' => $id]);
} }
@ -79,7 +78,6 @@ class CampusPay extends BaseAdminController
["wx_pay_key_id",""], ["wx_pay_key_id",""],
]); ]);
$this->validate($data, 'app\validate\campus_pay\CampusPay.edit');
(new CampusPayService())->edit($id, $data); (new CampusPayService())->edit($id, $data);
return success('EDIT_SUCCESS'); return success('EDIT_SUCCESS');
} }

2
niucloud/app/api/controller/apiController/CoachStudent.php

@ -37,6 +37,8 @@ class CoachStudent extends BaseApiService
["status", 0], // 学员状态 ["status", 0], // 学员状态
["course_id", 0], // 课程ID搜索 ["course_id", 0], // 课程ID搜索
["class_id", 0], // 班级ID搜索 ["class_id", 0], // 班级ID搜索
["page", 1], // 页码
["limit", 10], // 每页数量
["debug", false] // 调试模式 ["debug", false] // 调试模式
]); ]);

44
niucloud/app/job/schedule/HandleCourseSchedule.php

@ -27,7 +27,7 @@ class HandleCourseSchedule extends BaseJob
$lockFile = runtime_path() . 'course_status_update.lock'; $lockFile = runtime_path() . 'course_status_update.lock';
if (file_exists($lockFile) && (time() - filemtime($lockFile)) < 300) { // 5分钟锁定 if (file_exists($lockFile) && (time() - filemtime($lockFile)) < 300) { // 5分钟锁定
Log::write('课程状态更新任务正在执行中,跳过'); Log::write('课程状态更新任务正在执行中,跳过');
return ['status' => 'skipped', 'reason' => 'locked']; return false;
} }
// 创建锁文件 // 创建锁文件
@ -63,7 +63,19 @@ class HandleCourseSchedule extends BaseJob
$ongoingCount = 0; $ongoingCount = 0;
$pendingCount = 0; $pendingCount = 0;
// 1. 更新已完成课程:course_date < 当天的课程 // 1. 首先重置未来日期的课程为pending状态(待开始)
// 这是核心修正:course_date > 当前日期的所有课程状态改为pending
$futureRows = CourseSchedule::where('course_date', '>', $currentDate)
->where('status', '<>', 'pending')
->update([
'status' => 'pending',
'updated_at' => time()
]);
$pendingCount += $futureRows;
Log::write("重置未来课程状态完成 - 重置为pending(待开始): {$futureRows}个");
// 2. 更新已完成课程:course_date < 当天的课程
$completedRows = CourseSchedule::where('course_date', '<', $currentDate) $completedRows = CourseSchedule::where('course_date', '<', $currentDate)
->where('status', '<>', 'completed') ->where('status', '<>', 'completed')
->update([ ->update([
@ -72,7 +84,7 @@ class HandleCourseSchedule extends BaseJob
]); ]);
$completedCount = $completedRows; $completedCount = $completedRows;
// 2. 处理今天的课程,需要根据时间段判断状态 // 3. 处理今天的课程,需要根据时间段判断状态
$todaySchedules = CourseSchedule::where('course_date', '=', $currentDate) $todaySchedules = CourseSchedule::where('course_date', '=', $currentDate)
->whereIn('status', ['pending', 'upcoming', 'ongoing']) ->whereIn('status', ['pending', 'upcoming', 'ongoing'])
->select(); ->select();
@ -124,36 +136,16 @@ class HandleCourseSchedule extends BaseJob
} }
} }
// 3. 重置未来日期的课程为pending状态 Log::write("课程状态更新完成 - 已完成: {$completedCount}个, 即将开始: {$upcomingCount}个, 进行中: {$ongoingCount}个, 待开始: {$pendingCount}个");
$futureRows = CourseSchedule::where('course_date', '>', $currentDate)
->where('status', '<>', 'pending')
->update([
'status' => 'pending',
'updated_at' => time()
]);
$pendingCount += $futureRows;
Log::write("课程状态更新完成 - 已完成: {$completedCount}个, 即将开始: {$upcomingCount}个, 进行中: {$ongoingCount}个, 待安排: {$pendingCount}个");
Db::commit(); Db::commit();
return [ return true;
'status' => 'success',
'completed_count' => $completedCount,
'upcoming_count' => $upcomingCount,
'ongoing_count' => $ongoingCount,
'pending_count' => $pendingCount
];
} catch (\Exception $e) { } catch (\Exception $e) {
Db::rollback(); Db::rollback();
Log::write('更新课程状态失败:' . $e->getMessage()); Log::write('更新课程状态失败:' . $e->getMessage());
return [ return false;
'status' => 'failed',
'total_count' => 0,
'updated_count' => 0,
'error' => $e->getMessage()
];
} }
} }

7
niucloud/app/service/admin/upload/UploadService.php

@ -58,8 +58,13 @@ class UploadService extends BaseAdminService
$dir = $this->root_path.'/document/'.$type.'/'.date('Ym').'/'.date('d'); $dir = $this->root_path.'/document/'.$type.'/'.date('Ym').'/'.date('d');
// $dir = $this->root_path.'/document/'.$type.'/'.$name[0]; // $dir = $this->root_path.'/document/'.$type.'/'.$name[0];
if ($type != 'cert'){
$storage_type = StorageDict::TENCENT;
}else{
$storage_type = StorageDict::LOCAL;
}
$core_upload_service = new CoreUploadService(); $core_upload_service = new CoreUploadService();
return $core_upload_service->document($file, $type, $dir, StorageDict::TENCENT); return $core_upload_service->document($file, $type, $dir, $storage_type);
} }

40
niucloud/app/service/api/apiService/CoachStudentService.php

@ -45,7 +45,14 @@ class CoachStudentService extends BaseApiService
if (empty($coachId)) { if (empty($coachId)) {
$res['code'] = 1; $res['code'] = 1;
$res['data'] = []; $res['data'] = [
'list' => [],
'total' => 0,
'page' => 1,
'limit' => 10,
'pages' => 0,
'has_more' => false
];
$res['msg'] = '获取成功'; $res['msg'] = '获取成功';
return $res; return $res;
} }
@ -55,7 +62,14 @@ class CoachStudentService extends BaseApiService
if (empty($studentIds)) { if (empty($studentIds)) {
$res['code'] = 1; $res['code'] = 1;
$res['data'] = []; $res['data'] = [
'list' => [],
'total' => 0,
'page' => 1,
'limit' => 10,
'pages' => 0,
'has_more' => false
];
$res['msg'] = '获取成功'; $res['msg'] = '获取成功';
return $res; return $res;
} }
@ -82,12 +96,23 @@ class CoachStudentService extends BaseApiService
$where[] = ['s.status', '=', $data['status']]; $where[] = ['s.status', '=', $data['status']];
} }
// 查询学员基础信息 // 分页参数处理
$page = max(1, intval($data['page'] ?? 1));
$limit = max(1, min(50, intval($data['limit'] ?? 10))); // 限制每页最多50条
// 查询总数
$total = Db::table('school_student s')
->leftJoin('school_campus c', 's.campus_id = c.id')
->where($where)
->count();
// 查询学员基础信息(分页)
$list = Db::table('school_student s') $list = Db::table('school_student s')
->leftJoin('school_campus c', 's.campus_id = c.id') ->leftJoin('school_campus c', 's.campus_id = c.id')
->where($where) ->where($where)
->field('s.*, c.campus_name as campus') ->field('s.*, c.campus_name as campus')
->order('s.created_at', 'desc') ->order('s.created_at', 'desc')
->page($page, $limit)
->select() ->select()
->toArray(); ->toArray();
@ -146,7 +171,14 @@ class CoachStudentService extends BaseApiService
} }
$res['code'] = 1; $res['code'] = 1;
$res['data'] = $list; $res['data'] = [
'list' => $list,
'total' => $total,
'page' => $page,
'limit' => $limit,
'pages' => ceil($total / $limit),
'has_more' => $page * $limit < $total
];
} catch (\Exception $e) { } catch (\Exception $e) {
$res['msg'] = '获取失败:' . $e->getMessage(); $res['msg'] = '获取失败:' . $e->getMessage();
} }

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

@ -832,22 +832,22 @@ class PersonnelService extends BaseApiService
$where = []; $where = [];
// 查询销售部门(dept_id=3)下的所有角色ID // 查询销售部门(dept_id=3)下的所有角色ID
$salesRoleIds = SysRole::where('dept_id', 3) // $salesRoleIds = SysRole::where('dept_id', 3)
->where('status', 1) // ->where('status', 1)
->column('role_id'); // ->column('role_id');
//
if (empty($salesRoleIds)) { // if (empty($salesRoleIds)) {
return [ // return [
'code' => 1, // 'code' => 1,
'msg' => '暂无销售部门角色', // 'msg' => '暂无销售部门角色',
'data' => [] // 'data' => []
]; // ];
} // }
//
// 构建校区人员角色关系查询条件 // // 构建校区人员角色关系查询条件
$campusPersonWhere = [ // $campusPersonWhere = [
['role_id', 'in', $salesRoleIds] // ['role_id', 'in', $salesRoleIds]
]; // ];
// 根据传入的校区进行筛选 // 根据传入的校区进行筛选
if (!empty($campus)) { if (!empty($campus)) {
@ -857,7 +857,7 @@ class PersonnelService extends BaseApiService
// 查询符合条件的销售人员ID // 查询符合条件的销售人员ID
$salesPersonIds = CampusPersonRole::where($campusPersonWhere) $salesPersonIds = CampusPersonRole::where($campusPersonWhere)
->column('person_id'); ->column('person_id');
if (empty($salesPersonIds)) { if (empty($salesPersonIds)) {
return [ return [
'code' => 1, 'code' => 1,

78
niucloud/test_upload.php

@ -1,78 +0,0 @@
<?php
/**
* 文件上传测试脚本
* 用于测试文档上传功能
*/
// 创建一个测试文件
$testContent = "这是一个测试文档\n创建时间: " . date('Y-m-d H:i:s');
$testFile = '/tmp/test_document.txt';
file_put_contents($testFile, $testContent);
echo "📄 文件上传测试\n";
echo "================\n";
echo "测试文件: $testFile\n";
echo "文件大小: " . filesize($testFile) . " bytes\n\n";
// 测试上传接口
$url = 'http://niucloud_nginx/api/uploadDocument';
// 创建 CURLFile 对象
$cfile = new CURLFile($testFile, 'text/plain', 'test_document.txt');
$postData = [
'file' => $cfile,
'type' => 'document'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json',
'token: test_token_for_upload_test',
// 注意:不要设置 Content-Type,让curl自动设置为 multipart/form-data
]);
echo "🚀 发送上传请求...\n";
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
echo "HTTP状态码: $httpCode\n";
if ($error) {
echo "CURL错误: $error\n";
}
echo "响应内容:\n";
echo $response . "\n\n";
// 解析响应
$responseData = json_decode($response, true);
if ($responseData) {
if ($responseData['code'] == 1) {
echo "✅ 上传成功!\n";
echo "文件URL: " . $responseData['data']['url'] . "\n";
echo "文件名: " . $responseData['data']['name'] . "\n";
echo "扩展名: " . $responseData['data']['ext'] . "\n";
} else {
echo "❌ 上传失败: " . $responseData['msg'] . "\n";
}
} else {
echo "❌ 响应解析失败\n";
}
// 清理测试文件
unlink($testFile);
echo "\n🗑️ 测试文件已清理\n";
echo "\n📋 调试建议:\n";
echo "1. 检查前端是否使用了正确的参数名 'file'\n";
echo "2. 检查请求是否为 multipart/form-data 格式\n";
echo "3. 检查文件大小是否超过限制\n";
echo "4. 检查服务器错误日志\n";
echo "5. 检查腾讯云COS配置和权限\n";
?>

71
test-student-orders-api.js

@ -1,71 +0,0 @@
// 测试学员端订单接口
const axios = require('axios');
const BASE_URL = 'http://localhost:20080/api';
async function testStudentOrdersAPI() {
console.log('🧪 开始测试学员端订单接口...\n');
try {
// 测试订单列表接口
console.log('📋 测试订单列表接口...');
const listResponse = await axios.get(`${BASE_URL}/xy/student/orders`, {
params: {
student_id: 31,
page: 1,
limit: 10
}
});
console.log('✅ 订单列表接口响应:');
console.log('状态码:', listResponse.status);
console.log('响应数据:', JSON.stringify(listResponse.data, null, 2));
console.log('');
// 测试订单统计接口
console.log('📊 测试订单统计接口...');
const statsResponse = await axios.get(`${BASE_URL}/xy/student/orders/stats`, {
params: {
student_id: 31
}
});
console.log('✅ 订单统计接口响应:');
console.log('状态码:', statsResponse.status);
console.log('响应数据:', JSON.stringify(statsResponse.data, null, 2));
console.log('');
// 如果有订单数据,测试订单详情接口
if (listResponse.data.code === 1 && listResponse.data.data && listResponse.data.data.data && listResponse.data.data.data.length > 0) {
const firstOrder = listResponse.data.data.data[0];
console.log('📄 测试订单详情接口...');
const detailResponse = await axios.get(`${BASE_URL}/xy/student/orders/detail`, {
params: {
id: firstOrder.id,
student_id: 31
}
});
console.log('✅ 订单详情接口响应:');
console.log('状态码:', detailResponse.status);
console.log('响应数据:', JSON.stringify(detailResponse.data, null, 2));
} else {
console.log('ℹ️ 没有订单数据,跳过详情接口测试');
}
console.log('\n🎉 所有接口测试完成!');
} catch (error) {
console.error('❌ 接口测试失败:');
if (error.response) {
console.error('状态码:', error.response.status);
console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
} else {
console.error('错误信息:', error.message);
}
}
}
// 运行测试
testStudentOrdersAPI();

125
uniapp/pages-coach/coach/schedule/schedule_table.vue

@ -126,36 +126,31 @@
<!-- 右侧内容区域 --> <!-- 右侧内容区域 -->
<view class="schedule-content-area"> <view class="schedule-content-area">
<!-- 表头 --> <!-- 合并的滚动区域表头+内容 -->
<scroll-view
class="date-header-scroll"
scroll-x
:scroll-left="scrollLeft"
>
<view class="date-header-container" :style="{ width: tableWidth + 'rpx', minWidth: '1260rpx' }">
<!-- 日期列 -->
<view
class="date-header-cell"
v-for="(date, index) in weekDates"
:key="index"
>
<view class="date-week">{{ date.weekName }}</view>
<view class="date-day">{{ date.dateStr }}</view>
<view class="date-courses">{{ date.courseCount }}节课</view>
</view>
</view>
</scroll-view>
<!-- 内容滚动区域 -->
<scroll-view <scroll-view
class="schedule-scroll" class="schedule-scroll"
scroll-x scroll-x
scroll-y scroll-y
:scroll-left="scrollLeft"
:scroll-top="scrollTop" :scroll-top="scrollTop"
@scroll="onScroll" @scroll="onScroll"
> >
<view class="schedule-grid" :style="{ width: tableWidth + 'rpx', minWidth: '1260rpx' }"> <view class="schedule-container-inner" :style="{ width: tableWidth + 'rpx', minWidth: '1260rpx' }">
<!-- 表头 -->
<view class="date-header-container">
<!-- 日期列 -->
<view
class="date-header-cell"
v-for="(date, index) in weekDates"
:key="index"
>
<view class="date-week">{{ date.weekName }}</view>
<view class="date-day">{{ date.dateStr }}</view>
<view class="date-courses">{{ date.courseCount }}节课</view>
</view>
</view>
<!-- 内容网格 -->
<view class="schedule-grid">
<!-- 时间模式内容 --> <!-- 时间模式内容 -->
<template v-if="activeFilter === 'time' || activeFilter === ''"> <template v-if="activeFilter === 'time' || activeFilter === ''">
<view <view
@ -299,6 +294,7 @@
</view> </view>
</view> </view>
</template> </template>
</view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@ -432,9 +428,7 @@ export default {
selectedClasses: [], selectedClasses: [],
// //
scrollLeft: 0,
scrollTop: 0, scrollTop: 0,
scrollAnimationFrame: null, //
// //
tableWidth: 1500, // 7 (7*180+120=1380rpx) tableWidth: 1500, // 7 (7*180+120=1380rpx)
@ -526,23 +520,29 @@ export default {
this.initCurrentWeek() this.initCurrentWeek()
this.initTimeSlots() this.initTimeSlots()
//
this.handleResize()
// //
this.loadFilterOptions().then(() => { this.loadFilterOptions().then(() => {
this.loadScheduleList() this.loadScheduleList()
}) })
// // H5
window.addEventListener('resize', this.handleResize) // #ifndef MP-WEIXIN
if (typeof window !== 'undefined') {
window.addEventListener('resize', this.handleResize)
}
// #endif
}, },
beforeDestroy() { beforeDestroy() {
// // H5
window.removeEventListener('resize', this.handleResize) // #ifndef MP-WEIXIN
if (typeof window !== 'undefined') {
// window.removeEventListener('resize', this.handleResize)
if (this.scrollAnimationFrame) {
cancelAnimationFrame(this.scrollAnimationFrame)
} }
// #endif
}, },
methods: { methods: {
@ -782,14 +782,8 @@ export default {
this.filterParams.class_id = ''; this.filterParams.class_id = '';
// //
this.scrollLeft = 0;
this.scrollTop = 0; this.scrollTop = 0;
//
if (this.scrollAnimationFrame) {
cancelAnimationFrame(this.scrollAnimationFrame);
}
// //
this.loadScheduleList(); this.loadScheduleList();
} }
@ -812,7 +806,6 @@ export default {
// //
await this.loadScheduleList() await this.loadScheduleList()
this.scrollLeft = 0
this.scrollTop = 0 this.scrollTop = 0
// //
@ -1162,24 +1155,12 @@ export default {
}); });
}, },
// // -
onScroll(e) { onScroll(e) {
if (this.scrollAnimationFrame) { //
cancelAnimationFrame(this.scrollAnimationFrame) if (e.detail.scrollTop !== undefined) {
this.scrollTop = e.detail.scrollTop
} }
this.scrollAnimationFrame = requestAnimationFrame(() => {
//
if (e.detail.scrollLeft !== undefined) {
this.scrollLeft = e.detail.scrollLeft
}
if (e.detail.scrollTop !== undefined) {
this.scrollTop = e.detail.scrollTop
// :scroll-top
}
})
}, },
// //
@ -1334,7 +1315,24 @@ export default {
// //
handleResize() { handleResize() {
// //
const width = window.innerWidth let width = 375 //
// #ifdef MP-WEIXIN
try {
const systemInfo = uni.getSystemInfoSync()
width = systemInfo.screenWidth || systemInfo.windowWidth || 375
} catch (e) {
console.warn('获取系统信息失败,使用默认宽度:', e)
width = 375
}
// #endif
// #ifndef MP-WEIXIN
if (typeof window !== 'undefined') {
width = window.innerWidth
}
// #endif
if (width <= 375) { if (width <= 375) {
this.tableWidth = 1220 // 7*160+100=1220rpx this.tableWidth = 1220 // 7*160+100=1220rpx
} else if (width <= 768) { } else if (width <= 768) {
@ -1531,15 +1529,20 @@ export default {
overflow: hidden; overflow: hidden;
} }
.date-header-scroll { //
overflow: hidden; .schedule-container-inner {
border-bottom: 2px solid #29d3b4; display: flex;
flex-direction: column;
min-width: 1260rpx; /* 7天 * 180rpx = 1260rpx */
} }
.date-header-container { .date-header-container {
display: flex; display: flex;
background: #434544; background: #434544;
min-width: 1260rpx; /* 7天 * 180rpx = 1260rpx */ border-bottom: 2px solid #29d3b4;
position: sticky;
top: 0;
z-index: 2;
} }
.date-header-cell { .date-header-cell {

256
uniapp/pages-coach/coach/student/student_list.vue

@ -4,37 +4,65 @@
<uni-icons type="search" size="22" color="#00d18c" /> <uni-icons type="search" size="22" color="#00d18c" />
<text class="search-placeholder">搜索学员...</text> <text class="search-placeholder">搜索学员...</text>
</view> </view>
<!-- 统计信息 -->
<view class="stats-bar">
<text class="stats-text">共找到 {{ pagination.total }} 名学员</text>
</view>
<view class="content"> <view class="content">
<view v-if="studentList.length === 0" class="empty-box"> <view v-if="studentList.length === 0" class="empty-box">
<image :src="$util.img('/static/icon-img/empty.png')" mode="aspectFit" class="empty-img"></image> <image :src="$util.img('/static/icon-img/empty.png')" mode="aspectFit" class="empty-img"></image>
<text class="empty-text">暂无学员数据</text> <text class="empty-text">暂无学员数据</text>
</view> </view>
<view v-else class="student-list"> <view v-else>
<view v-for="(item, index) in studentList" :key="index" class="student-item" @click="goToDetail(item)"> <scroll-view
<view class="student-card"> class="student-list-scroll"
<view class="student-avatar"> scroll-y
<image :src="item.avatar || $util.img('/static/icon-img/avatar.png')" mode="aspectFill" class="avatar-img"></image> @scrolltolower="loadMore"
</view> lower-threshold="100"
<view class="student-info"> >
<view class="student-name">{{item.name}}</view> <view class="student-list">
<view class="info-row"> <view v-for="(item, index) in studentList" :key="index" class="student-item" @click="goToDetail(item)">
<text class="info-label">所属校区</text> <view class="student-card">
<text class="info-value">{{item.campus}}</text> <view class="student-avatar">
</view> <image :src="item.avatar || $util.img('/static/icon-img/avatar.png')" mode="aspectFill" class="avatar-img"></image>
<view class="info-row"> </view>
<text class="info-label">剩余课程</text> <view class="student-info">
<text class="info-value">{{ getRemainingCourses(item) }}</text> <view class="student-name">{{item.name}}</view>
</view> <view class="info-row">
<view class="info-row"> <text class="info-label">所属校区</text>
<text class="info-label">到期时间</text> <text class="info-value">{{item.campus}}</text>
<text class="info-value">{{item.end_date}}</text> </view>
<view class="info-row">
<text class="info-label">剩余课程</text>
<text class="info-value">{{ getRemainingCourses(item) }}</text>
</view>
<view class="info-row">
<text class="info-label">到期时间</text>
<text class="info-value">{{item.end_date}}</text>
</view>
</view>
<view class="arrow-right">
<uni-icons type="right" size="16" color="#CCCCCC"></uni-icons>
</view>
</view> </view>
</view> </view>
<view class="arrow-right"> </view>
<uni-icons type="right" size="16" color="#CCCCCC"></uni-icons>
<!-- 加载更多提示 -->
<view class="load-more" v-if="studentList.length > 0">
<view v-if="pagination.loading" class="loading">
<uni-icons type="spinner-cycle" size="24" color="#00d18c" class="loading-icon"></uni-icons>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="pagination.hasMore" class="load-more-text">
上拉加载更多
</view>
<view v-else class="no-more">
已显示全部 {{ pagination.total }} 名学员
</view> </view>
</view> </view>
</view> </scroll-view>
</view> </view>
</view> </view>
<!-- 搜索弹窗 --> <!-- 搜索弹窗 -->
@ -141,13 +169,21 @@ import apiRoute from '@/api/apiRoute.js';
phone: '', phone: '',
lessonCount: '', lessonCount: '',
leaveCount: '', leaveCount: '',
courseId: null, course_id: null,
classId: null, class_id: null,
}, },
selectedCourseName: '', selectedCourseName: '',
selectedClassName: '', selectedClassName: '',
courseList: [], courseList: [],
classList: [], classList: [],
//
pagination: {
current: 1,
limit: 10,
total: 0,
hasMore: false,
loading: false
},
} }
}, },
onLoad() { onLoad() {
@ -158,14 +194,48 @@ import apiRoute from '@/api/apiRoute.js';
navigateBack() { navigateBack() {
uni.navigateBack(); uni.navigateBack();
}, },
async getStudentList() { async getStudentList(reset = false) {
try { try {
// 使 //
const res = await apiRoute.coach_getMyStudents(this.searchForm); if (reset) {
this.pagination.current = 1;
this.studentList = [];
}
//
if (this.pagination.loading) {
return;
}
this.pagination.loading = true;
//
const params = {
...this.searchForm,
page: this.pagination.current,
limit: this.pagination.limit
};
const res = await apiRoute.coach_getMyStudents(params);
console.log('获取教练端学员列表响应:', res); console.log('获取教练端学员列表响应:', res);
if(res.code == 1) { if(res.code == 1) {
this.studentList = res.data || []; const data = res.data || {};
const list = data.list || [];
//
this.pagination.total = data.total || 0;
this.pagination.hasMore = data.has_more || false;
//
if (reset) {
this.studentList = list;
} else {
this.studentList = [...this.studentList, ...list];
}
console.log('教练端学员列表更新成功:', this.studentList); console.log('教练端学员列表更新成功:', this.studentList);
console.log('分页信息:', this.pagination);
} else { } else {
console.error('API返回错误:', res); console.error('API返回错误:', res);
uni.showToast({ uni.showToast({
@ -173,13 +243,14 @@ import apiRoute from '@/api/apiRoute.js';
icon: 'none' icon: 'none'
}); });
} }
}catch ( error) { } catch (error) {
console.error('获取学员列表错误', error); console.error('获取学员列表错误', error);
uni.showToast({ uni.showToast({
title: '获取学员列表失败', title: '获取学员列表失败',
icon: 'none' icon: 'none'
}); });
return; } finally {
this.pagination.loading = false;
} }
}, },
goToDetail(student) { goToDetail(student) {
@ -204,6 +275,16 @@ import apiRoute from '@/api/apiRoute.js';
0) + (item.use_gift_hours || 0); 0) + (item.use_gift_hours || 0);
return totalHours - usedHours; return totalHours - usedHours;
}, },
//
async loadMore() {
if (!this.pagination.hasMore || this.pagination.loading) {
return;
}
this.pagination.current++;
await this.getStudentList(false);
},
// //
onNameInput(e) { onNameInput(e) {
@ -221,23 +302,23 @@ import apiRoute from '@/api/apiRoute.js';
// //
onCourseChange(e) { onCourseChange(e) {
this.searchForm.courseId = e.value; this.searchForm.course_id = e.value;
this.selectedCourseName = e.text; this.selectedCourseName = e.text;
}, },
onClassChange(e) { onClassChange(e) {
this.searchForm.classId = e.value; this.searchForm.class_id = e.value;
this.selectedClassName = e.text; this.selectedClassName = e.text;
}, },
// //
clearCourseSelection(e) { clearCourseSelection(e) {
e.stopPropagation(); // e.stopPropagation(); //
this.searchForm.courseId = null; this.searchForm.course_id = null;
this.selectedCourseName = ''; this.selectedCourseName = '';
}, },
clearClassSelection(e) { clearClassSelection(e) {
e.stopPropagation(); // e.stopPropagation(); //
this.searchForm.classId = null; this.searchForm.class_id = null;
this.selectedClassName = ''; this.selectedClassName = '';
}, },
@ -251,33 +332,50 @@ import apiRoute from '@/api/apiRoute.js';
this.showSearch = false; this.showSearch = false;
}, },
// //
async getClassesList() { async getClassesList() {
try { try {
const res = await apiRoute.jlGetClassesList(); //
if (res.code == 1) { const [courseRes, classRes] = await Promise.all([
// API apiRoute.getCourseListForSchedule(),
this.classList = res.data.classes || []; apiRoute.getClassListForSchedule()
this.courseList = res.data.course || []; ]);
//
if (courseRes.code == 1) {
this.courseList = courseRes.data.list || courseRes.data || [];
} else { } else {
uni.showToast({ console.warn('获取课程列表失败:', courseRes.msg);
title: res.msg || '获取班级列表失败', this.courseList = [];
icon: 'none' }
});
//
if (classRes.code == 1) {
this.classList = classRes.data.list || classRes.data || [];
} else {
console.warn('获取班级列表失败:', classRes.msg);
this.classList = [];
} }
console.log('课程列表:', this.courseList);
console.log('班级列表:', this.classList);
} catch (error) { } catch (error) {
console.error('获取班级列表错误', error); console.error('获取课程和班级列表错误', error);
uni.showToast({ uni.showToast({
title: '获取班级列表失败', title: '获取筛选数据失败',
icon: 'none' icon: 'none'
}); });
//
this.courseList = [];
this.classList = [];
} }
}, },
doSearch() { doSearch() {
// searchForm // searchForm
this.showSearch = false; this.showSearch = false;
this.getStudentList() this.getStudentList(true) //
}, },
doSearchAndClose() { doSearchAndClose() {
@ -290,12 +388,12 @@ import apiRoute from '@/api/apiRoute.js';
phone: '', phone: '',
lessonCount: '', lessonCount: '',
leaveCount: '', leaveCount: '',
courseId: null, course_id: null,
classId: null, class_id: null,
}; };
this.selectedCourseName = ''; this.selectedCourseName = '';
this.selectedClassName = ''; this.selectedClassName = '';
this.getStudentList(); this.getStudentList(true); //
} }
} }
} }
@ -708,4 +806,62 @@ import apiRoute from '@/api/apiRoute.js';
color: #fff; color: #fff;
} }
} }
//
.stats-bar {
padding: 20rpx 30rpx;
background: #23232a;
border-bottom: 1px solid #333;
margin: 24rpx;
}
.stats-text {
color: #00d18c;
font-size: 28rpx;
font-weight: 500;
}
//
.student-list-scroll {
height: calc(100vh - 300rpx);
background: #18181c;
}
//
.load-more {
padding: 30rpx;
text-align: center;
background: #18181c;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
}
.loading-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
color: #00d18c;
font-size: 28rpx;
}
.load-more-text {
color: #bdbdbd;
font-size: 26rpx;
}
.no-more {
color: #666;
font-size: 24rpx;
}
</style> </style>
Loading…
Cancel
Save