This commit is contained in:
Your Name
2026-08-10 17:29:05 +08:00
parent 2199887c07
commit 9add23e019
129 changed files with 34157 additions and 59 deletions
+16 -2
View File
@@ -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
}
})
+4 -2
View File
@@ -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