This commit is contained in:
Your Name
2026-04-07 18:13:03 +08:00
parent a780356908
commit fdf714f833
397 changed files with 15086 additions and 1043 deletions
+990
View File
@@ -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>