目录
src/views/dashboard/DataScreenView.vue
[2、通用面板(40px 标头)](#2、通用面板(40px 标头))
src/components/dashboard/DashboardPanel.vue
src/components/dashboard/CenterLightVisual.vue
src/composables/useDashboardClock.ts
[5、路由 /data-screen](#5、路由 /data-screen)
需求:
首页数据大屏需求:
页面布局:左上角为 logo,上方中间展示一个大的标题(内容大概为保险车险类的),右上角显示时间(时间、星期几、年月日)
中间主要区域显示一辆汽车和一栋大楼,之间光线互相牵连,有流动光感。
左下区域展示两个 Echarts 图表,一个柱状图一个饼状图,
右侧区域上下展示两个数据列表
左下区域和右侧区域都是上下结构布局,上方标头大概60px 高,下方根据屏幕看着给分配高度吧。根据以上图片和需求说明,帮我在此项目中新生成一个页面用于展示和测试。
利用 Cursor 实现起来会很快,我们的角色就跟 产品 / 需求 或者 项目经理似的,只需要在 Cursor Agents 窗口内提任务需求就可以了,它就能够快速理解需求并自动帮你实现,话不多说,先看下成果~

接下来看一下 Cursor 是如何一步一步实现的吧~
1、正在探索项目结构与技术栈,以便创建符合需求的数据大屏页面。
2、正在安装 ECharts 并创建数据大屏页面及相关组件。
bash
$ cd /Users/jimmy/Desktop/PROJECT/StarCollab && npm install echarts

1、大屏主页面
src/views/dashboard/DataScreenView.vue
javascript<template> <div class="data-screen"> <header class="data-screen__header"> <div class="data-screen__logo"> <el-icon :size="36" color="#00d4ff"><Star /></el-icon> <span class="data-screen__logo-text">星盘保险</span> </div> <h1 class="data-screen__title">车险智能运营数据大屏</h1> <div class="data-screen__clock"> <span class="data-screen__time">{{ timeText }}</span> <span class="data-screen__weekday">{{ weekdayText }}</span> <span class="data-screen__date">{{ dateText }}</span> </div> </header> <div class="data-screen__body"> <div class="data-screen__main"> <section class="data-screen__center"> <CenterLightVisual /> </section> <div class="data-screen__charts"> <DashboardPanel title="月度保费与赔付" subtitle="单位:万元"> <div ref="barChartRef" class="chart-box" /> </DashboardPanel> <DashboardPanel title="险种结构占比"> <div ref="pieChartRef" class="chart-box" /> </DashboardPanel> </div> </div> <aside class="data-screen__sidebar"> <DashboardPanel title="今日报案动态"> <ul class="data-list"> <li v-for="item in claimList" :key="item.id" class="data-list__row"> <span class="data-list__plate">{{ item.plate }}</span> <span class="data-list__type">{{ item.type }}</span> <span class="data-list__status" :class="`is-${item.status}`">{{ item.statusLabel }}</span> <span class="data-list__time">{{ item.time }}</span> </li> </ul> </DashboardPanel> <DashboardPanel title="渠道业绩排行"> <ul class="data-list data-list--rank"> <li v-for="(item, index) in rankList" :key="item.name" class="data-list__row"> <span class="data-list__rank" :class="{ 'is-top': index < 3 }">{{ index + 1 }}</span> <span class="data-list__name">{{ item.name }}</span> <span class="data-list__value">{{ item.premium }} 万</span> <span class="data-list__rate">{{ item.rate }}%</span> </li> </ul> </DashboardPanel> </aside> </div> </div> </template> <script setup lang="ts"> import { onMounted, onUnmounted, ref } from 'vue' import * as echarts from 'echarts' import { Star } from '@element-plus/icons-vue' import DashboardPanel from '@/components/dashboard/DashboardPanel.vue' import CenterLightVisual from '@/components/dashboard/CenterLightVisual.vue' import { useDashboardClock } from '@/composables/useDashboardClock' const { timeText, weekdayText, dateText } = useDashboardClock() const barChartRef = ref<HTMLElement | null>(null) const pieChartRef = ref<HTMLElement | null>(null) let barChart: echarts.ECharts | null = null let pieChart: echarts.ECharts | null = null const claimList = [ { id: 1, plate: '沪A·8K21X', type: '剐蹭', status: 'processing', statusLabel: '处理中', time: '20:41' }, { id: 2, plate: '苏E·3M902', type: '单方事故', status: 'done', statusLabel: '已结案', time: '19:58' }, { id: 3, plate: '浙B·7P118', type: '涉水', status: 'processing', statusLabel: '定损中', time: '19:22' }, { id: 4, plate: '粤S·2N556', type: '追尾', status: 'pending', statusLabel: '待派工', time: '18:47' }, { id: 5, plate: '京N·9D003', type: '玻璃险', status: 'done', statusLabel: '已赔付', time: '17:30' }, { id: 6, plate: '川A·5H778', type: '盗抢险', status: 'processing', statusLabel: '审核中', time: '16:15' }, ] const rankList = [ { name: '华东直营中心', premium: 1286, rate: 18.2 }, { name: '华南代理联盟', premium: 1104, rate: 15.6 }, { name: '华北车商渠道', premium: 982, rate: 12.4 }, { name: '西南电销团队', premium: 756, rate: 9.8 }, { name: '华中续保专席', premium: 688, rate: 8.1 }, { name: '东北县域网点', premium: 521, rate: 6.3 }, ] const chartTheme = { textStyle: { color: '#9ec8ff' }, axisLine: { lineStyle: { color: 'rgba(0, 180, 255, 0.35)' } }, splitLine: { lineStyle: { color: 'rgba(0, 120, 200, 0.15)' } }, } function initBarChart(): void { if (!barChartRef.value) return barChart = echarts.init(barChartRef.value) barChart.setOption({ grid: { left: 48, right: 24, top: 36, bottom: 32 }, tooltip: { trigger: 'axis' }, legend: { top: 4, textStyle: { color: '#9ec8ff' }, data: ['保费', '赔付'], }, xAxis: { type: 'category', data: ['1月', '2月', '3月', '4月', '5月', '6月'], axisLabel: chartTheme.textStyle, axisLine: chartTheme.axisLine, }, yAxis: { type: 'value', axisLabel: chartTheme.textStyle, splitLine: chartTheme.splitLine, }, series: [ { name: '保费', type: 'bar', barWidth: 18, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#00d4ff' }, { offset: 1, color: '#0066aa' }, ]), }, data: [820, 932, 901, 1034, 1090, 1180], }, { name: '赔付', type: 'bar', barWidth: 18, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#ffb347' }, { offset: 1, color: '#cc6600' }, ]), }, data: [420, 512, 488, 560, 602, 640], }, ], }) } function initPieChart(): void { if (!pieChartRef.value) return pieChart = echarts.init(pieChartRef.value) pieChart.setOption({ tooltip: { trigger: 'item' }, legend: { orient: 'vertical', right: 8, top: 'center', textStyle: { color: '#9ec8ff', fontSize: 11 }, }, series: [ { type: 'pie', radius: ['42%', '68%'], center: ['38%', '50%'], label: { color: '#cfe6ff', fontSize: 11 }, data: [ { value: 42, name: '交强险' }, { value: 28, name: '商业三者' }, { value: 15, name: '车损险' }, { value: 10, name: '驾乘意外险' }, { value: 5, name: '其他附加' }, ], itemStyle: { borderColor: '#041428', borderWidth: 2, }, color: ['#00d4ff', '#3d8fff', '#ffb347', '#67e8a3', '#b388ff'], }, ], }) } function handleResize(): void { barChart?.resize() pieChart?.resize() } onMounted(() => { initBarChart() initPieChart() window.addEventListener('resize', handleResize) }) onUnmounted(() => { window.removeEventListener('resize', handleResize) barChart?.dispose() pieChart?.dispose() }) </script> <style scoped> .data-screen { width: 100vw; height: 100vh; overflow: hidden; display: flex; flex-direction: column; background: #020b18 radial-gradient(ellipse at 50% 0%, #0a2848 0%, #020b18 55%); color: #e8f4ff; } .data-screen__header { flex-shrink: 0; height: 72px; display: grid; grid-template-columns: 220px 1fr 220px; align-items: center; padding: 0 28px; border-bottom: 1px solid rgba(0, 180, 255, 0.25); background: linear-gradient(180deg, rgba(0, 60, 120, 0.35), transparent); } .data-screen__logo { display: flex; align-items: center; gap: 10px; } .data-screen__logo-text { font-size: 18px; font-weight: 600; color: #7ec8ff; letter-spacing: 2px; } .data-screen__title { text-align: center; font-size: clamp(22px, 2.4vw, 36px); font-weight: 700; letter-spacing: 6px; background: linear-gradient(90deg, #7ec8ff, #00e5ff, #7ec8ff); -webkit-background-clip: text; background-clip: text; color: transparent; text-shadow: 0 0 30px rgba(0, 200, 255, 0.35); } .data-screen__clock { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; } .data-screen__time { font-size: 28px; font-weight: 700; font-variant-numeric: tabular-nums; color: #00e5ff; } .data-screen__weekday, .data-screen__date { font-size: 13px; color: rgba(160, 210, 255, 0.85); } .data-screen__body { flex: 1; min-height: 0; display: grid; grid-template-columns: 1fr minmax(280px, 26vw); gap: 14px; padding: 14px; } .data-screen__main { min-width: 0; display: flex; flex-direction: column; gap: 14px; } .data-screen__center { flex: 1.15; min-height: 0; border: 1px solid rgba(0, 180, 255, 0.2); background: rgba(4, 20, 45, 0.5); border-radius: 4px; padding: 8px; } .data-screen__charts { flex: 1; min-height: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } .data-screen__sidebar { min-height: 0; display: flex; flex-direction: column; gap: 14px; } .data-screen__sidebar > :deep(.dashboard-panel) { flex: 1; min-height: 0; } .chart-box { width: 100%; height: 100%; min-height: 160px; } .data-list { list-style: none; height: 100%; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; padding-right: 4px; } .data-list__row { display: grid; grid-template-columns: 1.2fr 0.8fr 0.7fr 0.5fr; gap: 8px; align-items: center; padding: 10px 12px; font-size: 13px; background: rgba(0, 80, 160, 0.12); border-left: 3px solid rgba(0, 200, 255, 0.45); border-radius: 2px; } .data-list--rank .data-list__row { grid-template-columns: 36px 1fr 72px 52px; } .data-list__plate, .data-list__name { color: #d8ecff; font-weight: 500; } .data-list__type, .data-list__time, .data-list__value { color: rgba(160, 210, 255, 0.8); } .data-list__status.is-processing { color: #ffb347; } .data-list__status.is-done { color: #67e8a3; } .data-list__status.is-pending { color: #9ec8ff; } .data-list__rank { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 4px; background: rgba(0, 100, 180, 0.35); color: #9ec8ff; font-weight: 600; } .data-list__rank.is-top { background: linear-gradient(135deg, #ffb347, #ff6b35); color: #1a0a00; } .data-list__rate { text-align: right; color: #00e5ff; } .data-list::-webkit-scrollbar { width: 4px; } .data-list::-webkit-scrollbar-thumb { background: rgba(0, 180, 255, 0.35); border-radius: 2px; } </style>
javascriptimport { onMounted, onUnmounted, ref } from 'vue' const WEEKDAY_LABELS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'] /** 数据大屏右上角实时时钟 */ export function useDashboardClock() { const timeText = ref('') const weekdayText = ref('') const dateText = ref('') function pad(n: number): string { return String(n).padStart(2, '0') } function tick(): void { const now = new Date() timeText.value = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}` weekdayText.value = WEEKDAY_LABELS[now.getDay()] dateText.value = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日` } let timer: ReturnType<typeof setInterval> | undefined onMounted(() => { tick() timer = setInterval(tick, 1000) }) onUnmounted(() => { if (timer) clearInterval(timer) }) return { timeText, weekdayText, dateText } }
2、通用面板(40px 标头)
src/components/dashboard/DashboardPanel.vue
javascript<template> <section class="dashboard-panel"> <header class="dashboard-panel__header"> <span class="dashboard-panel__title">{{ title }}</span> <span v-if="subtitle" class="dashboard-panel__subtitle">{{ subtitle }}</span> </header> <div class="dashboard-panel__body"> <slot /> </div> </section> </template> <script setup lang="ts"> defineProps<{ title: string subtitle?: string }>() </script> <style scoped> .dashboard-panel { display: flex; flex-direction: column; min-height: 0; border: 1px solid rgba(0, 180, 255, 0.25); background: linear-gradient(180deg, rgba(6, 30, 60, 0.85) 0%, rgba(4, 18, 40, 0.92) 100%); box-shadow: inset 0 0 24px rgba(0, 120, 255, 0.08); } .dashboard-panel__header { flex-shrink: 0; height: 60px; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; border-bottom: 1px solid rgba(0, 180, 255, 0.2); background: linear-gradient(90deg, rgba(0, 100, 200, 0.15), transparent 60%, rgba(0, 100, 200, 0.15)); } .dashboard-panel__title { font-size: 18px; font-weight: 600; color: #e8f4ff; letter-spacing: 2px; } .dashboard-panel__subtitle { font-size: 12px; color: rgba(160, 210, 255, 0.65); } .dashboard-panel__body { flex: 1; min-height: 0; padding: 12px; overflow: hidden; } </style>
3、中间光感联动区
src/components/dashboard/CenterLightVisual.vue
javascript<template> <div class="center-visual"> <svg class="center-visual__svg" viewBox="0 0 1000 420" preserveAspectRatio="xMidYMid meet" aria-hidden="true" > <defs> <linearGradient id="carBody" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" stop-color="#1a6bff" /> <stop offset="100%" stop-color="#0d3d8c" /> </linearGradient> <linearGradient id="buildingBody" x1="0%" y1="100%" x2="0%" y2="0%"> <stop offset="0%" stop-color="#0a2848" /> <stop offset="100%" stop-color="#1e5a9a" /> </linearGradient> <linearGradient id="beamGrad" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="200" y2="0"> <stop offset="0%" stop-color="transparent" /> <stop offset="35%" stop-color="#00e5ff" /> <stop offset="65%" stop-color="#4d9fff" /> <stop offset="100%" stop-color="transparent" /> <animate attributeName="x1" values="-200;800;-200" dur="3s" repeatCount="indefinite" /> <animate attributeName="x2" values="0;1000;0" dur="3s" repeatCount="indefinite" /> </linearGradient> <filter id="glow"> <feGaussianBlur stdDeviation="3" result="blur" /> <feMerge> <feMergeNode in="blur" /> <feMergeNode in="SourceGraphic" /> </feMerge> </filter> </defs> <!-- 光感连线 --> <g class="light-beams" filter="url(#glow)"> <path class="beam beam--1" d="M 280 200 Q 500 80 720 140" fill="none" stroke="url(#beamGrad)" stroke-width="3" /> <path class="beam beam--2" d="M 300 240 Q 500 320 700 220" fill="none" stroke="url(#beamGrad)" stroke-width="2.5" /> <path class="beam beam--3" d="M 260 260 Q 500 200 740 280" fill="none" stroke="rgba(0, 229, 255, 0.35)" stroke-width="1.5" /> <circle class="pulse pulse--1" r="5" fill="#00e5ff"> <animateMotion dur="4s" repeatCount="indefinite" path="M 280 200 Q 500 80 720 140" /> </circle> <circle class="pulse pulse--2" r="4" fill="#6eb6ff"> <animateMotion dur="3.2s" repeatCount="indefinite" path="M 300 240 Q 500 320 700 220" /> </circle> <circle class="pulse pulse--3" r="3" fill="#00e5ff"> <animateMotion dur="5s" repeatCount="indefinite" path="M 260 260 Q 500 200 740 280" /> </circle> </g> <!-- 汽车 --> <g class="car" transform="translate(80, 150)"> <rect x="20" y="70" width="200" height="50" rx="12" fill="url(#carBody)" stroke="#3d9fff" stroke-width="2" /> <rect x="50" y="35" width="120" height="45" rx="8" fill="#1a4a8a" stroke="#5eb0ff" stroke-width="1.5" /> <circle cx="60" cy="125" r="22" fill="#0a1e3a" stroke="#00d4ff" stroke-width="3" /> <circle cx="180" cy="125" r="22" fill="#0a1e3a" stroke="#00d4ff" stroke-width="3" /> <circle cx="60" cy="125" r="10" fill="#1a3d6e" /> <circle cx="180" cy="125" r="10" fill="#1a3d6e" /> <text x="110" y="105" text-anchor="middle" fill="#a8d8ff" font-size="14" font-weight="600">车险标的</text> </g> <!-- 大楼 --> <g class="building" transform="translate(680, 40)"> <rect x="0" y="0" width="200" height="320" fill="url(#buildingBody)" stroke="#3d8fd9" stroke-width="2" /> <g class="windows"> <rect v-for="(win, i) in windows" :key="i" :x="win.x" :y="win.y" width="28" height="22" :fill="win.on ? '#ffe566' : 'rgba(0, 80, 140, 0.6)'" stroke="rgba(0, 200, 255, 0.3)" /> </g> <text x="100" y="350" text-anchor="middle" fill="#a8d8ff" font-size="14" font-weight="600">承保机构</text> </g> </svg> <p class="center-visual__hint">车辆保障 ⇄ 机构承保 数据实时联动</p> </div> </template> <script setup lang="ts"> /** 大楼窗户亮灭(静态装饰) */ const windows = Array.from({ length: 24 }, (_, i) => { const col = i % 4 const row = Math.floor(i / 4) return { x: 24 + col * 44, y: 30 + row * 58, on: (i + col) % 3 !== 0, } }) </script> <style scoped> .center-visual { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 0; } .center-visual__svg { width: 100%; max-height: calc(100% - 32px); flex: 1; } .center-visual__hint { flex-shrink: 0; margin-top: 8px; font-size: 13px; color: rgba(140, 200, 255, 0.55); letter-spacing: 4px; } .beam { stroke-linecap: round; } .beam--1 { animation: beam-flicker 2.5s ease-in-out infinite; } .beam--2 { animation: beam-flicker 3s ease-in-out infinite 0.4s; } .beam--3 { stroke-dasharray: 8 12; animation: beam-dash 6s linear infinite; } .pulse { filter: drop-shadow(0 0 6px #00e5ff); } @keyframes beam-flicker { 0%, 100% { opacity: 0.75; } 50% { opacity: 1; } } @keyframes beam-dash { to { stroke-dashoffset: -120; } } </style>
4、实时时钟
src/composables/useDashboardClock.ts
javascriptimport { onMounted, onUnmounted, ref } from 'vue' const WEEKDAY_LABELS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'] /** 数据大屏右上角实时时钟 */ export function useDashboardClock() { const timeText = ref('') const weekdayText = ref('') const dateText = ref('') function pad(n: number): string { return String(n).padStart(2, '0') } function tick(): void { const now = new Date() timeText.value = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}` weekdayText.value = WEEKDAY_LABELS[now.getDay()] dateText.value = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日` } let timer: ReturnType<typeof setInterval> | undefined onMounted(() => { tick() timer = setInterval(tick, 1000) }) onUnmounted(() => { if (timer) clearInterval(timer) }) return { timeText, weekdayText, dateText } }
5、路由 /data-screen
src/router/index.ts
javascript{ path: '/data-screen', name: 'DataScreen', component: () => import('@/views/dashboard/DataScreenView.vue'), meta: { title: '数据大屏' }, },
6、首页增加入口卡片
src/views/home/HomeView.vue
javascript{ name: '数据大屏', desc: '车险智能运营可视化大屏', path: '/data-screen', icon: DataAnalysis, color: 'linear-gradient(135deg, #00d4ff, #0066cc)', },

成果图展示:

