| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- <template>
- <div class="panel personnel-trend">
- <div class="border-beam"></div>
- <div class="panel-header">
- <div class="panel-header-icon">👥</div>
- <span class="panel-title">实验室进入人数统计及走势</span>
- </div>
- <div class="panel-body">
- <!-- Flip counter digits -->
- <div class="flip-counters">
- <div class="flip-counter">
- <div class="fc-label">今日进入总人数</div>
- <div class="fc-digits">
- <div
- v-for="(digit, idx) in totalDigits"
- :key="'total-' + idx"
- class="flip-digit"
- >{{ digit }}</div>
- </div>
- </div>
- <!-- 等待后台老师数据相关-开始 -->
- <!--<div class="flip-counter">-->
- <!--<div class="fc-label">当前在场实验人数</div>-->
- <!--<div class="fc-digits">-->
- <!--<div-->
- <!--v-for="(digit, idx) in presentDigits"-->
- <!--:key="'present-' + idx"-->
- <!--class="flip-digit"-->
- <!-->{{ digit }}</div>-->
- <!--</div>-->
- <!--</div>-->
- <!-- 等待后台老师数据相关-结束 -->
- </div>
- <!-- Line chart -->
- <div class="chart-wrap">
- <div ref="chart" class="chart"></div>
- </div>
- </div>
- </div>
- </template>
- <script>
- import * as echarts from 'echarts'
- import { getPassOutTrend } from '@/api/screen'
- export default {
- name: 'PersonnelTrend',
- data() {
- return {
- totalEntry: 0,
- currentPresent: 0,
- displayTotal: 0,
- displayPresent: 0,
- chart: null,
- animTimerTotal: null,
- animTimerPresent: null,
- pollTimer: null
- }
- },
- computed: {
- totalDigits() {
- return String(this.displayTotal).padStart(4, '0').split('')
- },
- presentDigits() {
- return String(this.displayPresent).padStart(4, '0').split('')
- }
- },
- mounted() {
- this.fetchData()
- this.pollTimer = setInterval(this.fetchData, 5 * 60 * 1000)
- },
- beforeDestroy() {
- if (this.pollTimer) clearInterval(this.pollTimer)
- if (this.animTimerTotal) cancelAnimationFrame(this.animTimerTotal)
- if (this.animTimerPresent) cancelAnimationFrame(this.animTimerPresent)
- if (this.chart) {
- this.chart.dispose()
- this.chart = null
- }
- },
- methods: {
- async fetchData() {
- try {
- const res = await getPassOutTrend()
- if (res.code === 200) {
- const d = res.data
- this.totalEntry = d.todayEntryCount
- this.currentPresent = d.currentLabUserCount
- const hours = Array.from({ length: 24 }, (_, i) => i)
- const entryMap = {}
- const presentMap = {}
- ;(d.entryTrend || []).forEach(item => { entryMap[item.hour] = item.count })
- ;(d.labUserTrend || []).forEach(item => { presentMap[item.hour] = item.count })
- const trend = {
- times: hours.map(h => `${String(h).padStart(2, '0')}:00`),
- entry: hours.map(h => entryMap[h] || 0),
- present: hours.map(h => presentMap[h] || 0)
- }
- this.$nextTick(() => {
- setTimeout(() => this.animateCount('total', 0, this.totalEntry, 2000), 600)
- setTimeout(() => this.animateCount('present', 0, this.currentPresent, 1500), 900)
- if (this.chart) {
- this.chart.dispose()
- this.chart = null
- }
- this.initChart(trend)
- })
- }
- } catch (e) {
- // 错误已由拦截器处理
- }
- },
- /** Animate count from `from` to `to` over `duration` ms */
- animateCount(type, from, to, duration) {
- const startTime = Date.now()
- const tick = () => {
- const elapsed = Date.now() - startTime
- const progress = Math.min(elapsed / duration, 1)
- const current = Math.round(from + (to - from) * progress)
- if (type === 'total') {
- this.displayTotal = current
- } else {
- this.displayPresent = current
- }
- if (progress < 1) {
- if (type === 'total') {
- this.animTimerTotal = requestAnimationFrame(tick)
- } else {
- this.animTimerPresent = requestAnimationFrame(tick)
- }
- }
- }
- tick()
- },
- /** 初始化面积折线图 - 展示24小时进入/在场人数走势 */
- initChart(trend) {
- this.chart = echarts.init(this.$refs.chart, null, {
- renderer: 'canvas',
- devicePixelRatio: 2
- })
- const TOOLTIP_CFG = {
- backgroundColor: 'rgba(3,14,42,0.92)',
- borderColor: 'rgba(30,144,255,0.3)',
- textStyle: { color: '#a8cce8', fontSize: 28 }
- }
- const option = {
- backgroundColor: 'transparent',
- tooltip: {
- trigger: 'axis',
- ...TOOLTIP_CFG
- },
- legend: {
- data: ['进入人数', '在场人数'],
- top: 0,
- right: 0,
- textStyle: { color: '#a8cce8', fontSize: 25 },
- icon: 'circle',
- itemWidth: 20,
- itemHeight: 20
- },
- grid: {
- left: 28,
- right: 8,
- top: 45,
- bottom: 50,
- containLabel: true
- },
- xAxis: {
- type: 'category',
- data: trend.times,
- axisLabel: { color: '#5890b8', fontSize: 22 },
- axisLine: { lineStyle: { color: 'rgba(30,144,255,0.2)' } },
- axisTick: { show: false }
- },
- yAxis: {
- type: 'value',
- axisLabel: { color: '#5890b8', fontSize: 22 },
- axisLine: { show: false },
- splitLine: { lineStyle: { color: 'rgba(30,144,255,0.1)' } }
- },
- series: [
- {
- name: '进入人数',
- type: 'line',
- data: trend.entry,
- smooth: true,
- symbol: 'circle',
- symbolSize: 12,
- lineStyle: { color: '#1e90ff', width: 5 },
- itemStyle: { color: '#1e90ff' },
- areaStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: 'rgba(30,144,255,0.32)' },
- { offset: 1, color: 'rgba(30,144,255,0.02)' }
- ])
- }
- },
- // 等待后台老师数据相关-开始
- // {
- // name: '在场人数',
- // type: 'line',
- // data: trend.present,
- // smooth: true,
- // symbol: 'circle',
- // symbolSize: 12,
- // lineStyle: { color: '#ffd740', width: 5 },
- // itemStyle: { color: '#ffd740' },
- // areaStyle: {
- // color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- // { offset: 0, color: 'rgba(255,215,64,0.22)' },
- // { offset: 1, color: 'rgba(255,215,64,0.01)' }
- // ])
- // }
- // }
- // 等待后台老师数据相关-结束
- ]
- }
- this.chart.setOption(option)
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .personnel-trend {
- display: flex;
- flex-direction: column;
- }
- .panel-body {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- padding: 20px 25px;
- }
- // Flip counters section
- .flip-counters {
- display: flex;
- gap: 25px;
- }
- .flip-counter {
- flex: 1;
- text-align: center;
- padding: 18px 15px;
- border-radius: 10px;
- background: $bg-card;
- border: 1px solid $border;
- .fc-label {
- font-size: 25px;
- color: $text-dim;
- margin-bottom: 12px;
- letter-spacing: 2px;
- }
- .fc-digits {
- display: flex;
- gap: 8px;
- justify-content: center;
- }
- }
- .flip-digit {
- width: 50px;
- height: 75px;
- border-radius: 8px;
- background: linear-gradient(180deg, rgba(4, 18, 55, 0.9), rgba(2, 10, 32, 0.95));
- border: 1px solid rgba(30, 144, 255, 0.3);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 45px;
- font-weight: 700;
- color: $gold;
- font-variant-numeric: tabular-nums;
- position: relative;
- overflow: hidden;
- &::after {
- content: '';
- position: absolute;
- left: 0;
- right: 0;
- top: 50%;
- height: 2px;
- background: rgba(0, 0, 0, 0.4);
- }
- }
- // Chart area
- .chart-wrap {
- flex: 1;
- min-height: 0;
- margin-top: 12px;
- .chart {
- width: 100%;
- height: 100%;
- }
- }
- </style>
|