Browse Source

feat(market): 新增快速填写功能并优化线索添加流程

- 新增快速填写弹窗,支持粘贴文本解析客户信息
- 添加生日选择器和确认添加学员功能
- 优化表单布局和提示信息
- 更新相关API接口
master
王泽彦 8 months ago
parent
commit
36bb6689c8
  1. 2
      .gitignore
  2. 3
      admin/src/app/views/campus_person_role/campus_person_role.vue
  3. 6
      admin/src/app/views/campus_person_role/components/campus-person-role-edit.vue
  4. 98
      niucloud/app/api/controller/apiController/StudentManager.php
  5. 162
      niucloud/app/api/controller/login/UnifiedLogin.php
  6. 2
      niucloud/app/api/middleware/ApiPersonnelCheckToken.php
  7. 9
      niucloud/app/api/route/route.php
  8. 93
      niucloud/app/service/api/apiService/ResourceSharingService.php
  9. 163
      niucloud/app/service/api/apiService/StudentService.php
  10. 420
      niucloud/app/service/api/login/UnifiedLoginService.php
  11. 28
      uniapp/api/apiRoute.js
  12. 73
      uniapp/components/custom-top-popup/custom-top-popup.vue
  13. 38
      uniapp/pages.json
  14. 230
      uniapp/pages/common/home/index.vue
  15. 245
      uniapp/pages/common/profile/index.vue
  16. 449
      uniapp/pages/market/clue/add_clues.vue
  17. 1979
      uniapp/pages/market/clue/clue_info.vue
  18. 336
      uniapp/pages/market/clue/edit_clues.vue
  19. 819
      uniapp/pages/market/clue/index.vue
  20. 101
      uniapp/pages/student/login/login.vue
  21. BIN
      uniapp/static/icon-img/home-active.png
  22. BIN
      uniapp/static/icon-img/home.png
  23. BIN
      uniapp/static/icon-img/profile-active.png
  24. BIN
      uniapp/static/icon-img/profile.png

2
.gitignore

@ -14,4 +14,6 @@ node_modules
examples
PRPs
INITIAL.md
CLAUDE.local.md

3
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;
}

6
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{

98
niucloud/app/api/controller/apiController/StudentManager.php

@ -0,0 +1,98 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\api\controller\apiController;
use app\Request;
use app\service\api\apiService\StudentService;
use core\base\BaseApiService;
/**
* 学员管理相关接口
* Class StudentManager
* @package app\api\controller\apiController
*/
class StudentManager extends BaseApiService
{
/**
* 添加学员
* @param Request $request
* @return \think\Response
*/
public function add(Request $request)
{
try {
$data = $this->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());
}
}
}

162
niucloud/app/api/controller/login/UnifiedLogin.php

@ -0,0 +1,162 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\api\controller\login;
use app\service\api\login\LoginService;
use app\service\api\login\UnifiedLoginService;
use core\base\BaseController;
use think\Response;
/**
* 统一登录控制器
* 支持员工端和会员端两种登录方式
*/
class UnifiedLogin extends BaseController
{
/**
* 统一登录接口
* @return Response
*/
public function login()
{
$data = $this->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());
}
}
}

2
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);

9
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');

93
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 = [];
@ -247,9 +249,10 @@ class ResourceSharingService extends BaseApiService
->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;
}
}

163
niucloud/app/service/api/apiService/StudentService.php

@ -0,0 +1,163 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\service\api\apiService;
use core\base\BaseApiService;
use think\facade\Db;
/**
* 学员服务层
* Class StudentService
* @package app\service\api\apiService
*/
class StudentService extends BaseApiService
{
public function __construct()
{
parent::__construct();
}
/**
* 添加学员
* @param array $data
* @return array
*/
public function add(array $data)
{
$res = [
'code' => 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;
}
}

420
niucloud/app/service/api/login/UnifiedLoginService.php

@ -0,0 +1,420 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\service\api\login;
use app\dict\sys\AppTypeDict;
use app\model\member\Member;
use app\model\personnel\Personnel;
use app\model\site\Site;
use app\service\core\menu\CoreMenuService;
use core\util\TokenAuth;
use core\base\BaseService;
use core\exception\CommonException;
use think\facade\Cache;
/**
* 统一登录服务类
*/
class UnifiedLoginService extends BaseService
{
/**
* 用户类型常量
*/
const USER_TYPE_STAFF = 'staff'; // 员工端
const USER_TYPE_MEMBER = 'member'; // 会员端
/**
* 员工角色类型
*/
const STAFF_ROLE_MARKET = 1; // 市场人员
const STAFF_ROLE_COACH = 2; // 教练
const STAFF_ROLE_SALES = 3; // 销售
const STAFF_ROLE_TEACHER = 4; // 教师
/**
* 统一登录方法
* @param array $data
* @return array
* @throws \Exception
*/
public function unifiedLogin(array $data)
{
$username = trim($data['username']);
$password = trim($data['password']);
$loginType = trim($data['login_type']);
if (empty($username) || empty($password)) {
throw new CommonException('用户名和密码不能为空');
}
switch ($loginType) {
case self::USER_TYPE_STAFF:
return $this->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; // 默认为教师
}
}
}

28
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);

73
uniapp/components/custom-top-popup/custom-top-popup.vue

@ -0,0 +1,73 @@
<script>
export default {
name: 'CustomTopPopup',
props: {
modelValue: {
type: Boolean,
default: false
}
},
computed: {
show() {
return this.modelValue
}
},
methods: {
closePopup() {
console.log('关闭弹窗')
this.$emit('update:modelValue', false)
},
handleMaskClick() {
console.log('点击遮罩')
this.closePopup()
}
},
mounted() {
console.log('CustomTopPopup mounted, modelValue:', this.modelValue)
},
watch: {
modelValue(newVal) {
console.log('CustomTopPopup modelValue changed:', newVal)
}
}
}
</script>
<template>
<view v-if="show" class="top-popup-mask" @tap="handleMaskClick">
<view class="top-popup-content" @tap.stop>
<slot />
</view>
</view>
</template>
<style lang="scss">
.top-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 1001;
display: flex;
flex-direction: column;
}
.top-popup-content {
background: #fff;
border-bottom-left-radius: 24rpx;
border-bottom-right-radius: 24rpx;
animation: slideDown 0.3s ease-out;
width: 100%;
}
@keyframes slideDown {
from {
transform: translateY(-100%);
}
to {
transform: translateY(0);
}
}
</style>

38
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": {

230
uniapp/pages/common/home/index.vue

@ -0,0 +1,230 @@
<template>
<view class="home-container">
<!-- 自定义导航栏 -->
<uni-nav-bar
:statusBar="true"
backgroundColor="#181A20"
color="#fff"
title="首页"
/>
<!-- 用户信息区域 -->
<view class="user-info-section">
<view class="user-avatar">
<image :src="userInfo.avatar || '/static/icon-img/tou.png'" mode="aspectFill"></image>
</view>
<view class="user-details">
<text class="user-name">{{ userInfo.name || '员工姓名' }}</text>
<text class="user-role">{{ (userInfo.role_info && userInfo.role_info.role_name) || '员工角色' }}</text>
<text class="user-number">工号{{ userInfo.employee_number || '未设置' }}</text>
</view>
</view>
<!-- 九宫格操作区域 -->
<view class="grid-container">
<view class="grid-title">员工操作</view>
<view class="grid-content">
<view
class="grid-item"
v-for="(item, index) in gridItems"
:key="index"
@click="handleGridClick(item)"
>
<view class="grid-icon">
<uni-icons :type="item.icon" size="32" color="#29d3b4"></uni-icons>
</view>
<text class="grid-text">{{ item.title }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
userInfo: {},
gridItems: [
{
title: '客户资源',
icon: 'person-filled',
path: '/pages/market/clue/index'
},
{
title: '课程安排',
icon: 'calendar-filled',
path: '/pages/market/clue/class_arrangement'
},
{
title: '课程查询',
icon: 'search',
path: '/pages/coach/schedule/schedule_table'
},
{
title: '学员管理',
icon: 'contact-filled',
path: '/pages/coach/student/student_list'
},
{
title: '我的数据',
icon: 'bars',
path: '/pages/market/my/my_data'
},
{
title: '部门数据',
icon: 'staff',
path: '/pages/market/my/dept_data'
},
{
title: '校区数据',
icon: 'location-filled',
path: '/pages/market/my/campus_data'
},
{
title: '考勤管理',
icon: 'checkmarkempty',
path: '/pages/common/my_attendance'
},
{
title: '我的消息',
icon: 'chat-filled',
path: '/pages/common/my_message'
},
{
title: '报销管理',
icon: 'wallet-filled',
path: '/pages/market/reimbursement/list'
},
{
title: '资料库',
icon: 'folder-add-filled',
path: '/pages/coach/my/teaching_management'
}
]
}
},
onLoad() {
this.loadUserInfo();
},
methods: {
loadUserInfo() {
//
const userInfo = uni.getStorageSync('userInfo');
if (userInfo) {
this.userInfo = userInfo;
}
},
handleGridClick(item) {
uni.navigateTo({
url: item.path,
fail: (err) => {
console.error('页面跳转失败:', err);
uni.showToast({
title: '页面暂未开放',
icon: 'none'
});
}
});
}
}
}
</script>
<style scoped>
.home-container {
background-color: #181A20;
min-height: 100vh;
color: #fff;
}
.user-info-section {
display: flex;
align-items: center;
padding: 20px;
background: linear-gradient(135deg, #29d3b4 0%, #1a9b7c 100%);
margin: 20px;
border-radius: 12px;
}
.user-avatar {
width: 60px;
height: 60px;
margin-right: 15px;
}
.user-avatar image {
width: 100%;
height: 100%;
border-radius: 50%;
}
.user-details {
flex: 1;
display: flex;
flex-direction: column;
}
.user-name {
font-size: 18px;
font-weight: bold;
margin-bottom: 5px;
color: #fff;
}
.user-role {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 3px;
}
.user-number {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
.grid-container {
margin: 20px;
}
.grid-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #fff;
}
.grid-content {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.grid-item {
background-color: #292929;
border-radius: 8px;
padding: 20px 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 80px;
transition: all 0.3s ease;
}
.grid-item:active {
background-color: #3a3a3a;
transform: scale(0.95);
}
.grid-icon {
margin-bottom: 8px;
}
.grid-text {
font-size: 12px;
color: #fff;
text-align: center;
line-height: 1.2;
}
</style>

245
uniapp/pages/common/profile/index.vue

@ -0,0 +1,245 @@
<template>
<view class="profile-container">
<!-- 自定义导航栏 -->
<uni-nav-bar
:statusBar="true"
backgroundColor="#181A20"
color="#fff"
title="我的"
/>
<!-- 用户头像和基本信息 -->
<view class="profile-header">
<view class="avatar-section">
<image :src="userInfo.avatar || '/static/icon-img/tou.png'" mode="aspectFill"></image>
</view>
<view class="user-info">
<text class="user-name">{{ userInfo.name || '员工姓名' }}</text>
<text class="user-role">{{ (userInfo.role_info && userInfo.role_info.role_name) || '员工角色' }}</text>
<text class="user-phone">{{ userInfo.phone || '手机号码' }}</text>
</view>
</view>
<!-- 我的功能九宫格 -->
<view class="grid-container">
<view class="grid-title">个人中心</view>
<view class="grid-content">
<view
class="grid-item"
v-for="(item, index) in profileItems"
:key="index"
@click="handleProfileClick(item)"
>
<view class="grid-icon">
<uni-icons :type="item.icon" size="32" color="#29d3b4"></uni-icons>
</view>
<text class="grid-text">{{ item.title }}</text>
<text class="grid-desc" v-if="item.desc">{{ item.desc }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
userInfo: {},
profileItems: [
{
title: '我的资料',
icon: 'contact',
desc: '查看编辑个人信息',
action: 'viewProfile'
},
{
title: '我的合同',
icon: 'compose',
path: '/pages/parent/contracts/index'
},
{
title: '我的工资',
icon: 'wallet',
desc: '查看工资明细',
action: 'viewSalary'
},
{
title: '我的考勤',
icon: 'calendar',
path: '/pages/common/my_attendance'
},
{
title: '系统设置',
icon: 'settings',
path: '/pages/market/my/set_up'
}
]
}
},
onLoad() {
this.loadUserInfo();
},
onShow() {
this.loadUserInfo();
},
methods: {
loadUserInfo() {
//
const userInfo = uni.getStorageSync('userInfo');
if (userInfo) {
this.userInfo = userInfo;
}
},
handleProfileClick(item) {
if (item.action) {
//
switch (item.action) {
case 'viewProfile':
this.viewPersonalProfile();
break;
case 'viewSalary':
this.viewSalaryInfo();
break;
}
} else if (item.path) {
//
uni.navigateTo({
url: item.path,
fail: (err) => {
console.error('页面跳转失败:', err);
uni.showToast({
title: '页面暂未开放',
icon: 'none'
});
}
});
}
},
viewPersonalProfile() {
//
uni.showModal({
title: '个人资料',
content: '个人资料页面开发中,将包含employee_number、name、phone、email、address等字段的查看和编辑功能',
showCancel: false
});
},
viewSalaryInfo() {
//
uni.showModal({
title: '工资明细',
content: '工资明细页面开发中,将显示base_salary、performance_bonus、deductions、net_salary等字段,只能查看不能修改',
showCancel: false
});
}
}
}
</script>
<style scoped>
.profile-container {
background-color: #181A20;
min-height: 100vh;
color: #fff;
}
.profile-header {
display: flex;
align-items: center;
padding: 30px 20px;
background: linear-gradient(135deg, #29d3b4 0%, #1a9b7c 100%);
margin: 20px;
border-radius: 12px;
}
.avatar-section {
width: 80px;
height: 80px;
margin-right: 20px;
}
.avatar-section image {
width: 100%;
height: 100%;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.3);
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
}
.user-name {
font-size: 20px;
font-weight: bold;
margin-bottom: 8px;
color: #fff;
}
.user-role {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 5px;
}
.user-phone {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
.grid-container {
margin: 20px;
}
.grid-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #fff;
}
.grid-content {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.grid-item {
background-color: #292929;
border-radius: 8px;
padding: 20px 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 90px;
transition: all 0.3s ease;
}
.grid-item:active {
background-color: #3a3a3a;
transform: scale(0.95);
}
.grid-icon {
margin-bottom: 8px;
}
.grid-text {
font-size: 12px;
color: #fff;
text-align: center;
line-height: 1.2;
margin-bottom: 3px;
font-weight: bold;
}
.grid-desc {
font-size: 10px;
color: rgba(255, 255, 255, 0.6);
text-align: center;
line-height: 1.1;
}
</style>

449
uniapp/pages/market/clue/add_clues.vue

@ -24,7 +24,12 @@
<view class="form-style">
<fui-form ref="form" top="0" :model="formData" :show="false">
<view class="title" style="margin-top: 20rpx;">基础信息</view>
<view class="title" style="margin-top: 20rpx; display: flex; justify-content: space-between; align-items: center;">
<text>基础信息</text>
<view class="quick-fill-btn" @click="openQuickFill">
<text style="color: #29d3b4; font-size: 24rpx;">快速填写</text>
</view>
</view>
<view class="input-style">
<!--姓名-->
<fui-form-item
@ -130,27 +135,6 @@
</view>
</fui-form-item>
<!--顾问-->
<!--
<fui-form-item
label="顾问"
labelSize='26'
prop=""
background='#434544'
labelColor='#fff'
:bottomBorder='false'
>
<view class="input-title" style="margin-right:14rpx;">
<view
class="input-title"
style="margin-right:14rpx;"
@click="openCicker(`consultant`)">
{{ (formData.consultant) ? picker_config.consultant.text : '点击选择' }}
</view>
</view>
</fui-form-item>
-->
<fui-form-item
v-show="false"
label="顾问"
@ -173,43 +157,26 @@
</view>
</fui-form-item>
<!--年龄-->
<!--生日-->
<fui-form-item
label="年龄"
label="生日"
labelSize='26'
prop="age"
prop="birthday"
background='#434544'
labelColor='#fff'
:bottomBorder='false'>
<view class="input-title" style="margin-right:14rpx;">
<fui-input-number signColor="#FFF" :min="3" :max="18" v-model="formData.age"></fui-input-number>
</view>
</fui-form-item>
<!--性别-->
<fui-form-item
label="性别"
labelSize='26'
prop="age"
background='#434544'
labelColor='#fff'
:bottomBorder='false'>
<view class="input-title" style="margin-right:14rpx;">
<fui-radio-group name="radio" v-model="formData.gender" @change="changeSex">
<view class="fui-list__item" style="display: flex;justify-content: flex-end;">
<fui-label>
<view class="fui-align__center">
<fui-radio value="male" checked></fui-radio>
<text class="fui-text"></text>
</view>
</fui-label>
<fui-label :margin="['0','0','0','40rpx']">
<view class="fui-align__center">
<fui-radio value="female"></fui-radio>
<text class="fui-text"></text>
<view @click="picker_show_birthday = true">
{{ formData.birthday ? formData.birthday : '请选择生日' }}
</view>
</fui-label>
</view>
</fui-radio-group>
<fui-date-picker
:show="picker_show_birthday"
type="3"
:minDate="minDate"
:maxDate="maxDate"
@change="changePickerBirthday"
@cancel="picker_show_birthday = false"
></fui-date-picker>
</view>
</fui-form-item>
@ -468,6 +435,16 @@
<!--数据列表-->
<view class="ul" v-else>
<view class="duplicate-notice">
<text style="color: #ff6b35; font-size: 28rpx; margin-bottom: 20rpx;">检测到手机号重复是否为现有用户添加新学员</text>
<fui-button
background="#29d3b4"
color="#fff"
@click="confirmAddStudent"
style="margin: 20rpx 0;">
确认添加学员
</fui-button>
</view>
<view class="li" v-for="(v,k) in clientUserList" :key="k" @click="openViewClueInfo(v)">
<view class="left_box">
<view class="box_1">
@ -527,6 +504,34 @@
</view>
</fui-bottom-popup>
<!-- 快速填写弹窗 -->
<view v-if="showQuickFill" class="quick-fill-mask" @tap="showQuickFill=false">
<view class="quick-fill-content" @tap.stop>
<view class="quick-fill-header">
<view class="quick-fill-title">快速填写</view>
<view class="quick-fill-close" @tap="showQuickFill=false">
<text class="close-text"></text>
</view>
</view>
<view class="quick-fill-body">
<view class="quick-fill-tip">
<text>请粘贴包含客户信息的文本支持格式</text>
<text>姓名张三电话13800138000校区测试校区</text>
</view>
<textarea
class="quick-fill-textarea"
placeholder="请粘贴客户信息文本..."
v-model="quickFillText"
:maxlength="1000"
></textarea>
</view>
<view class="quick-fill-buttons">
<view class="quick-fill-btn cancel-btn" @click="showQuickFill=false">取消</view>
<view class="quick-fill-btn confirm-btn" @click="parseQuickFillText">解析并填写</view>
</view>
</view>
</view>
</view>
</template>
@ -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
}
//IDuser_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;
}
}
</style>

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

File diff suppressed because it is too large

336
uniapp/pages/market/clue/edit_clues.vue

@ -9,10 +9,12 @@
<view v-if="optionTableId == 0" style="margin-top: 20rpx;">
<view class="form-style">
<fui-form ref="form" top="0" :model="formData" :show="false">
<view class="title" style="margin-top: 20rpx;">基础信息</view>
<view class="title" style="margin-top: 20rpx; display: flex; justify-content: space-between; align-items: center;">
<text>基础信息</text>
</view>
<view class="input-style">
<!-- 校区 -->
<fui-form-item label="校区" labelSize='26' prop="campus" background='#434544' labelColor='#fff' :bottomBorder='false'>
<fui-form-item label="校区" labelSize='32' prop="campus" background='#434544' labelColor='#fff' :bottomBorder='false'>
<view class="input-title" style="margin-right:14rpx;">
<view class="input-title" style="margin-right:14rpx;" @click="openCicker('campus')">
{{ formData.campus ? picker_config.campus.text : '点击选择' }}
@ -69,10 +71,12 @@
</fui-radio-group>
</view>
</fui-form-item>
<!-- 年龄 -->
<fui-form-item label="年龄" labelSize='26' prop="age" background='#434544' labelColor='#fff' :bottomBorder='false'>
<!-- 生日 -->
<fui-form-item label="生日" labelSize='26' prop="birthday" background='#434544' labelColor='#fff' :bottomBorder='false'>
<view class="input-title" style="margin-right:14rpx;">
<fui-input-number signColor="#FFF" :min="3" :max="100" v-model="formData.age"></fui-input-number>
<view class="input-title" style="margin-right:14rpx;" @click="openDate('birthday')">
{{ formData.birthday || '请选择生日' }}
</view>
</view>
</fui-form-item>
<!-- 电话 -->
@ -305,6 +309,34 @@
<fui-date-picker :show="date_picker_show" type="3" @change="change_date" @cancel="cancel_date" :value="default_date_value"></fui-date-picker>
<fui-picker :linkage='picker_linkage' :options="picker_options" :layer="1" :show="picker_show" @change="changeCicker" @cancel="cancelCicker"></fui-picker>
<!-- 快速填写弹窗 -->
<view v-if="showQuickFill" class="quick-fill-mask" @tap="showQuickFill=false">
<view class="quick-fill-content" @tap.stop>
<view class="quick-fill-header">
<view class="quick-fill-title">快速填写</view>
<view class="quick-fill-close" @tap="showQuickFill=false">
<text class="close-text"></text>
</view>
</view>
<view class="quick-fill-body">
<view class="quick-fill-tip">
<text>请粘贴包含客户信息的文本支持格式</text>
<text>姓名张三电话13800138000校区测试校区</text>
</view>
<textarea
class="quick-fill-textarea"
placeholder="请粘贴客户信息文本..."
v-model="quickFillText"
:maxlength="1000"
></textarea>
</view>
<view class="quick-fill-buttons">
<view class="quick-fill-btn cancel-btn" @click="showQuickFill=false">取消</view>
<view class="quick-fill-btn confirm-btn" @click="parseQuickFillText">解析并填写</view>
</view>
</view>
</view>
<!-- 底部保存按钮 -->
<view class="save-btn-box">
<fui-button background="#434544" color="#24BA9F" borderColor="#24BA9F" @click="submit">保存</fui-button>
@ -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]));
try {
// 使
console.log('调用批量字典接口,keys:', dictKeys);
const batchResult = await apiRoute.common_getBatchDict(dictKeys);
//
for (const dictKey of criticalDicts) {
const key = Object.keys(dictMapping).find(k => dictMapping[k] === dictKey);
if (key) {
await this.loadDictData(key, dictMapping[key]);
}
}
if (batchResult && batchResult.code === 1 && batchResult.data) {
console.log('批量字典接口响应成功:', batchResult.data);
//
setTimeout(() => {
regularDicts.forEach(async (key) => {
//
Object.keys(batchResult.data).forEach(key => {
const localKey = dictMapping[key];
if (!window._dictCache[key]) {
await this.loadDictData(key, localKey);
if (localKey && Array.isArray(batchResult.data[key])) {
this.processDictData(localKey, batchResult.data[key]);
}
});
}, 100);
console.log('优化的批量字典数据加载初始化完成');
console.log('批量字典数据处理完成');
return;
} else {
console.warn('批量字典接口返回数据格式异常:', batchResult);
}
} catch (batchError) {
console.error('批量字典接口调用失败:', batchError);
}
// 退
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 || '', //
@ -1351,6 +1403,113 @@
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;
}
</style>

819
uniapp/pages/market/clue/index.vue

@ -6,12 +6,14 @@
<!--我的客户-->
<scroll-view v-if="segmented_type == 1" scroll-y="true" :lower-threshold="lowerThreshold"
@scrolltolower="loadMoreData_1" style="height: 100vh;">
<!-- 搜索按钮区域 -->
<view class="search_section">
<view class="item">
<view class="input_box" @click="openShowDrawer()">
<view class="input_box" @click="openSearchPopup">
<view class="input_box_text">校区/用户名/手机号/时间范围</view>
</view>
<view class="button" @click="openShowDrawer()">搜索</view>
<view class="button" @click="openSearchPopup">搜索</view>
</view>
</view>
<view class="card" v-for="(v,k) in tableList_1" :key="k">
@ -36,6 +38,19 @@
</view>
</view>
<view class="card-right">
<!-- 开单状态标签 -->
<view :class="['status-tag',getOrderStatusClass(v.customerResource.order_status)]">
{{ v.customerResource.order_status || '未开单' }}
</view>
<!-- 到访状态标签 -->
<view class="visit-status">
<view :class="['visit-tag-',getVisitStatusClass(v.customerResource.first_visit_status)]">
一访{{ v.customerResource.first_visit_status || '未到' }}
</view>
<view :class="['visit-tag-',getVisitStatusClass(v.customerResource.second_visit_status)]">
二访{{ v.customerResource.second_visit_status || '未到' }}
</view>
</view>
<!--只有注册了member表的账号才可操作IM对话-->
<view class="btn-item" v-if="v.customerResource.member_id" @click.stop>
<image :src="$util.img('/uniapp_src/static/images/index/message.png')" class="image"
@ -81,16 +96,36 @@
@scrolltolower="loadMoreData_2" style="height: 100vh;">
<view class="search_section">
<view class="item">
<view class="input_box" @click="openShowDrawer()">
<view class="input_box" @click="openSearchPopup()">
<view class="input_box_text">校区/用户名/手机号/时间范围</view>
</view>
<view class="button" @click="openShowDrawer()">搜索</view>
<view class="button" @click="openSearchPopup()">搜索</view>
</view>
<!-- 批量操作控制栏 -->
<view class="batch_control_bar">
<view class="batch_actions_left">
<view v-if="!batchMode" class="batch_toggle_btn" @click="enterBatchMode()">批量操作</view>
<view v-if="batchMode" class="batch_toggle_btn cancel" @click="exitBatchMode()">取消</view>
<view v-if="batchMode" class="batch_select_all" @click="toggleSelectAll()">
<text class="checkbox" :class="{ checked: isAllSelected }"></text>
<text class="select_all_text">全选</text>
</view>
</view>
<view class="card" v-for="(v,k) in tableList_2" :key="k">
<view v-if="batchMode && selectedItems.length > 0" class="batch_actions_right">
<view class="batch_assign_btn" @click="batchAssign()">
批量分配 ({{ selectedItems.length }})
</view>
</view>
</view>
</view>
<view class="card" v-for="(v,k) in tableList_2" :key="k" @click="batchMode ? toggleItemSelection(v) : null">
<view class="card-content">
<view class="card-left">
<view style="display: flex;align-items: center;padding: 12rpx;">
<!-- 批量选择复选框 -->
<view v-if="batchMode" class="batch_checkbox" @click.stop="toggleItemSelection(v)">
<text class="checkbox" :class="{ checked: selectedItems.includes(v.id) }"></text>
</view>
<view>
<image :src="$util.img('/uniapp_src/static/images/index/myk.png')" class="card-image">
</image>
@ -102,6 +137,9 @@
决策人{{v.customerResource.name}} <span
class="card-con-span">{{v.customerResource.decision_maker}}</span>
</view>
<view class="card-con">
分配给<span class="assigned-to" :class="v.shared_by_name === '未分配' ? 'unassigned' : 'assigned'">{{v.shared_by_name}}</span>
</view>
</view>
<view class="card-right">
<view class="btn-item" @click.stop="openAssign(v)">
@ -138,13 +176,115 @@
</view>
</scroll-view>
<!-- 顶部弹出搜索组件 -->
<view v-if="showSearchPopup" class="search_popup_mask" @tap="showSearchPopup=false">
<view class="search_popup_content" @tap.stop>
<view class="popup_search_content">
<view class="popup_header">
<view class="popup_title">筛选</view>
<view class="popup_close" @tap="showSearchPopup=false">
<text class="close_text"></text>
</view>
</view>
<scroll-view :scroll-y="true" class="popup_scroll_view">
<!-- 第一筛选区域 -->
<view class="popup_filter_section">
<view class="popup_filter_row">
<view class="popup_filter_item">
<text class="popup_filter_label">校区筛选</text>
<input class="popup_filter_input" placeholder="校区筛选" v-model="searchForm.campus_name" />
</view>
<view class="popup_filter_item">
<text class="popup_filter_label">姓名</text>
<input class="popup_filter_input" placeholder="姓名" v-model="searchForm.name" />
</view>
</view>
<view class="popup_filter_row">
<view class="popup_filter_item">
<text class="popup_filter_label">电话</text>
<input class="popup_filter_input" placeholder="电话" v-model="searchForm.phone_number" />
</view>
<view class="popup_filter_item">
<text class="popup_filter_label">来源</text>
<picker :value="sourceIndex" :range="sourceOptions" @change="onSourceChange">
<view class="popup_filter_picker">{{ sourceOptions[sourceIndex] }}</view>
</picker>
</view>
</view>
<view class="popup_filter_row" v-if="sourceIndex === 1">
<view class="popup_filter_item full_width">
<text class="popup_filter_label">来源渠道</text>
<input class="popup_filter_input" placeholder="来源渠道" v-model="searchForm.source_channel" />
</view>
</view>
</view>
<!-- 第二筛选区域 -->
<view class="popup_filter_section">
<view class="popup_filter_row">
<view class="popup_filter_item">
<text class="popup_filter_label">课程检索</text>
<input class="popup_filter_input" placeholder="课程检索" v-model="searchForm.course_search" />
</view>
<view class="popup_filter_item">
<text class="popup_filter_label">到课类型</text>
<picker :value="attendanceIndex" :range="attendanceOptions" @change="onAttendanceChange">
<view class="popup_filter_picker">{{ attendanceOptions[attendanceIndex] }}</view>
</picker>
</view>
</view>
<view class="popup_filter_row">
<view class="popup_filter_item">
<text class="popup_filter_label">成交类型</text>
<picker :value="dealIndex" :range="dealOptions" @change="onDealChange">
<view class="popup_filter_picker">{{ dealOptions[dealIndex] }}</view>
</picker>
</view>
<view class="popup_filter_item">
<text class="popup_filter_label">资源有效类型</text>
<picker :value="validIndex" :range="validOptions" @change="onValidChange">
<view class="popup_filter_picker">{{ validOptions[validIndex] }}</view>
</picker>
</view>
</view>
<view class="popup_filter_row">
<view class="popup_filter_item">
<text class="popup_filter_label">沟通情况</text>
<picker :value="communicationIndex" :range="communicationOptions" @change="onCommunicationChange">
<view class="popup_filter_picker">{{ communicationOptions[communicationIndex] }}</view>
</picker>
</view>
<view class="popup_filter_item">
<text class="popup_filter_label">时间</text>
<view class="popup_filter_input" @click="openDatePicker">
{{ searchForm.time_range || '开始时间-结束时间' }}
</view>
</view>
</view>
</view>
</scroll-view>
<view class="popup_filter_buttons">
<view class="popup_filter_btn reset_btn" @click="resetSearchOnly">重置</view>
<view class="popup_filter_btn search_btn" @click="searchDataAndClose">搜索</view>
<view class="popup_filter_btn close_btn" @click="closeSearchPopup">关闭</view>
</view>
</view>
</view>
</view>
<!--下拉选择器-->
<fui-select :show="select_show" :options="select_options" title="请选择员工" @confirm="getSales"
@close="closeAssign"></fui-select>
<!-- 时间范围选择器-->
<fui-date-picker range :show="date_picker_show" type="3" @change="changeDatePicker" :value="selectedDate"
@cancel="cancelDatePicker"></fui-date-picker>
@cancel="cancelDatePicker" :z-index="1500"></fui-date-picker>
<!-- 我的客户搜索条件栏目-->
<fui-drawer :show="showDrawer" direction="left" :maskClosable="true">
@ -222,6 +362,35 @@
lowerThreshold: 100, //
isReachedBottom: false, //|true=|false=
selectedDate: this.getCurrentDate(), //
//
showSearchPopup: false, //
tempHideSearchPopup: false, //
searchForm: {
campus_name: '',
name: '',
phone_number: '',
source: '',
source_channel: '',
course_search: '',
attendance_type: '',
deal_type: '',
valid_type: '',
communication_status: '',
time_range: ''
},
//
sourceIndex: 0,
sourceOptions: ['全部', '线上', '线下'],
attendanceIndex: 0,
attendanceOptions: ['全部', '一访未到', '一访已到', '二访未到', '二访已到', '未到访'],
dealIndex: 0,
dealOptions: ['全部', '已成交', '未成交'],
validIndex: 0,
validOptions: ['全部', '有效', '无效'],
communicationIndex: 0,
communicationOptions: ['全部', '已沟通', '未沟通'],
//
filteredData: {
page: 1, //
@ -252,9 +421,15 @@
total: 10, //
shared_by: '', //ID|0=
shared_at_str: '', //|(Y-m-d)-(Y-m-d)
shared_at_arr: [], //
phone_number: '', //-
name: '', //-
campus_name: '',
campus_name: '', //
source: '', //
source_channel: '', //
attendance_type: '', //
deal_type: '', //
valid_type: '', //
},
//
tableList_1: [], //
@ -274,6 +449,11 @@
//
tableList_2: [], //
//
batchMode: false, //
selectedItems: [], // ID
isAllSelected: false, //
//select
select_show: false, //
select_item: {}, //
@ -613,13 +793,20 @@
checked: false
}));
},
//-
//-
async getSales(e) {
let select_item = {
...this.select_item
}
console.log('选中', e, select_item)
this.closeAssign() //
//
if (select_item.isBatch && select_item.selectedIds) {
//
await this.batchAssignResources(select_item.selectedIds, e.options.value)
} else {
//
let param = {
resource_sharing_id: select_item.id, //id
shared_by: e.options.value, //id
@ -645,23 +832,81 @@
}
this.segmented(param)
}, 1000)
}
},
//
async batchAssignResources(selectedIds, assigneeId) {
let successCount = 0
let failCount = 0
for (let resourceId of selectedIds) {
try {
let param = {
resource_sharing_id: resourceId,
shared_by: assigneeId
}
let res = await apiRoute.xs_resourceSharingAssign(param)
if (res.code == 1) {
successCount++
} else {
failCount++
}
} catch (error) {
failCount++
}
}
//
if (failCount === 0) {
uni.showToast({
title: `成功分配 ${successCount} 个资源`,
icon: 'success'
})
} else {
uni.showToast({
title: `成功分配 ${successCount} 个,失败 ${failCount}`,
icon: 'none'
})
}
// 退
this.exitBatchMode()
setTimeout(() => {
let param = {
id: 2,
index: 1,
name: "资源分配",
}
this.segmented(param)
}, 1000)
},
//
//-
changeDatePicker(e) {
console.log('时间', e)
let shared_at_str = `${e.startDate.result} ~ ${e.endDate.result}`
this.showDrawerForm.shared_at_str = shared_at_str
let time_range = `${e.startDate.result} ~ ${e.endDate.result}`
this.searchForm.time_range = time_range
this.showDrawerForm.shared_at_str = time_range
this.cancelDatePicker()
},
//
openDatePicker() {
console.log('打开时间选择器')
this.date_picker_show = true
//
this.tempHideSearchPopup = this.showSearchPopup
this.showSearchPopup = false
},
//
cancelDatePicker() {
this.date_picker_show = false
//
if (this.tempHideSearchPopup) {
this.showSearchPopup = true
this.tempHideSearchPopup = false
}
},
//
@ -693,6 +938,225 @@
//
closeShowDrawer() {
this.showDrawer = false
},
//
//
openSearchPopup() {
console.log('打开搜索弹窗', this.showSearchPopup)
this.showSearchPopup = true
console.log('设置后的状态', this.showSearchPopup)
},
//
searchDataAndClose() {
console.log('执行搜索,表单数据:', this.searchForm)
this.searchDataFromPopup()
this.showSearchPopup = false
},
//
async searchDataFromPopup() {
//
const currentFilterData = this.segmented_type == 1 ? this.filteredData_1 : this.filteredData_2
//
currentFilterData.campus_name = this.searchForm.campus_name
currentFilterData.name = this.searchForm.name
currentFilterData.phone_number = this.searchForm.phone_number
// shared_at_arr
if (this.searchForm.time_range && this.searchForm.time_range.includes(' ~ ')) {
const timeArray = this.searchForm.time_range.split(' ~ ')
currentFilterData.shared_at_arr = timeArray
currentFilterData.shared_at_str = this.searchForm.time_range
} else {
currentFilterData.shared_at_arr = []
currentFilterData.shared_at_str = ''
}
//
if (this.searchForm.source && this.searchForm.source !== '全部') {
currentFilterData.source = this.searchForm.source
}
if (this.searchForm.source_channel) {
currentFilterData.source_channel = this.searchForm.source_channel
}
if (this.searchForm.attendance_type && this.searchForm.attendance_type !== '全部') {
currentFilterData.attendance_type = this.searchForm.attendance_type
}
if (this.searchForm.deal_type && this.searchForm.deal_type !== '全部') {
currentFilterData.deal_type = this.searchForm.deal_type
}
if (this.searchForm.valid_type && this.searchForm.valid_type !== '全部') {
currentFilterData.valid_type = this.searchForm.valid_type
}
console.log('映射后的筛选数据:', currentFilterData)
//
if (this.segmented_type == 1) {
//
await this.resetFilteredData_1()
await this.getList_1()
} else {
//
await this.resetFilteredData_2()
await this.getList_2()
}
},
//
resetSearchAndClose() {
this.resetSearch()
this.showSearchPopup = false
},
//
resetSearchOnly() {
this.resetSearch()
},
//
closeSearchPopup() {
this.showSearchPopup = false
},
//
onSourceChange(e) {
this.sourceIndex = e.detail.value
this.searchForm.source = this.sourceOptions[this.sourceIndex]
// 线
if (this.sourceIndex !== 1) {
this.searchForm.source_channel = ''
}
},
//
onAttendanceChange(e) {
this.attendanceIndex = e.detail.value
this.searchForm.attendance_type = this.attendanceOptions[this.attendanceIndex]
},
//
onDealChange(e) {
this.dealIndex = e.detail.value
this.searchForm.deal_type = this.dealOptions[this.dealIndex]
},
//
onValidChange(e) {
this.validIndex = e.detail.value
this.searchForm.valid_type = this.validOptions[this.validIndex]
},
//
onCommunicationChange(e) {
this.communicationIndex = e.detail.value
this.searchForm.communication_status = this.communicationOptions[this.communicationIndex]
},
//
async resetSearch() {
this.searchForm = {
campus_name: '',
name: '',
phone_number: '',
source: '',
source_channel: '',
course_search: '',
attendance_type: '',
deal_type: '',
valid_type: '',
communication_status: '',
time_range: ''
}
this.sourceIndex = 0
this.attendanceIndex = 0
this.dealIndex = 0
this.validIndex = 0
this.communicationIndex = 0
//
if (this.segmented_type == 1) {
//
await this.resetFilteredData_1()
await this.getList_1()
} else {
//
await this.resetFilteredData_2()
await this.getList_2()
}
},
//
getOrderStatusClass(status) {
return status === '已开单' ? 'status-closed' : 'status-open'
},
// 访
getVisitStatusClass(status) {
return status === '已到' ? 'visit-arrived' : 'visit-not-arrived'
},
//
//
enterBatchMode() {
this.batchMode = true
this.selectedItems = []
this.isAllSelected = false
},
// 退
exitBatchMode() {
this.batchMode = false
this.selectedItems = []
this.isAllSelected = false
},
//
toggleItemSelection(item) {
const index = this.selectedItems.indexOf(item.id)
if (index > -1) {
this.selectedItems.splice(index, 1)
} else {
this.selectedItems.push(item.id)
}
this.updateAllSelectedState()
},
//
toggleSelectAll() {
if (this.isAllSelected) {
this.selectedItems = []
this.isAllSelected = false
} else {
this.selectedItems = this.tableList_2.map(item => item.id)
this.isAllSelected = true
}
},
//
updateAllSelectedState() {
this.isAllSelected = this.selectedItems.length === this.tableList_2.length && this.tableList_2.length > 0
},
//
batchAssign() {
if (this.selectedItems.length === 0) {
uni.showToast({
title: '请先选择要分配的资源',
icon: 'none'
})
return
}
//
const firstSelectedItem = this.tableList_2.find(item => this.selectedItems.includes(item.id))
if (firstSelectedItem) {
this.getPersonnelAll(firstSelectedItem.customerResource.campus)
this.select_item = { isBatch: true, selectedIds: this.selectedItems }
this.select_show = true
}
}
}
}
@ -732,6 +1196,193 @@
}
}
//
.search_popup_mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 999;
display: flex;
flex-direction: column;
}
.search_popup_content {
background: #fff;
border-bottom-left-radius: 24rpx;
border-bottom-right-radius: 24rpx;
animation: slideDown 0.3s ease-out;
width: 100%;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
@keyframes slideDown {
from {
transform: translateY(-100%);
}
to {
transform: translateY(0);
}
}
//
.popup_search_content {
padding: 0;
background: #fff;
min-height: 60vh;
max-height: 80vh;
display: flex;
flex-direction: column;
border-bottom-left-radius: 24rpx;
border-bottom-right-radius: 24rpx;
overflow: hidden;
}
.popup_header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 1px solid #f0f0f0;
}
.popup_title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.popup_close {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.close_text {
font-size: 32rpx;
color: #999;
}
}
.popup_scroll_view {
flex: 1;
padding: 32rpx;
overflow-y: auto;
}
.popup_filter_section {
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
}
.popup_filter_row {
display: flex;
gap: 20rpx;
margin-bottom: 24rpx;
&:last-child {
margin-bottom: 0;
}
}
.popup_filter_item {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
&.full_width {
flex: 1;
}
.popup_filter_label {
font-size: 26rpx;
color: #666;
font-weight: 500;
}
.popup_filter_input {
height: 72rpx;
line-height: 72rpx;
padding: 0 16rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
background: #fff;
&::placeholder {
color: #999;
}
}
.popup_filter_picker {
height: 72rpx;
line-height: 72rpx;
padding: 0 16rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
background: #fff;
position: relative;
&::after {
content: '▼';
position: absolute;
right: 16rpx;
font-size: 20rpx;
color: #999;
}
}
}
.popup_filter_buttons {
display: flex;
gap: 20rpx;
padding: 32rpx;
margin-top: auto;
border-top: 1px solid #f0f0f0;
background: #fff;
border-bottom-left-radius: 24rpx;
border-bottom-right-radius: 24rpx;
}
.popup_filter_btn {
flex: 1;
height: 72rpx;
line-height: 72rpx;
text-align: center;
border-radius: 8rpx;
font-size: 28rpx;
font-weight: 600;
&.search_btn {
background: #29d3b4;
color: #fff;
}
&.reset_btn {
background: #f5f5f5;
color: #666;
border: 1px solid #ddd;
}
&.close_btn {
background: #666;
color: #fff;
}
}
.search {
width: 92%;
margin: auto;
@ -779,9 +1430,53 @@
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
justify-content: flex-start;
padding: 12rpx;
//
.status-tag {
font-size: 20rpx;
padding: 8rpx 16rpx;
border-radius: 16rpx;
color: #fff;
margin-bottom: 8rpx;
text-align: center;
min-width: 60rpx;
&.status-closed {
background-color: #52c41a; // 绿-
}
&.status-open {
background-color: #ff4d4f; // -
}
}
// 访
.visit-status {
display: flex;
flex-direction: column;
gap: 6rpx;
margin-bottom: 12rpx;
.visit-tag {
font-size: 18rpx;
padding: 6rpx 12rpx;
border-radius: 12rpx;
color: #fff;
text-align: center;
min-width: 60rpx;
&.visit-arrived {
background-color: #1890ff; // -
}
&.visit-not-arrived {
background-color: #8c8c8c; // -
}
}
}
.btn-item {
margin-bottom: 12rpx;
display: flex;
@ -956,4 +1651,106 @@
font-size: 16px;
border-radius: 5px;
}
/* 批量操作样式 - 优化为黑色主题 */
.batch_control_bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 24rpx;
background: rgba(41, 41, 41, 0.95);
border-top: 1px solid #404040;
margin-top: 20rpx;
border-radius: 12rpx;
backdrop-filter: blur(10rpx);
}
.batch_actions_left {
display: flex;
align-items: center;
gap: 24rpx;
}
.batch_toggle_btn {
padding: 16rpx 32rpx;
border-radius: 24rpx;
font-size: 28rpx;
background: linear-gradient(135deg, #29d3b4, #26c3a4);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(41, 211, 180, 0.3);
transition: all 0.3s ease;
&.cancel {
background: linear-gradient(135deg, #666, #555);
box-shadow: 0 4rpx 12rpx rgba(102, 102, 102, 0.3);
}
}
.batch_select_all {
display: flex;
align-items: center;
gap: 12rpx;
padding: 8rpx 0;
}
.checkbox {
width: 36rpx;
height: 36rpx;
border: 2rpx solid #666;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: transparent;
background: #404040;
transition: all 0.3s ease;
&.checked {
background: linear-gradient(135deg, #29d3b4, #26c3a4);
border-color: #29d3b4;
color: #fff;
box-shadow: 0 2rpx 8rpx rgba(41, 211, 180, 0.4);
}
}
.select_all_text {
font-size: 28rpx;
color: #fff;
font-weight: 500;
}
.batch_actions_right {
display: flex;
align-items: center;
}
.batch_assign_btn {
padding: 16rpx 32rpx;
border-radius: 24rpx;
font-size: 28rpx;
background: linear-gradient(135deg, #ff6b35, #ff5722);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3);
font-weight: 600;
transition: all 0.3s ease;
}
.batch_checkbox {
margin-right: 20rpx;
padding: 8rpx;
}
/* 分配状态样式 */
.assigned-to {
font-weight: 600;
&.unassigned {
color: #ff6b35;
}
&.assigned {
color: #29d3b4;
}
}
</style>

101
uniapp/pages/student/login/login.vue

@ -58,43 +58,28 @@
password1: '', //
mini_wx_openid: '', //openid
loginType: '', //|1=,2=,3=,4=,5=
loginType: '', //|staff=,member=
loginType_str: '', //
loginType_Arr: [{
value: '1',
text: '市场登陆'
value: 'staff',
text: '员工登录'
},
{
value: '2',
text: '教练登陆'
},
{
value: '3',
text: '销售登陆'
},
{
value: '2',
text: '教务登陆'
},
{
value: '4',
text: '学员登陆'
value: 'member',
text: '学员登录'
},
],
picker_show_loginType: false, //
path_arr: {
'1': '/pages/market/home/index', //
'2': '/pages/coach/home/index', //
'3': '/pages/market/index/index', //
'4': '/pages/student/index/index', //
'5': '/pages/academic/home/index', //
'staff': '/pages/common/home/index', //
'member': '/pages/student/index/index', //
},
}
},
onLoad(options) {
this.loginType = options.loginType ?? '1' //|1=,2=,3=,4=,5=
this.loginType = options.loginType ?? 'staff' //|staff=,member=
const selectedItem = this.loginType_Arr.find(item => item.value === String(this.loginType));
this.loginType_str = selectedItem ? selectedItem.text : '未知类型';
@ -145,20 +130,22 @@
return;
}
// 使
const params = {
phone: this.user,
username: this.user,
password: this.password1,
login_type: this.loginType,
mini_wx_openid: this.mini_wx_openid //openid
login_type: this.loginType // 使 'staff' 'member'
};
console.log('登录参数:', params);
let res;
if (this.loginType != 4) {
//
res = await apiRoute.personnelLogin(params);
} else {
// -
if (this.loginType === 'member') {
//
try {
res = await apiRoute.unifiedLogin(params);
} catch (error) {
console.log('学员登录失败,跳转到家长端用户信息页面');
uni.showToast({
title: '登录成功',
icon: 'success'
@ -166,7 +153,7 @@
// token
uni.setStorageSync("token", "mock_token_" + Date.now());
uni.setStorageSync("userType", "4");
uni.setStorageSync("userType", "member");
uni.setStorageSync("userInfo", {
id: 1001,
name: this.user,
@ -180,30 +167,47 @@
});
return;
}
} else {
//
res = await apiRoute.unifiedLogin(params);
}
if (res && res.code === 1) { // 1
//
if (res.data && res.data.token) {
// Token
uni.setStorageSync("token", res.data.token);
//
if (res.data.user_type) {
uni.setStorageSync("userType", res.data.user_type);
//
if (res.data.user_info) {
uni.setStorageSync('userInfo', res.data.user_info);
// userType
uni.setStorageSync("userType", this.loginType);
}
//
if (res.data.expires_time) {
uni.setStorageSync("expires_time", res.data.expires_time);
//
if (res.data.role_info) {
uni.setStorageSync('roleInfo', res.data.role_info);
}
//
if (res.data.userinfo){
uni.setStorageSync('userInfo',res.data.userinfo)
//
if (res.data.menu_list) {
uni.setStorageSync('menuList', res.data.menu_list);
}
console.log('Token保存成功:', res.data.token.substring(0, 20) + '...');
console.log('用户类型保存成功:', this.loginType);
}
uni.showToast({
title:'登录成功',
icon: 'success'
});
//
setTimeout(() => {
this.openViewHome();
}, 500);
} else {
uni.showToast({
title: res.msg || '登录失败',
@ -294,13 +298,24 @@
//
uni.setStorageSync('tabBerIndex', 0)
// 使 redirectTo navigateTo
// tabBar使
if (openPath === '/pages/common/home/index' || openPath === '/pages/common/profile/index') {
// tabBar使switchTab
uni.switchTab({
url: openPath,
complete(e) {
console.log('switchTab result:', e)
}
});
} else {
// tabBar使redirectTo
uni.redirectTo({
url: openPath,
complete(e) {
console.log(e)
console.log('redirectTo result:', e)
}
});
}
return
}
// onShow

BIN
uniapp/static/icon-img/home-active.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
uniapp/static/icon-img/home.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
uniapp/static/icon-img/profile-active.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
uniapp/static/icon-img/profile.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Loading…
Cancel
Save