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

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

🎯本节目标

  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. 驾驶舱页面【返回管理后台】按钮正常,切回工作台页面
相关推荐
ly76891 小时前
Spring Boot 3.2 虚拟线程在 Web 层与 JDBC 池的实践边界
前端·spring boot·后端·tomcat·jdbc·虚拟线程
Bingo_BIG1 小时前
LOV列表的查询
前端·javascript·lov
小静AI工程实验室1 小时前
JS 逆向接口 ID 变了?Python 与 Node.js 复现 JSON 大整数精度丢失
javascript·python·node.js
QQ_21696290961 小时前
基于SpringBoot+Vue的小生活平台的设计与实现
java·数据库·vue.js·spring boot·spring·微信小程序·生活
Setsuna_F_Seiei8 小时前
前端转型 Agent 开发 04 之 MCP 与 Skill(赋予 Agent 更广工作能力)
前端·agent
刘发财10 小时前
前端2秒生成500页矢量PDF,rust真的强到没朋友
前端·javascript·rust
郑州光合科技余经理10 小时前
国际版外卖系统:税率字段怎么和订单主流程解耦
android·java·开发语言·前端·后端·php·ai编程
梦想平凡11 小时前
百游棋牌源代码开发搭建教程(十):隔离部署、备份恢复与双端验收
java·前端·javascript·数据库·源代码管理
yume_sibai12 小时前
06-Rust Web 开发实战(Axum 框架 + 数据库 + JWT 认证 + 中间件 + 部署)
前端·数据库·rust