From f9ab5e18fb8ab624d8cf5a5cda052bf4b65ed858 Mon Sep 17 00:00:00 2001 From: Guoxianpeng <744964089@qq.com> Date: Thu, 9 Apr 2026 18:35:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8C=BB=E7=94=9F=E7=BB=9F=E8=AE=A1=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/views/doctor/tongji.vue | 411 ++++++++++++------ .../adminapi/lists/doctor/StatisticsLists.php | 154 +++++-- 2 files changed, 401 insertions(+), 164 deletions(-) diff --git a/admin/src/views/doctor/tongji.vue b/admin/src/views/doctor/tongji.vue index d486d1af..10955978 100644 --- a/admin/src/views/doctor/tongji.vue +++ b/admin/src/views/doctor/tongji.vue @@ -1,7 +1,12 @@ - - + + + + + 筛选条件 + + - - - - - - - - - - {{ item }} - - 未分配 - - - - - - - - - {{ item }} - - 未设置 - - - - - - - {{ row.total_count }} - - - - - - - {{ row.diagnosis_count ?? 0 }} - - - - - - - - {{ row.deal_count ?? 0 }} - - - - - - - - {{ row.deal_rate ?? 0 }}% - - - - - - - {{ row.registered_count }} - - - - - - {{ row.completed_count }} - - - - - - {{ row.missed_count }} - - - - - - {{ row.cancelled_count }} - - - - - - - {{ row.completion_rate }}% - - - - - - - - - - + + + + + 医生统计数据 + + + + + + + + + + + + + + 部门状态 + + + + + {{ dept.dept_name }} + ({{ getDeptTotalCount(dept) }}) + + + + {{ getStatusName(status) }}: {{ count }} + + + + + + 暂无部门状态明细 + + + + + 渠道状态 + + + + + {{ channel.channel_name }} + ({{ getChannelTotalCount(channel) }}) + + + + {{ getStatusName(status) }}: {{ count }} + + + + + + 暂无渠道状态明细 + + + + + + + + + + + @@ -213,6 +192,17 @@ const getStatistics = async () => { const res = await getDoctorStatistics(params) statisticsList.value = res.lists || [] + + // 为每个医生添加必要的属性 + statisticsList.value.forEach(item => { + // 确保数据结构完整 + if (!item.dept_status_details) { + item.dept_status_details = [] + } + if (!item.channel_status_details) { + item.channel_status_details = [] + } + }) } catch (error: any) { console.error('获取统计数据错误:', error) ElMessage.error(error.msg || '获取统计数据失败') @@ -234,23 +224,38 @@ const handleReset = () => { getStatistics() } -const getCompletionRateClass = (rate: number) => { - if (rate >= 80) return 'text-green-600 font-semibold' - if (rate >= 60) return 'text-blue-600 font-semibold' - if (rate >= 40) return 'text-orange-600 font-semibold' - return 'text-red-600 font-semibold' +// 计算部门总数量 +const getDeptTotalCount = (dept: any) => { + if (!dept.statuses) return 0 + return Object.values(dept.statuses).reduce((total: number, count: number) => total + count, 0) } -// 解析部门统计字符串为数组 -const parseDeptStats = (stats: string) => { - if (!stats || stats === '未分配') return [] - return stats.split(' ').filter(item => item.trim()) +// 计算渠道总数量 +const getChannelTotalCount = (channel: any) => { + if (!channel.statuses) return 0 + return Object.values(channel.statuses).reduce((total: number, count: number) => total + count, 0) } -// 解析渠道统计字符串为数组 -const parseChannelStats = (stats: string) => { - if (!stats || stats === '未设置') return [] - return stats.split(' ').filter(item => item.trim()) +// 获取状态名称 +const getStatusName = (status: string | number) => { + const statusMap: Record = { + 1: '已挂号', + 2: '已取消', + 3: '已完成', + 4: '已过号' + } + return statusMap[status] || `状态${status}` +} + +// 获取状态标签类型 +const getStatusType = (status: string | number) => { + const typeMap: Record = { + 1: 'info', + 2: 'danger', + 3: 'success', + 4: 'warning' + } + return typeMap[status] || 'default' } onMounted(() => { @@ -262,23 +267,155 @@ onMounted(() => { diff --git a/server/app/adminapi/lists/doctor/StatisticsLists.php b/server/app/adminapi/lists/doctor/StatisticsLists.php index 1f94be81..53187867 100644 --- a/server/app/adminapi/lists/doctor/StatisticsLists.php +++ b/server/app/adminapi/lists/doctor/StatisticsLists.php @@ -109,32 +109,75 @@ class StatisticsLists extends BaseAdminDataLists } $doctors = $doctorQuery->select()->toArray(); - // 查询2:获取部门统计信息(通过assistant_id直接关联,按部门分组统计数量) + // 查询2:获取部门统计信息(通过assistant_id直接关联,按部门和状态分组统计数量) $doctorIdsWithAppointments = array_keys($statsMap); $deptStatsMap = []; + $deptStatusMap = []; $channelStatsMap = []; + $channelStatusMap = []; if (!empty($doctorIdsWithAppointments)) { - // 通过 assistant_id → admin_dept → dept 获取部门统计 + // 通过 assistant_id → admin_dept → dept 获取部门统计(按状态分组) $deptStats = \think\facade\Db::name('doctor_appointment') ->alias('apt') ->leftJoin('admin_dept ad', 'apt.assistant_id = ad.admin_id') ->leftJoin('dept dept', 'ad.dept_id = dept.id') - ->field('apt.doctor_id, dept.name as dept_name, COUNT(*) as dept_count') + ->field([ + 'apt.doctor_id', + 'dept.id as dept_id', + 'dept.name as dept_name', + 'apt.status', + 'COUNT(*) as count' + ]) ->whereIn('apt.doctor_id', $doctorIdsWithAppointments) ->where('apt.appointment_date', '>=', $startDate) ->where('apt.appointment_date', '<=', $endDate) ->whereNotNull('dept.id') - ->group('apt.doctor_id, dept.id') + ->group('apt.doctor_id, dept.id, apt.status') ->select() ->toArray(); // 组装部门统计数据:医生ID => "部门1(数量1) 部门2(数量2)" + // 同时组装部门状态数据:医生ID => 部门ID => 状态 => 数量 foreach ($deptStats as $stat) { - if (!isset($deptStatsMap[$stat['doctor_id']])) { - $deptStatsMap[$stat['doctor_id']] = []; + $doctorId = $stat['doctor_id']; + $deptId = $stat['dept_id']; + $deptName = $stat['dept_name']; + $status = $stat['status']; + $count = $stat['count']; + + // 组装部门统计数据 + if (!isset($deptStatsMap[$doctorId])) { + $deptStatsMap[$doctorId] = []; } - $deptStatsMap[$stat['doctor_id']][] = $stat['dept_name'] . '(' . $stat['dept_count'] . ')'; + if (!isset($deptStatsMap[$doctorId][$deptId])) { + $deptStatsMap[$doctorId][$deptId] = [ + 'dept_name' => $deptName, + 'total_count' => 0 + ]; + } + $deptStatsMap[$doctorId][$deptId]['total_count'] += $count; + + // 组装部门状态数据 + if (!isset($deptStatusMap[$doctorId])) { + $deptStatusMap[$doctorId] = []; + } + if (!isset($deptStatusMap[$doctorId][$deptId])) { + $deptStatusMap[$doctorId][$deptId] = [ + 'dept_name' => $deptName, + 'statuses' => [] + ]; + } + $deptStatusMap[$doctorId][$deptId]['statuses'][$status] = $count; + } + + // 格式化部门统计数据 + foreach ($deptStatsMap as $doctorId => &$depts) { + $formattedDepts = []; + foreach ($depts as $dept) { + $formattedDepts[] = $dept['dept_name'] . '(' . $dept['total_count'] . ')'; + } + $deptStatsMap[$doctorId] = $formattedDepts; } // 获取渠道字典(dict_data.value => name),与挂号创建时 channel_source 存字典 value 一致 @@ -144,24 +187,28 @@ class StatisticsLists extends BaseAdminDataLists // 渠道:业务保存的是 channel_source(字典 value 字符串,见 AppointmentLogic::create);旧库可能仅有 channels(tinyint) $channelBuckets = []; + $channelStatusBuckets = []; try { $channelStats = \think\facade\Db::name('doctor_appointment') - ->field('doctor_id, channel_source, COUNT(*) as channel_count') + ->field('doctor_id, channel_source, status, COUNT(*) as channel_count') ->whereIn('doctor_id', $doctorIdsWithAppointments) ->where('appointment_date', '>=', $startDate) ->where('appointment_date', '<=', $endDate) ->where('channel_source', '<>', '') - ->group('doctor_id, channel_source') + ->group('doctor_id, channel_source, status') ->select() ->toArray(); foreach ($channelStats as $stat) { $did = (int) $stat['doctor_id']; $src = trim((string) ($stat['channel_source'] ?? '')); + $status = $stat['status']; $cnt = (int) ($stat['channel_count'] ?? 0); if ($src === '' || $cnt < 1) { continue; } + + // 组装渠道统计数据 if (!isset($channelBuckets[$did])) { $channelBuckets[$did] = []; } @@ -169,19 +216,31 @@ class StatisticsLists extends BaseAdminDataLists $channelBuckets[$did][$src] = 0; } $channelBuckets[$did][$src] += $cnt; + + // 组装渠道状态数据 + if (!isset($channelStatusBuckets[$did])) { + $channelStatusBuckets[$did] = []; + } + if (!isset($channelStatusBuckets[$did][$src])) { + $channelStatusBuckets[$did][$src] = []; + } + $channelStatusBuckets[$did][$src][$status] = $cnt; } } catch (\Throwable) { // 无 channel_source 字段等 } - $mergeLegacyChannels = static function (array &$buckets, array $rows): void { + $mergeLegacyChannels = static function (array &$buckets, array &$statusBuckets, array $rows): void { foreach ($rows as $stat) { $did = (int) $stat['doctor_id']; $key = (string) (int) ($stat['channels'] ?? 0); + $status = $stat['status']; $cnt = (int) ($stat['channel_count'] ?? 0); if ($key === '0' || $cnt < 1) { continue; } + + // 组装渠道统计数据 if (!isset($buckets[$did])) { $buckets[$did] = []; } @@ -189,12 +248,21 @@ class StatisticsLists extends BaseAdminDataLists $buckets[$did][$key] = 0; } $buckets[$did][$key] += $cnt; + + // 组装渠道状态数据 + if (!isset($statusBuckets[$did])) { + $statusBuckets[$did] = []; + } + if (!isset($statusBuckets[$did][$key])) { + $statusBuckets[$did][$key] = []; + } + $statusBuckets[$did][$key][$status] = $cnt; } }; try { $legacyStats = \think\facade\Db::name('doctor_appointment') - ->field('doctor_id, channels, COUNT(*) as channel_count') + ->field('doctor_id, channels, status, COUNT(*) as channel_count') ->whereIn('doctor_id', $doctorIdsWithAppointments) ->where('appointment_date', '>=', $startDate) ->where('appointment_date', '<=', $endDate) @@ -202,22 +270,22 @@ class StatisticsLists extends BaseAdminDataLists ->where(function ($q) { $q->whereNull('channel_source')->whereOr('channel_source', '=', ''); }) - ->group('doctor_id, channels') + ->group('doctor_id, channels, status') ->select() ->toArray(); - $mergeLegacyChannels($channelBuckets, $legacyStats); + $mergeLegacyChannels($channelBuckets, $channelStatusBuckets, $legacyStats); } catch (\Throwable) { try { $legacyStats = \think\facade\Db::name('doctor_appointment') - ->field('doctor_id, channels, COUNT(*) as channel_count') + ->field('doctor_id, channels, status, COUNT(*) as channel_count') ->whereIn('doctor_id', $doctorIdsWithAppointments) ->where('appointment_date', '>=', $startDate) ->where('appointment_date', '<=', $endDate) ->where('channels', '>', 0) - ->group('doctor_id, channels') + ->group('doctor_id, channels, status') ->select() ->toArray(); - $mergeLegacyChannels($channelBuckets, $legacyStats); + $mergeLegacyChannels($channelBuckets, $channelStatusBuckets, $legacyStats); } catch (\Throwable) { // 无 channels 字段 } @@ -231,6 +299,20 @@ class StatisticsLists extends BaseAdminDataLists } $channelStatsMap[$did] = $parts; } + + // 组装渠道状态明细数据 + foreach ($channelStatusBuckets as $did => $byKey) { + foreach ($byKey as $key => $statuses) { + $label = $channelDict[$key] ?? $channelDict[(string) $key] ?? $key; + if (!isset($channelStatusMap[$did])) { + $channelStatusMap[$did] = []; + } + $channelStatusMap[$did][] = [ + 'channel_name' => $label, + 'statuses' => $statuses + ]; + } + } } // 组装结果 @@ -256,20 +338,38 @@ class StatisticsLists extends BaseAdminDataLists ? round(($stat['completed_count'] / $stat['total_count']) * 100, 2) : 0; + // 格式化部门状态明细 + $deptStatusDetails = []; + if (isset($deptStatusMap[$doctor['id']])) { + foreach ($deptStatusMap[$doctor['id']] as $deptId => $deptInfo) { + $deptStatusDetails[] = [ + 'dept_id' => $deptId, + 'dept_name' => $deptInfo['dept_name'], + 'statuses' => $deptInfo['statuses'] + ]; + } + } + + // 格式化渠道状态明细 + $channelStatusDetails = []; + if (isset($channelStatusMap[$doctor['id']])) { + $channelStatusDetails = $channelStatusMap[$doctor['id']]; + } + $result[] = [ 'doctor_id' => $doctor['id'], 'doctor_name' => $doctor['name'], - 'total_count' => (int)$stat['total_count'], - 'registered_count' => (int)$stat['registered_count'], - 'completed_count' => (int)$stat['completed_count'], - 'missed_count' => (int)$stat['missed_count'], - 'cancelled_count' => (int)$stat['cancelled_count'], - 'diagnosis_count' => $diagnosisCount, - 'deal_count' => $dealCount, - 'deal_rate' => $dealRate, - 'completion_rate' => $completionRate, - 'dept_stats' => isset($deptStatsMap[$doctor['id']]) ? implode(' ', $deptStatsMap[$doctor['id']]) : '未分配', - 'channel_stats' => isset($channelStatsMap[$doctor['id']]) ? implode(' ', $channelStatsMap[$doctor['id']]) : '未设置', + 'total_count' => (int)$stat['total_count'] ?? 0, + 'registered_count' => (int)$stat['registered_count'] ?? 0, + 'completed_count' => (int)$stat['completed_count'] ?? 0, + 'missed_count' => (int)$stat['missed_count'] ?? 0, + 'cancelled_count' => (int)$stat['cancelled_count'] ?? 0, + 'diagnosis_count' => $diagnosisCount ?? 0, + 'deal_count' => $dealCount ?? 0, + 'deal_rate' => $dealRate ?? 0, + 'completion_rate' => $completionRate ?? 0, + 'dept_status_details' => $deptStatusDetails, + 'channel_status_details' => $channelStatusDetails, 'time_range' => $timeRangeText ]; }