32 changed files with 4926 additions and 764 deletions
@ -0,0 +1,400 @@ |
|||
<template> |
|||
<div class="personnel-approval-demo"> |
|||
<el-card class="demo-card" shadow="hover"> |
|||
<template #header> |
|||
<div class="card-header"> |
|||
<h3>人员添加审批功能演示</h3> |
|||
<p class="desc">该功能展示如何将人员添加接入审批流程</p> |
|||
</div> |
|||
</template> |
|||
|
|||
<!-- 审批配置选择 --> |
|||
<div class="section"> |
|||
<h4>第一步:选择审批配置</h4> |
|||
<el-form :model="form" label-width="120px" size="default"> |
|||
<el-form-item label="是否使用审批:"> |
|||
<el-switch |
|||
v-model="form.useApproval" |
|||
active-text="启用审批流程" |
|||
inactive-text="直接添加" |
|||
@change="onApprovalToggle"> |
|||
</el-switch> |
|||
</el-form-item> |
|||
|
|||
<el-form-item |
|||
v-if="form.useApproval" |
|||
label="审批配置:" |
|||
required> |
|||
<el-select |
|||
v-model="form.approvalConfigId" |
|||
placeholder="请选择审批配置" |
|||
:loading="configLoading" |
|||
clearable> |
|||
<el-option |
|||
v-for="config in approvalConfigs" |
|||
:key="config.id" |
|||
:label="config.config_name" |
|||
:value="config.id"> |
|||
<span>{{ config.config_name }}</span> |
|||
<span style="float: right; color: #8492a6;">{{ config.description }}</span> |
|||
</el-option> |
|||
</el-select> |
|||
</el-form-item> |
|||
</el-form> |
|||
</div> |
|||
|
|||
<!-- 人员信息填写 --> |
|||
<div class="section"> |
|||
<h4>第二步:填写人员信息</h4> |
|||
<el-form :model="personnelForm" :rules="rules" ref="personnelFormRef" label-width="120px"> |
|||
<el-row :gutter="20"> |
|||
<el-col :span="12"> |
|||
<el-form-item label="姓名:" prop="name"> |
|||
<el-input v-model="personnelForm.name" placeholder="请输入姓名"></el-input> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="12"> |
|||
<el-form-item label="性别:" prop="gender"> |
|||
<el-radio-group v-model="personnelForm.gender"> |
|||
<el-radio :label="0">男</el-radio> |
|||
<el-radio :label="1">女</el-radio> |
|||
</el-radio-group> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
|
|||
<el-row :gutter="20"> |
|||
<el-col :span="12"> |
|||
<el-form-item label="手机号:" prop="phone"> |
|||
<el-input v-model="personnelForm.phone" placeholder="请输入手机号"></el-input> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="12"> |
|||
<el-form-item label="状态:" prop="status"> |
|||
<el-select v-model="personnelForm.status" placeholder="请选择状态"> |
|||
<el-option label="正常" :value="1"></el-option> |
|||
<el-option label="禁用" :value="0"></el-option> |
|||
</el-select> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
|
|||
<el-form-item label="家庭住址:"> |
|||
<el-input v-model="personnelForm.address" placeholder="请输入家庭住址"></el-input> |
|||
</el-form-item> |
|||
|
|||
<el-form-item label="学历:"> |
|||
<el-select v-model="personnelForm.education" placeholder="请选择学历"> |
|||
<el-option label="高中及以下" value="高中及以下"></el-option> |
|||
<el-option label="大专" value="大专"></el-option> |
|||
<el-option label="本科" value="本科"></el-option> |
|||
<el-option label="硕士" value="硕士"></el-option> |
|||
<el-option label="博士" value="博士"></el-option> |
|||
</el-select> |
|||
</el-form-item> |
|||
|
|||
<el-form-item label="个人简介:"> |
|||
<el-input |
|||
v-model="personnelForm.profile" |
|||
type="textarea" |
|||
:rows="3" |
|||
placeholder="请输入个人简介"> |
|||
</el-input> |
|||
</el-form-item> |
|||
|
|||
<el-form-item label="是否系统用户:"> |
|||
<el-switch |
|||
v-model="personnelForm.isSysUser" |
|||
active-text="是" |
|||
inactive-text="否"> |
|||
</el-switch> |
|||
</el-form-item> |
|||
</el-form> |
|||
</div> |
|||
|
|||
<!-- 提交按钮 --> |
|||
<div class="section"> |
|||
<h4>第三步:提交申请</h4> |
|||
<el-button |
|||
type="primary" |
|||
size="large" |
|||
:loading="submitLoading" |
|||
@click="handleSubmit"> |
|||
{{ form.useApproval ? '提交审批申请' : '直接添加人员' }} |
|||
</el-button> |
|||
<el-button |
|||
size="large" |
|||
@click="handleReset"> |
|||
重置表单 |
|||
</el-button> |
|||
</div> |
|||
|
|||
<!-- 结果展示 --> |
|||
<div v-if="result" class="section result-section"> |
|||
<h4>处理结果</h4> |
|||
<el-alert |
|||
:title="result.title" |
|||
:description="result.description" |
|||
:type="result.type" |
|||
show-icon |
|||
:closable="false"> |
|||
</el-alert> |
|||
</div> |
|||
</el-card> |
|||
|
|||
<!-- 审批流程状态展示 --> |
|||
<el-card v-if="showApprovalStatus" class="approval-status-card" shadow="hover"> |
|||
<template #header> |
|||
<h3>审批流程状态</h3> |
|||
</template> |
|||
<div class="approval-flow"> |
|||
<el-steps :active="currentStep" finish-status="success" align-center> |
|||
<el-step |
|||
v-for="(step, index) in approvalSteps" |
|||
:key="index" |
|||
:title="step.title" |
|||
:description="step.description"> |
|||
</el-step> |
|||
</el-steps> |
|||
</div> |
|||
</el-card> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, reactive, onMounted } from 'vue' |
|||
import { ElMessage, ElMessageBox } from 'element-plus' |
|||
import { apiRequest } from '@/utils/request' |
|||
|
|||
// 响应式数据 |
|||
const form = reactive({ |
|||
useApproval: false, |
|||
approvalConfigId: null |
|||
}) |
|||
|
|||
const personnelForm = reactive({ |
|||
name: '', |
|||
gender: 0, |
|||
phone: '', |
|||
address: '', |
|||
education: '', |
|||
profile: '', |
|||
status: 1, |
|||
isSysUser: false, |
|||
info: {} |
|||
}) |
|||
|
|||
const approvalConfigs = ref([]) |
|||
const configLoading = ref(false) |
|||
const submitLoading = ref(false) |
|||
const result = ref(null) |
|||
const showApprovalStatus = ref(false) |
|||
const currentStep = ref(0) |
|||
const approvalSteps = ref([]) |
|||
|
|||
// 表单验证规则 |
|||
const rules = { |
|||
name: [ |
|||
{ required: true, message: '请输入姓名', trigger: 'blur' } |
|||
], |
|||
phone: [ |
|||
{ required: true, message: '请输入手机号', trigger: 'blur' }, |
|||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' } |
|||
], |
|||
gender: [ |
|||
{ required: true, message: '请选择性别', trigger: 'change' } |
|||
], |
|||
status: [ |
|||
{ required: true, message: '请选择状态', trigger: 'change' } |
|||
] |
|||
} |
|||
|
|||
const personnelFormRef = ref() |
|||
|
|||
// 获取审批配置列表 |
|||
const loadApprovalConfigs = async () => { |
|||
configLoading.value = true |
|||
try { |
|||
const response = await apiRequest({ |
|||
url: '/adminapi/personnel/approval-configs', |
|||
method: 'GET' |
|||
}) |
|||
if (response.data.code === 1) { |
|||
approvalConfigs.value = response.data.data.list || [] |
|||
} |
|||
} catch (error) { |
|||
ElMessage.error('获取审批配置失败') |
|||
} finally { |
|||
configLoading.value = false |
|||
} |
|||
} |
|||
|
|||
// 审批开关切换 |
|||
const onApprovalToggle = (value) => { |
|||
if (value && approvalConfigs.value.length === 0) { |
|||
loadApprovalConfigs() |
|||
} |
|||
if (!value) { |
|||
form.approvalConfigId = null |
|||
} |
|||
} |
|||
|
|||
// 提交表单 |
|||
const handleSubmit = async () => { |
|||
// 验证表单 |
|||
const valid = await personnelFormRef.value.validate() |
|||
if (!valid) { |
|||
return |
|||
} |
|||
|
|||
// 如果使用审批流程,检查是否选择了配置 |
|||
if (form.useApproval && !form.approvalConfigId) { |
|||
ElMessage.warning('请选择审批配置') |
|||
return |
|||
} |
|||
|
|||
submitLoading.value = true |
|||
try { |
|||
const submitData = { |
|||
...personnelForm, |
|||
use_approval: form.useApproval ? 1 : 0, |
|||
approval_config_id: form.approvalConfigId || 0 |
|||
} |
|||
|
|||
const response = await apiRequest({ |
|||
url: '/adminapi/personnel/personnel', |
|||
method: 'POST', |
|||
data: submitData |
|||
}) |
|||
|
|||
if (response.data.code === 1) { |
|||
if (form.useApproval) { |
|||
result.value = { |
|||
title: '审批申请提交成功', |
|||
description: `已成功提交人员添加审批申请,流程ID: ${response.data.data.process_id},请等待审批人处理。`, |
|||
type: 'success' |
|||
} |
|||
showApprovalDemo() |
|||
} else { |
|||
result.value = { |
|||
title: '人员添加成功', |
|||
description: `人员 ${personnelForm.name} 添加成功,ID: ${response.data.data.id}`, |
|||
type: 'success' |
|||
} |
|||
} |
|||
} else { |
|||
result.value = { |
|||
title: '提交失败', |
|||
description: response.data.msg || '操作失败,请重试', |
|||
type: 'error' |
|||
} |
|||
} |
|||
} catch (error) { |
|||
result.value = { |
|||
title: '提交失败', |
|||
description: '网络错误,请稍后重试', |
|||
type: 'error' |
|||
} |
|||
} finally { |
|||
submitLoading.value = false |
|||
} |
|||
} |
|||
|
|||
// 展示审批流程演示 |
|||
const showApprovalDemo = () => { |
|||
showApprovalStatus.value = true |
|||
approvalSteps.value = [ |
|||
{ title: '提交申请', description: '用户提交人员添加申请' }, |
|||
{ title: '部门审批', description: '部门负责人审批' }, |
|||
{ title: 'HR审批', description: 'HR部门审批' }, |
|||
{ title: '完成', description: '审批完成,创建人员记录' } |
|||
] |
|||
currentStep.value = 1 // 模拟当前在第二步 |
|||
} |
|||
|
|||
// 重置表单 |
|||
const handleReset = () => { |
|||
personnelFormRef.value.resetFields() |
|||
form.useApproval = false |
|||
form.approvalConfigId = null |
|||
result.value = null |
|||
showApprovalStatus.value = false |
|||
} |
|||
|
|||
// 组件挂载时初始化 |
|||
onMounted(() => { |
|||
// 可以在这里初始化一些演示数据 |
|||
personnelForm.name = '张三' |
|||
personnelForm.phone = '13800138000' |
|||
personnelForm.address = '北京市朝阳区' |
|||
personnelForm.education = '本科' |
|||
personnelForm.profile = '具有丰富的教学经验,专业技能扎实。' |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.personnel-approval-demo { |
|||
padding: 20px; |
|||
} |
|||
|
|||
.demo-card { |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.card-header { |
|||
text-align: center; |
|||
} |
|||
|
|||
.card-header h3 { |
|||
margin: 0 0 10px 0; |
|||
color: #303133; |
|||
} |
|||
|
|||
.card-header .desc { |
|||
margin: 0; |
|||
color: #909399; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.section { |
|||
margin-bottom: 30px; |
|||
padding-bottom: 20px; |
|||
border-bottom: 1px solid #ebeef5; |
|||
} |
|||
|
|||
.section:last-child { |
|||
border-bottom: none; |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.section h4 { |
|||
margin: 0 0 20px 0; |
|||
color: #606266; |
|||
font-size: 16px; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.result-section { |
|||
background: #f8f9fa; |
|||
padding: 20px; |
|||
border-radius: 8px; |
|||
} |
|||
|
|||
.approval-status-card { |
|||
margin-top: 20px; |
|||
} |
|||
|
|||
.approval-flow { |
|||
padding: 20px 0; |
|||
} |
|||
|
|||
:deep(.el-step__description) { |
|||
color: #909399; |
|||
font-size: 12px; |
|||
} |
|||
|
|||
:deep(.el-alert__description) { |
|||
font-size: 14px; |
|||
line-height: 1.6; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,155 @@ |
|||
<?php |
|||
// +---------------------------------------------------------------------- |
|||
// | Niucloud-admin 企业快速开发的多应用管理平台 |
|||
// +---------------------------------------------------------------------- |
|||
// | 官方网址:https://www.niucloud.com |
|||
// +---------------------------------------------------------------------- |
|||
// | niucloud团队 版权所有 开源版本可自由商用 |
|||
// +---------------------------------------------------------------------- |
|||
// | Author: Niucloud Team |
|||
// +---------------------------------------------------------------------- |
|||
|
|||
namespace app\api\controller\common; |
|||
|
|||
use app\service\api\common\DictService; |
|||
use core\base\BaseController; |
|||
|
|||
/** |
|||
* 字典批量获取控制器 |
|||
* Class Dict |
|||
* @package app\api\controller\common |
|||
*/ |
|||
class Dict extends BaseController |
|||
{ |
|||
/** |
|||
* 批量获取字典数据 |
|||
* @return \think\Response |
|||
*/ |
|||
public function getBatchDict() |
|||
{ |
|||
$keys = input('keys', []); |
|||
|
|||
// 支持字符串格式(逗号分隔)和数组格式 |
|||
if (is_string($keys)) { |
|||
$keys = array_filter(explode(',', $keys)); |
|||
} |
|||
|
|||
if (!is_array($keys) || empty($keys)) { |
|||
return fail('参数错误:keys必须是非空数组或逗号分隔的字符串'); |
|||
} |
|||
|
|||
// 限制一次最多获取20个字典,防止性能问题 |
|||
if (count($keys) > 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()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
<?php |
|||
// +---------------------------------------------------------------------- |
|||
// | Niucloud-admin 企业快速开发的多应用管理平台 |
|||
// +---------------------------------------------------------------------- |
|||
// | 官方网址:https://www.niucloud.com |
|||
// +---------------------------------------------------------------------- |
|||
// | niucloud团队 版权所有 开源版本可自由商用 |
|||
// +---------------------------------------------------------------------- |
|||
// | Author: Niucloud Team |
|||
// +---------------------------------------------------------------------- |
|||
|
|||
namespace app\model\sys; |
|||
|
|||
use core\base\BaseModel; |
|||
|
|||
/** |
|||
* 系统数据字典模型 |
|||
* Class SysDict |
|||
* @package app\model\sys |
|||
*/ |
|||
class SysDict extends BaseModel |
|||
{ |
|||
/** |
|||
* 数据表主键 |
|||
* @var string |
|||
*/ |
|||
protected $pk = 'id'; |
|||
|
|||
/** |
|||
* 模型名称 |
|||
* @var string |
|||
*/ |
|||
protected $name = 'sys_dict'; |
|||
|
|||
protected $type = [ |
|||
'value' => '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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
<?php |
|||
// +---------------------------------------------------------------------- |
|||
// | Niucloud-admin 企业快速开发的多应用管理平台 |
|||
// +---------------------------------------------------------------------- |
|||
// | 官方网址:https://www.niucloud.com |
|||
// +---------------------------------------------------------------------- |
|||
// | niucloud团队 版权所有 开源版本可自由商用 |
|||
// +---------------------------------------------------------------------- |
|||
// | Author: Niucloud Team |
|||
// +---------------------------------------------------------------------- |
|||
|
|||
namespace app\service\api\apiService; |
|||
|
|||
use app\model\service_logs\ServiceLogs; |
|||
use core\base\BaseApiService; |
|||
|
|||
/** |
|||
* 服务API服务层 |
|||
* Class ServiceService |
|||
* @package app\service\api\apiService |
|||
*/ |
|||
class ServiceService extends BaseApiService |
|||
{ |
|||
public function __construct() |
|||
{ |
|||
parent::__construct(); |
|||
$this->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()]; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,297 @@ |
|||
<?php |
|||
// +---------------------------------------------------------------------- |
|||
// | Niucloud-admin 企业快速开发的多应用管理平台 |
|||
// +---------------------------------------------------------------------- |
|||
// | 官方网址:https://www.niucloud.com |
|||
// +---------------------------------------------------------------------- |
|||
// | niucloud团队 版权所有 开源版本可自由商用 |
|||
// +---------------------------------------------------------------------- |
|||
// | Author: Niucloud Team |
|||
// +---------------------------------------------------------------------- |
|||
|
|||
namespace app\service\api\common; |
|||
|
|||
use app\model\dict\Dict; |
|||
use core\base\BaseApiService; |
|||
|
|||
/** |
|||
* 字典批量获取服务 |
|||
* Class DictService |
|||
* @package app\service\api\common |
|||
*/ |
|||
class DictService extends BaseApiService |
|||
{ |
|||
public function __construct() |
|||
{ |
|||
parent::__construct(); |
|||
$this->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] ?? []; |
|||
} |
|||
} |
|||
@ -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; |
|||
@ -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<Object>} 字典数据对象 |
|||
*/ |
|||
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<Object>} 字典数据对象 |
|||
*/ |
|||
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<Array>} 字典数据数组 |
|||
*/ |
|||
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<Object>} 映射关系对象 |
|||
*/ |
|||
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<void>} |
|||
*/ |
|||
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<Object>} 字典数据对象 |
|||
*/ |
|||
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 |
|||
@ -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<Object>} 字典数据对象 |
|||
*/ |
|||
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<Array>} 字典数据数组 |
|||
*/ |
|||
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 |
|||
@ -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') |
|||
} |
|||
``` |
|||
|
|||
通过这种方式,你可以大幅提升字典数据获取的性能和用户体验。 |
|||
@ -0,0 +1,542 @@ |
|||
<template> |
|||
<view class="container"> |
|||
<view class="header"> |
|||
<text class="title">字典获取优化演示</text> |
|||
<text class="subtitle">对比原方式和批量获取的性能差异</text> |
|||
</view> |
|||
|
|||
<!-- 性能对比 --> |
|||
<view class="performance-section"> |
|||
<view class="section-title">性能对比</view> |
|||
|
|||
<!-- 原有方式 --> |
|||
<view class="test-item"> |
|||
<view class="test-header"> |
|||
<text class="test-name">原方式(单个获取)</text> |
|||
<button |
|||
class="test-btn old-style" |
|||
@click="testOldMethod" |
|||
:disabled="oldTesting"> |
|||
{{ oldTesting ? '测试中...' : '开始测试' }} |
|||
</button> |
|||
</view> |
|||
<view class="test-result" v-if="oldResult"> |
|||
<text class="result-text">耗时: {{ oldResult.time }}ms</text> |
|||
<text class="result-text">请求次数: {{ oldResult.requests }}次</text> |
|||
<text class="result-text">获取数据: {{ oldResult.count }}个字典</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 新方式 --> |
|||
<view class="test-item"> |
|||
<view class="test-header"> |
|||
<text class="test-name">新方式(批量获取)</text> |
|||
<button |
|||
class="test-btn new-style" |
|||
@click="testNewMethod" |
|||
:disabled="newTesting"> |
|||
{{ newTesting ? '测试中...' : '开始测试' }} |
|||
</button> |
|||
</view> |
|||
<view class="test-result" v-if="newResult"> |
|||
<text class="result-text">耗时: {{ newResult.time }}ms</text> |
|||
<text class="result-text">请求次数: {{ newResult.requests }}次</text> |
|||
<text class="result-text">获取数据: {{ newResult.count }}个字典</text> |
|||
<text class="improvement"> |
|||
性能提升: {{ newResult.improvement }} |
|||
</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 缓存演示 --> |
|||
<view class="cache-section"> |
|||
<view class="section-title">缓存机制演示</view> |
|||
|
|||
<view class="cache-controls"> |
|||
<button class="cache-btn" @click="testCache">测试缓存效果</button> |
|||
<button class="cache-btn clear" @click="clearCache">清除缓存</button> |
|||
</view> |
|||
|
|||
<view class="cache-result" v-if="cacheResult"> |
|||
<text class="cache-text">第一次获取: {{ cacheResult.firstTime }}ms</text> |
|||
<text class="cache-text">缓存获取: {{ cacheResult.cacheTime }}ms</text> |
|||
<text class="cache-text">性能提升: {{ cacheResult.improvement }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 字典数据展示 --> |
|||
<view class="data-section"> |
|||
<view class="section-title">字典数据展示</view> |
|||
|
|||
<view class="dict-tabs"> |
|||
<view |
|||
class="tab-item" |
|||
:class="{ active: activeTab === key }" |
|||
v-for="(data, key) in dictData" |
|||
:key="key" |
|||
@click="activeTab = key"> |
|||
{{ getDictDisplayName(key) }} |
|||
</view> |
|||
</view> |
|||
|
|||
<scroll-view class="dict-content" scroll-y> |
|||
<view class="dict-items" v-if="dictData[activeTab]"> |
|||
<view |
|||
class="dict-item" |
|||
v-for="(item, index) in dictData[activeTab]" |
|||
:key="index"> |
|||
<text class="item-name">{{ item.name || item.text || '-' }}</text> |
|||
<text class="item-value">{{ item.value || '-' }}</text> |
|||
</view> |
|||
</view> |
|||
<view class="empty-state" v-else> |
|||
<text>暂无数据</text> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
|
|||
<!-- 使用说明 --> |
|||
<view class="usage-section"> |
|||
<view class="section-title">使用说明</view> |
|||
<view class="usage-content"> |
|||
<text class="usage-text">1. 使用 dictUtil.getBatchDict() 批量获取字典</text> |
|||
<text class="usage-text">2. 支持自动缓存,30分钟有效期</text> |
|||
<text class="usage-text">3. 支持业务场景批量获取</text> |
|||
<text class="usage-text">4. 向后兼容原有 util.getDict() 方法</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import dictUtil from '@/common/dictUtil.js' |
|||
import util from '@/common/util.js' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
// 测试状态 |
|||
oldTesting: false, |
|||
newTesting: false, |
|||
|
|||
// 测试结果 |
|||
oldResult: null, |
|||
newResult: null, |
|||
cacheResult: null, |
|||
|
|||
// 字典数据 |
|||
dictData: {}, |
|||
activeTab: '', |
|||
|
|||
// 测试用的字典keys |
|||
testKeys: [ |
|||
'SourceChannel', |
|||
'source', |
|||
'customer_purchasing_power', |
|||
'preliminarycustomerintention', |
|||
'cognitive_concept', |
|||
'kh_status', |
|||
'decision_maker', |
|||
'distance' |
|||
] |
|||
} |
|||
}, |
|||
onLoad() { |
|||
// 初始化时获取一次字典数据用于展示 |
|||
this.loadInitialData() |
|||
}, |
|||
methods: { |
|||
// 加载初始数据 |
|||
async loadInitialData() { |
|||
try { |
|||
const data = await dictUtil.getBatchDict(this.testKeys) |
|||
this.dictData = data |
|||
this.activeTab = Object.keys(data)[0] || '' |
|||
} catch (error) { |
|||
console.error('加载初始数据失败:', error) |
|||
} |
|||
}, |
|||
|
|||
// 测试原有方式(单个获取) |
|||
async testOldMethod() { |
|||
this.oldTesting = true |
|||
this.oldResult = null |
|||
|
|||
try { |
|||
const startTime = Date.now() |
|||
let successCount = 0 |
|||
|
|||
// 模拟原有的单个获取方式 |
|||
for (const key of this.testKeys) { |
|||
try { |
|||
await this.getOldDict(key) |
|||
successCount++ |
|||
} catch (error) { |
|||
console.warn(`获取字典 ${key} 失败:`, error) |
|||
} |
|||
} |
|||
|
|||
const endTime = Date.now() |
|||
const totalTime = endTime - startTime |
|||
|
|||
this.oldResult = { |
|||
time: totalTime, |
|||
requests: this.testKeys.length, |
|||
count: successCount |
|||
} |
|||
|
|||
uni.showToast({ |
|||
title: `原方式完成,耗时 ${totalTime}ms`, |
|||
icon: 'none' |
|||
}) |
|||
|
|||
} catch (error) { |
|||
console.error('测试原方式失败:', error) |
|||
uni.showToast({ |
|||
title: '测试失败', |
|||
icon: 'none' |
|||
}) |
|||
} finally { |
|||
this.oldTesting = false |
|||
} |
|||
}, |
|||
|
|||
// 测试新方式(批量获取) |
|||
async testNewMethod() { |
|||
this.newTesting = true |
|||
this.newResult = null |
|||
|
|||
try { |
|||
const startTime = Date.now() |
|||
|
|||
// 使用新的批量获取方式 |
|||
const data = await dictUtil.getBatchDict(this.testKeys, false) // 不使用缓存进行公平测试 |
|||
|
|||
const endTime = Date.now() |
|||
const totalTime = endTime - startTime |
|||
const successCount = Object.keys(data).length |
|||
|
|||
this.newResult = { |
|||
time: totalTime, |
|||
requests: 1, // 批量获取只需要1次请求 |
|||
count: successCount |
|||
} |
|||
|
|||
// 计算性能提升 |
|||
if (this.oldResult) { |
|||
const improvement = Math.round(((this.oldResult.time - totalTime) / this.oldResult.time) * 100) |
|||
this.newResult.improvement = `${improvement}%` |
|||
} |
|||
|
|||
// 更新展示数据 |
|||
this.dictData = data |
|||
|
|||
uni.showToast({ |
|||
title: `新方式完成,耗时 ${totalTime}ms`, |
|||
icon: 'none' |
|||
}) |
|||
|
|||
} catch (error) { |
|||
console.error('测试新方式失败:', error) |
|||
uni.showToast({ |
|||
title: '测试失败', |
|||
icon: 'none' |
|||
}) |
|||
} finally { |
|||
this.newTesting = false |
|||
} |
|||
}, |
|||
|
|||
// 模拟原有的单个字典获取 |
|||
async getOldDict(key) { |
|||
return new Promise((resolve, reject) => { |
|||
// 模拟单个接口请求的延迟 |
|||
setTimeout(async () => { |
|||
try { |
|||
// 这里可以调用原有的获取方法,或者模拟 |
|||
const result = await dictUtil.getDict(key, false) |
|||
resolve(result) |
|||
} catch (error) { |
|||
reject(error) |
|||
} |
|||
}, Math.random() * 200 + 100) // 模拟 100-300ms 的网络延迟 |
|||
}) |
|||
}, |
|||
|
|||
// 测试缓存效果 |
|||
async testCache() { |
|||
try { |
|||
// 清除缓存确保公平测试 |
|||
dictUtil.clearCache() |
|||
|
|||
// 第一次获取(无缓存) |
|||
const startTime1 = Date.now() |
|||
await dictUtil.getBatchDict(this.testKeys.slice(0, 3), true) |
|||
const firstTime = Date.now() - startTime1 |
|||
|
|||
// 第二次获取(有缓存) |
|||
const startTime2 = Date.now() |
|||
await dictUtil.getBatchDict(this.testKeys.slice(0, 3), true) |
|||
const cacheTime = Date.now() - startTime2 |
|||
|
|||
const improvement = Math.round(((firstTime - cacheTime) / firstTime) * 100) |
|||
|
|||
this.cacheResult = { |
|||
firstTime, |
|||
cacheTime, |
|||
improvement: `${improvement}%` |
|||
} |
|||
|
|||
uni.showToast({ |
|||
title: '缓存测试完成', |
|||
icon: 'success' |
|||
}) |
|||
|
|||
} catch (error) { |
|||
console.error('缓存测试失败:', error) |
|||
uni.showToast({ |
|||
title: '缓存测试失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 清除缓存 |
|||
clearCache() { |
|||
dictUtil.clearCache() |
|||
this.cacheResult = null |
|||
uni.showToast({ |
|||
title: '缓存已清除', |
|||
icon: 'success' |
|||
}) |
|||
}, |
|||
|
|||
// 获取字典显示名称 |
|||
getDictDisplayName(key) { |
|||
const nameMap = { |
|||
'SourceChannel': '来源渠道', |
|||
'source': '来源', |
|||
'customer_purchasing_power': '购买力', |
|||
'preliminarycustomerintention': '意向度', |
|||
'cognitive_concept': '认知理念', |
|||
'kh_status': '客户状态', |
|||
'decision_maker': '决策人', |
|||
'distance': '距离' |
|||
} |
|||
return nameMap[key] || key |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.container { |
|||
padding: 20rpx; |
|||
background: #f5f5f5; |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
.header { |
|||
background: #fff; |
|||
padding: 30rpx; |
|||
border-radius: 12rpx; |
|||
margin-bottom: 20rpx; |
|||
text-align: center; |
|||
|
|||
.title { |
|||
font-size: 36rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
display: block; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
|
|||
.subtitle { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
display: block; |
|||
} |
|||
} |
|||
|
|||
.performance-section, .cache-section, .data-section, .usage-section { |
|||
background: #fff; |
|||
border-radius: 12rpx; |
|||
padding: 30rpx; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.section-title { |
|||
font-size: 32rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
margin-bottom: 20rpx; |
|||
border-left: 4rpx solid #29d3b4; |
|||
padding-left: 16rpx; |
|||
} |
|||
|
|||
.test-item { |
|||
margin-bottom: 30rpx; |
|||
padding: 20rpx; |
|||
background: #f8f9fa; |
|||
border-radius: 8rpx; |
|||
|
|||
&:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
} |
|||
|
|||
.test-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 15rpx; |
|||
|
|||
.test-name { |
|||
font-size: 28rpx; |
|||
font-weight: 500; |
|||
color: #333; |
|||
} |
|||
|
|||
.test-btn { |
|||
padding: 12rpx 24rpx; |
|||
border-radius: 6rpx; |
|||
border: none; |
|||
font-size: 24rpx; |
|||
color: #fff; |
|||
|
|||
&.old-style { |
|||
background: #ff6b6b; |
|||
} |
|||
|
|||
&.new-style { |
|||
background: #29d3b4; |
|||
} |
|||
|
|||
&:disabled { |
|||
opacity: 0.6; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.test-result { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8rpx; |
|||
|
|||
.result-text { |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
} |
|||
|
|||
.improvement { |
|||
font-size: 26rpx; |
|||
color: #29d3b4; |
|||
font-weight: 600; |
|||
} |
|||
} |
|||
|
|||
.cache-controls { |
|||
display: flex; |
|||
gap: 20rpx; |
|||
margin-bottom: 20rpx; |
|||
|
|||
.cache-btn { |
|||
flex: 1; |
|||
padding: 16rpx; |
|||
border-radius: 8rpx; |
|||
border: none; |
|||
font-size: 26rpx; |
|||
color: #fff; |
|||
background: #29d3b4; |
|||
|
|||
&.clear { |
|||
background: #ff6b6b; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.cache-result { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8rpx; |
|||
|
|||
.cache-text { |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
} |
|||
} |
|||
|
|||
.dict-tabs { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 10rpx; |
|||
margin-bottom: 20rpx; |
|||
|
|||
.tab-item { |
|||
padding: 12rpx 20rpx; |
|||
background: #f0f0f0; |
|||
border-radius: 20rpx; |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
|
|||
&.active { |
|||
background: #29d3b4; |
|||
color: #fff; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.dict-content { |
|||
height: 400rpx; |
|||
border: 1rpx solid #eee; |
|||
border-radius: 8rpx; |
|||
} |
|||
|
|||
.dict-items { |
|||
padding: 20rpx; |
|||
} |
|||
|
|||
.dict-item { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 16rpx 0; |
|||
border-bottom: 1rpx solid #f0f0f0; |
|||
|
|||
&:last-child { |
|||
border-bottom: none; |
|||
} |
|||
|
|||
.item-name { |
|||
font-size: 26rpx; |
|||
color: #333; |
|||
flex: 1; |
|||
} |
|||
|
|||
.item-value { |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
margin-left: 20rpx; |
|||
} |
|||
} |
|||
|
|||
.empty-state { |
|||
padding: 60rpx; |
|||
text-align: center; |
|||
color: #999; |
|||
font-size: 26rpx; |
|||
} |
|||
|
|||
.usage-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 12rpx; |
|||
|
|||
.usage-text { |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
line-height: 1.6; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,263 @@ |
|||
<template> |
|||
<view class="container"> |
|||
<view class="header"> |
|||
<text class="title">字典功能测试</text> |
|||
</view> |
|||
|
|||
<view class="test-section"> |
|||
<view class="section-title">1. 测试单个字典获取</view> |
|||
<button class="test-btn" @click="testSingleDict">测试获取单个字典</button> |
|||
<view class="result" v-if="singleResult"> |
|||
<text class="result-title">结果:</text> |
|||
<text class="result-content">{{ JSON.stringify(singleResult, null, 2) }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="test-section"> |
|||
<view class="section-title">2. 测试批量字典获取</view> |
|||
<button class="test-btn" @click="testBatchDict">测试批量获取字典</button> |
|||
<view class="result" v-if="batchResult"> |
|||
<text class="result-title">结果:</text> |
|||
<text class="result-content">{{ JSON.stringify(batchResult, null, 2) }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="test-section"> |
|||
<view class="section-title">3. 测试字典缓存</view> |
|||
<button class="test-btn" @click="testCache">测试缓存机制</button> |
|||
<view class="result" v-if="cacheResult"> |
|||
<text class="result-title">缓存测试结果:</text> |
|||
<text class="result-content">{{ cacheResult }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="test-section"> |
|||
<view class="section-title">4. 测试静默请求</view> |
|||
<button class="test-btn" @click="testQuietRequest">测试静默请求</button> |
|||
<view class="result" v-if="quietResult"> |
|||
<text class="result-title">静默请求结果:</text> |
|||
<text class="result-content">{{ JSON.stringify(quietResult, null, 2) }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="test-section"> |
|||
<view class="section-title">5. 清除缓存</view> |
|||
<button class="test-btn clear" @click="clearAllCache">清除所有缓存</button> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import dictUtil from '@/common/dictUtil.js' |
|||
import axiosQuiet from '@/common/axiosQuiet.js' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
singleResult: null, |
|||
batchResult: null, |
|||
cacheResult: null, |
|||
quietResult: null |
|||
} |
|||
}, |
|||
methods: { |
|||
// 测试单个字典获取 |
|||
async testSingleDict() { |
|||
try { |
|||
console.log('开始测试单个字典获取') |
|||
const result = await dictUtil.getDict('source') |
|||
this.singleResult = result |
|||
console.log('单个字典获取结果:', result) |
|||
|
|||
uni.showToast({ |
|||
title: '单个字典测试完成', |
|||
icon: 'success' |
|||
}) |
|||
} catch (error) { |
|||
console.error('单个字典获取失败:', error) |
|||
this.singleResult = { error: error.message || '获取失败' } |
|||
|
|||
uni.showToast({ |
|||
title: '单个字典测试失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 测试批量字典获取 |
|||
async testBatchDict() { |
|||
try { |
|||
console.log('开始测试批量字典获取') |
|||
const keys = ['source', 'SourceChannel', 'customer_purchasing_power'] |
|||
const result = await dictUtil.getBatchDict(keys) |
|||
this.batchResult = result |
|||
console.log('批量字典获取结果:', result) |
|||
|
|||
uni.showToast({ |
|||
title: '批量字典测试完成', |
|||
icon: 'success' |
|||
}) |
|||
} catch (error) { |
|||
console.error('批量字典获取失败:', error) |
|||
this.batchResult = { error: error.message || '获取失败' } |
|||
|
|||
uni.showToast({ |
|||
title: '批量字典测试失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 测试缓存机制 |
|||
async testCache() { |
|||
try { |
|||
console.log('开始测试缓存机制') |
|||
|
|||
// 清除缓存 |
|||
dictUtil.clearCache(['source']) |
|||
|
|||
// 第一次获取(应该从接口获取) |
|||
const start1 = Date.now() |
|||
await dictUtil.getDict('source', true) |
|||
const time1 = Date.now() - start1 |
|||
|
|||
// 第二次获取(应该从缓存获取) |
|||
const start2 = Date.now() |
|||
await dictUtil.getDict('source', true) |
|||
const time2 = Date.now() - start2 |
|||
|
|||
this.cacheResult = `第一次获取: ${time1}ms, 第二次获取: ${time2}ms, 缓存提升: ${Math.round((time1 - time2) / time1 * 100)}%` |
|||
|
|||
uni.showToast({ |
|||
title: '缓存测试完成', |
|||
icon: 'success' |
|||
}) |
|||
} catch (error) { |
|||
console.error('缓存测试失败:', error) |
|||
this.cacheResult = '缓存测试失败: ' + (error.message || '未知错误') |
|||
|
|||
uni.showToast({ |
|||
title: '缓存测试失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 测试静默请求 |
|||
async testQuietRequest() { |
|||
try { |
|||
console.log('开始测试静默请求') |
|||
const result = await axiosQuiet.get('/dict/batch', { |
|||
keys: 'source,SourceChannel' |
|||
}) |
|||
this.quietResult = result |
|||
console.log('静默请求结果:', result) |
|||
|
|||
uni.showToast({ |
|||
title: '静默请求测试完成', |
|||
icon: 'success' |
|||
}) |
|||
} catch (error) { |
|||
console.error('静默请求失败:', error) |
|||
this.quietResult = { error: error.message || error.msg || '请求失败' } |
|||
|
|||
uni.showToast({ |
|||
title: '静默请求测试失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
// 清除所有缓存 |
|||
clearAllCache() { |
|||
dictUtil.clearCache() |
|||
this.singleResult = null |
|||
this.batchResult = null |
|||
this.cacheResult = null |
|||
this.quietResult = null |
|||
|
|||
uni.showToast({ |
|||
title: '缓存已清除', |
|||
icon: 'success' |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.container { |
|||
padding: 20rpx; |
|||
background: #f5f5f5; |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
.header { |
|||
background: #fff; |
|||
padding: 30rpx; |
|||
border-radius: 12rpx; |
|||
margin-bottom: 20rpx; |
|||
text-align: center; |
|||
|
|||
.title { |
|||
font-size: 36rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
} |
|||
} |
|||
|
|||
.test-section { |
|||
background: #fff; |
|||
border-radius: 12rpx; |
|||
padding: 30rpx; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.section-title { |
|||
font-size: 28rpx; |
|||
font-weight: 600; |
|||
color: #333; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.test-btn { |
|||
background: #29d3b4; |
|||
color: #fff; |
|||
border: none; |
|||
border-radius: 8rpx; |
|||
padding: 16rpx 32rpx; |
|||
font-size: 26rpx; |
|||
margin-bottom: 20rpx; |
|||
|
|||
&.clear { |
|||
background: #ff6b6b; |
|||
} |
|||
|
|||
&:active { |
|||
opacity: 0.8; |
|||
} |
|||
} |
|||
|
|||
.result { |
|||
background: #f8f9fa; |
|||
border-radius: 8rpx; |
|||
padding: 20rpx; |
|||
border-left: 4rpx solid #29d3b4; |
|||
|
|||
.result-title { |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
display: block; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
|
|||
.result-content { |
|||
font-size: 22rpx; |
|||
color: #333; |
|||
word-break: break-all; |
|||
white-space: pre-wrap; |
|||
font-family: monospace; |
|||
line-height: 1.5; |
|||
} |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue