第四十八节:驾驶舱大屏进阶:数字滚动动画 + 渐变边框美化

第四十八节:驾驶舱大屏进阶:数字滚动动画 + 渐变边框美化

🎯本节目标

  1. 实现统计卡片数字滚动动画(从 0 平滑增长到目标数值)
  2. 给卡片、图表容器增加大屏经典渐变发光边框
  3. 优化图表配色,适配深色大屏风格
  4. 封装数字动画工具函数,可复用

步骤 1:新建工具 src/utils/numberHelper.js

复制代码
// 数字滚动动画
export function animateNumber(targetDom, startNum, endNum, duration = 1500) {
  let startTime = null
  const animate = (timestamp) => {
    if (!startTime) startTime = timestamp
    const progress = timestamp - startTime
    const rate = Math.min(progress / duration, 1)
    const current = Math.floor(startNum + (endNum - startNum) * rate)
    targetDom.innerText = current
    if (rate < 1) {
      requestAnimationFrame(animate)
    }
  }
  requestAnimationFrame(animate)
}

步骤 2:修改 cockpit.vue 完整代码

复制代码
<template>
  <div class="cockpit-wrap">
    <!-- 返回后台按钮 -->
    <el-button
      class="back-btn"
      @click="backAdmin"
    >返回管理后台</el-button>
    <h1 class="title">驾驶舱 · 数据大屏</h1>

    <!-- 顶部指标卡片 -->
    <div class="card-row">
      <div class="stat-card">
        <div class="label">用户总数</div>
        <div class="num" ref="userTotalRef">0</div>
      </div>
      <div class="stat-card">
        <div class="label">资讯总数</div>
        <div class="num" ref="newsTotalRef">0</div>
      </div>
      <div class="stat-card">
        <div class="label">部门数量</div>
        <div class="num" ref="deptTotalRef">0</div>
      </div>
      <div class="stat-card">
        <div class="label">在线用户</div>
        <div class="num" ref="onlineUserRef">0</div>
      </div>
    </div>

    <!-- 图表行 -->
    <div class="chart-row">
      <div class="chart-item">
        <div
          ref="barRef"
          class="chart"></div>
      </div>
      <div class="chart-item">
        <div
          ref="pieRef"
          class="chart"></div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import * as echarts from 'echarts'
import axios from '@/api/request'
import { animateNumber } from '@/utils/numberHelper'

const router = useRouter()
const barRef = ref(null)
const pieRef = ref(null)
// 数字DOM ref
const userTotalRef = ref(null)
const newsTotalRef = ref(null)
const deptTotalRef = ref(null)
const onlineUserRef = ref(null)

let barChart = null
let pieChart = null

// 统计数据
const statData = ref({
  userTotal: 0,
  newsTotal: 0,
  deptTotal: 0,
  onlineUser: 0
})

// 返回后台
const backAdmin = () => {
  router.push('/dashboard/dashboard')
}

// 获取大屏统计接口
const getCockpitStat = async () => {
  try {
    const res = await axios.get('/cockpit/getStat')
    console.log('大屏接口返回:', res)
    if (res.code === 200) {
      statData.value = res.data
      // 启动数字动画
      animateNumber(userTotalRef.value, 0, statData.value.userTotal)
      animateNumber(newsTotalRef.value, 0, statData.value.newsTotal)
      animateNumber(deptTotalRef.value, 0, statData.value.deptTotal)
      animateNumber(onlineUserRef.value, 0, statData.value.onlineUser)
    }
  } catch (err) {
    console.error('大屏接口请求失败', err)
  }
  renderChart()
}

// 渲染图表
const renderChart = () => {
  if(!barRef.value || !pieRef.value) return
  // 柱状图
  barChart = echarts.init(barRef.value)
  barChart.setOption({
    title: { text: '月度用户增长', textStyle: { color: '#fff' } },
    tooltip: {},
    xAxis: {
      type: 'category',
      data: ['1月', '2月', '3月', '4月', '5月', '6月'],
      axisLine: { lineStyle: { color: '#506688' } }
    },
    yAxis: {
      type: 'value',
      axisLine: { lineStyle: { color: '#506688' } },
      splitLine: { lineStyle: { color: '#273b60' } }
    },
    series: [
      {
        data: [80, 120, 95, 160, 210, 240],
        type: 'bar',
        color: '#00bfff'
      }
    ]
  })

  // 饼图
  pieChart = echarts.init(pieRef.value)
  pieChart.setOption({
    title: { text: '部门人员分布', textStyle: { color: '#fff' } },
    tooltip: {},
    series: [
      {
        type: 'pie',
        radius: '60%',
        data: [
          { value: 85, name: '研发部' },
          { value: 42, name: '运营部' },
          { value: 26, name: '市场部' },
          { value: 18, name: '行政部' }
        ]
      }
    ]
  })
}

// 窗口大小变化自适应
const resizeHandler = () => {
  barChart?.resize()
  pieChart?.resize()
}

onMounted(() => {
  getCockpitStat()
  window.addEventListener('resize', resizeHandler)
})

// 销毁释放实例,防止内存泄漏
onUnmounted(() => {
  window.removeEventListener('resize', resizeHandler)
  barChart?.dispose()
  pieChart?.dispose()
})
</script>

<style scoped>
.cockpit-wrap {
  width: 100vw;
  height: 100vh;
  box-sizing: border-box;
  padding: 20px;
  background-color: #091b39;
  color: #fff;
}
.back-btn {
  position: absolute;
  top: 12px;
  right: 20px;
  z-index: 99;
}
.title {
  text-align: center;
  font-size: 28px;
  margin: 10px 0 30px;
}
.card-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  margin-bottom: 20px;
}
.stat-card {
  background: rgba(30, 60, 110, 0.6);
  padding: 20px;
  border-radius: 8px;
  text-align: center;
  /* 渐变边框 */
  border: 1px solid transparent;
  background-image: linear-gradient(rgba(30, 60, 110, 0.6), rgba(30, 60, 110, 0.6)), linear-gradient(135deg,#00bfff,#409eff);
  background-origin: border-box;
  background-clip: padding-box, border-box;
}
.stat-card .label {
  font-size: 16px;
  color: #a0b8e3;
}
.stat-card .num {
  font-size: 32px;
  font-weight: bold;
  margin-top: 8px;
}
.chart-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  height: 480px;
}
.chart-item {
  background: rgba(30, 60, 110, 0.6);
  border-radius: 8px;
  padding: 16px;
  width: 100%;
  height: 100%;
  border: 1px solid transparent;
  background-image: linear-gradient(rgba(30, 60, 110, 0.6), rgba(30, 60, 110, 0.6)), linear-gradient(135deg,#00bfff,#409eff);
  background-origin: border-box;
  background-clip: padding-box, border-box;
}
.chart {
  width:100%;
  height:100%;
}
</style>

✅测试清单

  1. 进入驾驶舱,数字从 0 平滑滚动到目标值
  2. 卡片、图表容器出现淡蓝色渐变边框,大屏质感提升
  3. 图表正常渲染,窗口缩放自适应
  4. 返回后台按钮正常跳转

登录后先进入驾驶舱(登录成功回调 + 路由守卫)

方案 1:修改登录页面 login.vue(登录成功时直接跳驾驶舱,推荐)

找到登录表单 handleLogin 登录成功的代码,原来跳转 /dashboard/dashboard,改成 /cockpit

复制代码
// login.vue
const handleLogin = async () => {
  const res = await loginApi(formData)
  if(res.code === 200) {
    // 保存token
    userStore.setToken(res.data.token)
    // 登录成功,直接跳驾驶舱大屏
    router.push('/cockpit')
  }
}

方案 2:修改路由守卫 router/index.js(刷新页面、访问/根路径也自动跳驾驶舱)

你原来守卫里这一行:

复制代码
if (to.path === '/login') {
  return '/dashboard/dashboard'
}

👉替换成:

复制代码
if (to.path === '/login') {
  return '/cockpit'
}

再修改根路由重定向:

复制代码
export const constantRoutes = [
  // 原来:{ path: '/', redirect: '/login' }
  { path: '/', redirect: '/cockpit' },
  // ✅驾驶舱:独立全屏页面,不套Layout
  {
    path: '/cockpit',
    name: 'Cockpit',
    component: () => import('@/views/cockpit/cockpit.vue'),
    meta: { title: '驾驶舱大屏' }
  },
  { path: '/login', component: () => import('@/views/login/login.vue') },
  { path: '/401', component: () => import('@/views/error/401.vue') },
  { path: '/404', component: () => import('@/views/error/404.vue') },
  { path: '/500', component: () => import('@/views/error/500.vue') }
]

📌完整修改后的路由守卫片段

复制代码
router.beforeEach(async (to, from) => {
  const userStore = useUserStore()
  // 白名单:不需要登录就能访问
  const whiteList = ['/login', '/404', '/401', '/500']

  if (userStore.token) {
    // 有 token,访问登录页 → 直接跳驾驶舱大屏
    if (to.path === '/login') {
      return '/cockpit'
    }

    // roles 为空 = 还没加载用户信息(刚登录 / 刷新页面)
    if (userStore.roles.length === 0) {
      try {
        // 1. 获取用户信息,拿到转换完成的动态路由
        const accessRoutes = await userStore.getUserInfo()

        // 2. 循环注册动态路由
        accessRoutes.forEach((route) => {
          router.addRoute(route)
        })

        // 3. 最后注册 404 通配路由(必须在所有业务路由之后!)
        router.addRoute({
          path: '/:pathMatch(.*)*',
          redirect: '/404'
        })

        // 4. 重新触发导航
        return { ...to, replace: true }
      } catch (err) {
        console.error('加载动态路由失败:', err)
        userStore.logout()
        return '/login'
      }
    }

    // 已经加载过用户信息,直接放行
    return true
  } else {
    // 没有 token
    if (whiteList.includes(to.path)) {
      return true
    } else {
      return '/login'
    }
  }
})

⚠️注意事项

  1. 现在登录成功后直接进入全屏驾驶舱,没有侧边栏、标签栏;页面内保留【返回管理后台】按钮,可以切回工作台
  2. 驾驶舱路由不在 Layout 布局,tagsView 后置守卫已经过滤,不会新增标签
  3. 刷新页面,有 token 时,访问根路径 / 也会自动跳转到驾驶舱

🧪测试清单

  1. 输入账号密码登录,登录成功直接进入驾驶舱大屏
  2. 浏览器地址栏输入 /login,已登录状态会自动跳转驾驶舱
  3. 浏览器直接访问根路径 /,自动跳驾驶舱
  4. 驾驶舱页面【返回管理后台】按钮正常,切回工作台页面
相关推荐
panxianren4 小时前
Bun 1.4 的 bun test --parallel 将 4 秒的测试套件缩短到约 1 秒
java·javascript
陈随易6 小时前
bm2,Node.js 与 Bun 项目部署新选择
前端·后端·程序员
可乐鸡翅yeah_7 小时前
FFmpeg 生成 HLS 独立分片 independent‑segments 参数到底有什么用
javascript·ffmpeg·音视频·safari·m3u8
福兮说7 小时前
canvas 旋转图片的六个坑:四角被裁、Math.ceil 多出 1px 黑边、JPG 角发黑、翻转方向反了
前端·javascript·图像处理·canvas
Ai-_Man9 小时前
您您这可以把Grok的多个会话比如说。左侧的多个会话一次性导出吗?不是单条会话里面的多次会对话。AI导出鸭
javascript·人工智能·ai·小程序·电脑
传人once9 小时前
多图片上传预览功能如何写
前端·css·html·jquery·html5
Eric.469 小时前
Wan‑Animate+MiniMax-H3 本地部署 AI 漫剧流水线:8G 显存动作迁移与动态分镜工程实战
前端·人工智能·comfyui·ai漫剧
liangshanbo121510 小时前
JavaScript 异步核心主线
javascript·事件循环·异步操作
传人once11 小时前
悬停旋转放大和位移效果如何写
javascript·css·动画
库拉镜像AI牛牛11 小时前
短剧内容自动化生产:知漫剧工作室落地教程
大数据·服务器·前端·人工智能·语音识别