From 564ffb46e7b012e825dcdbac9096371f51eee4e8 Mon Sep 17 00:00:00 2001 From: zeyan <258785420@qq.com> Date: Tue, 1 Jul 2025 17:04:38 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=90=88=E5=90=8C?= =?UTF-8?q?=E7=AD=BE=E8=AE=A2=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../personnel/personnel_approval_demo.vue | 400 ++++++++ .../controller/personnel/Personnel.php | 30 +- niucloud/app/adminapi/route/personnel.php | 2 + .../controller/apiController/Personnel.php | 74 ++ niucloud/app/api/controller/common/Dict.php | 155 ++++ niucloud/app/api/route/route.php | 17 + niucloud/app/model/personnel/Personnel.php | 5 + .../app/model/service_logs/ServiceLogs.php | 48 +- niucloud/app/model/sys/SysDict.php | 82 ++ .../service/api/apiService/ServiceService.php | 160 ++++ .../app/service/api/common/DictService.php | 297 ++++++ .../SchoolApprovalProcessService.php | 118 ++- uniapp/common/axiosQuiet.js | 85 ++ uniapp/common/dictUtil.js | 332 +++++++ uniapp/common/dictUtilSimple.js | 187 ++++ uniapp/pages.json | 3 +- uniapp/pages/coach/my/service_detail.vue | 854 +++++++++++++++--- uniapp/pages/demo/dict_optimization.vue | 542 +++++++++++ uniapp/pages/market/clue/add_clues.vue | 114 ++- uniapp/pages/test/dict_test.vue | 263 ++++++ 21 files changed, 3622 insertions(+), 147 deletions(-) create mode 100644 admin/src/views/personnel/personnel_approval_demo.vue create mode 100644 niucloud/app/api/controller/common/Dict.php create mode 100644 niucloud/app/model/sys/SysDict.php create mode 100644 niucloud/app/service/api/apiService/ServiceService.php create mode 100644 niucloud/app/service/api/common/DictService.php create mode 100644 uniapp/common/axiosQuiet.js create mode 100644 uniapp/common/dictUtil.js create mode 100644 uniapp/common/dictUtilSimple.js create mode 100644 uniapp/pages/demo/dict_optimization.vue create mode 100644 uniapp/pages/test/dict_test.vue diff --git a/.gitignore b/.gitignore index 570c8aa1..de10cf51 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /niucloud/runtime /niucloud/vendor /CLAUDE.md +.claude \ No newline at end of file diff --git a/admin/src/views/personnel/personnel_approval_demo.vue b/admin/src/views/personnel/personnel_approval_demo.vue new file mode 100644 index 00000000..4e981e92 --- /dev/null +++ b/admin/src/views/personnel/personnel_approval_demo.vue @@ -0,0 +1,400 @@ + + + + + \ No newline at end of file diff --git a/niucloud/app/adminapi/controller/personnel/Personnel.php b/niucloud/app/adminapi/controller/personnel/Personnel.php index 14553558..1ef74d83 100644 --- a/niucloud/app/adminapi/controller/personnel/Personnel.php +++ b/niucloud/app/adminapi/controller/personnel/Personnel.php @@ -69,10 +69,26 @@ class Personnel extends BaseAdminController ["status",0], ["is_sys_user",0], ["info",[]], + ["use_approval", 0], // 是否使用审批流程 + ["approval_config_id", 0], // 审批配置ID ]); $this->validate($data, 'app\validate\personnel\Personnel.add'); - $id = (new PersonnelService())->add($data); - return success('ADD_SUCCESS', ['id' => $id]); + + // 检查是否使用审批流程 + if ($data['use_approval'] && $data['approval_config_id'] > 0) { + // 使用审批流程 + $approvalService = new \app\service\school_approval\SchoolApprovalProcessService(); + $processId = $approvalService->createPersonnelApproval( + $data, + $this->request->uid(), + $data['approval_config_id'] + ); + return success('APPROVAL_CREATED_SUCCESS', ['process_id' => $processId]); + } else { + // 直接添加人员 + $id = (new PersonnelService())->add($data); + return success('ADD_SUCCESS', ['id' => $id]); + } } /** @@ -113,5 +129,15 @@ class Personnel extends BaseAdminController return success('DELETE_SUCCESS'); } + /** + * 获取可用的审批配置列表 + * @return \think\Response + */ + public function getApprovalConfigs(){ + $approvalConfigService = new \app\service\school_approval\SchoolApprovalConfigService(); + $configs = $approvalConfigService->getList(['status' => 1]); + return success($configs); + } + } diff --git a/niucloud/app/adminapi/route/personnel.php b/niucloud/app/adminapi/route/personnel.php index d1458304..e6277574 100644 --- a/niucloud/app/adminapi/route/personnel.php +++ b/niucloud/app/adminapi/route/personnel.php @@ -28,6 +28,8 @@ Route::group('personnel', function () { Route::put('personnel/:id', 'personnel.Personnel/edit'); //删除人力资源-人员 Route::delete('personnel/:id', 'personnel.Personnel/del'); + //获取审批配置列表 + Route::get('personnel/approval-configs', 'personnel.Personnel/getApprovalConfigs'); })->middleware([ AdminCheckToken::class, diff --git a/niucloud/app/api/controller/apiController/Personnel.php b/niucloud/app/api/controller/apiController/Personnel.php index 91727648..a1b65fea 100644 --- a/niucloud/app/api/controller/apiController/Personnel.php +++ b/niucloud/app/api/controller/apiController/Personnel.php @@ -15,6 +15,7 @@ use app\dict\member\MemberLoginTypeDict; use app\model\reimbursement\Reimbursement; use app\Request; use app\service\api\apiService\PersonnelService; +use app\service\api\apiService\ServiceService; use app\service\api\captcha\CaptchaService; use app\service\api\login\ConfigService; use app\service\api\login\LoginService; @@ -188,4 +189,77 @@ class Personnel extends BaseApiService } } + /** + * 获取我的服务记录列表 + * @param Request $request + * @return \think\Response + */ + public function myServiceLogs(Request $request) + { + try { + $params = $request->all(); + $res = (new ServiceService())->getMyServiceLogs($params); + return success($res); + } catch (\Exception $e) { + return fail('获取服务记录失败:' . $e->getMessage()); + } + } + + /** + * 获取服务记录详情 + * @param Request $request + * @return \think\Response + */ + public function serviceLogDetail(Request $request) + { + try { + $params = $request->all(); + $id = $params['id'] ?? 0; + + if (empty($id)) { + return fail('服务记录ID不能为空'); + } + + $res = (new ServiceService())->getServiceLogDetail($id); + if (empty($res)) { + return fail('服务记录不存在'); + } + + return success($res); + } catch (\Exception $e) { + return fail('获取服务记录详情失败:' . $e->getMessage()); + } + } + + /** + * 更新服务结果 + * @param Request $request + * @return \think\Response + */ + public function updateServiceRemark(Request $request) + { + try { + $params = $request->all(); + $id = $params['id'] ?? 0; + $serviceRemark = $params['service_remark'] ?? ''; + + if (empty($id)) { + return fail('服务记录ID不能为空'); + } + + if (empty($serviceRemark)) { + return fail('服务结果内容不能为空'); + } + + $res = (new ServiceService())->updateServiceRemark($id, $serviceRemark); + if (!$res['code']) { + return fail($res['msg']); + } + + return success([], '更新成功'); + } catch (\Exception $e) { + return fail('更新服务结果失败:' . $e->getMessage()); + } + } + } diff --git a/niucloud/app/api/controller/common/Dict.php b/niucloud/app/api/controller/common/Dict.php new file mode 100644 index 00000000..3aaaf6c9 --- /dev/null +++ b/niucloud/app/api/controller/common/Dict.php @@ -0,0 +1,155 @@ + 20) { + return fail('一次最多获取20个字典'); + } + + try { + $dictService = new DictService(); + $result = $dictService->getBatchDict($keys); + + return success($result); + } catch (\Exception $e) { + return fail('获取字典数据失败:' . $e->getMessage()); + } + } + + /** + * 根据业务场景获取字典数据 + * @return \think\Response + */ + public function getDictByScene() + { + $scene = input('scene', ''); + + if (empty($scene)) { + return fail('参数错误:scene不能为空'); + } + + try { + $dictService = new DictService(); + + // 获取场景对应的字典keys + $keys = $dictService->getDictKeysByScene($scene); + + if (empty($keys)) { + return fail('不支持的业务场景:' . $scene); + } + + // 批量获取字典数据 + $result = $dictService->getBatchDict($keys); + + return success([ + 'scene' => $scene, + 'data' => $result + ]); + } catch (\Exception $e) { + return fail('获取字典数据失败:' . $e->getMessage()); + } + } + + /** + * 获取单个字典数据 + * @return \think\Response + */ + public function getDict() + { + $key = input('key', ''); + $useCache = input('use_cache', 1); + + if (empty($key)) { + return fail('参数错误:key不能为空'); + } + + try { + $dictService = new DictService(); + $result = $dictService->getDict($key, (bool)$useCache); + + return success($result); + } catch (\Exception $e) { + return fail('获取字典数据失败:' . $e->getMessage()); + } + } + + /** + * 清除字典缓存 + * @return \think\Response + */ + public function clearDictCache() + { + $keys = input('keys', null); + + // 支持字符串格式(逗号分隔)和数组格式 + if (is_string($keys)) { + $keys = array_filter(explode(',', $keys)); + } + + try { + $dictService = new DictService(); + $result = $dictService->clearDictCache($keys); + + if ($result) { + return success('缓存清除成功'); + } else { + return fail('缓存清除失败'); + } + } catch (\Exception $e) { + return fail('缓存清除失败:' . $e->getMessage()); + } + } + + /** + * 获取常用字典映射关系 + * @return \think\Response + */ + public function getDictMapping() + { + try { + $dictService = new DictService(); + $mapping = $dictService->getCommonDictMapping(); + + return success($mapping); + } catch (\Exception $e) { + return fail('获取字典映射失败:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/niucloud/app/api/route/route.php b/niucloud/app/api/route/route.php index 6f821589..dbf67823 100644 --- a/niucloud/app/api/route/route.php +++ b/niucloud/app/api/route/route.php @@ -127,6 +127,18 @@ Route::group(function () { // 通过经纬度查询地址 Route::get('area/address_by_latlng', 'sys.Area/getAddressByLatlng'); + /***************************************************** 字典批量获取 ****************************************************/ + // 批量获取字典数据 + Route::get('dict/batch', 'common.Dict/getBatchDict'); + // 根据业务场景获取字典数据 + Route::get('dict/scene/:scene', 'common.Dict/getDictByScene'); + // 获取单个字典数据 + Route::get('dict/single/:key', 'common.Dict/getDict'); + // 获取字典映射关系 + Route::get('dict/mapping', 'common.Dict/getDictMapping'); + // 清除字典缓存 + Route::post('dict/clear_cache', 'common.Dict/clearDictCache'); + /***************************************************** 海报管理 ****************************************************/ //获取海报 Route::get('poster', 'poster.Poster/poster'); @@ -364,6 +376,11 @@ Route::group(function () { Route::get('contract/signStatus', 'apiController.Contract/signStatus'); Route::get('contract/download', 'apiController.Contract/download'); + //服务管理 + Route::get('personnel/myServiceLogs', 'apiController.Personnel/myServiceLogs'); + Route::get('personnel/serviceLogDetail', 'apiController.Personnel/serviceLogDetail'); + Route::post('personnel/updateServiceRemark', 'apiController.Personnel/updateServiceRemark'); + })->middleware(ApiChannel::class) ->middleware(ApiPersonnelCheckToken::class, true) diff --git a/niucloud/app/model/personnel/Personnel.php b/niucloud/app/model/personnel/Personnel.php index d7ae5c60..6eb20c11 100644 --- a/niucloud/app/model/personnel/Personnel.php +++ b/niucloud/app/model/personnel/Personnel.php @@ -26,6 +26,11 @@ class Personnel extends BaseModel use SoftDelete; + // 人员状态常量 + const STATUS_NORMAL = 1; // 正常 + const STATUS_DISABLED = 0; // 禁用 + const STATUS_PENDING_APPROVAL = 2; // 待审批 + /** * 数据表主键 * @var string diff --git a/niucloud/app/model/service_logs/ServiceLogs.php b/niucloud/app/model/service_logs/ServiceLogs.php index b0d2aafa..4c38a4a1 100644 --- a/niucloud/app/model/service_logs/ServiceLogs.php +++ b/niucloud/app/model/service_logs/ServiceLogs.php @@ -16,10 +16,13 @@ use think\model\concern\SoftDelete; use think\model\relation\HasMany; use think\model\relation\HasOne; +use app\model\service\Service; +use app\model\personnel\Personnel; + /** - * 服务模型 - * Class Service - * @package app\model\service + * 服务记录模型 + * Class ServiceLogs + * @package app\model\service_logs */ class ServiceLogs extends BaseModel { @@ -36,9 +39,42 @@ class ServiceLogs extends BaseModel */ protected $name = 'service_logs'; + /** + * 搜索器:服务记录员工ID + * @param $value + * @param $data + */ + public function searchStaffIdAttr($query, $value, $data) + { + if ($value) { + $query->where("staff_id", $value); + } + } + + /** + * 搜索器:服务记录状态 + * @param $value + * @param $data + */ + public function searchStatusAttr($query, $value, $data) + { + if ($value) { + $query->where("status", $value); + } + } + + /** + * 关联服务表 + */ + public function service(){ + return $this->hasOne(Service::class, 'id', 'service_id')->joinType('left')->withField('service_name,preview_image_url,description,service_type')->bind(['service_name'=>'service_name', 'preview_image_url'=>'preview_image_url', 'description'=>'description', 'service_type'=>'service_type']); + } - - - + /** + * 关联员工表 + */ + public function staff(){ + return $this->hasOne(Personnel::class, 'id', 'staff_id')->joinType('left')->withField('name,id')->bind(['staff_name'=>'name']); + } } diff --git a/niucloud/app/model/sys/SysDict.php b/niucloud/app/model/sys/SysDict.php new file mode 100644 index 00000000..064ad2d6 --- /dev/null +++ b/niucloud/app/model/sys/SysDict.php @@ -0,0 +1,82 @@ + 'json' + ]; + + // 设置json类型字段 + protected $json = ['value']; + // 设置JSON数据返回数组 + protected $jsonAssoc = true; + + /** + * 搜索器:数据字典字典名称 + * @param $query + * @param $value + * @param $data + */ + public function searchNameAttr($query, $value, $data) + { + if ($value != '') { + $query->where("name", $value); + } + } + + /** + * 搜索器:数据字典字典关键词 + * @param $query + * @param $value + * @param $data + */ + public function searchKeyAttr($query, $value, $data) + { + if ($value != '') { + $query->where("key", $value); + } + } + + /** + * 搜索器:数据字典状态 + * @param $query + * @param $value + * @param $data + */ + public function searchStatusAttr($query, $value, $data) + { + if ($value !== '') { + $query->where("status", $value); + } + } +} \ No newline at end of file diff --git a/niucloud/app/service/api/apiService/ServiceService.php b/niucloud/app/service/api/apiService/ServiceService.php new file mode 100644 index 00000000..3488bc76 --- /dev/null +++ b/niucloud/app/service/api/apiService/ServiceService.php @@ -0,0 +1,160 @@ +model = new ServiceLogs(); + } + + /** + * 获取员工服务记录列表 + * @param array $where + * @return array + */ + public function getMyServiceLogs(array $where = []) + { + // 获取当前登录的员工ID + $staffId = $this->uid; + + $field = 'id,service_id,staff_id,status,service_remark,feedback,score,created_at,updated_at'; + $order = 'created_at desc'; + + $search_model = $this->model + ->withSearch(["staff_id"], ['staff_id' => $staffId]) + ->with(['service', 'staff']) + ->field($field) + ->order($order); + + $list = $this->pageQuery($search_model); + + // 如果没有数据,为演示目的返回一些测试数据 + if (empty($list['data']) && isset($where['demo'])) { + $list = [ + 'data' => [ + [ + 'id' => 1, + 'service_id' => 1, + 'staff_id' => $staffId, + 'status' => 0, + 'service_remark' => '', + 'feedback' => '', + 'score' => null, + 'created_at' => time(), + 'updated_at' => time(), + 'service_name' => '体能训练指导服务', + 'preview_image_url' => '', + 'description' => '专业的体能训练指导,帮助学员提升身体素质', + 'service_type' => '训练指导' + ], + [ + 'id' => 2, + 'service_id' => 2, + 'staff_id' => $staffId, + 'status' => 1, + 'service_remark' => '学员表现**优秀**,完成了所有训练项目\n• 力量训练:90%完成度\n• 耐力训练:85%完成度\n• 柔韧性训练:95%完成度', + 'feedback' => '孩子很喜欢这次训练,教练很专业', + 'score' => 95, + 'created_at' => time() - 86400, + 'updated_at' => time() - 3600, + 'service_name' => '技能训练服务', + 'preview_image_url' => '', + 'description' => '专项技能训练,提升运动技巧', + 'service_type' => '技能培训' + ] + ], + 'current_page' => 1, + 'last_page' => 1, + 'per_page' => 10, + 'total' => 2 + ]; + } + + return $list; + } + + /** + * 获取服务记录详情 + * @param int $id + * @return array + */ + public function getServiceLogDetail(int $id) + { + // 获取当前登录的员工ID + $staffId = $this->uid; + + $field = 'id,service_id,staff_id,status,service_remark,feedback,score,created_at,updated_at'; + + $info = $this->model + ->with(['service', 'staff']) + ->field($field) + ->where([ + ['id', "=", $id], + ['staff_id', "=", $staffId] + ]) + ->findOrEmpty() + ->toArray(); + + return $info; + } + + /** + * 更新服务结果 + * @param int $id + * @param string $serviceRemark + * @return array + */ + public function updateServiceRemark(int $id, string $serviceRemark) + { + // 获取当前登录的员工ID + $staffId = $this->uid; + + // 查找对应的服务记录 + $serviceLog = $this->model + ->where([ + ['id', '=', $id], + ['staff_id', '=', $staffId] + ]) + ->find(); + + if (!$serviceLog) { + return ['code' => 0, 'msg' => '服务记录不存在']; + } + + // 检查状态,只有非完成状态才能修改 + if ($serviceLog->status == 1) { + return ['code' => 0, 'msg' => '服务已完成,无法修改']; + } + + try { + // 更新服务结果 + $serviceLog->service_remark = $serviceRemark; + $serviceLog->updated_at = time(); + $serviceLog->save(); + + return ['code' => 1, 'msg' => '更新成功']; + } catch (\Exception $e) { + return ['code' => 0, 'msg' => '更新失败:' . $e->getMessage()]; + } + } +} \ No newline at end of file diff --git a/niucloud/app/service/api/common/DictService.php b/niucloud/app/service/api/common/DictService.php new file mode 100644 index 00000000..c5fd9eb6 --- /dev/null +++ b/niucloud/app/service/api/common/DictService.php @@ -0,0 +1,297 @@ +model = new Dict(); + } + + /** + * 批量获取字典数据 + * @param array $keys 字典key数组 + * @return array + */ + public function getBatchDict(array $keys = []): array + { + if (empty($keys)) { + return []; + } + + // 验证keys参数 + $validKeys = array_filter($keys, function($key) { + return is_string($key) && !empty(trim($key)); + }); + + if (empty($validKeys)) { + return []; + } + + try { + // 批量查询字典数据 + $dictData = $this->model + ->whereIn('key', $validKeys) + ->field('key, dictionary') + ->select() + ->toArray(); + + $result = []; + + foreach ($dictData as $dict) { + $key = $dict['key']; + $dictionary = $dict['dictionary']; + + // 解析字典值 - 模型已自动处理JSON转换 + if (!empty($dictionary) && is_array($dictionary)) { + $result[$key] = $dictionary; + } else if (!empty($dictionary) && is_string($dictionary)) { + // 如果是字符串,尝试解析 + $decodedValue = json_decode($dictionary, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decodedValue)) { + $result[$key] = $decodedValue; + } else { + $result[$key] = $this->parseStringValue($dictionary); + } + } else { + $result[$key] = []; + } + } + + // 为没有找到的key返回空数组 + foreach ($validKeys as $key) { + if (!isset($result[$key])) { + $result[$key] = []; + } + } + + return $result; + + } catch (\Exception $e) { + // 记录错误日志 + trace('批量获取字典数据失败: ' . $e->getMessage(), 'error'); + + // 返回空结果 + $result = []; + foreach ($validKeys as $key) { + $result[$key] = []; + } + return $result; + } + } + + /** + * 获取单个字典数据(带缓存) + * @param string $key 字典key + * @param bool $useCache 是否使用缓存 + * @return array + */ + public function getDict(string $key, bool $useCache = true): array + { + if (empty($key)) { + return []; + } + + $cacheKey = "dict_cache_{$key}"; + + // 如果使用缓存,先尝试从缓存获取 + if ($useCache) { + $cached = cache($cacheKey); + if ($cached !== false) { + return $cached; + } + } + + try { + $dict = $this->model + ->where('key', $key) + ->field('dictionary') + ->find(); + + $result = []; + if ($dict && !empty($dict['dictionary'])) { + if (is_array($dict['dictionary'])) { + $result = $dict['dictionary']; + } else { + $decodedValue = json_decode($dict['dictionary'], true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decodedValue)) { + $result = $decodedValue; + } else { + $result = $this->parseStringValue($dict['dictionary']); + } + } + } + + // 缓存结果(缓存30分钟) + if ($useCache) { + cache($cacheKey, $result, 1800); + } + + return $result; + + } catch (\Exception $e) { + trace('获取字典数据失败: ' . $e->getMessage(), 'error'); + return []; + } + } + + /** + * 清除字典缓存 + * @param string|array $keys 要清除的字典key,为空则清除所有字典缓存 + * @return bool + */ + public function clearDictCache($keys = null): bool + { + try { + if (is_null($keys)) { + // 清除所有字典缓存(需要实现cache标签功能或使用其他方式) + // 这里简化处理,实际项目中可以使用cache标签 + return true; + } + + if (is_string($keys)) { + $keys = [$keys]; + } + + if (is_array($keys)) { + foreach ($keys as $key) { + cache("dict_cache_{$key}", null); + } + return true; + } + + return false; + } catch (\Exception $e) { + trace('清除字典缓存失败: ' . $e->getMessage(), 'error'); + return false; + } + } + + /** + * 解析字符串格式的字典值 + * @param string $value + * @return array + */ + private function parseStringValue(string $value): array + { + // 尝试按行分割,每行格式如:key|value 或 key:value + $lines = array_filter(explode("\n", $value)); + $result = []; + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) { + continue; + } + + // 尝试多种分隔符 + $separators = ['|', ':', '=', ',']; + $parsed = false; + + foreach ($separators as $sep) { + if (strpos($line, $sep) !== false) { + $parts = explode($sep, $line, 2); + if (count($parts) === 2) { + $result[] = [ + 'name' => trim($parts[1]), + 'value' => trim($parts[0]), + 'sort' => count($result), + 'memo' => '' + ]; + $parsed = true; + break; + } + } + } + + // 如果没有找到分隔符,将整行作为值,索引作为key + if (!$parsed) { + $result[] = [ + 'name' => $line, + 'value' => (string)count($result), + 'sort' => count($result), + 'memo' => '' + ]; + } + } + + return $result; + } + + /** + * 获取常用字典映射 + * @return array + */ + public function getCommonDictMapping(): array + { + return [ + // 客户来源相关 + 'source_channel' => 'SourceChannel', + 'source' => 'source', + 'purchasing_power' => 'customer_purchasing_power', + 'cognitive_idea' => 'cognitive_concept', + 'decision_maker' => 'decision_maker', + 'initial_intent' => 'preliminarycustomerintention', + 'status' => 'kh_status', + 'distance' => 'distance', + + // 人员管理相关 + 'gender' => 'gender', + 'education' => 'education', + 'position' => 'position', + 'department' => 'department', + + // 课程相关 + 'course_type' => 'course_type', + 'course_level' => 'course_level', + 'course_status' => 'course_status', + + // 其他常用字典 + 'yes_no' => 'yes_no', + 'enable_disable' => 'enable_disable' + ]; + } + + /** + * 根据业务场景获取字典keys + * @param string $scene 业务场景 + * @return array + */ + public function getDictKeysByScene(string $scene): array + { + $sceneMapping = [ + 'customer_add' => [ + 'SourceChannel', 'source', 'customer_purchasing_power', + 'cognitive_concept', 'decision_maker', 'preliminarycustomerintention', + 'kh_status', 'distance' + ], + 'personnel_add' => [ + 'gender', 'education', 'position', 'department', 'enable_disable' + ], + 'course_add' => [ + 'course_type', 'course_level', 'course_status', 'enable_disable' + ] + ]; + + return $sceneMapping[$scene] ?? []; + } +} \ No newline at end of file diff --git a/niucloud/app/service/school_approval/SchoolApprovalProcessService.php b/niucloud/app/service/school_approval/SchoolApprovalProcessService.php index 68f0e735..4d8e0ef2 100644 --- a/niucloud/app/service/school_approval/SchoolApprovalProcessService.php +++ b/niucloud/app/service/school_approval/SchoolApprovalProcessService.php @@ -100,7 +100,10 @@ class SchoolApprovalProcessService 'application_time' => date("Y-m-d H:i:s"), 'current_approver_id' => 0, // 初始时为0,后面会更新 'approval_status' => SchoolApprovalProcess::STATUS_PENDING, - 'remarks' => $data['remarks'] ?? '' + 'remarks' => $data['remarks'] ?? '', + 'business_type' => $data['business_type'] ?? '', + 'business_id' => $data['business_id'] ?? 0, + 'business_data' => isset($data['business_data']) ? json_encode($data['business_data']) : '' ]; $process_id = (new SchoolApprovalProcess())->insertGetId($process); @@ -143,6 +146,29 @@ class SchoolApprovalProcessService } } + /** + * 创建人员添加审批流程 + * @param array $personnelData 人员数据 + * @param int $applicantId 申请人ID + * @param int $configId 审批配置ID + * @return int + * @throws \Exception + */ + public function createPersonnelApproval(array $personnelData, int $applicantId, int $configId): int + { + // 创建审批流程数据 + $processData = [ + 'process_name' => '人员添加申请 - ' . $personnelData['name'], + 'applicant_id' => $applicantId, + 'remarks' => '申请添加新员工:' . $personnelData['name'] . ',职位:' . ($personnelData['position'] ?? '未指定'), + 'business_type' => 'personnel_add', + 'business_id' => 0, // 暂时为0,等人员创建后更新 + 'business_data' => $personnelData + ]; + + return $this->create($processData, $configId); + } + /** * 审批 * @param int $process_id 流程ID @@ -201,6 +227,9 @@ class SchoolApprovalProcessService 'remarks' => $remarks ]); + // 处理拒绝后的业务逻辑 + $this->handleApprovalRejected($process_id); + Db::commit(); return true; } @@ -237,6 +266,9 @@ class SchoolApprovalProcessService 'approval_status' => SchoolApprovalProcess::STATUS_APPROVED, 'approval_time' => time() ]); + + // 处理业务逻辑 + $this->handleApprovalCompleted($process_id); } else { // 更新当前审批人为下一个审批人 (new SchoolApprovalProcess())->where(['id' => $process_id]) @@ -302,4 +334,88 @@ class SchoolApprovalProcessService throw new Exception($e->getMessage()); } } + + /** + * 处理审批完成后的业务逻辑 + * @param int $process_id + * @throws \Exception + */ + private function handleApprovalCompleted(int $process_id): void + { + // 获取流程信息 + $process = (new SchoolApprovalProcess())->where(['id' => $process_id])->find(); + if (empty($process)) { + throw new Exception('流程信息不存在'); + } + + // 根据业务类型处理 + switch ($process['business_type']) { + case 'personnel_add': + $this->handlePersonnelAddApproval($process); + break; + default: + // 其他业务类型的处理逻辑 + break; + } + } + + /** + * 处理人员添加审批完成 + * @param $process + * @throws \Exception + */ + private function handlePersonnelAddApproval($process): void + { + if (empty($process['business_data'])) { + throw new Exception('人员数据不存在'); + } + + $personnelData = json_decode($process['business_data'], true); + if (empty($personnelData)) { + throw new Exception('人员数据格式错误'); + } + + try { + // 调用人员服务创建正式人员记录 + $personnelService = new \app\service\admin\personnel\PersonnelService(); + + // 准备人员数据 + $createData = $personnelData; + $createData['status'] = 1; // 设置为正常状态 + + // 创建人员记录 + $personnelId = $personnelService->add($createData); + + // 更新流程的business_id为实际创建的人员ID + (new SchoolApprovalProcess())->where(['id' => $process['id']]) + ->update(['business_id' => $personnelId]); + + } catch (\Exception $e) { + throw new Exception('创建人员记录失败:' . $e->getMessage()); + } + } + + /** + * 处理审批拒绝后的业务逻辑 + * @param int $process_id + * @throws \Exception + */ + private function handleApprovalRejected(int $process_id): void + { + // 获取流程信息 + $process = (new SchoolApprovalProcess())->where(['id' => $process_id])->find(); + if (empty($process)) { + return; + } + + // 根据业务类型处理 + switch ($process['business_type']) { + case 'personnel_add': + // 人员添加被拒绝,不需要特殊处理 + break; + default: + // 其他业务类型的处理逻辑 + break; + } + } } diff --git a/uniapp/common/axiosQuiet.js b/uniapp/common/axiosQuiet.js new file mode 100644 index 00000000..3c6ca514 --- /dev/null +++ b/uniapp/common/axiosQuiet.js @@ -0,0 +1,85 @@ +import { + Api_url +} from './config' + +// 静默请求工具 - 用于字典获取等不需要显示加载提示的场景 +const axiosQuiet = { + // 静默请求方法 + request(options) { + return new Promise((resolve, reject) => { + // 创建请求配置 + const config = { + url: Api_url + options.url, + data: options.data, + method: options.method || 'GET', + header: { + 'token': uni.getStorageSync("token") + }, + timeout: 10000 // 设置10秒超时 + }; + + console.log('静默请求配置:', config); + + uni.request({ + ...config, + success: (res) => { + try { + const { statusCode, data } = res; + console.log('静默请求响应:', res); + + // 处理HTTP状态码 + if (statusCode >= 200 && statusCode < 300) { + // 处理业务状态码 + if (data && data.code) { + if (data.code === 1) { // 成功状态码为1 + resolve(data); + } else if (data.code === 401) { + // 401错误静默处理,不显示提示 + console.warn('静默请求401错误:', data.msg); + reject(data); + } else { + // 其他业务错误也静默处理 + console.warn('静默请求业务错误:', data.msg); + reject(data); + } + } else { + resolve(data); + } + } else { + // HTTP错误 + console.warn('静默请求HTTP错误:', statusCode); + reject(res); + } + } catch (error) { + console.error('静默请求处理失败:', error); + reject(error); + } + }, + fail: (error) => { + console.warn('静默请求失败:', error); + reject(error); + } + }); + }); + }, + + // GET请求 + get(url, data = {}) { + return this.request({ + url, + data, + method: 'GET' + }); + }, + + // POST请求 + post(url, data = {}) { + return this.request({ + url, + data, + method: 'POST' + }); + } +}; + +export default axiosQuiet; \ No newline at end of file diff --git a/uniapp/common/dictUtil.js b/uniapp/common/dictUtil.js new file mode 100644 index 00000000..a5f26ff9 --- /dev/null +++ b/uniapp/common/dictUtil.js @@ -0,0 +1,332 @@ +import axiosQuiet from './axiosQuiet.js' + +/** + * 字典工具类 - 支持批量获取和缓存 + */ +class DictUtil { + constructor() { + this.cacheKey = 'dict_cache' + this.cacheExpire = 30 * 60 * 1000 // 30分钟过期 + } + + /** + * 批量获取字典数据 + * @param {Array} keys 字典key数组 + * @param {Boolean} useCache 是否使用缓存 + * @returns {Promise} 字典数据对象 + */ + async getBatchDict(keys = [], useCache = true) { + if (!Array.isArray(keys) || keys.length === 0) { + console.warn('字典keys参数必须是非空数组') + return {} + } + + try { + // 如果使用缓存,先检查缓存 + let cachedData = {} + let uncachedKeys = [] + + if (useCache) { + const cache = this.getCache() + uncachedKeys = keys.filter(key => { + if (cache[key] && this.isCacheValid(cache[key])) { + cachedData[key] = cache[key].data + return false + } + return true + }) + } else { + uncachedKeys = [...keys] + } + + // 如果所有数据都在缓存中,直接返回 + if (uncachedKeys.length === 0) { + return cachedData + } + + // 请求未缓存的数据 + try { + const response = await axiosQuiet.get('/dict/batch', { + keys: uncachedKeys.join(',') + }) + + if (response && response.code === 1) { + const newData = response.data || {} + + // 更新缓存 + if (useCache) { + this.updateCache(newData) + } + + // 合并缓存数据和新数据 + return { ...cachedData, ...newData } + } else { + console.warn('批量获取字典失败:', response?.msg || '未知错误') + return cachedData // 返回已缓存的数据 + } + } catch (requestError) { + console.warn('批量获取字典请求失败:', requestError) + return cachedData // 返回已缓存的数据 + } + } catch (error) { + console.error('批量获取字典异常:', error) + return {} + } + } + + /** + * 根据业务场景获取字典数据 + * @param {String} scene 业务场景 + * @param {Boolean} useCache 是否使用缓存 + * @returns {Promise} 字典数据对象 + */ + async getDictByScene(scene, useCache = true) { + if (!scene) { + console.warn('业务场景参数不能为空') + return {} + } + + try { + // 检查场景缓存 + const sceneCacheKey = `scene_${scene}` + if (useCache) { + const cache = this.getCache() + if (cache[sceneCacheKey] && this.isCacheValid(cache[sceneCacheKey])) { + return cache[sceneCacheKey].data + } + } + + const response = await axiosQuiet.get(`/dict/scene/${scene}`) + + if (response && response.code === 1) { + const data = response.data || {} + + // 缓存场景数据 + if (useCache) { + const cacheData = {} + cacheData[sceneCacheKey] = { + data: data.data || {}, + timestamp: Date.now() + } + this.updateCache(cacheData) + } + + return data.data || {} + } else { + console.warn('根据场景获取字典失败:', response?.msg || '未知错误') + return {} + } + } catch (error) { + console.error('根据场景获取字典异常:', error) + return {} + } + } + + /** + * 获取单个字典数据 + * @param {String} key 字典key + * @param {Boolean} useCache 是否使用缓存 + * @returns {Promise} 字典数据数组 + */ + async getDict(key, useCache = true) { + if (!key) { + console.warn('字典key不能为空') + return [] + } + + const result = await this.getBatchDict([key], useCache) + return result[key] || [] + } + + /** + * 获取缓存数据 + * @returns {Object} 缓存对象 + */ + getCache() { + try { + const cacheStr = uni.getStorageSync(this.cacheKey) + return cacheStr ? JSON.parse(cacheStr) : {} + } catch (error) { + console.error('获取字典缓存失败:', error) + return {} + } + } + + /** + * 更新缓存 + * @param {Object} data 要缓存的数据 + */ + updateCache(data) { + try { + const cache = this.getCache() + const timestamp = Date.now() + + // 更新缓存数据 + Object.keys(data).forEach(key => { + cache[key] = { + data: data[key], + timestamp: timestamp + } + }) + + uni.setStorageSync(this.cacheKey, JSON.stringify(cache)) + } catch (error) { + console.error('更新字典缓存失败:', error) + } + } + + /** + * 检查缓存是否有效 + * @param {Object} cacheItem 缓存项 + * @returns {Boolean} 是否有效 + */ + isCacheValid(cacheItem) { + if (!cacheItem || !cacheItem.timestamp) { + return false + } + return (Date.now() - cacheItem.timestamp) < this.cacheExpire + } + + /** + * 清除字典缓存 + * @param {Array} keys 要清除的字典key数组,为空则清除所有 + */ + clearCache(keys = null) { + try { + if (keys === null) { + // 清除所有缓存 + uni.removeStorageSync(this.cacheKey) + } else if (Array.isArray(keys)) { + // 清除指定keys的缓存 + const cache = this.getCache() + keys.forEach(key => { + delete cache[key] + }) + uni.setStorageSync(this.cacheKey, JSON.stringify(cache)) + } + } catch (error) { + console.error('清除字典缓存失败:', error) + } + } + + /** + * 获取字典映射关系 + * @returns {Promise} 映射关系对象 + */ + async getDictMapping() { + try { + const response = await axiosQuiet.get('/dict/mapping') + + if (response && response.code === 1) { + return response.data || {} + } else { + console.warn('获取字典映射失败:', response?.msg || '未知错误') + return {} + } + } catch (error) { + console.error('获取字典映射异常:', error) + return {} + } + } + + /** + * 将字典数据转换为选择器格式 + * @param {Array} dictData 字典数据 + * @returns {Array} 选择器格式数据 + */ + formatForPicker(dictData) { + if (!Array.isArray(dictData)) { + return [] + } + + return dictData.map(item => ({ + text: item.name || item.text || '', + value: item.value || '', + sort: item.sort || 0, + memo: item.memo || '' + })) + } + + /** + * 根据value查找字典项的名称 + * @param {Array} dictData 字典数据 + * @param {String} value 要查找的值 + * @returns {String} 对应的名称 + */ + getNameByValue(dictData, value) { + if (!Array.isArray(dictData)) { + return '' + } + + const item = dictData.find(item => String(item.value) === String(value)) + return item ? (item.name || item.text || '') : '' + } + + /** + * 预加载常用字典数据 + * @param {Array} keys 要预加载的字典keys + * @returns {Promise} + */ + async preloadDict(keys = []) { + if (!Array.isArray(keys) || keys.length === 0) { + return + } + + try { + await this.getBatchDict(keys, true) + console.log('字典预加载完成:', keys) + } catch (error) { + console.error('字典预加载失败:', error) + } + } + + /** + * 获取客户添加页面需要的字典数据 + * @returns {Promise} 字典数据对象 + */ + async getCustomerAddDict() { + const keys = [ + 'SourceChannel', 'source', 'customer_purchasing_power', + 'cognitive_concept', 'decision_maker', 'preliminarycustomerintention', + 'kh_status', 'distance' + ] + return await this.getBatchDict(keys) + } +} + +// 创建单例实例 +const dictUtil = new DictUtil() + +// 扩展 util 对象的字典方法(兼容原有代码) +if (typeof util !== 'undefined') { + // 保持原有的 getDict 方法兼容性 + const originalGetDict = util.getDict + util.getDict = async function(key) { + try { + // 优先使用新的字典工具 + const result = await dictUtil.getDict(key) + if (result && result.length > 0) { + return result + } + + // 如果新工具没有数据,回退到原方法 + if (originalGetDict && typeof originalGetDict === 'function') { + return await originalGetDict.call(this, key) + } + + return [] + } catch (error) { + console.error('获取字典失败:', error) + return [] + } + } + + // 添加新的批量获取方法 + util.getBatchDict = dictUtil.getBatchDict.bind(dictUtil) + util.getDictByScene = dictUtil.getDictByScene.bind(dictUtil) + util.clearDictCache = dictUtil.clearCache.bind(dictUtil) + util.preloadDict = dictUtil.preloadDict.bind(dictUtil) + util.getCustomerAddDict = dictUtil.getCustomerAddDict.bind(dictUtil) +} + +export default dictUtil \ No newline at end of file diff --git a/uniapp/common/dictUtilSimple.js b/uniapp/common/dictUtilSimple.js new file mode 100644 index 00000000..63e5a5b2 --- /dev/null +++ b/uniapp/common/dictUtilSimple.js @@ -0,0 +1,187 @@ +import axiosQuiet from './axiosQuiet.js' + +/** + * 简化版字典工具类 - 用于调试和测试 + */ +class DictUtilSimple { + constructor() { + this.cacheKey = 'dict_cache_simple' + this.cacheExpire = 30 * 60 * 1000 // 30分钟过期 + } + + /** + * 批量获取字典数据 + * @param {Array} keys 字典key数组 + * @param {Boolean} useCache 是否使用缓存 + * @returns {Promise} 字典数据对象 + */ + async getBatchDict(keys = [], useCache = true) { + console.log('getBatchDict 调用参数:', { keys, useCache }) + + if (!Array.isArray(keys) || keys.length === 0) { + console.warn('字典keys参数必须是非空数组') + return {} + } + + try { + // 检查缓存 + let cachedData = {} + let uncachedKeys = [...keys] + + if (useCache) { + const cache = this.getCache() + uncachedKeys = keys.filter(key => { + if (cache[key] && this.isCacheValid(cache[key])) { + cachedData[key] = cache[key].data + return false + } + return true + }) + } + + console.log('缓存检查结果:', { cachedData, uncachedKeys }) + + // 如果所有数据都在缓存中,直接返回 + if (uncachedKeys.length === 0) { + console.log('所有数据来自缓存') + return cachedData + } + + // 请求未缓存的数据 + console.log('开始请求未缓存的数据:', uncachedKeys) + + const response = await axiosQuiet.get('/dict/batch', { + keys: uncachedKeys.join(',') + }) + + console.log('API响应:', response) + + if (response && response.code === 1) { + const newData = response.data || {} + console.log('获取到新数据:', newData) + + // 更新缓存 + if (useCache) { + this.updateCache(newData) + } + + // 合并缓存数据和新数据 + const result = { ...cachedData, ...newData } + console.log('最终结果:', result) + return result + } else { + console.warn('批量获取字典失败:', response?.msg || '未知错误') + return cachedData // 返回已缓存的数据 + } + } catch (error) { + console.error('批量获取字典异常:', error) + return {} + } + } + + /** + * 获取单个字典数据 + * @param {String} key 字典key + * @param {Boolean} useCache 是否使用缓存 + * @returns {Promise} 字典数据数组 + */ + async getDict(key, useCache = true) { + console.log('getDict 调用参数:', { key, useCache }) + + if (!key) { + console.warn('字典key不能为空') + return [] + } + + try { + const result = await this.getBatchDict([key], useCache) + return result[key] || [] + } catch (error) { + console.error('获取单个字典失败:', error) + return [] + } + } + + /** + * 获取缓存数据 + * @returns {Object} 缓存对象 + */ + getCache() { + try { + const cacheStr = uni.getStorageSync(this.cacheKey) + const cache = cacheStr ? JSON.parse(cacheStr) : {} + console.log('获取缓存:', cache) + return cache + } catch (error) { + console.error('获取字典缓存失败:', error) + return {} + } + } + + /** + * 更新缓存 + * @param {Object} data 要缓存的数据 + */ + updateCache(data) { + try { + const cache = this.getCache() + const timestamp = Date.now() + + // 更新缓存数据 + Object.keys(data).forEach(key => { + cache[key] = { + data: data[key], + timestamp: timestamp + } + }) + + uni.setStorageSync(this.cacheKey, JSON.stringify(cache)) + console.log('缓存已更新:', cache) + } catch (error) { + console.error('更新字典缓存失败:', error) + } + } + + /** + * 检查缓存是否有效 + * @param {Object} cacheItem 缓存项 + * @returns {Boolean} 是否有效 + */ + isCacheValid(cacheItem) { + if (!cacheItem || !cacheItem.timestamp) { + return false + } + const isValid = (Date.now() - cacheItem.timestamp) < this.cacheExpire + console.log('缓存有效性检查:', { cacheItem, isValid }) + return isValid + } + + /** + * 清除字典缓存 + * @param {Array} keys 要清除的字典key数组,为空则清除所有 + */ + clearCache(keys = null) { + try { + if (keys === null) { + // 清除所有缓存 + uni.removeStorageSync(this.cacheKey) + console.log('已清除所有字典缓存') + } else if (Array.isArray(keys)) { + // 清除指定keys的缓存 + const cache = this.getCache() + keys.forEach(key => { + delete cache[key] + }) + uni.setStorageSync(this.cacheKey, JSON.stringify(cache)) + console.log('已清除指定字典缓存:', keys) + } + } catch (error) { + console.error('清除字典缓存失败:', error) + } + } +} + +// 创建单例实例 +const dictUtilSimple = new DictUtilSimple() + +export default dictUtilSimple \ No newline at end of file diff --git a/uniapp/pages.json b/uniapp/pages.json index ff208762..d90c3ca1 100644 --- a/uniapp/pages.json +++ b/uniapp/pages.json @@ -654,7 +654,8 @@ "style": { "navigationBarTitleText": "服务详情", "navigationBarBackgroundColor": "#29d3b4", - "navigationBarTextStyle": "black" + "navigationBarTextStyle": "white", + "enablePullDownRefresh": true } }, { diff --git a/uniapp/pages/coach/my/service_detail.vue b/uniapp/pages/coach/my/service_detail.vue index cef9645d..3e3d50c6 100644 --- a/uniapp/pages/coach/my/service_detail.vue +++ b/uniapp/pages/coach/my/service_detail.vue @@ -2,139 +2,465 @@ - \ No newline at end of file diff --git a/uniapp/pages/demo/dict_optimization.vue b/uniapp/pages/demo/dict_optimization.vue new file mode 100644 index 00000000..e7723979 --- /dev/null +++ b/uniapp/pages/demo/dict_optimization.vue @@ -0,0 +1,542 @@ + + + + + \ 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 086b0972..eea126c5 100644 --- a/uniapp/pages/market/clue/add_clues.vue +++ b/uniapp/pages/market/clue/add_clues.vue @@ -528,6 +528,8 @@ import commonApi from '@/api/common.js'; import marketApi from '@/api/market.js'; import memberApi from '@/api/member.js'; import util from '@/common/util.js'; +import dictUtil from '@/common/dictUtil.js'; +import dictUtilSimple from '@/common/dictUtilSimple.js'; const rules = [ @@ -731,26 +733,122 @@ export default { ], } }, + onLoad() { + // 预加载字典数据 + this.preloadDictData() + }, onShow() { this.init() }, methods: { + // 预加载字典数据 + async preloadDictData() { + const dictKeys = [ + 'SourceChannel', 'source', 'customer_purchasing_power', + 'preliminarycustomerintention', 'cognitive_concept', + 'kh_status', 'decision_maker', 'distance' + ] + + // 静默预加载,不阻塞页面显示 + // 暂时使用简化版本进行测试 + dictUtilSimple.getBatchDict(dictKeys).catch(error => { + console.warn('字典预加载失败:', error) + }) + }, + //初始化 async init() { //获取登录用户信息 await this.getUserInfo() - await this.getDict('source_channel')//获取字典-来源渠道 - await this.getDict('source')//获取字典-来源 - await this.getDict('purchasing_power')//获取字典-购买力 - await this.getDict('initial_intent')//获取字典-客户初步意向度 - await this.getDict('cognitive_idea')//获取字典-认知理念 - await this.getDict('status')//获取字典-客户状态 - await this.getDict('decision_maker')//获取字典-决策人 - await this.getDict('distance')//获取字典-距离 + // 使用批量获取字典数据,提升性能 + await this.getBatchDictData() + // this.getStaffList()//获取人员列表 // this.getAreaTree()//获取地区树形结构 }, + + // 批量获取字典数据 + async getBatchDictData() { + try { + // 定义需要的字典keys + const dictKeys = [ + 'SourceChannel', // 来源渠道 + 'source', // 来源 + 'customer_purchasing_power', // 购买力 + 'preliminarycustomerintention', // 客户初步意向度 + 'cognitive_concept', // 认知理念 + 'kh_status', // 客户状态 + 'decision_maker', // 决策人 + 'distance' // 距离 + ] + + // 批量获取字典数据(静默获取,不显示加载提示) + // 暂时使用简化版本进行测试 + const dictData = await dictUtilSimple.getBatchDict(dictKeys) + + // 处理字典数据 + this.processDictData(dictData) + + console.log('批量获取字典数据成功:', dictData) + + } catch (error) { + console.error('批量获取字典数据失败:', error) + + // 如果批量获取失败,回退到单个获取 + await this.fallbackGetDict() + } + }, + + // 处理批量获取的字典数据 + processDictData(dictData) { + // 字典key与本地配置的映射关系 + const keyMapping = { + 'SourceChannel': 'source_channel', + 'source': 'source', + 'customer_purchasing_power': 'purchasing_power', + 'preliminarycustomerintention': 'initial_intent', + 'cognitive_concept': 'cognitive_idea', + 'kh_status': 'status', + 'decision_maker': 'decision_maker', + 'distance': 'distance' + } + + // 处理每个字典数据 + Object.keys(keyMapping).forEach(dictKey => { + const localKey = keyMapping[dictKey] + const dictItems = dictData[dictKey] || [] + + if (Array.isArray(dictItems) && dictItems.length > 0) { + const formattedOptions = dictItems.map(item => ({ + text: item.name || '', + value: item.value || '' + })) + + // 设置到picker配置中 + if (this.picker_config[localKey]) { + this.picker_config[localKey].options = formattedOptions + } + } + }) + }, + + // 回退方案:单个获取字典(兼容原有逻辑) + async fallbackGetDict() { + console.log('使用回退方案获取字典数据') + try { + await this.getDict('source_channel') + await this.getDict('source') + await this.getDict('purchasing_power') + await this.getDict('initial_intent') + await this.getDict('cognitive_idea') + await this.getDict('status') + await this.getDict('decision_maker') + await this.getDict('distance') + } catch (error) { + console.error('回退方案也失败了:', error) + } + }, async get_campus_list(){ let res = await apiRoute.common_getCampusesList({}) diff --git a/uniapp/pages/test/dict_test.vue b/uniapp/pages/test/dict_test.vue new file mode 100644 index 00000000..85e703e3 --- /dev/null +++ b/uniapp/pages/test/dict_test.vue @@ -0,0 +1,263 @@ + + + + + \ No newline at end of file From 9712b76d7835a81af3b90febcd4c344fcb973677 Mon Sep 17 00:00:00 2001 From: zeyan <258785420@qq.com> Date: Tue, 1 Jul 2025 17:04:48 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=90=88=E5=90=8C?= =?UTF-8?q?=E7=AD=BE=E8=AE=A2=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uniapp/common/dictUtil使用说明.md | 283 ++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 uniapp/common/dictUtil使用说明.md diff --git a/uniapp/common/dictUtil使用说明.md b/uniapp/common/dictUtil使用说明.md new file mode 100644 index 00000000..b8e9330b --- /dev/null +++ b/uniapp/common/dictUtil使用说明.md @@ -0,0 +1,283 @@ +# 字典工具类使用说明 + +## 概述 + +`dictUtil.js` 是一个专门用于批量获取和缓存字典数据的工具类,解决了原有单个接口调用次数过多、用户体验不佳的问题。 + +## 主要功能 + +1. **批量获取字典数据** - 一次性获取多个字典,减少接口调用 +2. **智能缓存机制** - 自动缓存数据,避免重复请求 +3. **业务场景支持** - 根据业务场景批量获取相关字典 +4. **兼容性保障** - 保持与原有代码的兼容性 + +## API 接口 + +### 后端接口 + +```php +// 批量获取字典数据 +GET /api/dict/batch?keys=key1,key2,key3 + +// 根据业务场景获取字典 +GET /api/dict/scene/{scene} + +// 获取单个字典 +GET /api/dict/single/{key} + +// 获取字典映射关系 +GET /api/dict/mapping + +// 清除字典缓存 +POST /api/dict/clear_cache +``` + +## 前端使用方法 + +### 1. 导入工具类 + +```javascript +import dictUtil from '@/common/dictUtil.js' +``` + +### 2. 批量获取字典数据 + +```javascript +// 方法一:直接批量获取 +const dictKeys = ['SourceChannel', 'source', 'customer_purchasing_power'] +const dictData = await dictUtil.getBatchDict(dictKeys) + +// 结果格式: +// { +// 'SourceChannel': [ +// {name: '抖音', value: '1', sort: 0, memo: ''}, +// {name: '微信', value: '2', sort: 1, memo: ''} +// ], +// 'source': [...], +// 'customer_purchasing_power': [...] +// } +``` + +### 3. 根据业务场景获取 + +```javascript +// 获取客户添加场景的所有字典 +const dictData = await dictUtil.getDictByScene('customer_add') + +// 或者使用便捷方法 +const dictData = await dictUtil.getCustomerAddDict() +``` + +### 4. 单个字典获取 + +```javascript +// 获取单个字典(与原有 util.getDict 兼容) +const sourceData = await dictUtil.getDict('source') +``` + +### 5. 预加载字典数据 + +```javascript +onLoad() { + // 在页面加载时预加载,提升用户体验 + const dictKeys = ['SourceChannel', 'source', 'customer_purchasing_power'] + dictUtil.preloadDict(dictKeys) +} +``` + +### 6. 缓存管理 + +```javascript +// 清除指定字典缓存 +dictUtil.clearCache(['SourceChannel', 'source']) + +// 清除所有字典缓存 +dictUtil.clearCache() +``` + +## 在页面中的完整使用示例 + +### 原有方式(多次接口调用) + +```javascript +// 原有方式 - 问题:多次接口调用,用户体验差 +async init() { + await this.getDict('source_channel') + await this.getDict('source') + await this.getDict('purchasing_power') + await this.getDict('initial_intent') + await this.getDict('cognitive_idea') + await this.getDict('status') + await this.getDict('decision_maker') + await this.getDict('distance') +} +``` + +### 优化后的方式(批量获取) + +```javascript +// 优化方式 - 一次接口调用获取所有数据 +import dictUtil from '@/common/dictUtil.js' + +export default { + onLoad() { + // 预加载字典数据 + this.preloadDictData() + }, + + methods: { + // 预加载字典数据 + async preloadDictData() { + const dictKeys = [ + 'SourceChannel', 'source', 'customer_purchasing_power', + 'preliminarycustomerintention', 'cognitive_concept', + 'kh_status', 'decision_maker', 'distance' + ] + + // 静默预加载,不阻塞页面显示 + dictUtil.preloadDict(dictKeys).catch(error => { + console.warn('字典预加载失败:', error) + }) + }, + + // 批量获取字典数据 + async getBatchDictData() { + try { + uni.showLoading({ title: '加载字典数据...', mask: true }) + + const dictKeys = [ + 'SourceChannel', // 来源渠道 + 'source', // 来源 + 'customer_purchasing_power', // 购买力 + 'preliminarycustomerintention', // 客户初步意向度 + 'cognitive_concept', // 认知理念 + 'kh_status', // 客户状态 + 'decision_maker', // 决策人 + 'distance' // 距离 + ] + + // 批量获取字典数据 + const dictData = await dictUtil.getBatchDict(dictKeys) + + // 处理字典数据 + this.processDictData(dictData) + + } catch (error) { + console.error('批量获取字典数据失败:', error) + // 如果批量获取失败,回退到单个获取 + await this.fallbackGetDict() + } finally { + uni.hideLoading() + } + }, + + // 处理批量获取的字典数据 + processDictData(dictData) { + const keyMapping = { + 'SourceChannel': 'source_channel', + 'source': 'source', + 'customer_purchasing_power': 'purchasing_power', + 'preliminarycustomerintention': 'initial_intent', + 'cognitive_concept': 'cognitive_idea', + 'kh_status': 'status', + 'decision_maker': 'decision_maker', + 'distance': 'distance' + } + + Object.keys(keyMapping).forEach(dictKey => { + const localKey = keyMapping[dictKey] + const dictItems = dictData[dictKey] || [] + + if (Array.isArray(dictItems) && dictItems.length > 0) { + const formattedOptions = dictItems.map(item => ({ + text: item.name || '', + value: item.value || '' + })) + + if (this.picker_config[localKey]) { + this.picker_config[localKey].options = formattedOptions + } + } + }) + } + } +} +``` + +## 性能优化特性 + +### 1. 缓存机制 +- 自动缓存字典数据到本地存储 +- 缓存有效期 30 分钟 +- 避免重复请求相同数据 + +### 2. 批量请求 +- 一次接口调用获取多个字典 +- 减少网络请求次数 +- 提升页面加载速度 + +### 3. 预加载策略 +- 页面 onLoad 时预加载数据 +- 不阻塞页面显示 +- 用户操作时数据已就绪 + +### 4. 错误处理 +- 批量获取失败时自动回退到单个获取 +- 缓存获取失败时直接请求接口 +- 保证功能的健壮性 + +## 兼容性说明 + +### 向后兼容 +工具类保持与原有 `util.getDict()` 方法的完全兼容: + +```javascript +// 原有代码无需修改,自动使用新的缓存和批量获取机制 +const sourceData = await util.getDict('source') + +// 新增的批量获取方法 +const batchData = await util.getBatchDict(['source', 'status']) +``` + +### 渐进式升级 +可以逐步将页面迁移到新的批量获取方式: + +1. 先导入 `dictUtil` +2. 在 `onLoad` 中添加预加载 +3. 将 `init` 方法中的多个 `getDict` 调用替换为一次 `getBatchDict` 调用 + +## 注意事项 + +1. **字典 Key 映射**:确保后端字典 key 与前端使用的 key 一致 +2. **缓存清理**:如果字典数据有更新,记得清理对应缓存 +3. **错误处理**:在批量获取失败时,有回退机制保证功能正常 +4. **性能限制**:单次最多获取 20 个字典,防止接口性能问题 + +## 扩展功能 + +### 自定义业务场景 + +```javascript +// 在 DictService.php 中添加新的业务场景 +public function getDictKeysByScene(string $scene): array +{ + $sceneMapping = [ + 'customer_add' => ['SourceChannel', 'source', '...'], + 'personnel_add' => ['gender', 'education', '...'], + 'your_scene' => ['key1', 'key2', '...'] // 添加自定义场景 + ]; + + return $sceneMapping[$scene] ?? []; +} +``` + +### 添加新的工具方法 + +```javascript +// 在 dictUtil.js 中扩展 +dictUtil.getYourSceneDict = async function() { + return await this.getDictByScene('your_scene') +} +``` + +通过这种方式,你可以大幅提升字典数据获取的性能和用户体验。 \ No newline at end of file From ce7a8492176b71662d4921cd6bbc61d9ad05a0ab Mon Sep 17 00:00:00 2001 From: zeyan <258785420@qq.com> Date: Wed, 2 Jul 2025 12:29:17 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../course_schedule/CourseSchedule.php | 5 +- niucloud/app/api/controller/member/Member.php | 15 +- niucloud/app/api/route/route.php | 24 +- niucloud/app/common.php | 1 + .../course_schedule/CourseScheduleService.php | 4 +- .../service/api/apiService/CourseService.php | 2 +- .../api/apiService/ResourceSharingService.php | 2 +- .../app/service/api/member/MemberService.php | 53 +- uniapp/pages/market/clue/add_clues.vue | 104 +- .../market/clue/class_arrangement_detail.vue | 1099 +++++++++-------- uniapp/pages/market/clue/clue_info.vue | 163 ++- uniapp/pages/market/clue/edit_clues.vue | 190 ++- 12 files changed, 1033 insertions(+), 629 deletions(-) diff --git a/niucloud/app/adminapi/controller/course_schedule/CourseSchedule.php b/niucloud/app/adminapi/controller/course_schedule/CourseSchedule.php index eb95ddc2..a106a6c2 100644 --- a/niucloud/app/adminapi/controller/course_schedule/CourseSchedule.php +++ b/niucloud/app/adminapi/controller/course_schedule/CourseSchedule.php @@ -164,7 +164,10 @@ class CourseSchedule extends BaseAdminController ["person_type",''], ["schedule_id",''], ["course_date",''], - ["time_slot",''] + ["time_slot",''], + ["schedule_type", 1], // 1=正式位, 2=等待位 + ["course_type", 1], // 1=正式课, 2=体验课, 3=补课, 4=试听课 + ["position", ''] // 位置信息 ]); return (new CourseScheduleService())->addSchedule($data); } diff --git a/niucloud/app/api/controller/member/Member.php b/niucloud/app/api/controller/member/Member.php index c70e37aa..e5173756 100644 --- a/niucloud/app/api/controller/member/Member.php +++ b/niucloud/app/api/controller/member/Member.php @@ -18,6 +18,7 @@ use app\service\api\member\MemberLogService; use app\service\api\member\MemberService; use core\base\BaseApiController; use think\facade\Db; +use think\facade\Log; use think\Response; class Member extends BaseApiController @@ -127,9 +128,19 @@ class Member extends BaseApiController public function list_call_up() { $data = $this->request->params([ - ['sales_id', ''], + ['resource_id', ''], + ['sales_id', ''], // 保留旧参数名称以保持兼容性 ]); - return success((new MemberService())->list_call_up($data['sales_id'])); + + // 优先使用resource_id,如果不存在则使用sales_id + $resource_id = !empty($data['resource_id']) ? $data['resource_id'] : $data['sales_id']; + + // 记录日志 + Log::debug("Member/list_call_up - 请求参数: resource_id={$resource_id}"); + + $result = (new MemberService())->list_call_up($resource_id); + + return success($result); } public function update_call_up() diff --git a/niucloud/app/api/route/route.php b/niucloud/app/api/route/route.php index dbf67823..71ebd452 100644 --- a/niucloud/app/api/route/route.php +++ b/niucloud/app/api/route/route.php @@ -127,18 +127,6 @@ Route::group(function () { // 通过经纬度查询地址 Route::get('area/address_by_latlng', 'sys.Area/getAddressByLatlng'); - /***************************************************** 字典批量获取 ****************************************************/ - // 批量获取字典数据 - Route::get('dict/batch', 'common.Dict/getBatchDict'); - // 根据业务场景获取字典数据 - Route::get('dict/scene/:scene', 'common.Dict/getDictByScene'); - // 获取单个字典数据 - Route::get('dict/single/:key', 'common.Dict/getDict'); - // 获取字典映射关系 - Route::get('dict/mapping', 'common.Dict/getDictMapping'); - // 清除字典缓存 - Route::post('dict/clear_cache', 'common.Dict/clearDictCache'); - /***************************************************** 海报管理 ****************************************************/ //获取海报 Route::get('poster', 'poster.Poster/poster'); @@ -465,7 +453,17 @@ Route::group(function () { Route::post('xy/orderTable/add', 'apiController.OrderTable/add'); - + /***************************************************** 字典批量获取 ****************************************************/ + // 批量获取字典数据 + Route::get('dict/batch', 'common.Dict/getBatchDict'); + // 根据业务场景获取字典数据 + Route::get('dict/scene/:scene', 'common.Dict/getDictByScene'); + // 获取单个字典数据 + Route::get('dict/single/:key', 'common.Dict/getDict'); + // 获取字典映射关系 + Route::get('dict/mapping', 'common.Dict/getDictMapping'); + // 清除字典缓存 + Route::post('dict/clear_cache', 'common.Dict/clearDictCache'); })->middleware(ApiChannel::class) ->middleware(ApiPersonnelCheckToken::class, true) diff --git a/niucloud/app/common.php b/niucloud/app/common.php index b755957a..40fc8281 100644 --- a/niucloud/app/common.php +++ b/niucloud/app/common.php @@ -1229,6 +1229,7 @@ function get_dict_value($key, $value) $field = 'id,name,key,dictionary,memo,create_time,update_time'; $info = $dict->field($field)->where([['key', '=', $key]])->findOrEmpty()->toArray(); + if ($info['dictionary'] == null) { $info['dictionary'] = []; } diff --git a/niucloud/app/service/admin/course_schedule/CourseScheduleService.php b/niucloud/app/service/admin/course_schedule/CourseScheduleService.php index d76b8a2a..ec212ec9 100644 --- a/niucloud/app/service/admin/course_schedule/CourseScheduleService.php +++ b/niucloud/app/service/admin/course_schedule/CourseScheduleService.php @@ -299,7 +299,7 @@ class CourseScheduleService extends BaseAdminService ->alias('a') ->join(['school_customer_resources' => 'b'],'a.resources_id = b.id','left') ->where('a.schedule_id',$data['schedule_id']) - ->field("b.name,a.status") + ->field("a.id,a.resources_id,a.person_id,a.student_id,a.person_type,a.schedule_id,a.course_date,a.schedule_type,a.course_type,a.time_slot,a.status,a.remark,b.name") ->select()->toArray(); return $list; @@ -342,6 +342,8 @@ class CourseScheduleService extends BaseAdminService 'schedule_id' => $data['schedule_id'], 'course_date' => $data['course_date'], 'time_slot' => $data['time_slot'], + 'schedule_type' => $data['schedule_type'] ?? 1, // 1=正式位, 2=等待位 + 'course_type' => $data['course_type'] ?? 1, // 1=正式课, 2=体验课, 3=补课, 4=试听课 ]); $CourseSchedule->where(['id' => $data['schedule_id']])->dec("available_capacity")->update(); return success("添加成功"); diff --git a/niucloud/app/service/api/apiService/CourseService.php b/niucloud/app/service/api/apiService/CourseService.php index 770c7ff5..08c9cec1 100644 --- a/niucloud/app/service/api/apiService/CourseService.php +++ b/niucloud/app/service/api/apiService/CourseService.php @@ -327,7 +327,7 @@ class CourseService extends BaseApiService ->join(['school_customer_resources' => 'b'],'a.resources_id = b.id','left') ->join(['school_course_schedule' => 'c'],'c.id = a.schedule_id','left') ->where('a.schedule_id',$data['schedule_id']) - ->field("b.name,a.status,a.person_type,c.campus_id,b.id as resources_id") + ->field("b.name,a.status,a.person_type,c.campus_id,b.id as resources_id,a.schedule_type,a.course_type") ->select() ->toArray(); diff --git a/niucloud/app/service/api/apiService/ResourceSharingService.php b/niucloud/app/service/api/apiService/ResourceSharingService.php index c04df5ce..3977658d 100644 --- a/niucloud/app/service/api/apiService/ResourceSharingService.php +++ b/niucloud/app/service/api/apiService/ResourceSharingService.php @@ -300,7 +300,7 @@ class ResourceSharingService extends BaseApiService if (!empty($item['customerResource'])) { // 设置来源和渠道名称 $item['customerResource']['source'] = get_dict_value('source', $item['customerResource']['source']); - $item['customerResource']['source_channel'] = get_dict_value('source', $item['customerResource']['source_channel']); + $item['customerResource']['source_channel'] = get_dict_value('SourceChannel', $item['customerResource']['source_channel']); $item['customerResource']['campus_name'] = $campus_name[$item['customerResource']['campus']] ?? ''; $item['customerResource']['communication_time'] = $resultdata[$item['resource_id']] ?? ''; } diff --git a/niucloud/app/service/api/member/MemberService.php b/niucloud/app/service/api/member/MemberService.php index 018e5906..f3a06feb 100644 --- a/niucloud/app/service/api/member/MemberService.php +++ b/niucloud/app/service/api/member/MemberService.php @@ -26,6 +26,7 @@ use core\base\BaseApiService; use core\exception\ApiException; use core\util\Barcode; use think\facade\Db; +use think\facade\Log; use think\Model; /** @@ -181,10 +182,58 @@ class MemberService extends BaseApiService } + /** + * 日志记录工具方法 + * @param string $level 日志级别 + * @param string $message 日志信息 + * @return void + */ + private function log($level, $message) { + Log::$level('MemberService: ' . $message); + } + public function list_call_up($resource_id) { - $campus = new CommunicationRecords(); - return $campus->where('resource_id', $resource_id)->select()->toArray(); + $communication = new CommunicationRecords(); + // 添加日志记录以便调试 + $this->log('debug', "list_call_up请求参数: resource_id={$resource_id}"); + + try { + // 检查resource_id是否有效 + if (empty($resource_id)) { + $this->log('warning', "list_call_up: resource_id为空"); + return []; + } + + // 查询前打印SQL查询条件 + $sqlDebug = "SELECT * FROM school_communication_records WHERE resource_id = '{$resource_id}' ORDER BY communication_time DESC"; + $this->log('debug', "list_call_up对应SQL: {$sqlDebug}"); + + // 执行查询 + $result = $communication->where('resource_id', $resource_id) + ->order('communication_time DESC') + ->select()->toArray(); + + // 如果没有结果,尝试使用原生 SQL 查询检查记录存在性 + if (empty($result)) { + $this->log('debug', "list_call_up: 主要查询没有结果,尝试直接查询表"); + $rawResult = $communication->query("SELECT COUNT(*) as count FROM school_communication_records WHERE resource_id = '{$resource_id}'"); + $count = $rawResult[0]['count'] ?? 0; + $this->log('debug', "list_call_up: 原生SQL查询结果数量: {$count}"); + + // 如果原生查询有结果但模型查询没结果,尝试直接使用原生查询 + if ($count > 0) { + $this->log('debug', "list_call_up: 检测到数据存在,使用原生查询获取"); + $result = $communication->query("SELECT * FROM school_communication_records WHERE resource_id = '{$resource_id}' ORDER BY communication_time DESC"); + } + } + + $this->log('debug', "list_call_up查询结果数量: " . count($result)); + return $result; + } catch (\Exception $e) { + $this->log('error', "list_call_up查询异常: " . $e->getMessage()); + return []; + } } public function update_call_up($resource_id, $remarks) diff --git a/uniapp/pages/market/clue/add_clues.vue b/uniapp/pages/market/clue/add_clues.vue index eea126c5..7ee5438c 100644 --- a/uniapp/pages/market/clue/add_clues.vue +++ b/uniapp/pages/market/clue/add_clues.vue @@ -428,7 +428,15 @@ - + + { + this.date_picker_show = true + }) }, //选择跟进时间 change_date(e) { - //跟进时间 - let val = (e.result ?? '') - if(val){ - val = val + 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 + } + + // 确保日期格式为 YYYY-MM-DD + if (val && typeof val === 'string') { + // 如果是时间戳,转换为日期字符串 + if (/^\d+$/.test(val)) { + const date = new Date(parseInt(val)) + val = this.formatDate(date) + } + // 如果包含时间部分,只保留日期部分 + else if (val.includes(' ')) { + val = val.split(' ')[0] + } + // 统一格式为 YYYY-MM-DD + if (val.includes('/')) { + const parts = val.split('/') + if (parts.length === 3) { + val = `${parts[0]}-${parts[1].padStart(2, '0')}-${parts[2].padStart(2, '0')}` + } + } } let input_name = this.data_picker_input_name this.formData[input_name] = val - + + console.log(`设置${input_name}为:`, val) this.cancel_date() }, //关闭选择跟进时间 diff --git a/uniapp/pages/market/clue/class_arrangement_detail.vue b/uniapp/pages/market/clue/class_arrangement_detail.vue index 39466414..f63c0a48 100644 --- a/uniapp/pages/market/clue/class_arrangement_detail.vue +++ b/uniapp/pages/market/clue/class_arrangement_detail.vue @@ -1,490 +1,611 @@ - - - - - \ 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 dc751845..65ab87ac 100644 --- a/uniapp/pages/market/clue/clue_info.vue +++ b/uniapp/pages/market/clue/clue_info.vue @@ -49,7 +49,8 @@ 基本资料 - 课程信息 0; + } + }, onLoad(options) { console.log('onLoad - 接收到参数:', options); @@ -375,33 +382,6 @@ async init(){ console.log('init - 开始初始化流程'); - // 预加载常用字典数据 - try { - console.log('init - 开始预加载字典数据'); - const dictPromises = [ - this.$util.getDict('SourceChannel'), // 来源渠道 - this.$util.getDict('source'), // 来源 - this.$util.getDict('preliminarycustomerintention'), // 客户初步意向度 - this.$util.getDict('kh_status'), // 客户状态 - ]; - - // 使用Promise.all并行加载字典,但设置超时处理 - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('字典加载超时')), 5000) - ); - - await Promise.race([ - Promise.all(dictPromises), - timeoutPromise - ]).catch(err => { - console.warn('字典加载异常或超时,继续执行流程:', err); - }); - - console.log('init - 字典数据预加载完成或已超时'); - } catch (error) { - console.warn('init - 字典数据预加载失败,继续执行流程:', error); - } - // 恢复为串行请求处理,确保数据依赖关系正确 try { // 先获取客户详情,因为其他操作可能依赖于客户详情 @@ -409,14 +389,15 @@ await this.getInfo(); console.log('init - 客户详情获取完成'); - // 获取员工信息、通话记录和教练列表可以并行 - console.log('init - 开始获取员工信息、通话记录和教练列表'); + // 获取员工信息、通话记录、教练列表和课程信息可以并行 + console.log('init - 开始获取员工信息、通话记录、教练列表和课程信息'); await Promise.all([ this.getUserInfo(), this.getListCallUp(), - this.getPersonnelList() + this.getPersonnelList(), + this.getCourseInfo() // 添加课程信息获取 ]); - console.log('init - 员工信息、通话记录和教练列表获取完成'); + console.log('init - 员工信息、通话记录、教练列表和课程信息获取完成'); } catch (error) { console.error('init - 数据加载出错:', error); } @@ -502,25 +483,27 @@ async getListCallUp(){ console.log('getListCallUp - 开始获取通话记录'); try { - // 检查resource_sharing_id是否存在 - if (!this.resource_sharing_id) { - console.error('getListCallUp - resource_sharing_id为空,无法获取通话记录'); + // 检查clientInfo.resource_id是否存在 + if (!this.clientInfo || !this.clientInfo.resource_id) { + console.error('getListCallUp - resource_id为空,无法获取通话记录'); return false; } let data = { - sales_id:this.resource_sharing_id// + resource_id: this.clientInfo.resource_id // 使用正确的参数名和客户资源ID } + console.log('getListCallUp - 请求参数:', data); let res = await apiRoute.listCallUp(data) + console.log('getListCallUp - 响应:', res); if(res.code != 1){ uni.showToast({ - title: res.msg, + title: res.msg || '获取通话记录失败', icon: 'none' }) return false; } - this.listCallUp = res.data - console.log('getListCallUp - 通话记录获取成功'); + this.listCallUp = res.data || [] + console.log('getListCallUp - 通话记录获取成功, 数量:', this.listCallUp.length); return true; } catch (error) { console.error('getListCallUp - 获取通话记录失败:', error); @@ -738,11 +721,27 @@ //切换标签 async switch_tags(type){ + // 如果点击课程信息标签但没有课程数据,则不允许切换 + if (type === 2 && !this.hasCourseInfo) { + uni.showToast({ + title: '暂无课程信息', + icon: 'none' + }); + return; + } + this.switch_tags_type = type - // 当切换到课程信息时,获取课程数据 + + // 当切换到课程信息时,刷新课程数据 if (type === 2) { await this.getCourseInfo(); } + + // 当切换到通话记录时,刷新通话记录数据 + if (type === 3) { + await this.getListCallUp(); + console.log('刷新通话记录数据,当前记录数:', this.listCallUp.length); + } }, getSelect(type){ this.select_type = type @@ -753,37 +752,40 @@ try { if (!this.clientInfo.resource_id) { console.error('getCourseInfo - resource_id为空,无法获取课程信息'); + this.courseInfo = []; return false; } - // 使用新的学生课程信息接口 + // 使用学生课程信息接口 const params = { resource_id: this.clientInfo.resource_id, member_id: this.clientInfo.customerResource.member_id || '' }; - // 调用新的API获取课程信息 + console.log('getCourseInfo - 请求参数:', params); + try { const res = await apiRoute.getStudentCourseInfo(params); - if (res.code === 1 && res.data) { - this.courseInfo = res.data; + console.log('getCourseInfo - API响应:', res); + + if (res.code === 1) { + // 格式化课程数据 + this.courseInfo = this.formatCourseData(res.data || []); console.log('getCourseInfo - 课程信息获取成功:', this.courseInfo); return true; } else { console.warn('API返回错误:', res.msg); - throw new Error(res.msg); + this.courseInfo = []; + return false; } } catch (apiError) { - console.warn('使用API获取课程信息失败,使用模拟数据:', apiError); - // 如果API调用失败,使用模拟数据进行演示 - this.courseInfo = this.getMockCourseData(); - console.log('getCourseInfo - 使用模拟课程数据'); - return true; + console.warn('获取课程信息API调用失败:', apiError); + this.courseInfo = []; + return false; } } catch (error) { console.error('getCourseInfo - 获取课程信息异常:', error); - // 降级到模拟数据 - this.courseInfo = this.getMockCourseData(); + this.courseInfo = []; return false; } }, @@ -811,41 +813,6 @@ })); }, - // 获取模拟课程数据(用于演示) - getMockCourseData() { - return [ - { - id: 1, - course_name: '篮球基础课程', - total_count: 20, - used_count: 8, - leave_count: 2, - expiry_date: '2024-12-31', - status: 'active', - main_coach_id: 1, - main_coach_name: '张教练', - education_id: 2, - education_name: '李教务', - assistant_ids: '3,4', - assistant_names: '王助教, 赵助教' - }, - { - id: 2, - course_name: '足球进阶训练', - total_count: 15, - used_count: 15, - leave_count: 1, - expiry_date: '2024-10-31', - status: 'completed', - main_coach_id: 5, - main_coach_name: '陈教练', - education_id: 2, - education_name: '李教务', - assistant_ids: '6', - assistant_names: '孙助教' - } - ]; - }, // 获取人员列表(教练、教务、助教) async getPersonnelList() { @@ -1058,17 +1025,33 @@ }, - // 安全访问对象属性的方法 + // 安全访问对象属性的方法,优化性能 safeGet(obj, path, defaultValue = '') { if (!obj) return defaultValue; - const keys = path.split('.'); + + // 使用缓存来提高性能 + if (!this._pathCache) this._pathCache = {}; + + // 使用路径作为缓存键 + const cacheKey = path; + + // 如果这个路径没有缓存过分割结果,则计算并缓存 + if (!this._pathCache[cacheKey]) { + this._pathCache[cacheKey] = path.split('.'); + } + + const keys = this._pathCache[cacheKey]; let result = obj; - for (const key of keys) { + + // 使用for循环而不是for...of,更高效 + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; if (result === null || result === undefined || !result.hasOwnProperty(key)) { return defaultValue; } result = result[key]; } + return result || defaultValue; }, } diff --git a/uniapp/pages/market/clue/edit_clues.vue b/uniapp/pages/market/clue/edit_clues.vue index 10040b01..30ba148d 100644 --- a/uniapp/pages/market/clue/edit_clues.vue +++ b/uniapp/pages/market/clue/edit_clues.vue @@ -140,14 +140,22 @@ - - + + {{ formData.optional_class_time ? formData.optional_class_time : '点击选择' }} + + + + + {{ formData.promised_visit_time ? formData.promised_visit_time : '点击选择' }} + + + @@ -577,20 +585,8 @@ console.log('init - 开始加载字典数据'); - // 先加载所有字典数据 - const dictPromises = [ - this.getDict('source_channel'), //获取字典-来源渠道 - this.getDict('source'), //获取字典-来源 - this.getDict('purchasing_power'), //获取字典-购买力 - this.getDict('initial_intent'), //获取字典-客户初步意向度 - this.getDict('cognitive_idea'), //获取字典-认知理念 - this.getDict('status'), //获取字典-客户状态 - this.getDict('decision_maker'), //获取字典-决策人 - this.getDict('distance'), //获取字典-距离 - - ]; - - await Promise.all(dictPromises); + // 批量加载所有字典数据 + await this.getBatchDictData(); console.log('init - 字典数据加载完成'); // 加载校区列表 @@ -614,6 +610,139 @@ } }, + // 批量获取字典数据 + async getBatchDictData() { + try { + // 定义需要的字典keys和对应的本地键名 + const dictMapping = { + 'SourceChannel': 'source_channel', + 'source': 'source', + 'customer_purchasing_power': 'purchasing_power', + 'preliminarycustomerintention': 'initial_intent', + 'cognitive_concept': 'cognitive_idea', + 'kh_status': 'status', + 'decision_maker': 'decision_maker', + '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]); + } + } + + // 异步获取其他字典 + setTimeout(() => { + regularDicts.forEach(async (key) => { + const localKey = dictMapping[key]; + if (!window._dictCache[key]) { + await this.loadDictData(key, localKey); + } + }); + }, 100); + + console.log('优化的批量字典数据加载初始化完成'); + + } catch (error) { + console.error('批量获取字典数据失败:', error); + // 如果批量获取失败,回退到单个获取 + await this.fallbackGetDict(); + } + }, + + // 加载单个字典数据 + async loadDictData(key, localKey) { + try { + // 检查缓存 + if (window._dictCache[key]) { + // 使用缓存数据 + const dictData = window._dictCache[key]; + this.processDictData(localKey, dictData); + return dictData; + } + + // 加载字典数据 + const dictData = await this.$util.getDict(key); + + // 缓存数据 + if (Array.isArray(dictData) && dictData.length > 0) { + window._dictCache[key] = dictData; + this.processDictData(localKey, dictData); + } + + return dictData; + } catch (error) { + console.error(`加载字典 ${key} 失败:`, error); + return []; + } + }, + + // 处理字典数据 + processDictData(localKey, dictData) { + if (!Array.isArray(dictData) || dictData.length === 0) return; + + let formattedOptions = dictData.map(item => ({ + text: item.name || '', + value: item.value || '' + })); + + // 特殊处理来源渠道,添加线下选项 + if (localKey === 'source_channel') { + formattedOptions.unshift({ + text: '线下', + value: '0' + }); + } + + // 确保 picker_config 存在 + if (!this.picker_config[localKey]) { + this.picker_config[localKey] = { options: [], text: '点击选择' }; + } + + this.picker_config[localKey].options = formattedOptions; + }, + + // 回退方案:单个获取字典 + async fallbackGetDict() { + console.log('使用回退方案获取字典数据'); + try { + // 优先获取关键字典 + await Promise.all([ + this.getDict('source_channel'), + this.getDict('source') + ]); + + // 延迟加载其他字典 + setTimeout(async () => { + try { + await Promise.all([ + this.getDict('purchasing_power'), + this.getDict('initial_intent'), + this.getDict('cognitive_idea'), + this.getDict('status'), + this.getDict('decision_maker'), + this.getDict('distance') + ]); + } catch (error) { + console.error('回退方案第二阶段失败:', error); + } + }, 100); + } catch (error) { + console.error('回退方案也失败了:', error); + } + }, + async get_campus_list(){ let res = await apiRoute.common_getCampusesList({}) @@ -693,7 +822,7 @@ customer_type: customerResource.customer_type || '', // 客户分类 //六要素信息 - purchasing_power: sixSpeed.purchase_power || '', //购买力 + purchasing_power: sixSpeed.purchasing_power || '', //购买力 cognitive_idea: sixSpeed.concept_awareness || '', //认知理念 communication: sixSpeed.communication || '', //沟通备注 staff_id: sixSpeed.staff_id || '', //人员ID @@ -763,7 +892,7 @@ this.setPickerTextByValue('customer_type', this.formData.customer_type, customerResource.customer_type_name); // 六要素相关 - this.setPickerTextByValue('purchasing_power', this.formData.purchasing_power, sixSpeed.purchase_power_name); + this.setPickerTextByValue('purchasing_power', this.formData.purchasing_power, sixSpeed.purchasing_power_name); this.setPickerTextByValue('cognitive_idea', this.formData.cognitive_idea, sixSpeed.concept_awareness_name); this.setPickerTextByValue('distance', this.formData.distance, sixSpeed.distance_name); // 不再需要设置call_intent的选择器文本,因为已改为单选组件 @@ -776,12 +905,26 @@ // 根据值设置选择器文本 setPickerTextByValue(pickerName, value, defaultText) { + // 使用空值缓存加速处理 if (!value) { this.picker_config[pickerName] = this.picker_config[pickerName] || {}; this.picker_config[pickerName].text = '点击选择'; return; } + // 创建映射缓存,避免重复查找 + if (!this._valueTextMapping) { + this._valueTextMapping = {}; + } + + // 检查缓存 + const cacheKey = `${pickerName}_${value}`; + if (this._valueTextMapping[cacheKey]) { + this.picker_config[pickerName] = this.picker_config[pickerName] || {}; + this.picker_config[pickerName].text = this._valueTextMapping[cacheKey]; + return; + } + // 确保 picker_config[pickerName] 存在 if (!this.picker_config[pickerName]) { this.picker_config[pickerName] = { options: [] }; @@ -791,14 +934,19 @@ const options = this.picker_config[pickerName].options || []; const option = options.find(opt => String(opt.value) === String(value)); + let textValue; if (option) { - this.picker_config[pickerName].text = option.text; + textValue = option.text; } else if (defaultText) { // 如果找不到匹配的选项但有默认文本,则使用默认文本 - this.picker_config[pickerName].text = defaultText; + textValue = defaultText; } else { - this.picker_config[pickerName].text = '点击选择'; + textValue = '点击选择'; } + + // 保存到缓存 + this._valueTextMapping[cacheKey] = textValue; + this.picker_config[pickerName].text = textValue; }, // 获取资源详情(缓存数据,避免重复请求)