Explorar o código

feat: 初始化养老数字化治理中心前端项目

- 添加项目基础配置文件和构建工具配置
- 实现响应式布局和全局样式
- 添加静态资源文件(图片、字体等)
- 实现核心组件(数字动画、进度条、模块卡片等)
- 添加数据模块和模拟数据
- 实现路由配置和状态管理
- 添加表格组件和轮播组件
- 实现首页基本布局和功能模块
easyforever hai 2 semanas
achega
4e79a8add2
Modificáronse 55 ficheiros con 4238 adicións e 0 borrados
  1. 14 0
      .eslintrc.cjs
  2. 26 0
      .gitignore
  3. 6 0
      .prettierrc.json
  4. 46 0
      eslint.config.js
  5. 13 0
      index.html
  6. 35 0
      package.json
  7. 76 0
      src/App.vue
  8. 19 0
      src/assets/iconfont/iconfont.css
  9. 1 0
      src/assets/iconfont/iconfont.js
  10. BIN=BIN
      src/assets/iconfont/iconfont.ttf
  11. BIN=BIN
      src/assets/iconfont/iconfont.woff
  12. BIN=BIN
      src/assets/iconfont/iconfont.woff2
  13. BIN=BIN
      src/assets/icons/icon-warning-orange.png
  14. BIN=BIN
      src/assets/icons/icon-warning-red.png
  15. BIN=BIN
      src/assets/icons/subsidy_fund.png
  16. BIN=BIN
      src/assets/icons/subsidy_people.png
  17. BIN=BIN
      src/assets/icons/subsidy_rate.png
  18. BIN=BIN
      src/assets/images/footer_bg.png
  19. BIN=BIN
      src/assets/images/header_bg.png
  20. BIN=BIN
      src/assets/images/home_bg.png
  21. BIN=BIN
      src/assets/images/module_bg1.png
  22. BIN=BIN
      src/assets/images/module_bg2.png
  23. BIN=BIN
      src/assets/images/module_title_bg1.png
  24. BIN=BIN
      src/assets/images/module_title_bg2.png
  25. BIN=BIN
      src/assets/images/number_bg.png
  26. BIN=BIN
      src/assets/images/trapezoid_large.png
  27. BIN=BIN
      src/assets/images/trapezoid_small.png
  28. BIN=BIN
      src/assets/images/vertical_line.png
  29. 287 0
      src/components/DataTable.vue
  30. 158 0
      src/components/ElderlyHorizontalProgress.vue
  31. 151 0
      src/components/ElderlyInstitutionService.vue
  32. 130 0
      src/components/Footer.vue
  33. 321 0
      src/components/Header.vue
  34. 118 0
      src/components/HorizontalProgressChart.vue
  35. 138 0
      src/components/ImageCarousel.vue
  36. 100 0
      src/components/ModuleCard.vue
  37. 104 0
      src/components/NumberAnimation.vue
  38. 208 0
      src/components/PercentageDisplay.vue
  39. 143 0
      src/components/ProgressChart.vue
  40. 522 0
      src/components/RegionElderly.vue
  41. 121 0
      src/components/SubsidyDisplay.vue
  42. 179 0
      src/data/clothing.ts
  43. 149 0
      src/data/dignity.ts
  44. 135 0
      src/data/food.ts
  45. 152 0
      src/data/housing.ts
  46. 32 0
      src/data/institutionService.ts
  47. 33 0
      src/data/regionElderly.ts
  48. 81 0
      src/data/subsidy.ts
  49. 31 0
      src/env.d.ts
  50. 15 0
      src/main.ts
  51. 14 0
      src/router/index.ts
  52. 644 0
      src/views/HomeView.vue
  53. 12 0
      tsconfig.json
  54. 8 0
      tsconfig.node.json
  55. 16 0
      vite.config.ts

+ 14 - 0
.eslintrc.cjs

@@ -0,0 +1,14 @@
+module.exports = {
+  root: true,
+  env: {
+    node: true
+  },
+  extends: [
+    'plugin:vue/vue3-essential',
+    '@vue/eslint-config-typescript',
+    '@vue/eslint-config-prettier/skip-formatting'
+  ],
+  rules: {
+    'vue/multi-word-component-names': 'off'
+  }
+}

+ 26 - 0
.gitignore

@@ -0,0 +1,26 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
+
+tests/**/coverage/
+tests/e2e/reports
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+
+package-lock.json
+yarn.lock
+
+# Vite config bundler cache
+vite/*.timestamp-*.mjs

+ 6 - 0
.prettierrc.json

@@ -0,0 +1,6 @@
+{
+  "semi": false,
+  "singleQuote": true,
+  "printWidth": 100,
+  "trailingComma": "es5"
+}

+ 46 - 0
eslint.config.js

@@ -0,0 +1,46 @@
+import vue from 'eslint-plugin-vue'
+import typescript from '@typescript-eslint/eslint-plugin'
+import prettier from 'eslint-plugin-prettier'
+import vueParser from '@vue/eslint-parser'
+
+export default [
+  {
+    files: ['**/*.vue'],
+    languageOptions: {
+      parser: vueParser,
+      parserOptions: {
+        ecmaVersion: 2020,
+        sourceType: 'module',
+        tsconfigRootDir: __dirname,
+        parser: '@typescript-eslint/parser',
+      },
+    },
+    plugins: {
+      vue,
+      '@typescript-eslint': typescript,
+      prettier,
+    },
+    rules: {
+      'vue/multi-word-component-names': 'off',
+      'prettier/prettier': 'error',
+    },
+  },
+  {
+    files: ['**/*.ts', '**/*.js'],
+    languageOptions: {
+      parser: '@typescript-eslint/parser',
+      parserOptions: {
+        ecmaVersion: 2020,
+        sourceType: 'module',
+        tsconfigRootDir: __dirname,
+      },
+    },
+    plugins: {
+      '@typescript-eslint': typescript,
+      prettier,
+    },
+    rules: {
+      'prettier/prettier': 'error',
+    },
+  },
+]

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>淮阳区纪检同步·养老老五数字化治理中心</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 35 - 0
package.json

@@ -0,0 +1,35 @@
+{
+  "name": "huaiyang-yanglao-front",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc && vite build",
+    "preview": "vite preview",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
+  },
+  "dependencies": {
+    "axios": "^1.7.9",
+    "echarts": "^5.5.1",
+    "element-plus": "^2.8.5",
+    "pinia": "^2.2.6",
+    "swiper": "^12.1.3",
+    "vue": "^3.5.13",
+    "vue-router": "^4.4.5"
+  },
+  "devDependencies": {
+    "@types/node": "^22.10.1",
+    "@vitejs/plugin-vue": "^5.2.1",
+    "@vue/eslint-config-prettier": "^9.0.0",
+    "@vue/eslint-config-typescript": "^14.1.3",
+    "@vue/tsconfig": "^0.6.0",
+    "eslint": "^9.15.0",
+    "eslint-plugin-vue": "^9.30.0",
+    "prettier": "^3.3.3",
+    "sass": "^1.99.0",
+    "typescript": "~5.6.2",
+    "vite": "^6.0.5",
+    "vue-tsc": "^2.1.10"
+  }
+}

+ 76 - 0
src/App.vue

@@ -0,0 +1,76 @@
+<template>
+  <div class="app-container" ref="appContainer">
+    <div class="content-wrapper" ref="contentWrapper">
+      <router-view />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue'
+
+const appContainer = ref<HTMLElement>()
+const contentWrapper = ref<HTMLElement>()
+
+const DESIGN_WIDTH = 3840
+const DESIGN_HEIGHT = 2160
+
+const updateScale = () => {
+  if (!appContainer.value || !contentWrapper.value) return
+
+  const containerWidth = appContainer.value.clientWidth
+  const containerHeight = appContainer.value.clientHeight
+
+  const scaleX = containerWidth / DESIGN_WIDTH
+  const scaleY = containerHeight / DESIGN_HEIGHT
+  const scale = Math.min(scaleX, scaleY)
+
+  // 计算居中位移
+  const offsetX = (containerWidth - DESIGN_WIDTH * scale) / 2
+  const offsetY = (containerHeight - DESIGN_HEIGHT * scale) / 2
+
+  contentWrapper.value.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`
+  contentWrapper.value.style.transformOrigin = 'left top'
+  contentWrapper.value.style.width = `${DESIGN_WIDTH}px`
+  contentWrapper.value.style.height = `${DESIGN_HEIGHT}px`
+}
+
+onMounted(() => {
+  updateScale()
+  window.addEventListener('resize', updateScale)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', updateScale)
+})
+</script>
+
+<style>
+/* 全局样式 */
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: 'Arial', sans-serif;
+  background-color: #000;
+  color: #00ff88;
+  overflow: hidden;
+}
+
+.app-container {
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  align-items: flex-start;
+  justify-content: flex-start;
+  overflow: hidden;
+  background-color: #000;
+}
+
+.content-wrapper {
+  transition: transform 0.3s ease;
+}
+</style>

+ 19 - 0
src/assets/iconfont/iconfont.css

@@ -0,0 +1,19 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 5166250 */
+  src: url('iconfont.woff2?t=1776908731783') format('woff2'),
+       url('iconfont.woff?t=1776908731783') format('woff'),
+       url('iconfont.ttf?t=1776908731783') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-yanglaoyuan:before {
+  content: "\e663";
+}
+

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 0
src/assets/iconfont/iconfont.js


BIN=BIN
src/assets/iconfont/iconfont.ttf


BIN=BIN
src/assets/iconfont/iconfont.woff


BIN=BIN
src/assets/iconfont/iconfont.woff2


BIN=BIN
src/assets/icons/icon-warning-orange.png


BIN=BIN
src/assets/icons/icon-warning-red.png


BIN=BIN
src/assets/icons/subsidy_fund.png


BIN=BIN
src/assets/icons/subsidy_people.png


BIN=BIN
src/assets/icons/subsidy_rate.png


BIN=BIN
src/assets/images/footer_bg.png


BIN=BIN
src/assets/images/header_bg.png


BIN=BIN
src/assets/images/home_bg.png


BIN=BIN
src/assets/images/module_bg1.png


BIN=BIN
src/assets/images/module_bg2.png


BIN=BIN
src/assets/images/module_title_bg1.png


BIN=BIN
src/assets/images/module_title_bg2.png


BIN=BIN
src/assets/images/number_bg.png


BIN=BIN
src/assets/images/trapezoid_large.png


BIN=BIN
src/assets/images/trapezoid_small.png


BIN=BIN
src/assets/images/vertical_line.png


+ 287 - 0
src/components/DataTable.vue

@@ -0,0 +1,287 @@
+<template>
+  <div class="data-table" :style="{ width: width, height: height }">
+    <el-table
+      ref="tableRef"
+      :data="dataRef"
+      :show-header="showHeader"
+      :border="border"
+      class="custom-table"
+      @mouseenter.native="handleMouseEnter"
+      @mouseleave.native="handleMouseLeave"
+    >
+      <el-table-column
+        v-for="col in columns"
+        :key="col.prop"
+        :prop="col.prop"
+        :label="col.label"
+        :width="col.width"
+        :align="col.align || 'center'"
+      >
+        <template #default="scope" v-if="col.prop === 'riskLevel'">
+          <div class="risk-icon" :class="scope.row.riskLevel"></div>
+        </template>
+        <template #default="scope" v-else-if="col.date">
+          <div class="table-cell">{{ scope.row[col.prop] }}</div>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
+
+interface Column {
+  prop: string
+  label: string
+  width?: number
+  date?: boolean
+  align?: string
+}
+
+const props = withDefaults(
+  defineProps<{
+    columns: Column[]
+    data: any[]
+    autoScroll?: boolean
+    showHeader?: boolean
+    height?: string
+    width?: string
+    border?: boolean
+    scrollThreshold?: number
+  }>(),
+  {
+    autoScroll: false,
+    showHeader: true,
+    height: '100%',
+    width: '100%',
+    border: false,
+    scrollThreshold: 5,
+  }
+)
+
+const tableRef = ref<any>(null)
+const scrollId = ref<number | null>(null)
+const scrollSpeed = 0.5 // 滚动速度,每帧滚动的像素数
+const dataRef = ref<any[]>([]) // 用于显示的数据
+const originalData = ref<any[]>([]) // 原始数据副本
+const isAutoScrolling = ref(false) // 是否正在自动滚动
+
+// 初始化数据
+const init = () => {
+  originalData.value = [...props.data]
+  if (props.autoScroll && props.data.length > props.scrollThreshold) {
+    // 自动滚动时使用双倍数据
+    dataRef.value = [...props.data, ...props.data]
+    isAutoScrolling.value = true
+  } else {
+    dataRef.value = [...props.data]
+    isAutoScrolling.value = false
+  }
+
+  const tableBody = tableRef.value?.$el.querySelector('.el-scrollbar__wrap')
+  if (tableBody) {
+    tableBody.scrollTop = 0
+  }
+
+  // 初始化时如果需要自动滚动,启动滚动
+  if (props.autoScroll && props.data.length > props.scrollThreshold) {
+    startScroll()
+  }
+}
+
+// 监听数据变化
+watch(
+  () => props.data,
+  () => {
+    init()
+  },
+  { deep: true }
+)
+
+// 监听自动滚动状态变化
+watch(
+  () => props.autoScroll,
+  (newVal) => {
+    if (newVal && props.data.length > props.scrollThreshold) {
+      isAutoScrolling.value = true
+      dataRef.value = [...props.data, ...props.data]
+      startScroll()
+      const tableBody = tableRef.value?.$el.querySelector('.el-scrollbar__wrap')
+      if (tableBody) {
+        tableBody.scrollTop = 0
+      }
+    } else {
+      isAutoScrolling.value = false
+      // 停止滚动时恢复原始数据
+      dataRef.value = [...originalData.value]
+      const tableBody = tableRef.value?.$el.querySelector('.el-scrollbar__wrap')
+      if (tableBody) {
+        tableBody.scrollTop = 0
+      }
+    }
+  }
+)
+
+const startScroll = () => {
+  if (!props.autoScroll || props.data.length <= props.scrollThreshold) return
+
+  const animateScroll = () => {
+    if (!isAutoScrolling.value) return
+
+    const tableBody = tableRef.value?.$el.querySelector('.el-scrollbar__wrap')
+    if (tableBody) {
+      // 平滑滚动,每次滚动指定像素数
+      tableBody.scrollTop += scrollSpeed
+
+      // 当滚动到一半(第二份数据开始)时,重置到顶部
+      if (tableBody.scrollTop >= tableBody.scrollHeight / 2) {
+        tableBody.scrollTop = 0
+      }
+    }
+    scrollId.value = window.requestAnimationFrame(animateScroll)
+  }
+
+  scrollId.value = window.requestAnimationFrame(animateScroll)
+}
+
+const stopScroll = () => {
+  if (scrollId.value) {
+    window.cancelAnimationFrame(scrollId.value)
+    scrollId.value = null
+  }
+  // dataRef.value = [...originalData.value]
+}
+
+const handleMouseEnter = () => {
+  stopScroll()
+}
+
+const handleMouseLeave = () => {
+  if (props.autoScroll && props.data.length > props.scrollThreshold) {
+    isAutoScrolling.value = true
+    // dataRef.value = [...props.data, ...props.data]
+    startScroll()
+  }
+}
+
+onMounted(() => {
+  init()
+})
+
+onUnmounted(() => {
+  stopScroll()
+})
+</script>
+
+<style lang="scss" scoped>
+.data-table {
+  padding: 0 16px 30px;
+  overflow: hidden;
+  box-sizing: border-box;
+
+  .custom-table {
+    height: 100%;
+    background-color: transparent;
+
+    :deep(.el-table__header-wrapper tr) {
+      background-color: transparent;
+    }
+
+    :deep(.el-table__header-wrapper th) {
+      padding: 10px 0;
+      background-color: rgba(0, 255, 230, 0.15) !important;
+    }
+
+    :deep(.el-table__header th .cell) {
+      padding: 0 6px;
+      color: #00d6c0;
+      font-size: 24px;
+      font-weight: normal;
+      line-height: 24px;
+      text-align: center;
+    }
+
+    :deep(.el-table__body-wrapper) {
+      background-color: rgba(255, 255, 255, 0.01);
+    }
+
+    :deep(.el-table__body-wrapper tr) {
+      padding: 4px 0;
+      background-color: rgba(255, 255, 255, 0.01) !important;
+    }
+
+    :deep(.el-table__body-wrapper td) {
+      padding: 8px 0;
+      background-color: rgba(255, 255, 255, 0.01) !important;
+
+      .cell {
+        padding: 0 8px;
+        color: #02bcad;
+        font-size: 24px;
+        line-height: 30px;
+        // text-align: center;
+      }
+
+      .table-cell {
+        font-size: 20px;
+        line-height: 26px;
+        // text-align: center;
+      }
+    }
+
+    :deep(.el-table__body .el-table__row) {
+      background-color: rgba(255, 255, 255, 0.01) !important;
+
+      &:hover > td {
+        background-color: rgba(0, 255, 230, 0.08) !important;
+      }
+    }
+
+    :deep(.el-table__row td) {
+      border-color: #0c4044 !important;
+    }
+
+    :deep(.el-table__header th) {
+      border-color: #0c4044 !important;
+    }
+
+    :deep(.el-table) {
+      border-color: #0c4044 !important;
+    }
+
+    :deep(.el-table__empty-text) {
+      color: #02bcad;
+    }
+
+    .risk-icon {
+      width: 38px;
+      height: 34px;
+      margin: 0 auto;
+      background-size: auto;
+      background-repeat: no-repeat;
+      background-position: center;
+    }
+
+    .risk-icon.high {
+      background-image: url('@/assets/icons/icon-warning-red.png');
+    }
+
+    .risk-icon.medium {
+      background-image: url('@/assets/icons/icon-warning-orange.png');
+    }
+
+    .risk-icon.low {
+      background-image: url('@/assets/icons/icon-warning-orange.png');
+    }
+  }
+
+  :deep(.el-table--border:after),
+  :deep(.el-table--border:before),
+  :deep(.el-table--border .el-table__inner-wrapper:after),
+  :deep(.el-table__inner-wrapper:before),
+  :deep(.el-table__border-left-patch) {
+    background-color: #0c4044 !important;
+  }
+}
+</style>

+ 158 - 0
src/components/ElderlyHorizontalProgress.vue

@@ -0,0 +1,158 @@
+<template>
+  <div class="elderly-horizontal-progress">
+    <div v-for="(item, index) in processedData" :key="index" class="progress-item">
+      <span class="progress-label" :style="{ color: item.color }">{{ item.label }}</span>
+      <el-progress
+        :percentage="Number(displayValues[index].toFixed(1))"
+        :color="item.color"
+        :stroke-width="14"
+      >
+        <template #default>
+          <span class="progress-value" :style="{ color: item.color }"
+            >{{ Math.round(displayValuesValue[index]) }}人</span
+          >
+        </template>
+      </el-progress>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, watch } from 'vue'
+
+interface ProcessedData {
+  label: string
+  value: number
+  color: string
+  originalValue: number
+}
+
+const props = defineProps<{
+  data: number[]
+}>()
+
+const animationDuration = 1200
+
+const displayValues = ref<number[]>([])
+const displayValuesValue = ref<number[]>([])
+
+const processedData = computed<ProcessedData[]>(() => {
+  const maxValue = Math.max(...props.data)
+  const fullValue = maxValue / 0.8
+
+  const colors = ['#01ffe6', 'rgba(255, 255, 0, 0.8)', 'rgba(255, 165, 0, 0.8)']
+  const labels = ['全自理老人', '半自理老人', '全护理老人']
+
+  return props.data.map((value, index) => ({
+    label: labels[index],
+    value: (value / fullValue) * 100,
+    color: colors[index],
+    originalValue: value,
+  }))
+})
+
+const animateValue = (start: number, end: number, index: number) => {
+  const startTime = performance.now()
+
+  const animate = (currentTime: number) => {
+    const elapsed = currentTime - startTime
+    const progress = Math.min(elapsed / animationDuration, 1)
+
+    const easeOutQuad = (t: number) => t * (2 - t)
+    const easedProgress = easeOutQuad(progress)
+
+    displayValues.value[index] = start + (end - start) * easedProgress
+
+    if (progress < 1) {
+      requestAnimationFrame(animate)
+    }
+  }
+
+  requestAnimationFrame(animate)
+}
+
+const animateValueNumber = (start: number, end: number, index: number) => {
+  const startTime = performance.now()
+
+  const animate = (currentTime: number) => {
+    const elapsed = currentTime - startTime
+    const progress = Math.min(elapsed / animationDuration, 1)
+
+    const easeOutQuad = (t: number) => t * (2 - t)
+    const easedProgress = easeOutQuad(progress)
+
+    displayValuesValue.value[index] = start + (end - start) * easedProgress
+
+    if (progress < 1) {
+      requestAnimationFrame(animate)
+    }
+  }
+
+  requestAnimationFrame(animate)
+}
+
+const startAnimations = () => {
+  displayValues.value = processedData.value.map(() => 0)
+  displayValuesValue.value = processedData.value.map(() => 0)
+
+  processedData.value.forEach((item, index) => {
+    setTimeout(() => {
+      animateValue(0, item.value, index)
+      animateValueNumber(0, item.originalValue, index)
+    }, index * 200)
+  })
+}
+
+watch(
+  () => props.data,
+  () => {
+    startAnimations()
+  },
+  { deep: true, immediate: true }
+)
+
+onMounted(() => {
+  startAnimations()
+})
+</script>
+
+<style lang="scss" scoped>
+$text-color: #00ffe6;
+$track-color: #282c38;
+
+.elderly-horizontal-progress {
+  width: 100%;
+
+  .progress-item {
+    margin-bottom: 26px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .progress-label {
+      width: 160px;
+      margin-right: 12px;
+      font-size: 30px;
+      color: $text-color;
+    }
+
+    :deep(.el-progress) {
+      flex: 1;
+
+      .el-progress__text {
+        flex-shrink: 0 !important;
+        width: 150px;
+        margin-left: 24px;
+        font-size: 30px !important;
+        text-align: right;
+      }
+    }
+
+    :deep(.el-progress-bar__outer) {
+      background-color: $track-color;
+      background: linear-gradient(0deg, rgba(50, 125, 118, 0.31) 0%, rgba(0, 255, 230, 0.2) 100%);
+      box-shadow: inset -2px 0px 2px 0px rgba(0, 255, 230, 0.46);
+    }
+  }
+}
+</style>

+ 151 - 0
src/components/ElderlyInstitutionService.vue

@@ -0,0 +1,151 @@
+<template>
+  <ModuleCard title="养老机构五项服务状况" size="large">
+    <div class="elderly-institution-service">
+      <div class="institution-grid">
+        <div v-for="(institution, index) in institutions" :key="index" class="institution-item">
+          <div class="institution-icon" :class="getIconClass(institution.problemCount)">
+            <i class="iconfont icon-yanglaoyuan"></i>
+            <!-- <div class="problem-count">{{ institution.problemCount }}</div> -->
+          </div>
+          <div class="institution-name">{{ institution.name }}</div>
+        </div>
+      </div>
+    </div>
+  </ModuleCard>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import ModuleCard from './ModuleCard.vue'
+import { institutionServiceData } from '../data/institutionService'
+
+const institutions = computed(() => institutionServiceData.institutions)
+
+const getIconClass = (problemCount: number) => {
+  if (problemCount === 0) {
+    return 'status-good'
+  } else if (problemCount < 5) {
+    return 'status-warning'
+  } else {
+    return 'status-danger'
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+$green: #07ada0;
+$orange: #d49a3d;
+$red: #d45656;
+
+:deep(.module-content) {
+  padding: 40px 0 36px;
+}
+
+.elderly-institution-service {
+  width: 100%;
+  height: 100%;
+  padding: 0 20px;
+  box-sizing: border-box;
+  overflow-y: auto;
+  margin-right: 10px;
+
+  // 自定义滚动条
+  &::-webkit-scrollbar {
+    width: 8px;
+  }
+
+  &::-webkit-scrollbar-track {
+    background: rgba(0, 255, 230, 0.1);
+    border-radius: 4px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: rgba(0, 255, 230, 0.5);
+    border-radius: 4px;
+  }
+
+  &::-webkit-scrollbar-thumb:hover {
+    background: rgba(0, 255, 230, 0.8);
+  }
+
+  .institution-grid {
+    display: grid;
+    grid-template-columns: repeat(9, 1fr);
+    gap: 26px 20px;
+
+    .institution-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+
+      .institution-icon {
+        position: relative;
+        width: 80px;
+        height: 80px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border-radius: 8px;
+        margin-bottom: 10px;
+
+        .iconfont {
+          font-size: 60px;
+        }
+
+        .problem-count {
+          position: absolute;
+          bottom: -5px;
+          right: -5px;
+          width: 24px;
+          height: 24px;
+          background: rgba(0, 0, 0, 0.7);
+          color: white;
+          border-radius: 50%;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          font-size: 12px;
+          font-weight: bold;
+        }
+
+        &.status-good {
+          background: rgba(7, 173, 160, 0.1);
+          border: 2px solid $green;
+
+          .iconfont {
+            color: $green;
+          }
+        }
+
+        &.status-warning {
+          background: rgba(212, 154, 61, 0.1);
+          border: 2px solid $orange;
+
+          .iconfont {
+            color: $orange;
+          }
+        }
+
+        &.status-danger {
+          background: rgba(212, 86, 86, 0.1);
+          border: 2px solid $red;
+
+          .iconfont {
+            color: $red;
+          }
+        }
+      }
+
+      .institution-name {
+        font-size: 16px;
+        color: #00ffe6;
+        text-align: center;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        max-width: 192px;
+      }
+    }
+  }
+}
+</style>

+ 130 - 0
src/components/Footer.vue

@@ -0,0 +1,130 @@
+<template>
+  <footer class="footer">
+    <!-- 背景图片容器 -->
+    <div class="footer-background"></div>
+
+    <div class="footer-content">
+      <div class="robot-container">
+        <div class="robot-item">
+          <div class="robot-placeholder"></div>
+          <div class="robot-label">膳食营养AI监管员</div>
+        </div>
+        <div class="robot-item">
+          <div class="robot-placeholder"></div>
+          <div class="robot-label">消防安全AI监管员</div>
+        </div>
+        <div class="robot-item">
+          <div class="robot-placeholder"></div>
+          <div class="robot-label">津贴补贴AI审核员</div>
+        </div>
+        <div class="robot-item">
+          <div class="robot-placeholder"></div>
+          <div class="robot-label">居家服务AI调度员</div>
+        </div>
+        <div class="robot-item">
+          <div class="robot-placeholder"></div>
+          <div class="robot-label">家属沟通AI联络员</div>
+        </div>
+        <div class="robot-item">
+          <div class="robot-placeholder"></div>
+          <div class="robot-label">AI协同调度监管员</div>
+        </div>
+        <div class="robot-item">
+          <div class="robot-placeholder"></div>
+          <div class="robot-label">廉政风险AI监督员</div>
+        </div>
+      </div>
+    </div>
+  </footer>
+</template>
+
+<script setup lang="ts">
+// Footer组件
+</script>
+
+<style lang="scss" scoped>
+// SCSS变量定义
+$footer-height: 218.62px;
+$robot-height: 93.9px;
+$neon-cyan: #78fee0;
+$transition-speed: 0.3s ease;
+
+.footer {
+  height: $footer-height;
+  position: relative;
+  overflow: visible;
+  display: flex;
+  align-items: flex-end;
+  justify-content: center;
+
+  // 背景图片容器
+  .footer-background {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 280px;
+    background: url('@/assets/images/footer_bg.png');
+    background-size: contain;
+    background-position: center bottom;
+    background-repeat: no-repeat;
+    z-index: 1;
+  }
+
+  // 内容容器
+  .footer-content {
+    width: 100%;
+    height: 100%;
+    position: relative;
+    z-index: 10;
+    display: flex;
+    align-items: flex-end;
+    justify-content: center;
+
+    // 机器人容器
+    .robot-container {
+      display: flex;
+      gap: 40px;
+      align-items: flex-end;
+      padding-bottom: 23px;
+
+      // 机器人项
+      .robot-item {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        gap: 10px;
+        width: 204px;
+        height: 144px;
+
+        // 机器人占位符
+        .robot-placeholder {
+          width: 88px;
+          height: $robot-height;
+          background: rgba(0, 255, 230, 0.2);
+          border: 1px solid $neon-cyan;
+          border-radius: 8px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+
+          &:after {
+            content: '机器人';
+            color: $neon-cyan;
+            font-size: 14px;
+          }
+        }
+
+        // 机器人标签
+        .robot-label {
+          color: $neon-cyan;
+          font-size: 24px;
+          line-height: 42px;
+          text-align: center;
+          white-space: nowrap;
+        }
+      }
+    }
+  }
+}
+</style>

+ 321 - 0
src/components/Header.vue

@@ -0,0 +1,321 @@
+<template>
+  <header class="header">
+    <!-- 背景图片容器 -->
+    <div class="header-background"></div>
+
+    <div class="header-content">
+      <div class="header-left">
+        <div class="date-time">{{ currentDateTime }}</div>
+      </div>
+      <div class="header-center">
+        <h1>{{ projectName }}</h1>
+      </div>
+      <div class="header-right">
+        <button class="fullscreen-btn" @click="toggleFullscreen">
+          {{ isFullscreen ? '退出全屏' : '全屏' }}
+        </button>
+        <div class="year-selector">
+          <div class="year-display" @click="toggleDropdown">{{ selectedYear }}年</div>
+          <div class="year-dropdown" v-if="dropdownOpen">
+            <div
+              v-for="year in years"
+              :key="year"
+              class="year-option"
+              :class="{ active: year === selectedYear }"
+              @click="selectYear(year)"
+            >
+              {{ year }}年
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </header>
+</template>
+
+<script setup lang="ts">
+import type { number } from 'echarts'
+import { ref, onMounted, onUnmounted, computed } from 'vue'
+
+// 响应式数据
+const currentDateTime = ref('')
+const isFullscreen = ref(false)
+const selectedYear = ref(new Date().getFullYear())
+const dropdownOpen = ref(false)
+
+// Props
+const projectName = ref('淮阳区纪检同步·养老互老数字化治理中心')
+
+// 计算属性:生成年份选项(当前年及前后5年)
+const years = computed(() => {
+  const current = new Date().getFullYear()
+  const yearRange = []
+  for (let i = current - 5; i <= current + 5; i++) {
+    yearRange.push(i)
+  }
+  return yearRange
+})
+
+// 方法
+const updateDateTime = () => {
+  const now = new Date()
+  const year = now.getFullYear()
+  const month = String(now.getMonth() + 1).padStart(2, '0')
+  const day = String(now.getDate()).padStart(2, '0')
+  const hours = String(now.getHours()).padStart(2, '0')
+  const minutes = String(now.getMinutes()).padStart(2, '0')
+  const seconds = String(now.getSeconds()).padStart(2, '0')
+  currentDateTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+}
+
+const toggleFullscreen = () => {
+  if (!document.fullscreenElement) {
+    document.documentElement.requestFullscreen().catch((err) => {
+      console.error(`Error attempting to enable fullscreen: ${err.message}`)
+    })
+  } else {
+    if (document.exitFullscreen) {
+      document.exitFullscreen()
+    }
+  }
+}
+
+const toggleDropdown = () => {
+  dropdownOpen.value = !dropdownOpen.value
+}
+
+const selectYear = (year: number) => {
+  selectedYear.value = year
+  dropdownOpen.value = false
+  console.log('Selected year:', year)
+  // 这里可以添加年份变更后的逻辑
+}
+
+// 点击外部关闭下拉菜单
+document.addEventListener('click', (e: MouseEvent) => {
+  const yearSelector = document.querySelector('.year-selector')
+  if (yearSelector && !yearSelector.contains(e.target as Node)) {
+    dropdownOpen.value = false
+  }
+})
+
+// 监听全屏状态变化
+const handleFullscreenChange = () => {
+  isFullscreen.value = !!document.fullscreenElement
+}
+
+// 生命周期
+onMounted(() => {
+  updateDateTime()
+  setInterval(updateDateTime, 1000)
+  document.addEventListener('fullscreenchange', handleFullscreenChange)
+})
+
+onUnmounted(() => {
+  document.removeEventListener('fullscreenchange', handleFullscreenChange)
+})
+</script>
+
+<style lang="scss" scoped>
+// SCSS变量定义
+$header-height: 223px;
+$primary-color: #06d4c2;
+$neon-green: #00ff88;
+$neon-blue: #00aaff;
+$neon-cyan: #00ffe6;
+$dark-bg: rgba(0, 30, 20, 0.9);
+$transition-speed: 0.3s ease;
+
+.header {
+  height: $header-height;
+  position: relative;
+  overflow: visible;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  // 背景图片容器
+  .header-background {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: url('@/assets/images/header_bg.png');
+    background-size: contain;
+    background-position: center;
+    background-blend-mode: overlay;
+    z-index: 1;
+  }
+
+  // 内容容器
+  .header-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    padding: 0 56px;
+    position: relative;
+    z-index: 10;
+
+    // 左侧日期时间
+    .header-left {
+      position: absolute;
+      bottom: -6px;
+
+      .date-time {
+        font-family: PingFang SC;
+        font-size: 48px;
+        color: $primary-color;
+        font-weight: 500;
+        line-height: 56px;
+        letter-spacing: 2px;
+      }
+    }
+
+    // 中间标题
+    .header-center {
+      margin: 0 auto;
+
+      h1 {
+        font-family: FZFengYaSongS-GB, FZFengYaSongS-GB;
+        font-weight: 400;
+        font-size: 82px;
+        letter-spacing: 4px;
+        text-shadow:
+          1px 0px 1px rgba(184, 255, 248, 0.71),
+          0px 4px 4px rgba(0, 0, 0, 0);
+        text-align: center;
+        font-style: normal;
+        text-transform: none;
+        background: linear-gradient(270deg, #78fee0 0%, rgba(5, 246, 192, 0.63) 100%);
+        -webkit-background-clip: text;
+        -webkit-text-fill-color: transparent;
+        background-clip: text;
+      }
+    }
+
+    // 右侧功能区
+    .header-right {
+      position: absolute;
+      right: 56px;
+      bottom: -6px;
+      display: flex;
+      gap: 20px;
+      align-items: center;
+
+      // 全屏按钮
+      .fullscreen-btn {
+        position: relative;
+        width: 212px;
+        height: 80px;
+        background: rgba(255, 255, 255, 0.1);
+        // border-left: 8px solid $neon-cyan;
+        color: $neon-cyan;
+        padding: 0;
+        font-size: 32px;
+        cursor: pointer;
+        transition: all $transition-speed;
+        border-radius: 0;
+        box-shadow:
+          inset 6px 0 0 rgba(0, 201, 181, 1),
+          0 8px 8px 0 rgba(0, 0, 0, 0.25),
+          inset 0 0 6px 0 rgba(0, 255, 230, 0.1);
+        font-weight: normal;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+
+        &:hover {
+          background: rgba(255, 255, 255, 0.2);
+        }
+      }
+
+      // 年份选择器
+      .year-selector {
+        position: relative;
+        width: 274px;
+        height: 80px;
+        cursor: pointer;
+
+        // 年份显示区域
+        .year-display {
+          width: 100%;
+          height: 100%;
+          background: rgba(255, 255, 255, 0.1);
+          box-shadow:
+            inset 6px 0 0 rgba(0, 201, 181, 1),
+            0 8px 8px 0 rgba(0, 0, 0, 0.25),
+            inset 0 0 6px 0 rgba(0, 255, 230, 0.1);
+          border: none;
+          // border-left: 8px solid $neon-cyan;
+          color: $neon-cyan;
+          padding: 0 50px 0 10px;
+          font-size: 32px;
+          font-weight: normal;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          transition: background $transition-speed;
+
+          &:hover {
+            background: rgba(255, 255, 255, 0.2);
+          }
+        }
+
+        // 下拉菜单容器
+        .year-dropdown {
+          position: absolute;
+          top: 100%;
+          left: 0;
+          width: 100%;
+          background: rgba(0, 30, 20, 0.98);
+          border: 1px solid $neon-cyan;
+          border-top: none;
+          z-index: 100;
+          box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
+          max-height: 300px;
+          overflow-y: auto;
+
+          // 隐藏默认滚动条
+          &::-webkit-scrollbar {
+            width: 0;
+          }
+        }
+
+        // 年份选项
+        .year-option {
+          padding: 15px 10px;
+          color: $neon-cyan;
+          font-size: 30px;
+          transition: all $transition-speed;
+          cursor: pointer;
+
+          &:hover {
+            background: rgba(0, 255, 230, 0.2);
+          }
+
+          // 选中状态
+          &.active {
+            background: rgba(0, 255, 230, 0.3);
+            font-weight: bold;
+          }
+        }
+
+        // 自定义下拉箭头容器
+        &::after {
+          content: '▼';
+          position: absolute;
+          right: 28px;
+          top: 50%;
+          transform: translateY(-50%);
+          pointer-events: none;
+          color: $neon-cyan;
+          font-size: 16px;
+          z-index: 10;
+        }
+      }
+    }
+  }
+}
+</style>

+ 118 - 0
src/components/HorizontalProgressChart.vue

@@ -0,0 +1,118 @@
+<template>
+  <div class="horizontal-progress-chart">
+    <div v-for="(item, index) in data" :key="index" class="progress-item">
+      <span class="progress-label">{{ item.label }}</span>
+      <el-progress :percentage="displayValues[index]" :color="item.color" :stroke-width="8">
+      </el-progress>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch } from 'vue'
+
+// Props
+const props = defineProps<{
+  data: Array<{
+    label: string
+    value: number
+    color: string
+  }>
+}>()
+
+// 显示值,用于动画效果
+const displayValues = ref<number[]>([])
+
+// 动画配置
+const animationDuration = 1200
+
+// 动画函数
+const animateValue = (start: number, end: number, index: number) => {
+  const startTime = performance.now()
+
+  const animate = (currentTime: number) => {
+    const elapsed = currentTime - startTime
+    const progress = Math.min(elapsed / animationDuration, 1)
+
+    // 缓动函数:easeOutQuad
+    const easeOutQuad = (t: number) => t * (2 - t)
+    const easedProgress = easeOutQuad(progress)
+
+    displayValues.value[index] = Number((start + (end - start) * easedProgress).toFixed(1))
+
+    if (progress < 1) {
+      requestAnimationFrame(animate)
+    }
+  }
+
+  requestAnimationFrame(animate)
+}
+
+// 初始化和更新动画
+const startAnimations = () => {
+  displayValues.value = props.data.map(() => 0)
+
+  // 为每个进度条启动动画
+  props.data.forEach((item, index) => {
+    setTimeout(() => {
+      animateValue(0, item.value, index)
+    }, index * 200) // 错开动画开始时间,增加视觉效果
+  })
+}
+
+// 监听数据变化,重新启动动画
+watch(
+  () => props.data,
+  () => {
+    startAnimations()
+  },
+  { deep: true, immediate: true }
+)
+
+// 组件挂载时启动动画
+onMounted(() => {
+  startAnimations()
+})
+</script>
+
+<style lang="scss" scoped>
+// SCSS变量定义
+$text-color: #00ffe6;
+$track-color: #282c38;
+
+.horizontal-progress-chart {
+  width: 100%;
+
+  .progress-item {
+    margin-bottom: 26px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .progress-label {
+      width: 176px;
+      margin-right: 12px;
+      font-size: 24px;
+      color: $text-color;
+    }
+
+    :deep(.el-progress) {
+      flex: 1;
+
+      .el-progress__text {
+        flex-shrink: 0;
+        width: 100px;
+        margin-left: 24px;
+        color: #03a99d;
+        font-size: 24px !important;
+      }
+    }
+
+    :deep(.el-progress-bar__outer) {
+      background-color: $track-color;
+      background: linear-gradient(0deg, rgba(50, 125, 118, 0.31) 0%, rgba(0, 255, 230, 0.2) 100%);
+      box-shadow: inset -2px 0px 2px 0px rgba(0, 255, 230, 0.46);
+    }
+  }
+}
+</style>

+ 138 - 0
src/components/ImageCarousel.vue

@@ -0,0 +1,138 @@
+<template>
+  <div class="image-carousel">
+    <swiper
+      :modules="modules"
+      :slides-per-view="props.slidesPerView / props.rows"
+      :space-between="props.spaceBetween"
+      :loop="true"
+      :autoplay="{
+        delay: 10,
+        disableOnInteraction: false,
+        pauseOnMouseEnter: true,
+      }"
+      :speed="3000"
+      :grab-cursor="true"
+      :allow-touch-move="true"
+      :loop-add-blank-slides="true"
+      :grid="{
+        rows: props.rows,
+        fill: 'column',
+      }"
+      class="swiper-container"
+    >
+      <swiper-slide v-for="(image, index) in images" :key="index">
+        <el-image
+          :src="image.url"
+          fit="cover"
+          class="carousel-image"
+          :preview-src-list="previewList"
+          :preview-teleported="true"
+        >
+          <template #placeholder>
+            <div class="image-placeholder">
+              <el-icon><Picture /></el-icon>
+              <span>图片加载中</span>
+            </div>
+          </template>
+          <template #error>
+            <div class="image-placeholder">
+              <el-icon><Picture /></el-icon>
+              <span>图片加载失败</span>
+            </div>
+          </template>
+        </el-image>
+      </swiper-slide>
+    </swiper>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { Swiper, SwiperSlide } from 'swiper/vue'
+import { Autoplay, FreeMode, Grid } from 'swiper/modules'
+import { Picture } from '@element-plus/icons-vue'
+
+// 导入 Swiper 样式
+import 'swiper/css'
+import 'swiper/css/free-mode'
+import 'swiper/css/grid'
+
+// Props
+const props = withDefaults(
+  defineProps<{
+    images: Array<{
+      id: number
+      url: string
+      alt: string
+    }>
+    slidesPerView?: number
+    rows?: number
+    itemWidth?: number
+    itemHeight?: number
+    spaceBetween?: number
+  }>(),
+  {
+    slidesPerView: 4,
+    rows: 1,
+    itemWidth: 150,
+    itemHeight: 190,
+    spaceBetween: 16,
+  }
+)
+
+// Swiper 模块
+const modules = [Autoplay, FreeMode, Grid]
+
+// 占位符图片URL
+const placeholderUrl =
+  'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjE5MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTUwIiBoZWlnaHQ9IjE5MCIgZmlsbD0iIzE4MTg1MSIvPjx0ZXh0IHg9Ijc1IiB5PSI5NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjIwIiBmaWxsPSIjMDBmZmU2Ij7mmK/lu7rliIPlh7rluo88L3RleHQ+PC9zdmc+'
+
+// 预览图片列表
+const previewList = computed(() => {
+  return props.images.map((image) => image.url || placeholderUrl)
+})
+</script>
+
+<style lang="scss" scoped>
+.image-carousel {
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+  overflow: hidden;
+
+  .swiper-container {
+    width: 100%;
+    height: 100%;
+  }
+
+  :deep(.swiper-slide) {
+    cursor: pointer;
+  }
+
+  .carousel-image {
+    width: calc(v-bind('props.itemWidth') * 1px) !important;
+    height: calc(v-bind('props.itemHeight') * 1px);
+  }
+
+  .image-placeholder {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background-color: rgba(255, 255, 255, 0.1);
+    border: 1px solid rgba(0, 255, 230, 0.3);
+    color: rgba(0, 255, 230, 0.5);
+
+    .el-icon {
+      font-size: calc(v-bind('props.itemHeight') * 0.3 * 1px);
+      margin-bottom: 12px;
+    }
+
+    span {
+      font-size: 14px;
+    }
+  }
+}
+</style>

+ 100 - 0
src/components/ModuleCard.vue

@@ -0,0 +1,100 @@
+<template>
+  <div class="module-card" :style="{ width: moduleWidth, height: moduleHeight }">
+    <!-- 背景图片 -->
+    <div class="module-background" :style="{ backgroundImage: `url(${backgroundImage})` }"></div>
+
+    <!-- 标题区域 -->
+    <div
+      class="module-header"
+      :style="{ height: headerHeight, backgroundImage: `url(${titleBackgroundImage})` }"
+    >
+      <div class="module-title">{{ title }}</div>
+    </div>
+
+    <!-- 内容区域 -->
+    <div class="module-content">
+      <slot></slot>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import module_bg1 from '@/assets/images/module_bg1.png'
+import module_bg2 from '@/assets/images/module_bg2.png'
+import module_title_bg1 from '@/assets/images/module_title_bg1.png'
+import module_title_bg2 from '@/assets/images/module_title_bg2.png'
+import { computed } from 'vue'
+
+// Props
+const props = withDefaults(
+  defineProps<{
+    title: string
+    size?: 'default' | 'large'
+  }>(),
+  {
+    size: 'default',
+  }
+)
+
+// 计算属性
+const isLarge = computed(() => props.size === 'large')
+const moduleWidth = computed(() => (isLarge.value ? '1842px' : '704px'))
+const moduleHeight = computed(() => (isLarge.value ? '550px' : '910px'))
+const headerHeight = computed(() => (isLarge.value ? '84px' : '100px'))
+const backgroundImage = computed(() => (isLarge.value ? module_bg2 : module_bg1))
+const titleBackgroundImage = computed(() => (isLarge.value ? module_title_bg2 : module_title_bg1))
+</script>
+
+<style lang="scss" scoped>
+// SCSS变量定义
+$neon-cyan: rgba(1, 255, 230, 0.8);
+$transition-speed: 0.3s ease;
+
+.module-card {
+  position: relative;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+
+  // 背景图片
+  .module-background {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-size: 100% 100%;
+    background-position: center;
+    z-index: 1;
+  }
+
+  // 标题区域
+  .module-header {
+    position: relative;
+    z-index: 10;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding: 0 30px;
+    background-size: cover;
+    background-position: center;
+
+    // 标题
+    .module-title {
+      font-size: 44px;
+      font-weight: bold;
+      color: $neon-cyan;
+      line-height: 56px;
+      letter-spacing: 2px;
+    }
+  }
+
+  // 内容区域
+  .module-content {
+    flex: 1;
+    position: relative;
+    z-index: 10;
+    overflow: hidden;
+  }
+}
+</style>

+ 104 - 0
src/components/NumberAnimation.vue

@@ -0,0 +1,104 @@
+<template>
+  <span class="number-animation" :class="{ 'with-unit': unit }" :style="style">
+    {{ displayValue }}
+    <span v-if="unit" class="unit" :style="unitStyle">{{ unit }}</span>
+  </span>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch, computed } from 'vue'
+
+interface Props {
+  value: number | string
+  decimals?: number
+  thousands?: boolean
+  duration?: number
+  unit?: string
+  style?: Record<string, string>
+  unitStyle?: Record<string, string>
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  decimals: 0,
+  thousands: false,
+  duration: 1500,
+  unit: '',
+  style: () => ({}),
+  unitStyle: () => ({}),
+})
+
+const displayValue = ref('0')
+const parsedValue = computed(() => {
+  if (typeof props.value === 'string') {
+    // 处理带单位的字符串,如 "149.7万人"
+    const numStr = props.value.replace(/[^0-9.]/g, '')
+    return parseFloat(numStr) || 0
+  }
+  return props.value
+})
+
+const formatNumber = (num: number): string => {
+  let result = num.toFixed(props.decimals)
+
+  if (props.thousands) {
+    const parts = result.split('.')
+    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
+    result = parts.join('.')
+  }
+
+  return result
+}
+
+const animateValue = () => {
+  const start = 0
+  const end = parsedValue.value
+  const duration = props.duration
+  const startTime = performance.now()
+
+  const animate = (currentTime: number) => {
+    const elapsed = currentTime - startTime
+    const progress = Math.min(elapsed / duration, 1)
+
+    // 缓动函数:easeOutQuad
+    const easeOutQuad = (t: number) => t * (2 - t)
+    const easedProgress = easeOutQuad(progress)
+
+    const currentValue = start + (end - start) * easedProgress
+    displayValue.value = formatNumber(currentValue)
+
+    if (progress < 1) {
+      requestAnimationFrame(animate)
+    }
+  }
+
+  requestAnimationFrame(animate)
+}
+
+watch(
+  () => props.value,
+  () => {
+    animateValue()
+  },
+  { deep: true, immediate: true }
+)
+
+onMounted(() => {
+  animateValue()
+})
+</script>
+
+<style lang="scss" scoped>
+.number-animation {
+  display: inline-block;
+  white-space: nowrap;
+
+  &.with-unit {
+    display: flex;
+    align-items: baseline;
+
+    .unit {
+      margin-left: 4px;
+    }
+  }
+}
+</style>

+ 208 - 0
src/components/PercentageDisplay.vue

@@ -0,0 +1,208 @@
+<template>
+  <div class="percentage-display">
+    <div v-for="(item, index) in data" :key="index" class="percentage-item">
+      <div class="percentage-bar">
+        <div class="percentage-value">
+          {{
+            formatNumber(
+              displayValues[index],
+              item.decimals || props.decimals,
+              item.unit || props.unit,
+              isIntegerList[index]
+            )
+          }}
+        </div>
+        <div class="percentage-label">{{ item.label }}</div>
+        <div class="percentage-desc">{{ item.description }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch } from 'vue'
+
+// Props
+const props = withDefaults(
+  defineProps<{
+    data: Array<{
+      label: string
+      value: number
+      description: string
+      unit?: string
+      decimals?: number
+    }>
+    unit?: string
+    decimals?: number
+    useThousandsSeparator?: boolean
+  }>(),
+  {
+    unit: '%',
+    decimals: 1,
+    useThousandsSeparator: true,
+  }
+)
+
+// 显示值,用于动画效果
+const displayValues = ref<number[]>([])
+// 记录每个数据项是否为整数
+const isIntegerList = ref<boolean[]>([])
+
+// 数字格式化函数
+const formatNumber = (
+  value: number,
+  decimals: number,
+  unit: string,
+  isOriginalInteger: boolean
+): string => {
+  // 格式化数字
+  let formatted: string
+  if (isOriginalInteger) {
+    // 原始数据是整数,无论动画过程中是否有小数,都显示为整数
+    formatted = Math.round(value).toString()
+  } else {
+    // 原始数据不是整数,保留指定小数位数
+    formatted = value.toFixed(decimals)
+  }
+
+  // 分割整数部分和小数部分
+  const [integerPart, decimalPart] = formatted.split('.')
+
+  // 添加千位分隔符
+  const integerWithSeparator = props.useThousandsSeparator
+    ? integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
+    : integerPart
+
+  // 组合整数部分、小数部分和单位
+  if (decimalPart) {
+    return `${integerWithSeparator}.${decimalPart}${unit}`
+  } else {
+    return `${integerWithSeparator}${unit}`
+  }
+}
+
+// 动画配置
+const animationDuration = 1200
+
+// 动画函数
+const animateValue = (start: number, end: number, index: number) => {
+  const startTime = performance.now()
+
+  const animate = (currentTime: number) => {
+    const elapsed = currentTime - startTime
+    const progress = Math.min(elapsed / animationDuration, 1)
+
+    // 缓动函数:easeOutQuad
+    const easeOutQuad = (t: number) => t * (2 - t)
+    const easedProgress = easeOutQuad(progress)
+
+    displayValues.value[index] = start + (end - start) * easedProgress
+
+    if (progress < 1) {
+      requestAnimationFrame(animate)
+    }
+  }
+
+  requestAnimationFrame(animate)
+}
+
+// 初始化和更新动画
+const startAnimations = () => {
+  displayValues.value = props.data.map(() => 0)
+  // 记录每个数据项是否为整数
+  isIntegerList.value = props.data.map((item) => Number.isInteger(item.value))
+
+  // 为每个百分比启动动画
+  props.data.forEach((item, index) => {
+    setTimeout(() => {
+      animateValue(0, item.value, index)
+    }, index * 200) // 错开动画开始时间,增加视觉效果
+  })
+}
+
+// 监听数据变化,重新启动动画
+watch(
+  () => props.data,
+  () => {
+    startAnimations()
+  },
+  { deep: true, immediate: true }
+)
+
+// 组件挂载时启动动画
+onMounted(() => {
+  startAnimations()
+})
+</script>
+
+<style lang="scss" scoped>
+// SCSS变量定义
+$text-color: rgba(0, 255, 230, 0.7);
+$percentage-color: #01ffe6;
+
+.percentage-display {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 30px;
+
+  .percentage-item {
+    flex: 1;
+    text-align: center;
+    position: relative;
+    height: 150px;
+
+    .percentage-bar {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      height: 100%;
+      width: calc(100% - 12px);
+    }
+
+    &:last-child .percentage-bar {
+      width: 100%;
+    }
+
+    &:not(:last-child)::after {
+      content: '';
+      position: absolute;
+      right: 0;
+      top: 0;
+      height: 100%;
+      width: 2px;
+      margin: 0 10px;
+      background: url('@/assets/images/vertical_line.png') center no-repeat;
+      background-size: 100% 100%;
+    }
+
+    .percentage-value {
+      font-size: 44px;
+      font-weight: 500;
+      color: $percentage-color;
+      line-height: 56px;
+      letter-spacing: 2px;
+      margin-bottom: 8px;
+      white-space: nowrap;
+    }
+
+    .percentage-label {
+      font-size: 30px;
+      font-weight: 500;
+      color: $text-color;
+      letter-spacing: 1px;
+      margin-bottom: 8px;
+      white-space: nowrap;
+    }
+
+    .percentage-desc {
+      font-size: 26px;
+      font-weight: 500;
+      letter-spacing: 1px;
+      color: rgba(0, 255, 230, 0.3);
+      white-space: nowrap;
+    }
+  }
+}
+</style>

+ 143 - 0
src/components/ProgressChart.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="progress-chart">
+    <template v-for="(item, index) in data" :key="index">
+      <div class="progress-item">
+        <el-progress
+          type="circle"
+          :percentage="Number(displayValues[index])"
+          :color="item.color"
+          :width="159"
+          :stroke-width="12"
+        >
+          <template #default>
+            <div class="progress-text">{{ displayValues[index].toFixed(1) }}%</div>
+          </template>
+        </el-progress>
+        <div class="progress-label">{{ item.label }}</div>
+      </div>
+      <div v-if="index < data.length - 1" class="vertical-line"></div>
+    </template>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch } from 'vue'
+
+// Props
+const props = defineProps<{
+  data: Array<{
+    label: string
+    value: number
+    color: string
+  }>
+}>()
+
+// 显示值,用于动画效果
+const displayValues = ref<number[]>([])
+
+// 动画配置
+const animationDuration = 1200
+const animationSteps = 60
+
+// 动画函数
+const animateValue = (start: number, end: number, index: number) => {
+  const startTime = performance.now()
+
+  const animate = (currentTime: number) => {
+    const elapsed = currentTime - startTime
+    const progress = Math.min(elapsed / animationDuration, 1)
+
+    // 缓动函数:easeOutQuad
+    const easeOutQuad = (t: number) => t * (2 - t)
+    const easedProgress = easeOutQuad(progress)
+
+    displayValues.value[index] = start + (end - start) * easedProgress
+
+    if (progress < 1) {
+      requestAnimationFrame(animate)
+    }
+  }
+
+  requestAnimationFrame(animate)
+}
+
+// 初始化和更新动画
+const startAnimations = () => {
+  displayValues.value = props.data.map(() => 0)
+
+  // 为每个进度条启动动画
+  props.data.forEach((item, index) => {
+    setTimeout(() => {
+      animateValue(0, item.value, index)
+    }, index * 200) // 错开动画开始时间,增加视觉效果
+  })
+}
+
+// 监听数据变化,重新启动动画
+watch(
+  () => props.data,
+  () => {
+    startAnimations()
+  },
+  { deep: true, immediate: true }
+)
+
+// 组件挂载时启动动画
+onMounted(() => {
+  startAnimations()
+})
+</script>
+
+<style lang="scss" scoped>
+// SCSS变量定义
+$text-color: #68d0c6;
+$track-color: #282c38;
+
+.progress-chart {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20px 0;
+
+  // 进度条项
+  .progress-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    flex: 1;
+    position: relative;
+
+    // 进度条轨迹颜色
+    :deep(.el-progress-circle__track) {
+      stroke: $track-color;
+    }
+
+    // 进度条文字
+    .progress-text {
+      font-size: 30px;
+      font-weight: bold;
+      color: #fff;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+    }
+
+    // 进度条标签
+    .progress-label {
+      margin-top: 17px;
+      font-size: 26px;
+      color: $text-color;
+      text-align: center;
+    }
+  }
+
+  // 垂直分割线
+  .vertical-line {
+    width: 2px;
+    height: 212px;
+    background: url('@/assets/images/vertical_line.png') center no-repeat;
+    flex-shrink: 0;
+  }
+}
+</style>

+ 522 - 0
src/components/RegionElderly.vue

@@ -0,0 +1,522 @@
+<template>
+  <ModuleCard title="区域老人情况" size="large">
+    <div class="region-elderly-content">
+      <!-- 三列布局 -->
+      <div class="content-grid">
+        <!-- 第一列 -->
+        <div class="column column-1">
+          <!-- 第一行:三个统计数据 -->
+          <div class="row row-1">
+            <div class="stat-item">
+              <div class="stat-value">
+                <NumberAnimation :value="institutionStats.total" unit="家" />
+              </div>
+              <div class="stat-label">机构总数</div>
+            </div>
+            <div class="stat-item">
+              <div class="stat-value">
+                <NumberAnimation :value="institutionStats.public" unit="家" />
+              </div>
+              <div class="stat-label">公办养老机构</div>
+            </div>
+            <div class="stat-item">
+              <div class="stat-value">
+                <NumberAnimation :value="institutionStats.private" unit="家" />
+              </div>
+              <div class="stat-label">社会办养老机构</div>
+            </div>
+          </div>
+
+          <!-- 第二行:两个重叠的梯形 -->
+          <div class="row row-2">
+            <div class="trapezoid-container">
+              <!-- 大梯形 -->
+              <div class="trapezoid large-trapezoid"></div>
+              <!-- 小梯形 -->
+              <div class="trapezoid small-trapezoid"></div>
+
+              <div class="trapezoid-item">
+                <div class="trapezoid-label">床位总数:</div>
+                <div class="trapezoid-value"><NumberAnimation :value="bedStats.total" /></div>
+                <div class="trapezoid-unit">个</div>
+              </div>
+
+              <div class="trapezoid-item">
+                <div class="trapezoid-label">入住老人:</div>
+                <div class="trapezoid-value"><NumberAnimation :value="bedStats.occupied" /></div>
+                <div class="trapezoid-unit">人</div>
+              </div>
+
+              <div class="trapezoid-item">
+                <div class="trapezoid-label">入住率:</div>
+                <div class="trapezoid-value">
+                  <NumberAnimation :value="bedStats.occupancyRate" :decimals="1" />
+                </div>
+                <div class="trapezoid-unit">%</div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 第二列 -->
+        <div class="column column-2">
+          <!-- 第一行:三个统计数据 -->
+          <div class="row row-1">
+            <div class="stat-item">
+              <div class="stat-value">
+                <NumberAnimation
+                  :value="populationStats.registered"
+                  :decimals="1"
+                  unit="万人"
+                  :unitStyle="{ color: '#01FFE6', fontSize: '30px' }"
+                />
+              </div>
+              <div class="stat-label">户籍人口</div>
+            </div>
+            <div class="stat-item">
+              <div class="stat-value">
+                <NumberAnimation
+                  :value="populationStats.elderly60Plus"
+                  :decimals="1"
+                  unit="万人"
+                  :unitStyle="{ color: '#01FFE6', fontSize: '30px' }"
+                />
+              </div>
+              <div class="stat-label">60+老人</div>
+            </div>
+            <div class="stat-item">
+              <div class="stat-value">
+                <NumberAnimation
+                  :value="populationStats.agingRate"
+                  :decimals="1"
+                  unit="%"
+                  :unitStyle="{ color: '#01FFE6', fontSize: '30px' }"
+                />
+              </div>
+              <div class="stat-label">老龄化率</div>
+            </div>
+          </div>
+
+          <!-- 第二行:三个进度条 -->
+          <div class="row row-2">
+            <ElderlyHorizontalProgress :data="careData" />
+          </div>
+        </div>
+
+        <!-- 第三列 -->
+        <div class="column column-3">
+          <div class="subsidy-data">
+            <div class="subsidy-item">
+              <div class="subsidy-icon subsidy-fund"></div>
+              <div class="subsidy-content">
+                <div class="subsidy-label">补贴资金</div>
+                <div class="subsidy-value">
+                  <NumberAnimation
+                    :value="subsidyStats.amount"
+                    :thousands="true"
+                    unit="元"
+                    :unitStyle="{ color: '#aed1ce', fontSize: '30px', fontWeight: '400' }"
+                  />
+                </div>
+              </div>
+            </div>
+            <div class="subsidy-item">
+              <div class="subsidy-icon subsidy-people"></div>
+              <div class="subsidy-content">
+                <div class="subsidy-label">补贴人数</div>
+                <div class="subsidy-value">
+                  <NumberAnimation
+                    :value="subsidyStats.people"
+                    :thousands="true"
+                    unit="人"
+                    :unitStyle="{ color: '#aed1ce', fontSize: '30px', fontWeight: '400' }"
+                  />
+                </div>
+              </div>
+            </div>
+            <div class="subsidy-item">
+              <div class="subsidy-icon subsidy-rate"></div>
+              <div class="subsidy-content">
+                <div class="subsidy-label">补贴到位率</div>
+                <div class="subsidy-value">
+                  <NumberAnimation
+                    :value="subsidyStats.completionRate"
+                    :decimals="1"
+                    unit="%"
+                    :unitStyle="{ color: '#aed1ce', fontSize: '30px', fontWeight: '400' }"
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </ModuleCard>
+</template>
+
+<script setup lang="ts">
+import ModuleCard from './ModuleCard.vue'
+import ElderlyHorizontalProgress from './ElderlyHorizontalProgress.vue'
+import NumberAnimation from './NumberAnimation.vue'
+import { regionElderlyData } from '../data/regionElderly'
+
+const { institutionStats, bedStats, populationStats, careData, subsidyStats } = regionElderlyData
+</script>
+
+<style lang="scss" scoped>
+// SCSS变量定义
+$neon-cyan: #01ffe6;
+$neon-yellow: rgba(255, 255, 0, 0.8);
+$neon-orange: rgba(255, 165, 0, 0.8);
+
+.region-elderly-content {
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+
+  // 三列布局
+  .content-grid {
+    display: grid;
+    grid-template-columns: 1fr 1fr 1fr;
+    gap: 20px;
+    height: 100%;
+
+    // 列
+    .column {
+      display: flex;
+      flex-direction: column;
+      gap: 20px;
+
+      // 第一列
+      &.column-1 {
+        width: 668px;
+
+        // 第一行:三个统计数据
+        .row.row-1 {
+          display: flex;
+          justify-content: space-between;
+          height: 118px;
+          margin-top: 20px;
+
+          .stat-item {
+            position: relative;
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            height: 100%;
+
+            &:not(:last-child) {
+              padding-right: 38px;
+            }
+
+            &:not(:last-child)::after {
+              content: '';
+              position: absolute;
+              right: 0;
+              top: 0;
+              height: 100%;
+              width: 2px;
+              margin: 0 18px;
+              background: url('@/assets/images/vertical_line.png') center no-repeat;
+              background-size: 100% 100%;
+            }
+
+            .stat-value {
+              font-size: 44px;
+              font-weight: 500;
+              color: $neon-cyan;
+              line-height: 56px;
+              letter-spacing: 2px;
+              margin-bottom: 8px;
+            }
+
+            .stat-label {
+              font-size: 26px;
+              font-weight: 300;
+              line-height: 39px;
+              letter-spacing: 1px;
+              color: $neon-cyan;
+              text-align: center;
+              white-space: nowrap;
+              opacity: 0.8;
+            }
+          }
+        }
+
+        // 第二行:两个重叠的梯形
+        .row.row-2 {
+          flex: 1;
+          display: flex;
+          align-items: flex-end;
+          justify-content: flex-end;
+          padding-left: 20px;
+          padding-bottom: 40px;
+
+          .trapezoid-container {
+            position: relative;
+            width: 640px;
+            height: 234px;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+
+            .trapezoid {
+              position: absolute;
+              color: white;
+              display: flex;
+              align-items: center;
+
+              &.large-trapezoid {
+                background: url('@/assets/images/trapezoid_large.png') no-repeat center;
+                background-size: 100% 100%;
+                width: 100%;
+                height: 100%;
+                bottom: 0;
+                right: 0;
+              }
+
+              &.small-trapezoid {
+                background: url('@/assets/images/trapezoid_small.png') no-repeat center;
+                background-size: 100% 100%;
+                width: 538px;
+                height: 151px;
+                bottom: 0;
+                right: 0;
+              }
+            }
+
+            .trapezoid-item {
+              display: flex;
+              align-items: center;
+              color: $neon-cyan;
+
+              &:nth-last-child(2) {
+                margin-top: 26px;
+                color: #fdffe0;
+              }
+
+              &:last-child {
+                margin-top: 2px;
+                color: #fdffe0;
+              }
+            }
+
+            .trapezoid-label {
+              flex-shrink: 0;
+              width: 200px;
+              margin-left: 10px;
+              font-weight: 400;
+              font-size: 30px;
+              text-align: right;
+            }
+
+            .trapezoid-value {
+              flex-shrink: 0;
+              width: 140px;
+              font-weight: 600;
+              font-size: 44px;
+              margin-right: 8px;
+              text-align: right;
+            }
+
+            .trapezoid-unit {
+              font-weight: 400;
+              font-size: 30px;
+            }
+          }
+        }
+      }
+
+      // 第二列
+      &.column-2 {
+        width: 636px;
+        margin-left: 20px;
+        margin-right: 40px;
+
+        // 第一行:三个统计数据
+        .row.row-1 {
+          display: flex;
+          justify-content: space-between;
+          height: 118px;
+          margin-top: 20px;
+
+          .stat-item {
+            position: relative;
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            height: 100%;
+
+            &:not(:last-child) {
+              padding-right: 38px;
+            }
+
+            &:not(:last-child)::after {
+              content: '';
+              position: absolute;
+              right: 0;
+              top: 0;
+              height: 100%;
+              width: 2px;
+              margin: 0 18px;
+              background: url('@/assets/images/vertical_line.png') center no-repeat;
+              background-size: 100% 100%;
+            }
+
+            .stat-value {
+              font-size: 44px;
+              font-weight: 500;
+              color: $neon-cyan;
+              line-height: 56px;
+              letter-spacing: 2px;
+              margin-bottom: 8px;
+            }
+
+            .stat-unit {
+              color: $neon-cyan;
+              font-weight: 500;
+              font-size: 30px;
+            }
+
+            .stat-label {
+              font-size: 26px;
+              font-weight: 400;
+              line-height: 39px;
+              letter-spacing: 1px;
+              color: $neon-cyan;
+              text-align: center;
+              white-space: nowrap;
+              opacity: 0.5;
+            }
+          }
+        }
+
+        // 第二行:三个进度条
+        .row.row-2 {
+          flex: 1;
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+          gap: 15px;
+          margin-top: 66px;
+
+          .care-item {
+            display: flex;
+            align-items: center;
+
+            .care-label {
+              width: 100px;
+              font-size: 16px;
+              color: rgba(255, 255, 255, 0.7);
+            }
+
+            .care-bar-container {
+              flex: 1;
+              height: 10px;
+              background-color: rgba(255, 255, 255, 0.1);
+              margin: 0 20px;
+              border-radius: 5px;
+              overflow: hidden;
+
+              .care-bar {
+                height: 100%;
+                border-radius: 5px;
+
+                &.full-care {
+                  width: 70%;
+                  background-color: $neon-cyan;
+                }
+
+                &.half-care {
+                  width: 25%;
+                  background-color: $neon-yellow;
+                }
+
+                &.total-care {
+                  width: 5%;
+                  background-color: $neon-orange;
+                }
+              }
+            }
+
+            .care-value {
+              width: 100px;
+              font-size: 16px;
+              color: rgba(255, 255, 255, 0.7);
+              text-align: right;
+            }
+          }
+        }
+      }
+
+      // 第三列
+      &.column-3 {
+        display: flex;
+        flex-direction: column;
+        justify-content: space-around;
+
+        .subsidy-data {
+          display: flex;
+          flex-direction: column;
+          gap: 30px;
+          height: 100%;
+          padding: 40px 12px 36px;
+          justify-content: space-between;
+
+          .subsidy-item {
+            display: flex;
+            align-items: center;
+
+            .subsidy-icon {
+              width: 76px;
+              height: 86px;
+              margin-right: 15px;
+
+              &.subsidy-fund {
+                background: url('@/assets/icons/subsidy_fund.png') no-repeat center;
+                background-size: cover;
+              }
+
+              &.subsidy-people {
+                background: url('@/assets/icons/subsidy_people.png') no-repeat center;
+                background-size: cover;
+              }
+
+              &.subsidy-rate {
+                background: url('@/assets/icons/subsidy_rate.png') no-repeat center;
+                background-size: cover;
+              }
+            }
+
+            .subsidy-label {
+              font-size: 28px;
+              font-weight: 400;
+              color: #6ae7da;
+            }
+
+            .subsidy-value {
+              margin-top: 4px;
+              font-size: 44px;
+              font-weight: 600;
+              letter-spacing: 2px;
+              color: $neon-cyan;
+            }
+
+            .stat-unit {
+              color: #aed1ce;
+              font-weight: 400;
+              font-size: 30px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 121 - 0
src/components/SubsidyDisplay.vue

@@ -0,0 +1,121 @@
+<template>
+  <div class="subsidy-container">
+    <div class="subsidy-label">发放补贴</div>
+    <div class="number-container">
+      <div class="number-item" v-for="(digit, index) in displayDigits" :key="index">
+        {{ digit }}
+      </div>
+    </div>
+    <div class="subsidy-unit">{{ unit }}</div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue'
+
+const props = defineProps<{
+  amount: number
+}>()
+
+const displayAmount = ref(0)
+const animationDuration = 2000
+const animationSteps = 60
+
+const unit = computed(() => {
+  if (displayAmount.value >= 10000) {
+    return '亿元'
+  }
+  return '万元'
+})
+
+const displayDigits = computed(() => {
+  let value = displayAmount.value
+
+  if (value >= 10000) {
+    value = value / 10000
+    const integerPart = Math.floor(value)
+    const decimalPart = Math.round((value - integerPart) * 100)
+      .toString()
+      .padStart(2, '0')
+    return [integerPart.toString(), '.', ...decimalPart]
+  } else if (value < 1) {
+    const decimalStr = value.toFixed(2).replace('0.', '')
+    return ['0', '.', ...decimalStr.padEnd(2, '0')]
+  } else {
+    const integerStr = Math.floor(value).toString().padStart(4, '0').slice(-4)
+    return integerStr.split('')
+  }
+})
+
+const animateValue = (start: number, end: number, duration: number) => {
+  const startTime = performance.now()
+
+  const animate = (currentTime: number) => {
+    const elapsed = currentTime - startTime
+    const progress = Math.min(elapsed / duration, 1)
+
+    const easeOutQuad = (t: number) => t * (2 - t)
+    const easedProgress = easeOutQuad(progress)
+
+    displayAmount.value = start + (end - start) * easedProgress
+
+    if (progress < 1) {
+      requestAnimationFrame(animate)
+    }
+  }
+
+  requestAnimationFrame(animate)
+}
+
+watch(
+  () => props.amount,
+  (newAmount) => {
+    animateValue(displayAmount.value, newAmount, animationDuration)
+  },
+  { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped>
+.subsidy-container {
+  display: flex;
+  align-items: center;
+  margin: 30px 0;
+  padding: 0 24px;
+
+  .subsidy-label {
+    color: #06baad;
+    font-size: 36px;
+    line-height: 56px;
+    letter-spacing: 1px;
+    margin-right: 20px;
+  }
+
+  .number-container {
+    display: flex;
+    gap: 10px;
+  }
+
+  .number-item {
+    width: 80px;
+    height: 100px;
+    background: url('@/assets/images/number_bg.png');
+    background-size: 100% 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 64px;
+    font-weight: bold;
+    color: #01ffe6;
+    transition: transform 0.1s ease;
+  }
+
+  .subsidy-unit {
+    color: #06baad;
+    font-size: 36px;
+    line-height: 56px;
+    letter-spacing: 1px;
+    margin-left: 20px;
+  }
+}
+</style>

+ 179 - 0
src/data/clothing.ts

@@ -0,0 +1,179 @@
+// 衣穿得舒适模块模拟数据
+export const clothingData = {
+  // 顶部数字数据
+  percentageData: [
+    {
+      label: '采购总支出',
+      value: 62800,
+      description: '',
+      unit: '元',
+      decimals: 0,
+    },
+    {
+      label: '人均衣物支出',
+      value: 1200,
+      description: '',
+      unit: '元',
+      decimals: 0,
+    },
+    {
+      label: '发放到位率',
+      value: 100,
+      description: '',
+      unit: '%',
+      decimals: 0,
+    },
+  ],
+  // 表格数据
+  tableData: [
+    {
+      date: '2026-03-10',
+      item: '春季外套',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+    {
+      date: '2026-03-10',
+      item: '棉鞋',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37双',
+    },
+    {
+      date: '2026-01-15',
+      item: '冬季棉衣',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+    {
+      date: '2026-01-15',
+      item: '冬季棉衣',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+    {
+      date: '2026-01-15',
+      item: '冬季棉衣',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+    {
+      date: '2026-01-15',
+      item: '冬季棉衣',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+    {
+      date: '2026-01-15',
+      item: '冬季棉衣',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+    {
+      date: '2026-01-15',
+      item: '冬季棉衣',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+    {
+      date: '2026-01-15',
+      item: '冬季棉衣',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+    {
+      date: '2026-01-15',
+      item: '冬季棉衣',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+    {
+      date: '2026-01-15',
+      item: '冬季棉衣',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+    {
+      date: '2026-01-15',
+      item: '冬季棉衣',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+    {
+      date: '2026-01-15',
+      item: '冬季棉衣',
+      supplier: '周口羁风服装有限公司',
+      quantity: '37件',
+    },
+  ],
+  // 表格列定义
+  tableColumns: [
+    {
+      prop: 'date',
+      label: '采购日期',
+      width: 163,
+    },
+    {
+      prop: 'item',
+      label: '采购物品',
+      width: 135,
+    },
+    {
+      prop: 'supplier',
+      label: '供应商',
+      width: 270,
+    },
+    {
+      prop: 'quantity',
+      label: '数量',
+      width: 80,
+    },
+  ],
+  // 图片数据
+  images: [
+    {
+      id: 1,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '衣物图片1',
+    },
+    {
+      id: 2,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '衣物图片2',
+    },
+    {
+      id: 3,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '衣物图片3',
+    },
+    {
+      id: 4,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '衣物图片4',
+    },
+    {
+      id: 5,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '衣物图片5',
+    },
+    {
+      id: 6,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '衣物图片6',
+    },
+    {
+      id: 7,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '衣物图片7',
+    },
+    {
+      id: 8,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '衣物图片8',
+    },
+    {
+      id: 9,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '衣物图片9',
+    },
+  ],
+}

+ 149 - 0
src/data/dignity.ts

@@ -0,0 +1,149 @@
+// 人活得体面模块模拟数据
+export const dignityData = {
+  // 顶部统计数据
+  percentageData: [
+    {
+      value: 1.8,
+      unit: '次',
+      label: '年度体检',
+      description: '不少于1次',
+    },
+    {
+      value: 4.2,
+      unit: '次',
+      label: '组织文娱活动',
+      description: '每月不少于两次',
+    },
+    {
+      value: 98.2,
+      unit: '%',
+      label: '房间设施卫生',
+      description: '每月消毒、每日打扫',
+    },
+  ],
+
+  // 表格列定义
+  tableColumns: [
+    {
+      prop: 'institution',
+      label: '单位',
+      width: 180,
+    },
+    {
+      prop: 'problem',
+      label: '问题',
+      width: 248,
+      align: 'left',
+    },
+    {
+      prop: 'riskLevel',
+      label: '风险等级',
+      width: 100,
+    },
+    {
+      prop: 'date',
+      label: '预警时间',
+      width: 120,
+    },
+  ],
+
+  // 表格数据
+  tableData: [
+    {
+      institution: '王店乡敬老院',
+      problem: '上月没有检测到组织活动',
+      riskLevel: 'high',
+      date: '2026-04-03',
+    },
+    {
+      institution: '刘振屯敬老院',
+      problem: '上年度没有检查到安排体检记录',
+      riskLevel: 'medium',
+      date: '2026-04-03',
+    },
+    {
+      institution: '豆门乡老院',
+      problem: '走廊出现活体动物(疑似老鼠)',
+      riskLevel: 'high',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      problem: '上月没有检测到组织活动',
+      riskLevel: 'high',
+      date: '2026-04-03',
+    },
+    {
+      institution: '刘振屯敬老院',
+      problem: '上年度没有检查到安排体检记录',
+      riskLevel: 'medium',
+      date: '2026-04-03',
+    },
+    {
+      institution: '豆门乡老院',
+      problem: '走廊出现活体动物(疑似老鼠)',
+      riskLevel: 'high',
+      date: '2026-04-03',
+    },
+    {
+      institution: '豆门乡老院',
+      problem: '走廊出现活体动物(疑似老鼠)',
+      riskLevel: 'high',
+      date: '2026-04-03',
+    },
+  ],
+
+  // 图片数据
+  images: [
+    {
+      id: 1,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '活动图片1',
+    },
+    {
+      id: 2,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '活动图片2',
+    },
+    {
+      id: 3,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '活动图片3',
+    },
+    {
+      id: 4,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '活动图片4',
+    },
+    {
+      id: 5,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '活动图片5',
+    },
+    {
+      id: 6,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '活动图片6',
+    },
+    {
+      id: 7,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '活动图片7',
+    },
+    {
+      id: 8,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '活动图片8',
+    },
+    {
+      id: 9,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '活动图片9',
+    },
+    {
+      id: 10,
+      url: '', // 实际项目中使用真实图片路径
+      alt: '活动图片10',
+    },
+  ],
+}

+ 135 - 0
src/data/food.ts

@@ -0,0 +1,135 @@
+// 饭吃得满意模块模拟数据
+export const foodData = {
+  // 顶部百分比数据
+  percentageData: [
+    {
+      label: '餐费支出比例',
+      value: 61.2,
+      description: '不低于60%',
+    },
+    {
+      label: '食谱更新率',
+      value: 61.2,
+      description: '每周更新',
+    },
+    {
+      label: '每日菜品',
+      value: 61.2,
+      description: '不低于4个',
+    },
+  ],
+  // 表格数据
+  tableData: [
+    {
+      institution: '王店乡敬老院',
+      issue: '厨师疑似没有佩戴口罩',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '刘振屯敬老院',
+      issue: '午餐图片与食谱不一致',
+      riskLevel: 'medium',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '豆门乡敬老院',
+      issue: '午餐没有上传存档图片',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+  ],
+  // 表格列定义
+  tableColumns: [
+    {
+      prop: 'institution',
+      label: '单位',
+      width: 180,
+    },
+    {
+      prop: 'issue',
+      label: '问题',
+      width: 178,
+      align: 'left',
+    },
+    {
+      prop: 'riskLevel',
+      label: '风险等级',
+      width: 110,
+    },
+    {
+      prop: 'image',
+      label: '图片',
+      width: 100,
+    },
+    {
+      prop: 'date',
+      label: '日期',
+      width: 80,
+      date: true,
+    },
+  ],
+  // 底部横向进度条数据
+  progressData: [
+    {
+      label: '明厨亮灶接入率',
+      value: 98.7,
+      color: '#00d6c0',
+    },
+    {
+      label: '卫生等合规率',
+      value: 68.7,
+      color: '#ffb800',
+    },
+    {
+      label: '食谱合规率',
+      value: 32.1,
+      color: '#ff7a00',
+    },
+  ],
+}

+ 152 - 0
src/data/housing.ts

@@ -0,0 +1,152 @@
+// 居住得安全模块模拟数据
+export const housingData = {
+  // 顶部百分比数据
+  percentageData: [
+    {
+      label: '适老化改造',
+      value: 61.2,
+      description: '设施齐全',
+    },
+    {
+      label: '摄像头安装率',
+      value: 61.2,
+      description: '每周更新',
+    },
+    {
+      label: '有人值班',
+      value: 61.2,
+      description: '24小时',
+    },
+  ],
+  // 表格数据
+  tableData: [
+    {
+      institution: '区民政局',
+      issue: '厨师疑似没有佩戴口罩',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '刘振屯敬老院',
+      issue: '午餐图片与食谱不一致',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '豆门乡敬老院',
+      issue: '午餐没有上传存档图片',
+      riskLevel: 'medium',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'high',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'medium',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'medium',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'medium',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'medium',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'medium',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'medium',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'medium',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'medium',
+      image: '',
+      date: '2026-04-03',
+    },
+    {
+      institution: '王店乡敬老院',
+      issue: '厨房疑似出现活体动物(苍蝇)',
+      riskLevel: 'medium',
+      image: '',
+      date: '2026-04-03',
+    },
+  ],
+  // 表格列定义
+  tableColumns: [
+    {
+      prop: 'institution',
+      label: '单位',
+      width: 180,
+    },
+    {
+      prop: 'issue',
+      label: '问题',
+      width: 178,
+      align: 'left',
+    },
+    {
+      prop: 'riskLevel',
+      label: '风险等级',
+      width: 110,
+    },
+    {
+      prop: 'image',
+      label: '图片',
+      width: 100,
+    },
+    {
+      prop: 'date',
+      label: '日期',
+      width: 80,
+      date: true,
+    },
+  ],
+}

+ 32 - 0
src/data/institutionService.ts

@@ -0,0 +1,32 @@
+// 养老机构五项服务状况数据
+export const institutionServiceData = {
+  institutions: [
+    { name: '安城区养老服务中心', problemCount: 0 },
+    { name: '四区通养老服务中心', problemCount: 2 },
+    { name: '黄集区养老服务中心', problemCount: 1 },
+    { name: '大道区养老服务中心', problemCount: 3 },
+    { name: '润城区养老服务中心', problemCount: 4 },
+    { name: '基层区养老服务中心', problemCount: 6 },
+    { name: '临城区养老服务中心', problemCount: 0 },
+    { name: '郑城区养老服务中心', problemCount: 2 },
+    { name: '王城区养老服务中心', problemCount: 1 },
+    { name: '民政养老院中心', problemCount: 3 },
+    { name: '失能老人养护中心', problemCount: 5 },
+    { name: '区团街文生养老院', problemCount: 0 },
+    { name: '区团门乡整托园长', problemCount: 2 },
+    { name: '安城区养老服务中心', problemCount: 1 },
+    { name: '安城区养老服务中心', problemCount: 4 },
+    { name: '安城区养老服务中心', problemCount: 3 },
+    { name: '安城区养老服务中心', problemCount: 0 },
+    { name: '安城区养老服务中心', problemCount: 2 },
+    { name: '安城区养老服务中心', problemCount: 1 },
+    { name: '安城区养老服务中心', problemCount: 5 },
+    { name: '安城区养老服务中心', problemCount: 3 },
+    { name: '安城区养老服务中心', problemCount: 0 },
+    { name: '安城区养老服务中心', problemCount: 2 },
+    { name: '安城区养老服务中心', problemCount: 4 },
+    { name: '安城区养老服务中心', problemCount: 1 },
+    { name: '安城区养老服务中心', problemCount: 3 },
+    { name: '安城区养老服务中心', problemCount: 0 },
+  ]
+}

+ 33 - 0
src/data/regionElderly.ts

@@ -0,0 +1,33 @@
+// 区域老人情况模块模拟数据
+export const regionElderlyData = {
+  // 第一列 - 机构统计数据
+  institutionStats: {
+    total: 36,
+    public: 11,
+    private: 25,
+  },
+
+  // 第一列 - 床位统计数据
+  bedStats: {
+    total: 3503,
+    occupied: 1924,
+    occupancyRate: 55,
+  },
+
+  // 第二列 - 人口统计数据
+  populationStats: {
+    registered: 149.7,
+    elderly60Plus: 20.5,
+    agingRate: 13.7,
+  },
+
+  // 第二列 - 护理情况数据(用于进度条)
+  careData: [145317, 51273, 9318],
+
+  // 第三列 - 补贴统计数据
+  subsidyStats: {
+    amount: 65439464,
+    people: 36124,
+    completionRate: 98.2,
+  },
+}

+ 81 - 0
src/data/subsidy.ts

@@ -0,0 +1,81 @@
+// 补贴相关数据
+export const subsidyData = {
+  // 发放补贴数字(以万元为单位)
+  amount: 7182,
+
+  // 统计图表数据
+  progressData: [
+    {
+      label: '用途合规率',
+      value: 100,
+      color: '#06BBAD',
+    },
+    {
+      label: '公示达标率',
+      value: 96.8,
+      color: '#D49A3C',
+    },
+    {
+      label: '零用钱到位率',
+      value: 80,
+      color: '#D45656',
+    },
+  ],
+
+  // 存在问题数量
+  problemCount: 12,
+
+  // 表格列定义
+  tableColumns: [
+    { prop: 'unit', label: '单位', width: 180 },
+    { prop: 'problem', label: '问题', width: 186 },
+    { prop: 'riskLevel', label: '风险等级', width: 110 },
+    { prop: 'warningTime', label: '预警时间', width: 176, date: true },
+  ],
+
+  // 表格数据
+  tableData: [
+    {
+      id: 1,
+      unit: '王店乡敬老院',
+      problem: '护理费与失能等级不匹配',
+      riskLevel: 'high',
+      warningTime: '2026-04-10 10:10',
+    },
+    {
+      id: 2,
+      unit: '王店乡敬老院',
+      problem: '年龄未满80周岁',
+      riskLevel: 'medium',
+      warningTime: '2026-04-03 14:20',
+    },
+    {
+      id: 3,
+      unit: '王店乡敬老院',
+      problem: '户籍不在本区',
+      riskLevel: 'high',
+      warningTime: '2026-04-03 10:15',
+    },
+    {
+      id: 4,
+      unit: '王店乡敬老院',
+      problem: '死亡冒领',
+      riskLevel: 'high',
+      warningTime: '2026-04-03 10:20',
+    },
+    {
+      id: 5,
+      unit: '李庄乡敬老院',
+      problem: '护理费与失能等级不匹配',
+      riskLevel: 'medium',
+      warningTime: '2026-04-02 15:30',
+    },
+    {
+      id: 6,
+      unit: '李庄乡敬老院',
+      problem: '年龄未满80周岁',
+      riskLevel: 'low',
+      warningTime: '2026-04-02 11:10',
+    },
+  ],
+}

+ 31 - 0
src/env.d.ts

@@ -0,0 +1,31 @@
+/// <reference types="vite/client" />
+
+declare module '*.png' {
+  const content: string
+  export default content
+}
+
+declare module '*.jpg' {
+  const content: string
+  export default content
+}
+
+declare module '*.jpeg' {
+  const content: string
+  export default content
+}
+
+declare module '*.gif' {
+  const content: string
+  export default content
+}
+
+declare module '*.svg' {
+  const content: string
+  export default content
+}
+
+declare module '*.webp' {
+  const content: string
+  export default content
+}

+ 15 - 0
src/main.ts

@@ -0,0 +1,15 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import './assets/iconfont/iconfont.css'
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+
+app.use(createPinia())
+app.use(router)
+app.use(ElementPlus)
+
+app.mount('#app')

+ 14 - 0
src/router/index.ts

@@ -0,0 +1,14 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes: [
+    {
+      path: '/',
+      name: 'home',
+      component: () => import('../views/HomeView.vue'),
+    },
+  ],
+})
+
+export default router

+ 644 - 0
src/views/HomeView.vue

@@ -0,0 +1,644 @@
+<template>
+  <div class="home-container">
+    <!-- 顶部标题栏 -->
+    <Header />
+
+    <!-- 主要内容区域 -->
+    <main class="main-content">
+      <!-- 中间主要内容 -->
+      <div class="center-content">
+        <!-- 第一行:五个主要模块 -->
+        <div class="module-row first-row">
+          <ModuleCard title="钱花得明白" class="left-side-panel">
+            <!-- 发放补贴 -->
+            <SubsidyDisplay :amount="subsidyData.amount" />
+
+            <!-- 统计图表 -->
+            <ProgressChart :data="subsidyData.progressData" />
+
+            <!-- 横线 -->
+            <div class="divider"></div>
+
+            <!-- 存在问题数量 -->
+            <div class="problem-container">
+              <span class="problem-label">存在问题数量:</span>
+              <span class="problem-value">{{ subsidyData.problemCount }}</span>
+              <span class="problem-unit">个</span>
+            </div>
+
+            <!-- 表格 -->
+            <DataTable
+              :columns="subsidyData.tableColumns"
+              :data="subsidyData.tableData"
+              :auto-scroll="true"
+              :show-header="true"
+              :border="true"
+              height="314px"
+              width="100%"
+            />
+          </ModuleCard>
+
+          <ModuleCard title="饭吃得满意">
+            <!-- 顶部百分比数字 -->
+            <PercentageDisplay :data="foodData.percentageData" />
+
+            <!-- 表格 -->
+            <DataTable
+              :columns="foodData.tableColumns"
+              :data="foodData.tableData"
+              :auto-scroll="true"
+              :show-header="false"
+              :border="false"
+              :scroll-threshold="7"
+              height="370px"
+              width="100%"
+              style="padding: 0 28px; margin-top: 36px"
+            />
+
+            <!-- 底部横向进度条 -->
+            <HorizontalProgressChart
+              :data="foodData.progressData"
+              style="padding: 0 28px; margin-top: 36px"
+            />
+          </ModuleCard>
+
+          <ModuleCard title="居住得安全">
+            <!-- 顶部百分比数字 -->
+            <PercentageDisplay :data="housingData.percentageData" />
+
+            <!-- 表格 -->
+            <DataTable
+              :columns="housingData.tableColumns"
+              :data="housingData.tableData"
+              :auto-scroll="true"
+              :show-header="false"
+              :border="false"
+              :scroll-threshold="7"
+              height="560px"
+              width="100%"
+              style="padding: 0 28px; margin-top: 36px"
+            />
+          </ModuleCard>
+
+          <ModuleCard title="衣穿得舒适">
+            <!-- 顶部数字 -->
+            <PercentageDisplay :data="clothingData.percentageData" />
+
+            <!-- 表格 -->
+            <DataTable
+              :columns="clothingData.tableColumns"
+              :data="clothingData.tableData"
+              :auto-scroll="true"
+              :show-header="true"
+              :border="true"
+              height="330px"
+              width="100%"
+              style="padding: 0 28px; margin-top: 36px"
+            />
+
+            <!-- 图片轮播 -->
+            <div style="height: 190px; margin-top: 40px; padding: 0 28px">
+              <ImageCarousel :images="clothingData.images" />
+            </div>
+          </ModuleCard>
+
+          <ModuleCard title="人活得体面" class="right-side-panel">
+            <!-- 顶部统计数字 -->
+            <PercentageDisplay :data="dignityData.percentageData" style="padding-right: 20px" />
+
+            <!-- 表格 -->
+            <DataTable
+              :columns="dignityData.tableColumns"
+              :data="dignityData.tableData"
+              :auto-scroll="true"
+              :show-header="false"
+              :border="false"
+              :scroll-threshold="6"
+              height="328px"
+              width="100%"
+              style="padding: 0 16px; margin-top: 36px"
+            />
+
+            <!-- 图片轮播 -->
+            <div style="height: 190px; margin-top: 40px; padding: 0 28px">
+              <ImageCarousel
+                :images="dignityData.images"
+                :slides-per-view="6"
+                :rows="2"
+                :item-width="206"
+                :item-height="88"
+                :space-between="16"
+              />
+            </div>
+          </ModuleCard>
+        </div>
+
+        <!-- 第二行:区域养老情况和养老机构五项服务状况 -->
+        <div class="module-row second-row">
+          <RegionElderly />
+
+          <ElderlyInstitutionService />
+        </div>
+      </div>
+    </main>
+
+    <!-- 底部 -->
+    <Footer />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, computed } from 'vue'
+import * as echarts from 'echarts'
+import { Bell, Warning, InfoFilled, Picture } from '@element-plus/icons-vue'
+import { Swiper, SwiperSlide } from 'swiper/vue'
+import { Autoplay } from 'swiper/modules'
+import 'swiper/css'
+import Header from '../components/Header.vue'
+import Footer from '../components/Footer.vue'
+import ModuleCard from '../components/ModuleCard.vue'
+import ProgressChart from '../components/ProgressChart.vue'
+import SubsidyDisplay from '../components/SubsidyDisplay.vue'
+import DataTable from '../components/DataTable.vue'
+import PercentageDisplay from '../components/PercentageDisplay.vue'
+import HorizontalProgressChart from '../components/HorizontalProgressChart.vue'
+import ImageCarousel from '../components/ImageCarousel.vue'
+import RegionElderly from '../components/RegionElderly.vue'
+import ElderlyInstitutionService from '../components/ElderlyInstitutionService.vue'
+import { subsidyData } from '../data/subsidy'
+import { foodData } from '../data/food'
+import { housingData } from '../data/housing'
+import { clothingData } from '../data/clothing'
+import { dignityData } from '../data/dignity'
+
+// 滚动相关
+const scrollIntervals = ref<{ [key: string]: number }>({})
+
+// 生命周期
+onMounted(() => {
+  onUnmounted(() => {
+    // cleanupCharts()
+    Object.values(scrollIntervals.value).forEach((interval) => clearInterval(interval))
+  })
+})
+</script>
+
+<style lang="scss" scoped>
+$text-color: rgba(0, 255, 230, 0.7);
+
+.home-container {
+  width: 3840px;
+  height: 2160px;
+  background-color: rgba(0, 39, 41, 0.6);
+  color: $text-color;
+  font-family: PingFang SC;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  // background-image: url('../assets/images/home_bg.png');
+  background-size: 100% 100%;
+  background-position: center;
+}
+
+/* 存在问题数量 */
+.problem-container {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+  padding: 0 28px;
+
+  .problem-label {
+    font-size: 28px;
+    color: $text-color;
+  }
+
+  .problem-value {
+    font-size: 28px;
+    font-weight: bold;
+    color: #ff1607;
+  }
+
+  .problem-unit {
+    font-size: 28px;
+    color: $text-color;
+  }
+}
+
+/* 横线 */
+.divider {
+  width: 100%;
+  height: 1px;
+  background-color: #00c3b0;
+  margin: 16px 0 14px;
+}
+
+/* 主要内容区域 */
+.main-content {
+  flex: 1;
+  display: flex;
+  padding: 0 52px;
+  gap: 20px;
+  overflow: hidden;
+}
+
+/* 中间主要内容 */
+.center-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  overflow: hidden;
+}
+
+.module-row {
+  display: flex;
+  overflow: hidden;
+
+  &.first-row {
+    padding: 60px 0;
+    justify-content: space-between;
+  }
+
+  .left-side-panel {
+    transform: perspective(1000px) rotateY(10deg) translate3d(0, 0, 55px);
+
+    :deep(.module-content) {
+      padding-left: 20px;
+    }
+  }
+
+  .right-side-panel {
+    transform: perspective(1000px) rotateY(-10deg) translate3d(0, 0, 55px);
+
+    :deep(.module-content) {
+      padding-right: 20px;
+    }
+  }
+
+  &.second-row {
+    margin-top: 30px;
+    justify-content: space-between;
+  }
+}
+
+.module {
+  flex: 1;
+  background-color: rgba(0, 40, 30, 0.8);
+  border: 1px solid #00ff88;
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
+  display: flex;
+  flex-direction: column;
+}
+
+.module.large-module {
+  flex: 1;
+  min-height: 300px;
+}
+
+.module h3 {
+  font-size: 16px;
+  margin: 0 0 15px 0;
+  text-align: center;
+  border-bottom: 1px solid #00ff88;
+  padding-bottom: 10px;
+}
+
+.module-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+}
+
+.percentage-display {
+  margin-top: 40px;
+}
+
+/* 统计行 */
+.stats-row {
+  display: flex;
+  justify-content: space-around;
+  gap: 10px;
+}
+
+.stat-item {
+  text-align: center;
+  flex: 1;
+}
+
+.stat-value {
+  font-size: 20px;
+  font-weight: bold;
+  color: #00aaff;
+}
+
+.stat-label {
+  font-size: 12px;
+  margin-top: 5px;
+}
+
+/* 图表容器 */
+.chart-container {
+  flex: 1;
+  display: flex;
+  gap: 10px;
+  min-height: 100px;
+}
+
+.chart {
+  flex: 1;
+  min-height: 100px;
+  border-radius: 4px;
+}
+
+/* 列表容器 */
+.list-container {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.list-item {
+  display: flex;
+  justify-content: space-between;
+  font-size: 12px;
+  padding: 5px 0;
+  border-bottom: 1px solid rgba(0, 255, 136, 0.2);
+}
+
+/* 图片滚动容器 */
+.image-scroll-container {
+  height: 100px;
+  overflow: hidden;
+  position: relative;
+  border: 1px solid rgba(0, 255, 136, 0.3);
+  border-radius: 4px;
+}
+
+.image-scroll {
+  display: flex;
+  gap: 10px;
+  padding: 10px;
+  width: max-content;
+  animation: scroll 30s linear infinite;
+}
+
+.image-scroll:hover {
+  animation-play-state: paused;
+}
+
+@keyframes scroll {
+  0% {
+    transform: translateX(0);
+  }
+  100% {
+    transform: translateX(-50%);
+  }
+}
+
+.scroll-item {
+  width: 80px;
+  height: 80px;
+  cursor: pointer;
+  transition: transform 0.3s ease;
+}
+
+.scroll-item:hover {
+  transform: scale(1.1);
+}
+
+.image-placeholder {
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 255, 136, 0.1);
+  border: 1px solid rgba(0, 255, 136, 0.3);
+  border-radius: 4px;
+}
+
+/* 统计网格 */
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 10px;
+  margin-bottom: 15px;
+}
+
+.stat-grid-item {
+  text-align: center;
+  padding: 10px;
+  border: 1px solid rgba(0, 255, 136, 0.3);
+  border-radius: 4px;
+}
+
+.stat-grid-item .stat-label {
+  font-size: 18px;
+  font-weight: bold;
+  color: #00aaff;
+  margin: 0;
+}
+
+.stat-grid-item .stat-desc {
+  font-size: 12px;
+  margin-top: 5px;
+}
+
+/* 底部统计 */
+.bottom-stats {
+  display: flex;
+  justify-content: space-around;
+  gap: 10px;
+  margin-top: 15px;
+  padding-top: 15px;
+  border-top: 1px solid rgba(0, 255, 136, 0.3);
+}
+
+.bottom-stat-item {
+  text-align: center;
+  flex: 1;
+}
+
+.bottom-stat-item .stat-label {
+  font-size: 12px;
+  margin-bottom: 5px;
+}
+
+.bottom-stat-item .stat-value {
+  font-size: 16px;
+  font-weight: bold;
+  color: #00aaff;
+}
+
+/* 服务图标 */
+.service-icons {
+  display: grid;
+  grid-template-columns: repeat(5, 1fr);
+  gap: 10px;
+  padding: 10px;
+}
+
+.service-icon {
+  width: 100%;
+  height: 60px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.icon-placeholder {
+  width: 40px;
+  height: 40px;
+  background-color: rgba(0, 255, 136, 0.1);
+  border: 1px solid rgba(0, 255, 136, 0.3);
+  border-radius: 4px;
+}
+
+/* 图片放大弹窗 */
+.dialog-image-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 20px;
+}
+
+.dialog-image {
+  max-width: 100%;
+  max-height: 600px;
+  border-radius: 8px;
+}
+
+/* 人活得体面模块样式 */
+.dignity-stats {
+  display: flex;
+  justify-content: space-around;
+  padding: 0 28px;
+  margin-top: 40px;
+  gap: 20px;
+}
+
+.dignity-stat-item {
+  flex: 1;
+  text-align: center;
+  padding: 0 10px;
+}
+
+.dignity-stat-value {
+  font-size: 36px;
+  font-weight: bold;
+  color: #00aaff;
+  line-height: 56px;
+}
+
+.dignity-stat-unit {
+  font-size: 36px;
+  color: #00aaff;
+  line-height: 56px;
+  margin-left: 5px;
+}
+
+.dignity-stat-label {
+  font-size: 20px;
+  color: #06baad;
+  margin-top: 10px;
+  line-height: 30px;
+}
+
+.problem-list {
+  .problem-item {
+    display: flex;
+    align-items: center;
+    padding: 12px 0;
+    border-bottom: 1px solid rgba(0, 255, 230, 0.2);
+
+    .problem-institution {
+      flex: 1;
+      font-size: 20px;
+      color: #02bcad;
+    }
+
+    .problem-content {
+      flex: 2;
+      font-size: 20px;
+      color: #02bcad;
+      margin: 0 20px;
+    }
+
+    .problem-level {
+      width: 40px;
+      height: 40px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      &.high {
+        color: #ff1607;
+      }
+
+      &.medium {
+        color: #ff8800;
+      }
+
+      &.low {
+        color: #00ff88;
+      }
+    }
+
+    .problem-date {
+      font-size: 20px;
+      color: #02bcad;
+      margin-left: 20px;
+    }
+  }
+}
+
+.dignity-swiper {
+  width: 100%;
+  height: 100px;
+
+  :deep(.swiper-slide) {
+    width: 80px !important;
+    height: 80px;
+    cursor: pointer;
+  }
+
+  .dignity-image {
+    width: 100%;
+    height: 100%;
+  }
+
+  .image-placeholder {
+    width: 100%;
+    height: 100%;
+    background-color: rgba(255, 255, 255, 0.1);
+    border: 1px solid rgba(0, 255, 230, 0.3);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .el-icon {
+      font-size: 32px;
+      color: rgba(0, 255, 230, 0.5);
+    }
+  }
+}
+
+/* 图片轮播样式 */
+.image-carousel {
+  .carousel-image {
+    width: 150px;
+    height: 190px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .image-placeholder {
+      width: 100%;
+      height: 100%;
+      background-color: rgba(255, 255, 255, 0.1);
+      border: 1px solid rgba(0, 255, 230, 0.3);
+    }
+  }
+}
+</style>

+ 12 - 0
tsconfig.json

@@ -0,0 +1,12 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  }
+}

+ 8 - 0
tsconfig.node.json

@@ -0,0 +1,8 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.node.json",
+  "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
+  }
+}

+ 16 - 0
vite.config.ts

@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import { resolve } from 'path'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': resolve(__dirname, 'src'),
+    },
+  },
+  server: {
+    port: 5175,
+  },
+})