| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- <template>
- <div class="panel equipment-stats">
- <div class="border-beam"></div>
- <div class="panel-header">
- <div class="panel-header-icon">🔬</div>
- <span class="panel-title">实验室设备分类及使用统计</span>
- </div>
- <!-- 4:6 vertical layout -->
- <div class="equip-layout">
- <!-- Top flex:4: ring chart - device classification -->
- <div class="equip-section equip-top">
- <div ref="ringChart" class="ring-chart"></div>
- </div>
- <!-- Bottom flex:6: stats row + two pie charts -->
- <div class="equip-section equip-bottom">
- <!-- 3 stat items -->
- <div class="equip-mid-row">
- <div class="equip-stat-item">
- <div class="ev">{{ totalEquipment.toLocaleString() }}</div>
- <div class="el">设备总数(台)</div>
- </div>
- <div class="equip-stat-item">
- <div class="ev">{{ totalHours.toLocaleString() }}</div>
- <div class="el">使用时长(h)</div>
- </div>
- <div class="equip-stat-item">
- <div class="ev">{{ usageRate }}%</div>
- <div class="el">设备使用率</div>
- </div>
- </div>
- <!-- Two pie charts side by side -->
- <div class="equip-pie-row">
- <div class="equip-pie-col">
- <div class="pie-col-title">设备状态统计</div>
- <div ref="pieStatusChart" class="pie-chart"></div>
- </div>
- <div class="equip-pie-col">
- <div class="pie-col-title">使用状态统计</div>
- <div ref="pieUsageChart" class="pie-chart"></div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script>
- import * as echarts from 'echarts'
- import { getDeviceCategoryStat } from '@/api/screen'
- const TOOLTIP_CFG = {
- backgroundColor: 'rgba(3,14,42,0.92)',
- borderColor: 'rgba(30,144,255,0.3)',
- textStyle: { color: '#a8cce8', fontSize: 28 }
- }
- const CATEGORY_COLORS = ['#1e90ff', '#4361ee', '#00e676', '#ffd740', '#00e5c8', '#f97316', '#e040fb', '#ff5252']
- export default {
- name: 'EquipmentStats',
- data() {
- return {
- totalEquipment: 0,
- totalHours: 0,
- usageRate: 0,
- categories: [],
- deviceStatus: [],
- usageStatus: [],
- ringChart: null,
- pieStatusChart: null,
- pieUsageChart: null,
- pollTimer: null
- }
- },
- mounted() {
- this.fetchData()
- this.pollTimer = setInterval(this.fetchData, 5 * 60 * 1000)
- },
- beforeDestroy() {
- if (this.pollTimer) clearInterval(this.pollTimer)
- if (this.ringChart) this.ringChart.dispose()
- if (this.pieStatusChart) this.pieStatusChart.dispose()
- if (this.pieUsageChart) this.pieUsageChart.dispose()
- window.removeEventListener('resize', this.handleResize)
- },
- methods: {
- async fetchData() {
- try {
- const res = await getDeviceCategoryStat()
- if (res.code === 200) {
- const d = res.data
- this.totalEquipment = d.totalCount
- this.totalHours = d.totalUsageHours
- this.usageRate = d.usageRate
- this.categories = (d.categoryList || []).map((c, i) => ({
- value: c.totalCount,
- name: c.categoryOneName,
- color: CATEGORY_COLORS[i % CATEGORY_COLORS.length]
- }))
- const STATUS_COLORS = { '正常': '#00e676', '维修': '#f59e0b', '报废': '#ef4444' }
- const USAGE_COLORS = { '使用': '#1e90ff', '空闲': '#00e676' }
- this.deviceStatus = (d.deviceStatusList || []).map(s => ({
- value: s.count, name: s.name,
- color: STATUS_COLORS[s.name] || '#a8cce8'
- }))
- this.usageStatus = (d.usageStatusList || []).map(s => ({
- value: s.count, name: s.name,
- color: USAGE_COLORS[s.name] || '#a8cce8'
- }))
- }
- } catch (e) {
- // 保持默认值
- }
- if (this.ringChart) { this.ringChart.dispose(); this.ringChart = null }
- if (this.pieStatusChart) { this.pieStatusChart.dispose(); this.pieStatusChart = null }
- if (this.pieUsageChart) { this.pieUsageChart.dispose(); this.pieUsageChart = null }
- this.$nextTick(() => {
- this.initRingChart()
- this.initPieStatusChart()
- this.initPieUsageChart()
- window.addEventListener('resize', this.handleResize)
- })
- },
- handleResize() {
- if (this.ringChart) this.ringChart.resize()
- if (this.pieStatusChart) this.pieStatusChart.resize()
- if (this.pieUsageChart) this.pieUsageChart.resize()
- },
- /** 初始化环形图 - 设备分类 */
- initRingChart() {
- this.ringChart = echarts.init(this.$refs.ringChart, null, { renderer: 'canvas', devicePixelRatio: 2 })
- // Build count map for legend formatter
- const countMap = {}
- this.categories.forEach(c => { countMap[c.name] = c.value })
- const ringData = this.categories.map(c => ({
- value: c.value,
- name: c.name,
- itemStyle: {
- color: c.color,
- shadowBlur: 8,
- shadowColor: c.color.replace(')', ',0.5)').replace('rgb', 'rgba')
- }
- }))
- const option = {
- backgroundColor: 'transparent',
- tooltip: {
- trigger: 'item',
- formatter: '{b}: {c}台 ({d}%)',
- ...TOOLTIP_CFG
- },
- legend: {
- orient: 'vertical',
- right: '1%',
- top: 'middle',
- icon: 'circle',
- itemWidth: 28,
- itemHeight: 28,
- itemGap: 22,
- textStyle: { color: '#a8cce8', fontSize: 22 },
- formatter: function(name) {
- return name + ' ' + countMap[name] + '台'
- },
- rich: {
- nm: { fontSize: 22, color: '#a8cce8', width: 90 },
- vl: { fontSize: 26, fontWeight: 700, color: '#fff' }
- }
- },
- series: [{
- type: 'pie',
- radius: ['38%', '62%'],
- center: ['36%', '50%'],
- itemStyle: {
- borderRadius: 4,
- borderColor: 'rgba(3,14,31,0.5)',
- borderWidth: 2
- },
- label: {
- show: true,
- formatter: '{c}台',
- fontSize: 20,
- color: '#a8cce8'
- },
- labelLine: {
- length: 10,
- length2: 10,
- lineStyle: { color: 'rgba(30,144,255,0.4)', width: 2 }
- },
- data: ringData,
- emphasis: {
- scale: true,
- scaleSize: 5,
- itemStyle: {
- shadowBlur: 20,
- shadowColor: 'rgba(30,144,255,0.6)'
- }
- }
- }]
- }
- this.ringChart.setOption(option)
- },
- /** 初始化饼图 - 设备状态统计 */
- initPieStatusChart() {
- this.pieStatusChart = echarts.init(this.$refs.pieStatusChart, null, { renderer: 'canvas', devicePixelRatio: 2 })
- this.pieStatusChart.setOption(this._buildPieOption(this.deviceStatus))
- },
- /** 初始化饼图 - 使用状态统计 */
- initPieUsageChart() {
- this.pieUsageChart = echarts.init(this.$refs.pieUsageChart, null, { renderer: 'canvas', devicePixelRatio: 2 })
- this.pieUsageChart.setOption(this._buildPieOption(this.usageStatus))
- },
- _buildPieOption(list) {
- const countMap = {}
- list.forEach(s => { countMap[s.name] = s.value })
- const pieData = list.map(s => ({
- value: s.value, name: s.name,
- itemStyle: { color: s.color, shadowBlur: 10, shadowColor: s.color }
- }))
- return {
- backgroundColor: 'transparent',
- tooltip: { trigger: 'item', formatter: '{b}: {c}台 ({d}%)', ...TOOLTIP_CFG },
- legend: {
- show:false,
- orient: 'vertical', right: '2%', top: 'middle',
- icon: 'circle', itemWidth: 28, itemHeight: 28, itemGap: 28,
- textStyle: { color: '#a8cce8', fontSize: 22 },
- formatter: name => name + ' ' + countMap[name] + '台'
- },
- series: [{
- type: 'pie', radius: ['36%', '62%'], center: ['38%', '52%'],
- itemStyle: { borderRadius: 5, borderColor: 'rgba(3,14,31,0.5)', borderWidth: 2 },
- label: { show: true, formatter: '{b}\n{c}台', fontSize: 20, color: '#a8cce8', lineHeight: 30 },
- labelLine: { length: 24, length2: 40, lineStyle: { color: 'rgba(30,144,255,0.4)', width: 2 } },
- data: pieData,
- emphasis: { scale: true, scaleSize: 6, itemStyle: { shadowBlur: 25, shadowColor: 'rgba(30,144,255,0.6)' } }
- }]
- }
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .equipment-stats {
- position: relative;
- display: flex;
- flex-direction: column;
- height:1150px;
- }
- /* ---- 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%; } }
- /* ---- 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;
- }
- /* ---- 4:2:4 Layout ---- */
- .equip-layout {
- flex: 1;
- display: flex;
- flex-direction: column;
- padding: 15px 25px;
- gap: 12px;
- min-height: 0;
- }
- .equip-section {
- min-height: 0;
- }
- .equip-top {
- flex: 4;
- }
- .equip-bottom {
- flex: 6;
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .equip-pie-row {
- flex: 1;
- min-height: 0;
- display: flex;
- gap: 12px;
- }
- .equip-pie-col {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- gap: 6px;
- .pie-col-title {
- text-align: center;
- font-size: 22px;
- font-weight: 600;
- letter-spacing: 3px;
- color: $cyan;
- padding: 5px 0 7px;
- border-bottom: 1px solid rgba(0, 216, 255, 0.22);
- text-shadow: 0 0 10px rgba(0, 216, 255, 0.45);
- flex-shrink: 0;
- }
- }
- .ring-chart,
- .pie-chart {
- width: 100%;
- height: 100%;
- }
- /* ---- Middle stat row ---- */
- .equip-mid-row {
- display: flex;
- gap: 15px;
- width: 100%;
- }
- .equip-stat-item {
- flex: 1;
- text-align: center;
- padding: 15px 10px;
- border-radius: 10px;
- background: $bg-card;
- border: 1px solid $border;
- .ev {
- font-size: 38px;
- font-weight: 700;
- color: $gold;
- }
- .el {
- font-size: 22px;
- color: $text-dim;
- margin-top: 5px;
- }
- }
- </style>
|