Files
zyt/admin/src/views/doctor/tongji.vue
T

509 lines
17 KiB
Vue

<template>
<div class="statistics-page">
<!-- 筛选区域卡片 -->
<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-item label="医生">
<el-select
v-model="queryParams.doctor_id"
placeholder="全部医生"
clearable
filterable
style="width: 200px"
>
<el-option
v-for="doctor in doctorList"
:key="doctor.id"
:label="doctor.name"
:value="doctor.id"
/>
</el-select>
</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-radio-group v-model="queryParams.time_type" @change="handleTimeTypeChange">
<el-radio-button label="week">最近7天</el-radio-button>
<el-radio-button label="month">最近30天</el-radio-button>
<el-radio-button label="custom">自定义</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="queryParams.time_type === 'custom'">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getStatistics" :loading="loading">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 统计数据卡片 -->
<el-card v-loading="loading" class="mb-4" shadow="hover">
<template #header>
<div class="card-header">
<span class="card-title">医生统计数据</span>
</div>
</template>
<div v-if="statisticsList.length > 0">
<el-table
:data="statisticsList"
style="width: 100%"
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '600' }"
>
<el-table-column prop="doctor_name" label="医生姓名" width="120" />
<!-- 部门和渠道状态明细 -->
<el-table-column label="状态明细" min-width="700">
<template #default="{ row }">
<div class="status-overview">
<!-- 部门状态 -->
<div class="status-section">
<h4 class="section-title">部门状态</h4>
<div v-if="row.dept_status_details && row.dept_status_details.length > 0">
<div class="dept-grid">
<div
v-for="(dept, index) in row.dept_status_details"
:key="index"
class="dept-item"
>
<div class="dept-header">
<span class="dept-name">{{ dept.dept_name }}</span>
<span class="dept-total">({{ getDeptTotalCount(dept) }})</span>
</div>
<div class="status-tags">
<el-tag
v-for="(count, status) in dept.statuses"
:key="status"
:type="getStatusType(status)"
size="small"
class="compact-tag"
>
{{ getStatusName(status) }}: {{ count }}
</el-tag>
</div>
</div>
</div>
</div>
<span v-else class="text-gray-400">暂无部门状态明细</span>
</div>
<!-- 渠道状态 -->
<div class="status-section">
<h4 class="section-title">渠道状态</h4>
<div v-if="row.channel_status_details && row.channel_status_details.length > 0">
<div class="channel-grid">
<div
v-for="(channel, index) in row.channel_status_details"
:key="index"
class="channel-item"
>
<div class="channel-header">
<span class="channel-name">{{ channel.channel_name }}</span>
<span class="channel-total">({{ getChannelTotalCount(channel) }})</span>
</div>
<div class="status-tags">
<el-tag
v-for="(count, status) in channel.statuses"
:key="status"
:type="getStatusType(status)"
size="small"
class="compact-tag"
>
{{ getStatusName(status) }}: {{ count }}
</el-tag>
</div>
</div>
</div>
</div>
<span v-else class="text-gray-400">暂无渠道状态明细</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="time_range" label="统计时间" min-width="200" />
</el-table>
</div>
<div v-else class="empty-data">
<el-empty description="暂无统计数据" />
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { doctorLists, getDoctorStatistics } from '@/api/doctor'
const loading = ref(false)
const doctorList = ref<any[]>([])
const statisticsList = ref<any[]>([])
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({
doctor_id: '',
time_type: 'week',
start_date: '',
end_date: ''
})
const getDoctorList = async () => {
try {
const res = await doctorLists({ role_id: 1 })
doctorList.value = res.lists || []
} catch (error) {
console.error('获取医生列表失败:', error)
}
}
const getStatistics = async () => {
loading.value = true
try {
const params: any = {
time_type: 'custom' // 始终使用custom类型,只传时间区间
}
if (queryParams.doctor_id) {
params.doctor_id = queryParams.doctor_id
}
if (dateRange.value && dateRange.value.length === 2) {
params.start_date = dateRange.value[0]
params.end_date = dateRange.value[1]
} else {
// 否则使用currentDate作为查询参数
params.start_date = currentDate.value
params.end_date = currentDate.value
}
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 || '获取统计数据失败')
} finally {
loading.value = false
}
}
const handleTimeTypeChange = () => {
if (queryParams.time_type !== 'custom') {
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 = () => {
queryParams.doctor_id = ''
queryParams.time_type = 'today'
dateRange.value = []
currentDate.value = new Date().toISOString().split('T')[0]
updateDateButtons()
getStatistics()
}
// 计算部门总数量
const getDeptTotalCount = (dept: any) => {
if (!dept.statuses) return 0
return Object.values(dept.statuses).reduce((total: number, count: number) => total + count, 0)
}
// 计算渠道总数量
const getChannelTotalCount = (channel: any) => {
if (!channel.statuses) return 0
return Object.values(channel.statuses).reduce((total: number, count: number) => total + count, 0)
}
// 获取状态名称
const getStatusName = (status: string | number) => {
const statusMap: Record<string | number, string> = {
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(() => {
getDoctorList()
updateDateButtons()
getStatistics()
})
</script>
<style scoped lang="scss">
.statistics-page {
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 {
padding: 10px 0;
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.status-overview {
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;
flex-wrap: wrap;
gap: 6px;
}
.compact-tag {
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>