更新
This commit is contained in:
@@ -8,6 +8,7 @@ interface Options {
|
||||
params?: Record<any, any>
|
||||
fixedParams?: Record<any, any>
|
||||
firstLoading?: boolean
|
||||
latestOnly?: boolean
|
||||
}
|
||||
|
||||
export function usePaging(options: Options) {
|
||||
@@ -17,7 +18,8 @@ export function usePaging(options: Options) {
|
||||
fetchFun,
|
||||
params = {},
|
||||
fixedParams = {},
|
||||
firstLoading = false
|
||||
firstLoading = false,
|
||||
latestOnly = false
|
||||
} = options
|
||||
// 记录分页初始参数
|
||||
const paramsInit: Record<any, any> = Object.assign({}, toRaw(params))
|
||||
@@ -30,8 +32,10 @@ export function usePaging(options: Options) {
|
||||
lists: [] as any[],
|
||||
extend: {} as Record<string, any>
|
||||
})
|
||||
let latestRequestId = 0
|
||||
// 请求分页接口;silent: true 时不改 loading(用于定时静默刷新,避免表格闪 loading)
|
||||
const getLists = (opts?: { silent?: boolean }) => {
|
||||
const requestId = ++latestRequestId
|
||||
const silent = opts?.silent === true
|
||||
if (!silent) {
|
||||
pager.loading = true
|
||||
@@ -43,16 +47,26 @@ export function usePaging(options: Options) {
|
||||
...fixedParams
|
||||
})
|
||||
.then((res: any) => {
|
||||
if (latestOnly && requestId !== latestRequestId) {
|
||||
return Promise.resolve(res)
|
||||
}
|
||||
pager.count = res?.count
|
||||
pager.lists = res?.lists
|
||||
pager.extend = res?.extend
|
||||
return Promise.resolve(res)
|
||||
})
|
||||
.catch((err: any) => {
|
||||
if (latestOnly && requestId !== latestRequestId) {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
return Promise.reject(err)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!silent) {
|
||||
if (latestOnly) {
|
||||
if (requestId === latestRequestId) {
|
||||
pager.loading = false
|
||||
}
|
||||
} else if (!silent) {
|
||||
pager.loading = false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
>
|
||||
<div class="slot-time">{{ slot.time }}</div>
|
||||
<div class="slot-status" :class="{ 'status-available': slot.available }">
|
||||
{{ slot.available ? '可约' : '已约' }}
|
||||
{{ slot.available ? '可约' : slot.hasAppointment ? '已约' : '空号' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,6 +115,7 @@ dayjs.extend(isoWeek)
|
||||
interface TimeSlot {
|
||||
time: string
|
||||
available: boolean
|
||||
hasAppointment: boolean
|
||||
quota: number
|
||||
}
|
||||
|
||||
@@ -186,7 +187,7 @@ const filteredTimeSlots = computed(() => {
|
||||
const slotDateTime = dayjs(`${form.date} ${slot.time}`)
|
||||
const isPast = slotDateTime.isBefore(now) || slotDateTime.isSame(now, 'minute')
|
||||
|
||||
// 如果时间已过,标记为不可用
|
||||
// 如果时间已过,标记为不可用;保留原始挂号状态以区分“已约”和“空号”
|
||||
if (isPast) {
|
||||
return {
|
||||
...slot,
|
||||
@@ -315,6 +316,7 @@ const loadTimeSlots = async (silent = false) => {
|
||||
timeSlots.value = (response?.slots || []).map((slot: any) => ({
|
||||
time: slot.time,
|
||||
available: slot.available,
|
||||
hasAppointment: Boolean(slot.has_appointment ?? (slot.available === false)),
|
||||
quota: slot.available ? 1 : 0
|
||||
}))
|
||||
} catch (error) {
|
||||
|
||||
@@ -61,12 +61,23 @@
|
||||
class="channel-select"
|
||||
@change="loadDashboard"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in dashboard.filters.media_channels"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code"
|
||||
/>
|
||||
<el-option-group
|
||||
v-for="group in mediaChannelGroups"
|
||||
:key="group.group_name || '_'"
|
||||
:label="`${group.group_name || '(未分组)'} · ${group.customer_count}`"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in group.channels"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code"
|
||||
>
|
||||
<span class="channel-option">
|
||||
<span>{{ item.name }}</span>
|
||||
<span>{{ item.customer_count }} 人</span>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</div>
|
||||
<span class="range-text">{{ dashboard.meta.start_date }} 至 {{ dashboard.meta.end_date }}</span>
|
||||
@@ -246,6 +257,13 @@ import vCharts from 'vue-echarts'
|
||||
import { firstVisitConversionOverview, type FirstVisitConversionParams } from '@/api/first_visit'
|
||||
|
||||
type MetricType = 'count' | 'money' | 'ratio'
|
||||
type MediaChannelOption = {
|
||||
code: string
|
||||
name: string
|
||||
tag_id?: string
|
||||
group_name?: string
|
||||
customer_count?: number
|
||||
}
|
||||
|
||||
const emptyDashboard = () => ({
|
||||
meta: {
|
||||
@@ -256,7 +274,7 @@ const emptyDashboard = () => ({
|
||||
filters: {
|
||||
departments: [] as any[],
|
||||
assistants: [] as Array<{ id: number; name: string }>,
|
||||
media_channels: [] as Array<{ code: string; name: string }>
|
||||
media_channels: [] as MediaChannelOption[]
|
||||
},
|
||||
summary: {} as Record<string, any>,
|
||||
rankings: { orders: [] as any[], amounts: [] as any[] },
|
||||
@@ -303,6 +321,23 @@ const scopeDescription = computed(() => {
|
||||
if (dashboard.meta.selected_media_channel_name) parts.push(`渠道:${dashboard.meta.selected_media_channel_name}`)
|
||||
return parts.join(' · ')
|
||||
})
|
||||
const mediaChannelGroups = computed(() => {
|
||||
const groups = new Map<string, {
|
||||
group_name: string
|
||||
customer_count: number
|
||||
channels: MediaChannelOption[]
|
||||
}>()
|
||||
for (const channel of dashboard.filters.media_channels) {
|
||||
const groupName = channel.group_name || ''
|
||||
if (!groups.has(groupName)) {
|
||||
groups.set(groupName, { group_name: groupName, customer_count: 0, channels: [] })
|
||||
}
|
||||
const group = groups.get(groupName)!
|
||||
group.channels.push(channel)
|
||||
group.customer_count = Math.max(group.customer_count, Number(channel.customer_count || 0))
|
||||
}
|
||||
return Array.from(groups.values())
|
||||
})
|
||||
const maxOrderValue = computed(() => Math.max(0, ...dashboard.rankings.orders.map(item => Number(item.value || 0))))
|
||||
const maxAmountValue = computed(() => Math.max(0, ...dashboard.rankings.amounts.map(item => Number(item.value || 0))))
|
||||
const targetChartOption = computed(() => ({
|
||||
@@ -445,6 +480,18 @@ onMounted(loadDashboard)
|
||||
.employee-select { width: 190px; }
|
||||
.dept-select { width: 220px; }
|
||||
.channel-select { width: 180px; }
|
||||
.channel-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
|
||||
span:last-child {
|
||||
color: #98a2b3;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<section class="board-section queue-section">
|
||||
<div class="section-heading queue-heading">
|
||||
<div class="heading-copy">
|
||||
<h2>候诊列表</h2>
|
||||
<h2>{{ queueDateLabel }}候诊列表</h2>
|
||||
<span class="heading-badge">按医生排队</span>
|
||||
<span class="queue-count">共 {{ pager.count }} 人</span>
|
||||
</div>
|
||||
@@ -168,7 +168,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="当前权限范围内今日暂无候诊患者" />
|
||||
<el-empty :description="`当前权限范围内${queueDateLabel}暂无候诊患者`" />
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
@@ -196,11 +196,12 @@ const formData = reactive({
|
||||
end_date: today
|
||||
})
|
||||
|
||||
const { pager, getLists } = usePaging({
|
||||
const { pager, getLists, resetPage } = usePaging({
|
||||
fetchFun: myPatientProgressLists as any,
|
||||
params: formData,
|
||||
size: 15,
|
||||
firstLoading: true
|
||||
firstLoading: true,
|
||||
latestOnly: true
|
||||
})
|
||||
|
||||
const todayOverview = computed(() => ({
|
||||
@@ -259,6 +260,10 @@ const selectedScheduleLabel = computed(() => {
|
||||
const day = selectedScheduleDay.value
|
||||
return `${day.date_text || ''} ${day.weekday || ''} `
|
||||
})
|
||||
const queueDateLabel = computed(() => {
|
||||
if (selectedScheduleDate.value === today) return '今日'
|
||||
return selectedScheduleLabel.value.trim() || selectedScheduleDate.value
|
||||
})
|
||||
|
||||
const scopeLabel = computed(() => pager.extend?.scope?.label || '按权限加载')
|
||||
const scheduleRange = computed(() => {
|
||||
@@ -274,7 +279,11 @@ function refreshPanel(options?: { silent?: boolean }) {
|
||||
}
|
||||
|
||||
function selectScheduleDay(date: string) {
|
||||
if (!date) return
|
||||
selectedScheduleDate.value = date
|
||||
formData.start_date = date
|
||||
formData.end_date = date
|
||||
resetPage()
|
||||
}
|
||||
|
||||
function doctorWindows(doctor: Record<string, any>) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user