991 lines
29 KiB
Vue
991 lines
29 KiB
Vue
<template>
|
||
<div class="dp-page">
|
||
|
||
<section class="dp-glass dp-toolbar">
|
||
<div class="dp-toolbar-row">
|
||
<span class="dp-field-label">
|
||
<el-icon><Calendar /></el-icon>
|
||
预约日期
|
||
</span>
|
||
<el-date-picker
|
||
v-model="dateRange"
|
||
type="daterange"
|
||
range-separator="—"
|
||
start-placeholder="开始"
|
||
end-placeholder="结束"
|
||
value-format="YYYY-MM-DD"
|
||
unlink-panels
|
||
class="dp-field-date"
|
||
@change="() => loadAppointments()"
|
||
/>
|
||
<span class="dp-field-label">
|
||
<el-icon><Filter /></el-icon>
|
||
状态
|
||
</span>
|
||
<el-select
|
||
v-model="filters.status"
|
||
placeholder="全部"
|
||
clearable
|
||
class="dp-field-select"
|
||
@change="() => loadAppointments()"
|
||
>
|
||
<el-option label="全部状态" value="" />
|
||
<el-option label="已预约(待就诊)" value="1" />
|
||
<el-option label="已完成" value="3" />
|
||
<el-option label="已过号" value="4" />
|
||
</el-select>
|
||
<span class="dp-field-label">
|
||
<el-icon><Search /></el-icon>
|
||
患者
|
||
</span>
|
||
<el-input
|
||
v-model="filters.patient_name"
|
||
placeholder="姓名"
|
||
clearable
|
||
class="dp-field-search"
|
||
@keyup.enter="() => loadAppointments()"
|
||
/>
|
||
<div class="dp-toolbar-actions">
|
||
<el-button type="primary" :loading="loading" :icon="Search" @click="() => loadAppointments()">
|
||
查询
|
||
</el-button>
|
||
<el-button :icon="RefreshRight" @click="resetFilters">重置</el-button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="doctors.length" class="dp-glass dp-doc-section">
|
||
<div class="dp-section-head">
|
||
<el-icon class="dp-section-icon"><User /></el-icon>
|
||
<span class="dp-section-title">选择医生</span>
|
||
<span class="dp-section-hint">切换后自动加载该医生挂号</span>
|
||
</div>
|
||
<el-radio-group
|
||
v-model="selectedDoctorId"
|
||
class="dp-doc-pills"
|
||
@change="() => loadAppointments()"
|
||
>
|
||
<el-radio-button
|
||
v-for="d in doctors"
|
||
:key="d.id"
|
||
:label="Number(d.id)"
|
||
class="dp-doc-pill"
|
||
>
|
||
{{ d.name || d.account || '—' }}
|
||
</el-radio-button>
|
||
</el-radio-group>
|
||
</section>
|
||
|
||
<section class="dp-glass dp-table-wrap" v-loading="loading">
|
||
<div class="dp-table-head">
|
||
<div class="dp-table-head-left">
|
||
<span class="dp-table-title">患者列表</span>
|
||
<span v-if="dateRange?.length === 2" class="dp-table-range">{{ dateRange[0] }} ~ {{ dateRange[1] }}</span>
|
||
</div>
|
||
<div class="dp-table-badges">
|
||
<span v-if="currentAccountLabel" class="dp-chip dp-chip--account">当前账号 {{ currentAccountLabel }}</span>
|
||
<span v-if="currentDoctorName" class="dp-chip dp-chip--doc">{{ currentDoctorName }}</span>
|
||
<span v-if="selectedDoctorId !== undefined && !loading" class="dp-chip dp-chip--count">{{ displayRows.length }} 人</span>
|
||
<span v-if="hiddenCompletedCount > 0" class="dp-chip dp-chip--muted">已隐藏已完成 {{ hiddenCompletedCount }}</span>
|
||
</div>
|
||
</div>
|
||
<el-empty v-if="!doctors.length" description="暂无医生数据" />
|
||
<el-empty v-else-if="selectedDoctorId === undefined" description="请选择医生" />
|
||
<el-table
|
||
v-else
|
||
:data="displayRows"
|
||
class="dp-table"
|
||
size="default"
|
||
:empty-text="tableEmptyText"
|
||
>
|
||
<el-table-column label="患者" min-width="200" fixed="left">
|
||
<template #default="{ row }">
|
||
<div class="dp-cell-user" :class="{ 'is-my-patients': isMyAssistantPatient(row) }">
|
||
<template v-if="isMyAssistantPatient(row)">
|
||
<div class="dp-my-patients-caption">我的患者</div>
|
||
<div class="dp-my-patients-eta">{{ assistantVisitEtaText(row) }}</div>
|
||
</template>
|
||
<div class="dp-cell-user-body">
|
||
<div class="dp-avatar">{{ patientInitial(row.patient_name) }}</div>
|
||
<div class="dp-user-meta">
|
||
<span class="dp-user-name">{{ row.patient_name || '—' }}</span>
|
||
<span class="dp-user-phone">{{ row.patient_phone || '—' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="预约时间" width="170">
|
||
<template #default="{ row }">
|
||
<span class="dp-time">{{ formatApptTime(row) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="挂号" width="104" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="appointmentTagType(row.status)" effect="light" round size="small">
|
||
{{ row.status_desc }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="面诊进度" min-width="360">
|
||
<template #default="{ row }">
|
||
<div class="dp-steps">
|
||
<template v-for="(s, i) in buildSteps(row)" :key="i">
|
||
<div
|
||
class="dp-step"
|
||
:class="{ 'is-done': s.done, 'is-current': s.current && !s.done }"
|
||
>
|
||
<div class="dp-step-dot">{{ i + 1 }}</div>
|
||
<div class="dp-step-text">
|
||
<span class="dp-step-name">{{ s.label }}</span>
|
||
<span v-if="s.hint" class="dp-step-hint">{{ s.hint }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="i < 3" class="dp-step-line" :class="{ 'is-done': s.done }" />
|
||
</template>
|
||
</div>
|
||
<el-progress :percentage="progressPercent(row)" :stroke-width="8" class="dp-progress" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="处方" width="92" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.has_prescription" type="success" effect="dark" round size="small">已开</el-tag>
|
||
<span v-else class="dp-muted">未开</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="96" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-button
|
||
v-if="row.diagnosis_id"
|
||
type="primary"
|
||
size="small"
|
||
round
|
||
plain
|
||
@click="goDiagnosis(row.diagnosis_id)"
|
||
>
|
||
诊单
|
||
</el-button>
|
||
<span v-else class="dp-muted">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts" name="doctorProgress">
|
||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import dayjs from 'dayjs'
|
||
import { Calendar, Filter, List, QuestionFilled, RefreshRight, Search, User } from '@element-plus/icons-vue'
|
||
import { appointmentLists, doctorLists } from '@/api/doctor'
|
||
import useUserStore from '@/stores/modules/user'
|
||
|
||
const router = useRouter()
|
||
const userStore = useUserStore()
|
||
const loading = ref(false)
|
||
const doctors = ref<any[]>([])
|
||
/** 当前选中医师(与排班页「选择医生」一致的横向单选) */
|
||
const selectedDoctorId = ref<number | undefined>(undefined)
|
||
const tableRows = ref<any[]>([])
|
||
|
||
const todayStr = () => dayjs().format('YYYY-MM-DD')
|
||
const dateRange = ref<[string, string]>([todayStr(), todayStr()])
|
||
|
||
const filters = reactive({
|
||
status: '' as string,
|
||
patient_name: ''
|
||
})
|
||
|
||
const currentDoctorName = computed(() => {
|
||
const id = selectedDoctorId.value
|
||
if (id === undefined) return ''
|
||
const d = doctors.value.find((x) => Number(x.id) === id)
|
||
return d ? String(d.name || d.account || '') : ''
|
||
})
|
||
|
||
const currentAdminId = computed(() => {
|
||
const id = userStore.userInfo?.id
|
||
return id != null && id !== '' ? Number(id) : NaN
|
||
})
|
||
|
||
const currentAccountLabel = computed(() => {
|
||
const u = userStore.userInfo || {}
|
||
return String(u.name || u.account || '').trim()
|
||
})
|
||
|
||
/** 诊单上的医助与当前登录账号一致时显示「我的患者」(医助视角) */
|
||
function isMyAssistantPatient(row: any) {
|
||
const me = currentAdminId.value
|
||
if (Number.isNaN(me)) return false
|
||
const aid = row?.assistant_id
|
||
if (aid == null || aid === '') return false
|
||
return Number(aid) === me
|
||
}
|
||
|
||
/** 粗估每位待就诊患者占用时长(分钟),用于候诊顺序预估 */
|
||
const AVG_MINUTES_PER_VISIT = 15
|
||
|
||
/** 触发接诊预估文案随时间刷新(与列表静默刷新互补) */
|
||
const etaClock = ref(0)
|
||
let etaTickTimer: ReturnType<typeof setInterval> | null = null
|
||
|
||
function apptTimeSortKey(row: any) {
|
||
const d = String(row.appointment_date || '')
|
||
let t = row.appointment_time || ''
|
||
if (typeof t === 'string' && t.length > 5) {
|
||
t = t.slice(0, 5)
|
||
}
|
||
return `${d} ${t}`.trim()
|
||
}
|
||
|
||
function parseApptSlot(row: any) {
|
||
const d = row.appointment_date
|
||
if (!d) return null
|
||
let t = row.appointment_time || '00:00'
|
||
if (typeof t === 'string' && t.length > 5) {
|
||
t = t.slice(0, 5)
|
||
}
|
||
const parsed = dayjs(`${d} ${t}`)
|
||
return parsed.isValid() ? parsed : null
|
||
}
|
||
|
||
function formatWaitMinutes(totalMin: number) {
|
||
const m = Math.max(0, Math.round(totalMin))
|
||
if (m === 0) return '不到 1 分钟'
|
||
if (m < 60) return `${m} 分钟`
|
||
const h = Math.floor(m / 60)
|
||
const mm = m % 60
|
||
if (mm === 0) return `${h} 小时`
|
||
return `${h} 小时${mm} 分钟`
|
||
}
|
||
|
||
/**
|
||
* 医助「我的患者」接诊预估:医生在挂号上点「完成」即面诊结束(status=3)。
|
||
* 待就诊:按当日同医生、待就诊列表的排序位次粗估;若预约时段未到会合并提示。
|
||
*/
|
||
function assistantVisitEtaText(row: any) {
|
||
void etaClock.value
|
||
if (!isMyAssistantPatient(row)) return ''
|
||
const st = Number(row.status)
|
||
if (st === 3) {
|
||
return '面诊已完成(医生已点完成)'
|
||
}
|
||
if (st === 4) {
|
||
return '已过号,无法按序预估'
|
||
}
|
||
if (st !== 1) {
|
||
return ''
|
||
}
|
||
|
||
const now = dayjs()
|
||
const slot = parseApptSlot(row)
|
||
const slotAfterNow = slot ? slot.isAfter(now) : false
|
||
const untilSlotMin = slotAfterNow && slot ? Math.max(0, slot.diff(now, 'minute')) : 0
|
||
|
||
const pendingSameDay = tableRows.value
|
||
.filter((r) => r.appointment_date === row.appointment_date && Number(r.status) === 1)
|
||
.slice()
|
||
.sort((a, b) => {
|
||
const c = apptTimeSortKey(a).localeCompare(apptTimeSortKey(b))
|
||
if (c !== 0) return c
|
||
return Number(a.id) - Number(b.id)
|
||
})
|
||
|
||
const idx = pendingSameDay.findIndex((r) => Number(r.id) === Number(row.id))
|
||
const ahead = idx >= 0 ? idx : 0
|
||
const queueMin = ahead * AVG_MINUTES_PER_VISIT
|
||
|
||
const parts: string[] = []
|
||
if (slotAfterNow) {
|
||
parts.push(`距预约时段还有约 ${formatWaitMinutes(untilSlotMin)}`)
|
||
}
|
||
if (ahead === 0) {
|
||
parts.push(slotAfterNow ? '到号后可优先安排面诊' : '已到号,可尽快面诊')
|
||
} else {
|
||
parts.push(`前面约 ${ahead} 人,按序粗估约 ${formatWaitMinutes(queueMin)}`)
|
||
}
|
||
return parts.join(';')
|
||
}
|
||
|
||
/** 未指定具体状态时视为「全部」,此时隐藏挂号「已完成」 */
|
||
function isBroadStatusFilter() {
|
||
const s = filters.status as string | undefined | null
|
||
return s === '' || s === undefined || s === null
|
||
}
|
||
|
||
/** 「全部状态」时默认不展示挂号已完成,便于看板聚焦进行中 */
|
||
const hiddenCompletedCount = computed(() => {
|
||
if (!isBroadStatusFilter()) return 0
|
||
return tableRows.value.filter((r) => Number(r.status) === 3).length
|
||
})
|
||
|
||
const displayRows = computed(() => {
|
||
if (!isBroadStatusFilter()) {
|
||
return tableRows.value
|
||
}
|
||
return tableRows.value.filter((r) => Number(r.status) !== 3)
|
||
})
|
||
|
||
const tableEmptyText = computed(() => {
|
||
if (hiddenCompletedCount.value > 0 && displayRows.value.length === 0) {
|
||
return `本时段有 ${hiddenCompletedCount.value} 条「已完成」已隐藏,可在状态中选择「已完成」查看`
|
||
}
|
||
return '该时段内暂无挂号'
|
||
})
|
||
|
||
function patientInitial(name: unknown) {
|
||
const s = String(name || '?').trim()
|
||
return s ? s[0] : '?'
|
||
}
|
||
|
||
function formatApptTime(row: any) {
|
||
const d = row.appointment_date || ''
|
||
let t = row.appointment_time || ''
|
||
if (typeof t === 'string' && t.length > 5) {
|
||
t = t.slice(0, 5)
|
||
}
|
||
return d ? `${d} ${t}`.trim() : '—'
|
||
}
|
||
|
||
function appointmentTagType(status: number) {
|
||
if (status === 3) return 'success'
|
||
if (status === 4) return 'warning'
|
||
if (status === 2) return 'info'
|
||
return 'primary'
|
||
}
|
||
|
||
/** 面诊进度四步:挂号有效 → 诊单已确认 → 已完诊 → 已开方(开方=今日已创建处方,见 prescription_today_only) */
|
||
function buildSteps(row: any) {
|
||
const cancelled = Number(row.status) === 2
|
||
const regOk = !cancelled && [1, 3, 4].includes(Number(row.status))
|
||
const confirmed = !!row.diagnosis_confirmed
|
||
const visitDone = Number(row.status) === 3
|
||
const rx = !!row.has_prescription
|
||
|
||
const steps = [
|
||
{
|
||
label: '挂号',
|
||
done: regOk,
|
||
current: regOk && !confirmed && Number(row.status) === 1,
|
||
hint: cancelled ? '已取消' : Number(row.status) === 4 ? '已过号' : ''
|
||
},
|
||
{
|
||
label: '确认诊单',
|
||
done: confirmed,
|
||
current: regOk && !confirmed,
|
||
hint: ''
|
||
},
|
||
{
|
||
label: '就诊中',
|
||
done: visitDone,
|
||
current: regOk && confirmed && !visitDone && Number(row.status) === 1,
|
||
hint: Number(row.status) === 1 && confirmed ? '待完诊' : ''
|
||
},
|
||
{
|
||
label: '开方',
|
||
done: rx,
|
||
current: visitDone && !rx,
|
||
hint: ''
|
||
}
|
||
]
|
||
return steps
|
||
}
|
||
|
||
function progressPercent(row: any) {
|
||
const steps = buildSteps(row)
|
||
const n = steps.filter((s) => s.done).length
|
||
return Math.round((n / 4) * 100)
|
||
}
|
||
|
||
/** 单次请求最大条数(避免 page_type=0 一次拉全表导致服务端 500/超时) */
|
||
const APPOINT_PAGE_SIZE = 2000
|
||
/** 自动刷新间隔(毫秒) */
|
||
const AUTO_REFRESH_MS = 15_000
|
||
|
||
let autoRefreshTimer: ReturnType<typeof setInterval> | null = null
|
||
|
||
async function loadDoctors() {
|
||
const res = await doctorLists({
|
||
role_id: 1,
|
||
page_type: 0,
|
||
page_no: 1,
|
||
/** 仅登录即可拉取,不依赖菜单权限(与接口 progress_board 约定一致) */
|
||
progress_board: 1
|
||
})
|
||
doctors.value = res?.lists || []
|
||
if (!doctors.value.length) {
|
||
selectedDoctorId.value = undefined
|
||
tableRows.value = []
|
||
return
|
||
}
|
||
const valid = doctors.value.some((d) => Number(d.id) === selectedDoctorId.value)
|
||
if (selectedDoctorId.value === undefined || !valid) {
|
||
selectedDoctorId.value = Number(doctors.value[0].id)
|
||
}
|
||
}
|
||
|
||
async function loadAppointments(opts?: { silent?: boolean }) {
|
||
if (selectedDoctorId.value === undefined) {
|
||
tableRows.value = []
|
||
return
|
||
}
|
||
const silent = opts?.silent === true
|
||
if (!silent) {
|
||
loading.value = true
|
||
}
|
||
try {
|
||
const params: Record<string, any> = {
|
||
doctor_id: selectedDoctorId.value,
|
||
page_type: 1,
|
||
page_no: 1,
|
||
page_size: APPOINT_PAGE_SIZE,
|
||
/** 开方状态仅认「今日创建」的处方,避免历史处方误判进度 */
|
||
prescription_today_only: 1,
|
||
/** 本页不展示已取消挂号(status=2) */
|
||
exclude_cancelled: 1,
|
||
/** 不按医生/医助角色收窄数据;与接口免菜单权限约定一致 */
|
||
progress_board: 1
|
||
}
|
||
if (dateRange.value?.length === 2) {
|
||
params.start_date = dateRange.value[0]
|
||
params.end_date = dateRange.value[1]
|
||
}
|
||
if (filters.status !== '') {
|
||
params.status = filters.status
|
||
}
|
||
if (filters.patient_name?.trim()) {
|
||
params.patient_name = filters.patient_name.trim()
|
||
}
|
||
const res = await appointmentLists(params)
|
||
let list = res?.lists || []
|
||
const total = Number(res?.count ?? list.length)
|
||
if (total > list.length && list.length >= APPOINT_PAGE_SIZE) {
|
||
const extra: any[] = []
|
||
const pages = Math.min(Math.ceil(total / APPOINT_PAGE_SIZE), 20)
|
||
for (let p = 2; p <= pages; p++) {
|
||
const r = await appointmentLists({ ...params, page_no: p })
|
||
const chunk = r?.lists || []
|
||
if (!chunk.length) break
|
||
extra.push(...chunk)
|
||
}
|
||
list = [...list, ...extra]
|
||
}
|
||
tableRows.value = [...list].sort((a, b) => {
|
||
const da = `${a.appointment_date || ''} ${a.appointment_time || ''}`
|
||
const db = `${b.appointment_date || ''} ${b.appointment_time || ''}`
|
||
return da.localeCompare(db)
|
||
})
|
||
} catch (e) {
|
||
console.error(e)
|
||
if (!silent) {
|
||
tableRows.value = []
|
||
}
|
||
} finally {
|
||
if (!silent) {
|
||
loading.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
function runAutoRefresh() {
|
||
if (typeof document !== 'undefined' && document.hidden) {
|
||
return
|
||
}
|
||
void loadAppointments({ silent: true }).catch(() => {})
|
||
}
|
||
|
||
function resetFilters() {
|
||
filters.status = ''
|
||
filters.patient_name = ''
|
||
dateRange.value = [todayStr(), todayStr()]
|
||
loadAppointments()
|
||
}
|
||
|
||
function goDiagnosis(diagnosisId: number) {
|
||
router.push({ path: '/tcm/diagnosis', query: { id: String(diagnosisId) } })
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadDoctors()
|
||
await loadAppointments()
|
||
autoRefreshTimer = setInterval(runAutoRefresh, AUTO_REFRESH_MS)
|
||
etaTickTimer = setInterval(() => {
|
||
etaClock.value += 1
|
||
}, 60_000)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (autoRefreshTimer != null) {
|
||
clearInterval(autoRefreshTimer)
|
||
autoRefreshTimer = null
|
||
}
|
||
if (etaTickTimer != null) {
|
||
clearInterval(etaTickTimer)
|
||
etaTickTimer = null
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.dp-page {
|
||
padding: 20px 24px 40px;
|
||
min-height: 100%;
|
||
box-sizing: border-box;
|
||
background:
|
||
radial-gradient(ellipse 120% 80% at 8% -15%, rgba(64, 158, 255, 0.12), transparent 52%),
|
||
radial-gradient(ellipse 90% 55% at 96% 8%, rgba(103, 194, 58, 0.08), transparent 48%),
|
||
linear-gradient(180deg, #f8fafc 0%, #eef2f7 45%, #e8edf4 100%);
|
||
}
|
||
|
||
.dp-hero {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
padding: 2px 0;
|
||
}
|
||
|
||
.dp-hero-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.dp-hero-badge {
|
||
width: 52px;
|
||
height: 52px;
|
||
border-radius: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
||
color: #fff;
|
||
box-shadow: 0 8px 28px rgba(64, 158, 255, 0.32);
|
||
}
|
||
|
||
.dp-hero-title {
|
||
margin: 0 0 6px;
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.dp-hero-sub {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.dp-hero-aside {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.dp-pulse {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 14px;
|
||
border-radius: 999px;
|
||
background: rgba(255, 255, 255, 0.75);
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
backdrop-filter: blur(8px);
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.dp-pulse-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--el-color-success);
|
||
box-shadow: 0 0 0 3px var(--el-color-success-light-8);
|
||
animation: dp-pulse 2.2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes dp-pulse {
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
50% {
|
||
opacity: 0.72;
|
||
transform: scale(0.9);
|
||
}
|
||
}
|
||
|
||
.dp-icon-btn {
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
.dp-tip-lines {
|
||
max-width: 280px;
|
||
line-height: 1.6;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.dp-glass {
|
||
background: rgba(255, 255, 255, 0.86);
|
||
backdrop-filter: blur(12px);
|
||
border: 1px solid rgba(255, 255, 255, 0.95);
|
||
border-radius: 16px;
|
||
box-shadow:
|
||
0 1px 2px rgba(15, 23, 42, 0.04),
|
||
0 14px 44px -14px rgba(15, 23, 42, 0.14);
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.dp-toolbar {
|
||
padding: 16px 20px;
|
||
}
|
||
|
||
.dp-toolbar-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12px 16px;
|
||
}
|
||
|
||
.dp-field-label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--el-text-color-secondary);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.dp-field-date {
|
||
width: 260px;
|
||
}
|
||
|
||
.dp-field-select {
|
||
width: 168px;
|
||
}
|
||
|
||
.dp-field-search {
|
||
width: 140px;
|
||
}
|
||
|
||
.dp-toolbar-actions {
|
||
margin-left: auto;
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.dp-doc-section {
|
||
padding: 14px 18px 18px;
|
||
}
|
||
|
||
.dp-section-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.dp-section-icon {
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.dp-section-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.dp-section-hint {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
|
||
.dp-doc-pills {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
width: 100%;
|
||
}
|
||
|
||
.dp-doc-pills :deep(.el-radio-button__inner) {
|
||
border-radius: 10px !important;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
padding: 8px 16px;
|
||
font-weight: 500;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.dp-doc-pills :deep(.el-radio-button.is-active .el-radio-button__inner) {
|
||
background: linear-gradient(135deg, var(--el-color-primary-light-3), var(--el-color-primary)) !important;
|
||
border-color: transparent !important;
|
||
color: #fff !important;
|
||
box-shadow: 0 4px 14px rgba(64, 158, 255, 0.32);
|
||
}
|
||
|
||
.dp-table-wrap {
|
||
padding: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.dp-table-wrap :deep(.el-loading-mask) {
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.dp-table-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 20px 12px;
|
||
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.dp-table-head-left {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.dp-table-title {
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.dp-table-range {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.dp-table-badges {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.dp-chip {
|
||
font-size: 12px;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.dp-chip--account {
|
||
background: var(--el-fill-color-blank);
|
||
color: var(--el-text-color-regular);
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
.dp-chip--doc {
|
||
background: var(--el-color-primary-light-9);
|
||
color: var(--el-color-primary);
|
||
border: 1px solid var(--el-color-primary-light-5);
|
||
}
|
||
|
||
.dp-chip--count {
|
||
background: var(--el-fill-color-light);
|
||
color: var(--el-text-color-secondary);
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
.dp-chip--muted {
|
||
background: transparent;
|
||
color: var(--el-text-color-placeholder);
|
||
border: 1px dashed var(--el-border-color);
|
||
font-weight: 400;
|
||
}
|
||
|
||
.dp-table {
|
||
width: 100%;
|
||
--el-table-border-color: var(--el-border-color-extra-light);
|
||
}
|
||
|
||
.dp-table :deep(.el-table__header th) {
|
||
background: var(--el-fill-color-light) !important;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.dp-table :deep(.el-table__body tr:hover > td) {
|
||
background: var(--el-fill-color-lighter) !important;
|
||
}
|
||
|
||
.dp-cell-user {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.dp-cell-user.is-my-patients {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 6px;
|
||
}
|
||
|
||
.dp-my-patients-caption {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
color: var(--el-color-primary);
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.dp-my-patients-eta {
|
||
font-size: 11px;
|
||
line-height: 1.45;
|
||
color: var(--el-text-color-secondary);
|
||
max-width: 220px;
|
||
}
|
||
|
||
.dp-cell-user-body {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.dp-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
background: linear-gradient(145deg, #6366f1 0%, #8b5cf6 100%);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.dp-user-meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.dp-user-name {
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.dp-user-phone {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.dp-time {
|
||
font-variant-numeric: tabular-nums;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
|
||
.dp-steps {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
margin-bottom: 10px;
|
||
max-width: 500px;
|
||
}
|
||
|
||
.dp-step {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
gap: 6px;
|
||
padding: 6px 2px;
|
||
}
|
||
|
||
.dp-step-dot {
|
||
width: 26px;
|
||
height: 26px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--el-text-color-secondary);
|
||
background: var(--el-fill-color);
|
||
border: 2px solid var(--el-border-color);
|
||
}
|
||
|
||
.dp-step.is-done .dp-step-dot {
|
||
background: var(--el-color-success);
|
||
border-color: var(--el-color-success);
|
||
color: #fff;
|
||
}
|
||
|
||
.dp-step.is-current:not(.is-done) .dp-step-dot {
|
||
border-color: var(--el-color-primary);
|
||
color: var(--el-color-primary);
|
||
background: var(--el-color-primary-light-9);
|
||
box-shadow: 0 0 0 3px var(--el-color-primary-light-8);
|
||
}
|
||
|
||
.dp-step-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.dp-step-name {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.dp-step-hint {
|
||
font-size: 11px;
|
||
color: var(--el-text-color-placeholder);
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.dp-step-line {
|
||
flex: 0 0 14px;
|
||
height: 2px;
|
||
background: var(--el-border-color);
|
||
border-radius: 1px;
|
||
align-self: center;
|
||
}
|
||
|
||
.dp-step-line.is-done {
|
||
background: linear-gradient(90deg, var(--el-color-success-light-5), var(--el-color-success));
|
||
}
|
||
|
||
.dp-progress {
|
||
max-width: 420px;
|
||
|
||
:deep(.el-progress-bar__outer) {
|
||
border-radius: 999px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
:deep(.el-progress__text) {
|
||
font-size: 12px !important;
|
||
min-width: 36px;
|
||
}
|
||
}
|
||
|
||
.dp-muted {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
</style>
|