Files
zyt/admin/src/views/doctor/progress.vue
T
2026-04-07 18:13:03 +08:00

991 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>