新增
This commit is contained in:
@@ -0,0 +1,990 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user