From 36bb6689c8ad9cc400781825a2ce4aed2512af79 Mon Sep 17 00:00:00 2001 From: zeyan <258785420@qq.com> Date: Tue, 22 Jul 2025 14:16:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(market):=20=E6=96=B0=E5=A2=9E=E5=BF=AB?= =?UTF-8?q?=E9=80=9F=E5=A1=AB=E5=86=99=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=BA=BF=E7=B4=A2=E6=B7=BB=E5=8A=A0=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增快速填写弹窗,支持粘贴文本解析客户信息 - 添加生日选择器和确认添加学员功能 - 优化表单布局和提示信息 - 更新相关API接口 --- .gitignore | 2 + .../campus_person_role/campus_person_role.vue | 3 +- .../components/campus-person-role-edit.vue | 6 - .../apiController/StudentManager.php | 98 + .../app/api/controller/login/UnifiedLogin.php | 162 ++ .../api/middleware/ApiPersonnelCheckToken.php | 2 +- niucloud/app/api/route/route.php | 9 +- .../api/apiService/ResourceSharingService.php | 95 +- .../service/api/apiService/StudentService.php | 163 ++ .../service/api/login/UnifiedLoginService.php | 420 ++++ uniapp/api/apiRoute.js | 28 +- .../custom-top-popup/custom-top-popup.vue | 73 + uniapp/pages.json | 38 + uniapp/pages/common/home/index.vue | 230 ++ uniapp/pages/common/profile/index.vue | 245 ++ uniapp/pages/market/clue/add_clues.vue | 451 +++- uniapp/pages/market/clue/clue_info.vue | 2029 +++++++++++++++-- uniapp/pages/market/clue/edit_clues.vue | 344 ++- uniapp/pages/market/clue/index.vue | 845 ++++++- uniapp/pages/student/login/login.vue | 149 +- uniapp/static/icon-img/home-active.png | Bin 0 -> 36567 bytes uniapp/static/icon-img/home.png | Bin 0 -> 36567 bytes uniapp/static/icon-img/profile-active.png | Bin 0 -> 36567 bytes uniapp/static/icon-img/profile.png | Bin 0 -> 36567 bytes 24 files changed, 5069 insertions(+), 323 deletions(-) create mode 100644 niucloud/app/api/controller/apiController/StudentManager.php create mode 100644 niucloud/app/api/controller/login/UnifiedLogin.php create mode 100644 niucloud/app/service/api/apiService/StudentService.php create mode 100644 niucloud/app/service/api/login/UnifiedLoginService.php create mode 100644 uniapp/components/custom-top-popup/custom-top-popup.vue create mode 100644 uniapp/pages/common/home/index.vue create mode 100644 uniapp/pages/common/profile/index.vue create mode 100644 uniapp/static/icon-img/home-active.png create mode 100644 uniapp/static/icon-img/home.png create mode 100644 uniapp/static/icon-img/profile-active.png create mode 100644 uniapp/static/icon-img/profile.png diff --git a/.gitignore b/.gitignore index 5dcf4e1d..b8fb0403 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,6 @@ node_modules examples PRPs INITIAL.md +CLAUDE.local.md + diff --git a/admin/src/app/views/campus_person_role/campus_person_role.vue b/admin/src/app/views/campus_person_role/campus_person_role.vue index 014211dd..2a7549b7 100644 --- a/admin/src/app/views/campus_person_role/campus_person_role.vue +++ b/admin/src/app/views/campus_person_role/campus_person_role.vue @@ -215,11 +215,10 @@ let campusPersonRoleTable = reactive({ }) -if(pageName == '市场人员列表'){ +if(pageName == '市场人员'){ campusPersonRoleTable.searchParam.dept_id = 1; }else if(pageName == '销售人员列表'){ campusPersonRoleTable.searchParam.dept_id = 3; - // campusPersonRoleTable.searchParam.role_id = 2; }else if(pageName == '教练管理'){ campusPersonRoleTable.searchParam.dept_id = 2; } diff --git a/admin/src/app/views/campus_person_role/components/campus-person-role-edit.vue b/admin/src/app/views/campus_person_role/components/campus-person-role-edit.vue index c856899e..2879691c 100644 --- a/admin/src/app/views/campus_person_role/components/campus-person-role-edit.vue +++ b/admin/src/app/views/campus_person_role/components/campus-person-role-edit.vue @@ -140,11 +140,6 @@ let showDialog = ref(false) const loading = ref(false) const pageName = route.meta.title -// const tasks = ref([ -// { month: '2025-03', value: '123', editable: false }, -// { month: '2025-04', value: '123', editable: false }, -// { month: '2025-05', value: '123', editable: true }, // 只最后一条可编辑 -// ]); /** * 表单数据 */ @@ -161,7 +156,6 @@ if(pageName == '市场人员列表'){ initialFormData.dept_id = 1; }else if(pageName == '销售人员列表'){ initialFormData.dept_id = 3; - // campusPersonRoleTable.searchParam.role_id = 2; }else if(pageName == '教练管理'){ initialFormData.dept_id = 2; }else{ diff --git a/niucloud/app/api/controller/apiController/StudentManager.php b/niucloud/app/api/controller/apiController/StudentManager.php new file mode 100644 index 00000000..b21bac3c --- /dev/null +++ b/niucloud/app/api/controller/apiController/StudentManager.php @@ -0,0 +1,98 @@ +request->params([ + ["name", ""], + ["gender", 0], + ["age", 0.00], + ["birthday", ""], + ["user_id", 0], + ["campus_id", 0], + ["class_id", 0], + ["note", ""], + ["status", 1], + ["emergency_contact", ""], + ["contact_phone", ""], + ["member_label", ""], + ["consultant_id", ""], + ["coach_id", ""], + ["trial_class_count", 2] + ]); + + // 表单验证 + if (empty($data['name'])) { + return fail('学员姓名不能为空'); + } + if ($data['gender'] == 0) { + return fail('请选择学员性别'); + } + if (empty($data['birthday'])) { + return fail('请选择学员出生日期'); + } + + $result = (new StudentService())->add($data); + if ($result['code'] === 1) { + return success('添加成功', $result['data']); + } else { + return fail($result['msg']); + } + } catch (\Exception $e) { + return fail('添加学员失败:' . $e->getMessage()); + } + } + + /** + * 获取学员列表 + * @param Request $request + * @return \think\Response + */ + public function list(Request $request) + { + try { + $data = $this->request->params([ + ["parent_resource_id", 0], + ["user_id", 0], + ["campus_id", 0], + ["status", 0] + ]); + + $result = (new StudentService())->getList($data); + if ($result['code'] === 1) { + return success('获取成功', $result['data']); + } else { + return fail($result['msg']); + } + } catch (\Exception $e) { + return fail('获取学员列表失败:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/niucloud/app/api/controller/login/UnifiedLogin.php b/niucloud/app/api/controller/login/UnifiedLogin.php new file mode 100644 index 00000000..7f0e6524 --- /dev/null +++ b/niucloud/app/api/controller/login/UnifiedLogin.php @@ -0,0 +1,162 @@ +request->params([ + ['username', ''], // 用户名/手机号 + ['password', ''], // 密码 + ['login_type', ''], // 登录类型: staff=员工端, member=会员端 + ]); + + // 参数验证 + $this->validate($data, [ + 'username' => 'require', + 'password' => 'require', + 'login_type' => 'require|in:staff,member' + ]); + + try { + $service = new UnifiedLoginService(); + $result = $service->unifiedLogin($data); + + if (!$result) { + return fail('登录失败,请检查用户名和密码'); + } + + return success($result, '登录成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 员工端登录(兼容旧接口) + * @return Response + */ + public function staffLogin() + { + $data = $this->request->params([ + ['phone', ''], + ['password', ''], + ['user_type', ''], // 1=市场, 2=教练, 3=销售 + ]); + + // 参数验证 + $this->validate($data, [ + 'phone' => 'require|mobile', + 'password' => 'require', + 'user_type' => 'require|in:1,2,3' + ]); + + try { + $service = new UnifiedLoginService(); + $result = $service->staffLogin($data); + + return success($result, '员工登录成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 会员端登录(兼容旧接口) + * @return Response + */ + public function memberLogin() + { + $data = $this->request->params([ + ['username', ''], + ['password', ''], + ['mobile', ''], + ]); + + try { + $service = new UnifiedLoginService(); + $result = $service->memberLogin($data); + + return success($result, '会员登录成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 登出 + * @return Response + */ + public function logout() + { + try { + $service = new UnifiedLoginService(); + $service->logout(); + + return success([], '登出成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 获取用户信息 + * @return Response + */ + public function getUserInfo() + { + try { + $service = new UnifiedLoginService(); + $result = $service->getUserInfo(); + + return success($result, '获取用户信息成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } + + /** + * 刷新Token + * @return Response + */ + public function refreshToken() + { + try { + $service = new UnifiedLoginService(); + $result = $service->refreshToken(); + + return success($result, 'Token刷新成功'); + + } catch (\Exception $e) { + return fail($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/niucloud/app/api/middleware/ApiPersonnelCheckToken.php b/niucloud/app/api/middleware/ApiPersonnelCheckToken.php index dd8ee824..816a0071 100644 --- a/niucloud/app/api/middleware/ApiPersonnelCheckToken.php +++ b/niucloud/app/api/middleware/ApiPersonnelCheckToken.php @@ -44,7 +44,7 @@ class ApiPersonnelCheckToken $token = $request->apiToken(); $token_info = ( new LoginService() )->parsePersonnelToken($token); if (!empty($token_info)) { - $request->memberId($token_info[ 'member_id' ]); + $request->memberId($token_info[ 'user_id' ]); } //校验会员和站点 $a= ( new AuthService() )->checkPersonnelMember($request); diff --git a/niucloud/app/api/route/route.php b/niucloud/app/api/route/route.php index 7c0485b1..81d75c30 100644 --- a/niucloud/app/api/route/route.php +++ b/niucloud/app/api/route/route.php @@ -174,7 +174,9 @@ Route::group(function () { //↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓-----员工端相关-----↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ //无需token验证的 Route::group(function () { - //员工登录 + //统一登录接口 + Route::post('login/unified', 'login.UnifiedLogin/login'); + //员工登录(兼容旧接口) Route::post('personnelLogin', 'login.Login/personnelLogin'); //获取字典 Route::get('common/getDictionary', 'apiController.Common/getDictionary'); @@ -254,7 +256,10 @@ Route::group(function () { Route::get('resourceSharing/info', 'apiController.ResourceSharing/info'); //资源共享-分配员工 Route::post('resourceSharing/assign', 'apiController.ResourceSharing/assign'); - + //销售端-学员-新增 + Route::post('student/add', 'apiController.StudentManager/add'); + //销售端-学员-列表 + Route::get('student/list', 'apiController.StudentManager/list'); //沟通记录-添加 Route::post('communicationRecords/add', 'apiController.CommunicationRecords/add'); diff --git a/niucloud/app/service/api/apiService/ResourceSharingService.php b/niucloud/app/service/api/apiService/ResourceSharingService.php index da59855a..cb9f714e 100644 --- a/niucloud/app/service/api/apiService/ResourceSharingService.php +++ b/niucloud/app/service/api/apiService/ResourceSharingService.php @@ -160,6 +160,7 @@ class ResourceSharingService extends BaseApiService } } } + // 共享人查询 - 如果指定了shared_by,优先处理这个条件 if (isset($where['shared_by']) && $where['shared_by']) { $model = $model->where(function ($query) use ($where) { @@ -191,6 +192,7 @@ class ResourceSharingService extends BaseApiService }); } } + // 处理查询条件 - CustomerResources模型的字段 $resource_conditions = []; @@ -246,10 +248,11 @@ class ResourceSharingService extends BaseApiService $model = $model->where('shared_at', '>=', $where['shared_at_arr'][0]) ->where('shared_at', '<=', $where['shared_at_arr'][1]); } - - // 调试SQL语句 - // $sql = $model->with(['customerResource', 'sixSpeed'])->fetchSql(true)->select(); - // var_dump($sql); + + // 过滤已分配的资源(只显示可再分配的资源) + $model = $model->where(function ($query) { + $query->where('shared_by', 0)->whereOr('shared_by', null); + }); // 查询数据 $list = $model->with(['customerResource', 'sixSpeed']) @@ -277,8 +280,20 @@ class ResourceSharingService extends BaseApiService $campus_names = $campus->whereIn('id', $campus_ids)->column('campus_name', 'id'); } - // 获取资源ID列表 + // 获取资源ID列表用于查询关联信息 $resource_ids = array_column($list['data'], 'resource_id'); + $resource_ids = array_unique(array_filter($resource_ids)); + + // 获取分配人员ID列表 + $shared_by_ids = array_column($list['data'], 'shared_by'); + $shared_by_ids = array_unique(array_filter($shared_by_ids)); + + // 查询分配人员信息 + $shared_by_names = []; + if (!empty($shared_by_ids)) { + $personnel = new \app\model\personnel\Personnel(); + $shared_by_names = $personnel->whereIn('id', $shared_by_ids)->column('name', 'id'); + } // 查询最近沟通记录 $communication_times = []; @@ -295,6 +310,58 @@ class ResourceSharingService extends BaseApiService } } + // 查询到访信息 + $visit_info = []; + if (!empty($resource_ids)) { + // 需要先导入PersonCourseSchedule模型 + $person_course_model = new \app\model\person_course_schedule\PersonCourseSchedule(); + $visit_records = $person_course_model + ->whereIn('person_id', $resource_ids) + ->where('person_type', 'customer_resource') + ->field('person_id, course_date, time_slot, status') + ->order('course_date', 'desc') + ->select() + ->toArray(); + + // 处理到访信息 + foreach ($visit_records as $record) { + $resource_id = $record['person_id']; + + if (!isset($visit_info[$resource_id])) { + $visit_info[$resource_id] = [ + 'first_visit_status' => '未到', + 'second_visit_status' => '未到', + 'visit_count' => 0 + ]; + } + + if ($record['status'] == 1) { // 假设status=1表示已到 + $visit_info[$resource_id]['visit_count']++; + + if ($visit_info[$resource_id]['visit_count'] == 1) { + $visit_info[$resource_id]['first_visit_status'] = '已到'; + } elseif ($visit_info[$resource_id]['visit_count'] == 2) { + $visit_info[$resource_id]['second_visit_status'] = '已到'; + } + } + } + } + + // 查询开单状态 + $order_status = []; + if (!empty($resource_ids)) { + $six_speed_model = new \app\model\six_speed\SixSpeed(); + $six_speed_records = $six_speed_model + ->whereIn('resource_id', $resource_ids) + ->field('resource_id, is_closed') + ->select() + ->toArray(); + + foreach ($six_speed_records as $record) { + $order_status[$record['resource_id']] = $record['is_closed'] ? '已开单' : '未开单'; + } + } + // 处理每条数据 foreach ($list['data'] as &$item) { if (!empty($item['customerResource'])) { @@ -302,8 +369,24 @@ class ResourceSharingService extends BaseApiService $item['customerResource']['source'] = get_dict_value('source', $item['customerResource']['source']); $item['customerResource']['source_channel'] = get_dict_value('SourceChannel', $item['customerResource']['source_channel']); $item['customerResource']['campus_name'] = $campus_names[$item['customerResource']['campus']] ?? ''; - $item['customerResource']['communication_time'] = $resultdata[$item['resource_id']] ?? ''; + + // 修复沟通时间字段 + $item['customerResource']['communication_time'] = $communication_times[$item['resource_id']] ?? ''; + + // 添加到访信息 + $resource_id = $item['resource_id']; + $item['customerResource']['first_visit_status'] = $visit_info[$resource_id]['first_visit_status'] ?? '未到'; + $item['customerResource']['second_visit_status'] = $visit_info[$resource_id]['second_visit_status'] ?? '未到'; + + // 添加开单状态 + $item['customerResource']['order_status'] = $order_status[$resource_id] ?? '未开单'; } + + // 添加分配人员信息 + $item['shared_by_name'] = $shared_by_names[$item['shared_by']] ?? '未分配'; + + // 判断资源是否可再分配(如果已有shared_by且不为0,说明已分配) + $item['can_reassign'] = empty($item['shared_by']) || $item['shared_by'] == 0; } } diff --git a/niucloud/app/service/api/apiService/StudentService.php b/niucloud/app/service/api/apiService/StudentService.php new file mode 100644 index 00000000..8ee1f99c --- /dev/null +++ b/niucloud/app/service/api/apiService/StudentService.php @@ -0,0 +1,163 @@ + 0, + 'msg' => '操作失败', + 'data' => [] + ]; + + try { + // 准备插入数据 + $insertData = [ + 'name' => $data['name'], + 'gender' => $data['gender'], + 'age' => $data['age'], + 'birthday' => $data['birthday'], + 'user_id' => $data['user_id'], + 'campus_id' => $data['campus_id'], + 'class_id' => $data['class_id'], + 'note' => $data['note'], + 'status' => $data['status'], + 'emergency_contact' => $data['emergency_contact'], + 'contact_phone' => $data['contact_phone'], + 'member_label' => $data['member_label'], + 'consultant_id' => $data['consultant_id'], + 'coach_id' => $data['coach_id'], + 'trial_class_count' => $data['trial_class_count'], + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + 'deleted_at' => 0 + ]; + + // 插入数据库 + $id = Db::table('school_student')->insertGetId($insertData); + + if ($id) { + $res['code'] = 1; + $res['msg'] = '添加成功'; + $res['data'] = ['id' => $id]; + } else { + $res['msg'] = '添加失败'; + } + } catch (\Exception $e) { + $res['msg'] = '添加失败:' . $e->getMessage(); + } + + return $res; + } + + /** + * 获取学员列表 + * @param array $data + * @return array + */ + public function getList(array $data) + { + $res = [ + 'code' => 0, + 'msg' => '操作失败', + 'data' => [] + ]; + + try { + $where = []; + $where[] = ['deleted_at', '=', 0]; + + if (!empty($data['user_id'])) { + $where[] = ['user_id', '=', $data['user_id']]; + } + + if (!empty($data['parent_resource_id'])) { + $where[] = ['user_id', '=', $data['parent_resource_id']]; + } + + if (!empty($data['campus_id'])) { + $where[] = ['campus_id', '=', $data['campus_id']]; + } + + if (!empty($data['status'])) { + $where[] = ['status', '=', $data['status']]; + } + + $list = Db::table('school_student') + ->where($where) + ->order('created_at', 'desc') + ->select() + ->toArray(); + + // 为每个学员添加到访情况信息 + foreach ($list as &$student) { + // 查询该学员的课程安排记录,按日期排序获取一访和二访信息 + $visitRecords = Db::table('school_person_course_schedule') + ->where([ + ['person_id', '=', $student['id']], + ['person_type', '=', 'student'] + ]) + ->order('course_date', 'asc') + ->select() + ->toArray(); + + // 初始化到访信息 + $student['first_visit_time'] = ''; + $student['second_visit_time'] = ''; + $student['first_visit_status'] = '未到访'; + $student['second_visit_status'] = '未到访'; + + // 设置一访和二访信息 + if (!empty($visitRecords)) { + if (isset($visitRecords[0])) { + $student['first_visit_time'] = $visitRecords[0]['course_date']; + $student['first_visit_status'] = '已到访'; + } + if (isset($visitRecords[1])) { + $student['second_visit_time'] = $visitRecords[1]['course_date']; + $student['second_visit_status'] = '已到访'; + } + } + + // 保留原始访问记录数据供前端使用 + $student['visit_records'] = $visitRecords; + } + + $res['code'] = 1; + $res['data'] = $list; + } catch (\Exception $e) { + $res['msg'] = '获取失败:' . $e->getMessage(); + } + + return $res; + } +} \ No newline at end of file diff --git a/niucloud/app/service/api/login/UnifiedLoginService.php b/niucloud/app/service/api/login/UnifiedLoginService.php new file mode 100644 index 00000000..a124b1a6 --- /dev/null +++ b/niucloud/app/service/api/login/UnifiedLoginService.php @@ -0,0 +1,420 @@ +handleStaffLogin($username, $password); + case self::USER_TYPE_MEMBER: + return $this->handleMemberLogin($username, $password); + default: + throw new CommonException('不支持的登录类型'); + } + } + + /** + * 员工端登录处理 + * @param string $username + * @param string $password + * @return array + * @throws \Exception + */ + private function handleStaffLogin(string $username, string $password) + { + // 查找员工信息及关联的系统用户信息 + $personnel = new Personnel(); + $staffInfo = $personnel->alias('p') + ->leftJoin('school_sys_user u', 'p.sys_user_id = u.uid') + ->where('p.phone', $username) + ->where('p.status', 2) // 2=已审核(正常状态) + ->where('u.status', 1) + ->field('p.*, u.username, u.password, u.real_name') + ->find(); + + if (!$staffInfo) { + throw new CommonException('员工账号不存在或已禁用'); + } + + // 验证密码 + if (!password_verify($password, $staffInfo['password'])) { + throw new CommonException('密码错误'); + } + + // 根据account_type确定角色类型 + $roleType = $this->getAccountTypeCode($staffInfo['account_type']); + + // 生成Token + $tokenData = [ + 'user_id' => $staffInfo['id'], + 'user_type' => self::USER_TYPE_STAFF, + 'role_type' => $roleType, + 'site_id' => 0, // 默认站点ID + ]; + + $tokenResult = TokenAuth::createToken($staffInfo['id'], AppTypeDict::PERSONNEL, $tokenData, 86400); + $token = $tokenResult['token']; + + // 获取角色信息和菜单权限 + $roleInfo = $this->getStaffRoleInfo($roleType); + $menuList = $this->getStaffMenuList($roleType); + + return [ + 'token' => $token, + 'user_info' => [ + 'id' => $staffInfo['id'], + 'name' => $staffInfo['name'], + 'phone' => $staffInfo['phone'], + 'avatar' => $staffInfo['head_img'] ?? '', + 'real_name' => $staffInfo['real_name'] ?? $staffInfo['name'], + 'account_type' => $staffInfo['account_type'], + 'employee_number' => $staffInfo['employee_number'], + 'user_type' => self::USER_TYPE_STAFF, + 'role_type' => $roleType, + ], + 'role_info' => $roleInfo, + 'menu_list' => $menuList, + ]; + } + + /** + * 会员端登录处理 + * @param string $username + * @param string $password + * @return array + * @throws \Exception + */ + private function handleMemberLogin(string $username, string $password) + { + // 查找会员信息 + $member = new Member(); + $memberInfo = $member->where(function($query) use ($username) { + $query->where('username', $username) + ->whereOr('mobile', $username); + }) + ->where('status', 1) + ->find(); + + if (!$memberInfo) { + throw new CommonException('会员账号不存在或已禁用'); + } + + // 验证密码 + if (!password_verify($password, $memberInfo['password'])) { + throw new CommonException('密码错误'); + } + + // 生成Token + $tokenData = [ + 'user_id' => $memberInfo['member_id'], + 'user_type' => self::USER_TYPE_MEMBER, + 'site_id' => $memberInfo['site_id'] ?? 0, + ]; + + $tokenResult = TokenAuth::createToken($memberInfo['member_id'], AppTypeDict::API, $tokenData, 86400); + $token = $tokenResult['token']; + + // 获取会员菜单权限 + $menuList = $this->getMemberMenuList(); + + return [ + 'token' => $token, + 'user_info' => [ + 'id' => $memberInfo['member_id'], + 'username' => $memberInfo['username'], + 'nickname' => $memberInfo['nickname'], + 'mobile' => $memberInfo['mobile'], + 'avatar' => $memberInfo['headimg'] ?? '', + 'user_type' => self::USER_TYPE_MEMBER, + ], + 'role_info' => [ + 'role_name' => '会员', + 'role_type' => 'member', + ], + 'menu_list' => $menuList, + ]; + } + + /** + * 员工端登录(兼容旧接口) + * @param array $data + * @return array + */ + public function staffLogin(array $data) + { + $staffData = [ + 'username' => $data['phone'], + 'password' => $data['password'], + 'login_type' => self::USER_TYPE_STAFF, + ]; + + $result = $this->unifiedLogin($staffData); + + // 添加user_type到用户信息中用于前端判断 + $result['user_info']['user_type_code'] = $data['user_type']; + + return $result; + } + + /** + * 会员端登录(兼容旧接口) + * @param array $data + * @return array + */ + public function memberLogin(array $data) + { + $memberData = [ + 'username' => $data['username'] ?: $data['mobile'], + 'password' => $data['password'], + 'login_type' => self::USER_TYPE_MEMBER, + ]; + + return $this->unifiedLogin($memberData); + } + + /** + * 获取员工角色信息 + * @param int $roleType + * @return array + */ + private function getStaffRoleInfo(int $roleType) + { + $roles = [ + self::STAFF_ROLE_MARKET => ['role_name' => '市场人员', 'role_code' => 'market'], + self::STAFF_ROLE_COACH => ['role_name' => '教练', 'role_code' => 'coach'], + self::STAFF_ROLE_SALES => ['role_name' => '销售人员', 'role_code' => 'sales'], + self::STAFF_ROLE_TEACHER => ['role_name' => '教师', 'role_code' => 'teacher'], + ]; + + return $roles[$roleType] ?? ['role_name' => '教师', 'role_code' => 'teacher']; + } + + /** + * 获取员工菜单列表 + * @param int $roleType + * @return array + */ + private function getStaffMenuList(int $roleType) + { + // 根据角色类型返回对应的菜单权限 + switch ($roleType) { + case self::STAFF_ROLE_MARKET: + return [ + ['path' => '/pages/market/home/index', 'name' => '首页', 'icon' => 'home'], + ['path' => '/pages/market/clue/index', 'name' => '线索管理', 'icon' => 'clue'], + ['path' => '/pages/market/clue/add_clues', 'name' => '添加客户', 'icon' => 'add'], + ['path' => '/pages/market/data/statistics', 'name' => '数据统计', 'icon' => 'data'], + ['path' => '/pages/market/my/index', 'name' => '个人中心', 'icon' => 'user'], + ]; + + case self::STAFF_ROLE_COACH: + case self::STAFF_ROLE_TEACHER: + return [ + ['path' => '/pages/coach/home/index', 'name' => '首页', 'icon' => 'home'], + ['path' => '/pages/coach/course/list', 'name' => '课表管理', 'icon' => 'course'], + ['path' => '/pages/coach/student/student_list', 'name' => '我的学员', 'icon' => 'student'], + ['path' => '/pages/coach/job/list', 'name' => '作业管理', 'icon' => 'job'], + ['path' => '/pages/coach/my/index', 'name' => '个人中心', 'icon' => 'user'], + ]; + + case self::STAFF_ROLE_SALES: + return [ + ['path' => '/pages/market/index/index', 'name' => '首页', 'icon' => 'home'], + ['path' => '/pages/market/clue/index', 'name' => '线索管理', 'icon' => 'clue'], + ['path' => '/pages/market/clue/add_clues', 'name' => '添加客户', 'icon' => 'add'], + ['path' => '/pages/market/clue/clue_table', 'name' => '数据统计', 'icon' => 'data'], + ['path' => '/pages/market/my/index', 'name' => '个人中心', 'icon' => 'user'], + ]; + + default: + return [ + ['path' => '/pages/coach/home/index', 'name' => '首页', 'icon' => 'home'], + ['path' => '/pages/coach/my/index', 'name' => '个人中心', 'icon' => 'user'], + ]; + } + } + + /** + * 获取会员菜单列表 + * @return array + */ + private function getMemberMenuList() + { + return [ + ['path' => '/pages/student/index/index', 'name' => '首页', 'icon' => 'home'], + ['path' => '/pages/student/timetable/index', 'name' => '课表', 'icon' => 'timetable'], + ['path' => '/pages/student/my/my', 'name' => '个人中心', 'icon' => 'user'], + // 家长端菜单 + ['path' => '/pages/parent/user-info/index', 'name' => '用户信息', 'icon' => 'user-info'], + ['path' => '/pages/parent/courses/index', 'name' => '课程管理', 'icon' => 'course'], + ['path' => '/pages/parent/materials/index', 'name' => '教学资料', 'icon' => 'material'], + ['path' => '/pages/parent/services/index', 'name' => '服务管理', 'icon' => 'service'], + ['path' => '/pages/parent/orders/index', 'name' => '订单管理', 'icon' => 'order'], + ['path' => '/pages/parent/messages/index', 'name' => '消息管理', 'icon' => 'message'], + ['path' => '/pages/parent/contracts/index', 'name' => '合同管理', 'icon' => 'contract'], + ]; + } + + /** + * 登出 + * @throws \Exception + */ + public function logout() + { + $token = request()->header('token'); + if ($token) { + (new CoreTokenService())->delete($token); + } + } + + /** + * 获取用户信息 + * @return array + * @throws \Exception + */ + public function getUserInfo() + { + $token = request()->header('token'); + if (!$token) { + throw new CommonException('未登录'); + } + + $tokenData = (new CoreTokenService())->verify($token); + if (!$tokenData) { + throw new CommonException('Token无效'); + } + + $userType = $tokenData['user_type']; + $userId = $tokenData['user_id']; + + if ($userType === self::USER_TYPE_STAFF) { + $personnel = new Personnel(); + $userInfo = $personnel->alias('p') + ->leftJoin('school_sys_user u', 'p.sys_user_id = u.uid') + ->where('p.id', $userId) + ->field('p.*, u.real_name') + ->find(); + if (!$userInfo) { + throw new CommonException('员工信息不存在'); + } + + $roleType = $this->getAccountTypeCode($userInfo['account_type']); + + return [ + 'id' => $userInfo['id'], + 'name' => $userInfo['name'], + 'phone' => $userInfo['phone'], + 'avatar' => $userInfo['head_img'] ?? '', + 'real_name' => $userInfo['real_name'] ?? $userInfo['name'], + 'account_type' => $userInfo['account_type'], + 'user_type' => self::USER_TYPE_STAFF, + 'role_type' => $roleType, + ]; + } else { + $member = new Member(); + $userInfo = $member->where('member_id', $userId)->find(); + if (!$userInfo) { + throw new CommonException('会员信息不存在'); + } + + return [ + 'id' => $userInfo['member_id'], + 'username' => $userInfo['username'], + 'nickname' => $userInfo['nickname'], + 'mobile' => $userInfo['mobile'], + 'avatar' => $userInfo['headimg'] ?? '', + 'user_type' => self::USER_TYPE_MEMBER, + ]; + } + } + + /** + * 刷新Token + * @return array + * @throws \Exception + */ + public function refreshToken() + { + $token = request()->header('token'); + if (!$token) { + throw new CommonException('未登录'); + } + + $newToken = (new CoreTokenService())->refresh($token); + if (!$newToken) { + throw new CommonException('Token刷新失败'); + } + + return ['token' => $newToken]; + } + + /** + * 根据账户类型获取角色编码 + * @param string $accountType + * @return int + */ + private function getAccountTypeCode(string $accountType) + { + switch ($accountType) { + case 'teacher': + return self::STAFF_ROLE_TEACHER; + case 'market': + return self::STAFF_ROLE_MARKET; + default: + return self::STAFF_ROLE_TEACHER; // 默认为教师 + } + } +} \ No newline at end of file diff --git a/uniapp/api/apiRoute.js b/uniapp/api/apiRoute.js index e9987d33..9660e0a7 100644 --- a/uniapp/api/apiRoute.js +++ b/uniapp/api/apiRoute.js @@ -3,7 +3,13 @@ import http from '../common/axios.js' //全部api接口 export default { //↓↓↓↓↓↓↓↓↓↓↓↓-----公共接口相关-----↓↓↓↓↓↓↓↓↓↓↓↓ - //教师/销售端登陆 + //统一登录接口 + async unifiedLogin(data = {}) { + const response = await http.post('/login/unified', data); + console.log('统一登录响应:', response); + return response; + }, + //教师/销售端登陆(兼容旧接口) async personnelLogin(data = {}) { const response = await http.post('/personnelLogin', data); console.log('登录响应:', response); @@ -30,6 +36,18 @@ export default { return await http.get('/common/getDictionary', data); }, + //批量获取字典数据 + async common_getBatchDict(keys = []) { + // 支持传入数组或字符串 + const keyParam = Array.isArray(keys) ? keys.join(',') : keys; + return await http.get('/dict/batch', { keys: keyParam }); + }, + + //根据业务场景获取字典数据 + async common_getDictByScene(scene = '') { + return await http.get(`/dict/scene/${scene}`); + }, + // 课程安排列表 - Mock版本 async getCourseScheduleListMock(data = {}) { @@ -404,6 +422,14 @@ export default { async xs_resourceSharingInfo(data = {}) { return await http.get('/resourceSharing/info', data); }, + //销售端-学员-新增 + async xs_addStudent(data = {}) { + return await http.post('/student/add', data); + }, + //销售端-学员-列表 + async xs_getStudentList(data = {}) { + return await http.get('/student/list', data); + }, //销售端-沟通记录-添加 async xs_communicationRecordsAdd(data = {}) { return await http.post('/communicationRecords/add', data); diff --git a/uniapp/components/custom-top-popup/custom-top-popup.vue b/uniapp/components/custom-top-popup/custom-top-popup.vue new file mode 100644 index 00000000..8d20bcec --- /dev/null +++ b/uniapp/components/custom-top-popup/custom-top-popup.vue @@ -0,0 +1,73 @@ + + + + + \ No newline at end of file diff --git a/uniapp/pages.json b/uniapp/pages.json index 98336c6d..8b2f430a 100644 --- a/uniapp/pages.json +++ b/uniapp/pages.json @@ -834,6 +834,24 @@ "navigationBarBackgroundColor": "#29d3b4", "navigationBarTextStyle": "white" } + }, + { + "path": "pages/common/home/index", + "style": { + "navigationBarTitleText": "首页", + "navigationStyle": "custom", + "navigationBarBackgroundColor": "#181A20", + "navigationBarTextStyle": "white" + } + }, + { + "path": "pages/common/profile/index", + "style": { + "navigationBarTitleText": "我的", + "navigationStyle": "custom", + "navigationBarBackgroundColor": "#181A20", + "navigationBarTextStyle": "white" + } } ], @@ -844,6 +862,26 @@ "backgroundColor": "#FDFDFD", "enablePullDownRefresh": true }, + "tabBar": { + "color": "#7A7E83", + "selectedColor": "#3cc51f", + "borderStyle": "black", + "backgroundColor": "#1a1a1a", + "list": [ + { + "pagePath": "pages/common/home/index", + "iconPath": "static/icon-img/home.png", + "selectedIconPath": "static/icon-img/home-active.png", + "text": "首页" + }, + { + "pagePath": "pages/common/profile/index", + "iconPath": "static/icon-img/profile.png", + "selectedIconPath": "static/icon-img/profile-active.png", + "text": "我的" + } + ] + }, "easycom": { "autoscan": true, "custom": { diff --git a/uniapp/pages/common/home/index.vue b/uniapp/pages/common/home/index.vue new file mode 100644 index 00000000..7257c690 --- /dev/null +++ b/uniapp/pages/common/home/index.vue @@ -0,0 +1,230 @@ + + + + + \ No newline at end of file diff --git a/uniapp/pages/common/profile/index.vue b/uniapp/pages/common/profile/index.vue new file mode 100644 index 00000000..b2004030 --- /dev/null +++ b/uniapp/pages/common/profile/index.vue @@ -0,0 +1,245 @@ + + + + + \ No newline at end of file diff --git a/uniapp/pages/market/clue/add_clues.vue b/uniapp/pages/market/clue/add_clues.vue index 7ee5438c..d04b2851 100644 --- a/uniapp/pages/market/clue/add_clues.vue +++ b/uniapp/pages/market/clue/add_clues.vue @@ -24,7 +24,12 @@ - 基础信息 + + 基础信息 + + 快速填写 + + - - - + - - - - - - - - - - - - - - - - - - - - - - + + {{ formData.birthday ? formData.birthday : '请选择生日' }} + + @@ -468,6 +435,16 @@ + + 检测到手机号重复,是否为现有用户添加新学员? + + 确认添加学员 + + @@ -527,6 +504,34 @@ + + + + + 快速填写 + + + + + + + 请粘贴包含客户信息的文本,支持格式: + 姓名:张三,电话:13800138000,校区:测试校区 + + + + + 取消 + 解析并填写 + + + + @@ -615,6 +620,10 @@ export default { is_submit: true,//是否提交(防止重复提交)|true=可提交,false=不可提交 + // 快速填写相关 + showQuickFill: false, // 是否显示快速填写弹窗 + quickFillText: '', // 快速填写文本内容 + //表单 formData: { // 客户基础信息 @@ -629,6 +638,7 @@ export default { decision_maker:'',//决策人 initial_intent:'',//客户初步意向度: high-高, medium-中, low-低 status:'pending',//客户状态: active-活跃, inactive-不活跃, pending-待定 + birthday:'',//生日 //六要素信息 purchasing_power:'',//购买力 @@ -713,6 +723,11 @@ export default { data_picker_input_name:'',//时间组件的input_name date_picker_show:false,//时间选择器是否展示 + //生日选择器 + picker_show_birthday: false, + minDate: new Date(1900, 0, 1).toISOString(), // 最小日期 + maxDate: new Date().toISOString(), // 最大日期 + //地区三级联动 show_area: false, @@ -790,6 +805,113 @@ export default { console.log('时间默认值已清空,用户需手动选择') }, + // 快速填写相关方法 + // 打开快速填写弹窗 + openQuickFill() { + this.showQuickFill = true + this.quickFillText = '' + }, + + // 解析快速填写文本 + parseQuickFillText() { + if (!this.quickFillText.trim()) { + uni.showToast({ + title: '请输入要解析的文本', + icon: 'none' + }) + return + } + + try { + // 定义字段映射规则 + const fieldRules = [ + { key: 'name', patterns: ['姓名', '客户姓名', '用户姓名', '学员姓名', '学生姓名'] }, + { key: 'phone_number', patterns: ['电话', '手机', '联系电话', '手机号', '电话号码', '联系方式'] }, + { key: 'campus', patterns: ['校区', '所属校区', '校区名称'] }, + { key: 'age', patterns: ['年龄'] }, + { key: 'birthday', patterns: ['生日', '出生日期', '生日日期'] } + ] + + // 用于存储解析结果 + const parsedData = {} + const text = this.quickFillText.trim() + + // 对每个字段规则进行匹配 + fieldRules.forEach(rule => { + rule.patterns.forEach(pattern => { + // 匹配模式:字段名 + 冒号/等号 + 数据内容 + const regex = new RegExp(`${pattern}\\s*[::=]\\s*([^,,\\n\\s]+)`, 'g') + const match = regex.exec(text) + if (match && match[1]) { + parsedData[rule.key] = match[1].trim() + } + }) + }) + + console.log('解析结果:', parsedData) + + // 填写到表单中 + let fillCount = 0 + if (parsedData.name) { + this.formData.name = parsedData.name + fillCount++ + } + if (parsedData.phone_number) { + this.formData.phone_number = parsedData.phone_number + fillCount++ + } + if (parsedData.campus) { + // 需要在校区选项中查找匹配的项 + this.findAndSetCampus(parsedData.campus) + fillCount++ + } + if (parsedData.age) { + const age = parseInt(parsedData.age) + if (!isNaN(age) && age > 0 && age < 150) { + this.formData.age = age + fillCount++ + } + } + if (parsedData.birthday) { + this.formData.birthday = parsedData.birthday + fillCount++ + } + + if (fillCount > 0) { + uni.showToast({ + title: `成功填写${fillCount}个字段`, + icon: 'success' + }) + this.showQuickFill = false + } else { + uni.showToast({ + title: '未能识别到有效信息,请检查格式', + icon: 'none' + }) + } + + } catch (error) { + console.error('解析失败:', error) + uni.showToast({ + title: '解析失败,请检查格式', + icon: 'none' + }) + } + }, + + // 查找并设置校区 + findAndSetCampus(campusText) { + const campusOptions = this.picker_config.campus?.options || [] + const matchedCampus = campusOptions.find(option => + option.text.includes(campusText) || campusText.includes(option.text) + ) + + if (matchedCampus) { + this.formData.campus = matchedCampus.value + this.picker_config.campus.text = matchedCampus.text + } + }, + // 统一的日期格式化方法 formatDate(date) { const year = date.getFullYear() @@ -1199,6 +1321,76 @@ export default { this.openDuplicateCheck() }, + //确认添加学员 + async confirmAddStudent(){ + if(!this.clientUserList || this.clientUserList.length == 0){ + uni.showToast({ + title: '没有找到重复的用户', + icon: 'none' + }) + return + } + + //取第一个重复用户的ID作为user_id + const userId = this.clientUserList[0].id + + //检查必要字段 + if(!this.formData.name){ + uni.showToast({ + title: '请填写学员姓名', + icon: 'none' + }) + return + } + + if(!this.formData.gender){ + uni.showToast({ + title: '请选择学员性别', + icon: 'none' + }) + return + } + + //准备学员数据 + const studentData = { + name: this.formData.name, + gender: this.formData.gender, + user_id: userId, + status: 1 + } + + try { + const res = await apiRoute.xs_addStudent(studentData) + if(res.code != 1){ + uni.showToast({ + title: res.msg || '添加学员失败', + icon: 'none' + }) + return + } + + uni.showToast({ + title: '学员添加成功', + icon: 'success' + }) + + //关闭弹窗并跳转回列表页 + this.showDuplicateCheck = false + setTimeout(() => { + uni.redirectTo({ + url: `/pages/market/clue/index` + }) + }, 1000) + + } catch (error) { + console.error('添加学员失败:', error) + uni.showToast({ + title: '添加学员失败', + icon: 'none' + }) + } + }, + //多选客户标签-选中后回调 @@ -1378,6 +1570,31 @@ export default { this.date_picker_show = false }, + //生日选择器 + changePickerBirthday(e) { + console.log('生日选择器返回数据:', e) + let val = '' + if (e.result) { + val = e.result + } else if (e.value) { + val = e.value + } else if (e.detail && e.detail.result) { + val = e.detail.result + } else if (e.detail && e.detail.value) { + val = e.detail.value + } + + if (val && typeof val === 'string') { + if (/^\d+$/.test(val)) { + const date = new Date(parseInt(val)) + val = this.formatDate(date) + } + } + + this.formData.birthday = val + this.picker_show_birthday = false + }, + //下一步 index|0=添加客户,1六要素 async nextStep(index) { this.optionTableId = String(index) @@ -1648,4 +1865,132 @@ export default { } } + +// 快速填写弹窗样式 +.quick-fill-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + z-index: 1500; + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx; +} + +.quick-fill-content { + background: #fff; + border-radius: 16rpx; + width: 100%; + max-width: 600rpx; + max-height: 80vh; + display: flex; + flex-direction: column; +} + +.quick-fill-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32rpx; + border-bottom: 1px solid #f0f0f0; +} + +.quick-fill-title { + font-size: 32rpx; + font-weight: 600; + color: #333; +} + +.quick-fill-close { + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; + + .close-text { + font-size: 32rpx; + color: #999; + } +} + +.duplicate-notice { + padding: 32rpx; + text-align: center; + border-bottom: 1px solid #eee; + margin-bottom: 20rpx; + + text { + display: block; + margin-bottom: 20rpx; + } +} + +.quick-fill-body { + padding: 32rpx; + flex: 1; + overflow-y: auto; +} + +.quick-fill-tip { + margin-bottom: 24rpx; + font-size: 24rpx; + color: #666; + line-height: 1.5; + + text:first-child { + display: block; + margin-bottom: 8rpx; + } + + text:last-child { + display: block; + color: #29d3b4; + font-weight: 500; + } +} + +.quick-fill-textarea { + width: 100%; + height: 300rpx; + border: 1px solid #ddd; + border-radius: 8rpx; + padding: 16rpx; + font-size: 28rpx; + line-height: 1.5; + background: #fafafa; + box-sizing: border-box; +} + +.quick-fill-buttons { + display: flex; + gap: 20rpx; + padding: 32rpx; + border-top: 1px solid #f0f0f0; +} + +.quick-fill-btn { + flex: 1; + height: 72rpx; + line-height: 72rpx; + text-align: center; + border-radius: 8rpx; + font-size: 28rpx; + font-weight: 600; + + &.cancel-btn { + background: #f5f5f5; + color: #666; + border: 1px solid #ddd; + } + + &.confirm-btn { + background: #29d3b4; + color: #fff; + } +} \ No newline at end of file diff --git a/uniapp/pages/market/clue/clue_info.vue b/uniapp/pages/market/clue/clue_info.vue index a04d7aaa..64c0c3dd 100644 --- a/uniapp/pages/market/clue/clue_info.vue +++ b/uniapp/pages/market/clue/clue_info.vue @@ -58,39 +58,146 @@ - 基本信息 - - - 来源渠道 - {{ safeGet(clientInfo, 'customerResource.source_channel_name', '未知渠道') }} - - - 来源 - {{ safeGet(clientInfo, 'customerResource.source_name', '未知来源') }} - - - 顾问 - {{ safeGet(clientInfo, 'customerResource.consultant_name', '未知顾问') }} - - - 学生姓名 - {{ safeGet(clientInfo, 'customerResource.name', '未知学生') }} - - - 性别 - {{ safeGet(clientInfo, 'customerResource.gender_name', '未知性别') }} - - - - 附加信息 - - - 已成交次数 - {{ safeGet(clientInfo, 'customerResource.cj_count', 0) }}次 + + + + 客户和学生信息 + + + + 添加学生 + - - 体验课程 - {{ safeGet(clientInfo, 'customerResource.trial_class_count', 0) }}次 + + + + + + + + + + + {{ safeGet(clientInfo, 'customerResource.name', '客').charAt(0) }} + + + {{ safeGet(clientInfo, 'customerResource.name', '未知客户') }} + + {{ safeGet(clientInfo, 'customerResource.phone_number', '') }} + + + + + 📞 + + + 💬 + + + + + + + 来源渠道 + {{ safeGet(clientInfo, 'customerResource.source_channel_name', '未知渠道') }} + + + 来源 + {{ safeGet(clientInfo, 'customerResource.source_name', '未知来源') }} + + + 顾问 + {{ safeGet(clientInfo, 'customerResource.consultant_name', '未知顾问') }} + + + 性别 + {{ safeGet(clientInfo, 'customerResource.gender_name', '未知性别') }} + + + + + + + + + + + {{ student.name ? student.name.charAt(0) : '学' }} + + + {{ student.name || '未知学生' }} + + {{ formatAge(student.age) }} + {{ formatGender(student.gender) }} + + + + 编辑 + + + + + + 出生日期: + {{ student.birthday || '未设置' }} + + + 联系电话: + {{ student.contact_phone || '未设置' }} + + + 紧急联系人: + {{ student.emergency_contact || '未设置' }} + + + 体验课次数: + {{ student.trial_class_count || 0 }}次 + + + + + + + + + + + + + + + + + {{ action.icon }} + {{ action.text }} + + + + + + + + + + + + 暂无学生信息 + + 添加第一个学生 + + @@ -244,14 +351,7 @@ - - - 编辑详情 - 课程安排 - 修改记录 - 订单列表 - 服务列表 - + @@ -389,6 +489,116 @@ + + + + + + {{ isEditingStudent ? '编辑学生信息' : '添加学生信息' }} + + + + + + + 基本信息 + + + 姓名 + + + + + + + 性别 + + + + {{ formatGender(currentStudent.gender) || '请选择性别' }} + + + + + + + + 出生日期 + + + + {{ currentStudent.birthday || '请选择出生日期' }} + + + + + + + + 联系电话 + + + + + + + 紧急联系人 + + + + + + + + + 学员信息 + + + 会员标签 + + + + + + + 体验课次数 + + + + + + + 学员状态 + + + + {{ currentStudent.status === 1 ? '有效' : '无效' }} + + + + + + + + + + 其他信息 + + + 备注 + + + + + + + + + 取消 + 保存 + + + @@ -452,6 +662,46 @@ export default { pdf_files: [] }, // 当前编辑的体测记录 isEditingFitnessRecord: false, // 是否为编辑模式 + + // 学生信息相关 (基于school_student表结构) + studentList: [], // 学生列表 + currentStudent: { + id: null, + name: '', + gender: 0, // 性别: 0未知, 1男, 2女 + age: 0.00, // 年龄,支持小数表示,例如3.11表示3岁11个月 + birthday: null, + user_id: 0, // 资源ID + campus_id: null, + class_id: null, + note: '', // 备注信息 + status: 1, // 学员状态: 0无效, 1有效 + emergency_contact: '', // 紧急联系人 + contact_phone: '', // 联系人电话 + member_label: '', // 会员标签 + consultant_id: null, + coach_id: null, + trial_class_count: 2, // 体验课次数|默认2(新增学员赠送) + actionsExpanded: false // 操作面板展开状态 + }, // 当前编辑的学生信息 + isEditingStudent: false, // 是否为编辑模式 + currentStudentIndex: 0, // 当前显示的学生索引 + + // 操作按钮相关 + actionButtons: [ + { key: 'edit_detail', text: '编辑详情', icon: '✏️' }, + { key: 'course_arrangement', text: '课程安排', icon: '📅' }, + { key: 'edit_record', text: '修改记录', icon: '📝' }, + { key: 'order_list', text: '订单列表', icon: '📋' }, + { key: 'service_list', text: '服务列表', icon: '🔧' } + ], // 默认操作按钮 + + // 选项数据 + genderOptions: [ + { label: '未知', value: 0 }, + { label: '男', value: 1 }, + { label: '女', value: 2 } + ] } }, computed: { @@ -459,6 +709,58 @@ export default { hasCourseInfo() { return this.courseInfo && Array.isArray(this.courseInfo) && this.courseInfo.length > 0 }, + + // 获取排序后的操作按钮 + sortedActions() { + // 从本地存储获取排序配置 + const savedOrder = uni.getStorageSync('action_buttons_order') + if (savedOrder && Array.isArray(savedOrder)) { + // 根据保存的顺序重新排列 + const sortedButtons = [] + savedOrder.forEach(key => { + const button = this.actionButtons.find(btn => btn.key === key) + if (button) sortedButtons.push(button) + }) + // 添加新增的按钮(如果有) + this.actionButtons.forEach(button => { + if (!savedOrder.includes(button.key)) { + sortedButtons.push(button) + } + }) + return sortedButtons + } + return this.actionButtons + }, + + // 获取学生专用的排序后操作按钮 + sortedStudentActions() { + const studentActions = [ + { key: 'course_info', text: '课程信息', icon: '📚' }, + { key: 'fitness_record', text: '体测记录', icon: '📊' }, + { key: 'study_plan', text: '学习计划', icon: '📋' }, + { key: 'course_arrangement', text: '课程安排', icon: '📅' }, + { key: 'order_list', text: '订单列表', icon: '📃' }, + { key: 'service_list', text: '服务列表', icon: '🔧' } + ] + + // 从本地存储获取学生操作按钮排序配置 + const savedOrder = uni.getStorageSync('student_action_buttons_order') + if (savedOrder && Array.isArray(savedOrder)) { + const sortedButtons = [] + savedOrder.forEach(key => { + const button = studentActions.find(btn => btn.key === key) + if (button) sortedButtons.push(button) + }) + // 添加新增的按钮(如果有) + studentActions.forEach(button => { + if (!savedOrder.includes(button.key)) { + sortedButtons.push(button) + } + }) + return sortedButtons + } + return studentActions + } }, onLoad(options) { console.log('onLoad - 接收到参数:', options) @@ -496,16 +798,17 @@ export default { await this.getInfo() console.log('init - 客户详情获取完成') - // 获取员工信息、通话记录、教练列表、课程信息和体测记录可以并行 - console.log('init - 开始获取员工信息、通话记录、教练列表、课程信息和体测记录') + // 获取员工信息、通话记录、教练列表、课程信息、体测记录和学生列表可以并行 + console.log('init - 开始获取员工信息、通话记录、教练列表、课程信息、体测记录和学生列表') await Promise.all([ this.getUserInfo(), this.getListCallUp(), this.getPersonnelList(), this.getCourseInfo(), // 添加课程信息获取 this.getFitnessRecords(), // 添加体测记录获取 + this.getStudentList(), // 添加学生列表获取 ]) - console.log('init - 员工信息、通话记录、教练列表、课程信息和体测记录获取完成') + console.log('init - 员工信息、通话记录、教练列表、课程信息、体测记录和学生列表获取完成') } catch (error) { console.error('init - 数据加载出错:', error) } @@ -655,8 +958,8 @@ export default { }) }, - //跳转页面-订单列表 - openViewOrder() { + //跳转页面-订单列表(支持学生参数) + openViewOrder(student = null) { // 检查必要参数是否存在 const resource_id = this.safeGet(this.clientInfo, 'resource_id') if (!resource_id) { @@ -672,21 +975,79 @@ export default { let staff_id = this.safeGet(this.userInfo, 'id', '') //员工id let staff_id_name = this.safeGet(this.userInfo, 'name', '') //员工姓名 + // 如果有学生信息,添加学生参数 + let url = `/pages/market/clue/order_list?resource_id=${resource_id}&resource_name=${resource_name}&staff_id=${staff_id}&staff_id_name=${staff_id_name}` + if (student) { + url += `&student_id=${student.id}&student_name=${encodeURIComponent(student.name)}` + } + this.$navigateTo({ - url: `/pages/market/clue/order_list?resource_id=${resource_id}&resource_name=${resource_name}&staff_id=${staff_id}&staff_id_name=${staff_id_name}`, + url: url, }) }, - //跳转页面-课程安排 - openViewEditClassLog() { + //跳转页面-课程安排(支持学生参数) + openViewEditClassLog(student = null) { let resource_id = this.clientInfo.resource_id //客户资源id let resource_name = this.clientInfo.customerResource.name || '' //客户资源id姓名 let staff_id = this.userInfo.id //员工id let staff_id_name = this.userInfo.name || '' //员工姓名 + // 如果有学生信息,添加学生参数 + let url = `/pages/market/clue/class_arrangement?resource_id=${resource_id}&resource_name=${resource_name}&staff_id=${staff_id}&staff_id_name=${staff_id_name}` + if (student) { + url += `&student_id=${student.id}&student_name=${encodeURIComponent(student.name)}` + } + + this.$navigateTo({ + url: url, + }) + }, + + //跳转页面-编辑客户详情(支持学生参数) + openViewEditClues(student = null) { + // 检查resource_sharing_id是否存在 + if (!this.resource_sharing_id) { + console.error('openViewEditClues - resource_sharing_id为空,无法跳转') + uni.showToast({ + title: '缺少必要参数', + icon: 'none', + }) + return + } + + let resource_sharing_id = this.resource_sharing_id //共享资源表id + let url = `/pages/market/clue/edit_clues?resource_sharing_id=${resource_sharing_id}` + if (student) { + url += `&student_id=${student.id}&student_name=${encodeURIComponent(student.name)}` + } + + this.$navigateTo({ + url: url, + }) + }, + + //跳转页面-客户信息修改记录(支持学生参数) + openViewEditCluesLog(student = null) { + // 检查clientInfo.resource_id是否存在 + const resource_id = this.safeGet(this.clientInfo, 'resource_id') + if (!resource_id) { + console.error('openViewEditCluesLog - resource_id为空,无法跳转') + uni.showToast({ + title: '缺少必要参数', + icon: 'none', + }) + return + } + + let url = `/pages/market/clue/edit_clues_log?resource_id=${resource_id}` + if (student) { + url += `&student_id=${student.id}&student_name=${encodeURIComponent(student.name)}` + } + this.$navigateTo({ - url: `/pages/market/clue/class_arrangement?resource_id=${resource_id}&resource_name=${resource_name}&staff_id=${staff_id}&staff_id_name=${staff_id_name}`, + url: url, }) }, @@ -1559,101 +1920,609 @@ export default { const day = String(now.getDate()).padStart(2, '0') return `${year}-${month}-${day}` }, - }, -} - - \ No newline at end of file diff --git a/uniapp/pages/market/clue/edit_clues.vue b/uniapp/pages/market/clue/edit_clues.vue index 30ba148d..fadbf615 100644 --- a/uniapp/pages/market/clue/edit_clues.vue +++ b/uniapp/pages/market/clue/edit_clues.vue @@ -9,10 +9,12 @@ - 基础信息 + + 基础信息 + - + {{ formData.campus ? picker_config.campus.text : '点击选择' }} @@ -69,10 +71,12 @@ - - + + - + + {{ formData.birthday || '请选择生日' }} + @@ -305,6 +309,34 @@ + + + + + 快速填写 + + + + + + + 请粘贴包含客户信息的文本,支持格式: + 姓名:张三,电话:13800138000,校区:测试校区 + + + + + 取消 + 解析并填写 + + + + 保存 @@ -394,6 +426,11 @@ is_submit: true, //是否提交(防止重复提交)|true=可提交,false=不可提交 resource_sharing_id: '', //resource_sharing_id(资源共享表id) + + // 快速填写相关 + showQuickFill: false, // 是否显示快速填写弹窗 + quickFillText: '', // 快速填写文本内容 + //表单 formData: { // 客户基础信息 @@ -402,6 +439,7 @@ consultant: '', //顾问 name: '', //姓名 age: '', //年龄 + birthday: '', //生日 gender: 'male', //性别|male-男性, female-女性, other-其他 phone_number: '', //联系电话 demand: '', //需求 @@ -613,7 +651,20 @@ // 批量获取字典数据 async getBatchDictData() { try { + console.log('开始批量获取字典数据'); + // 定义需要的字典keys和对应的本地键名 + const dictKeys = [ + 'SourceChannel', + 'source', + 'customer_purchasing_power', + 'preliminarycustomerintention', + 'cognitive_concept', + 'kh_status', + 'decision_maker', + 'distance' + ]; + const dictMapping = { 'SourceChannel': 'source_channel', 'source': 'source', @@ -625,34 +676,34 @@ 'distance': 'distance' }; - // 获取缓存的字典数据 - if (!window._dictCache) { - window._dictCache = {}; - } - - // 处理优先级,先请求用户可能立即需要的字典 - const criticalDicts = ['source', 'source_channel']; - const regularDicts = Object.keys(dictMapping).filter(key => !criticalDicts.includes(dictMapping[key])); - - // 先获取关键字典 - for (const dictKey of criticalDicts) { - const key = Object.keys(dictMapping).find(k => dictMapping[k] === dictKey); - if (key) { - await this.loadDictData(key, dictMapping[key]); + try { + // 使用批量接口一次性获取所有字典 + console.log('调用批量字典接口,keys:', dictKeys); + const batchResult = await apiRoute.common_getBatchDict(dictKeys); + + if (batchResult && batchResult.code === 1 && batchResult.data) { + console.log('批量字典接口响应成功:', batchResult.data); + + // 处理批量返回的字典数据 + Object.keys(batchResult.data).forEach(key => { + const localKey = dictMapping[key]; + if (localKey && Array.isArray(batchResult.data[key])) { + this.processDictData(localKey, batchResult.data[key]); + } + }); + + console.log('批量字典数据处理完成'); + return; + } else { + console.warn('批量字典接口返回数据格式异常:', batchResult); } + } catch (batchError) { + console.error('批量字典接口调用失败:', batchError); } - - // 异步获取其他字典 - setTimeout(() => { - regularDicts.forEach(async (key) => { - const localKey = dictMapping[key]; - if (!window._dictCache[key]) { - await this.loadDictData(key, localKey); - } - }); - }, 100); - - console.log('优化的批量字典数据加载初始化完成'); + + // 如果批量接口失败,回退到单个获取 + console.log('批量接口失败,使用回退方案'); + await this.fallbackGetDict(); } catch (error) { console.error('批量获取字典数据失败:', error); @@ -812,6 +863,7 @@ source: customerResource.source || '', //来源 name: customerResource.name || '', //姓名 age: customerResource.age || '', //年龄 + birthday: customerResource.birthday || '', //生日 gender: customerResource.gender || 'male', //性别|male-男性, female-女性, other-其他 phone_number: customerResource.phone_number || '', //联系电话 demand: customerResource.demand || '', //需求 @@ -1350,6 +1402,113 @@ let status = e.id this.optionTableId = String(status) }, + + // 快速填写相关方法 + // 打开快速填写弹窗 + openQuickFill() { + this.showQuickFill = true + this.quickFillText = '' + }, + + // 解析快速填写文本 + parseQuickFillText() { + if (!this.quickFillText.trim()) { + uni.showToast({ + title: '请输入要解析的文本', + icon: 'none' + }) + return + } + + try { + // 定义字段映射规则 + const fieldRules = [ + { key: 'name', patterns: ['姓名', '客户姓名', '用户姓名', '学员姓名', '学生姓名'] }, + { key: 'phone_number', patterns: ['电话', '手机', '联系电话', '手机号', '电话号码', '联系方式'] }, + { key: 'campus', patterns: ['校区', '所属校区', '校区名称'] }, + { key: 'age', patterns: ['年龄'] }, + { key: 'birthday', patterns: ['生日', '出生日期', '生日日期'] } + ] + + // 用于存储解析结果 + const parsedData = {} + const text = this.quickFillText.trim() + + // 对每个字段规则进行匹配 + fieldRules.forEach(rule => { + rule.patterns.forEach(pattern => { + // 匹配模式:字段名 + 冒号/等号 + 数据内容 + const regex = new RegExp(`${pattern}\\s*[::=]\\s*([^,,\\n\\s]+)`, 'g') + const match = regex.exec(text) + if (match && match[1]) { + parsedData[rule.key] = match[1].trim() + } + }) + }) + + console.log('解析结果:', parsedData) + + // 填写到表单中 + let fillCount = 0 + if (parsedData.name) { + this.formData.name = parsedData.name + fillCount++ + } + if (parsedData.phone_number) { + this.formData.phone_number = parsedData.phone_number + fillCount++ + } + if (parsedData.campus) { + // 需要在校区选项中查找匹配的项 + this.findAndSetCampus(parsedData.campus) + fillCount++ + } + if (parsedData.age) { + const age = parseInt(parsedData.age) + if (!isNaN(age) && age > 0 && age < 150) { + this.formData.age = age + fillCount++ + } + } + if (parsedData.birthday) { + this.formData.birthday = parsedData.birthday + fillCount++ + } + + if (fillCount > 0) { + uni.showToast({ + title: `成功填写${fillCount}个字段`, + icon: 'success' + }) + this.showQuickFill = false + } else { + uni.showToast({ + title: '未能识别到有效信息,请检查格式', + icon: 'none' + }) + } + + } catch (error) { + console.error('解析失败:', error) + uni.showToast({ + title: '解析失败,请检查格式', + icon: 'none' + }) + } + }, + + // 查找并设置校区 + findAndSetCampus(campusText) { + const campusOptions = this.picker_config.campus?.options || [] + const matchedCampus = campusOptions.find(option => + option.text.includes(campusText) || campusText.includes(option.text) + ) + + if (matchedCampus) { + this.formData.campus = matchedCampus.value + this.picker_config.campus.text = matchedCampus.text + } + }, //######-----下拉选择器组件相关-----###### @@ -1819,4 +1978,127 @@ width: 100%; height: 180rpx; /* 增加高度,确保有足够的空间 */ } + + // 快速填写弹窗样式 + .quick-fill-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + z-index: 1500; + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx; + } + + .quick-fill-content { + background: #fff; + border-radius: 16rpx; + width: 100%; + max-width: 600rpx; + max-height: 80vh; + display: flex; + flex-direction: column; + } + + .quick-fill-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32rpx; + border-bottom: 1px solid #f0f0f0; + } + + .quick-fill-title { + font-size: 32rpx; + font-weight: 600; + color: #333; + } + + .quick-fill-close { + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; + + .close-text { + font-size: 32rpx; + color: #999; + } + } + + .quick-fill-body { + padding: 32rpx; + flex: 1; + overflow-y: auto; + } + + .quick-fill-tip { + margin-bottom: 24rpx; + font-size: 24rpx; + color: #666; + line-height: 1.5; + + text:first-child { + display: block; + margin-bottom: 8rpx; + } + + text:last-child { + display: block; + color: #29d3b4; + font-weight: 500; + } + } + + .quick-fill-textarea { + width: 100%; + height: 300rpx; + border: 1px solid #ddd; + border-radius: 8rpx; + padding: 16rpx; + font-size: 28rpx; + line-height: 1.5; + background: #fafafa; + box-sizing: border-box; + } + + .quick-fill-buttons { + display: flex; + gap: 20rpx; + padding: 32rpx; + border-top: 1px solid #f0f0f0; + } + + .quick-fill-btn { + flex: 1; + height: 72rpx; + line-height: 72rpx; + text-align: center; + border-radius: 8rpx; + font-size: 28rpx; + font-weight: 600; + + &.cancel-btn { + background: #f5f5f5; + color: #666; + border: 1px solid #ddd; + } + + &.confirm-btn { + background: #29d3b4; + color: #fff; + } + } + + .quick-fill-btn { + padding: 0 32rpx; + background: #f5f5f5; + border: 1px solid #ddd; + border-radius: 8rpx; + } \ No newline at end of file diff --git a/uniapp/pages/market/clue/index.vue b/uniapp/pages/market/clue/index.vue index 102a1537..cb8ec8e2 100644 --- a/uniapp/pages/market/clue/index.vue +++ b/uniapp/pages/market/clue/index.vue @@ -1,4 +1,4 @@ -