Browse Source

添加合同签订模块

master
王泽彦 9 months ago
parent
commit
564ffb46e7
  1. 1
      .gitignore
  2. 400
      admin/src/views/personnel/personnel_approval_demo.vue
  3. 30
      niucloud/app/adminapi/controller/personnel/Personnel.php
  4. 2
      niucloud/app/adminapi/route/personnel.php
  5. 74
      niucloud/app/api/controller/apiController/Personnel.php
  6. 155
      niucloud/app/api/controller/common/Dict.php
  7. 17
      niucloud/app/api/route/route.php
  8. 5
      niucloud/app/model/personnel/Personnel.php
  9. 48
      niucloud/app/model/service_logs/ServiceLogs.php
  10. 82
      niucloud/app/model/sys/SysDict.php
  11. 160
      niucloud/app/service/api/apiService/ServiceService.php
  12. 297
      niucloud/app/service/api/common/DictService.php
  13. 118
      niucloud/app/service/school_approval/SchoolApprovalProcessService.php
  14. 85
      uniapp/common/axiosQuiet.js
  15. 332
      uniapp/common/dictUtil.js
  16. 187
      uniapp/common/dictUtilSimple.js
  17. 3
      uniapp/pages.json
  18. 854
      uniapp/pages/coach/my/service_detail.vue
  19. 542
      uniapp/pages/demo/dict_optimization.vue
  20. 114
      uniapp/pages/market/clue/add_clues.vue
  21. 263
      uniapp/pages/test/dict_test.vue

1
.gitignore

@ -8,3 +8,4 @@
/niucloud/runtime
/niucloud/vendor
/CLAUDE.md
.claude

400
admin/src/views/personnel/personnel_approval_demo.vue

@ -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>

30
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);
}
}

2
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,

74
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());
}
}
}

155
niucloud/app/api/controller/common/Dict.php

@ -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());
}
}
}

17
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)

5
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

48
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']);
}
}

82
niucloud/app/model/sys/SysDict.php

@ -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);
}
}
}

160
niucloud/app/service/api/apiService/ServiceService.php

@ -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()];
}
}
}

297
niucloud/app/service/api/common/DictService.php

@ -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] ?? [];
}
}

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

85
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;

332
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<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

187
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<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

3
uniapp/pages.json

@ -654,7 +654,8 @@
"style": {
"navigationBarTitleText": "服务详情",
"navigationBarBackgroundColor": "#29d3b4",
"navigationBarTextStyle": "black"
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true
}
},
{

854
uniapp/pages/coach/my/service_detail.vue

@ -2,139 +2,465 @@
<template>
<view class="container">
<view class="main-content">
<view class="service-header">
<view class="service-title">我的服务详情</view>
<view class="service-subtitle">查看当前服务状态和详细信息</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-container">
<uni-load-more status="loading" content-text="加载中..."></uni-load-more>
</view>
<view class="service-cards">
<view class="service-card" v-for="(service, index) in serviceList" :key="index">
<view class="card-header">
<view class="service-name">{{ service.name }}</view>
<view class="service-status" :class="service.status === '正常' ? 'status-active' : 'status-inactive'">
{{ service.status }}
</view>
<!-- 服务记录列表 -->
<view v-else class="service-cards">
<view class="service-card" v-for="(service, index) in serviceList" :key="index" @click="viewServiceDetail(service)">
<!-- 服务预览图 -->
<view class="service-preview" v-if="service.preview_image_url">
<image :src="getImageUrl(service.preview_image_url)" class="preview-image" mode="aspectFill"></image>
</view>
<view class="card-content">
<!-- 服务名称和状态 -->
<view class="card-header">
<view class="service-name">{{ service.service_name }}</view>
<view class="service-status" :class="service.status === 1 ? 'status-active' : (service.status === 0 ? 'status-pending' : 'status-inactive')">
{{ service.status === 1 ? '已完成' : (service.status === 0 ? '待处理' : '未知状态') }}
</view>
</view>
<!-- 服务信息 -->
<view class="service-info">
<view class="info-item">
<view class="info-item" v-if="service.service_type">
<text class="label">服务类型</text>
<text class="value">{{ service.type }}</text>
<text class="value">{{ service.service_type }}</text>
</view>
<view class="info-item">
<text class="label">开始时间</text>
<text class="value">{{ service.startTime }}</text>
<view class="info-item" v-if="service.description">
<text class="label">服务描述</text>
<text class="value">{{ service.description }}</text>
</view>
<view class="info-item">
<text class="label">结束时间</text>
<text class="value">{{ service.endTime }}</text>
<view class="info-item" v-if="service.service_remark || service.status !== 1">
<text class="label">服务结果</text>
<view class="value-content" v-if="service.service_remark">
<rich-text :nodes="formatRichText(service.service_remark)"></rich-text>
</view>
<text class="value placeholder" v-else-if="service.status !== 1">点击编辑服务结果</text>
</view>
<view class="info-item" v-if="service.feedback">
<text class="label">家长反馈</text>
<text class="value">{{ service.feedback }}</text>
</view>
<view class="info-item" v-if="service.score">
<text class="label">家长评分</text>
<text class="value score">{{ service.score }}</text>
</view>
<view class="info-item">
<text class="label">服务描述</text>
<text class="value">{{ service.description }}</text>
<text class="label">创建时间</text>
<text class="value">{{ formatDate(service.created_at) }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="card-actions" v-if="service.status !== 1">
<button class="edit-btn" @click.stop="editServiceRemark(service)">
<text class="iconfont icon-edit"></text>
编辑服务结果
</button>
</view>
</view>
<view class="card-actions">
<view class="btn btn-detail" @click="viewDetail(service)">查看详情</view>
<view class="btn btn-contact" @click="contactService(service)">联系客服</view>
<!-- 箭头图标 -->
<view class="card-arrow">
<text class="iconfont icon-arrow-right"></text>
</view>
</view>
</view>
<view class="empty-state" v-if="serviceList.length === 0">
<image class="empty-icon" src="/static/icon-img/empty.png"></image>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && serviceList.length === 0">
<view class="empty-icon">📝</view>
<view class="empty-text">暂无服务记录</view>
</view>
<!-- 底部加载更多 -->
<view v-if="hasMore && !loading && serviceList.length > 0" class="load-more">
<uni-load-more :status="loadMoreStatus" @clickLoadMore="loadMore"></uni-load-more>
</view>
</view>
<!-- 编辑服务结果弹窗 -->
<view v-if="showEditModal" class="edit-modal" @click="closeEditModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">编辑服务结果</text>
<view class="modal-close" @click="closeEditModal">
<text class="iconfont icon-close"></text>
</view>
</view>
<view class="modal-body">
<view class="form-item">
<text class="form-label">服务结果</text>
<!-- 富文本工具栏 -->
<view class="editor-toolbar">
<view class="toolbar-group">
<view class="tool-btn" :class="{ active: bold }" @click="toggleBold">
<text class="tool-text">B</text>
</view>
<view class="tool-btn" :class="{ active: italic }" @click="toggleItalic">
<text class="tool-text">I</text>
</view>
<view class="tool-btn" @click="insertBulletList">
<text class="tool-text"></text>
</view>
<view class="tool-btn" @click="insertNumberList">
<text class="tool-text">1.</text>
</view>
</view>
</view>
<!-- 文本输入区域 -->
<textarea
class="form-textarea"
v-model="editForm.service_remark"
placeholder="请输入服务结果内容,支持简单的格式化文本"
maxlength="1000"
:show-confirm-bar="false"
@focus="onTextareaFocus"
@blur="onTextareaBlur">
</textarea>
<!-- 字数统计 -->
<view class="char-count">{{ editForm.service_remark.length }}/1000</view>
<!-- 提示信息 -->
<view class="editor-tips">
<text class="tip-text">支持基础格式粗体斜体列表等</text>
</view>
</view>
</view>
<view class="modal-footer">
<button class="modal-btn cancel" @click="closeEditModal">取消</button>
<button class="modal-btn confirm" @click="saveServiceRemark" :disabled="saving">
{{ saving ? '保存中...' : '保存' }}
</button>
</view>
</view>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js';
import apiRoute from '@/common/axios.js';
export default {
data() {
return {
serviceList: []
loading: true,
serviceList: [],
currentPage: 1,
pageSize: 10,
hasMore: true,
loadMoreStatus: 'more',
showEditModal: false,
saving: false,
editForm: {
id: 0,
service_remark: ''
},
//
bold: false,
italic: false,
textareaFocused: false
}
},
onLoad() {
this.init();
},
onPullDownRefresh() {
this.refreshServiceList();
},
onReachBottom() {
if (this.hasMore && !this.loading) {
this.loadMore();
}
},
methods: {
async init() {
this.getServiceList();
},
//
async getServiceList() {
//
async getServiceList(refresh = false) {
if (refresh) {
this.currentPage = 1;
this.hasMore = true;
this.serviceList = [];
}
this.loading = true;
this.loadMoreStatus = 'loading';
try {
// API
this.serviceList = [
{
id: 1,
name: '教练服务套餐A',
type: '专业训练',
status: '正常',
startTime: '2024-01-01',
endTime: '2024-12-31',
description: '专业体能训练指导服务'
},
{
id: 2,
name: '教练服务套餐B',
type: '基础指导',
status: '即将到期',
startTime: '2024-06-01',
endTime: '2024-06-30',
description: '基础动作指导和纠正'
const response = await apiRoute.get('/personnel/myServiceLogs', {
page: this.currentPage,
limit: this.pageSize,
demo: 1 //
});
if (response.data.code === 1) {
const newServices = response.data.data.data || [];
if (refresh) {
this.serviceList = newServices;
} else {
this.serviceList = [...this.serviceList, ...newServices];
}
];
// API
// let res = await apiRoute.getServiceList({});
// if (res.code === 1) {
// this.serviceList = res.data;
// }
//
this.hasMore = newServices.length === this.pageSize;
this.loadMoreStatus = this.hasMore ? 'more' : 'noMore';
} else {
uni.showToast({
title: response.data.msg || '加载失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取服务记录失败:', error);
uni.showToast({
title: '网络错误,请稍后重试',
icon: 'none'
});
} finally {
this.loading = false;
if (refresh) {
uni.stopPullDownRefresh();
}
}
},
//
refreshServiceList() {
this.getServiceList(true);
},
//
loadMore() {
if (this.hasMore && !this.loading) {
this.currentPage++;
this.getServiceList();
}
},
//
async viewServiceDetail(service) {
try {
const response = await apiRoute.get('/personnel/serviceLogDetail', {
id: service.id
});
if (response.data.code === 1) {
const detail = response.data.data;
this.showServiceDetailModal(detail);
} else {
uni.showToast({
title: response.data.msg || '获取详情失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取服务列表失败:', error);
console.error('获取服务详情失败:', error);
uni.showToast({
title: '获取数据失败',
title: '网络错误,请稍后重试',
icon: 'none'
});
}
},
//
viewDetail(service) {
//
showServiceDetailModal(detail) {
let content = `服务名称:${detail.service_name || '-'}\n`;
content += `服务类型:${detail.service_type || '-'}\n`;
content += `状态:${detail.status === 1 ? '已完成' : (detail.status === 0 ? '待处理' : '未知状态')}\n`;
if (detail.description) {
content += `描述:${detail.description}\n`;
}
if (detail.service_remark) {
content += `服务结果:${detail.service_remark}\n`;
}
if (detail.feedback) {
content += `家长反馈:${detail.feedback}\n`;
}
if (detail.score) {
content += `家长评分:${detail.score}`;
}
uni.showModal({
title: '服务详情',
content: `服务名称:${service.name}\n服务类型:${service.type}\n状态:${service.status}\n描述:${service.description}`,
content: content,
showCancel: false
});
},
//
contactService(service) {
uni.showActionSheet({
itemList: ['电话客服', '在线客服', '邮件客服'],
success: (res) => {
const actions = ['拨打客服电话', '打开在线客服', '发送邮件'];
// URL
getImageUrl(url) {
if (!url) return '';
if (url.startsWith('http')) {
return url;
}
return this.$baseUrl + '/' + url;
},
//
formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
// HTML
stripHtml(html) {
if (!html) return '';
return html.replace(/<[^>]*>/g, '').trim();
},
//
formatRichText(text) {
if (!text) return '';
// markdown
let formatted = text
//
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
//
.replace(/\*(.*?)\*/g, '<em>$1</em>')
//
.replace(/^\u2022\s(.+)$/gm, '<li>$1</li>')
//
.replace(/^\d+\.\s(.+)$/gm, '<li>$1</li>')
//
.replace(/\n/g, '<br/>');
// ul
if (formatted.includes('<li>')) {
formatted = formatted.replace(/(<li>.*?<\/li>)/g, '<ul>$1</ul>');
}
return formatted;
},
//
editServiceRemark(service) {
//
if (service.status === 1) {
uni.showToast({
title: '服务已完成,无法修改',
icon: 'none'
});
return;
}
this.editForm.id = service.id;
this.editForm.service_remark = this.stripHtml(service.service_remark || '');
this.showEditModal = true;
},
//
closeEditModal() {
this.showEditModal = false;
this.editForm = {
id: 0,
service_remark: ''
};
},
//
async saveServiceRemark() {
if (!this.editForm.service_remark.trim()) {
uni.showToast({
title: '请输入服务结果内容',
icon: 'none'
});
return;
}
this.saving = true;
try {
const response = await apiRoute.post('/personnel/updateServiceRemark', {
id: this.editForm.id,
service_remark: this.editForm.service_remark
});
if (response.data.code === 1) {
uni.showToast({
title: '保存成功',
icon: 'success'
});
//
const index = this.serviceList.findIndex(item => item.id === this.editForm.id);
if (index !== -1) {
this.serviceList[index].service_remark = this.editForm.service_remark;
}
this.closeEditModal();
} else {
uni.showToast({
title: actions[res.tapIndex],
title: response.data.msg || '保存失败',
icon: 'none'
});
}
});
} catch (error) {
console.error('保存服务结果失败:', error);
uni.showToast({
title: '网络错误,请稍后重试',
icon: 'none'
});
} finally {
this.saving = false;
}
},
//
onTextareaFocus() {
this.textareaFocused = true;
},
onTextareaBlur() {
this.textareaFocused = false;
},
toggleBold() {
this.bold = !this.bold;
this.insertFormatText('**', '**');
},
toggleItalic() {
this.italic = !this.italic;
this.insertFormatText('*', '*');
},
insertBulletList() {
this.insertFormatText('\n• ', '');
},
insertNumberList() {
this.insertFormatText('\n1. ', '');
},
insertFormatText(before, after) {
const textarea = this.editForm.service_remark;
const newText = textarea + before + '请输入内容' + after;
this.editForm.service_remark = newText;
}
}
}
</script>
<style lang="less" scoped>
<style lang="scss" scoped>
.container {
background: #f5f5f5;
background: #1a1a1a;
min-height: 100vh;
}
@ -142,25 +468,6 @@ export default {
padding: 20rpx;
}
.service-header {
background: #29d3b4;
border-radius: 16rpx;
padding: 40rpx;
margin-bottom: 30rpx;
color: white;
.service-title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.service-subtitle {
font-size: 26rpx;
opacity: 0.8;
}
}
.service-cards {
display: flex;
flex-direction: column;
@ -168,87 +475,177 @@ export default {
}
.service-card {
background: white;
background: #2a2a2a;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.3);
border: 1rpx solid #444;
position: relative;
display: flex;
align-items: center;
padding: 24rpx;
}
.service-preview {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
overflow: hidden;
margin-right: 24rpx;
flex-shrink: 0;
.preview-image {
width: 100%;
height: 100%;
}
}
.card-content {
flex: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
margin-bottom: 16rpx;
.service-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
font-weight: 600;
color: #fff;
flex: 1;
margin-right: 16rpx;
}
.service-status {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 500;
&.status-active {
background: #e8f5e8;
color: #52c41a;
background: rgba(41, 211, 180, 0.2);
color: #29d3b4;
border: 1rpx solid #29d3b4;
}
&.status-pending {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
border: 1rpx solid #ffc107;
}
&.status-inactive {
background: #fff2e8;
color: #fa8c16;
background: rgba(220, 53, 69, 0.2);
color: #dc3545;
border: 1rpx solid #dc3545;
}
}
}
.card-content {
margin-bottom: 25rpx;
}
.service-info {
display: flex;
flex-direction: column;
gap: 15rpx;
gap: 12rpx;
}
.info-item {
display: flex;
align-items: flex-start;
.label {
color: #666;
font-size: 28rpx;
min-width: 160rpx;
color: #999;
font-size: 26rpx;
min-width: 140rpx;
flex-shrink: 0;
}
.value {
color: #333;
font-size: 28rpx;
color: #ccc;
font-size: 26rpx;
flex: 1;
word-break: break-all;
&.score {
color: #29d3b4;
font-weight: 600;
}
&.placeholder {
color: #666;
font-style: italic;
}
}
.value-content {
flex: 1;
color: #ccc;
font-size: 26rpx;
line-height: 1.6;
/* 富文本样式 */
:deep(strong) {
font-weight: 600;
color: #fff;
}
:deep(em) {
font-style: italic;
color: #29d3b4;
}
:deep(ul) {
margin: 8rpx 0;
padding-left: 24rpx;
}
:deep(li) {
margin: 4rpx 0;
list-style: disc;
}
}
}
.card-actions {
.card-arrow {
margin-left: 16rpx;
width: 40rpx;
height: 40rpx;
display: flex;
gap: 20rpx;
align-items: center;
justify-content: center;
flex-shrink: 0;
.iconfont {
font-size: 24rpx;
color: #666;
}
}
.btn {
flex: 1;
padding: 20rpx;
border-radius: 12rpx;
text-align: center;
font-size: 28rpx;
.card-actions {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #444;
&.btn-detail {
.edit-btn {
background: #29d3b4;
color: white;
}
&.btn-contact {
background: #f0f0f0;
color: #333;
color: #fff;
border: none;
border-radius: 8rpx;
padding: 12rpx 24rpx;
font-size: 26rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
&:active {
background: #22b39a;
}
.iconfont {
font-size: 24rpx;
}
}
}
@ -257,18 +654,217 @@ export default {
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
padding: 120rpx 40rpx;
.empty-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
font-size: 120rpx;
margin-bottom: 24rpx;
opacity: 0.3;
}
.empty-text {
color: #999;
color: #666;
font-size: 28rpx;
}
}
.loading-container {
padding: 120rpx 40rpx;
text-align: center;
}
.load-more {
padding: 40rpx 0;
}
/* 动画效果 */
.service-card {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 编辑弹窗样式 */
.edit-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
.modal-content {
background-color: #2a2a2a;
border-radius: 20rpx;
width: 90%;
max-height: 80%;
overflow: hidden;
border: 1rpx solid #444;
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid #444;
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #fff;
}
.modal-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 32rpx;
color: #999;
}
}
}
.modal-body {
padding: 32rpx;
.form-item {
.form-label {
font-size: 28rpx;
color: #ccc;
margin-bottom: 16rpx;
display: block;
}
.editor-toolbar {
background-color: #1a1a1a;
border: 1rpx solid #444;
border-bottom: none;
border-radius: 12rpx 12rpx 0 0;
padding: 16rpx;
.toolbar-group {
display: flex;
gap: 16rpx;
.tool-btn {
width: 48rpx;
height: 48rpx;
background-color: #444;
border: 1rpx solid #666;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
&.active {
background-color: #29d3b4;
border-color: #29d3b4;
}
.tool-text {
font-size: 24rpx;
color: #fff;
font-weight: 600;
}
}
}
}
.form-textarea {
width: 100%;
min-height: 300rpx;
background-color: #1a1a1a;
border: 1rpx solid #444;
border-radius: 0 0 12rpx 12rpx;
border-top: none;
padding: 20rpx;
font-size: 28rpx;
color: #fff;
line-height: 1.6;
box-sizing: border-box;
&::placeholder {
color: #666;
}
}
.char-count {
text-align: right;
margin-top: 8rpx;
font-size: 24rpx;
color: #666;
}
.editor-tips {
margin-top: 12rpx;
.tip-text {
font-size: 22rpx;
color: #666;
line-height: 1.4;
}
}
}
}
.modal-footer {
display: flex;
gap: 24rpx;
padding: 32rpx;
border-top: 1rpx solid #444;
.modal-btn {
flex: 1;
height: 72rpx;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 600;
border: none;
display: flex;
align-items: center;
justify-content: center;
&.cancel {
background-color: #444;
color: #ccc;
&:active {
background-color: #555;
}
}
&.confirm {
background-color: #29d3b4;
color: #fff;
&:active:not(:disabled) {
background-color: #22b39a;
}
&:disabled {
background-color: #666;
color: #999;
}
}
}
}
}
}
</style>

542
uniapp/pages/demo/dict_optimization.vue

@ -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>

114
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({})

263
uniapp/pages/test/dict_test.vue

@ -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…
Cancel
Save