| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822 |
- <template>
- <div v-loading="loading" class="oa-dashboard">
- <ContentWrap>
- <div class="page-header">
- <div class="header-title">
- <Icon icon="ep:data-analysis" class="title-icon" />
- <span>院区运营驾驶舱</span>
- </div>
- <div class="header-extra">
- <span class="update-time">数据更新时间:{{ updateTime }}</span>
- <el-button type="primary" :icon="Refresh" @click="loadAll">刷新数据</el-button>
- </div>
- </div>
- <!-- 动态指标加载失败提示 -->
- <el-alert
- v-if="!dynamicsReady"
- type="warning"
- :closable="false"
- show-icon
- class="api-tip"
- title="今日新收、今日退院、请假人数、当天请假、轮候待住、预订在途、退住趋势及护理等级分布加载失败,请稍后刷新重试;床位与在院人数不受影响。"
- />
- <!-- 实时动态 KPI -->
- <el-row :gutter="12" class="kpi-row">
- <el-col v-for="card in kpiCards" :key="card.label" :xs="12" :sm="8" :md="6" :lg="6" :xl="3">
- <div class="kpi-card" :class="card.theme">
- <div class="kpi-label">
- <Icon :icon="card.icon" />
- <span>{{ card.label }}</span>
- </div>
- <div class="kpi-value">
- {{ card.value }}
- <span class="kpi-unit">{{ card.unit }}</span>
- </div>
- </div>
- </el-col>
- </el-row>
- <!-- 运营周期汇总 -->
- <div class="period-card">
- <div class="period-head">
- <span class="period-title">
- <Icon icon="ep:calendar" />
- 运营周期汇总<template v-if="weekly.institutionName"
- >({{ weekly.institutionName }})</template
- >
- </span>
- <span class="period-tip">按所选院区合计口径统计</span>
- </div>
- <div v-if="!hasWeekly" class="period-empty">
- 暂无数据:多院区运营周期汇总需后端在聚合接口中补充,当前仅登录机构查看本院时展示
- </div>
- <el-row v-else :gutter="10">
- <el-col :xs="8" :sm="8" :md="4" :lg="4">
- <div class="period-item">
- <div class="period-label">本周期初人数</div>
- <div class="period-num">{{ weekly.periodBeginCount ?? '-' }}</div>
- </div>
- </el-col>
- <el-col :xs="8" :sm="8" :md="4" :lg="4">
- <div class="period-item">
- <div class="period-label">本周期末人数</div>
- <div class="period-num">{{ weekly.periodEndCount ?? '-' }}</div>
- </div>
- </el-col>
- <el-col :xs="8" :sm="8" :md="5" :lg="5">
- <div class="period-item">
- <div class="period-label">本周入住 / 退住 / 净增</div>
- <div class="period-num triple">
- <em class="up">{{ weekly.weeklyCheckInCount ?? 0 }}</em>
- <span class="sep">/</span>
- <em class="down">{{ weekly.weeklyCheckOutCount ?? 0 }}</em>
- <span class="sep">/</span>
- <em :class="netClass(weekly.weeklyNetGrowthCount)">
- {{ formatNet(weekly.weeklyNetGrowthCount) }}
- </em>
- </div>
- </div>
- </el-col>
- <el-col :xs="12" :sm="8" :md="6" :lg="6">
- <div class="period-item">
- <div class="period-label">本月入住 / 退住 / 净增</div>
- <div class="period-num triple">
- <em class="up">{{ weekly.monthlyCheckInCount ?? 0 }}</em>
- <span class="sep">/</span>
- <em class="down">{{ weekly.monthlyCheckOutCount ?? 0 }}</em>
- <span class="sep">/</span>
- <em :class="netClass(weekly.monthlyNetGrowthCount)">
- {{ formatNet(weekly.monthlyNetGrowthCount) }}
- </em>
- </div>
- </div>
- </el-col>
- <el-col :xs="12" :sm="8" :md="5" :lg="5">
- <div class="period-item">
- <div class="period-label">期末空余床位数</div>
- <div class="period-num">
- {{ weekly.periodEndEmptyBedCount ?? '-' }}
- <span class="kpi-unit">张</span>
- </div>
- </div>
- </el-col>
- </el-row>
- </div>
- </ContentWrap>
- <el-row :gutter="12" class="chart-row">
- <el-col :xs="24" :lg="15">
- <ContentWrap>
- <div class="panel-title">各院区床位与入住率</div>
- <Echart :options="campusChartOption" height="360px" />
- </ContentWrap>
- </el-col>
- <el-col :xs="24" :lg="9">
- <ContentWrap>
- <div class="panel-title">{{ campusOptions.length }} 家院区床位结构</div>
- <div class="donut-wrap">
- <Echart :options="bedStructureOption" height="360px" />
- <div class="donut-center">
- <div class="donut-total">{{ bedSummary.total || '-' }}</div>
- <div class="donut-label">总床位(张)</div>
- </div>
- </div>
- </ContentWrap>
- </el-col>
- </el-row>
- <el-row :gutter="12" class="chart-row">
- <el-col :span="24">
- <ContentWrap>
- <div class="panel-title">
- 近 12 个月已完成退住趋势
- <span v-if="dynamicsReady" class="title-tip">合计 {{ checkoutTotal }} 人</span>
- </div>
- <Echart v-if="dynamicsReady" :options="checkoutTrendOption" height="300px" />
- <el-empty
- v-else
- description="暂无数据:动态指标加载失败"
- :image-size="90"
- class="chart-empty"
- />
- </ContentWrap>
- </el-col>
- </el-row>
- <el-row :gutter="12" class="chart-row">
- <el-col :xs="24" :lg="12">
- <ContentWrap>
- <div class="panel-title">
- 护理等级分布 TOP10
- <span class="title-tip">各院区护理等级名称按同名合并统计</span>
- </div>
- <Echart v-if="careLevels.length" :options="careLevelOption" height="380px" />
- <el-empty
- v-else
- :description="dynamicsReady ? '所选院区暂无护理等级数据' : '暂无数据:动态指标加载失败'"
- :image-size="90"
- class="chart-empty"
- />
- </ContentWrap>
- </el-col>
- <el-col :xs="24" :lg="12">
- <ContentWrap>
- <div class="panel-title">院区入住率区间分布</div>
- <Echart :options="occupancyTierOption" height="380px" />
- </ContentWrap>
- </el-col>
- </el-row>
- </div>
- </template>
- <script lang="ts" setup>
- import { Refresh } from '@element-plus/icons-vue'
- import Echart from '@/components/Echart/src/Echart.vue'
- import {
- fetchCampusBedStats,
- fetchInHospitalCount,
- fetchOverviewDynamics,
- getCampusOptions,
- type CampusBedStat,
- type NameCountItem,
- type OverviewDynamics
- } from '../campus-data'
- defineOptions({ name: 'OaOverviewDashboard' })
- const message = useMessage()
- const loading = ref(false)
- const updateTime = ref('')
- /** 在院人数(档案在院状态) */
- const inHospitalCount = ref<number | null>(null)
- /** 后端聚合动态指标(新收/退院/请假/轮候/预订/退住趋势/护理等级/周期汇总),加载失败为 null */
- const dynamics = ref<OverviewDynamics | null>(null)
- const campusStats = ref<CampusBedStat[]>([])
- const campusOptions = getCampusOptions()
- const dynamicsReady = computed(() => dynamics.value !== null)
- const hasWeekly = computed(() => dynamics.value?.weekly != null)
- const weekly = computed<Record<string, any>>(() => dynamics.value?.weekly ?? {})
- const careLevels = computed<NameCountItem[]>(() => dynamics.value?.nursingLevels ?? [])
- const checkoutTrend = computed(() => dynamics.value?.checkoutTrend ?? [])
- const fmtCount = (val: number | null | undefined) => (val === null || val === undefined ? '-' : val)
- const kpiCards = computed(() => [
- {
- label: '总床位数',
- value: bedSummary.value.total || '-',
- unit: '张',
- icon: 'ep:house',
- theme: 'blue'
- },
- {
- label: '在院人数',
- value: fmtCount(inHospitalCount.value),
- unit: '人',
- icon: 'ep:user',
- theme: 'green'
- },
- {
- label: '今日新收',
- value: fmtCount(dynamics.value?.todayCheckInCount),
- unit: '人',
- icon: 'ep:plus',
- theme: 'cyan'
- },
- {
- label: '今日退院',
- value: fmtCount(dynamics.value?.todayCheckOutCount),
- unit: '人',
- icon: 'ep:minus',
- theme: 'orange'
- },
- {
- label: '请假人数',
- value: fmtCount(dynamics.value?.leaveOutCount),
- unit: '人',
- icon: 'ep:clock',
- theme: 'purple'
- },
- {
- label: '当天请假人数',
- value: fmtCount(dynamics.value?.todayLeaveCount),
- unit: '人',
- icon: 'ep:timer',
- theme: 'grey'
- },
- {
- label: '轮候待住',
- value: fmtCount(dynamics.value?.waitingCount),
- unit: '人',
- icon: 'ep:bell',
- theme: 'red'
- },
- {
- label: '预订在途',
- value: fmtCount(dynamics.value?.reservationCount),
- unit: '人',
- icon: 'ep:document',
- theme: 'pink'
- }
- ])
- const formatNet = (val: number | undefined | null) => {
- if (val === undefined || val === null) return '-'
- return val > 0 ? `+${val}` : `${val}`
- }
- const netClass = (val: number | undefined | null) => {
- if (!val) return 'zero'
- return val > 0 ? 'up' : 'down'
- }
- /** 床位结构汇总 */
- const bedSummary = computed(() =>
- campusStats.value.reduce(
- (sum, item) => {
- sum.used += item.usedBed
- sum.unused += item.unusedBed
- sum.temp += item.tempBed
- sum.total += item.totalBed
- return sum
- },
- { used: 0, unused: 0, temp: 0, total: 0 }
- )
- )
- /** 院区床位 + 入住率组合图,按入住率降序 */
- const campusChartOption = computed<any>(() => {
- const sorted = [...campusStats.value].sort((a, b) => {
- const ra = a.occupancyRate ?? -1
- const rb = b.occupancyRate ?? -1
- return rb - ra
- })
- return {
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' },
- formatter: (params: any[]) => {
- const campus = sorted[params[0].dataIndex]
- const lines = params.map(
- (p) => `${p.marker}${p.seriesName}:${p.value}${p.seriesName === '入住率' ? '%' : ' 张'}`
- )
- const extra = campus
- ? `<br/>空闲床位:${campus.unusedBed} 张<br/>楼栋数:${campus.buildCount} 栋`
- : ''
- return `${params[0].axisValue}${extra}<br/>${lines.join('<br/>')}`
- }
- },
- legend: { data: ['总床位', '在住床位', '入住率'], top: 0 },
- grid: { left: 50, right: 50, top: 40, bottom: 90 },
- xAxis: {
- type: 'category',
- data: sorted.map((item) => item.campusName),
- axisLabel: { interval: 0, rotate: 35 }
- },
- yAxis: [
- { type: 'value', name: '床位数(张)' },
- { type: 'value', name: '入住率', min: 0, max: 100, axisLabel: { formatter: '{value}%' } }
- ],
- series: [
- {
- name: '总床位',
- type: 'bar',
- barMaxWidth: 18,
- itemStyle: { color: '#8cc5ff', borderRadius: [3, 3, 0, 0] },
- data: sorted.map((item) => item.totalBed)
- },
- {
- name: '在住床位',
- type: 'bar',
- barMaxWidth: 18,
- itemStyle: { color: '#409eff', borderRadius: [3, 3, 0, 0] },
- data: sorted.map((item) => item.usedBed)
- },
- {
- name: '入住率',
- type: 'line',
- yAxisIndex: 1,
- smooth: true,
- symbol: 'circle',
- symbolSize: 7,
- lineStyle: { color: '#fa8c16', width: 2 },
- itemStyle: { color: '#fa8c16' },
- label: { show: true, formatter: '{c}%', fontSize: 10, color: '#fa8c16' },
- data: sorted.map((item) => item.occupancyRate ?? 0)
- }
- ]
- }
- })
- /** 床位结构环形图 */
- const bedStructureOption = computed<any>(() => ({
- tooltip: { trigger: 'item', formatter: '{b}:{c} 张({d}%)' },
- legend: { bottom: 0 },
- series: [
- {
- name: '床位结构',
- type: 'pie',
- radius: ['45%', '70%'],
- center: ['50%', '46%'],
- avoidLabelOverlap: true,
- itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
- label: {
- formatter: '{b}\n{c} 张'
- },
- data: [
- { value: bedSummary.value.used, name: '在住床位', itemStyle: { color: '#67c23a' } },
- { value: bedSummary.value.unused, name: '空闲床位', itemStyle: { color: '#c0c4cc' } },
- { value: bedSummary.value.temp, name: '临时占用', itemStyle: { color: '#e6a23c' } }
- ]
- }
- ]
- }))
- const checkoutTotal = computed(() => checkoutTrend.value.reduce((sum, item) => sum + item.count, 0))
- /** 近 12 个月退住趋势(柱+月均标线) */
- const checkoutTrendOption = computed<any>(() => {
- const data = checkoutTrend.value
- return {
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' },
- formatter: (params: any[]) => {
- const item = data[params[0].dataIndex]
- return `${item.month}<br/>${params[0].marker}已完成退住:${params[0].value} 人`
- }
- },
- grid: { left: 45, right: 25, top: 40, bottom: 30 },
- xAxis: {
- type: 'category',
- data: data.map((item) => item.label),
- axisLabel: {
- color: '#606266',
- formatter: (val: string, index: number) =>
- index === 0 || data[index].month.endsWith('-01') ? data[index].month : val
- }
- },
- yAxis: { type: 'value', name: '人数', minInterval: 1 },
- series: [
- {
- name: '已完成退住',
- type: 'bar',
- barMaxWidth: 26,
- label: { show: true, position: 'top', formatter: '{c}' },
- itemStyle: {
- borderRadius: [4, 4, 0, 0],
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [
- { offset: 0, color: '#ffb066' },
- { offset: 1, color: '#f5851d' }
- ]
- }
- },
- markLine: {
- symbol: 'none',
- lineStyle: { color: '#f56c6c', type: 'dashed' },
- label: {
- formatter: '月均 {c} 人',
- color: '#f56c6c',
- position: 'insideEndTop'
- },
- data: [{ type: 'average', name: '月均' }]
- },
- data: data.map((item) => item.count)
- }
- ]
- }
- })
- /** 护理等级 TOP10 横向条形图 */
- const careLevelOption = computed<any>(() => {
- const top = careLevels.value.slice(0, 10)
- return {
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' },
- formatter: (params: any[]) => `${params[0].name}:${params[0].value} 人`
- },
- grid: { left: 10, right: 40, top: 15, bottom: 10, containLabel: true },
- xAxis: { type: 'value', name: '人数' },
- yAxis: {
- type: 'category',
- inverse: true,
- data: top.map((item) => item.name),
- axisLabel: { width: 120, overflow: 'truncate' }
- },
- series: [
- {
- name: '人数',
- type: 'bar',
- barMaxWidth: 18,
- label: { show: true, position: 'right', formatter: '{c} 人' },
- itemStyle: {
- borderRadius: [0, 4, 4, 0],
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 1,
- y2: 0,
- colorStops: [
- { offset: 0, color: '#79bbff' },
- { offset: 1, color: '#409eff' }
- ]
- }
- },
- data: top.map((item) => item.count)
- }
- ]
- }
- })
- /** 入住率区间分布 */
- const occupancyTierOption = computed<any>(() => {
- const tiers = [
- { key: 'tight', label: '入住紧张(≥90%)', color: '#f56c6c', campuses: [] as string[] },
- { key: 'good', label: '运行良好(70%-90%)', color: '#67c23a', campuses: [] as string[] },
- { key: 'normal', label: '入住一般(50%-70%)', color: '#409eff', campuses: [] as string[] },
- { key: 'low', label: '空置预警(>0且<50%)', color: '#e6a23c', campuses: [] as string[] },
- { key: 'none', label: '未配置房源', color: '#c0c4cc', campuses: [] as string[] }
- ]
- campusStats.value.forEach((item) => {
- const rate = item.occupancyRate
- if (rate === null) tiers[4].campuses.push(item.campusName)
- else if (rate >= 90) tiers[0].campuses.push(`${item.campusName}(${rate}%)`)
- else if (rate >= 70) tiers[1].campuses.push(`${item.campusName}(${rate}%)`)
- else if (rate >= 50) tiers[2].campuses.push(`${item.campusName}(${rate}%)`)
- else tiers[3].campuses.push(`${item.campusName}(${rate}%)`)
- })
- return {
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' },
- formatter: (params: any[]) => {
- const tier = tiers[params[0].dataIndex]
- const names = tier.campuses.length ? tier.campuses.join('、') : '无'
- return `${tier.label}<br/>院区数量:${tier.campuses.length} 个<br/>${names}`
- }
- },
- grid: { left: 45, right: 30, top: 30, bottom: 70 },
- xAxis: {
- type: 'category',
- data: tiers.map((item) => item.label),
- axisLabel: { interval: 0, rotate: 18, fontSize: 11 }
- },
- yAxis: { type: 'value', name: '院区数量(个)', minInterval: 1 },
- series: [
- {
- name: '院区数量',
- type: 'bar',
- barMaxWidth: 46,
- label: { show: true, position: 'top', formatter: '{c} 个' },
- data: tiers.map((item) => ({
- value: item.campuses.length,
- itemStyle: { color: item.color, borderRadius: [4, 4, 0, 0] }
- }))
- }
- ]
- }
- })
- const formatTime = (date: Date) => {
- const pad = (n: number) => String(n).padStart(2, '0')
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
- date.getHours()
- )}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
- }
- const loadAll = async () => {
- loading.value = true
- try {
- const tenantIds = campusOptions.map((item) => item.value)
- const [bedStats, inHospital, dyn] = await Promise.all([
- fetchCampusBedStats(tenantIds),
- fetchInHospitalCount(tenantIds),
- fetchOverviewDynamics(tenantIds)
- ])
- campusStats.value = bedStats
- inHospitalCount.value = inHospital
- dynamics.value = dyn
- if (!dyn) {
- message.warning('今日动态、退住趋势及护理等级数据加载失败,床位与在院人数为实时统计数据')
- }
- updateTime.value = formatTime(new Date())
- } catch (e) {
- message.error('运营数据加载失败,请稍后重试')
- } finally {
- loading.value = false
- }
- }
- onMounted(() => {
- loadAll()
- })
- </script>
- <style lang="scss" scoped>
- .oa-dashboard {
- .page-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 16px;
- .header-title {
- display: flex;
- align-items: center;
- font-size: 18px;
- font-weight: 600;
- color: #303133;
- .title-icon {
- margin-right: 8px;
- font-size: 22px;
- color: #409eff;
- }
- }
- .header-extra {
- display: flex;
- align-items: center;
- .update-time {
- margin-right: 14px;
- font-size: 13px;
- color: #909399;
- }
- }
- }
- .api-tip {
- margin-bottom: 14px;
- }
- .kpi-row {
- margin-bottom: 14px;
- .kpi-card {
- padding: 16px 18px;
- border-radius: 8px;
- color: #fff;
- margin-bottom: 10px;
- .kpi-label {
- display: flex;
- align-items: center;
- font-size: 13px;
- opacity: 0.95;
- .el-icon {
- margin-right: 6px;
- font-size: 16px;
- }
- }
- .kpi-value {
- margin-top: 8px;
- font-size: 30px;
- font-weight: 700;
- line-height: 1.1;
- .kpi-unit {
- margin-left: 4px;
- font-size: 13px;
- font-weight: 400;
- opacity: 0.85;
- }
- }
- &.blue {
- background: linear-gradient(135deg, #5b9bff 0%, #2f7cf6 100%);
- }
- &.green {
- background: linear-gradient(135deg, #62d391 0%, #36a866 100%);
- }
- &.cyan {
- background: linear-gradient(135deg, #4fd0e0 0%, #1fa9bf 100%);
- }
- &.orange {
- background: linear-gradient(135deg, #ffb066 0%, #f5851d 100%);
- }
- &.purple {
- background: linear-gradient(135deg, #b794f6 0%, #8b5cf6 100%);
- }
- &.grey {
- background: linear-gradient(135deg, #a8abb2 0%, #7a7f8a 100%);
- }
- &.red {
- background: linear-gradient(135deg, #ff8f8f 0%, #f56c6c 100%);
- }
- &.pink {
- background: linear-gradient(135deg, #ff9ebd 0%, #ec5f86 100%);
- }
- }
- }
- .period-card {
- padding: 14px 16px;
- background: #f5f8ff;
- border: 1px solid #e1ecff;
- border-radius: 8px;
- .period-empty {
- padding: 26px 0;
- text-align: center;
- font-size: 13px;
- color: #a8abb2;
- }
- .period-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
- .period-title {
- display: flex;
- align-items: center;
- font-size: 15px;
- font-weight: 600;
- color: #303133;
- .el-icon {
- margin-right: 6px;
- color: #409eff;
- }
- }
- .period-tip {
- font-size: 12px;
- color: #a8abb2;
- }
- }
- .period-item {
- padding: 10px 12px;
- background: #fff;
- border-radius: 6px;
- height: 100%;
- box-sizing: border-box;
- .period-label {
- font-size: 12px;
- color: #909399;
- margin-bottom: 6px;
- }
- .period-num {
- font-size: 22px;
- font-weight: 700;
- color: #303133;
- .kpi-unit {
- font-size: 12px;
- font-weight: 400;
- color: #909399;
- }
- &.triple {
- font-size: 19px;
- .sep {
- margin: 0 5px;
- color: #dcdfe6;
- font-weight: 400;
- }
- em {
- font-style: normal;
- &.up {
- color: #67c23a;
- }
- &.down {
- color: #f56c6c;
- }
- &.zero {
- color: #909399;
- }
- }
- }
- }
- }
- }
- .chart-row {
- margin-top: 12px;
- }
- .chart-empty {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 380px;
- }
- .donut-wrap {
- position: relative;
- }
- .donut-center {
- position: absolute;
- top: 46%;
- left: 50%;
- transform: translate(-50%, -50%);
- text-align: center;
- pointer-events: none;
- }
- .donut-total {
- font-size: 26px;
- font-weight: 700;
- color: #303133;
- line-height: 1.2;
- }
- .donut-label {
- margin-top: 2px;
- font-size: 12px;
- color: #909399;
- }
- .panel-title {
- position: relative;
- padding-left: 10px;
- margin-bottom: 10px;
- font-size: 15px;
- font-weight: 600;
- color: #303133;
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 2px;
- width: 4px;
- height: 16px;
- background: #409eff;
- border-radius: 2px;
- }
- .title-tip {
- margin-left: 10px;
- font-size: 12px;
- font-weight: 400;
- color: #a8abb2;
- }
- }
- }
- </style>
|