Compare commits

...
3 Commits
Author SHA1 Message Date
Guoxianpeng 44ddff9b26 医生统计页面增加动态切换天 2026-04-11 15:25:31 +08:00
Guoxianpeng 3246735b18 工作台页面优化 2026-04-10 17:21:43 +08:00
Guoxianpeng f9ab5e18fb 医生统计页面优化 2026-04-09 18:35:25 +08:00
6 changed files with 1875 additions and 1358 deletions
+15
View File
@@ -0,0 +1,15 @@
import request from '@/utils/request'
/**
* 工作台相关API
*/
export const workbenchApi = {
/**
* 获取工作台数据
*/
getWorkbenchData: () => {
return request.get({
url: '/workbench/index'
})
}
}
+365 -141
View File
@@ -1,7 +1,12 @@
<template> <template>
<div class="statistics-page"> <div class="statistics-page">
<el-card shadow="never" class="!border-none"> <!-- 筛选区域卡片 -->
<!-- 筛选区域 --> <el-card class="mb-4" shadow="hover">
<template #header>
<div class="card-header">
<span class="card-title">筛选条件</span>
</div>
</template>
<el-form :inline="true" :model="queryParams" class="filter-form"> <el-form :inline="true" :model="queryParams" class="filter-form">
<el-form-item label="医生"> <el-form-item label="医生">
<el-select <el-select
@@ -20,9 +25,33 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item>
<el-button-group>
<el-button
type="primary"
:disabled="isPrevDayDisabled"
@click="handlePrevDay"
>
前一天
</el-button>
<el-button
type="primary"
@click="handleToday"
>
今天
</el-button>
<el-button
type="primary"
:disabled="isNextDayDisabled"
@click="handleNextDay"
>
后一天
</el-button>
</el-button-group>
</el-form-item>
<el-form-item label="时间范围"> <el-form-item label="时间范围">
<el-radio-group v-model="queryParams.time_type" @change="handleTimeTypeChange"> <el-radio-group v-model="queryParams.time_type" @change="handleTimeTypeChange">
<el-radio-button label="today">今天</el-radio-button>
<el-radio-button label="week">最近7天</el-radio-button> <el-radio-button label="week">最近7天</el-radio-button>
<el-radio-button label="month">最近30天</el-radio-button> <el-radio-button label="month">最近30天</el-radio-button>
<el-radio-button label="custom">自定义</el-radio-button> <el-radio-button label="custom">自定义</el-radio-button>
@@ -48,123 +77,97 @@
</el-form> </el-form>
</el-card> </el-card>
<!-- 统计表格 --> <!-- 统计数据卡片 -->
<el-card shadow="never" class="!border-none mt-4"> <el-card v-loading="loading" class="mb-4" shadow="hover">
<el-table <template #header>
v-loading="loading" <div class="card-header">
:data="statisticsList" <span class="card-title">医生统计数据</span>
border </div>
stripe </template>
style="width: 100%"
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '600' }" <div v-if="statisticsList.length > 0">
> <el-table
<el-table-column prop="doctor_name" label="医生姓名" width="120" fixed /> :data="statisticsList"
style="width: 100%"
<el-table-column label="部门统计" min-width="250" fixed> :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '600' }"
<template #default="{ row }"> >
<div class="dept-stats"> <el-table-column prop="doctor_name" label="医生姓名" width="120" />
<el-tag
v-for="(item, index) in parseDeptStats(row.dept_stats)" <!-- 部门和渠道状态明细 -->
:key="index" <el-table-column label="状态明细" min-width="700">
type="primary" <template #default="{ row }">
size="small" <div class="status-overview">
class="stat-tag" <!-- 部门状态 -->
> <div class="status-section">
{{ item }} <h4 class="section-title">部门状态</h4>
</el-tag> <div v-if="row.dept_status_details && row.dept_status_details.length > 0">
<span v-if="!row.dept_stats || row.dept_stats === '未分配'" class="text-gray-400">未分配</span> <div class="dept-grid">
</div> <div
</template> v-for="(dept, index) in row.dept_status_details"
</el-table-column> :key="index"
class="dept-item"
<el-table-column label="渠道统计" min-width="250"> >
<template #default="{ row }"> <div class="dept-header">
<div class="channel-stats"> <span class="dept-name">{{ dept.dept_name }}</span>
<el-tag <span class="dept-total">({{ getDeptTotalCount(dept) }})</span>
v-for="(item, index) in parseChannelStats(row.channel_stats)" </div>
:key="index" <div class="status-tags">
type="success" <el-tag
size="small" v-for="(count, status) in dept.statuses"
class="stat-tag" :key="status"
> :type="getStatusType(status)"
{{ item }} size="small"
</el-tag> class="compact-tag"
<span v-if="!row.channel_stats || row.channel_stats === '未设置'" class="text-gray-400">未设置</span> >
</div> {{ getStatusName(status) }}: {{ count }}
</template> </el-tag>
</el-table-column> </div>
</div>
<el-table-column prop="total_count" label="总诊单" width="90" align="center"> </div>
<template #default="{ row }"> </div>
<span class="font-semibold text-blue-600">{{ row.total_count }}</span> <span v-else class="text-gray-400">暂无部门状态明细</span>
</template> </div>
</el-table-column>
<!-- 渠道状态 -->
<el-table-column prop="diagnosis_count" label="诊单数" width="90" align="center"> <div class="status-section">
<template #default="{ row }"> <h4 class="section-title">渠道状态</h4>
<el-tooltip <div v-if="row.channel_status_details && row.channel_status_details.length > 0">
content="统计期内该医生挂号涉及的去重诊单数,用于计算成交率" <div class="channel-grid">
placement="top" <div
> v-for="(channel, index) in row.channel_status_details"
<span class="text-gray-700">{{ row.diagnosis_count ?? 0 }}</span> :key="index"
</el-tooltip> class="channel-item"
</template> >
</el-table-column> <div class="channel-header">
<span class="channel-name">{{ channel.channel_name }}</span>
<el-table-column prop="deal_count" label="成交单" width="90" align="center"> <span class="channel-total">({{ getChannelTotalCount(channel) }})</span>
<template #default="{ row }"> </div>
<el-tooltip content="上述诊单中已开有效处方(未删除、未作废)的数量" placement="top"> <div class="status-tags">
<el-tag type="primary" size="small">{{ row.deal_count ?? 0 }}</el-tag> <el-tag
</el-tooltip> v-for="(count, status) in channel.statuses"
</template> :key="status"
</el-table-column> :type="getStatusType(status)"
size="small"
<el-table-column prop="deal_rate" label="成交率" width="100" align="center"> class="compact-tag"
<template #default="{ row }"> >
<span :class="getCompletionRateClass(Number(row.deal_rate ?? 0))"> {{ getStatusName(status) }}: {{ count }}
{{ row.deal_rate ?? 0 }}% </el-tag>
</span> </div>
</template> </div>
</el-table-column> </div>
</div>
<el-table-column prop="registered_count" label="已挂号" width="90" align="center"> <span v-else class="text-gray-400">暂无渠道状态明细</span>
<template #default="{ row }"> </div>
<el-tag type="info" size="small">{{ row.registered_count }}</el-tag> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="completed_count" label="已完成" width="90" align="center"> <el-table-column prop="time_range" label="统计时间" min-width="200" />
<template #default="{ row }"> </el-table>
<el-tag type="success" size="small">{{ row.completed_count }}</el-tag> </div>
</template> <div v-else class="empty-data">
</el-table-column> <el-empty description="暂无统计数据" />
</div>
<el-table-column prop="missed_count" label="已过号" width="90" align="center">
<template #default="{ row }">
<el-tag type="warning" size="small">{{ row.missed_count }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="cancelled_count" label="已取消" width="90" align="center">
<template #default="{ row }">
<el-tag type="danger" size="small">{{ row.cancelled_count }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="completion_rate" label="完成率" width="100" align="center">
<template #default="{ row }">
<span :class="getCompletionRateClass(row.completion_rate)">
{{ row.completion_rate }}%
</span>
</template>
</el-table-column>
<el-table-column prop="time_range" label="统计时间" min-width="200" />
<template #empty>
<el-empty description="暂无统计数据" />
</template>
</el-table>
</el-card> </el-card>
</div> </div>
</template> </template>
@@ -178,10 +181,13 @@ const loading = ref(false)
const doctorList = ref<any[]>([]) const doctorList = ref<any[]>([])
const statisticsList = ref<any[]>([]) const statisticsList = ref<any[]>([])
const dateRange = ref<string[]>([]) const dateRange = ref<string[]>([])
const currentDate = ref<string>(new Date().toISOString().split('T')[0])
const isPrevDayDisabled = ref<boolean>(false)
const isNextDayDisabled = ref<boolean>(false)
const queryParams = reactive({ const queryParams = reactive({
doctor_id: '', doctor_id: '',
time_type: 'today', time_type: 'week',
start_date: '', start_date: '',
end_date: '' end_date: ''
}) })
@@ -199,20 +205,35 @@ const getStatistics = async () => {
loading.value = true loading.value = true
try { try {
const params: any = { const params: any = {
time_type: queryParams.time_type time_type: 'custom' // 始终使用custom类型,只传时间区间
} }
if (queryParams.doctor_id) { if (queryParams.doctor_id) {
params.doctor_id = queryParams.doctor_id params.doctor_id = queryParams.doctor_id
} }
if (queryParams.time_type === 'custom' && dateRange.value && dateRange.value.length === 2) { if (dateRange.value && dateRange.value.length === 2) {
params.start_date = dateRange.value[0] params.start_date = dateRange.value[0]
params.end_date = dateRange.value[1] params.end_date = dateRange.value[1]
} else {
// 否则使用currentDate作为查询参数
params.start_date = currentDate.value
params.end_date = currentDate.value
} }
const res = await getDoctorStatistics(params) const res = await getDoctorStatistics(params)
statisticsList.value = res.lists || [] 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) { } catch (error: any) {
console.error('获取统计数据错误:', error) console.error('获取统计数据错误:', error)
ElMessage.error(error.msg || '获取统计数据失败') ElMessage.error(error.msg || '获取统计数据失败')
@@ -225,36 +246,107 @@ const handleTimeTypeChange = () => {
if (queryParams.time_type !== 'custom') { if (queryParams.time_type !== 'custom') {
dateRange.value = [] dateRange.value = []
} }
// 根据时间类型更新currentDate
switch (queryParams.time_type) {
case 'week':
currentDate.value = new Date().toISOString().split('T')[0]
break
case 'month':
currentDate.value = new Date().toISOString().split('T')[0]
break
case 'custom':
// 自定义类型不更新currentDate
break
}
updateDateButtons()
}
// 更新日期按钮状态
const updateDateButtons = () => {
const today = new Date().toISOString().split('T')[0]
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
const thirtyDaysAgoStr = thirtyDaysAgo.toISOString().split('T')[0]
// 后一天超过今天则禁用
isNextDayDisabled.value = currentDate.value >= today
// 前一天超过30天则禁用
isPrevDayDisabled.value = currentDate.value <= thirtyDaysAgoStr
}
// 处理前一天
const handlePrevDay = () => {
const date = new Date(currentDate.value)
date.setDate(date.getDate() - 1)
currentDate.value = date.toISOString().split('T')[0]
updateDateButtons()
getStatistics()
}
// 处理后一天
const handleNextDay = () => {
const date = new Date(currentDate.value)
date.setDate(date.getDate() + 1)
currentDate.value = date.toISOString().split('T')[0]
updateDateButtons()
getStatistics()
}
// 处理今天
const handleToday = () => {
currentDate.value = new Date().toISOString().split('T')[0]
updateDateButtons()
getStatistics()
} }
const handleReset = () => { const handleReset = () => {
queryParams.doctor_id = '' queryParams.doctor_id = ''
queryParams.time_type = 'today' queryParams.time_type = 'today'
dateRange.value = [] dateRange.value = []
currentDate.value = new Date().toISOString().split('T')[0]
updateDateButtons()
getStatistics() getStatistics()
} }
const getCompletionRateClass = (rate: number) => { // 计算部门总数量
if (rate >= 80) return 'text-green-600 font-semibold' const getDeptTotalCount = (dept: any) => {
if (rate >= 60) return 'text-blue-600 font-semibold' if (!dept.statuses) return 0
if (rate >= 40) return 'text-orange-600 font-semibold' return Object.values(dept.statuses).reduce((total: number, count: number) => total + count, 0)
return 'text-red-600 font-semibold'
} }
// 解析部门统计字符串为数组 // 计算渠道总数量
const parseDeptStats = (stats: string) => { const getChannelTotalCount = (channel: any) => {
if (!stats || stats === '未分配') return [] if (!channel.statuses) return 0
return stats.split(' ').filter(item => item.trim()) return Object.values(channel.statuses).reduce((total: number, count: number) => total + count, 0)
} }
// 解析渠道统计字符串为数组 // 获取状态名称
const parseChannelStats = (stats: string) => { const getStatusName = (status: string | number) => {
if (!stats || stats === '未设置') return [] const statusMap: Record<string | number, string> = {
return stats.split(' ').filter(item => item.trim()) 1: '已挂号',
2: '已取消',
3: '已完成',
4: '已过号'
}
return statusMap[status] || `状态${status}`
}
// 获取状态标签类型
const getStatusType = (status: string | number) => {
const typeMap: Record<string | number, string> = {
1: 'info',
2: 'danger',
3: 'success',
4: 'warning'
}
return typeMap[status] || 'default'
} }
onMounted(() => { onMounted(() => {
getDoctorList() getDoctorList()
updateDateButtons()
getStatistics() getStatistics()
}) })
</script> </script>
@@ -262,23 +354,155 @@ onMounted(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.statistics-page { .statistics-page {
padding: 20px; padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
} }
.filter-form { .filter-form {
padding: 10px 0;
:deep(.el-form-item) { :deep(.el-form-item) {
margin-bottom: 0; margin-bottom: 0;
} }
} }
.dept-stats, .status-overview {
.channel-stats { padding: 10px 0;
}
.status-section {
margin-bottom: 15px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px solid #ebeef5;
}
.dept-grid, .channel-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
margin-top: 8px;
}
.dept-item, .channel-item {
background-color: #f9f9f9;
border-radius: 4px;
padding: 8px;
border: 1px solid #ebeef5;
}
.dept-header, .channel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
font-size: 13px;
font-weight: 500;
}
.dept-name, .channel-name {
color: #303133;
}
.dept-total, .channel-total {
color: #606266;
font-size: 12px;
}
.status-tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 6px; gap: 6px;
align-items: center;
} }
.stat-tag { .compact-tag {
font-weight: 500; font-size: 11px !important;
padding: 2px 8px !important;
margin: 0 !important;
}
.empty-data {
padding: 40px 0;
text-align: center;
}
/* 表格样式优化 */
:deep(.el-table) {
border-radius: 4px;
overflow: hidden;
}
:deep(.el-table th) {
background-color: #f5f7fa !important;
font-weight: 600 !important;
color: #606266 !important;
}
:deep(.el-table tr:hover) {
background-color: #f5f7fa !important;
}
/* 卡片样式优化 */
:deep(.el-card) {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !important;
margin-bottom: 16px !important;
transition: box-shadow 0.3s ease;
}
:deep(.el-card:hover) {
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15) !important;
}
/* 折叠面板样式优化 */
:deep(.el-collapse-item__header) {
font-weight: 600;
color: #303133;
padding: 10px 20px;
text-align: left !important;
}
:deep(.el-collapse-item__content) {
padding: 10px 20px 15px !important;
background-color: #f9f9f9;
border-top: 1px solid #ebeef5;
text-align: left !important;
}
/* 表格单元格对齐 */
:deep(.el-table__cell) {
text-align: left !important;
vertical-align: top !important;
}
/* 确保折叠面板从顶部开始显示 */
:deep(.el-table__row) {
vertical-align: top !important;
}
/* 去除默认的垂直居中样式 */
:deep(.el-table__row > td) {
vertical-align: top !important;
}
.text-gray-400 {
color: #909399;
} }
</style> </style>
File diff suppressed because it is too large Load Diff
@@ -109,32 +109,75 @@ class StatisticsLists extends BaseAdminDataLists
} }
$doctors = $doctorQuery->select()->toArray(); $doctors = $doctorQuery->select()->toArray();
// 查询2:获取部门统计信息(通过assistant_id直接关联,按部门分组统计数量) // 查询2:获取部门统计信息(通过assistant_id直接关联,按部门和状态分组统计数量)
$doctorIdsWithAppointments = array_keys($statsMap); $doctorIdsWithAppointments = array_keys($statsMap);
$deptStatsMap = []; $deptStatsMap = [];
$deptStatusMap = [];
$channelStatsMap = []; $channelStatsMap = [];
$channelStatusMap = [];
if (!empty($doctorIdsWithAppointments)) { if (!empty($doctorIdsWithAppointments)) {
// 通过 assistant_id → admin_dept → dept 获取部门统计 // 通过 assistant_id → admin_dept → dept 获取部门统计(按状态分组)
$deptStats = \think\facade\Db::name('doctor_appointment') $deptStats = \think\facade\Db::name('doctor_appointment')
->alias('apt') ->alias('apt')
->leftJoin('admin_dept ad', 'apt.assistant_id = ad.admin_id') ->leftJoin('admin_dept ad', 'apt.assistant_id = ad.admin_id')
->leftJoin('dept dept', 'ad.dept_id = dept.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) ->whereIn('apt.doctor_id', $doctorIdsWithAppointments)
->where('apt.appointment_date', '>=', $startDate) ->where('apt.appointment_date', '>=', $startDate)
->where('apt.appointment_date', '<=', $endDate) ->where('apt.appointment_date', '<=', $endDate)
->whereNotNull('dept.id') ->whereNotNull('dept.id')
->group('apt.doctor_id, dept.id') ->group('apt.doctor_id, dept.id, apt.status')
->select() ->select()
->toArray(); ->toArray();
// 组装部门统计数据:医生ID => "部门1(数量1) 部门2(数量2)" // 组装部门统计数据:医生ID => "部门1(数量1) 部门2(数量2)"
// 同时组装部门状态数据:医生ID => 部门ID => 状态 => 数量
foreach ($deptStats as $stat) { foreach ($deptStats as $stat) {
if (!isset($deptStatsMap[$stat['doctor_id']])) { $doctorId = $stat['doctor_id'];
$deptStatsMap[$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 一致 // 获取渠道字典(dict_data.value => name),与挂号创建时 channel_source 存字典 value 一致
@@ -144,24 +187,28 @@ class StatisticsLists extends BaseAdminDataLists
// 渠道:业务保存的是 channel_source(字典 value 字符串,见 AppointmentLogic::create);旧库可能仅有 channels(tinyint) // 渠道:业务保存的是 channel_source(字典 value 字符串,见 AppointmentLogic::create);旧库可能仅有 channels(tinyint)
$channelBuckets = []; $channelBuckets = [];
$channelStatusBuckets = [];
try { try {
$channelStats = \think\facade\Db::name('doctor_appointment') $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) ->whereIn('doctor_id', $doctorIdsWithAppointments)
->where('appointment_date', '>=', $startDate) ->where('appointment_date', '>=', $startDate)
->where('appointment_date', '<=', $endDate) ->where('appointment_date', '<=', $endDate)
->where('channel_source', '<>', '') ->where('channel_source', '<>', '')
->group('doctor_id, channel_source') ->group('doctor_id, channel_source, status')
->select() ->select()
->toArray(); ->toArray();
foreach ($channelStats as $stat) { foreach ($channelStats as $stat) {
$did = (int) $stat['doctor_id']; $did = (int) $stat['doctor_id'];
$src = trim((string) ($stat['channel_source'] ?? '')); $src = trim((string) ($stat['channel_source'] ?? ''));
$status = $stat['status'];
$cnt = (int) ($stat['channel_count'] ?? 0); $cnt = (int) ($stat['channel_count'] ?? 0);
if ($src === '' || $cnt < 1) { if ($src === '' || $cnt < 1) {
continue; continue;
} }
// 组装渠道统计数据
if (!isset($channelBuckets[$did])) { if (!isset($channelBuckets[$did])) {
$channelBuckets[$did] = []; $channelBuckets[$did] = [];
} }
@@ -169,19 +216,31 @@ class StatisticsLists extends BaseAdminDataLists
$channelBuckets[$did][$src] = 0; $channelBuckets[$did][$src] = 0;
} }
$channelBuckets[$did][$src] += $cnt; $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) { } catch (\Throwable) {
// 无 channel_source 字段等 // 无 channel_source 字段等
} }
$mergeLegacyChannels = static function (array &$buckets, array $rows): void { $mergeLegacyChannels = static function (array &$buckets, array &$statusBuckets, array $rows): void {
foreach ($rows as $stat) { foreach ($rows as $stat) {
$did = (int) $stat['doctor_id']; $did = (int) $stat['doctor_id'];
$key = (string) (int) ($stat['channels'] ?? 0); $key = (string) (int) ($stat['channels'] ?? 0);
$status = $stat['status'];
$cnt = (int) ($stat['channel_count'] ?? 0); $cnt = (int) ($stat['channel_count'] ?? 0);
if ($key === '0' || $cnt < 1) { if ($key === '0' || $cnt < 1) {
continue; continue;
} }
// 组装渠道统计数据
if (!isset($buckets[$did])) { if (!isset($buckets[$did])) {
$buckets[$did] = []; $buckets[$did] = [];
} }
@@ -189,12 +248,21 @@ class StatisticsLists extends BaseAdminDataLists
$buckets[$did][$key] = 0; $buckets[$did][$key] = 0;
} }
$buckets[$did][$key] += $cnt; $buckets[$did][$key] += $cnt;
// 组装渠道状态数据
if (!isset($statusBuckets[$did])) {
$statusBuckets[$did] = [];
}
if (!isset($statusBuckets[$did][$key])) {
$statusBuckets[$did][$key] = [];
}
$statusBuckets[$did][$key][$status] = $cnt;
} }
}; };
try { try {
$legacyStats = \think\facade\Db::name('doctor_appointment') $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) ->whereIn('doctor_id', $doctorIdsWithAppointments)
->where('appointment_date', '>=', $startDate) ->where('appointment_date', '>=', $startDate)
->where('appointment_date', '<=', $endDate) ->where('appointment_date', '<=', $endDate)
@@ -202,22 +270,22 @@ class StatisticsLists extends BaseAdminDataLists
->where(function ($q) { ->where(function ($q) {
$q->whereNull('channel_source')->whereOr('channel_source', '=', ''); $q->whereNull('channel_source')->whereOr('channel_source', '=', '');
}) })
->group('doctor_id, channels') ->group('doctor_id, channels, status')
->select() ->select()
->toArray(); ->toArray();
$mergeLegacyChannels($channelBuckets, $legacyStats); $mergeLegacyChannels($channelBuckets, $channelStatusBuckets, $legacyStats);
} catch (\Throwable) { } catch (\Throwable) {
try { try {
$legacyStats = \think\facade\Db::name('doctor_appointment') $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) ->whereIn('doctor_id', $doctorIdsWithAppointments)
->where('appointment_date', '>=', $startDate) ->where('appointment_date', '>=', $startDate)
->where('appointment_date', '<=', $endDate) ->where('appointment_date', '<=', $endDate)
->where('channels', '>', 0) ->where('channels', '>', 0)
->group('doctor_id, channels') ->group('doctor_id, channels, status')
->select() ->select()
->toArray(); ->toArray();
$mergeLegacyChannels($channelBuckets, $legacyStats); $mergeLegacyChannels($channelBuckets, $channelStatusBuckets, $legacyStats);
} catch (\Throwable) { } catch (\Throwable) {
// 无 channels 字段 // 无 channels 字段
} }
@@ -231,6 +299,20 @@ class StatisticsLists extends BaseAdminDataLists
} }
$channelStatsMap[$did] = $parts; $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) ? round(($stat['completed_count'] / $stat['total_count']) * 100, 2)
: 0; : 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[] = [ $result[] = [
'doctor_id' => $doctor['id'], 'doctor_id' => $doctor['id'],
'doctor_name' => $doctor['name'], 'doctor_name' => $doctor['name'],
'total_count' => (int)$stat['total_count'], 'total_count' => (int)$stat['total_count'] ?? 0,
'registered_count' => (int)$stat['registered_count'], 'registered_count' => (int)$stat['registered_count'] ?? 0,
'completed_count' => (int)$stat['completed_count'], 'completed_count' => (int)$stat['completed_count'] ?? 0,
'missed_count' => (int)$stat['missed_count'], 'missed_count' => (int)$stat['missed_count'] ?? 0,
'cancelled_count' => (int)$stat['cancelled_count'], 'cancelled_count' => (int)$stat['cancelled_count'] ?? 0,
'diagnosis_count' => $diagnosisCount, 'diagnosis_count' => $diagnosisCount ?? 0,
'deal_count' => $dealCount, 'deal_count' => $dealCount ?? 0,
'deal_rate' => $dealRate, 'deal_rate' => $dealRate ?? 0,
'completion_rate' => $completionRate, 'completion_rate' => $completionRate ?? 0,
'dept_stats' => isset($deptStatsMap[$doctor['id']]) ? implode(' ', $deptStatsMap[$doctor['id']]) : '未分配', 'dept_status_details' => $deptStatusDetails,
'channel_stats' => isset($channelStatsMap[$doctor['id']]) ? implode(' ', $channelStatsMap[$doctor['id']]) : '未设置', 'channel_status_details' => $channelStatusDetails,
'time_range' => $timeRangeText 'time_range' => $timeRangeText
]; ];
} }
+1
View File
@@ -173,3 +173,4 @@ class Order extends BaseModel
} }
} }
} }
}
@@ -96,8 +96,8 @@ class Appointment extends BaseModel
return $this->hasOne('app\adminapi\logic\auth\AdminLogic', 'id', 'doctor_id') return $this->hasOne('app\adminapi\logic\auth\AdminLogic', 'id', 'doctor_id')
->bind(['doctor_name' => 'name']); ->bind(['doctor_name' => 'name']);
} }
public function diagnosis(){ public function diagnosis()
{
return $this->belongsTo('app\common\model\tcm\Diagnosis', 'patient_id', 'id'); return $this->belongsTo('app\common\model\tcm\Diagnosis', 'patient_id', 'id');
} }
} }