yanghao 1 dzień temu
rodzic
commit
5ccf877229
5 zmienionych plików z 1403 dodań i 30 usunięć
  1. BIN
      src/assets/kb/bg.png
  2. BIN
      src/assets/kb/header.png
  3. 666 0
      src/styles/qhse_kb.scss
  4. 171 0
      src/utils/kb.ts
  5. 566 30
      src/views/Home/Index.vue

BIN
src/assets/kb/bg.png


BIN
src/assets/kb/header.png


+ 666 - 0
src/styles/qhse_kb.scss

@@ -0,0 +1,666 @@
+@font-face {
+  font-family: YouSheBiaoTiHei;
+  font-style: normal;
+  font-weight: normal;
+  src: url('@/assets/font/YouSheBiaoTiHei.ttf') format('truetype');
+  font-display: swap;
+}
+
+.bg {
+  --fvs-theme-color-1-2: #f5f9ff;
+
+  background: url('@/assets/kb/bg.png') center center / 100% 100% no-repeat;
+  background-color: #fff;
+}
+
+.header {
+  height: calc(52px * var(--kb-scale, 1));
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(32px * var(--kb-scale, 1));
+  color: var(--fvs-theme-color-1-2);
+  text-align: center;
+  background: url('@/assets/kb/header.png') center center / 100% 100% no-repeat;
+}
+
+.panel {
+  background: linear-gradient(180deg, rgb(235 243 255 / 76%) 0%, rgb(221 233 251 / 58%) 100%);
+  border: 1px solid rgb(255 255 255 / 58%);
+  border-radius: calc(20px * var(--kb-scale, 1));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 72%),
+    0 18px 36px rgb(46 90 164 / 12%);
+  backdrop-filter: blur(12px);
+}
+
+.panel-title {
+  position: relative;
+  display: flex;
+  height: calc(36px * var(--kb-scale, 1));
+  padding: 0 calc(16px * var(--kb-scale, 1)) 0 calc(36px * var(--kb-scale, 1));
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(24px * var(--kb-scale, 1));
+  letter-spacing: 1px;
+  color: #03409b;
+  align-items: center;
+
+  &::after {
+    position: absolute;
+    right: calc(10px * var(--kb-scale, 1));
+    bottom: 0;
+    left: calc(10px * var(--kb-scale, 1));
+    height: calc(2px * var(--kb-scale, 1));
+    background: linear-gradient(
+      to right,
+      rgb(3 64 155 / 15%) 0%,
+      rgb(3 64 155 / 15%) calc(100% - 80px),
+      #03409b calc(100% - 80px),
+      #03409b 100%
+    );
+    content: '';
+  }
+
+  &::before {
+    position: absolute;
+    right: calc(10px * var(--kb-scale, 1));
+    bottom: 0;
+    z-index: 1;
+    width: calc(80px * var(--kb-scale, 1));
+    height: calc(4px * var(--kb-scale, 1));
+    background: #03409b;
+    border-radius: calc(2px * var(--kb-scale, 1)) calc(2px * var(--kb-scale, 1)) 0 0;
+    content: '';
+  }
+
+  .icon-decorator {
+    position: absolute;
+    left: calc(14px * var(--kb-scale, 1));
+    display: flex;
+    align-items: center;
+    gap: calc(3px * var(--kb-scale, 1));
+
+    span {
+      width: calc(4px * var(--kb-scale, 1));
+      height: calc(18px * var(--kb-scale, 1));
+      background: #03409b;
+      border-radius: calc(2px * var(--kb-scale, 1));
+
+      &:last-child {
+        height: calc(12px * var(--kb-scale, 1));
+        opacity: 0.5;
+      }
+    }
+  }
+}
+
+.panel-title--lg {
+  height: calc(48px * var(--kb-scale, 1));
+}
+
+.panel-title--md {
+  height: calc(40px * var(--kb-scale, 1));
+}
+
+.kb-panel-title-text {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(24px * var(--kb-scale, 1));
+  font-weight: normal;
+  line-height: 1;
+  letter-spacing: 1px;
+  color: #03409b;
+}
+
+.summary-panel {
+  height: calc(112px * var(--kb-scale, 1));
+}
+
+.summary-panel__title {
+  height: calc(32px * var(--kb-scale, 1));
+}
+
+.summary-panel__grid {
+  padding: calc(6px * var(--kb-scale, 1)) calc(10px * var(--kb-scale, 1));
+  gap: calc(10px * var(--kb-scale, 1));
+}
+
+.summary-card {
+  gap: calc(8px * var(--kb-scale, 1));
+  padding: calc(8px * var(--kb-scale, 1));
+}
+
+.device-list-panel {
+  height: calc(280px * var(--kb-scale, 1));
+  margin-top: calc(12px * var(--kb-scale, 1));
+}
+
+.device-list-panel__body {
+  padding: calc(8px * var(--kb-scale, 1)) calc(16px * var(--kb-scale, 1));
+}
+
+.device-list-panel__picker {
+  display: flex;
+  width: calc(260px * var(--kb-scale, 1));
+  align-items: center;
+}
+
+.device-list-panel__picker-input {
+  width: calc(260px * var(--kb-scale, 1)) !important;
+
+  :deep(.el-input__wrapper) {
+    min-height: calc(28px * var(--kb-scale, 1));
+    padding: 0 calc(10px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-range-input),
+  :deep(.el-range-separator) {
+    font-size: calc(14px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-range__icon),
+  :deep(.el-range__close-icon) {
+    font-size: calc(14px * var(--kb-scale, 1));
+  }
+}
+
+.kb-inline-picker {
+  display: flex;
+  width: var(--kb-picker-width);
+  align-items: center;
+}
+
+.kb-inline-picker--260 {
+  --kb-picker-width: calc(260px * var(--kb-scale, 1));
+}
+
+.kb-inline-picker__input {
+  width: var(--kb-picker-width) !important;
+
+  :deep(.el-input__wrapper) {
+    min-height: calc(28px * var(--kb-scale, 1));
+    padding: 0 calc(10px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-range-input),
+  :deep(.el-range-separator) {
+    font-size: calc(14px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-range__icon),
+  :deep(.el-range__close-icon) {
+    font-size: calc(14px * var(--kb-scale, 1));
+  }
+}
+
+.summary-card {
+  position: relative;
+  cursor: default;
+  background: linear-gradient(180deg, rgb(255 255 255 / 52%) 0%, rgb(213 227 249 / 38%) 100%);
+  border: 1px solid rgb(255 255 255 / 74%);
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 78%),
+    0 6px 14px rgb(63 103 171 / 7%);
+  transition:
+    transform 0.28s ease,
+    box-shadow 0.28s ease,
+    border-color 0.28s ease,
+    background 0.28s ease;
+  isolation: isolate;
+}
+
+.summary-card::before {
+  position: absolute;
+  z-index: 0;
+  pointer-events: none;
+  background: linear-gradient(
+    135deg,
+    rgb(255 255 255 / 0%) 0%,
+    rgb(255 255 255 / 0%) 58%,
+    rgb(255 255 255 / 18%) 100%
+  );
+  content: '';
+  inset: 0;
+}
+
+.summary-card::after {
+  position: absolute;
+  right: calc(-16px * var(--kb-scale, 1));
+  bottom: calc(-28px * var(--kb-scale, 1));
+  z-index: 0;
+  width: calc(84px * var(--kb-scale, 1));
+  height: calc(84px * var(--kb-scale, 1));
+  pointer-events: none;
+  background: radial-gradient(circle, var(--card-glow) 0%, rgb(255 255 255 / 0%) 70%);
+  content: '';
+  opacity: 0.86;
+  transition:
+    transform 0.28s ease,
+    opacity 0.28s ease;
+}
+
+.summary-card:hover {
+  background: linear-gradient(180deg, rgb(255 255 255 / 72%) 0%, rgb(219 233 255 / 46%) 100%);
+  border-color: rgb(255 255 255 / 90%);
+  transform: translateY(-3px);
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 85%),
+    0 10px 18px rgb(41 89 164 / 13%);
+}
+
+.summary-card:hover::after {
+  opacity: 1;
+  transform: scale(1.08);
+}
+
+.summary-card__shine {
+  position: absolute;
+  top: 0;
+  left: -130%;
+  z-index: 1;
+  width: 60%;
+  height: 100%;
+  pointer-events: none;
+  background: linear-gradient(
+    90deg,
+    rgb(255 255 255 / 0%) 0%,
+    rgb(255 255 255 / 18%) 45%,
+    rgb(255 255 255 / 45%) 50%,
+    rgb(255 255 255 / 18%) 55%,
+    rgb(255 255 255 / 0%) 100%
+  );
+  transform: skewX(-22deg);
+}
+
+.summary-card:hover .summary-card__shine {
+  left: 150%;
+  transition: left 0.8s ease;
+}
+
+.summary-card__icon {
+  position: relative;
+  z-index: 2;
+  display: flex;
+  width: calc(48px * var(--kb-scale, 1));
+  height: calc(48px * var(--kb-scale, 1));
+  background: linear-gradient(180deg, rgb(255 255 255 / 96%) 0%, rgb(242 247 255 / 92%) 100%);
+  border: 1px solid rgb(255 255 255 / 88%);
+  border-radius: calc(12px * var(--kb-scale, 1));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 95%),
+    0 6px 12px rgb(152 181 230 / 15%);
+  flex-shrink: 0;
+  align-items: center;
+  justify-content: center;
+  transition:
+    transform 0.28s ease,
+    box-shadow 0.28s ease;
+}
+
+.summary-card__icon::after {
+  position: absolute;
+  right: calc(-3px * var(--kb-scale, 1));
+  bottom: calc(-3px * var(--kb-scale, 1));
+  width: calc(15px * var(--kb-scale, 1));
+  height: calc(15px * var(--kb-scale, 1));
+  background: radial-gradient(
+    circle at 30% 30%,
+    rgb(255 255 255 / 96%) 0%,
+    var(--card-glow) 58%,
+    rgb(255 255 255 / 0%) 75%
+  );
+  border-radius: 999px;
+  content: '';
+  opacity: 0.95;
+}
+
+.summary-card:hover .summary-card__icon {
+  transform: translateY(-2px) scale(1.03);
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 95%),
+    0 8px 14px rgb(120 158 221 / 20%);
+}
+
+.summary-card__icon-glyph {
+  position: relative;
+  z-index: 1;
+  font-size: calc(25px * var(--kb-scale, 1));
+  color: var(--card-accent);
+  transition:
+    transform 0.28s ease,
+    filter 0.28s ease;
+}
+
+.summary-card:hover .summary-card__icon-glyph {
+  filter: drop-shadow(0 4px 8px rgb(0 0 0 / 8%));
+  transform: scale(1.08);
+}
+
+.summary-card__body {
+  position: relative;
+  z-index: 2;
+  display: flex;
+  min-height: calc(48px * var(--kb-scale, 1));
+  min-width: 0;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.summary-card__label {
+  overflow: hidden;
+  font-size: calc(14px * var(--kb-scale, 1));
+  font-weight: 600;
+  color: #24364f;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  transition:
+    transform 0.28s ease,
+    color 0.28s ease;
+}
+
+.summary-card__value {
+  margin-top: calc(4px * var(--kb-scale, 1));
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(19px * var(--kb-scale, 1));
+  line-height: 1;
+  letter-spacing: 1px;
+  color: #1f5bb8;
+  transition:
+    transform 0.28s ease,
+    text-shadow 0.28s ease;
+}
+
+.summary-card:hover .summary-card__label {
+  color: #173b72;
+  transform: translateX(1px);
+}
+
+.summary-card:hover .summary-card__value {
+  text-shadow: 0 4px 10px rgb(31 91 184 / 16%);
+  transform: translateX(1px);
+}
+
+.summary-card__corner {
+  position: absolute;
+  top: 0;
+  right: 0;
+  z-index: 1;
+  width: calc(36px * var(--kb-scale, 1));
+  height: calc(36px * var(--kb-scale, 1));
+  pointer-events: none;
+  background: linear-gradient(135deg, rgb(255 255 255 / 30%) 0%, rgb(255 255 255 / 0%) 70%);
+  opacity: 0.85;
+  clip-path: polygon(100% 0, 0 0, 100% 100%);
+}
+
+.summary-card {
+  animation: card-in 0.5s ease both;
+}
+
+.summary-card:nth-child(1) {
+  animation-delay: 0.05s;
+}
+
+.summary-card:nth-child(2) {
+  animation-delay: 0.1s;
+}
+
+.summary-card:nth-child(3) {
+  animation-delay: 0.15s;
+}
+
+.summary-card:nth-child(4) {
+  animation-delay: 0.2s;
+}
+
+.summary-card:nth-child(5) {
+  animation-delay: 0.25s;
+}
+
+.summary-card:nth-child(6) {
+  animation-delay: 0.3s;
+}
+
+.summary-card:nth-child(7) {
+  animation-delay: 0.35s;
+}
+
+.summary-card:nth-child(8) {
+  animation-delay: 0.4s;
+}
+
+.summary-card:nth-child(9) {
+  animation-delay: 0.4s;
+}
+
+.summary-card:nth-child(10) {
+  animation-delay: 0.4s;
+}
+
+@keyframes card-in {
+  from {
+    opacity: 0;
+    transform: translateY(12px);
+  }
+
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.summary-card__placeholder {
+  display: inline-block;
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(19px * var(--kb-scale, 1));
+  line-height: 1;
+  letter-spacing: 1px;
+  color: rgb(31 91 184 / 45%);
+}
+
+.device-list-table,
+.team-list-table {
+  --el-table-border-color: rgb(31 91 184 / 12%);
+  --el-table-border: 1px solid rgb(31 91 184 / 12%);
+  --el-table-header-bg-color: rgb(255 255 255 / 82%);
+  --el-table-tr-bg-color: transparent;
+  --el-table-bg-color: transparent;
+  --el-table-row-hover-bg-color: rgb(31 91 184 / 6%);
+  --el-fill-color-lighter: rgb(255 255 255 / 55%);
+
+  background: transparent;
+
+  :deep(.el-table__inner-wrapper::before) {
+    display: none;
+  }
+
+  :deep(.el-table__header-wrapper th.el-table__cell) {
+    // font-family: YouSheBiaoTiHei, sans-serif;
+    font-size: calc(20px * var(--kb-scale, 1));
+    font-weight: 500;
+    letter-spacing: 0.5px;
+    color: #03409b;
+    background: linear-gradient(180deg, rgb(255 255 255 / 92%) 0%, rgb(231 240 255 / 82%) 100%);
+    border-bottom: 1px solid rgb(31 91 184 / 16%);
+  }
+
+  :deep(.el-table__body td.el-table__cell) {
+    padding: calc(8px * var(--kb-scale, 1)) 0;
+    // font-family: YouSheBiaoTiHei, sans-serif;
+    font-size: calc(16px * var(--kb-scale, 1));
+    color: #24364f;
+    background: rgb(255 255 255 / 16%);
+    border-bottom: 1px solid rgb(31 91 184 / 8%);
+  }
+
+  :deep(.el-table__body tr:nth-child(2n) td.el-table__cell) {
+    background: rgb(255 255 255 / 28%);
+  }
+
+  :deep(.el-table__row:hover > td.el-table__cell) {
+    background: rgb(31 91 184 / 8%) !important;
+  }
+
+  :deep(.el-scrollbar__bar.is-vertical) {
+    width: calc(8px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-scrollbar__bar.is-horizontal) {
+    height: calc(8px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-scrollbar__thumb) {
+    background: rgb(31 91 184 / 26%);
+    border-radius: 999px;
+  }
+}
+
+.device-list-table {
+  :deep(.el-table__row) {
+    cursor: pointer;
+  }
+}
+
+.kb-stage-card {
+  --panel-delay: 0s;
+  --panel-glow: rgb(31 91 184 / 12%);
+
+  position: relative;
+  opacity: 0;
+  transform: translateY(22px) scale(0.985);
+  animation: kb-panel-enter 0.8s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+  animation-delay: var(--panel-delay);
+  isolation: isolate;
+  will-change: transform, opacity;
+}
+
+.kb-stage-card::before {
+  position: absolute;
+  z-index: 0;
+  pointer-events: none;
+  background: linear-gradient(
+    112deg,
+    rgb(255 255 255 / 0%) 0%,
+    rgb(255 255 255 / 0%) 34%,
+    rgb(255 255 255 / 26%) 48%,
+    rgb(255 255 255 / 8%) 56%,
+    rgb(255 255 255 / 0%) 68%,
+    rgb(255 255 255 / 0%) 100%
+  );
+  border-radius: calc(20px * var(--kb-scale, 1));
+  content: '';
+  opacity: 0;
+  transform: translateX(-140%) skewX(-22deg);
+  animation: kb-panel-sheen 1.05s ease-out forwards;
+  animation-delay: calc(var(--panel-delay) + 0.18s);
+  inset: 0;
+}
+
+.kb-stage-card::after {
+  position: absolute;
+  right: calc(-24px * var(--kb-scale, 1));
+  bottom: calc(-30px * var(--kb-scale, 1));
+  z-index: 0;
+  width: calc(120px * var(--kb-scale, 1));
+  height: calc(120px * var(--kb-scale, 1));
+  pointer-events: none;
+  background: radial-gradient(circle, var(--panel-glow) 0%, rgb(255 255 255 / 0%) 72%);
+  content: '';
+  opacity: 0.88;
+  transition:
+    transform 0.32s ease,
+    opacity 0.32s ease;
+}
+
+.kb-stage-card:hover {
+  transform: translateY(-4px) scale(1.005);
+  transition:
+    transform 0.28s ease,
+    box-shadow 0.28s ease;
+}
+
+.kb-stage-card:hover::after {
+  opacity: 1;
+  transform: scale(1.08);
+}
+
+.kb-stage-card--list {
+  --panel-glow: rgb(45 124 248 / 14%);
+}
+
+.kb-stage-card--1 {
+  --panel-delay: 0.02s;
+}
+
+.kb-stage-card--2 {
+  --panel-delay: 0.08s;
+}
+
+.kb-stage-card--3 {
+  --panel-delay: 0.14s;
+}
+
+.kb-stage-card--4 {
+  --panel-delay: 0.2s;
+}
+
+.kb-stage-card--5 {
+  --panel-delay: 0.26s;
+}
+
+.kb-stage-card--6 {
+  --panel-delay: 0.32s;
+}
+
+.kb-stage-card--7 {
+  --panel-delay: 0.38s;
+}
+
+.kb-stage-card--8 {
+  --panel-delay: 0.46s;
+}
+
+@keyframes kb-panel-enter {
+  0% {
+    opacity: 0;
+    filter: blur(4px);
+    transform: translateY(22px) scale(0.985);
+  }
+
+  100% {
+    opacity: 1;
+    filter: blur(0);
+    transform: translateY(0) scale(1);
+  }
+}
+
+@keyframes kb-panel-sheen {
+  0% {
+    opacity: 0;
+    transform: translateX(-140%) skewX(-22deg);
+  }
+
+  18% {
+    opacity: 0.85;
+  }
+
+  100% {
+    opacity: 0;
+    transform: translateX(165%) skewX(-22deg);
+  }
+}
+
+@media (prefers-reduced-motion: reduce) {
+  .kb-stage-card,
+  .kb-stage-card::before,
+  .kb-stage-card::after {
+    animation: none;
+    transition: none;
+  }
+
+  .kb-stage-card {
+    opacity: 1;
+    transform: none;
+  }
+
+  .kb-stage-card:hover {
+    transform: none;
+  }
+}

+ 171 - 0
src/utils/kb.ts

@@ -0,0 +1,171 @@
+import { LegendOption, TooltipOption } from 'echarts/types/dist/shared'
+
+export const DESIGN_WIDTH = 1920
+export const DESIGN_HEIGHT = 1080
+
+export interface SummaryCardConfig<CardKey> {
+  key: CardKey
+  title: string
+  icon: string
+  accent: string
+  glow: string
+}
+
+export interface CardStateItem {
+  value: number
+  loading: boolean
+}
+
+export interface ChartItem {
+  name: string
+  value: number
+}
+
+export type SeriesItem = {
+  name: string
+  data: number[]
+}
+
+export type ChartData = {
+  xAxis: string[]
+  series: SeriesItem[]
+}
+
+// export const FONT_FAMILY = 'YouSheBiaoTiHei, sans-serif'
+export const FONT_FAMILY = undefined
+export const CHART_RENDERER = 'svg'
+export const ANIMATION = {
+  animationDuration: 500,
+  animationEasing: 'cubicOut' as const
+}
+
+export const THEME = {
+  text: {
+    primary: '#24364f',
+    regular: '#000',
+    strong: '#1f5bb8',
+    black: '#3c3c3c'
+  },
+  tooltip: {
+    cssText: `
+      background: linear-gradient(180deg, rgba(255,255,255,0.96) 0%, rgba(235,243,255,0.96) 100%);
+      border: 1px solid rgba(255,255,255,0.95);
+      border-radius: 8px;
+      box-shadow: 0 8px 20px rgba(31, 91, 184, 0.16);
+      overflow: hidden;
+      backdrop-filter: blur(12px);
+      -webkit-backdrop-filter: blur(12px);
+    `
+  },
+  grid: {
+    left: 16,
+    right: 16,
+    top: 16,
+    bottom: 16,
+    containLabel: true
+  },
+  split: 'rgba(31, 91, 184, 0.08)',
+  color: {
+    blue: {
+      line: '#1f5bb8',
+      light: '#8ec5ff',
+      mid: '#5e9cff',
+      strong: '#2d7cf8',
+      glow: 'rgba(45, 124, 248, 0.34)',
+      bg: 'rgba(31, 91, 184, 0.08)',
+      shadow: 'rgba(31, 91, 184, 0.16)'
+    },
+    orange: {
+      line: '#f08c2e',
+      light: '#ffd8a8',
+      mid: '#ffb454',
+      strong: '#ff8b3d',
+      glow: 'rgba(255, 139, 61, 0.34)',
+      bg: 'rgba(240, 140, 46, 0.08)',
+      shadow: 'rgba(240, 140, 46, 0.18)'
+    },
+    green: {
+      line: '#2e9a5b',
+      light: '#a8e0c8',
+      mid: '#82c793',
+      strong: '#2fb990',
+      glow: 'rgba(46, 154, 91, 0.34)',
+      bg: 'rgba(34, 197, 94, 0.08)',
+      shadow: 'rgba(34, 197, 94, 0.18)'
+    },
+    red: {
+      line: '#ff4d4f',
+      light: '#fdd5d5',
+      mid: '#ff9999',
+      strong: '#ff6666',
+      glow: 'rgba(255, 77, 79, 0.34)',
+      bg: 'rgba(255, 77, 79, 0.08)',
+      shadow: 'rgba(255, 77, 79, 0.18)'
+    }
+  }
+}
+
+export function createTooltip(extra: Partial<TooltipOption> = {}): any {
+  return {
+    textStyle: {
+      color: THEME.text.black,
+      fontSize: 14,
+      fontFamily: FONT_FAMILY
+    },
+    extraCssText: THEME.tooltip.cssText,
+    ...extra
+  }
+}
+
+export function createLegend(extra: Partial<LegendOption> = {}, data: string[] = []): any {
+  return {
+    data,
+    textStyle: {
+      color: THEME.text.regular,
+      fontSize: 10,
+      fontFamily: FONT_FAMILY
+    },
+    ...extra
+  }
+}
+
+export function formatDateLabel(value: string) {
+  if (!value) return value
+  const parts = value.split('-')
+  if (parts.length === 3) {
+    return `${parts[1]}-${parts[2]}`
+  }
+  return value
+}
+
+export function formatSeriesName(name: string) {
+  return name.split('~~en**')[0] || name
+}
+
+export function formatMonthLabel(value: string) {
+  if (!value) return value
+  const [year, month] = value.split('-')
+  if (!year || !month) return value
+  return `${year.slice(-2)}年${month}月`
+}
+
+export function formatGasAxisValue(value: number) {
+  if (value >= 100000000) {
+    return `${(value / 100000000).toFixed(1)}亿`
+  }
+  if (value >= 10000) {
+    return `${(value / 10000).toFixed(0)}万`
+  }
+  return `${value}`
+}
+
+export function formatGasLabelValue(value: number) {
+  if (!value) return ''
+  if (value >= 100000000) {
+    return `${(value / 100000000).toFixed(2)}亿`
+  }
+  if (value >= 10000) {
+    return `${(value / 10000).toFixed(1)}万`
+  }
+  return `${value}`
+}

+ 566 - 30
src/views/Home/Index.vue

@@ -1,43 +1,579 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+
+defineOptions({ name: 'HomeIndex' })
+
+type SummaryCard = {
+  title: string
+  value: string | number
+  change: string
+  color: string
+}
+
+type ProgressItem = {
+  label: string
+  value: number
+  color: string
+}
+
+const dateText = '2026-08-06'
+const period = ref<'month' | 'quarter' | 'year'>('month')
+
+const summaryCards: SummaryCard[] = [
+  { title: '专家数量', value: 150, change: '1.0%', color: '#5d7dff' },
+  { title: '科研项目数量', value: 10, change: '0.0%', color: '#47d39b' },
+  { title: '核心技术', value: 8, change: '0.0%', color: '#ffb84d' },
+  { title: '专利', value: 18, change: '150.0%', color: '#4d96ff' },
+  { title: '论文', value: 28, change: '17.0%', color: '#a88cff' },
+  { title: '荣誉', value: 38, change: '100.0%', color: '#ff93dd' },
+  { title: '资料库', value: 3800, change: '100.0%', color: '#4bc7ff' }
+]
+
+const projectProgress: ProgressItem[] = [
+  { label: '审批中', value: 7, color: '#2e9f8c' },
+  { label: '生效中', value: 18, color: '#208d78' },
+  { label: '已验收', value: 12, color: '#2b6aa8' },
+  { label: '已终止', value: 2, color: '#9da8a1' },
+  { label: '延期', value: 5, color: '#cf4a4a' }
+]
+
+const projectDonutData = [
+  { name: '完工', value: 54, color: '#f35e5c' },
+  { name: '闲置', value: 28, color: '#8dd46c' },
+  { name: '施工', value: 5, color: '#5578d4' },
+  { name: '对接', value: 7, color: '#8f7cff' },
+  { name: '待修缮', value: 0, color: '#f2c04e' },
+  { name: '待分布', value: 2, color: '#4cc2c8' },
+  { name: '现场待命', value: 2, color: '#f08ac7' },
+  { name: '待管', value: 2, color: '#5fb96f' },
+  { name: '待建', value: 0, color: '#607ad8' }
+]
+
+const expertSeries = [
+  { name: '技术', value: 195 },
+  { name: '经营', value: 116 },
+  { name: '商务', value: 86 },
+  { name: '安全', value: 61 },
+  { name: '法务', value: 50 },
+  { name: '研发', value: 34 },
+  { name: '质量', value: 23 },
+  { name: '综合', value: 8 },
+  { name: '其他', value: 6 }
+]
+
+const budgetSeries = {
+  month: [
+    { date: '07-31', value: 61 },
+    { date: '08-01', value: 64 },
+    { date: '08-02', value: 63 },
+    { date: '08-03', value: 62 },
+    { date: '08-04', value: 60 },
+    { date: '08-05', value: 62 },
+    { date: '08-06', value: 62 }
+  ],
+  quarter: [
+    { date: '06-30', value: 58 },
+    { date: '07-15', value: 60 },
+    { date: '07-31', value: 61 },
+    { date: '08-10', value: 62 },
+    { date: '08-20', value: 63 },
+    { date: '08-30', value: 64 },
+    { date: '09-10', value: 63 }
+  ],
+  year: [
+    { date: '01', value: 57 },
+    { date: '03', value: 58 },
+    { date: '05', value: 59 },
+    { date: '07', value: 61 },
+    { date: '09', value: 62 },
+    { date: '11', value: 63 },
+    { date: '12', value: 64 }
+  ]
+} as const
+
+const donutRef = ref<HTMLDivElement | null>(null)
+const barRef = ref<HTMLDivElement | null>(null)
+const lineRef = ref<HTMLDivElement | null>(null)
+
+let donutChart: echarts.ECharts | null = null
+let barChart: echarts.ECharts | null = null
+let lineChart: echarts.ECharts | null = null
+
+function createDonutOption(): echarts.EChartsOption {
+  return {
+    grid: { left: 0, right: 0, top: 0, bottom: 0 },
+    tooltip: { trigger: 'item' },
+    legend: {
+      bottom: 0,
+      icon: 'rect',
+      itemWidth: 10,
+      itemHeight: 10,
+      textStyle: { color: '#5f6f88', fontSize: 12 }
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: ['48%', '72%'],
+        center: ['50%', '48%'],
+        avoidLabelOverlap: false,
+        itemStyle: { borderRadius: 8, borderColor: 'transparent', borderWidth: 4 },
+        label: {
+          formatter: '{b|{b}}  {c|{d}%}',
+          rich: {
+            b: { color: '#3e5575', fontWeight: 700, fontSize: 12 },
+            c: { color: '#3e5575', fontWeight: 700, fontSize: 12 }
+          }
+        },
+        labelLine: { length: 16, length2: 22 },
+        data: projectDonutData.map((item) => ({
+          value: item.value,
+          name: item.name,
+          itemStyle: { color: item.color }
+        }))
+      }
+    ]
+  }
+}
+
+function createBarOption(): echarts.EChartsOption {
+  return {
+    grid: { left: 80, right: 20, top: 24, bottom: 10, containLabel: true },
+    tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
+    xAxis: {
+      type: 'value',
+      max: 200,
+      splitLine: { lineStyle: { color: 'rgba(127,153,200,0.2)' } },
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: { color: '#51637f' }
+    },
+    yAxis: {
+      type: 'category',
+      data: expertSeries.map((item) => item.name),
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: { color: '#e02828', fontWeight: 700 }
+    },
+    series: [
+      {
+        type: 'bar',
+        data: expertSeries.map((item) => item.value),
+        barWidth: 16,
+        itemStyle: {
+          borderRadius: [0, 12, 12, 0],
+          color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
+            { offset: 0, color: '#2f74e8' },
+            { offset: 1, color: '#7bb2ff' }
+          ])
+        },
+        label: {
+          show: true,
+          position: 'right',
+          color: '#2f74e8',
+          fontWeight: 700
+        }
+      }
+    ]
+  }
+}
+
+function createLineOption(): echarts.EChartsOption {
+  const series = budgetSeries[period.value]
+  return {
+    grid: { left: 50, right: 24, top: 30, bottom: 30, containLabel: true },
+    tooltip: { trigger: 'axis' },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: series.map((item) => item.date),
+      axisLine: { lineStyle: { color: 'rgba(127,153,200,0.4)' } },
+      axisTick: { show: false },
+      axisLabel: { color: '#5a6e89' }
+    },
+    yAxis: {
+      type: 'value',
+      min: 0,
+      max: 100,
+      splitLine: { lineStyle: { color: 'rgba(127,153,200,0.2)' } },
+      axisLabel: { color: '#5a6e89' }
+    },
+    series: [
+      {
+        type: 'line',
+        smooth: true,
+        symbol: 'circle',
+        symbolSize: 8,
+        lineStyle: { color: '#2e9e5f', width: 3 },
+        itemStyle: { color: '#2e9e5f' },
+        areaStyle: { color: 'rgba(46,158,95,0.14)' },
+        data: series.map((item) => item.value),
+        label: {
+          show: true,
+          position: 'top',
+          color: '#2e9e5f',
+          formatter: '{c}.00%'
+        }
+      }
+    ]
+  }
+}
+
+function resizeCharts() {
+  donutChart?.resize()
+  barChart?.resize()
+  lineChart?.resize()
+}
+
+function initCharts() {
+  if (donutRef.value) {
+    donutChart?.dispose()
+    donutChart = echarts.init(donutRef.value)
+    donutChart.setOption(createDonutOption())
+  }
+  if (barRef.value) {
+    barChart?.dispose()
+    barChart = echarts.init(barRef.value)
+    barChart.setOption(createBarOption())
+  }
+  if (lineRef.value) {
+    lineChart?.dispose()
+    lineChart = echarts.init(lineRef.value)
+    lineChart.setOption(createLineOption())
+  }
+}
+
+watch(period, () => {
+  nextTick(() => {
+    lineChart?.setOption(createLineOption(), true)
+  })
+})
+
+onMounted(() => {
+  initCharts()
+  window.addEventListener('resize', resizeCharts)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', resizeCharts)
+  donutChart?.dispose()
+  barChart?.dispose()
+  lineChart?.dispose()
+})
+</script>
+
 <template>
-  <div class="home-background">
-    <div class="welcome-text"> 欢迎来到 </div>
+  <div class="research-board">
+    <div class="research-board__topbar">
+      <div class="research-board__title">技术研发</div>
+      <div class="research-board__tools">
+        <span>日期:</span>
+        <strong>{{ dateText }}</strong>
+        <button
+          class="tool-btn"
+          :class="{ 'is-active': period === 'month' }"
+          @click="period = 'month'"
+          >月度</button
+        >
+        <button
+          class="tool-btn"
+          :class="{ 'is-active': period === 'quarter' }"
+          @click="period = 'quarter'"
+          >季度</button
+        >
+        <button
+          class="tool-btn"
+          :class="{ 'is-active': period === 'year' }"
+          @click="period = 'year'"
+          >年度</button
+        >
+        <button class="tool-btn" @click="period = 'month'">初始化</button>
+      </div>
+    </div>
+
+    <section class="panel summary-panel">
+      <div class="panel__head">
+        <h2><span>技术研发</span>概况</h2>
+      </div>
+      <div class="summary-grid">
+        <div v-for="card in summaryCards" :key="card.title" class="summary-card">
+          <div class="summary-card__bar" :style="{ background: card.color }"></div>
+          <div class="summary-card__title">{{ card.title }}</div>
+          <div class="summary-card__row">
+            <span>月度累计:</span>
+            <strong>{{ card.value }}</strong>
+          </div>
+          <div class="summary-card__row">
+            <span>环比变化:</span>
+            <strong>{{ card.change }} ↑</strong>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <section class="board-grid">
+      <div class="panel chart-panel">
+        <div class="panel__head"><h2>项目进度统计</h2></div>
+        <div ref="donutRef" class="chart-box"></div>
+      </div>
+
+      <div class="panel chart-panel">
+        <div class="panel__head">
+          <h2>专家序列统计</h2>
+          <div class="tab-switch">
+            <span>梯队</span>
+            <span class="is-active">序列</span>
+          </div>
+        </div>
+        <div ref="barRef" class="chart-box"></div>
+      </div>
+
+      <div class="panel chart-panel">
+        <div class="panel__head">
+          <h2>预算费用</h2>
+          <div class="tab-switch">
+            <span :class="{ 'is-active': period === 'month' }" @click="period = 'month'"
+              >近一周</span
+            >
+            <span :class="{ 'is-active': period === 'quarter' }" @click="period = 'quarter'"
+              >近六个月</span
+            >
+          </div>
+        </div>
+        <div ref="lineRef" class="chart-box"></div>
+      </div>
+    </section>
+
+    <section class="panel progress-panel">
+      <div class="panel__head">
+        <h2>项目组合进展</h2>
+        <button class="filter-btn">按部门筛选</button>
+      </div>
+      <div class="progress-list">
+        <div v-for="item in projectProgress" :key="item.label" class="progress-item">
+          <span class="progress-item__label">{{ item.label }}</span>
+          <div class="progress-item__track">
+            <div
+              class="progress-item__bar"
+              :style="{ width: `${item.value * 5}%`, background: item.color }"
+            ></div>
+          </div>
+          <span class="progress-item__value">{{ item.value }}</span>
+        </div>
+      </div>
+    </section>
   </div>
 </template>
-<script lang="ts" setup></script>
-<style scoped>
-.home-background {
-  background-image: url('../../assets//imgs/bg.jpg'); /* 假设图片放在 assets 目录下 */
-  background-size: cover;
-  background-position: center;
-  background-repeat: no-repeat;
-  height: 100vh;
-  display: flex;
-  /* align-items: center;
-  justify-content: center; */
-  color: white;
-  font-size: 2rem;
-  font-weight: bold;
-  text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.7);
+
+<style scoped lang="scss">
+.research-board {
+  min-height: 100vh;
+  padding: 14px 18px 20px;
+  background: radial-gradient(
+      circle at top,
+      rgba(255, 255, 255, 0.8),
+      rgba(193, 219, 255, 0.22) 38%,
+      rgba(176, 205, 244, 0.1) 70%
+    ),
+    linear-gradient(180deg, #d8e8fb 0%, #edf4fd 100%);
+  color: #28415f;
+}
+
+.research-board__topbar {
   position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 8px 14px;
+}
+
+.research-board__title {
+  width: 100%;
+  max-width: 420px;
+  margin: 0 auto;
+  padding: 10px 28px;
+  text-align: center;
+  font-size: 28px;
+  font-weight: 800;
+  color: #c31b1b;
+  background: linear-gradient(180deg, #f6fbff, #8cb9f5 55%, #4f88d7);
+  border-radius: 0 0 30px 30px;
+  box-shadow: 0 8px 20px rgba(65, 109, 176, 0.22);
 }
 
-.home-background::before {
-  content: '';
+.research-board__tools {
   position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
+  right: 28px;
+  top: 24px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.tool-btn,
+.filter-btn,
+.tab-switch span {
+  border: 1px solid rgba(132, 167, 224, 0.45);
+  background: rgba(255, 255, 255, 0.8);
+  color: #405a7b;
+  border-radius: 18px;
+}
+
+.tool-btn {
+  padding: 6px 14px;
+  cursor: pointer;
+}
+
+.panel {
+  background: linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(225, 236, 250, 0.78));
+  border: 1px solid rgba(255, 255, 255, 0.9);
+  border-radius: 18px;
+  box-shadow:
+    inset 0 1px 0 rgba(255, 255, 255, 0.8),
+    0 10px 24px rgba(86, 118, 170, 0.12);
+}
 
-  z-index: 1;
+.summary-panel,
+.progress-panel {
+  margin-top: 10px;
+  padding: 14px 16px 18px;
 }
 
-.home-background .welcome-text {
+.panel__head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 2px 12px;
+  border-bottom: 1px solid rgba(109, 139, 190, 0.26);
+}
+
+.panel__head h2 {
+  margin: 0;
+  font-size: 22px;
+  font-weight: 800;
+  color: #1c4d93;
+}
+
+.panel__head h2 span {
+  color: #ef1f1f;
+}
+
+.summary-grid {
+  display: grid;
+  grid-template-columns: repeat(7, minmax(0, 1fr));
+  gap: 12px;
+  margin-top: 12px;
+}
+
+.summary-card {
   position: relative;
-  z-index: 2;
-  text-align: center;
-  color: #00195f;
-  margin-top: 40vh;
-  margin-left: 50%;
+  padding: 18px 16px 16px 22px;
+  min-height: 130px;
+  border-radius: 18px;
+  background: rgba(255, 255, 255, 0.4);
+}
+
+.summary-card__bar {
+  position: absolute;
+  left: 12px;
+  top: 18px;
+  width: 4px;
+  height: 46px;
+  border-radius: 3px;
+}
+
+.summary-card__title {
+  color: #f32b1f;
+  font-size: 18px;
+  font-weight: 800;
+  margin-left: 10px;
+}
+
+.summary-card__row {
+  display: flex;
+  justify-content: space-between;
+  margin-top: 14px;
+  font-size: 16px;
+  font-weight: 700;
+  color: #18314f;
+}
+
+.summary-card__row strong {
+  color: #f23022;
+}
+
+.board-grid {
+  display: grid;
+  grid-template-columns: 1fr 1.15fr 1fr;
+  gap: 12px;
+  margin-top: 12px;
+}
+
+.chart-panel {
+  padding: 14px 14px 10px;
+  min-height: 420px;
+}
+
+.chart-box {
+  width: 100%;
+  height: 350px;
+}
+
+.tab-switch {
+  display: flex;
+  gap: 8px;
+  font-size: 16px;
+  font-weight: 700;
+}
+
+.tab-switch span {
+  padding: 5px 12px;
+}
+
+.tab-switch .is-active {
+  background: #fff;
+  color: #1c5cc0;
+  box-shadow: inset 0 -3px 0 #1c5cc0;
+}
+
+.progress-panel {
+  margin-top: 12px;
+}
+
+.filter-btn {
+  padding: 8px 18px;
+  font-size: 16px;
+  font-weight: 700;
+}
+
+.progress-list {
+  padding: 22px 6px 6px;
+}
+
+.progress-item {
+  display: grid;
+  grid-template-columns: 100px 1fr 36px;
+  align-items: center;
+  gap: 14px;
+  margin-bottom: 16px;
+  font-size: 16px;
+  font-weight: 700;
+}
+
+.progress-item__track {
+  height: 10px;
+  border-radius: 999px;
+  background: #e4ece7;
+  overflow: hidden;
+}
+
+.progress-item__bar {
+  height: 100%;
+  border-radius: inherit;
+}
+
+.progress-item__value {
+  text-align: right;
 }
 </style>