记录利用Cursor快速实现首页数据大屏

目录

1、大屏主页面

src/views/dashboard/DataScreenView.vue

[2、通用面板(40px 标头)](#2、通用面板(40px 标头))

src/components/dashboard/DashboardPanel.vue

3、中间光感联动区

src/components/dashboard/CenterLightVisual.vue

4、实时时钟

src/composables/useDashboardClock.ts

[5、路由 /data-screen](#5、路由 /data-screen)

src/router/index.ts

6、首页增加入口卡片

src/views/home/HomeView.vue


需求:

首页数据大屏需求:

页面布局:左上角为 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>
javascript 复制代码
import { 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

javascript 复制代码
import { 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)',
  },

成果图展示:

相关推荐
像我这样帅的人丶你还1 小时前
🚀🚀🚀2026年还不会Nginx?
前端·nginx
用户059540174461 小时前
把对话记忆从内存搬到 Redis,长期记忆准确率从 63% 提升到 98%
前端·css
无心使然1 小时前
Openlayers图层按需分层渲染到不同Canvas画布
前端·vue.js·gis
木斯佳1 小时前
前端八股文面经大全:字节跳动-存储部门一面(2026-05-29)·面经深度解析
前端·状态模式
ayqy贾杰1 小时前
有AI了,我当超大头兵还苟得住吗?
前端·后端·架构
Aotman_1 小时前
JavaScript数组对象中指定字段转换
java·开发语言·前端·javascript·vue.js·前端框架·es6
姓蔡小朋友1 小时前
React基础
前端·react.js·前端框架
-嘟囔着拯救世界-1 小时前
Claude Code 平替来了?DeepSeek-TUI 保姆级安装教程
人工智能·ai·ai编程·deepseek·vibecoding·deepseek-tui
IT_陈寒2 小时前
Vue的动态组件坑了我整整一天!
前端·人工智能·后端