| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- <template>
- <div class="panel lab-stats">
- <!-- Border beam animation -->
- <div class="border-beam"></div>
- <!-- Corner ornaments -->
- <svg class="pc tl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc tr" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc bl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <svg class="pc br" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
- <!-- Panel header -->
- <div class="panel-header">
- <div class="panel-header-icon">🏛️</div>
- <span class="panel-title">实验室基本情况统计</span>
- <div class="status-dot green"></div>
- </div>
- <!-- Upper section: left gauge + right donut -->
- <div class="upper-section">
- <!-- Left: SVG gauge showing total -->
- <div class="gauge-side">
- <svg width="320" height="320" viewBox="0 0 320 320">
- <defs>
- <linearGradient id="arcG1" x1="0%" y1="0%" x2="100%" y2="100%">
- <stop offset="0%" stop-color="#1e90ff"/>
- <stop offset="100%" stop-color="#00d8ff"/>
- </linearGradient>
- <filter id="glow1">
- <feGaussianBlur stdDeviation="4" result="blur"/>
- <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
- </filter>
- </defs>
- <!-- Outermost decorative ring -->
- <circle cx="160" cy="160" r="148" fill="none" stroke="rgba(30,144,255,0.08)" stroke-width="1" stroke-dasharray="6 6"/>
- <!-- Outer track -->
- <circle cx="160" cy="160" r="136" fill="none" stroke="rgba(30,144,255,0.14)" stroke-width="3"/>
- <!-- Main arc 300deg -->
- <circle cx="160" cy="160" r="136" fill="none" stroke="url(#arcG1)" stroke-width="8"
- stroke-dasharray="711 143" stroke-linecap="round" transform="rotate(-240 160 160)" filter="url(#glow1)"/>
- <!-- Inner track -->
- <circle cx="160" cy="160" r="110" fill="rgba(3,14,31,0.7)" stroke="rgba(0,216,255,0.15)" stroke-width="2"/>
- <!-- Four-level color arcs -->
- <circle cx="160" cy="160" r="88" fill="none" stroke="#cc0000" stroke-width="12"
- :stroke-dasharray="levelArcs[0].dash" stroke-linecap="round" transform="rotate(-90 160 160)" opacity="0.9"/>
- <circle cx="160" cy="160" r="88" fill="none" stroke="#ff8000" stroke-width="12"
- :stroke-dasharray="levelArcs[1].dash" stroke-linecap="round" :transform="'rotate(' + levelArcs[1].rotate + ' 160 160)'" opacity="0.9"/>
- <circle cx="160" cy="160" r="88" fill="none" stroke="#ffcc00" stroke-width="12"
- :stroke-dasharray="levelArcs[2].dash" stroke-linecap="round" :transform="'rotate(' + levelArcs[2].rotate + ' 160 160)'" opacity="0.9"/>
- <circle cx="160" cy="160" r="88" fill="none" stroke="#0066cc" stroke-width="12"
- :stroke-dasharray="levelArcs[3].dash" stroke-linecap="round" :transform="'rotate(' + levelArcs[3].rotate + ' 160 160)'" opacity="0.9"/>
- <!-- Endpoint glow -->
- <circle cx="160" cy="24" r="6" fill="#1e90ff" opacity="0.9"/>
- <!-- Center number -->
- <text x="160" y="148" text-anchor="middle" fill="#ffd740" font-size="46" font-weight="900" font-family="Arial,sans-serif" letter-spacing="-2">{{ total }}</text>
- <text x="160" y="178" text-anchor="middle" fill="rgba(168,204,232,0.75)" font-size="20" font-family="Arial,sans-serif" letter-spacing="4">间</text>
- </svg>
- <!-- Labels -->
- <div class="gauge-label">
- <div class="gauge-label-main">实验室总数</div>
- <div class="gauge-label-sub">TOTAL LABORATORIES</div>
- </div>
- <!-- Color bar showing proportions -->
- <div class="color-bar-wrap">
- <div class="color-bar">
- <div v-for="level in levels" :key="level.name" :style="{ flex: level.value, background: level.color, boxShadow: '0 0 10px ' + level.color + '80' }"></div>
- </div>
- <div class="color-bar-labels">
- <span v-for="level in levels" :key="'lbl-' + level.name" :style="{ flex: level.value }">{{ level.name.replace('级', '') }}</span>
- </div>
- </div>
- </div>
- <!-- Right: ECharts donut + level detail list -->
- <div class="donut-side">
- <div ref="donutChart" class="donut-chart"></div>
- <div class="level-list">
- <div
- v-for="level in levels"
- :key="'detail-' + level.name"
- class="level-item"
- :style="{ background: level.color + '12', borderLeftColor: level.color }"
- >
- <span class="level-item-left">
- <span class="level-dot" :style="{ background: level.color, boxShadow: '0 0 8px ' + level.color + 'cc' }"></span>
- {{ level.label }}
- </span>
- <span class="level-item-val" :style="{ color: levelValColor(level.color) }">{{ level.value }}间</span>
- </div>
- </div>
- </div>
- </div>
- <!-- Lower section: 3 status badges -->
- <div class="lower-section">
- <div class="status-row">
- <div class="status-badge active">
- <div class="val">{{ status.active }}</div>
- <div class="lbl">使用(间)</div>
- </div>
- <div class="status-badge warning">
- <div class="val">{{ status.warning }}</div>
- <div class="lbl">异常(间)</div>
- </div>
- <div class="status-badge idle">
- <div class="val">{{ status.idle }}</div>
- <div class="lbl">空闲(间)</div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script>
- import * as echarts from 'echarts'
- import { getLabBasicStatistics,getLevelTitleList } from '@/api/screen'
- export default {
- name: 'EventStats',
- data() {
- return {
- total: 0,
- levels: [],
- status: { active: 0, warning: 0, idle: 0 },
- chart: null,
- timer: null
- }
- },
- computed: {
- /** 计算SVG仪表盘中四个分级色弧的dasharray和旋转角度 */
- levelArcs() {
- if (!this.levels.length) {
- return [
- { dash: '0 553', rotate: -90 },
- { dash: '0 553', rotate: -90 },
- { dash: '0 553', rotate: -90 },
- { dash: '0 553', rotate: -90 }
- ]
- }
- const circumference = 2 * Math.PI * 88 // ~553
- const totalVal = this.levels.reduce((s, l) => s + l.value, 0)
- const arcs = []
- let accAngle = -90 // start at top
- for (let i = 0; i < this.levels.length; i++) {
- const ratio = this.levels[i].value / totalVal
- const arcLen = Math.round(ratio * circumference)
- const gapLen = Math.round(circumference - arcLen)
- arcs.push({
- dash: arcLen + ' ' + gapLen,
- rotate: Math.round(accAngle)
- })
- accAngle += ratio * 360
- }
- return arcs
- }
- },
- mounted() {
- this.getLevelTitleList()
- this.timer = setInterval(this.fetchData, 5 * 60 * 1000)
- },
- beforeDestroy() {
- if (this.timer) clearInterval(this.timer)
- if (this.chart) {
- this.chart.dispose()
- this.chart = null
- }
- },
- methods: {
- async getLevelTitleList() {
- const res = await getLevelTitleList()
- if (res.code === 200) {
- let list = [];
- for(let i=0;i<res.data.length;i++){
- list.push({
- levelGrade: res.data[i].levelGrade,
- name: res.data[i].titleName,
- label: res.data[i].showTitleName,
- value: '',
- color: res.data[i].titleColor
- })
- }
- this.$set(this,'levels',list);
- this.fetchData();
- }
- },
- async fetchData() {
- let self = this;
- try {
- const res = await getLabBasicStatistics()
- if (res.code === 200) {
- const d = res.data
- this.total = d.labTotal
- let list = JSON.parse(JSON.stringify(self.levels));
- for(let i=0;i<list.length;i++){
- if(list[i].levelGrade == 1){
- list[i].value = d.levelOneCount;
- }else if(list[i].levelGrade == 2){
- list[i].value = d.levelTwoCount;
- }else if(list[i].levelGrade == 3){
- list[i].value = d.levelThreeCount;
- }else if(list[i].levelGrade == 4){
- list[i].value = d.levelFourCount;
- }
- }
- this.$set(this,'levels',list);
- this.status = {
- active: d.useTotal,
- warning: d.exceptionalTotal,
- idle: d.availableTotal
- }
- this.$nextTick(() => this.initDonutChart())
- }
- } catch (e) {
- // 错误已由拦截器处理
- }
- },
- /** 根据分级主色计算稍亮的数值展示色 */
- levelValColor(color) {
- const map = {
- '#cc0000': '#ff6666',
- '#ff8000': '#ffaa44',
- '#ffcc00': '#ffe066',
- '#0066cc': '#4499ff'
- }
- return map[color] || color
- },
- /** 初始化ECharts环形图 - 展示四级分类占比,中心显示总数 */
- initDonutChart() {
- this.chart = echarts.init(this.$refs.donutChart, null, {
- renderer: 'canvas',
- devicePixelRatio: 2
- })
- const option = {
- backgroundColor: 'transparent',
- tooltip: {
- trigger: 'item',
- formatter: '{b}: {c}间 ({d}%)',
- backgroundColor: 'rgba(3,14,42,0.92)',
- borderColor: 'rgba(30,144,255,0.3)',
- textStyle: { color: '#a8cce8', fontSize: 13 }
- },
- graphic: [
- {
- type: 'text',
- left: 'center',
- top: '38%',
- style: {
- text: String(this.total),
- fill: '#ffd740',
- font: 'bold 38px Arial',
- textAlign: 'center'
- }
- },
- {
- type: 'text',
- left: 'center',
- top: '56%',
- style: {
- text: '实验室总数(间)',
- fill: 'rgba(168,204,232,0.65)',
- font: '13px Arial',
- textAlign: 'center'
- }
- }
- ],
- series: [{
- type: 'pie',
- radius: ['42%', '68%'],
- center: ['50%', '50%'],
- startAngle: 100,
- itemStyle: {
- borderRadius: 6,
- borderColor: 'rgba(3,14,31,0.6)',
- borderWidth: 3
- },
- label: {
- show: true,
- formatter: function (params) {
- return '{lvl|' + params.name + '}\n{cnt|' + params.value + '间}\n{pct|' + params.percent + '%}'
- },
- rich: {
- lvl: { fontSize: 13, fontWeight: 700, color: '#ddf0ff', lineHeight: 22 },
- cnt: { fontSize: 16, fontWeight: 900, color: '#ffd740', lineHeight: 26 },
- pct: { fontSize: 12, color: 'rgba(168,204,232,0.6)', lineHeight: 20 }
- },
- distanceToLabelLine: 8
- },
- labelLine: {
- show: true,
- length: 20,
- length2: 25,
- lineStyle: { color: 'rgba(30,144,255,0.45)', width: 2 }
- },
- data: this.levels.map(function (l) {
- return {
- value: l.value,
- name: l.name,
- itemStyle: {
- color: l.color,
- shadowBlur: 12,
- shadowColor: l.color + '80'
- }
- }
- }),
- emphasis: {
- scale: true,
- scaleSize: 8,
- itemStyle: { shadowBlur: 30, shadowColor: 'rgba(30,144,255,0.6)' },
- label: { fontSize: 14 }
- }
- }]
- }
- this.chart.setOption(option)
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .lab-stats {
- display: flex;
- flex-direction: column;
- position: relative;
- border-radius: 15px;
- overflow: hidden;
- background: $bg-panel;
- border: 1px solid $border;
- }
- .lab-stats::before {
- content: '';
- position: absolute;
- inset: 0;
- pointer-events: none;
- border-radius: inherit;
- background: linear-gradient(135deg, rgba(30, 144, 255, 0.05) 0%, transparent 50%, rgba(0, 216, 255, 0.03) 100%);
- }
- /* ---- Border beam ---- */
- .border-beam {
- position: absolute;
- inset: 0;
- pointer-events: none;
- border-radius: inherit;
- overflow: hidden;
- z-index: 2;
- &::before {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 40%;
- height: 3px;
- background: linear-gradient(90deg, transparent, rgba(30, 144, 255, 0.9), rgba(0, 216, 255, 0.7), transparent);
- animation: beamTop 5s linear infinite;
- }
- &::after {
- content: '';
- position: absolute;
- bottom: 0;
- right: -100%;
- width: 40%;
- height: 3px;
- background: linear-gradient(90deg, transparent, rgba(0, 216, 255, 0.7), rgba(30, 144, 255, 0.9), transparent);
- animation: beamBottom 5s linear infinite 2.5s;
- }
- }
- @keyframes beamTop { from { left: -40%; } to { left: 100%; } }
- @keyframes beamBottom { from { right: -40%; } to { right: 100%; } }
- /* ---- Corner ornaments ---- */
- .pc {
- position: absolute;
- width: 35px;
- height: 35px;
- z-index: 3;
- pointer-events: none;
- &.tl { top: 0; left: 0; }
- &.tr { top: 0; right: 0; transform: scaleX(-1); }
- &.bl { bottom: 0; left: 0; transform: scaleY(-1); }
- &.br { bottom: 0; right: 0; transform: scale(-1); }
- }
- /* ---- Panel header ---- */
- .panel-header {
- display: flex;
- align-items: center;
- gap: 25px;
- padding: 20px 30px 18px;
- border-bottom: 1px solid $border;
- background: linear-gradient(90deg, rgba(0, 60, 160, 0.18), transparent);
- flex-shrink: 0;
- }
- .panel-header-icon {
- width: 65px;
- height: 65px;
- border-radius: 12px;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 32px;
- background: linear-gradient(135deg, rgba(30, 144, 255, 0.25), rgba(0, 216, 255, 0.15));
- border: 1px solid rgba(30, 144, 255, 0.35);
- animation: iconGlow 3s ease-in-out infinite;
- }
- @keyframes iconGlow {
- 0%, 100% { box-shadow: 0 0 15px rgba(30, 144, 255, 0.3); }
- 50% { box-shadow: 0 0 35px rgba(30, 144, 255, 0.7), 0 0 12px rgba(0, 216, 255, 0.3); }
- }
- .panel-title {
- font-size: 30px;
- font-weight: 600;
- letter-spacing: 2px;
- color: $cyan;
- }
- /* ---- Status dot ---- */
- .status-dot {
- width: 20px;
- height: 20px;
- border-radius: 50%;
- display: inline-block;
- margin-left: auto;
- animation: dotPulse 2s ease-in-out infinite;
- &.green {
- background: $green;
- box-shadow: 0 0 15px $green;
- }
- }
- @keyframes dotPulse {
- 0%, 100% { transform: scale(1); opacity: 1; }
- 50% { transform: scale(1.3); opacity: 0.7; }
- }
- /* ---- Upper section ---- */
- .upper-section {
- flex: 4;
- display: flex;
- min-height: 0;
- overflow: hidden;
- }
- /* Left gauge side */
- .gauge-side {
- flex: 4;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 18px;
- padding: 25px 20px;
- border-right: 1px solid $border;
- background: linear-gradient(180deg, rgba(30, 144, 255, 0.04), transparent);
- }
- .gauge-label {
- text-align: center;
- &-main {
- font-size: 30px;
- font-weight: 700;
- color: $cyan;
- letter-spacing: 4px;
- }
- &-sub {
- font-size: 22px;
- color: $text-dim;
- margin-top: 6px;
- letter-spacing: 2px;
- }
- }
- .color-bar-wrap {
- width: 100%;
- padding: 0 10px;
- }
- .color-bar {
- display: flex;
- gap: 5px;
- height: 12px;
- border-radius: 6px;
- overflow: hidden;
- }
- .color-bar-labels {
- display: flex;
- gap: 5px;
- margin-top: 8px;
- font-size: 22px;
- color: $text-dim;
- text-align: center;
- }
- /* Right donut side */
- .donut-side {
- flex: 6;
- display: flex;
- flex-direction: column;
- justify-content: center;
- padding: 25px 25px 20px;
- gap: 0;
- }
- .donut-chart {
- flex: 0 0 340px;
- width: 100%;
- }
- /* ---- Level detail list ---- */
- .level-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
- margin-top: 10px;
- }
- .level-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 26px;
- padding: 10px 16px;
- border-radius: 8px;
- border-left: 4px solid transparent;
- &-left {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- &-val {
- font-weight: 700;
- font-size: 28px;
- }
- }
- .level-dot {
- width: 16px;
- height: 16px;
- border-radius: 3px;
- display: inline-block;
- }
- /* ---- Lower section ---- */
- .lower-section {
- flex: 1;
- display: flex;
- align-items: center;
- padding: 0 25px 20px;
- border-top: 1px solid $border;
- gap: 15px;
- min-height: 0;
- }
- .status-row {
- display: flex;
- gap: 15px;
- width: 100%;
- }
- .status-badge {
- flex: 1;
- padding: 15px 10px;
- border-radius: 10px;
- text-align: center;
- background: $bg-card;
- border: 1px solid $border;
- &.active {
- border-color: $green;
- background: rgba(0, 230, 118, 0.07);
- .val { color: $green; }
- }
- &.warning {
- border-color: #f59e0b;
- background: rgba(245, 158, 11, 0.07);
- .val { color: #f59e0b; }
- }
- &.idle {
- border-color: $indigo;
- background: rgba(67, 97, 238, 0.07);
- .val { color: $indigo; }
- }
- .val {
- font-size: 48px;
- font-weight: 700;
- }
- .lbl {
- font-size: 25px;
- color: $text-dim;
- margin-top: 5px;
- }
- }
- </style>
|