Browse Source

Merge branch 'master' of http://gitlab.frkj.cc/php/zhjwxt

master
于宏哲PHP 9 months ago
parent
commit
cb0bb04717
  1. 1
      niucloud/app/api/controller/apiController/Contract.php
  2. 216
      niucloud/app/api/controller/apiController/CourseSchedule.php
  3. 217
      niucloud/app/api/controller/apiController/CourseScheduleController.php
  4. 28
      niucloud/app/api/route/route.php
  5. 567
      niucloud/app/service/api/apiService/CourseScheduleService.php
  6. 173
      niucloud/课程安排资料.md
  7. 56
      uniapp/api/apiRoute.js
  8. 8
      uniapp/common/axios.js
  9. 66
      uniapp/common/util.js
  10. 5
      uniapp/components/AQ/AQUplodeImgMulti.vue
  11. 240
      uniapp/components/CommentList.vue
  12. 166
      uniapp/components/J-skeleton/J-skeleton.vue
  13. 77
      uniapp/components/J-skeleton/README.md
  14. 438
      uniapp/components/aui-dialog/aui-dialog.vue
  15. 292
      uniapp/components/aui-dialog/common/aui/css/aui.css
  16. 1
      uniapp/components/aui-dialog/common/aui/css/aui.iconfont.css
  17. 55
      uniapp/components/aui-dialog/common/aui/js/aui.js
  18. 181
      uniapp/components/coolc-coupon/coolc-coupon.vue
  19. 234
      uniapp/components/custom-modal/example.vue
  20. 393
      uniapp/components/modal/modal.vue
  21. 517
      uniapp/components/schedule/ScheduleDetail.vue
  22. 96
      uniapp/components/uni-icons/icons.js
  23. 57
      uniapp/components/uni-icons/uni-icons.vue
  24. 4
      uniapp/components/uni-nav-bar/uni-nav-bar.vue
  25. 13
      uniapp/pages.json
  26. 28
      uniapp/pages/coach/my/index.vue
  27. 175
      uniapp/pages/coach/schedule/README.md
  28. 481
      uniapp/pages/coach/schedule/schedule_detail.vue
  29. 1066
      uniapp/pages/coach/schedule/schedule_table.vue
  30. 306
      uniapp/pages/coach/student/student_list.vue
  31. 2
      uniapp/pages/common/contract/contract_detail.vue
  32. 51
      uniapp/pages/common/contract/contract_sign.vue
  33. 6
      uniapp/pages/common/contract/my_contract.vue
  34. 2325
      uniapp/pages/market/clue/clue_info.vue
  35. 114
      uniapp/pages/market/clue/order_list.vue
  36. 263
      uniapp/pages/test/dict_test.vue

1
niucloud/app/api/controller/apiController/Contract.php

@ -107,7 +107,6 @@ class Contract extends BaseApiService
'personnel_id' => $this->member_id,
'sign_file' => $sign_file
];
try {
$service = new ContractService();
$res = $service->signContract($data);

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

@ -0,0 +1,216 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\api\controller\apiController;
use app\Request;
use app\service\api\apiService\CourseScheduleService;
use core\base\BaseApiService;
/**
* 课程安排相关接口
* Class CourseSchedule
* @package app\api\controller\apiController
*/
class CourseSchedule extends BaseApiService
{
/**
* 获取课程安排列表
* @param Request $request
* @return \think\Response
*/
public function getScheduleList(Request $request)
{
$data = $request->all();
return success((new CourseScheduleService())->getScheduleList($data));
}
/**
* 获取课程安排详情
* @param Request $request
* @return \think\Response
*/
public function getScheduleInfo(Request $request)
{
$data = $this->request->params([
["id", 0]
]);
$result = (new CourseScheduleService())->getScheduleInfo($data['id']);
if (isset($result['code']) && $result['code'] === 0) {
return fail($result['msg']);
}
return success('SUCCESS', $result);
}
/**
* 创建课程安排
* @param Request $request
* @return \think\Response
*/
public function createSchedule(Request $request)
{
$data = $request->all();
$result = (new CourseScheduleService())->createSchedule($data);
if (!$result['code']) {
return fail($result['msg']);
}
return success($result['msg'] ?? '创建成功', $result['data'] ?? []);
}
/**
* 批量创建课程安排
* @param Request $request
* @return \think\Response
*/
public function batchCreateSchedule(Request $request)
{
$data = $request->all();
$result = (new CourseScheduleService())->batchCreateSchedule($data);
if (!$result['code']) {
return fail($result['msg']);
}
return success($result['msg'] ?? '批量创建成功', $result['data'] ?? []);
}
/**
* 更新课程安排
* @param Request $request
* @return \think\Response
*/
public function updateSchedule(Request $request)
{
$data = $request->all();
$result = (new CourseScheduleService())->updateSchedule($data);
if (!$result['code']) {
return fail($result['msg']);
}
return success($result['msg'] ?? '更新成功', $result['data'] ?? []);
}
/**
* 删除课程安排
* @param Request $request
* @return \think\Response
*/
public function deleteSchedule(Request $request)
{
$data = $this->request->params([
["id", 0]
]);
$result = (new CourseScheduleService())->deleteSchedule($data['id']);
if (!$result['code']) {
return fail($result['msg']);
}
return success($result['msg'] ?? '删除成功');
}
/**
* 获取场地列表
* @param Request $request
* @return \think\Response
*/
public function getVenueList(Request $request)
{
$data = $request->all();
return success((new CourseScheduleService())->getVenueList($data));
}
/**
* 获取场地可用时间
* @param Request $request
* @return \think\Response
*/
public function getVenueAvailableTime(Request $request)
{
$data = $this->request->params([
["venue_id", 0],
["date", ""]
]);
return success((new CourseScheduleService())->getVenueAvailableTime($data));
}
/**
* 检查教练时间冲突
* @param Request $request
* @return \think\Response
*/
public function checkCoachConflict(Request $request)
{
$data = $this->request->params([
["coach_id", 0],
["date", ""],
["time_slot", ""],
["schedule_id", 0] // 排除当前正在编辑的课程安排
]);
return success((new CourseScheduleService())->checkCoachConflict($data));
}
/**
* 获取课程安排统计
* @param Request $request
* @return \think\Response
*/
public function getScheduleStatistics(Request $request)
{
$data = $request->all();
return success((new CourseScheduleService())->getScheduleStatistics($data));
}
/**
* 学员加入课程安排
* @param Request $request
* @return \think\Response
*/
public function joinSchedule(Request $request)
{
$data = $this->request->params([
["schedule_id", 0],
["student_id", 0],
["course_type", 0], // 0-正常, 1-加课, 2-补课, 3-等待位
["resources_id", 0]
]);
$result = (new CourseScheduleService())->joinSchedule($data);
if (!$result['code']) {
return fail($result['msg']);
}
return success($result['msg'] ?? '添加成功', $result['data'] ?? []);
}
/**
* 学员退出课程安排
* @param Request $request
* @return \think\Response
*/
public function leaveSchedule(Request $request)
{
$data = $this->request->params([
["schedule_id", 0],
["student_id", 0],
["remark", ""]
]);
$result = (new CourseScheduleService())->leaveSchedule($data);
if (!$result['code']) {
return fail($result['msg']);
}
return success($result['msg'] ?? '操作成功');
}
/**
* 获取筛选选项
* @param Request $request
* @return \think\Response
*/
public function getFilterOptions(Request $request)
{
$data = $request->all();
return success((new CourseScheduleService())->getFilterOptions($data));
}
}

217
niucloud/app/api/controller/apiController/CourseScheduleController.php

@ -0,0 +1,217 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\api\controller\apiController;
use app\service\api\apiService\CourseScheduleService;
use core\base\BaseApiController;
/**
* 课程安排控制器
*/
class CourseScheduleController extends BaseApiController
{
/**
* 获取课程安排列表(支持多维度筛选)
*/
public function getScheduleList()
{
$data = $this->request->get();
$service = new CourseScheduleService();
return success('获取成功', $service->getScheduleList($data));
}
/**
* 获取课程安排详情
*/
public function getScheduleInfo()
{
$scheduleId = $this->request->get('schedule_id', 0);
if (empty($scheduleId)) {
return fail('课程安排ID不能为空');
}
$service = new CourseScheduleService();
$result = $service->getScheduleInfo($scheduleId);
if (isset($result['code']) && $result['code'] == 0) {
return fail($result['msg'] ?? '获取课程安排详情失败');
}
return success('获取成功', $result);
}
/**
* 创建课程安排
*/
public function createSchedule()
{
$data = $this->request->post();
$service = new CourseScheduleService();
$result = $service->createSchedule($data);
if ($result['code'] == 1) {
return success($result['msg'], $result['data']);
} else {
return fail($result['msg']);
}
}
/**
* 批量创建课程安排
*/
public function batchCreateSchedule()
{
$data = $this->request->post();
$service = new CourseScheduleService();
$result = $service->batchCreateSchedule($data);
if ($result['code'] == 1) {
return success($result['msg'], $result['data']);
} else {
return fail($result['msg']);
}
}
/**
* 更新课程安排
*/
public function updateSchedule()
{
$data = $this->request->post();
$scheduleId = $data['schedule_id'] ?? 0;
if (empty($scheduleId)) {
return fail('课程安排ID不能为空');
}
$service = new CourseScheduleService();
$result = $service->updateSchedule($scheduleId, $data);
if ($result['code'] == 1) {
return success($result['msg'], $result['data']);
} else {
return fail($result['msg']);
}
}
/**
* 删除课程安排
*/
public function deleteSchedule()
{
$scheduleId = $this->request->post('schedule_id', 0);
if (empty($scheduleId)) {
return fail('课程安排ID不能为空');
}
$service = new CourseScheduleService();
$result = $service->deleteSchedule($scheduleId);
if ($result['code'] == 1) {
return success($result['msg']);
} else {
return fail($result['msg']);
}
}
/**
* 获取场地列表
*/
public function getVenueList()
{
$data = $this->request->get();
$service = new CourseScheduleService();
return success('获取成功', $service->getVenueList($data));
}
/**
* 获取场地可用时间
*/
public function getVenueAvailableTime()
{
$venueId = $this->request->get('venue_id', 0);
$date = $this->request->get('date', '');
if (empty($venueId)) {
return fail('场地ID不能为空');
}
if (empty($date)) {
return fail('日期不能为空');
}
$service = new CourseScheduleService();
return success('获取成功', $service->getVenueAvailableTime($venueId, $date));
}
/**
* 检查教练时间冲突
*/
public function checkCoachConflict()
{
$data = $this->request->get();
$service = new CourseScheduleService();
return success('检查完成', $service->checkCoachConflict($data));
}
/**
* 获取课程安排统计
*/
public function getScheduleStatistics()
{
$data = $this->request->get();
$service = new CourseScheduleService();
return success('获取成功', $service->getScheduleStatistics($data));
}
/**
* 学员加入课程安排
*/
public function joinSchedule()
{
$data = $this->request->post();
$service = new CourseScheduleService();
$result = $service->joinSchedule($data);
if ($result['code'] == 1) {
return success($result['msg'], $result['data']);
} else {
return fail($result['msg']);
}
}
/**
* 学员退出课程安排
*/
public function leaveSchedule()
{
$data = $this->request->post();
$service = new CourseScheduleService();
$result = $service->leaveSchedule($data);
if ($result['code'] == 1) {
return success($result['msg']);
} else {
return fail($result['msg']);
}
}
/**
* 获取筛选选项(教练、课程、班级等)
*/
public function getFilterOptions()
{
$data = $this->request->get();
$service = new CourseScheduleService();
return success('获取成功', $service->getFilterOptions($data));
}
}

28
niucloud/app/api/route/route.php

@ -292,6 +292,34 @@ Route::group(function () {
//员工端-更新学员课程人员配置
Route::post('updateStudentCoursePersonnel', 'apiController.Course/updateStudentCoursePersonnel');
// 课程安排相关接口
//员工端-获取课程安排列表
Route::get('courseSchedule/list', 'apiController.CourseSchedule/getScheduleList');
//员工端-获取课程安排详情
Route::get('courseSchedule/info', 'apiController.CourseSchedule/getScheduleInfo');
//员工端-创建课程安排
Route::post('courseSchedule/create', 'apiController.CourseSchedule/createSchedule');
//员工端-批量创建课程安排
Route::post('courseSchedule/batchCreate', 'apiController.CourseSchedule/batchCreateSchedule');
//员工端-更新课程安排
Route::post('courseSchedule/update', 'apiController.CourseSchedule/updateSchedule');
//员工端-删除课程安排
Route::post('courseSchedule/delete', 'apiController.CourseSchedule/deleteSchedule');
//员工端-获取场地列表
Route::get('courseSchedule/venues', 'apiController.CourseSchedule/getVenueList');
//员工端-获取场地可用时间
Route::get('courseSchedule/venueAvailableTime', 'apiController.CourseSchedule/getVenueAvailableTime');
//员工端-检查教练时间冲突
Route::get('courseSchedule/checkCoachConflict', 'apiController.CourseSchedule/checkCoachConflict');
//员工端-获取课程安排统计
Route::get('courseSchedule/statistics', 'apiController.CourseSchedule/getScheduleStatistics');
//员工端-学员加入课程安排
Route::post('courseSchedule/joinSchedule', 'apiController.CourseSchedule/joinSchedule');
//员工端-学员退出课程安排
Route::post('courseSchedule/leaveSchedule', 'apiController.CourseSchedule/leaveSchedule');
//员工端-获取筛选选项
Route::get('courseSchedule/filterOptions', 'apiController.CourseSchedule/getFilterOptions');

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

@ -0,0 +1,567 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\service\api\apiService;
use core\base\BaseApiService;
use think\facade\Db;
/**
* 课程安排服务类
*/
class CourseScheduleService extends BaseApiService
{
// 定义表前缀,可以更方便地引用数据表
protected $prefix = 'school_';
public function __construct()
{
parent::__construct();
}
/**
* 获取课程安排列表(支持多维度筛选)
* @param array $data 筛选参数
* @return array 课程安排列表数据
*/
public function getScheduleList($data = [])
{
try {
// 构建查询条件
$where = $this->buildScheduleWhere($data);
// 分页参数
$page = intval($data['page'] ?? 1);
$limit = intval($data['limit'] ?? 20);
$offset = ($page - 1) * $limit;
// 基础查询
$query = Db::name($this->prefix . 'course_schedule')
->alias('cs')
->leftJoin($this->prefix . 'course c', 'cs.course_id = c.id')
->leftJoin($this->prefix . 'venue v', 'cs.venue_id = v.id')
->leftJoin($this->prefix . 'campus cap', 'cs.campus_id = cap.id')
->leftJoin($this->prefix . 'personnel coach', 'cs.coach_id = coach.id')
->leftJoin($this->prefix . 'personnel edu', 'cs.education_id = edu.id')
->where($where)
->where('cs.deleted_at', 0);
// 获取总数
$total = $query->count();
// 获取列表数据
$list = $query->field([
'cs.*',
'c.course_name',
'c.course_type',
'c.duration as course_duration',
'c.session_count',
'c.single_session_count',
'v.venue_name',
'v.capacity as venue_capacity',
'cap.campus_name',
'coach.name as coach_name',
'coach.head_img as coach_avatar',
'edu.name as education_name'
])
->order('cs.course_date DESC, cs.time_slot ASC')
->limit($offset, $limit)
->select()
->toArray();
// 处理列表数据
foreach ($list as &$item) {
// 解析时间段
$item['time_info'] = $this->parseTimeSlot($item['time_slot']);
// 获取参与学员信息
$item['students'] = $this->getScheduleStudents($item['id']);
// 获取助教信息
$item['assistants'] = $this->getScheduleAssistants($item['assistant_ids']);
// 计算已报名人数
$item['enrolled_count'] = count($item['students']);
// 计算剩余容量
$item['remaining_capacity'] = max(0, ($item['available_capacity'] ?? $item['venue_capacity']) - $item['enrolled_count']);
// 格式化状态
$item['status_text'] = $this->getStatusText($item['status']);
// 格式化创建方式
$item['created_by_text'] = $item['created_by'] == 'manual' ? '手动安排' : '系统创建';
// 处理图片路径
$item['coach_avatar'] = $item['coach_avatar'] ? $this->formatImageUrl($item['coach_avatar']) : '';
}
return [
'list' => $list,
'total' => $total,
'page' => $page,
'limit' => $limit,
'pages' => ceil($total / $limit)
];
} catch (\Exception $e) {
return [
'list' => [],
'total' => 0,
'page' => 1,
'limit' => $limit ?? 20,
'pages' => 0,
'error' => $e->getMessage()
];
}
}
/**
* 构建查询条件
* @param array $data 筛选参数
* @return array 条件数组
*/
private function buildScheduleWhere($data)
{
$where = [];
// 日期范围筛选
if (!empty($data['start_date'])) {
$where[] = ['cs.course_date', '>=', $data['start_date']];
}
if (!empty($data['end_date'])) {
$where[] = ['cs.course_date', '<=', $data['end_date']];
}
// 校区筛选
if (!empty($data['campus_id'])) {
if (is_array($data['campus_id'])) {
$where[] = ['cs.campus_id', 'in', $data['campus_id']];
} else {
$where[] = ['cs.campus_id', '=', $data['campus_id']];
}
}
// 场地筛选
if (!empty($data['venue_id'])) {
if (is_array($data['venue_id'])) {
$where[] = ['cs.venue_id', 'in', $data['venue_id']];
} else {
$where[] = ['cs.venue_id', '=', $data['venue_id']];
}
}
// 教练筛选
if (!empty($data['coach_id'])) {
if (is_array($data['coach_id'])) {
$where[] = ['cs.coach_id', 'in', $data['coach_id']];
} else {
$where[] = ['cs.coach_id', '=', $data['coach_id']];
}
}
// 课程筛选
if (!empty($data['course_id'])) {
if (is_array($data['course_id'])) {
$where[] = ['cs.course_id', 'in', $data['course_id']];
} else {
$where[] = ['cs.course_id', '=', $data['course_id']];
}
}
// 状态筛选
if (!empty($data['status'])) {
if (is_array($data['status'])) {
$where[] = ['cs.status', 'in', $data['status']];
} else {
$where[] = ['cs.status', '=', $data['status']];
}
}
// 教务筛选
if (!empty($data['education_id'])) {
$where[] = ['cs.education_id', '=', $data['education_id']];
}
// 时间段筛选
if (!empty($data['time_range'])) {
switch ($data['time_range']) {
case 'morning':
$where[] = ['cs.time_slot', 'like', '0%'];
break;
case 'afternoon':
$where[] = ['cs.time_slot', 'like', '1%'];
break;
case 'evening':
$where[] = ['cs.time_slot', 'like', '1[8-9]%'];
break;
}
}
// 自动排课筛选
if (isset($data['auto_schedule'])) {
$where[] = ['cs.auto_schedule', '=', $data['auto_schedule']];
}
// 创建方式筛选
if (!empty($data['created_by'])) {
$where[] = ['cs.created_by', '=', $data['created_by']];
}
return $where;
}
/**
* 解析时间段
* @param string $timeSlot 时间段字符串(格式如:09:00-10:30)
* @return array 解析后的时间段信息
*/
private function parseTimeSlot($timeSlot)
{
if (strpos($timeSlot, '-') !== false) {
list($startTime, $endTime) = explode('-', $timeSlot);
return [
'start_time' => trim($startTime),
'end_time' => trim($endTime),
'duration' => $this->calculateDuration(trim($startTime), trim($endTime))
];
}
return [
'start_time' => $timeSlot,
'end_time' => '',
'duration' => 60 // 默认1小时
];
}
/**
* 计算时长(分钟)
* @param string $startTime 开始时间
* @param string $endTime 结束时间
* @return int 时长(分钟)
*/
private function calculateDuration($startTime, $endTime)
{
try {
$start = strtotime($startTime);
$end = strtotime($endTime);
return ($end - $start) / 60;
} catch (\Exception $e) {
return 60; // 默认1小时
}
}
/**
* 获取课程安排的学员信息
* @param int $scheduleId 课程安排ID
* @return array 学员信息数组
*/
private function getScheduleStudents($scheduleId)
{
try {
$students = Db::name($this->prefix . 'person_course_schedule')
->alias('pcs')
->leftJoin($this->prefix . 'student s', 'pcs.student_id = s.id')
->leftJoin($this->prefix . 'customer_resources cr', 'pcs.resources_id = cr.id')
->leftJoin($this->prefix . 'member m', 'cr.member_id = m.member_id')
->where('pcs.schedule_id', $scheduleId)
->where('pcs.deleted_at', 0)
->field([
'pcs.*',
's.name as student_name',
'cr.name as resource_name',
'cr.phone_number',
'cr.age',
'cr.gender',
'm.headimg as avatar'
])
->select()
->toArray();
foreach ($students as &$student) {
$student['name'] = $student['student_name'] ?: $student['resource_name'];
$student['avatar'] = $student['avatar'] ? $this->formatImageUrl($student['avatar']) : '';
$student['status_text'] = $this->getStudentStatusText($student['status']);
$student['person_type_text'] = $student['person_type'] == 'student' ? '正式学员' : '体验学员';
$student['course_type_text'] = $this->getCourseTypeText($student['course_type']);
}
return $students;
} catch (\Exception $e) {
return [];
}
}
/**
* 获取助教信息
* @param string $assistantIds 助教ID字符串,使用逗号分隔
* @return array 助教信息数组
*/
private function getScheduleAssistants($assistantIds)
{
if (empty($assistantIds)) {
return [];
}
try {
$ids = explode(',', $assistantIds);
$assistants = Db::name($this->prefix . 'personnel')
->whereIn('id', $ids)
->field('id, name, head_img, phone')
->select()
->toArray();
foreach ($assistants as &$assistant) {
$assistant['head_img'] = $assistant['head_img'] ? $this->formatImageUrl($assistant['head_img']) : '';
}
return $assistants;
} catch (\Exception $e) {
return [];
}
}
/**
* 获取状态文本
* @param string $status 状态码
* @return string 状态文本描述
*/
private function getStatusText($status)
{
$statusMap = [
'pending' => '待开始',
'upcoming' => '即将开始',
'ongoing' => '进行中',
'completed' => '已结束'
];
return $statusMap[$status] ?? $status;
}
/**
* 获取学员状态文本
* @param int $status 学员状态码
* @return string 学员状态文本描述
*/
private function getStudentStatusText($status)
{
$statusMap = [
0 => '待上课',
1 => '已上课',
2 => '请假'
];
return $statusMap[$status] ?? '未知';
}
/**
* 获取课程类型文本
* @param int $courseType 课程类型码
* @return string 课程类型文本描述
*/
private function getCourseTypeText($courseType)
{
$typeMap = [
1 => '加课',
2 => '补课',
3 => '等待位'
];
return $typeMap[$courseType] ?? '正常课程';
}
/**
* 格式化图片URL
* @param string $imagePath 图片路径
* @return string 格式化后的图片URL
*/
private function formatImageUrl($imagePath)
{
if (empty($imagePath)) {
return '';
}
// 如果已经是完整URL,直接返回
if (strpos($imagePath, 'http') === 0) {
return $imagePath;
}
// 拼接域名
$domain = request()->domain();
return $domain . '/' . ltrim($imagePath, '/');
}
/**
* 获取筛选选项(教练、课程、班级等)
* @param array $data 请求参数
* @return array 筛选选项数据
*/
public function getFilterOptions($data = [])
{
try {
$result = [
'coaches' => [], // 教练列表
'courses' => [], // 课程列表
'classes' => [], // 班级列表
'venues' => [], // 场地列表
'campuses' => [], // 校区列表
'status_options' => [] // 状态选项
];
// 获取教练列表
$result['coaches'] = Db::name($this->prefix . 'personnel')
->where('is_coach', 1)
->where('deleted_at', 0)
->field('id, name, head_img as avatar, phone')
->select()
->toArray();
foreach ($result['coaches'] as &$coach) {
$coach['avatar'] = $coach['avatar'] ? $this->formatImageUrl($coach['avatar']) : '';
}
// 获取课程列表
$result['courses'] = Db::name($this->prefix . 'course')
->where('deleted_at', 0)
->field('id, course_name, course_type, duration')
->select()
->toArray();
// 获取班级列表
$result['classes'] = Db::name($this->prefix . 'class')
->where('deleted_at', 0)
->field('id, class_name, class_level, total_students')
->select()
->toArray();
// 获取场地列表
$result['venues'] = Db::name($this->prefix . 'venue')
->where('deleted_at', 0)
->field('id, venue_name, capacity, description')
->select()
->toArray();
// 获取校区列表
$result['campuses'] = Db::name($this->prefix . 'campus')
->where('deleted_at', 0)
->field('id, campus_name, address')
->select()
->toArray();
// 状态选项
$result['status_options'] = [
['value' => 'pending', 'label' => '待开始'],
['value' => 'upcoming', 'label' => '即将开始'],
['value' => 'ongoing', 'label' => '进行中'],
['value' => 'completed', 'label' => '已结束']
];
return $result;
} catch (\Exception $e) {
return [
'coaches' => [],
'courses' => [],
'classes' => [],
'venues' => [],
'campuses' => [],
'status_options' => [],
'error' => $e->getMessage()
];
}
}
/**
* 获取课程安排详情
* @param int $scheduleId 课程安排ID
* @return array 课程安排详细信息或错误信息
*/
public function getScheduleInfo($scheduleId)
{
try {
// 查询课程安排信息
$schedule = Db::name($this->prefix . 'course_schedule')
->alias('cs')
->leftJoin($this->prefix . 'course c', 'cs.course_id = c.id')
->leftJoin($this->prefix . 'venue v', 'cs.venue_id = v.id')
->leftJoin($this->prefix . 'campus cap', 'cs.campus_id = cap.id')
->leftJoin($this->prefix . 'personnel coach', 'cs.coach_id = coach.id')
->leftJoin($this->prefix . 'personnel edu', 'cs.education_id = edu.id')
->where('cs.id', $scheduleId)
->where('cs.deleted_at', 0)
->field([
'cs.*',
'c.course_name',
'c.course_type',
'c.duration as course_duration',
'c.session_count',
'c.single_session_count',
'v.venue_name',
'v.capacity as venue_capacity',
'cap.campus_name',
'coach.name as coach_name',
'coach.head_img as coach_avatar',
'edu.name as education_name'
])
->find();
if (empty($schedule)) {
return ['code' => 0, 'msg' => '课程安排不存在或已被删除'];
}
// 解析时间段
$schedule['time_info'] = $this->parseTimeSlot($schedule['time_slot']);
// 获取参与学员信息
$schedule['students'] = $this->getScheduleStudents($schedule['id']);
// 获取助教信息
$schedule['assistants'] = $this->getScheduleAssistants($schedule['assistant_ids']);
// 计算已报名人数
$schedule['enrolled_count'] = count($schedule['students']);
// 计算剩余容量
$schedule['remaining_capacity'] = max(0, ($schedule['available_capacity'] ?? $schedule['venue_capacity']) - $schedule['enrolled_count']);
// 格式化状态
$schedule['status_text'] = $this->getStatusText($schedule['status']);
// 格式化创建方式
$schedule['created_by_text'] = $schedule['created_by'] == 'manual' ? '手动安排' : '系统创建';
// 处理图片路径
$schedule['coach_avatar'] = $schedule['coach_avatar'] ? $this->formatImageUrl($schedule['coach_avatar']) : '';
// 获取班级相关信息
if (!empty($schedule['class_id'])) {
$schedule['class_info'] = Db::name($this->prefix . 'class')
->where('id', $schedule['class_id'])
->field('id, class_name, class_level, total_students')
->find();
} else {
$schedule['class_info'] = null;
}
// 获取历史变更记录
$schedule['change_history'] = Db::name($this->prefix . 'course_schedule_changes')
->where('schedule_id', $scheduleId)
->order('created_at DESC')
->select()
->toArray();
return $schedule;
} catch (\Exception $e) {
return ['code' => 0, 'msg' => $e->getMessage()];
}
}
}

173
niucloud/课程安排资料.md

@ -0,0 +1,173 @@
school_campus(校区表)
CREATE TABLE `school_campus` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`campus_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '校区名称',
`campus_address` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '校区地址',
`campus_preview_image` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '校区预览图,存储图片路径',
`campus_coordinates` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '校区坐标,格式为经度,纬度',
`campus_introduction` text COLLATE utf8mb4_general_ci COMMENT '校区介绍',
`campus_status` tinyint DEFAULT '1' COMMENT '校区状态:0-禁用,1-启用',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '校区创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '校区更新时间',
`delete_time` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段,0表示未删除,非空表示已删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='校区表';
做为基础数据,下面一级就是场地。
school_venue(场地表)
CREATE TABLE `school_venue` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '场地编号',
`campus_id` int NOT NULL COMMENT '校区ID',
`venue_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '场地名称',
`capacity` int NOT NULL COMMENT '场地可容纳人数上限',
`availability_status` tinyint(1) NOT NULL COMMENT '场地可用状态: 1-可用, 0-不可用',
`time_range_type` enum('range','fixed','all') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '场地可用时间范围类型: range-范围类型, fixed-固定时间范围类型',
`time_range_start` varchar(8) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '范围类型的开始时间',
`time_range_end` varchar(8) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '范围类型的结束时间',
`fixed_time_ranges` json DEFAULT NULL COMMENT '固定时间范围类型的可用时间, 存储为JSON数组',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted_at` int DEFAULT '0' COMMENT '逻辑删除时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='场地表';
场地表做为课程的容器,主要是用来限制人数,时间,和可用状态。如果场地被设置为不可用,那么该场地下的所有课程都会被取消。其中time_range_type字段决定这个时间范围是范围类型还是固定时间类型。
这个字段配合time_range_start,time_range_end,fixed_time_ranges这三个字段,可以确定这个场地可用的时间范围。
school_course(课程表)
CREATE TABLE `school_course` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '课程编号',
`course_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '课程名称',
`course_type` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '课程类型',
`duration` int NOT NULL COMMENT '课程时长',
`session_count` int NOT NULL COMMENT '课时数量',
`single_session_count` int NOT NULL DEFAULT '0' COMMENT '单次消课数量',
`gift_session_count` int NOT NULL DEFAULT '0' COMMENT '赠送课时数量',
`price` decimal(10,2) NOT NULL COMMENT '课程价格',
`internal_reminder` int NOT NULL COMMENT '内部提醒课时',
`customer_reminder` int NOT NULL COMMENT '客户提醒课时',
`remarks` text COLLATE utf8mb4_general_ci COMMENT '课程备注',
`created_at` int DEFAULT '0' COMMENT '创建时间',
`updated_at` int DEFAULT '0' COMMENT '更新时间',
`deleted_at` int DEFAULT '0' COMMENT '逻辑删除时间',
`contract_id` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='课程表';
课程表主要是用户上课的内容,在排课阶段主要是看用户排的是什么课在哪个课程安排里展示。课程表和课程安排表是一对多的关系,一个课程可以有多个课程安排。但是在一个周期范围内,这个安排的数量始终是不会超过课程表的课时数量的。
举例来说single_session_count字段表示一次课程安排要消耗多少课时,如果值为 2,session_count课时数量是 10,那么这次课程周期一个用户只能排 5 次课程安排。
school_course_schedule(课程安排表)
CREATE TABLE `school_course_schedule` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '课程安排编号',
`campus_id` int NOT NULL COMMENT '校区ID',
`venue_id` int NOT NULL COMMENT '场地ID',
`course_date` date NOT NULL COMMENT '上课日期',
`time_slot` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '上课时段',
`course_id` int NOT NULL COMMENT '课程ID',
`coach_id` int NOT NULL COMMENT '上课教练ID',
`participants` json DEFAULT NULL COMMENT '参与人员列表,存储为JSON数组,包含学员ID和来源信息',
`student_ids` json DEFAULT NULL COMMENT '上课学生ID列表,存储为JSON数组',
`available_capacity` int DEFAULT NULL COMMENT '根据场地容量判断的可安排学员位置数量',
`status` enum('pending','upcoming','ongoing','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'pending' COMMENT '课程状态: pending-待开始, upcoming-即将开始, ongoing-进行中, completed-已结束',
`auto_schedule` tinyint(1) DEFAULT NULL COMMENT '是否自动排课1是0否',
`created_by` enum('manual','system') COLLATE utf8mb4_general_ci NOT NULL COMMENT '课程安排创建方式: manual-人员安排, system-系统创建',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted_at` int DEFAULT '0' COMMENT '逻辑删除时间',
`assistant_ids` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '助教ID列表(逗号分隔)',
`education_id` int DEFAULT NULL COMMENT '教务ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=120 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='课程安排表';
这个表是在学校层面针对教室的情况做的课程安排情况表。这个表和课程表是一对多的关系,一个课程可以有多个课程安排。而且支持一次配置后自动排课的功能。这个表的主要作用是记录课程安排的情况,包括上课日期,时间段,教练,参与人员等信息。
school_person_course_schedule(人员与课程安排关系表)
CREATE TABLE `school_person_course_schedule` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '关系编号',
`resources_id` int DEFAULT NULL COMMENT '资源ID',
`person_id` int DEFAULT NULL COMMENT '人员ID',
`student_id` int DEFAULT NULL COMMENT '学员ID',
`person_type` enum('student','customer_resource') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '人员类型: student-正式学员, customer_resource-客户资源,teacher教练',
`schedule_id` int NOT NULL COMMENT '课程安排ID',
`course_date` date NOT NULL COMMENT '上课日期',
`schedule_type` tinyint(1) DEFAULT NULL COMMENT '课程安排类型1临时课2固定课',
`course_type` tinyint(1) DEFAULT NULL COMMENT '课程类型1加课2补课3 等待位',
`time_slot` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '上课时段',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` int DEFAULT '0' COMMENT '删除',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态0待上课1已上课2请假',
`remark` varchar(512) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '请假备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='人员与课程安排关系表';
这个表记录着学员和school_course_schedule中安排的关系。这里面有几种情况 1.正式的付费学员上课(临时的)2.临时的体验学员上课 3.正式的付费学员固定这个时段上课的。
然后这个课程安排的属性还有是正常消耗课时的,还是补课、还是送课。还是这个教室没有位置单纯是等待的课程。
school_student_courses(学员课程表)
CREATE TABLE `school_student_courses` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '记录编号',
`student_id` int NOT NULL COMMENT '学员ID',
`course_id` int NOT NULL COMMENT '课程ID',
`total_hours` int NOT NULL COMMENT '总正式课时数',
`gift_hours` int DEFAULT '0' COMMENT '赠送课时数',
`start_date` date NOT NULL COMMENT '课程开始日期',
`end_date` date NOT NULL COMMENT '课程结束日期',
`use_total_hours` int NOT NULL DEFAULT '0' COMMENT '已使用课包课时数',
`use_gift_hours` int NOT NULL DEFAULT '0' COMMENT '已使用课包赠送课时数',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`single_session_count` int DEFAULT NULL COMMENT '单次消课数量',
`status` tinyint(1) DEFAULT NULL COMMENT '课程状态1有效2过期3等待期4延期',
`resource_id` int DEFAULT NULL COMMENT '资源ID',
`main_coach_id` int DEFAULT NULL COMMENT '主教练ID',
`assistant_ids` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '助教ID列表(逗号分隔)',
`education_id` int DEFAULT NULL COMMENT '教务ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='学员课程表';
这个是一个学员在一个课程周期里可以上课的课时记录以及课时有效期记录,在这个周期范围内为这个学员服务的教练和助教信息。这个表的主要作用是记录学员的课程信息,包括总课时数,赠送课时数,开始和结束日期等信息。这个表和人员与课程安排关系表是一对多的关系,一个学员可以有多个课程安排。
school_student_course_usage(学员课程使用记录表)
CREATE TABLE `school_student_course_usage` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '记录编号',
`student_course_id` int NOT NULL COMMENT '学员课程ID(关联到student_courses表)',
`used_hours` int NOT NULL COMMENT '本次使用的课时数',
`usage_date` date NOT NULL COMMENT '课时使用日期',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='学员课时消费记录表';
这个表记录了学员在某个课程周期内使用的课时数,这个表和学员课程表是一对多的关系,一个学员可以有多个课时使用记录。
school_class(班级表)
CREATE TABLE `school_class` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '班级编号',
`campus_id` int NOT NULL COMMENT '校区ID',
`campus_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '校区名称',
`class_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '班级名称',
`head_coach` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '班继续
级主教练',
`age_group` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '班级授课年龄段',
`class_type` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '班级类型',
`assistant_coach` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '班级助教',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`deleted_at` int DEFAULT '0' COMMENT '逻辑删除时间',
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '班级状态(1开启 2关闭)',
`sort_order` int NOT NULL COMMENT '班级排序',
`remarks` text COLLATE utf8mb4_general_ci COMMENT '班级备注',
`educational_id` int NOT NULL DEFAULT '0' COMMENT '教务id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='班级表';
学员报名时会分配班级,默认班级里的主教练和助教会被分配到学员课程表里。班级表主要是记录班级的信息,包括班级名称,主教练,助教,授课年龄段等信息。这个表和学员课程表是一对多的关系,一个班级可以有多个学员。
school_class_resources_rel(班级学员关系表)
CREATE TABLE `school_class_resources_rel` (
`id` int NOT NULL AUTO_INCREMENT,
`class_id` int NOT NULL COMMENT '班级id',
`resource_id` int DEFAULT NULL COMMENT '资源id',
`campus_id` int DEFAULT NULL COMMENT '校区id',
`source_id` int DEFAULT NULL COMMENT '数据id',
`source_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '数据资源类型student是学员,temporary是非正式学员',
`join_time` int DEFAULT NULL COMMENT '加入时间',
`out_time` int DEFAULT NULL COMMENT '离开时间',
`status` tinyint DEFAULT NULL COMMENT '状态1新入2续费3过期4转班5转校',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='班级和资源关系表';
这个表记录了班级和资源的关系,包括班级id,资源id,校区id,数据id,数据资源类型,加入时间,离开时间,状态等信息。这个表和班级表是一对多的关系,一个班级可以有多个资源。

56
uniapp/api/apiRoute.js

@ -397,8 +397,62 @@ export default {
return await http.post('/updateStudentCoursePersonnel', data);
},
// 获取订单支付二维码
// 获取订单支付二维码
async getOrderPayQrcode(data = {}) {
return await http.get('/getQrcode', data);
},
//↓↓↓↓↓↓↓↓↓↓↓↓-----课程安排相关接口-----↓↓↓↓↓↓↓↓↓↓↓↓
// 获取课程安排列表
async getCourseScheduleList(data = {}) {
return await http.get('/courseSchedule/list', data);
},
// 获取课程安排详情
async getCourseScheduleInfo(data = {}) {
return await http.get('/courseSchedule/info', data);
},
// 创建课程安排
async createCourseSchedule(data = {}) {
return await http.post('/courseSchedule/create', data);
},
// 批量创建课程安排
async batchCreateCourseSchedule(data = {}) {
return await http.post('/courseSchedule/batchCreate', data);
},
// 更新课程安排
async updateCourseSchedule(data = {}) {
return await http.post('/courseSchedule/update', data);
},
// 删除课程安排
async deleteCourseSchedule(data = {}) {
return await http.post('/courseSchedule/delete', data);
},
// 获取场地列表
async getCourseScheduleVenues(data = {}) {
return await http.get('/courseSchedule/venues', data);
},
// 获取场地可用时间
async getVenueAvailableTime(data = {}) {
return await http.get('/courseSchedule/venueAvailableTime', data);
},
// 检查教练时间冲突
async checkCoachConflict(data = {}) {
return await http.get('/courseSchedule/checkCoachConflict', data);
},
// 获取课程安排统计
async getCourseScheduleStatistics(data = {}) {
return await http.get('/courseSchedule/statistics', data);
},
// 学员加入课程安排
async joinCourseSchedule(data = {}) {
return await http.post('/courseSchedule/joinSchedule', data);
},
// 学员退出课程安排
async leaveCourseSchedule(data = {}) {
return await http.post('/courseSchedule/leaveSchedule', data);
},
// 获取筛选选项
async getCourseScheduleFilterOptions(data = {}) {
return await http.get('/courseSchedule/filterOptions', data);
},
}

8
uniapp/common/axios.js

@ -152,19 +152,11 @@ export default {
uni.showLoading({
title: '加载中...'
});
console.log('请求配置:', interceptedConfig);
console.log('请求URL:', interceptedConfig.url);
console.log('请求方法:', interceptedConfig.method);
console.log('请求数据:', interceptedConfig.data);
uni.request({
...interceptedConfig,
success: (res) => {
try {
console.log('原始响应数据:', res);
const response = responseInterceptor(res);
console.log('处理后的响应数据:', response);
resolve(response);
} catch (error) {
console.error('请求处理失败:', error);

66
uniapp/common/util.js

@ -1,4 +1,4 @@
import {img_domian} from "./config";
import {img_domian,Api_url} from "./config";
import marketApi from '@/api/apiRoute.js';
function formatTime(time) {
@ -326,6 +326,67 @@ async function getDict(dictKey) {
}
}
/**
* 上传文件通用方法
* @param {string} filePath 文件路径
* @param {Function} successCallback 成功回调
* @param {Function} errorCallback 失败回调
*/
export function uploadFile(filePath, successCallback, errorCallback) {
const token = uni.getStorageSync('token') || '';
uni.uploadFile({
url: Api_url + '/file/image', // 上传地址
filePath: filePath,
name: 'file',
header: {
'token': token
},
success: (res) => {
let response;
try {
// 去除 BOM 字符并解析 JSON
response = JSON.parse(res.data.replace(/\ufeff/g, '') || '{}');
} catch (e) {
uni.showToast({ title: '响应格式错误', icon: 'none' });
if (errorCallback) errorCallback(e);
return;
}
if (response.code === 1) {
const fileData = {
url: response.data.url,
extname: response.data.ext,
name: response.data.name
};
if (successCallback) {
successCallback(fileData);
}
} else if (response.code === 401) {
uni.showToast({ title: response.msg, icon: 'none' });
setTimeout(() => {
uni.navigateTo({ url: '/pages/student/login/login' });
}, 1000);
} else {
uni.showToast({ title: response.msg || '上传失败', icon: 'none' });
if (errorCallback) errorCallback(response);
}
},
fail: (err) => {
uni.showToast({ title: err.errMsg || '网络异常', icon: 'none' });
if (errorCallback) errorCallback(err);
}
});
}
/**
* 获取服务器上的资源完整 url
* @return {string} 完整的资源 URL
*/
function getResourceUrl(resource) {
//如果没有 http 协议,则加上 http 协议+服务域名
return resource.indexOf('http') === -1 ? 'https://' + img_domian + resource : resource;
}
module.exports = {
loginOut,
openHomeView,
@ -336,5 +397,6 @@ module.exports = {
hexToRgba,
img,
formatToDateTime,
getDict
getDict,
uploadFile
}

5
uniapp/components/AQ/AQUplodeImgMulti.vue

@ -159,12 +159,8 @@ import {Api_url} from "../../common/config";
header: {
'token': `${token}`,//token
},
// formData: {
// 'age': ''
// },
success: (uploadFileRes) => {
let res = JSON.parse(uploadFileRes.data.replace(/\ufeff/g, "") || "{}")
console.log('上传成功1',res);
if (res.code == 1){
let _arr = {}
// 3uni-app
@ -173,7 +169,6 @@ import {Api_url} from "../../common/config";
_arr.name = res.data.name
console.log('xxx',_arr)
this.fileList.push(_arr)
console.log('上传成功2',_arr);
this.filePathArr.push(res.data.url)
//
this.emitUploadSuccess(this.filePathArr)

240
uniapp/components/CommentList.vue

@ -1,240 +0,0 @@
<template>
<view>
<view style="" v-if="list.length>0">
<fui-collapse-item v-for="(item,index) in list" :key="index" :disabled="shouldDisableItem(item)" :arrow="shouldDisableItems(item)">
<view class="fui-item__box">
<view style="display: flex;width: 100%;position: relative;">
<view class="content_img" v-if="item.headimg != 0">
<image style="width: 50rpx;height:50rpx;border-radius: 50%;" :src="item.headimg"></image>
</view>
<view class="content_img" v-else>
<image style="width: 50rpx;height: 50rpx;border-radius: 50%;" src="/static/images/home/tixing.png"></image>
</view>
<view style="padding: 5rpx 20rpx;">
{{item.content}}
</view>
<view style="position: absolute;right: 7%;top: 1%;" @click="comments(item.comment_id)">
评论
</view>
</view>
<view class="middles_con" v-if="item.imagesarr!=0" style="">
<view style="width: 32%;height: 200rpx;margin:2rpx;" v-for="(item2, index2) in item.imagesarr"
:key="index2" @click="tankuang(item2,item.imagesarr)">
<image :src="item2" style="width: 100%;height: 100%;"></image>
</view>
</view>
</view>
<template v-slot:content>
<view class="fui-descr" v-if="item.comments_list.length>0">
<view class="flex" style="position: relative;" v-for="(item1,index1) in item.comments_list" :key="index1">
<view style="display: flex;width: 100%;position: relative;">
<view class="content_img" v-if="item1.headimg != 0">
<image style="width: 50rpx;height:50rpx;border-radius: 50%;" :src="item1.headimg"></image>
</view>
<view class="content_img" v-else>
<image style="width: 50rpx;height: 50rpx;border-radius: 50%;" src="/static/images/home/tixing.png"></image>
</view>
<view style="padding: 5rpx 20rpx;">
{{item1.content}}
</view>
</view>
<view class="middles_con" v-if="item1.imagesarr!=0" style="">
<view style="width: 32%;height: 200rpx;margin:2rpx;" v-for="(item2, index2) in item1.imagesarr"
:key="index2" @click="tankuang(item2,item1.imagesarr)">
<image :src="item2" style="width: 100%;height: 100%;"></image>
</view>
</view>
</view>
</view>
<view class="fui-descr" v-else style="text-align: center;">
暂无子评论
</view>
</template>
</fui-collapse-item>
</view>
<view style="font-size: 40rpx;font-weight: bold;text-align: center;padding-top: 50rpx;" v-else>
来当第一个评论的人吧!
</view>
<view style="margin-top: 30rpx;width: 100%;background-color: #fff;">
<view style="width: 95%;height: 200rpx;margin: auto;">
<textarea v-model="textAreaValue" placeholder="输入内容"
:style="{ width: '100%',padding:'30rpx'}"></textarea>
</view>
<view class="fui-section__title" style="width: 95%;margin: auto;z-index: 9999;">
<fui-upload :max="3" immediate :url="url" ref="upload" @complete="complete" @success="success"
:isDel="false">
<fui-icon name="plus"></fui-icon>
</fui-upload>
</view>
<fui-button background="#00be8c" width="95%" :margin="['35rpx','0','0','35rpx']" @click="submit()">
</fui-button>
</view>
<view style="height: 30rpx;width: 100%;"></view>
</view>
</template>
<script>
// import user from '@/api/user.js';
import fuiCollapse from "@/components/firstui/fui-collapse/fui-collapse.vue"
import fuiCollapseItem from "@/components/firstui/fui-collapse-item/fui-collapse-item.vue"
import fuiUpload from "@/components/firstui/fui-upload/fui-upload.vue"
import fuiIcon from "@/components/firstui/fui-icon/fui-icon.vue"
import fuiButton from "@/components/firstui/fui-button/fui-button.vue"
export default {
name: 'CommentList',
props: {
list: Array, //
actid: {
type: String,
default: '' //
}, //
um_id: {
type: String,
default: '' //
}, //
},
components: {
fuiCollapse,
fuiCollapseItem,
fuiUpload,
fuiIcon,
fuiButton
},
data() {
return {
url: 'http://medication.zeyan.wang/dnseyeapi/Base/upload1',
urls: [],
status: '',
textAreaValue: '',
lists: [],
listlength: ''
}
},
onLoad() {
this.actid = uni.getStorageSync('actid');
this.um_id = uni.getStorageSync('um_id');
this.fetchData(this.actid)
},
methods: {
fetchData(actid) {
user.comment_list({
act_id: actid
}).then(res => {
console.log(res)
if (res.status == 200) {
this.list = res.data
} else {
uni.showToast({
title: res.msg,
icon: 'none'
})
}
});
},
onPullDownRefresh(){
this.fetchData(this.actid)
},
complete(e) {
console.log(e)
this.status = e.status
// this.urls = e.urls
if (this.status === 'success' && e.action === 'upload') {
this.fui.toast('上传完成!')
// this.urls
// console.log(this.urls)
}
},
success(e) {
var responseString = e.res.data;
var cleanedString = responseString.replace('{"status":"200","msg":"文件上传成功","data":"', '').replace('"}',
'');
console.log(cleanedString)
this.urls += cleanedString + ','
},
submit() {
user.comment({
act_id: this.actid,
um_id: this.um_id,
content: this.textAreaValue,
image: this.urls,
}).then(res => {
console.log(res)
if (res.status == 200) {
this.fetchData(this.actid)
this.textAreaValue = ''
} else {
uni.showToast({
title: res.msg,
icon: 'none'
})
}
});
},
tankuang(url,arr){
uni.previewImage({
current: url,
urls: arr,
loop: true,
});
},
comments(id){
uni.setStorageSync('comment_id',id);
uni.redirectTo({
url: '/pages/index/comments'
});
},
shouldDisableItem(item){
return item.comments_list.length == 0;
},
shouldDisableItems(item){
return !item.comments_list.length == 0;
}
}
}
</script>
<style scoped>
.middles_con {
width: 100%;
display: flex;
margin: 20rpx 0;
font-size: 30rpx;
}
.flex {
padding: 20rpx 45rpx;
}
.fui-item__box {
width: 100%;
padding: 26rpx 32rpx;
box-sizing: border-box;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.fui-logo {
width: 48rpx;
height: 48rpx;
margin-right: 24rpx;
display: block;
}
.fui-descr {
width: 100%;
padding: 32rpx;
font-size: 28rpx;
line-height: 52rpx;
color: #7F7F7F;
word-break: break-all;
box-sizing: border-box;
}
</style>

166
uniapp/components/J-skeleton/J-skeleton.vue

@ -1,166 +0,0 @@
<template>
<view>
<view v-if="loading" class="skeleton" :class="{ animate: animate }" :style="{ justifyContent: flexType}">
<!-- 轮播图 -->
<view
v-if="imgTitle"
class="skeleton-imgTitle"
style="width: 95%;border-radius: 10px;height: 100px;display: block;"
></view>
<!-- 头像图 -->
<view
v-if="showAvatar && !imgTitle"
class="skeleton-avatar"
v-for="(item, index) in nameRow"
:key="index"
:class="[avatarShape]"
:style="{ width: avatarSize, height: avatarSize}"
></view>
<!-- 文字条 -->
<view class="skeleton-content" v-if="showTitle && !imgTitle">
<view class="skeleton-title" :style="{ width: titleWidth }"></view>
<view class="skeleton-rows">
<view
class="skeleton-row-item"
v-for="(item, index) in rowList"
:key="index"
:style="{ width: item.width }"
></view>
</view>
</view>
</view>
<view v-else><slot></slot></view>
</view>
</template>
<script>
const DEFAULT_ROW_WIDTH = '100%'
const DEFAULT_LAST_ROW_WIDTH = '60%'
export default {
props: {
loading: {
type: Boolean,
default: true,
},
imgTitle: {
type: Boolean,
default: false,
},
nameRow:{
type: Number,
default: 1,
},
flexType:{
type: String,
default: 'flex-start', // center space-between space-around flex-start flex-end
},
showAvatar: {
type: Boolean,
default: true,
},
avatarSize: {
type: String,
default: '50px',
},
avatarShape: {
type: String,
default: 'round', // square | round
},
showTitle: {
type: Boolean,
default: false,
},
titleWidth: {
type: String,
default: '40%',
},
row: {
type: Number,
default: 3,
},
animate: {
type: Boolean,
default: true,
},
},
data() {
return {}
},
computed: {
rowList() {
let list = []
for (let i = 0; i < this.row; i++) {
list.push({
width: i === this.row - 1 && i !== 0 ? DEFAULT_LAST_ROW_WIDTH : DEFAULT_ROW_WIDTH,
})
}
return list
},
},
}
</script>
<style scoped>
.skeleton {
display: flex;
margin: 16px;
--bg-color: #f2f3f5;
--row-height: 16px;
--row-margin-top: 16px;
}
.skeleton-imgTitle {
flex-wrap: wrap;
background: var(--bg-color);
margin: 10px auto;
}
.skeleton-avatar {
flex-shrink: 0;
background: var(--bg-color);
margin-right: 8px;
}
.skeleton-avatar.round {
border-radius: 50%;
}
.skeleton-content {
width: 100%;
}
.skeleton-title {
background-color: var(--bg-color);
height: var(--row-height);
}
.skeleton-title + .skeleton-rows {
margin-top: var(--row-margin-top);
}
.skeleton-rows {
}
.skeleton-row-item {
background-color: var(--bg-color);
height: var(--row-height);
}
.skeleton-row-item:not(:first-child) {
margin-top: var(--row-margin-top);
}
.skeleton.animate {
animation: skeleton-blink 1.2s ease-in-out infinite;
}
@keyframes skeleton-blink {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
</style>

77
uniapp/components/J-skeleton/README.md

@ -1,77 +0,0 @@
# skeleton
感谢原作者 https://ext.dcloud.net.cn/plugin?id=852
自己项目非常需要骨架,正好原作者发布了1.0 根据自己项目 自己修改了下。
目前仅支持:
1.轮播图
2.分类栏
3.头像
4.文章条
5.动态心情
以上是根据自己项目修改的,后续再拓展,或者自己根据自己项目修改,原作者写的还是很灵活的,修改方便!
## 属性说明
|属性名|类型|默认值|说明|
| -- | -- | --|--|
| loading | Boolean | true | 是否显示占位图 |
| flexType | String | flex-start | 排列方式 center 居中 √ space-between 两端对齐 √ space-around 子元素拉手分布 √ flex-start 居左 flex-end 居右 |
| imgTitle | Boolean | false | 轮播图占位图 |
| showAvatar | Boolean | true | 是否显示头像占位图 |
| nameRow | Number | 1 | 显示头像圆1个 |
| avatarSize | String | 50px | 头像站占位图大小 |
| avatarShape | String | round | 头像形状,可选值:round, square |
| showTitle | Boolean | true | 是否显示标题占位图 |
| titleWidth | String | 40% | 标题占位图宽度 |
| row | Number| 3 | 标题段落占位图行数 |
| animate | Boolean | true | 是否开启动画 |
## 使用示例
```html
<skeleton
:loading="loading"
:avatarSize="skeleton1.avatarSize"
:row="skeleton1.row"
:showTitle="skeleton1.showTitle"
>
<view class="section-content">我是段落1</view>
</skeleton>
```
```javascript
import Skeleton from '../components/skeleton/index.vue'
export default {
components: {
Skeleton
},
data() {
return {
loading: true,
skeleton1 : {
avatarSize: '52px',
row: 3,
showTitle: true,
}
}
},
created() {
this.reloadData()
},
methods: {
reloadData() {
this.loading = true
setTimeout(() => {
this.loading = false
}, 3000)
},
},
}
```
## 效果图
![](http://images.alisali.cn/img_20191014113211.png)

438
uniapp/components/aui-dialog/aui-dialog.vue

@ -1,438 +0,0 @@
<template name="aui-dialog">
<view class="aui-dialog" v-if="SHOW" :class="{'aui-dialog-in': FADE==1, 'aui-dialog-out': FADE==0}">
<view class="aui-mask" v-if="mask" @touchmove.stop.prevent @click.stop="maskTapClose ? hide() : ''"></view>
<view class="aui-dialog-main"
:class="{
'aui-dialog-main-style-1': theme==1,
'aui-dialog-main-style-2': theme==2,
'aui-dialog-main-style-3': theme==3
}"
>
<view class="aui-dialog-title" v-if="title">{{title}}</view>
<view class="aui-dialog-content" v-if="msg!=''" :style="{'text-align': msg.length > 15 ? 'left' : 'center'}" v-html="msg"></view>
<view class="aui-dialog-content" v-if="items.length > 0">
<view class="aui-dialog-input-list" v-for="(item, index) in items" :key="index" :data-index="index">
<view class="aui-dialog-input-label" v-if="item.label">{{item.label}}</view>
<view class="aui-dialog-input-list-input">
<input :type="item.type ? item.type : 'text'" :value="item.value" :data-index="index" @input="_onInput" :placeholder="item.placeholder" />
</view>
</view>
</view>
<view class="aui-dialog-down">
<view
class="aui-dialog-down-btn"
v-for="(item, index) in btns"
:class="{'aui-dialog-down-cancel-btn': item.name=='取消', 'aui-dialog-down-delete-btn': item.name=='删除'}"
:key="index"
:data-index="index"
:style="{
'color': touchIndex == index ? touchStyle.color : item.color,
'background': touchIndex == index ? touchStyle.background : '',
'width': theme==1?'calc(100% / '+ btns.length +')':''
}"
@click.stop="_btnTab($event)"
@touchstart="_btnTouchStart($event)"
@touchmove="_btnTouchEnd($event)"
@touchend="_btnTouchEnd($event)"
>{{item.name}}</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: "aui-dialog",
props: {
title: { //
type: String,
default: ''
},
msg: { //
type: String,
default: ''
},
mask: { //false
type: Boolean,
default: true
},
maskTapClose: { //
type: Boolean,
default: true
},
btns: { //("row")("col")
type: Array,
default (){
return [
{name: '确定', color: '#197DE0', isTouch: false}
]
}
},
items: {
type: Array,
default (){
return [
{label: '', type: 'text', value: '', placeholder: ''}
]
}
},
theme: { //
type: Number,
default: 1
},
},
data() {
return {
SHOW: false,
FADE: -1,
ITEMS: [],
touchIndex: -1, //
touchStyle: { //
color: '',
background: '#EFEFEF'
}
};
},
created(){
var _this = this;
},
onLoad(){
},
methods:{
//
show(){
var _this = this;
return new Promise(function(resolve, reject){
_this.SHOW = true;
var _showtimer = setTimeout(()=>{
_this.FADE = 1;
clearTimeout(_showtimer);
},50)
resolve();
});
},
//
hide(){
var _this = this;
return new Promise(function(resolve, reject){
_this.FADE = 0;
var _hidetimer = setTimeout(()=>{
_this.SHOW = false;
_this.FADE = -1;
clearTimeout(_hidetimer);
},50)
resolve();
});
},
//
_btnTab(e){
var _this = this,
index = Number(e.currentTarget.dataset.index);
_this.hide();
var _closetimer = setTimeout(()=>{
var data = {
status: 0,
msg: _this.btns[index].name,
index: index
};
_this.$emit("callback", data);
clearTimeout(_closetimer);
},100)
},
//
_onInput(e){
var _this = this,
index = Number(e.currentTarget.dataset.index),
value = e.detail.value;
if(_this.ITEMS.length <= 0)
{
_this.items.forEach((item, index)=>{
_this.ITEMS.push({label: item.label, type: item.type, value: item.value, placeholder: item.placeholder});
});
}
_this.$set(_this.ITEMS[index], 'value', value);
},
getVal(){
var _this = this;
setTimeout(()=>{
_this.ITEMS = [];
},200)
return _this.ITEMS;
},
_btnTouchStart(e){
var _this = this,
index = Number(e.currentTarget.dataset.index);
_this.touchIndex = index;
},
_btnTouchEnd(e){
var _this = this,
index = Number(e.currentTarget.dataset.index);
_this.touchIndex = -1;
},
}
}
</script>
<style>
/* dialog 模态弹窗样式 */
.aui-dialog{
width: 100vw;
height: 100vh;
opacity: 0;
position: fixed;
top: 0;
left: 0;
z-index: 999;
}
.aui-dialog-main{
min-width: 280px;
max-width: 300px;
background: #fff;
border-radius: 13px;
position: absolute;
top: 50%;
left: 50%;
-webkit-transition-property: -webkit-transform,opacity;
transition-property: transform,opacity;
-webkit-transform: translate3d(-50%,-50%,0) scale(1.185);
transform: translate3d(-50%,-50%,0) scale(1.185);
opacity: 0;
z-index: 999;
}
.aui-dialog.aui-dialog-in{
-webkit-transition-duration: 100ms;
transition-duration: 100ms;
opacity: 1;
}
.aui-dialog.aui-dialog-out{
-webkit-transition-duration: 100ms;
transition-duration: 100ms;
opacity: 0;
}
.aui-dialog.aui-dialog-in .aui-mask{
-webkit-transition-duration: 200ms;
transition-duration: 200ms;
opacity: 1;
}
.aui-dialog.aui-dialog-out .aui-mask{
-webkit-transition-duration: 200ms;
transition-duration: 200ms;
opacity: 0;
}
.aui-dialog.aui-dialog-out .aui-dialog-main{
-webkit-transition-duration: 200ms;
transition-duration: 200ms;
-webkit-transform: translate3d(-50%,-50%,0) scale(0.8);
transform: translate3d(-50%,-50%,0) scale(0.8);
opacity: 0
}
.aui-dialog.aui-dialog-in .aui-dialog-main{
-webkit-transition-duration: 200ms;
transition-duration: 200ms;
-webkit-transform: translate3d(-50%,-50%,0) scale(1);
transform: translate3d(-50%,-50%,0) scale(1);
opacity: 1;
}
.aui-dialog-title{
width: 100%;
height: 40px;
line-height: 55px;
position: relative;
font-size: 18px;
/*font-weight: bolder;*/
display: inline-block;
border-top-left-radius: 13px;
border-top-right-radius: 13px;
text-align: center;
color: #333;
box-sizing: border-box;
}
.aui-dialog-content{
width: 100%;
max-height: 70vh;
line-height: 27px;
font-size: 16px;
color: #555555;
text-align: center;
display: inline-block;
overflow-y: scroll;
padding: 30px 20px 25px 20px;
box-sizing: border-box;
}
.aui-dialog-content::-webkit-scrollbar {
width: 0px;
}
.aui-dialog-down{
width: 100%;
height: 50px;
text-align: right;
position: relative;
overflow: hidden;
}
.aui-dialog-down-btn{
width: auto;
height: 100%;
display: inline-block;
font-size: 17px;
color: #197DE0;
text-align: center;
position: relative;
}
.aui-dialog-down-btn.active{
background: #EFEFEF;
}
.aui-dialog-down-cancel-btn{
color: #909090;
}
.aui-dialog-down-delete-btn{
color: #FF0000;
}
.aui-dialog-main-style-1 .aui-dialog-content{
text-align: center;
}
.aui-dialog-main-style-1 .aui-dialog-down{
height: 50px;
display: flex;
justify-content: center;
align-items: center;
}
.aui-dialog-main-style-1 .aui-dialog-down-btn{
line-height: 50px;
display: flex;
justify-content: center;
align-items: center;
flex: auto;
}
.aui-dialog-main-style-1 .aui-dialog-down:before{
content: '';
width: 100%;
height: 1px;
-ms-transform: scaleY(.3);
-webkit-transform: scaleY(.3);
transform: scaleY(.3);
background: rgba(100,100,100,.3);
position: absolute;
top: 0;
left: 0;
z-index: 999;
}
.aui-dialog-main-style-1 .aui-dialog-down-btn:first-child{
border-bottom-left-radius: 13px;
}
.aui-dialog-main-style-1 .aui-dialog-down-btn:last-child{
border-bottom-right-radius: 13px;
}
.aui-dialog-main-style-1 .aui-dialog-down-btn:after{
content: '';
width: 1px;
height: 100%;
-ms-transform: scaleX(.3);
-webkit-transform: scaleX(.3);
transform: scaleX(.3);
background: rgba(100,100,100,.3);
position: absolute;
top: 0;
right: 0;
z-index: 999;
}
.aui-dialog-main-style-1 .aui-dialog-down-btn:last-child:after{display: none;}
.aui-dialog-main-style-2{
border-radius: 6px;
}
.aui-dialog-main-style-2 .aui-dialog-title{
padding: 0 15px;
box-sizing: border-box;
}
.aui-dialog-main-style-2 .aui-dialog-down{
height: 40px;
padding: 0 10px 10px 10px;
box-sizing: border-box;
}
.aui-dialog-main-style-2 .aui-dialog-down-btn{
height: 30px;
line-height: 30px;
padding: 0 10px;
margin: 0 0 0 10px;
}
.aui-dialog-main-style-3 .aui-dialog-down{
height: auto;
}
.aui-dialog-main-style-3 .aui-dialog-down-btn{
width: 100%;
line-height: 50px;
}
.aui-dialog-main-style-3 .aui-dialog-down-btn:before{
content: '';
width: 100%;
height: 1px;
-ms-transform: scaleY(.3);
-webkit-transform: scaleY(.3);
transform: scaleY(.3);
background: rgba(100,100,100,.4);
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
.aui-dialog-main-style-3 .aui-dialog-down-btn:last-child{
border-bottom-left-radius: 13px;
border-bottom-right-radius: 13px;
}
.aui-dialog-main-style-3 .aui-dialog-down-btn:first-child:after{display: none;}
/*input 输入弹窗样式设置*/
.aui-dialog-input-list{
width: 100%;
position: relative;
text-align: left;
}
.aui-dialog-input-list .aui-dialog-input-label{
width: 260px;
height: 40px;
line-height: 40px;
display: inline-block;
font-size: 16px;
color: #646464;
}
.aui-dialog-input-list-input{
width: 100%;
background: #FFFFFF;
border-radius: 3px;
border: none;
box-sizing: border-box;
padding: 2px;
margin: 0 0 15px 0;
color: #515151;
position: relative;
}
.aui-dialog-input-list input{
width: 100%;
height: 40px;
line-height: 20px;
border-radius: 3px;
border: none;
margin: 0;
padding: 0 10px;
box-sizing: border-box;
font-size: 15px;
color: #515151;
position: relative;
z-index: 1;
}
.aui-dialog-input-list-input:after{
content: '';
width: 200%;
height: 200%;
border: 1px solid rgba(100,100,100,.3);
-ms-transform: scale(.5, .5);
-webkit-transform: scale(.5, .5);
transform: scale(.5, .5);
position: absolute;
top: -50%;
left: -50%;
border-radius: 10px;
z-index: 0;
}
</style>

292
uniapp/components/aui-dialog/common/aui/css/aui.css

@ -1,292 +0,0 @@
.aui-content{
width: 100%;
height: 100vh;
/* #ifndef MP */
height: -webkit-calc(100vh - 44px);
height: calc(100vh - 44px);
/* #endif */
background: #EFEFEF;
overflow-y: scroll;
padding: 0;
box-sizing: border-box;
position: relative;
}
/* 横向分割线 */
.row-before{position: relative;}
.row-before:before{content: ''; width: 100%; height: 1px !important; background: rgba(100,100,100,.3); -ms-transform: scaleY(.3); -webkit-transform: scaleY(.3); transform: scaleY(.3); position: absolute; top: 0; right: 0; left: auto; z-index: 1;}
.row-after{position: relative;}
.row-after:after{content: ''; width: 100%; height: 1px !important; background: rgba(100,100,100,.3); -ms-transform: scaleY(.3); -webkit-transform: scaleY(.3); transform: scaleY(.3); position: absolute; bottom: 0; right: 0; left: auto; z-index: 1;}
/* 纵向分割线 */
.col-before{position: relative;}
.col-before:before{content: ''; width: 1px !important; height: 100%; background: rgba(100,100,100,.3); -ms-transform: scaleX(.3); -webkit-transform: scaleX(.3); transform: scaleX(.3); position: absolute; top: 0; left: 0; z-index: 1;}
.col-after{position: relative;}
.col-after:after{content: ''; width: 1px !important; height: 100%; background: rgba(100,100,100,.3); -ms-transform: scaleX(.3); -webkit-transform: scaleX(.3); transform: scaleX(.3); position: absolute; top: 0; right: 0; z-index: 1;}
/*按钮边框线*/
.border{position: relative;}
.border:after{content: ''; width: -webkit-calc(200% - 2px); width: calc(200% - 2px); height: -webkit-calc(200% - 2px); height: calc(200% - 2px); border-radius: 3px; border: 1px solid rgba(100,100,100,.3); position: absolute; left: -50%; top: -50%; -ms-transform: scale(.5, .5); -webkit-transform: scale(.5, .5); transform: scale(.5, .5); z-index: 1;}
.aui-lists{
width: 100%;
}
.aui-list{
width: 100%;
height: 55px;
font-size: 0;
padding: 0 15px;
background: #FFFFFF;
box-sizing: border-box;
position: relative;
z-index: 1;
}
.aui-list:after{
content: '';
width: 100%;
height: 1px;
background: rgba(100,100,100,.3);
-ms-transform: scaleY(.3);
-webkit-transform: scaleY(.3);
transform: scaleY(.3);
position: absolute;
bottom: 0;
left: 0;
}
.aui-list:last-child:after{
display: none;
}
.aui-list-title{
width: 100%;
height: 44px;
line-height: 44px;
font-size: 14px;
padding: 0 15px;
box-sizing: border-box;
color: #999;
display: inline-block;
}
.aui-list-left{
height: 100%;
line-height: 55px;
font-size: 15px;
color: #333;
display: inline-block;
vertical-align: top;
}
.aui-list-right{
height: 100%;
display: inline-block;
vertical-align: top;
float: right;
}
.aui-list-right .aui-btn-right{
height: 55px;
text-align: center;
line-height: 55px;
font-size: 14px;
display: inline-block;
color: #aaa;
vertical-align: top;
}
.aui-btn{
width: -webkit-calc(100% - 30px);
width: calc(100% - 30px);
height: 50px;
line-height: 50px;
text-align: center;
border: none;
color: #333;
font-size: 15px;
border-radius: 5px;
margin: 0 15px 15px 15px;
padding: 0 10px;
transition: background-color .2s;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
}
.aui-btn-blue{
background: #197DE0;
color: #FFF;
}
/* 遮罩层样式 */
.aui-mask{
width: 100%;
height: 100%;
background: rgba(0,0,0,.6);
-ms-animation: aui-fade-in .2s ease-out forwards;
-webkit-animation: aui-fade-in .2s ease-out forwards;
animation: aui-fade-in .2s ease-out forwards;
position: fixed;
top: 0px;
left: 0px;
z-index: 998;
}
/* 动画设计 */
/* fade-in */
@-ms-keyframes aui-fade-in{
0%{opacity: 0;}
100%{opacity: 1;}
}
@-webkit-keyframes aui-fade-in{
0%{opacity: 0;}
100%{opacity: 1;}
}
@keyframes aui-fade-in{
0%{opacity: 0;}
100%{opacity: 1;}
}
/* fade-out */
@-ms-keyframes aui-fade-out{
0%{opacity: 1;}
100%{opacity: 0;}
}
@-webkit-keyframes aui-fade-out{
0%{opacity: 1;}
100%{opacity: 0;}
}
@keyframes aui-fade-out{
0%{opacity: 1;}
100%{opacity: 0;}
}
/* aui-scale-in */
@-ms-keyframes aui-scale-in{
0%{-ms-transform: scale(0.8); -webkit-transform: scale(0.8); transform: scale(0.8);}
100%{-ms-transform: scale(1); -webkit-transform: scale(1); transform: scale(1);}
}
@-webkit-keyframes aui-scale-in{
0%{-ms-transform: scale(0.8); -webkit-transform: scale(0.8); transform: scale(0.8);}
100%{-ms-transform: scale(1); -webkit-transform: scale(1); transform: scale(1);}
}
@keyframes aui-scale-in{
0%{-ms-transform: scale(0.8); -webkit-transform: scale(0.8); transform: scale(0.8);}
100%{-ms-transform: scale(1); -webkit-transform: scale(1); transform: scale(1);}
}
/* aui-scale-out */
@-ms-keyframes aui-scale-out{
0%{-ms-transform: scale(1); -webkit-transform: scale(1); transform: scale(1);}
100%{-ms-transform: scale(0.8); -webkit-transform: scale(0.8); transform: scale(0.8);}
}
@-webkit-keyframes aui-scale-out{
0%{-ms-transform: scale(1); -webkit-transform: scale(1); transform: scale(1);}
100%{-ms-transform: scale(0.8); -webkit-transform: scale(0.8); transform: scale(0.8);}
}
@keyframes aui-scale-out{
0%{-ms-transform: scale(1); -webkit-transform: scale(1); transform: scale(1);}
100%{-ms-transform: scale(0.8); -webkit-transform: scale(0.8); transform: scale(0.8);}
}
/* aui-scale-in-tosmall */
@-ms-keyframes aui-scale-in-tosmall{
0%{-ms-transform: scale(1.2); -webkit-transform: scale(1.2); transform: scale(1.2);}
100%{-ms-transform: scale(1); -webkit-transform: scale(1); transform: scale(1);}
}
@-webkit-keyframes aui-scale-in-tosmall{
0%{-ms-transform: scale(1.2); -webkit-transform: scale(1.2); transform: scale(1.2);}
100%{-ms-transform: scale(1); -webkit-transform: scale(1); transform: scale(1);}
}
@keyframes aui-scale-in-tosmall{
0%{-ms-transform: scale(1.2); -webkit-transform: scale(1.2); transform: scale(1.2);}
100%{-ms-transform: scale(1); -webkit-transform: scale(1); transform: scale(1);}
}
/* aui-scale-in-tosmall-dialog */
@-ms-keyframes aui-scale-in-tosmall-dialog{
0%{-ms-transform: translate3d(-50%,-50%,0) scale(1.16); -webkit-transform: translate3d(-50%,-50%,0) scale(1.16); transform: translate3d(-50%,-50%,0) scale(1.16); opacity: 0;}
100%{-ms-transform: translate3d(-50%,-50%,0) scale(1); -webkit-transform: translate3d(-50%,-50%,0) scale(1); transform: translate3(-50%,-50%,0) scale(1); opacity: 1;}
}
@-webkit-keyframes aui-scale-in-tosmall-dialog{
0%{-ms-transform: translate3d(-50%,-50%,0) scale(1.16); -webkit-transform: translate3d(-50%,-50%,0) scale(1.16); transform: translate3d(-50%,-50%,0) scale(1.16); opacity: 0;}
100%{-ms-transform: translate3d(-50%,-50%,0) scale(1); -webkit-transform: translate3d(-50%,-50%,0) scale(1); transform: translate3(-50%,-50%,0) scale(1); opacity: 1;}
}
@keyframes aui-scale-in-tosmall-dialog{
0%{-ms-transform: translate3d(-50%,-50%,0) scale(1.16); -webkit-transform: translate3d(-50%,-50%,0) scale(1.16); transform: translate3d(-50%,-50%,0) scale(1.16); opacity: 0;}
100%{-ms-transform: translate3d(-50%,-50%,0) scale(1); -webkit-transform: translate3d(-50%,-50%,0) scale(1); transform: translate3(-50%,-50%,0) scale(1); opacity: 1;}
}
/* aui-scale-out-tosmall-dialog */
@-ms-keyframes aui-scale-out-tosmall-dialog{
0%{-ms-transform: translate3d(-50%,-50%,0) scale(1); -webkit-transform: translate3d(-50%,-50%,0) scale(1); transform: translate3d(-50%,-50%,0) scale(1); opacity: 1;}
100%{-ms-transform: translate3d(-50%,-50%,0) scale(0.8); -webkit-transform: translate3d(-50%,-50%,0) scale(0.8); transform: translate3(-50%,-50%,0) scale(0.8); opacity: 0;}
}
@-webkit-keyframes aui-scale-out-tosmall-dialog{
0%{-ms-transform: translate3d(-50%,-50%,0) scale(1); -webkit-transform: translate3d(-50%,-50%,0) scale(1); transform: translate3d(-50%,-50%,0) scale(1); opacity: 1;}
100%{-ms-transform: translate3d(-50%,-50%,0) scale(0.8); -webkit-transform: translate3d(-50%,-50%,0) scale(0.8); transform: translate3(-50%,-50%,0) scale(0.8); opacity: 0;}
}
@keyframes aui-scale-out-tosmall-dialog{
0%{-ms-transform: translate3d(-50%,-50%,0) scale(1); -webkit-transform: translate3d(-50%,-50%,0) scale(1); transform: translate3d(-50%,-50%,0) scale(1); opacity: 1;}
100%{-ms-transform: translate3d(-50%,-50%,0) scale(0.8); -webkit-transform: translate3d(-50%,-50%,0) scale(0.8); transform: translate3(-50%,-50%,0) scale(0.8); opacity: 0;}
}
/* aui-slide-up */
@-ms-keyframes aui-slide-up{
0%{bottom: -40vh;}
100%{bottom: 10px;}
}
@-webkit-keyframes aui-slide-up{
0%{bottom: -40vh;}
100%{bottom: 10px;}
}
@keyframes aui-slide-up{
0%{bottom: -40vh;}
100%{bottom: 10px;}
}
/* aui-slide-down */
@-ms-keyframes aui-slide-down{
0%{bottom: 10px;}
100%{bottom: -40vh;}
}
@-webkit-keyframes aui-slide-down{
0%{bottom: 10px;}
100%{bottom: -40vh;}
}
@keyframes aui-slide-down{
0%{bottom: 10px;}
100%{bottom: -40vh;}
}
/* aui-slide-up-screen */
@-ms-keyframes aui-slide-up-screen{
0%{bottom: -60vh;}
100%{bottom: 0px;}
}
@-webkit-keyframes aui-slide-up-screen{
0%{bottom: -60vh;}
100%{bottom: 0px;}
}
@keyframes aui-slide-up-screen{
0%{bottom: -60vh;}
100%{bottom: 0px;}
}
/* aui-slide-down-screen */
@-ms-keyframes aui-slide-down-screen{
0%{bottom: 0px;}
100%{bottom: -60vh;}
}
@-webkit-keyframes aui-slide-down-screen{
0%{bottom: 0px;}
100%{bottom: -60vh;}
}
@keyframes aui-slide-down-screen{
0%{bottom: 0px;}
100%{bottom: -60vh;}
}
@-webkit-keyframes aui-slide-up_to_middle {
0%{opacity: 0; top: -50vh; -ms-transform: translate(0,0); -webkit-transform: translate(0,0); transform: translate(0,0);}
100%{opacity: 1; top: 45%; -ms-transform: translate(0, -50%); -webkit-transform: translate(0, -50%); transform: translate(0, -50%);}
}
@keyframes aui-slide-up_to_middle {
0%{opacity: 0; top: -50vh; -ms-transform: translate(0,0); -webkit-transform: translate(0,0); transform: translate(0,0);}
100%{opacity: 1; top: 45%; -ms-transform: translate(0, -50%); -webkit-transform: translate(0, -50%); transform: translate(0, -50%);}
}
@-webkit-keyframes aui-slide-middle_to_up {
0%{opacity: 1; top: 45%; -ms-transform: translate(0, -50%); -webkit-transform: translate(0, -50%); transform: translate(0, -50%);}
100%{opacity: 0; top: -50vh; -ms-transform: translate(0,0); -webkit-transform: translate(0,0); transform: translate(0,0);}
}
@keyframes aui-slide-middle_to_up {
0%{opacity: 1; top: 45%; -ms-transform: translate(0, -50%); -webkit-transform: translate(0, -50%); transform: translate(0, -50%);}
100%{opacity: 0; top: -50vh; -ms-transform: translate(0,0); -webkit-transform: translate(0,0); transform: translate(0,0);}
}

1
uniapp/components/aui-dialog/common/aui/css/aui.iconfont.css

File diff suppressed because one or more lines are too long

55
uniapp/components/aui-dialog/common/aui/js/aui.js

@ -1,55 +0,0 @@
const aui = {
console: function(str){
console.log(str);
},
/***
@param {string} url 页面路径
@param {Object} opts 参数 {id: ''}
@example: aui.openWin("index.html", {id: 1})
*/
openWin(url, opts){
var _this = this;
var str = '?';
for(var i in opts){
if(_this.isDefine(opts[i])){
str += i + '=' + opts[i] + '&';
}
}
uni.navigateTo({
url: _this.isDefine(opts) ? url + str : url
})
},
/***
@example: aui.closeWin()
*/
closeWin(callback){
//直接关闭页面,并向后台发送数据
if(typeof callback == "function"){
if(window.addEventListener) {
window.addEventListener("beforeunload", callback, false);
} else {
window.attachEvent("onbeforeunload", callback, false);
}
}
uni.navigateBack({
delta: 1
});
},
/***
@param {string} str 变量
@example: aui.isDefine("变量");
*/
isDefine(str){
if (str == null || str == "" || str == "undefined" || str == undefined || str == "null" || str == "(null)" || str == 'NULL' || typeof (str) == 'undefined'){
return false;
}else{
str = str + "";
str = str.replace(/\s/g, "");
if (str == ""){return false;}
return true;
}
},
}
export {
aui
}

181
uniapp/components/coolc-coupon/coolc-coupon.vue

@ -1,181 +0,0 @@
<template>
<view class="coupon-item">
<view class="coupon-money">
<view class="nick" v-if="!types">{{item.full}}使用</view>
<view class="layof" :style="{color:theme}">{{item.reduce}}</view>
<view class="end_time">{{item.end_time}}前使用</view>
<view v-if="!types">
<!-- <view class="tit">券号{{item.ticket}}</view> -->
<view class="demand">{{item.coupon.name}}</view>
</view>
</view>
<!-- <view class="get-btn" v-if="types" :style="{color:color, borderColor:color, background:solid}">选择使用</view> -->
<button @tap="jump" class="get-btn" v-if="item.status == 0" :style="{color:color, borderColor:color, background:solid}">立即使用</button>
<image v-else-if="item.status == 1" class="img" src="../../static/icon-img/used.png" mode=""></image>
<image v-else-if="item.status == 3" class="img" src="../../static/icon-img/guoqi.png" mode=""></image>
</view>
</template>
<script>
export default {
components: {
},
data() {
return {
}
},
mounted() {
},
props: {
item: {
type: Object
},
types: {
type: String,
default: ''
},
theme: {
type: String,
default: '#ff9000'
},
solid: {
type: String,
default: '#ffffff'
},
color: {
type: String,
default: '#ff9000'
},
},
methods: {
jump() {
uni.switchTab({
url: '../../pages/index/index'
})
}
}
}
</script>
<style lang='scss'>
.coupon-item {
width: 100%;
height: auto;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 10upx;
padding: 0 20upx;
margin-top: 22upx;
border: 1px solid #eeeeee;
position: relative;
background: linear-gradient(to bottom right, transparent, #DBA871);
.coupon-money {
width: 465upx;
height: auto;
display: table;
float: left;
padding: 26upx 0;
border-style: none dotted none none;
border-color: #eeeeee;
.nick {
width: 100%;
height: 50upx;
line-height: 30upx;
font-size: $font-sm;
color: $font-color-999;
}
.tit {
width: 100%;
height: 50upx;
line-height: 50upx;
font-size: $font-sm;
color: $font-color-999;
}
.demand {
width: 100%;
height: 30upx;
line-height: 30upx;
font-size: $font-sm;
color: $font-color-999;
}
.layof {
width: 100%;
height: 48upx;
line-height: 30upx;
font-size: 44upx;
color: #ff9000;
font-weight: bold;
}
.end_time {
width: 100%;
height: 30upx;
line-height: 30upx;
font-size: $font-sm;
color: $font-color-999;
margin-bottom: 10px;
}
}
.img {
position: absolute;
width: 150rpx;
height: 150rpx;
right: 28upx;
top: 40upx;
}
.get-btn {
width: 160upx;
height: 52upx;
line-height: 50upx;
position: absolute;
top: 50%;
right: 26upx;
margin-top: -26upx;
text-align: center;
border-radius: 60upx;
color: #ff9000;
border: 1px solid #ff9000;
font-size: $font-sm;
float: right;
}
}
.coupon-item:after {
width: 40upx;
height: 20upx;
position: absolute;
left: 460upx;
top: -1px;
border-radius: 0 0 40upx 40upx;
content: "";
display: block;
background: #F8F8F8;
border: 1px solid #eeeeee;
border-top: 0px;
}
.coupon-item:before {
width: 40upx;
height: 20upx;
position: absolute;
left: 460upx;
bottom: -1px;
border-radius: 40upx 40upx 0 0;
content: "";
display: block;
background: #F8F8F8;
border: 1px solid #eeeeee;
border-bottom: 0px;
}
</style>

234
uniapp/components/custom-modal/example.vue

@ -1,234 +0,0 @@
<template>
<view class="container">
<fui-button @click="showSuccessModal = true" text="显示成功弹窗"></fui-button>
<fui-button @click="showConfirmModal = true" text="显示确认弹窗"></fui-button>
<fui-button @click="showCustomModal = true" text="显示自定义弹窗"></fui-button>
<!-- 成功弹窗示例 -->
<custom-modal
:show="showSuccessModal"
width="600"
@cancel="handleModalCancel"
>
<fui-icon name="checkbox-fill" :size="108" color="#09BE4F"></fui-icon>
<text class="fui-title">购买成功</text>
<text class="fui-descr">成功购买一张月卡可免费阅读30天</text>
<template #buttons>
<fui-button
text="我知道了"
width="240rpx"
height="72rpx"
:size="28"
radius="36rpx"
background="#FFB703"
borderWidth="0"
:margin="['0','0','24rpx']"
@click="handleButtonClick('success_confirm')"
/>
</template>
</custom-modal>
<!-- 确认弹窗示例 -->
<custom-modal
:show="showConfirmModal"
width="500"
@cancel="handleModalCancel"
>
<fui-icon name="warning-fill" :size="108" color="#FF6B35"></fui-icon>
<text class="fui-title">确认删除</text>
<text class="fui-descr">删除后无法恢复确定要继续吗</text>
<template #buttons>
<fui-button
text="取消"
width="200rpx"
height="72rpx"
:size="28"
radius="36rpx"
background="#F5F5F5"
color="#333"
borderWidth="0"
:margin="['0','0','12rpx']"
@click="handleButtonClick('confirm_cancel')"
/>
<fui-button
text="确定删除"
width="200rpx"
height="72rpx"
:size="28"
radius="36rpx"
background="#FF6B35"
borderWidth="0"
@click="handleButtonClick('confirm_delete')"
/>
</template>
</custom-modal>
<!-- 自定义内容弹窗示例 -->
<custom-modal
:show="showCustomModal"
width="700"
:showClose="false"
@cancel="handleModalCancel"
>
<view class="custom-content">
<text class="custom-title">支付二维码</text>
<view class="custom-form">
<fui-qrcode :value="qrcode"></fui-qrcode>
</view>
</view>
<template #buttons>
<view class="button-row">
<fui-button
text="发送二维码给用户"
width="200rpx"
height="72rpx"
:size="28"
radius="36rpx"
background="#007AFF"
borderWidth="0"
@click="handleButtonClick('custom_submit')"
/>
</view>
</template>
</custom-modal>
</view>
</template>
<script>
import CustomModal from './custom-modal.vue'
export default {
components: {
CustomModal
},
data() {
return {
showSuccessModal: false,
showConfirmModal: false,
showCustomModal: false,
qrcode: 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET',
formData: {
name: '',
phone: ''
}
}
},
methods: {
//
handleModalCancel(type) {
console.log('弹窗取消:', type)
this.showSuccessModal = false
this.showConfirmModal = false
this.showCustomModal = false
},
//
handleButtonClick(action) {
console.log('按钮点击:', action)
switch(action) {
case 'success_confirm':
this.showSuccessModal = false
uni.showToast({
title: '确认成功',
icon: 'success'
})
break
case 'confirm_cancel':
this.showConfirmModal = false
break
case 'confirm_delete':
this.showConfirmModal = false
uni.showToast({
title: '删除成功',
icon: 'success'
})
break
case 'custom_cancel':
this.showCustomModal = false
break
case 'custom_submit':
if (!this.formData.name || !this.formData.phone) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
this.showCustomModal = false
uni.showToast({
title: '提交成功',
icon: 'success'
})
//
this.formData = {
name: '',
phone: ''
}
break
}
}
}
}
</script>
<style lang="less" scoped>
.container {
padding: 40rpx;
display: flex;
flex-direction: column;
gap: 30rpx;
}
.fui-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
text-align: center;
margin: 20rpx 0;
}
.fui-descr {
font-size: 28rpx;
color: #666;
text-align: center;
margin: 0 0 30rpx 0;
line-height: 1.5;
}
.custom-content {
width: 100%;
padding: 20rpx;
}
.custom-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
text-align: center;
display: block;
margin-bottom: 30rpx;
}
.custom-form {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.button-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 40rpx;
}
</style>

393
uniapp/components/modal/modal.vue

@ -1,393 +0,0 @@
<template>
<view @touchmove.stop.prevent>
<view class="modal-box" :style="{width:width,padding:padding,borderRadius:radius}" :class="[(fadein || show)?'modal-normal':'modal-scale',show?'modal-show':'']">
<view v-if="custom">
<slot></slot>
</view>
<view v-else>
<view class="modal-title" v-if="title">{{title}}</view>
<view class="modal-content" :class="[title?'':'mtop']" :style="{color:color,fontSize:size+'rpx'}">
<slot></slot>
</view>
<view class="modalBtn-box" :class="[button.length!=2?'flex-column':'']">
<block v-for="(item,index) in button" :key="index">
<button class="modal-btn"
:class="[
''+(item.type || 'primary')+(item.plain?'-outline':''),
button.length!=2?'btn-width':'',
button.length>2?'mbtm':'',
shape=='circle'?'circle-btn':'',
'btn-' + (item.size || 'default'),
]"
:hover-class="''+(item.plain?'outline':(item.type || 'primary'))+'-hover'" :data-index="index" @tap="handleClick">{{item.text || "确定"}}</button>
</block>
</view>
</view>
</view>
<view class="modal-mask" :class="[show?'mask-show':'']" @tap="handleClickCancel"></view>
</view>
</template>
<script>
export default {
name: "Modal",
props: {
//
show: {
type: Boolean,
default: false
},
//modal
custom: {
type: Boolean,
default: false
},
width: {
type: String,
default: "80%"
},
padding: {
type: String,
default: "30rpx"
},
radius: {
type: String,
default: "12rpx"
},
//
title: {
type: String,
default: ""
},
//
content: {
type: String,
default: ""
},
//
color: {
type: String,
default: "#343434"
},
// rpx
size: {
type: Number,
default: 28
},
// circle, square
shape: {
type: String,
default: 'square'
},
button: {
type: Array,
default: function() {
return [{
text: "取消",
type: "red",
plain: true //
}, {
text: "确定",
type: "red",
plain: false
}]
}
},
//
maskClosable: {
type: Boolean,
default: true
},
//inputtrue
fadein: {
type: Boolean,
default: false
}
},
data() {
return {
};
},
methods: {
handleClick(e) {
if (!this.show) return;
const dataset = e.currentTarget.dataset;
this.$emit('click', {
index: Number(dataset.index)
});
},
handleClickCancel() {
if (!this.maskClosable) return;
this.$emit('cancel');
}
}
}
</script>
<style lang="scss">
.modal-box {
position: fixed;
left: 50%;
top: 50%;
margin: auto;
background: #fff;
z-index: 1000;
transition: all 0.3s ease-in-out;
opacity: 0;
box-sizing: border-box;
visibility: hidden;
}
.modal-scale {
transform: translate(-50%, -50%) scale(0);
}
.modal-normal {
transform: translate(-50%, -50%) scale(1);
}
.modal-show {
opacity: 1;
visibility: visible;
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 999;
transition: all 0.3s ease-in-out;
opacity: 0;
visibility: hidden;
}
.mask-show {
visibility: visible;
opacity: 1;
}
.modal-title {
text-align: center;
font-size: 34rpx;
color: #333;
padding-top: 20rpx;
font-weight: bold;
}
.modal-content {
color: #999;
font-size: 28rpx;
padding-top: 20rpx;
padding-bottom: 60rpx;
}
.mtop {
margin-top: 30rpx;
}
.mbtm {
margin-bottom: 30rpx;
}
.modalBtn-box {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between
}
.flex-column {
flex-direction: column;
}
.modal-btn {
width: 46%;
height: 68rpx;
line-height: 68rpx;
position: relative;
border-radius: 60rpx;
font-size: 28rpx;
overflow: visible;
margin-left: 0;
margin-right: 0;
&.btn-default {
font-size: 28rpx;
}
&.btn-lg {
font-size: 32rpx;
}
&.btn-sm {
font-size: 24rpx;
}
}
.modal-btn::after {
content: "";
position: absolute;
width: 200%;
height: 200%;
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
-webkit-transform: scale(0.5, 0.5);
transform: scale(0.5, 0.5);
left: 0;
top: 0;
border-radius: 60rpx;
}
.btn-width {
width: 80% !important;
}
.primary {
background: #97AF13;
color: #fff;
}
.primary-hover {
background: #97AF13;
color: #e5e5e5;
}
.primary-outline {
color: #97AF13;
background: none;
}
.primary-outline::after {
border: 1px solid #97AF13;
}
.danger {
background: #ed3f14;
color: #fff;
}
.danger-hover {
background: #d53912;
color: #e5e5e5;
}
.danger-outline {
color: #ed3f14;
background: none;
}
.danger-outline::after {
border: 1px solid #ed3f14;
}
.red {
background: #e41f19;
color: #fff;
}
.red-hover {
background: #c51a15;
color: #e5e5e5;
}
.red-outline {
color: #e41f19;
background: none;
}
.red-outline::after {
border: 1px solid #e41f19;
}
.warning {
background: #ff7900;
color: #fff;
}
.warning-hover {
background: #e56d00;
color: #e5e5e5;
}
.warning-outline {
color: #ff7900;
background: none;
}
.warning-outline::after {
border: 1px solid #ff7900;
}
.green {
background: #19be6b;
color: #fff;
}
.green-hover {
background: #16ab60;
color: #e5e5e5;
}
.green-outline {
color: #19be6b;
background: none;
}
.green-outline::after {
border: 1px solid #19be6b;
}
.white {
background: #fff;
color: #333;
}
.white-hover {
background: #f7f7f9;
color: #666;
}
.white-outline {
color: #333;
background: none;
}
.white-outline::after {
border: 1px solid #333;
}
.gray {
background: #ededed;
color: #999;
}
.gray-hover {
background: #d5d5d5;
color: #898989;
}
.gray-outline {
color: #999;
background: none;
}
.gray-outline::after {
border: 1px solid #999;
}
.outline-hover {
opacity: 0.6;
}
.circle-btn {
border-radius: 40rpx !important;
}
.circle-btn::after {
border-radius: 80rpx !important;
}
</style>

517
uniapp/components/schedule/ScheduleDetail.vue

@ -0,0 +1,517 @@
<template>
<view class="schedule-detail" v-if="visible">
<!-- <view class="popup-wrapper">-->
<!-- <view class="popup-header">-->
<!-- <text class="popup-title">课次详情</text>-->
<!-- <view class="close-btn" @click="closePopup">-->
<!-- <text class="close-icon">×</text>-->
<!-- </view>-->
<!-- </view>-->
<!-- <view class="popup-content" v-if="loading">-->
<!-- <view class="loading">-->
<!-- <fui-loading></fui-loading>-->
<!-- <text class="loading-text">加载中...</text>-->
<!-- </view>-->
<!-- </view>-->
<!-- <view class="popup-content" v-else-if="error">-->
<!-- <view class="error-message">-->
<!-- <text>{{ errorMessage }}</text>-->
<!-- <view class="retry-btn" @click="fetchScheduleDetail">-->
<!-- <text>重试</text>-->
<!-- </view>-->
<!-- </view>-->
<!-- </view>-->
<!-- <view class="popup-content" v-else>-->
<!-- <view class="course-title">-->
<!-- <text>{{ scheduleDetail.title || '暂无课程名称' }}</text>-->
<!-- </view>-->
<!-- <view class="course-time">-->
<!-- <text>{{ scheduleDetail.course_date || '' }} {{ scheduleDetail.time_slot || '' }}</text>-->
<!-- </view>-->
<!-- <view class="schedule-info">-->
<!-- <view class="info-item">-->
<!-- <text class="info-label">授课教师:</text>-->
<!-- <text class="info-value">{{ scheduleDetail.coach?.name || '未设置' }}</text>-->
<!-- </view>-->
<!-- <view class="info-item">-->
<!-- <text class="info-label">教室:</text>-->
<!-- <text class="info-value">{{ scheduleDetail.venue?.venue_name || '未设置' }}</text>-->
<!-- </view>-->
<!-- <view class="info-item">-->
<!-- <text class="info-label">当前人数:</text>-->
<!-- <text class="info-value">{{ studentCount }}/{{ scheduleDetail.venue?.capacity || 0 }}</text>-->
<!-- </view>-->
<!-- <view class="info-item">-->
<!-- <text class="info-label">课程内容:</text>-->
<!-- <text class="info-value">{{ scheduleDetail.content || '未设置上课内容' }}</text>-->
<!-- </view>-->
<!-- <view class="info-item" v-if="scheduleDetail.remark">-->
<!-- <text class="info-label">备注:</text>-->
<!-- <text class="info-value">{{ scheduleDetail.remark }}</text>-->
<!-- </view>-->
<!-- &lt;!&ndash; 学员列表 &ndash;&gt;-->
<!-- <view class="student-list-section">-->
<!-- <view class="section-title">-->
<!-- <text>学员列表</text>-->
<!-- <text class="status-tag" :class="statusClass">{{ statusText }}</text>-->
<!-- </view>-->
<!-- <view class="student-list" v-if="scheduleDetail.student_courses && scheduleDetail.student_courses.length > 0">-->
<!-- <view class="student-item" v-for="(student, index) in scheduleDetail.student_courses" :key="index">-->
<!-- <view class="student-avatar">-->
<!-- <image :src="$util.img(student.avatar)" mode="aspectFill"></image>-->
<!-- </view>-->
<!-- <view class="student-info">-->
<!-- <text class="student-name">{{ student.name }}</text>-->
<!-- <text class="student-status" :class="{'signed': student.status === 'signed'}">-->
<!-- {{ student.status === 'signed' ? '已签到' : '未签到' }}-->
<!-- </text>-->
<!-- </view>-->
<!-- </view>-->
<!-- </view>-->
<!-- <view class="empty-list" v-else>-->
<!-- <text>暂无学员参与此课程</text>-->
<!-- </view>-->
<!-- </view>-->
<!-- </view>-->
<!-- </view>-->
<!-- <view class="popup-footer">-->
<!-- <view class="action-btn adjust-btn" @click="handleAdjustClass">-->
<!-- <text>调课</text>-->
<!-- </view>-->
<!-- <view class="action-btn sign-btn" @click="handleSignIn">-->
<!-- <text>点名</text>-->
<!-- </view>-->
<!-- </view>-->
<!-- </view>-->
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
export default {
name: 'ScheduleDetail',
props: {
visible: {
type: Boolean,
default: false
},
scheduleId: {
type: [String, Number],
default: ''
}
},
data() {
return {
loading: false,
error: false,
errorMessage: '加载失败,请重试',
scheduleDetail: {},
studentCount: 0
}
},
computed: {
//
statusText() {
if (!this.scheduleDetail.student_courses || !this.scheduleDetail.student_courses[0]) {
return '未开始';
}
const now = new Date();
const startDate = this.scheduleDetail.student_courses[0].start_date ?
new Date(this.scheduleDetail.student_courses[0].start_date) : null;
const endDate = this.scheduleDetail.student_courses[0].end_date ?
new Date(this.scheduleDetail.student_courses[0].end_date) : null;
if (startDate && endDate) {
if (now >= startDate && now <= endDate) {
return '上课中';
} else if (now > endDate) {
return '已结束';
} else if (now < startDate) {
return '未开始';
}
}
return '未开始';
},
statusClass() {
switch (this.statusText) {
case '上课中':
return 'status-in-progress';
case '已结束':
return 'status-ended';
case '未开始':
default:
return 'status-not-started';
}
}
},
watch: {
// ID
scheduleId: {
immediate: true,
handler(newVal) {
if (newVal && this.visible) {
this.fetchScheduleDetail();
}
}
},
//
visible(newVal) {
if (newVal && this.scheduleId) {
this.fetchScheduleDetail();
}
}
},
methods: {
//
async fetchScheduleDetail() {
if (!this.scheduleId) {
this.error = true;
this.errorMessage = '课程ID不能为空';
return;
}
this.loading = true;
this.error = false;
try {
// 使
const res = await apiRoute.getCourseScheduleInfo({
id: this.scheduleId
});
if (res.code === 1 && res.data) {
this.scheduleDetail = res.data;
//
this.studentCount = this.scheduleDetail.student_courses ?
this.scheduleDetail.student_courses.length : 0;
} else {
// 使
const fallbackRes = await apiRoute.courseInfo({
id: this.scheduleId
});
if (fallbackRes.code === 1 && fallbackRes.data) {
this.scheduleDetail = fallbackRes.data;
this.studentCount = this.scheduleDetail.student_courses ?
this.scheduleDetail.student_courses.length : 0;
} else {
throw new Error(res.msg || fallbackRes.msg || '获取课程详情失败');
}
}
} catch (error) {
console.error('获取课程详情失败:', error);
this.error = true;
this.errorMessage = error.message || '获取课程详情失败,请重试';
} finally {
this.loading = false;
}
},
//
closePopup() {
this.$emit('update:visible', false);
},
//
handleSignIn() {
//
if (this.statusText === '已结束') {
uni.showToast({
title: '课程已结束,无法点名',
icon: 'none'
});
return;
}
//
if (!this.scheduleDetail.student_courses || this.scheduleDetail.student_courses.length === 0) {
uni.showToast({
title: '暂无学员,无法点名',
icon: 'none'
});
return;
}
//
this.$emit('sign-in', {
scheduleId: this.scheduleId,
scheduleDetail: this.scheduleDetail
});
},
//
handleAdjustClass() {
//
if (this.statusText === '已结束') {
uni.showToast({
title: '课程已结束,无法调课',
icon: 'none'
});
return;
}
//
this.$emit('adjust-class', {
scheduleId: this.scheduleId,
scheduleDetail: this.scheduleDetail
});
}
}
}
</script>
<style lang="less" scoped>
.schedule-detail {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
}
.popup-wrapper {
width: 90%;
max-height: 80vh;
background-color: #434544;
border-radius: 16rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.popup-header {
padding: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #555;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #fff;
}
.close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
justify-content: center;
align-items: center;
}
.close-icon {
font-size: 40rpx;
color: #fff;
}
.popup-content {
flex: 1;
padding: 30rpx;
overflow-y: auto;
}
.loading, .error-message {
height: 300rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #ccc;
}
.error-message {
color: #ff6b6b;
font-size: 28rpx;
text-align: center;
}
.retry-btn {
margin-top: 30rpx;
padding: 12rpx 30rpx;
background-color: #29d3b4;
border-radius: 8rpx;
color: #fff;
font-size: 24rpx;
}
.course-title {
font-size: 36rpx;
font-weight: bold;
color: #fff;
margin-bottom: 16rpx;
}
.course-time {
font-size: 28rpx;
color: #FAD24E;
margin-bottom: 30rpx;
}
.schedule-info {
background-color: #333;
border-radius: 12rpx;
padding: 24rpx;
}
.info-item {
display: flex;
margin-bottom: 20rpx;
}
.info-label {
width: 160rpx;
font-size: 26rpx;
color: #ccc;
}
.info-value {
flex: 1;
font-size: 26rpx;
color: #fff;
}
.student-list-section {
margin-top: 30rpx;
}
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16rpx;
border-bottom: 1px solid #555;
margin-bottom: 20rpx;
}
.section-title text {
font-size: 28rpx;
color: #fff;
}
.status-tag {
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 30rpx;
}
.status-in-progress {
background-color: #FAD24E;
color: #333;
}
.status-ended {
background-color: #e2e2e2;
color: #333;
}
.status-not-started {
background-color: #1cd188;
color: #fff;
}
.student-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.student-item {
display: flex;
align-items: center;
}
.student-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
background-color: #555;
margin-right: 20rpx;
}
.student-avatar image {
width: 100%;
height: 100%;
}
.student-info {
flex: 1;
display: flex;
flex-direction: column;
}
.student-name {
font-size: 28rpx;
color: #fff;
margin-bottom: 6rpx;
}
.student-status {
font-size: 24rpx;
color: #ff6b6b;
}
.student-status.signed {
color: #1cd188;
}
.empty-list {
padding: 40rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}
.popup-footer {
display: flex;
padding: 30rpx;
gap: 20rpx;
border-top: 1px solid #555;
}
.action-btn {
flex: 1;
height: 80rpx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8rpx;
font-size: 28rpx;
}
.adjust-btn {
background-color: #555;
color: #fff;
}
.sign-btn {
background-color: #29d3b4;
color: #fff;
}
</style>

96
uniapp/components/uni-icons/icons.js

@ -1,96 +0,0 @@
export default {
'contact': '\ue100',
'person': '\ue101',
'personadd': '\ue102',
'contact-filled': '\ue130',
'person-filled': '\ue131',
'personadd-filled': '\ue132',
'phone': '\ue200',
'email': '\ue201',
'chatbubble': '\ue202',
'chatboxes': '\ue203',
'phone-filled': '\ue230',
'email-filled': '\ue231',
'chatbubble-filled': '\ue232',
'chatboxes-filled': '\ue233',
'weibo': '\ue260',
'weixin': '\ue261',
'pengyouquan': '\ue262',
'chat': '\ue263',
'qq': '\ue264',
'videocam': '\ue300',
'camera': '\ue301',
'mic': '\ue302',
'location': '\ue303',
'mic-filled': '\ue332',
'speech': '\ue332',
'location-filled': '\ue333',
'micoff': '\ue360',
'image': '\ue363',
'map': '\ue364',
'compose': '\ue400',
'trash': '\ue401',
'upload': '\ue402',
'download': '\ue403',
'close': '\ue404',
'redo': '\ue405',
'undo': '\ue406',
'refresh': '\ue407',
'star': '\ue408',
'plus': '\ue409',
'minus': '\ue410',
'circle': '\ue411',
'checkbox': '\ue411',
'close-filled': '\ue434',
'clear': '\ue434',
'refresh-filled': '\ue437',
'star-filled': '\ue438',
'plus-filled': '\ue439',
'minus-filled': '\ue440',
'circle-filled': '\ue441',
'checkbox-filled': '\ue442',
'closeempty': '\ue460',
'refreshempty': '\ue461',
'reload': '\ue462',
'starhalf': '\ue463',
'spinner': '\ue464',
'spinner-cycle': '\ue465',
'search': '\ue466',
'plusempty': '\ue468',
'forward': '\ue470',
'back': '\ue471',
'left-nav': '\ue471',
'checkmarkempty': '\ue472',
'home': '\ue500',
'navigate': '\ue501',
'gear': '\ue502',
'paperplane': '\ue503',
'info': '\ue504',
'help': '\ue505',
'locked': '\ue506',
'more': '\ue507',
'flag': '\ue508',
'home-filled': '\ue530',
'gear-filled': '\ue532',
'info-filled': '\ue534',
'help-filled': '\ue535',
'more-filled': '\ue537',
'settings': '\ue560',
'list': '\ue562',
'bars': '\ue563',
'loop': '\ue565',
'paperclip': '\ue567',
'eye': '\ue568',
'arrowup': '\ue580',
'arrowdown': '\ue581',
'arrowleft': '\ue582',
'arrowright': '\ue583',
'arrowthinup': '\ue584',
'arrowthindown': '\ue585',
'arrowthinleft': '\ue586',
'arrowthinright': '\ue587',
'pulldown': '\ue588',
'closefill': '\ue589',
'sound': '\ue590',
'scan': '\ue612'
}

57
uniapp/components/uni-icons/uni-icons.vue

File diff suppressed because one or more lines are too long

4
uniapp/components/uni-nav-bar/uni-nav-bar.vue

@ -42,13 +42,11 @@
<script>
import uniStatusBar from "../uni-status-bar/uni-status-bar.vue";
import uniIcons from "../uni-icons/uni-icons.vue";
export default {
name: "UniNavBar",
components: {
uniStatusBar,
uniIcons
uniStatusBar
},
props: {
title: {

13
uniapp/pages.json

@ -666,6 +666,14 @@
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/coach/schedule/schedule_table",
"style": {
"navigationBarTitleText": "课程安排",
"navigationBarBackgroundColor": "#292929",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/academic/home/index",
"style": {
@ -697,7 +705,10 @@
"easycom": {
"autoscan": true,
"custom": {
"fui-(.*)": "@/components/firstui/fui-$1/fui-$1.vue"
"fui-(.*)": "@/components/firstui/fui-$1/fui-$1.vue",
"uni-icons": "@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue",
"uni-calendar": "@/uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue",
"uni-file-picker": "@/uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue"
}
}
}

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

@ -38,13 +38,6 @@
</view>
</view>
</view>
<view class="bottom">
月授课数
<text>{{statisticsInfo.courseMonthNum}}</text>
月负责学员
<text>{{statisticsInfo.studentMonthNum}}</text>
</view>
</view>
</view>
<view class="bg_box bg_top"></view>
@ -56,11 +49,6 @@
<view class="main_section">
<view class="section_box">
<!-- <view class="item" @click="openViewSportsVenue()">
<view>我的体育场</view>
<view>xxx场馆</view>
</view> -->
<view class="item" @click="openViewDueSoon()">
<view>即将到期</view>
<view></view>
@ -88,11 +76,16 @@
<view></view>
</view>
<!-- <view class="item" @click="openServiceDetail()">
<view class="item" @click="openServiceDetail()">
<view>服务详情</view>
<view></view>
</view> -->
</view>
<view class="item" @click="goCourseSchedule()">
<view>课程安排</view>
<view></view>
</view>
<view class="item" @click="my_contract()">
<view>我的合同</view>
<view></view>
@ -254,6 +247,11 @@
showCancel: false
})
},
goCourseSchedule(){
this.$navigateTo({
url: '/pages/coach/schedule/schedule_table'
})
}
}
}
</script>

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

@ -0,0 +1,175 @@
# 课程安排表组件
## 功能特性
### 📅 完整的课程表视图
- **时间轴显示**:11:00-17:00 每小时时间段
- **周视图**:显示一周7天的课程安排
- **双向滚动**:支持水平(日期)和垂直(时间)滚动
- **响应式布局**:适配微信小程序端
### 🔍 智能筛选功能
- **顶部快捷筛选**:时间、老师、教室、班级
- **详细筛选弹窗**:支持多选条件组合筛选
- **实时统计**:显示总课程数和未点名课程数
### 🎨 主题样式
- **暗黑主题**:#292929 背景色
- **绿色主色调**:#29d3b4 强调色
- **层次化设计**:清晰的视觉层级
### 📱 交互体验
- **日期导航**:支持上一周/下一周切换
- **课程类型**:普通课程、私教课程、活动课程区分显示
- **点击交互**:支持单元格点击添加课程
- **悬浮按钮**:快速添加课程
## 使用方法
### 1. 页面导航
```javascript
// 跳转到课程安排表
this.$navigateTo({
url: '/pages/coach/schedule/schedule_table'
})
```
### 2. 数据结构
#### 课程数据格式
```javascript
{
id: 1, // 课程ID
date: '2025-07-02', // 日期
time: '11:00', // 时间
courseName: '花花-丁颖', // 课程名称
students: '小鱼-周子', // 学员
teacher: '燕子-符', // 老师
status: '燕菜', // 状态
type: 'normal', // 类型:normal/private/activity
duration: 1 // 持续时间(小时)
}
```
#### 筛选选项格式
```javascript
{
teacherOptions: [
{ id: 1, name: '张老师' }
],
classroomOptions: [
{ id: 1, name: '教室1' }
],
classOptions: [
{ id: 1, name: '花花-丁颖' }
]
}
```
### 3. 自定义配置
#### 修改时间段
```javascript
// 在 data 中修改 timeSlots
timeSlots: [
{ time: '09:00', value: 9 },
{ time: '10:00', value: 10 },
// ... 更多时间段
]
```
#### 修改表格宽度
```javascript
// 调整表格总宽度(rpx)
tableWidth: 1200
```
### 4. API 接口集成
#### 获取课程数据
```javascript
async loadCourses() {
try {
const res = await apiRoute.getCourseSchedule({
startDate: this.currentWeekStart,
endDate: this.getWeekEndDate()
})
this.courses = res.data
} catch (error) {
console.error('加载课程失败:', error)
}
}
```
#### 添加课程
```javascript
async addCourse(courseData) {
try {
const res = await apiRoute.addCourseSchedule(courseData)
if (res.code === 1) {
this.loadCourses() // 重新加载数据
}
} catch (error) {
console.error('添加课程失败:', error)
}
}
```
## 性能优化建议
### 1. 数据懒加载
- 只加载当前周的数据
- 切换周时再加载新数据
### 2. 滚动优化
- 使用 `scroll-view` 组件的惯性滚动
- 避免在滚动事件中进行复杂计算
### 3. 渲染优化
- 使用 `v-show` 替代 `v-if` 进行显示隐藏
- 合理使用 `key` 属性优化列表渲染
## 扩展功能
### 1. 课程详情
```javascript
// 点击课程查看详情
handleCourseClick(course) {
this.$navigateTo({
url: `/pages/coach/course/info?id=${course.id}`
})
}
```
### 2. 拖拽排课
```javascript
// 可以集成拖拽功能实现课程时间调整
// 使用 movable-view 组件或手势事件
```
### 3. 批量操作
```javascript
// 支持批量选择和操作课程
// 添加多选模式
```
## 注意事项
1. **微信小程序兼容性**:使用了 `scroll-view` 组件,确保在微信小程序中正常滚动
2. **数据更新**:切换日期时需要重新加载数据
3. **内存管理**:大量数据时考虑虚拟滚动优化
4. **网络状态**:处理网络异常情况,提供离线缓存
## troubleshooting
### 滚动不流畅
- 检查 `scroll-view` 的高度设置
- 减少滚动事件中的计算量
### 数据不更新
- 确保数据是响应式的
- 检查 Vue 数据绑定是否正确
### 样式错位
- 检查 flex 布局设置
- 确保单元格宽度一致

481
uniapp/pages/coach/schedule/schedule_detail.vue

@ -0,0 +1,481 @@
<template>
<view class="schedule-detail-container">
<!-- 页面加载状态 -->
<view class="loading-container" v-if="loading">
<fui-loading></fui-loading>
<text class="loading-text">加载中...</text>
</view>
<!-- 错误状态显示 -->
<view class="error-container" v-else-if="error">
<text class="error-text">{{ errorMessage }}</text>
<view class="retry-btn" @click="fetchScheduleDetail">
<text>重试</text>
</view>
</view>
<view class="schedule-content" v-else>
<!-- 课程基本信息 -->
<view class="schedule-header">
<view class="course-title">
<text>{{ scheduleDetail.title || '暂无课程名称' }}</text>
<text class="status-tag" :class="statusClass">{{ statusText }}</text>
</view>
<view class="course-time">{{ scheduleDetail.course_date || '' }} {{ scheduleDetail.time_slot || '' }}</view>
</view>
<!-- 课程详细信息 -->
<view class="info-card">
<view class="info-item">
<text class="info-label">授课教师:</text>
<text class="info-value">{{ scheduleDetail.coach?.name || '未设置' }}</text>
</view>
<view class="info-item">
<text class="info-label">教室:</text>
<text class="info-value">{{ scheduleDetail.venue?.venue_name || '未设置' }}</text>
</view>
<view class="info-item">
<text class="info-label">当前人数:</text>
<text class="info-value">{{ studentCount }}/{{ scheduleDetail.venue?.capacity || 0 }}</text>
</view>
<view class="info-item">
<text class="info-label">课程内容:</text>
<text class="info-value">{{ scheduleDetail.content || '未设置上课内容' }}</text>
</view>
<view class="info-item" v-if="scheduleDetail.remark">
<text class="info-label">备注:</text>
<text class="info-value">{{ scheduleDetail.remark }}</text>
</view>
</view>
<!-- 学员列表 -->
<view class="student-list-section">
<view class="section-title">
<text>学员列表</text>
</view>
<view class="student-list" v-if="scheduleDetail.student_courses && scheduleDetail.student_courses.length > 0">
<view class="student-item" v-for="(student, index) in scheduleDetail.student_courses" :key="index">
<view class="student-avatar">
<image :src="$util.img(student.avatar)" mode="aspectFill"></image>
</view>
<view class="student-info">
<text class="student-name">{{ student.name }}</text>
<text class="student-status" :class="{'signed': student.status === 'signed'}">
{{ student.status === 'signed' ? '已签到' : '未签到' }}
</text>
</view>
</view>
</view>
<view class="empty-list" v-else>
<text>暂无学员参与此课程</text>
</view>
</view>
<!-- 底部按钮区域 -->
<view class="footer-actions">
<view class="action-btn adjust-btn" @click="handleAdjustClass">
<text>调课</text>
</view>
<view class="action-btn sign-btn" @click="handleSignIn">
<text>点名</text>
</view>
</view>
</view>
<!-- 引入课程详情组件 -->
<schedule-detail
:visible="showDetailPopup"
:scheduleId="scheduleId"
@update:visible="showDetailPopup = $event"
@sign-in="onSignIn"
@adjust-class="onAdjustClass"
></schedule-detail>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js'
import ScheduleDetail from '@/components/schedule/ScheduleDetail.vue'
export default {
components: {
ScheduleDetail
},
data() {
return {
scheduleId: '', // ID
loading: false,
error: false,
errorMessage: '加载失败,请重试',
scheduleDetail: {},
studentCount: 0,
showDetailPopup: false
}
},
computed: {
//
statusText() {
if (!this.scheduleDetail.student_courses || !this.scheduleDetail.student_courses[0]) {
return '未开始';
}
const now = new Date();
const startDate = this.scheduleDetail.student_courses[0].start_date ?
new Date(this.scheduleDetail.student_courses[0].start_date) : null;
const endDate = this.scheduleDetail.student_courses[0].end_date ?
new Date(this.scheduleDetail.student_courses[0].end_date) : null;
if (startDate && endDate) {
if (now >= startDate && now <= endDate) {
return '上课中';
} else if (now > endDate) {
return '已结束';
} else if (now < startDate) {
return '未开始';
}
}
return '未开始';
},
statusClass() {
switch (this.statusText) {
case '上课中':
return 'status-in-progress';
case '已结束':
return 'status-ended';
case '未开始':
default:
return 'status-not-started';
}
}
},
onLoad(options) {
if (options.id) {
this.scheduleId = options.id;
this.fetchScheduleDetail();
} else {
this.error = true;
this.errorMessage = '未找到课程安排ID';
}
},
methods: {
//
async fetchScheduleDetail() {
if (!this.scheduleId) {
this.error = true;
this.errorMessage = '课程ID不能为空';
return;
}
this.loading = true;
this.error = false;
try {
// 使
const res = await apiRoute.getCourseScheduleInfo({
id: this.scheduleId
});
if (res.code === 1 && res.data) {
this.scheduleDetail = res.data;
//
this.studentCount = this.scheduleDetail.student_courses ?
this.scheduleDetail.student_courses.length : 0;
} else {
// 使
const fallbackRes = await apiRoute.courseInfo({
id: this.scheduleId
});
if (fallbackRes.code === 1 && fallbackRes.data) {
this.scheduleDetail = fallbackRes.data;
this.studentCount = this.scheduleDetail.student_courses ?
this.scheduleDetail.student_courses.length : 0;
} else {
throw new Error(res.msg || fallbackRes.msg || '获取课程详情失败');
}
}
} catch (error) {
console.error('获取课程详情失败:', error);
this.error = true;
this.errorMessage = error.message || '获取课程详情失败,请重试';
} finally {
this.loading = false;
}
},
//
handleSignIn() {
//
if (this.statusText === '已结束') {
uni.showToast({
title: '课程已结束,无法点名',
icon: 'none'
});
return;
}
//
if (!this.scheduleDetail.student_courses || this.scheduleDetail.student_courses.length === 0) {
uni.showToast({
title: '暂无学员,无法点名',
icon: 'none'
});
return;
}
//
uni.navigateTo({
url: `/pages/coach/schedule/sign_in?id=${this.scheduleId}`
});
},
//
handleAdjustClass() {
//
if (this.statusText === '已结束') {
uni.showToast({
title: '课程已结束,无法调课',
icon: 'none'
});
return;
}
//
uni.navigateTo({
url: `/pages/coach/schedule/adjust_course?id=${this.scheduleId}`
});
},
//
onSignIn(data) {
console.log('处理点名:', data);
uni.navigateTo({
url: `/pages/coach/schedule/sign_in?id=${data.scheduleId}`
});
},
//
onAdjustClass(data) {
console.log('处理调课:', data);
uni.navigateTo({
url: `/pages/coach/schedule/adjust_course?id=${data.scheduleId}`
});
},
//
showPopup() {
this.showDetailPopup = true;
}
}
}
</script>
<style lang="less" scoped>
.schedule-detail-container {
background-color: #292929;
min-height: 100vh;
padding-bottom: 140rpx; //
}
.loading-container, .error-container {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-text, .error-text {
margin-top: 30rpx;
font-size: 28rpx;
color: #ccc;
}
.retry-btn {
margin-top: 30rpx;
padding: 12rpx 30rpx;
background-color: #29d3b4;
border-radius: 8rpx;
color: #fff;
font-size: 24rpx;
}
.schedule-content {
padding: 30rpx;
}
.schedule-header {
margin-bottom: 30rpx;
}
.course-title {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 36rpx;
font-weight: bold;
color: #fff;
margin-bottom: 16rpx;
}
.status-tag {
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 30rpx;
}
.status-in-progress {
background-color: #FAD24E;
color: #333;
}
.status-ended {
background-color: #e2e2e2;
color: #333;
}
.status-not-started {
background-color: #1cd188;
color: #fff;
}
.course-time {
font-size: 28rpx;
color: #FAD24E;
margin-bottom: 30rpx;
}
.info-card {
background-color: #434544;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 30rpx;
}
.info-item {
display: flex;
margin-bottom: 20rpx;
}
.info-label {
width: 160rpx;
font-size: 26rpx;
color: #ccc;
}
.info-value {
flex: 1;
font-size: 26rpx;
color: #fff;
}
.student-list-section {
background-color: #434544;
border-radius: 16rpx;
padding: 24rpx;
}
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16rpx;
border-bottom: 1px solid #555;
margin-bottom: 20rpx;
}
.section-title text {
font-size: 28rpx;
color: #fff;
}
.student-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.student-item {
display: flex;
align-items: center;
}
.student-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
background-color: #555;
margin-right: 20rpx;
}
.student-avatar image {
width: 100%;
height: 100%;
}
.student-info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.student-name {
font-size: 28rpx;
color: #fff;
}
.student-status {
font-size: 24rpx;
color: #ff6b6b;
background: rgba(255, 107, 107, 0.1);
padding: 4rpx 12rpx;
border-radius: 20rpx;
}
.student-status.signed {
color: #1cd188;
background: rgba(28, 209, 136, 0.1);
}
.empty-list {
padding: 40rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}
.footer-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 30rpx;
gap: 20rpx;
background-color: #292929;
border-top: 1px solid #434544;
}
.action-btn {
flex: 1;
height: 80rpx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8rpx;
font-size: 28rpx;
}
.adjust-btn {
background-color: #555;
color: #fff;
}
.sign-btn {
background-color: #29d3b4;
color: #fff;
}
</style>

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

File diff suppressed because it is too large

306
uniapp/pages/coach/student/student_list.vue

@ -1,5 +1,9 @@
<template>
<view class="container">
<view class="search-bar" @click="showSearch = true">
<uni-icons type="search" size="22" color="#00d18c" />
<text class="search-placeholder">搜索学员...</text>
</view>
<view class="content">
<view v-if="studentList.length === 0" class="empty-box">
<image src="/static/icon-img/empty.png" mode="aspectFit" class="empty-img"></image>
@ -19,9 +23,7 @@
</view>
<view class="info-row">
<text class="info-label">剩余课程</text>
<text class="info-value">{{
(item.total_hours + item.gift_hours) - (item.use_total_hours + item.use_gift_hours)
}}</text>
<text class="info-value">{{ getRemainingCourses(item) }}</text>
</view>
<view class="info-row">
<text class="info-label">到期时间</text>
@ -35,6 +37,65 @@
</view>
</view>
</view>
<fui-drawer :show="showSearch" position="top" @close="closeSearch">
<view class="fui-page__bd">
<view class="fui-section__title">学员搜索</view>
<fui-form>
<fui-form-item label="学生姓名" required>
<fui-input :value="searchForm.name" placeholder="请输入学生姓名" @input="onNameInput"></fui-input>
</fui-form-item>
<fui-form-item label="联系电话">
<fui-input :value="searchForm.phone" placeholder="请输入联系电话" type="number" @input="onPhoneInput"></fui-input>
</fui-form-item>
<fui-form-item label="课时数量">
<fui-input :value="searchForm.lessonCount" placeholder="请输入课时数量" type="number" @input="onLessonCountInput"></fui-input>
</fui-form-item>
<fui-form-item label="请假次数">
<fui-input :value="searchForm.leaveCount" placeholder="请输入请假次数" type="number" @input="onLeaveCountInput"></fui-input>
</fui-form-item>
<fui-form-item label="课程名称">
<view class="custom-picker-input" @click="showCoursePicker = true">
<text>{{ searchForm.courseIndex >= 0 ? courseList[searchForm.courseIndex].name : '请选择' }}</text>
<fui-icon name="arrowdown" :size="32" color="#CCCCCC"></fui-icon>
</view>
<!-- 使用独立的 picker 组件 -->
<picker
v-if="showCoursePicker"
mode="selector"
:range="courseList"
range-key="name"
@change="onCourseChange"
@cancel="showCoursePicker = false"
></picker>
</fui-form-item>
<fui-form-item label="班级">
<view class="custom-picker-input" @click="showClassPicker = true">
<text>{{ searchForm.classIndex >= 0 ? classList[searchForm.classIndex].name : '请选择' }}</text>
<fui-icon name="arrowdown" :size="32" color="#CCCCCC"></fui-icon>
</view>
<!-- 使用独立的 picker 组件 -->
<picker
v-if="showClassPicker"
mode="selector"
:range="classList"
range-key="name"
@change="onClassChange"
@cancel="showClassPicker = false"
></picker>
</fui-form-item>
</fui-form>
<view class="fui-btn__box">
<fui-button type="primary" @click="doSearch">搜索</fui-button>
</view>
</view>
</fui-drawer>
<AQTabber />
</view>
</template>
@ -48,7 +109,28 @@
},
data() {
return {
studentList: []
studentList: [],
showSearch: false,
showCoursePicker: false,
showClassPicker: false,
searchForm: {
name: '',
phone: '',
lessonCount: '',
leaveCount: '',
courseIndex: -1,
classIndex: -1,
},
courseList: [
{ name: '英语基础班' },
{ name: '数学提高班' },
{ name: '编程兴趣班' },
],
classList: [
{ name: '一班' },
{ name: '二班' },
{ name: '三班' },
],
}
},
onLoad() {
@ -69,63 +151,64 @@
icon: 'none'
});
}
// 使
// this.studentList = [
// {
// id: 1,
// name: '',
// avatar: '/static/icon-img/avatar.png',
// campus: '',
// remainingCourses: 10,
// expiryDate: '2023-12-31'
// },
// {
// id: 2,
// name: '',
// avatar: '/static/icon-img/avatar.png',
// campus: '西',
// remainingCourses: 5,
// expiryDate: '2023-11-15'
// },
// {
// id: 3,
// name: '',
// avatar: '/static/icon-img/avatar.png',
// campus: '',
// remainingCourses: 15,
// expiryDate: '2024-01-20'
// },
// {
// id: 4,
// name: '',
// avatar: '/static/icon-img/avatar.png',
// campus: '',
// remainingCourses: 8,
// expiryDate: '2023-11-30'
// },
// {
// id: 5,
// name: '',
// avatar: '/static/icon-img/avatar.png',
// campus: '',
// remainingCourses: 20,
// expiryDate: '2024-02-15'
// },
// {
// id: 6,
// name: '',
// avatar: '/static/icon-img/avatar.png',
// campus: '',
// remainingCourses: 3,
// expiryDate: '2023-10-30'
// }
// ];
},
goToDetail(student) {
uni.navigateTo({
url: `/pages/market/clue/clue_info?resource_sharing_id=`+student.resource_sharing_id
});
},
getRemainingCourses(item) {
const totalHours = (item.total_hours || 0)
+ (item.gift_hours || 0);
const usedHours = (item.use_total_hours ||
0) + (item.use_gift_hours || 0);
return totalHours - usedHours;
},
//
onNameInput(e) {
this.searchForm.name = e;
},
onPhoneInput(e) {
this.searchForm.phone = e;
},
onLessonCountInput(e) {
this.searchForm.lessonCount = e;
},
onLeaveCountInput(e) {
this.searchForm.leaveCount = e;
},
//
onCourseChange(e) {
this.searchForm.courseIndex = e.detail.value;
},
onClassChange(e) {
this.searchForm.classIndex = e.detail.value;
},
//
closeSearch() {
this.showSearch = false;
},
doSearch() {
// searchForm
this.showSearch = false;
//
uni.showLoading({
title: '搜索中...'
});
//
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '搜索功能待实现',
icon: 'none'
});
}, 800);
}
}
}
@ -134,7 +217,27 @@
<style lang="scss">
.container {
min-height: 100vh;
background-color: #F5F5F5;
background-color: #18181c;
}
.search-bar {
display: flex;
align-items: center;
background: #23232a;
border-radius: 12rpx;
padding: 18rpx 24rpx;
margin: 24rpx 24rpx 0 24rpx;
color: #bdbdbd;
font-size: 28rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.08);
&:active {
background: #2a2a31;
}
}
.search-placeholder {
margin-left: 12rpx;
color: #bdbdbd;
flex: 1;
}
.content {
@ -168,10 +271,14 @@
.student-card {
display: flex;
align-items: center;
background-color: #FFFFFF;
background-color: #23232a;
border-radius: 12rpx;
padding: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.12);
transition: box-shadow 0.2s;
&:active {
box-shadow: 0 4rpx 20rpx rgba(0,255,128,0.12);
}
}
.student-avatar {
@ -195,7 +302,7 @@
font-size: 32rpx;
font-weight: bold;
margin-bottom: 10rpx;
color: #333;
color: #fff;
}
.info-row {
@ -204,11 +311,11 @@
margin-top: 8rpx;
.info-label {
color: #666;
color: #bdbdbd;
}
.info-value {
color: #333;
color: #fff;
}
}
@ -216,4 +323,79 @@
padding-left: 20rpx;
}
}
.popup-content {
background: #23232a;
color: #fff;
padding: 32rpx 24rpx;
border-radius: 16rpx;
min-width: 300rpx;
text-align: left;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #00d18c;
margin-bottom: 24rpx;
text-align: center;
}
.picker-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 0;
border-bottom: 1px solid #333;
.picker-label {
color: #bdbdbd;
font-size: 28rpx;
}
.picker-value {
color: #fff;
font-size: 28rpx;
}
}
.search-btn {
width: 100%;
background: #00d18c;
color: #fff;
border: none;
border-radius: 12rpx;
font-size: 30rpx;
padding: 20rpx 0;
margin-top: 32rpx;
margin-bottom: 8rpx;
}
.fui-picker__input {
display: flex;
justify-content: space-between;
align-items: center;
height: 72rpx;
padding: 0 24rpx;
background-color: #2c2c34;
border-radius: 8rpx;
text {
font-size: 28rpx;
color: #fff;
}
}
.fui-btn__box {
margin-top: 40rpx;
padding: 0 24rpx;
}
.fui-page__bd {
padding: 30rpx;
background-color: #18181c;
}
.fui-section__title {
font-size: 36rpx;
color: #00d18c;
font-weight: bold;
margin-bottom: 30rpx;
text-align: center;
}
</style>

2
uniapp/pages/common/contract/contract_detail.vue

@ -174,7 +174,7 @@ export default {
id: this.contractId
})
if (response.data.code === 1) {
if (response.code === 1) {
this.contractData = response.data.data || {}
} else {
uni.showToast({

51
uniapp/pages/common/contract/contract_sign.vue

@ -113,6 +113,7 @@
<script>
import apiRoute from '@/common/axios.js'
import { uploadFile } from '@/common/util.js';
export default {
data() {
@ -335,12 +336,12 @@ export default {
}
//
const response = await apiRoute.post('/contract/sign', {
contract_id: this.contractId,
const response = await apiRoute.post('/member/contract_sign', {
contract_sign_id: this.contractId,
sign_file: uploadResult.url
})
if (response.data.code === 1) {
if (response.code === 1) {
uni.showToast({
title: '签名提交成功',
icon: 'success',
@ -368,44 +369,18 @@ export default {
},
//
uploadSignatureFile() {
async uploadSignatureFile() {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: this.$baseUrl + '/uploadImage',
filePath: this.signatureImageUrl,
name: 'file',
header: {
'Authorization': uni.getStorageSync('token')
uploadFile(
this.signatureImageUrl,
(fileData) => {
resolve({ success: true, url: fileData.url });
},
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.code === 1) {
resolve({
success: true,
url: data.data.url
})
} else {
resolve({
success: false,
message: data.msg || '上传失败'
})
}
} catch (error) {
resolve({
success: false,
message: '上传响应解析失败'
})
}
},
fail: (error) => {
resolve({
success: false,
message: '上传请求失败'
})
(err) => {
resolve({ success: false, message: '上传失败' });
}
})
})
);
});
},
}

6
uniapp/pages/common/contract/my_contract.vue

@ -142,10 +142,8 @@ export default {
page: this.currentPage,
limit: this.pageSize
})
if (response.data.code === 1) {
const newContracts = response.data.data.data || []
if (response.code === 1) {
const newContracts = response.data.data || []
if (refresh) {
this.contracts = newContracts
} else {

2325
uniapp/pages/market/clue/clue_info.vue

File diff suppressed because it is too large

114
uniapp/pages/market/clue/order_list.vue

@ -57,7 +57,18 @@
<view class="title">支付时间</view>
<view class="content">{{ v.payment_time || '' }}</view>
</view>
<!-- 已支付状态下显示的按钮区域 -->
<view class="action-buttons" v-if="v.order_status !== 'pending'">
<view class="action-btn contract-btn" @click.stop="handleContractSign(v)">
<fui-icon name="calendar" color="#29D3B4" size="32"></fui-icon>
<text>合同签订</text>
</view>
<view class="action-btn invoice-btn" @click.stop="handleInvoiceDownload(v)">
<fui-icon name="download" color="#29D3B4" size="32"></fui-icon>
<text>发票下载</text>
</view>
</view>
</view>
</view>
@ -259,7 +270,13 @@
<view class="custom-content">
<text class="custom-title">支付二维码</text>
<view class="custom-form">
<!-- #ifndef MP-WEIXIN -->
<fui-qrcode :value="qrcode"></fui-qrcode>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<image :src="qrcodeImage" mode="aspectFit"></image>
<!-- #endif -->
</view>
</view>
@ -298,6 +315,7 @@ export default {
isReachedBottom: false,//|true=|false=
showCustomModal: false,
qrcode: 'https://www.baidu.com',
qrcodeImage:'',
//
filteredData: {
page: 1,//
@ -842,6 +860,7 @@ export default {
//
apiRoute.getOrderPayQrcode({ order_id: orderData.id }).then(res => {
this.qrcode = res.data.code_url
this.qrcodeImage = res.data.qrcode_url
this.showCustomModal = true
})
},
@ -884,8 +903,70 @@ export default {
})
},
handleButtonClick(){
//
handleButtonClick() {
this.showCustomModal = false
uni.showToast({
title: '二维码已发送',
icon: 'success'
})
},
//
handleContractSign(orderData) {
console.log('处理合同签订:', orderData)
//
event.stopPropagation()
//
uni.showLoading({
title: '加载中...'
})
//
setTimeout(() => {
uni.hideLoading()
//
uni.navigateTo({
url: `/pages/market/clue/contract_sign?order_id=${orderData.id}`
}).catch(err => {
//
console.error('导航错误:', err)
uni.showToast({
title: '合同签订功能开发中',
icon: 'none'
})
})
}, 500)
},
//
handleInvoiceDownload(orderData) {
console.log('处理发票下载:', orderData)
//
event.stopPropagation()
//
if (!orderData.invoice_url) {
uni.showToast({
title: '暂无电子发票',
icon: 'none'
})
return
}
//
uni.showLoading({
title: '准备下载...'
})
//
this.downloadFile(orderData.invoice_url || this.$util.img(orderData.file_data))
setTimeout(() => {
uni.hideLoading()
}, 1000)
}
},
}
@ -977,6 +1058,33 @@ export default {
width: 100%;
}
}
//
.action-buttons {
display: flex;
justify-content: space-between;
margin-top: 30rpx;
padding-top: 20rpx;
border-top: 1px solid #555;
.action-btn {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
padding: 16rpx 0;
text {
margin-left: 10rpx;
color: #29D3B4;
font-size: 28rpx;
}
&.contract-btn {
border-right: 1px solid #555;
}
}
}
}
}

263
uniapp/pages/test/dict_test.vue

@ -1,263 +0,0 @@
<template>
<view class="container">
<view class="header">
<text class="title">字典功能测试</text>
</view>
<view class="test-section">
<view class="section-title">1. 测试单个字典获取</view>
<button class="test-btn" @click="testSingleDict">测试获取单个字典</button>
<view class="result" v-if="singleResult">
<text class="result-title">结果:</text>
<text class="result-content">{{ JSON.stringify(singleResult, null, 2) }}</text>
</view>
</view>
<view class="test-section">
<view class="section-title">2. 测试批量字典获取</view>
<button class="test-btn" @click="testBatchDict">测试批量获取字典</button>
<view class="result" v-if="batchResult">
<text class="result-title">结果:</text>
<text class="result-content">{{ JSON.stringify(batchResult, null, 2) }}</text>
</view>
</view>
<view class="test-section">
<view class="section-title">3. 测试字典缓存</view>
<button class="test-btn" @click="testCache">测试缓存机制</button>
<view class="result" v-if="cacheResult">
<text class="result-title">缓存测试结果:</text>
<text class="result-content">{{ cacheResult }}</text>
</view>
</view>
<view class="test-section">
<view class="section-title">4. 测试静默请求</view>
<button class="test-btn" @click="testQuietRequest">测试静默请求</button>
<view class="result" v-if="quietResult">
<text class="result-title">静默请求结果:</text>
<text class="result-content">{{ JSON.stringify(quietResult, null, 2) }}</text>
</view>
</view>
<view class="test-section">
<view class="section-title">5. 清除缓存</view>
<button class="test-btn clear" @click="clearAllCache">清除所有缓存</button>
</view>
</view>
</template>
<script>
import dictUtil from '@/common/dictUtil.js'
import axiosQuiet from '@/common/axiosQuiet.js'
export default {
data() {
return {
singleResult: null,
batchResult: null,
cacheResult: null,
quietResult: null
}
},
methods: {
//
async testSingleDict() {
try {
console.log('开始测试单个字典获取')
const result = await dictUtil.getDict('source')
this.singleResult = result
console.log('单个字典获取结果:', result)
uni.showToast({
title: '单个字典测试完成',
icon: 'success'
})
} catch (error) {
console.error('单个字典获取失败:', error)
this.singleResult = { error: error.message || '获取失败' }
uni.showToast({
title: '单个字典测试失败',
icon: 'none'
})
}
},
//
async testBatchDict() {
try {
console.log('开始测试批量字典获取')
const keys = ['source', 'SourceChannel', 'customer_purchasing_power']
const result = await dictUtil.getBatchDict(keys)
this.batchResult = result
console.log('批量字典获取结果:', result)
uni.showToast({
title: '批量字典测试完成',
icon: 'success'
})
} catch (error) {
console.error('批量字典获取失败:', error)
this.batchResult = { error: error.message || '获取失败' }
uni.showToast({
title: '批量字典测试失败',
icon: 'none'
})
}
},
//
async testCache() {
try {
console.log('开始测试缓存机制')
//
dictUtil.clearCache(['source'])
//
const start1 = Date.now()
await dictUtil.getDict('source', true)
const time1 = Date.now() - start1
//
const start2 = Date.now()
await dictUtil.getDict('source', true)
const time2 = Date.now() - start2
this.cacheResult = `第一次获取: ${time1}ms, 第二次获取: ${time2}ms, 缓存提升: ${Math.round((time1 - time2) / time1 * 100)}%`
uni.showToast({
title: '缓存测试完成',
icon: 'success'
})
} catch (error) {
console.error('缓存测试失败:', error)
this.cacheResult = '缓存测试失败: ' + (error.message || '未知错误')
uni.showToast({
title: '缓存测试失败',
icon: 'none'
})
}
},
//
async testQuietRequest() {
try {
console.log('开始测试静默请求')
const result = await axiosQuiet.get('/dict/batch', {
keys: 'source,SourceChannel'
})
this.quietResult = result
console.log('静默请求结果:', result)
uni.showToast({
title: '静默请求测试完成',
icon: 'success'
})
} catch (error) {
console.error('静默请求失败:', error)
this.quietResult = { error: error.message || error.msg || '请求失败' }
uni.showToast({
title: '静默请求测试失败',
icon: 'none'
})
}
},
//
clearAllCache() {
dictUtil.clearCache()
this.singleResult = null
this.batchResult = null
this.cacheResult = null
this.quietResult = null
uni.showToast({
title: '缓存已清除',
icon: 'success'
})
}
}
}
</script>
<style lang="scss" scoped>
.container {
padding: 20rpx;
background: #f5f5f5;
min-height: 100vh;
}
.header {
background: #fff;
padding: 30rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
text-align: center;
.title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
}
.test-section {
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.test-btn {
background: #29d3b4;
color: #fff;
border: none;
border-radius: 8rpx;
padding: 16rpx 32rpx;
font-size: 26rpx;
margin-bottom: 20rpx;
&.clear {
background: #ff6b6b;
}
&:active {
opacity: 0.8;
}
}
.result {
background: #f8f9fa;
border-radius: 8rpx;
padding: 20rpx;
border-left: 4rpx solid #29d3b4;
.result-title {
font-size: 24rpx;
color: #666;
display: block;
margin-bottom: 10rpx;
}
.result-content {
font-size: 22rpx;
color: #333;
word-break: break-all;
white-space: pre-wrap;
font-family: monospace;
line-height: 1.5;
}
}
</style>
Loading…
Cancel
Save