Browse Source

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

master
于宏哲PHP 9 months ago
parent
commit
150004c557
  1. 4
      .gitignore
  2. 6673
      admin/yarn.lock
  3. 119
      docker-compose.yml
  4. 3
      niucloud/.gitignore
  5. 16
      niucloud/app/api/controller/apiController/CommunicationRecords.php
  6. 26
      niucloud/app/api/controller/apiController/Course.php
  7. 40
      niucloud/app/api/controller/apiController/OrderTable.php
  8. 18
      niucloud/app/api/controller/apiController/Personnel.php
  9. 6
      niucloud/app/api/controller/pay/Pay.php
  10. 8
      niucloud/app/api/route/route.php
  11. 3
      niucloud/app/command/TestCommand.php
  12. 2
      niucloud/app/dict/schedule/schedule.php
  13. 3
      niucloud/app/job/transfer/schedule/PerformanceCalculation.php
  14. 3
      niucloud/app/listener/personnel/Student.php
  15. 8
      niucloud/app/model/student_courses/StudentCourses.php
  16. 65
      niucloud/app/service/admin/pay/PayService.php
  17. 19
      niucloud/app/service/api/apiService/CommunicationRecordsService.php
  18. 135
      niucloud/app/service/api/apiService/CourseService.php
  19. 247
      niucloud/app/service/api/apiService/PersonnelService.php
  20. 268
      start.sh
  21. 15
      uniapp/api/apiRoute.js
  22. 109
      uniapp/common/util.js
  23. 2
      uniapp/components/AQ/AQTabber.vue
  24. 70
      uniapp/components/custom-modal/custom-modal.vue
  25. 234
      uniapp/components/custom-modal/example.vue
  26. 10
      uniapp/pages.json
  27. 147
      uniapp/pages/coach/course/info_list.vue
  28. 337
      uniapp/pages/market/clue/clue_info.vue
  29. 164
      uniapp/pages/market/clue/clue_table.vue
  30. 811
      uniapp/pages/market/clue/order_list.vue
  31. 15
      uniapp/pages/student/login/login.vue

4
.gitignore

@ -8,4 +8,6 @@
/niucloud/runtime /niucloud/runtime
/niucloud/vendor /niucloud/vendor
/CLAUDE.md /CLAUDE.md
.claude .claude
node_modules
/docker

6673
admin/yarn.lock

File diff suppressed because it is too large

119
docker-compose.yml

@ -0,0 +1,119 @@
version: '3.8'
services:
# PHP 服务
php:
image: php:8.2-fpm
container_name: niucloud_php
volumes:
- ./niucloud:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
working_dir: /var/www/html
depends_on:
- mysql
- redis
environment:
- PHP_IDE_CONFIG=serverName=niucloud
command: >
bash -c "
apt-get update &&
apt-get install -y libzip-dev zip unzip git libpng-dev libjpeg-dev libfreetype6-dev &&
docker-php-ext-configure gd --with-freetype --with-jpeg &&
docker-php-ext-install pdo pdo_mysql mysqli zip gd &&
pecl install redis &&
docker-php-ext-enable redis &&
php-fpm
"
networks:
- niucloud_network
# Nginx 服务
nginx:
image: nginx:alpine
container_name: niucloud_nginx
ports:
- "20080:80" # 原本是 80 映射到 20080
- "20081:8080" # 原本是 8080 映射到 20081
volumes:
- ./niucloud:/var/www/html
- ./admin/dist:/var/www/admin
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./docker/nginx/conf.d:/etc/nginx/conf.d
- ./docker/logs/nginx:/var/log/nginx
depends_on:
- php
- node
networks:
- niucloud_network
# MySQL 数据库
mysql:
image: mysql:8.0
container_name: niucloud_mysql
ports:
- "23306:3306" # 原本是 3306 映射到 23306
environment:
MYSQL_ROOT_PASSWORD: root123456
MYSQL_DATABASE: niucloud
MYSQL_USER: niucloud
MYSQL_PASSWORD: niucloud123
volumes:
- ./docker/data/mysql:/var/lib/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
- ./docker/logs/mysql:/var/log/mysql
command: --default-authentication-plugin=mysql_native_password
networks:
- niucloud_network
# Redis 缓存
redis:
image: redis:alpine
container_name: niucloud_redis
ports:
- "26379:6379" # 原本是 6379 映射到 26379
volumes:
- ./docker/data/redis:/data
- ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf
command: redis-server /usr/local/etc/redis/redis.conf
networks:
- niucloud_network
# Node.js 服务 (用于构建前端)
node:
image: node:18-alpine
container_name: niucloud_node
working_dir: /app
volumes:
- ./admin:/app
- ./docker/data/node_modules:/app/node_modules
ports:
- "23000:3000" # 原本是 3000 映射到 23000
command: >
sh -c "
npm config set registry https://registry.npmmirror.com &&
npm install &&
npm run dev
"
networks:
- niucloud_network
# Composer 服务 (用于 PHP 依赖管理)
composer:
image: composer:latest
container_name: niucloud_composer
volumes:
- ./niucloud:/app
working_dir: /app
command: install --ignore-platform-reqs
networks:
- niucloud_network
networks:
niucloud_network:
driver: bridge
volumes:
mysql_data:
redis_data:
node_modules:

3
niucloud/.gitignore

@ -4,4 +4,5 @@
install.lock install.lock
/vendor/* /vendor/*
.env .env
runtime/* runtime/*
composer.lock

16
niucloud/app/api/controller/apiController/CommunicationRecords.php

@ -46,4 +46,20 @@ class CommunicationRecords extends BaseApiService
} }
return success('操作成功'); return success('操作成功');
} }
public function edit(Request $request){
$date = date('Y-m-d H:i:s');
$where = [
['id', '=', $request->param('id', '')],
];
$data = [
'remarks' => $request->param('remarks', ''),//备注
'updated_at' => $date
];
$res = (new CommunicationRecordsService())->edit($where,$data);
if(!$res['code']){
return fail('操作失败');
}
return success('操作成功');
}
} }

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

@ -124,4 +124,30 @@ class Course extends BaseApiService
return (new CourseService())->schedule_del($data); return (new CourseService())->schedule_del($data);
} }
/**
* 更新学员课程人员配置
* @param Request $request
* @return \think\Response
*/
public function updateStudentCoursePersonnel(Request $request)
{
try {
$params = $request->all();
// 验证必要参数
if (empty($params['student_course_id'])) {
return fail('学员课程ID不能为空');
}
$res = (new CourseService())->updateStudentCoursePersonnel($params);
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data'], '更新成功');
} catch (\Exception $e) {
return fail('更新学员课程人员配置失败:' . $e->getMessage());
}
}
} }

40
niucloud/app/api/controller/apiController/OrderTable.php

@ -72,21 +72,42 @@ class OrderTable extends BaseApiService
//订单-创建 //订单-创建
public function add(Request $request) public function add(Request $request)
{ {
// 获取当前登录的员工ID
$staff_id = $request->memberId() ?? 0;
$params = $request->params([ $params = $request->params([
["payment_type", ""], // 付款类型必填验证 ["payment_type", ""], // 付款类型必填验证
["course_id", ""], // 课程ID必填验证 ["course_id", ""], // 课程ID必填验证
["class_id", ""], // 班级ID必填验证 ["class_id", ""], // 班级ID必填验证
["staff_id", ""], // 员工ID必填验证 ["staff_id", ""], // 员工ID(可选)
["resource_id", ""], // 客户资源表ID必填验证 ["resource_id", ""], // 客户资源表ID必填验证
["order_type", ""], // 客户资源表ID必填验证
]); ]);
foreach($params as $k=>$v){ // 验证必要参数
if(empty($v)){ if(empty($params['payment_type']) || empty($params['course_id']) ||
return fail('缺少参数'); empty($params['class_id']) || empty($params['resource_id'])) {
return fail('缺少必要参数');
}
// 如果前端没提供员工ID,使用当前登录的员工ID
if(empty($params['staff_id'])) {
if(empty($staff_id)) {
return fail('无法获取员工信息');
} }
$params['staff_id'] = $staff_id;
} }
// 获取班级信息以查询campus_id
$class = \app\model\class_grade\ClassGrade::where('id', $params['class_id'])->find();
if (!$class) {
return fail('班级不存在');
}
$class = $class->toArray();
$campus_id = $class['campus_id'] ?? 0;
if(empty($campus_id)) {
return fail('班级没有关联校区');
}
$course = \app\model\course\Course::where('id', $params['course_id'])->find(); $course = \app\model\course\Course::where('id', $params['course_id'])->find();
if (!$course) { if (!$course) {
@ -95,7 +116,6 @@ class OrderTable extends BaseApiService
$course = $course->toArray(); $course = $course->toArray();
$order_amount = $course['price'];//课程的价格 $order_amount = $course['price'];//课程的价格
$data = [ $data = [
'payment_type' => $params['payment_type'],//付款类型: cash-现金支付, scan_code-扫码支付, subscription-订阅支付 'payment_type' => $params['payment_type'],//付款类型: cash-现金支付, scan_code-扫码支付, subscription-订阅支付
'order_amount' => $order_amount,//订单金额 'order_amount' => $order_amount,//订单金额
@ -103,12 +123,10 @@ class OrderTable extends BaseApiService
'class_id' => $params['class_id'],//班级ID 'class_id' => $params['class_id'],//班级ID
'staff_id' => $params['staff_id'],//员工表ID 'staff_id' => $params['staff_id'],//员工表ID
'resource_id' => $params['resource_id'],//客户资源表id 'resource_id' => $params['resource_id'],//客户资源表id
'campus_id' => $campus_id,//校区ID
'order_type' => $params['order_type'],
]; ];
$res = (new OrderTableService())->addData($data); $res = (new OrderTableService())->addData($data);
if (!$res['code']) { if (!$res['code']) {

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

@ -262,4 +262,22 @@ class Personnel extends BaseApiService
} }
} }
/**
* 获取教练数据列表
* @param Request $request
* @return \think\Response
*/
public function getCoachList(Request $request)
{
try {
$res = (new PersonnelService())->getCoachList();
if (!$res['code']) {
return fail($res['msg']);
}
return success($res['data']);
} catch (\Exception $e) {
return fail('获取教练列表失败:' . $e->getMessage());
}
}
} }

6
niucloud/app/api/controller/pay/Pay.php

@ -101,11 +101,9 @@ class Pay extends BaseApiController
*/ */
public function getQrcode(){ public function getQrcode(){
$data = $this->request->params([ $data = $this->request->params([
['trade_type', ''], ['order_id', ''],
['trade_id', ''],
['type', ''],
]); ]);
return success('SUCCESS',(new PayService())->getQrcode($data['type'], $data['trade_type'], $data['trade_id'])); return success('SUCCESS',(new \app\service\admin\pay\PayService())->order_pay($data));
} }
public function qrcodeNotify(int $order_id) public function qrcodeNotify(int $order_id)

8
niucloud/app/api/route/route.php

@ -226,6 +226,8 @@ Route::group(function () {
//员工端-获取全部人员列表 //员工端-获取全部人员列表
Route::get('personnel/getPersonnelAll', 'apiController.Personnel/getPersonnelAll'); Route::get('personnel/getPersonnelAll', 'apiController.Personnel/getPersonnelAll');
//员工端-获取教练数据列表
Route::get('personnel/getCoachList', 'apiController.Personnel/getCoachList');
//员工端-添加新员工信息 //员工端-添加新员工信息
Route::post('personnel/add', 'apiController.Personnel/add'); Route::post('personnel/add', 'apiController.Personnel/add');
@ -255,6 +257,7 @@ Route::group(function () {
//沟通记录-添加 //沟通记录-添加
Route::post('communicationRecords/add', 'apiController.CommunicationRecords/add'); Route::post('communicationRecords/add', 'apiController.CommunicationRecords/add');
Route::post('communicationRecords/edit', 'apiController.CommunicationRecords/edit');
//校区-获取员工下的全部校区 //校区-获取员工下的全部校区
Route::get('campus/getPersonnelCampus', 'apiController.Campus/getPersonnelCampus'); Route::get('campus/getPersonnelCampus', 'apiController.Campus/getPersonnelCampus');
@ -286,6 +289,9 @@ Route::group(function () {
//员工端-订单管理-创建 //员工端-订单管理-创建
Route::post('orderTable/add', 'apiController.OrderTable/add'); Route::post('orderTable/add', 'apiController.OrderTable/add');
//员工端-更新学员课程人员配置
Route::post('updateStudentCoursePersonnel', 'apiController.Course/updateStudentCoursePersonnel');
@ -371,6 +377,8 @@ Route::group(function () {
Route::get('personnel/serviceLogDetail', 'apiController.Personnel/serviceLogDetail'); Route::get('personnel/serviceLogDetail', 'apiController.Personnel/serviceLogDetail');
Route::post('personnel/updateServiceRemark', 'apiController.Personnel/updateServiceRemark'); Route::post('personnel/updateServiceRemark', 'apiController.Personnel/updateServiceRemark');
Route::get('getQrcode', 'pay.Pay/getQrcode');
})->middleware(ApiChannel::class) })->middleware(ApiChannel::class)
->middleware(ApiPersonnelCheckToken::class, true) ->middleware(ApiPersonnelCheckToken::class, true)

3
niucloud/app/command/TestCommand.php

@ -4,6 +4,7 @@ declare (strict_types = 1);
namespace app\command; namespace app\command;
use app\job\transfer\schedule\CourseScheduleJob; use app\job\transfer\schedule\CourseScheduleJob;
use app\job\transfer\schedule\PerformanceCalculation;
use app\job\transfer\schedule\ResourceAutoAllocation; use app\job\transfer\schedule\ResourceAutoAllocation;
use think\console\Command; use think\console\Command;
use think\console\Input; use think\console\Input;
@ -23,7 +24,7 @@ class TestCommand extends Command
protected function execute(Input $input, Output $output) protected function execute(Input $input, Output $output)
{ {
// 指令输出 // 指令输出
$obj = new CourseScheduleJob(); $obj = new ResourceAutoAllocation();
$obj->doJob(); $obj->doJob();
$output->writeln('testcommand'); $output->writeln('testcommand');
} }

2
niucloud/app/dict/schedule/schedule.php

@ -11,7 +11,7 @@ return [
'hour' => 0, 'hour' => 0,
'min' => 5 'min' => 5
], ],
'class' => 'app\job\transfer\schedule\CheckFinish', 'class' => 'app\job\transfer\schedule\ResourceAutoAllocation',
'function' => '' 'function' => ''
], ],
[ [

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

@ -235,8 +235,7 @@ class PerformanceCalculation extends BaseJob
$orders = OrderTable::with(['course', 'personnel']) $orders = OrderTable::with(['course', 'personnel'])
->where('order_status', 'completed') // 假设只计算已完成的订单 ->where('order_status', 'completed') // 假设只计算已完成的订单
->where(function ($query) { ->where(function ($query) {
$query->where('performance_calculated', 0) // 未计算过绩效的订单 $query->whereNull('accounting_time'); // 或者核算时间为空的订单
->whereOr('accounting_time', null); // 或者核算时间为空的订单
}) })
->select() ->select()
->toArray(); ->toArray();

3
niucloud/app/listener/personnel/Student.php

@ -91,7 +91,8 @@ class Student
'gift_hours' => $course_info['gift_session_count'], 'gift_hours' => $course_info['gift_session_count'],
'start_date' => date("Y-m-d"), 'start_date' => date("Y-m-d"),
'end_date' => date("Y-m-d", strtotime("+30 days")), 'end_date' => date("Y-m-d", strtotime("+30 days")),
'single_session_count' => $course_info['single_session_count'] 'single_session_count' => $course_info['single_session_count'],
'resource_id'=> $order_info['resource_id']
]); ]);

8
niucloud/app/model/student_courses/StudentCourses.php

@ -69,18 +69,14 @@ class StudentCourses extends BaseModel
$query->where("course_id", $value); $query->where("course_id", $value);
} }
} }
public function student(){ public function student(){
return $this->hasOne(Student::class, 'id', 'student_id')->joinType('left')->withField('name,id')->bind(['student_id_name'=>'name']); return $this->hasOne(Student::class, 'id', 'student_id');
} }
public function course(){ public function course(){
return $this->hasOne(Course::class, 'id', 'course_id')->joinType('left')->withField('course_name,id')->bind(['course_id_name'=>'course_name']); return $this->hasOne(Course::class, 'id', 'course_id');
} }
} }

65
niucloud/app/service/admin/pay/PayService.php

@ -50,7 +50,7 @@ class PayService extends BaseAdminService
public function getAuditPage(array $where) public function getAuditPage(array $where)
{ {
$field = 'id, out_trade_no, type, money, body, voucher, create_time, trade_id, trade_type, status'; $field = 'id, out_trade_no, type, money, body, voucher, create_time, trade_id, trade_type, status';
$search_model = $this->model->where([ [ 'type', '=', PayDict::OFFLINEPAY ] ])->withSearch([ 'create_time', 'out_trade_no', 'status' ], $where)->field($field)->append([ 'type_name' ])->order('create_time desc'); $search_model = $this->model->where([['type', '=', PayDict::OFFLINEPAY]])->withSearch(['create_time', 'out_trade_no', 'status'], $where)->field($field)->append(['type_name'])->order('create_time desc');
return $this->pageQuery($search_model); return $this->pageQuery($search_model);
} }
@ -62,9 +62,9 @@ class PayService extends BaseAdminService
public function getDetail(int $id) public function getDetail(int $id)
{ {
$field = 'id,out_trade_no,trade_type,trade_id,trade_no,body,money,voucher,status,create_time,pay_time,cancel_time,type,channel,fail_reason'; $field = 'id,out_trade_no,trade_type,trade_id,trade_no,body,money,voucher,status,create_time,pay_time,cancel_time,type,channel,fail_reason';
return $this->model->where([ [ 'id', '=', $id ] ]) return $this->model->where([['id', '=', $id]])
->field($field) ->field($field)
->append([ 'type_name', 'channel_name', 'status_name' ]) ->append(['type_name', 'channel_name', 'status_name'])
->findOrEmpty() ->findOrEmpty()
->toArray(); ->toArray();
} }
@ -76,7 +76,7 @@ class PayService extends BaseAdminService
*/ */
public function pass(string $out_trade_no) public function pass(string $out_trade_no)
{ {
return ( new CoreOfflineService() )->pass($out_trade_no); return (new CoreOfflineService())->pass($out_trade_no);
} }
/** /**
@ -86,7 +86,7 @@ class PayService extends BaseAdminService
*/ */
public function refuse(string $out_trade_no, string $reason) public function refuse(string $out_trade_no, string $reason)
{ {
return ( new CoreOfflineService() )->refuse($out_trade_no, $reason); return (new CoreOfflineService())->refuse($out_trade_no, $reason);
} }
/** /**
@ -115,7 +115,7 @@ class PayService extends BaseAdminService
*/ */
public function pay(string $type, string $trade_type, int $trade_id, string $return_url = '', string $quit_url = '', string $buyer_id = '', string $voucher = '', string $openid = '') public function pay(string $type, string $trade_type, int $trade_id, string $return_url = '', string $quit_url = '', string $buyer_id = '', string $voucher = '', string $openid = '')
{ {
return ( new CorePayService() )->pay($trade_type, $trade_id, $type, ChannelDict::PC, $openid, $return_url, $quit_url, $buyer_id, $voucher); return (new CorePayService())->pay($trade_type, $trade_id, $type, ChannelDict::PC, $openid, $return_url, $quit_url, $buyer_id, $voucher);
} }
/** /**
@ -126,7 +126,7 @@ class PayService extends BaseAdminService
*/ */
public function getInfoByTrade(string $trade_type, int $trade_id) public function getInfoByTrade(string $trade_type, int $trade_id)
{ {
return ( new CorePayService() )->getInfoByTrade($trade_type, $trade_id, ChannelDict::H5); return (new CorePayService())->getInfoByTrade($trade_type, $trade_id, ChannelDict::H5);
} }
/** /**
@ -137,26 +137,26 @@ class PayService extends BaseAdminService
*/ */
public function getFriendspayInfoByTrade(string $trade_type, int $trade_id, string $channel) public function getFriendspayInfoByTrade(string $trade_type, int $trade_id, string $channel)
{ {
$pay_info = ( new CorePayService() )->getInfoByTrade($trade_type, $trade_id, ChannelDict::H5, PaySceneDict::FRIENDSPAY); $pay_info = (new CorePayService())->getInfoByTrade($trade_type, $trade_id, ChannelDict::H5, PaySceneDict::FRIENDSPAY);
if (!empty($pay_info)) { if (!empty($pay_info)) {
//海报 //海报
$poster = ( new Poster() )->field('id')->where([ $poster = (new Poster())->field('id')->where([
[ 'type', '=', 'friendspay' ], ['type', '=', 'friendspay'],
[ 'status', '=', 1 ], ['status', '=', 1],
[ 'is_default', '=', 1 ] ['is_default', '=', 1]
])->findOrEmpty()->toArray(); ])->findOrEmpty()->toArray();
if (!empty($poster)) { if (!empty($poster)) {
$pay_info[ 'poster_id' ] = $poster[ 'id' ]; $pay_info['poster_id'] = $poster['id'];
} }
//发起帮付会员信息 //发起帮付会员信息
$member = ( new Member() )->field('member_id,nickname,headimg')->where([ $member = (new Member())->field('member_id,nickname,headimg')->where([
[ 'member_id', '=', $pay_info[ 'from_main_id' ] ] ['member_id', '=', $pay_info['from_main_id']]
])->findOrEmpty()->toArray(); ])->findOrEmpty()->toArray();
$pay_info[ 'member' ] = $member; $pay_info['member'] = $member;
//二维码 //二维码
$qrcode = $this->getQrcode($trade_type, $trade_id, $channel); $qrcode = $this->getQrcode($trade_type, $trade_id, $channel);
$pay_info[ 'link' ] = $qrcode[ 'url' ]; $pay_info['link'] = $qrcode['url'];
$pay_info[ 'qrcode' ] = $qrcode[ 'path' ]; $pay_info['qrcode'] = $qrcode['path'];
} }
return $pay_info; return $pay_info;
} }
@ -169,7 +169,7 @@ class PayService extends BaseAdminService
*/ */
public function getQrcode(string $trade_type, int $trade_id, string $channel) public function getQrcode(string $trade_type, int $trade_id, string $channel)
{ {
$url = ( new CoreSysConfigService() )->getSceneDomain()[ 'wap_url' ]; $url = (new CoreSysConfigService())->getSceneDomain()['wap_url'];
$page = 'app/pages/friendspay/money'; $page = 'app/pages/friendspay/money';
$data = [ $data = [
@ -194,7 +194,7 @@ class PayService extends BaseAdminService
$url = $url . '/' . $page; $url = $url . '/' . $page;
$scene = []; $scene = [];
foreach ($data as $v) { foreach ($data as $v) {
$scene[] = $v[ 'key' ] . '=' . $v[ 'value' ]; $scene[] = $v['key'] . '=' . $v['value'];
} }
$url .= '?' . implode('&', $scene); $url .= '?' . implode('&', $scene);
@ -213,11 +213,11 @@ class PayService extends BaseAdminService
*/ */
public function getPayTypeList() public function getPayTypeList()
{ {
$pay_type_list = ( new CorePayService() )->getPayTypeByTrade('', ChannelDict::H5); $pay_type_list = (new CorePayService())->getPayTypeByTrade('', ChannelDict::H5);
if (!empty($pay_type_list)) { if (!empty($pay_type_list)) {
foreach ($pay_type_list as $k => $v) { foreach ($pay_type_list as $k => $v) {
if (!in_array($v['key'], [ PayDict::BALANCEPAY, PayDict::FRIENDSPAY ])) { if (!in_array($v['key'], [PayDict::BALANCEPAY, PayDict::FRIENDSPAY])) {
unset($pay_type_list[ $k ]); unset($pay_type_list[$k]);
} }
} }
$pay_type_list = array_values($pay_type_list); $pay_type_list = array_values($pay_type_list);
@ -225,17 +225,18 @@ class PayService extends BaseAdminService
return $pay_type_list; return $pay_type_list;
} }
public function order_pay($data){ public function order_pay($data)
$out_trade_no = 'sm'.date("YmdHis").time(); {
$out_trade_no = 'sm' . date("YmdHis") . time();
$order = new OrderTable(); $order = new OrderTable();
$order_info = $order->where(['id' => $data['order_id']])->find(); $order_info = $order->where(['id' => $data['order_id']])->find();
$cr = new CustomerResources(); $cr = new CustomerResources();
$resource_info = $cr->where(['id' => $order_info['resource_id']])->find(); $resource_info = $cr->where(['id' => $order_info['resource_id']])->find();
$config = return_pay_config($resource_info['campus'],$data['order_id']); $config = return_pay_config($resource_info['campus'], $data['order_id']);
$params = [ $params = [
'out_trade_no' => $out_trade_no, 'out_trade_no' => $out_trade_no,
'body' => '订单扫码支付:'.$out_trade_no, 'body' => '订单扫码支付:' . $out_trade_no,
'money' => $order_info['order_amount'] * 100, 'money' => $order_info['order_amount'] * 100,
]; ];
@ -243,7 +244,7 @@ class PayService extends BaseAdminService
$url = $pay->scan($params); $url = $pay->scan($params);
$path = qrcode($url['code_url'],'',[],'upload/qrcode/pay/'); $path = qrcode($url['code_url'], '', [], 'upload/qrcode/pay/');
$order->where(['id' => $data['order_id']])->update(['payment_id' => $out_trade_no,'ipv3' => $config['mch_secret_key']]); $order->where(['id' => $data['order_id']])->update(['payment_id' => $out_trade_no,'ipv3' => $config['mch_secret_key']]);
@ -267,13 +268,13 @@ class PayService extends BaseAdminService
'channel' => '微信扫码支付' 'channel' => '微信扫码支付'
]); ]);
return ['qrcode_url' => getCurrentDomain().$path,'out_trade_no'=>$out_trade_no]; return ['qrcode_url' => getCurrentDomain().$path,'out_trade_no'=>$out_trade_no,'code_url'=> $url['code_url']];
} }
public function check_payment_status($data){ public function check_payment_status($data)
{
$order = new OrderTable(); $order = new OrderTable();
$order_status = $order->where(['payment_id' => $data['out_trade_no']])->value("order_status"); $order_status = $order->where(['payment_id' => $data['out_trade_no']])->value("order_status");
return ['order_status' => $order_status];
return ['order_status'=>$order_status];
} }
} }

19
niucloud/app/service/api/apiService/CommunicationRecordsService.php

@ -86,4 +86,23 @@ class CommunicationRecordsService extends BaseApiService
]; ];
} }
public function edit(array $where, array $data)
{
$model = $this->model;
$edit = $model->where($where)->update($data);
if ($edit) {
$res = [
'code' => 1,
'msg' => '操作成功',
'data' => []
];
} else {
$res = [
'code' => 0,
'msg' => '操作失败',
'data' => []
];
}
return $res;
}
} }

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

@ -200,17 +200,66 @@ class CourseService extends BaseApiService
$PersonCourseSchedule = new PersonCourseSchedule(); $PersonCourseSchedule = new PersonCourseSchedule();
$student_arr = $PersonCourseSchedule->where('schedule_id',$id)->column('student_id'); $student_arr = $PersonCourseSchedule->where('schedule_id',$id)->column('student_id');
$query = $StudentCourses->where('course_id', $id); // 获取当前时间
$now = date('Y-m-d H:i:s');
// 查询end_date大于当前时间的课程记录
$query = $StudentCourses->where('course_id', $id)
->where('end_date', '>', $now);
if (!empty($student_arr)) { if (!empty($student_arr)) {
$query->whereNotIn('student_id', $student_arr); $query->whereNotIn('student_id', $student_arr);
} }
$res = $query->field('student_id as value')->select()->toArray();
$Student = new Student(); $studentCourses = $query->field('student_id as value, resource_id')->select()->toArray();
foreach ($res as $k => &$v) {
$StudentName = $Student->where('id',$v['value'])->value('name'); // 如果没有符合条件的记录,直接返回空数组
$v['text'] = $StudentName; if (empty($studentCourses)) {
return [];
} }
return $res;
// 收集所有的resource_id
$resourceIds = array_column($studentCourses, 'resource_id');
$resourceIds = array_filter($resourceIds); // 过滤掉空值
if (empty($resourceIds)) {
return [];
}
// 查询客户资源表获取详细信息
$resources = Db::name('customer_resources')
->whereIn('id', $resourceIds)
->field('id, name, source_channel, source, age')
->select()
->toArray();
// 将资源信息与学生ID关联
$result = [];
foreach ($resources as $resource) {
// 查找对应的学生ID
foreach ($studentCourses as $course) {
if ($course['resource_id'] == $resource['id']) {
// 源和来源渠道转换为字典值
$source = get_dict_value("source", $resource['source']);
$sourceChannel = get_dict_value("source_channel", $resource['source_channel']);
$result[] = [
'value' => $course['value'], // 学生ID
'resource_id' => $resource['id'],
'text' => $resource['name'], // 学生姓名
'name' => $resource['name'],
'age' => $resource['age'],
'source' => $source,
'source_channel' => $sourceChannel,
'source_raw' => $resource['source'],
'source_channel_raw' => $resource['source_channel']
];
break;
}
}
}
return $result;
} }
public function addStudent($data) public function addStudent($data)
@ -377,5 +426,77 @@ class CourseService extends BaseApiService
} }
} }
/**
* 更新学员课程人员配置
* @param array $data
* @return array
*/
public function updateStudentCoursePersonnel(array $data)
{
$res = [
'code' => 0,
'msg' => '更新失败',
'data' => []
];
try {
// 开启事务
Db::startTrans();
$studentCourseId = $data['student_course_id'];
$mainCoachId = $data['main_coach_id'] ?? null;
$educationId = $data['education_id'] ?? null;
$assistantIds = $data['assistant_ids'] ?? '';
// 更新学员课程表
$updateData = [
'updated_at' => date('Y-m-d H:i:s')
];
if ($mainCoachId !== null) {
$updateData['main_coach_id'] = $mainCoachId;
}
if ($educationId !== null) {
$updateData['education_id'] = $educationId;
}
if ($assistantIds !== '') {
$updateData['assistant_ids'] = $assistantIds;
}
// 更新学员课程表
$updateResult = Db::name('student_courses')
->where('id', $studentCourseId)
->update($updateData);
if (!$updateResult) {
Db::rollback();
$res['msg'] = '更新学员课程配置失败';
return $res;
}
// 提交事务
Db::commit();
$res = [
'code' => 1,
'msg' => '更新成功',
'data' => [
'student_course_id' => $studentCourseId,
'main_coach_id' => $mainCoachId,
'education_id' => $educationId,
'assistant_ids' => $assistantIds
]
];
} catch (\Exception $e) {
Db::rollback();
$res['msg'] = '更新学员课程人员配置异常:' . $e->getMessage();
}
return $res;
}
} }

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

@ -189,52 +189,51 @@ class PersonnelService extends BaseApiService
$model = $this->model; $model = $this->model;
//存在员工id的时候 //存在员工id的时候
if ((!empty($where['personnel_id']) || isset($where['personnel_id'])) && $where['personnel_id'] !== '') { // if ((!empty($where['personnel_id']) || isset($where['personnel_id'])) && $where['personnel_id'] !== '') {
//查询这个员工的校区id // //查询这个员工的校区id
$campus_id = CampusPersonRole::where('person_id', $where['personnel_id']) // $campus_id = CampusPersonRole::where('person_id', $where['personnel_id'])
->distinct(true) // ->distinct(true)
->column('campus_id'); // ->column('campus_id');
if ($campus_id[0]) { // if ($campus_id[0]) {
//
$person_id_arr = CampusPersonRole::whereIn('campus_id', $campus_id) // $person_id_arr = CampusPersonRole::whereIn('campus_id', $campus_id)
->where(['dept_id' => 3]) // ->where(['dept_id' => 3])
->distinct(true) // ->distinct(true)
->column('person_id'); // ->column('person_id');
if ($person_id_arr) { // if ($person_id_arr) {
//根据校区id获取校区下的全部员工 // //根据校区id获取校区下的全部员工
$model = $model->whereIn('id', $person_id_arr); // $model = $model->whereIn('id', $person_id_arr);
} // }
}else{ // }else{
echo 123;die; // $person_id_arr = CampusPersonRole::whereIn('campus_id', $campus_id)
$person_id_arr = CampusPersonRole::whereIn('campus_id', $campus_id) // ->where(['dept_id' => 3])
->where(['dept_id' => 3]) // ->distinct(true)
->distinct(true) // ->column('person_id');
->column('person_id'); // if ($person_id_arr) {
if ($person_id_arr) { // //根据校区id获取校区下的全部员工
//根据校区id获取校区下的全部员工 // $model = $model->whereIn('id', $person_id_arr);
$model = $model->whereIn('id', $person_id_arr); // }
} // }
} // }
} //
// if (empty($where['account_type'])) {
if (empty($where['account_type'])) { // $model = $model->where('account_type', $where['account_type']);
$model = $model->where('account_type', $where['account_type']); // }
} //
// if (!empty($where['campus'])) {
if (!empty($where['campus'])) { //// $model = $model->where('campus', $where['campus']);
// $model = $model->where('campus', $where['campus']); //
// $person_id_arr = CampusPersonRole::whereIn('campus_id', $where['campus'])
$person_id_arr = CampusPersonRole::whereIn('campus_id', $where['campus']) // ->distinct(true)
->distinct(true) // ->where(['dept_id' => 3])
->where(['dept_id' => 3]) // ->column('person_id');
->column('person_id'); //
// if ($person_id_arr) {
if ($person_id_arr) { // //根据校区id获取校区下的全部员工
//根据校区id获取校区下的全部员工 // $model = $model->whereIn('id', $person_id_arr);
$model = $model->whereIn('id', $person_id_arr); // }
} //
// }
}
$res = $model->field($field) $res = $model->field($field)
@ -573,4 +572,160 @@ class PersonnelService extends BaseApiService
return $prefix . $date . str_pad($sequence, 4, '0', STR_PAD_LEFT); return $prefix . $date . str_pad($sequence, 4, '0', STR_PAD_LEFT);
} }
/**
* 获取教练数据列表
* @return array
*/
public function getCoachList()
{
$res = [
'code' => 0,
'msg' => '获取教练列表失败',
'data' => []
];
try {
// 查询dept_id=2的所有人员关系
$campusPersonRoles = CampusPersonRole::where('dept_id', 2)
->field('person_id')
->select()
->toArray();
if (empty($campusPersonRoles)) {
$res = [
'code' => 1,
'msg' => '暂无教练数据',
'data' => [
'coach_list' => [],
'education_list' => [],
'assistant_list' => []
]
];
return $res;
}
// 提取所有person_id
$personIds = array_unique(array_column($campusPersonRoles, 'person_id'));
// 根据person_id查询Personnel表获取姓名、头像、手机号
$personnelList = $this->model
->whereIn('id', $personIds)
->field('id as person_id, name, head_img, phone')
->select()
->toArray();
if (empty($personnelList)) {
$res = [
'code' => 1,
'msg' => '暂无教练数据',
'data' => [
'coach_list' => [],
'education_list' => [],
'assistant_list' => []
]
];
return $res;
}
// 查询每个人员的具体角色信息
$coachList = [];
$educationList = [];
$assistantList = [];
foreach ($personnelList as $personnel) {
// 查询该人员的角色信息
$roles = CampusPersonRole::where('person_id', $personnel['person_id'])
->where('dept_id', 2)
->with(['sysRole' => function ($query) {
$query->field('role_id, role_name, role_key');
}])
->select()
->toArray();
$roleNames = [];
$roleKeys = [];
foreach ($roles as $role) {
if (!empty($role['sys_role'])) {
$roleNames[] = $role['sys_role']['role_name'];
$roleKeys[] = $role['sys_role']['role_key'];
}
}
$roleNameStr = implode(',', $roleNames);
$roleKeyStr = implode(',', $roleKeys);
// 根据角色名称分类
$personnelData = [
'id' => $personnel['person_id'],
'person_id' => $personnel['person_id'], // 确保person_id字段存在
'name' => $personnel['name'],
'head_img' => $personnel['head_img'],
'phone' => $personnel['phone'],
'role_names' => $roleNameStr,
'role_keys' => $roleKeyStr
];
// 根据角色进行分类 - 使用更灵活的匹配规则
$isCoach = false;
$isEducation = false;
$isAssistant = false;
// 检查是否为教练类角色
if (stripos($roleNameStr, '教练') !== false ||
stripos($roleNameStr, '教师') !== false ||
stripos($roleNameStr, '主教') !== false ||
stripos($roleKeyStr, 'coach') !== false ||
stripos($roleKeyStr, 'teacher') !== false) {
$isCoach = true;
}
// 检查是否为教务类角色
if (stripos($roleNameStr, '教务') !== false ||
stripos($roleKeyStr, 'education') !== false ||
stripos($roleKeyStr, 'academic') !== false) {
$isEducation = true;
}
// 检查是否为助教类角色
if (stripos($roleNameStr, '助教') !== false ||
stripos($roleNameStr, '助理') !== false ||
stripos($roleKeyStr, 'assistant') !== false) {
$isAssistant = true;
}
// 如果没有明确的角色分类,则作为通用人员添加到所有列表
if (!$isCoach && !$isEducation && !$isAssistant) {
$isCoach = $isEducation = $isAssistant = true;
}
// 根据分类结果添加到对应列表
if ($isCoach) {
$coachList[] = $personnelData;
}
if ($isEducation) {
$educationList[] = $personnelData;
}
if ($isAssistant) {
$assistantList[] = $personnelData;
}
}
$res = [
'code' => 1,
'msg' => '获取成功',
'data' => [
'coach_list' => $coachList,
'education_list' => $educationList,
'assistant_list' => $assistantList,
'all_personnel' => $personnelList // 返回所有人员数据,前端可以根据需要自行分类
]
];
} catch (\Exception $e) {
$res['msg'] = '获取教练列表异常:' . $e->getMessage();
}
return $res;
}
} }

268
start.sh

@ -0,0 +1,268 @@
#!/bin/bash
# 颜色输出函数
print_message() {
echo -e "\033[1;32m[INFO] $1\033[0m"
}
print_warning() {
echo -e "\033[1;33m[WARNING] $1\033[0m"
}
print_error() {
echo -e "\033[1;31m[ERROR] $1\033[0m"
}
# 设置项目名称
PROJECT_NAME="MyApp"
# 检查Docker和Docker Compose是否安装
check_docker() {
if ! command -v docker &> /dev/null; then
print_error "Docker未安装,请先安装Docker"
exit 1
fi
if ! command -v docker-compose &> /dev/null; then
print_error "Docker Compose未安装,请先安装Docker Compose"
exit 1
fi
}
# 创建必要的目录结构
create_directories() {
print_message "创建必要的目录结构..."
mkdir -p docker/{logs/{nginx,mysql},data/{mysql,redis,node_modules},nginx/conf.d,php,mysql,redis}
}
# 检查并生成配置文件
check_config_files() {
print_message "检查必要的配置文件..."
# Nginx 主配置
if [ ! -f "docker/nginx/nginx.conf" ]; then
print_warning "Nginx主配置文件不存在,创建默认配置..."
cat > docker/nginx/nginx.conf << 'EOF'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
client_max_body_size 20M;
include /etc/nginx/conf.d/*.conf;
}
EOF
fi
# Nginx 站点配置
if [ ! -f "docker/nginx/conf.d/default.conf" ]; then
print_warning "Nginx站点配置文件不存在,创建默认配置..."
cat > docker/nginx/conf.d/default.conf << 'EOF'
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
server {
listen 8080;
server_name localhost;
root /var/www/admin;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
EOF
fi
# PHP 配置
if [ ! -f "docker/php/php.ini" ]; then
print_warning "PHP配置文件不存在,创建默认配置..."
cat > docker/php/php.ini << 'EOF'
[PHP]
memory_limit = 256M
upload_max_filesize = 20M
post_max_size = 20M
max_execution_time = 300
date.timezone = Asia/Shanghai
[opcache]
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
EOF
fi
# PHP-FPM 配置
if [ ! -f "docker/php/www.conf" ]; then
print_warning "PHP-FPM配置文件不存在,创建默认配置..."
cat > docker/php/www.conf << 'EOF'
[www]
user = www-data
group = www-data
listen = 9000
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
EOF
fi
# MySQL 配置
if [ ! -f "docker/mysql/my.cnf" ]; then
print_warning "MySQL配置文件不存在,创建默认配置..."
cat > docker/mysql/my.cnf << 'EOF'
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
default-authentication-plugin = mysql_native_password
max_allowed_packet = 64M
sql_mode = ''
[client]
default-character-set = utf8mb4
EOF
fi
# Redis 配置
if [ ! -f "docker/redis/redis.conf" ]; then
print_warning "Redis配置文件不存在,创建默认配置..."
cat > docker/redis/redis.conf << 'EOF'
requirepass niucloud123456
appendonly yes
EOF
fi
}
# 启动服务
start_services() {
print_message "启动 ${PROJECT_NAME} 服务..."
docker-compose up -d
}
# 停止服务
stop_services() {
print_message "停止 ${PROJECT_NAME} 服务..."
docker-compose down
}
# 重启服务
restart_services() {
print_message "重启 ${PROJECT_NAME} 服务..."
docker-compose restart
}
# 查看状态
check_status() {
print_message "${PROJECT_NAME} 服务状态:"
docker-compose ps
}
# 查看日志
view_logs() {
service=$1
if [ -z "$service" ]; then
print_message "查看所有服务日志..."
docker-compose logs
else
print_message "查看 $service 服务日志..."
docker-compose logs $service
fi
}
# 初始化项目
init_project() {
print_message "初始化 ${PROJECT_NAME} 项目..."
# 检查是否存在 docker-compose.yml
if [ ! -f "docker-compose.yml" ]; then
print_error "缺少 docker-compose.yml 文件,请先创建!"
exit 1
fi
create_directories
check_config_files
print_message "拉取Docker镜像..."
docker-compose pull
print_message "项目初始化完成,现在可以启动服务了"
}
# 显示帮助信息
show_help() {
echo "用法: $0 [选项]"
echo ""
echo "选项:"
echo " start 启动所有服务"
echo " stop 停止所有服务"
echo " restart 重启所有服务"
echo " status 查看服务状态"
echo " logs 查看所有服务日志"
echo " logs <服务名> 查看指定服务日志"
echo " init 初始化项目"
echo " help 显示此帮助信息"
}
# 主程序入口
main() {
check_docker
case "$1" in
start)
start_services
;;
stop)
stop_services
;;
restart)
restart_services
;;
status)
check_status
;;
logs)
view_logs "$2"
;;
init)
init_project
;;
help|*)
show_help
;;
esac
}
main "$@"

15
uniapp/api/apiRoute.js

@ -33,6 +33,10 @@ export default {
async common_getPersonnelAll(data = {}) { async common_getPersonnelAll(data = {}) {
return await http.get('/personnel/getPersonnelAll', data); return await http.get('/personnel/getPersonnelAll', data);
}, },
//公共端-获取教练数据列表
async common_getCoachList(data = {}) {
return await http.get('/personnel/getCoachList', data);
},
//公共端-获取全部课程列表 //公共端-获取全部课程列表
async common_getCourseAll(data = {}) { async common_getCourseAll(data = {}) {
return await http.get('/common/getCourseAll', data); return await http.get('/common/getCourseAll', data);
@ -206,6 +210,10 @@ export default {
async xs_communicationRecordsAdd(data = {}) { async xs_communicationRecordsAdd(data = {}) {
return await http.post('/communicationRecords/add', data); return await http.post('/communicationRecords/add', data);
}, },
//销售端-沟通记录-编辑
async xs_communicationRecordsEdit(data = {}) {
return await http.post('/communicationRecords/edit', data);
},
//销售端-获取好友关系绑定详情 //销售端-获取好友关系绑定详情
async xs_chatGetChatFriendsInfo(data = {}) { async xs_chatGetChatFriendsInfo(data = {}) {
return await http.get('/chat/getChatFriendsInfo', data); return await http.get('/chat/getChatFriendsInfo', data);
@ -387,7 +395,10 @@ export default {
//更新学生课程人员配置 //更新学生课程人员配置
async updateStudentCoursePersonnel(data = {}) { async updateStudentCoursePersonnel(data = {}) {
return await http.post('/updateStudentCoursePersonnel', data); return await http.post('/updateStudentCoursePersonnel', data);
} },
// 获取订单支付二维码
async getOrderPayQrcode(data = {}) {
return await http.get('/getQrcode', data);
},
} }

109
uniapp/common/util.js

@ -226,63 +226,102 @@ function loginOut() {
}) })
} }
// 全局内存字典缓存
// 使用模块级的变量保存缓存,在整个应用生命周期内都有效
const dictMemoryCache = {};
/** /**
* 获取字典值带1小时缓存 * 获取字典值内存和存储双重缓存
* @param {String} dictKey 字典key * @param {String} dictKey 字典key
* @returns {Promise<Array|Object>} * @returns {Promise<Array|Object>}
*/ */
async function getDict(dictKey) { async function getDict(dictKey) {
console.log(`util.getDict - 开始获取字典: ${dictKey}`); console.log(`getDict - 开始获取字典: ${dictKey}`);
const cacheKey = `dict_${dictKey}`; // 先从内存缓存中获取
const cache = uni.getStorageSync(cacheKey);
const now = Date.now(); const now = Date.now();
if (dictMemoryCache[dictKey] && dictMemoryCache[dictKey].expire > now) {
console.log(`getDict - 使用内存缓存: ${dictKey}`);
return dictMemoryCache[dictKey].data;
}
// 检查缓存是否有效 // 内存缓存不存在或已过期,尝试从存储中获取
if (cache && cache.data && cache.expire > now) { const cacheKey = `dict_${dictKey}`;
console.log(`util.getDict - 使用缓存数据: ${dictKey}, 数据条数: ${cache.data.length}`); try {
return cache.data; const storageCache = uni.getStorageSync(cacheKey);
if (storageCache && storageCache.data && storageCache.expire > now) {
console.log(`getDict - 使用存储缓存: ${dictKey}`);
// 更新内存缓存
dictMemoryCache[dictKey] = storageCache;
return storageCache.data;
}
} catch (e) {
console.error(`getDict - 读取存储缓存异常: ${dictKey}`, e);
} }
console.log(`util.getDict - 缓存无效,请求接口: ${dictKey}`); console.log(`getDict - 缓存无效,请求接口: ${dictKey}`);
// 缓存无效,请求接口 // 缓存不存在或已过期,从服务器获取
try { try {
console.log(`util.getDict - 调用API: common_Dictionary, key=${dictKey}`); // 添加超时处理和重试机制
let retryCount = 0;
// 添加超时处理 const maxRetries = 2;
const timeoutPromise = new Promise((_, reject) => { const fetchWithRetry = async () => {
setTimeout(() => reject(new Error(`字典加载超时: ${dictKey}`)), 3000); try {
}); // 超时控制
const timeoutPromise = new Promise((_, reject) => {
// 实际API请求 setTimeout(() => reject(new Error(`字典请求超时: ${dictKey}`)), 3000);
const apiPromise = marketApi.common_Dictionary({ key: dictKey }); });
// 使用Promise.race来实现超时控制 // 实际API请求
const res = await Promise.race([apiPromise, timeoutPromise]); const apiPromise = marketApi.common_Dictionary({ key: dictKey });
// 使用Promise.race来实现超时控制
return await Promise.race([apiPromise, timeoutPromise]);
} catch (error) {
if (retryCount < maxRetries) {
retryCount++;
console.log(`getDict - 重试第${retryCount}次: ${dictKey}`);
return await fetchWithRetry();
}
throw error;
}
};
console.log(`util.getDict - API响应:`, res); const res = await fetchWithRetry();
if (res && res.code === 1) { if (res && res.code === 1) {
// 处理接口返回的数据,确保返回格式一致 // 处理接口返回的数据
// 接口返回的是 [{name: "抖音", value: "1", sort: 0, memo: ""}, ...]
const formattedData = Array.isArray(res.data) ? res.data : []; const formattedData = Array.isArray(res.data) ? res.data : [];
console.log(`util.getDict - 保存缓存: ${dictKey}, 数据条数: ${formattedData.length}`); if (formattedData.length > 0) {
uni.setStorageSync(cacheKey, { // 设置缓存过期时间,默认1小时
data: formattedData, const cacheData = {
expire: now + 3600 * 1000 data: formattedData,
}); expire: now + 3600 * 1000
console.log(`util.getDict - 字典获取成功: ${dictKey}`); };
// 同时更新内存缓存和存储缓存
dictMemoryCache[dictKey] = cacheData;
try {
uni.setStorageSync(cacheKey, cacheData);
} catch (e) {
console.error(`getDict - 存储缓存异常: ${dictKey}`, e);
}
console.log(`getDict - 字典获取成功: ${dictKey}, 条数: ${formattedData.length}`);
} else {
console.warn(`getDict - 字典数据为空: ${dictKey}`);
}
return formattedData; return formattedData;
} else { } else {
console.error(`util.getDict - API请求失败: ${dictKey}`, res); console.error(`getDict - API请求失败: ${dictKey}`, res);
// 返回空数组而不是抛出异常,避免阻塞流程
return []; return [];
} }
} catch (error) { } catch (error) {
console.error(`util.getDict - 异常: ${dictKey}`, error); console.error(`getDict - 异常: ${dictKey}`, error);
// 返回空数组,避免阻塞流程
return []; return [];
} }
} }

2
uniapp/components/AQ/AQTabber.vue

@ -142,7 +142,7 @@
}, },
{ {
text: "数据", text: "数据",
urlPath: '/pages/market/data/statistics', // urlPath: '/pages/market/clue/clue_table', //
iconPath: util.img('/uniapp_src/static/images/tabbar/timetable.png'), iconPath: util.img('/uniapp_src/static/images/tabbar/timetable.png'),
selectedIconPath: util.img('/uniapp_src/static/images/tabbar/timetables.png'), selectedIconPath: util.img('/uniapp_src/static/images/tabbar/timetables.png'),
}, },

70
uniapp/components/custom-modal/custom-modal.vue

@ -0,0 +1,70 @@
<template>
<fui-modal :buttons="[]" :width="width" :show="show" :maskClose="maskClose">
<!-- 默认内容插槽 -->
<slot>
<!-- 默认内容 -->
</slot>
<!-- 按钮插槽 -->
<view v-if="$slots.buttons" class="button-section">
<slot name="buttons"></slot>
</view>
<!-- 关闭按钮 -->
<view v-if="showClose" class="fui-icon__close" @tap="handleClose">
<fui-icon name="close" color="#B2B2B2" :size="48"></fui-icon>
</view>
</fui-modal>
</template>
<script>
export default {
name: 'CustomModal',
props: {
//
show: {
type: Boolean,
default: false
},
//
width: {
type: [String, Number],
default: 600
},
//
showClose: {
type: Boolean,
default: true
},
//
maskClose: {
type: Boolean,
default: true
}
},
methods: {
//
handleClose() {
this.$emit('cancel', 'close')
}
}
}
</script>
<style lang="less" scoped>
.button-section {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 20rpx;
margin-top: 40rpx;
}
.fui-icon__close {
position: absolute;
right: 24rpx;
top: 20rpx;
z-index: 1000;
}
</style>

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

@ -0,0 +1,234 @@
<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>

10
uniapp/pages.json

@ -177,7 +177,7 @@
{ {
"path": "pages/common/im_chat_info", "path": "pages/common/im_chat_info",
"style": { "style": {
"navigationBarTitleText": "", "navigationBarTitleText": "消息",
"navigationStyle": "default", "navigationStyle": "default",
"navigationBarBackgroundColor": "#292929", "navigationBarBackgroundColor": "#292929",
"navigationBarTextStyle": "white" "navigationBarTextStyle": "white"
@ -673,6 +673,14 @@
"navigationBarBackgroundColor": "#007ACC", "navigationBarBackgroundColor": "#007ACC",
"navigationBarTextStyle": "white" "navigationBarTextStyle": "white"
} }
},
{
"path": "pages/market/clue/clue_table",
"style": {
"navigationBarTitleText": "数据统计",
"navigationBarBackgroundColor": "#292929",
"navigationBarTextStyle": "white"
}
} }

147
uniapp/pages/coach/course/info_list.vue

@ -146,11 +146,32 @@
</view> </view>
</view> </view>
<fui-picker :options="StudentList" :linkage="true" :show="show" :layer="1" @change="change" <fui-modal :show="show" title="选择学员" @click="handleModalClick" :buttons="[]" @cancel="cancel">
<view class="student-list">
<view class="student-item" v-for="(item, index) in StudentList" :key="index" @click="change(item)">
<view class="student-info">
<view class="student-name">{{item.text}}</view>
<view class="student-details">
<text>年龄: {{item.age || '未知'}}</text>
<text>来源: {{item.source || '未知'}}</text>
<text>渠道: {{item.source_channel || '未知'}}</text>
</view>
</view>
</view>
<view class="empty-list" v-if="StudentList.length === 0">
<text>暂无可添加的学员</text>
</view>
</view>
</fui-modal>
<fui-picker :options="StudentList" :linkage="false" :show="false" :layer="1" @change="change"
@cancel="cancel"></fui-picker> @cancel="cancel"></fui-picker>
<fui-actionsheet :show="written_show" :tips="written_tips" :isCancel="isCancel" <fui-actionsheet :show="written_show" :tips="written_tips" :isCancel="isCancel"
:itemList="itemList" @cancel="written_cancel" @click="written_click"></fui-actionsheet> :itemList="itemList" @cancel="written_cancel" @click="written_click"></fui-actionsheet>
<uni-popup ref="popup" type="center">
<view class="popup-content">{{ tipContent }}</view>
</uni-popup>
<AQTabber />
</view> </view>
</template> </template>
@ -315,41 +336,27 @@
}) })
return return
} else { } else {
//
this.show = true this.show = true
} }
// 使
// this.StudentList = [
// {
// text: '',
// value: '1001'
// },
// {
// text: '',
// value: '1002'
// },
// {
// text: '',
// value: '1003'
// },
// {
// text: '',
// value: '1004'
// },
// {
// text: '',
// value: '1005'
// }
// ];
// this.show = true;
}, },
async change(e) { //
handleModalClick(e) {
//
console.log('模态框点击事件', e)
},
async change(student) {
this.show = false this.show = false
// student_idresource_id
let studentId = student.value
let resourceId = student.resource_id
let res = await apiRoute.addStudent({ let res = await apiRoute.addStudent({
student_id: e.value, student_id: studentId,
schedule_id: this.course_id, schedule_id: this.course_id,
time_slot: this.courseInfo.time_slot, time_slot: this.courseInfo.time_slot,
course_date: this.courseInfo.course_date course_date: this.courseInfo.course_date,
resource_id: resourceId // resource_id
}) })
if (res.code != 1) { if (res.code != 1) {
uni.showToast({ uni.showToast({
@ -358,36 +365,14 @@
}) })
return return
} }
this.init()
// 使 uni.showToast({
// const selectedStudent = this.StudentList.find(student => student.value === e.value); title: '添加学员成功',
// if (!selectedStudent) return; icon: 'success'
})
// //
// if (!this.courseInfo.student_courses) {
// this.courseInfo.student_courses = [];
// }
// //
// const now = new Date();
// const futureDate = new Date();
// futureDate.setMonth(futureDate.getMonth() + 3); // 3
// //
// this.courseInfo.student_courses.push({
// student_id: e.value,
// name: selectedStudent.text,
// avatar: '', //
// start_date: now.toISOString().split('T')[0],
// end_date: futureDate.toISOString().split('T')[0],
// status: 'pending' //
// });
// uni.showToast({ //
// title: '', this.init()
// icon: 'success'
// });
}, },
cancel() { cancel() {
this.show = false this.show = false
@ -495,6 +480,54 @@
color: #fff; color: #fff;
} }
} }
/* 学员选择列表样式 */
.student-list {
max-height: 600rpx;
overflow-y: auto;
}
.student-item {
padding: 20rpx 30rpx;
border-bottom: 1px solid #eee;
background-color: #f8f8f8;
margin-bottom: 10rpx;
border-radius: 8rpx;
}
.student-item:active {
background-color: #e0e0e0;
}
.student-info {
display: flex;
flex-direction: column;
}
.student-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.student-details {
display: flex;
flex-direction: column;
font-size: 24rpx;
color: #666;
}
.student-details text {
margin-bottom: 6rpx;
}
.empty-list {
padding: 40rpx;
text-align: center;
color: #999;
font-size: 28rpx;
}
.main_section { .main_section {
min-height: 100vh; min-height: 100vh;

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

@ -194,7 +194,7 @@
size="mini" size="mini"
class="remark-btn" class="remark-btn"
@click="openAddRemark(v)"> @click="openAddRemark(v)">
添加备注 {{v.remarks ? '修改备注' : '添加备注'}}
</button> </button>
</view> </view>
<view class="call-remark"> <view class="call-remark">
@ -216,21 +216,26 @@
<!-- 添加备注弹窗 --> <!-- 添加备注弹窗 -->
<uni-popup ref="remarkPopup" type="dialog"> <uni-popup ref="remarkPopup" type="dialog">
<uni-popup-dialog <view class="custom-popup-dialog">
:title="currentRecord ? `添加备注 (${currentRecord.name || '未知'})` : '添加备注'" <view class="dialog-header">
:before-close="true" <text class="dialog-title">{{currentRecord && currentRecord.remarks ? '修改备注' : '添加备注'}}</text>
@confirm="confirmRemark"
@close="closeRemark">
<view class="remark-textarea-container">
<textarea
class="remark-textarea"
v-model="remark_content"
placeholder="请输入备注内容"
maxlength="200"
></textarea>
<text class="remark-count">{{remark_content.length}}/200</text>
</view> </view>
</uni-popup-dialog> <view class="dialog-content">
<view class="remark-textarea-container">
<textarea
class="remark-textarea"
v-model="remark_content"
placeholder="请输入备注内容"
maxlength="200"
></textarea>
<text class="remark-count">{{remark_content.length}}/200</text>
</view>
</view>
<view class="dialog-footer">
<view class="dialog-btn cancel-btn" @click="closeRemark">取消</view>
<view class="dialog-btn confirm-btn" @click="confirmRemark">确定</view>
</view>
</view>
</uni-popup> </uni-popup>
<!-- 教练配置编辑弹窗 --> <!-- 教练配置编辑弹窗 -->
@ -243,45 +248,48 @@
<view class="course-edit-container"> <view class="course-edit-container">
<view class="edit-section"> <view class="edit-section">
<view class="section-title">主教练单选</view> <view class="section-title">主教练单选</view>
<view v-if="coachList.length === 0" class="empty-tip">暂无主教练数据</view>
<view class="coach-list"> <view class="coach-list">
<view <view
v-for="coach in coachList" v-for="coach in coachList"
:key="coach.id" :key="coach.person_id"
class="coach-item" class="coach-item"
:class="{selected: selectedMainCoach === coach.id}" :class="{selected: selectedMainCoach === coach.person_id}"
@click="selectMainCoach(coach.id)"> @click="selectMainCoach(coach.person_id)">
<view class="coach-name">{{ coach.name }}</view> <view class="coach-name">{{ coach.name }}</view>
<view class="coach-check" v-if="selectedMainCoach === coach.id"></view> <view class="coach-check" v-if="selectedMainCoach === coach.person_id"></view>
</view> </view>
</view> </view>
</view> </view>
<view class="edit-section"> <view class="edit-section">
<view class="section-title">教务单选</view> <view class="section-title">教务单选</view>
<view v-if="educationList.length === 0" class="empty-tip">暂无教务数据</view>
<view class="coach-list"> <view class="coach-list">
<view <view
v-for="education in educationList" v-for="education in educationList"
:key="education.id" :key="education.person_id"
class="coach-item" class="coach-item"
:class="{selected: selectedEducation === education.id}" :class="{selected: selectedEducation === education.person_id}"
@click="selectEducation(education.id)"> @click="selectEducation(education.person_id)">
<view class="coach-name">{{ education.name }}</view> <view class="coach-name">{{ education.name }}</view>
<view class="coach-check" v-if="selectedEducation === education.id"></view> <view class="coach-check" v-if="selectedEducation === education.person_id"></view>
</view> </view>
</view> </view>
</view> </view>
<view class="edit-section"> <view class="edit-section">
<view class="section-title">助教多选</view> <view class="section-title">助教多选</view>
<view v-if="assistantList.length === 0" class="empty-tip">暂无助教数据</view>
<view class="coach-list"> <view class="coach-list">
<view <view
v-for="assistant in assistantList" v-for="assistant in assistantList"
:key="assistant.id" :key="assistant.person_id"
class="coach-item" class="coach-item"
:class="{selected: selectedAssistants.includes(assistant.id)}" :class="{selected: selectedAssistants.includes(assistant.person_id)}"
@click="toggleAssistant(assistant.id)"> @click="toggleAssistant(assistant.person_id)">
<view class="coach-name">{{ assistant.name }}</view> <view class="coach-name">{{ assistant.name }}</view>
<view class="coach-check" v-if="selectedAssistants.includes(assistant.id)"></view> <view class="coach-check" v-if="selectedAssistants.includes(assistant.person_id)"></view>
</view> </view>
</view> </view>
</view> </view>
@ -304,7 +312,6 @@
components: { components: {
// //
uniPopup: () => import('@/components/uni-popup/uni-popup.vue'), uniPopup: () => import('@/components/uni-popup/uni-popup.vue'),
uniPopupDialog: () => import('@/components/uni-popup/uni-popup-dialog.vue'),
}, },
data() { data() {
return { return {
@ -617,7 +624,7 @@
// //
openAddRemark(record) { openAddRemark(record) {
this.remark_content = ''; this.remark_content = record.remarks || ''; //
this.currentRecord = record; this.currentRecord = record;
this.$refs.remarkPopup.open(); this.$refs.remarkPopup.open();
}, },
@ -632,55 +639,57 @@
return; return;
} }
//
if (!this.currentRecord || !this.currentRecord.id) {
uni.showToast({
title: '未选中通话记录',
icon: 'none'
});
return;
}
try { try {
uni.showLoading({ uni.showLoading({
title: '提交中...', title: '提交中...',
mask: true mask: true
}); });
// 使
const params = { const params = {
staff_id: this.userInfo.id, // id id: this.currentRecord.id, // ID
resource_id: this.clientInfo.resource_id, // ID remarks: this.remark_content //
resource_type: '', //
communication_type: 'note', // : phone-, note-
communication_result: 'success', //
remarks: this.remark_content, //
tag: null //
}; };
// ID console.log('更新通话记录备注:', params);
if (this.currentRecord && this.currentRecord.id) {
params.related_record_id = this.currentRecord.id; // ID
console.log('添加备注到通话记录:', this.currentRecord);
}
const res = await apiRoute.xs_communicationRecordsAdd(params); //
const res = await apiRoute.xs_communicationRecordsEdit(params);
if (res.code !== 1) { if (res.code !== 1) {
uni.showToast({ uni.showToast({
title: res.msg || '添加备注失败', title: res.msg || '更新备注失败',
icon: 'none' icon: 'none'
}); });
return; return;
} }
uni.showToast({ uni.showToast({
title: '添加备注成功', title: '备注更新成功',
icon: 'success' icon: 'success'
}); });
// //
this.getListCallUp(); await this.getListCallUp();
} catch (error) { } catch (error) {
console.error('添加备注失败:', error); console.error('更新备注失败:', error);
uni.showToast({ uni.showToast({
title: '添加备注失败,请重试', title: '更新备注失败,请重试',
icon: 'none' icon: 'none'
}); });
} finally { } finally {
uni.hideLoading(); uni.hideLoading();
this.$refs.remarkPopup.close(); this.closeRemark();
} }
}, },
@ -710,13 +719,12 @@
}); });
return; return;
} }
let member_id = this.clientInfo.customerResource.member_id; let from_id = uni.getStorageSync('userInfo').id //id
let member_name = this.clientInfo.customerResource.name || ''; let to_id = this.clientInfo.customerResource.member_id //ID
this.$navigateTo({ this.$navigateTo({
url: `/pages/market/clue/chat?member_id=${member_id}&member_name=${member_name}` url: `/pages/common/im_chat_info?from_id=${from_id}&to_id=${to_id}`
}); })
}, },
// //
@ -817,61 +825,67 @@
// //
async getPersonnelList() { async getPersonnelList() {
try { try {
// 使API console.log('getPersonnelList - 开始获取教练数据');
try {
const res = await apiRoute.getPersonnelList(); uni.showLoading({
if (res.code === 1 && res.data) { title: '获取教练数据...'
const personnel = res.data || []; });
// - const res = await apiRoute.common_getCoachList();
this.coachList = personnel.filter(p => { console.log('getPersonnelList - API响应:', res);
return p.role_name && (
p.role_name.includes('教练') || uni.hideLoading();
p.role_name.includes('教师') ||
(p.roles && p.roles.some(role => role.includes('教练') || role.includes('教师'))) if (res.code === 1 && res.data) {
); // 使
}); this.coachList = res.data.coach_list || [];
this.educationList = res.data.education_list || [];
this.educationList = personnel.filter(p => { this.assistantList = res.data.assistant_list || [];
return p.role_name && (
p.role_name.includes('教务') || console.log('getPersonnelList - 教练数据获取成功');
(p.roles && p.roles.some(role => role.includes('教务'))) console.log('教练列表:', this.coachList);
); console.log('教务列表:', this.educationList);
}); console.log('助教列表:', this.assistantList);
return true;
this.assistantList = personnel.filter(p => { } else {
return p.role_name && ( console.warn('API返回错误:', res);
p.role_name.includes('助教') ||
(p.roles && p.roles.some(role => role.includes('助教'))) // API
); uni.showToast({
}); title: res.msg || '获取教练数据失败',
icon: 'none'
console.log('getPersonnelList - 人员列表获取成功'); });
console.log('教练列表:', this.coachList);
console.log('教务列表:', this.educationList);
console.log('助教列表:', this.assistantList);
return true;
} else {
console.warn('API返回错误:', res.msg);
throw new Error(res.msg);
}
} catch (apiError) {
console.warn('使用API获取人员列表失败,使用模拟数据:', apiError);
// API使 // API使
this.coachList = this.getMockCoachList(); this.coachList = this.getMockCoachList();
this.educationList = this.getMockEducationList(); this.educationList = this.getMockEducationList();
this.assistantList = this.getMockAssistantList(); this.assistantList = this.getMockAssistantList();
console.log('getPersonnelList - 使用模拟人员数据'); console.log('getPersonnelList - 使用模拟教练数据');
console.log('模拟教练列表:', this.coachList);
console.log('模拟教务列表:', this.educationList);
console.log('模拟助教列表:', this.assistantList);
return true; return true;
} }
} catch (error) { } catch (error) {
console.error('getPersonnelList - 获取人员列表异常:', error); console.error('getPersonnelList - 获取教练列表异常:', error);
uni.hideLoading();
//
uni.showToast({
title: '网络错误,使用模拟数据',
icon: 'none'
});
// //
this.coachList = this.getMockCoachList(); this.coachList = this.getMockCoachList();
this.educationList = this.getMockEducationList(); this.educationList = this.getMockEducationList();
this.assistantList = this.getMockAssistantList(); this.assistantList = this.getMockAssistantList();
console.log('getPersonnelList - 使用模拟教练数据(异常情况)');
console.log('模拟教练列表:', this.coachList);
console.log('模拟教务列表:', this.educationList);
console.log('模拟助教列表:', this.assistantList);
return false; return false;
} }
}, },
@ -879,60 +893,86 @@
// //
getMockCoachList() { getMockCoachList() {
return [ return [
{ id: 1, name: '张教练' }, { id: 1, person_id: 1, name: '张教练', head_img: '', phone: '' },
{ id: 5, name: '陈教练' }, { id: 5, person_id: 5, name: '陈教练', head_img: '', phone: '' },
{ id: 7, name: '刘教练' } { id: 7, person_id: 7, name: '刘教练', head_img: '', phone: '' }
]; ];
}, },
// //
getMockEducationList() { getMockEducationList() {
return [ return [
{ id: 2, name: '李教务' }, { id: 2, person_id: 2, name: '李教务', head_img: '', phone: '' },
{ id: 8, name: '周教务' } { id: 8, person_id: 8, name: '周教务', head_img: '', phone: '' }
]; ];
}, },
// //
getMockAssistantList() { getMockAssistantList() {
return [ return [
{ id: 3, name: '王助教' }, { id: 3, person_id: 3, name: '王助教', head_img: '', phone: '' },
{ id: 4, name: '赵助教' }, { id: 4, person_id: 4, name: '赵助教', head_img: '', phone: '' },
{ id: 6, name: '孙助教' }, { id: 6, person_id: 6, name: '孙助教', head_img: '', phone: '' },
{ id: 9, name: '钱助教' } { id: 9, person_id: 9, name: '钱助教', head_img: '', phone: '' }
]; ];
}, },
// //
openCourseEditDialog(course) { async openCourseEditDialog(course) {
this.currentCourse = course; this.currentCourse = course;
// //
console.log('打开教练配置弹窗,检查教练数据:', {
coachList: this.coachList,
educationList: this.educationList,
assistantList: this.assistantList
});
//
if (!this.coachList.length && !this.educationList.length && !this.assistantList.length) {
console.log('教练数据为空,重新获取...');
await this.getPersonnelList();
}
// 使person_id
this.selectedMainCoach = course.main_coach_id; this.selectedMainCoach = course.main_coach_id;
this.selectedEducation = course.education_id; this.selectedEducation = course.education_id;
this.selectedAssistants = course.assistant_ids ? course.assistant_ids.split(',').map(Number) : []; this.selectedAssistants = course.assistant_ids ? course.assistant_ids.split(',').map(Number) : [];
console.log('打开教练配置弹窗:', {
course: course,
selectedMainCoach: this.selectedMainCoach,
selectedEducation: this.selectedEducation,
selectedAssistants: this.selectedAssistants,
coachListLength: this.coachList.length,
educationListLength: this.educationList.length,
assistantListLength: this.assistantList.length
});
this.$refs.courseEditPopup.open(); this.$refs.courseEditPopup.open();
}, },
// //
selectMainCoach(coachId) { selectMainCoach(personId) {
this.selectedMainCoach = coachId; this.selectedMainCoach = personId;
console.log('选择主教练:', personId);
}, },
// //
selectEducation(educationId) { selectEducation(personId) {
this.selectedEducation = educationId; this.selectedEducation = personId;
console.log('选择教务:', personId);
}, },
// //
toggleAssistant(assistantId) { toggleAssistant(personId) {
const index = this.selectedAssistants.indexOf(assistantId); const index = this.selectedAssistants.indexOf(personId);
if (index > -1) { if (index > -1) {
this.selectedAssistants.splice(index, 1); this.selectedAssistants.splice(index, 1);
} else { } else {
this.selectedAssistants.push(assistantId); this.selectedAssistants.push(personId);
} }
console.log('切换助教选择:', personId, '当前选中:', this.selectedAssistants);
}, },
// //
@ -945,9 +985,9 @@
const params = { const params = {
student_course_id: this.currentCourse.id, student_course_id: this.currentCourse.id,
main_coach_id: this.selectedMainCoach, main_coach_id: this.selectedMainCoach, // 使person_id
education_id: this.selectedEducation, education_id: this.selectedEducation, // 使person_id
assistant_ids: this.selectedAssistants.join(',') assistant_ids: this.selectedAssistants.join(',') // 使person_id
}; };
console.log('准备保存教练配置:', params); console.log('准备保存教练配置:', params);
@ -978,22 +1018,22 @@
const courseIndex = this.courseInfo.findIndex(c => c.id === this.currentCourse.id); const courseIndex = this.courseInfo.findIndex(c => c.id === this.currentCourse.id);
if (courseIndex > -1) { if (courseIndex > -1) {
// //
const mainCoach = this.coachList.find(c => c.id === this.selectedMainCoach); const mainCoach = this.coachList.find(c => c.person_id === this.selectedMainCoach);
if (mainCoach) { if (mainCoach) {
this.courseInfo[courseIndex].main_coach_id = this.selectedMainCoach; this.courseInfo[courseIndex].main_coach_id = this.selectedMainCoach;
this.courseInfo[courseIndex].main_coach_name = mainCoach.name; this.courseInfo[courseIndex].main_coach_name = mainCoach.name;
} }
// //
const education = this.educationList.find(e => e.id === this.selectedEducation); const education = this.educationList.find(e => e.person_id === this.selectedEducation);
if (education) { if (education) {
this.courseInfo[courseIndex].education_id = this.selectedEducation; this.courseInfo[courseIndex].education_id = this.selectedEducation;
this.courseInfo[courseIndex].education_name = education.name; this.courseInfo[courseIndex].education_name = education.name;
} }
// //
const assistantNames = this.selectedAssistants.map(id => { const assistantNames = this.selectedAssistants.map(personId => {
const assistant = this.assistantList.find(a => a.id === id); const assistant = this.assistantList.find(a => a.person_id === personId);
return assistant ? assistant.name : ''; return assistant ? assistant.name : '';
}).filter(name => name).join(', '); }).filter(name => name).join(', ');
@ -1227,6 +1267,52 @@
padding: 0 30rpx; padding: 0 30rpx;
} }
//
.custom-popup-dialog {
width: 650rpx;
border-radius: 16rpx;
background-color: #fff;
overflow: hidden;
}
.dialog-header {
padding: 30rpx;
text-align: center;
border-bottom: 1px solid #eee;
}
.dialog-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.dialog-content {
padding: 30rpx;
}
.dialog-footer {
display: flex;
border-top: 1px solid #eee;
}
.dialog-btn {
flex: 1;
padding: 25rpx 0;
text-align: center;
font-size: 30rpx;
}
.cancel-btn {
color: #666;
border-right: 1px solid #eee;
}
.confirm-btn {
color: #29D3B4;
font-weight: bold;
}
// //
.remark-textarea-container { .remark-textarea-container {
width: 100%; width: 100%;
@ -1236,7 +1322,7 @@
.remark-textarea { .remark-textarea {
width: 100%; width: 100%;
height: 200rpx; height: 240rpx;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 8rpx; border-radius: 8rpx;
padding: 20rpx; padding: 20rpx;
@ -1705,4 +1791,11 @@
width: 40rpx; width: 40rpx;
text-align: center; text-align: center;
} }
.empty-tip {
color: #999;
font-size: 28rpx;
text-align: center;
padding: 40rpx 0;
}
</style> </style>

164
uniapp/pages/market/clue/clue_table.vue

@ -0,0 +1,164 @@
<template>
<view class="dark-table-container">
<view v-for="(row, rIdx) in data" :key="rIdx" class="card">
<view class="card-title" @click="toggleExpand(rIdx)">
{{ row.channel }}
<text class="expand-icon">{{ expanded[rIdx] ? '▲' : '▼' }}</text>
</view>
<view class="card-content">
<view
class="card-row"
v-for="(col, cIdx) in getVisibleColumns(rIdx)"
:key="cIdx"
>
<view class="card-label">
{{ col.label }}
<text v-if="col.tip" class="tip" @click.stop="showTip(col.tip)">?</text>
</view>
<view class="card-value">{{ row[col.key] || '-' }}</view>
</view>
</view>
<view v-if="columns.length > 6" class="expand-btn" @click="toggleExpand(rIdx)">
{{ expanded[rIdx] ? '收起' : '展开全部' }}
</view>
</view>
<!-- 说明弹窗 -->
<uni-popup ref="popup" type="center">
<view class="popup-content">{{ tipContent }}</view>
</uni-popup>
<AQTabber></AQTabber>
</view>
</template>
<script>
import AQTabber from "@/components/AQ/AQTabber.vue"
export default {
components: {
AQTabber,
},
data() {
return {
columns: [
{ label: '渠道', key: 'channel' },
{ label: '资源数', key: 'resource', tip: '本月的新资源' },
{ label: '到访数(一访)', key: 'visit1', tip: '本月资源的一访到访数' },
{ label: '到访率(一访)', key: 'visitRate1', tip: '一访到访数/本月新资源数' },
{ label: '到访数(二访)', key: 'visit2', tip: '二访到访数/一访到访数' },
{ label: '到访率(二访)', key: 'visitRate2', tip: '二访到访数/一访到访数' },
{ label: '关单数(一访)', key: 'close1', tip: '本月一访到访的关单数' },
{ label: '关单率(一访)', key: 'closeRate1', tip: '一访关单数/一访到访数' },
{ label: '关单数(二访)', key: 'close2', tip: '二访到访的关单数' },
{ label: '关单率(二访)', key: 'closeRate2', tip: '二访关单数/二访到访数' },
{ label: '月总转', key: 'monthTrans', tip: '关单数(合计)/资源数' },
{ label: '往月到访数', key: 'lastMonthVisit', tip: '本月资源在往月到访的到访数' },
{ label: '月共到访', key: 'monthTotalVisit', tip: '本月资源任在本月到访/关单' },
{ label: '往月关单数', key: 'lastMonthClose', tip: '本月关单-往月关单' },
{ label: '月共关单', key: 'monthTotalClose', tip: '本月关单+往月关单' },
],
data: [
{ channel: '体检包(地推)', resource: 10, visit1: 5, visitRate1: '50%', visit2: 2, visitRate2: '40%', close1: 1, closeRate1: '20%', close2: 1, closeRate2: '50%', monthTrans: '10%', lastMonthVisit: 0, monthTotalVisit: 5, lastMonthClose: 0, monthTotalClose: 1 },
],
tipContent: '',
expanded: {},
}
},
methods: {
showTip(content) {
this.tipContent = content
this.$refs.popup.open()
},
toggleExpand(idx) {
this.$set(this.expanded, idx, !this.expanded[idx])
},
getVisibleColumns(idx) {
// 5
const cols = this.columns.slice(1)
if (this.expanded[idx]) return cols
return cols.slice(0, 5)
}
}
}
</script>
<style scoped>
.dark-table-container {
background: #18181c;
color: #f1f1f1;
min-height: 100vh;
padding: 24rpx 12rpx;
}
.card {
background: #23232a;
border-radius: 18rpx;
box-shadow: 0 4rpx 24rpx rgba(0,0,0,0.18);
margin-bottom: 32rpx;
padding: 24rpx 20rpx 8rpx 20rpx;
transition: box-shadow 0.2s;
}
.card-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 18rpx;
color: #ffb300;
letter-spacing: 2rpx;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.expand-icon {
font-size: 24rpx;
color: #bdbdbd;
margin-left: 12rpx;
}
.card-content {
display: flex;
flex-direction: column;
gap: 12rpx;
overflow: hidden;
transition: max-height 0.3s;
}
.card-row {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px dashed #333;
padding: 8rpx 0;
}
.card-label {
color: #bdbdbd;
font-size: 26rpx;
display: flex;
align-items: center;
}
.card-value {
color: #f1f1f1;
font-size: 26rpx;
text-align: right;
max-width: 60%;
word-break: break-all;
}
.tip {
color: #ffb300;
margin-left: 8rpx;
font-size: 24rpx;
cursor: pointer;
}
.expand-btn {
color: #ffb300;
text-align: center;
font-size: 26rpx;
margin: 10rpx 0 0 0;
padding-bottom: 8rpx;
cursor: pointer;
}
.popup-content {
background: #23232a;
color: #fff;
padding: 32rpx;
border-radius: 16rpx;
min-width: 300rpx;
text-align: center;
}
</style>

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

File diff suppressed because it is too large

15
uniapp/pages/student/login/login.vue

@ -72,14 +72,14 @@
value: '3', value: '3',
text: '销售登陆' text: '销售登陆'
}, },
{
value: '2',
text: '教务登陆'
},
{ {
value: '4', value: '4',
text: '学员登陆' text: '学员登陆'
}, },
{
value: '5',
text: '教务登陆'
}
], ],
picker_show_loginType: false, // picker_show_loginType: false, //
@ -161,8 +161,7 @@
// //
res = await apiRoute.xy_login(params); res = await apiRoute.xy_login(params);
} }
console.log('登录响应:', res);
if (res && res.code === 1) { // 1 if (res && res.code === 1) { // 1
// //
if (res.data && res.data.token) { if (res.data && res.data.token) {
@ -175,6 +174,10 @@
if (res.data.expires_time) { if (res.data.expires_time) {
uni.setStorageSync("expires_time", res.data.expires_time); uni.setStorageSync("expires_time", res.data.expires_time);
} }
//
if (res.data.userinfo){
uni.setStorageSync('userInfo',res.data.userinfo)
}
} }
uni.showToast({ uni.showToast({

Loading…
Cancel
Save